diff --git a/.gitignore b/.gitignore index 26ff709..2855d28 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ +**~ node_modules dev +/ci/bin/*dev* diff --git a/bin/democratic-csi b/bin/democratic-csi index 6a0bec9..f616a8b 100755 --- a/bin/democratic-csi +++ b/bin/democratic-csi @@ -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)) { diff --git a/ci/bin/build.ps1 b/ci/bin/build.ps1 new file mode 100644 index 0000000..e883378 --- /dev/null +++ b/ci/bin/build.ps1 @@ -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 diff --git a/ci/bin/helper.ps1 b/ci/bin/helper.ps1 new file mode 100644 index 0000000..6e8a357 --- /dev/null +++ b/ci/bin/helper.ps1 @@ -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 +} \ No newline at end of file diff --git a/ci/bin/launch-csi-grpc-proxy.ps1 b/ci/bin/launch-csi-grpc-proxy.ps1 new file mode 100644 index 0000000..8dbf6b7 --- /dev/null +++ b/ci/bin/launch-csi-grpc-proxy.ps1 @@ -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 | % { "$_" } diff --git a/ci/bin/launch-csi-sanity.ps1 b/ci/bin/launch-csi-sanity.ps1 new file mode 100644 index 0000000..d36ab5e --- /dev/null +++ b/ci/bin/launch-csi-sanity.ps1 @@ -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) diff --git a/ci/bin/launch-server.ps1 b/ci/bin/launch-server.ps1 new file mode 100644 index 0000000..3cf17a7 --- /dev/null +++ b/ci/bin/launch-server.ps1 @@ -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 | % { "$_" } diff --git a/ci/bin/run.ps1 b/ci/bin/run.ps1 new file mode 100644 index 0000000..24a244c --- /dev/null +++ b/ci/bin/run.ps1 @@ -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 diff --git a/ci/configs/windows/iscsi.yaml b/ci/configs/windows/iscsi.yaml new file mode 100644 index 0000000..38dc594 --- /dev/null +++ b/ci/configs/windows/iscsi.yaml @@ -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 diff --git a/ci/configs/windows/smb.yaml b/ci/configs/windows/smb.yaml new file mode 100644 index 0000000..272a590 --- /dev/null +++ b/ci/configs/windows/smb.yaml @@ -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 diff --git a/src/driver/controller-zfs/index.js b/src/driver/controller-zfs/index.js index 94f7ea7..d88d150 100644 --- a/src/driver/controller-zfs/index.js +++ b/src/driver/controller-zfs/index.js @@ -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; diff --git a/src/driver/index.js b/src/driver/index.js index 8a0bd20..bccbd78 100644 --- a/src/driver/index.js +++ b/src/driver/index.js @@ -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", ""); diff --git a/src/utils/csi_proxy_client.js b/src/utils/csi_proxy_client.js index fb656a1..d0587c0 100644 --- a/src/utils/csi_proxy_client.js +++ b/src/utils/csi_proxy_client.js @@ -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, diff --git a/src/utils/powershell.js b/src/utils/powershell.js new file mode 100644 index 0000000..08d2a56 --- /dev/null +++ b/src/utils/powershell.js @@ -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; \ No newline at end of file diff --git a/src/utils/windows.js b/src/utils/windows.js new file mode 100644 index 0000000..6f2d853 --- /dev/null +++ b/src/utils/windows.js @@ -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;