democratic-csi/src/utils/windows.js

730 lines
19 KiB
JavaScript

const { result } = require("lodash");
const _ = require("lodash");
const Powershell = require("./powershell").Powershell;
/**
* https://kubernetes.io/blog/2021/08/16/windows-hostprocess-containers/
* https://github.com/kubernetes-csi/csi-proxy/tree/master/pkg/os
*
* multipath notes:
* - http://scst.sourceforge.net/mc_s.html
* - https://github.com/kubernetes-csi/csi-proxy/pull/99
* - https://docs.microsoft.com/en-us/azure/storsimple/storsimple-8000-configure-mpio-windows-server
* - https://support.purestorage.com/Legacy_Documentation/Setting_the_MPIO_Policy
* - https://docs.microsoft.com/en-us/powershell/module/mpio/?view=windowsserver2022-ps
*
* Get-WindowsFeature -Name 'Multipath-IO'
* Add-WindowsFeature -Name 'Multipath-IO'
*
* Enable-MSDSMAutomaticClaim -BusType "iSCSI"
* Disable-MSDSMAutomaticClaim -BusType "iSCSI"
*
* Get-MSDSMGlobalDefaultLoadBalancePolicy
* Set-MSDSMGlobalLoadBalancePolicy -Policy RR
*
* synology woes:
* - https://community.spiceworks.com/topic/2279882-synology-iscsi-will-not-disconnect-using-powershell-commands
* - https://support.hpe.com/hpesc/public/docDisplay?docId=c01880810&docLocale=en_US
* - https://askubuntu.com/questions/1159103/why-is-iscsi-trying-to-connect-on-ipv6-at-boot
*/
class Windows {
constructor() {
this.ps = new Powershell();
}
resultToArray(result) {
if (!result.parsed) {
result.parsed = [];
}
if (!Array.isArray(result.parsed)) {
result.parsed = [result.parsed];
}
}
async GetRealTarget(path) {
let item;
let target;
do {
item = await this.GetItem(path);
path = null;
target = _.get(item, "Target.[0]", "");
if (target.startsWith("UNC")) {
let parts = target.split("\\", 3);
return `\\\\${parts[1]}\\${parts[2]}`;
} else if (target.startsWith("Volume")) {
return `\\\\?\\${target}`;
} else {
path = target;
}
} while (path);
}
async GetItem(localPath) {
let command;
let result;
command = 'Get-Item "$Env:localpath" | ConvertTo-Json';
try {
result = await this.ps.exec(command, {
env: {
localpath: localPath,
},
});
return result.parsed;
} catch (err) {}
}
async GetSmbGlobalMapping(remotePath) {
let command;
command =
"Get-SmbGlobalMapping -RemotePath $Env:smbremotepath | ConvertTo-Json";
try {
return await this.ps.exec(command, {
env: {
smbremotepath: remotePath,
},
});
} catch (err) {}
}
async NewSmbGlobalMapping(remotePath, username, password) {
let command;
command =
"$PWord = ConvertTo-SecureString -String $Env:smbpassword -AsPlainText -Force;$Credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $Env:smbuser, $PWord;New-SmbGlobalMapping -RemotePath $Env:smbremotepath -Credential $Credential -RequirePrivacy $true";
await this.ps.exec(command, {
env: {
smbuser: username,
smbpassword: password,
smbremotepath: remotePath,
},
});
}
async RemoveSmbGlobalMapping(remotePath) {
let result;
let command;
command = "Remove-SmbGlobalMapping -RemotePath $Env:smbremotepath -Force";
do {
result = await this.GetSmbGlobalMapping(remotePath);
if (result) {
await this.ps.exec(command, {
env: {
smbremotepath: remotePath,
},
});
}
} while (result);
}
async NewSmbLink(remotePath, localPath) {
let command;
if (!remotePath.endsWith("\\")) {
remotePath = `${remotePath}\\`;
}
command =
"New-Item -ItemType SymbolicLink $Env:smblocalPath -Target $Env:smbremotepath";
await this.ps.exec(command, {
env: {
smblocalpath: localPath,
smbremotepath: remotePath,
},
});
}
async NewIscsiTargetPortal(address, port) {
let command;
command =
"New-IscsiTargetPortal -TargetPortalAddress ${Env:iscsi_tp_address} -TargetPortalPortNumber ${Env:iscsi_tp_port}";
await this.ps.exec(command, {
env: {
iscsi_tp_address: address,
iscsi_tp_port: port,
},
});
}
async RemoveIscsiTargetPortalByTargetPortalAddress(targetPortalAddress) {
let command;
command = `Remove-IscsiTargetPortal -TargetPortalAddress ${targetPortalAddress} -Confirm:$false`;
await this.ps.exec(command);
}
async RemoveIscsiTargetPortalByTargetPortalAddressTargetPortalPort(
targetPortalAddress,
targetPortalPort
) {
let command;
command = `Get-IscsiTargetPortal -TargetPortalAddress ${targetPortalAddress} -TargetPortalPortNumber ${targetPortalPort} | Remove-IscsiTargetPortal -Confirm:$false`;
await this.ps.exec(command);
}
async IscsiTargetIsConnectedByPortalAddressPortalPort(address, port, iqn) {
let sessions = await this.GetIscsiSessionsByTargetNodeAddress(iqn);
for (let session of sessions) {
let connections = await this.GetIscsiConnectionsByIscsiSessionIdentifier(
session.SessionIdentifier
);
for (let connection of connections) {
if (
connection.TargetAddress == address &&
connection.TargetPortNumber == port
) {
return true;
}
}
}
//process.exit(1);
return false;
}
/**
* -IsMultipathEnabled
*
* @param {*} address
* @param {*} port
* @param {*} iqn
* @param {*} authType
* @param {*} chapUser
* @param {*} chapSecret
*/
async ConnectIscsiTarget(
address,
port,
iqn,
authType,
chapUser,
chapSecret,
multipath = false
) {
let is_connected = await this.IscsiTargetIsConnectedByPortalAddressPortalPort(address, port, iqn);
if (is_connected) {
return;
}
let command;
// -IsMultipathEnabled $([System.Convert]::ToBoolean(${Env:iscsi_is_multipath}))
// -InitiatorPortalAddress
command =
"Connect-IscsiTarget -TargetPortalAddress ${Env:iscsi_tp_address} -TargetPortalPortNumber ${Env:iscsi_tp_port} -NodeAddress ${Env:iscsi_target_iqn} -AuthenticationType ${Env:iscsi_auth_type}";
if (chapUser) {
command += " -ChapUsername ${Env:iscsi_chap_user}";
}
if (chapSecret) {
command += " -ChapSecret ${Env:iscsi_chap_secret}";
}
if (multipath) {
command +=
" -IsMultipathEnabled $([System.Convert]::ToBoolean(${Env:iscsi_is_multipath}))";
}
try {
await this.ps.exec(command, {
env: {
iscsi_tp_address: address,
iscsi_tp_port: port,
iscsi_target_iqn: iqn,
iscsi_auth_type: authType,
iscsi_chap_user: chapUser,
iscsi_chap_secret: chapSecret,
iscsi_is_multipath: String(multipath),
},
});
} catch (err) {
let details = _.get(err, "stderr", "");
if (
!details.includes(
"The target has already been logged in via an iSCSI session"
)
) {
throw err;
}
}
}
async GetIscsiTargetsByTargetPortalAddressTargetPortalPort(address, port) {
let command;
let result;
command =
"Get-IscsiTargetPortal -TargetPortalAddress ${Env:iscsi_tp_address} -TargetPortalPortNumber ${Env:iscsi_tp_port} | Get-IscsiTarget | ConvertTo-Json";
result = await this.ps.exec(command, {
env: {
iscsi_tp_address: address,
iscsi_tp_port: port,
},
});
this.resultToArray(result);
return result.parsed;
}
/**
* This disconnects *all* sessions from the target
*
* @param {*} nodeAddress
*/
async DisconnectIscsiTargetByNodeAddress(nodeAddress) {
let command;
command = `Disconnect-IscsiTarget -NodeAddress ${nodeAddress.toLowerCase()} -Confirm:$false`;
await this.ps.exec(command);
}
async GetIscsiConnectionsByIscsiSessionIdentifier(iscsiSessionIdentifier) {
let command;
let result;
command = `Get-IscsiSession -SessionIdentifier ${iscsiSessionIdentifier} | Get-IscsiConnection | ConvertTo-Json`;
result = await this.ps.exec(command);
this.resultToArray(result);
return result.parsed;
}
async GetIscsiSessions() {
let command;
let result;
command = `Get-IscsiSession | ConvertTo-Json`;
result = await this.ps.exec(command);
this.resultToArray(result);
return result.parsed;
}
async GetIscsiSessionsByDiskNumber(diskNumber) {
let command;
let result;
command = `Get-Disk -Number ${diskNumber} | Get-IscsiSession | ConvertTo-Json`;
result = await this.ps.exec(command);
this.resultToArray(result);
return result.parsed;
}
async GetIscsiSessionsByVolumeId(volumeId) {
let sessions = [];
let disks = await this.GetDisksByVolumeId(volumeId);
for (let disk of disks) {
let i_sessions = await this.GetIscsiSessionsByDiskNumber(disk.DiskNumber);
sessions.push(...i_sessions);
}
return sessions;
}
async GetIscsiSessionsByTargetNodeAddress(targetNodeAddress) {
let sessions = await this.GetIscsiSessions();
let r_sessions = [];
// Where-Object { $_.TargetNodeAddress -eq ${targetNodeAddress} }
for (let session of sessions) {
if (session.TargetNodeAddress == targetNodeAddress) {
r_sessions.push(session);
}
}
return r_sessions;
}
async GetIscsiSessionByIscsiConnectionIdentifier(iscsiConnectionIdentifier) {
let command;
let result;
command = `Get-IscsiConnection -ConnectionIdentifier ${iscsiConnectionIdentifier} | Get-IscsiSession | ConvertTo-Json`;
result = await this.ps.exec(command);
return result.parsed;
}
async GetIscsiTargetPortalBySessionId(sessionId) {
let command;
let result;
command = `Get-IscsiSession -SessionIdentifier ${sessionId} | Get-IscsiTargetPortal | ConvertTo-Json`;
result = await this.ps.exec(command);
return result.parsed;
}
async UpdateHostStorageCache() {
let command;
command = "Update-HostStorageCache";
await this.ps.exec(command);
}
async GetIscsiDisks() {
let command;
let result;
command = "Get-iSCSISession | Get-Disk | ConvertTo-Json";
result = await this.ps.exec(command);
this.resultToArray(result);
return result.parsed;
}
async GetWin32DiskDrives() {
let command;
let result;
command = "Get-WmiObject Win32_DiskDrive | ConvertTo-Json";
result = await this.ps.exec(command);
this.resultToArray(result);
return result.parsed;
}
async GetDiskLunByDiskNumber(diskNumber) {
let result;
result = await this.GetWin32DiskDrives();
for (let drive of result) {
if (drive.Index == diskNumber) {
return drive.SCSILogicalUnit;
}
}
}
async GetTargetDisks(address, port, iqn) {
let command;
let result;
// this fails for synology for some reason
//command =
// '$ErrorActionPreference = "Stop"; $tp = Get-IscsiTargetPortal -TargetPortalAddress ${Env:iscsi_tp_address} -TargetPortalPortNumber ${Env:iscsi_tp_port}; $t = $tp | Get-IscsiTarget | Where-Object { $_.NodeAddress -eq ${Env:iscsi_target_iqn} }; $s = Get-iSCSISession -IscsiTarget $t; $s | Get-Disk | ConvertTo-Json';
command =
'$ErrorActionPreference = "Stop"; $s = Get-iSCSISession | Where-Object { $_.TargetNodeAddress -eq ${Env:iscsi_target_iqn} }; $s | Get-Disk | ConvertTo-Json';
result = await this.ps.exec(command, {
env: {
iscsi_tp_address: address,
iscsi_tp_port: port,
iscsi_target_iqn: iqn,
},
});
this.resultToArray(result);
return result.parsed;
}
async GetTargetDisksByIqn(iqn) {
let command;
let result;
command =
'$ErrorActionPreference = "Stop"; $s = Get-iSCSISession | Where-Object { $_.TargetNodeAddress -eq ${Env:iscsi_target_iqn} }; $s | Get-Disk | ConvertTo-Json';
result = await this.ps.exec(command, {
env: {
iscsi_target_iqn: iqn,
},
});
this.resultToArray(result);
return result.parsed;
}
/**
* This can be multiple when mpio is not configured properly and each
* session creates a new disk
*
* @param {*} iqn
* @param {*} lun
* @returns
*/
async GetTargetDisksByIqnLun(iqn, lun) {
let result;
let dlun;
let disks = [];
result = await this.GetTargetDisksByIqn(iqn);
for (let disk of result) {
dlun = await this.GetDiskLunByDiskNumber(disk.DiskNumber);
if (dlun == lun) {
disks.push(disk);
}
}
return disks;
}
async GetDiskByDiskNumber(diskNumber) {
let command;
let result;
command = `Get-Disk -Number ${diskNumber} | ConvertTo-Json`;
result = await this.ps.exec(command);
return result.parsed;
}
async GetDisks() {
let command;
let result;
command = "Get-Disk | ConvertTo-Json";
result = await this.ps.exec(command);
this.resultToArray(result);
return result.parsed;
}
async GetPartitions() {
let command;
let result;
command = "Get-Partition | ConvertTo-Json";
result = await this.ps.exec(command);
this.resultToArray(result);
return result.parsed;
}
async GetPartitionsByDiskNumber(diskNumber) {
let command;
let result;
command = `Get-Disk -Number ${diskNumber} | Get-Partition | ConvertTo-Json`;
result = await this.ps.exec(command);
this.resultToArray(result);
return result.parsed;
}
async DiskIsInitialized(diskNumber) {
let disk = await this.GetDiskByDiskNumber(diskNumber);
return disk.PartitionStyle != "RAW";
}
async InitializeDisk(diskNumber) {
let command;
command = `Initialize-Disk -Number ${diskNumber} -PartitionStyle GPT`;
await this.ps.exec(command);
}
async DiskHasBasicPartition(diskNumber) {
let command;
let result;
command = `Get-Partition | Where DiskNumber -eq ${diskNumber} | Where Type -ne Reserved | ConvertTo-Json`;
result = await this.ps.exec(command);
this.resultToArray(result);
return result.parsed.length > 0;
}
async NewPartition(diskNumber) {
let command;
command = `New-Partition -DiskNumber ${diskNumber} -UseMaximumSize`;
await this.ps.exec(command);
}
async PartitionDisk(diskNumber) {
let is_intialized;
let has_basic_partition;
is_intialized = await this.DiskIsInitialized(diskNumber);
if (!is_intialized) {
await this.InitializeDisk(diskNumber);
}
has_basic_partition = await this.DiskHasBasicPartition(diskNumber);
if (!has_basic_partition) {
await this.NewPartition(diskNumber);
}
}
async GetLastPartitionByDiskNumber(diskNumber) {
let partitions = await this.GetPartitionsByDiskNumber(diskNumber);
let p;
for (let partition of partitions) {
if (!p) {
p = partition;
}
if (partition.PartitionNumber > p.PartitionNumber) {
p = partition;
}
}
return p;
}
async GetVolumesByDiskNumber(diskNumber) {
let command;
command = `Get-Disk -Number ${diskNumber} | Get-Partition | Get-Volume | ConvertTo-Json`;
result = await this.ps.exec(command);
this.resultToArray(result);
return result.parsed;
}
async GetVolumeByDiskNumberPartitionNumber(diskNumber, partitionNumber) {
let command;
let result;
command = `Get-Disk -Number ${diskNumber} | Get-Partition -PartitionNumber ${partitionNumber} | Get-Volume | ConvertTo-Json`;
result = await this.ps.exec(command);
return result.parsed;
}
async GetVolumeByVolumeId(volumeId) {
let command;
let result;
command = `Get-Volume -UniqueId \"${volumeId}\" -ErrorAction Stop | ConvertTo-Json`;
result = await this.ps.exec(command);
return result.parsed;
}
async GetPartitionsByVolumeId(volumeId) {
let partitions = await this.GetPartitions();
let p = [];
for (let partition of partitions) {
let paths = _.get(partition, "AccessPaths", []);
if (paths === null) {
paths = [];
}
if (!Array.isArray(paths)) {
paths = [];
}
if (paths.includes(volumeId)) {
p.push(partition);
}
}
return p;
}
async GetDisksByVolumeId(volumeId) {
let partitions = await this.GetPartitionsByVolumeId(volumeId);
let diskNumbers = new Set();
for (let parition of partitions) {
diskNumbers.add(parition.DiskNumber);
}
let disks = [];
let disk;
for (let diskNumber of diskNumbers) {
disk = await this.GetDiskByDiskNumber(diskNumber);
if (disk) {
disks.push(disk);
}
}
return disks;
}
async VolumeIsFormatted(volumeId) {
let volume = await this.GetVolumeByVolumeId(volumeId);
let type = volume.FileSystemType || "";
type = type.toLowerCase().trim();
if (!type || type == "unknown") {
return false;
}
return true;
}
async VolumeIsIscsi(volumeId) {
let disks = await this.GetDisksByVolumeId(volumeId);
for (let disk of disks) {
if (_.get(disk, "BusType", "").toLowerCase() == "iscsi") {
return true;
}
}
return false;
}
async FormatVolume(volumeId) {
let command;
command = `Get-Volume -UniqueId \"${volumeId}\" | Format-Volume -FileSystem ntfs -Confirm:$false`;
await this.ps.exec(command);
}
async ResizeVolume(volumeId, size = 0) {
let command;
let final_size;
if (!size) {
final_size = await this.GetVolumeMaxSize(volumeId);
} else {
final_size = size;
}
let current_size = await this.GetVolumeSize(volumeId);
if (current_size >= final_size) {
return;
}
command = `Get-Volume -UniqueId \"${volumeId}\" | Get-Partition | Resize-Partition -Size ${final_size}`;
try {
await this.ps.exec(command);
} catch (err) {
let details = _.get(err, "stderr", "");
if (
!details.includes(
"The size of the extent is less than the minimum of 1MB"
)
) {
throw err;
}
}
}
async GetVolumeMaxSize(volumeId) {
let command;
let result;
command = `Get-Volume -UniqueId \"${volumeId}\" | Get-partition | Get-PartitionSupportedSize | Select SizeMax | ConvertTo-Json`;
result = await this.ps.exec(command);
return result.parsed.SizeMax;
}
async GetVolumeSize(volumeId) {
let command;
let result;
command = `Get-Volume -UniqueId \"${volumeId}\" | Get-partition | ConvertTo-Json`;
result = await this.ps.exec(command);
return result.parsed.Size;
}
async MountVolume(volumeId, path) {
let command;
command = `Get-Volume -UniqueId \"${volumeId}\" | Get-Partition | Add-PartitionAccessPath -AccessPath ${path}`;
await this.ps.exec(command);
}
async UnmountVolume(volumeId, path) {
let command;
command = `Get-Volume -UniqueId \"${volumeId}\" | Get-Partition | Remove-PartitionAccessPath -AccessPath ${path}`;
await this.ps.exec(command);
}
async WriteVolumeCache(volumeId) {
let command;
command = `Get-Volume -UniqueId \"${volumeId}\" | Write-Volumecache`;
await this.ps.exec(command);
}
}
module.exports.Windows = Windows;