native windows support

This commit is contained in:
Travis Glenn Hansen 2022-05-05 10:38:30 -06:00
parent 7fe916c916
commit 7a5b6b58b1
15 changed files with 1810 additions and 9 deletions

2
.gitignore vendored
View File

@ -1,2 +1,4 @@
**~
node_modules
dev
/ci/bin/*dev*

View File

@ -183,6 +183,7 @@ async function requestHandlerProxy(call, callback, serviceMethodName) {
// for testing purposes
//await GeneralUtils.sleep(10000);
//throw new Error("fake error");
// for CI/testing purposes
if (["NodePublishVolume", "NodeStageVolume"].includes(serviceMethodName)) {

8
ci/bin/build.ps1 Normal file
View File

@ -0,0 +1,8 @@
node --version
npm --version
# install deps
npm i
# tar node_modules to keep the number of files low to upload
tar -zcf node_modules.tar.gz node_modules

16
ci/bin/helper.ps1 Normal file
View File

@ -0,0 +1,16 @@
#Set-StrictMode -Version Latest
#$ErrorActionPreference = "Stop"
#$PSDefaultParameterValues['*:ErrorAction'] = "Stop"
function ThrowOnNativeFailure {
if (-not $?) {
throw 'Native Failure'
}
}
function psenvsubstr($data) {
foreach($v in Get-ChildItem env:) {
$key = '${' + $v.Name + '}'
$data = $data.Replace($key, $v.Value)
}
return $data
}

View File

@ -0,0 +1,15 @@
if (! $PSScriptRoot) {
$PSScriptRoot = $args[0]
}
. "${PSScriptRoot}\helper.ps1"
Set-Location $env:PWD
Write-Output "launching csi-grpc-proxy"
$env:PROXY_TO = "npipe://" + $env:NPIPE_ENDPOINT
$env:BIND_TO = "unix://" + $env:CSI_ENDPOINT
# https://stackoverflow.com/questions/2095088/error-when-calling-3rd-party-executable-from-powershell-when-using-an-ide
csi-grpc-proxy.exe 2>&1 | % { "$_" }

View File

@ -0,0 +1,60 @@
if (! $PSScriptRoot) {
$PSScriptRoot = $args[0]
}
. "${PSScriptRoot}\helper.ps1"
Set-Location $env:PWD
$exit_code = 0
$tmpdir = New-Item -ItemType Directory -Path ([System.IO.Path]::GetTempPath()) -Name ([System.IO.Path]::GetRandomFileName())
$env:CSI_SANITY_TEMP_DIR = $tmpdir.FullName
if (! $env:CSI_SANITY_FOCUS) {
$env:CSI_SANITY_FOCUS = "Node Service"
}
if (! $env:CSI_SANITY_SKIP) {
$env:CSI_SANITY_SKIP = ""
}
# cleanse endpoint to something csi-sanity plays nicely with
$endpoint = ${env:CSI_ENDPOINT}
$endpoint = $endpoint.replace("C:\", "/")
$endpoint = $endpoint.replace("\", "/")
Write-Output "launching csi-sanity"
Write-Output "connecting to: ${endpoint}"
Write-Output "skip: ${env:CSI_SANITY_SKIP}"
Write-Output "focus: ${env:CSI_SANITY_FOCUS}"
csi-sanity.exe -"csi.endpoint" "unix://${endpoint}" `
-"ginkgo.failFast" `
-"csi.mountdir" "${env:CSI_SANITY_TEMP_DIR}\mnt" `
-"csi.stagingdir" "${env:CSI_SANITY_TEMP_DIR}\stage" `
-"csi.testvolumeexpandsize" 2147483648 `
-"csi.testvolumesize" 1073741824 `
-"ginkgo.focus" "${env:CSI_SANITY_FOCUS}"
# does not work the same as linux for some reason
#-"ginkgo.skip" "${env:CSI_SANITY_SKIP}" `
if (-not $?) {
$exit_code = $LASTEXITCODE
Write-Output "csi-sanity exit code: ${exit_code}"
$exit_code = 1
}
# remove tmp dir
Remove-Item -Path "$env:CSI_SANITY_TEMP_DIR" -Force -Recurse
#Exit $exit_code
Write-Output "exiting with exit code: ${exit_code}"
if ($exit_code -gt 0) {
throw "csi-sanity failed"
}
# these do not work for whatever reason
#Exit $exit_code
#[System.Environment]::Exit($exit_code)

29
ci/bin/launch-server.ps1 Normal file
View File

@ -0,0 +1,29 @@
if (! $PSScriptRoot) {
$PSScriptRoot = $args[0]
}
. "${PSScriptRoot}\helper.ps1"
Set-Location $env:PWD
Write-Output "launching server"
$env:LOG_LEVEL = "debug"
$env:CSI_VERSION = "1.5.0"
$env:CSI_NAME = "driver-test"
$env:CSI_SANITY = "1"
if (! ${env:CONFIG_FILE}) {
$env:CONFIG_FILE = $env:TEMP + "\csi-config-" + $env:CI_BUILD_KEY + ".yaml"
if ($env:TEMPLATE_CONFIG_FILE) {
$config_data = Get-Content "${env:TEMPLATE_CONFIG_FILE}" -Raw
$config_data = psenvsubstr($config_data)
$config_data | Set-Content "${env:CONFIG_FILE}"
}
}
node "${PSScriptRoot}\..\..\bin\democratic-csi" `
--log-level "$env:LOG_LEVEL" `
--driver-config-file "$env:CONFIG_FILE" `
--csi-version "$env:CSI_VERSION" `
--csi-name "$env:CSI_NAME" `
--server-socket "${env:NPIPE_ENDPOINT}" 2>&1 | % { "$_" }

107
ci/bin/run.ps1 Normal file
View File

@ -0,0 +1,107 @@
# https://stackoverflow.com/questions/2095088/error-when-calling-3rd-party-executable-from-powershell-when-using-an-ide
#
# Examples:
#
# $mypath = $MyInvocation.MyCommand.Path
# Get-ChildItem env:\
# Get-Job | Where-Object -Property State -eq “Running”
# Get-Location (like pwd)
# if ($null -eq $env:FOO) { $env:FOO = 'bar' }
. "${PSScriptRoot}\helper.ps1"
function Job-Cleanup() {
Get-Job | Stop-Job
Get-Job | Remove-Job
}
# start clean
Job-Cleanup
# install from artifacts
if (Test-Path "node_modules.tar.gz") {
Write-Output "extracting node_modules.tar.gz"
tar -zxf node_modules.tar.gz
}
# setup env
$env:PWD = (Get-Location).Path
$env:CI_BUILD_KEY = ([guid]::NewGuid() -Split "-")[0]
$env:CSI_ENDPOINT = $env:TEMP + "\csi-sanity-" + $env:CI_BUILD_KEY + ".sock"
$env:NPIPE_ENDPOINT = "//./pipe/csi-sanity-" + $env:CI_BUILD_KEY + "csi.sock"
# testing values
if (Test-Path "${PSScriptRoot}\run-dev.ps1") {
. "${PSScriptRoot}\run-dev.ps1"
}
# launch server
$server_job = Start-Job -FilePath .\ci\bin\launch-server.ps1 -InitializationScript {} -ArgumentList $PSScriptRoot
# launch csi-grpc-proxy
$csi_grpc_proxy_job = Start-Job -FilePath .\ci\bin\launch-csi-grpc-proxy.ps1 -InitializationScript {} -ArgumentList $PSScriptRoot
# wait for socket to appear
$iter = 0
$max_iter = 60
$started = 1
while (!(Test-Path "${env:CSI_ENDPOINT}")) {
$iter++
Write-Output "Waiting for ${env:CSI_ENDPOINT} to appear"
Start-Sleep 1
Get-Job | Receive-Job
if ($iter -gt $max_iter) {
Write-Output "${env:CSI_ENDPOINT} failed to appear"
$started = 0
break
}
}
# launch csi-sanity
if ($started -eq 1) {
$csi_sanity_job = Start-Job -FilePath .\ci\bin\launch-csi-sanity.ps1 -InitializationScript {} -ArgumentList $PSScriptRoot
}
# https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/get-job?view=powershell-7.2
# -ChildJobState
while ($csi_sanity_job -and ($csi_sanity_job.State -eq "Running" -or $csi_sanity_job.State -eq "NotStarted")) {
foreach ($job in Get-Job) {
try {
$job | Receive-Job
}
catch {
if ($job.State -ne "Failed") {
throw $_
}
}
}
}
# spew any remaining job output to the console
foreach ($job in Get-Job) {
try {
$job | Receive-Job
}
catch {}
}
# wait for good measure
if ($csi_sanity_job) {
Wait-Job -Job $csi_sanity_job
}
#Get-Job | fl
$exit_code = 0
if (! $csi_sanity_job) {
$exit_code = 1
}
if ($csi_sanity_job -and $csi_sanity_job.State -eq "Failed") {
$exit_code = 1
}
# cleanup after ourselves
Job-Cleanup
Exit $exit_code

View File

@ -0,0 +1,31 @@
driver: zfs-generic-iscsi
sshConnection:
host: ${SERVER_HOST}
port: 22
username: ${SERVER_USERNAME}
password: ${SERVER_PASSWORD}
zfs:
datasetParentName: tank/ci/${CI_BUILD_KEY}/v
detachedSnapshotsDatasetParentName: tank/ci/${CI_BUILD_KEY}/s
zvolCompression:
zvolDedup:
zvolEnableReservation: false
zvolBlocksize:
iscsi:
targetPortal: ${SERVER_HOST}
interface: ""
namePrefix: "csi-ci-${CI_BUILD_KEY}"
nameSuffix: ""
shareStrategy: "targetCli"
shareStrategyTargetCli:
basename: "iqn.2003-01.org.linux-iscsi.ubuntu-19.x8664"
tpg:
attributes:
authentication: 0
generate_node_acls: 1
cache_dynamic_acls: 1
demo_mode_write_protect: 0

View File

@ -0,0 +1,40 @@
driver: zfs-generic-smb
sshConnection:
host: ${SERVER_HOST}
port: 22
username: ${SERVER_USERNAME}
password: ${SERVER_PASSWORD}
zfs:
datasetParentName: tank/ci/${CI_BUILD_KEY}/v
detachedSnapshotsDatasetParentName: tank/ci/${CI_BUILD_KEY}/s
datasetProperties:
#aclmode: restricted
#aclinherit: passthrough
#acltype: nfsv4
casesensitivity: insensitive
datasetEnableQuotas: true
datasetEnableReservation: false
datasetPermissionsMode: "0770"
datasetPermissionsUser: smbroot
datasetPermissionsGroup: smbroot
smb:
shareHost: ${SERVER_HOST}
shareStrategy: "setDatasetProperties"
shareStrategySetDatasetProperties:
properties:
sharesmb: "on"
node:
mount:
mount_flags: "username=smbroot,password=smbroot"
_private:
csi:
volume:
idHash:
strategy: crc16

View File

@ -1327,7 +1327,10 @@ class ControllerZfsBaseDriver extends CsiBaseDriver {
await zb.zfs.destroy(datasetName, { recurse: true, force: true });
success = true;
} catch (err) {
if (err.toString().includes("dataset is busy")) {
if (
err.toString().includes("dataset is busy") ||
err.toString().includes("target is busy")
) {
current_try++;
if (current_try > max_tries) {
throw err;

View File

@ -18,6 +18,7 @@ const __REGISTRY_NS__ = "CsiBaseDriver";
const NODE_OS_DRIVER_CSI_PROXY = "csi-proxy";
const NODE_OS_DRIVER_POSIX = "posix";
const NODE_OS_DRIVER_WINDOWS = "windows";
/**
* common code shared between all drivers
@ -199,10 +200,14 @@ class CsiBaseDriver {
}
__getNodeOsDriver() {
if (this.getNodeIsWindows() || this.getCsiProxyEnabled()) {
return NODE_OS_DRIVER_CSI_PROXY;
if (this.getNodeIsWindows()) {
return NODE_OS_DRIVER_WINDOWS;
}
//if (this.getNodeIsWindows() || this.getCsiProxyEnabled()) {
// return NODE_OS_DRIVER_CSI_PROXY;
//}
return NODE_OS_DRIVER_POSIX;
}
@ -1215,6 +1220,383 @@ class CsiBaseDriver {
);
}
break;
case NODE_OS_DRIVER_WINDOWS:
// sanity check node_attach_driver
if (!["smb", "iscsi"].includes(node_attach_driver)) {
throw new GrpcError(
grpc.status.UNIMPLEMENTED,
`csi-proxy does not work with node_attach_driver: ${node_attach_driver}`
);
}
// sanity check fs_type
if (fs_type && !["ntfs", "cifs"].includes(fs_type)) {
throw new GrpcError(
grpc.status.UNIMPLEMENTED,
`csi-proxy does not work with fs_type: ${fs_type}`
);
}
const WindowsUtils = require("../utils/windows").Windows;
const wutils = new WindowsUtils();
switch (node_attach_driver) {
case "smb":
device = `//${volume_context.server}/${volume_context.share}`;
const username = driver.getMountFlagValue(mount_flags, "username");
const password = driver.getMountFlagValue(mount_flags, "password");
if (!username || !password) {
throw new Error("username and password required");
}
/**
* smb mount creates a link at this location and if the dir already exists
* it explodes
*
* if path exists but is NOT symlink delete it
*/
try {
fs.statSync(staging_target_path);
result = true;
} catch (err) {
if (err.code === "ENOENT") {
result = false;
} else {
throw err;
}
}
if (result) {
result = fs.lstatSync(staging_target_path);
if (!result.isSymbolicLink()) {
fs.rmdirSync(staging_target_path);
} else {
result = await wutils.GetItem(staging_target_path);
// UNC\172.29.0.111\tank_k8s_test_PVC_111\
let target = _.get(result, "Target.[0]", "");
let parts = target.split("\\");
if (
parts[1] != volume_context.server &&
parts[2] != volume_context.share
) {
throw new Error(
`${target} mounted already at ${staging_target_path}`
);
} else {
// finish early, assured we have what we need
return {};
}
}
}
try {
result = await wutils.GetSmbGlobalMapping(
filesystem.covertUnixSeparatorToWindowsSeparator(device)
);
if (!result) {
await wutils.NewSmbGlobalMapping(
filesystem.covertUnixSeparatorToWindowsSeparator(device),
`${volume_context.server}\\${username}`,
password
);
}
} catch (e) {
let details = _.get(e, "stderr", "");
if (!details.includes("0x80041001")) {
throw e;
}
}
try {
await wutils.NewSmbLink(
filesystem.covertUnixSeparatorToWindowsSeparator(device),
staging_target_path
);
} catch (e) {
let details = _.get(e, "stderr", "");
if (!details.includes("ResourceExists")) {
throw e;
} else {
result = fs.lstatSync(staging_target_path);
if (!result.isSymbolicLink()) {
throw new Error("staging path exists but is not symlink");
}
}
}
break;
case "iscsi":
switch (access_type) {
case "mount":
let portals = [];
if (volume_context.portal) {
portals.push(volume_context.portal.trim());
}
if (volume_context.portals) {
volume_context.portals.split(",").forEach((portal) => {
portals.push(portal.trim());
});
}
// ensure full portal value
portals = portals.map((value) => {
if (!value.includes(":")) {
value += ":3260";
}
return value.trim();
});
// ensure unique entries only
portals = [...new Set(portals)];
// stores configuration of targets/iqn/luns to connect to
let iscsiConnections = [];
for (let portal of portals) {
iscsiConnections.push({
portal,
iqn: volume_context.iqn,
lun: volume_context.lun,
});
}
let successful_logins = 0;
let multipath = iscsiConnections.length > 1;
// no multipath support yet
// https://github.com/kubernetes-csi/csi-proxy/pull/99
for (let iscsiConnection of iscsiConnections) {
// add target portal
let parts = iscsiConnection.portal.split(":");
let target_address = parts[0];
let target_port = parts[1] || "3260";
// this is idempotent
try {
await wutils.NewIscsiTargetPortal(
target_address,
target_port
);
} catch (e) {
driver.ctx.logger.warn(
`failed adding target portal: ${JSON.stringify(
iscsiConnection
)}: ${e.stderr}`
);
if (!multipath) {
throw e;
} else {
continue;
}
}
// login
try {
let auth_type = "NONE";
let chap_username = "";
let chap_secret = "";
if (
normalizedSecrets[
"node-db.node.session.auth.authmethod"
] == "CHAP"
) {
// set auth_type
if (
normalizedSecrets[
"node-db.node.session.auth.username"
] &&
normalizedSecrets[
"node-db.node.session.auth.password"
] &&
normalizedSecrets[
"node-db.node.session.auth.username_in"
] &&
normalizedSecrets[
"node-db.node.session.auth.password_in"
]
) {
auth_type = "MUTUAL_CHAP";
} else if (
normalizedSecrets[
"node-db.node.session.auth.username"
] &&
normalizedSecrets["node-db.node.session.auth.password"]
) {
auth_type = "ONE_WAY_CHAP";
}
// set credentials
if (
normalizedSecrets[
"node-db.node.session.auth.username"
] &&
normalizedSecrets["node-db.node.session.auth.password"]
) {
chap_username =
normalizedSecrets[
"node-db.node.session.auth.username"
];
chap_secret =
normalizedSecrets[
"node-db.node.session.auth.password"
];
}
}
await wutils.ConnectIscsiTarget(
target_address,
target_port,
iscsiConnection.iqn,
auth_type,
chap_username,
chap_secret,
multipath
);
} catch (e) {
let details = _.get(e, "stderr", "");
if (
!details.includes(
"The target has already been logged in via an iSCSI session"
)
) {
driver.ctx.logger.warn(
`failed connection to ${JSON.stringify(
iscsiConnection
)}: ${e.stderr}`
);
if (!multipath) {
throw e;
}
}
}
// discover?
//await csiProxyClient.executeRPC("iscsi", "DiscoverTargetPortal", {
// target_portal,
//});
successful_logins++;
}
if (iscsiConnections.length != successful_logins) {
driver.ctx.logger.warn(
`failed to login to all portals: total - ${iscsiConnections.length}, logins - ${successful_logins}`
);
}
// let things settle
// this will help in dm scenarios
await GeneralUtils.sleep(2000);
// rescan
await wutils.UpdateHostStorageCache();
// get device
let disks = await wutils.GetTargetDisksByIqnLun(
volume_context.iqn,
volume_context.lun
);
let disk;
if (disks.length == 0) {
throw new GrpcError(
grpc.status.UNAVAILABLE,
`0 disks created by ${successful_logins} successful logins`
);
}
if (disks.length > 1) {
if (multipath) {
let disk_number_set = new Set();
disks.forEach((i_disk) => {
disk_number_set.add(i_disk.DiskNumber);
});
if (disk_number_set.length > 1) {
throw new GrpcError(
grpc.status.FAILED_PRECONDITION,
"using multipath but mpio is not properly configured (multiple disk numbers with same iqn/lun)"
);
}
// find first disk that is online
disk = disks.find((i_disk) => {
return i_disk.OperationalStatus == "Online";
});
if (!disk) {
throw new GrpcError(
grpc.status.FAILED_PRECONDITION,
"using multipath but mpio is not properly configured (failed to detect an online disk)"
);
}
} else {
throw new GrpcError(
grpc.status.FAILED_PRECONDITION,
`not using multipath but discovered ${disks.length} disks (multiple disks with same iqn/lun)`
);
}
} else {
disk = disks[0];
}
if (multipath && !disk.Path.startsWith("\\\\?\\mpio#")) {
throw new GrpcError(
grpc.status.FAILED_PRECONDITION,
"using multipath but mpio is not properly configured (discover disk is not an mpio disk)"
);
}
// needs to be initialized
await wutils.PartitionDisk(disk.DiskNumber);
let partition = await wutils.GetLastPartitionByDiskNumber(
disk.DiskNumber
);
let volume = await wutils.GetVolumeByDiskNumberPartitionNumber(
disk.DiskNumber,
partition.PartitionNumber
);
if (!volume) {
throw new Error("failed to create/discover volume for disk");
}
result = await wutils.VolumeIsFormatted(volume.UniqueId);
if (!result) {
// format device
await wutils.FormatVolume(volume.UniqueId);
}
result = await wutils.GetItem(staging_target_path);
if (!result) {
fs.mkdirSync(staging_target_path, {
recursive: true,
mode: "755",
});
result = await wutils.GetItem(staging_target_path);
}
if (!volume.UniqueId.includes(result.Target[0])) {
// mount up!
await wutils.MountVolume(
volume.UniqueId,
staging_target_path
);
}
break;
case "block":
default:
throw new GrpcError(
grpc.status.UNIMPLEMENTED,
`access_type ${access_type} unsupported`
);
}
break;
default:
throw new GrpcError(
grpc.status.INVALID_ARGUMENT,
`unknown/unsupported node_attach_driver: ${node_attach_driver}`
);
}
break;
case NODE_OS_DRIVER_CSI_PROXY:
// sanity check node_attach_driver
if (!["smb", "iscsi"].includes(node_attach_driver)) {
@ -1546,9 +1928,7 @@ class CsiBaseDriver {
grpc.status.INVALID_ARGUMENT,
`unknown/unsupported node_attach_driver: ${node_attach_driver}`
);
break;
}
break;
default:
throw new GrpcError(
@ -1798,6 +2178,89 @@ class CsiBaseDriver {
result = await filesystem.rmdir(staging_target_path);
}
break;
case NODE_OS_DRIVER_WINDOWS: {
const WindowsUtils = require("../utils/windows").Windows;
const wutils = new WindowsUtils();
async function removePath(p) {
// remove staging path
try {
fs.rmdirSync(p);
} catch (e) {
if (e.code !== "ENOENT") {
throw e;
}
}
}
let node_attach_driver;
let win_volume_id;
result = await wutils.GetItem(normalized_staging_path);
if (result) {
let target = _.get(result, "Target.[0]", "");
if (target.startsWith("UNC")) {
node_attach_driver = "smb";
}
if (target.startsWith("Volume")) {
win_volume_id = `\\\\?\\${target}`;
if (await wutils.VolumeIsIscsi(win_volume_id)) {
node_attach_driver = "iscsi";
}
}
if (!node_attach_driver) {
// nothing we care about
node_attach_driver = "bypass";
}
switch (node_attach_driver) {
case "smb":
let parts = target.split("\\");
await wutils.RemoveSmbGlobalMapping(
`\\\\${parts[1]}\\${parts[2]}`
);
break;
case "iscsi":
// write volume cache
await wutils.WriteVolumeCache(win_volume_id);
// unmount volume
await wutils.UnmountVolume(
win_volume_id,
normalized_staging_path
);
// find sessions associated with volume/disks
let sessions = await wutils.GetIscsiSessionsByVolumeId(
win_volume_id
);
// logout of sessions
for (let session of sessions) {
await wutils.DisconnectIscsiTargetByNodeAddress(
session.TargetNodeAddress
);
}
// delete target/target portal/etc
// do NOT do this now as removing the portal will remove all targets associated with it
break;
case "bypass":
break;
default:
throw new GrpcError(
grpc.status.INVALID_ARGUMENT,
`unknown/unsupported node_attach_driver: ${node_attach_driver}`
);
}
}
// remove staging path
await removePath(normalized_staging_path);
break;
}
case NODE_OS_DRIVER_CSI_PROXY:
// load up the client instance
const csiProxyClient = driver.getDefaultCsiProxyClientInstance();
@ -1891,6 +2354,9 @@ class CsiBaseDriver {
}
}
// do NOT remove target portal etc, windows handles this quite differently than
// linux and removing the portal would remove all the targets/etc
/*
try {
await csiProxyClient.executeRPC("iscsi", "RemoveTargetPortal", {
target_portal,
@ -1901,6 +2367,7 @@ class CsiBaseDriver {
throw e;
}
}
*/
break;
default:
@ -2077,6 +2544,79 @@ class CsiBaseDriver {
);
}
break;
case NODE_OS_DRIVER_WINDOWS:
const WindowsUtils = require("../utils/windows").Windows;
const wutils = new WindowsUtils();
switch (node_attach_driver) {
//case "nfs":
case "smb":
//case "lustre":
//case "oneclient":
//case "hostpath":
case "iscsi":
//case "zfs-local":
// ensure appropriate directories/files
switch (access_type) {
case "mount":
break;
case "block":
default:
throw new GrpcError(
grpc.status.INVALID_ARGUMENT,
`unsupported/unknown access_type ${access_type}`
);
}
// ensure bind mount
if (staging_target_path) {
let normalized_staging_path;
if (access_type == "block") {
normalized_staging_path = staging_target_path + "/block_device";
} else {
normalized_staging_path = staging_target_path;
}
// source path
result = await wutils.GetItem(normalized_staging_path);
if (!result) {
throw new GrpcError(
grpc.status.FAILED_PRECONDITION,
`staging path is not mounted: ${normalized_staging_path}`
);
}
// target path
result = await wutils.GetItem(target_path);
// already published
if (result) {
if (_.get(result, "LinkType") != "SymbolicLink") {
throw new GrpcError(
grpc.status.FAILED_PRECONDITION,
`target path exists but is not a symlink as it should be: ${target_path}`
);
}
return {};
}
// create symlink
fs.symlinkSync(normalized_staging_path, target_path);
return {};
}
// unsupported filesystem
throw new GrpcError(
grpc.status.FAILED_PRECONDITION,
`only staged configurations are valid`
);
default:
throw new GrpcError(
grpc.status.INVALID_ARGUMENT,
`unknown/unsupported node_attach_driver: ${node_attach_driver}`
);
}
break;
case NODE_OS_DRIVER_CSI_PROXY:
switch (node_attach_driver) {
//case "nfs":
@ -2242,6 +2782,24 @@ class CsiBaseDriver {
}
}
break;
case NODE_OS_DRIVER_WINDOWS:
const WindowsUtils = require("../utils/windows").Windows;
const wutils = new WindowsUtils();
result = await wutils.GetItem(target_path);
if (!result) {
return {};
}
if (_.get(result, "LinkType") != "SymbolicLink") {
throw new GrpcError(
grpc.status.FAILED_PRECONDITION,
`target path is not a symlink ${target_path}`
);
}
fs.rmdirSync(target_path);
break;
case NODE_OS_DRIVER_CSI_PROXY:
const csiProxyClient = driver.getDefaultCsiProxyClientInstance();
@ -2368,6 +2926,61 @@ class CsiBaseDriver {
}
break;
case NODE_OS_DRIVER_WINDOWS: {
const WindowsUtils = require("../utils/windows").Windows;
const wutils = new WindowsUtils();
// ensure path is mounted
result = await wutils.GetItem(volume_path);
if (!result) {
throw new GrpcError(
grpc.status.NOT_FOUND,
`volume_path ${volume_path} is not currently mounted`
);
}
let node_attach_driver;
let target = await wutils.GetRealTarget(volume_path);
if (target.startsWith("\\\\")) {
node_attach_driver = "smb";
}
if (target.startsWith("\\\\?\\Volume")) {
if (await wutils.VolumeIsIscsi(target)) {
node_attach_driver = "iscsi";
}
}
if (!node_attach_driver) {
// nothing we care about
node_attach_driver = "bypass";
}
switch (node_attach_driver) {
case "smb":
res.usage = [{ total: 0, unit: "BYTES" }];
break;
case "iscsi":
let node_volume = await wutils.GetVolumeByVolumeId(target);
res.usage = [
{
available: node_volume.SizeRemaining,
total: node_volume.Size,
used: node_volume.Size - node_volume.SizeRemaining,
unit: "BYTES",
},
];
break;
case "bypass":
break;
default:
throw new GrpcError(
grpc.status.INVALID_ARGUMENT,
`unknown/unsupported node_attach_driver: ${node_attach_driver}`
);
}
break;
}
case NODE_OS_DRIVER_CSI_PROXY:
const csiProxyClient = driver.getDefaultCsiProxyClientInstance();
const volume_context = await driver.getDerivedVolumeContext(call);
@ -2562,6 +3175,56 @@ class CsiBaseDriver {
}
break;
case NODE_OS_DRIVER_WINDOWS: {
const WindowsUtils = require("../utils/windows").Windows;
const wutils = new WindowsUtils();
let node_attach_driver;
// ensure path is mounted
result = await wutils.GetItem(volume_path);
if (!result) {
throw new GrpcError(
grpc.status.NOT_FOUND,
`volume_path ${volume_path} is not currently mounted`
);
}
let target = await wutils.GetRealTarget(volume_path);
if (target.startsWith("\\\\")) {
node_attach_driver = "smb";
}
if (target.startsWith("\\\\?\\Volume")) {
if (await wutils.VolumeIsIscsi(target)) {
node_attach_driver = "iscsi";
}
}
if (!node_attach_driver) {
// nothing we care about
node_attach_driver = "bypass";
}
switch (node_attach_driver) {
case "smb":
// noop
break;
case "iscsi":
// rescan devices
await wutils.UpdateHostStorageCache();
await wutils.ResizeVolume(target);
break;
case "bypass":
break;
default:
throw new GrpcError(
grpc.status.INVALID_ARGUMENT,
`unknown/unsupported node_attach_driver: ${node_attach_driver}`
);
}
break;
}
case NODE_OS_DRIVER_CSI_PROXY:
const csiProxyClient = driver.getDefaultCsiProxyClientInstance();
const volume_context = await driver.getDerivedVolumeContext(call);
@ -2606,7 +3269,7 @@ class CsiBaseDriver {
try {
await csiProxyClient.executeRPC("volume", "ResizeVolume", {
volume_id: node_volume_id,
resize_bytes: required_bytes,
resize_bytes: 0,
});
} catch (e) {
let details = _.get(e, "details", "");

View File

@ -1,8 +1,10 @@
const _ = require("lodash");
const grpc = require("./grpc").grpc;
const path = require("path");
const protoLoader = require("@grpc/proto-loader");
const PROTO_BASE_PATH = __dirname + "/../../csi_proxy_proto";
const PROTO_BASE_PATH =
path.dirname(path.dirname(__dirname)) + path.sep + "csi_proxy_proto";
/**
* leave connection null as by default the named pipe is derrived
@ -38,10 +40,20 @@ class CsiProxyClient {
const serviceVersion =
service.version || DEFAULT_SERVICES[serviceName].version;
const serviceConnection =
// HANGS
// Http2Session client (38) nghttp2 has 13 bytes to send directly
// Http2Session client (38) wants read? 1
// Then pipe closes after 60 seconds-ish
service.connection ||
`\\\\.\\\\pipe\\\\${pipePrefix}-${serviceName}-${serviceVersion}`;
`unix:////./pipe/${pipePrefix}-${serviceName}-${serviceVersion}`;
// EACCESS
//service.connection ||
//`unix:///csi/${pipePrefix}-${serviceName}-${serviceVersion}`;
//service.connection ||
//`unix:///csi/csi.sock.internal`;
const PROTO_PATH = `${PROTO_BASE_PATH}\\${serviceName}\\${serviceVersion}\\api.proto`;
const PROTO_PATH = `/${PROTO_BASE_PATH}/${serviceName}/${serviceVersion}/api.proto`;
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
keepCase: true,
longs: String,

85
src/utils/powershell.js Normal file
View File

@ -0,0 +1,85 @@
const cp = require("child_process");
class Powershell {
async exec(command, options = {}) {
if (!options.hasOwnProperty("timeout")) {
// TODO: cannot use this as fsck etc are too risky to kill
//options.timeout = DEFAULT_TIMEOUT;
}
//cmd := exec.Command("powershell", "-Mta", "-NoProfile", "-Command", command)
let stdin;
if (options.stdin) {
stdin = options.stdin;
delete options.stdin;
}
// https://github.com/kubernetes-csi/csi-proxy/blob/master/pkg/utils/utils.go
const _command = "powershell";
const args = [
"-Mta",
"-NoProfile",
"-Command",
command
];
let command_log = `${_command} ${args.join(" ")}`.trim();
if (stdin) {
command_log = `echo '${stdin}' | ${command_log}`
.trim()
.replace(/\n/, "\\n");
}
console.log("executing powershell command: %s", command_log);
return new Promise((resolve, reject) => {
const child = cp.spawn(_command, args, options);
let stdout = "";
let stderr = "";
child.on("spawn", function () {
if (stdin) {
child.stdin.setEncoding("utf-8");
child.stdin.write(stdin);
child.stdin.end();
}
});
child.stdout.on("data", function (data) {
stdout = stdout + data;
});
child.stderr.on("data", function (data) {
stderr = stderr + data;
});
child.on("close", function (code) {
const result = { code, stdout, stderr, timeout: false };
// timeout scenario
if (code === null) {
result.timeout = true;
reject(result);
}
if (code) {
console.log(
"failed to execute powershell command: %s, response: %j",
command_log,
result
);
reject(result);
} else {
try {
result.parsed = JSON.parse(result.stdout);
} catch (err) { };
resolve(result);
}
});
});
}
}
module.exports.Powershell = Powershell;

729
src/utils/windows.js Normal file
View File

@ -0,0 +1,729 @@
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;