Add iSCSI targets/LUNs through Pacemaker clusters

Support pcs for zfs-generic to configure iSCSI
targets and LUNs. It uses the targetcli implementation
since it's the default pcs resources and also to
keep compatibility with the rest of the code.

closes #462
This commit is contained in:
Michel Peterson 2025-03-22 19:36:00 +02:00
parent 93e0446fa3
commit 9252b4e779
2 changed files with 175 additions and 2 deletions

View File

@ -44,6 +44,7 @@ zfs:
iscsi: iscsi:
shareStrategy: "targetCli" shareStrategy: "targetCli"
#shareStrategy: "pcs"
# https://kifarunix.com/how-to-install-and-configure-iscsi-storage-server-on-ubuntu-18-04/ # https://kifarunix.com/how-to-install-and-configure-iscsi-storage-server-on-ubuntu-18-04/
# https://kifarunix.com/how-install-and-configure-iscsi-storage-server-on-centos-7/ # https://kifarunix.com/how-install-and-configure-iscsi-storage-server-on-centos-7/
@ -75,6 +76,17 @@ iscsi:
attributes: attributes:
# set to 1 to enable Thin Provisioning Unmap # set to 1 to enable Thin Provisioning Unmap
emulate_tpu: 0 emulate_tpu: 0
shareStrategyPcs:
#sudoEnabled: true
pcs_group: "group-nas"
basename: "iqn.2003-01.org.linux-iscsi.ubuntu-19.x8664"
auth:
enabled: 0
# CHAP incoming only, mutual not supported by the Pacemaker resource agent
incoming_username: "foo"
incoming_password: "bar"
targetPortal: "server[:port]" targetPortal: "server[:port]"
# for multipath # for multipath
targetPortals: [] # [ "server[:port]", "server[:port]", ... ] targetPortals: [] # [ "server[:port]", "server[:port]", ... ]

View File

@ -298,8 +298,70 @@ create /backstores/block/${assetName}
} }
); );
break; break;
default: case "pcs":
basename = this.options.iscsi.shareStrategyPcs.basename;
pcs_group = this.options.iscsi.shareStrategyPcs.pcs_group;
let groupText = `group ${pcs_group}`;
let createTargetText = [
`resource create --future target-${assetName} iSCSITarget`,
'implementation="lio-t"',
`iqn="${basename}:${assetName}"`
];
if (this.options.iscsi.shareStrategyPcs.auth.enabled) {
createTargetText.push(`incoming_username="${this.options.iscsi.shareStrategyPcs.auth.incoming_username}"`);
createTargetText.push(`incoming_password="${this.options.iscsi.shareStrategyPcs.auth.incoming_password}"`);
}
createTargetText.push(groupText);
await GeneralUtils.retry(
3,
2000,
async () => {
await this.pcsCommand(createTargetText);
},
{
retryCondition: (err) => {
if (err.stdout && err.stdout.includes("Timed Out")) {
return true;
}
return false;
},
}
);
let createLunText = [
`resource create --future lun-${assetName} iSCSILogicalUnit`,
'implementation="lio-t"',
`target_iqn="${basename}:${assetName}" lun="1"`,
`path="/dev/${extentDiskName}"`,
groupText
];
await GeneralUtils.retry(
3,
2000,
async () => {
await this.pcsCommand(createLunText);
},
{
retryCondition: (err) => {
if (err.stdout && err.stdout.includes("Timed Out")) {
return true;
}
return false;
},
}
);
break; break;
default:
throw new GrpcError(
grpc.status.FAILED_PRECONDITION,
`invalid configuration: unknown shareStrategy ${this.options.iscsi.shareStrategy}`
);
} }
// iqn = target // iqn = target
@ -690,8 +752,54 @@ delete ${assetName}
); );
break; break;
default: case "pcs":
let deleteLunText = [
`resource delete lun-${assetName}`
];
await GeneralUtils.retry(
3,
2000,
async () => {
await this.pcsCommand(deleteLunText);
},
{
retryCondition: (err) => {
if (err.stdout && err.stdout.includes("Timed Out")) {
return true;
}
return false;
},
}
);
let deleteTargetText = [
`resource delete target-${assetName}`
];
await GeneralUtils.retry(
3,
2000,
async () => {
await this.pcsCommand(deleteTargetText);
},
{
retryCondition: (err) => {
if (err.stdout && err.stdout.includes("Timed Out")) {
return true;
}
return false;
},
}
);
break; break;
default:
throw new GrpcError(
grpc.status.FAILED_PRECONDITION,
`invalid configuration: unknown shareStrategy ${this.options.iscsi.shareStrategy}`
);
} }
break; break;
} }
@ -841,6 +949,9 @@ save_config filename=${this.options.nvmeof.shareStrategySpdkCli.configPath}
case "targetCli": case "targetCli":
// nothing required, just need to rescan on the node // nothing required, just need to rescan on the node
break; break;
case "pcs":
// nothing required, just need to rescan on the node
break;
default: default:
break; break;
} }
@ -851,6 +962,56 @@ save_config filename=${this.options.nvmeof.shareStrategySpdkCli.configPath}
} }
} }
async pcsCommand(commandLines) {
const execClient = this.getExecClient();
const driver = this;
let command = "sh";
let args = ["-c"];
let cliArgs = ["pcs"];
if (
_.get(this.options, "iscsi.shareStrategyPcs.sudoEnabled", false)
) {
cliArgs.unshift("sudo");
}
let cliCommand = [];
cliCommand.push(cliArgs.join(" "));
cliCommand.push(commandLines.join(" "));
args.push("'" + cliCommand.join(" ") + "'");
let logCommandTmp = command + " " + args.join(" ");
let logCommand = "";
logCommandTmp.split(" ").forEach((term) => {
logCommand += " ";
if (term.startsWith("incoming_password=")) {
logCommand += "incoming_password=<redacted>";
} else {
logCommand += term;
}
});
driver.ctx.logger.verbose("Pcs command:" + logCommand);
let options = {
pty: true,
};
let response = await execClient.exec(
execClient.buildCommand(command, args),
options
);
driver.ctx.logger.verbose(
"Pcs response: " + JSON.stringify(response)
);
if (response.code != 0) {
throw response;
}
return response;
}
async targetCliCommand(data) { async targetCliCommand(data) {
const execClient = this.getExecClient(); const execClient = this.getExecClient();
const driver = this; const driver = this;