ZertoAVSModule.psm1
using module Microsoft.AVS.Management $PUBLIC_KEY = ('{0}/ZertoPublicKey.pem' -f $psScriptRoot) $ZERTO_FOLDER_ON_HOST = "/var/zerto" $LOCAL_TEMP_FOLDER_FOR_ZERTO_DRIVER_LOGS = ('{0}/zertoDriverLogs/' -f $psScriptRoot) $LOCAL_TEMP_FOLDER_FOR_DOWNLOADED_FILES = ('{0}/filesFromDatastore/' -f $psScriptRoot) #region Zerto Validations Function IsZertoUserExists { <# .DESCRIPTION Get a zertoUsername and a domain, and return whether or not the user exists in the domain. .PARAMETER zertoUsername Zerto user name (defaule value is ZertoDR) .PARAMETER domain Domain name (defaule value is vsphere.local) .EXAMPLE IsZertoUserExists -zertoUsername <Username> -domain <domain> #> [CmdletBinding()] [AVSAttribute(30, UpdatesSDDC = $false)] param( [parameter(Mandatory=$false, HelpMessage = "Zerto user name for ZVM installation")] [string]$zertoUsername = "ZertoDR", [parameter(Mandatory=$false, HelpMessage = "Domain to search the user at")] [string]$domain = "vsphere.local" ) Process { if(Get-SsoPersonUser -Name $zertoUsername -Domain $domain -ErrorAction SilentlyContinue) { Write-Host "$zertoUsername already exists in $VC_ADDRESS, domain: $domain." return $true; } Write-Host "$zertoUsername doesn't exist in $VC_ADDRESS, domain: $domain." return $false; } } Function IsZertoRoleExists { <# .DESCRIPTION Return true if ZertoRole exists, otherwise return false. .PARAMETER zertoRole Zerto role name (defaule value is ZertoRole) .EXAMPLE IsZertoRoleExists -zertoRole <role> #> [CmdletBinding()] [AVSAttribute(30, UpdatesSDDC = $false)] param( [parameter(Mandatory=$false, HelpMessage = "Zerto role name for ZVM installation")] [string]$zertoRole = "ZertoRole" ) Process { If (Get-VIRole -Name $zertoRole -ErrorAction SilentlyContinue) { Write-Host "$zertoRole already exists in $VC_ADDRESS" return $true } Write-Host "$zertoRole doesn't exist in $VC_ADDRESS" return $false; } } Function IsZertoDriverLoaded { <# .DESCRIPTION Return true if Zerto driver is loaded, otherwise return false. .PARAMETER HostName Host Name to connect with ssh .EXAMPLE IsZertoDriverLoaded -HostName <HostName> #> [CmdletBinding()] [AVSAttribute(30, UpdatesSDDC = $false)] param( [Parameter(Mandatory = $true, HelpMessage = "Host Name to connect with SSH")] [string]$HostName ) Process { $vmkload = '/tmp/vmklod_output' $ZertoDriver = '/tmp/zertoDriver' $Command = ('vmkload_mod -l > {0}' -f $vmkload) $Res = RunSSHCommands -HostName $HostName -Commands $Command $Command = ('grep "zdriver" {0} > {1}' -f $vmkload, $ZertoDriver) $Res = RunSSHCommands -HostName $HostName -Commands $Command -ExitStatusAction "Skip" $ExitStatus = $Res["0_exitStatus"] if ($ExitStatus -eq '0') { Write-Host "Zerto driver is loaded" return $true; } if ($ExitStatus -eq '1') { Write-Host "Zerto driver is not loaded" return $false; } } } Function GetDriverLogsFromHost { <# .DESCRIPTION Copy Zerto driver logs from host to datastore .PARAMETER HostName Host Name to connect with ssh .PARAMETER DatastoreUuid Datastore Uuid .EXAMPLE GetDriverLogsFromHost -HostName <HostName> -DatastoreUUid <DatastoreUuid> #> [CmdletBinding()] [AVSAttribute(30, UpdatesSDDC = $false)] param( [Parameter(Mandatory = $true, HelpMessage = "Datastore Uuid")] [string]$DatastoreUuid, [Parameter(Mandatory = $true, HelpMessage = "Host Name to connect with SSH")] [string]$HostName ) DownloadDriverLogFilesFromHostToPSEngine -HostName $HostName UploadDriverLogFilesFromPSEngineToDatastore -DatastoreUuid $DatastoreUuid -HostName $HostName } Function DownloadDriverLogFilesFromHostToPSEngine { param( [Parameter(Mandatory = $true, HelpMessage = "Host Name to connect with SSH")] [string]$HostName ) $sftpSessionId = ($SFTP_Sessions[$HostName]).Value.SessionId if (!(Test-Path -path $LOCAL_TEMP_FOLDER_FOR_ZERTO_DRIVER_LOGS)) { New-Item $LOCAL_TEMP_FOLDER_FOR_ZERTO_DRIVER_LOGS -Type Directory Write-Host "Create $LOCAL_TEMP_FOLDER_FOR_ZERTO_DRIVER_LOGS folder on powerShell engine" } $files = '/etc/vmware/zloadmod.txt', '/etc/vmware/zunloadmod.txt' foreach ($file in $files) { if (Test-SFTPPath -SessionId $sftpSessionId -Path $file) { Write-Host "Going to download $file from $HostName to powerShell engine ($LOCAL_TEMP_FOLDER_FOR_ZERTO_DRIVER_LOGS)" Get-SFTPItem -SessionId $sftpSessionId -Destination $LOCAL_TEMP_FOLDER_FOR_ZERTO_DRIVER_LOGS -Path $file -Force Write-Host "$file was copied from $HostName to powerShell engine" } else { Write-Host "File $file doesn't exist on $HostName" } } } Function UploadDriverLogFilesFromPSEngineToDatastore { param( [Parameter(Mandatory = $true, HelpMessage = "Datastore Uuid")] [string]$DatastoreUuid, [Parameter(Mandatory = $true, HelpMessage = "Host Name to connect with SSH")] [string]$HostName ) $psDriverName = "ds" $zertoDriverLogsDSPath = ('{0}:\zertoDriverLogsFromHost\{1}\' -f $psDriverName, $HostName) $zertoDriverLogsPSPath = ('{0}*' -f $LOCAL_TEMP_FOLDER_FOR_ZERTO_DRIVER_LOGS) $datastore = Get-Datastore $DatastoreUuid New-PSDrive -Location $datastore -Name $psDriverName -PSProvider VimDatastore -Root "\" Copy-DatastoreItem -Item $zertoDriverLogsPSPath -Destination $zertoDriverLogsDSPath -Force $files = (Get-ChildItem -Path $zertoDriverLogsDSPath -Name) -join ";" Write-Host "ZertoDriverFiles: $files were copied from powerShell enging ($zertoDriverLogsPSPath) to datastore ($zertoDriverLogsDSPath)" Remove-PSDrive -Name $psDriverName } Function GetZertoFilesListFromHost { <# .DESCRIPTION Return a list of all Zerto files on host under /var/zerto .PARAMETER HostName Host Name to connect with ssh .EXAMPLE GetZertoFilesListFromHost -HostName <HostName> #> [CmdletBinding()] [AVSAttribute(5, UpdatesSDDC = $false)] param( [Parameter(Mandatory = $true, HelpMessage = "Host Name to connect with SSH")] [string]$HostName ) process { $Command = ('ls -l {0}' -f $ZERTO_FOLDER_ON_HOST) return RunSSHCommands -HostName $HostName -Commands $Command } } #end Region Function TestConnection { return "TestConnection" } Function GenerateRandomPassword { #Generate a password with at least 2 uppercase, 4 lowercase, 4 digits & 2 special character (!@#$%^&*()) $upperChars =(65..90) $lowerChars = (97..122) $numerics = (48..57) $specialChars = @(33, 35, 36, 37, 38, 40, 41, 42, 45, 64, 94) $seedArray = ($upperChars | Get-Random -Count 2) $seedArray += ($lowerChars | Get-Random -Count 4) $seedArray += ($numerics | Get-Random -Count 4) $seedArray += ($specialChars | Get-Random -Count 2) Foreach ($a in $seedArray){ $passwordAscii += , [char][byte]$a } $password = $passwordAscii -join "" return $password } Function CreateZertoUser { <# .DESCRIPTION Create a ZertoDR user and a ZertoDR role which includes required privileges. The script creates a permission by assigning the ZertoDR role to the ZertoDR user. .PARAMETER zertoUsername Zerto user name (defaule value is ZertoDR) .PARAMETER zertoRole Zerto role name (defaule value is ZertoRole) .EXAMPLE CreateZertoUser -zertoUsername <Username> -zertoRole <Role> #> [CmdletBinding()] [AVSAttribute(30, UpdatesSDDC = $false)] param( [parameter(Mandatory=$false, HelpMessage = "Zerto user name for ZVM installation")] [string]$zertoUsername = "ZertoDR", [parameter(Mandatory=$false, HelpMessage = "Zerto role name for ZVM installation")] [string]$zertoRole = "ZertoRole" ) Process{ $domain = "vsphere.local" $zertoPrincipal = $domain + "\" + $zertoUsername $zertoPrivileges = @( "Alarm.Create", "Alarm.Delete", "Authorization.ModifyPermissions", "Cryptographer.Access", "Datastore.AllocateSpace", "Datastore.Browse", "Datastore.Config", "Datastore.DeleteFile", "Datastore.FileManagement", "Datastore.UpdateVirtualMachineFiles", "StoragePod.Config", "Extension.Register", "Extension.Unregister", "Folder.Create", "Global.CancelTask", "Global.Diagnostics", "Global.DisableMethods", "Global.EnableMethods", "Global.LogEvent", "Host.Config.AdvancedConfig", "Host.Config.AutoStart", "Host.Config.Settings", "Host.Config.NetService", "Host.Config.Patch", "Host.Inventory.EditCluster", "Network.Assign", "Resource.AssignVAppToPool", "Resource.AssignVMToPool", "Resource.ColdMigrate", "Resource.HotMigrate", "Sessions.ValidateSession", "Task.Create", "Task.Update", "VApp.ApplicationConfig", "VApp.AssignResourcePool", "VApp.AssignVM", "VApp.Create", "VApp.Delete", "VApp.Import", "VApp.PowerOff", "VApp.PowerOn", "VirtualMachine.Config.AddExistingDisk", "VirtualMachine.Config.AddNewDisk", "VirtualMachine.Config.AddRemoveDevice", "VirtualMachine.Config.AdvancedConfig", "VirtualMachine.Config.CPUCount", "VirtualMachine.Config.DiskExtend", "VirtualMachine.Config.EditDevice", "VirtualMachine.Config.ManagedBy", "VirtualMachine.Config.Memory", "VirtualMachine.Config.RawDevice", "VirtualMachine.Config.RemoveDisk", "VirtualMachine.Config.Resource", "VirtualMachine.Config.Settings", "VirtualMachine.Config.SwapPlacement", "VirtualMachine.Config.UpgradeVirtualHardware", "VirtualMachine.Interact.PowerOff", "VirtualMachine.Interact.PowerOn", "VirtualMachine.Inventory.CreateFromExisting", "VirtualMachine.Inventory.Create", "VirtualMachine.Inventory.Register", "VirtualMachine.Inventory.Delete", "VirtualMachine.Inventory.Unregister", "VirtualMachine.State.RemoveSnapshot" ) #if the user already exists - do nothing if(IsZertoUserExists $zertoUsername) { Write-Host "$zertoUsername already exists. Cotinue..." } else { #Create Zerto user $PersistentSecrets.ZertoPassword = GenerateRandomPassword New-SsoPersonUser -UserName $zertoUsername -Password $PersistentSecrets.ZertoPassword -Description "Zerto DR user" -EmailAddress "ZertoDR@zerto.com" -FirstName "Zerto" -LastName "DR" -ErrorAction Stop # Add user to CloudAdmins group $group = "CloudAdmins" $SsoGroup = Get-SsoGroup -Name $group -Domain $domain Get-SsoPersonUser -Name $zertoUsername -Domain $domain -ErrorAction Stop | Add-UserToSsoGroup -TargetGroup $SsoGroup -ErrorAction Stop } #If ZertoRole already exists - overwrite it with the new privilages If (IsZertoRoleExists $zertoRole) { $joinedPrivilgaes = ($zertoPrivileges -join ";") Write-Host "Role: $zertoRole already exists. Overwrite it with the following privilages: $joinedPrivilgaes" Remove-VIRole -Role (Get-VIRole -Name $zertoRole) -Force:$true -Confirm:$false New-VIRole -name $zertoRole -Privilege (Get-VIPrivilege -Server $VC_ADDRESS -id $zertoPrivileges) -Server $VC_ADDRESS } else { #Create a new role New-VIRole -name $zertoRole -Privilege (Get-VIPrivilege -Server $VC_ADDRESS -id $zertoPrivileges) -Server $VC_ADDRESS Write-Host "Role $zertoRole created on $VC_ADDRESS" } # Get the Root Folder $rootFolder = Get-Folder -NoRecursion # Create permission on vCenter object by assigning role to user New-VIPermission -Entity $rootFolder -Principal $zertoPrincipal -Role $zertoRole -Propagate:$true -ErrorAction Stop } } Function CreateZertoFolderOnHost { param( [Parameter(Mandatory = $true, HelpMessage = "Host Name to connect with SSH")] [string]$HostName ) process { $Command = "mkdir -p $ZERTO_FOLDER_ON_HOST" $Res = RunSSHCommands -HostName $HostName -Commands $Command $ExitStatus = $Res["0_exitStatus"]; if ( $ExitStatus -ne '0' ) { throw "failed to create $ZERTO_FOLDER_ON_HOST on host $HostName. Exit status for ""$Command"" is $ExitStatus" } } } Function VerifyAndUploadFilesFromPSEngineToHost { param( [Parameter(Mandatory = $true, HelpMessage = "Host Name to connect with SSH")] [string]$HostName ) CreateZertoFolderOnHost -HostName $HostName foreach ($file in Get-ChildItem $LOCAL_TEMP_FOLDER_FOR_DOWNLOADED_FILES* -Include *.sh, *.o) { $signature = ("{0}_signature" -f $file) $isVerified = (openssl dgst -sha256 -verify $PUBLIC_KEY -signature $signature $file 2>&1) -join ";" if ($isVerified -eq "Verified OK") { Set-SFTPItem -SessionId ($SFTP_Sessions[$HostName]).Value.SessionId -Destination $ZERTO_FOLDER_ON_HOST -Path $file -Force } else { throw "Error! host $HostName failed to verify $file with $signature, openSSL output: $isVerified" } } } Function DownloadFilesFromDatastoreToPSEngine { param( [Parameter(Mandatory = $true, HelpMessage = "Datastore Uuid")] [string]$DatastoreUuid, [Parameter(Mandatory = $true, HelpMessage = "Host Bios Uuid || mob-> Property Path: host.hardware.systemInfo.uuid")] [string]$BiosUuid ) $psDriverName = "ds" $FullRemoteFileLocation = ('{0}:\zagentid\{1}\*' -f $psDriverName, $BiosUuid) $datastore = Get-Datastore $DatastoreUuid New-PSDrive -Location $datastore -Name $psDriverName -PSProvider VimDatastore -Root "\" Copy-DatastoreItem -Item $FullRemoteFileLocation -Destination $LOCAL_TEMP_FOLDER_FOR_DOWNLOADED_FILES -Force $files = (Get-ChildItem -Path $LOCAL_TEMP_FOLDER_FOR_DOWNLOADED_FILES -Name) -join ";" Write-Host "ZertoFiles: $files from darastore $DatastoreUUid ($FullRemoteFileLocation) were copied to $LOCAL_TEMP_FOLDER_FOR_DOWNLOADED_FILES" Remove-PSDrive -Name $psDriverName } Function CopyFilesFromDatastoreToHost { param( [Parameter(Mandatory = $true, HelpMessage = "Host Name to connect with SSH")] [string]$HostName, [Parameter(Mandatory = $true, HelpMessage = "Datastore Uuid")] [string]$DatastoreUuid, [Parameter(Mandatory = $true, HelpMessage = "Host Bios Uuid || mob-> Property Path: host.hardware.systemInfo.uuid")] [string]$BiosUuid ) DownloadFilesFromDatastoreToPSEngine -DatastoreUuid $DatastoreUuid -BiosUuid $BiosUuid VerifyAndUploadFilesFromPSEngineToHost -HostName $HostName } Function RunSSHCommands { param( [Parameter(Mandatory = $true, HelpMessage = "Host Name to connect with SSH")] [string]$HostName, [Parameter(Mandatory = $true, HelpMessage = "Commands to execute")] [String[]]$Commands, [Parameter(Mandatory = $false, HelpMessage = "Action on exitStatus 1")] [string]$ExitStatusAction = "Stop" ) process { $NamedOutputs = @{} Set-Variable -Name NamedOutputs -Value $NamedOutputs -Scope Global $i = 0 foreach ($Command in $Commands) { $SSH = Invoke-SSHCommand -SSHSession $SSH_Sessions[$HostName].Value -Command $Command if (!$SSH) { throw "Error! failed to Invoke-SSHCommand ""$Command"" on host $HostName" } $ExitStatus = $SSH.ExitStatus $Error = $SSH.Error $Output = ($SSH.Output -join ";") if ($ExitStatus -ne 0 -Or $Error) { if (($ExitStatus -eq 1) -And (!$Error) -And ($ExitStatusAction -eq "Skip")) { Write-Host "ExitStatus of ""$Command"" is 1, while ExitStatusAction = Skip. Skipping..." } else { throw "Error! failed to run ""$Command"" on host $HostName, ExitStatus: $ExitStatus, Output: $Output, Error: $Error, ExitStatusAction: $ExitStatusAction" } } $NamedOutputs["$($i)_cmd"] = $Command $NamedOutputs["$($i)_exitStatus"] = $ExitStatus $NamedOutputs["$($i)_output"] = $Output $NamedOutputs["$($i)_error"] = $Error $i++; } return $NamedOutputs } } Function Get-HostTempFolderInfo { <# .DESCRIPTION Display information about the available disk space (For Internal Use) .PARAMETER HostName Host Name to connect with ssh .EXAMPLE Get-HostTempFolderInfo -HostName xxx.xxx.xxx.xxx #> [CmdletBinding()] [AVSAttribute(5, UpdatesSDDC = $false)] param( [Parameter(Mandatory = $true, HelpMessage = "Host Name to connect with SSH")] [string]$HostName ) process { $Command = "vdf" return RunSSHCommands -HostName $HostName -Commands $Command } } Function EnsureConnectivity { <# .DESCRIPTION Check if the host is up and running (For Internal Use) .PARAMETER HostName Host Name to connect with ssh .EXAMPLE EnsureConnectivity -HostName xxx.xxx.xxx.xxx #> [CmdletBinding()] [AVSAttribute(5, UpdatesSDDC = $false)] param( [Parameter(Mandatory = $true, HelpMessage = "Host Name to connect with SSH")] [string]$HostName ) process { $Command = "echo testing123" return RunSSHCommands -HostName $HostName -Commands $Command } } Function Get-HostEsxiVersion { <# .DESCRIPTION Retrieve the ESXi version (For Internal Use) .PARAMETER HostName Host Name to connect with ssh .EXAMPLE Get-HostEsxiVersion -HostName xxx.xxx.xxx.xxx #> [CmdletBinding()] [AVSAttribute(5, UpdatesSDDC = $false)] param( [Parameter(Mandatory = $true, HelpMessage = "Host Name to connect with SSH")] [string]$HostName ) process { $Command = "vmware -l" return RunSSHCommands -HostName $HostName -Commands $Command } } Function ChangeStartupFile { <# .DESCRIPTION Responsible for loading the driver when the host is booting. /etc/rc.local.d/local.sh file is executed after all the normal system services are started .PARAMETER HostName Host Name to connect with ssh .PARAMETER DatastoreUuid Datastore Uuid .PARAMETER BiosUuid "Host Bios Uuid || mob-> Property Path: host.hardware.systemInfo.uuid" .EXAMPLE ChangeStartupFile -HostName xxx.xxx.xxx.xxx -DatastoreUuid xxx -BiosUuid xxx #> [CmdletBinding()] [AVSAttribute(30, UpdatesSDDC = $false)] param( [Parameter(Mandatory = $true, HelpMessage = "Host Name to connect with SSH")] [string]$HostName, [Parameter(Mandatory = $true, HelpMessage = "Datastore Uuid")] [string]$DatastoreUuid, [Parameter(Mandatory = $true, HelpMessage = "Host Bios Uuid || mob-> Property Path: host.hardware.systemInfo.uuid")] [string]$BiosUuid ) Process { $zloadmod = ('{0}/zloadmod.sh' -f $ZERTO_FOLDER_ON_HOST) CopyFilesFromDatastoreToHost -HostName $HostName -DatastoreUuid $DatastoreUuid -BiosUuid $BiosUuid $startupFile = ('{0}/startup_file.sh' -f $ZERTO_FOLDER_ON_HOST) $Commands = ('grep -v "ZeRTO\|exit 0" /etc/rc.local.d/local.sh > {0}' -f $startupFile), ('echo \#ZeRTO\ >> {0}' -f $startupFile), ('echo sh {0} load {1} {2} \"\" \"\" 1 \> /etc/vmware/zloadmod.txt \2\>\&\1 \#ZeRTO\ >> {3}' -f $zloadmod, $DatastoreUuid, $BiosUuid, $startupFile), ('echo \#ZeRTO\ >> {0}' -f $startupFile), ('echo "exit 0" >> {0}' -f $startupFile), ('cp -f {0} /etc/rc.local.d/local.sh' -f $startupFile), ('chmod a+x {0}' -f $zloadmod) return RunSSHCommands -HostName $HostName -Commands $Commands } } Function InstallDriver { <# .DESCRIPTION Install the driver .PARAMETER HostName Host Name to connect with SSH .PARAMETER DatastoreUuid Datastore Uuid .PARAMETER BiosUuid Host Bios Uuid || mob-> Property Path: host.hardware.systemInfo.uuid .PARAMETER IsInit Init or load the driver .PARAMETER EsxiVersion Esxi version .EXAMPLE InstallDriver -HostName xxx.xxx.xxx.xxx -DatastoreUuid <UUID> -BiosUuid <UUID> -IsInit <init/load> -EsxiVersion xx #> [CmdletBinding()] [AVSAttribute(30, UpdatesSDDC = $false)] param( [Parameter(Mandatory = $true, HelpMessage = "Host Name to connect with SSH")] [string]$HostName, [Parameter(Mandatory = $true, HelpMessage = "Datastore Uuid")] [string]$DatastoreUuid, [Parameter(Mandatory = $true, HelpMessage = "Host Bios Uuid || mob-> Property Path: host.hardware.systemInfo.uuid")] [string]$BiosUuid, [Parameter(Mandatory = $true, HelpMessage = "Init or load the driver")] [string]$IsInit, [Parameter(Mandatory = $true, HelpMessage = "Esxi version")] [string]$EsxiVersion ) Process { $zloadmod = ('{0}/zloadmod.sh' -f $ZERTO_FOLDER_ON_HOST) CopyFilesFromDatastoreToHost -HostName $HostName -DatastoreUuid $DatastoreUuid -BiosUuid $BiosUuid $Commands = ('chmod a+x {0}' -f $zloadmod), ('{0} {1} {2} {3} 1 {4} 1 > /etc/vmware/zloadmod.txt' -f $zloadmod, $IsInit, $DatastoreUuid, $BiosUuid, $EsxiVersion) return RunSSHCommands -HostName $HostName -Commands $Commands } } Function UninstallDriver { <# .DESCRIPTION Uninstall the driver .PARAMETER HostName Host Name to connect with SSH .PARAMETER DatastoreUuid Datastore Uuid .PARAMETER BiosUuid Host Bios Uuid || mob-> Property Path: host.hardware.systemInfo.uuid .EXAMPLE UninstallDriver -HostName xxx.xxx.xxx.xxx -DatastoreUuid <UUID> -BiosUuid <UUID> #> [CmdletBinding()] [AVSAttribute(30, UpdatesSDDC = $false)] param( [Parameter(Mandatory = $true, HelpMessage = "Host Name to connect with SSH")] [string]$HostName, [Parameter(Mandatory = $true, HelpMessage = "Datastore Uuid")] [string]$DatastoreUuid, [Parameter(Mandatory = $true, HelpMessage = "Host Bios Uuid || mob-> Property Path: host.hardware.systemInfo.uuid")] [string]$BiosUuid ) process { $zunloadmod = ('{0}/zunloadmod.sh' -f $ZERTO_FOLDER_ON_HOST) CopyFilesFromDatastoreToHost -HostName $HostName -DatastoreUuid $DatastoreUuid -BiosUuid $BiosUuid $Commands = ('chmod a+x {0}' -f $zunloadmod), ('{0} cleanup > /etc/vmware/zunloadmod.txt' -f $zunloadmod) return RunSSHCommands -HostName $HostName -Commands $Commands } } |