Infrastructure.HyperV.psm1

<#
.SYNOPSIS
    Hyper-V VM utilities for infrastructure repos.
 
.DESCRIPTION
    Provides VM-facing functions extracted from Infrastructure.Common to
    keep each module cohesive and single-purpose. All functions in this
    module assume a Hyper-V VM on an internal switch that the host can
    reach over SSH or HTTP.
 
    Current functions:
      - Add-VmFileServerFile : stages a host file and returns its VM URL
      - Assert-VmFilesField : shared schema validator for a 'files' array
                                  on a VM definition; consumers extend via
                                  -AllowedSubFields and -PostEntryValidator
      - Assert-VmEnvVarsField : shared schema validator for an 'envVars'
                                  array on a VM definition; fixed rule set
                                  (POSIX-identifier name, non-empty value
                                  with no LF/CR/NUL, no duplicate names)
      - Copy-VmFiles : per-entry transport (Add-VmFileServerFile +
                                  curl -o + chown + chmod under sudo); each
                                  entry is { Source, Target, Owner?, Mode? }
      - Copy-VmFilesByPattern : wildcard front-end to Copy-VmFiles; expands
                                  a host-side pattern, validates host-side,
                                  then forwards to Copy-VmFiles
      - Expand-VmTarball : stages a host-side .tar.gz via the file
                                  server and extracts it into Destination
                                  on the VM under sudo via an atomic
                                  mktemp + curl|tar + mv (skip-unchanged
                                  marker lands in a follow-up step)
      - Invoke-SshClientCommand : runs a shell command via SSH.NET SshClient
      - Invoke-WithVmFileServer : runs a script block with a live HTTP file
                                  server bound to the Hyper-V internal switch
      - New-VmSshClient : creates and connects a SSH.NET SshClient
      - New-VmSymlink : idempotent symlink creation under sudo;
                                  fails if Path exists as anything other
                                  than a matching symlink (data-loss
                                  guard) - first of the "VM install
                                  primitives" family
      - Remove-VmDirectory : idempotent removal of a directory tree
                                  on a VM under sudo, gated by a
                                  hard-coded allowlist of safe parent
                                  prefixes. Refuses to delete a
                                  non-directory at the target path
                                  (data-loss guard)
      - Remove-VmProfileDScript : idempotent removal of a
                                  /etc/profile.d/<Name>.sh script
                                  under sudo. No-op when the target
                                  is absent; mirrors
                                  Set-VmProfileDScript on the
                                  uninstall side
      - Remove-VmSymlink : idempotent symlink removal under sudo;
                                  no-op when Path is absent, refuses to
                                  delete anything that is not a symlink
                                  (data-loss guard)
      - Set-VmEnvironmentVariables : writes a sentinel-delimited managed
                                  block of NAME="VALUE" lines to
                                  /etc/environment on the VM. Reconciles
                                  against the existing block and skips
                                  when unchanged (default);
                                  -NoSkipUnchanged forces a write. Empty
                                  entries array removes the managed block
      - Set-VmProfileDScript : writes a /etc/profile.d/<Name>.sh
                                  shell snippet on the VM under sudo
                                  via an atomic temp-file + mv. Byte-
                                  compares against the existing file
                                  and skips when unchanged (default);
                                  -NoSkipUnchanged forces a write
      - Start-VmIfStopped : idempotent Hyper-V power-on. Starts Off /
                                  resumes Saved / no-ops on Running; throws
                                  on transient or unrecognised states. Pair
                                  with Wait-VmSshReady for "up and reachable"
      - Stop-VmProcessesUsingPath : sends SIGTERM (this step) to every
                                  VM process holding a given path open,
                                  waits a caller-specified grace
                                  period, and reports survivors. The
                                  SIGKILL fallback lands in a follow-up
                                  step; KilledPids in the result is
                                  always empty until then
      - Test-VmSshPort : single-shot TCP probe of an SSH port; the
                                  ICMP-ping replacement for callers that
                                  intend to SSH immediately afterwards
      - Wait-VmSshReady : polls Test-VmSshPort until the port comes
                                  up or a deadline expires; used to gate
                                  post-boot/reboot SSH work
 
    Private helpers (Assert-HyperVModuleLoaded, Assert-PsModuleLoaded,
    Assert-SshNetLoaded, Get-VmSwitchHostIp, Start-VmFileServer,
    Stop-VmFileServer) are dot-sourced below but not exported.
 
    Functions are grouped by concern under Public\ and Private\ into
    subfolders that share a name across the two trees:
      - EnvVars\ : VM-side system environment variable management.
      - FileServer\ : host-side HTTP file server used to stage VM downloads,
                        plus VM-side tarball extraction primitive.
      - Filesystem\ : VM-side directory-tree removal primitives gated by
                        a hard-coded allowlist of safe parent prefixes.
      - FileTransfer\ : VM-side transport on top of Ssh + FileServer.
      - PsModules\ : guards that ensure a PowerShell module prerequisite
                        is installed and in scope before the caller runs.
      - Power\ : Hyper-V power-state management (start / resume).
      - Processes\ : VM-side process termination primitives keyed by
                        filesystem path holders.
      - ProfileD\ : VM-side /etc/profile.d/*.sh install primitives.
      - Ssh\ : SSH client + port-probe primitives.
      - Symlinks\ : VM-side symbolic-link install / uninstall primitives.
    Each function still lives in its own file so diffs stay focused on a
    single function per commit.
#>


Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

# Private functions:

. "$PSScriptRoot\Private\Bash\New-AtomicWriteBashFragment.ps1"

. "$PSScriptRoot\Private\FileServer\Get-VmSwitchHostIp.ps1"
. "$PSScriptRoot\Private\FileServer\Start-VmFileServer.ps1"
. "$PSScriptRoot\Private\FileServer\Stop-VmFileServer.ps1"

. "$PSScriptRoot\Private\FileTransfer\Assert-VmFileBulkEntry.ps1"
. "$PSScriptRoot\Private\FileTransfer\Assert-VmFileSingleEntry.ps1"
. "$PSScriptRoot\Private\FileTransfer\Resolve-VmFileEntries.ps1"

. "$PSScriptRoot\Private\Power\Assert-HyperVModuleLoaded.ps1"

. "$PSScriptRoot\Private\ProfileD\Assert-VmProfileDScriptName.ps1"

. "$PSScriptRoot\Private\PsModules\Assert-PsModuleLoaded.ps1"

. "$PSScriptRoot\Private\Ssh\Assert-SshNetLoaded.ps1"

# Public functions:

. "$PSScriptRoot\Public\EnvVars\Assert-VmEnvVarsField.ps1"
. "$PSScriptRoot\Public\EnvVars\Set-VmEnvironmentVariables.ps1"

. "$PSScriptRoot\Public\FileServer\Add-VmFileServerFile.ps1"
. "$PSScriptRoot\Public\FileServer\Expand-VmTarball.ps1"
. "$PSScriptRoot\Public\FileServer\Invoke-WithVmFileServer.ps1"

. "$PSScriptRoot\Public\Filesystem\Remove-VmDirectory.ps1"

. "$PSScriptRoot\Public\FileTransfer\Assert-VmFilesField.ps1"
. "$PSScriptRoot\Public\FileTransfer\Copy-VmFiles.ps1"
. "$PSScriptRoot\Public\FileTransfer\Copy-VmFilesByPattern.ps1"

. "$PSScriptRoot\Public\Power\Start-VmIfStopped.ps1"

. "$PSScriptRoot\Public\Processes\Stop-VmProcessesUsingPath.ps1"

. "$PSScriptRoot\Public\Ssh\Invoke-SshClientCommand.ps1"
. "$PSScriptRoot\Public\Ssh\New-VmSshClient.ps1"
. "$PSScriptRoot\Public\Ssh\Test-VmSshPort.ps1"
. "$PSScriptRoot\Public\Ssh\Wait-VmSshReady.ps1"

. "$PSScriptRoot\Public\ProfileD\Remove-VmProfileDScript.ps1"
. "$PSScriptRoot\Public\ProfileD\Set-VmProfileDScript.ps1"

. "$PSScriptRoot\Public\Symlinks\New-VmSymlink.ps1"
. "$PSScriptRoot\Public\Symlinks\Remove-VmSymlink.ps1"

# Export-ModuleMember controls what is actually callable after Import-Module.
# It takes precedence over FunctionsToExport in the psd1 at runtime, so both
# must be kept in sync. FunctionsToExport serves a separate purpose: it is
# read by Get-Module -ListAvailable, Find-Module, and PSGallery for fast
# discovery without loading the module. The shared Module.Tests.ps1 in the
# run-unit-tests action enforces that every Public\*.ps1 file appears in both.
Export-ModuleMember -Function @(
    'Add-VmFileServerFile',
    'Assert-VmEnvVarsField',
    'Assert-VmFilesField',
    'Copy-VmFiles',
    'Copy-VmFilesByPattern',
    'Expand-VmTarball',
    'Invoke-SshClientCommand',
    'Invoke-WithVmFileServer',
    'New-VmSshClient',
    'New-VmSymlink',
    'Remove-VmDirectory',
    'Remove-VmProfileDScript',
    'Remove-VmSymlink',
    'Set-VmEnvironmentVariables',
    'Set-VmProfileDScript',
    'Start-VmIfStopped',
    'Stop-VmProcessesUsingPath',
    'Test-VmSshPort',
    'Wait-VmSshReady'
)