preview support for cifs/smb

This commit is contained in:
Travis Glenn Hansen 2020-09-08 10:06:09 -06:00
parent f5ba51c9b8
commit 5ae0248ba8
5 changed files with 293 additions and 31 deletions

View File

@ -116,7 +116,7 @@ You may install multiple deployments of each/any driver. It requires the followi
Install beta (v1.17+) CRDs (once per cluster): Install beta (v1.17+) CRDs (once per cluster):
- https://github.com/kubernetes-csi/external-snapshotter/tree/master/config/crd - https://github.com/kubernetes-csi/external-snapshotter/tree/master/client/config/crd
``` ```
kubectl apply -f snapshot.storage.k8s.io_volumesnapshotclasses.yaml kubectl apply -f snapshot.storage.k8s.io_volumesnapshotclasses.yaml

View File

@ -181,7 +181,7 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver {
if ( if (
capability.mount.fs_type && capability.mount.fs_type &&
!["nfs"].includes(capability.mount.fs_type) !["nfs", "cifs"].includes(capability.mount.fs_type)
) { ) {
message = `invalid fs_type ${capability.mount.fs_type}`; message = `invalid fs_type ${capability.mount.fs_type}`;
return false; return false;
@ -694,9 +694,21 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver {
response = await sshClient.exec(command); response = await sshClient.exec(command);
} }
// set acls
// TODO: this is unsfafe approach, make it better
if (this.options.zfs.datasetPermissionsAcls) {
for (const acl of this.options.zfs.datasetPermissionsAcls) {
command = sshClient.buildCommand("setfacl", [
acl,
properties.mountpoint.value,
]);
driver.ctx.logger.verbose("set acl command: %s", command);
response = await sshClient.exec(command);
}
}
break; break;
case "volume": case "volume":
// TODO: create all the necessary iscsi stuff
// set properties // set properties
// set reserve // set reserve
setProps = true; setProps = true;

View File

@ -9,8 +9,10 @@ const { ControllerNfsClientDriver } = require("./controller-nfs-client");
function factory(ctx, options) { function factory(ctx, options) {
switch (options.driver) { switch (options.driver) {
case "freenas-nfs": case "freenas-nfs":
case "freenas-smb":
case "freenas-iscsi": case "freenas-iscsi":
case "truenas-nfs": case "truenas-nfs":
case "truenas-smb":
case "truenas-iscsi": case "truenas-iscsi":
return new FreeNASDriver(ctx, options); return new FreeNASDriver(ctx, options);
case "zfs-generic-nfs": case "zfs-generic-nfs":

View File

@ -6,6 +6,7 @@ const Handlebars = require("handlebars");
// freenas properties // freenas properties
const FREENAS_NFS_SHARE_PROPERTY_NAME = "democratic-csi:freenas_nfs_share_id"; const FREENAS_NFS_SHARE_PROPERTY_NAME = "democratic-csi:freenas_nfs_share_id";
const FREENAS_SMB_SHARE_PROPERTY_NAME = "democratic-csi:freenas_smb_share_id";
const FREENAS_ISCSI_TARGET_ID_PROPERTY_NAME = const FREENAS_ISCSI_TARGET_ID_PROPERTY_NAME =
"democratic-csi:freenas_iscsi_target_id"; "democratic-csi:freenas_iscsi_target_id";
const FREENAS_ISCSI_EXTENT_ID_PROPERTY_NAME = const FREENAS_ISCSI_EXTENT_ID_PROPERTY_NAME =
@ -22,6 +23,8 @@ class FreeNASDriver extends ControllerZfsSshBaseDriver {
switch (this.options.driver) { switch (this.options.driver) {
case "freenas-nfs": case "freenas-nfs":
case "truenas-nfs": case "truenas-nfs":
case "freenas-smb":
case "truenas-smb":
return "filesystem"; return "filesystem";
case "freenas-iscsi": case "freenas-iscsi":
case "truenas-iscsi": case "truenas-iscsi":
@ -45,6 +48,9 @@ class FreeNASDriver extends ControllerZfsSshBaseDriver {
case "freenas-iscsi": case "freenas-iscsi":
case "truenas-iscsi": case "truenas-iscsi":
return "iscsi"; return "iscsi";
case "freenas-smb":
case "truenas-smb":
return "smb";
default: default:
throw new Error("unknown driver: " + this.ctx.args.driver); throw new Error("unknown driver: " + this.ctx.args.driver);
} }
@ -123,6 +129,7 @@ class FreeNASDriver extends ControllerZfsSshBaseDriver {
const zb = this.getZetabyte(); const zb = this.getZetabyte();
let properties; let properties;
let endpoint;
let response; let response;
let share = {}; let share = {};
@ -207,7 +214,9 @@ class FreeNASDriver extends ControllerZfsSshBaseDriver {
} else { } else {
throw new GrpcError( throw new GrpcError(
grpc.status.UNKNOWN, grpc.status.UNKNOWN,
`received error creating nfs share - code: ${response.statusCode} body: ${response.body}` `received error creating nfs share - code: ${
response.statusCode
} body: ${JSON.stringify(response.body)}`
); );
} }
} }
@ -234,6 +243,179 @@ class FreeNASDriver extends ControllerZfsSshBaseDriver {
return volume_context; return volume_context;
} }
break; break;
case "smb":
properties = await zb.zfs.get(datasetName, [
"mountpoint",
FREENAS_SMB_SHARE_PROPERTY_NAME,
]);
properties = properties[datasetName];
this.ctx.logger.debug("zfs props data: %j", properties);
let smbName;
if (this.options.smb.nameTemplate) {
smbName = Handlebars.compile(this.options.smb.nameTemplate)({
name: call.request.name,
parameters: call.request.parameters,
});
} else {
smbName = zb.helpers.extractLeafName(datasetName);
}
if (this.options.smb.namePrefix) {
smbName = this.options.smb.namePrefix + smbName;
}
if (this.options.smb.nameSuffix) {
smbName += this.options.smb.nameSuffix;
}
smbName = smbName.toLowerCase();
this.ctx.logger.info(
"FreeNAS creating smb share with name: " + smbName
);
// create smb share
if (
!zb.helpers.isPropertyValueSet(
properties[FREENAS_SMB_SHARE_PROPERTY_NAME].value
)
) {
/**
* The only required parameters are:
* - path
* - name
*
* Note that over time it appears the list of available parameters has increased
* so in an effort to best support old versions of FreeNAS we should check the
* presense of each parameter in the config and set the corresponding parameter in
* the API request *only* if present in the config.
*/
switch (apiVersion) {
case 1:
case 2:
share = {
name: smbName,
path: properties.mountpoint.value,
};
let propertyMapping = {
shareTemplate: "auxsmbconf",
shareHome: "home",
shareAllowedHosts: "hostsallow",
shareDeniedHosts: "hostsdeny",
shareDefaultPermissions: "default_permissions",
shareGuestOk: "guestok",
shareGuestOnly: "guestonly",
shareShowHiddenFiles: "showhiddenfiles",
shareRecycleBin: "recyclebin",
shareBrowsable: "browsable",
shareAccessBasedEnumeration: "abe",
shareTimeMachine: "timemachine",
shareStorageTask: "storage_task",
};
for (const key in propertyMapping) {
if (this.options.smb.hasOwnProperty(key)) {
let value;
switch (key) {
case "shareTemplate":
value = Handlebars.compile(
this.options.smb.shareTemplate
)({
name: call.request.name,
parameters: call.request.parameters,
});
break;
default:
value = this.options.smb[key];
break;
}
share[propertyMapping[key]] = value;
}
}
switch (apiVersion) {
case 1:
endpoint = "/sharing/cifs";
// rename keys with cifs_ prefix
for (const key in share) {
share["cifs_" + key] = share[key];
delete share[key];
}
// convert to comma-separated list
if (share.cifs_hostsallow) {
share.cifs_hostsallow = share.cifs_hostsallow.join(",");
}
// convert to comma-separated list
if (share.cifs_hostsdeny) {
share.cifs_hostsdeny = share.cifs_hostsdeny.join(",");
}
break;
case 2:
endpoint = "/sharing/smb";
break;
}
response = await httpClient.post(endpoint, share);
/**
* v1 = 201
* v2 = 200
*/
if ([200, 201].includes(response.statusCode)) {
//set zfs property
await zb.zfs.set(datasetName, {
[FREENAS_SMB_SHARE_PROPERTY_NAME]: response.body.id,
});
} else {
/**
* v1 = 409
* v2 = 422
*/
if (
[409, 422].includes(response.statusCode) &&
JSON.stringify(response.body).includes(
"You can't share same filesystem with all hosts twice."
)
) {
// move along
} else {
throw new GrpcError(
grpc.status.UNKNOWN,
`received error creating smb share - code: ${
response.statusCode
} body: ${JSON.stringify(response.body)}`
);
}
}
let volume_context = {
node_attach_driver: "smb",
server: this.options.smb.shareHost,
share: smbName,
};
return volume_context;
default:
throw new GrpcError(
grpc.status.FAILED_PRECONDITION,
`invalid configuration: unknown apiVersion ${apiVersion}`
);
}
} else {
let volume_context = {
node_attach_driver: "smb",
server: this.options.smb.shareHost,
share: smbName,
};
return volume_context;
}
break;
case "iscsi": case "iscsi":
properties = await zb.zfs.get(datasetName, [ properties = await zb.zfs.get(datasetName, [
FREENAS_ISCSI_TARGET_ID_PROPERTY_NAME, FREENAS_ISCSI_TARGET_ID_PROPERTY_NAME,
@ -835,6 +1017,7 @@ class FreeNASDriver extends ControllerZfsSshBaseDriver {
let properties; let properties;
let response; let response;
let endpoint; let endpoint;
let shareId;
switch (driverShareType) { switch (driverShareType) {
case "nfs": case "nfs":
@ -851,7 +1034,7 @@ class FreeNASDriver extends ControllerZfsSshBaseDriver {
properties = properties[datasetName]; properties = properties[datasetName];
this.ctx.logger.debug("zfs props data: %j", properties); this.ctx.logger.debug("zfs props data: %j", properties);
let shareId = properties[FREENAS_NFS_SHARE_PROPERTY_NAME].value; shareId = properties[FREENAS_NFS_SHARE_PROPERTY_NAME].value;
// remove nfs share // remove nfs share
if ( if (
@ -896,6 +1079,68 @@ class FreeNASDriver extends ControllerZfsSshBaseDriver {
} }
} }
break; break;
case "smb":
try {
properties = await zb.zfs.get(datasetName, [
FREENAS_SMB_SHARE_PROPERTY_NAME,
]);
} catch (err) {
if (err.toString().includes("dataset does not exist")) {
return;
}
throw err;
}
properties = properties[datasetName];
this.ctx.logger.debug("zfs props data: %j", properties);
shareId = properties[FREENAS_SMB_SHARE_PROPERTY_NAME].value;
// remove smb share
if (
properties &&
properties[FREENAS_SMB_SHARE_PROPERTY_NAME] &&
properties[FREENAS_SMB_SHARE_PROPERTY_NAME].value != "-"
) {
switch (apiVersion) {
case 1:
case 2:
switch (apiVersion) {
case 1:
endpoint = `/sharing/cifs/${shareId}`;
break;
case 2:
endpoint = `/sharing/smb/id/${shareId}`;
break;
}
response = await httpClient.get(endpoint);
// assume share is gone for now
if ([404, 500].includes(response.statusCode)) {
} else {
response = await httpClient.delete(endpoint);
// returns a 500 if does not exist
// v1 = 204
// v2 = 200
if (![200, 204].includes(response.statusCode)) {
throw new GrpcError(
grpc.status.UNKNOWN,
`received error deleting smb share - share: ${shareId} code: ${
response.statusCode
} body: ${JSON.stringify(response.body)}`
);
}
}
break;
default:
throw new GrpcError(
grpc.status.FAILED_PRECONDITION,
`invalid configuration: unknown apiVersion ${apiVersion}`
);
}
}
break;
case "iscsi": case "iscsi":
// Delete target // Delete target
// NOTE: deletting a target inherently deletes associated targetgroup(s) and targettoextent(s) // NOTE: deletting a target inherently deletes associated targetgroup(s) and targettoextent(s)

View File

@ -75,14 +75,14 @@ class CsiBaseDriver {
async GetPluginInfo(call) { async GetPluginInfo(call) {
return { return {
name: this.ctx.args.csiName, name: this.ctx.args.csiName,
vendor_version: this.ctx.args.version vendor_version: this.ctx.args.version,
}; };
} }
async GetPluginCapabilities(call) { async GetPluginCapabilities(call) {
let capabilities; let capabilities;
const response = { const response = {
capabilities: [] capabilities: [],
}; };
//UNKNOWN = 0; //UNKNOWN = 0;
@ -104,12 +104,12 @@ class CsiBaseDriver {
// accessible from a given node when scheduling workloads. // accessible from a given node when scheduling workloads.
//VOLUME_ACCESSIBILITY_CONSTRAINTS = 2; //VOLUME_ACCESSIBILITY_CONSTRAINTS = 2;
capabilities = this.options.service.identity.capabilities.service || [ capabilities = this.options.service.identity.capabilities.service || [
"UNKNOWN" "UNKNOWN",
]; ];
capabilities.forEach(item => { capabilities.forEach((item) => {
response.capabilities.push({ response.capabilities.push({
service: { type: item } service: { type: item },
}); });
}); });
@ -155,9 +155,9 @@ class CsiBaseDriver {
capabilities = this.options.service.identity.capabilities capabilities = this.options.service.identity.capabilities
.volume_expansion || ["UNKNOWN"]; .volume_expansion || ["UNKNOWN"];
capabilities.forEach(item => { capabilities.forEach((item) => {
response.capabilities.push({ response.capabilities.push({
volume_expansion: { type: item } volume_expansion: { type: item },
}); });
}); });
@ -171,7 +171,7 @@ class CsiBaseDriver {
async ControllerGetCapabilities(call) { async ControllerGetCapabilities(call) {
let capabilities; let capabilities;
const response = { const response = {
capabilities: [] capabilities: [],
}; };
//UNKNOWN = 0; //UNKNOWN = 0;
@ -199,12 +199,12 @@ class CsiBaseDriver {
// See VolumeExpansion for details. // See VolumeExpansion for details.
//EXPAND_VOLUME = 9; //EXPAND_VOLUME = 9;
capabilities = this.options.service.controller.capabilities.rpc || [ capabilities = this.options.service.controller.capabilities.rpc || [
"UNKNOWN" "UNKNOWN",
]; ];
capabilities.forEach(item => { capabilities.forEach((item) => {
response.capabilities.push({ response.capabilities.push({
rpc: { type: item } rpc: { type: item },
}); });
}); });
@ -214,7 +214,7 @@ class CsiBaseDriver {
async NodeGetCapabilities(call) { async NodeGetCapabilities(call) {
let capabilities; let capabilities;
const response = { const response = {
capabilities: [] capabilities: [],
}; };
//UNKNOWN = 0; //UNKNOWN = 0;
@ -227,9 +227,9 @@ class CsiBaseDriver {
//EXPAND_VOLUME = 3; //EXPAND_VOLUME = 3;
capabilities = this.options.service.node.capabilities.rpc || ["UNKNOWN"]; capabilities = this.options.service.node.capabilities.rpc || ["UNKNOWN"];
capabilities.forEach(item => { capabilities.forEach((item) => {
response.capabilities.push({ response.capabilities.push({
rpc: { type: item } rpc: { type: item },
}); });
}); });
@ -239,7 +239,7 @@ class CsiBaseDriver {
async NodeGetInfo(call) { async NodeGetInfo(call) {
return { return {
node_id: process.env.CSI_NODE_ID || os.hostname(), node_id: process.env.CSI_NODE_ID || os.hostname(),
max_volumes_per_node: 0 max_volumes_per_node: 0,
}; };
} }
@ -296,12 +296,15 @@ class CsiBaseDriver {
case "nfs": case "nfs":
device = `${volume_context.server}:${volume_context.share}`; device = `${volume_context.server}:${volume_context.share}`;
break; break;
case "smb":
device = `//${volume_context.server}/${volume_context.share}`;
break;
case "iscsi": case "iscsi":
// create DB entry // create DB entry
// https://library.netapp.com/ecmdocs/ECMP1654943/html/GUID-8EC685B4-8CB6-40D8-A8D5-031A3899BCDC.html // https://library.netapp.com/ecmdocs/ECMP1654943/html/GUID-8EC685B4-8CB6-40D8-A8D5-031A3899BCDC.html
// put these options in place to force targets managed by csi to be explicitly attached (in the case of unclearn shutdown etc) // put these options in place to force targets managed by csi to be explicitly attached (in the case of unclearn shutdown etc)
let nodeDB = { let nodeDB = {
"node.startup": "manual" "node.startup": "manual",
}; };
const nodeDBKeyPrefix = "node-db."; const nodeDBKeyPrefix = "node-db.";
const normalizedSecrets = this.getNormalizedParameters( const normalizedSecrets = this.getNormalizedParameters(
@ -420,7 +423,7 @@ class CsiBaseDriver {
await mount.bindMount(device, block_path, [ await mount.bindMount(device, block_path, [
"-o", "-o",
bind_mount_flags.join(",") bind_mount_flags.join(","),
]); ]);
} }
break; break;
@ -506,7 +509,7 @@ class CsiBaseDriver {
session.attached_scsi_devices.host.devices session.attached_scsi_devices.host.devices
) { ) {
is_attached_to_session = session.attached_scsi_devices.host.devices.some( is_attached_to_session = session.attached_scsi_devices.host.devices.some(
device => { (device) => {
if (device.attached_scsi_disk == block_device_info.name) { if (device.attached_scsi_disk == block_device_info.name) {
return true; return true;
} }
@ -525,7 +528,7 @@ class CsiBaseDriver {
while (!loggedOut) { while (!loggedOut) {
try { try {
await iscsi.iscsiadm.logout(session.target, [ await iscsi.iscsiadm.logout(session.target, [
session.persistent_portal session.persistent_portal,
]); ]);
loggedOut = true; loggedOut = true;
} catch (err) { } catch (err) {
@ -659,7 +662,7 @@ class CsiBaseDriver {
if (!result) { if (!result) {
await mount.bindMount(normalized_staging_path, target_path, [ await mount.bindMount(normalized_staging_path, target_path, [
"-o", "-o",
bind_mount_flags.join(",") bind_mount_flags.join(","),
]); ]);
} else { } else {
// if is mounted, ensure proper source // if is mounted, ensure proper source
@ -760,9 +763,9 @@ class CsiBaseDriver {
available: result.avail, available: result.avail,
total: result.size, total: result.size,
used: result.used, used: result.used,
unit: "BYTES" unit: "BYTES",
} },
] ],
}; };
case "block": case "block":
result = await filesystem.getBlockDevice(device_path); result = await filesystem.getBlockDevice(device_path);
@ -771,9 +774,9 @@ class CsiBaseDriver {
usage: [ usage: [
{ {
total: result.size, total: result.size,
unit: "BYTES" unit: "BYTES",
} },
] ],
}; };
default: default:
throw new GrpcError( throw new GrpcError(