Compare commits

...
3 Commits
Author SHA1 Message Date
dyoder 819b8d72f0 added Upload-File.ps1 2026-10-01 11:16:36 -04:00
dyoder a53dcfb6a9 added boolean conversion helper and replaced code in scripts to use it 2026-10-01 11:16:23 -04:00
dyoder 2efb7116d8 removed, not necessary 2026-10-01 11:14:25 -04:00
7 changed files with 188 additions and 18 deletions
+4 -12
View File
@@ -21,18 +21,10 @@ If ( [string]::IsNullOrWhiteSpace([string]$DSAManifestSource) ) {
}
# Normalize RMM text values without treating every nonempty string as true.
$DsaOptions = @{ Zip = $Zip; Upload = $Upload; DeleteAfterUpload = $DeleteAfterUpload }
ForEach ( $DsaOptionName in @('Zip', 'Upload', 'DeleteAfterUpload') ) {
$DsaOptionValue = ([string]$DsaOptions[$DsaOptionName]).Trim().ToLowerInvariant()
If ( $DsaOptionValue -in @('true', '$true', 'yes', 'y', '1') ) {
$DsaOptions[$DsaOptionName] = $true
}
ElseIf ( $DsaOptionValue -in @('', 'false', '$false', 'no', 'n', '0') ) {
$DsaOptions[$DsaOptionName] = $false
}
Else {
Throw ('Invalid ${0} value. Use true, $true, yes, y, 1, false, $false, no, n, or 0.' -f $DsaOptionName)
}
$DsaOptions = @{
Zip = ConvertTo-RmmBoolean -Value $Zip -VariableName 'Zip'
Upload = ConvertTo-RmmBoolean -Value $Upload -VariableName 'Upload'
DeleteAfterUpload = ConvertTo-RmmBoolean -Value $DeleteAfterUpload -VariableName 'DeleteAfterUpload'
}
$DsaZipEnabled = $DsaOptions.Zip
+20
View File
@@ -20,6 +20,25 @@ For download failures, use the standalone [Test-FileDownload.ps1](Test-FileDownl
`Get-DSAManifest.ps1` uses `Download-FileDirectly`, which passes the original URL to BITS without preliminary `Get-AbsoluteURI` or `IsURLValid` requests. It accepts the same URL and optional `-File` arguments as `Download-File`, waits for completion, and throws on BITS errors. If `-File` is omitted, its temporary filename comes from the original URL; supply `-File` for extensionless URLs or URLs with query strings. Existing `Download-File` callers retain their previous behavior. Publish `Tools.ps1` together with `Get-DSAManifest.ps1` so the new helper is available.
## Converting RMM boolean variables
After dot-sourcing `Tools.ps1`, use `ConvertTo-RmmBoolean` with any RMM variable:
```powershell
$RestartEnabled = ConvertTo-RmmBoolean -Value $Restart -VariableName 'Restart'
If ( $RestartEnabled ) {
# Perform the requested restart.
}
# The variable name is optional; positional and pipeline input also work.
$ZipEnabled = ConvertTo-RmmBoolean $Zip
$UploadEnabled = $Upload | ConvertTo-RmmBoolean
```
Each input returns a `System.Boolean`: `true`, `$true`, `yes`, `y`, and `1` become `$true`; `false`, `$false`, `no`, `n`, `0`, blank strings, and `$null` become `$false`. Case and surrounding whitespace are ignored, and native boolean values also work. Other values throw an error; `-VariableName` identifies the input in that error. The function returns the converted value without changing the original variable; assign the result wherever needed.
`Get-DSAManifest.ps1` uses this helper for its three options. Publish it together with the updated `Tools.ps1` so the helper is available to the RMM caller.
## Uploading files
After dot-sourcing `Tools.ps1`, scripts can upload a completed file to the Emberkom FileDrop:
@@ -44,4 +63,5 @@ Offline tests (no uploads or notifications):
powershell.exe -NoProfile -File .\tests\Upload-File.Tests.ps1
powershell.exe -NoProfile -File .\tests\Get-DSAManifest.Tests.ps1
powershell.exe -NoProfile -File .\tests\Download-File.Tests.ps1
powershell.exe -NoProfile -File .\tests\ConvertTo-RmmBoolean.Tests.ps1
```
+39
View File
@@ -180,6 +180,45 @@ Function Get-Version {
Return [version]$Return.Trim('.')
}
# Convert an RMM text variable into an actual Boolean value.
Function ConvertTo-RmmBoolean {
<#
.SYNOPSIS
Converts an RMM text value to a System.Boolean.
.DESCRIPTION
Accepts true, $true, yes, y, or 1; false, $false, no, n, or 0.
Matching ignores case and surrounding whitespace. Null and empty values
return false. Unrecognized values throw instead of silently enabling an option.
.PARAMETER Value
The value to convert. Native Boolean values are also accepted.
.PARAMETER VariableName
Optional RMM variable name, without the leading $, for error messages.
.EXAMPLE
$ZipEnabled = ConvertTo-RmmBoolean -Value $Zip -VariableName 'Zip'
.EXAMPLE
ConvertTo-RmmBoolean ' No '
#>
[CmdletBinding()]
[OutputType([bool])]
param(
[Parameter(Mandatory=$true,Position=0,ValueFromPipeline=$true)]
[AllowNull()]
[AllowEmptyString()]
[string]$Value,
[string]$VariableName = 'Value'
)
process {
$NormalizedValue = ([string]$Value).Trim().ToLowerInvariant()
If ( $NormalizedValue -in @('true', '$true', 'yes', 'y', '1') ) {
Return $true
}
If ( $NormalizedValue -in @('', 'false', '$false', 'no', 'n', '0') ) {
Return $false
}
Throw ('Invalid ${0} value. Use true, $true, yes, y, 1, false, $false, no, n, or 0.' -f $VariableName)
}
}
# Determines whether a URL is valid
Function IsURLValid {
param([Parameter(Mandatory=$true,ValueFromPipeline=$true)][string]$URL)
-6
View File
@@ -1,6 +0,0 @@
. $([Scriptblock]::Create((New-Object Net.WebClient).DownloadString('https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1')))
Run-Script -LivePSScript Upload-Data
$AteraAPIKey = '13ae6a43d13441b4aaf2f59e481fa11c'
$DataToUpload = '{[PathToUpload]}'
+45
View File
@@ -0,0 +1,45 @@
#Requires -Version 5.0
# The following variables need to be set in the RMM platform
# [string]$PathToFile
# [string]$DeleteAfterUpload
# [string]$MessageSubject
# [string]$MessageBody
# This must run before downloading Tools.ps1, including on Server 2016.
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$RmmToolsUrl = 'https://dev.emberkom.com/emberkom/management-scripts/raw/branch/master/Tools.ps1'
$RmmToolsClient = New-Object Net.WebClient
Try {
$RmmToolsText = $RmmToolsClient.DownloadString($RmmToolsUrl)
$RmmToolsScript = [Scriptblock]::Create($RmmToolsText)
}
Catch {
Throw "Unable to download or parse Tools.ps1 from ${RmmToolsUrl}: $($_.Exception.Message)"
}
Finally {
$RmmToolsClient.Dispose()
}
# Dot-source in this scope so the RMM input variables remain accessible.
. $RmmToolsScript
If ( !(Test-Path -LiteralPath $PathToFile -PathType Leaf) ) { Write-Error "The specified path does not exist: ${PathToFile}" -ErrorAction Stop }
$Delete = ConvertTo-RmmBoolean -Value $DeleteAfterUpload
Write-Output "Uploading file: ${PathToFile}"
$UploadReceipt = Upload-File -Path $PathToFile `
-Subject "${MessageSubject} [${Env:COMPUTERNAME}]" `
-Message "The file ""$(Split-Path -Path $PathToFile -Leaf)"" has been uploaded on $((Get-Date).ToString('yyyy-MM-dd HH:mm:ss zzz'))`n`n${MessageBody}" `
-ErrorAction Stop
If ( [string]::IsNullOrWhiteSpace([string]$UploadReceipt.AttachmentId) -or
[string]::IsNullOrWhiteSpace([string]$UploadReceipt.Status) ) {
Throw 'The upload did not return a confirmation receipt. The local manifest has been retained.'
}
Write-Output "File uploaded: ${PathToFile}"
If ( $Delete ) {
Remove-Item -LiteralPath $PathToFile -Force -ErrorAction Stop
Write-Output "Deleted uploaded file: ${PathToFile}"
}
$UploadReceipt
+69
View File
@@ -0,0 +1,69 @@
#Requires -Version 5.0
# Offline tests: load only the helper to avoid Tools.ps1 setup and logging.
$ErrorActionPreference = 'Stop'
$BooleanToolsPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'Tools.ps1'
$BooleanTokens = $null
$BooleanParseErrors = $null
$BooleanAst = [Management.Automation.Language.Parser]::ParseFile($BooleanToolsPath, [ref]$BooleanTokens, [ref]$BooleanParseErrors)
If ($BooleanParseErrors.Count) { Throw ($BooleanParseErrors | Out-String) }
$BooleanDefinition = $BooleanAst.Find({
param($Node)
$Node -is [Management.Automation.Language.FunctionDefinitionAst] -and $Node.Name -eq 'ConvertTo-RmmBoolean'
}, $true)
If (!$BooleanDefinition) { Throw 'Missing helper: ConvertTo-RmmBoolean' }
. ([scriptblock]::Create($BooleanDefinition.Extent.Text))
# Tools.ps1 overrides Write-Output with a string parameter. Boolean results must
# bypass that wrapper so $false remains a Boolean, without log text or side effects.
Function Write-Output { Throw 'Conversion must not use the string-based logging wrapper.' }
$BooleanCases = @(
@{Values=@('true', '$true', 'yes', 'y', '1', $true, 1); Expected=$true}
@{Values=@('false', '$false', 'no', 'n', '0', '', $null, $false, 0); Expected=$false}
)
Foreach ($Case in $BooleanCases) {
Foreach ($Value in $Case.Values) {
$Variants = @($Value)
If ($Value -is [string]) { $Variants += " `t$($Value.ToUpperInvariant())`r`n " }
Foreach ($Variant in $Variants) {
$Result = @(ConvertTo-RmmBoolean -Value $Variant)
If ($Result.Count -ne 1 -or $Result[0] -isnot [bool] -or $Result[0] -ne $Case.Expected) {
Throw "Input '$Variant' must return exactly one Boolean with value $($Case.Expected)."
}
}
}
}
Write-Host 'PASS: accepted text, case, whitespace, null, native booleans, and numeric 0/1 return one Boolean.'
$RmmInput = ' no '
$Result = ConvertTo-RmmBoolean $RmmInput
If ($Result -isnot [bool] -or $Result -ne $false -or $RmmInput -cne ' no ') {
Throw 'Positional conversion must return false without changing the original input.'
}
$Result = @(' yes ', 'false', '', $null, '$TRUE', '0' | ConvertTo-RmmBoolean)
$Expected = @($true, $false, $false, $false, $true, $false)
If ($Result.Count -ne $Expected.Count) { Throw 'Pipeline conversion must return one value for every input.' }
For ($Index = 0; $Index -lt $Expected.Count; $Index++) {
If ($Result[$Index] -isnot [bool] -or $Result[$Index] -ne $Expected[$Index]) {
Throw "Pipeline conversion returned the wrong type or value at index $Index."
}
}
Write-Host 'PASS: positional input, preserved caller value, and multiple pipeline inputs.'
# RMM scripts may use Continue and omit -ErrorAction. Invalid input must still stop.
$ErrorActionPreference = 'Continue'
Foreach ($Invalid in @('maybe', 'on', 'off', '2', 'true false', '$null')) {
Foreach ($Parameters in @(@{}, @{VariableName='Restart'})) {
$Caught = $null
$Returned = @()
Try { ConvertTo-RmmBoolean -Value $Invalid @Parameters | ForEach-Object { $Returned += $_ } }
Catch { $Caught = $_ }
$Name = If ($Parameters.ContainsKey('VariableName')) { 'Restart' } Else { 'Value' }
If ($null -eq $Caught -or !$Caught.Exception.Message.StartsWith(('Invalid ${0} value.' -f $Name))) {
Throw "Invalid input '$Invalid' must throw an error identifying $Name."
}
If ($Returned.Count -ne 0) { Throw 'Invalid input must not return a value.' }
}
}
Write-Host 'PASS: invalid input throws with default or caller-supplied variable name.'
Write-Host "All ConvertTo-RmmBoolean tests passed on PowerShell $($PSVersionTable.PSVersion), $([IntPtr]::Size * 8)-bit."
+11
View File
@@ -8,6 +8,17 @@ $DsaErrors = $null
$null = [Management.Automation.Language.Parser]::ParseFile($DsaScriptUnderTest, [ref]$DsaTokens, [ref]$DsaErrors)
If ($DsaErrors.Count) { Throw ($DsaErrors | Out-String) }
# Load only the real conversion helper, without running Tools.ps1 setup.
$DsaToolsPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'Tools.ps1'
$DsaToolsAst = [Management.Automation.Language.Parser]::ParseFile($DsaToolsPath, [ref]$DsaTokens, [ref]$DsaErrors)
If ($DsaErrors.Count) { Throw ($DsaErrors | Out-String) }
$DsaBooleanDefinition = $DsaToolsAst.Find({
param($Node)
$Node -is [Management.Automation.Language.FunctionDefinitionAst] -and $Node.Name -eq 'ConvertTo-RmmBoolean'
}, $true)
If (!$DsaBooleanDefinition) { Throw 'Missing helper: ConvertTo-RmmBoolean' }
. ([scriptblock]::Create($DsaBooleanDefinition.Extent.Text))
Function Assert-Dsa { param([bool]$Condition, [string]$Description) If (!$Condition) { Throw $Description } }
Function Download-File { Throw 'DSA must use Download-FileDirectly.' }
Function Download-FileDirectly {