From 804608f34a926c3356fb9e643f94c55749baa179 Mon Sep 17 00:00:00 2001 From: praveenmathamsetty Date: Fri, 23 Dec 2016 19:07:40 +0530 Subject: [PATCH 001/112] Adding features WhatIf, Cloning, Get-HVDesktopSpec and Get-HVInternalName 1) Enabling WhatIf functionality to Advanced Functions (All get-xxx AF not required) 2) Cloning Added support for all AFs except instant clone pools(need to fix a bug in server side for instant clone) 3) Get-HVDesktopSpec Converts DesktopInfo to DesktopSpec 4) Converts View API ids to human readbale names --- .../VMware.Hv.Helper/VMware.HV.Helper.psm1 | 514 ++++++++++++++++-- 1 file changed, 462 insertions(+), 52 deletions(-) diff --git a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 index 583933d..f3cb9ea 100644 --- a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 +++ b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 @@ -1,7 +1,7 @@ #Script Module : VMware.Hv.Helper #Version : 1.0 -#Copyright © 2016 VMware, Inc. All Rights Reserved. +#Copyright © 2016 VMware, Inc. All Rights Reserved. #Permission is hereby granted, free of charge, to any person obtaining a copy of #this software and associated documentation files (the "Software"), to deal in @@ -48,10 +48,8 @@ function Get-ViewAPIService { return $hvServer.ExtensionData } } elseif ($global:DefaultHVServers.Length -gt 0) { - if ($pscmdlet.ShouldProcess($global:DefaultHVServers[0].uid,'hvServer not specified, use default hvServer connection?')) { - $hvServer = $global:DefaultHVServers[0] - return $hvServer.ExtensionData - } + $hvServer = $global:DefaultHVServers[0] + return $hvServer.ExtensionData } return $null } @@ -296,7 +294,10 @@ The Add-HVDesktop adds virtual machines to already exiting pools by using view A return } } - $desktop_service_helper.Desktop_AddMachinesToManualDesktop($services,$id,$machineList) + if ($pscmdlet.ShouldProcess($machineList)) { + $desktop_service_helper.Desktop_AddMachinesToManualDesktop($services,$id,$machineList) + } + return $machineList } default { Write-Error "Only Automated/Manual pool types support this add operation" @@ -347,6 +348,7 @@ function Get-MachinesByVCenter ($MachineList,$VcId) { } return $machines } + function Add-HVRDSServer { <# .SYNOPSIS @@ -430,7 +432,10 @@ function Add-HVRDSServer { 'MANUAL' { try { $serverList = Get-RegisteredRDSServer -services $services -serverList $rdsServers - $farm_service_helper.Farm_AddRDSServers($services, $id, $serverList) + if ($pscmdlet.ShouldProcess($serverList)) { + $farm_service_helper.Farm_AddRDSServers($services, $id, $serverList) + } + return $serverList } catch { Write-Error "Failed to Add RDS Server to Farm with error: $_" break @@ -443,7 +448,8 @@ function Add-HVRDSServer { [System.gc]::collect() } } -[System.Reflection.Assembly]::LoadWithPartialName("System.Data.OracleClient") | Out-Null + + function Connect-HVEvent { <# @@ -506,6 +512,7 @@ function Connect-HVEvent { ) begin { + [System.Reflection.Assembly]::LoadWithPartialName("System.Data.OracleClient") | Out-Null # Connect to Connection Server and call the View API service $services = Get-ViewAPIService -hvServer $hvServer if ($null -eq $services) { @@ -1350,6 +1357,10 @@ function Get-HVPoolSummary { break } $poolList = Find-HVPool -Param $psboundparameters + if (!$poolList) { + Write-Host "No Pool Found with given search parameters" + break + } Return $poolList } @@ -1844,7 +1855,7 @@ function New-HVFarm { Reference to Horizon View Server to query the farms from. If the value is not passed or null then first element from global:DefaultHVServers would be considered inplace of hvServer. .EXAMPLE - New-HVFarm -LinkedClone -FarmName 'LCFarmTest' -ParentVM 'Win_Server_2012_R2' -SnapshotVM 'Snap_RDS' -VmFolder 'PoolVM' -HostOrCluster 'cls' -ResourcePool 'cls' -Datastores 'datastore1 (5)' -FarmDisplayName 'LC Farm Test' -Description  'created LC Farm from PS' -EnableProvisioning $true -StopOnProvisioningError $false -NamingPattern  "LCFarmVM_PS" -MinReady 1 -MaximumCount 1 -SysPrepName "RDSH_Cust2" -NetBiosName "adviewdev" + New-HVFarm -LinkedClone -FarmName 'LCFarmTest' -ParentVM 'Win_Server_2012_R2' -SnapshotVM 'Snap_RDS' -VmFolder 'PoolVM' -HostOrCluster 'cls' -ResourcePool 'cls' -Datastores 'datastore1 (5)' -FarmDisplayName 'LC Farm Test' -Description  'created LC Farm from PS' -EnableProvisioning $true -StopOnProvisioningError $false -NamingPattern  "LCFarmVM_PS" -MinReady 1 -MaximumCount 1 -SysPrepName "RDSH_Cust2" -NetBiosName "adviewdev" .EXAMPLE New-HVFarm -Spec C:\VMWare\Specs\LinkedClone.json @@ -2246,10 +2257,13 @@ function New-HVFarm { # Please uncomment below code, if you want to save the json file <# -$myDebug = convertto-json -InputObject $farmSpecObj -depth 12 -$myDebug | out-file -filepath c:\temp\copiedfarm.json -#> - $farm_service_helper.Farm_Create($services, $farmSpecObj) + $myDebug = convertto-json -InputObject $farmSpecObj -depth 12 + $myDebug | out-file -filepath c:\temp\copiedfarm.json + #> + if ($pscmdlet.ShouldProcess($farmSpecObj)) { + $Id = $farm_service_helper.Farm_Create($services, $farmSpecObj) + } + return $farmSpecObj } end { @@ -2454,8 +2468,6 @@ function Get-FarmSpec { if ($farmType -eq 'AUTOMATED') { $farm_spec_helper.getDataObject().AutomatedFarmSpec.RdsServerNamingSpec.PatternNamingSettings = $farm_helper.getFarmPatternNamingSettingsHelper().getDataObject() $farm_spec_helper.getDataObject().AutomatedFarmSpec.VirtualCenterProvisioningSettings.VirtualCenterStorageSettings.ViewComposerStorageSettings = $farm_helper.getFarmViewComposerStorageSettingsHelper().getDataObject() - } elseif ($farmType -eq 'MANUAL') { - # No need to set } return $farm_spec_helper.getDataObject() } @@ -2988,10 +3000,13 @@ function New-HVPool { [Parameter(Mandatory = $true,ParameterSetName = 'MANUAL')] [Parameter(Mandatory = $false,ParameterSetName = "JSON_FILE")] + [Parameter(Mandatory = $false,ParameterSetName = 'CLONED_POOL')] [string[]]$VM, #farm [Parameter(Mandatory = $false,ParameterSetName = 'RDS')] + [Parameter(Mandatory = $false,ParameterSetName = 'CLONED_POOL')] + [string] $Farm, @@ -3163,7 +3178,7 @@ function New-HVPool { $poolType = $clonePool.type $desktopBase = $clonePool.base $desktopSettings = $clonePool.DesktopSettings - $provisioningType = $null + $provisioningType = $clonePool.source if ($clonePool.AutomatedDesktopData) { $provisioningType = $clonePool.AutomatedDesktopData.ProvisioningType $virtualCenterID = $clonePool.AutomatedDesktopData.VirtualCenter @@ -3176,8 +3191,25 @@ function New-HVPool { $desktopVirtualCenterManagedCommonSettings = $clonePool.AutomatedDesktopData.virtualCenterManagedCommonSettings $desktopCustomizationSettings = $clonePool.AutomatedDesktopData.CustomizationSettings } - if (($null -eq $provisioningType) -or ($provisioningType -eq 'INSTANT_CLONE_ENGINE')) { - Write-Error "Only Automated linked clone or full clone pool support cloning" + elseif ($clonePool.ManualDesktopData) { + if (! $VM) { + Write-Error "ManualDesktop pool cloning requires list of machines, parameter VM is empty" + break + } + $source = $clonePool.source + $virtualCenterID = $clonePool.ManualDesktopData.VirtualCenter + $desktopUserAssignment = $clonePool.ManualDesktopData.userAssignment + $desktopVirtualCenterStorageSettings = $clonePool.ManualDesktopData.viewStorageAcceleratorSettings + $desktopVirtualCenterManagedCommonSettings = $clonePool.ManualDesktopData.virtualCenterManagedCommonSettings + } + elseif($clonePool.RdsDesktopData) { + if (! $Farm) { + Write-Error "RdsDesktop pool cloning requires farm, parameter Farm is not set" + break + } + } + if ($provisioningType -eq 'INSTANT_CLONE_ENGINE' -and $poolType -eq 'AUTOMATED') { + Write-Error "Cloning is not supported for instant clone pools" break } } else { @@ -3198,7 +3230,7 @@ function New-HVPool { elseif ($RDS) { $poolType = 'RDS' } } - $script:desktopSpecObj = Get-HVDesktopSpec -poolType $poolType -provisioningType $provisioningType -namingMethod $namingMethod + $script:desktopSpecObj = Get-DesktopSpec -poolType $poolType -provisioningType $provisioningType -namingMethod $namingMethod # # accumulate properties that are shared among various type @@ -3400,7 +3432,10 @@ function New-HVPool { $myDebug | out-file -filepath c:\temp\copieddesktop.json #> $desktop_helper = New-Object VMware.Hv.DesktopService - $desktop_helper.Desktop_create($services,$desktopSpecObj) + if ($pscmdlet.ShouldProcess($desktopSpecObj)) { + $id = $desktop_helper.Desktop_create($services,$desktopSpecObj) + } + return $desktopSpecObj } end { @@ -3457,7 +3492,7 @@ function Get-HVPoolProvisioningData { $folderList += $folders while ($folderList.Length -gt 0) { $item = $folderList[0] - if ($item -and !$_.folderdata.incompatiblereasons.inuse -and !$_.folderdata.incompatiblereasons.viewcomposerreplicafolder -and ($item.folderdata.name -eq $vmFolder)) { + if ($item -and !$_.folderdata.incompatiblereasons.inuse -and !$_.folderdata.incompatiblereasons.viewcomposerreplicafolder -and (($item.folderdata.path -eq $vmFolder) -or ($item.folderdata.name -eq $vmFolder))) { $vmObject.VmFolder = $item.id break } @@ -3473,7 +3508,7 @@ function Get-HVPoolProvisioningData { if ($hostOrCluster) { $vmFolder_helper = New-Object VMware.Hv.HostOrClusterService $hostClusterList = ($vmFolder_helper.HostOrCluster_GetHostOrClusterTree($services,$vmobject.datacenter)).treeContainer.children.info - $hostClusterObj = $hostClusterList | Where-Object { $_.name -eq $hostOrCluster } + $hostClusterObj = $hostClusterList | Where-Object { ($_.path -eq $hostOrCluster) -or ($_.name -eq $hostOrCluster) } if ($null -eq $hostClusterObj) { throw "No hostOrCluster found with Name: [$hostOrCluster]" } @@ -3482,7 +3517,7 @@ function Get-HVPoolProvisioningData { if ($resourcePool) { $resourcePool_helper = New-Object VMware.Hv.ResourcePoolService $resourcePoolList = $resourcePool_helper.ResourcePool_GetResourcePoolTree($services,$vmobject.HostOrCluster) - $resourcePoolObj = $resourcePoolList | Where-Object { $_.resourcepooldata.name -eq $resourcePool } + $resourcePoolObj = $resourcePoolList | Where-Object { ($_.resourcepooldata.path -eq $resourcePool) -or ($_.resourcepooldata.name -eq $resourcePool) } if ($null -eq $resourcePoolObj) { throw "No hostOrCluster found with Name: [$resourcePool]" } @@ -3526,7 +3561,7 @@ function Get-HVPoolStorageObject { $datastoreList = $datastore_helper.Datastore_ListDatastoresByHostOrCluster($services,$hostClusterID) $datastoresSelected = @() foreach ($ds in $datastores) { - $datastoresSelected += ($datastoreList | Where-Object { $_.datastoredata.name -eq $ds }).id + $datastoresSelected += ($datastoreList | Where-Object { ($_.DatastoreData.Path -eq $ds) -or ($_.datastoredata.name -eq $ds) }).id } foreach ($ds in $datastoresSelected) { $myDatastores = New-Object VMware.Hv.DesktopVirtualCenterDatastoreSettings @@ -3664,7 +3699,7 @@ function Get-CustomizationObject { } } -function Get-HVDesktopSpec { +function Get-DesktopSpec { param( [Parameter(Mandatory = $true)] @@ -3775,16 +3810,16 @@ function Remove-HVFarm { } if ($farmSpecObj) { foreach ($farmObj in $farmSpecObj) { - $farmList += $farmObj.id + $farmList += @{"id" = $farmObj.id; "Name" = $farmObj.data.name} } } else { Write-Error "Unable to retrieve FarmSummaryView with given farmName [$farmName]" break } - } elseif ($PSCmdlet.MyInvocation.ExpectingInput) { + } elseif ($PSCmdlet.MyInvocation.ExpectingInput -or $Farm) { foreach ($item in $farm) { - if ($item.GetType().name -eq 'FarmInfo' -or $item.GetType().name -eq 'FarmSummaryView') { - $farmList += $item.id + if (($item.GetType().name -eq 'FarmInfo') -or ($item.GetType().name -eq 'FarmSummaryView')) { + $farmList += @{"id" = $item.id; "Name" = $item.data.name} } else { Write-Error "In pipeline did not get object of expected type FarmSummaryView/FarmInfo" @@ -3795,10 +3830,11 @@ function Remove-HVFarm { } $farm_service_helper = New-Object VMware.Hv.FarmService foreach ($item in $farmList) { - $farm_service_helper.Farm_Delete($services, $item) + if ($pscmdlet.ShouldProcess($item)) { + $farm_service_helper.Farm_Delete($services, $item.id) + } + Write-Host "Farm Deleted: " $item.Name } - Write-Host "Farm Deleted" - } end { [System.gc]::collect() @@ -3893,7 +3929,7 @@ function Remove-HVPool { } if ($myPools) { foreach ($poolObj in $myPools) { - $poolList += $poolObj.id + $poolList += @{id = $poolObj.id; name = $poolObj.desktopSummaryData.name} } } else { Write-Error "No desktopsummarydata found with pool name: [$pool]" @@ -3901,8 +3937,11 @@ function Remove-HVPool { } } elseif ($PSCmdlet.MyInvocation.ExpectingInput) { foreach ($item in $pool) { - if (($item.GetType().name -eq 'DesktopInfo') -or ($item.GetType().name -eq 'DesktopSummaryView')) { - $poolList += $item.id + if ($item.GetType().name -eq 'DesktopSummaryView') { + $poolList += @{id = $item.id; name = $item.desktopSummaryData.name} + } + elseif ($item.GetType().name -eq 'DesktopInfo') { + $poolList += @{id = $item.id; name = $item.base.name} } else { Write-Error "In pipeline did not get object of expected type DesktopSummaryView/DesktopInfo" @@ -3917,9 +3956,8 @@ function Remove-HVPool { foreach ($item in $poolList) { if ($terminateSession) { #Terminate session - $queryResults = Get-HVQueryResults MachineSummaryView (Get-HVQueryFilter base.desktop -eq $item) + $queryResults = Get-HVQueryResults MachineSummaryView (Get-HVQueryFilter base.desktop -eq $item.id) $sessions += $queryResults.base.session - if ($null -ne $sessions) { $session_service_helper = New-Object VMware.Hv.SessionService try { @@ -3932,8 +3970,10 @@ function Remove-HVPool { Write-Host "No session found." } } - Write-Host "Deleting Pool" - $desktop_service_helper.Desktop_Delete($services,$item,$deleteSpec) + Write-Host "Deleting Pool: " $item.Name + if ($pscmdlet.ShouldProcess($deleteSpec)) { + $desktop_service_helper.Desktop_Delete($services,$item.id,$deleteSpec) + } } } @@ -4126,7 +4166,10 @@ function Set-HVFarm { } $farm_service_helper = New-Object VMware.Hv.FarmService foreach ($item in $farmList) { - $farm_service_helper.Farm_Update($services,$item,$updates) + if ($pscmdlet.ShouldProcess($updates)) { + $farm_service_helper.Farm_Update($services,$item,$updates) + } + Write-Host "Updated Farm Member $updates.Key with value $updates.value" } } @@ -4326,7 +4369,10 @@ function Set-HVPool { } $desktop_helper = New-Object VMware.Hv.DesktopService foreach ($item in $poolList) { - $desktop_helper.Desktop_Update($services,$item,$updates) + if ($pscmdlet.ShouldProcess($updates)) { + $desktop_helper.Desktop_Update($services,$item,$updates) + } + Write-Host "Updated Pool member $updates.key with value $updates.value" } } @@ -4519,9 +4565,11 @@ function Start-HVFarm { $updates = @() $updates += Get-MapEntry -key 'automatedFarmData.virtualCenterProvisioningSettings.virtualCenterProvisioningData.parentVm' -value $spec.ParentVM $updates += Get-MapEntry -key 'automatedFarmData.virtualCenterProvisioningSettings.virtualCenterProvisioningData.snapshot' -value $spec.Snapshot - $farm_service_helper.Farm_Update($services,$item,$updates) - - $farm_service_helper.Farm_Recompose($services,$item,$spec) + if ($pscmdlet.ShouldProcess($spec)) { + $farm_service_helper.Farm_Update($services,$item,$updates) + $farm_service_helper.Farm_Recompose($services,$item,$spec) + } + Write-Host "Performed recompose task on farm: $farmList.item" } } } @@ -4800,14 +4848,20 @@ function Start-HVPool { $spec = Get-HVTaskSpec -Source $poolSource.$item -poolName $poolList.$item -operation $operation -taskSpecName 'DesktopRebalanceSpec' -desktopId $item if ($null -ne $spec) { # make sure current task on VMs, must be None - $desktop_helper.Desktop_Rebalance($services,$item,$spec) + if ($pscmdlet.ShouldProcess($spec)) { + $desktop_helper.Desktop_Rebalance($services,$item,$spec) + } + Write-Host "Performed rebalance task on Pool: $PoolList.item" } } 'REFRESH' { $spec = Get-HVTaskSpec -Source $poolSource.$item -poolName $poolList.$item -operation $operation -taskSpecName 'DesktopRefreshSpec' -desktopId $item if ($null -ne $spec) { # make sure current task on VMs, must be None - $desktop_helper.Desktop_Refresh($services,$item,$spec) + if ($pscmdlet.ShouldProcess($spec)) { + $desktop_helper.Desktop_Refresh($services,$item,$spec) + } + Write-Host "Performed refresh task on Pool: $PoolList.item" } } 'RECOMPOSE' { @@ -4823,8 +4877,10 @@ function Start-HVPool { $updates = @() $updates += Get-MapEntry -key 'automatedDesktopData.virtualCenterProvisioningSettings.virtualCenterProvisioningData.parentVm' -value $spec.ParentVM $updates += Get-MapEntry -key 'automatedDesktopData.virtualCenterProvisioningSettings.virtualCenterProvisioningData.snapshot' -value $spec.Snapshot - $desktop_helper.Desktop_Update($services,$item,$updates) - + if ($pscmdlet.ShouldProcess($spec)) { + $desktop_helper.Desktop_Update($services,$item,$updates) + } + Write-Host "Performed recompose task on Pool: $PoolList.item" } } 'PUSH_IMAGE' { @@ -4839,7 +4895,10 @@ function Start-HVPool { $spec.Settings.LogoffSetting = $logoffSetting $spec.Settings.StopOnFirstError = $stopOnFirstError if ($startTime) { $spec.Settings.startTime = $startTime } - $desktop_helper.Desktop_SchedulePushImage($services,$item,$spec) + if ($pscmdlet.ShouldProcess($spec)) { + $desktop_helper.Desktop_SchedulePushImage($services,$item,$spec) + } + Write-Host "Performed push_image task on Pool: $PoolList.item" } } 'CANCEL_PUSH_IMAGE' { @@ -4847,7 +4906,10 @@ function Start-HVPool { Write-Error "$poolList.$item is not a INSTANT CLONE pool" break } else { - $desktop_helper.Desktop_CancelScheduledPushImage($services,$item) + if ($pscmdlet.ShouldProcess($spec)) { + $desktop_helper.Desktop_CancelScheduledPushImage($services,$item) + } + Write-Host "Performed cancel_push_image task on Pool: $PoolList.item" } } } @@ -4956,7 +5018,7 @@ function Find-HVMachine { if ($params['PoolName']) { $poolObj = Get-HVPoolSummary -poolName $params['PoolName'] -hvServer $params['HvServer'] if ($poolObj.Length -ne 1) { - Write-Host "Failed to retrieve specific pool object with given PoolName : "$params['PoolName'] + Write-Host "Failed to retrieve specific pool object with given PoolName : " $params['PoolName'] break; } else { $desktopId = $poolObj.Id @@ -5260,5 +5322,353 @@ function Get-HVMachineSummary { return $machineList } -Export-ModuleMember Add-HVDesktop,Add-HVRDSServer,Connect-HVEvent,Disconnect-HVEvent,Get-HVEvent,Get-HVFarm,Get-HVFarmSummary,Get-HVPool,Get-HVPoolSummary,Get-HVMachine,Get-HVMachineSummary,Get-HVQueryResult,Get-HVQueryFilter,New-HVFarm,New-HVPool,Remove-HVFarm,Remove-HVPool,Set-HVFarm,Set-HVPool,Start-HVFarm,Start-HVPool +function Get-HVDesktopSpec { +<# +.Synopsis + Gets desktop specification +.DESCRIPTION + Converts DesktopInfo Object to DesktopSpec. Also Converts view API Ids to human readable names + +.PARAMETER DesktopInfo + An object with detailed description of a desktop instance. + +.PARAMETER HvServer + Reference to Horizon View Server to query the virtual machines from. If the value is not passed or null then + first element from global:DefaultHVServers would be considered inplace of hvServer + +.EXAMPLE + Converts DesktopInfo to DesktopSpec + Get-HVDesktopSpec -DesktopInfo $DesktopInfoObj + +.EXAMPLE + Converts DesktopInfo to DesktopSpec and also dumps json object + Get-HVPool -PoolName 'LnkClnJson' | Get-HVDesktopSpec -FilePath "C:\temp\LnkClnJson.json" + +.OUTPUTS + Returns desktop specification + +.NOTES + Author : Praveen Mathamsetty. + Author email : pmathamsetty@vmware.com + Version : 1.1 + + ===Tested Against Environment==== + Horizon View Server Version : 7.0.2, 7.0.3 + PowerCLI Version : PowerCLI 6.5 + PowerShell Version : 5.0 +#> + [CmdletBinding( + SupportsShouldProcess = $true, + ConfirmImpact = 'High' + )] + + param( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [VMware.HV.DesktopInfo] + $DesktopInfo, + + [Parameter(Mandatory = $false)] + [String] + $FilePath, + + [Parameter(Mandatory = $false)] + $HvServer = $null + ) + + $DesktopSpec = New-Object VMware.HV.DesktopSpec + $DesktopPsObj = (($DesktopSpec | ConvertTo-Json -Depth 14) | ConvertFrom-Json) + $DesktopInfoPsObj = (($DesktopInfo | ConvertTo-Json -Depth 14) | ConvertFrom-Json) + $DesktopPsObj.Type = $DesktopInfoPsObj.Type + $DesktopPsObj.DesktopSettings = $DesktopInfoPsObj.DesktopSettings + + $entityId = New-Object VMware.HV.EntityId + $entityId.Id = $DesktopInfoPsObj.Base.AccessGroup.Id + $DesktopPsObj.Base = New-Object PsObject -Property @{ + name = $DesktopInfoPsObj.Base.Name; + displayName = $DesktopInfoPsObj.Base.displayName; + accessGroup = (Get-HVInternalName -EntityId $entityId); + description = $DesktopInfoPsObj.Base.description; + } + + if (! $DesktopInfoPsObj.GlobalEntitlementData.GlobalEntitlement) { + $DesktopPsObj.GlobalEntitlementData = $null + } else { + $entityId.Id = $DesktopInfoPsObj.GlobalEntitlementData.GlobalEntitlement.Id + $DesktopPsObj.GlobalEntitlementData = Get-HVInternalName -EntityId $entityId + } + + Switch ($DesktopInfo.Type) { + "AUTOMATED" { + $specificNamingSpecObj = $null + if ("SPECIFIED" -eq $DesktopInfoPsObj.AutomatedDesktopData.vmNamingSettings.NamingMethod) { + $specificNamingSpecObj = New-Object PsObject -Property @{ + specifiedNames = $null; + startMachinesInMaintenanceMode = $DesktopInfoPsObj.AutomatedDesktopData.vmNamingSettings.SpecificNamingSettings.StartMachinesInMaintenanceMode; + numUnassignedMachinesKeptPoweredOn = $DesktopInfoPsObj.AutomatedDesktopData.vmNamingSettings.SpecificNamingSettings.NumUnassignedMachinesKeptPoweredOn; + } + } + $vmNamingSpecObj = New-Object PsObject -Property @{ + namingMethod = $DesktopInfoPsObj.AutomatedDesktopData.vmNamingSettings.NamingMethod; + patternNamingSettings = $DesktopInfoPsObj.AutomatedDesktopData.VmNamingSettings.PatternNamingSettings; + specificNamingSpec = $specificNamingSpecObj; + } + $virtualCenterProvisioningDataObj = New-Object PsObject @{ + template = $null; + parentVm = $null; + snapshot = $null; + datacenter = $null; + vmFolder = $null; + hostOrCluster = $null; + resourcePool= $null; + } + $ProvisioningSettingsObj = $DesktopInfoPsObj.AutomatedDesktopData.VirtualCenterProvisioningSettings + if ($ProvisioningSettingsObj.VirtualCenterProvisioningData.Datacenter){ + $entityId.Id = $ProvisioningSettingsObj.VirtualCenterProvisioningData.Datacenter.Id + $virtualCenterProvisioningDataObj.Datacenter = Get-HVInternalName -EntityId $entityId + } + if ($ProvisioningSettingsObj.VirtualCenterProvisioningData.HostOrCluster){ + $entityId.Id = $ProvisioningSettingsObj.VirtualCenterProvisioningData.HostOrCluster.Id + $virtualCenterProvisioningDataObj.HostOrCluster = Get-HVInternalName -EntityId $entityId + } + if ($ProvisioningSettingsObj.VirtualCenterProvisioningData.ResourcePool){ + $entityId.Id = $ProvisioningSettingsObj.VirtualCenterProvisioningData.ResourcePool.Id + $virtualCenterProvisioningDataObj.ResourcePool = Get-HVInternalName -EntityId $entityId + } + if ($ProvisioningSettingsObj.VirtualCenterProvisioningData.ParentVm){ + $entityId.Id = $ProvisioningSettingsObj.VirtualCenterProvisioningData.ParentVm.Id + $virtualCenterProvisioningDataObj.ParentVm = Get-HVInternalName -EntityId $entityId ` + -VcId $DesktopInfo.AutomatedDesktopData.virtualCenter + } + if ($ProvisioningSettingsObj.VirtualCenterProvisioningData.Snapshot){ + $entityId.Id = $ProvisioningSettingsObj.VirtualCenterProvisioningData.Snapshot.Id + $virtualCenterProvisioningDataObj.Snapshot = Get-HVInternalName -EntityId $entityId ` + -BaseImageVmId $DesktopInfo.AutomatedDesktopData.VirtualCenterProvisioningSettings.VirtualCenterProvisioningData.ParentVm + } + if ($ProvisioningSettingsObj.VirtualCenterProvisioningData.Template){ + $entityId.Id = $ProvisioningSettingsObj.VirtualCenterProvisioningData.Template.Id + $virtualCenterProvisioningDataObj.Template = Get-HVInternalName -EntityId $entityId + } + if ($ProvisioningSettingsObj.VirtualCenterProvisioningData.VmFolder){ + $entityId.Id = $ProvisioningSettingsObj.VirtualCenterProvisioningData.VmFolder.Id + $virtualCenterProvisioningDataObj.VmFolder = Get-HVInternalName -EntityId $entityId + } + + $DesktopInfoPsObj.AutomatedDesktopData.VirtualCenterProvisioningSettings.VirtualCenterProvisioningData = ` + $virtualCenterProvisioningDataObj + $datastores = $DesktopInfoPsObj.AutomatedDesktopData.VirtualCenterProvisioningSettings.virtualCenterStorageSettings.datastores + $dataStoresObj = Get-DataStoreName -datastores $datastores + $DesktopInfoPsObj.AutomatedDesktopData.VirtualCenterProvisioningSettings.virtualCenterStorageSettings.datastores = ` + $dataStoresObj + $virtualCenterStorageSettingsObj = ` + $DesktopInfoPsObj.AutomatedDesktopData.VirtualCenterProvisioningSettings.virtualCenterStorageSettings + if($virtualCenterStorageSettingsObj.replicaDiskDatastore) { + $entityId.Id = $virtualCenterStorageSettingsObj.replicaDiskDatastore.Id + $DesktopInfoPsObj.AutomatedDesktopData.VirtualCenterProvisioningSettings.virtualCenterStorageSettings.replicaDiskDatastore =` + Get-HVInternalName -EntityId $entityId + } + if($virtualCenterStorageSettingsObj.persistentDiskSettings) { + $datastores = $virtualCenterStorageSettingsObj.persistentDiskSettings.persistentDiskDatastores + $dataStoresObj = Get-DataStoreName -datastores $datastores + $DesktopInfoPsObj.AutomatedDesktopData.VirtualCenterProvisioningSettings.virtualCenterStorageSettings.persistentDiskSettings.persistentDiskDatastores = ` + $dataStoresObj + } + if ($DesktopInfoPsObj.AutomatedDesktopData.customizationSettings.domainAdministrator) { + $entityId.Id = $DesktopInfoPsObj.AutomatedDesktopData.customizationSettings.domainAdministrator.Id + $DesktopInfoPsObj.AutomatedDesktopData.customizationSettings.domainAdministrator = Get-HVInternalName -EntityId $entityId + } + if ($DesktopInfoPsObj.AutomatedDesktopData.customizationSettings.adContainer) { + $entityId.Id = $DesktopInfoPsObj.AutomatedDesktopData.customizationSettings.adContainer.Id + $DesktopInfoPsObj.AutomatedDesktopData.customizationSettings.adContainer = Get-HVInternalName -EntityId $entityId + } + if ($DesktopInfoPsObj.AutomatedDesktopData.customizationSettings.sysprepCustomizationSettings) { + $entityId.Id = ` + $DesktopInfoPsObj.AutomatedDesktopData.customizationSettings.sysprepCustomizationSettings.customizationSpec.Id + $DesktopInfoPsObj.AutomatedDesktopData.customizationSettings.sysprepCustomizationSettings.customizationSpec = ` + Get-HVInternalName -EntityId $entityId + } + if ($DesktopInfoPsObj.AutomatedDesktopData.customizationSettings.cloneprepCustomizationSettings) { + $entityId.Id = ` + $DesktopInfoPsObj.AutomatedDesktopData.customizationSettings.cloneprepCustomizationSettings.instantCloneEngineDomainAdministrator.Id + $DesktopInfoPsObj.AutomatedDesktopData.customizationSettings.cloneprepCustomizationSettings.instantCloneEngineDomainAdministrator = ` + Get-HVInternalName -EntityId $entityId + } + + $DesktopPsObj.AutomatedDesktopSpec = New-Object PsObject -Property @{ + provisioningType = $DesktopInfoPsObj.AutomatedDesktopData.ProvisioningType; + virtualCenter = $null; + userAssignment = $DesktopInfoPsObj.AutomatedDesktopData.UserAssignment; + virtualCenterProvisioningSettings = $DesktopInfoPsObj.AutomatedDesktopData.VirtualCenterProvisioningSettings; + virtualCenterManagedCommonSettings = $DesktopInfoPsObj.AutomatedDesktopData.virtualCenterManagedCommonSettings; + customizationSettings = $DesktopInfoPsObj.AutomatedDesktopData.customizationSettings; + vmNamingSpec = $VmNamingSpecObj; + } + if ($DesktopInfoPsObj.AutomatedDesktopData.virtualCenter) { + $entityId.Id = $DesktopInfoPsObj.AutomatedDesktopData.virtualCenter.Id + $DesktopPsObj.AutomatedDesktopSpec.virtualCenter = Get-HVInternalName ` + -EntityId $entityId + } + break + } + "MANUAL" { + $DesktopPsObj.ManualDesktopSpec = New-Object PsObject -Property @{ + userAssignment = $DesktopInfoPsObj.ManualDesktopData.UserAssignment; + source = $DesktopInfoPsObj.ManualDesktopData.Source; + virtualCenter = $null; + machines = $null; + viewStorageAcceleratorSettings = $DesktopInfoPsObj.ManualDesktopData.ViewStorageAcceleratorSettings; + virtualCenterManagedCommonSettings = $DesktopInfoPsObj.ManualDesktopData.VirtualCenterManagedCommonSettings; + } + if ($DesktopInfoPsObj.ManualDesktopData.virtualCenter) { + $entityId.Id = $DesktopInfoPsObj.ManualDesktopData.virtualCenter.Id + $DesktopPsObj.ManualDesktopSpec.virtualCenter = Get-HVInternalName ` + -EntityId $entityId + } + break + } + "RDS" { + $DesktopPsObj.rdsDesktopSpec = New-Object PsObject -Property @{ + farm = $null; + } + break + } + } + $DesktopSpecJson = ($DesktopPsObj | ConvertTo-Json -Depth 14) + if ($filePath) { + $DesktopSpecJson | Out-File -FilePath $filePath + } + return $DesktopSpecJson +} + +function Get-DataStoreName { + param( + [Parameter(Mandatory = $true)] + $datastores + ) + $dataStoresObj = @() + $entityId = New-Object VMware.Hv.EntityId + $datastores | % { + $entityId.Id = $_.datastore.Id + $dataStoresObj += , (New-Object PsObject -Property @{ + datastore = Get-HVInternalName -EntityId $entityId; + storageOvercommit = $_.storageOvercommit; + }) + } + return $dataStoresObj +} + +function Get-HVInternalName { +<# +.Synopsis + Gets human readable name + +.DESCRIPTION + Converts Horizon API Ids to human readable names. Horizon API Ids are base64 encoded, this function + will decode and returns internal/human readable names. + +.PARAMETER EntityId + Representation of a manageable entity id. + +.PARAMETER HvServer + Reference to Horizon View Server to query the virtual machines from. If the value is not passed or null then + first element from global:DefaultHVServers would be considered inplace of hvServer + +.EXAMPLE + Decodes and returns human readable name + Get-HVInternalName -EntityId $entityId + +.OUTPUTS + Returns human readable name + +.NOTES + Author : Praveen Mathamsetty. + Author email : pmathamsetty@vmware.com + Version : 1.1 + + ===Tested Against Environment==== + Horizon View Server Version : 7.0.2, 7.0.3 + PowerCLI Version : PowerCLI 6.5 + PowerShell Version : 5.0 +#> + [CmdletBinding( + SupportsShouldProcess = $true, + ConfirmImpact = 'High' + )] + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [VMware.HV.EntityId] + $EntityId, + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [VMware.HV.VirtualCenterId] + $VcId, + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [VMware.HV.BaseImageVmId] + $BaseImageVmId, + + [Parameter(Mandatory = $false)] + $HvServer = $null + ) + begin { + $services = Get-ViewAPIService -hvServer $hvServer + if ($null -eq $services) { + Write-Error "Could not retrieve ViewApi services from connection object" + break + } + } + process { + $tokens = ($EntityId.id -split "/") + $serviceName = $tokens[0] + Switch ($serviceName) { + 'VirtualCenter' { + $vc_id = New-Object VMware.HV.VirtualCenterId + $vc_id.Id = $EntityId.Id + return ($services.VirtualCenter.VirtualCenter_Get($vc_id)).serverSpec.serverName + } + 'InstantCloneEngineDomainAdministrator' { + $Icid = New-Object VMware.HV.InstantCloneEngineDomainAdministratorId + $Icid.Id = $EntityId.Id + $Info = $services.InstantCloneEngineDomainAdministrator.InstantCloneEngineDomainAdministrator_Get($Icid) + return $Info.Base.Username + } + 'BaseImageVm' { + $info = $services.BaseImageVm.BaseImageVm_List($VcId) | where { $_.id.id -eq $EntityId.id } + return $info.name + } + 'BaseImageSnapshot' { + $info = $services.BaseImageSnapshot.BaseImageSnapshot_List($BaseImageVmId) | where { $_.id.id -eq $EntityId.id } + return $info.name + } + 'VmTemplate' { + $info = $services.VmTemplate.VmTemplate_List($VcId) | where { $_.id.id -eq $EntityId.id } + return $info.name + } + 'ViewComposerDomainAdministrator' { + $AdministratorId = New-Object VMware.HV.ViewComposerDomainAdministratorId + $AdministratorId.id = $EntityId.id + $info = $services.ViewComposerDomainAdministrator.ViewComposerDomainAdministrator_Get($AdministratorId) + return $info.base.userName + } + default { + $base64String = $tokens[$tokens.Length-1] + $mod = $base64String.Length % 4 + if ($mod -ne 0) { + #Length of a string must be multiples of 4 + $base64String = $base64String.PadRight(($base64String.Length + (4 - $mod)), "=") + } + #Convert 4 bytes to 3 bytes base64 decoding + return ([System.Text.Encoding]::ASCII.GetString([System.Convert]:: ` + FromBase64String($base64String))) + } + } + } + end { + [System.gc]::collect() + } +} + +Export-ModuleMember Add-HVDesktop,Add-HVRDSServer,Connect-HVEvent,Disconnect-HVEvent,Get-HVDesktopSpec,Get-HVInternalName, Get-HVEvent,Get-HVFarm,Get-HVFarmSummary,Get-HVPool,Get-HVPoolSummary,Get-HVMachine,Get-HVMachineSummary,Get-HVQueryResult,Get-HVQueryFilter,New-HVFarm,New-HVPool,Remove-HVFarm,Remove-HVPool,Set-HVFarm,Set-HVPool,Start-HVFarm,Start-HVPool From 608ca60d34220416855dbe9c769b320ecaf09040 Mon Sep 17 00:00:00 2001 From: praveenmathamsetty Date: Wed, 4 Jan 2017 13:02:27 +0530 Subject: [PATCH 002/112] Text description for examples, wild card support to farm 1) Added text description for all the advanced function examples 2) Wild card support(only * character support) for farm (Get-HVFarm, Get-HVFarmSummary) --- .../VMware.Hv.Helper/VMware.HV.Helper.psm1 | 169 ++++++++++++++---- 1 file changed, 134 insertions(+), 35 deletions(-) diff --git a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 index f3cb9ea..e1ff3d6 100644 --- a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 +++ b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 @@ -918,22 +918,27 @@ function Get-HVFarm { Reference to Horizon View Server to query the data from. If the value is not passed or null then first element from global:DefaultHVServers would be considered inplace of hvServer. .EXAMPLE + Queries and returns farmInfo based on given parameter farmName Get-HVFarm -FarmName 'Farm-01' .EXAMPLE + Queries and returns farmInfo based on given parameters farmName, farmDisplayName Get-HVFarm -FarmName 'Farm-01' -FarmDisplayName 'Sales RDS Farm' .EXAMPLE + Queries and returns farmInfo based on given parameters farmName, farmType Get-HVFarm -FarmName 'Farm-01' -FarmType 'MANUAL' .EXAMPLE + Queries and returns farmInfo based on given parameters farmName, FarmType etc Get-HVFarm -FarmName 'Farm-01' -FarmType 'MANUAL' -Enabled $true .EXAMPLE - Get-HVFarm -FarmName 'Farm-01' + Queries and returns farmInfo based on parameter farmName with wild character * + Get-HVFarm -FarmName 'Farm-0*' .OUTPUTs - Returns the list of FarmSummaryView or FarmInfo object matching the query criteria. + Returns the list of FarmInfo object matching the query criteria. .NOTES Author : Ankit Gupta. @@ -979,6 +984,10 @@ function Get-HVFarm { break } $farmList = Find-HVFarm -Param $PSBoundParameters + if (! $farmList) { + Write-Host "No farm Found with given search parameters" + breakss + } $farm_service_helper = New-Object VMware.Hv.FarmService $queryResults = @() foreach ($id in $farmList.id) { @@ -1015,22 +1024,27 @@ function Get-HVFarmSummary { Reference to Horizon View Server to query the data from. If the value is not passed or null then first element from global:DefaultHVServers would be considered inplace of hvServer. .EXAMPLE - Get-HVFarm -FarmName 'Farm-01' + Queries and returns farmSummary objects based on given parameter farmName + Get-HVFarmSummary -FarmName 'Farm-01' .EXAMPLE - Get-HVFarm -FarmName 'Farm-01' -FarmDisplayName 'Sales RDS Farm' + Queries and returns farmSummary objects based on given parameters farmName, farmDisplayName + Get-HVFarmSummary -FarmName 'Farm-01' -FarmDisplayName 'Sales RDS Farm' .EXAMPLE - Get-HVFarm -FarmName 'Farm-01' -FarmType 'MANUAL' + Queries and returns farmSummary objects based on given parameters farmName, farmType + Get-HVFarmSummary -FarmName 'Farm-01' -FarmType 'MANUAL' .EXAMPLE - Get-HVFarm -FarmName 'Farm-01' -FarmType 'MANUAL' -Enabled $true + Queries and returns farmSummary objects based on given parameters farmName, FarmType etc + Get-HVFarmSummary -FarmName 'Farm-01' -FarmType 'MANUAL' -Enabled $true .EXAMPLE - Get-HVFarm -FarmName 'Farm-01' + Queries and returns farmSummary objects based on given parameter farmName with wild character * + Get-HVFarmSummary -FarmName 'Farm-0*' .OUTPUTs - Returns the list of FarmSummaryView or FarmInfo object matching the query criteria. + Returns the list of FarmSummary object matching the query criteria. .NOTES Author : Praveen Mathamsetty. @@ -1076,6 +1090,10 @@ function Get-HVFarmSummary { break } $farmList = Find-HVFarm -Param $PSBoundParameters + if (! $farmList) { + Write-Host "No farm Found with given search parameters" + break + } return $farmList } @@ -1099,27 +1117,51 @@ function Find-HVFarm { $query_service_helper = New-Object VMware.Hv.QueryServiceService $query = New-Object VMware.Hv.QueryDefinition + $wildcard = $false # build the query values + if ($params['FarmName'] -and $params['FarmName'].contains('*')) { + $wildcard = $true + } + if ($params['FarmDisplayName'] -and $params['FarmDisplayName'].contains('*')) { + $wildcard = $true + } $query.queryEntityType = 'FarmSummaryView' - [VMware.Hv.queryfilter[]]$filterSet = @() - foreach ($setting in $farmSelectors.Keys) { - if ($null -ne $params[$setting]) { - $equalsFilter = New-Object VMware.Hv.QueryFilterEquals - $equalsFilter.memberName = $farmSelectors[$setting] - $equalsFilter.value = $params[$setting] - $filterSet += $equalsFilter + if (! $wildcard) { + [VMware.Hv.queryfilter[]]$filterSet = @() + foreach ($setting in $farmSelectors.Keys) { + if ($null -ne $params[$setting]) { + $equalsFilter = New-Object VMware.Hv.QueryFilterEquals + $equalsFilter.memberName = $farmSelectors[$setting] + $equalsFilter.value = $params[$setting] + $filterSet += $equalsFilter + } + } + if ($filterSet.Count -gt 0) { + $queryList = New-Object VMware.Hv.QueryFilterAnd + $queryList.Filters = $filterset + $query.Filter = $queryList } - } - if ($filterSet.Count -gt 0) { - $queryList = New-Object VMware.Hv.QueryFilterAnd - $queryList.Filters = $filterset - $query.Filter = $queryList - } - $queryResults = $query_service_helper.QueryService_Query($services, $query) - $farmList = $queryResults.results - + $queryResults = $query_service_helper.QueryService_Query($services, $query) + $farmList = $queryResults.results + } elseif ($wildcard -or [string]::IsNullOrEmpty($farmList)){ + $query.Filter = $null + $queryResults = $query_service_helper.QueryService_Query($services,$query) + $strFilterSet = @() + foreach ($setting in $farmSelectors.Keys) { + if ($null -ne $params[$setting]) { + if ($wildcard -and (($setting -eq 'FarmName') -or ($setting -eq 'FarmDisplayName')) ) { + $strFilterSet += '($_.' + $farmSelectors[$setting] + ' -like "' + $params[$setting] + '")' + } else { + $strFilterSet += '($_.' + $farmSelectors[$setting] + ' -eq "' + $params[$setting] + '")' + } + } + } + $whereClause = [string]::Join(' -and ', $strFilterSet) + $scriptBlock = [Scriptblock]::Create($whereClause) + $farmList = $queryResults.results | where $scriptBlock + } Return $farmList } @@ -1168,19 +1210,23 @@ function Get-HVPool { first element from global:DefaultHVServers would be considered inplace of hvServer .EXAMPLE + Queries and returns pool object(s) based on given parameters poolName, poolType etc. Get-HVPool -PoolName 'mypool' -PoolType MANUAL -UserAssignment FLOATING -Enabled $true -ProvisioningEnabled $true .EXAMPLE + Queries and returns pool object(s) based on given parameters poolType and userAssignment Get-HVPool -PoolType AUTOMATED -UserAssignment FLOATING .EXAMPLE + Queries and returns pool object(s) based on given parameters poolName, PoolType etc. Get-HVPool -PoolName 'myrds' -PoolType RDS -UserAssignment DEDICATED -Enabled $false .EXAMPLE + Queries and returns pool object(s) based on given parameters poolName and HvServer etc. Get-HVPool -PoolName 'myrds' -PoolType RDS -UserAssignment DEDICATED -Enabled $false -HvServer $mycs .OUTPUTS - Returns list of objects of type Desktop + Returns list of objects of type DesktopInfo .NOTES Author : Praveen Mathamsetty. @@ -1235,6 +1281,10 @@ function Get-HVPool { break } $poolList = Find-HVPool -Param $PSBoundParameters + if (! $poolList) { + Write-Host "No Pool Found with given search parameters" + break + } $queryResults = @() $desktop_helper = New-Object VMware.Hv.DesktopService foreach ($id in $poolList.id) { @@ -1290,15 +1340,19 @@ function Get-HVPoolSummary { first element from global:DefaultHVServers would be considered inplace of hvServer .EXAMPLE + Queries and returns desktopSummaryView based on given parameters poolName, poolType etc. Get-HVPoolSummary -PoolName 'mypool' -PoolType MANUAL -UserAssignment FLOATING -Enabled $true -ProvisioningEnabled $true .EXAMPLE + Queries and returns desktopSummaryView based on given parameters poolType, userAssignment. Get-HVPoolSummary -PoolType AUTOMATED -UserAssignment FLOATING .EXAMPLE + Queries and returns desktopSummaryView based on given parameters poolName, poolType, userAssignment etc. Get-HVPoolSummary -PoolName 'myrds' -PoolType RDS -UserAssignment DEDICATED -Enabled $false .EXAMPLE + Queries and returns desktopSummaryView based on given parameters poolName, HvServer etc. Get-HVPoolSummary -PoolName 'myrds' -PoolType RDS -UserAssignment DEDICATED -Enabled $false -HvServer $mycs .OUTPUTS @@ -1478,30 +1532,38 @@ function Get-HVQueryFilter { .EXAMPLE + Creates queryFilterEquals with given parameters memberName(position 0) and memberValue(position 2) Get-HVQueryFilter data.name -Eq vmware .EXAMPLE + Creates queryFilterEquals with given parameters memberName and memberValue Get-HVQueryFilter -MemberName data.name -Eq -MemberValue vmware .EXAMPLE + Creates queryFilterNotEquals filter with given parameters memberName and memberValue Get-HVQueryFilter data.name -Ne vmware .EXAMPLE + Creates queryFilterContains with given parameters memberName and memberValue Get-HVQueryFilter data.name -Contains vmware .EXAMPLE + Creates queryFilterStartsWith with given parameters memberName and memberValue Get-HVQueryFilter data.name -Startswith vmware .EXAMPLE + Creates queryFilterNot with given parameter filter $filter = Get-HVQueryFilter data.name -Startswith vmware Get-HVQueryFilter -Not $filter .EXAMPLE + Creates queryFilterAnd with given parameter filters array $filter1 = Get-HVQueryFilter data.name -Startswith vmware $filter2 = Get-HVQueryFilter data.name -Contains pool Get-HVQueryFilter -And @($filter1, $filter2) .EXAMPLE + Creates queryFilterOr with given parameter filters array $filter1 = Get-HVQueryFilter data.name -Startswith vmware $filter2 = Get-HVQueryFilter data.name -Contains pool Get-HVQueryFilter -Or @($filter1, $filter2) @@ -1630,22 +1692,24 @@ function Get-HVQueryResult { first element from global:DefaultHVServers would be considered inplace of hvServer .EXAMPLE + Returns query results of entityType DesktopSummaryView(position 0) Get-HVQueryResult DesktopSummaryView .EXAMPLE + Returns query results of entityType DesktopSummaryView(position 0) with given filter(position 1) Get-HVQueryResult DesktopSummaryView (Get-HVQueryFilter data.name -Eq vmware) .EXAMPLE + Returns query results of entityType DesktopSummaryView with given filter Get-HVQueryResult -EntityType DesktopSummaryView -Filter (Get-HVQueryFilter desktopSummaryData.name -Eq vmware) .EXAMPLE - Get-HVQueryResult -EntityType DesktopSummaryView -Filter (Get-HVQueryFilter desktopSummaryData.name -Eq vmware) -SortBy desktopSummaryData.displayName - -.EXAMPLE + Returns query results of entityType DesktopSummaryView with given filter and also sorted based on dispalyName $myFilter = Get-HVQueryFilter data.name -Contains vmware Get-HVQueryResult -EntityType DesktopSummaryView -Filter $myFilter -SortBy desktopSummaryData.displayName -SortDescending $false .EXAMPLE + Returns query results of entityType DesktopSummaryView, maximum count equal to limit Get-HVQueryResult DesktopSummaryView -Limit 10 .OUTPUTS @@ -1855,12 +1919,15 @@ function New-HVFarm { Reference to Horizon View Server to query the farms from. If the value is not passed or null then first element from global:DefaultHVServers would be considered inplace of hvServer. .EXAMPLE + Creates new linkedClone farm by using naming pattern New-HVFarm -LinkedClone -FarmName 'LCFarmTest' -ParentVM 'Win_Server_2012_R2' -SnapshotVM 'Snap_RDS' -VmFolder 'PoolVM' -HostOrCluster 'cls' -ResourcePool 'cls' -Datastores 'datastore1 (5)' -FarmDisplayName 'LC Farm Test' -Description  'created LC Farm from PS' -EnableProvisioning $true -StopOnProvisioningError $false -NamingPattern  "LCFarmVM_PS" -MinReady 1 -MaximumCount 1 -SysPrepName "RDSH_Cust2" -NetBiosName "adviewdev" .EXAMPLE + Creates new linkedClone farm by using json file New-HVFarm -Spec C:\VMWare\Specs\LinkedClone.json .EXAMPLE + Creates new manual farm by using rdsServers names New-HVFarm -Manual -FarmName "manualFarmTest" -FarmDisplayName "manualFarmTest" -Description "Manual PS Test" -RdsServers "vm-for-rds.eng.vmware.com","vm-for-rds-2.eng.vmware.com" .OUTPUTS @@ -2683,7 +2750,7 @@ function New-HVPool { New-HVPool -Spec C:\VMWare\Specs\LinkedClone.json .EXAMPLE - Clone new pool from automated linked (or) full clone pool + Clones new pool by using existing pool configuration Get-HVPool -PoolName 'vmwarepool' | New-HVPool -PoolName 'clonedPool' -NamingPattern 'clonelnk1'; (OR) $vmwarepool = Get-HVPool -PoolName 'vmwarepool'; New-HVPool -ClonePool $vmwarepool -PoolName 'clonedPool' -NamingPattern 'clonelnk1'; @@ -3752,12 +3819,15 @@ function Remove-HVFarm { Reference to Horizon View Server to query the data from. If the value is not passed or null then first element from global:DefaultHVServers would be considered inplace of hvServer. .EXAMPLE + Delete a given farm. For an automated farm, all the RDS Server VMs are deleted from disk whereas for a manual farm only the RDS Server associations are removed. Remove-HVFarm -FarmName 'Farm-01' -HvServer $hvServer .EXAMPLE + Deletes a given Farm object(s). For an automated farm, all the RDS Server VMs are deleted from disk whereas for a manual farm only the RDS Server associations are removed. $farm_array | Remove-HVFarm -HvServer $hvServer .EXAMPLE + Deletes a given Farm object. For an automated farm, all the RDS Server VMs are deleted from disk whereas for a manual farm only the RDS Server associations are removed. $farm1 = Get-HVFarm -FarmName 'Farm-01' Remove-HVFarm -Farm $farm1 @@ -3866,12 +3936,15 @@ function Remove-HVPool { Logs off a session forcibly to virtual machine(s). This operation will also log off a locked session. .EXAMPLE + Deletes pool from disk with given parameters PoolName etc. Remove-HVPool -HvServer $hvServer -PoolName 'FullClone' -DeleteFromDisk .EXAMPLE + Deletes specified pool from disk $pool_array | Remove-HVPool -HvServer $hvServer -DeleteFromDisk .EXAMPLE + Deletes specified pool and VM(s) associations are removed from view Manager Remove-HVPool -Pool $pool1 .OUTPUTS @@ -3956,7 +4029,7 @@ function Remove-HVPool { foreach ($item in $poolList) { if ($terminateSession) { #Terminate session - $queryResults = Get-HVQueryResults MachineSummaryView (Get-HVQueryFilter base.desktop -eq $item.id) + $queryResults = Get-HVQueryResult MachineSummaryView (Get-HVQueryFilter base.desktop -eq $item.id) $sessions += $queryResults.base.session if ($null -ne $sessions) { $session_service_helper = New-Object VMware.Hv.SessionService @@ -4021,18 +4094,23 @@ function Set-HVFarm { Reference to Horizon View Server to query the data from. If the value is not passed or null then first element from global:DefaultHVServers would be considered inplace of hvServer. .EXAMPLE - Set-HVFarm -FarmName 'Farm-o1' -Spec 'C:\Edit-HVFarm\ManualEditFarm.json' + Updates farm configuration by using json file + Set-HVFarm -FarmName 'Farm-01' -Spec 'C:\Edit-HVFarm\ManualEditFarm.json' .EXAMPLE - Set-HVFarm -FarmName 'Farm-o1' -Key 'base.description' -Value 'updated description' + Updates farm configuration with given parameters key and value + Set-HVFarm -FarmName 'Farm-01' -Key 'base.description' -Value 'updated description' .EXAMPLE + Updates farm(s) configuration with given parameters key and value $farm_array | Set-HVFarm -Key 'base.description' -Value 'updated description' .EXAMPLE + Enables provisioning to specified farm Set-HVFarm -farm 'Farm2' -Start .EXAMPLE + Enables specified farm Set-HVFarm -farm 'Farm2' -Enable .OUTPUTS @@ -4218,21 +4296,27 @@ function Set-HVPool { Path of the JSON specification file containing key/value pair. .EXAMPLE + Updates pool configuration by using json file Set-HVPool -PoolName 'ManualPool' -Spec 'C:\Edit-HVPool\EditPool.json' .EXAMPLE + Updates pool configuration with given parameters key and value Set-HVPool -PoolName 'RDSPool' -Key 'base.description' -Value 'update description' .Example + Disables specified pool Set-HVPool -PoolName 'LnkClone' -Disable .Example + Enables specified pool Set-HVPool -PoolName 'LnkClone' -Enable .Example + Enables provisioning to specified pool Set-HVPool -PoolName 'LnkClone' -Start .Example + Disables provisioning to specified pool Set-HVPool -PoolName 'LnkClone' -Stop .OUTPUTS @@ -4422,9 +4506,11 @@ function Start-HVFarm { Reference to Horizon View Server to query the data from. If the value is not passed or null then first element from global:DefaultHVServers would be considered inplace of hvServer. .EXAMPLE + Requests a recompose of RDS Servers in the specified automated farm Start-HVFarm -Recompose -Farm 'Farm-01' -LogoffSetting FORCE_LOGOFF -ParentVM 'View-Agent-Win8' -SnapshotVM 'Snap_USB' .EXAMPLE + Requests a recompose task for automated farm in specified time $myTime = Get-Date '10/03/2016 12:30:00' Start-HVFarm -Farm 'Farm-01' -Recompose -LogoffSetting 'FORCE_LOGOFF' -ParentVM 'ParentVM' -SnapshotVM 'SnapshotVM' -StartTime $myTime @@ -4692,19 +4778,24 @@ function Start-HVPool { View API service object of Connect-HVServer cmdlet. .EXAMPLE + Requests a recompose of machines in the specified pool Start-HVPool -Recompose -Pool 'LCPool3' -LogoffSetting FORCE_LOGOFF -ParentVM 'View-Agent-Win8' -SnapshotVM 'Snap_USB' .EXAMPLE + Requests a refresh of machines in the specified pool Start-HVPool -Refresh -Pool 'LCPool3' -LogoffSetting FORCE_LOGOFF .EXAMPLE + Requests a rebalance of machines in a pool with specified time $myTime = Get-Date '10/03/2016 12:30:00' Start-HVPool -Rebalance -Pool 'LCPool3' -LogoffSetting FORCE_LOGOFF -StartTime $myTime .EXAMPLE + Requests an update of push image operation on the specified Instant Clone Engine sourced pool Start-HVPool -SchedulePushImage -Pool 'InstantPool' -LogoffSetting FORCE_LOGOFF -ParentVM 'InsParentVM' -SnapshotVM 'InsSnapshotVM' .EXAMPLE + Requests a cancellation of the current scheduled push image operation on the specified Instant Clone Engine sourced pool Start-HVPool -CancelPushImage -Pool 'InstantPool' .OUTPUTS @@ -5130,16 +5221,20 @@ function Get-HVMachine { first element from global:DefaultHVServers would be considered inplace of hvServer .EXAMPLE + Queries VM(s) with given parameter poolName Get-HVDesktop -PoolName 'ManualPool' .EXAMPLE + Queries VM(s) with given parameter machineName Get-HVDesktop -MachineName 'PowerCLIVM' .EXAMPLE + Queries VM(s) with given parameter vm state Get-HVDesktop -State CUSTOMIZING .EXAMPLE - Get-HVDesktop -DnsName 'powercli-*' -State CUSTOMIZING + Queries VM(s) with given parameter dnsName with wildcard character * + Get-HVDesktop -DnsName 'powercli-*' .OUTPUTS Returns list of objects of type MachineInfo @@ -5247,16 +5342,20 @@ function Get-HVMachineSummary { first element from global:DefaultHVServers would be considered inplace of hvServer .EXAMPLE + Queries VM(s) with given parameter poolName Get-HVDesktopSummary -PoolName 'ManualPool' .EXAMPLE + Queries VM(s) with given parameter machineName Get-HVDesktopSummary -MachineName 'PowerCLIVM' .EXAMPLE + Queries VM(s) with given parameter vm state Get-HVDesktopSummary -State CUSTOMIZING .EXAMPLE - Get-HVDesktopSummary -DnsName 'powercli-*' -State CUSTOMIZING + Queries VM(s) with given parameter dnsName with wildcard character * + Get-HVDesktopSummary -DnsName 'powercli-*' .OUTPUTS Returns list of objects of type MachineNamesView @@ -5574,7 +5673,7 @@ function Get-HVInternalName { first element from global:DefaultHVServers would be considered inplace of hvServer .EXAMPLE - Decodes and returns human readable name + Decodes Horizon API Id and returns human readable name Get-HVInternalName -EntityId $entityId .OUTPUTS From 0c5518d439e57dbc6cb89e8427779f87d2ab0015 Mon Sep 17 00:00:00 2001 From: praveenmathamsetty Date: Sun, 8 Jan 2017 21:33:47 +0530 Subject: [PATCH 003/112] All parameters should be configured through JSON 1. Create JSON files that will help in creation of new pools based on existing pools configuration. 2. Configure all the pool attributes through JSON file. 3. Validation of the attributes passed in the JSON file. --- .../Farm}/AutomatedLinkedCloneFarm.json | 44 +- .../{New-HVFarm => Json/Farm}/ManualFarm.json | 22 +- .../{New-HVPool => Json/Pool}/FullClone.json | 44 +- .../Pool}/InstantClone.json | 39 +- .../Pool}/LinkedClone.json | 69 +- .../{New-HVPool => Json/Pool}/ManualSpec.json | 42 +- .../VMware.Hv.Helper/Json/Pool/RdsSpec.json | 27 + .../VMware.Hv.Helper/New-HVPool/RdsSpec.json | 16 - .../VMware.Hv.Helper/VMware.HV.Helper.psm1 | 1269 +++++++++++++++-- 9 files changed, 1382 insertions(+), 190 deletions(-) rename Modules/VMware.Hv.Helper/{New-HVFarm => Json/Farm}/AutomatedLinkedCloneFarm.json (84%) rename Modules/VMware.Hv.Helper/{New-HVFarm => Json/Farm}/ManualFarm.json (51%) rename Modules/VMware.Hv.Helper/{New-HVPool => Json/Pool}/FullClone.json (82%) rename Modules/VMware.Hv.Helper/{New-HVPool => Json/Pool}/InstantClone.json (88%) rename Modules/VMware.Hv.Helper/{New-HVPool => Json/Pool}/LinkedClone.json (80%) rename Modules/VMware.Hv.Helper/{New-HVPool => Json/Pool}/ManualSpec.json (54%) create mode 100644 Modules/VMware.Hv.Helper/Json/Pool/RdsSpec.json delete mode 100644 Modules/VMware.Hv.Helper/New-HVPool/RdsSpec.json diff --git a/Modules/VMware.Hv.Helper/New-HVFarm/AutomatedLinkedCloneFarm.json b/Modules/VMware.Hv.Helper/Json/Farm/AutomatedLinkedCloneFarm.json similarity index 84% rename from Modules/VMware.Hv.Helper/New-HVFarm/AutomatedLinkedCloneFarm.json rename to Modules/VMware.Hv.Helper/Json/Farm/AutomatedLinkedCloneFarm.json index 3706374..32c3f9a 100644 --- a/Modules/VMware.Hv.Helper/New-HVFarm/AutomatedLinkedCloneFarm.json +++ b/Modules/VMware.Hv.Helper/Json/Farm/AutomatedLinkedCloneFarm.json @@ -1,17 +1,31 @@ { "Type": "AUTOMATED", "Data": { - "Name": "LCFarmTest", - "DisplayName": "Ankit LC Farm Test", + "Name": "LCFarmJson", + "DisplayName": "FarmJsonTest", "AccessGroup": "Root", - "Description": "created LC Farm from PS", + "Description": "created LC Farm from PS via JSON", "Enabled": null, "Deleting": false, - "Settings": null, + "Settings": { + "DisconnectedSessionTimeoutPolicy" : "NEVER", + "DisconnectedSessionTimeoutMinutes" : 1, + "EmptySessionTimeoutPolicy" : "AFTER", + "EmptySessionTimeoutMinutes" : 1, + "LogoffAfterTimeout" : false + }, "Desktop": null, - "DisplayProtocolSettings": null, + "DisplayProtocolSettings": { + "DefaultDisplayProtocol" : "PCOIP", + "AllowDisplayProtocolOverride" : false, + "EnableHTMLAccess" : false + }, "ServerErrorThreshold": null, - "MirageConfigurationOverrides": null + "MirageConfigurationOverrides": { + "OverrideGlobalSetting" : false, + "Enabled" : false, + "Url" : null + } }, "AutomatedFarmSpec": { "ProvisioningType": "VIEW_COMPOSER", @@ -19,7 +33,7 @@ "RdsServerNamingSpec": { "NamingMethod": "PATTERN", "PatternNamingSettings": { - "NamingPattern": "LCFarmVM_PS", + "NamingPattern": "LCFarmVMPS", "MaxNumberOfRDSServers": 1 } }, @@ -28,17 +42,17 @@ "StopProvisioningOnError": true, "MinReadyVMsOnVComposerMaintenance": 0, "VirtualCenterProvisioningData": { - "ParentVm": "Win_Server_2012_R2", - "Snapshot": "Snap_RDS", + "ParentVm": "RDSServer", + "Snapshot": "RDS_SNAP1", "Datacenter": null, - "VmFolder": "AnkitPoolVM", - "HostOrCluster": "cls", - "ResourcePool": "cls" + "VmFolder": "Praveen", + "HostOrCluster": "CS-1", + "ResourcePool": "CS-1" }, "VirtualCenterStorageSettings": { "Datastores": [ { - "Datastore": "datastore1 (5)", + "Datastore": "Datastore1", "StorageOvercommit": "UNBOUNDED" } ], @@ -67,7 +81,7 @@ "AdContainer": "CN=Computers", "ReusePreExistingAccounts": false, "SysprepCustomizationSettings": { - "CustomizationSpec": "RDSH_Cust2" + "CustomizationSpec": "PraveenCust" } }, "RdsServerMaxSessionsData": { @@ -76,5 +90,5 @@ } }, "ManualFarmSpec": null, - "NetBiosName" : "adankit" + "NetBiosName" : "adviewdev" } diff --git a/Modules/VMware.Hv.Helper/New-HVFarm/ManualFarm.json b/Modules/VMware.Hv.Helper/Json/Farm/ManualFarm.json similarity index 51% rename from Modules/VMware.Hv.Helper/New-HVFarm/ManualFarm.json rename to Modules/VMware.Hv.Helper/Json/Farm/ManualFarm.json index cf674c1..3dd1678 100644 --- a/Modules/VMware.Hv.Helper/New-HVFarm/ManualFarm.json +++ b/Modules/VMware.Hv.Helper/Json/Farm/ManualFarm.json @@ -7,17 +7,31 @@ "Description": "Manual PS Test", "Enabled": null, "Deleting": false, - "Settings": null, + "Settings": { + "DisconnectedSessionTimeoutPolicy" : "NEVER", + "DisconnectedSessionTimeoutMinutes" : 1, + "EmptySessionTimeoutPolicy" : "AFTER", + "EmptySessionTimeoutMinutes" : 1, + "LogoffAfterTimeout" : false + }, "Desktop": null, - "DisplayProtocolSettings": null, + "DisplayProtocolSettings": { + "DefaultDisplayProtocol" : "PCOIP", + "AllowDisplayProtocolOverride" : false, + "EnableHTMLAccess" : false + }, "ServerErrorThreshold": null, - "MirageConfigurationOverrides": null + "MirageConfigurationOverrides": { + "OverrideGlobalSetting" : false, + "Enabled" : false, + "Url" : null + } }, "AutomatedFarmSpec": null, "ManualFarmSpec": { "RdsServers": [ { - "rdsServer": "WIN-ORKA1Q8B0P7" + "rdsServer": "RDSServer.adviewdev.eng.vmware.com" } ] } diff --git a/Modules/VMware.Hv.Helper/New-HVPool/FullClone.json b/Modules/VMware.Hv.Helper/Json/Pool/FullClone.json similarity index 82% rename from Modules/VMware.Hv.Helper/New-HVPool/FullClone.json rename to Modules/VMware.Hv.Helper/Json/Pool/FullClone.json index 2ae9539..e6d7fae 100644 --- a/Modules/VMware.Hv.Helper/New-HVPool/FullClone.json +++ b/Modules/VMware.Hv.Helper/Json/Pool/FullClone.json @@ -5,7 +5,44 @@ "AccessGroup": "Root", "Description": "create full clone via JSON" }, - "DesktopSettings": null, + "DesktopSettings": { + "enabled": true, + "deleting": false, + "connectionServerRestrictions": null, + "logoffSettings": { + "powerPolicy": "TAKE_NO_POWER_ACTION", + "automaticLogoffPolicy": "NEVER", + "automaticLogoffMinutes": 120, + "allowUsersToResetMachines": false, + "allowMultipleSessionsPerUser": false, + "deleteOrRefreshMachineAfterLogoff": "NEVER", + "refreshOsDiskAfterLogoff": "NEVER", + "refreshPeriodDaysForReplicaOsDisk": 5, + "refreshThresholdPercentageForReplicaOsDisk": 10 + }, + "displayProtocolSettings": { + "supportedDisplayProtocols": ["PCOIP", "BLAST" ], + "defaultDisplayProtocol": "BLAST", + "allowUsersToChooseProtocol": true, + "pcoipDisplaySettings": { + "renderer3D": "DISABLED", + "enableGRIDvGPUs": false, + "vRamSizeMB": 96, + "maxNumberOfMonitors": 3, + "maxResolutionOfAnyOneMonitor": "WSXGA_PLUS" + }, + "enableHTMLAccess": true + }, + "flashSettings": { + "quality": "NO_CONTROL", + "throttling": "DISABLED" + }, + "mirageConfigurationOverrides": { + "overrideGlobalSetting": false, + "enabled": false, + "url": false + } + }, "Type": "AUTOMATED", "AutomatedDesktopSpec": { "ProvisioningType": "VIRTUAL_CENTER", @@ -69,7 +106,7 @@ "NoCustomizationSettings": { "DoNotPowerOnVMsAfterCreation": false }, - "SysprepCustomizationSettings": null, + "SysprepCustomizationSettings": {"customizationSpec" : "praveencust"}, "QuickprepCustomizationSettings": null, "CloneprepCustomizationSettings": null } @@ -77,6 +114,5 @@ "ManualDesktopSpec": null, "RdsDesktopSpec": null, "GlobalEntitlementData": null, - "NetBiosName" : "adviewdev", - "SysPrepName" : "praveencust" + "NetBiosName" : "adviewdev" } diff --git a/Modules/VMware.Hv.Helper/New-HVPool/InstantClone.json b/Modules/VMware.Hv.Helper/Json/Pool/InstantClone.json similarity index 88% rename from Modules/VMware.Hv.Helper/New-HVPool/InstantClone.json rename to Modules/VMware.Hv.Helper/Json/Pool/InstantClone.json index a8da482..4c3c584 100644 --- a/Modules/VMware.Hv.Helper/New-HVPool/InstantClone.json +++ b/Modules/VMware.Hv.Helper/Json/Pool/InstantClone.json @@ -5,7 +5,44 @@ "AccessGroup": "ROOT", "Description": "create instant pool" }, - "DesktopSettings": null, + "DesktopSettings": { + "enabled": true, + "deleting": false, + "connectionServerRestrictions": null, + "logoffSettings": { + "powerPolicy": "ALWAYS_POWERED_ON", + "automaticLogoffPolicy": "NEVER", + "automaticLogoffMinutes": 120, + "allowUsersToResetMachines": false, + "allowMultipleSessionsPerUser": false, + "deleteOrRefreshMachineAfterLogoff": "DELETE", + "refreshOsDiskAfterLogoff": "NEVER", + "refreshPeriodDaysForReplicaOsDisk": 5, + "refreshThresholdPercentageForReplicaOsDisk": 10 + }, + "displayProtocolSettings": { + "supportedDisplayProtocols": ["PCOIP", "BLAST" ], + "defaultDisplayProtocol": "BLAST", + "allowUsersToChooseProtocol": true, + "pcoipDisplaySettings": { + "renderer3D": "DISABLED", + "enableGRIDvGPUs": false, + "vRamSizeMB": 96, + "maxNumberOfMonitors": 3, + "maxResolutionOfAnyOneMonitor": "WSXGA_PLUS" + }, + "enableHTMLAccess": true + }, + "flashSettings": { + "quality": "NO_CONTROL", + "throttling": "DISABLED" + }, + "mirageConfigurationOverrides": { + "overrideGlobalSetting": false, + "enabled": false, + "url": false + } + }, "Type": "AUTOMATED", "AutomatedDesktopSpec": { "ProvisioningType": "INSTANT_CLONE_ENGINE", diff --git a/Modules/VMware.Hv.Helper/New-HVPool/LinkedClone.json b/Modules/VMware.Hv.Helper/Json/Pool/LinkedClone.json similarity index 80% rename from Modules/VMware.Hv.Helper/New-HVPool/LinkedClone.json rename to Modules/VMware.Hv.Helper/Json/Pool/LinkedClone.json index 53171a6..ee3dfac 100644 --- a/Modules/VMware.Hv.Helper/New-HVPool/LinkedClone.json +++ b/Modules/VMware.Hv.Helper/Json/Pool/LinkedClone.json @@ -5,7 +5,44 @@ "AccessGroup": "Root", "Description": "created linkedclone pool from ps" }, - "DesktopSettings": null, + "DesktopSettings": { + "enabled": true, + "deleting": false, + "connectionServerRestrictions": null, + "logoffSettings": { + "powerPolicy": "TAKE_NO_POWER_ACTION", + "automaticLogoffPolicy": "NEVER", + "automaticLogoffMinutes": 120, + "allowUsersToResetMachines": false, + "allowMultipleSessionsPerUser": false, + "deleteOrRefreshMachineAfterLogoff": "NEVER", + "refreshOsDiskAfterLogoff": "NEVER", + "refreshPeriodDaysForReplicaOsDisk": 5, + "refreshThresholdPercentageForReplicaOsDisk": 10 + }, + "displayProtocolSettings": { + "supportedDisplayProtocols": ["RDP","PCOIP", "BLAST" ], + "defaultDisplayProtocol": "PCOIP", + "allowUsersToChooseProtocol": true, + "pcoipDisplaySettings": { + "renderer3D": "DISABLED", + "enableGRIDvGPUs": false, + "vRamSizeMB": 96, + "maxNumberOfMonitors": 3, + "maxResolutionOfAnyOneMonitor": "WSXGA_PLUS" + }, + "enableHTMLAccess": true + }, + "flashSettings": { + "quality": "NO_CONTROL", + "throttling": "DISABLED" + }, + "mirageConfigurationOverrides": { + "overrideGlobalSetting": false, + "enabled": false, + "url": null + } + }, "Type": "AUTOMATED", "AutomatedDesktopSpec": { "ProvisioningType": "VIEW_COMPOSER", @@ -33,7 +70,7 @@ "Template": null, "ParentVm": "Agent_pra", "Snapshot": "kb-hotfix", - "Datacenter": null, + "Datacenter": "Dev-Dc", "VmFolder": "Praveen", "HostOrCluster": "CS-1", "ResourcePool": "CS-1" @@ -52,7 +89,8 @@ "UseNativeSnapshots": false, "SpaceReclamationSettings": { "ReclaimVmDiskSpace": false, - "ReclamationThresholdGB": null + "ReclamationThresholdGB": null, + "BlackoutTimes" : null }, "PersistentDiskSettings": { "RedirectWindowsProfile": false, @@ -75,19 +113,31 @@ } }, "VirtualCenterNetworkingSettings": { - "Nics": null + "Nics": [ + { + "Nic": "nicName", + "NetworkLabelAssignmentSpecs": [ + { + "Enabled" : false, + "networkLabel" : null, + "maxLabelType" : null, + "maxLabel" : null + } + ] + } + ] } }, "VirtualCenterManagedCommonSettings": { "TransparentPageSharingScope": "VM" }, "CustomizationSettings": { - "CustomizationType": "QUICK_PREP", - "DomainAdministrator": null, + "CustomizationType": "SYS_PREP", + "DomainAdministrator": "administrator", "AdContainer": "CN=Computers", "ReusePreExistingAccounts": false, "NoCustomizationSettings": null, - "SysprepCustomizationSettings": null, + "SysprepCustomizationSettings": {"customizationSpec" : "praveencust"}, "QuickprepCustomizationSettings": { "PowerOffScriptName": null, "PowerOffScriptParameters": null, @@ -99,7 +149,6 @@ }, "ManualDesktopSpec": null, "RdsDesktopSpec": null, - "GlobalEntitlementData": null, - "NetBiosName" : "adviewdev", - "SysPrepName" : "praveencust" + "GlobalEntitlementData": null, + "NetBiosName" : "adviewdev" } diff --git a/Modules/VMware.Hv.Helper/New-HVPool/ManualSpec.json b/Modules/VMware.Hv.Helper/Json/Pool/ManualSpec.json similarity index 54% rename from Modules/VMware.Hv.Helper/New-HVPool/ManualSpec.json rename to Modules/VMware.Hv.Helper/Json/Pool/ManualSpec.json index b302bba..8b95389 100644 --- a/Modules/VMware.Hv.Helper/New-HVPool/ManualSpec.json +++ b/Modules/VMware.Hv.Helper/Json/Pool/ManualSpec.json @@ -5,7 +5,44 @@ "AccessGroup": "ROOT", "Description": "Manual pool creation" }, - "DesktopSettings": null, + "DesktopSettings": { + "enabled": true, + "deleting": false, + "connectionServerRestrictions": null, + "logoffSettings": { + "powerPolicy": "TAKE_NO_POWER_ACTION", + "automaticLogoffPolicy": "NEVER", + "automaticLogoffMinutes": 120, + "allowUsersToResetMachines": false, + "allowMultipleSessionsPerUser": false, + "deleteOrRefreshMachineAfterLogoff": "NEVER", + "refreshOsDiskAfterLogoff": "NEVER", + "refreshPeriodDaysForReplicaOsDisk": 5, + "refreshThresholdPercentageForReplicaOsDisk": 10 + }, + "displayProtocolSettings": { + "supportedDisplayProtocols": ["PCOIP", "BLAST" ], + "defaultDisplayProtocol": "BLAST", + "allowUsersToChooseProtocol": true, + "pcoipDisplaySettings": { + "renderer3D": "DISABLED", + "enableGRIDvGPUs": false, + "vRamSizeMB": 96, + "maxNumberOfMonitors": 3, + "maxResolutionOfAnyOneMonitor": "WSXGA_PLUS" + }, + "enableHTMLAccess": true + }, + "flashSettings": { + "quality": "NO_CONTROL", + "throttling": "DISABLED" + }, + "mirageConfigurationOverrides": { + "overrideGlobalSetting": false, + "enabled": false, + "url": false + } + }, "Type": "MANUAL", "AutomatedDesktopSpec": null, "ManualDesktopSpec": { @@ -16,7 +53,7 @@ "Source": "VIRTUAL_CENTER", "Machines": [ { - "Machine" : "PowerCLI-VM" + "Machine" : "Praveen_Agent" } ], "VirtualCenter": null, @@ -32,4 +69,5 @@ }, "RdsDesktopSpec": null, "GlobalEntitlementData": null + } diff --git a/Modules/VMware.Hv.Helper/Json/Pool/RdsSpec.json b/Modules/VMware.Hv.Helper/Json/Pool/RdsSpec.json new file mode 100644 index 0000000..bab0c67 --- /dev/null +++ b/Modules/VMware.Hv.Helper/Json/Pool/RdsSpec.json @@ -0,0 +1,27 @@ +{ + "Base": { + "Name" : "RdsJson", + "DisplayName": "TestRDSPS", + "AccessGroup": "Root", + "Description": "Testing PS" + }, + "DesktopSettings": { + "enabled": true, + "deleting": false, + "connectionServerRestrictions": null, + "logoffSettings": null, + "displayProtocolSettings": null, + "flashSettings": { + "quality": "NO_CONTROL", + "throttling": "DISABLED" + }, + "mirageConfigurationOverrides": null + }, + "Type": "RDS", + "AutomatedDesktopSpec": null, + "ManualDesktopSpec": null, + "RdsDesktopSpec": { + "Farm": "test1" + }, + "GlobalEntitlementData": null +} diff --git a/Modules/VMware.Hv.Helper/New-HVPool/RdsSpec.json b/Modules/VMware.Hv.Helper/New-HVPool/RdsSpec.json deleted file mode 100644 index 86b3571..0000000 --- a/Modules/VMware.Hv.Helper/New-HVPool/RdsSpec.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "Base": { - "Name" : "RdsJson", - "DisplayName": "TestRDSPS", - "AccessGroup": "Root", - "Description": "Testing PS" - }, - "DesktopSettings": null, - "Type": "RDS", - "AutomatedDesktopSpec": null, - "ManualDesktopSpec": null, - "RdsDesktopSpec": { - "Farm": "Farm2" - }, - "GlobalEntitlementData": null -} diff --git a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 index e1ff3d6..2a0967b 100644 --- a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 +++ b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 @@ -294,7 +294,7 @@ The Add-HVDesktop adds virtual machines to already exiting pools by using view A return } } - if ($pscmdlet.ShouldProcess($machineList)) { + if ($pscmdlet.ShouldProcess($machines)) { $desktop_service_helper.Desktop_AddMachinesToManualDesktop($services,$id,$machineList) } return $machineList @@ -412,7 +412,7 @@ function Add-HVRDSServer { try { $farmSpecObj = Get-HVFarmSummary -farmName $farmName -hvServer $hvServer } catch { - Write-Error "Make sure Get-HVFarm advanced function is loaded, $_" + Write-Error "Make sure Get-HVFarmSummary advanced function is loaded, $_" break } if ($farmSpecObj) { @@ -432,7 +432,7 @@ function Add-HVRDSServer { 'MANUAL' { try { $serverList = Get-RegisteredRDSServer -services $services -serverList $rdsServers - if ($pscmdlet.ShouldProcess($serverList)) { + if ($pscmdlet.ShouldProcess($rdsServers)) { $farm_service_helper.Farm_AddRDSServers($services, $id, $serverList) } return $serverList @@ -985,8 +985,8 @@ function Get-HVFarm { } $farmList = Find-HVFarm -Param $PSBoundParameters if (! $farmList) { - Write-Host "No farm Found with given search parameters" - breakss + Write-Host "Get-HVFarm: No Farm Found with given search parameters" + break } $farm_service_helper = New-Object VMware.Hv.FarmService $queryResults = @() @@ -1089,12 +1089,7 @@ function Get-HVFarmSummary { Write-Error "Could not retrieve ViewApi services from connection object" break } - $farmList = Find-HVFarm -Param $PSBoundParameters - if (! $farmList) { - Write-Host "No farm Found with given search parameters" - break - } - return $farmList + Return Find-HVFarm -Param $PSBoundParameters } function Find-HVFarm { @@ -1282,7 +1277,7 @@ function Get-HVPool { } $poolList = Find-HVPool -Param $PSBoundParameters if (! $poolList) { - Write-Host "No Pool Found with given search parameters" + Write-Host "Get-HVPool: No Pool Found with given search parameters" break } $queryResults = @() @@ -1410,12 +1405,7 @@ function Get-HVPoolSummary { Write-Error "Could not retrieve ViewApi services from connection object" break } - $poolList = Find-HVPool -Param $psboundparameters - if (!$poolList) { - Write-Host "No Pool Found with given search parameters" - break - } - Return $poolList + Return Find-HVPool -Param $psboundparameters } function Find-HVPool { @@ -1798,6 +1788,7 @@ function Get-HVQueryResult { } } + function New-HVFarm { <# .Synopsis @@ -1984,6 +1975,71 @@ function New-HVFarm { [boolean] $Enable = $true, + #farmSpec.data.settings.disconnectedSessionTimeoutPolicy + [Parameter(Mandatory = $false)] + [ValidateSet("IMMEDIATE","NEVER","AFTER")] + [string] + $DisconnectedSessionTimeoutPolicy = "NEVER", + + #farmSpec.data.settings.disconnectedSessionTimeoutMinutes + [Parameter(Mandatory = $false)] + [ValidateRange(1,[Int]::MaxValue)] + [int] + $DisconnectedSessionTimeoutMinutes, + + #farmSpec.data.settings.emptySessionTimeoutPolicy + [Parameter(Mandatory = $false)] + [ValidateSet("NEVER","AFTER")] + [string] + $EmptySessionTimeoutPolicy = "AFTER", + + #farmSpec.data.settings.emptySessionTimeoutMinutes + [Parameter(Mandatory = $false)] + [ValidateSet(1,[Int]::MaxValue)] + [int] + $EmptySessionTimeoutMinutes = 1, + + #farmSpec.data.settings.logoffAfterTimeout + [Parameter(Mandatory = $false)] + [boolean] + $LogoffAfterTimeout = $false, + + #farmSpec.data.displayProtocolSettings.defaultDisplayProtocol + [Parameter(Mandatory = $false)] + [ValidateSet("RDP","PCOIP","BLAST")] + [string] + $DefaultDisplayProtocol = "PCOIP", + + #farmSpec.data.displayProtocolSettings.allowDisplayProtocolOverride + [Parameter(Mandatory = $false)] + [boolean] + $AllowDisplayProtocolOverride = $true, + + #farmSpec.data.displayProtocolSettings.enableHTMLAccess + [Parameter(Mandatory = $false)] + [boolean] + $EnableHTMLAccess = $false, + + #farmSpec.data.serverErrorThreshold + [Parameter(Mandatory = $false)] + [ValidateRange(0,[Int]::MaxValue)] + $ServerErrorThreshold = 0, + + #farmSpec.data.mirageConfigurationOverrides.overrideGlobalSetting + [Parameter(Mandatory = $false)] + [boolean] + $OverrideGlobalSetting = $false, + + #farmSpec.data.mirageConfigurationOverrides.enabled + [Parameter(Mandatory = $false)] + [boolean] + $MirageServerEnabled, + + #farmSpec.data.mirageConfigurationOverrides.url + [Parameter(Mandatory = $false)] + [string] + $Url, + #farmSpec.automatedfarmSpec.virtualCenter if LINKED_CLONE [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] [string] @@ -2014,15 +2070,25 @@ function New-HVFarm { [string] $ResourcePool, + #farmSpec.automatedfarmSpec.virtualCenterProvisioningSettings.virtualCenterProvisioningData.dataCenter if LINKED_CLONE, INSTANT_CLONE, FULL_CLONE + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [string] + $dataCenter, + #farmSpec.automatedfarmSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.datastore if LINKED_CLONE [Parameter(Mandatory = $true,ParameterSetName = "LINKED_CLONE")] [string[]] $Datastores, + #farmSpec.automatedfarmSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.datastores.storageOvercommit if LINKED_CLONE + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [string[]] + $StorageOvercommit = $null, + #farmSpec.automatedfarmSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.useVSAN if LINKED_CLONE [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] - [string] - $UseVSAN, + [boolean] + $UseVSAN = $false, #farmSpec.automatedfarmSpec.virtualCenterProvisioningSettings.enableProvsioning if LINKED_CLONE [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] @@ -2059,6 +2125,37 @@ function New-HVFarm { [int] $MaximumCount = 1, + #farmSpec.automatedfarmSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.viewComposerStorageSettings.useSeparateDatastoresReplicaAndOSDisks + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [boolean] + $UseSeparateDatastoresReplicaAndOSDisks = $false, + + #farmSpec.automatedfarmSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.viewComposerStorageSettings.replicaDiskDatastore + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [string] + $ReplicaDiskDatastore, + + #farmSpec.automatedfarmSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.viewComposerStorageSettings.useNativeSnapshots + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [boolean] + $UseNativeSnapshots = $false, + + #farmSpec.automatedfarmSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.viewComposerStorageSettings.spaceReclamationSettings.reclaimVmDiskSpace + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [boolean] + $ReclaimVmDiskSpace = $false, + + #farmSpec.automatedfarmSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.viewComposerStorageSettings.spaceReclamationSettings.reclamationThresholdGB + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [ValidateRange(0,[Int]::MaxValue)] + [int] + $ReclamationThresholdGB = 1, + + #farmSpec.automatedfarmSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.viewComposerStorageSettings.spaceReclamationSettings.blackoutTimes + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [VMware.Hv.FarmBlackoutTime[]] + $BlackoutTimes, + #farmSpec.automatedfarmSpec.customizationSettings.adContainer if LINKED_CLONE [Parameter(Mandatory = $false,ParameterSetName = 'LINKED_CLONE')] [string] @@ -2074,12 +2171,29 @@ function New-HVFarm { [string] $DomainAdmin = $null, + #farmSpec.automatedfarmSpec.customizationSettings.reusePreExistingAccounts + [Parameter(Mandatory = $false,ParameterSetName = 'LINKED_CLONE')] + [Boolean] + $ReusePreExistingAccounts = $false, + #farmSpec.automatedfarmSpec.customizationSettings.sysprepCustomizationSettings.customizationSpec if LINKED_CLONE [Parameter(Mandatory = $true,ParameterSetName = "LINKED_CLONE")] [string] $SysPrepName, - ##farmSpec.manualfarmSpec.rdsServers + #farmSpec.automatedfarmSpec.rdsServerMaxSessionsData.maxSessionsType if LINKED_CLONE + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [ValidateSet("UNLIMITED", "LIMITED")] + [string] + $MaxSessionsType = "UNLIMITED", + + #farmSpec.automatedfarmSpec.rdsServerMaxSessionsData.maxSessionsType if LINKED_CLONE + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [ValidateRange(1, [int]::MaxValue)] + [int] + $MaxSessions, + + #farmSpec.manualfarmSpec.rdsServers [Parameter(Mandatory = $true,ParameterSetName = 'MANUAL')] [string[]] $RdsServers, @@ -2127,9 +2241,9 @@ function New-HVFarm { if ($farmName) { try { - $sourceFarm = Get-HVFarm -farmName $farmName -hvServer $hvServer + $sourceFarm = Get-HVFarmSummary -farmName $farmName -hvServer $hvServer } catch { - Write-Error "Make sure Get-HVFarm advanced function is loaded, $_" + Write-Error "Make sure Get-HVFarmSummary advanced function is loaded, $_" break } if ($sourceFarm) { @@ -2145,18 +2259,33 @@ function New-HVFarm { Write-Error "Json file exception, $_" break } + try { + Test-HVFarmSpec -JsonObject $jsonObject + } catch { + Write-Error "Json object validation failed, $_" + break + } if ($jsonObject.type -eq 'AUTOMATED') { $farmType = 'AUTOMATED' + $provisioningType = $jsonObject.ProvisioningType if ($null -ne $jsonObject.AutomatedFarmSpec.VirtualCenter) { $vCenter = $jsonObject.AutomatedFarmSpec.VirtualCenter } $linkedClone = $true $netBiosName = $jsonObject.NetBiosName + if ($null -ne $jsonObject.AutomatedFarmSpec.CustomizationSettings.DomainAdministrator) { + $domainAdministrator = $jsonObject.AutomatedFarmSpec.CustomizationSettings.DomainAdministrator + } $adContainer = $jsonObject.AutomatedFarmSpec.CustomizationSettings.AdContainer + $reusePreExistingAccounts = $jsonObject.AutomatedFarmSpec.CustomizationSettings.ReusePreExistingAccounts + $sysPrepName = $jsonObject.AutomatedFarmSpec.CustomizationSettings.SysprepCustomizationSettings.CustomizationSpec $namingMethod = $jsonObject.AutomatedFarmSpec.RdsServerNamingSpec.NamingMethod $namingPattern = $jsonObject.AutomatedFarmSpec.RdsServerNamingSpec.patternNamingSettings.namingPattern $maximumCount = $jsonObject.AutomatedFarmSpec.RdsServerNamingSpec.patternNamingSettings.maxNumberOfRDSServers + $enableProvisioning = $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.EnableProvisioning + $stopProvisioningOnError = $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.StopProvisioningOnError + $minReady = $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.MinReadyVMsOnVComposerMaintenance $transparentPageSharingScope = $jsonObject.AutomatedFarmSpec.virtualCenterManagedCommonSettings.TransparentPageSharingScope @@ -2170,10 +2299,36 @@ function New-HVFarm { $hostOrCluster = $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.VirtualCenterProvisioningData.HostOrCluster $resourcePool = $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.VirtualCenterProvisioningData.ResourcePool $dataStoreList = $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.VirtualCenterStorageSettings.Datastores + foreach ($dtStore in $dataStoreList) { $datastores += $dtStore.Datastore + $storageOvercommit += $dtStore.StorageOvercommit } - $sysPrepName = $jsonObject.AutomatedFarmSpec.CustomizationSettings.SysprepCustomizationSettings.CustomizationSpec + $useVSan = $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.virtualCenterStorageSettings.UseVSan + $useSeparateDatastoresReplicaAndOSDisks = $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.virtualCenterStorageSettings.ViewComposerStorageSettings.UseSeparateDatastoresReplicaAndOSDisks + if ($useSeparateDatastoresReplicaAndOSDisks) { + $replicaDiskDatastore = $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.virtualCenterStorageSettings.ViewComposerStorageSettings.ReplicaDiskDatastore + } + $useNativeSnapshots = $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.virtualCenterStorageSettings.ViewComposerStorageSettings.UseNativeSnapshots + $reclaimVmDiskSpace = $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.virtualCenterStorageSettings.ViewComposerStorageSettings.SpaceReclamationSettings.ReclaimVmDiskSpace + if ($reclaimVmDiskSpace) { + $ReclamationThresholdGB = $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.virtualCenterStorageSettings.ViewComposerStorageSettings.SpaceReclamationSettings.ReclamationThresholdGB + if ($null -ne $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.virtualCenterStorageSettings.ViewComposerStorageSettings.SpaceReclamationSettings.blackoutTimes) { + $blackoutTimesList = $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.virtualCenterStorageSettings.ViewComposerStorageSettings.SpaceReclamationSettings.blackoutTimes + foreach ($blackout in $blackoutTimesList) { + $blackoutObj = New-Object VMware.Hv.DesktopBlackoutTime + $blackoutObj.Days = $blackout.Days + $blackoutObj.StartTime = $blackout.StartTime + $blackoutObj.EndTime = $blackoutObj.EndTime + $blackoutTimes += $blackoutObj + } + } + } + $maxSessionsType = $jsonObject.AutomatedFarmSpec.RdsServerMaxSessionsData.MaxSessionsType + if ($maxSessionsType -eq "LIMITED") { + $maxSessions = $jsonObject.AutomatedFarmSpec.RdsServerMaxSessionsData.MaxSessions + } + } elseif ($jsonObject.type -eq 'MANUAL') { $manual = $true $farmType = 'MANUAL' @@ -2183,10 +2338,33 @@ function New-HVFarm { $rdsServers += $RdsServerObj.rdsServer } } - $farmDisplayName = $jsonObject.data.DisplayName - $description = $jsonObject.data.Description - $accessGroup = $jsonObject.data.AccessGroup - $farmName = $jsonObject.data.name + $farmDisplayName = $jsonObject.Data.DisplayName + $description = $jsonObject.Data.Description + $accessGroup = $jsonObject.Data.AccessGroup + $farmName = $jsonObject.Data.name + if ($null -ne $jsonObject.Data.Enabled) { + $enable = $jsonObject.Data.Enabled + } + if ($null -ne $jsonObject.Data.Settings) { + $disconnectedSessionTimeoutPolicy = $jsonObject.Data.Settings.DisconnectedSessionTimeoutPolicy + $disconnectedSessionTimeoutMinutes = $jsonObject.Data.Settings.DisconnectedSessionTimeoutMinutes + $emptySessionTimeoutPolicy = $jsonObject.Data.Settings.EmptySessionTimeoutPolicy + $emptySessionTimeoutMinutes = $jsonObject.Data.Settings.EmptySessionTimeoutMinutes + $logoffAfterTimeout = $jsonObject.Data.Settings.LogoffAfterTimeout + } + if ($null -ne $jsonObject.Data.DisplayProtocolSettings) { + $defaultDisplayProtocol = $jsonObject.Data.DisplayProtocolSettings.DefaultDisplayProtocol + $allowDisplayProtocolOverride = $jsonObject.Data.DisplayProtocolSettings.AllowDisplayProtocolOverride + $enableHTMLAccess = $jsonObject.Data.DisplayProtocolSettings.EnableHTMLAccess + } + if ($null -ne $jsonObject.Data.serverErrorThreshold) { + $serverErrorThreshold = $jsonObject.Data.serverErrorThreshold + } + if ($null -ne $jsonObject.Data.MirageConfigurationOverrides) { + $overrideGlobalSetting = $jsonObject.Data.MirageConfigurationOverrides.OverrideGlobalSetting + $mirageserverEnabled = $jsonObject.Data.MirageConfigurationOverrides.Enabled + $url = $jsonObject.Data.MirageConfigurationOverrides.url + } } if ($linkedClone) { @@ -2245,17 +2423,17 @@ function New-HVFarm { $farmVirtualCenterManagedCommonSettings = $farmSpecObj.AutomatedFarmSpec.virtualCenterManagedCommonSettings } - if (!$farmVirtualMachineNamingSpec) { + if ($farmSpecObj.AutomatedFarmSpec.RdsServerNamingSpec) { $farmSpecObj.AutomatedFarmSpec.RdsServerNamingSpec.NamingMethod = $namingMethod $farmSpecObj.AutomatedFarmSpec.RdsServerNamingSpec.patternNamingSettings.namingPattern = $namingPattern $farmSpecObj.AutomatedFarmSpec.RdsServerNamingSpec.patternNamingSettings.maxNumberOfRDSServers = $maximumCount } else { $vmNamingSpec = New-Object VMware.Hv.FarmRDSServerNamingSpec - $vmNamingSpec.NamingMethod = 'PATTERN' - + $vmNamingSpec.NamingMethod = $namingMethod $vmNamingSpec.patternNamingSettings = New-Object VMware.Hv.FarmPatternNamingSettings $vmNamingSpec.patternNamingSettings.namingPattern = $namingPattern $vmNamingSpec.patternNamingSettings.maxNumberOfRDSServers = $maximumCount + $farmSpecObj.AutomatedFarmSpec.RdsServerNamingSpec = $vmNamingSpec } # @@ -2263,8 +2441,10 @@ function New-HVFarm { # try { $farmVirtualCenterProvisioningData = Get-HVFarmProvisioningData -vc $virtualCenterID -vmObject $farmVirtualCenterProvisioningData - $hostClusterId = $farmVirtualCenterProvisioningData.HostOrCluster - $farmVirtualCenterStorageSettings = Get-HVFarmStorageObject -hostclusterID $hostClusterId -storageObject $farmVirtualCenterStorageSettings + + $HostOrCluster_helper = New-Object VMware.Hv.HostOrClusterService + $hostClusterIds = (($HostOrCluster_helper.HostOrCluster_GetHostOrClusterTree($services, $farmVirtualCenterProvisioningData.datacenter)).treeContainer.children.info).Id + $farmVirtualCenterStorageSettings = Get-HVFarmStorageObject -hostclusterIDs $hostClusterIds -storageObject $farmVirtualCenterStorageSettings $farmVirtualCenterNetworkingSettings = Get-HVFarmNetworkSetting -networkObject $farmVirtualCenterNetworkingSettings $farmCustomizationSettings = Get-HVFarmCustomizationSetting -vc $virtualCenterID -customObject $farmCustomizationSettings } catch { @@ -2273,31 +2453,20 @@ function New-HVFarm { break } - $farmSpecObj.AutomatedFarmSpec.RdsServerMaxSessionsData.MaxSessionsType = "UNLIMITED" - - if (!$FarmVirtualCenterProvisioningSettings) { - $farmSpecObj.AutomatedFarmSpec.VirtualCenterProvisioningSettings.enableProvisioning = $true - $farmSpecObj.AutomatedFarmSpec.VirtualCenterProvisioningSettings.stopProvisioningOnError = $true - $farmSpecObj.AutomatedFarmSpec.VirtualCenterProvisioningSettings.minReadyVMsOnVComposerMaintenance = 0 - $farmSpecObj.AutomatedFarmSpec.VirtualCenterProvisioningSettings.VirtualCenterProvisioningData = $farmVirtualCenterProvisioningData - $farmSpecObj.AutomatedFarmSpec.VirtualCenterProvisioningSettings.VirtualCenterStorageSettings = $farmVirtualCenterStorageSettings - $farmSpecObj.AutomatedFarmSpec.VirtualCenterProvisioningSettings.VirtualCenterNetworkingSettings = $FarmVirtualCenterNetworkingSettings - - $farmSpecObj.AutomatedFarmSpec.CustomizationSettings = $farmCustomizationSettings - $farmSpecObj.AutomatedFarmSpec.ProvisioningType = $provisioningType - $farmSpecObj.AutomatedFarmSpec.VirtualCenter = $virtualCenterID - } else { - $FarmVirtualCenterProvisioningSettings.VirtualCenterProvisioningData = $farmVirtualCenterProvisioningData - $FarmVirtualCenterProvisioningSettings.VirtualCenterStorageSettings = $farmVirtualCenterStorageSettings - $FarmVirtualCenterProvisioningSettings.VirtualCenterNetworkingSettings = $FarmVirtualCenterNetworkingSettings - - $FarmAutomatedFarmSpec = New-Object VMware.Hv.FarmAutomatedFarmSpec - $FarmAutomatedFarmSpec.ProvisioningType = $provisioningType - $FarmAutomatedFarmSpec.VirtualCenter = $virtualCenterID - $FarmAutomatedFarmSpec.VirtualCenterProvisioningSettings = $farmVirtualCenterProvisioningSettings - $FarmAutomatedFarmSpec.virtualCenterManagedCommonSettings = $farmVirtualCenterManagedCommonSettings - $FarmAutomatedFarmSpec.CustomizationSettings = $farmCustomizationSettings + $farmSpecObj.AutomatedFarmSpec.RdsServerMaxSessionsData.MaxSessionsType = $maxSessionsType + if ($maxSessionsType -eq "LIMITED") { + $farmSpecObj.AutomatedFarmSpec.RdsServerMaxSessionsData.MaxSessionsType = $maxSessions } + $farmSpecObj.AutomatedFarmSpec.VirtualCenterProvisioningSettings.enableProvisioning = $enableProvisioning + $farmSpecObj.AutomatedFarmSpec.VirtualCenterProvisioningSettings.stopProvisioningOnError = $stopProvisioningOnError + $farmSpecObj.AutomatedFarmSpec.VirtualCenterProvisioningSettings.minReadyVMsOnVComposerMaintenance = $minReady + $farmSpecObj.AutomatedFarmSpec.VirtualCenterProvisioningSettings.VirtualCenterProvisioningData = $farmVirtualCenterProvisioningData + $farmSpecObj.AutomatedFarmSpec.VirtualCenterProvisioningSettings.VirtualCenterStorageSettings = $farmVirtualCenterStorageSettings + $farmSpecObj.AutomatedFarmSpec.VirtualCenterProvisioningSettings.VirtualCenterNetworkingSettings = $FarmVirtualCenterNetworkingSettings + + $farmSpecObj.AutomatedFarmSpec.CustomizationSettings = $farmCustomizationSettings + $farmSpecObj.AutomatedFarmSpec.ProvisioningType = $provisioningType + $farmSpecObj.AutomatedFarmSpec.VirtualCenter = $virtualCenterID } } @@ -2313,6 +2482,29 @@ function New-HVFarm { $farmData.name = $farmName $farmData.DisplayName = $farmDisplayName $farmData.Description = $description + if ($farmData.Settings) { + $farmData.Settings.DisconnectedSessionTimeoutPolicy = $disconnectedSessionTimeoutPolicy + if ($disconnectedSessionTimeoutPolicy -eq "AFTER") { + $farmData.Settings.DisconnectedSessionTimeoutMinutes = $disconnectedSessionTimeoutMinutes + } + $farmData.Settings.EmptySessionTimeoutPolicy = $emptySessionTimeoutPolicy + if ($emptySessionTimeoutPolicy -eq "AFTER") { + $farmData.Settings.EmptySessionTimeoutMinutes = $emptySessionTimeoutMinutes + } + $logoffAfterTimeout = $farmData.Settings.logoffAfterTimeout + } + if ($farmData.DisplayProtocolSettings) { + $farmData.DisplayProtocolSettings.DefaultDisplayProtocol = $defaultDisplayProtocol + $farmData.DisplayProtocolSettings.AllowDisplayProtocolOverride = $AllowDisplayProtocolOverride + $farmData.DisplayProtocolSettings.EnableHTMLAccess = $enableHTMLAccess + } + if ($farmData.MirageConfigurationOverrides){ + $farmData.MirageConfigurationOverrides.OverrideGlobalSetting = $overrideGlobalSetting + $farmData.MirageConfigurationOverrides.Enabled = $mirageServerEnabled + if ($url) { + $farmData.MirageConfigurationOverrides.Url = $url + } + } $farmSpecObj.type = $farmType if ($FarmAutomatedFarmSpec) { @@ -2327,7 +2519,8 @@ function New-HVFarm { $myDebug = convertto-json -InputObject $farmSpecObj -depth 12 $myDebug | out-file -filepath c:\temp\copiedfarm.json #> - if ($pscmdlet.ShouldProcess($farmSpecObj)) { + + if ($pscmdlet.ShouldProcess($farmSpecObj.data.name)) { $Id = $farm_service_helper.Farm_Create($services, $farmSpecObj) } return $farmSpecObj @@ -2339,6 +2532,85 @@ function New-HVFarm { } +function Test-HVFarmSpec { + param( + [Parameter(Mandatory = $true)] + $JsonObject + ) + if ($null -eq $jsonObject.Type) { + Throw "Specify type of farm in json file" + } + $jsonFarmTypeArray = @('AUTOMATED','MANUAL') + if (! ($jsonFarmTypeArray -contains $jsonObject.Type)) { + Throw "Farm type must be AUTOMATED or MANUAL" + } + if ($null -eq $jsonObject.Data.Name) { + Throw "Specify farm name in json file" + } + if ($null -eq $jsonObject.Data.AccessGroup) { + Throw "Specify horizon access group in json file" + } + if ($jsonObject.Type -eq "AUTOMATED"){ + $jsonProvisioningType = $jsonObject.AutomatedFarmSpec.ProvisioningType + if ($null -eq $jsonProvisioningType) { + Throw "Must specify provisioningType in json file" + } + if ($null -eq $jsonObject.AutomatedFarmSpec.RdsServerNamingSpec.namingMethod) { + Throw "Must specify naming method to PATTERN in json file" + } + if ($null -eq $jsonObject.AutomatedFarmSpec.RdsServerNamingSpec.patternNamingSettings) { + Throw "Specify Naming pattern settings in json file" + } + if ($null -eq $jsonObject.AutomatedFarmSpec.RdsServerNamingSpec.patternNamingSettings.namingPattern) { + Throw "Specify specified naming pattern in json file" + } + if ($null -eq $jsonObject.AutomatedFarmSpec.virtualCenterProvisioningSettings.enableProvisioning) { + Throw "Specify Whether to enable provisioning in json file" + } + if ($null -eq $jsonObject.AutomatedFarmSpec.virtualCenterProvisioningSettings.stopProvisioningOnError) { + Throw "Specify Whether provisioning on all VMs stops on error in json file" + } + $jsonTemplate = $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.virtualCenterProvisioningData.Template + $jsonParentVm = $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.virtualCenterProvisioningData.ParentVm + $jsonSnapshot = $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.virtualCenterProvisioningData.Snapshot + $jsonVmFolder = $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.virtualCenterProvisioningData.VmFolder + $jsonHostOrCluster = $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.virtualCenterProvisioningData.HostOrCluster + $ResourcePool = $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.virtualCenterProvisioningData.ResourcePool + if (!( ($null -ne $jsonTemplate) -or (($null -ne $jsonParentVm) -and ($null -ne $jsonSnapshot) )) ) { + Throw "Must specify Template or (ParentVm and Snapshot) names in json file" + } + if ($null -eq $jsonVmFolder) { + Throw "Must specify VM folder in json file to deploy the VMs" + } + if ($null -eq $jsonHostOrCluster) { + Throw "Must specify Host or cluster in json file to deploy the VMs" + } + if ($null -eq $resourcePool) { + Throw "Must specify Resource pool in json file to deploy the VMs" + } + if ($null -eq $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.VirtualCenterStorageSettings.Datastores) { + Throw "Must specify datastores names in json file" + } + if ($null -eq $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.VirtualCenterStorageSettings.useVSan) { + Throw "Must specify whether to use virtual SAN or not" + } + if ($null -eq $jsonObject.AutomatedFarmSpec.CustomizationSettings.customizationType) { + Throw "Specify Type of customization to use in json file" + } + if ($null -eq $jsonObject.AutomatedFarmSpec.CustomizationSettings.SysprepCustomizationSettings) { + Throw "Specify Sysprep customization settings in json file" + } + if ($null -eq $jsonObject.AutomatedFarmSpec.RdsServerMaxSessionsData.MaxSessionsType) { + Throw "Specify MaxSessionsType in json file" + } + } elseif ($jsonObject.Type -eq "MANUAL") { + if ($null -eq $jsonObject.manualFarmSpec.rdsServers) { + Throw "Specify rdsServers name in json file" + } + } +} + + function Get-HVFarmProvisioningData { param( [Parameter(Mandatory = $false)] @@ -2359,6 +2631,12 @@ function Get-HVFarmProvisioningData { } $vmObject.ParentVm = $parentVMObj.id $dataCenterID = $parentVMObj.datacenter + if ($dataCenter -and $dataCenterID) { + $baseImageVmInfo = $base_imageVm_helper.BaseImageVm_ListByDatacenter($dataCenterID) + if (! ($baseImageVmInfo.Path -like "/$dataCenter/*")) { + throw "$parentVM not exists in datacenter: [$dataCenter]" + } + } $vmObject.datacenter = $dataCenterID } if ($snapshotVM) { @@ -2411,23 +2689,30 @@ function Get-HVFarmProvisioningData { return $vmObject } + function Get-HVFarmStorageObject { param( - [Parameter(Mandatory = $false)] - [VMware.Hv.FarmVirtualCenterStorageSettings]$StorageObject, [Parameter(Mandatory = $true)] - [VMware.Hv.HostOrClusterId]$HostClusterID + [VMware.Hv.HostOrClusterId[]]$HostClusterIDs, + + [Parameter(Mandatory = $false)] + [VMware.Hv.FarmVirtualCenterStorageSettings]$StorageObject ) if (!$storageObject) { $storageObject = New-Object VMware.Hv.FarmVirtualCenterStorageSettings $FarmSpaceReclamationSettings = New-Object VMware.Hv.FarmSpaceReclamationSettings -Property @{ 'reclaimVmDiskSpace' = $false } + if ($reclaimVmDiskSpace) { + $FarmSpaceReclamationSettings.ReclamationThresholdGB = $reclamationThresholdGB + if ($blackoutTimes) { + $FarmSpaceReclamationSettings.BlackoutTimes = $blackoutTimes + } + } $FarmViewComposerStorageSettingsList = @{ - 'useSeparateDatastoresReplicaAndOSDisks' = $false; - 'replicaDiskDatastore' = $FarmReplicaDiskDatastore - 'useNativeSnapshots' = $false; + 'useSeparateDatastoresReplicaAndOSDisks' = $UseSeparateDatastoresReplicaAndOSDisks; + 'useNativeSnapshots' = $useNativeSnapshots; 'spaceReclamationSettings' = $FarmSpaceReclamationSettings; } @@ -2435,18 +2720,33 @@ function Get-HVFarmStorageObject { } if ($datastores) { + if ($StorageOvercommit -and ($datastores.Length -ne $StorageOvercommit.Length) ) { + throw "Parameters datastores length: [$datastores.Length] and StorageOvercommit length: [$StorageOvercommit.Length] should be of same size" + } $Datastore_service_helper = New-Object VMware.Hv.DatastoreService - $datastoreList = $Datastore_service_helper.Datastore_ListDatastoresByHostOrCluster($services, $hostClusterID) + foreach ($hostClusterID in $hostClusterIDs) { + $datastoreList += $Datastore_service_helper.Datastore_ListDatastoresByHostOrCluster($services, $hostClusterID) + } $datastoresSelected = @() foreach ($ds in $datastores) { $datastoresSelected += ($datastoreList | Where-Object { $_.datastoredata.name -eq $ds }).id } + if (! $storageOvercommit) { + foreach ($ds in $datastoresSelected) { + $storageOvercommit += ,'UNBOUNDED' + } + } + $StorageOvercommitCnt = 0 foreach ($ds in $datastoresSelected) { $datastoresObj = New-Object VMware.Hv.FarmVirtualCenterDatastoreSettings $datastoresObj.Datastore = $ds - $datastoresObj.StorageOvercommit = 'UNBOUNDED' + $datastoresObj.StorageOvercommit = $storageOvercommit[$StorageOvercommitCnt] $StorageObject.Datastores += $datastoresObj } + if ($useSeparateDatastoresReplicaAndOSDisks) { + $FarmReplicaDiskDatastore = ($datastoreList | Where-Object { $_.datastoredata.name -eq $replicaDiskDatastore }).id + } + $StorageObject.ViewComposerStorageSettings.ReplicaDiskDatastore = $FarmReplicaDiskDatastore } if ($storageObject.Datastores.Count -eq 0) { throw "No datastores found with name: [$datastores]" @@ -2455,6 +2755,7 @@ function Get-HVFarmStorageObject { return $storageObject } + function Get-HVFarmNetworkSetting { param( [Parameter(Mandatory = $false)] @@ -2466,6 +2767,7 @@ function Get-HVFarmNetworkSetting { return $networkObject } + function Get-HVFarmCustomizationSetting { param( [Parameter(Mandatory = $false)] @@ -2479,7 +2781,7 @@ function Get-HVFarmCustomizationSetting { $ViewComposerDomainAdministratorID = ($ViewComposerDomainAdministrator_service_helper.ViewComposerDomainAdministrator_List($services, $vcID) | Where-Object { $_.base.domain -match $netBiosName }) if (! [string]::IsNullOrWhitespace($domainAdmin)) { $ViewComposerDomainAdministratorID = ($ViewComposerDomainAdministratorID | Where-Object { $_.base.userName -ieq $domainAdmin }).id - } else { + } elseif ($null -ne $ViewComposerDomainAdministratorID) { $ViewComposerDomainAdministratorID = $ViewComposerDomainAdministratorID[0].id } if ($null -eq $ViewComposerDomainAdministratorID) { @@ -2509,7 +2811,7 @@ function Get-HVFarmCustomizationSetting { $farmSpecObj.AutomatedFarmSpec.CustomizationSettings.CustomizationType = 'SYS_PREP' $farmSpecObj.AutomatedFarmSpec.CustomizationSettings.DomainAdministrator = $ViewComposerDomainAdministratorID $farmSpecObj.AutomatedFarmSpec.CustomizationSettings.AdContainer = $adContainerId - $farmSpecObj.AutomatedFarmSpec.CustomizationSettings.ReusePreExistingAccounts = $false + $farmSpecObj.AutomatedFarmSpec.CustomizationSettings.ReusePreExistingAccounts = $reusePreExistingAccounts $farmSpecObj.AutomatedFarmSpec.CustomizationSettings.SysprepCustomizationSettings = $sysprepCustomizationSettings $customObject = $farmSpecObj.AutomatedFarmSpec.CustomizationSettings @@ -2535,10 +2837,16 @@ function Get-FarmSpec { if ($farmType -eq 'AUTOMATED') { $farm_spec_helper.getDataObject().AutomatedFarmSpec.RdsServerNamingSpec.PatternNamingSettings = $farm_helper.getFarmPatternNamingSettingsHelper().getDataObject() $farm_spec_helper.getDataObject().AutomatedFarmSpec.VirtualCenterProvisioningSettings.VirtualCenterStorageSettings.ViewComposerStorageSettings = $farm_helper.getFarmViewComposerStorageSettingsHelper().getDataObject() + + } + $farm_spec_helper.getDataObject().Data.Settings = $farm_helper.getFarmSessionSettingsHelper().getDataObject() + $farm_spec_helper.getDataObject().Data.DisplayProtocolSettings = $farm_helper.getFarmDisplayProtocolSettingsHelper().getDataObject() + $farm_spec_helper.getDataObject().Data.MirageConfigurationOverrides = $farm_helper.getFarmMirageConfigurationOverridesHelper( ).getDataObject() return $farm_spec_helper.getDataObject() } + function New-HVPool { <# .Synopsis @@ -2743,7 +3051,7 @@ function New-HVPool { .EXAMPLE Create new automated linked clone pool with naming method pattern - New-HVPool -LinkedClone -PoolName 'vmwarepool' -UserAssignment FLOATING -ParentVM 'Agent_vmware' -SnapshotVM 'kb-hotfix' -VmFolder 'vmware' -HostOrCluster 'CS-1' -ResourcePool 'CS-1' -Datastores 'datastore1' -NamingMethod PATTERN -PoolDisplayName 'vmware linkedclone pool' -Description 'created linkedclone pool from ps' -EnableProvisioning $true -StopOnProvisioningError $false -NamingPattern "vmware2" -MinReady 1 -MaximumCount 1 -SpareCount 1 -ProvisioningTime UP_FRONT -SysPrepName vmwarecust -CustType SYS_PREP -NetBiosName adviewdev -DomainAdmin root + New-HVPool -LinkedClone -PoolName 'vmwarepool' -UserAssignment FLOATING -ParentVM 'Agent_vmware' -SnapshotVM 'kb-hotfix' -VmFolder 'vmware' -HostOrCluster 'CS-1' -ResourcePool 'CS-1' -Datastores 'datastore1' -NamingMethod PATTERN -PoolDisplayName 'vmware linkedclone pool' -Description 'created linkedclone pool from ps' -EnableProvisioning $true -StopOnProvisioningError $false -NamingPattern "vmware2" -MinReady 0 -MaximumCount 1 -SpareCount 1 -ProvisioningTime UP_FRONT -SysPrepName vmwarecust -CustType SYS_PREP -NetBiosName adviewdev -DomainAdmin root .EXAMPLE Create new automated linked clone pool by using JSON spec file @@ -2850,7 +3158,6 @@ function New-HVPool { #desktopSpec.automatedDesktopSpec.desktopUserAssignment.userAssigment if LINKED_CLONE, INSTANT_CLONE, FULL_CLONE #desktopSpec.manualDesktopSpec.desktopUserAssignment.userAssigment if MANUAL - [Parameter(Mandatory = $true,ParameterSetName = 'MANUAL')] [Parameter(Mandatory = $true,ParameterSetName = "LINKED_CLONE")] [Parameter(Mandatory = $true,ParameterSetName = 'INSTANT_CLONE')] @@ -2877,6 +3184,120 @@ function New-HVPool { [string[]] $ConnectionServerRestrictions, + #desktopSpec.desktopSettings.deleting + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [boolean]$Deleting = $false, + + #desktopSpec.desktopSettings.logoffSettings.powerPloicy + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [ValidateSet('TAKE_NO_POWER_ACTION', 'ALWAYS_POWERED_ON', 'SUSPEND', 'POWER_OFF')] + [string]$PowerPolicy = 'TAKE_NO_POWER_ACTION', + + #desktopSpec.desktopSettings.logoffSettings.powerPloicy + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [ValidateSet('IMMEDIATELY', 'NEVER', 'AFTER')] + [string]$AutomaticLogoffPolicy = 'NEVER', + + #desktopSpec.desktopSettings.logoffSettings.automaticLogoffMinutes + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [ValidateRange(1,120)] + [int]$AutomaticLogoffMinutes = 120, + + #desktopSpec.desktopSettings.logoffSettings.allowUsersToResetMachines + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [boolean]$allowUsersToResetMachines = $false, + + #desktopSpec.desktopSettings.logoffSettings.allowMultipleSessionsPerUser + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [boolean]$allowMultipleSessionsPerUser = $false, + + #desktopSpec.desktopSettings.logoffSettings.deleteOrRefreshMachineAfterLogoff + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [ValidateSet('NEVER', 'DELETE', 'AFTER')] + [string]$deleteOrRefreshMachineAfterLogoff = 'NEVER', + + #desktopSpec.desktopSettings.logoffSettings.refreshOsDiskAfterLogoff + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [ValidateSet('NEVER', 'ALWAYS', 'EVERY', 'AT_SIZE')] + [string]$refreshOsDiskAfterLogoff = 'NEVER', + + #desktopSpec.desktopSettings.logoffSettings.refreshPeriodDaysForReplicaOsDisk + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [int]$refreshPeriodDaysForReplicaOsDisk = 120, + + #desktopSpec.desktopSettings.logoffSettings.refreshThresholdPercentageForReplicaOsDisk + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [ValidateRange(1,100)] + [int]$refreshThresholdPercentageForReplicaOsDisk, + + #DesktopDisplayProtocolSettings + #desktopSpec.desktopSettings.logoffSettings.supportedDisplayProtocols + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [ValidateSet('RDP', 'PCOIP', 'BLAST')] + [string[]]$supportedDisplayProtocols = @('RDP', 'PCOIP', 'BLAST'), + + #desktopSpec.desktopSettings.logoffSettings.defaultDisplayProtocol + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [ValidateSet('RDP', 'PCOIP', 'BLAST')] + [string]$defaultDisplayProtocol = 'PCOIP', + + #desktopSpec.desktopSettings.logoffSettings.allowUsersToChooseProtocol + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [int]$allowUsersToChooseProtocol = $true, + + #desktopSpec.desktopSettings.logoffSettings.enableHTMLAccess + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [boolean]$enableHTMLAccess = $false, + + # DesktopPCoIPDisplaySettings + #desktopSpec.desktopSettings.logoffSettings.pcoipDisplaySettings.renderer3D + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [ValidateSet('MANAGE_BY_VSPHERE_CLIENT', 'AUTOMATIC', 'SOFTWARE', 'HARDWARE', 'DISABLED')] + [string]$renderer3D = 'DISABLED', + + #desktopSpec.desktopSettings.logoffSettings.pcoipDisplaySettings.enableGRIDvGPUs + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [boolean]$enableGRIDvGPUs = $false, + + #desktopSpec.desktopSettings.logoffSettings.pcoipDisplaySettings.vRamSizeMB + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [ValidateRange(64,512)] + [int]$vRamSizeMB = 96, + + #desktopSpec.desktopSettings.logoffSettings.pcoipDisplaySettings.maxNumberOfMonitors + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [ValidateRange(1,4)] + [int]$maxNumberOfMonitors = 2, + + #desktopSpec.desktopSettings.logoffSettings.pcoipDisplaySettings.maxResolutionOfAnyOneMonitor + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [ValidateSet('WUXGA', 'WSXGA_PLUS', 'WQXGA', 'UHD')] + [string]$maxResolutionOfAnyOneMonitor = 'WUXGA', + + # flashSettings + #desktopSpec.desktopSettings.flashSettings.quality + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [ValidateSet('NO_CONTROL', 'LOW', 'MEDIUM', 'HIGH')] + [string]$quality = 'NO_CONTROL', + + #desktopSpec.desktopSettings.flashSettings.throttling + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [ValidateSet('DISABLED', 'CONSERVATIVE', 'MODERATE', 'AGGRESSIVE')] + [string]$throttling = 'DISABLED', + + #mirageConfigurationOverrides + #desktopSpec.desktopSettings.mirageConfigurationOverrides.overrideGlobalSetting + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [boolean]$overrideGlobalSetting = $false, + + #desktopSpec.desktopSettings.mirageConfigurationOverrides.enabled + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [boolean]$enabled = $true, + + #desktopSpec.desktopSettings.mirageConfigurationOverrides.url + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [string]$url = $true, + #desktopSpec.automatedDesktopSpec.virtualCenter if LINKED_CLONE, INSTANT_CLONE, FULL_CLONE #desktopSpec.manualDesktopSpec.virtualCenter if MANUAL [Parameter(Mandatory = $false,ParameterSetName = 'MANUAL')] @@ -2924,6 +3345,12 @@ function New-HVPool { [string] $ResourcePool, + #desktopSpec.automatedDesktopSpec.virtualCenterProvisioningSettings.virtualCenterProvisioningData.datacenter if LINKED_CLONE, INSTANT_CLONE, FULL_CLONE + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [Parameter(Mandatory = $false,ParameterSetName = 'INSTANT_CLONE')] + [Parameter(Mandatory = $false,ParameterSetName = 'FULL_CLONE')] + [string] + $datacenter, #desktopSpec.automatedDesktopSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.datastore if LINKED_CLONE, INSTANT_CLONE, FULL_CLONE [Parameter(Mandatory = $true,ParameterSetName = "LINKED_CLONE")] [Parameter(Mandatory = $true,ParameterSetName = 'INSTANT_CLONE')] @@ -2931,13 +3358,116 @@ function New-HVPool { [string[]] $Datastores, + #desktopSpec.automatedDesktopSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.datastores.storageOvercommit if LINKED_CLONE, INSTANT_CLONE, FULL_CLONE + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [Parameter(Mandatory = $false,ParameterSetName = 'INSTANT_CLONE')] + [Parameter(Mandatory = $false,ParameterSetName = 'FULL_CLONE')] + [string[]] + $StorageOvercommit = $null, #desktopSpec.automatedDesktopSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.useVSAN if LINKED_CLONE, INSTANT_CLONE, FULL_CLONE [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] [Parameter(Mandatory = $false,ParameterSetName = 'INSTANT_CLONE')] [Parameter(Mandatory = $false,ParameterSetName = 'FULL_CLONE')] - [string] - $UseVSAN, + [boolean] + $UseVSAN = $false, + #desktopSpec.automatedDesktopSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.viewComposerStorageSettings.useSeparateDatastoresReplicaAndOSDisks if LINKED_CLONE, INSTANT_CLONE + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [Parameter(Mandatory = $false,ParameterSetName = 'INSTANT_CLONE')] + [boolean] + $UseSeparateDatastoresReplicaAndOSDisks = $false, + #desktopSpec.automatedDesktopSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.viewComposerStorageSettings.replicaDiskDatastore if LINKED_CLONE, INSTANT_CLONE + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [Parameter(Mandatory = $false,ParameterSetName = 'INSTANT_CLONE')] + [string] + $ReplicaDiskDatastore, + + #desktopSpec.automatedDesktopSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.viewComposerStorageSettings.replicaDiskDatastore if LINKED_CLONE, INSTANT_CLONE + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [Parameter(Mandatory = $false,ParameterSetName = 'INSTANT_CLONE')] + [boolean] + $UseNativeSnapshots = $false, + #desktopSpec.automatedDesktopSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.viewComposerStorageSettings.spaceReclamationSettings.reclaimVmDiskSpace if LINKED_CLONE, INSTANT_CLONE + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [Parameter(Mandatory = $false,ParameterSetName = 'INSTANT_CLONE')] + [boolean] + $ReclaimVmDiskSpace = $false, + #desktopSpec.automatedDesktopSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.viewComposerStorageSettings.spaceReclamationSettings.reclamationThresholdGB if LINKED_CLONE + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [ValidateRange(0,[Int]::MaxValue)] + [int] + $ReclamationThresholdGB = 1, + #desktopSpec.automatedDesktopSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.viewComposerStorageSettings.persistentDiskSettings.redirectWindowsProfile if LINKED_CLONE, INSTANT_CLONE + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [Parameter(Mandatory = $false,ParameterSetName = 'INSTANT_CLONE')] + [boolean] + $RedirectWindowsProfile = $true, + #desktopSpec.automatedDesktopSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.viewComposerStorageSettings.persistentDiskSettings.useSeparateDatastoresPersistentAndOSDisks if LINKED_CLONE + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [boolean] + $UseSeparateDatastoresPersistentAndOSDisks = $false, + #desktopSpec.automatedDesktopSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.viewComposerStorageSettings.persistentDiskSettings.PersistentDiskDatastores if LINKED_CLONE + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [string[]] + $PersistentDiskDatastores, + #desktopSpec.automatedDesktopSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.viewComposerStorageSettings.persistentDiskSettings.PersistentDiskDatastores if LINKED_CLONE + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [string[]] + $PersistentDiskStorageOvercommit = $null, + + #desktopSpec.automatedDesktopSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.viewComposerStorageSettings.persistentDiskSettings.diskSizeMB if LINKED_CLONE + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [ValidateRange(128,[Int]::MaxValue)] + [int] + $DiskSizeMB = 2048, + #desktopSpec.automatedDesktopSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.viewComposerStorageSettings.persistentDiskSettings.diskDriveLetter if LINKED_CLONE + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [ValidatePattern("^[D-Z]$")] + [string] + $DiskDriveLetter = "D", + #desktopSpec.automatedDesktopSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.viewComposerStorageSettings.nonPersistentDiskSettings.redirectDisposableFiles if LINKED_CLONE + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [boolean] + $redirectDisposableFiles, + + #desktopSpec.automatedDesktopSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.viewComposerStorageSettings.nonPersistentDiskSettings.diskSizeMB if LINKED_CLONE + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [ValidateRange(512,[Int]::MaxValue)] + [int] + $NonPersistentDiskSizeMB = 4096, + #desktopSpec.automatedDesktopSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.viewComposerStorageSettings.nonPersistentDiskSettings.diskDriveLetter if LINKED_CLONE + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [ValidatePattern("^[D-Z]|Auto$")] + [string] + $NonPersistentDiskDriveLetter = "Auto", + + #desktopSpec.automatedDesktopSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.viewStorageAcceleratorSettings.useViewStorageAccelerator if LINKED_CLONE + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [boolean] + $UseViewStorageAccelerator = $false, + + #desktopSpec.automatedDesktopSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.viewStorageAcceleratorSettings.useViewStorageAccelerator if LINKED_CLONE + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [string] + $ViewComposerDiskTypes = "OS_DISKS", + + #desktopSpec.automatedDesktopSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.viewStorageAcceleratorSettings.regenerateViewStorageAcceleratorDays if LINKED_CLONE + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [ValidateRange(1,999)] + [int] + $RegenerateViewStorageAcceleratorDays = 7, + + #desktopSpec.automatedDesktopSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.viewStorageAcceleratorSettings.blackoutTimes if LINKED_CLONE + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [VMware.Hv.DesktopBlackoutTime[]] + $BlackoutTimes, + + #desktopSpec.automatedDesktopSpec.virtualCenterProvisioningSettings.virtualCenterNetworkingSettings.nics + [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [Parameter(Mandatory = $false,ParameterSetName = 'INSTANT_CLONE')] + [Parameter(Mandatory = $false,ParameterSetName = 'FULL_CLONE')] + [VMware.Hv.DesktopNetworkInterfaceCardSettings[]] + $Nics, #desktopSpec.automatedDesktopSpec.virtualCenterProvisioningSettings.enableProvsioning if LINKED_CLONE, INSTANT_CLONE, FULL_CLONE [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] [Parameter(Mandatory = $false,ParameterSetName = 'INSTANT_CLONE')] @@ -3042,6 +3572,8 @@ function New-HVPool { [Parameter(Mandatory = $false,ParameterSetName = 'FULL_CLONE')] [string]$NetBiosName, + #desktopSpec.automatedDesktopSpec.customizationSettings.domainAdministrator + #desktopSpec.automatedDesktopSpec.customizationSettings.cloneprepCustomizationSettings.instantCloneEngineDomainAdministrator [Parameter(Mandatory = $false,ParameterSetName = 'INSTANT_CLONE')] [Parameter(Mandatory = $false,ParameterSetName = 'LINKED_CLONE')] [string]$DomainAdmin = $null, @@ -3052,13 +3584,48 @@ function New-HVPool { [ValidateSet('CLONE_PREP','QUICK_PREP','SYS_PREP','NONE')] [string] $CustType, - + #desktopSpec.automatedDesktopSpec.customizationSettings.reusePreExistingAccounts if LINKED_CLONE + [Parameter(Mandatory = $false,ParameterSetName = 'LINKED_CLONE')] + [Boolean] + $ReusePreExistingAccounts = $false, #desktopSpec.automatedDesktopSpec.customizationSettings.sysprepCustomizationSettings.customizationSpec if LINKED_CLONE, FULL_CLONE [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] [Parameter(Mandatory = $false,ParameterSetName = "FULL_CLONE")] [string] $SysPrepName, + #desktopSpec.automatedDesktopSpec.customizationSettings.noCustomizationSettings.doNotPowerOnVMsAfterCreation if FULL_CLONE + [Parameter(Mandatory = $false,ParameterSetName = "FULL_CLONE")] + [boolean] + $DoNotPowerOnVMsAfterCreation = $false, + + #desktopSpec.automatedDesktopSpec.customizationSettings.quickprepCustomizationSettings.powerOffScriptName if LINKED_CLONE, INSTANT_CLONE + #desktopSpec.automatedDesktopSpec.customizationSettings.cloneprepCustomizationSettings.powerOffScriptName + [Parameter(Mandatory = $false,ParameterSetName = 'INSTANT_CLONE')] + [Parameter(Mandatory = $false,ParameterSetName = 'LINKED_CLONE')] + [string] + $PowerOffScriptName, + + #desktopSpec.automatedDesktopSpec.customizationSettings.quickprepCustomizationSettings.powerOffScriptParameters + #desktopSpec.automatedDesktopSpec.customizationSettings.cloneprepCustomizationSettings.powerOffScriptParameters + [Parameter(Mandatory = $false,ParameterSetName = 'INSTANT_CLONE')] + [Parameter(Mandatory = $false,ParameterSetName = 'LINKED_CLONE')] + [string] + $PowerOffScriptParameters, + + #desktopSpec.automatedDesktopSpec.customizationSettings.quickprepCustomizationSettings.postSynchronizationScriptName + #desktopSpec.automatedDesktopSpec.customizationSettings.cloneprepCustomizationSettings.postSynchronizationScriptName + [Parameter(Mandatory = $false,ParameterSetName = 'INSTANT_CLONE')] + [Parameter(Mandatory = $false,ParameterSetName = 'LINKED_CLONE')] + [string] + $PostSynchronizationScriptName, + + #desktopSpec.automatedDesktopSpec.customizationSettings.quickprepCustomizationSettings.postSynchronizationScriptParameters + #desktopSpec.automatedDesktopSpec.customizationSettings.cloneprepCustomizationSettings.postSynchronizationScriptParameters + [Parameter(Mandatory = $false,ParameterSetName = 'INSTANT_CLONE')] + [Parameter(Mandatory = $false,ParameterSetName = 'LINKED_CLONE')] + [string] + $PostSynchronizationScriptParameters, #manual desktop [Parameter(Mandatory = $true,ParameterSetName = 'MANUAL')] [ValidateSet('VIRTUAL_CENTER','UNMANAGED')] @@ -3160,6 +3727,13 @@ function New-HVPool { Write-Error "Json file exception, $_" break } + + try { + Test-HVDesktopSpec -JsonObject $jsonObject + } catch { + Write-Error "Json object validation failed, $_" + break + } if ($jsonObject.type -eq "AUTOMATED") { $poolType = 'AUTOMATED' if ($null -ne $jsonObject.AutomatedDesktopSpec.VirtualCenter) { @@ -3176,13 +3750,37 @@ function New-HVPool { $custType = $jsonObject.AutomatedDesktopSpec.CustomizationSettings.CustomizationType if ($jsonObject.AutomatedDesktopSpec.ProvisioningType -eq "INSTANT_CLONE_ENGINE") { $InstantClone = $true + if ($null -ne $jsonObject.AutomatedDesktopSpec.CustomizationSettings.CloneprepCustomizationSettings) { + $domainAdmin = $jsonObject.AutomatedDesktopSpec.CustomizationSettings.CloneprepCustomizationSettings.InstantCloneEngineDomainAdministrator + $powerOffScriptName = $jsonObject.AutomatedDesktopSpec.CustomizationSettings.CloneprepCustomizationSettings.PowerOffScriptName + $powerOffScriptParameters = $jsonObject.AutomatedDesktopSpec.CustomizationSettings.CloneprepCustomizationSettings.PowerOffScriptParameters + $postSynchronizationScriptName = $jsonObject.AutomatedDesktopSpec.CustomizationSettings.CloneprepCustomizationSettings.PostSynchronizationScriptName + $postSynchronizationScriptParameters = $jsonObject.AutomatedDesktopSpec.CustomizationSettings.CloneprepCustomizationSettings.PostSynchronizationScriptParameters + } } else { if ($jsonObject.AutomatedDesktopSpec.ProvisioningType -eq "VIEW_COMPOSER") { $LinkedClone = $true - } else { + $domainAdmin = $jsonObject.AutomatedDesktopSpec.CustomizationSettings.domainAdministrator + } elseIf($jsonObject.AutomatedDesktopSpec.ProvisioningType -eq "VIRTUAL_CENTER") { $FullClone = $true } - $sysPrepName = $jsonObject.SysPrepName + switch ($custType) { + 'SYS_PREP' { + $sysprepCustomizationSettings = $jsonObject.AutomatedDesktopSpec.CustomizationSettings.SysprepCustomizationSettings + $sysPrepName = $sysprepCustomizationSettings.customizationSpec + $reusePreExistingAccounts = $jsonObject.AutomatedDesktopSpec.CustomizationSettings.reusePreExistingAccounts + } + 'QUICK_PREP' { + $powerOffScriptName= $jsonObject.AutomatedDesktopSpec.CustomizationSettings.QuickprepCustomizationSettings.PowerOffScriptName + $powerOffScriptParameters = $jsonObject.AutomatedDesktopSpec.CustomizationSettings.QuickprepCustomizationSettings.PowerOffScriptParameters + $postSynchronizationScriptName = $jsonObject.AutomatedDesktopSpec.CustomizationSettings.QuickprepCustomizationSettings.PostSynchronizationScriptName + $postSynchronizationScriptParameters = $jsonObject.AutomatedDesktopSpec.CustomizationSettings.QuickprepCustomizationSettings.PostSynchronizationScriptParameters + } + 'NONE' { + $doNotPowerOnVMsAfterCreation = $jsonObject.AutomatedDesktopSpec.CustomizationSettings.NoCustomizationSettings.DoNotPowerOnVMsAfterCreation + } + + } } $namingMethod = $jsonObject.AutomatedDesktopSpec.VmNamingSpec.NamingMethod $transparentPageSharingScope = $jsonObject.AutomatedDesktopSpec.virtualCenterManagedCommonSettings.TransparentPageSharingScope @@ -3196,6 +3794,9 @@ function New-HVPool { $startInMaintenanceMode = $jsonObject.AutomatedDesktopSpec.VmNamingSpec.SpecificNamingSpec.startMachinesInMaintenanceMode $numUnassignedMachinesKeptPoweredOn = $jsonObject.AutomatedDesktopSpec.VmNamingSpec.SpecificNamingSpec.numUnassignedMachinesKeptPoweredOn } + $enableProvisioning = $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.EnableProvisioning + $stopProvisioningOnError = $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.stopProvisioningOnError + $minReady = $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.minReadyVMsOnVComposerMaintenance if ($null -ne $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.VirtualCenterProvisioningData.Template) { $template = $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.VirtualCenterProvisioningData.Template } @@ -3205,14 +3806,87 @@ function New-HVPool { if ($null -ne $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.VirtualCenterProvisioningData.Snapshot) { $snapshotVM = $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.VirtualCenterProvisioningData.Snapshot } + $dataCenter = $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.VirtualCenterProvisioningData.dataCenter $vmFolder = $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.VirtualCenterProvisioningData.VmFolder $hostOrCluster = $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.VirtualCenterProvisioningData.HostOrCluster $resourcePool = $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.VirtualCenterProvisioningData.ResourcePool $dataStoreList = $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.VirtualCenterStorageSettings.Datastores foreach ($dtStore in $dataStoreList) { $datastores += $dtStore.Datastore + $storageOvercommit += $dtStore.StorageOvercommit } - } elseif ($jsonObject.type -eq "MANUAL") { + $useVSan = $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.VirtualCenterStorageSettings.UseVSan + if ($LinkedClone -or $InstantClone) { + $useSeparateDatastoresReplicaAndOSDisks = $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.VirtualCenterStorageSettings.ViewComposerStorageSettings.UseSeparateDatastoresReplicaAndOSDisks + if ($useSeparateDatastoresReplicaAndOSDisks) { + $replicaDiskDatastore = $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.VirtualCenterStorageSettings.ViewComposerStorageSettings.replicaDiskDatastore + } + if ($LinkedClone) { + #For Instant clone desktops, this setting can only be set to false + $useNativeSnapshots = $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.VirtualCenterStorageSettings.ViewComposerStorageSettings.useNativeSnapshots + $reclaimVmDiskSpace = $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.VirtualCenterStorageSettings.ViewComposerStorageSettings.spaceReclamationSettings.reclaimVmDiskSpace + if ($reclaimVmDiskSpace) { + $reclamationThresholdGB = $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.VirtualCenterStorageSettings.ViewComposerStorageSettings.spaceReclamationSettings.reclamationThresholdGB + } + if ($null -ne $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.VirtualCenterStorageSettings.ViewComposerStorageSettings.PersistentDiskSettings) { + $redirectWindowsProfile = $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.VirtualCenterStorageSettings.ViewComposerStorageSettings.PersistentDiskSettings.RedirectWindowsProfile + if ($redirectWindowsProfile) { + $useSeparateDatastoresPersistentAndOSDisks = $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.VirtualCenterStorageSettings.ViewComposerStorageSettings.PersistentDiskSettings.UseSeparateDatastoresPersistentAndOSDisks + } + $dataStoreList = $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.VirtualCenterStorageSettings.ViewComposerStorageSettings.PersistentDiskSettings.persistentDiskDatastores + foreach ($dtStore in $dataStoreList) { + $persistentDiskDatastores += $dtStore.Datastore + $PersistentDiskStorageOvercommit += $dtStore.StorageOvercommit + } + if ($null -ne $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.VirtualCenterStorageSettings.ViewComposerStorageSettings.PersistentDiskSettings.DiskSizeMB) { + $diskSizeMB = $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.VirtualCenterStorageSettings.ViewComposerStorageSettings.PersistentDiskSettings.DiskSizeMB + } + if ($null -ne $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.VirtualCenterStorageSettings.ViewComposerStorageSettings.PersistentDiskSettings.DiskDriveLetter) { + $diskDriveLetter = $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.VirtualCenterStorageSettings.ViewComposerStorageSettings.PersistentDiskSettings.DiskDriveLetter + } + } + if ($null -ne $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.VirtualCenterStorageSettings.ViewComposerStorageSettings.nonPersistentDiskSettings) { + $redirectDisposableFiles = $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.VirtualCenterStorageSettings.ViewComposerStorageSettings.NonPersistentDiskSettings.RedirectDisposableFiles + if ($null -ne $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.VirtualCenterStorageSettings.ViewComposerStorageSettings.NonPersistentDiskSettings.DiskSizeMB) { + $nonPersistentDiskSizeMB = $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.VirtualCenterStorageSettings.ViewComposerStorageSettings.NonPersistentDiskSettings.DiskSizeMB + } + if ($null -ne $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.VirtualCenterStorageSettings.ViewComposerStorageSettings.NonPersistentDiskSettings.DiskDriveLetter) { + $nonPersistentDiskDriveLetter = $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.VirtualCenterStorageSettings.ViewComposerStorageSettings.NonPersistentDiskSettings.DiskDriveLetter + } + } + } else { + $useNativeSnapshots = $false + $redirectWindowsProfile = $false + } + } + if ($null -ne $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.viewStorageAcceleratorSettings) { + $useViewStorageAccelerator = $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.viewStorageAcceleratorSettings.UseViewStorageAccelerator + if ($useViewStorageAccelerator -and $LinkedClone) { + $viewComposerDiskTypes = $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.viewStorageAcceleratorSettings.ViewComposerDiskTypes + } + if (! $InstantClone -and $useViewStorageAccelerator) { + $regenerateViewStorageAcceleratorDays = $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.viewStorageAcceleratorSettings.RegenerateViewStorageAcceleratorDays + if ($null -ne $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.viewStorageAcceleratorSettings.blackoutTimes) { + $blackoutTimesList =$jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.viewStorageAcceleratorSettings.blackoutTimes + foreach ($blackout in $blackoutTimesList) { + $blackoutObj = New-Object VMware.Hv.DesktopBlackoutTime + $blackoutObj.Days = $blackout.Days + $blackoutObj.StartTime = $blackout.StartTime + $blackoutObj.EndTime = $blackoutObj.EndTime + $blackoutTimes += $blackoutObj + } + } + } + } + <# ToDo Nic + if ($null -ne $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.nics) { + $nicList = $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.nics + foreach($nicObj in $nicList) { + $nic = New-Object VMware.Hv.DesktopNetworkInterfaceCardSettings + } + } + #> + } elseIf ($jsonObject.type -eq "MANUAL") { $MANUAL = $true $poolType = 'MANUAL' $userAssignment = $jsonObject.ManualDesktopSpec.userAssignment.userAssignment @@ -3232,13 +3906,67 @@ function New-HVPool { $description = $jsonObject.base.Description $accessGroup = $jsonObject.base.AccessGroup $poolName = $jsonObject.base.name + + <# + # Populate desktop settings + #> + if ($null -ne $jsonObject.DesktopSettings) { + $Enable = $jsonObject.DesktopSettings.enabled + $deleting = $jsonObject.DesktopSettings.deleting + if ($null -ne $jsonObject.DesktopSettings.connectionServerRestrictions) { + $ConnectionServerRestrictions = $jsonObject.DesktopSettings.connectionServerRestrictions + } + if ($poolType -ne 'RDS') { + if ($null -ne $jsonObject.DesktopSettings.logoffSettings) { + $powerPolicy = $jsonObject.DesktopSettings.logoffSettings.powerPolicy + $automaticLogoffPolicy = $jsonObject.DesktopSettings.logoffSettings.automaticLogoffPolicy + if ($null -ne $jsonObject.DesktopSettings.logoffSettings.automaticLogoffMinutes) { + $automaticLogoffMinutes = $jsonObject.DesktopSettings.logoffSettings.automaticLogoffMinutes + } + $allowUsersToResetMachines = $jsonObject.DesktopSettings.logoffSettings.allowUsersToResetMachines + $allowMultipleSessionsPerUser = $jsonObject.DesktopSettings.logoffSettings.allowMultipleSessionsPerUser + $deleteOrRefreshMachineAfterLogoff = $jsonObject.DesktopSettings.logoffSettings.deleteOrRefreshMachineAfterLogoff + $refreshOsDiskAfterLogoff = $jsonObject.DesktopSettings.logoffSettings.refreshOsDiskAfterLogoff + $refreshPeriodDaysForReplicaOsDisk = $jsonObject.DesktopSettings.logoffSettings.refreshPeriodDaysForReplicaOsDisk + $refreshThresholdPercentageForReplicaOsDisk = $jsonObject.DesktopSettings.logoffSettings.refreshThresholdPercentageForReplicaOsDisk + } + + if ($null -ne $jsonObject.DesktopSettings.displayProtocolSettings) { + $supportedDisplayProtocols = $jsonObject.DesktopSettings.displayProtocolSettings.supportedDisplayProtocols + $defaultDisplayProtocol = $jsonObject.DesktopSettings.displayProtocolSettings.defaultDisplayProtocol + $allowUsersToChooseProtocol = $jsonObject.DesktopSettings.displayProtocolSettings.allowUsersToChooseProtocol + if ($null -ne $jsonObject.DesktopSettings.displayProtocolSettings.pcoipDisplaySettings) { + $renderer3D = $jsonObject.DesktopSettings.displayProtocolSettings.pcoipDisplaySettings.renderer3D + $enableGRIDvGPUs = $jsonObject.DesktopSettings.displayProtocolSettings.pcoipDisplaySettings.enableGRIDvGPUs + $vRamSizeMB = $jsonObject.DesktopSettings.displayProtocolSettings.pcoipDisplaySettings.vRamSizeMB + $maxNumberOfMonitors = $jsonObject.DesktopSettings.displayProtocolSettings.pcoipDisplaySettings.maxNumberOfMonitors + $maxResolutionOfAnyOneMonitor = $jsonObject.DesktopSettings.displayProtocolSettings.pcoipDisplaySettings.maxResolutionOfAnyOneMonitor + } + $enableHTMLAccess = $jsonObject.DesktopSettings.displayProtocolSettings.enableHTMLAccess + } + + if ($null -ne $jsonObject.DesktopSettings.mirageConfigurationOverrides) { + $overrideGlobalSetting = $jsonObject.DesktopSettings.mirageConfigurationOverrides.overrideGlobalSetting + $enabled = $jsonObject.DesktopSettings.mirageConfigurationOverrides.enabled + $url = $jsonObject.DesktopSettings.mirageConfigurationOverrides.url + } + } + if ($null -ne $jsonObject.DesktopSettings.flashSettings) { + $quality = $jsonObject.DesktopSettings.flashSettings.quality + $throttling = $jsonObject.DesktopSettings.flashSettings.throttling + } + #desktopsettings ends + } + if ($null -ne $jsonObject.GlobalEntitlementData) { + $globalEntitlement = $jsonObject.GlobalEntitlementData.globalEntitlement + } } if ($PSCmdlet.MyInvocation.ExpectingInput -or $clonePool) { if ($clonePool -and ($clonePool.GetType().name -eq 'DesktopSummaryView')) { $clonePool = Get-HVPool -poolName $clonePool.desktopsummarydata.name - } elseif (!($clonePool -and ($clonePool.GetType().name -eq 'DesktopInfo'))) { + } elseIf (!($clonePool -and ($clonePool.GetType().name -eq 'DesktopInfo'))) { Write-Error "In pipeline did not get object of expected type DesktopSummaryView/DesktopInfo" return } @@ -3255,10 +3983,10 @@ function New-HVPool { $DesktopVirtualCenterProvisioningData = $DesktopVirtualCenterProvisioningSettings.VirtualCenterProvisioningData $DesktopVirtualCenterStorageSettings = $DesktopVirtualCenterProvisioningSettings.VirtualCenterStorageSettings $DesktopVirtualCenterNetworkingSettings = $DesktopVirtualCenterProvisioningSettings.VirtualCenterNetworkingSettings - $desktopVirtualCenterManagedCommonSettings = $clonePool.AutomatedDesktopData.virtualCenterManagedCommonSettings - $desktopCustomizationSettings = $clonePool.AutomatedDesktopData.CustomizationSettings + $DesktopVirtualCenterManagedCommonSettings = $clonePool.AutomatedDesktopData.virtualCenterManagedCommonSettings + $DesktopCustomizationSettings = $clonePool.AutomatedDesktopData.CustomizationSettings } - elseif ($clonePool.ManualDesktopData) { + elseIf ($clonePool.ManualDesktopData) { if (! $VM) { Write-Error "ManualDesktop pool cloning requires list of machines, parameter VM is empty" break @@ -3269,7 +3997,7 @@ function New-HVPool { $desktopVirtualCenterStorageSettings = $clonePool.ManualDesktopData.viewStorageAcceleratorSettings $desktopVirtualCenterManagedCommonSettings = $clonePool.ManualDesktopData.virtualCenterManagedCommonSettings } - elseif($clonePool.RdsDesktopData) { + elseIf($clonePool.RdsDesktopData) { if (! $Farm) { Write-Error "RdsDesktop pool cloning requires farm, parameter Farm is not set" break @@ -3285,16 +4013,16 @@ function New-HVPool { $poolType = 'AUTOMATED' $provisioningType = 'INSTANT_CLONE_ENGINE' } - elseif ($LinkedClone) { + elseIf ($LinkedClone) { $poolType = 'AUTOMATED' $provisioningType = 'VIEW_COMPOSER' } - elseif ($FullClone) { + elseIf ($FullClone) { $poolType = 'AUTOMATED' $provisioningType = 'VIRTUAL_CENTER' } - elseif ($Manual) { $poolType = 'MANUAL' } - elseif ($RDS) { $poolType = 'RDS' } + elseIf ($Manual) { $poolType = 'MANUAL' } + elseIf ($RDS) { $poolType = 'RDS' } } $script:desktopSpecObj = Get-DesktopSpec -poolType $poolType -provisioningType $provisioningType -namingMethod $namingMethod @@ -3355,8 +4083,8 @@ function New-HVPool { { 'RDS' { <# - Query FarmId from Farm Name - #> + Query FarmId from Farm Name + #> $QueryFilterEquals = New-Object VMware.Hv.QueryFilterEquals $QueryFilterEquals.memberName = 'data.name' $QueryFilterEquals.value = $farm @@ -3388,6 +4116,9 @@ function New-HVPool { $machineList = Get-RegisteredPhysicalMachine -services $services -machinesList $VM } $desktopSpecObj.ManualDesktopSpec.Machines = $machineList + if ($desktopUserAssignment) { + $desktopSpecObj.ManualDesktopSpec.userAssignment = $desktopUserAssignment + } } default { if (!$desktopVirtualMachineNamingSpec) { @@ -3431,7 +4162,9 @@ function New-HVPool { try { $desktopVirtualCenterProvisioningData = Get-HVPoolProvisioningData -vc $virtualCenterID -vmObject $desktopVirtualCenterProvisioningData $hostClusterId = $desktopVirtualCenterProvisioningData.HostOrCluster - $desktopVirtualCenterStorageSettings = Get-HVPoolStorageObject -hostclusterID $hostClusterId -storageObject $desktopVirtualCenterStorageSettings + $hostOrCluster_helper = New-Object VMware.Hv.HostOrClusterService + $hostClusterIds = (($hostOrCluster_helper.HostOrCluster_GetHostOrClusterTree($services, $desktopVirtualCenterProvisioningData.datacenter)).treeContainer.children.info).Id + $desktopVirtualCenterStorageSettings = Get-HVPoolStorageObject -hostClusterIds $hostClusterId -storageObject $desktopVirtualCenterStorageSettings $DesktopVirtualCenterNetworkingSettings = Get-HVPoolNetworkSetting -networkObject $DesktopVirtualCenterNetworkingSettings $desktopCustomizationSettings = Get-HVPoolCustomizationSetting -vc $virtualCenterID -customObject $desktopCustomizationSettings } catch { @@ -3440,10 +4173,10 @@ function New-HVPool { break } - if (!$DesktopVirtualCenterProvisioningSettings) { - $desktopSpecObj.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.enableProvisioning = $true - $desktopSpecObj.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.stopProvisioningOnError = $true - $desktopSpecObj.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.minReadyVMsOnVComposerMaintenance = 0 + if (! $DesktopVirtualCenterProvisioningSettings) { + $desktopSpecObj.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.enableProvisioning = $enableProvisioning + $desktopSpecObj.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.stopProvisioningOnError = $stopProvisioningOnError + $desktopSpecObj.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.minReadyVMsOnVComposerMaintenance = $minReady $desktopSpecObj.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.VirtualCenterProvisioningData = $desktopVirtualCenterProvisioningData $desktopSpecObj.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.VirtualCenterStorageSettings = $desktopVirtualCenterStorageSettings $desktopSpecObj.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.VirtualCenterNetworkingSettings = $DesktopVirtualCenterNetworkingSettings @@ -3484,8 +4217,83 @@ function New-HVPool { $desktopSpecObj.base.Description = $description $desktopSpecObj.type = $poolType - if ($desktopSettings) { $desktopSpecObj.DesktopSettings = $desktopSettings } + if (! $desktopSettings) { + $desktopSettingsService = New-Object VMware.Hv.DesktopService + $desktopSettingsHelper = $desktopSettingsService.getDesktopSettingsHelper() + $desktopSettingsHelper.setEnabled($Enable) + $desktopSettingsHelper.setConnectionServerRestrictions($ConnectionServerRestrictions) + #$desktopLogoffSettings = New-Object VMware.Hv.DesktopLogoffSettings + $desktopLogoffSettings = $desktopSettingsService.getDesktopLogoffSettingsHelper() + if ($InstantClone) { + $deleteOrRefreshMachineAfterLogoff = "DELETE" + $powerPolicy = "ALWAYS_POWERED_ON" + } + $desktopLogoffSettings.setPowerPolicy($powerPolicy) + $desktopLogoffSettings.setAutomaticLogoffPolicy($automaticLogoffPolicy) + $desktopLogoffSettings.setAutomaticLogoffMinutes($automaticLogoffMinutes) + $desktopLogoffSettings.setAllowUsersToResetMachines($allowUsersToResetMachines) + $desktopLogoffSettings.setAllowMultipleSessionsPerUser($allowMultipleSessionsPerUser) + $desktopLogoffSettings.setDeleteOrRefreshMachineAfterLogoff($deleteOrRefreshMachineAfterLogoff) + $desktopLogoffSettings.setRefreshOsDiskAfterLogoff($refreshOsDiskAfterLogoff) + $desktopLogoffSettings.setRefreshPeriodDaysForReplicaOsDisk($refreshPeriodDaysForReplicaOsDisk) + if ($refreshThresholdPercentageForReplicaOsDisk -and $refreshOsDiskAfterLogoff -eq "AT_SIZE") { + $desktopLogoffSettings.setRefreshThresholdPercentageForReplicaOsDisk($refreshThresholdPercentageForReplicaOsDisk) + } + if ($poolType -ne 'RDS') { + $desktopSettingsHelper.setLogoffSettings($desktopLogoffSettings.getDataObject()) + + $desktopDisplayProtocolSettings = $desktopSettingsService.getDesktopDisplayProtocolSettingsHelper() + #setSupportedDisplayProtocols is not exists, because this property cannot be updated. + $desktopDisplayProtocolSettings.getDataObject().SupportedDisplayProtocols = $supportedDisplayProtocols + $desktopDisplayProtocolSettings.setDefaultDisplayProtocol($defaultDisplayProtocol) + $desktopDisplayProtocolSettings.setEnableHTMLAccess($enableHTMLAccess) + $desktopDisplayProtocolSettings.setAllowUsersToChooseProtocol($allowUsersToChooseProtocol) + + $desktopPCoIPDisplaySettings = $desktopSettingsService.getDesktopPCoIPDisplaySettingsHelper() + $desktopPCoIPDisplaySettings.setRenderer3D($renderer3D) + #setEnableGRIDvGPUs is not exists, because this property cannot be updated. + $desktopPCoIPDisplaySettings.getDataObject().EnableGRIDvGPUs = $enableGRIDvGPUs + $desktopPCoIPDisplaySettings.setVRamSizeMB($vRamSizeMB) + $desktopPCoIPDisplaySettings.setMaxNumberOfMonitors($maxNumberOfMonitors) + $desktopPCoIPDisplaySettings.setMaxResolutionOfAnyOneMonitor($maxResolutionOfAnyOneMonitor) + $desktopDisplayProtocolSettings.setPcoipDisplaySettings($desktopPCoIPDisplaySettings.getDataObject()) + $desktopSettingsHelper.setDisplayProtocolSettings($desktopDisplayProtocolSettings.getDataObject()) + + $desktopMirageConfigOverrides = $desktopSettingsService.getDesktopMirageConfigurationOverridesHelper() + $desktopMirageConfigOverrides.setEnabled($enabled) + $desktopMirageConfigOverrides.setOverrideGlobalSetting($overrideGlobalSetting) + $desktopMirageConfigOverrides.setUrl($url) + $desktopSettingsHelper.setMirageConfigurationOverrides($desktopMirageConfigOverrides.getDataObject()) + $desktopSettings = $desktopSettingsHelper.getDataObject() + } + $desktopFlashSettings = $desktopSettingsService.getDesktopAdobeFlashSettingsHelper() + $desktopFlashSettings.setQuality($quality) + $desktopFlashSettings.setThrottling($throttling) + $desktopSettingsHelper.setFlashSettings($desktopFlashSettings.getDataObject()) + } + + $desktopSpecObj.DesktopSettings = $desktopSettings + + if ($globalEntitlement) { + $QueryFilterEquals = New-Object VMware.Hv.QueryFilterEquals + $QueryFilterEquals.memberName = 'base.displayName' + $QueryFilterEquals.value = $globalEntitlement + $defn = New-Object VMware.Hv.QueryDefinition + $defn.queryEntityType = 'GlobalEntitlementSummaryView' + $defn.Filter = $QueryFilterEquals + $query_service_helper = New-Object VMware.Hv.QueryServiceService + try { + $queryResults = $query_service_helper.QueryService_Query($services,$defn) + $globalEntitlementid = $queryResults.Results.id + if ($globalEntitlementid.length -eq 1) { + $desktopGlobalEntitlementData = New-Object VMware.Hv.DesktopGlobalEntitlementData -Property @{'globalEntitlement'= $globalEntitlementid;} + } + } + catch { + Write-Host "GlobalEntitlement " $_ + } + } if ($desktopAutomatedDesktopSpec) { $desktopSpecObj.AutomatedDesktopSpec = $desktopAutomatedDesktopSpec } @@ -3495,11 +4303,11 @@ function New-HVPool { # Please uncomment below code, if you want save desktopSpec object to json file <# - $myDebug = convertto-json -InputObject $desktopSpecObj -depth 12 - $myDebug | out-file -filepath c:\temp\copieddesktop.json - #> + $myDebug = convertto-json -InputObject $desktopSpecObj -depth 12 + $myDebug | out-file -filepath c:\temp\copieddesktop.json + #> $desktop_helper = New-Object VMware.Hv.DesktopService - if ($pscmdlet.ShouldProcess($desktopSpecObj)) { + if ($pscmdlet.ShouldProcess($desktopSpecObj.base.name)) { $id = $desktop_helper.Desktop_create($services,$desktopSpecObj) } return $desktopSpecObj @@ -3509,7 +4317,6 @@ function New-HVPool { $desktopSpecObj = $null [System.gc]::collect() } - } function Get-HVPoolProvisioningData { @@ -3530,6 +4337,12 @@ function Get-HVPoolProvisioningData { } $vmObject.Template = $templateVM.id $dataCenterID = $templateVM.datacenter + if ($dataCenter -and $dataCenterID) { + $VmTemplateInfo = $vm_template_helper.VmTemplate_ListByDatacenter($dataCenterID) + if (! ($VmTemplateInfo.Path -like "/$dataCenter/*")) { + throw "$template not exists in datacenter: [$dataCenter]" + } + } $vmObject.datacenter = $dataCenterID } if ($parentVM) { @@ -3541,6 +4354,12 @@ function Get-HVPoolProvisioningData { } $vmObject.ParentVm = $parentVmObj.id $dataCenterID = $parentVmObj.datacenter + if ($dataCenter -and $dataCenterID) { + $baseImageVmInfo = $base_imageVm_helper.BaseImageVm_ListByDatacenter($services,$dataCenterID) + if (! ($baseImageVmInfo.Path -like "/$dataCenter/*")) { + throw "$parentVM not exists in datacenter: [$dataCenter]" + } + } $vmObject.datacenter = $dataCenterID } if ($snapshotVM) { @@ -3559,7 +4378,9 @@ function Get-HVPoolProvisioningData { $folderList += $folders while ($folderList.Length -gt 0) { $item = $folderList[0] - if ($item -and !$_.folderdata.incompatiblereasons.inuse -and !$_.folderdata.incompatiblereasons.viewcomposerreplicafolder -and (($item.folderdata.path -eq $vmFolder) -or ($item.folderdata.name -eq $vmFolder))) { + if ($item -and !$_.folderdata.incompatiblereasons.inuse -and ` + !$_.folderdata.incompatiblereasons.viewcomposerreplicafolder -and ` + (($item.folderdata.path -eq $vmFolder) -or ($item.folderdata.name -eq $vmFolder))) { $vmObject.VmFolder = $item.id break } @@ -3595,26 +4416,54 @@ function Get-HVPoolProvisioningData { function Get-HVPoolStorageObject { param( - [Parameter(Mandatory = $false)] - [VMware.Hv.DesktopVirtualCenterStorageSettings]$StorageObject, - [Parameter(Mandatory = $true)] - [VMware.Hv.HostOrClusterId]$HostClusterID + [VMware.Hv.HostOrClusterId[]]$HostClusterIDs, + + [Parameter(Mandatory = $false)] + [VMware.Hv.DesktopVirtualCenterStorageSettings]$StorageObject ) + $datastoreList = $null if (!$storageObject) { + $datastore_helper = New-Object VMware.Hv.DatastoreService + foreach ($hostClusterID in $hostClusterIDs){ + $datastoreList += $datastore_helper.Datastore_ListDatastoresByHostOrCluster($services,$hostClusterID) + } $storageObject = New-Object VMware.Hv.DesktopVirtualCenterStorageSettings $storageAcceleratorList = @{ - 'useViewStorageAccelerator' = $false + 'useViewStorageAccelerator' = $useViewStorageAccelerator } $desktopViewStorageAcceleratorSettings = New-Object VMware.Hv.DesktopViewStorageAcceleratorSettings -Property $storageAcceleratorList $storageObject.viewStorageAcceleratorSettings = $desktopViewStorageAcceleratorSettings - $desktopSpaceReclamationSettings = New-Object VMware.Hv.DesktopSpaceReclamationSettings -Property @{ 'reclaimVmDiskSpace' = $false } + $desktopSpaceReclamationSettings = New-Object VMware.Hv.DesktopSpaceReclamationSettings -Property @{ 'reclaimVmDiskSpace' = $reclaimVmDiskSpace; 'reclamationThresholdGB' = $reclamationThresholdGB} $desktopPersistentDiskSettings = New-Object VMware.Hv.DesktopPersistentDiskSettings -Property @{ 'redirectWindowsProfile' = $false } $desktopNonPersistentDiskSettings = New-Object VMware.Hv.DesktopNonPersistentDiskSettings -Property @{ 'redirectDisposableFiles' = $false } + if ($LinkedClone) { + if ($blackoutTimes) { + $storageObject.viewStorageAcceleratorSettings.BlackoutTimes = $blackoutTimes + } + if ($useViewStorageAccelerator) { + $storageObject.viewStorageAcceleratorSettings.ViewComposerDiskTypes = $viewComposerDiskTypes + $storageObject.viewStorageAcceleratorSettings.RegenerateViewStorageAcceleratorDays = $regenerateViewStorageAcceleratorDays + } + $desktopPersistentDiskSettings.RedirectWindowsProfile = $redirectWindowsProfile + if ($redirectWindowsProfile) { + $desktopPersistentDiskSettings.UseSeparateDatastoresPersistentAndOSDisks = $useSeparateDatastoresPersistentAndOSDisks + $desktopPersistentDiskSettings.DiskSizeMB = $diskSizeMB + $desktopPersistentDiskSettings.DiskDriveLetter = $diskDriveLetter + } + if ($useSeparateDatastoresPersistentAndOSDisks) { + if ($persistentDiskStorageOvercommit -and ($persistentDiskDatastores.Length -ne $persistentDiskStorageOvercommit.Length) ) { + throw "Parameters persistentDiskDatastores length: [$persistentDiskDatastores.Length] and persistentDiskStorageOvercommit length: [$persistentDiskStorageOvercommit.Length] should be of same size" + } + $desktopPersistentDiskSettings.PersistentDiskDatastores = Get_Datastore -DatastoreInfoList $datastoreList -DatastoreNames $PersistentDiskDatastores -DsStorageOvercommit $persistentDiskStorageOvercommit + } + $desktopNonPersistentDiskSettings.RedirectDisposableFiles = $redirectDisposableFiles + $desktopNonPersistentDiskSettings.DiskSizeMB = $nonPersistentDiskSizeMB + $desktopNonPersistentDiskSettings.DiskDriveLetter = $nonPersistentDiskDriveLetter + } $desktopViewComposerStorageSettingsList = @{ - 'useSeparateDatastoresReplicaAndOSDisks' = $false; - 'useNativeSnapshots' = $false; + 'useNativeSnapshots' = $useNativeSnapshots; 'spaceReclamationSettings' = $desktopSpaceReclamationSettings; 'persistentDiskSettings' = $desktopPersistentDiskSettings; 'nonPersistentDiskSettings' = $desktopNonPersistentDiskSettings @@ -3624,17 +4473,13 @@ function Get-HVPoolStorageObject { } } if ($datastores) { - $datastore_helper = New-Object VMware.Hv.DatastoreService - $datastoreList = $datastore_helper.Datastore_ListDatastoresByHostOrCluster($services,$hostClusterID) - $datastoresSelected = @() - foreach ($ds in $datastores) { - $datastoresSelected += ($datastoreList | Where-Object { ($_.DatastoreData.Path -eq $ds) -or ($_.datastoredata.name -eq $ds) }).id + if ($StorageOvercommit -and ($datastores.Length -ne $StorageOvercommit.Length) ) { + throw "Parameters datastores length: [$datastores.Length] and StorageOvercommit length: [$StorageOvercommit.Length] should be of same size" } - foreach ($ds in $datastoresSelected) { - $myDatastores = New-Object VMware.Hv.DesktopVirtualCenterDatastoreSettings - $myDatastores.Datastore = $ds - $mydatastores.StorageOvercommit = 'UNBOUNDED' - $storageObject.Datastores += $myDatastores + $storageObject.Datastores = Get-HVDatastore -DatastoreInfoList $datastoreList -DatastoreNames $datastores -DsStorageOvercommit $StorageOvercommit + if ($useSeparateDatastoresReplicaAndOSDisks) { + $storageObject.ViewComposerStorageSettings.UseSeparateDatastoresReplicaAndOSDisks = $UseSeparateDatastoresReplicaAndOSDisks + $storageObject.ViewComposerStorageSettings.ReplicaDiskDatastore = ($datastoreInfoList | Where-Object { ($_.datastoredata.name -eq $replicaDiskDatastore) -or ($_.datastoredata.path -eq $replicaDiskDatastore)}).id } } if ($storageObject.Datastores.Count -eq 0) { @@ -3644,6 +4489,40 @@ function Get-HVPoolStorageObject { return $storageObject } +function Get-HVDatastore { + param( + [Parameter(Mandatory = $true)] + [VMware.Hv.DatastoreInfo[]] + $DatastoreInfoList, + + [Parameter(Mandatory = $true)] + [string[]] + $DatastoreNames, + + [Parameter(Mandatory = $false)] + [string[]] + $DsStorageOvercommit + + ) + $datastoresSelected = @() + foreach ($ds in $datastoreNames) { + $datastoresSelected += ($datastoreInfoList | Where-Object { ($_.DatastoreData.Path -eq $ds) -or ($_.datastoredata.name -eq $ds) }).id + } + $Datastores = $null + if (! $DsStorageOvercommit) { + $DsStorageOvercommit += 'UNBOUNDED' + } + $StorageOvercommitCnt = 0 + foreach ($ds in $datastoresSelected) { + $myDatastores = New-Object VMware.Hv.DesktopVirtualCenterDatastoreSettings + $myDatastores.Datastore = $ds + $mydatastores.StorageOvercommit = $DsStorageOvercommit[$StorageOvercommitCnt] + $Datastores += $myDatastores + $StorageOvercommitCnt++ + } + return $Datastores +} + function Get-HVPoolNetworkSetting { param( [Parameter(Mandatory = $false)] @@ -3684,7 +4563,7 @@ function Get-HVPoolCustomizationSetting { $instantCloneEngineDomainAdministrator = ($instantCloneEngineDomainAdministrator_helper.InstantCloneEngineDomainAdministrator_List($services) | Where-Object { $_.namesData.dnsName -match $netBiosName }) if (![string]::IsNullOrWhitespace($domainAdmin)) { $instantCloneEngineDomainAdministrator = ($instantCloneEngineDomainAdministrator | Where-Object { $_.base.userName -eq $domainAdmin }).id - } else { + } elseIf ($null -ne $instantCloneEngineDomainAdministrator) { $instantCloneEngineDomainAdministrator = $instantCloneEngineDomainAdministrator[0].id } if ($null -eq $instantCloneEngineDomainAdministrator) { @@ -3699,7 +4578,7 @@ function Get-HVPoolCustomizationSetting { $ViewComposerDomainAdministratorID = ($viewComposerDomainAdministrator_helper.ViewComposerDomainAdministrator_List($services,$vcID) | Where-Object { $_.base.domain -match $netBiosName }) if (![string]::IsNullOrWhitespace($domainAdmin)) { $ViewComposerDomainAdministratorID = ($ViewComposerDomainAdministratorID | Where-Object { $_.base.userName -ieq $domainAdmin }).id - } else { + } elseIf ($null -ne $ViewComposerDomainAdministratorID) { $ViewComposerDomainAdministratorID = $ViewComposerDomainAdministratorID[0].id } if ($null -eq $ViewComposerDomainAdministratorID) { @@ -3716,14 +4595,15 @@ function Get-HVPoolCustomizationSetting { throw "No Sysprep Customization Spec found with Name: [$sysPrepName]" } $desktopSpecObj.AutomatedDesktopSpec.CustomizationSettings.SysprepCustomizationSettings.CustomizationSpec = $sysPrepIds[0].id - } elseif ($custType -eq 'QUICK_PREP') { + $desktopSpecObj.AutomatedDesktopSpec.CustomizationSettings.ReusePreExistingAccounts = $reusePreExistingAccounts + } elseIf ($custType -eq 'QUICK_PREP') { $desktopSpecObj.AutomatedDesktopSpec.CustomizationSettings.CustomizationType = 'QUICK_PREP' $desktopSpecObj.AutomatedDesktopSpec.CustomizationSettings.QuickprepCustomizationSettings = Get-CustomizationObject } else { throw "The customization type: [$custType] is not supported for LinkedClone Pool" } $desktopSpecObj.AutomatedDesktopSpec.CustomizationSettings.DomainAdministrator = $ViewComposerDomainAdministratorID - } elseif ($FullClone) { + } elseIf ($FullClone) { if ($custType -eq 'SYS_PREP') { $desktopSpecObj.AutomatedDesktopSpec.CustomizationSettings.CustomizationType = 'SYS_PREP' $desktopSpecObj.AutomatedDesktopSpec.CustomizationSettings.SysprepCustomizationSettings = Get-CustomizationObject @@ -3734,7 +4614,7 @@ function Get-HVPoolCustomizationSetting { throw "No Sysprep Customization Spec found with Name: [$sysPrepName]" } $desktopSpecObj.AutomatedDesktopSpec.CustomizationSettings.SysprepCustomizationSettings.CustomizationSpec = $sysPrepIds[0].id - } elseif ($custType -eq 'NONE') { + } elseIf ($custType -eq 'NONE') { $desktopSpecObj.AutomatedDesktopSpec.CustomizationSettings.NoCustomizationSettings = Get-CustomizationObject $desktopSpecObj.AutomatedDesktopSpec.CustomizationSettings.NoCustomizationSettings.DoNotPowerOnVMsAfterCreation = $false $desktopSpecObj.AutomatedDesktopSpec.CustomizationSettings.CustomizationType = "NONE" @@ -3751,7 +4631,7 @@ function Get-HVPoolCustomizationSetting { function Get-CustomizationObject { if ($InstantClone) { return New-Object VMware.Hv.DesktopCloneprepCustomizationSettings - } elseif ($LinkedClone) { + } elseIf ($LinkedClone) { if ($custType -eq 'QUICK_PREP') { return New-Object VMware.Hv.DesktopQuickPrepCustomizationSettings } else { @@ -3790,7 +4670,7 @@ function Get-DesktopSpec { if ($provisioningType -ne 'VIRTUAL_CENTER') { $desktop_spec_helper.getDataObject().AutomatedDesktopSpec.VirtualCenterProvisioningSettings.VirtualCenterStorageSettings.ViewComposerStorageSettings = $desktop_helper.getDesktopViewComposerStorageSettingsHelper().getDataObject() } - } elseif ($poolType -eq 'MANUAL') { + } elseIf ($poolType -eq 'MANUAL') { $desktop_spec_helper.getDataObject().ManualDesktopSpec.userAssignment = $desktop_helper.getDesktopUserAssignmentHelper().getDataObject() $desktop_spec_helper.getDataObject().ManualDesktopSpec.viewStorageAcceleratorSettings = $desktop_helper.getDesktopViewStorageAcceleratorSettingsHelper().getDataObject() $desktop_spec_helper.getDataObject().ManualDesktopSpec.virtualCenterManagedCommonSettings = $desktop_helper.getDesktopVirtualCenterManagedCommonSettingsHelper().getDataObject() @@ -3801,6 +4681,119 @@ function Get-DesktopSpec { } +function Test-HVDesktopSpec { + param( + [Parameter(Mandatory = $true)] + $JsonObject + ) + if ($null -eq $jsonObject.type) { + Throw "Pool type is empty, need to be configure in json file" + } + if ($null -eq $jsonObject.Base.Name) { + Throw "Pool name is empty, need to be configure in json file" + } + if ($null -eq $jsonObject.Base.AccessGroup) { + Throw "AccessGroup of pool is empty, need to be configure in json file" + } + if ($jsonObject.type -eq "AUTOMATED") { + if (! (($jsonObject.AutomatedDesktopSpec.UserAssignment.UserAssignment -eq "FLOATING") -or ($jsonObject.AutomatedDesktopSpec.UserAssignment.UserAssignment -eq "DEDICATED")) ) { + Throw "UserAssignment must be FLOATING or DEDICATED" + } + if ($jsonObject.AutomatedDesktopSpec.ProvisioningType -eq $null) { + Throw "Pool Provisioning type is empty, need to be configure in json file" + } + $provisionTypeArray = @('VIRTUAL_CENTER', 'VIEW_COMPOSER', 'INSTANT_CLONE_ENGINE') + if (! ($provisionTypeArray -contains $jsonObject.AutomatedDesktopSpec.provisioningType)) { + Throw "ProvisioningType of pool is invalid" + } + if ($null -eq $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.EnableProvisioning) { + Throw "Whether to enable provisioning immediately or not, need to configure in json file" + } + if ($null -eq $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.StopProvisioningOnError) { + Throw "Whether to stop provisioning immediately or not on error, need to configure in json file" + } + if ($null -eq $jsonObject.AutomatedDesktopSpec.VmNamingSpec.NamingMethod) { + Throw "Determines how the VMs in the desktop are named, need to configure in json file" + } + if ($null -ne $jsonObject.AutomatedDesktopSpec.VmNamingSpec.NamingMethod) { + $namingMethodArray = @('PATTERN','SPECIFIED') + if (! ($namingMethodArray -contains $jsonObject.AutomatedDesktopSpec.VmNamingSpec.NamingMethod)) { + Throw "NamingMethod property must to be one of these SPECIFIED or PATTERN" + } + if (($null -eq $jsonObject.AutomatedDesktopSpec.VmNamingSpec.patternNamingSettings) -and ($null -eq $jsonObject.AutomatedDesktopSpec.VmNamingSpec.specificNamingSpec)) { + Throw "Naming pattern (or) Specified name settings need to be configure in json file" + } + } + if ($null -eq $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.VirtualCenterStorageSettings.UseVSan) { + Throw "Must specify whether to use virtual SAN or not" + } + $jsonTemplate = $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.virtualCenterProvisioningData.Template + $jsonParentVm = $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.virtualCenterProvisioningData.ParentVm + $jsonSnapshot = $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.virtualCenterProvisioningData.Snapshot + $jsonVmFolder = $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.virtualCenterProvisioningData.VmFolder + $jsonHostOrCluster = $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.virtualCenterProvisioningData.HostOrCluster + $ResourcePool = $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.virtualCenterProvisioningData.ResourcePool + if (! (($null -ne $jsonTemplate) -or (($null -ne $jsonParentVm) -and ($null -ne $jsonSnapshot) ))) { + Throw "Must specify Template or (ParentVm and Snapshot) names in json file" + } + if ($null -eq $jsonVmFolder) { + Throw "Must specify VM folder in json file to deploy the VMs" + } + if ($null -eq $jsonHostOrCluster) { + Throw "Must specify Host or cluster in json file to deploy the VMs" + } + if ($null -eq $resourcePool) { + Throw "Must specify Resource pool in json file to deploy the VMs" + } + if ($null -eq $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.VirtualCenterStorageSettings.Datastores) { + Throw "Must specify datastores names in json file" + } + if ($null -eq $jsonObject.AutomatedDesktopSpec.VirtualCenterManagedCommonSettings.transparentPageSharingScope) { + Throw "Must specify transparent page sharing scope in json file" + } + $jsonCustomizationType = $jsonObject.AutomatedDesktopSpec.CustomizationSettings.CustomizationType + switch ($jsonCustomizationType) { + "NONE" { + if ($null -eq $jsonObject.AutomatedDesktopSpec.CustomizationSettings.noCustomizationSettings) { + Throw "Specify noCustomization Settings in json file" + } + } + "QUICK_PREP" { + if ($null -eq $jsonObject.AutomatedDesktopSpec.CustomizationSettings.quickprepCustomizationSettings) { + Throw "Specify quickprep CustomizationSettings in json file" + } + } + "SYS_PREP" { + if ($null -eq $jsonObject.AutomatedDesktopSpec.CustomizationSettings.sysprepCustomizationSettings) { + Throw "Specify sysprep CustomizationSettings in json file" + } + } + "CLONE_PREP" { + if ($null -eq $jsonObject.AutomatedDesktopSpec.CustomizationSettings.cloneprepCustomizationSettings) { + Throw "Specify ClonePrep CustomizationSettings in json file" + } + } + } + } elseIf ($jsonObject.Type -eq "MANUAL") { + $jsonUserAssignment = $jsonObject.ManualDesktopSpec.UserAssignment.UserAssignment + if (! (($jsonUserAssignment -eq "FLOATING") -or ($jsonUserAssignment -eq "DEDICATED")) ) { + Throw "UserAssignment must be FLOATING or DEDICATED" + } + $jsonSource = @('VIRTUAL_CENTER','UNMANAGED') + if (! ($jsonSource -contains $jsonObject.ManualDesktopSpec.Source)) { + Throw "The Source of machines must be VIRTUAL_CENTER or UNMANAGED" + } + if ($null -eq $jsonObject.ManualDesktopSpec.Machines) { + Throw "Specify list of machines to add to this desktop during creation" + } + } + elseIf ($jsonObject.type -eq "RDS") { + if ($null -eq $jsonObject.RdsDesktopSpec.Farm) { + Throw "Specify Farm needed to create RDS Desktop" + } + } +} + function Remove-HVFarm { <# .SYNOPSIS @@ -3900,7 +4893,7 @@ function Remove-HVFarm { } $farm_service_helper = New-Object VMware.Hv.FarmService foreach ($item in $farmList) { - if ($pscmdlet.ShouldProcess($item)) { + if ($pscmdlet.ShouldProcess($item.Name)) { $farm_service_helper.Farm_Delete($services, $item.id) } Write-Host "Farm Deleted: " $item.Name @@ -4044,7 +5037,7 @@ function Remove-HVPool { } } Write-Host "Deleting Pool: " $item.Name - if ($pscmdlet.ShouldProcess($deleteSpec)) { + if ($pscmdlet.ShouldProcess($item.Name)) { $desktop_service_helper.Desktop_Delete($services,$item.id,$deleteSpec) } } @@ -4177,7 +5170,7 @@ function Set-HVFarm { try { $farmSpecObj = Get-HVFarmSummary -farmName $farmName -hvServer $hvServer } catch { - Write-Error "Make sure Get-HVFarm advanced function is loaded, $_" + Write-Error "Make sure Get-HVFarmSummary advanced function is loaded, $_" break } if ($farmSpecObj) { @@ -4651,7 +5644,7 @@ function Start-HVFarm { $updates = @() $updates += Get-MapEntry -key 'automatedFarmData.virtualCenterProvisioningSettings.virtualCenterProvisioningData.parentVm' -value $spec.ParentVM $updates += Get-MapEntry -key 'automatedFarmData.virtualCenterProvisioningSettings.virtualCenterProvisioningData.snapshot' -value $spec.Snapshot - if ($pscmdlet.ShouldProcess($spec)) { + if ($pscmdlet.ShouldProcess($farmList.$item)) { $farm_service_helper.Farm_Update($services,$item,$updates) $farm_service_helper.Farm_Recompose($services,$item,$spec) } @@ -4939,20 +5932,20 @@ function Start-HVPool { $spec = Get-HVTaskSpec -Source $poolSource.$item -poolName $poolList.$item -operation $operation -taskSpecName 'DesktopRebalanceSpec' -desktopId $item if ($null -ne $spec) { # make sure current task on VMs, must be None - if ($pscmdlet.ShouldProcess($spec)) { + if ($pscmdlet.ShouldProcess($poolList.$item)) { $desktop_helper.Desktop_Rebalance($services,$item,$spec) } - Write-Host "Performed rebalance task on Pool: $PoolList.item" + Write-Host "Performed rebalance task on Pool: $PoolList.$item" } } 'REFRESH' { $spec = Get-HVTaskSpec -Source $poolSource.$item -poolName $poolList.$item -operation $operation -taskSpecName 'DesktopRefreshSpec' -desktopId $item if ($null -ne $spec) { # make sure current task on VMs, must be None - if ($pscmdlet.ShouldProcess($spec)) { + if ($pscmdlet.ShouldProcess($poolList.$item)) { $desktop_helper.Desktop_Refresh($services,$item,$spec) } - Write-Host "Performed refresh task on Pool: $PoolList.item" + Write-Host "Performed refresh task on Pool: $PoolList.$item" } } 'RECOMPOSE' { @@ -4968,10 +5961,10 @@ function Start-HVPool { $updates = @() $updates += Get-MapEntry -key 'automatedDesktopData.virtualCenterProvisioningSettings.virtualCenterProvisioningData.parentVm' -value $spec.ParentVM $updates += Get-MapEntry -key 'automatedDesktopData.virtualCenterProvisioningSettings.virtualCenterProvisioningData.snapshot' -value $spec.Snapshot - if ($pscmdlet.ShouldProcess($spec)) { + if ($pscmdlet.ShouldProcess($poolList.$item)) { $desktop_helper.Desktop_Update($services,$item,$updates) } - Write-Host "Performed recompose task on Pool: $PoolList.item" + Write-Host "Performed recompose task on Pool: $PoolList.$item" } } 'PUSH_IMAGE' { @@ -4986,10 +5979,10 @@ function Start-HVPool { $spec.Settings.LogoffSetting = $logoffSetting $spec.Settings.StopOnFirstError = $stopOnFirstError if ($startTime) { $spec.Settings.startTime = $startTime } - if ($pscmdlet.ShouldProcess($spec)) { + if ($pscmdlet.ShouldProcess($poolList.$item)) { $desktop_helper.Desktop_SchedulePushImage($services,$item,$spec) } - Write-Host "Performed push_image task on Pool: $PoolList.item" + Write-Host "Performed push_image task on Pool: $PoolList.$item" } } 'CANCEL_PUSH_IMAGE' { @@ -4997,10 +5990,10 @@ function Start-HVPool { Write-Error "$poolList.$item is not a INSTANT CLONE pool" break } else { - if ($pscmdlet.ShouldProcess($spec)) { + if ($pscmdlet.ShouldProcess($poolList.$item)) { $desktop_helper.Desktop_CancelScheduledPushImage($services,$item) } - Write-Host "Performed cancel_push_image task on Pool: $PoolList.item" + Write-Host "Performed cancel_push_image task on Pool: $PoolList.$item" } } } From a30d1449742c2fbb8361b7d9e02de0f3e97f3daf Mon Sep 17 00:00:00 2001 From: praveenmathamsetty Date: Mon, 9 Jan 2017 21:11:54 +0530 Subject: [PATCH 004/112] Enhancements to examples, validation and netBiosName 1) Renamed Get-HVDesktopSpec to Get-HVPoolSpec. 2) Enhancements to examples 3) Enhancements to validation for farm and pool 4) ADDomainId handling for farm and pool --- .../VMware.Hv.Helper/VMware.HV.Helper.psm1 | 333 ++++++++++-------- 1 file changed, 184 insertions(+), 149 deletions(-) diff --git a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 index 2a0967b..d388441 100644 --- a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 +++ b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 @@ -176,7 +176,7 @@ The Add-HVDesktop adds virtual machines to already exiting pools by using view A .EXAMPLE Add managed manual VMs to existing manual pool - Add-HVDesktop -PoolName 'ManualPool' -Machines 'manualPool1', 'manualPool2'. + Add-HVDesktop -PoolName 'ManualPool' -Machines 'manualPool1', 'manualPool2' -Confirm:$false .EXAMPLE Add virtual machines to automated specific named dedicated pool @@ -368,7 +368,7 @@ function Add-HVRDSServer { .EXAMPLE Add RDSServers to manual farm - Add-HVRDSServer -Farm "manualFarmTest" -RdsServers "vm-for-rds","vm-for-rds-2" + Add-HVRDSServer -Farm "manualFarmTest" -RdsServers "vm-for-rds","vm-for-rds-2" -Confirm:$false .OUTPUTS None @@ -1915,11 +1915,11 @@ function New-HVFarm { .EXAMPLE Creates new linkedClone farm by using json file - New-HVFarm -Spec C:\VMWare\Specs\LinkedClone.json + New-HVFarm -Spec C:\VMWare\Specs\LinkedClone.json -Confirm:$false .EXAMPLE Creates new manual farm by using rdsServers names - New-HVFarm -Manual -FarmName "manualFarmTest" -FarmDisplayName "manualFarmTest" -Description "Manual PS Test" -RdsServers "vm-for-rds.eng.vmware.com","vm-for-rds-2.eng.vmware.com" + New-HVFarm -Manual -FarmName "manualFarmTest" -FarmDisplayName "manualFarmTest" -Description "Manual PS Test" -RdsServers "vm-for-rds.eng.vmware.com","vm-for-rds-2.eng.vmware.com" -Confirm:$false .OUTPUTS None @@ -2260,7 +2260,7 @@ function New-HVFarm { break } try { - Test-HVFarmSpec -JsonObject $jsonObject + Test-HVFarmSpec -PoolObject $jsonObject } catch { Write-Error "Json object validation failed, $_" break @@ -2522,7 +2522,14 @@ function New-HVFarm { if ($pscmdlet.ShouldProcess($farmSpecObj.data.name)) { $Id = $farm_service_helper.Farm_Create($services, $farmSpecObj) - } + } else { + try { + Test-HVFarmSpec -PoolObject $farmSpecObj + } catch { + Write-Error "FarmSpec object validation failed, $_" + break + } + } return $farmSpecObj } @@ -2535,77 +2542,77 @@ function New-HVFarm { function Test-HVFarmSpec { param( [Parameter(Mandatory = $true)] - $JsonObject + $PoolObject ) - if ($null -eq $jsonObject.Type) { - Throw "Specify type of farm in json file" + if ($null -eq $PoolObject.Type) { + Throw "Specify type of farm" } $jsonFarmTypeArray = @('AUTOMATED','MANUAL') - if (! ($jsonFarmTypeArray -contains $jsonObject.Type)) { + if (! ($jsonFarmTypeArray -contains $PoolObject.Type)) { Throw "Farm type must be AUTOMATED or MANUAL" } - if ($null -eq $jsonObject.Data.Name) { - Throw "Specify farm name in json file" + if ($null -eq $PoolObject.Data.Name) { + Throw "Specify farm name" } - if ($null -eq $jsonObject.Data.AccessGroup) { - Throw "Specify horizon access group in json file" + if ($null -eq $PoolObject.Data.AccessGroup) { + Throw "Specify horizon access group" } - if ($jsonObject.Type -eq "AUTOMATED"){ - $jsonProvisioningType = $jsonObject.AutomatedFarmSpec.ProvisioningType + if ($PoolObject.Type -eq "AUTOMATED"){ + $jsonProvisioningType = $PoolObject.AutomatedFarmSpec.ProvisioningType if ($null -eq $jsonProvisioningType) { - Throw "Must specify provisioningType in json file" + Throw "Must specify provisioningType" } - if ($null -eq $jsonObject.AutomatedFarmSpec.RdsServerNamingSpec.namingMethod) { - Throw "Must specify naming method to PATTERN in json file" + if ($null -eq $PoolObject.AutomatedFarmSpec.RdsServerNamingSpec.namingMethod) { + Throw "Must specify naming method to PATTERN" } - if ($null -eq $jsonObject.AutomatedFarmSpec.RdsServerNamingSpec.patternNamingSettings) { - Throw "Specify Naming pattern settings in json file" + if ($null -eq $PoolObject.AutomatedFarmSpec.RdsServerNamingSpec.patternNamingSettings) { + Throw "Specify Naming pattern settings" } - if ($null -eq $jsonObject.AutomatedFarmSpec.RdsServerNamingSpec.patternNamingSettings.namingPattern) { - Throw "Specify specified naming pattern in json file" + if ($null -eq $PoolObject.AutomatedFarmSpec.RdsServerNamingSpec.patternNamingSettings.namingPattern) { + Throw "Specify specified naming pattern" } - if ($null -eq $jsonObject.AutomatedFarmSpec.virtualCenterProvisioningSettings.enableProvisioning) { - Throw "Specify Whether to enable provisioning in json file" + if ($null -eq $PoolObject.AutomatedFarmSpec.virtualCenterProvisioningSettings.enableProvisioning) { + Throw "Specify Whether to enable provisioning or not" } - if ($null -eq $jsonObject.AutomatedFarmSpec.virtualCenterProvisioningSettings.stopProvisioningOnError) { - Throw "Specify Whether provisioning on all VMs stops on error in json file" + if ($null -eq $PoolObject.AutomatedFarmSpec.virtualCenterProvisioningSettings.stopProvisioningOnError) { + Throw "Specify Whether provisioning on all VMs stops on error" } - $jsonTemplate = $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.virtualCenterProvisioningData.Template - $jsonParentVm = $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.virtualCenterProvisioningData.ParentVm - $jsonSnapshot = $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.virtualCenterProvisioningData.Snapshot - $jsonVmFolder = $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.virtualCenterProvisioningData.VmFolder - $jsonHostOrCluster = $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.virtualCenterProvisioningData.HostOrCluster - $ResourcePool = $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.virtualCenterProvisioningData.ResourcePool + $jsonTemplate = $PoolObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.virtualCenterProvisioningData.Template + $jsonParentVm = $PoolObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.virtualCenterProvisioningData.ParentVm + $jsonSnapshot = $PoolObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.virtualCenterProvisioningData.Snapshot + $jsonVmFolder = $PoolObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.virtualCenterProvisioningData.VmFolder + $jsonHostOrCluster = $PoolObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.virtualCenterProvisioningData.HostOrCluster + $ResourcePool = $PoolObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.virtualCenterProvisioningData.ResourcePool if (!( ($null -ne $jsonTemplate) -or (($null -ne $jsonParentVm) -and ($null -ne $jsonSnapshot) )) ) { - Throw "Must specify Template or (ParentVm and Snapshot) names in json file" + Throw "Must specify Template or (ParentVm and Snapshot) names" } if ($null -eq $jsonVmFolder) { - Throw "Must specify VM folder in json file to deploy the VMs" + Throw "Must specify VM folder to deploy the VMs" } if ($null -eq $jsonHostOrCluster) { - Throw "Must specify Host or cluster in json file to deploy the VMs" + Throw "Must specify Host or cluster to deploy the VMs" } if ($null -eq $resourcePool) { - Throw "Must specify Resource pool in json file to deploy the VMs" + Throw "Must specify Resource pool to deploy the VMs" } - if ($null -eq $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.VirtualCenterStorageSettings.Datastores) { - Throw "Must specify datastores names in json file" + if ($null -eq $PoolObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.VirtualCenterStorageSettings.Datastores) { + Throw "Must specify datastores names" } - if ($null -eq $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.VirtualCenterStorageSettings.useVSan) { + if ($null -eq $PoolObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.VirtualCenterStorageSettings.useVSan) { Throw "Must specify whether to use virtual SAN or not" } - if ($null -eq $jsonObject.AutomatedFarmSpec.CustomizationSettings.customizationType) { - Throw "Specify Type of customization to use in json file" + if ($null -eq $PoolObject.AutomatedFarmSpec.CustomizationSettings.customizationType) { + Throw "Specify customization type" } - if ($null -eq $jsonObject.AutomatedFarmSpec.CustomizationSettings.SysprepCustomizationSettings) { - Throw "Specify Sysprep customization settings in json file" + if ($null -eq $PoolObject.AutomatedFarmSpec.CustomizationSettings.SysprepCustomizationSettings) { + Throw "Specify sysPrep customization settings" } - if ($null -eq $jsonObject.AutomatedFarmSpec.RdsServerMaxSessionsData.MaxSessionsType) { - Throw "Specify MaxSessionsType in json file" + if ($null -eq $PoolObject.AutomatedFarmSpec.RdsServerMaxSessionsData.MaxSessionsType) { + Throw "Specify MaxSessionsType" } - } elseif ($jsonObject.Type -eq "MANUAL") { - if ($null -eq $jsonObject.manualFarmSpec.rdsServers) { - Throw "Specify rdsServers name in json file" + } elseif ($PoolObject.Type -eq "MANUAL") { + if ($null -eq $PoolObject.manualFarmSpec.rdsServers) { + Throw "Specify rdsServers name" } } } @@ -2788,9 +2795,17 @@ function Get-HVFarmCustomizationSetting { throw "No Composer Domain Administrator found with netBiosName: [$netBiosName]" } $ADDomain_service_helper = New-Object VMware.Hv.ADDomainService - $adDomianId = ($ADDomain_service_helper.ADDomain_List($services) | Where-Object { $_.NetBiosName -eq $netBiosName } | Select-Object -Property id) - if ($null -eq $adDomianId) { - throw "No Domain found with netBiosName: [$netBiosName]" + $ADDomains = $ADDomain_service_helper.ADDomain_List($services) + if ($netBiosName) { + $adDomianId = ( $ADDomains| Where-Object { $_.NetBiosName -eq $netBiosName } | Select-Object -Property id) + if ($null -eq $adDomianId) { + throw "No Domain found with netBiosName: [$netBiosName]" + } + } else { + $adDomianId = ( $ADDomains[0] | Select-Object -Property id) + if ($null -eq $adDomianId) { + throw "No Domain configured in view administrator UI" + } } $ad_containder_service_helper = New-Object VMware.Hv.AdContainerService $adContainerId = ($ad_containder_service_helper.ADContainer_ListByDomain($services, $adDomianId.id) | Where-Object { $_.Rdn -eq $adContainer } | Select-Object -Property id).id @@ -3055,7 +3070,7 @@ function New-HVPool { .EXAMPLE Create new automated linked clone pool by using JSON spec file - New-HVPool -Spec C:\VMWare\Specs\LinkedClone.json + New-HVPool -Spec C:\VMWare\Specs\LinkedClone.json -Confirm:$false .EXAMPLE Clones new pool by using existing pool configuration @@ -3729,7 +3744,8 @@ function New-HVPool { } try { - Test-HVDesktopSpec -JsonObject $jsonObject + #Json object validation + Test-HVPoolSpec -PoolObject $jsonObject } catch { Write-Error "Json object validation failed, $_" break @@ -3765,21 +3781,20 @@ function New-HVPool { $FullClone = $true } switch ($custType) { - 'SYS_PREP' { - $sysprepCustomizationSettings = $jsonObject.AutomatedDesktopSpec.CustomizationSettings.SysprepCustomizationSettings - $sysPrepName = $sysprepCustomizationSettings.customizationSpec - $reusePreExistingAccounts = $jsonObject.AutomatedDesktopSpec.CustomizationSettings.reusePreExistingAccounts - } - 'QUICK_PREP' { - $powerOffScriptName= $jsonObject.AutomatedDesktopSpec.CustomizationSettings.QuickprepCustomizationSettings.PowerOffScriptName - $powerOffScriptParameters = $jsonObject.AutomatedDesktopSpec.CustomizationSettings.QuickprepCustomizationSettings.PowerOffScriptParameters - $postSynchronizationScriptName = $jsonObject.AutomatedDesktopSpec.CustomizationSettings.QuickprepCustomizationSettings.PostSynchronizationScriptName - $postSynchronizationScriptParameters = $jsonObject.AutomatedDesktopSpec.CustomizationSettings.QuickprepCustomizationSettings.PostSynchronizationScriptParameters - } - 'NONE' { - $doNotPowerOnVMsAfterCreation = $jsonObject.AutomatedDesktopSpec.CustomizationSettings.NoCustomizationSettings.DoNotPowerOnVMsAfterCreation - } - + 'SYS_PREP' { + $sysprepCustomizationSettings = $jsonObject.AutomatedDesktopSpec.CustomizationSettings.SysprepCustomizationSettings + $sysPrepName = $sysprepCustomizationSettings.customizationSpec + $reusePreExistingAccounts = $jsonObject.AutomatedDesktopSpec.CustomizationSettings.reusePreExistingAccounts + } + 'QUICK_PREP' { + $powerOffScriptName= $jsonObject.AutomatedDesktopSpec.CustomizationSettings.QuickprepCustomizationSettings.PowerOffScriptName + $powerOffScriptParameters = $jsonObject.AutomatedDesktopSpec.CustomizationSettings.QuickprepCustomizationSettings.PowerOffScriptParameters + $postSynchronizationScriptName = $jsonObject.AutomatedDesktopSpec.CustomizationSettings.QuickprepCustomizationSettings.PostSynchronizationScriptName + $postSynchronizationScriptParameters = $jsonObject.AutomatedDesktopSpec.CustomizationSettings.QuickprepCustomizationSettings.PostSynchronizationScriptParameters + } + 'NONE' { + $doNotPowerOnVMsAfterCreation = $jsonObject.AutomatedDesktopSpec.CustomizationSettings.NoCustomizationSettings.DoNotPowerOnVMsAfterCreation + } } } $namingMethod = $jsonObject.AutomatedDesktopSpec.VmNamingSpec.NamingMethod @@ -3927,8 +3942,12 @@ function New-HVPool { $allowMultipleSessionsPerUser = $jsonObject.DesktopSettings.logoffSettings.allowMultipleSessionsPerUser $deleteOrRefreshMachineAfterLogoff = $jsonObject.DesktopSettings.logoffSettings.deleteOrRefreshMachineAfterLogoff $refreshOsDiskAfterLogoff = $jsonObject.DesktopSettings.logoffSettings.refreshOsDiskAfterLogoff - $refreshPeriodDaysForReplicaOsDisk = $jsonObject.DesktopSettings.logoffSettings.refreshPeriodDaysForReplicaOsDisk - $refreshThresholdPercentageForReplicaOsDisk = $jsonObject.DesktopSettings.logoffSettings.refreshThresholdPercentageForReplicaOsDisk + if ($jsonObject.DesktopSettings.logoffSettings.refreshPeriodDaysForReplicaOsDisk) { + $refreshPeriodDaysForReplicaOsDisk = $jsonObject.DesktopSettings.logoffSettings.refreshPeriodDaysForReplicaOsDisk + } + if ($jsonObject.DesktopSettings.logoffSettings.refreshThresholdPercentageForReplicaOsDisk) { + $refreshThresholdPercentageForReplicaOsDisk = $jsonObject.DesktopSettings.logoffSettings.refreshThresholdPercentageForReplicaOsDisk + } } if ($null -ne $jsonObject.DesktopSettings.displayProtocolSettings) { @@ -3938,7 +3957,9 @@ function New-HVPool { if ($null -ne $jsonObject.DesktopSettings.displayProtocolSettings.pcoipDisplaySettings) { $renderer3D = $jsonObject.DesktopSettings.displayProtocolSettings.pcoipDisplaySettings.renderer3D $enableGRIDvGPUs = $jsonObject.DesktopSettings.displayProtocolSettings.pcoipDisplaySettings.enableGRIDvGPUs - $vRamSizeMB = $jsonObject.DesktopSettings.displayProtocolSettings.pcoipDisplaySettings.vRamSizeMB + if ($jsonObject.DesktopSettings.displayProtocolSettings.pcoipDisplaySettings.vRamSizeMB) { + $vRamSizeMB = $jsonObject.DesktopSettings.displayProtocolSettings.pcoipDisplaySettings.vRamSizeMB + } $maxNumberOfMonitors = $jsonObject.DesktopSettings.displayProtocolSettings.pcoipDisplaySettings.maxNumberOfMonitors $maxResolutionOfAnyOneMonitor = $jsonObject.DesktopSettings.displayProtocolSettings.pcoipDisplaySettings.maxResolutionOfAnyOneMonitor } @@ -3947,8 +3968,12 @@ function New-HVPool { if ($null -ne $jsonObject.DesktopSettings.mirageConfigurationOverrides) { $overrideGlobalSetting = $jsonObject.DesktopSettings.mirageConfigurationOverrides.overrideGlobalSetting - $enabled = $jsonObject.DesktopSettings.mirageConfigurationOverrides.enabled - $url = $jsonObject.DesktopSettings.mirageConfigurationOverrides.url + if ($jsonObject.DesktopSettings.mirageConfigurationOverrides.enabled) { + $enabled = $jsonObject.DesktopSettings.mirageConfigurationOverrides.enabled + } + if ($jsonObject.DesktopSettings.mirageConfigurationOverrides.url) { + $url = $jsonObject.DesktopSettings.mirageConfigurationOverrides.url + } } } if ($null -ne $jsonObject.DesktopSettings.flashSettings) { @@ -4309,7 +4334,15 @@ function New-HVPool { $desktop_helper = New-Object VMware.Hv.DesktopService if ($pscmdlet.ShouldProcess($desktopSpecObj.base.name)) { $id = $desktop_helper.Desktop_create($services,$desktopSpecObj) - } + } else { + try { + #DesktopSpec validation + Test-HVPoolSpec -PoolObject $desktopSpecObj + } catch { + Write-Error "DesktopSpec object validation failed, $_" + break + } + } return $desktopSpecObj } @@ -4354,12 +4387,6 @@ function Get-HVPoolProvisioningData { } $vmObject.ParentVm = $parentVmObj.id $dataCenterID = $parentVmObj.datacenter - if ($dataCenter -and $dataCenterID) { - $baseImageVmInfo = $base_imageVm_helper.BaseImageVm_ListByDatacenter($services,$dataCenterID) - if (! ($baseImageVmInfo.Path -like "/$dataCenter/*")) { - throw "$parentVM not exists in datacenter: [$dataCenter]" - } - } $vmObject.datacenter = $dataCenterID } if ($snapshotVM) { @@ -4546,9 +4573,17 @@ function Get-HVPoolCustomizationSetting { # View Composer and Instant Clone Engine Active Directory container for QuickPrep and ClonePrep. This must be set for Instant Clone Engine or SVI sourced desktops. if ($InstantClone -or $LinkedClone) { $ad_domain_helper = New-Object VMware.Hv.ADDomainService - $adDomianId = ($ad_domain_helper.ADDomain_List($services) | Where-Object { $_.NetBiosName -eq $netBiosName } | Select-Object -Property id) - if ($null -eq $adDomianId) { - throw "No Domain found with netBiosName: [$netBiosName]" + $ADDomains = $ad_domain_helper.ADDomain_List($services) + if ($netBiosName) { + $adDomianId = ($ADDomains | Where-Object { $_.NetBiosName -eq $netBiosName } | Select-Object -Property id) + if ($null -eq $adDomianId) { + throw "No Domain found with netBiosName: [$netBiosName]" + } + } else { + $adDomianId = ($ADDomains[0] | Select-Object -Property id) + if ($null -eq $adDomianId) { + throw "No Domain configured in view administrator UI" + } } $ad_container_helper = New-Object VMware.Hv.AdContainerService $adContainerId = ($ad_container_helper.ADContainer_ListByDomain($services,$adDomianId.id) | Where-Object { $_.Rdn -eq $adContainer } | Select-Object -Property id).id @@ -4681,115 +4716,115 @@ function Get-DesktopSpec { } -function Test-HVDesktopSpec { +function Test-HVPoolSpec { param( [Parameter(Mandatory = $true)] - $JsonObject + $PoolObject ) - if ($null -eq $jsonObject.type) { - Throw "Pool type is empty, need to be configure in json file" + if ($null -eq $PoolObject.type) { + Throw "Pool type is empty, need to be configured" } - if ($null -eq $jsonObject.Base.Name) { - Throw "Pool name is empty, need to be configure in json file" + if ($null -eq $PoolObject.Base.Name) { + Throw "Pool name is empty, need to be configured" } - if ($null -eq $jsonObject.Base.AccessGroup) { - Throw "AccessGroup of pool is empty, need to be configure in json file" + if ($null -eq $PoolObject.Base.AccessGroup) { + Throw "AccessGroup of pool is empty, need to be configured" } - if ($jsonObject.type -eq "AUTOMATED") { - if (! (($jsonObject.AutomatedDesktopSpec.UserAssignment.UserAssignment -eq "FLOATING") -or ($jsonObject.AutomatedDesktopSpec.UserAssignment.UserAssignment -eq "DEDICATED")) ) { + if ($PoolObject.type -eq "AUTOMATED") { + if (! (($PoolObject.AutomatedDesktopSpec.UserAssignment.UserAssignment -eq "FLOATING") -or ($PoolObject.AutomatedDesktopSpec.UserAssignment.UserAssignment -eq "DEDICATED")) ) { Throw "UserAssignment must be FLOATING or DEDICATED" } - if ($jsonObject.AutomatedDesktopSpec.ProvisioningType -eq $null) { - Throw "Pool Provisioning type is empty, need to be configure in json file" + if ($PoolObject.AutomatedDesktopSpec.ProvisioningType -eq $null) { + Throw "Pool Provisioning type is empty, need to be configured" } $provisionTypeArray = @('VIRTUAL_CENTER', 'VIEW_COMPOSER', 'INSTANT_CLONE_ENGINE') - if (! ($provisionTypeArray -contains $jsonObject.AutomatedDesktopSpec.provisioningType)) { + if (! ($provisionTypeArray -contains $PoolObject.AutomatedDesktopSpec.provisioningType)) { Throw "ProvisioningType of pool is invalid" } - if ($null -eq $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.EnableProvisioning) { - Throw "Whether to enable provisioning immediately or not, need to configure in json file" + if ($null -eq $PoolObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.EnableProvisioning) { + Throw "Whether to enable provisioning immediately or not, need to be configured" } - if ($null -eq $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.StopProvisioningOnError) { - Throw "Whether to stop provisioning immediately or not on error, need to configure in json file" + if ($null -eq $PoolObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.StopProvisioningOnError) { + Throw "Whether to stop provisioning immediately or not on error, need to be configured" } - if ($null -eq $jsonObject.AutomatedDesktopSpec.VmNamingSpec.NamingMethod) { - Throw "Determines how the VMs in the desktop are named, need to configure in json file" + if ($null -eq $PoolObject.AutomatedDesktopSpec.VmNamingSpec.NamingMethod) { + Throw "Determines how the VMs in the desktop are named, need to be configured" } - if ($null -ne $jsonObject.AutomatedDesktopSpec.VmNamingSpec.NamingMethod) { + if ($null -ne $PoolObject.AutomatedDesktopSpec.VmNamingSpec.NamingMethod) { $namingMethodArray = @('PATTERN','SPECIFIED') - if (! ($namingMethodArray -contains $jsonObject.AutomatedDesktopSpec.VmNamingSpec.NamingMethod)) { + if (! ($namingMethodArray -contains $PoolObject.AutomatedDesktopSpec.VmNamingSpec.NamingMethod)) { Throw "NamingMethod property must to be one of these SPECIFIED or PATTERN" } - if (($null -eq $jsonObject.AutomatedDesktopSpec.VmNamingSpec.patternNamingSettings) -and ($null -eq $jsonObject.AutomatedDesktopSpec.VmNamingSpec.specificNamingSpec)) { - Throw "Naming pattern (or) Specified name settings need to be configure in json file" + if (($null -eq $PoolObject.AutomatedDesktopSpec.VmNamingSpec.patternNamingSettings) -and ($null -eq $PoolObject.AutomatedDesktopSpec.VmNamingSpec.specificNamingSpec)) { + Throw "Naming pattern (or) Specified name settings need to be configured" } } - if ($null -eq $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.VirtualCenterStorageSettings.UseVSan) { + if ($null -eq $PoolObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.VirtualCenterStorageSettings.UseVSan) { Throw "Must specify whether to use virtual SAN or not" } - $jsonTemplate = $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.virtualCenterProvisioningData.Template - $jsonParentVm = $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.virtualCenterProvisioningData.ParentVm - $jsonSnapshot = $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.virtualCenterProvisioningData.Snapshot - $jsonVmFolder = $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.virtualCenterProvisioningData.VmFolder - $jsonHostOrCluster = $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.virtualCenterProvisioningData.HostOrCluster - $ResourcePool = $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.virtualCenterProvisioningData.ResourcePool + $jsonTemplate = $PoolObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.virtualCenterProvisioningData.Template + $jsonParentVm = $PoolObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.virtualCenterProvisioningData.ParentVm + $jsonSnapshot = $PoolObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.virtualCenterProvisioningData.Snapshot + $jsonVmFolder = $PoolObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.virtualCenterProvisioningData.VmFolder + $jsonHostOrCluster = $PoolObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.virtualCenterProvisioningData.HostOrCluster + $ResourcePool = $PoolObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.virtualCenterProvisioningData.ResourcePool if (! (($null -ne $jsonTemplate) -or (($null -ne $jsonParentVm) -and ($null -ne $jsonSnapshot) ))) { - Throw "Must specify Template or (ParentVm and Snapshot) names in json file" + Throw "Must specify Template or (ParentVm and Snapshot) names" } if ($null -eq $jsonVmFolder) { - Throw "Must specify VM folder in json file to deploy the VMs" + Throw "Must specify VM folder to deploy the VMs" } if ($null -eq $jsonHostOrCluster) { - Throw "Must specify Host or cluster in json file to deploy the VMs" + Throw "Must specify HostOrCluster to deploy the VMs" } if ($null -eq $resourcePool) { - Throw "Must specify Resource pool in json file to deploy the VMs" + Throw "Must specify Resource pool to deploy the VMs" } - if ($null -eq $jsonObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.VirtualCenterStorageSettings.Datastores) { - Throw "Must specify datastores names in json file" + if ($null -eq $PoolObject.AutomatedDesktopSpec.VirtualCenterProvisioningSettings.VirtualCenterStorageSettings.Datastores) { + Throw "Must specify datastores names" } - if ($null -eq $jsonObject.AutomatedDesktopSpec.VirtualCenterManagedCommonSettings.transparentPageSharingScope) { - Throw "Must specify transparent page sharing scope in json file" + if ($null -eq $PoolObject.AutomatedDesktopSpec.VirtualCenterManagedCommonSettings.transparentPageSharingScope) { + Throw "Must specify transparent page sharing scope" } - $jsonCustomizationType = $jsonObject.AutomatedDesktopSpec.CustomizationSettings.CustomizationType + $jsonCustomizationType = $PoolObject.AutomatedDesktopSpec.CustomizationSettings.CustomizationType switch ($jsonCustomizationType) { "NONE" { - if ($null -eq $jsonObject.AutomatedDesktopSpec.CustomizationSettings.noCustomizationSettings) { - Throw "Specify noCustomization Settings in json file" + if ($null -eq $PoolObject.AutomatedDesktopSpec.CustomizationSettings.noCustomizationSettings) { + Throw "Specify noCustomization Settings" } } "QUICK_PREP" { - if ($null -eq $jsonObject.AutomatedDesktopSpec.CustomizationSettings.quickprepCustomizationSettings) { - Throw "Specify quickprep CustomizationSettings in json file" + if ($null -eq $PoolObject.AutomatedDesktopSpec.CustomizationSettings.quickprepCustomizationSettings) { + Throw "Specify quickPrep customizationSettings" } } "SYS_PREP" { - if ($null -eq $jsonObject.AutomatedDesktopSpec.CustomizationSettings.sysprepCustomizationSettings) { - Throw "Specify sysprep CustomizationSettings in json file" + if ($null -eq $PoolObject.AutomatedDesktopSpec.CustomizationSettings.sysprepCustomizationSettings) { + Throw "Specify sysPrep customizationSettings" } } "CLONE_PREP" { - if ($null -eq $jsonObject.AutomatedDesktopSpec.CustomizationSettings.cloneprepCustomizationSettings) { - Throw "Specify ClonePrep CustomizationSettings in json file" + if ($null -eq $PoolObject.AutomatedDesktopSpec.CustomizationSettings.cloneprepCustomizationSettings) { + Throw "Specify clonePrep customizationSettings" } } } - } elseIf ($jsonObject.Type -eq "MANUAL") { - $jsonUserAssignment = $jsonObject.ManualDesktopSpec.UserAssignment.UserAssignment + } elseIf ($PoolObject.Type -eq "MANUAL") { + $jsonUserAssignment = $PoolObject.ManualDesktopSpec.UserAssignment.UserAssignment if (! (($jsonUserAssignment -eq "FLOATING") -or ($jsonUserAssignment -eq "DEDICATED")) ) { Throw "UserAssignment must be FLOATING or DEDICATED" } $jsonSource = @('VIRTUAL_CENTER','UNMANAGED') - if (! ($jsonSource -contains $jsonObject.ManualDesktopSpec.Source)) { + if (! ($jsonSource -contains $PoolObject.ManualDesktopSpec.Source)) { Throw "The Source of machines must be VIRTUAL_CENTER or UNMANAGED" } - if ($null -eq $jsonObject.ManualDesktopSpec.Machines) { - Throw "Specify list of machines to add to this desktop during creation" + if ($null -eq $PoolObject.ManualDesktopSpec.Machines) { + Throw "Specify list of virtual machines to be added to this pool" } } - elseIf ($jsonObject.type -eq "RDS") { - if ($null -eq $jsonObject.RdsDesktopSpec.Farm) { - Throw "Specify Farm needed to create RDS Desktop" + elseIf ($PoolObject.type -eq "RDS") { + if ($null -eq $PoolObject.RdsDesktopSpec.Farm) { + Throw "Specify farm needed to create RDS desktop" } } } @@ -4813,7 +4848,7 @@ function Remove-HVFarm { .EXAMPLE Delete a given farm. For an automated farm, all the RDS Server VMs are deleted from disk whereas for a manual farm only the RDS Server associations are removed. - Remove-HVFarm -FarmName 'Farm-01' -HvServer $hvServer + Remove-HVFarm -FarmName 'Farm-01' -HvServer $hvServer -Confirm:$false .EXAMPLE Deletes a given Farm object(s). For an automated farm, all the RDS Server VMs are deleted from disk whereas for a manual farm only the RDS Server associations are removed. @@ -4930,7 +4965,7 @@ function Remove-HVPool { .EXAMPLE Deletes pool from disk with given parameters PoolName etc. - Remove-HVPool -HvServer $hvServer -PoolName 'FullClone' -DeleteFromDisk + Remove-HVPool -HvServer $hvServer -PoolName 'FullClone' -DeleteFromDisk -Confirm:$false .EXAMPLE Deletes specified pool from disk @@ -5088,7 +5123,7 @@ function Set-HVFarm { .EXAMPLE Updates farm configuration by using json file - Set-HVFarm -FarmName 'Farm-01' -Spec 'C:\Edit-HVFarm\ManualEditFarm.json' + Set-HVFarm -FarmName 'Farm-01' -Spec 'C:\Edit-HVFarm\ManualEditFarm.json' -Confirm:$false .EXAMPLE Updates farm configuration with given parameters key and value @@ -5290,7 +5325,7 @@ function Set-HVPool { .EXAMPLE Updates pool configuration by using json file - Set-HVPool -PoolName 'ManualPool' -Spec 'C:\Edit-HVPool\EditPool.json' + Set-HVPool -PoolName 'ManualPool' -Spec 'C:\Edit-HVPool\EditPool.json' -Confirm:$false .EXAMPLE Updates pool configuration with given parameters key and value @@ -5500,7 +5535,7 @@ function Start-HVFarm { .EXAMPLE Requests a recompose of RDS Servers in the specified automated farm - Start-HVFarm -Recompose -Farm 'Farm-01' -LogoffSetting FORCE_LOGOFF -ParentVM 'View-Agent-Win8' -SnapshotVM 'Snap_USB' + Start-HVFarm -Recompose -Farm 'Farm-01' -LogoffSetting FORCE_LOGOFF -ParentVM 'View-Agent-Win8' -SnapshotVM 'Snap_USB' -Confirm:$false .EXAMPLE Requests a recompose task for automated farm in specified time @@ -5776,7 +5811,7 @@ function Start-HVPool { .EXAMPLE Requests a refresh of machines in the specified pool - Start-HVPool -Refresh -Pool 'LCPool3' -LogoffSetting FORCE_LOGOFF + Start-HVPool -Refresh -Pool 'LCPool3' -LogoffSetting FORCE_LOGOFF -Confirm:$false .EXAMPLE Requests a rebalance of machines in a pool with specified time @@ -6414,7 +6449,7 @@ function Get-HVMachineSummary { return $machineList } -function Get-HVDesktopSpec { +function Get-HVPoolSpec { <# .Synopsis Gets desktop specification @@ -6431,11 +6466,11 @@ function Get-HVDesktopSpec { .EXAMPLE Converts DesktopInfo to DesktopSpec - Get-HVDesktopSpec -DesktopInfo $DesktopInfoObj + Get-HVPoolSpec -DesktopInfo $DesktopInfoObj .EXAMPLE Converts DesktopInfo to DesktopSpec and also dumps json object - Get-HVPool -PoolName 'LnkClnJson' | Get-HVDesktopSpec -FilePath "C:\temp\LnkClnJson.json" + Get-HVPool -PoolName 'LnkClnJson' | Get-HVPoolSpec -FilePath "C:\temp\LnkClnJson.json" .OUTPUTS Returns desktop specification @@ -6763,4 +6798,4 @@ function Get-HVInternalName { } } -Export-ModuleMember Add-HVDesktop,Add-HVRDSServer,Connect-HVEvent,Disconnect-HVEvent,Get-HVDesktopSpec,Get-HVInternalName, Get-HVEvent,Get-HVFarm,Get-HVFarmSummary,Get-HVPool,Get-HVPoolSummary,Get-HVMachine,Get-HVMachineSummary,Get-HVQueryResult,Get-HVQueryFilter,New-HVFarm,New-HVPool,Remove-HVFarm,Remove-HVPool,Set-HVFarm,Set-HVPool,Start-HVFarm,Start-HVPool +Export-ModuleMember Add-HVDesktop,Add-HVRDSServer,Connect-HVEvent,Disconnect-HVEvent,Get-HVPoolSpec,Get-HVInternalName, Get-HVEvent,Get-HVFarm,Get-HVFarmSummary,Get-HVPool,Get-HVPoolSummary,Get-HVMachine,Get-HVMachineSummary,Get-HVQueryResult,Get-HVQueryFilter,New-HVFarm,New-HVPool,Remove-HVFarm,Remove-HVPool,Set-HVFarm,Set-HVPool,Start-HVFarm,Start-HVPool From c3d82c2119a0d118eadab90d79d3460c0dd0bbc3 Mon Sep 17 00:00:00 2001 From: praveenmathamsetty Date: Tue, 10 Jan 2017 16:27:48 +0530 Subject: [PATCH 005/112] Update task info Task information is not displaying properly, now fixed it. --- Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 index d388441..f04dfef 100644 --- a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 +++ b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 @@ -5683,7 +5683,7 @@ function Start-HVFarm { $farm_service_helper.Farm_Update($services,$item,$updates) $farm_service_helper.Farm_Recompose($services,$item,$spec) } - Write-Host "Performed recompose task on farm: $farmList.item" + Write-Host "Performed recompose task on farm: " $farmList.$item } } } @@ -5970,7 +5970,7 @@ function Start-HVPool { if ($pscmdlet.ShouldProcess($poolList.$item)) { $desktop_helper.Desktop_Rebalance($services,$item,$spec) } - Write-Host "Performed rebalance task on Pool: $PoolList.$item" + Write-Host "Performed rebalance task on Pool: " $PoolList.$item } } 'REFRESH' { @@ -5980,7 +5980,7 @@ function Start-HVPool { if ($pscmdlet.ShouldProcess($poolList.$item)) { $desktop_helper.Desktop_Refresh($services,$item,$spec) } - Write-Host "Performed refresh task on Pool: $PoolList.$item" + Write-Host "Performed refresh task on Pool: " $PoolList.$item } } 'RECOMPOSE' { @@ -5999,7 +5999,7 @@ function Start-HVPool { if ($pscmdlet.ShouldProcess($poolList.$item)) { $desktop_helper.Desktop_Update($services,$item,$updates) } - Write-Host "Performed recompose task on Pool: $PoolList.$item" + Write-Host "Performed recompose task on Pool: " $PoolList.$item } } 'PUSH_IMAGE' { @@ -6017,7 +6017,7 @@ function Start-HVPool { if ($pscmdlet.ShouldProcess($poolList.$item)) { $desktop_helper.Desktop_SchedulePushImage($services,$item,$spec) } - Write-Host "Performed push_image task on Pool: $PoolList.$item" + Write-Host "Performed push_image task on Pool: " $PoolList.$item } } 'CANCEL_PUSH_IMAGE' { @@ -6028,7 +6028,7 @@ function Start-HVPool { if ($pscmdlet.ShouldProcess($poolList.$item)) { $desktop_helper.Desktop_CancelScheduledPushImage($services,$item) } - Write-Host "Performed cancel_push_image task on Pool: $PoolList.$item" + Write-Host "Performed cancel_push_image task on Pool: " $PoolList.$item } } } From dd8906d65fab1d7ea2cd217dfec4316f5ddd9289 Mon Sep 17 00:00:00 2001 From: praveenmathamsetty Date: Fri, 20 Jan 2017 19:27:31 +0530 Subject: [PATCH 006/112] Adding of new AFs New-HVEntitlement, Get-HVEntitlement and Remove-HVEntitlement 1) This represents a simple association between a single user/group and a resource that they can be assigned to. Examples of associated resources are Desktops, Applications, GlobalEntitlements,GlobalApplicationEntitlements,or URLRedirection Settings. Individual users/groups and resources may be associated with multiple user entitlements. 2) Adding Get-Help changes for Pool 3) Configuring Clone Prep and Sys Prep script parameters to pool --- .../VMware.HV.Helper.format.ps1xml | 35 +- .../VMware.Hv.Helper/VMware.HV.Helper.psm1 | 920 +++++++++++++++++- 2 files changed, 933 insertions(+), 22 deletions(-) diff --git a/Modules/VMware.Hv.Helper/VMware.HV.Helper.format.ps1xml b/Modules/VMware.Hv.Helper/VMware.HV.Helper.format.ps1xml index 62ce001..7f8a6aa 100644 --- a/Modules/VMware.Hv.Helper/VMware.HV.Helper.format.ps1xml +++ b/Modules/VMware.Hv.Helper/VMware.HV.Helper.format.ps1xml @@ -27,6 +27,10 @@ 16 + + + 8 + 7 @@ -56,10 +60,23 @@ $_.desktopSummaryData.userAssignment - + + + $filterContains = Get-HVQueryFilter localData.desktops -contains ([VMware.Hv.DesktopId[]]$_.id) + $GlobalfilterContains = Get-HVQueryFilter localData.desktops -contains ([VMware.Hv.DesktopId[]]$_.id) + Try { + $results += Get-HVQueryResult -EntityType EntitledUserOrGroupLocalSummaryView -Filter $filterContains + $results += Get-HVQueryResult -EntityType EntitledUserOrGroupGlobalSummaryView -Filter $GlobalfilterContains + } Catch { + #Do nothing + } + $results.length + + + $_.desktopSummaryData.enabled - + $_.desktopSummaryData.numSessions @@ -97,6 +114,20 @@ $_.desktopSummaryData.userAssignment + + + $filterContains = Get-HVQueryFilter localData.desktops -contains ([VMware.Hv.DesktopId[]]$_.id) + $GlobalfilterContains = Get-HVQueryFilter localData.desktops -contains ([VMware.Hv.DesktopId[]]$_.id) + Try { + $results += Get-HVQueryResult -EntityType EntitledUserOrGroupLocalSummaryView -Filter $filterContains + $results += Get-HVQueryResult -EntityType EntitledUserOrGroupGlobalSummaryView -Filter $GlobalfilterContains + } Catch { + #Do nothing + } + $results.length + + + $_.desktopSummaryData.enabled diff --git a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 index f04dfef..053d1f6 100644 --- a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 +++ b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 @@ -2920,6 +2920,78 @@ function New-HVPool { This is a list of tags that access to the desktop is restricted to. No list means that the desktop can be accessed from any connection server. +.PARAMETER PowerPolicy + Power policy for the machines in the desktop after logoff. + This setting is only relevant for managed machines + +.PARAMETER AutomaticLogoffPolicy + Automatically log-off policy after disconnect. + This property has a default value of "NEVER". + +.PARAMETER AutomaticLogoffMinutes + The timeout in minutes for automatic log-off after disconnect. + This property is required if automaticLogoffPolicy is set to "AFTER". + +.PARAMETER AllowUsersToResetMachines + Whether users are allowed to reset/restart their machines. + +.PARAMETER AllowMultipleSessionsPerUser + Whether multiple sessions are allowed per user in case of Floating User Assignment. + +.PARAMETER DeleteOrRefreshMachineAfterLogoff + Whether machines are to be deleted or refreshed after logoff in case of Floating User Assignment. + +.PARAMETER RefreshOsDiskAfterLogoff + Whether and when to refresh the OS disks for dedicated-assignment, linked-clone machines. + +.PARAMETER RefreshPeriodDaysForReplicaOsDisk + Regular interval at which to refresh the OS disk. + +.PARAMETER RefreshThresholdPercentageForReplicaOsDisk + With the 'AT_SIZE' option for refreshOsDiskAfterLogoff, the size of the linked clone's OS disk in the datastore is compared to its maximum allowable size. + +.PARAMETER SupportedDisplayProtocols + The list of supported display protocols for the desktop. + +.PARAMETER DefaultDisplayProtocol + The default display protocol for the desktop. For a managed desktop, this will default to "PCOIP". For an unmanaged desktop, this will default to "RDP". + +.PARAMETER AllowUsersToChooseProtocol + Whether the users can choose the protocol. + +.PARAMETER Renderer3D + Specify 3D rendering dependent types hardware, software, vsphere client etc. + +.PARAMETER EnableGRIDvGPUs + Whether GRIDvGPUs enabled or not + +.PARAMETER VRamSizeMB + VRAM size for View managed 3D rendering. More VRAM can improve 3D performance. + +.PARAMETER MaxNumberOfMonitors + The greater these values are, the more memory will be consumed on the associated ESX hosts + +.PARAMETER MaxResolutionOfAnyOneMonitor + The greater these values are, the more memory will be consumed on the associated ESX hosts. + +.PARAMETER EnableHTMLAccess + HTML Access, enabled by VMware Blast technology, allows users to connect to View machines from Web browsers. + +.PARAMETER Quality + This setting determines the image quality that the flash movie will render. Lower quality results in less bandwidth usage. + +.PARAMETER Throttling + This setting affects the frame rate of the flash movie. If enabled, the frames per second will be reduced based on the aggressiveness level. + +.PARAMETER OverrideGlobalSetting + Mirage configuration specified here will be used for this Desktop + +.PARAMETER Enabled + Whether a Mirage server is enabled. + +.PARAMETER Url + The URL of the Mirage server. This should be in the form "<(DNS name)|(IPv4)|(IPv6)><:(port)>". IPv6 addresses must be enclosed in square brackets. + .PARAMETER Vcenter Virtual Center server-address (IP or FQDN) where the pool virtual machines are located. This should be same as provided to the Connection Server while adding the vCenter server. @@ -2954,6 +3026,60 @@ function New-HVPool { Whether to use vSphere VSAN. This is applicable for vSphere 5.5 or later. Applicable to Full, Linked, Instant Clone Pools. +.PARAMETER UseSeparateDatastoresReplicaAndOSDisks + Whether to use separate datastores for replica and OS disks. + +.PARAMETER ReplicaDiskDatastore + Datastore to store replica disks for View Composer and Instant clone engine sourced machines. + +.PARAMETER UseNativeSnapshots + Native NFS Snapshots is a hardware feature, specify whether to use or not + +.PARAMETER ReclaimVmDiskSpace + virtual machines can be configured to use a space efficient disk format that supports reclamation of unused disk space. + +.PARAMETER ReclamationThresholdGB + Initiate reclamation when unused space on VM exceeds the threshold. + +.PARAMETER RedirectWindowsProfile + Windows profiles will be redirected to persistent disks, which are not affected by View Composer operations such as refresh, recompose and rebalance. + +.PARAMETER UseSeparateDatastoresPersistentAndOSDisks + Whether to use separate datastores for persistent and OS disks. This must be false if redirectWindowsProfile is false. + +.PARAMETER PersistentDiskDatastores + Name of the Persistent disk datastore + +.PARAMETER PersistentDiskStorageOvercommit + Storage overcommit determines how view places new VMs on the selected datastores. + +.PARAMETER DiskSizeMB + Size of the persistent disk in MB. + +.PARAMETER DiskDriveLetter + Persistent disk drive letter. + +.PARAMETER RedirectDisposableFiles + Redirect disposable files to a non-persistent disk that will be deleted automatically when a user's session ends. + +.PARAMETER NonPersistentDiskSizeMB + Size of the non persistent disk in MB. + +.PARAMETER NonPersistentDiskDriveLetter + Non persistent disk drive letter. + +.PARAMETER UseViewStorageAccelerator + Whether to use View Storage Accelerator. + +.PARAMETER ViewComposerDiskTypes + Disk types to enable for the View Storage Accelerator feature. + +.PARAMETER RegenerateViewStorageAcceleratorDays + How often to regenerate the View Storage Accelerator cache. + +.PARAMETER BlackoutTimes + A list of blackout times. + .PARAMETER StopOnProvisioningError Set to true to stop provisioning of all VMs on error. Applicable to Full, Linked, Instant Clone Pools. @@ -3039,6 +3165,21 @@ function New-HVPool { The customization spec to use. Applicable to Full, Linked Clone Pools. +.PARAMETER PowerOffScriptName + Power off script. ClonePrep/QuickPrep can run a customization script on instant/linked clone machines before they are powered off. Provide the path to the script on the parent virtual machine. + Applicable to Linked, Instant Clone pools. + +.PARAMETER PowerOffScriptParameters + Power off script parameters. Example: p1 p2 p3 + Applicable to Linked, Instant Clone pools. + +.PARAMETER PostSynchronizationScriptName + Post synchronization script. ClonePrep/QuickPrep can run a customization script on instant/linked clone machines after they are created or recovered or a new image is pushed. Provide the path to the script on the parent virtual machine. + Applicable to Linked, Instant Clone pools. + +.PARAMETER PostSynchronizationScriptParameters + Post synchronization script parameters. Example: p1 p2 p3 + Applicable to Linked, Instant Clone pools. .PARAMETER Source Source of the Virtual machines for manual pool. Supported values are 'VIRTUAL_CENTER','UNMANAGED'. @@ -3366,6 +3507,7 @@ function New-HVPool { [Parameter(Mandatory = $false,ParameterSetName = 'FULL_CLONE')] [string] $datacenter, + #desktopSpec.automatedDesktopSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.datastore if LINKED_CLONE, INSTANT_CLONE, FULL_CLONE [Parameter(Mandatory = $true,ParameterSetName = "LINKED_CLONE")] [Parameter(Mandatory = $true,ParameterSetName = 'INSTANT_CLONE')] @@ -3379,12 +3521,14 @@ function New-HVPool { [Parameter(Mandatory = $false,ParameterSetName = 'FULL_CLONE')] [string[]] $StorageOvercommit = $null, + #desktopSpec.automatedDesktopSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.useVSAN if LINKED_CLONE, INSTANT_CLONE, FULL_CLONE [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] [Parameter(Mandatory = $false,ParameterSetName = 'INSTANT_CLONE')] [Parameter(Mandatory = $false,ParameterSetName = 'FULL_CLONE')] [boolean] $UseVSAN = $false, + #desktopSpec.automatedDesktopSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.viewComposerStorageSettings.useSeparateDatastoresReplicaAndOSDisks if LINKED_CLONE, INSTANT_CLONE [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] [Parameter(Mandatory = $false,ParameterSetName = 'INSTANT_CLONE')] @@ -3397,21 +3541,24 @@ function New-HVPool { [string] $ReplicaDiskDatastore, - #desktopSpec.automatedDesktopSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.viewComposerStorageSettings.replicaDiskDatastore if LINKED_CLONE, INSTANT_CLONE + #desktopSpec.automatedDesktopSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.viewComposerStorageSettings.UseNativeSnapshots if LINKED_CLONE, INSTANT_CLONE [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] [Parameter(Mandatory = $false,ParameterSetName = 'INSTANT_CLONE')] [boolean] $UseNativeSnapshots = $false, + #desktopSpec.automatedDesktopSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.viewComposerStorageSettings.spaceReclamationSettings.reclaimVmDiskSpace if LINKED_CLONE, INSTANT_CLONE [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] [Parameter(Mandatory = $false,ParameterSetName = 'INSTANT_CLONE')] [boolean] $ReclaimVmDiskSpace = $false, + #desktopSpec.automatedDesktopSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.viewComposerStorageSettings.spaceReclamationSettings.reclamationThresholdGB if LINKED_CLONE [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] [ValidateRange(0,[Int]::MaxValue)] [int] $ReclamationThresholdGB = 1, + #desktopSpec.automatedDesktopSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.viewComposerStorageSettings.persistentDiskSettings.redirectWindowsProfile if LINKED_CLONE, INSTANT_CLONE [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] [Parameter(Mandatory = $false,ParameterSetName = 'INSTANT_CLONE')] @@ -3435,11 +3582,13 @@ function New-HVPool { [ValidateRange(128,[Int]::MaxValue)] [int] $DiskSizeMB = 2048, + #desktopSpec.automatedDesktopSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.viewComposerStorageSettings.persistentDiskSettings.diskDriveLetter if LINKED_CLONE [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] [ValidatePattern("^[D-Z]$")] [string] $DiskDriveLetter = "D", + #desktopSpec.automatedDesktopSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.viewComposerStorageSettings.nonPersistentDiskSettings.redirectDisposableFiles if LINKED_CLONE [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] [boolean] @@ -3450,6 +3599,7 @@ function New-HVPool { [ValidateRange(512,[Int]::MaxValue)] [int] $NonPersistentDiskSizeMB = 4096, + #desktopSpec.automatedDesktopSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.viewComposerStorageSettings.nonPersistentDiskSettings.diskDriveLetter if LINKED_CLONE [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] [ValidatePattern("^[D-Z]|Auto$")] @@ -3483,6 +3633,7 @@ function New-HVPool { [Parameter(Mandatory = $false,ParameterSetName = 'FULL_CLONE')] [VMware.Hv.DesktopNetworkInterfaceCardSettings[]] $Nics, + #desktopSpec.automatedDesktopSpec.virtualCenterProvisioningSettings.enableProvsioning if LINKED_CLONE, INSTANT_CLONE, FULL_CLONE [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] [Parameter(Mandatory = $false,ParameterSetName = 'INSTANT_CLONE')] @@ -3599,10 +3750,12 @@ function New-HVPool { [ValidateSet('CLONE_PREP','QUICK_PREP','SYS_PREP','NONE')] [string] $CustType, + #desktopSpec.automatedDesktopSpec.customizationSettings.reusePreExistingAccounts if LINKED_CLONE [Parameter(Mandatory = $false,ParameterSetName = 'LINKED_CLONE')] [Boolean] $ReusePreExistingAccounts = $false, + #desktopSpec.automatedDesktopSpec.customizationSettings.sysprepCustomizationSettings.customizationSpec if LINKED_CLONE, FULL_CLONE [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] [Parameter(Mandatory = $false,ParameterSetName = "FULL_CLONE")] @@ -3641,6 +3794,7 @@ function New-HVPool { [Parameter(Mandatory = $false,ParameterSetName = 'LINKED_CLONE')] [string] $PostSynchronizationScriptParameters, + #manual desktop [Parameter(Mandatory = $true,ParameterSetName = 'MANUAL')] [ValidateSet('VIRTUAL_CENTER','UNMANAGED')] @@ -3787,7 +3941,7 @@ function New-HVPool { $reusePreExistingAccounts = $jsonObject.AutomatedDesktopSpec.CustomizationSettings.reusePreExistingAccounts } 'QUICK_PREP' { - $powerOffScriptName= $jsonObject.AutomatedDesktopSpec.CustomizationSettings.QuickprepCustomizationSettings.PowerOffScriptName + $powerOffScriptName = $jsonObject.AutomatedDesktopSpec.CustomizationSettings.QuickprepCustomizationSettings.PowerOffScriptName $powerOffScriptParameters = $jsonObject.AutomatedDesktopSpec.CustomizationSettings.QuickprepCustomizationSettings.PowerOffScriptParameters $postSynchronizationScriptName = $jsonObject.AutomatedDesktopSpec.CustomizationSettings.QuickprepCustomizationSettings.PostSynchronizationScriptName $postSynchronizationScriptParameters = $jsonObject.AutomatedDesktopSpec.CustomizationSettings.QuickprepCustomizationSettings.PostSynchronizationScriptParameters @@ -4299,8 +4453,8 @@ function New-HVPool { } $desktopSpecObj.DesktopSettings = $desktopSettings - - if ($globalEntitlement) { + $info = $services.PodFederation.PodFederation_get() + if ($globalEntitlement -and ("ENABLED" -eq $info.localPodStatus.status)) { $QueryFilterEquals = New-Object VMware.Hv.QueryFilterEquals $QueryFilterEquals.memberName = 'base.displayName' $QueryFilterEquals.value = $globalEntitlement @@ -4606,6 +4760,10 @@ function Get-HVPoolCustomizationSetting { } $desktopSpecObj.AutomatedDesktopSpec.CustomizationSettings.CloneprepCustomizationSettings = Get-CustomizationObject $desktopSpecObj.AutomatedDesktopSpec.CustomizationSettings.CloneprepCustomizationSettings.InstantCloneEngineDomainAdministrator = $instantCloneEngineDomainAdministrator + $desktopSpecObj.AutomatedDesktopSpec.CustomizationSettings.CloneprepCustomizationSettings.powerOffScriptName = $powerOffScriptName + $desktopSpecObj.AutomatedDesktopSpec.CustomizationSettings.CloneprepCustomizationSettings.powerOffScriptParameters = $powerOffScriptParameters + $desktopSpecObj.AutomatedDesktopSpec.CustomizationSettings.CloneprepCustomizationSettings.postSynchronizationScriptName = $postSynchronizationScriptName + $desktopSpecObj.AutomatedDesktopSpec.CustomizationSettings.CloneprepCustomizationSettings.postSynchronizationScriptParameters = $postSynchronizationScriptParameters } else { if ($LinkedClone) { @@ -4634,6 +4792,10 @@ function Get-HVPoolCustomizationSetting { } elseIf ($custType -eq 'QUICK_PREP') { $desktopSpecObj.AutomatedDesktopSpec.CustomizationSettings.CustomizationType = 'QUICK_PREP' $desktopSpecObj.AutomatedDesktopSpec.CustomizationSettings.QuickprepCustomizationSettings = Get-CustomizationObject + $desktopSpecObj.AutomatedDesktopSpec.CustomizationSettings.QuickprepCustomizationSettings.powerOffScriptName = $powerOffScriptName + $desktopSpecObj.AutomatedDesktopSpec.CustomizationSettings.QuickprepCustomizationSettings.powerOffScriptParameters = $powerOffScriptParameters + $desktopSpecObj.AutomatedDesktopSpec.CustomizationSettings.QuickprepCustomizationSettings.postSynchronizationScriptName = $postSynchronizationScriptName + $desktopSpecObj.AutomatedDesktopSpec.CustomizationSettings.QuickprepCustomizationSettings.postSynchronizationScriptParameters = $postSynchronizationScriptParameters } else { throw "The customization type: [$custType] is not supported for LinkedClone Pool" } @@ -5200,7 +5362,7 @@ function Set-HVFarm { } process { - $farmList = @() + $farmList = @{} if ($farmName) { try { $farmSpecObj = Get-HVFarmSummary -farmName $farmName -hvServer $hvServer @@ -5214,7 +5376,7 @@ function Set-HVFarm { Write-Error "Start/Stop operation is not supported for farm with name : [$farmObj.Data.Name]" return } - $farmList += $farmObj.id + $farmList.add($farmObj.id, $farmObj.data.name) } } else { Write-Error "Unable to retrieve FarmSummaryView with given farmName [$farmName]" @@ -5227,14 +5389,14 @@ function Set-HVFarm { Write-Error "Start/Stop operation is not supported for farm with name : [$item.Data.Name]" return } - $farmList += $item.id + $farmList.add($item.id, $item.data.name) } elseif ($item.GetType().name -eq 'FarmInfo') { if (($Start -or $Stop) -and ("AUTOMATED" -ne $item.Type)) { Write-Error "Start/Stop operation is not supported for farm with name : [$item.Data.Name]" return } - $farmList += $item.id + $farmList.add($item.id, $item.data.name) } else { Write-Error "In pipeline did not get object of expected type FarmSummaryView/FarmInfo" @@ -5271,11 +5433,11 @@ function Set-HVFarm { -value $false } $farm_service_helper = New-Object VMware.Hv.FarmService - foreach ($item in $farmList) { - if ($pscmdlet.ShouldProcess($updates)) { + foreach ($item in $farmList.Keys) { + if ($pscmdlet.ShouldProcess($farmList.$item)) { $farm_service_helper.Farm_Update($services,$item,$updates) } - Write-Host "Updated Farm Member $updates.Key with value $updates.value" + Write-Host "Update successful for farm: " $farmList.$item } } @@ -5408,7 +5570,7 @@ function Set-HVPool { } process { - $poolList = @() + $poolList = @{} if ($poolName) { try { $desktopPools = Get-HVPoolSummary -poolName $poolName -hvServer $hvServer @@ -5422,24 +5584,24 @@ function Set-HVPool { Write-Error "Start/Stop operation is not supported for Poll with name : [$item.DesktopSummaryData.Name]" return } - $poolList += $desktopObj.id + $poolList.add($desktopObj.id, $desktopObj.DesktopSummaryData.Name) } } - } elseif ($PSCmdlet.MyInvocation.ExpectingInput) { + } elseif ($PSCmdlet.MyInvocation.ExpectingInput -or $Pool) { foreach ($item in $pool) { if ($item.GetType().name -eq 'DesktopInfo') { if (($Start -or $Stop) -and ("AUTOMATED" -ne $item.Type)) { Write-Error "Start/Stop operation is not supported for Pool with name : [$item.Base.Name]" return } - $poolList += $item.id + $poolList.add($item.id, $item.Base.Name) } elseif ($item.GetType().name -eq 'DesktopSummaryView') { if (($Start -or $Stop) -and ("AUTOMATED" -ne $item.DesktopSummaryData.Type)) { Write-Error "Start/Stop operation is not supported for Poll with name : [$item.DesktopSummaryData.Name]" return } - $poolList += $item.id + $poolList.add($item.id, $item.DesktopSummaryData.Name) } else { Write-Error "In pipeline did not get object of expected type DesktopSummaryView/DesktopInfo" @@ -5480,12 +5642,12 @@ function Set-HVPool { -value $false } $desktop_helper = New-Object VMware.Hv.DesktopService - foreach ($item in $poolList) { - if ($pscmdlet.ShouldProcess($updates)) { + foreach ($item in $poolList.Keys) { + if ($pscmdlet.ShouldProcess($poolList.$item)) { $desktop_helper.Desktop_Update($services,$item,$updates) } - Write-Host "Updated Pool member $updates.key with value $updates.value" } + Write-Host "Update successful for Pool: " $poolList.$item } end { @@ -6798,4 +6960,722 @@ function Get-HVInternalName { } } -Export-ModuleMember Add-HVDesktop,Add-HVRDSServer,Connect-HVEvent,Disconnect-HVEvent,Get-HVPoolSpec,Get-HVInternalName, Get-HVEvent,Get-HVFarm,Get-HVFarmSummary,Get-HVPool,Get-HVPoolSummary,Get-HVMachine,Get-HVMachineSummary,Get-HVQueryResult,Get-HVQueryFilter,New-HVFarm,New-HVPool,Remove-HVFarm,Remove-HVPool,Set-HVFarm,Set-HVPool,Start-HVFarm,Start-HVPool + +function Get-UserInfo { + [CmdletBinding( + SupportsShouldProcess = $true, + ConfirmImpact = 'High' + )] + param( + [Parameter(Mandatory = $true)] + [ValidatePattern("^.+?[@\\].+?$")] + [String] + $UserName + ) + + if ($UserName -match '^.+?[@].+?$') { + $info = $UserName -split "@" + $Domain = $info[1] + $Name = $Info[0] + } else { + $info = $UserName -split "\\" + $Domain = $info[0] + $Name = $Info[1] + } + return @{'Name' = $Name; 'Domain' = $Domain} +} + +function New-HVEntitlement { +<# +.Synopsis + Associates a user/group with a resource + +.DESCRIPTION + This represents a simple association between a single user/group and a resource that they can be assigned. + +.PARAMETER User + User prinicipal name of user or group + +.PARAMETER ResourceName + The resource(Application, Pool etc.) name + +.PARAMETER Resource + Object(s) of the resource(Application, Desktop etc) to entitle + +.PARAMETER ResourceType + Type of Resource(Application, Desktop etc) + +.PARAMETER Type + Whether or not this is a group or a user. + +.PARAMETER HvServer + Reference to Horizon View Server. If the value is not passed or null then + first element from global:DefaultHVServers would be considered inplace of hvServer + +.EXAMPLE + Associate a user/group with a pool + New-HVEntitlement -User 'administrator@adviewdev.eng.vmware.com' -ResourceName 'InsClnPol' -Confirm:$false + +.EXAMPLE + Associate a user/group with a application + New-HVEntitlement -User 'adviewdev\administrator' -ResourceName 'Calculator' -ResourceType Application + +.EXAMPLE + Associate a user/group with a URLRedirection settings + New-HVEntitlement -User 'adviewdev.eng.vmware.com\administrator' -ResourceName 'UrlSetting1' -ResourceType URLRedirection + +.EXAMPLE + Associate a user/group with a desktop entitlement + New-HVEntitlement -User 'adviewdev.eng.vmware.com\administrator' -ResourceName 'GE1' -ResourceType GlobalEntitlement + +.EXAMPLE + Associate a user/group with a application entitlement + New-HVEntitlement -User 'adviewdev\administrator' -ResourceName 'GEAPP1' -ResourceType GlobalApplicationEntitlement + +.EXAMPLE + Associate a user/group with list of pools + $pools = Get-HVPool; $pools | New-HVEntitlement -User 'adviewdev\administrator' -Confirm:$false + + +.NOTES + Author : Praveen Mathamsetty. + Author email : pmathamsetty@vmware.com + Version : 1.1 + + ===Tested Against Environment==== + Horizon View Server Version : 7.0.2, 7.0.3 + PowerCLI Version : PowerCLI 6.5 + PowerShell Version : 5.0 +#> + [CmdletBinding( + SupportsShouldProcess = $true, + ConfirmImpact = 'High' + )] + param( + [Parameter(Mandatory = $true)] + [ValidatePattern("^.+?[@\\].+?$")] + [String] + $User, + + [Parameter(Mandatory = $true,ParameterSetName ='Default')] + [ValidateNotNullOrEmpty()] + [String] + $ResourceName, + + [Parameter(Mandatory = $true,ValueFromPipeline = $true,ParameterSetName ='PipeLine')] + $Resource, + + [Parameter(Mandatory = $false)] + [ValidateSet('Application','Desktop','GlobalApplicationEntitlement','GlobalEntitlement', + 'URLRedirection')] + [String] + $ResourceType = 'Desktop', + + [Parameter(Mandatory = $false)] + [ValidateSet('User','Group')] + [String] + $Type = 'User', + + [Parameter(Mandatory = $false)] + $HvServer = $null + ) + begin { + $services = Get-ViewAPIService -hvServer $hvServer + if ($null -eq $services) { + Write-Error "Could not retrieve ViewApi services from connection object" + break + } + } + process { + $userInfo = Get-UserInfo -UserName $User + $UserOrGroupName = $userInfo.Name + $Domain = $userInfo.Domain + $IsGroup = ($Type -eq 'Group') + $filter1 = Get-HVQueryFilter 'base.name' -Eq $UserOrGroupName + $filter2 = Get-HVQueryFilter 'base.domain' -Eq $Domain + $filter3 = Get-HVQueryFilter 'base.group' -Eq $IsGroup + $andFilter = Get-HVQueryFilter -And -Filters @($filter1, $filter2, $filter3) + $results = Get-HVQueryResult -EntityType ADUserOrGroupSummaryView -Filter $andFilter -HvServer $HvServer + if ($results.length -ne 1) { + Write-Host "Unable to find specific user or group with given search parameters" + return + } + $ResourceObjs = $null + $info = $services.PodFederation.PodFederation_get() + switch($ResourceType){ + "Desktop" { + if ($ResourceName) { + $ResourceObjs = Get-HVPool -PoolName $ResourceName + if (! $ResourceObjs) { + Write-Host "No pool found with given resourceName: " $ResourceName + return + } + } elseif ($PSCmdlet.MyInvocation.ExpectingInput -or $Resource) { + foreach ($item in $Resource) { + if ($item.GetType().name -eq 'DesktopInfo') { + $ResourceObjs += ,$item + } + elseif ($item.GetType().name -eq 'DesktopSummaryView') { + $ResourceObjs += ,$item + } + else { + Write-Error "In pipeline didn't received object(s) of expected type DesktopSummaryView/DesktopInfo" + return + } + } + } + } + "Application" { + if ($ResourceName) { + $eqFilter = Get-HVQueryFilter 'data.name' -Eq $ResourceName + $ResourceObjs = Get-HVQueryResult -EntityType ApplicationInfo -Filter $eqFilter -HvServer $HvServer + if (! $ResourceObjs) { + Write-Host "No Application found with given resourceName: " $ResourceName + return + } + } elseif ($PSCmdlet.MyInvocation.ExpectingInput -or $Resource) { + foreach ($item in $Resource) { + if ($item.GetType().name -eq 'ApplicationInfo') { + $ResourceObjs += ,$item + + } else { + Write-Error "In pipeline didn't received object(s) of expected type ApplicationInfo" + return + } + } + } + } + "URLRedirection" { + if ($ResourceName) { + $UrlRedirectionList = $services.URLRedirection.URLRedirection_List() + $ResourceObjs = $UrlRedirectionList | Where-Object { $_.urlRedirectionData.displayName -like $ResourceName} + if (! $ResourceObjs) { + Write-Host "No URLRedirectionData found with given resourceName: " $ResourceName + return + } + } elseif ($PSCmdlet.MyInvocation.ExpectingInput -or $Resource) { + foreach ($item in $Resource) { + if ($item.GetType().name -eq 'URLRedirectionInfo') { + $ResourceObjs += ,$item + } else { + Write-Error "In pipeline didn't received object(s) of expected type URLRedirectionInfo" + return + } + } + } + } + "GlobalApplicationEntitlement" { + if ("ENABLED" -eq $info.localPodStatus.status) { + if ($ResourceName) { + $eqFilter = Get-HVQueryFilter 'base.displayName' -Eq $ResourceName + $ResourceObjs = Get-HVQueryResult -EntityType GlobalApplicationEntitlementInfo -Filter $eqFilter -HvServer $HvServer + if (! $ResourceObjs) { + Write-Host "No globalApplicationEntitlementInfo found with given resourceName: " $ResourceName + return + } elseif ($PSCmdlet.MyInvocation.ExpectingInput -or $Resource) { + foreach ($item in $Resource) { + if ($item.GetType().name -eq 'GlobalApplicationEntitlementInfo') { + $ResourceObjs += ,$item + } else { + Write-Error "In pipeline didn't received object(s) of expected type globalApplicationEntitlementInfo" + return + } + } + } + } + } else { + Write-Host "Multi-DataCenter-View/CPA is not enabled" + return + } + } + "GlobalEntitlement" { + if ("ENABLED" -eq $info.localPodStatus.status) { + if ($ResourceName) { + $eqFilter = Get-HVQueryFilter 'base.displayName' -Eq $ResourceName + $ResourceObjs = Get-HVQueryResult -EntityType GlobalEntitlementSummaryView -Filter $eqFilter -HvServer $HvServer + if (! $ResourceObjs) { + Write-Host "No globalEntitlementSummary found with given resourceName: " $ResourceName + return + } elseif ($PSCmdlet.MyInvocation.ExpectingInput -or $Resource) { + foreach ($item in $Resource) { + if ($item.GetType().name -eq 'GlobalEntitlementSummaryView') { + $ResourceObjs += ,$item + } else { + Write-Error "In pipeline didn't received object(s) of expected type GlobalEntitlementSummaryView" + return + } + } + } + } + } else { + Write-Host "Multi-DataCenter-View/CPA is not enabled" + return + } + } + } + $base = New-Object VMware.HV.UserEntitlementBase + $base.UserOrGroup = $results.id + foreach ($ResourceObj in $ResourceObjs) { + $base.Resource = $ResourceObj.id + if ($pscmdlet.ShouldProcess($User)) { + $id = $services.UserEntitlement.UserEntitlement_Create($base) + } + } + Write-host $ResourceObjs.Length " resource(s) entitled with User or group: " $User + } + end { + [System.gc]::collect() + } +} + + +function Get-HVEntitlement { +<# +.Synopsis + Gets association data between a user/group and a resource + +.DESCRIPTION + Provides entitlement Info between a single user/group and a resource that they can be assigned. + +.PARAMETER User + User prinicipal name of user or group + +.PARAMETER ResourceName + The resource(Application, Pool etc.) name + +.PARAMETER Resource + Object(s) of the resource(Application, Desktop etc) to entitle + +.PARAMETER ResourceType + Type of Resource(Application, Desktop etc) + +.PARAMETER Type + Whether or not this is a group or a user. + +.PARAMETER HvServer + Reference to Horizon View Server. If the value is not passed or null then + first element from global:DefaultHVServers would be considered inplace of hvServer + +.EXAMPLE + Gets all the entitlements related to application pool + Get-HVEntitlement -ResourceType Application + +.EXAMPLE + Gets entitlements specific to user or group name and application resource + Get-HVEntitlement -User 'adviewdev.eng.vmware.com\administrator' -ResourceName 'calculator' -ResourceType Application + +.EXAMPLE + Gets entitlements specific to user or group and URLRedirection resource + Get-HVEntitlement -User 'adviewdev.eng.vmware.com\administrator' -ResourceName 'UrlSetting1' -ResourceType URLRedirection + +.EXAMPLE + Gets entitlements specific to user or group and GlobalEntitlement resource + Get-HVEntitlement -User 'administrator@adviewdev.eng.vmware.com' -ResourceName 'GE1' -ResourceType GlobalEntitlement + +.NOTES + Author : Praveen Mathamsetty. + Author email : pmathamsetty@vmware.com + Version : 1.1 + + ===Tested Against Environment==== + Horizon View Server Version : 7.0.2, 7.0.3 + PowerCLI Version : PowerCLI 6.5 + PowerShell Version : 5.0 +#> + + + [CmdletBinding( + SupportsShouldProcess = $true, + ConfirmImpact = 'High' + )] + param( + [Parameter(Mandatory = $false)] + [ValidatePattern("^.+?[@\\].+?$")] + [String] + $User, + + [Parameter(Mandatory = $false)] + [ValidateSet('User','Group')] + [String] + $Type = 'User', + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [String] + $ResourceName, + + [Parameter(Mandatory = $false)] + [ValidateSet('Application','Desktop','GlobalApplicationEntitlement','GlobalEntitlement', + 'URLRedirection')] + [String] + $ResourceType = 'Desktop', + + [Parameter(Mandatory = $false)] + $HvServer = $null + ) + begin { + $services = Get-ViewAPIService -hvServer $hvServer + if ($null -eq $services) { + Write-Error "Could not retrieve ViewApi services from connection object" + break + } + } + process { + $AndFilter = @() + $results = @() + $ResourceObjs = $null + if ($User) { + $userInfo = Get-UserInfo -UserName $User + $UserOrGroupName = $userInfo.Name + $Domain = $userInfo.Domain + $nameFilter = Get-HVQueryFilter 'base.name' -Eq $UserOrGroupName + $AndFilter += $nameFilter + $doaminFilter = Get-HVQueryFilter 'base.domain' -Eq $Domain + $AndFilter += $doaminFilter + } + $IsGroup = ($Type -eq 'Group') + $groupFilter = Get-HVQueryFilter 'base.group' -Eq $IsGroup + $AndFilter += $groupFilter + $info = $services.PodFederation.PodFederation_get() + $cpaEnabled = ("ENABLED" -eq $info.localPodStatus.status) + switch($ResourceType) { + "Desktop" { + if ($ResourceName) { + $ResourceObjs = Get-HVPool -PoolName $ResourceName -HvServer $HvServer + if (! $ResourceObjs) { + Write-Host "No pool found with given resourceName: " $ResourceName + return + } + $AndFilter += Get-HVQueryFilter 'localData.desktops' -Contains ([VMware.Hv.DesktopId[]]$ResourceObjs.Id) + } + $AndFilter = Get-HVQueryFilter -And -Filters $AndFilter + $results = (Get-HVQueryResult -EntityType EntitledUserOrGroupLocalSummaryView -Filter $AndFilter -HvServer $HvServer) + $results = $results | where {$_.localData.desktops -ne $null} + } + "Application" { + if ($ResourceName) { + $eqFilter = Get-HVQueryFilter 'data.name' -Eq $ResourceName + $ResourceObjs = Get-HVQueryResult -EntityType ApplicationInfo -Filter $eqFilter -HvServer $HvServer + if (! $ResourceObjs) { + Write-Host "No Application found with given resourceName: " $ResourceName + return + } + $AndFilter += Get-HVQueryFilter 'localData.applications' -Contains ([VMware.Hv.ApplicationId[]]$ResourceObjs.Id) + } + $AndFilter = Get-HVQueryFilter -And -Filters $AndFilter + $results = (Get-HVQueryResult -EntityType EntitledUserOrGroupLocalSummaryView -Filter $AndFilter -HvServer $HvServer) + $results = $results | where {$_.localData.applications -ne $null} + } + "URLRedirection" { + $localFilter = @() + $globalFilter = @() + $localFilter += $AndFilter + $globalFilter += $AndFilter + if ($ResourceName) { + $UrlRedirectionList = $services.URLRedirection.URLRedirection_List() + $ResourceObjs = $UrlRedirectionList | Where-Object { $_.urlRedirectionData.displayName -like $ResourceName} + if (! $ResourceObjs) { + Write-Host "No URLRedirectionData found with given resourceName: " $ResourceName + return + } + $localFilter += Get-HVQueryFilter 'localData.urlRedirectionSettings' -Contains ([VMware.Hv.URLRedirectionId[]]$ResourceObjs.Id) + if ($cpaEnabled) { + $globalFilter += Get-HVQueryFilter 'globalData.urlRedirectionSettings' -Contains ([VMware.Hv.URLRedirectionId[]]$ResourceObjs.Id) + } + } + $localFilter = Get-HVQueryFilter -And -Filters $localFilter + $localResults = Get-HVQueryResult -EntityType EntitledUserOrGroupLocalSummaryView -Filter $localFilter -HvServer $HvServer + $results += ($localResults | where {$_.localData.urlRedirectionSettings -ne $null}) + if ($cpaEnabled) { + $globalFilter = Get-HVQueryFilter -And -Filters $globalFilter + $globalResults = Get-HVQueryResult -EntityType EntitledUserOrGroupGlobalSummaryView -Filter $globalFilter -HvServer $HvServer + $globalResults = $globalResults | where {$_.globalData.urlRedirectionSettings -ne $null} + $results += $globalResults + } + } + "GlobalApplicationEntitlement" { + if (! $cpaEnabled) { + Write-Host "Multi-DataCenter-View/CPA is not enabled" + return + } + if ($ResourceName) { + $eqFilter = Get-HVQueryFilter 'base.displayName' -Eq $ResourceName + $ResourceObjs = Get-HVQueryResult -EntityType GlobalApplicationEntitlementInfo -Filter $eqFilter -HvServer $HvServer + if (! $ResourceObjs) { + Write-Host "No globalApplicationEntitlementInfo found with given resourceName: " $ResourceName + return + } + $AndFilter += Get-HVQueryFilter 'globalData.globalApplicationEntitlements' -Contains ([VMware.Hv.GlobalApplicationEntitlementId[]]$ResourceObjs.Id) + } + $AndFilter = Get-HVQueryFilter -And -Filters $AndFilter + $results = (Get-HVQueryResult -EntityType EntitledUserOrGroupGlobalSummaryView -Filter $AndFilter -HvServer $HvServer) + $results = $results| where {$_.globalData.globalApplicationEntitlements -ne $null} + } + "GlobalEntitlement" { + if (! $cpaEnabled) { + Write-Host "Multi-DataCenter-View/CPA is not enabled" + return + } + if ($ResourceName) { + $eqFilter = Get-HVQueryFilter 'base.displayName' -Eq $ResourceName + $ResourceObjs = Get-HVQueryResult -EntityType GlobalEntitlementSummaryView -Filter $eqFilter -HvServer $HvServer + if (! $ResourceObjs) { + Write-Host "No globalEntitlementSummary found with given resourceName: " $ResourceName + return + } + $AndFilter += Get-HVQueryFilter 'globalData.globalEntitlements' -Contains ([VMware.Hv.GlobalEntitlementId[]]$ResourceObjs.Id) + } + $AndFilter = Get-HVQueryFilter -And -Filters $AndFilter + $results = (Get-HVQueryResult -EntityType EntitledUserOrGroupGlobalSummaryView -Filter $AndFilter -HvServer $HvServer) + $results = $results | where {$_.globalData.globalEntitlements -ne $null} + } + } + if (! $results) { + Write-Host "Get-HVEntitlement: No entitlements found with given search parameters" + break + } + return $results + } + end { + [System.gc]::collect() + } +} + +function Remove-HVEntitlement { +<# +.Synopsis + Deletes association data between a user/group and a resource + +.DESCRIPTION + Removes entitlement between a single user/group and a resource that already been assigned. + +.PARAMETER User + User prinicipal name of user or group + +.PARAMETER ResourceName + The resource(Application, Pool etc.) name + +.PARAMETER Resource + Object(s) of the resource(Application, Desktop etc) to entitle + +.PARAMETER ResourceType + Type of Resource(Application, Desktop etc) + +.PARAMETER Type + Whether or not this is a group or a user. + +.PARAMETER HvServer + Reference to Horizon View Server. If the value is not passed or null then + first element from global:DefaultHVServers would be considered inplace of hvServer + +.EXAMPLE + Deletes entitlement between a user/group and a pool resource + Remove-HVEntitlement -User 'administrator@adviewdev' -ResourceName LnkClnJSon -Confirm:$false + +.EXAMPLE + Deletes entitlement between a user/group and a Application resource + Remove-HVEntitlement -User 'adviewdev\puser2' -ResourceName 'calculator' -ResourceType Application + +.EXAMPLE + Deletes entitlement between a user/group and a GlobalApplicationEntitlement resource + Remove-HVEntitlement -User 'adviewdev\administrator' -ResourceName 'GEAPP1' -ResourceType GlobalApplicationEntitlement + +.NOTES + Author : Praveen Mathamsetty. + Author email : pmathamsetty@vmware.com + Version : 1.1 + + ===Tested Against Environment==== + Horizon View Server Version : 7.0.2, 7.0.3 + PowerCLI Version : PowerCLI 6.5 + PowerShell Version : 5.0 +#> + + + [CmdletBinding( + SupportsShouldProcess = $true, + ConfirmImpact = 'High' + )] + param( + [Parameter(Mandatory = $true)] + [ValidatePattern("^.+?[@\\].+?$")] + [String] + $User, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $ResourceName, + + [Parameter(Mandatory = $false)] + [ValidateSet('User','Group')] + [String] + $Type = 'User', + + [Parameter(Mandatory = $false)] + [ValidateSet('Application','Desktop','GlobalApplicationEntitlement','GlobalEntitlement', + 'URLRedirection')] + [String] + $ResourceType = 'Desktop', + + [Parameter(Mandatory = $false)] + $HvServer = $null + ) + begin { + $services = Get-ViewAPIService -hvServer $hvServer + if ($null -eq $services) { + Write-Error "Could not retrieve ViewApi services from connection object" + break + } + } + process { + $AndFilter = @() + $results = $null + $userInfo = Get-UserInfo -UserName $User + $UserOrGroupName = $userInfo.Name + $Domain = $userInfo.Domain + $nameFilter = Get-HVQueryFilter 'base.name' -Eq $UserOrGroupName + $doaminFilter = Get-HVQueryFilter 'base.domain' -Eq $Domain + $IsGroup = ($Type -eq 'Group') + $groupFilter = Get-HVQueryFilter 'base.group' -Eq $IsGroup + [VMware.Hv.UserEntitlementId[]] $userEntitlements = $null + if ($ResourceName) { + $info = $services.PodFederation.PodFederation_get() + switch($ResourceType) { + "Desktop" { + $ResourceObjs = Get-HVPool -PoolName $ResourceName -HvServer $HvServer + if (! $ResourceObjs) { + Write-Host "No pool found with given resourceName: " $ResourceName + return + } + $AndFilter += Get-HVQueryFilter 'localData.desktops' -Contains ([VMware.HV.DesktopId[]] $ResourceObjs.Id) + $filters = Get-HVQueryFilter -And -Filters $AndFilter + $results = Get-HVQueryResult -EntityType EntitledUserOrGroupLocalSummaryView -Filter $filters -HvServer $HvServer + if ($results) { + foreach ($result in $Results) { + $userEntitlements = $result.localData.desktopUserEntitlements + if ($pscmdlet.ShouldProcess($User)) { + $services.UserEntitlement.UserEntitlement_DeleteUserEntitlements($userEntitlements) + } + Write-Host $userEntitlements.Length " desktopUserEntitlement(s) are removed for UserOrGroup " $user + } + } + } + "Application" { + $eqFilter = Get-HVQueryFilter 'data.name' -Eq $ResourceName + $ResourceObjs = Get-HVQueryResult -EntityType ApplicationInfo -Filter $eqFilter -HvServer $HvServer + if (! $ResourceObjs) { + Write-Host "No Application found with given resourceName: " $ResourceName + return + } + $AndFilter += Get-HVQueryFilter 'localData.applications' -Contains ([VMware.HV.ApplicationId[]] $ResourceObjs.Id) + $AndFilter = Get-HVQueryFilter -And -Filters $AndFilter + $results = Get-HVQueryResult -EntityType EntitledUserOrGroupLocalSummaryView -Filter $AndFilter -HvServer $HvServer + if ($results) { + foreach ($result in $Results) { + $userEntitlements = $result.localData.applicationUserEntitlements + if ($pscmdlet.ShouldProcess($User)) { + $services.UserEntitlement.UserEntitlement_DeleteUserEntitlements($userEntitlements) + } + Write-Host $userEntitlements.Length " applicationUserEntitlement(s) are removed for UserOrGroup " $user + } + } + } + "URLRedirection" { + $UrlRedirectionList = $services.URLRedirection.URLRedirection_List() + $ResourceObjs = $UrlRedirectionList | Where-Object { $_.urlRedirectionData.displayName -like $ResourceName} + if (! $ResourceObjs) { + Write-Host "No URLRedirectionData found with given resourceName: " $ResourceName + return + } + $localFilter = @() + $localFilter += $AndFilter + $localFilter += (Get-HVQueryFilter 'localData.urlRedirectionSettings' -Contains ([VMware.HV.URLRedirectionId[]]$ResourceObjs.Id)) + $localFilter = Get-HVQueryFilter -And -Filters $localFilter + $results = Get-HVQueryResult -EntityType EntitledUserOrGroupLocalSummaryView -Filter $localFilter -HvServer $HvServer + if ("ENABLED" -eq $info.localPodStatus.status) { + $globalFilter = @() + $globalFilter += $AndFilter + $globalFilter += Get-HVQueryFilter 'globalData.urlRedirectionSettings' -Contains ([VMware.HV.URLRedirectionId[]]$ResourceObjs.Id) + $globalFilter = Get-HVQueryFilter -And -Filters $globalFilter + $results += Get-HVQueryResult -EntityType EntitledUserOrGroupGlobalSummaryView -Filter $globalFilter -HvServer $HvServer + } + if ($results) { + foreach ($result in $Results) { + if ($result.GetType().Name -eq 'EntitledUserOrGroupLocalSummaryView') { + $userEntitlements = $result.localData.urlRedirectionUserEntitlements + if ($pscmdlet.ShouldProcess($User)) { + $services.UserEntitlement.UserEntitlement_DeleteUserEntitlements($userEntitlements) + } + } else { + $userEntitlements = $result.globalData.urlRedirectionUserEntitlements + if ($pscmdlet.ShouldProcess($User)) { + $services.UserEntitlement.UserEntitlement_DeleteUserEntitlements($userEntitlements) + } + } + Write-Host $userEntitlements.Length " urlRedirectionUserEntitlement(s) are removed for UserOrGroup " $user + } + } + } + "GlobalApplicationEntitlement" { + if ("ENABLED" -ne $info.localPodStatus.status) { + Write-Host "Multi-DataCenter-View/CPA is not enabled" + return + } + $eqFilter = Get-HVQueryFilter 'base.displayName' -Eq $ResourceName + $ResourceObjs = Get-HVQueryResult -EntityType GlobalApplicationEntitlementInfo -Filter $eqFilter -HvServer $HvServer + if (! $ResourceObjs) { + Write-Host "No globalApplicationEntitlementInfo found with given resourceName: " $ResourceName + return + } + $AndFilter += Get-HVQueryFilter 'globalData.globalApplicationEntitlements' -Contains ([VMware.Hv.GlobalApplicationEntitlementId[]]$ResourceObjs.Id) + $AndFilter = Get-HVQueryFilter -And -Filters $AndFilter + $results = Get-HVQueryResult -EntityType EntitledUserOrGroupGlobalSummaryView -Filter $AndFilter -HvServer $HvServer + if ($results) { + foreach ($result in $Results) { + $userEntitlements = $result.globalData.globalUserApplicationEntitlements + if ($pscmdlet.ShouldProcess($User)) { + $services.UserEntitlement.UserEntitlement_DeleteUserEntitlements($userEntitlements) + } + Write-Host $userEntitlements.Length " GlobalApplicationEntitlement(s) are removed for UserOrGroup " $user + } + } + } + "GlobalEntitlement" { + if ("ENABLED" -ne $info.localPodStatus.status) { + Write-Host "Multi-DataCenter-View/CPA is not enabled" + return + } + $eqFilter = Get-HVQueryFilter 'base.displayName' -Eq $ResourceName + $ResourceObjs = Get-HVQueryResult -EntityType GlobalEntitlementSummaryView -Filter $eqFilter -HvServer $HvServer + if (! $ResourceObjs) { + Write-Host "No globalEntitlementSummary found with given resourceName: " $ResourceName + return + } + $AndFilter += Get-HVQueryFilter 'globalData.globalEntitlements' -Contains ([VMware.Hv.GlobalEntitlementId[]]$ResourceObjs.Id) + $AndFilter = Get-HVQueryFilter -And -Filters $AndFilter + $results = Get-HVQueryResult -EntityType EntitledUserOrGroupGlobalSummaryView -Filter $AndFilter -HvServer $HvServer + if ($results) { + foreach ($result in $Results) { + $userEntitlements = $result.globalData.globalUserEntitlements + if ($pscmdlet.ShouldProcess($User)) { + $services.UserEntitlement.UserEntitlement_DeleteUserEntitlements($userEntitlements) + } + Write-Host $userEntitlements.Length " GlobalEntitlement(s) are removed for UserOrGroup " $user + } + + } + } + } + } + if (! $results) { + Write-Host "Remove-HVEntitlement: No entitlements found with given search parameters" + return + } + } + end { + [System.gc]::collect() + } +} + +Export-ModuleMember Add-HVDesktop,Add-HVRDSServer,Connect-HVEvent,Disconnect-HVEvent,Get-HVPoolSpec,Get-HVInternalName, Get-HVEvent,Get-HVFarm,Get-HVFarmSummary,Get-HVPool,Get-HVPoolSummary,Get-HVMachine,Get-HVMachineSummary,Get-HVQueryResult,Get-HVQueryFilter,New-HVFarm,New-HVPool,Remove-HVFarm,Remove-HVPool,Set-HVFarm,Set-HVPool,Start-HVFarm,Start-HVPool,New-HVEntitlement,Get-HVEntitlement,Remove-HVEntitlement From 42d33acb4c10adc4e8e257b5eccc6b49c5cfae09 Mon Sep 17 00:00:00 2001 From: vkg1215 Date: Mon, 23 Jan 2017 12:04:48 +0530 Subject: [PATCH 007/112] Code for Instant clone farm and farm maintenance Code for Instant clone farm and farm maintenance --- .../Json/Farm/AutomatedInstantCloneFarm.json | 98 ++++ .../VMware.Hv.Helper/VMware.HV.Helper.psm1 | 504 ++++++++++++++---- 2 files changed, 489 insertions(+), 113 deletions(-) create mode 100644 Modules/VMware.Hv.Helper/Json/Farm/AutomatedInstantCloneFarm.json diff --git a/Modules/VMware.Hv.Helper/Json/Farm/AutomatedInstantCloneFarm.json b/Modules/VMware.Hv.Helper/Json/Farm/AutomatedInstantCloneFarm.json new file mode 100644 index 0000000..605e6a5 --- /dev/null +++ b/Modules/VMware.Hv.Helper/Json/Farm/AutomatedInstantCloneFarm.json @@ -0,0 +1,98 @@ +{ + "Type": "AUTOMATED", + "Data": { + "Name": "ICFarmJson", + "DisplayName": "FarmJsonTest", + "AccessGroup": "Root", + "Description": "created IC Farm from PS via JSON", + "Enabled": null, + "Deleting": false, + "Settings": { + "DisconnectedSessionTimeoutPolicy" : "NEVER", + "DisconnectedSessionTimeoutMinutes" : 1, + "EmptySessionTimeoutPolicy" : "AFTER", + "EmptySessionTimeoutMinutes" : 1, + "LogoffAfterTimeout" : false + }, + "Desktop": null, + "DisplayProtocolSettings": { + "DefaultDisplayProtocol" : "PCOIP", + "AllowDisplayProtocolOverride" : false, + "EnableHTMLAccess" : false + }, + "ServerErrorThreshold": null, + "MirageConfigurationOverrides": { + "OverrideGlobalSetting" : false, + "Enabled" : false, + "Url" : null + } + }, + "AutomatedFarmSpec": { + "ProvisioningType": "INSTANT_CLONE_ENGINE", + "VirtualCenter": null, + "RdsServerNamingSpec": { + "NamingMethod": "PATTERN", + "PatternNamingSettings": { + "NamingPattern": "ICFarmVMPS", + "MaxNumberOfRDSServers": 1 + } + }, + "VirtualCenterProvisioningSettings": { + "EnableProvisioning": true, + "StopProvisioningOnError": true, + "MinReadyVMsOnVComposerMaintenance": 0, + "VirtualCenterProvisioningData": { + "ParentVm": "vm-rdsh-ic", + "Snapshot": "snap_5", + "Datacenter": null, + "VmFolder": "Instant_Clone_VMs", + "HostOrCluster": "vimal-cluster", + "ResourcePool": "vimal-cluster" + }, + "VirtualCenterStorageSettings": { + "Datastores": [ + { + "Datastore": "Datastore1", + "StorageOvercommit": "UNBOUNDED" + } + ], + "UseVSan": false, + "ViewComposerStorageSettings": { + "UseSeparateDatastoresReplicaAndOSDisks": false, + "ReplicaDiskDatastore": null, + "UseNativeSnapshots": false, + "SpaceReclamationSettings": { + "ReclaimVmDiskSpace": false, + "ReclamationThresholdGB": null, + "BlackoutTimes": null + } + } + }, + "VirtualCenterNetworkingSettings": { + "Nics": null + } + }, + "VirtualCenterManagedCommonSettings": { + "TransparentPageSharingScope": "VM" + }, + "CustomizationSettings": { + "CustomizationType": "CLONE_PREP", + "DomainAdministrator": null, + "AdContainer": "CN=Computers", + "ReusePreExistingAccounts": false, + "ClonePrepCustomizationSettings": { + "InstantCloneEngineDomainAdministrator": null, + "PowerOffScriptName": null, + "PowerOffScriptParameters": null, + "PostSynchronizationScriptName": null, + "PostSynchronizationScriptParameters": null + } + }, + "RdsServerMaxSessionsData": { + "MaxSessionsType": "UNLIMITED", + "MaxSessions": null + } + }, + "ManualFarmSpec": null, + "NetBiosName" : "ad-vimalg" +} diff --git a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 index ed97a24..e5cb880 100644 --- a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 +++ b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 @@ -1800,6 +1800,9 @@ function New-HVFarm { .PARAMETER LinkedClone Switch to Create Automated Linked Clone farm. +.PARAMETER InstantClone + Switch to Create Automated Instant Clone farm. + .PARAMETER Manual Switch to Create Manual farm. @@ -1824,38 +1827,38 @@ function New-HVFarm { .PARAMETER ParentVM Base image VM for RDS Servers. - Applicable only to Linked Clone farms. + Applicable to Linked Clone and Instant Clone farms. .PARAMETER SnapshotVM Base image snapshot for RDS Servers. .PARAMETER VmFolder VM folder to deploy the RDSServers to. - Applicable to Linked Clone farms. + Applicable to Linked Clone and Instant Clone farms. .PARAMETER HostOrCluster Host or cluster to deploy the RDSServers in. - Applicable to Linked Clone farms. + Applicable to Linked Clone and Instant Clone farms. .PARAMETER ResourcePool Resource pool to deploy the RDSServers. - Applicable to Linked Clone farms. + Applicable to Linked Clone and Instant Clone farms. .PARAMETER Datastores Datastore names to store the RDSServer. - Applicable to Linked Clone farms. + Applicable to Linked Clone and Instant Clone farms. .PARAMETER UseVSAN Whether to use vSphere VSAN. This is applicable for vSphere 5.5 or later. - Applicable to Linked Clone farms. + Applicable to Linked Clone and Instant Clone farms. .PARAMETER EnableProvisioning Set to true to enable provision of RDSServers immediately in farm. - Applicable to Linked Clone farms. + Applicable to Linked Clone and Instant Clone farms. .PARAMETER StopOnProvisioningError Set to true to stop provisioning of all RDSServers on error. - Applicable to Linked Clone farms. + Applicable to Linked Clone and Instant Clone farms. .PARAMETER TransparentPageSharingScope The transparent page sharing scope. @@ -1864,7 +1867,7 @@ function New-HVFarm { .PARAMETER NamingMethod Determines how the VMs in the farm are named. Set PATTERN to use naming pattern. - The default value is PATTERN. Curentlly only PATTERN is allowed. + The default value is PATTERN. Currently only PATTERN is allowed. .PARAMETER NamingPattern RDS Servers will be named according to the specified naming pattern. @@ -1879,26 +1882,42 @@ function New-HVFarm { .PARAMETER MaximumCount Maximum number of Servers in the farm. The default value is 1. - Applicable to Linked Clone farms. + Applicable to Linked Clone and Instant Clone farms. .PARAMETER AdContainer This is the Active Directory container which the Servers will be added to upon creation. The default value is 'CN=Computers'. - Applicable to Linked Clone farm. + Applicable to Linked Clone and Instant Clone farms. .PARAMETER NetBiosName Domain Net Bios Name. - Applicable to Linked Clone farms. + Applicable to Linked Clone and Instant Clone farms. .PARAMETER DomainAdmin Domain Administrator user name which will be used to join the domain. Default value is null. - Applicable to Linked Clone farms. + Applicable to Linked Clone and Instant Clone farms. .PARAMETER SysPrepName The customization spec to use. Applicable to Linked Clone farms. +.PARAMETER PowerOffScriptName + Power off script. ClonePrep can run a customization script on instant-clone machines before they are powered off. Provide the path to the script on the parent virtual machine. + Applicable to Instant Clone farms. + +.PARAMETER PowerOffScriptParameters + Power off script parameters. Example: p1 p2 p3 + Applicable to Instant Clone farms. + +.PARAMETER PostSynchronizationScriptName + Post synchronization script. ClonePrep can run a customization script on instant-clone machines after they are created or recovered or a new image is pushed. Provide the path to the script on the parent virtual machine. + Applicable to Instant Clone farms. + +.PARAMETER PostSynchronizationScriptParameters + Post synchronization script parameters. Example: p1 p2 p3 + Applicable to Instant Clone farms. + .PARAMETER RdsServers List of existing registered RDS server names to add into manual farm. Applicable to Manual farms. @@ -1911,12 +1930,20 @@ function New-HVFarm { .EXAMPLE Creates new linkedClone farm by using naming pattern - New-HVFarm -LinkedClone -FarmName 'LCFarmTest' -ParentVM 'Win_Server_2012_R2' -SnapshotVM 'Snap_RDS' -VmFolder 'PoolVM' -HostOrCluster 'cls' -ResourcePool 'cls' -Datastores 'datastore1 (5)' -FarmDisplayName 'LC Farm Test' -Description  'created LC Farm from PS' -EnableProvisioning $true -StopOnProvisioningError $false -NamingPattern  "LCFarmVM_PS" -MinReady 1 -MaximumCount 1 -SysPrepName "RDSH_Cust2" -NetBiosName "adviewdev" + New-HVFarm -LinkedClone -FarmName 'LCFarmTest' -ParentVM 'Win_Server_2012_R2' -SnapshotVM 'Snap_RDS' -VmFolder 'PoolVM' -HostOrCluster 'cls' -ResourcePool 'cls' -Datastores 'datastore1 (5)' -FarmDisplayName 'LC Farm Test' -Description 'created LC Farm from PS' -EnableProvisioning $true -StopOnProvisioningError $false -NamingPattern "LCFarmVM_PS" -MinReady 1 -MaximumCount 1 -SysPrepName "RDSH_Cust2" -NetBiosName "adviewdev" + +.EXAMPLE + Creates new linkedClone farm by using naming pattern + New-HVFarm -InstantClone -FarmName 'ICFarmCL' -ParentVM 'vm-rdsh-ic' -SnapshotVM 'Snap_5' -VmFolder 'Instant_Clone_VMs' -HostOrCluster 'vimal-cluster' -ResourcePool 'vimal-cluster' -Datastores 'datastore1' -FarmDisplayName 'IC Farm using CL' -Description 'created IC Farm from PS command-line' -EnableProvisioning $true -StopOnProvisioningError $false -NamingPattern "ICFarmCL-" -NetBiosName "ad-vimalg" .EXAMPLE Creates new linkedClone farm by using json file New-HVFarm -Spec C:\VMWare\Specs\LinkedClone.json -Confirm:$false +.EXAMPLE + Creates new linkedClone farm by using json file + New-HVFarm -Spec C:\VMWare\Specs\InstantCloneFarm.json -Confirm:$false + .EXAMPLE Creates new manual farm by using rdsServers names New-HVFarm -Manual -FarmName "manualFarmTest" -FarmDisplayName "manualFarmTest" -Description "Manual PS Test" -RdsServers "vm-for-rds.eng.vmware.com","vm-for-rds-2.eng.vmware.com" -Confirm:$false @@ -1945,6 +1972,10 @@ function New-HVFarm { [switch] $LinkedClone, + [Parameter(Mandatory = $true,ParameterSetName = "INSTANT_CLONE")] + [switch] + $InstantClone, + [Parameter(Mandatory = $true,ParameterSetName = 'MANUAL')] [switch] $Manual, @@ -1952,6 +1983,7 @@ function New-HVFarm { #farmSpec.farmData.name [Parameter(Mandatory = $true,ParameterSetName = 'MANUAL')] [Parameter(Mandatory = $true,ParameterSetName = "LINKED_CLONE")] + [Parameter(Mandatory = $true,ParameterSetName = "INSTANT_CLONE")] [string] $FarmName, @@ -2040,108 +2072,129 @@ function New-HVFarm { [string] $Url, - #farmSpec.automatedfarmSpec.virtualCenter if LINKED_CLONE + #farmSpec.automatedfarmSpec.virtualCenter if LINKED_CLONE, INSTANT_CLONE [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [Parameter(Mandatory = $false,ParameterSetName = 'INSTANT_CLONE')] [string] $Vcenter, - #farmSpec.automatedfarmSpec.virtualCenterProvisioningSettings.virtualCenterProvisioningData.parentVM if LINKED_CLONE + #farmSpec.automatedfarmSpec.virtualCenterProvisioningSettings.virtualCenterProvisioningData.parentVM if LINKED_CLONE, INSTANT_CLONE [Parameter(Mandatory = $true,ParameterSetName = "LINKED_CLONE")] + [Parameter(Mandatory = $true,ParameterSetName = 'INSTANT_CLONE')] [string] $ParentVM, - #farmSpec.automatedfarmSpec.virtualCenterProvisioningSettings.virtualCenterProvisioningData.snapshotVM if LINKED_CLONE + #farmSpec.automatedfarmSpec.virtualCenterProvisioningSettings.virtualCenterProvisioningData.snapshotVM if LINKED_CLONE, INSTANT_CLONE [Parameter(Mandatory = $true,ParameterSetName = "LINKED_CLONE")] + [Parameter(Mandatory = $true,ParameterSetName = 'INSTANT_CLONE')] [string] $SnapshotVM, - #farmSpec.automatedfarmSpec.virtualCenterProvisioningSettings.virtualCenterProvisioningData.vmFolder if LINKED_CLONE + #farmSpec.automatedfarmSpec.virtualCenterProvisioningSettings.virtualCenterProvisioningData.vmFolder if LINKED_CLONE, INSTANT_CLONE [Parameter(Mandatory = $true,ParameterSetName = "LINKED_CLONE")] + [Parameter(Mandatory = $true,ParameterSetName = 'INSTANT_CLONE')] [string] $VmFolder, - #farmSpec.automatedfarmSpec.virtualCenterProvisioningSettings.virtualCenterProvisioningData.hostOrCluster if LINKED_CLONE + #farmSpec.automatedfarmSpec.virtualCenterProvisioningSettings.virtualCenterProvisioningData.hostOrCluster if LINKED_CLONE, INSTANT_CLONE [Parameter(Mandatory = $true,ParameterSetName = "LINKED_CLONE")] + [Parameter(Mandatory = $true,ParameterSetName = 'INSTANT_CLONE')] [string] $HostOrCluster, - #farmSpec.automatedfarmSpec.virtualCenterProvisioningSettings.virtualCenterProvisioningData.resourcePool if LINKED_CLONE, INSTANT_CLONE, FULL_CLONE + #farmSpec.automatedfarmSpec.virtualCenterProvisioningSettings.virtualCenterProvisioningData.resourcePool if LINKED_CLONE, INSTANT_CLONE [Parameter(Mandatory = $true,ParameterSetName = "LINKED_CLONE")] + [Parameter(Mandatory = $true,ParameterSetName = 'INSTANT_CLONE')] [string] $ResourcePool, - #farmSpec.automatedfarmSpec.virtualCenterProvisioningSettings.virtualCenterProvisioningData.dataCenter if LINKED_CLONE, INSTANT_CLONE, FULL_CLONE + #farmSpec.automatedfarmSpec.virtualCenterProvisioningSettings.virtualCenterProvisioningData.dataCenter if LINKED_CLONE, INSTANT_CLONE [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [Parameter(Mandatory = $false,ParameterSetName = 'INSTANT_CLONE')] [string] $dataCenter, - #farmSpec.automatedfarmSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.datastore if LINKED_CLONE + #farmSpec.automatedfarmSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.datastore if LINKED_CLONE, INSTANT_CLONE [Parameter(Mandatory = $true,ParameterSetName = "LINKED_CLONE")] + [Parameter(Mandatory = $true,ParameterSetName = 'INSTANT_CLONE')] [string[]] $Datastores, - #farmSpec.automatedfarmSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.datastores.storageOvercommit if LINKED_CLONE + #farmSpec.automatedfarmSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.datastores.storageOvercommit if LINKED_CLONE, INSTANT_CLONE [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [Parameter(Mandatory = $false,ParameterSetName = 'INSTANT_CLONE')] [string[]] $StorageOvercommit = $null, - #farmSpec.automatedfarmSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.useVSAN if LINKED_CLONE + #farmSpec.automatedfarmSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.useVSAN if LINKED_CLONE, INSTANT_CLONE [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [Parameter(Mandatory = $false,ParameterSetName = 'INSTANT_CLONE')] [boolean] $UseVSAN = $false, - #farmSpec.automatedfarmSpec.virtualCenterProvisioningSettings.enableProvsioning if LINKED_CLONE + #farmSpec.automatedfarmSpec.virtualCenterProvisioningSettings.enableProvsioning if LINKED_CLONE, INSTANT_CLONE [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [Parameter(Mandatory = $false,ParameterSetName = 'INSTANT_CLONE')] [boolean] $EnableProvisioning = $true, - #farmSpec.automatedfarmSpec.virtualCenterProvisioningSettings.stopOnProvisioningError if LINKED_CLONE + #farmSpec.automatedfarmSpec.virtualCenterProvisioningSettings.stopOnProvisioningError if LINKED_CLONE, INSTANT_CLONE [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [Parameter(Mandatory = $false,ParameterSetName = 'INSTANT_CLONE')] [boolean] $StopOnProvisioningError = $true, [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [Parameter(Mandatory = $false,ParameterSetName = 'INSTANT_CLONE')] [string] $TransparentPageSharingScope = 'VM', - #farmSpec.automatedfarmSpec.rdsServerNamingSpec.namingMethod if LINKED_CLONE, INSTANT_CLONE, FULL_CLONE + #farmSpec.automatedfarmSpec.rdsServerNamingSpec.namingMethod if LINKED_CLONE, INSTANT_CLONE [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [Parameter(Mandatory = $false,ParameterSetName = 'INSTANT_CLONE')] [ValidateSet('PATTERN')] [string] $NamingMethod = 'PATTERN', - #farmSpec.automatedfarmSpec.rdsServerNamingSpec.patternNamingSettings.namingPattern if LINKED_CLONE + #farmSpec.automatedfarmSpec.rdsServerNamingSpec.patternNamingSettings.namingPattern if LINKED_CLONE, INSTANT_CLONE [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [Parameter(Mandatory = $false,ParameterSetName = 'INSTANT_CLONE')] [string] $NamingPattern = $farmName + '{n:fixed=4}', - #farmSpec.automatedfarmSpec.virtualCenterProvisioningSettings.minReadyVMsOnVComposerMaintenance if LINKED_CLONE + #farmSpec.automatedfarmSpec.virtualCenterProvisioningSettings.minReadyVMsOnVComposerMaintenance if LINKED_CLONE, INSTANT_CLONE [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [Parameter(Mandatory = $false,ParameterSetName = 'INSTANT_CLONE')] [int] $MinReady = 0, - #farmSpec.automatedfarmSpec.rdsServerNamingSpec.patternNamingSettings.maxNumberOfRDSServers if LINKED_CLONE + #farmSpec.automatedfarmSpec.rdsServerNamingSpec.patternNamingSettings.maxNumberOfRDSServers if LINKED_CLONE, INSTANT_CLONE [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [Parameter(Mandatory = $false,ParameterSetName = 'INSTANT_CLONE')] [int] $MaximumCount = 1, - #farmSpec.automatedfarmSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.viewComposerStorageSettings.useSeparateDatastoresReplicaAndOSDisks + #farmSpec.automatedfarmSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.viewComposerStorageSettings.useSeparateDatastoresReplicaAndOSDisks if INSTANT_CLONE, LINKED_CLONE [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [Parameter(Mandatory = $false,ParameterSetName = 'INSTANT_CLONE')] [boolean] $UseSeparateDatastoresReplicaAndOSDisks = $false, - #farmSpec.automatedfarmSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.viewComposerStorageSettings.replicaDiskDatastore + #farmSpec.automatedfarmSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.viewComposerStorageSettings.replicaDiskDatastore, if LINKED_CLONE, INSTANT_CLONE [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [Parameter(Mandatory = $false,ParameterSetName = 'INSTANT_CLONE')] [string] $ReplicaDiskDatastore, - #farmSpec.automatedfarmSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.viewComposerStorageSettings.useNativeSnapshots + #farmSpec.automatedfarmSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.viewComposerStorageSettings.useNativeSnapshots, if LINKED_CLONE, INSTANT_CLONE [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [Parameter(Mandatory = $false,ParameterSetName = 'INSTANT_CLONE')] [boolean] $UseNativeSnapshots = $false, - #farmSpec.automatedfarmSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.viewComposerStorageSettings.spaceReclamationSettings.reclaimVmDiskSpace + #farmSpec.automatedfarmSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.viewComposerStorageSettings.spaceReclamationSettings.reclaimVmDiskSpace, if LINKED_CLONE, INSTANT_CLONE [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [Parameter(Mandatory = $false,ParameterSetName = 'INSTANT_CLONE')] [boolean] $ReclaimVmDiskSpace = $false, @@ -2156,18 +2209,23 @@ function New-HVFarm { [VMware.Hv.FarmBlackoutTime[]] $BlackoutTimes, - #farmSpec.automatedfarmSpec.customizationSettings.adContainer if LINKED_CLONE + #farmSpec.automatedfarmSpec.customizationSettings.adContainer if LINKED_CLONE, INSTANT_CLONE [Parameter(Mandatory = $false,ParameterSetName = 'LINKED_CLONE')] + [Parameter(Mandatory = $false,ParameterSetName = "INSTANT_CLONE")] [string] $AdContainer = 'CN=Computers', #farmSpec.automatedfarmSpec.customizationSettings.domainAdministrator + #farmSpec.automatedfarmSpec.customizationSettings.cloneprepCustomizationSettings.instantCloneEngineDomainAdministrator [Parameter(Mandatory = $true,ParameterSetName = 'LINKED_CLONE')] + [Parameter(Mandatory = $true,ParameterSetName = 'INSTANT_CLONE')] [string] $NetBiosName, #farmSpec.automatedfarmSpec.customizationSettings.domainAdministrator + #farmSpec.automatedfarmSpec.customizationSettings.cloneprepCustomizationSettings.instantCloneEngineDomainAdministrator [Parameter(Mandatory = $false,ParameterSetName = 'LINKED_CLONE')] + [Parameter(Mandatory = $false,ParameterSetName = "INSTANT_CLONE")] [string] $DomainAdmin = $null, @@ -2181,14 +2239,36 @@ function New-HVFarm { [string] $SysPrepName, - #farmSpec.automatedfarmSpec.rdsServerMaxSessionsData.maxSessionsType if LINKED_CLONE + #desktopSpec.automatedfarmSpec.customizationSettings.cloneprepCustomizationSettings.powerOffScriptName if INSTANT_CLONE + [Parameter(Mandatory = $false,ParameterSetName = 'INSTANT_CLONE')] + [string] + $PowerOffScriptName, + + #farmSpec.automatedfarmSpec.customizationSettings.cloneprepCustomizationSettings.powerOffScriptParameters if INSTANT_CLONE + [Parameter(Mandatory = $false,ParameterSetName = 'INSTANT_CLONE')] + [string] + $PowerOffScriptParameters, + + #farmSpec.automatedfarmSpec.customizationSettings.cloneprepCustomizationSettings.postSynchronizationScriptName if INSTANT_CLONE + [Parameter(Mandatory = $false,ParameterSetName = 'INSTANT_CLONE')] + [string] + $PostSynchronizationScriptName, + + #farmSpec.automatedfarmSpec.customizationSettings.cloneprepCustomizationSettings.postSynchronizationScriptParameters if INSTANT_CLONE + [Parameter(Mandatory = $false,ParameterSetName = 'INSTANT_CLONE')] + [string] + $PostSynchronizationScriptParameters, + + #farmSpec.automatedfarmSpec.rdsServerMaxSessionsData.maxSessionsType if LINKED_CLONE, INSTANT_CLONE [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [Parameter(Mandatory = $false,ParameterSetName = "INSTANT_CLONE")] [ValidateSet("UNLIMITED", "LIMITED")] [string] $MaxSessionsType = "UNLIMITED", - #farmSpec.automatedfarmSpec.rdsServerMaxSessionsData.maxSessionsType if LINKED_CLONE + #farmSpec.automatedfarmSpec.rdsServerMaxSessionsData.maxSessionsType if LINKED_CLONE, INSTANT_CLONE [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] + [Parameter(Mandatory = $false,ParameterSetName = "INSTANT_CLONE")] [ValidateRange(1, [int]::MaxValue)] [int] $MaxSessions, @@ -2225,6 +2305,9 @@ function New-HVFarm { # ADContainerId # FarmSysprepCustomizationSettings # CustomizationSpecId + # FarmCloneprepCustomizationSettings + # InstantCloneEngineDomainAdministratorId + # # FarmManualfarmSpec # RDSServerId[] # @@ -2271,14 +2354,31 @@ function New-HVFarm { if ($null -ne $jsonObject.AutomatedFarmSpec.VirtualCenter) { $vCenter = $jsonObject.AutomatedFarmSpec.VirtualCenter } - $linkedClone = $true + $netBiosName = $jsonObject.NetBiosName - if ($null -ne $jsonObject.AutomatedFarmSpec.CustomizationSettings.DomainAdministrator) { - $domainAdministrator = $jsonObject.AutomatedFarmSpec.CustomizationSettings.DomainAdministrator + if (!$jsonObject.AutomatedFarmSpec.CustomizationSettings.AdContainer) { + Write-Host "adContainer was empty using CN=Computers" + } else { + $AdContainer = $jsonObject.AutomatedFarmSpec.CustomizationSettings.AdContainer + } + + #populate customization settings attributes based on the cutomizationType + if ($jsonObject.AutomatedFarmSpec.ProvisioningType -eq "INSTANT_CLONE_ENGINE") { + $InstantClone = $true + if ($null -ne $jsonObject.AutomatedFarmSpec.CustomizationSettings.CloneprepCustomizationSettings) { + $DomainAdmin = $jsonObject.AutomatedFarmSpec.CustomizationSettings.CloneprepCustomizationSettings.InstantCloneEngineDomainAdministrator + $powerOffScriptName = $jsonObject.AutomatedFarmSpec.CustomizationSettings.CloneprepCustomizationSettings.PowerOffScriptName + $powerOffScriptParameters = $jsonObject.AutomatedFarmSpec.CustomizationSettings.CloneprepCustomizationSettings.PowerOffScriptParameters + $postSynchronizationScriptName = $jsonObject.AutomatedFarmSpec.CustomizationSettings.CloneprepCustomizationSettings.PostSynchronizationScriptName + $postSynchronizationScriptParameters = $jsonObject.AutomatedFarmSpec.CustomizationSettings.CloneprepCustomizationSettings.PostSynchronizationScriptParameters + } + } elseif ($jsonObject.AutomatedFarmSpec.ProvisioningType -eq "VIEW_COMPOSER") { + $LinkedClone = $true + $DomainAdmin = $jsonObject.AutomatedFarmSpec.CustomizationSettings.domainAdministrator + $reusePreExistingAccounts = $jsonObject.AutomatedFarmSpec.CustomizationSettings.ReusePreExistingAccounts + $sysprepCustomizationSettings = $jsonObject.AutomatedFarmSpec.CustomizationSettings.SysprepCustomizationSettings + $sysPrepName = $sysprepCustomizationSettings.CustomizationSpec } - $adContainer = $jsonObject.AutomatedFarmSpec.CustomizationSettings.AdContainer - $reusePreExistingAccounts = $jsonObject.AutomatedFarmSpec.CustomizationSettings.ReusePreExistingAccounts - $sysPrepName = $jsonObject.AutomatedFarmSpec.CustomizationSettings.SysprepCustomizationSettings.CustomizationSpec $namingMethod = $jsonObject.AutomatedFarmSpec.RdsServerNamingSpec.NamingMethod $namingPattern = $jsonObject.AutomatedFarmSpec.RdsServerNamingSpec.patternNamingSettings.namingPattern @@ -2305,25 +2405,33 @@ function New-HVFarm { $storageOvercommit += $dtStore.StorageOvercommit } $useVSan = $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.virtualCenterStorageSettings.UseVSan - $useSeparateDatastoresReplicaAndOSDisks = $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.virtualCenterStorageSettings.ViewComposerStorageSettings.UseSeparateDatastoresReplicaAndOSDisks - if ($useSeparateDatastoresReplicaAndOSDisks) { + + ## ViewComposerStorageSettings for Linked-Clone farms + if ($LinkedClone -or $InstantClone) { + $useSeparateDatastoresReplicaAndOSDisks = $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.virtualCenterStorageSettings.ViewComposerStorageSettings.UseSeparateDatastoresReplicaAndOSDisks + if ($useSeparateDatastoresReplicaAndOSDisks) { $replicaDiskDatastore = $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.virtualCenterStorageSettings.ViewComposerStorageSettings.ReplicaDiskDatastore - } - $useNativeSnapshots = $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.virtualCenterStorageSettings.ViewComposerStorageSettings.UseNativeSnapshots - $reclaimVmDiskSpace = $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.virtualCenterStorageSettings.ViewComposerStorageSettings.SpaceReclamationSettings.ReclaimVmDiskSpace - if ($reclaimVmDiskSpace) { - $ReclamationThresholdGB = $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.virtualCenterStorageSettings.ViewComposerStorageSettings.SpaceReclamationSettings.ReclamationThresholdGB - if ($null -ne $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.virtualCenterStorageSettings.ViewComposerStorageSettings.SpaceReclamationSettings.blackoutTimes) { - $blackoutTimesList = $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.virtualCenterStorageSettings.ViewComposerStorageSettings.SpaceReclamationSettings.blackoutTimes - foreach ($blackout in $blackoutTimesList) { - $blackoutObj = New-Object VMware.Hv.DesktopBlackoutTime - $blackoutObj.Days = $blackout.Days - $blackoutObj.StartTime = $blackout.StartTime - $blackoutObj.EndTime = $blackoutObj.EndTime - $blackoutTimes += $blackoutObj + } + if ($LinkedClone) { + #For Instant clone desktops, this setting can only be set to false + $useNativeSnapshots = $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.virtualCenterStorageSettings.ViewComposerStorageSettings.UseNativeSnapshots + $reclaimVmDiskSpace = $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.virtualCenterStorageSettings.ViewComposerStorageSettings.SpaceReclamationSettings.ReclaimVmDiskSpace + if ($reclaimVmDiskSpace) { + $ReclamationThresholdGB = $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.virtualCenterStorageSettings.ViewComposerStorageSettings.SpaceReclamationSettings.ReclamationThresholdGB + if ($null -ne $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.virtualCenterStorageSettings.ViewComposerStorageSettings.SpaceReclamationSettings.blackoutTimes) { + $blackoutTimesList = $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.virtualCenterStorageSettings.ViewComposerStorageSettings.SpaceReclamationSettings.blackoutTimes + foreach ($blackout in $blackoutTimesList) { + $blackoutObj = New-Object VMware.Hv.DesktopBlackoutTime + $blackoutObj.Days = $blackout.Days + $blackoutObj.StartTime = $blackout.StartTime + $blackoutObj.EndTime = $blackoutObj.EndTime + $blackoutTimes += $blackoutObj + } } } + } } + $maxSessionsType = $jsonObject.AutomatedFarmSpec.RdsServerMaxSessionsData.MaxSessionsType if ($maxSessionsType -eq "LIMITED") { $maxSessions = $jsonObject.AutomatedFarmSpec.RdsServerMaxSessionsData.MaxSessions @@ -2370,7 +2478,10 @@ function New-HVFarm { if ($linkedClone) { $farmType = 'AUTOMATED' $provisioningType = 'VIEW_COMPOSER' - } elseif ($manual) { + } elseif ($InstantClone) { + $farmType = 'AUTOMATED' + $provisioningType = 'INSTANT_CLONE_ENGINE' + }elseif ($manual) { $farmType = 'MANUAL' } @@ -2601,12 +2712,16 @@ function Test-HVFarmSpec { if ($null -eq $PoolObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.VirtualCenterStorageSettings.useVSan) { Throw "Must specify whether to use virtual SAN or not" } - if ($null -eq $PoolObject.AutomatedFarmSpec.CustomizationSettings.customizationType) { + $customizationType = $PoolObject.AutomatedFarmSpec.CustomizationSettings.customizationType + if ($null -eq $customizationType) { Throw "Specify customization type" } - if ($null -eq $PoolObject.AutomatedFarmSpec.CustomizationSettings.SysprepCustomizationSettings) { + if ($customizationType -eq 'SYS_PREP' -and $null -eq $PoolObject.AutomatedFarmSpec.CustomizationSettings.SysprepCustomizationSettings) { Throw "Specify sysPrep customization settings" } + if ($customizationType -eq 'CLONE_PREP' -and $null -eq $PoolObject.AutomatedFarmSpec.CustomizationSettings.CloneprepCustomizationSettings) { + Throw "Specify clone customization settings" + } if ($null -eq $PoolObject.AutomatedFarmSpec.RdsServerMaxSessionsData.MaxSessionsType) { Throw "Specify MaxSessionsType" } @@ -2751,9 +2866,11 @@ function Get-HVFarmStorageObject { $StorageObject.Datastores += $datastoresObj } if ($useSeparateDatastoresReplicaAndOSDisks) { + $StorageObject.ViewComposerStorageSettings.UseSeparateDatastoresReplicaAndOSDisks = $UseSeparateDatastoresReplicaAndOSDisks $FarmReplicaDiskDatastore = ($datastoreList | Where-Object { $_.datastoredata.name -eq $replicaDiskDatastore }).id + $StorageObject.ViewComposerStorageSettings.ReplicaDiskDatastore = $FarmReplicaDiskDatastore } - $StorageObject.ViewComposerStorageSettings.ReplicaDiskDatastore = $FarmReplicaDiskDatastore + } if ($storageObject.Datastores.Count -eq 0) { throw "No datastores found with name: [$datastores]" @@ -2784,52 +2901,77 @@ function Get-HVFarmCustomizationSetting { [VMware.Hv.VirtualCenterId]$VcID ) if (!$customObject) { - $ViewComposerDomainAdministrator_service_helper = New-Object VMware.Hv.ViewComposerDomainAdministratorService - $ViewComposerDomainAdministratorID = ($ViewComposerDomainAdministrator_service_helper.ViewComposerDomainAdministrator_List($services, $vcID) | Where-Object { $_.base.domain -match $netBiosName }) - if (! [string]::IsNullOrWhitespace($domainAdmin)) { - $ViewComposerDomainAdministratorID = ($ViewComposerDomainAdministratorID | Where-Object { $_.base.userName -ieq $domainAdmin }).id - } elseif ($null -ne $ViewComposerDomainAdministratorID) { - $ViewComposerDomainAdministratorID = $ViewComposerDomainAdministratorID[0].id + # View Composer and Instant Clone Engine Active Directory container for QuickPrep and ClonePrep. This must be set for Instant Clone Engine or SVI sourced desktops. + if ($InstantClone -or $LinkedClone) { + $ad_domain_helper = New-Object VMware.Hv.ADDomainService + $ADDomains = $ad_domain_helper.ADDomain_List($services) + if ($netBiosName) { + $adDomianId = ($ADDomains | Where-Object { $_.NetBiosName -eq $netBiosName } | Select-Object -Property id) + if ($null -eq $adDomianId) { + throw "No Domain found with netBiosName: [$netBiosName]" + } + } else { + $adDomianId = ($ADDomains[0] | Select-Object -Property id) + if ($null -eq $adDomianId) { + throw "No Domain configured in view administrator UI" + } + } + $ad_container_helper = New-Object VMware.Hv.AdContainerService + $adContainerId = ($ad_container_helper.ADContainer_ListByDomain($services,$adDomianId.id) | Where-Object { $_.Rdn -eq $adContainer } | Select-Object -Property id).id + if ($null -eq $adContainerId) { + throw "No AdContainer found with name: [$adContainer]" + } + $farmSpecObj.AutomatedFarmSpec.CustomizationSettings.AdContainer = $adContainerId } - if ($null -eq $ViewComposerDomainAdministratorID) { - throw "No Composer Domain Administrator found with netBiosName: [$netBiosName]" - } - $ADDomain_service_helper = New-Object VMware.Hv.ADDomainService - $ADDomains = $ADDomain_service_helper.ADDomain_List($services) - if ($netBiosName) { - $adDomianId = ( $ADDomains| Where-Object { $_.NetBiosName -eq $netBiosName } | Select-Object -Property id) - if ($null -eq $adDomianId) { - throw "No Domain found with netBiosName: [$netBiosName]" + + if ($InstantClone) { + $farmSpecObj.AutomatedFarmSpec.CustomizationSettings.CustomizationType = 'CLONE_PREP' + $instantCloneEngineDomainAdministrator_helper = New-Object VMware.Hv.InstantCloneEngineDomainAdministratorService + $instantCloneEngineDomainAdministrator = ($instantCloneEngineDomainAdministrator_helper.InstantCloneEngineDomainAdministrator_List($services) | Where-Object { $_.namesData.dnsName -match $netBiosName }) + if (![string]::IsNullOrWhitespace($domainAdmin)) { + $instantCloneEngineDomainAdministrator = ($instantCloneEngineDomainAdministrator | Where-Object { $_.base.userName -eq $domainAdmin }).id + } elseIf ($null -ne $instantCloneEngineDomainAdministrator) { + $instantCloneEngineDomainAdministrator = $instantCloneEngineDomainAdministrator[0].id } - } else { - $adDomianId = ( $ADDomains[0] | Select-Object -Property id) - if ($null -eq $adDomianId) { - throw "No Domain configured in view administrator UI" + if ($null -eq $instantCloneEngineDomainAdministrator) { + throw "No Instant Clone Engine Domain Administrator found with netBiosName: [$netBiosName]" + } + $farmSpecObj.AutomatedFarmSpec.CustomizationSettings.CloneprepCustomizationSettings = New-Object VMware.Hv.FarmClonePrepCustomizationSettings + $farmSpecObj.AutomatedFarmSpec.CustomizationSettings.CloneprepCustomizationSettings.InstantCloneEngineDomainAdministrator = $instantCloneEngineDomainAdministrator + $farmSpecObj.AutomatedFarmSpec.CustomizationSettings.CloneprepCustomizationSettings.powerOffScriptName = $powerOffScriptName + $farmSpecObj.AutomatedFarmSpec.CustomizationSettings.CloneprepCustomizationSettings.powerOffScriptParameters = $powerOffScriptParameters + $farmSpecObj.AutomatedFarmSpec.CustomizationSettings.CloneprepCustomizationSettings.postSynchronizationScriptName = $postSynchronizationScriptName + $farmSpecObj.AutomatedFarmSpec.CustomizationSettings.CloneprepCustomizationSettings.postSynchronizationScriptParameters = $postSynchronizationScriptParameters + $customObject = $farmSpecObj.AutomatedFarmSpec.CustomizationSettings + } elseif ($LinkedClone) { + $ViewComposerDomainAdministrator_service_helper = New-Object VMware.Hv.ViewComposerDomainAdministratorService + $ViewComposerDomainAdministratorID = ($ViewComposerDomainAdministrator_service_helper.ViewComposerDomainAdministrator_List($services, $vcID) | Where-Object { $_.base.domain -match $netBiosName }) + if (! [string]::IsNullOrWhitespace($domainAdmin)) { + $ViewComposerDomainAdministratorID = ($ViewComposerDomainAdministratorID | Where-Object { $_.base.userName -ieq $domainAdmin }).id + } elseif ($null -ne $ViewComposerDomainAdministratorID) { + $ViewComposerDomainAdministratorID = $ViewComposerDomainAdministratorID[0].id + } + if ($null -eq $ViewComposerDomainAdministratorID) { + throw "No Composer Domain Administrator found with netBiosName: [$netBiosName]" } - } - $ad_containder_service_helper = New-Object VMware.Hv.AdContainerService - $adContainerId = ($ad_containder_service_helper.ADContainer_ListByDomain($services, $adDomianId.id) | Where-Object { $_.Rdn -eq $adContainer } | Select-Object -Property id).id - if ($null -eq $adContainerId) { - throw "No AdContainer found with name: [$adContainer]" - } - #Support only Sysprep Customization - $sysprepCustomizationSettings = $farmSpecObj.AutomatedFarmSpec.CustomizationSettings.SysprepCustomizationSettings - # Get SysPrep CustomizationSpec ID - $CustomizationSpec_service_helper = New-Object VMware.Hv.CustomizationSpecService - $sysPrepIds = $CustomizationSpec_service_helper.CustomizationSpec_List($services, $vcID) | Where-Object { $_.customizationSpecData.name -eq $sysPrepName } | Select-Object -Property id - if ($sysPrepIds.Count -eq 0) { - throw "No Sysprep Customization spec found with Name: [$sysPrepName]" + #Support only Sysprep Customization + $sysprepCustomizationSettings = $farmSpecObj.AutomatedFarmSpec.CustomizationSettings.SysprepCustomizationSettings + + # Get SysPrep CustomizationSpec ID + $CustomizationSpec_service_helper = New-Object VMware.Hv.CustomizationSpecService + $sysPrepIds = $CustomizationSpec_service_helper.CustomizationSpec_List($services, $vcID) | Where-Object { $_.customizationSpecData.name -eq $sysPrepName } | Select-Object -Property id + if ($sysPrepIds.Count -eq 0) { + throw "No Sysprep Customization spec found with Name: [$sysPrepName]" + } + $sysprepCustomizationSettings.CustomizationSpec = $sysPrepIds[0].id + + $farmSpecObj.AutomatedFarmSpec.CustomizationSettings.CustomizationType = 'SYS_PREP' + $farmSpecObj.AutomatedFarmSpec.CustomizationSettings.DomainAdministrator = $ViewComposerDomainAdministratorID + $farmSpecObj.AutomatedFarmSpec.CustomizationSettings.ReusePreExistingAccounts = $reusePreExistingAccounts + $farmSpecObj.AutomatedFarmSpec.CustomizationSettings.SysprepCustomizationSettings = $sysprepCustomizationSettings + $customObject = $farmSpecObj.AutomatedFarmSpec.CustomizationSettings } - $sysprepCustomizationSettings.CustomizationSpec = $sysPrepIds[0].id - - $farmSpecObj.AutomatedFarmSpec.CustomizationSettings.CustomizationType = 'SYS_PREP' - $farmSpecObj.AutomatedFarmSpec.CustomizationSettings.DomainAdministrator = $ViewComposerDomainAdministratorID - $farmSpecObj.AutomatedFarmSpec.CustomizationSettings.AdContainer = $adContainerId - $farmSpecObj.AutomatedFarmSpec.CustomizationSettings.ReusePreExistingAccounts = $reusePreExistingAccounts - $farmSpecObj.AutomatedFarmSpec.CustomizationSettings.SysprepCustomizationSettings = $sysprepCustomizationSettings - - $customObject = $farmSpecObj.AutomatedFarmSpec.CustomizationSettings } return $customObject } @@ -5658,10 +5800,10 @@ function Set-HVPool { function Start-HVFarm { <# .SYNOPSIS - Perform maintenance tasks on the farm(s). + Performs maintenance tasks on the farm(s). .DESCRIPTION - This function is used to perform maintenance tasks like enable/disable, start/stop and recompose the farm. + This function is used to perform maintenance tasks like enable/disable, start/stop and recompose the farm. This function is also used for scheduling maintenance operation on instant-clone farm(s). .PARAMETER Farm Name/Object(s) of the farm. Object(s) should be of type FarmSummaryView/FarmInfo. @@ -5669,16 +5811,23 @@ function Start-HVFarm { .PARAMETER Recompose Switch for recompose operation. Requests a recompose of RDS Servers in the specified 'AUTOMATED' farm. This marks the RDS Servers for recompose, which is performed asynchronously. +.PARAMETER ScheduleMaintenance + Switch for ScheduleMaintenance operation. Requests for scheduling maintenance operation on RDS Servers in the specified Instant clone farm. This marks the RDS Servers for scheduled maintenance, which is performed according to the schedule. + +.PARAMETER CancelMaintenance + Switch for cancelling maintenance operation. Requests for cancelling a scheduled maintenance operation on the specified Instant clone farm. This stops further maintenance operation on the given farm. + .PARAMETER StartTime - Specifies when to start the operation. If unset, the operation will begin immediately. + Specifies when to start the recompose/scheduleMaintenance operation. If unset, the recompose operation will begin immediately. + For IMMEDIATE maintenance if unset, maintenance will begin immediately. For RECURRING maintenance if unset, will be calculated based on recurring maintenance configuration. If in the past, maintenance will begin immediately. .PARAMETER LogoffSetting Determines when to perform the operation on machines which have an active session. This property will be one of: - "FORCE_LOGOFF" - Users will be forced to log off when the system is ready to operate on their RDS Servers. Before being forcibly logged off, users may have a grace period in which to save their work (Global Settings). + "FORCE_LOGOFF" - Users will be forced to log off when the system is ready to operate on their RDS Servers. Before being forcibly logged off, users may have a grace period in which to save their work (Global Settings). This is the default value. "WAIT_FOR_LOGOFF" - Wait for connected users to disconnect before the task starts. The operation starts immediately on RDS Servers without active sessions. .PARAMETER StopOnFirstError - Indicates that the operation should stop on first error. + Indicates that the operation should stop on first error. Defaults to true. .PARAMETER Servers The RDS Server(s) id to recompose. Provide a comma separated list for multiple RDSServerIds. @@ -5692,6 +5841,30 @@ function Start-HVFarm { .PARAMETER Vcenter Virtual Center server-address (IP or FQDN) of the given farm. This should be same as provided to the Connection Server while adding the vCenter server. +.PARAMETER MaintenanceMode + The mode of schedule maintenance for Instant Clone Farm. This property will be one of: + "IMMEDIATE" - All server VMs will be refreshed once, immediately or at user scheduled time. + "RECURRING" - All server VMs will be periodically refreshed based on MaintenancePeriod and MaintenanceStartTime. + +.PARAMETER MaintenanceStartTime + Configured start time for the recurring maintenance. This property must be in the form hh:mm in 24 hours format. + +.PARAMETER MaintenancePeriod + This represents the frequency at which to perform recurring maintenance. This property will be one of: + "DAILY" - Daily recurring maintenance + "WEEKLY" - Weekly recurring maintenance + "MONTHLY" - Monthly recurring maintenance + +.PARAMETER StartInt + Start index for weekly or monthly maintenance. Weekly: 1-7 (Sun-Sat), Monthly: 1-31. + This property is required if maintenancePeriod is set to "WEEKLY"or "MONTHLY". + This property has values 1-7 for maintenancePeriod "WEEKLY". + This property has values 1-31 for maintenancePeriod "MONTHLY". + +.PARAMETER EveryInt + How frequently to repeat maintenance, expressed as a multiple of the maintenance period. e.g. Every 2 weeks. + This property has a default value of 1. This property has values 1-100. + .PARAMETER HvServer Reference to Horizon View Server to query the data from. If the value is not passed or null then first element from global:DefaultHVServers would be considered inplace of hvServer. @@ -5704,6 +5877,18 @@ function Start-HVFarm { $myTime = Get-Date '10/03/2016 12:30:00' Start-HVFarm -Farm 'Farm-01' -Recompose -LogoffSetting 'FORCE_LOGOFF' -ParentVM 'ParentVM' -SnapshotVM 'SnapshotVM' -StartTime $myTime +.EXAMPLE + Requests a ScheduleMaintenance task for instant-clone farm. Schedules an IMMEDIATE maintenance. + Start-HVFarm -Farm 'ICFarm-01' -ScheduleMaintenance -MaintenanceMode IMMEDIATE + +.EXAMPLE + Requests a ScheduleMaintenance task for instant-clone farm. Schedules a recurring weekly maintenace every Saturday night at 23:30 and updates the parentVM and snapshot. + Start-HVFarm -ScheduleMaintenance -Farm 'ICFarm-01' -MaintenanceMode RECURRING -MaintenancePeriod WEEKLY -MaintenanceStartTime '11:30' -StartInt 6 -EveryInt 1 -ParentVM 'vm-rdsh-ic' -SnapshotVM 'Snap_Updated' + +.EXAMPLE + Requests a CancelMaintenance task for instant-clone farm. Cancels recurring maintenance. + Start-HVFarm -CancelMaintenance -Farm 'ICFarm-01' -MaintenanceMode RECURRING + .OUTPUTS None @@ -5729,28 +5914,61 @@ function Start-HVFarm { [Parameter(Mandatory = $false,ParameterSetName = 'RECOMPOSE')] [switch]$Recompose, + [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULEMAINTENANCE')] + [switch]$ScheduleMaintenance, + + [Parameter(Mandatory = $false,ParameterSetName = 'CANCELMAINTENANCE')] + [switch]$CancelMaintenance, + [Parameter(Mandatory = $false,ParameterSetName = 'RECOMPOSE')] + [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULEMAINTENANCE')] [System.DateTime]$StartTime, [Parameter(Mandatory = $true,ParameterSetName = 'RECOMPOSE')] + [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULEMAINTENANCE')] [ValidateSet('FORCE_LOGOFF','WAIT_FOR_LOGOFF')] - [string]$LogoffSetting, + [string]$LogoffSetting = 'FORCE_LOGOFF', [Parameter(Mandatory = $false,ParameterSetName = 'RECOMPOSE')] + [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULEMAINTENANCE')] [boolean]$StopOnFirstError = $true, [Parameter(Mandatory = $false,ParameterSetName = 'RECOMPOSE')] [string []]$Servers, [Parameter(Mandatory = $true,ParameterSetName = 'RECOMPOSE')] + [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULEMAINTENANCE')] [string]$ParentVM, [Parameter(Mandatory = $true,ParameterSetName = 'RECOMPOSE')] + [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULEMAINTENANCE')] [string]$SnapshotVM, [Parameter(Mandatory = $false,ParameterSetName = 'RECOMPOSE')] + [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULEMAINTENANCE')] [string]$Vcenter, + [Parameter(Mandatory = $true,ParameterSetName = 'SCHEDULEMAINTENANCE')] + [Parameter(Mandatory = $true,ParameterSetName = 'CANCELMAINTENANCE')] + [ValidateSet('IMMEDIATE','RECURRING')] + [string]$MaintenanceMode, + + [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULEMAINTENANCE')] + [ValidatePattern('^([0-9]|0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$')] + [string]$MaintenanceStartTime, + + [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULEMAINTENANCE')] + [ValidateSet('DAILY','WEEKLY','MONTHLY')] + [string]$MaintenancePeriod, + + [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULEMAINTENANCE')] + [ValidateRange(1, 31)] + [int]$StartInt, + + [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULEMAINTENANCE')] + [ValidateRange(1, 100)] + [int]$EveryInt = 1, + [Parameter(Mandatory = $false)] $HvServer = $null ) @@ -5773,23 +5991,26 @@ function Start-HVFarm { $id = $farm.id $name = $farm.data.name $type = $farm.type + $source = $farm.source } elseif ($farm.GetType().name -eq 'FarmSummaryView') { $id = $farm.id $name = $farm.data.name $type = $farm.data.type + $source = $farm.data.source } elseif ($farm.GetType().name -eq 'String') { try { - $farmSpecObj = Get-HVFarm -farmName $farm -hvServer $hvServer + $farmSpecObj = Get-HVFarmSummary -farmName $farm -hvServer $hvServer } catch { - Write-Error "Make sure Get-HVFarm advanced function is loaded, $_" + Write-Error "Make sure Get-HVFarmSummary advanced function is loaded, $_" break } if ($farmSpecObj) { $id = $farmSpecObj.id $name = $farmSpecObj.data.name $type = $farmSpecObj.data.type + $source = $farmSpecObj.data.source } else { Write-Error "Unable to retrieve FarmSummaryView with given farmName [$farm]" break @@ -5798,7 +6019,7 @@ function Start-HVFarm { Write-Error "In pipeline did not get object of expected type FarmSummaryView/FarmInfo" break } - if ($type -eq 'AUTOMATED') { + if (!$source) { $source = 'VIEW_COMPOSER' } $farmList.Add($id,$name) @@ -5848,7 +6069,64 @@ function Start-HVFarm { Write-Host "Performed recompose task on farm: " $farmList.$item } } - } + 'SCHEDULEMAINTENANCE' { + if ($farmSource.$item -ne 'INSTANT_CLONE_ENGINE') { + Write-Error "SCHEDULEMAINTENANCE operation is not supported for farm with name [$farmList.$item]. It is only supported for instant-clone farms." + break + } else { + $spec = New-Object VMware.Hv.FarmMaintenanceSpec + $spec.MaintenanceMode = $MaintenanceMode + $spec.ScheduledTime = $StartTime + if ($MaintenanceMode -eq "RECURRING") { + $spec.RecurringMaintenanceSettings = New-Object VMware.Hv.FarmRecurringMaintenanceSettings + $spec.RecurringMaintenanceSettings.MaintenancePeriod = $MaintenancePeriod + $spec.RecurringMaintenanceSettings.EveryInt = $EveryInt + if (!$MaintenanceStartTime) { + Write-Error "MaintenanceStartTime must be defined for MaintenanceMode = RECURRING." + break; + } else { + $spec.RecurringMaintenanceSettings.StartTime = $MaintenanceStartTime + } + if ($MaintenancePeriod -ne 'DAILY') { + if (!$StartInt) { + Write-Error "StartInt must be defined for MaintenancePeriod WEEKLY or MONTHLY." + break; + } else { + $spec.RecurringMaintenanceSettings.StartInt = $StartInt + } + } + } + #image settings are specified + if ($ParentVM -and $SnapshotVM) { + $spec.ImageMaintenanceSettings = New-Object VMware.Hv.FarmImageMaintenanceSettings + $spec.ImageMaintenanceSettings.LogoffSetting = $LogoffSetting + $spec.ImageMaintenanceSettings.StopOnFirstError = $StopOnFirstError + $vcId = Get-VcenterID -services $services -vCenter $Vcenter + if ($null -eq $vcId) { + Write-Error "VCenter is required if you specify ParentVM name." + break + } + try { + $spec.ImageMaintenanceSettings = Set-HVFarmSpec -vcId $vcId -spec $spec.ImageMaintenanceSettings + } catch { + Write-Error "SCHEDULEMAINTENANCE task failed with error: $_" + break + } + } + # call scheduleMaintenance service on farm + if ($pscmdlet.ShouldProcess($farmList.$item)) { + $farm_service_helper.Farm_ScheduleMaintenance($services, $item, $spec) + Write-Host "Performed SCHEDULEMAINTENANCE task on farm: " $farmList.$item + } + } + } + 'CANCELMAINTENANCE' { + if ($pscmdlet.ShouldProcess($farmList.$item)) { + $farm_service_helper.Farm_CancelScheduleMaintenance($services, $item, $MaintenanceMode) + Write-Host "Performed CancelMaintenance task on farm: " $farmList.$item + } + } + } return } } From ce4dab7df6c5285b738af1a38c823305548deef6 Mon Sep 17 00:00:00 2001 From: vkg1215 Date: Mon, 23 Jan 2017 12:15:03 +0530 Subject: [PATCH 008/112] Use Get-HVFarm instead of Get-HVFarmSummary Use Get-HVFarm instead of Get-HVFarmSummary --- Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 index e5cb880..9e51390 100644 --- a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 +++ b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 @@ -6001,16 +6001,16 @@ function Start-HVFarm { } elseif ($farm.GetType().name -eq 'String') { try { - $farmSpecObj = Get-HVFarmSummary -farmName $farm -hvServer $hvServer + $farmSpecObj = Get-HVFarm -farmName $farm -hvServer $hvServer } catch { - Write-Error "Make sure Get-HVFarmSummary advanced function is loaded, $_" + Write-Error "Make sure Get-HVFarm advanced function is loaded, $_" break } if ($farmSpecObj) { $id = $farmSpecObj.id $name = $farmSpecObj.data.name - $type = $farmSpecObj.data.type - $source = $farmSpecObj.data.source + $type = $farmSpecObj.type + $source = $farmSpecObj.source } else { Write-Error "Unable to retrieve FarmSummaryView with given farmName [$farm]" break From 142e6361efcf9bb114889b1c65eb689502827356 Mon Sep 17 00:00:00 2001 From: vkg1215 Date: Mon, 23 Jan 2017 13:06:33 +0530 Subject: [PATCH 009/112] remove unused variable, and gave better name to farm operations Renamed SCHEDULEDMAINTENANCE operation to SCHEDULED_MAINTENANCE and CANCELMAINTENANCE to CANCEL_MAINTENANCE --- .../VMware.Hv.Helper/VMware.HV.Helper.psm1 | 67 +++++++++---------- 1 file changed, 33 insertions(+), 34 deletions(-) diff --git a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 index 9e51390..c0ffe49 100644 --- a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 +++ b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 @@ -2376,8 +2376,7 @@ function New-HVFarm { $LinkedClone = $true $DomainAdmin = $jsonObject.AutomatedFarmSpec.CustomizationSettings.domainAdministrator $reusePreExistingAccounts = $jsonObject.AutomatedFarmSpec.CustomizationSettings.ReusePreExistingAccounts - $sysprepCustomizationSettings = $jsonObject.AutomatedFarmSpec.CustomizationSettings.SysprepCustomizationSettings - $sysPrepName = $sysprepCustomizationSettings.CustomizationSpec + $sysPrepName = $jsonObject.AutomatedFarmSpec.CustomizationSettings.SysprepCustomizationSettings.CustomizationSpec } $namingMethod = $jsonObject.AutomatedFarmSpec.RdsServerNamingSpec.NamingMethod @@ -5811,14 +5810,14 @@ function Start-HVFarm { .PARAMETER Recompose Switch for recompose operation. Requests a recompose of RDS Servers in the specified 'AUTOMATED' farm. This marks the RDS Servers for recompose, which is performed asynchronously. -.PARAMETER ScheduleMaintenance - Switch for ScheduleMaintenance operation. Requests for scheduling maintenance operation on RDS Servers in the specified Instant clone farm. This marks the RDS Servers for scheduled maintenance, which is performed according to the schedule. +.PARAMETER Schedule_Maintenance + Switch for Schedule_Maintenance operation. Requests for scheduling maintenance operation on RDS Servers in the specified Instant clone farm. This marks the RDS Servers for scheduled maintenance, which is performed according to the schedule. -.PARAMETER CancelMaintenance +.PARAMETER Cancel_Maintenance Switch for cancelling maintenance operation. Requests for cancelling a scheduled maintenance operation on the specified Instant clone farm. This stops further maintenance operation on the given farm. .PARAMETER StartTime - Specifies when to start the recompose/scheduleMaintenance operation. If unset, the recompose operation will begin immediately. + Specifies when to start the recompose/Schedule_Maintenance operation. If unset, the recompose operation will begin immediately. For IMMEDIATE maintenance if unset, maintenance will begin immediately. For RECURRING maintenance if unset, will be calculated based on recurring maintenance configuration. If in the past, maintenance will begin immediately. .PARAMETER LogoffSetting @@ -5878,16 +5877,16 @@ function Start-HVFarm { Start-HVFarm -Farm 'Farm-01' -Recompose -LogoffSetting 'FORCE_LOGOFF' -ParentVM 'ParentVM' -SnapshotVM 'SnapshotVM' -StartTime $myTime .EXAMPLE - Requests a ScheduleMaintenance task for instant-clone farm. Schedules an IMMEDIATE maintenance. - Start-HVFarm -Farm 'ICFarm-01' -ScheduleMaintenance -MaintenanceMode IMMEDIATE + Requests a Schedule_Maintenance task for instant-clone farm. Schedules an IMMEDIATE maintenance. + Start-HVFarm -Farm 'ICFarm-01' -Schedule_Maintenance -MaintenanceMode IMMEDIATE .EXAMPLE - Requests a ScheduleMaintenance task for instant-clone farm. Schedules a recurring weekly maintenace every Saturday night at 23:30 and updates the parentVM and snapshot. - Start-HVFarm -ScheduleMaintenance -Farm 'ICFarm-01' -MaintenanceMode RECURRING -MaintenancePeriod WEEKLY -MaintenanceStartTime '11:30' -StartInt 6 -EveryInt 1 -ParentVM 'vm-rdsh-ic' -SnapshotVM 'Snap_Updated' + Requests a Schedule_Maintenance task for instant-clone farm. Schedules a recurring weekly maintenace every Saturday night at 23:30 and updates the parentVM and snapshot. + Start-HVFarm -Schedule_Maintenance -Farm 'ICFarm-01' -MaintenanceMode RECURRING -MaintenancePeriod WEEKLY -MaintenanceStartTime '11:30' -StartInt 6 -EveryInt 1 -ParentVM 'vm-rdsh-ic' -SnapshotVM 'Snap_Updated' .EXAMPLE - Requests a CancelMaintenance task for instant-clone farm. Cancels recurring maintenance. - Start-HVFarm -CancelMaintenance -Farm 'ICFarm-01' -MaintenanceMode RECURRING + Requests a Cancel_Maintenance task for instant-clone farm. Cancels recurring maintenance. + Start-HVFarm -Cancel_Maintenance -Farm 'ICFarm-01' -MaintenanceMode RECURRING .OUTPUTS None @@ -5914,58 +5913,58 @@ function Start-HVFarm { [Parameter(Mandatory = $false,ParameterSetName = 'RECOMPOSE')] [switch]$Recompose, - [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULEMAINTENANCE')] - [switch]$ScheduleMaintenance, + [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULE_MAINTENANCE')] + [switch]$Schedule_Maintenance, - [Parameter(Mandatory = $false,ParameterSetName = 'CANCELMAINTENANCE')] - [switch]$CancelMaintenance, + [Parameter(Mandatory = $false,ParameterSetName = 'CANCEL_MAINTENANCE')] + [switch]$Cancel_Maintenance, [Parameter(Mandatory = $false,ParameterSetName = 'RECOMPOSE')] - [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULEMAINTENANCE')] + [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULE_MAINTENANCE')] [System.DateTime]$StartTime, [Parameter(Mandatory = $true,ParameterSetName = 'RECOMPOSE')] - [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULEMAINTENANCE')] + [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULE_MAINTENANCE')] [ValidateSet('FORCE_LOGOFF','WAIT_FOR_LOGOFF')] [string]$LogoffSetting = 'FORCE_LOGOFF', [Parameter(Mandatory = $false,ParameterSetName = 'RECOMPOSE')] - [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULEMAINTENANCE')] + [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULE_MAINTENANCE')] [boolean]$StopOnFirstError = $true, [Parameter(Mandatory = $false,ParameterSetName = 'RECOMPOSE')] [string []]$Servers, [Parameter(Mandatory = $true,ParameterSetName = 'RECOMPOSE')] - [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULEMAINTENANCE')] + [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULE_MAINTENANCE')] [string]$ParentVM, [Parameter(Mandatory = $true,ParameterSetName = 'RECOMPOSE')] - [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULEMAINTENANCE')] + [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULE_MAINTENANCE')] [string]$SnapshotVM, [Parameter(Mandatory = $false,ParameterSetName = 'RECOMPOSE')] - [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULEMAINTENANCE')] + [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULE_MAINTENANCE')] [string]$Vcenter, - [Parameter(Mandatory = $true,ParameterSetName = 'SCHEDULEMAINTENANCE')] - [Parameter(Mandatory = $true,ParameterSetName = 'CANCELMAINTENANCE')] + [Parameter(Mandatory = $true,ParameterSetName = 'SCHEDULE_MAINTENANCE')] + [Parameter(Mandatory = $true,ParameterSetName = 'CANCEL_MAINTENANCE')] [ValidateSet('IMMEDIATE','RECURRING')] [string]$MaintenanceMode, - [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULEMAINTENANCE')] + [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULE_MAINTENANCE')] [ValidatePattern('^([0-9]|0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$')] [string]$MaintenanceStartTime, - [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULEMAINTENANCE')] + [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULE_MAINTENANCE')] [ValidateSet('DAILY','WEEKLY','MONTHLY')] [string]$MaintenancePeriod, - [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULEMAINTENANCE')] + [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULE_MAINTENANCE')] [ValidateRange(1, 31)] [int]$StartInt, - [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULEMAINTENANCE')] + [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULE_MAINTENANCE')] [ValidateRange(1, 100)] [int]$EveryInt = 1, @@ -6069,9 +6068,9 @@ function Start-HVFarm { Write-Host "Performed recompose task on farm: " $farmList.$item } } - 'SCHEDULEMAINTENANCE' { + 'SCHEDULE_MAINTENANCE' { if ($farmSource.$item -ne 'INSTANT_CLONE_ENGINE') { - Write-Error "SCHEDULEMAINTENANCE operation is not supported for farm with name [$farmList.$item]. It is only supported for instant-clone farms." + Write-Error "SCHEDULE_MAINTENANCE operation is not supported for farm with name [$farmList.$item]. It is only supported for instant-clone farms." break } else { $spec = New-Object VMware.Hv.FarmMaintenanceSpec @@ -6109,21 +6108,21 @@ function Start-HVFarm { try { $spec.ImageMaintenanceSettings = Set-HVFarmSpec -vcId $vcId -spec $spec.ImageMaintenanceSettings } catch { - Write-Error "SCHEDULEMAINTENANCE task failed with error: $_" + Write-Error "SCHEDULE_MAINTENANCE task failed with error: $_" break } } # call scheduleMaintenance service on farm if ($pscmdlet.ShouldProcess($farmList.$item)) { $farm_service_helper.Farm_ScheduleMaintenance($services, $item, $spec) - Write-Host "Performed SCHEDULEMAINTENANCE task on farm: " $farmList.$item + Write-Host "Performed SCHEDULE_MAINTENANCE task on farm: " $farmList.$item } } } - 'CANCELMAINTENANCE' { + 'CANCEL_MAINTENANCE' { if ($pscmdlet.ShouldProcess($farmList.$item)) { $farm_service_helper.Farm_CancelScheduleMaintenance($services, $item, $MaintenanceMode) - Write-Host "Performed CancelMaintenance task on farm: " $farmList.$item + Write-Host "Performed CANCEL_MAINTENANCE task on farm: " $farmList.$item } } } From a15c61cedfc25c28f7c23d6c81eaeb9fd89b49ac Mon Sep 17 00:00:00 2001 From: vkg1215 Date: Mon, 23 Jan 2017 14:36:26 +0530 Subject: [PATCH 010/112] Following code convention. do not name variables having _ Renamed SCHEDULEDMAINTENANCE operation to SCHEDULED_MAINTENANCE and CANCELMAINTENANCE to CANCEL_MAINTENANCE --- .../VMware.Hv.Helper/VMware.HV.Helper.psm1 | 66 +++++++++---------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 index c0ffe49..b7f272a 100644 --- a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 +++ b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 @@ -1941,7 +1941,7 @@ function New-HVFarm { New-HVFarm -Spec C:\VMWare\Specs\LinkedClone.json -Confirm:$false .EXAMPLE - Creates new linkedClone farm by using json file + Creates new instantClone farm by using json file New-HVFarm -Spec C:\VMWare\Specs\InstantCloneFarm.json -Confirm:$false .EXAMPLE @@ -5810,14 +5810,14 @@ function Start-HVFarm { .PARAMETER Recompose Switch for recompose operation. Requests a recompose of RDS Servers in the specified 'AUTOMATED' farm. This marks the RDS Servers for recompose, which is performed asynchronously. -.PARAMETER Schedule_Maintenance - Switch for Schedule_Maintenance operation. Requests for scheduling maintenance operation on RDS Servers in the specified Instant clone farm. This marks the RDS Servers for scheduled maintenance, which is performed according to the schedule. +.PARAMETER ScheduleMaintenance + Switch for ScheduleMaintenance operation. Requests for scheduling maintenance operation on RDS Servers in the specified Instant clone farm. This marks the RDS Servers for scheduled maintenance, which is performed according to the schedule. -.PARAMETER Cancel_Maintenance +.PARAMETER CancelMaintenance Switch for cancelling maintenance operation. Requests for cancelling a scheduled maintenance operation on the specified Instant clone farm. This stops further maintenance operation on the given farm. .PARAMETER StartTime - Specifies when to start the recompose/Schedule_Maintenance operation. If unset, the recompose operation will begin immediately. + Specifies when to start the recompose/ScheduleMaintenance operation. If unset, the recompose operation will begin immediately. For IMMEDIATE maintenance if unset, maintenance will begin immediately. For RECURRING maintenance if unset, will be calculated based on recurring maintenance configuration. If in the past, maintenance will begin immediately. .PARAMETER LogoffSetting @@ -5877,16 +5877,16 @@ function Start-HVFarm { Start-HVFarm -Farm 'Farm-01' -Recompose -LogoffSetting 'FORCE_LOGOFF' -ParentVM 'ParentVM' -SnapshotVM 'SnapshotVM' -StartTime $myTime .EXAMPLE - Requests a Schedule_Maintenance task for instant-clone farm. Schedules an IMMEDIATE maintenance. - Start-HVFarm -Farm 'ICFarm-01' -Schedule_Maintenance -MaintenanceMode IMMEDIATE + Requests a ScheduleMaintenance task for instant-clone farm. Schedules an IMMEDIATE maintenance. + Start-HVFarm -Farm 'ICFarm-01' -ScheduleMaintenance -MaintenanceMode IMMEDIATE .EXAMPLE - Requests a Schedule_Maintenance task for instant-clone farm. Schedules a recurring weekly maintenace every Saturday night at 23:30 and updates the parentVM and snapshot. - Start-HVFarm -Schedule_Maintenance -Farm 'ICFarm-01' -MaintenanceMode RECURRING -MaintenancePeriod WEEKLY -MaintenanceStartTime '11:30' -StartInt 6 -EveryInt 1 -ParentVM 'vm-rdsh-ic' -SnapshotVM 'Snap_Updated' + Requests a ScheduleMaintenance task for instant-clone farm. Schedules a recurring weekly maintenace every Saturday night at 23:30 and updates the parentVM and snapshot. + Start-HVFarm -ScheduleMaintenance -Farm 'ICFarm-01' -MaintenanceMode RECURRING -MaintenancePeriod WEEKLY -MaintenanceStartTime '11:30' -StartInt 6 -EveryInt 1 -ParentVM 'vm-rdsh-ic' -SnapshotVM 'Snap_Updated' .EXAMPLE - Requests a Cancel_Maintenance task for instant-clone farm. Cancels recurring maintenance. - Start-HVFarm -Cancel_Maintenance -Farm 'ICFarm-01' -MaintenanceMode RECURRING + Requests a CancelMaintenance task for instant-clone farm. Cancels recurring maintenance. + Start-HVFarm -CancelMaintenance -Farm 'ICFarm-01' -MaintenanceMode RECURRING .OUTPUTS None @@ -5913,58 +5913,58 @@ function Start-HVFarm { [Parameter(Mandatory = $false,ParameterSetName = 'RECOMPOSE')] [switch]$Recompose, - [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULE_MAINTENANCE')] - [switch]$Schedule_Maintenance, + [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULEMAINTENANCE')] + [switch]$ScheduleMaintenance, - [Parameter(Mandatory = $false,ParameterSetName = 'CANCEL_MAINTENANCE')] - [switch]$Cancel_Maintenance, + [Parameter(Mandatory = $false,ParameterSetName = 'CANCELMAINTENANCE')] + [switch]$CancelMaintenance, [Parameter(Mandatory = $false,ParameterSetName = 'RECOMPOSE')] - [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULE_MAINTENANCE')] + [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULEMAINTENANCE')] [System.DateTime]$StartTime, [Parameter(Mandatory = $true,ParameterSetName = 'RECOMPOSE')] - [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULE_MAINTENANCE')] + [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULEMAINTENANCE')] [ValidateSet('FORCE_LOGOFF','WAIT_FOR_LOGOFF')] [string]$LogoffSetting = 'FORCE_LOGOFF', [Parameter(Mandatory = $false,ParameterSetName = 'RECOMPOSE')] - [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULE_MAINTENANCE')] + [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULEMAINTENANCE')] [boolean]$StopOnFirstError = $true, [Parameter(Mandatory = $false,ParameterSetName = 'RECOMPOSE')] [string []]$Servers, [Parameter(Mandatory = $true,ParameterSetName = 'RECOMPOSE')] - [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULE_MAINTENANCE')] + [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULEMAINTENANCE')] [string]$ParentVM, [Parameter(Mandatory = $true,ParameterSetName = 'RECOMPOSE')] - [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULE_MAINTENANCE')] + [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULEMAINTENANCE')] [string]$SnapshotVM, [Parameter(Mandatory = $false,ParameterSetName = 'RECOMPOSE')] - [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULE_MAINTENANCE')] + [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULEMAINTENANCE')] [string]$Vcenter, - [Parameter(Mandatory = $true,ParameterSetName = 'SCHEDULE_MAINTENANCE')] - [Parameter(Mandatory = $true,ParameterSetName = 'CANCEL_MAINTENANCE')] + [Parameter(Mandatory = $true,ParameterSetName = 'SCHEDULEMAINTENANCE')] + [Parameter(Mandatory = $true,ParameterSetName = 'CANCELMAINTENANCE')] [ValidateSet('IMMEDIATE','RECURRING')] [string]$MaintenanceMode, - [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULE_MAINTENANCE')] + [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULEMAINTENANCE')] [ValidatePattern('^([0-9]|0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$')] [string]$MaintenanceStartTime, - [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULE_MAINTENANCE')] + [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULEMAINTENANCE')] [ValidateSet('DAILY','WEEKLY','MONTHLY')] [string]$MaintenancePeriod, - [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULE_MAINTENANCE')] + [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULEMAINTENANCE')] [ValidateRange(1, 31)] [int]$StartInt, - [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULE_MAINTENANCE')] + [Parameter(Mandatory = $false,ParameterSetName = 'SCHEDULEMAINTENANCE')] [ValidateRange(1, 100)] [int]$EveryInt = 1, @@ -6068,9 +6068,9 @@ function Start-HVFarm { Write-Host "Performed recompose task on farm: " $farmList.$item } } - 'SCHEDULE_MAINTENANCE' { + 'SCHEDULEMAINTENANCE' { if ($farmSource.$item -ne 'INSTANT_CLONE_ENGINE') { - Write-Error "SCHEDULE_MAINTENANCE operation is not supported for farm with name [$farmList.$item]. It is only supported for instant-clone farms." + Write-Error "SCHEDULEMAINTENANCE operation is not supported for farm with name [$farmList.$item]. It is only supported for instant-clone farms." break } else { $spec = New-Object VMware.Hv.FarmMaintenanceSpec @@ -6108,21 +6108,21 @@ function Start-HVFarm { try { $spec.ImageMaintenanceSettings = Set-HVFarmSpec -vcId $vcId -spec $spec.ImageMaintenanceSettings } catch { - Write-Error "SCHEDULE_MAINTENANCE task failed with error: $_" + Write-Error "SCHEDULEMAINTENANCE task failed with error: $_" break } } # call scheduleMaintenance service on farm if ($pscmdlet.ShouldProcess($farmList.$item)) { $farm_service_helper.Farm_ScheduleMaintenance($services, $item, $spec) - Write-Host "Performed SCHEDULE_MAINTENANCE task on farm: " $farmList.$item + Write-Host "Performed SCHEDULEMAINTENANCE task on farm: " $farmList.$item } } } - 'CANCEL_MAINTENANCE' { + 'CANCELMAINTENANCE' { if ($pscmdlet.ShouldProcess($farmList.$item)) { $farm_service_helper.Farm_CancelScheduleMaintenance($services, $item, $MaintenanceMode) - Write-Host "Performed CANCEL_MAINTENANCE task on farm: " $farmList.$item + Write-Host "Performed CANCELMAINTENANCE task on farm: " $farmList.$item } } } From aae3d54c68c72bf9393710ba8905b435dada600b Mon Sep 17 00:00:00 2001 From: praveenmathamsetty Date: Tue, 24 Jan 2017 17:39:20 +0530 Subject: [PATCH 011/112] Remove overhead of passing -Confirm:$false as extra parameter Now default behavior will be no pop-up will be shown for confirmation --- .../VMware.Hv.Helper/VMware.HV.Helper.psm1 | 72 ++++++++++++------- 1 file changed, 46 insertions(+), 26 deletions(-) diff --git a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 index b7f272a..6c94206 100644 --- a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 +++ b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 @@ -54,6 +54,17 @@ function Get-ViewAPIService { return $null } +function Get-HVConfirmFlag { + Param( + [Parameter(Mandatory = $true)] + $keys + ) + if (($keys -contains 'Confirm') -or ($keys -contains 'WhatIf')) { + return $true + } + return $false +} + function Get-VcenterID { param( [Parameter(Mandatory = $true)] @@ -237,6 +248,7 @@ The Add-HVDesktop adds virtual machines to already exiting pools by using view A } process { + $confirmFlag = Get-HVConfirmFlag -keys $PsBoundParameters.Keys try { $desktopPool = Get-HVPoolSummary -poolName $poolName -hvServer $hvServer } catch { @@ -294,7 +306,7 @@ The Add-HVDesktop adds virtual machines to already exiting pools by using view A return } } - if ($pscmdlet.ShouldProcess($machines)) { + if (!$confirmFlag -OR $pscmdlet.ShouldProcess($machines)) { $desktop_service_helper.Desktop_AddMachinesToManualDesktop($services,$id,$machineList) } return $machineList @@ -409,6 +421,7 @@ function Add-HVRDSServer { } process { + $confirmFlag = Get-HVConfirmFlag -keys $PsBoundParameters.Keys try { $farmSpecObj = Get-HVFarmSummary -farmName $farmName -hvServer $hvServer } catch { @@ -432,7 +445,7 @@ function Add-HVRDSServer { 'MANUAL' { try { $serverList = Get-RegisteredRDSServer -services $services -serverList $rdsServers - if ($pscmdlet.ShouldProcess($rdsServers)) { + if (!$confirmFlag -OR $pscmdlet.ShouldProcess($rdsServers)) { $farm_service_helper.Farm_AddRDSServers($services, $id, $serverList) } return $serverList @@ -2321,7 +2334,7 @@ function New-HVFarm { } process { - + $confirmFlag = Get-HVConfirmFlag -keys $PsBoundParameters.Keys if ($farmName) { try { $sourceFarm = Get-HVFarmSummary -farmName $farmName -hvServer $hvServer @@ -2630,7 +2643,7 @@ function New-HVFarm { $myDebug | out-file -filepath c:\temp\copiedfarm.json #> - if ($pscmdlet.ShouldProcess($farmSpecObj.data.name)) { + if (!$confirmFlag -OR $pscmdlet.ShouldProcess($farmSpecObj.data.name)) { $Id = $farm_service_helper.Farm_Create($services, $farmSpecObj) } else { try { @@ -4016,7 +4029,7 @@ function New-HVPool { } process { - + $confirmFlag = Get-HVConfirmFlag -keys $PsBoundParameters.Keys if ($poolName) { try { $sourcePool = Get-HVPoolSummary -poolName $poolName -hvServer $hvServer @@ -4627,7 +4640,7 @@ function New-HVPool { $myDebug | out-file -filepath c:\temp\copieddesktop.json #> $desktop_helper = New-Object VMware.Hv.DesktopService - if ($pscmdlet.ShouldProcess($desktopSpecObj.base.name)) { + if (!$confirmFlag -OR $pscmdlet.ShouldProcess($desktopSpecObj.base.name)) { $id = $desktop_helper.Desktop_create($services,$desktopSpecObj) } else { try { @@ -5201,6 +5214,7 @@ function Remove-HVFarm { } } process { + $confirmFlag = Get-HVConfirmFlag -keys $PsBoundParameters.Keys $farmList = @() if ($farmName) { try { @@ -5231,7 +5245,7 @@ function Remove-HVFarm { } $farm_service_helper = New-Object VMware.Hv.FarmService foreach ($item in $farmList) { - if ($pscmdlet.ShouldProcess($item.Name)) { + if (!$confirmFlag -OR $pscmdlet.ShouldProcess($item.Name)) { $farm_service_helper.Farm_Delete($services, $item.id) } Write-Host "Farm Deleted: " $item.Name @@ -5323,6 +5337,7 @@ function Remove-HVPool { } process { + $confirmFlag = Get-HVConfirmFlag -keys $PsBoundParameters.Keys $poolList = @() if ($poolName) { try { @@ -5375,7 +5390,7 @@ function Remove-HVPool { } } Write-Host "Deleting Pool: " $item.Name - if ($pscmdlet.ShouldProcess($item.Name)) { + if (!$confirmFlag -OR $pscmdlet.ShouldProcess($item.Name)) { $desktop_service_helper.Desktop_Delete($services,$item.id,$deleteSpec) } } @@ -5503,6 +5518,7 @@ function Set-HVFarm { } process { + $confirmFlag = Get-HVConfirmFlag -keys $PsBoundParameters.Keys $farmList = @{} if ($farmName) { try { @@ -5575,7 +5591,7 @@ function Set-HVFarm { } $farm_service_helper = New-Object VMware.Hv.FarmService foreach ($item in $farmList.Keys) { - if ($pscmdlet.ShouldProcess($farmList.$item)) { + if (!$confirmFlag -OR $pscmdlet.ShouldProcess($farmList.$item)) { $farm_service_helper.Farm_Update($services,$item,$updates) } Write-Host "Update successful for farm: " $farmList.$item @@ -5711,6 +5727,7 @@ function Set-HVPool { } process { + $confirmFlag = Get-HVConfirmFlag -keys $PsBoundParameters.Keys $poolList = @{} if ($poolName) { try { @@ -5784,7 +5801,7 @@ function Set-HVPool { } $desktop_helper = New-Object VMware.Hv.DesktopService foreach ($item in $poolList.Keys) { - if ($pscmdlet.ShouldProcess($poolList.$item)) { + if (!$confirmFlag -OR $pscmdlet.ShouldProcess($poolList.$item)) { $desktop_helper.Desktop_Update($services,$item,$updates) } } @@ -5981,6 +5998,7 @@ function Start-HVFarm { } process { + $confirmFlag = Get-HVConfirmFlag -keys $PsBoundParameters.Keys $farmList = @{} $farmType = @{} $farmSource = @{} @@ -6061,7 +6079,7 @@ function Start-HVFarm { $updates = @() $updates += Get-MapEntry -key 'automatedFarmData.virtualCenterProvisioningSettings.virtualCenterProvisioningData.parentVm' -value $spec.ParentVM $updates += Get-MapEntry -key 'automatedFarmData.virtualCenterProvisioningSettings.virtualCenterProvisioningData.snapshot' -value $spec.Snapshot - if ($pscmdlet.ShouldProcess($farmList.$item)) { + if (!$confirmFlag -OR $pscmdlet.ShouldProcess($farmList.$item)) { $farm_service_helper.Farm_Update($services,$item,$updates) $farm_service_helper.Farm_Recompose($services,$item,$spec) } @@ -6113,14 +6131,14 @@ function Start-HVFarm { } } # call scheduleMaintenance service on farm - if ($pscmdlet.ShouldProcess($farmList.$item)) { + if (!$confirmFlag -OR $pscmdlet.ShouldProcess($farmList.$item)) { $farm_service_helper.Farm_ScheduleMaintenance($services, $item, $spec) Write-Host "Performed SCHEDULEMAINTENANCE task on farm: " $farmList.$item } } } 'CANCELMAINTENANCE' { - if ($pscmdlet.ShouldProcess($farmList.$item)) { + if (!$confirmFlag -OR $pscmdlet.ShouldProcess($farmList.$item)) { $farm_service_helper.Farm_CancelScheduleMaintenance($services, $item, $MaintenanceMode) Write-Host "Performed CANCELMAINTENANCE task on farm: " $farmList.$item } @@ -6353,7 +6371,7 @@ function Start-HVPool { } process { - + $confirmFlag = Get-HVConfirmFlag -keys $PsBoundParameters.Keys $poolList = @{} $poolType = @{} $poolSource = @{} @@ -6406,7 +6424,7 @@ function Start-HVPool { $spec = Get-HVTaskSpec -Source $poolSource.$item -poolName $poolList.$item -operation $operation -taskSpecName 'DesktopRebalanceSpec' -desktopId $item if ($null -ne $spec) { # make sure current task on VMs, must be None - if ($pscmdlet.ShouldProcess($poolList.$item)) { + if (!$confirmFlag -OR $pscmdlet.ShouldProcess($poolList.$item)) { $desktop_helper.Desktop_Rebalance($services,$item,$spec) } Write-Host "Performed rebalance task on Pool: " $PoolList.$item @@ -6416,7 +6434,7 @@ function Start-HVPool { $spec = Get-HVTaskSpec -Source $poolSource.$item -poolName $poolList.$item -operation $operation -taskSpecName 'DesktopRefreshSpec' -desktopId $item if ($null -ne $spec) { # make sure current task on VMs, must be None - if ($pscmdlet.ShouldProcess($poolList.$item)) { + if (!$confirmFlag -OR $pscmdlet.ShouldProcess($poolList.$item)) { $desktop_helper.Desktop_Refresh($services,$item,$spec) } Write-Host "Performed refresh task on Pool: " $PoolList.$item @@ -6435,7 +6453,7 @@ function Start-HVPool { $updates = @() $updates += Get-MapEntry -key 'automatedDesktopData.virtualCenterProvisioningSettings.virtualCenterProvisioningData.parentVm' -value $spec.ParentVM $updates += Get-MapEntry -key 'automatedDesktopData.virtualCenterProvisioningSettings.virtualCenterProvisioningData.snapshot' -value $spec.Snapshot - if ($pscmdlet.ShouldProcess($poolList.$item)) { + if (!$confirmFlag -OR $pscmdlet.ShouldProcess($poolList.$item)) { $desktop_helper.Desktop_Update($services,$item,$updates) } Write-Host "Performed recompose task on Pool: " $PoolList.$item @@ -6453,7 +6471,7 @@ function Start-HVPool { $spec.Settings.LogoffSetting = $logoffSetting $spec.Settings.StopOnFirstError = $stopOnFirstError if ($startTime) { $spec.Settings.startTime = $startTime } - if ($pscmdlet.ShouldProcess($poolList.$item)) { + if (!$confirmFlag -OR $pscmdlet.ShouldProcess($poolList.$item)) { $desktop_helper.Desktop_SchedulePushImage($services,$item,$spec) } Write-Host "Performed push_image task on Pool: " $PoolList.$item @@ -6464,7 +6482,7 @@ function Start-HVPool { Write-Error "$poolList.$item is not a INSTANT CLONE pool" break } else { - if ($pscmdlet.ShouldProcess($poolList.$item)) { + if (!$confirmFlag -OR $pscmdlet.ShouldProcess($poolList.$item)) { $desktop_helper.Desktop_CancelScheduledPushImage($services,$item) } Write-Host "Performed cancel_push_image task on Pool: " $PoolList.$item @@ -7360,6 +7378,7 @@ function New-HVEntitlement { } } process { + $confirmFlag = Get-HVConfirmFlag -keys $PsBoundParameters.Keys $userInfo = Get-UserInfo -UserName $User $UserOrGroupName = $userInfo.Name $Domain = $userInfo.Domain @@ -7490,7 +7509,7 @@ function New-HVEntitlement { $base.UserOrGroup = $results.id foreach ($ResourceObj in $ResourceObjs) { $base.Resource = $ResourceObj.id - if ($pscmdlet.ShouldProcess($User)) { + if (!$confirmFlag -OR $pscmdlet.ShouldProcess($User)) { $id = $services.UserEntitlement.UserEntitlement_Create($base) } } @@ -7802,6 +7821,7 @@ function Remove-HVEntitlement { } } process { + $confirmFlag = Get-HVConfirmFlag -keys $PsBoundParameters.Keys $AndFilter = @() $results = $null $userInfo = Get-UserInfo -UserName $User @@ -7827,7 +7847,7 @@ function Remove-HVEntitlement { if ($results) { foreach ($result in $Results) { $userEntitlements = $result.localData.desktopUserEntitlements - if ($pscmdlet.ShouldProcess($User)) { + if (!$confirmFlag -OR $pscmdlet.ShouldProcess($User)) { $services.UserEntitlement.UserEntitlement_DeleteUserEntitlements($userEntitlements) } Write-Host $userEntitlements.Length " desktopUserEntitlement(s) are removed for UserOrGroup " $user @@ -7847,7 +7867,7 @@ function Remove-HVEntitlement { if ($results) { foreach ($result in $Results) { $userEntitlements = $result.localData.applicationUserEntitlements - if ($pscmdlet.ShouldProcess($User)) { + if (!$confirmFlag -OR $pscmdlet.ShouldProcess($User)) { $services.UserEntitlement.UserEntitlement_DeleteUserEntitlements($userEntitlements) } Write-Host $userEntitlements.Length " applicationUserEntitlement(s) are removed for UserOrGroup " $user @@ -7877,12 +7897,12 @@ function Remove-HVEntitlement { foreach ($result in $Results) { if ($result.GetType().Name -eq 'EntitledUserOrGroupLocalSummaryView') { $userEntitlements = $result.localData.urlRedirectionUserEntitlements - if ($pscmdlet.ShouldProcess($User)) { + if (!$confirmFlag -OR $pscmdlet.ShouldProcess($User)) { $services.UserEntitlement.UserEntitlement_DeleteUserEntitlements($userEntitlements) } } else { $userEntitlements = $result.globalData.urlRedirectionUserEntitlements - if ($pscmdlet.ShouldProcess($User)) { + if (!$confirmFlag -OR $pscmdlet.ShouldProcess($User)) { $services.UserEntitlement.UserEntitlement_DeleteUserEntitlements($userEntitlements) } } @@ -7907,7 +7927,7 @@ function Remove-HVEntitlement { if ($results) { foreach ($result in $Results) { $userEntitlements = $result.globalData.globalUserApplicationEntitlements - if ($pscmdlet.ShouldProcess($User)) { + if (!$confirmFlag -OR $pscmdlet.ShouldProcess($User)) { $services.UserEntitlement.UserEntitlement_DeleteUserEntitlements($userEntitlements) } Write-Host $userEntitlements.Length " GlobalApplicationEntitlement(s) are removed for UserOrGroup " $user @@ -7931,7 +7951,7 @@ function Remove-HVEntitlement { if ($results) { foreach ($result in $Results) { $userEntitlements = $result.globalData.globalUserEntitlements - if ($pscmdlet.ShouldProcess($User)) { + if (!$confirmFlag -OR $pscmdlet.ShouldProcess($User)) { $services.UserEntitlement.UserEntitlement_DeleteUserEntitlements($userEntitlements) } Write-Host $userEntitlements.Length " GlobalEntitlement(s) are removed for UserOrGroup " $user From 3f8d35a92aecb3ccd51cd9480def8246c929ee4f Mon Sep 17 00:00:00 2001 From: praveenmathamsetty Date: Fri, 27 Jan 2017 10:54:16 +0530 Subject: [PATCH 012/112] NetBiosName Fix making netbiosname as optional. --- .../VMware.Hv.Helper/VMware.HV.Helper.psm1 | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 index 6c94206..5cf780e 100644 --- a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 +++ b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 @@ -2939,11 +2939,15 @@ function Get-HVFarmCustomizationSetting { if ($InstantClone) { $farmSpecObj.AutomatedFarmSpec.CustomizationSettings.CustomizationType = 'CLONE_PREP' $instantCloneEngineDomainAdministrator_helper = New-Object VMware.Hv.InstantCloneEngineDomainAdministratorService - $instantCloneEngineDomainAdministrator = ($instantCloneEngineDomainAdministrator_helper.InstantCloneEngineDomainAdministrator_List($services) | Where-Object { $_.namesData.dnsName -match $netBiosName }) + $insDomainAdministrators = $instantCloneEngineDomainAdministrator_helper.InstantCloneEngineDomainAdministrator_List($services) + $instantCloneEngineDomainAdministrator = ($insDomainAdministrators | Where-Object { $_.namesData.dnsName -match $netBiosName }) if (![string]::IsNullOrWhitespace($domainAdmin)) { $instantCloneEngineDomainAdministrator = ($instantCloneEngineDomainAdministrator | Where-Object { $_.base.userName -eq $domainAdmin }).id - } elseIf ($null -ne $instantCloneEngineDomainAdministrator) { + } + If ($null -ne $instantCloneEngineDomainAdministrator) { $instantCloneEngineDomainAdministrator = $instantCloneEngineDomainAdministrator[0].id + } elseif ($null -ne $insDomainAdministrators) { + $instantCloneEngineDomainAdministrator = $insDomainAdministrators[0].id } if ($null -eq $instantCloneEngineDomainAdministrator) { throw "No Instant Clone Engine Domain Administrator found with netBiosName: [$netBiosName]" @@ -4903,11 +4907,15 @@ function Get-HVPoolCustomizationSetting { if ($InstantClone) { $desktopSpecObj.AutomatedDesktopSpec.CustomizationSettings.CustomizationType = 'CLONE_PREP' $instantCloneEngineDomainAdministrator_helper = New-Object VMware.Hv.InstantCloneEngineDomainAdministratorService - $instantCloneEngineDomainAdministrator = ($instantCloneEngineDomainAdministrator_helper.InstantCloneEngineDomainAdministrator_List($services) | Where-Object { $_.namesData.dnsName -match $netBiosName }) + $insDomainAdministrators = $instantCloneEngineDomainAdministrator_helper.InstantCloneEngineDomainAdministrator_List($services) + $instantCloneEngineDomainAdministrator = ($insDomainAdministrators | Where-Object { $_.namesData.dnsName -match $netBiosName }) if (![string]::IsNullOrWhitespace($domainAdmin)) { $instantCloneEngineDomainAdministrator = ($instantCloneEngineDomainAdministrator | Where-Object { $_.base.userName -eq $domainAdmin }).id - } elseIf ($null -ne $instantCloneEngineDomainAdministrator) { + } + If ($null -ne $instantCloneEngineDomainAdministrator) { $instantCloneEngineDomainAdministrator = $instantCloneEngineDomainAdministrator[0].id + } elseif ($null -ne $insDomainAdministrators) { + $instantCloneEngineDomainAdministrator = $insDomainAdministrators[0].id } if ($null -eq $instantCloneEngineDomainAdministrator) { throw "No Instant Clone Engine Domain Administrator found with netBiosName: [$netBiosName]" @@ -6093,7 +6101,9 @@ function Start-HVFarm { } else { $spec = New-Object VMware.Hv.FarmMaintenanceSpec $spec.MaintenanceMode = $MaintenanceMode - $spec.ScheduledTime = $StartTime + if ($startTime) { + $spec.ScheduledTime = $StartTime + } if ($MaintenanceMode -eq "RECURRING") { $spec.RecurringMaintenanceSettings = New-Object VMware.Hv.FarmRecurringMaintenanceSettings $spec.RecurringMaintenanceSettings.MaintenancePeriod = $MaintenancePeriod From fd13b0b16d4e5ed7d24a56c7eeaceb67cf6cc99d Mon Sep 17 00:00:00 2001 From: praveenmathamsetty Date: Fri, 27 Jan 2017 14:32:21 +0530 Subject: [PATCH 013/112] Mixed mode changes Use few parameters from json file and few parameters from command line to create new pool or farm. --- .../VMware.Hv.Helper/VMware.HV.Helper.psm1 | 93 +++++++++++++++---- 1 file changed, 74 insertions(+), 19 deletions(-) diff --git a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 index 5cf780e..7a0741d 100644 --- a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 +++ b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 @@ -1961,6 +1961,10 @@ function New-HVFarm { Creates new manual farm by using rdsServers names New-HVFarm -Manual -FarmName "manualFarmTest" -FarmDisplayName "manualFarmTest" -Description "Manual PS Test" -RdsServers "vm-for-rds.eng.vmware.com","vm-for-rds-2.eng.vmware.com" -Confirm:$false +.EXAMPLE + Creates new instant clone farm by reading few parameters from json and few parameters from command line. + New-HVFarm -Spec C:\VMWare\Specs\AutomatedInstantCloneFarm.json -FarmName 'InsPool' -NamingPattern 'InsFarm-' + .OUTPUTS None @@ -1997,6 +2001,7 @@ function New-HVFarm { [Parameter(Mandatory = $true,ParameterSetName = 'MANUAL')] [Parameter(Mandatory = $true,ParameterSetName = "LINKED_CLONE")] [Parameter(Mandatory = $true,ParameterSetName = "INSTANT_CLONE")] + [Parameter(Mandatory = $false,ParameterSetName = 'JSON_FILE')] [string] $FarmName, @@ -2172,6 +2177,7 @@ function New-HVFarm { #farmSpec.automatedfarmSpec.rdsServerNamingSpec.patternNamingSettings.namingPattern if LINKED_CLONE, INSTANT_CLONE [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] [Parameter(Mandatory = $false,ParameterSetName = 'INSTANT_CLONE')] + [Parameter(Mandatory = $false,ParameterSetName = 'JSON_FILE')] [string] $NamingPattern = $farmName + '{n:fixed=4}', @@ -2393,7 +2399,9 @@ function New-HVFarm { } $namingMethod = $jsonObject.AutomatedFarmSpec.RdsServerNamingSpec.NamingMethod - $namingPattern = $jsonObject.AutomatedFarmSpec.RdsServerNamingSpec.patternNamingSettings.namingPattern + if (! $NamingPattern) { + $namingPattern = $jsonObject.AutomatedFarmSpec.RdsServerNamingSpec.patternNamingSettings.namingPattern + } $maximumCount = $jsonObject.AutomatedFarmSpec.RdsServerNamingSpec.patternNamingSettings.maxNumberOfRDSServers $enableProvisioning = $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.EnableProvisioning $stopProvisioningOnError = $jsonObject.AutomatedFarmSpec.VirtualCenterProvisioningSettings.StopProvisioningOnError @@ -2461,7 +2469,9 @@ function New-HVFarm { $farmDisplayName = $jsonObject.Data.DisplayName $description = $jsonObject.Data.Description $accessGroup = $jsonObject.Data.AccessGroup - $farmName = $jsonObject.Data.name + if (! $FarmName) { + $farmName = $jsonObject.Data.name + } if ($null -ne $jsonObject.Data.Enabled) { $enable = $jsonObject.Data.Enabled } @@ -2940,10 +2950,16 @@ function Get-HVFarmCustomizationSetting { $farmSpecObj.AutomatedFarmSpec.CustomizationSettings.CustomizationType = 'CLONE_PREP' $instantCloneEngineDomainAdministrator_helper = New-Object VMware.Hv.InstantCloneEngineDomainAdministratorService $insDomainAdministrators = $instantCloneEngineDomainAdministrator_helper.InstantCloneEngineDomainAdministrator_List($services) - $instantCloneEngineDomainAdministrator = ($insDomainAdministrators | Where-Object { $_.namesData.dnsName -match $netBiosName }) - if (![string]::IsNullOrWhitespace($domainAdmin)) { - $instantCloneEngineDomainAdministrator = ($instantCloneEngineDomainAdministrator | Where-Object { $_.base.userName -eq $domainAdmin }).id + $strFilterSet = @() + if (![string]::IsNullOrWhitespace($netBiosName)) { + $strFilterSet += '$_.namesData.dnsName -match $netBiosName' } + if (![string]::IsNullOrWhitespace($domainAdmin)) { + $strFilterSet += '$_.base.userName -eq $domainAdmin' + } + $whereClause = [string]::Join(' -and ', $strFilterSet) + $scriptBlock = [Scriptblock]::Create($whereClause) + $instantCloneEngineDomainAdministrator = $insDomainAdministrators | Where $scriptBlock If ($null -ne $instantCloneEngineDomainAdministrator) { $instantCloneEngineDomainAdministrator = $instantCloneEngineDomainAdministrator[0].id } elseif ($null -ne $insDomainAdministrators) { @@ -2961,11 +2977,21 @@ function Get-HVFarmCustomizationSetting { $customObject = $farmSpecObj.AutomatedFarmSpec.CustomizationSettings } elseif ($LinkedClone) { $ViewComposerDomainAdministrator_service_helper = New-Object VMware.Hv.ViewComposerDomainAdministratorService - $ViewComposerDomainAdministratorID = ($ViewComposerDomainAdministrator_service_helper.ViewComposerDomainAdministrator_List($services, $vcID) | Where-Object { $_.base.domain -match $netBiosName }) - if (! [string]::IsNullOrWhitespace($domainAdmin)) { - $ViewComposerDomainAdministratorID = ($ViewComposerDomainAdministratorID | Where-Object { $_.base.userName -ieq $domainAdmin }).id - } elseif ($null -ne $ViewComposerDomainAdministratorID) { + $lcDomainAdministrators = $ViewComposerDomainAdministrator_service_helper.ViewComposerDomainAdministrator_List($services, $vcID) + $strFilterSet = @() + if (![string]::IsNullOrWhitespace($netBiosName)) { + $strFilterSet += '$_.base.domain -match $netBiosName' + } + if (![string]::IsNullOrWhitespace($domainAdmin)) { + $strFilterSet += '$_.base.userName -ieq $domainAdmin' + } + $whereClause = [string]::Join(' -and ', $strFilterSet) + $scriptBlock = [Scriptblock]::Create($whereClause) + $ViewComposerDomainAdministratorID = $lcDomainAdministrators | Where $scriptBlock + if ($null -ne $ViewComposerDomainAdministratorID) { $ViewComposerDomainAdministratorID = $ViewComposerDomainAdministratorID[0].id + } elseif ($null -ne $lcDomainAdministrators) { + $ViewComposerDomainAdministratorID = $lcDomainAdministrators[0].id } if ($null -eq $ViewComposerDomainAdministratorID) { throw "No Composer Domain Administrator found with netBiosName: [$netBiosName]" @@ -3393,6 +3419,10 @@ function New-HVPool { Create new unmanaged manual pool from unmanaged VirtualMachines. New-HVPool -MANUAL -PoolName 'unmangedVMWare' -PoolDisplayName 'unMngPl' -Description 'unmanaged Manual Pool creation' -UserAssignment FLOATING -Source UNMANAGED -VM 'myphysicalmachine.vmware.com' +.EXAMPLE + Creates new instant clone pool by reading few parameters from json and few parameters from command line. + New-HVPool -spec 'C:\Json\InstantClone.json' -PoolName 'InsPool1'-NamingPattern 'INSPool-' + .OUTPUTS None @@ -3447,6 +3477,7 @@ function New-HVPool { [Parameter(Mandatory = $true,ParameterSetName = 'FULL_CLONE')] [Parameter(Mandatory = $true,ParameterSetName = 'RDS')] [Parameter(Mandatory = $true,ParameterSetName = 'CLONED_POOL')] + [Parameter(Mandatory = $true,ParameterSetName = 'JSON_FILE')] [string] $PoolName, @@ -3722,14 +3753,17 @@ function New-HVPool { [Parameter(Mandatory = $false,ParameterSetName = 'INSTANT_CLONE')] [boolean] $RedirectWindowsProfile = $true, + #desktopSpec.automatedDesktopSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.viewComposerStorageSettings.persistentDiskSettings.useSeparateDatastoresPersistentAndOSDisks if LINKED_CLONE [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] [boolean] $UseSeparateDatastoresPersistentAndOSDisks = $false, + #desktopSpec.automatedDesktopSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.viewComposerStorageSettings.persistentDiskSettings.PersistentDiskDatastores if LINKED_CLONE [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] [string[]] $PersistentDiskDatastores, + #desktopSpec.automatedDesktopSpec.virtualCenterProvisioningSettings.virtualCenterStorageSettings.viewComposerStorageSettings.persistentDiskSettings.PersistentDiskDatastores if LINKED_CLONE [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] [string[]] @@ -3827,6 +3861,7 @@ function New-HVPool { [Parameter(Mandatory = $false,ParameterSetName = 'INSTANT_CLONE')] [Parameter(Mandatory = $false,ParameterSetName = 'FULL_CLONE')] [Parameter(Mandatory = $false,ParameterSetName = 'CLONED_POOL')] + [Parameter(Mandatory = $true,ParameterSetName = 'JSON_FILE')] [string] $NamingPattern = $poolName + '{n:fixed=4}', @@ -4112,7 +4147,9 @@ function New-HVPool { $namingMethod = $jsonObject.AutomatedDesktopSpec.VmNamingSpec.NamingMethod $transparentPageSharingScope = $jsonObject.AutomatedDesktopSpec.virtualCenterManagedCommonSettings.TransparentPageSharingScope if ($namingMethod -eq "PATTERN") { - $namingPattern = $jsonObject.AutomatedDesktopSpec.VmNamingSpec.patternNamingSettings.namingPattern + if (!$namingPattern) { + $namingPattern = $jsonObject.AutomatedDesktopSpec.VmNamingSpec.patternNamingSettings.namingPattern + } $maximumCount = $jsonObject.AutomatedDesktopSpec.VmNamingSpec.patternNamingSettings.maxNumberOfMachines $spareCount = $jsonObject.AutomatedDesktopSpec.VmNamingSpec.patternNamingSettings.numberOfSpareMachines $provisioningTime = $jsonObject.AutomatedDesktopSpec.VmNamingSpec.patternNamingSettings.provisioningTime @@ -4232,7 +4269,9 @@ function New-HVPool { $poolDisplayName = $jsonObject.base.DisplayName $description = $jsonObject.base.Description $accessGroup = $jsonObject.base.AccessGroup - $poolName = $jsonObject.base.name + if (!$poolName) { + $poolName = $jsonObject.base.name + } <# # Populate desktop settings @@ -4908,10 +4947,16 @@ function Get-HVPoolCustomizationSetting { $desktopSpecObj.AutomatedDesktopSpec.CustomizationSettings.CustomizationType = 'CLONE_PREP' $instantCloneEngineDomainAdministrator_helper = New-Object VMware.Hv.InstantCloneEngineDomainAdministratorService $insDomainAdministrators = $instantCloneEngineDomainAdministrator_helper.InstantCloneEngineDomainAdministrator_List($services) - $instantCloneEngineDomainAdministrator = ($insDomainAdministrators | Where-Object { $_.namesData.dnsName -match $netBiosName }) - if (![string]::IsNullOrWhitespace($domainAdmin)) { - $instantCloneEngineDomainAdministrator = ($instantCloneEngineDomainAdministrator | Where-Object { $_.base.userName -eq $domainAdmin }).id + $strFilterSet = @() + if (![string]::IsNullOrWhitespace($netBiosName)) { + $strFilterSet += '$_.namesData.dnsName -match $netBiosName' } + if (![string]::IsNullOrWhitespace($domainAdmin)) { + $strFilterSet += '$_.base.userName -eq $domainAdmin' + } + $whereClause = [string]::Join(' -and ', $strFilterSet) + $scriptBlock = [Scriptblock]::Create($whereClause) + $instantCloneEngineDomainAdministrator = $insDomainAdministrators | Where $scriptBlock If ($null -ne $instantCloneEngineDomainAdministrator) { $instantCloneEngineDomainAdministrator = $instantCloneEngineDomainAdministrator[0].id } elseif ($null -ne $insDomainAdministrators) { @@ -4930,14 +4975,24 @@ function Get-HVPoolCustomizationSetting { else { if ($LinkedClone) { $viewComposerDomainAdministrator_helper = New-Object VMware.Hv.ViewComposerDomainAdministratorService - $ViewComposerDomainAdministratorID = ($viewComposerDomainAdministrator_helper.ViewComposerDomainAdministrator_List($services,$vcID) | Where-Object { $_.base.domain -match $netBiosName }) + $lcDomainAdministrators = $viewComposerDomainAdministrator_helper.ViewComposerDomainAdministrator_List($services,$vcID) + $strFilterSet = @() + if (![string]::IsNullOrWhitespace($netBiosName)) { + $strFilterSet += '$_.base.domain -match $netBiosName' + } if (![string]::IsNullOrWhitespace($domainAdmin)) { - $ViewComposerDomainAdministratorID = ($ViewComposerDomainAdministratorID | Where-Object { $_.base.userName -ieq $domainAdmin }).id - } elseIf ($null -ne $ViewComposerDomainAdministratorID) { - $ViewComposerDomainAdministratorID = $ViewComposerDomainAdministratorID[0].id + $strFilterSet += '$_.base.userName -ieq $domainAdmin' + } + $whereClause = [string]::Join(' -and ', $strFilterSet) + $scriptBlock = [Scriptblock]::Create($whereClause) + $ViewComposerDomainAdministratorID = $lcDomainAdministrators | Where $scriptBlock + If ($null -ne $ViewComposerDomainAdministratorID) { + $ViewComposerDomainAdministratorID = $ViewComposerDomainAdministratorID[0].id + } elseif ($null -ne $lcDomainAdministrators) { + $ViewComposerDomainAdministratorID = $lcDomainAdministrators[0].id } if ($null -eq $ViewComposerDomainAdministratorID) { - throw "No Composer Domain Administrator found with netBiosName: [$netBiosName]" + throw "No Composer Domain Administrator found with netBiosName: [$netBiosName]" } if ($custType -eq 'SYS_PREP') { $desktopSpecObj.AutomatedDesktopSpec.CustomizationSettings.CustomizationType = 'SYS_PREP' From 769401af818f6631e3cc48ae1eff42203f323d6e Mon Sep 17 00:00:00 2001 From: praveenmathamsetty Date: Wed, 1 Feb 2017 15:00:33 +0530 Subject: [PATCH 014/112] Fix to mixed mode bug making few attributes like NamingPattern as optional. --- Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 index 7a0741d..0f3bf52 100644 --- a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 +++ b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 @@ -2399,7 +2399,7 @@ function New-HVFarm { } $namingMethod = $jsonObject.AutomatedFarmSpec.RdsServerNamingSpec.NamingMethod - if (! $NamingPattern) { + if ($NamingPattern -eq '{n:fixed=4}') { $namingPattern = $jsonObject.AutomatedFarmSpec.RdsServerNamingSpec.patternNamingSettings.namingPattern } $maximumCount = $jsonObject.AutomatedFarmSpec.RdsServerNamingSpec.patternNamingSettings.maxNumberOfRDSServers @@ -2998,6 +2998,7 @@ function Get-HVFarmCustomizationSetting { } #Support only Sysprep Customization + $farmSpecObj.AutomatedFarmSpec.CustomizationSettings.SysprepCustomizationSettings = New-Object VMware.Hv.FarmSysprepCustomizationSettings $sysprepCustomizationSettings = $farmSpecObj.AutomatedFarmSpec.CustomizationSettings.SysprepCustomizationSettings # Get SysPrep CustomizationSpec ID @@ -3477,7 +3478,7 @@ function New-HVPool { [Parameter(Mandatory = $true,ParameterSetName = 'FULL_CLONE')] [Parameter(Mandatory = $true,ParameterSetName = 'RDS')] [Parameter(Mandatory = $true,ParameterSetName = 'CLONED_POOL')] - [Parameter(Mandatory = $true,ParameterSetName = 'JSON_FILE')] + [Parameter(Mandatory = $false,ParameterSetName = 'JSON_FILE')] [string] $PoolName, @@ -3861,7 +3862,7 @@ function New-HVPool { [Parameter(Mandatory = $false,ParameterSetName = 'INSTANT_CLONE')] [Parameter(Mandatory = $false,ParameterSetName = 'FULL_CLONE')] [Parameter(Mandatory = $false,ParameterSetName = 'CLONED_POOL')] - [Parameter(Mandatory = $true,ParameterSetName = 'JSON_FILE')] + [Parameter(Mandatory = $false,ParameterSetName = 'JSON_FILE')] [string] $NamingPattern = $poolName + '{n:fixed=4}', @@ -4147,7 +4148,7 @@ function New-HVPool { $namingMethod = $jsonObject.AutomatedDesktopSpec.VmNamingSpec.NamingMethod $transparentPageSharingScope = $jsonObject.AutomatedDesktopSpec.virtualCenterManagedCommonSettings.TransparentPageSharingScope if ($namingMethod -eq "PATTERN") { - if (!$namingPattern) { + if ($NamingPattern -eq '{n:fixed=4}') { $namingPattern = $jsonObject.AutomatedDesktopSpec.VmNamingSpec.patternNamingSettings.namingPattern } $maximumCount = $jsonObject.AutomatedDesktopSpec.VmNamingSpec.patternNamingSettings.maxNumberOfMachines From 04985c7301a43e1e3daae9fbdbcee177d4281638 Mon Sep 17 00:00:00 2001 From: praveenmathamsetty Date: Wed, 1 Feb 2017 17:38:36 +0530 Subject: [PATCH 015/112] Changing module version from 1.0 --> 1.1 Update version 1.0 to 1.1 in VMware.HV.Helper --- Modules/VMware.Hv.Helper/VMware.HV.Helper.psd1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psd1 b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psd1 index 705cc80..42dc6aa 100644 --- a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psd1 +++ b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psd1 @@ -12,7 +12,7 @@ # RootModule = '' # Version number of this module. -ModuleVersion = '1.0' +ModuleVersion = '1.1' # ID used to uniquely identify this module GUID = '6d3f7fb5-4e52-43d8-91e1-f65f72532a1d' From 4b711cca91b2609c60c71912e4fcd73c730cd690 Mon Sep 17 00:00:00 2001 From: praveenmathamsetty Date: Fri, 3 Feb 2017 15:50:03 +0530 Subject: [PATCH 016/112] Enabling cloning for Instant clone pool If current image state is not ready, clonging of instant clone pools is not supported. --- Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 index 0f3bf52..d10215e 100644 --- a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 +++ b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 @@ -4362,6 +4362,8 @@ function New-HVPool { $DesktopVirtualCenterNetworkingSettings = $DesktopVirtualCenterProvisioningSettings.VirtualCenterNetworkingSettings $DesktopVirtualCenterManagedCommonSettings = $clonePool.AutomatedDesktopData.virtualCenterManagedCommonSettings $DesktopCustomizationSettings = $clonePool.AutomatedDesktopData.CustomizationSettings + $CurrentImageState =` + $clonePool.AutomatedDesktopData.provisioningStatusData.instantCloneProvisioningStatusData.instantCloneCurrentImageState } elseIf ($clonePool.ManualDesktopData) { if (! $VM) { @@ -4380,8 +4382,8 @@ function New-HVPool { break } } - if ($provisioningType -eq 'INSTANT_CLONE_ENGINE' -and $poolType -eq 'AUTOMATED') { - Write-Error "Cloning is not supported for instant clone pools" + if ($provisioningType -eq 'INSTANT_CLONE_ENGINE' -and $poolType -eq 'AUTOMATED' -and $CurrentImageState -ne 'READY') { + Write-Error "Instant clone pool's Current Image State should be in 'READY' state, otherwise cloning is not supported" break } } else { From 7a919a49b9a1426b621e1720be1eda678f20c0e1 Mon Sep 17 00:00:00 2001 From: praveenmathamsetty Date: Fri, 3 Feb 2017 17:39:47 +0530 Subject: [PATCH 017/112] Adding new Advanced function Set-HVMachine Using this AF, we can allow the machine to enter the maintanence mode and as well as exit the maintanence mode --- .../VMware.Hv.Helper/VMware.HV.Helper.psm1 | 150 +++++++++++++++++- 1 file changed, 148 insertions(+), 2 deletions(-) diff --git a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 index d10215e..eeee4aa 100644 --- a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 +++ b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 @@ -5867,11 +5867,11 @@ function Set-HVPool { } $desktop_helper = New-Object VMware.Hv.DesktopService foreach ($item in $poolList.Keys) { + Write-Host "Updating the Pool: " $poolList.$item if (!$confirmFlag -OR $pscmdlet.ShouldProcess($poolList.$item)) { $desktop_helper.Desktop_Update($services,$item,$updates) } } - Write-Host "Update successful for Pool: " $poolList.$item } end { @@ -8039,4 +8039,150 @@ function Remove-HVEntitlement { } } -Export-ModuleMember Add-HVDesktop,Add-HVRDSServer,Connect-HVEvent,Disconnect-HVEvent,Get-HVPoolSpec,Get-HVInternalName, Get-HVEvent,Get-HVFarm,Get-HVFarmSummary,Get-HVPool,Get-HVPoolSummary,Get-HVMachine,Get-HVMachineSummary,Get-HVQueryResult,Get-HVQueryFilter,New-HVFarm,New-HVPool,Remove-HVFarm,Remove-HVPool,Set-HVFarm,Set-HVPool,Start-HVFarm,Start-HVPool,New-HVEntitlement,Get-HVEntitlement,Remove-HVEntitlement +function Set-HVMachine { +<# +.Synopsis + Sets existing virtual Machine(s). + +.DESCRIPTION + This cmdlet allows user to edit Machine configuration by passing key/value pair. + Allows the machine in to Maintenance mode and vice versa + +.PARAMETER MachineName + The name of the Machine to edit. + +.PARAMETER Machine + Object(s) of the virtual Machine(s) to edit. + +.PARAMETER Maintenance + The virtual machine is in maintenance mode. Users cannot log in or use the virtual machine + +PARAMETER Key + Property names path separated by . (dot) from the root of machine info spec. + +.PARAMETER Value + Property value corresponds to above key name. + +.PARAMETER HvServer + Reference to Horizon View Server to query the virtual machines from. If the value is not passed or null then + first element from global:DefaultHVServers would be considered inplace of hvServer + +.EXAMPLE + Moving the machine in to Maintenance mode using machine name + Set-HVMachine -MachineName 'Agent_Praveen' -Maintenance ENTER_MAINTENANCE_MODE + +.EXAMPLE + Moving the machine in to Maintenance mode using machine object(s) + Get-HVMachine -MachineName 'Agent_Praveen' | Set-HVMachine -Maintenance ENTER_MAINTENANCE_MODE + +.EXAMPLE + Moving the machine in to Maintenance mode using machine object(s) + $machine = Get-HVMachine -MachineName 'Agent_Praveen'; Set-HVMachine -Machine $machine -Maintenance EXIT_MAINTENANCE_MODE + +.OUTPUTS + None + +.NOTES + Author : Praveen Mathamsetty. + Author email : pmathamsetty@vmware.com + Version : 1.1 + + ===Tested Against Environment==== + Horizon View Server Version : 7.0.2, 7.0.3 + PowerCLI Version : PowerCLI 6.5 + PowerShell Version : 5.0 +#> + + [CmdletBinding( + SupportsShouldProcess = $true, + ConfirmImpact = 'High' + )] + + param( + + [Parameter(Mandatory = $true ,ParameterSetName = 'option')] + [string] + $MachineName, + + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'pipeline')] + $Machine, + + [Parameter(Mandatory = $false)] + [ValidateSet('ENTER_MAINTENANCE_MODE', 'EXIT_MAINTENANCE_MODE')] + [string] + $Maintenance, + + [Parameter(Mandatory = $false)] + [string]$Key, + + [Parameter(Mandatory = $false)] + $Value, + + [Parameter(Mandatory = $false)] + $HvServer = $null + ) + + begin { + $services = Get-ViewAPIService -hvServer $hvServer + if ($null -eq $services) { + Write-Error "Could not retrieve ViewApi services from connection object" + break + } + } + + process { + $confirmFlag = Get-HVConfirmFlag -keys $PsBoundParameters.Keys + $machineList = @{} + if ($machineName) { + try { + $machines = Get-HVMachineSummary -MachineName $machineName -hvServer $hvServer + } catch { + Write-Error "Make sure Get-HVMachineSummary advanced function is loaded, $_" + break + } + if ($machines) { + foreach ($macineObj in $machines) { + $machineList.add($macineObj.id, $macineObj.base.Name) + } + } + } elseif ($PSCmdlet.MyInvocation.ExpectingInput -or $Machine) { + foreach ($item in $machine) { + if (($item.GetType().name -eq 'MachineNamesView') -or ($item.GetType().name -eq 'MachineInfo')) { + $machineList.add($item.id, $item.Base.Name) + } else { + Write-Error "In pipeline did not get object of expected type MachineNamesView/MachineInfo" + [System.gc]::collect() + return + } + } + } + $updates = @() + if ($key -and $value) { + $updates += Get-MapEntry -key $key -value $value + } elseif ($key -or $value) { + Write-Error "Both key:[$key] and value:[$value] needs to be specified" + } + + if ($Maintenance) { + if ($Maintenance -eq 'ENTER_MAINTENANCE_MODE') { + $updates += Get-MapEntry -key 'managedMachineData.inMaintenanceMode' -value $true + } else { + $updates += Get-MapEntry -key 'managedMachineData.inMaintenanceMode' -value $false + } + } + $machine_helper = New-Object VMware.Hv.MachineService + foreach ($item in $machineList.Keys) { + Write-Host "Updating the Machine: " $machineList.$item + if (!$confirmFlag -OR $pscmdlet.ShouldProcess($machineList.$item)) { + $machine_helper.Machine_Update($services,$item,$updates) + } + } + } + + end { + [System.gc]::collect() + } + +} + +Export-ModuleMember Add-HVDesktop,Add-HVRDSServer,Connect-HVEvent,Disconnect-HVEvent,Get-HVPoolSpec,Get-HVInternalName, Get-HVEvent,Get-HVFarm,Get-HVFarmSummary,Get-HVPool,Get-HVPoolSummary,Get-HVMachine,Get-HVMachineSummary,Get-HVQueryResult,Get-HVQueryFilter,New-HVFarm,New-HVPool,Remove-HVFarm,Remove-HVPool,Set-HVFarm,Set-HVPool,Start-HVFarm,Start-HVPool,New-HVEntitlement,Get-HVEntitlement,Remove-HVEntitlement, Set-HVMachine From d55a16f19bd327e134149511a3352738116e5266 Mon Sep 17 00:00:00 2001 From: praveenmathamsetty Date: Tue, 7 Feb 2017 16:40:29 +0530 Subject: [PATCH 018/112] Adding new AFs New-HVGlobalEntitlement, Remove-HVGlobalEntitlement, Get-HVGlobalEntitlement New-HVGlobalEntitlement: Creates a Global Entitlement. , Remove-HVGlobalEntitlement: Deletes a Global Entitlement., Get-HVGlobalEntitlement: Gets Global Entitlement(s). --- .../VMware.Hv.Helper/VMware.HV.Helper.psm1 | 455 +++++++++++++++++- 1 file changed, 452 insertions(+), 3 deletions(-) diff --git a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 index eeee4aa..8c8db50 100644 --- a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 +++ b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 @@ -5377,7 +5377,7 @@ function Remove-HVPool { ConfirmImpact = 'High' )] param( - [Parameter(Mandatory = $false,ParameterSetName = 'option')] + [Parameter(Mandatory = $true,ParameterSetName = 'option')] [string] $poolName, # PoolObject @@ -5420,7 +5420,7 @@ function Remove-HVPool { Write-Error "No desktopsummarydata found with pool name: [$pool]" break } - } elseif ($PSCmdlet.MyInvocation.ExpectingInput) { + } elseif ($PSCmdlet.MyInvocation.ExpectingInput -or $Pool) { foreach ($item in $pool) { if ($item.GetType().name -eq 'DesktopSummaryView') { $poolList += @{id = $item.id; name = $item.desktopSummaryData.name} @@ -8185,4 +8185,453 @@ PARAMETER Key } -Export-ModuleMember Add-HVDesktop,Add-HVRDSServer,Connect-HVEvent,Disconnect-HVEvent,Get-HVPoolSpec,Get-HVInternalName, Get-HVEvent,Get-HVFarm,Get-HVFarmSummary,Get-HVPool,Get-HVPoolSummary,Get-HVMachine,Get-HVMachineSummary,Get-HVQueryResult,Get-HVQueryFilter,New-HVFarm,New-HVPool,Remove-HVFarm,Remove-HVPool,Set-HVFarm,Set-HVPool,Start-HVFarm,Start-HVPool,New-HVEntitlement,Get-HVEntitlement,Remove-HVEntitlement, Set-HVMachine +function New-HVGlobalEntitlement { + + <# +.Synopsis + Creates a Global Entitlement. + +.DESCRIPTION + Global entitlements are used to route users to their resources across multiple pods. + These are persisted in a global ldap instance that is replicated across all pods in a linked mode view set. + +.PARAMETER DisplayName + Display Name of Global Entitlement. + +.PARAMETER Type + Specify whether to create desktop/app global entitlement + +.PARAMETER Description + Description of Global Entitlement. + +.PARAMETER Scope + Scope for this global entitlement. Visibility and Placement policies are defined by this value. + +.PARAMETER Dedicated + Specifies whether dedicated/floating resources associated with this global entitlement. + +.PARAMETER FromHome + This value defines the starting location for resource placement and search. + When true, a pod in the user's home site is used to start the search. When false, the current site is used. + +.PARAMETER RequireHomeSite + This value determines whether we fail if a home site isn't defined for this global entitlement. + +.PARAMETER MultipleSessionAutoClean + This value is used to determine if automatic session clean up is enabled. + This cannot be enabled when this Global Entitlement is associated with a Desktop that has dedicated user assignment. + +.PARAMETER Enabled + If this Global Entitlement is enabled. + +.PARAMETER SupportedDisplayProtocols + The set of supported display protocols for the global entitlement. + +.PARAMETER DefaultDisplayProtocol + The default display protocol for the global entitlement. + +.PARAMETER AllowUsersToChooseProtocol + Whether the users can choose the protocol used. + +.PARAMETER AllowUsersToResetMachines + Whether users are allowed to reset/restart their machines. + +.PARAMETER EnableHTMLAccess + If set to true, the desktops that are associated with this GlobalEntitlement must also have HTML Access enabled. + +.PARAMETER HvServer + Reference to Horizon View Server. If the value is not passed or null then + first element from global:DefaultHVServers would be considered inplace of hvServer + +.EXAMPLE + Creates new global application entitlement + New-HVGlobalEntitlement -DisplayName 'GE_APP' -Type APPLICATION_ENTITLEMENT + +.EXAMPLE + Creates new global desktop entitlement + New-HVGlobalEntitlement -DisplayName 'GE_DESKTOP' -Type DESKTOP_ENTITLEMENT + + +.NOTES + Author : Praveen Mathamsetty. + Author email : pmathamsetty@vmware.com + Version : 1.1 + + ===Tested Against Environment==== + Horizon View Server Version : 7.0.2, 7.0.3 + PowerCLI Version : PowerCLI 6.5 + PowerShell Version : 5.0 +#> + +[CmdletBinding( + SupportsShouldProcess = $true, + ConfirmImpact = 'High' + )] + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $DisplayName, + + [Parameter(Mandatory = $true)] + [ValidateSet('DESKTOP_ENTITLEMENT','APPLICATION_ENTITLEMENT')] + [String] + $Type, + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [String] + $Description, + + [Parameter(Mandatory = $false)] + [ValidateSet('LOCAL','SITE','ANY')] + [String] + $Scope = "ANY", + + [Parameter(Mandatory = $false)] + [Boolean] + $Dedicated, + + [Parameter(Mandatory = $false)] + [Boolean] + $FromHome, + + [Parameter(Mandatory = $false)] + [Boolean] + $RequireHomeSite, + + [Parameter(Mandatory = $false)] + [Boolean] + $MultipleSessionAutoClean, + + [Parameter(Mandatory = $false)] + [Boolean] + $Enabled, + + [Parameter(Mandatory = $false)] + [ValidateSet('RDP', 'PCOIP', 'BLAST')] + [String[]] + $SupportedDisplayProtocols = @("PCOIP","BLAST"), + + [Parameter(Mandatory = $false)] + [ValidateSet("PCOIP",'RDP',"BLAST")] + [String] + $DefaultDisplayProtocol = 'PCOIP', + + [Parameter(Mandatory = $false)] + [Boolean] + $AllowUsersToChooseProtocol = $true, + + [Parameter(Mandatory = $false)] + [Boolean] + $AllowUsersToResetMachines = $false, + + [Parameter(Mandatory = $false)] + [Boolean] + $EnableHTMLAccess = $false, + + [Parameter(Mandatory = $false)] + $HvServer = $null + ) + begin { + $services = Get-ViewAPIService -hvServer $hvServer + if ($null -eq $services) { + Write-Error "Could not retrieve ViewApi services from connection object" + break + } + } + process { + $info = $services.PodFederation.PodFederation_get() + if ("ENABLED" -ne $info.localPodStatus.status) { + Write-Host "Multi-DataCenter-View/CPA is not enabled" + return + } + $confirmFlag = Get-HVConfirmFlag -keys $PsBoundParameters.Keys + if ($Type -eq 'DESKTOP_ENTITLEMENT') { + $GeService = New-Object VMware.HV.GlobalEntitlementService + $geBaseHelper = $GeService.getGlobalEntitlementBaseHelper() + $geBase = $geBaseHelper.getDataObject() + $geBase.Dedicated = $dedicated + $geBase.AllowUsersToResetMachines = $AllowUsersToResetMachines + } else { + $GeService = New-Object VMware.Hv.GlobalApplicationEntitlementService + $geBaseHelper = $GeService.getGlobalApplicationEntitlementBaseHelper() + $geBase = $geBaseHelper.getDataObject() + } + $geBase.DisplayName = $displayName + if ($description) { + $geBaseHelper.setDescription($Description) + } + $geBase.Scope = $Scope + $geBase.FromHome = $fromHome + $geBase.RequireHomeSite = $requireHomeSite + $geBase.MultipleSessionAutoClean = $multipleSessionAutoClean + $geBase.Enabled = $enabled + $geBase.DefaultDisplayProtocol = $defaultDisplayProtocol + $geBase.AllowUsersToChooseProtocol = $AllowUsersToChooseProtocol + $geBase.EnableHTMLAccess = $enableHTMLAccess + $geBase.SupportedDisplayProtocols = $supportedDisplayProtocols + Write-Host "Creating new global entitlement with DisplayName: " $DisplayName + if (!$confirmFlag -OR $pscmdlet.ShouldProcess($displayName)) { + if ($type -eq 'DESKTOP_ENTITLEMENT') { + $GeService.GlobalEntitlement_Create($services, $geBase) + } else { + $GeService.GlobalApplicationEntitlement_Create($services, $geBase) + } + } + } + end { + [System.gc]::collect() + } + +} + + +function Find-HVGlobalEntitlement { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + $Param, + [Parameter(Mandatory = $true)] + [String] + $Type + ) + + # This translates the function arguments into the View API properties that must be queried + $GeSelectors = @{ + 'displayName' = 'base.displayName'; + 'description' = 'base.description'; + } + + $params = $Param + + $query_service_helper = New-Object VMware.Hv.QueryServiceService + $query = New-Object VMware.Hv.QueryDefinition + + $wildCard = $false + #Only supports wild card '*' + if ($params['displayName'] -and $params['displayName'].contains('*')) { + $wildcard = $true + } + + # build the query values + $query.queryEntityType = $Type + if (! $wildcard) { + [VMware.Hv.queryfilter[]]$filterSet = @() + foreach ($setting in $GeSelectors.Keys) { + if ($null -ne $params[$setting]) { + $equalsFilter = New-Object VMware.Hv.QueryFilterEquals + $equalsFilter.memberName = $GeSelectors[$setting] + $equalsFilter.value = $params[$setting] + $filterSet += $equalsFilter + } + } + if ($filterSet.Count -gt 0) { + $andFilter = New-Object VMware.Hv.QueryFilterAnd + $andFilter.Filters = $filterset + $query.Filter = $andFilter + } + $queryResults = $query_service_helper.QueryService_Query($services,$query) + $GeList = $queryResults.results + } + if ($wildcard -or [string]::IsNullOrEmpty($GeList)) { + $query.Filter = $null + $queryResults = $query_service_helper.QueryService_Query($services,$query) + $strFilterSet = @() + foreach ($setting in $GeSelectors.Keys) { + if ($null -ne $params[$setting]) { + if ($wildcard -and ($setting -eq 'displayName') ) { + $strFilterSet += '($_.' + $GeSelectors[$setting] + ' -like "' + $params[$setting] + '")' + } else { + $strFilterSet += '($_.' + $GeSelectors[$setting] + ' -eq "' + $params[$setting] + '")' + } + } + } + $whereClause = [string]::Join(' -and ', $strFilterSet) + $scriptBlock = [Scriptblock]::Create($whereClause) + $GeList = $queryResults.results | where $scriptBlock + } + Return $GeList +} + +function Get-HVGlobalEntitlement { + + <# +.Synopsis + +.DESCRIPTION + Global entitlements are used to route users to their resources across multiple pods. + +.PARAMETER DisplayName + Display Name of Global Entitlement. + +.PARAMETER Description + Description of Global Entitlement. + +.PARAMETER HvServer + Reference to Horizon View Server. If the value is not passed or null then + first element from global:DefaultHVServers would be considered inplace of hvServer + +.EXAMPLE + Retrieves global application/desktop entitlement(s) with displayName 'GEAPP' + Get-HVGlobalEntitlement -DisplayName 'GEAPP' + + +.NOTES + Author : Praveen Mathamsetty. + Author email : pmathamsetty@vmware.com + Version : 1.1 + + ===Tested Against Environment==== + Horizon View Server Version : 7.0.2, 7.0.3 + PowerCLI Version : PowerCLI 6.5 + PowerShell Version : 5.0 +#> + +[CmdletBinding( + SupportsShouldProcess = $true, + ConfirmImpact = 'High' + )] + param( + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [String] + $DisplayName, + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [String] + $Description, + + [Parameter(Mandatory = $false)] + $HvServer = $null + ) + begin { + $services = Get-ViewAPIService -hvServer $hvServer + if ($null -eq $services) { + Write-Error "Could not retrieve ViewApi services from connection object" + break + } + } + process { + $info = $services.PodFederation.PodFederation_get() + if ("ENABLED" -ne $info.localPodStatus.status) { + Write-Host "Multi-DataCenter-View/CPA is not enabled" + return + } + $result = @() + $result += Find-HVGlobalEntitlement -Param $psboundparameters -Type 'GlobalEntitlementSummaryView' + $result += Find-HVGlobalEntitlement -Param $psboundparameters -Type 'GlobalApplicationEntitlementInfo' + if (! $result) { + Write-Host "Get-HVGlobalEntitlement: No global entitlement Found with given search parameters" + break + } + return $result + } + end { + [System.gc]::collect() + } +} + + +function Remove-HVGlobalEntitlement { + + <# +.Synopsis + Deletes a Global Entitlement. + +.DESCRIPTION + +.PARAMETER DisplayName + Display Name of Global Entitlement. + +.PARAMETER HvServer + Reference to Horizon View Server. If the value is not passed or null then + first element from global:DefaultHVServers would be considered inplace of hvServer + +.EXAMPLE + Deletes global application/desktop entitlement with displayName 'GE_APP' + Remove-HVGlobalEntitlement -DisplayName 'GE_APP' + +.EXAMPLE + Deletes global application/desktop entitlement(s), if displayName matches with 'GE_*' + Get-HVGlobalEntitlement -DisplayName 'GE_*' | Remove-HVGlobalEntitlement + + +.NOTES + Author : Praveen Mathamsetty. + Author email : pmathamsetty@vmware.com + Version : 1.1 + + ===Tested Against Environment==== + Horizon View Server Version : 7.0.2, 7.0.3 + PowerCLI Version : PowerCLI 6.5 + PowerShell Version : 5.0 +#> + +[CmdletBinding( + SupportsShouldProcess = $true, + ConfirmImpact = 'High' + )] + param( + [Parameter(Mandatory = $true, ParameterSetName = 'Default')] + [ValidateNotNullOrEmpty()] + [String] + $DisplayName, + + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'pipeline')] + $GlobalEntitlement, + + [Parameter(Mandatory = $false)] + $HvServer = $null + ) + begin { + $services = Get-ViewAPIService -hvServer $hvServer + if ($null -eq $services) { + Write-Error "Could not retrieve ViewApi services from connection object" + break + } + } + process { + $info = $services.PodFederation.PodFederation_get() + if ("ENABLED" -ne $info.localPodStatus.status) { + Write-Host "Multi-DataCenter-View/CPA is not enabled" + return + } + $confirmFlag = Get-HVConfirmFlag -keys $PsBoundParameters.Keys + $GeList = @() + if ($DisplayName) { + try { + $GeList = Get-HVGlobalEntitlement -DisplayName $DisplayName -hvServer $hvServer + } catch { + Write-Error "Make sure Get-HVGlobalEntitlement advanced function is loaded, $_" + break + } + } elseif ($PSCmdlet.MyInvocation.ExpectingInput -or $GlobalEntitlement) { + foreach ($item in $GlobalEntitlement) { + if (($item.GetType().name -ne 'GlobalEntitlementSummaryView') -and ($item.GetType().name -ne 'GlobalApplicationEntitlementInfo')) { + Write-Error "In pipeline did not get object of expected type GlobalApplicationEntitlementInfo/GlobalEntitlementSummaryView" + [System.gc]::collect() + return + } + $GeList += ,$item + } + } + foreach ($item in $GeList) { + Write-Host "Deleting global entitlement with DisplayName: " $item.base.displayName + if (!$confirmFlag -OR $pscmdlet.ShouldProcess($item.base.displayName)) { + if ($item.GetType().Name -eq 'GlobalEntitlementSummaryView') { + $services.GlobalEntitlement.GlobalEntitlement_Delete($item.id) + } else { + $services.GlobalApplicationEntitlement.GlobalApplicationEntitlement_Delete($item.id) + } + } + } + } + end { + [System.gc]::collect() + } + +} + +Export-ModuleMember Add-HVDesktop,Add-HVRDSServer,Connect-HVEvent,Disconnect-HVEvent,Get-HVPoolSpec,Get-HVInternalName, Get-HVEvent,Get-HVFarm,Get-HVFarmSummary,Get-HVPool,Get-HVPoolSummary,Get-HVMachine,Get-HVMachineSummary,Get-HVQueryResult,Get-HVQueryFilter,New-HVFarm,New-HVPool,Remove-HVFarm,Remove-HVPool,Set-HVFarm,Set-HVPool,Start-HVFarm,Start-HVPool,New-HVEntitlement,Get-HVEntitlement,Remove-HVEntitlement, Set-HVMachine, New-HVGlobalEntitlement, Remove-HVGlobalEntitlement, Get-HVGlobalEntitlement From 552793585ae2ce7cc944520e173558c99cae37b0 Mon Sep 17 00:00:00 2001 From: praveenmathamsetty Date: Tue, 7 Feb 2017 16:43:29 +0530 Subject: [PATCH 019/112] Added more description to Global Entitlement Added more description to Global Entitlement(s) --- Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 index 8c8db50..b668296 100644 --- a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 +++ b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 @@ -8458,8 +8458,10 @@ function Get-HVGlobalEntitlement { <# .Synopsis + Gets Global Entitlement(s) with given search parameters. .DESCRIPTION + Queries and returns global entitlement(s) and global application entitlement(s). Global entitlements are used to route users to their resources across multiple pods. .PARAMETER DisplayName @@ -8541,6 +8543,8 @@ function Remove-HVGlobalEntitlement { Deletes a Global Entitlement. .DESCRIPTION + Deletes global entitlement(s) and global application entitlement(s). + Optionally, user can pipe the global entitlement(s) as input to this function. .PARAMETER DisplayName Display Name of Global Entitlement. From 49834682a95177cada9139af2dec37dbebffd1a5 Mon Sep 17 00:00:00 2001 From: praveenmathamsetty Date: Fri, 10 Feb 2017 12:48:29 +0530 Subject: [PATCH 020/112] Formatting Examples Formatting Advanced functions Examples and Description --- .../VMware.Hv.Helper/VMware.HV.Helper.psm1 | 263 +++++++++--------- 1 file changed, 132 insertions(+), 131 deletions(-) diff --git a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 index b668296..01b1170 100644 --- a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 +++ b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 @@ -186,20 +186,20 @@ The Add-HVDesktop adds virtual machines to already exiting pools by using view A View API service object of Connect-HVServer cmdlet. .EXAMPLE - Add managed manual VMs to existing manual pool Add-HVDesktop -PoolName 'ManualPool' -Machines 'manualPool1', 'manualPool2' -Confirm:$false + Add managed manual VMs to existing manual pool .EXAMPLE - Add virtual machines to automated specific named dedicated pool Add-HVDesktop -PoolName 'SpecificNamed' -Machines 'vm-01', 'vm-02' -Users 'user1', 'user2' + Add virtual machines to automated specific named dedicated pool .EXAMPLE - Add machines to automated specific named Floating pool Add-HVDesktop -PoolName 'SpecificNamed' -Machines 'vm-03', 'vm-04' + Add machines to automated specific named Floating pool .EXAMPLE - Add machines to unmanged manual pool Add-HVDesktop -PoolName 'Unmanaged' -Machines 'desktop-1.eng.vmware.com' + Add machines to unmanged manual pool .NOTES Author : Praveen Mathamsetty. @@ -379,8 +379,8 @@ function Add-HVRDSServer { View API service object of Connect-HVServer cmdlet. .EXAMPLE - Add RDSServers to manual farm Add-HVRDSServer -Farm "manualFarmTest" -RdsServers "vm-for-rds","vm-for-rds-2" -Confirm:$false + Add RDSServers to manual farm .OUTPUTS None @@ -483,21 +483,21 @@ function Connect-HVEvent { Password corresponds to 'dbUserName' user. .EXAMPLE - Connecting to the database with default username configured on Connection Server $hvServer. Connect-HVEvent -HvServer $hvServer + Connecting to the database with default username configured on Connection Server $hvServer. .EXAMPLE - Connecting to the database configured on Connection Server $hvServer with customised user name 'system'. $hvDbServer = Connect-HVEvent -HvServer $hvServer -DbUserName 'system' + Connecting to the database configured on Connection Server $hvServer with customised user name 'system'. .EXAMPLE - Connecting to the database with customised user name and password. $hvDbServer = Connect-HVEvent -HvServer $hvServer -DbUserName 'system' -DbPassword 'censored' + Connecting to the database with customised user name and password. .EXAMPLE + C:\PS>$password = Read-Host 'Database Password' -AsSecureString + C:\PS>$hvDbServer = Connect-HVEvent -HvServer $hvServer -DbUserName 'system' -DbPassword $password Connecting to the database with customised user name and password, with password being a SecureString. - $password = Read-Host 'Database Password' -AsSecureString - $hvDbServer = Connect-HVEvent -HvServer $hvServer -DbUserName 'system' -DbPassword $password .OUTPUTS Returns a custom object that has database connection as 'dbConnection' property. @@ -614,8 +614,8 @@ function Disconnect-HVEvent { Connection object returned by Connect-HVEvent advanced function. This is a mandatory input. .EXAMPLE - Disconnecting the database connection on $hvDbServer. Disconnect-HVEvent -HvDbServer $hvDbServer + Disconnecting the database connection on $hvDbServer. .OUTPUTS None @@ -704,15 +704,15 @@ function Get-HVEvent { String that can applied in filtering on 'Message' column. .EXAMPLE + C:\PS>$e = Get-HVEvent -hvDbServer $hvDbServer + C:\PS>$e.Events Querying all the database events on database $hvDbServer. - $e = Get-HVEvent -hvDbServer $hvDbServer - $e.Events .EXAMPLE + C:\PS>$e = Get-HVEvent -HvDbServer $hvDbServer -TimePeriod 'all' -FilterType 'startsWith' -UserFilter 'aduser' -SeverityFilter 'err' -TimeFilter 'HH:MM:SS.fff' -ModuleFilter 'broker' -MessageFilter 'aduser' + C:\PS>$e.Events | Export-Csv -Path 'myEvents.csv' -NoTypeInformation Querying all the database events where user name startswith 'aduser', severity is of 'err' type, having module name as 'broker', message starting with 'aduser' and time starting with 'HH:MM:SS.fff'. The resulting events will be exported to a csv file 'myEvents.csv'. - $e = Get-HVEvent -HvDbServer $hvDbServer -TimePeriod 'all' -FilterType 'startsWith' -UserFilter 'aduser' -SeverityFilter 'err' -TimeFilter 'HH:MM:SS.fff' -ModuleFilter 'broker' -MessageFilter 'aduser' - $e.Events | Export-Csv -Path 'myEvents.csv' -NoTypeInformation .OUTPUTS Returns a custom object that has events information in 'Events' property. Events property will have events information with five columns: UserName, Severity, EventTime, Module and Message. @@ -931,24 +931,24 @@ function Get-HVFarm { Reference to Horizon View Server to query the data from. If the value is not passed or null then first element from global:DefaultHVServers would be considered inplace of hvServer. .EXAMPLE - Queries and returns farmInfo based on given parameter farmName Get-HVFarm -FarmName 'Farm-01' + Queries and returns farmInfo based on given parameter farmName .EXAMPLE - Queries and returns farmInfo based on given parameters farmName, farmDisplayName Get-HVFarm -FarmName 'Farm-01' -FarmDisplayName 'Sales RDS Farm' + Queries and returns farmInfo based on given parameters farmName, farmDisplayName .EXAMPLE - Queries and returns farmInfo based on given parameters farmName, farmType Get-HVFarm -FarmName 'Farm-01' -FarmType 'MANUAL' + Queries and returns farmInfo based on given parameters farmName, farmType .EXAMPLE - Queries and returns farmInfo based on given parameters farmName, FarmType etc Get-HVFarm -FarmName 'Farm-01' -FarmType 'MANUAL' -Enabled $true + Queries and returns farmInfo based on given parameters farmName, FarmType etc .EXAMPLE - Queries and returns farmInfo based on parameter farmName with wild character * Get-HVFarm -FarmName 'Farm-0*' + Queries and returns farmInfo based on parameter farmName with wild character * .OUTPUTs Returns the list of FarmInfo object matching the query criteria. @@ -1037,24 +1037,24 @@ function Get-HVFarmSummary { Reference to Horizon View Server to query the data from. If the value is not passed or null then first element from global:DefaultHVServers would be considered inplace of hvServer. .EXAMPLE - Queries and returns farmSummary objects based on given parameter farmName Get-HVFarmSummary -FarmName 'Farm-01' + Queries and returns farmSummary objects based on given parameter farmName .EXAMPLE - Queries and returns farmSummary objects based on given parameters farmName, farmDisplayName Get-HVFarmSummary -FarmName 'Farm-01' -FarmDisplayName 'Sales RDS Farm' + Queries and returns farmSummary objects based on given parameters farmName, farmDisplayName .EXAMPLE - Queries and returns farmSummary objects based on given parameters farmName, farmType Get-HVFarmSummary -FarmName 'Farm-01' -FarmType 'MANUAL' + Queries and returns farmSummary objects based on given parameters farmName, farmType .EXAMPLE - Queries and returns farmSummary objects based on given parameters farmName, FarmType etc Get-HVFarmSummary -FarmName 'Farm-01' -FarmType 'MANUAL' -Enabled $true + Queries and returns farmSummary objects based on given parameters farmName, FarmType etc .EXAMPLE - Queries and returns farmSummary objects based on given parameter farmName with wild character * Get-HVFarmSummary -FarmName 'Farm-0*' + Queries and returns farmSummary objects based on given parameter farmName with wild character * .OUTPUTs Returns the list of FarmSummary object matching the query criteria. @@ -1218,20 +1218,20 @@ function Get-HVPool { first element from global:DefaultHVServers would be considered inplace of hvServer .EXAMPLE - Queries and returns pool object(s) based on given parameters poolName, poolType etc. Get-HVPool -PoolName 'mypool' -PoolType MANUAL -UserAssignment FLOATING -Enabled $true -ProvisioningEnabled $true + Queries and returns pool object(s) based on given parameters poolName, poolType etc. .EXAMPLE - Queries and returns pool object(s) based on given parameters poolType and userAssignment Get-HVPool -PoolType AUTOMATED -UserAssignment FLOATING + Queries and returns pool object(s) based on given parameters poolType and userAssignment .EXAMPLE - Queries and returns pool object(s) based on given parameters poolName, PoolType etc. Get-HVPool -PoolName 'myrds' -PoolType RDS -UserAssignment DEDICATED -Enabled $false + Queries and returns pool object(s) based on given parameters poolName, PoolType etc. .EXAMPLE - Queries and returns pool object(s) based on given parameters poolName and HvServer etc. Get-HVPool -PoolName 'myrds' -PoolType RDS -UserAssignment DEDICATED -Enabled $false -HvServer $mycs + Queries and returns pool object(s) based on given parameters poolName and HvServer etc. .OUTPUTS Returns list of objects of type DesktopInfo @@ -1348,20 +1348,20 @@ function Get-HVPoolSummary { first element from global:DefaultHVServers would be considered inplace of hvServer .EXAMPLE - Queries and returns desktopSummaryView based on given parameters poolName, poolType etc. Get-HVPoolSummary -PoolName 'mypool' -PoolType MANUAL -UserAssignment FLOATING -Enabled $true -ProvisioningEnabled $true + Queries and returns desktopSummaryView based on given parameters poolName, poolType etc. .EXAMPLE - Queries and returns desktopSummaryView based on given parameters poolType, userAssignment. Get-HVPoolSummary -PoolType AUTOMATED -UserAssignment FLOATING + Queries and returns desktopSummaryView based on given parameters poolType, userAssignment. .EXAMPLE - Queries and returns desktopSummaryView based on given parameters poolName, poolType, userAssignment etc. Get-HVPoolSummary -PoolName 'myrds' -PoolType RDS -UserAssignment DEDICATED -Enabled $false + Queries and returns desktopSummaryView based on given parameters poolName, poolType, userAssignment etc. .EXAMPLE - Queries and returns desktopSummaryView based on given parameters poolName, HvServer etc. Get-HVPoolSummary -PoolName 'myrds' -PoolType RDS -UserAssignment DEDICATED -Enabled $false -HvServer $mycs + Queries and returns desktopSummaryView based on given parameters poolName, HvServer etc. .OUTPUTS Returns list of DesktopSummaryView @@ -1535,41 +1535,41 @@ function Get-HVQueryFilter { .EXAMPLE - Creates queryFilterEquals with given parameters memberName(position 0) and memberValue(position 2) Get-HVQueryFilter data.name -Eq vmware + Creates queryFilterEquals with given parameters memberName(position 0) and memberValue(position 2) .EXAMPLE - Creates queryFilterEquals with given parameters memberName and memberValue Get-HVQueryFilter -MemberName data.name -Eq -MemberValue vmware + Creates queryFilterEquals with given parameters memberName and memberValue .EXAMPLE - Creates queryFilterNotEquals filter with given parameters memberName and memberValue Get-HVQueryFilter data.name -Ne vmware + Creates queryFilterNotEquals filter with given parameters memberName and memberValue .EXAMPLE - Creates queryFilterContains with given parameters memberName and memberValue Get-HVQueryFilter data.name -Contains vmware + Creates queryFilterContains with given parameters memberName and memberValue .EXAMPLE - Creates queryFilterStartsWith with given parameters memberName and memberValue Get-HVQueryFilter data.name -Startswith vmware + Creates queryFilterStartsWith with given parameters memberName and memberValue .EXAMPLE + C:\PS>$filter = Get-HVQueryFilter data.name -Startswith vmware + C:\PS>Get-HVQueryFilter -Not $filter Creates queryFilterNot with given parameter filter - $filter = Get-HVQueryFilter data.name -Startswith vmware - Get-HVQueryFilter -Not $filter .EXAMPLE + C:\PS>$filter1 = Get-HVQueryFilter data.name -Startswith vmware + C:\PS>$filter2 = Get-HVQueryFilter data.name -Contains pool + C:\PS>Get-HVQueryFilter -And @($filter1, $filter2) Creates queryFilterAnd with given parameter filters array - $filter1 = Get-HVQueryFilter data.name -Startswith vmware - $filter2 = Get-HVQueryFilter data.name -Contains pool - Get-HVQueryFilter -And @($filter1, $filter2) .EXAMPLE + C:\PS>$filter1 = Get-HVQueryFilter data.name -Startswith vmware + C:\PS>$filter2 = Get-HVQueryFilter data.name -Contains pool + C:\PS>Get-HVQueryFilter -Or @($filter1, $filter2) Creates queryFilterOr with given parameter filters array - $filter1 = Get-HVQueryFilter data.name -Startswith vmware - $filter2 = Get-HVQueryFilter data.name -Contains pool - Get-HVQueryFilter -Or @($filter1, $filter2) .OUTPUTS Returns the QueryFilter object @@ -1695,25 +1695,25 @@ function Get-HVQueryResult { first element from global:DefaultHVServers would be considered inplace of hvServer .EXAMPLE - Returns query results of entityType DesktopSummaryView(position 0) Get-HVQueryResult DesktopSummaryView + Returns query results of entityType DesktopSummaryView(position 0) .EXAMPLE - Returns query results of entityType DesktopSummaryView(position 0) with given filter(position 1) Get-HVQueryResult DesktopSummaryView (Get-HVQueryFilter data.name -Eq vmware) + Returns query results of entityType DesktopSummaryView(position 0) with given filter(position 1) .EXAMPLE - Returns query results of entityType DesktopSummaryView with given filter Get-HVQueryResult -EntityType DesktopSummaryView -Filter (Get-HVQueryFilter desktopSummaryData.name -Eq vmware) + Returns query results of entityType DesktopSummaryView with given filter .EXAMPLE + C:\PS>$myFilter = Get-HVQueryFilter data.name -Contains vmware + C:\PS>Get-HVQueryResult -EntityType DesktopSummaryView -Filter $myFilter -SortBy desktopSummaryData.displayName -SortDescending $false Returns query results of entityType DesktopSummaryView with given filter and also sorted based on dispalyName - $myFilter = Get-HVQueryFilter data.name -Contains vmware - Get-HVQueryResult -EntityType DesktopSummaryView -Filter $myFilter -SortBy desktopSummaryData.displayName -SortDescending $false .EXAMPLE - Returns query results of entityType DesktopSummaryView, maximum count equal to limit Get-HVQueryResult DesktopSummaryView -Limit 10 + Returns query results of entityType DesktopSummaryView, maximum count equal to limit .OUTPUTS Returns the list of objects of entityType @@ -1942,28 +1942,28 @@ function New-HVFarm { Reference to Horizon View Server to query the farms from. If the value is not passed or null then first element from global:DefaultHVServers would be considered inplace of hvServer. .EXAMPLE - Creates new linkedClone farm by using naming pattern New-HVFarm -LinkedClone -FarmName 'LCFarmTest' -ParentVM 'Win_Server_2012_R2' -SnapshotVM 'Snap_RDS' -VmFolder 'PoolVM' -HostOrCluster 'cls' -ResourcePool 'cls' -Datastores 'datastore1 (5)' -FarmDisplayName 'LC Farm Test' -Description 'created LC Farm from PS' -EnableProvisioning $true -StopOnProvisioningError $false -NamingPattern "LCFarmVM_PS" -MinReady 1 -MaximumCount 1 -SysPrepName "RDSH_Cust2" -NetBiosName "adviewdev" + Creates new linkedClone farm by using naming pattern .EXAMPLE - Creates new linkedClone farm by using naming pattern New-HVFarm -InstantClone -FarmName 'ICFarmCL' -ParentVM 'vm-rdsh-ic' -SnapshotVM 'Snap_5' -VmFolder 'Instant_Clone_VMs' -HostOrCluster 'vimal-cluster' -ResourcePool 'vimal-cluster' -Datastores 'datastore1' -FarmDisplayName 'IC Farm using CL' -Description 'created IC Farm from PS command-line' -EnableProvisioning $true -StopOnProvisioningError $false -NamingPattern "ICFarmCL-" -NetBiosName "ad-vimalg" + Creates new linkedClone farm by using naming pattern .EXAMPLE - Creates new linkedClone farm by using json file New-HVFarm -Spec C:\VMWare\Specs\LinkedClone.json -Confirm:$false + Creates new linkedClone farm by using json file -.EXAMPLE - Creates new instantClone farm by using json file +.EXAMPLE New-HVFarm -Spec C:\VMWare\Specs\InstantCloneFarm.json -Confirm:$false + Creates new instantClone farm by using json file .EXAMPLE - Creates new manual farm by using rdsServers names New-HVFarm -Manual -FarmName "manualFarmTest" -FarmDisplayName "manualFarmTest" -Description "Manual PS Test" -RdsServers "vm-for-rds.eng.vmware.com","vm-for-rds-2.eng.vmware.com" -Confirm:$false + Creates new manual farm by using rdsServers names .EXAMPLE - Creates new instant clone farm by reading few parameters from json and few parameters from command line. New-HVFarm -Spec C:\VMWare\Specs\AutomatedInstantCloneFarm.json -FarmName 'InsPool' -NamingPattern 'InsFarm-' + Creates new instant clone farm by reading few parameters from json and few parameters from command line. .OUTPUTS None @@ -3365,6 +3365,7 @@ function New-HVPool { .PARAMETER PostSynchronizationScriptParameters Post synchronization script parameters. Example: p1 p2 p3 Applicable to Linked, Instant Clone pools. + .PARAMETER Source Source of the Virtual machines for manual pool. Supported values are 'VIRTUAL_CENTER','UNMANAGED'. @@ -3391,38 +3392,38 @@ function New-HVPool { first element from global:DefaultHVServers would be considered inplace of hvServer. .EXAMPLE + C:\PS>New-HVPool -LinkedClone -PoolName 'vmwarepool' -UserAssignment FLOATING -ParentVM 'Agent_vmware' -SnapshotVM 'kb-hotfix' -VmFolder 'vmware' -HostOrCluster 'CS-1' -ResourcePool 'CS-1' -Datastores 'datastore1' -NamingMethod PATTERN -PoolDisplayName 'vmware linkedclone pool' -Description 'created linkedclone pool from ps' -EnableProvisioning $true -StopOnProvisioningError $false -NamingPattern "vmware2" -MinReady 0 -MaximumCount 1 -SpareCount 1 -ProvisioningTime UP_FRONT -SysPrepName vmwarecust -CustType SYS_PREP -NetBiosName adviewdev -DomainAdmin root Create new automated linked clone pool with naming method pattern - New-HVPool -LinkedClone -PoolName 'vmwarepool' -UserAssignment FLOATING -ParentVM 'Agent_vmware' -SnapshotVM 'kb-hotfix' -VmFolder 'vmware' -HostOrCluster 'CS-1' -ResourcePool 'CS-1' -Datastores 'datastore1' -NamingMethod PATTERN -PoolDisplayName 'vmware linkedclone pool' -Description 'created linkedclone pool from ps' -EnableProvisioning $true -StopOnProvisioningError $false -NamingPattern "vmware2" -MinReady 0 -MaximumCount 1 -SpareCount 1 -ProvisioningTime UP_FRONT -SysPrepName vmwarecust -CustType SYS_PREP -NetBiosName adviewdev -DomainAdmin root .EXAMPLE - Create new automated linked clone pool by using JSON spec file New-HVPool -Spec C:\VMWare\Specs\LinkedClone.json -Confirm:$false + Create new automated linked clone pool by using JSON spec file .EXAMPLE - Clones new pool by using existing pool configuration - Get-HVPool -PoolName 'vmwarepool' | New-HVPool -PoolName 'clonedPool' -NamingPattern 'clonelnk1'; + C:\PS>Get-HVPool -PoolName 'vmwarepool' | New-HVPool -PoolName 'clonedPool' -NamingPattern 'clonelnk1'; (OR) - $vmwarepool = Get-HVPool -PoolName 'vmwarepool'; New-HVPool -ClonePool $vmwarepool -PoolName 'clonedPool' -NamingPattern 'clonelnk1'; + C:\PS>$vmwarepool = Get-HVPool -PoolName 'vmwarepool'; New-HVPool -ClonePool $vmwarepool -PoolName 'clonedPool' -NamingPattern 'clonelnk1'; + Clones new pool by using existing pool configuration .EXAMPLE - Create new automated instant clone pool with naming method pattern New-HVPool -InstantClone -PoolName "InsPoolvmware" -PoolDisplayName "insPool" -Description "create instant pool" -UserAssignment FLOATING -ParentVM 'Agent_vmware' -SnapshotVM 'kb-hotfix' -VmFolder 'vmware' -HostOrCluster 'CS-1' -ResourcePool 'CS-1' -NamingMethod PATTERN -Datastores 'datastore1' -NamingPattern "inspool2" -NetBiosName 'adviewdev' -DomainAdmin root + Create new automated instant clone pool with naming method pattern .EXAMPLE - Create new automated full clone pool with naming method pattern New-HVPool -FullClone -PoolName "FullClone" -PoolDisplayName "FullClonePra" -Description "create full clone" -UserAssignment DEDICATED -Template 'powerCLI-VM-TEMPLATE' -VmFolder 'vmware' -HostOrCluster 'CS-1' -ResourcePool 'CS-1' -Datastores 'datastore1' -NamingMethod PATTERN -NamingPattern 'FullCln1' -SysPrepName vmwarecust -CustType SYS_PREP -NetBiosName adviewdev -DomainAdmin root + Create new automated full clone pool with naming method pattern .EXAMPLE - Create new managed manual pool from virtual center managed VirtualMachines. New-HVPool -MANUAL -PoolName 'manualVMWare' -PoolDisplayName 'MNLPUL' -Description 'Manual pool creation' -UserAssignment FLOATING -Source VIRTUAL_CENTER -VM 'PowerCLIVM1', 'PowerCLIVM2' + Create new managed manual pool from virtual center managed VirtualMachines. .EXAMPLE - Create new unmanaged manual pool from unmanaged VirtualMachines. New-HVPool -MANUAL -PoolName 'unmangedVMWare' -PoolDisplayName 'unMngPl' -Description 'unmanaged Manual Pool creation' -UserAssignment FLOATING -Source UNMANAGED -VM 'myphysicalmachine.vmware.com' + Create new unmanaged manual pool from unmanaged VirtualMachines. .EXAMPLE - Creates new instant clone pool by reading few parameters from json and few parameters from command line. New-HVPool -spec 'C:\Json\InstantClone.json' -PoolName 'InsPool1'-NamingPattern 'INSPool-' + Creates new instant clone pool by reading few parameters from json and few parameters from command line. .OUTPUTS None @@ -5229,17 +5230,17 @@ function Remove-HVFarm { Reference to Horizon View Server to query the data from. If the value is not passed or null then first element from global:DefaultHVServers would be considered inplace of hvServer. .EXAMPLE - Delete a given farm. For an automated farm, all the RDS Server VMs are deleted from disk whereas for a manual farm only the RDS Server associations are removed. Remove-HVFarm -FarmName 'Farm-01' -HvServer $hvServer -Confirm:$false + Delete a given farm. For an automated farm, all the RDS Server VMs are deleted from disk whereas for a manual farm only the RDS Server associations are removed. .EXAMPLE - Deletes a given Farm object(s). For an automated farm, all the RDS Server VMs are deleted from disk whereas for a manual farm only the RDS Server associations are removed. $farm_array | Remove-HVFarm -HvServer $hvServer + Deletes a given Farm object(s). For an automated farm, all the RDS Server VMs are deleted from disk whereas for a manual farm only the RDS Server associations are removed. .EXAMPLE + C:\PS>$farm1 = Get-HVFarm -FarmName 'Farm-01' + C:\PS>Remove-HVFarm -Farm $farm1 Deletes a given Farm object. For an automated farm, all the RDS Server VMs are deleted from disk whereas for a manual farm only the RDS Server associations are removed. - $farm1 = Get-HVFarm -FarmName 'Farm-01' - Remove-HVFarm -Farm $farm1 .OUTPUTS None @@ -5347,16 +5348,16 @@ function Remove-HVPool { Logs off a session forcibly to virtual machine(s). This operation will also log off a locked session. .EXAMPLE - Deletes pool from disk with given parameters PoolName etc. Remove-HVPool -HvServer $hvServer -PoolName 'FullClone' -DeleteFromDisk -Confirm:$false + Deletes pool from disk with given parameters PoolName etc. .EXAMPLE - Deletes specified pool from disk $pool_array | Remove-HVPool -HvServer $hvServer -DeleteFromDisk + Deletes specified pool from disk .EXAMPLE - Deletes specified pool and VM(s) associations are removed from view Manager Remove-HVPool -Pool $pool1 + Deletes specified pool and VM(s) associations are removed from view Manager .OUTPUTS None @@ -5506,24 +5507,24 @@ function Set-HVFarm { Reference to Horizon View Server to query the data from. If the value is not passed or null then first element from global:DefaultHVServers would be considered inplace of hvServer. .EXAMPLE - Updates farm configuration by using json file Set-HVFarm -FarmName 'Farm-01' -Spec 'C:\Edit-HVFarm\ManualEditFarm.json' -Confirm:$false + Updates farm configuration by using json file .EXAMPLE - Updates farm configuration with given parameters key and value Set-HVFarm -FarmName 'Farm-01' -Key 'base.description' -Value 'updated description' + Updates farm configuration with given parameters key and value .EXAMPLE - Updates farm(s) configuration with given parameters key and value $farm_array | Set-HVFarm -Key 'base.description' -Value 'updated description' + Updates farm(s) configuration with given parameters key and value .EXAMPLE - Enables provisioning to specified farm Set-HVFarm -farm 'Farm2' -Start + Enables provisioning to specified farm .EXAMPLE - Enables specified farm Set-HVFarm -farm 'Farm2' -Enable + Enables specified farm .OUTPUTS None @@ -5709,28 +5710,28 @@ function Set-HVPool { Path of the JSON specification file containing key/value pair. .EXAMPLE - Updates pool configuration by using json file Set-HVPool -PoolName 'ManualPool' -Spec 'C:\Edit-HVPool\EditPool.json' -Confirm:$false + Updates pool configuration by using json file .EXAMPLE - Updates pool configuration with given parameters key and value Set-HVPool -PoolName 'RDSPool' -Key 'base.description' -Value 'update description' + Updates pool configuration with given parameters key and value .Example - Disables specified pool Set-HVPool -PoolName 'LnkClone' -Disable + Disables specified pool .Example - Enables specified pool Set-HVPool -PoolName 'LnkClone' -Enable + Enables specified pool .Example - Enables provisioning to specified pool Set-HVPool -PoolName 'LnkClone' -Start + Enables provisioning to specified pool .Example - Disables provisioning to specified pool Set-HVPool -PoolName 'LnkClone' -Stop + Disables provisioning to specified pool .OUTPUTS None @@ -5951,25 +5952,25 @@ function Start-HVFarm { Reference to Horizon View Server to query the data from. If the value is not passed or null then first element from global:DefaultHVServers would be considered inplace of hvServer. .EXAMPLE - Requests a recompose of RDS Servers in the specified automated farm Start-HVFarm -Recompose -Farm 'Farm-01' -LogoffSetting FORCE_LOGOFF -ParentVM 'View-Agent-Win8' -SnapshotVM 'Snap_USB' -Confirm:$false + Requests a recompose of RDS Servers in the specified automated farm .EXAMPLE + C:\PS>$myTime = Get-Date '10/03/2016 12:30:00' + C:\PS>Start-HVFarm -Farm 'Farm-01' -Recompose -LogoffSetting 'FORCE_LOGOFF' -ParentVM 'ParentVM' -SnapshotVM 'SnapshotVM' -StartTime $myTime Requests a recompose task for automated farm in specified time - $myTime = Get-Date '10/03/2016 12:30:00' - Start-HVFarm -Farm 'Farm-01' -Recompose -LogoffSetting 'FORCE_LOGOFF' -ParentVM 'ParentVM' -SnapshotVM 'SnapshotVM' -StartTime $myTime .EXAMPLE - Requests a ScheduleMaintenance task for instant-clone farm. Schedules an IMMEDIATE maintenance. Start-HVFarm -Farm 'ICFarm-01' -ScheduleMaintenance -MaintenanceMode IMMEDIATE + Requests a ScheduleMaintenance task for instant-clone farm. Schedules an IMMEDIATE maintenance. .EXAMPLE - Requests a ScheduleMaintenance task for instant-clone farm. Schedules a recurring weekly maintenace every Saturday night at 23:30 and updates the parentVM and snapshot. Start-HVFarm -ScheduleMaintenance -Farm 'ICFarm-01' -MaintenanceMode RECURRING -MaintenancePeriod WEEKLY -MaintenanceStartTime '11:30' -StartInt 6 -EveryInt 1 -ParentVM 'vm-rdsh-ic' -SnapshotVM 'Snap_Updated' + Requests a ScheduleMaintenance task for instant-clone farm. Schedules a recurring weekly maintenace every Saturday night at 23:30 and updates the parentVM and snapshot. .EXAMPLE - Requests a CancelMaintenance task for instant-clone farm. Cancels recurring maintenance. Start-HVFarm -CancelMaintenance -Farm 'ICFarm-01' -MaintenanceMode RECURRING + Requests a CancelMaintenance task for instant-clone farm. Cancels recurring maintenance. .OUTPUTS None @@ -6331,25 +6332,25 @@ function Start-HVPool { View API service object of Connect-HVServer cmdlet. .EXAMPLE - Requests a recompose of machines in the specified pool Start-HVPool -Recompose -Pool 'LCPool3' -LogoffSetting FORCE_LOGOFF -ParentVM 'View-Agent-Win8' -SnapshotVM 'Snap_USB' + Requests a recompose of machines in the specified pool .EXAMPLE - Requests a refresh of machines in the specified pool Start-HVPool -Refresh -Pool 'LCPool3' -LogoffSetting FORCE_LOGOFF -Confirm:$false + Requests a refresh of machines in the specified pool .EXAMPLE + C:\PS>$myTime = Get-Date '10/03/2016 12:30:00' + C:\PS>Start-HVPool -Rebalance -Pool 'LCPool3' -LogoffSetting FORCE_LOGOFF -StartTime $myTime Requests a rebalance of machines in a pool with specified time - $myTime = Get-Date '10/03/2016 12:30:00' - Start-HVPool -Rebalance -Pool 'LCPool3' -LogoffSetting FORCE_LOGOFF -StartTime $myTime .EXAMPLE - Requests an update of push image operation on the specified Instant Clone Engine sourced pool Start-HVPool -SchedulePushImage -Pool 'InstantPool' -LogoffSetting FORCE_LOGOFF -ParentVM 'InsParentVM' -SnapshotVM 'InsSnapshotVM' + Requests an update of push image operation on the specified Instant Clone Engine sourced pool .EXAMPLE - Requests a cancellation of the current scheduled push image operation on the specified Instant Clone Engine sourced pool Start-HVPool -CancelPushImage -Pool 'InstantPool' + Requests a cancellation of the current scheduled push image operation on the specified Instant Clone Engine sourced pool .OUTPUTS None @@ -6774,20 +6775,20 @@ function Get-HVMachine { first element from global:DefaultHVServers would be considered inplace of hvServer .EXAMPLE - Queries VM(s) with given parameter poolName Get-HVDesktop -PoolName 'ManualPool' + Queries VM(s) with given parameter poolName .EXAMPLE - Queries VM(s) with given parameter machineName Get-HVDesktop -MachineName 'PowerCLIVM' + Queries VM(s) with given parameter machineName .EXAMPLE - Queries VM(s) with given parameter vm state Get-HVDesktop -State CUSTOMIZING + Queries VM(s) with given parameter vm state .EXAMPLE - Queries VM(s) with given parameter dnsName with wildcard character * Get-HVDesktop -DnsName 'powercli-*' + Queries VM(s) with given parameter dnsName with wildcard character * .OUTPUTS Returns list of objects of type MachineInfo @@ -6895,20 +6896,20 @@ function Get-HVMachineSummary { first element from global:DefaultHVServers would be considered inplace of hvServer .EXAMPLE - Queries VM(s) with given parameter poolName Get-HVDesktopSummary -PoolName 'ManualPool' + Queries VM(s) with given parameter poolName .EXAMPLE - Queries VM(s) with given parameter machineName Get-HVDesktopSummary -MachineName 'PowerCLIVM' + Queries VM(s) with given parameter machineName .EXAMPLE - Queries VM(s) with given parameter vm state Get-HVDesktopSummary -State CUSTOMIZING + Queries VM(s) with given parameter vm state .EXAMPLE - Queries VM(s) with given parameter dnsName with wildcard character * Get-HVDesktopSummary -DnsName 'powercli-*' + Queries VM(s) with given parameter dnsName with wildcard character * .OUTPUTS Returns list of objects of type MachineNamesView @@ -6986,12 +6987,12 @@ function Get-HVPoolSpec { first element from global:DefaultHVServers would be considered inplace of hvServer .EXAMPLE - Converts DesktopInfo to DesktopSpec Get-HVPoolSpec -DesktopInfo $DesktopInfoObj + Converts DesktopInfo to DesktopSpec .EXAMPLE - Converts DesktopInfo to DesktopSpec and also dumps json object Get-HVPool -PoolName 'LnkClnJson' | Get-HVPoolSpec -FilePath "C:\temp\LnkClnJson.json" + Converts DesktopInfo to DesktopSpec and also dumps json object .OUTPUTS Returns desktop specification @@ -7222,8 +7223,8 @@ function Get-HVInternalName { first element from global:DefaultHVServers would be considered inplace of hvServer .EXAMPLE - Decodes Horizon API Id and returns human readable name Get-HVInternalName -EntityId $entityId + Decodes Horizon API Id and returns human readable name .OUTPUTS Returns human readable name @@ -7372,28 +7373,28 @@ function New-HVEntitlement { first element from global:DefaultHVServers would be considered inplace of hvServer .EXAMPLE - Associate a user/group with a pool New-HVEntitlement -User 'administrator@adviewdev.eng.vmware.com' -ResourceName 'InsClnPol' -Confirm:$false + Associate a user/group with a pool .EXAMPLE - Associate a user/group with a application New-HVEntitlement -User 'adviewdev\administrator' -ResourceName 'Calculator' -ResourceType Application + Associate a user/group with a application .EXAMPLE - Associate a user/group with a URLRedirection settings New-HVEntitlement -User 'adviewdev.eng.vmware.com\administrator' -ResourceName 'UrlSetting1' -ResourceType URLRedirection + Associate a user/group with a URLRedirection settings .EXAMPLE - Associate a user/group with a desktop entitlement New-HVEntitlement -User 'adviewdev.eng.vmware.com\administrator' -ResourceName 'GE1' -ResourceType GlobalEntitlement + Associate a user/group with a desktop entitlement .EXAMPLE - Associate a user/group with a application entitlement New-HVEntitlement -User 'adviewdev\administrator' -ResourceName 'GEAPP1' -ResourceType GlobalApplicationEntitlement + Associate a user/group with a application entitlement .EXAMPLE - Associate a user/group with list of pools $pools = Get-HVPool; $pools | New-HVEntitlement -User 'adviewdev\administrator' -Confirm:$false + Associate a user/group with list of pools .NOTES @@ -7617,20 +7618,20 @@ function Get-HVEntitlement { first element from global:DefaultHVServers would be considered inplace of hvServer .EXAMPLE - Gets all the entitlements related to application pool Get-HVEntitlement -ResourceType Application + Gets all the entitlements related to application pool .EXAMPLE - Gets entitlements specific to user or group name and application resource Get-HVEntitlement -User 'adviewdev.eng.vmware.com\administrator' -ResourceName 'calculator' -ResourceType Application + Gets entitlements specific to user or group name and application resource .EXAMPLE - Gets entitlements specific to user or group and URLRedirection resource Get-HVEntitlement -User 'adviewdev.eng.vmware.com\administrator' -ResourceName 'UrlSetting1' -ResourceType URLRedirection + Gets entitlements specific to user or group and URLRedirection resource .EXAMPLE - Gets entitlements specific to user or group and GlobalEntitlement resource Get-HVEntitlement -User 'administrator@adviewdev.eng.vmware.com' -ResourceName 'GE1' -ResourceType GlobalEntitlement + Gets entitlements specific to user or group and GlobalEntitlement resource .NOTES Author : Praveen Mathamsetty. @@ -7829,16 +7830,16 @@ function Remove-HVEntitlement { first element from global:DefaultHVServers would be considered inplace of hvServer .EXAMPLE - Deletes entitlement between a user/group and a pool resource Remove-HVEntitlement -User 'administrator@adviewdev' -ResourceName LnkClnJSon -Confirm:$false + Deletes entitlement between a user/group and a pool resource .EXAMPLE - Deletes entitlement between a user/group and a Application resource Remove-HVEntitlement -User 'adviewdev\puser2' -ResourceName 'calculator' -ResourceType Application + Deletes entitlement between a user/group and a Application resource .EXAMPLE - Deletes entitlement between a user/group and a GlobalApplicationEntitlement resource Remove-HVEntitlement -User 'adviewdev\administrator' -ResourceName 'GEAPP1' -ResourceType GlobalApplicationEntitlement + Deletes entitlement between a user/group and a GlobalApplicationEntitlement resource .NOTES Author : Praveen Mathamsetty. @@ -8068,16 +8069,16 @@ PARAMETER Key first element from global:DefaultHVServers would be considered inplace of hvServer .EXAMPLE - Moving the machine in to Maintenance mode using machine name Set-HVMachine -MachineName 'Agent_Praveen' -Maintenance ENTER_MAINTENANCE_MODE + Moving the machine in to Maintenance mode using machine name .EXAMPLE - Moving the machine in to Maintenance mode using machine object(s) Get-HVMachine -MachineName 'Agent_Praveen' | Set-HVMachine -Maintenance ENTER_MAINTENANCE_MODE + Moving the machine in to Maintenance mode using machine object(s) .EXAMPLE - Moving the machine in to Maintenance mode using machine object(s) $machine = Get-HVMachine -MachineName 'Agent_Praveen'; Set-HVMachine -Machine $machine -Maintenance EXIT_MAINTENANCE_MODE + Moving the machine in to Maintenance mode using machine object(s) .OUTPUTS None @@ -8244,12 +8245,12 @@ function New-HVGlobalEntitlement { first element from global:DefaultHVServers would be considered inplace of hvServer .EXAMPLE - Creates new global application entitlement New-HVGlobalEntitlement -DisplayName 'GE_APP' -Type APPLICATION_ENTITLEMENT + Creates new global application entitlement .EXAMPLE - Creates new global desktop entitlement New-HVGlobalEntitlement -DisplayName 'GE_DESKTOP' -Type DESKTOP_ENTITLEMENT + Creates new global desktop entitlement .NOTES @@ -8475,8 +8476,8 @@ function Get-HVGlobalEntitlement { first element from global:DefaultHVServers would be considered inplace of hvServer .EXAMPLE - Retrieves global application/desktop entitlement(s) with displayName 'GEAPP' Get-HVGlobalEntitlement -DisplayName 'GEAPP' + Retrieves global application/desktop entitlement(s) with displayName 'GEAPP' .NOTES @@ -8554,12 +8555,12 @@ function Remove-HVGlobalEntitlement { first element from global:DefaultHVServers would be considered inplace of hvServer .EXAMPLE - Deletes global application/desktop entitlement with displayName 'GE_APP' Remove-HVGlobalEntitlement -DisplayName 'GE_APP' + Deletes global application/desktop entitlement with displayName 'GE_APP' .EXAMPLE - Deletes global application/desktop entitlement(s), if displayName matches with 'GE_*' Get-HVGlobalEntitlement -DisplayName 'GE_*' | Remove-HVGlobalEntitlement + Deletes global application/desktop entitlement(s), if displayName matches with 'GE_*' .NOTES From dcf76d6b1a39c2e6bc09b2b86a59ed9eca929194 Mon Sep 17 00:00:00 2001 From: praveenmathamsetty Date: Fri, 10 Feb 2017 15:06:02 +0530 Subject: [PATCH 021/112] Example Text correction Example Text correction --- .../VMware.Hv.Helper/VMware.HV.Helper.psm1 | 58 +++++++++---------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 index 01b1170..009cb47 100644 --- a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 +++ b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 @@ -928,7 +928,7 @@ function Get-HVFarm { Switch to get list of FarmSummaryView or FarmInfo objects in the result. If it is true a list of FarmInfo objects is returned ohterwise a list of FarmSummaryView objects is returned. .PARAMETER HvServer - Reference to Horizon View Server to query the data from. If the value is not passed or null then first element from global:DefaultHVServers would be considered inplace of hvServer. + Reference to Horizon View Server to query the data from. If the value is not passed or null then first element from global:DefaultHVServers would be considered in-place of hvServer. .EXAMPLE Get-HVFarm -FarmName 'Farm-01' @@ -1034,7 +1034,7 @@ function Get-HVFarmSummary { search for farms which are enabled .PARAMETER HvServer - Reference to Horizon View Server to query the data from. If the value is not passed or null then first element from global:DefaultHVServers would be considered inplace of hvServer. + Reference to Horizon View Server to query the data from. If the value is not passed or null then first element from global:DefaultHVServers would be considered in-place of hvServer. .EXAMPLE Get-HVFarmSummary -FarmName 'Farm-01' @@ -1215,7 +1215,7 @@ function Get-HVPool { .PARAMETER HvServer Reference to Horizon View Server to query the pools from. If the value is not passed or null then - first element from global:DefaultHVServers would be considered inplace of hvServer + first element from global:DefaultHVServers would be considered in-place of hvServer .EXAMPLE Get-HVPool -PoolName 'mypool' -PoolType MANUAL -UserAssignment FLOATING -Enabled $true -ProvisioningEnabled $true @@ -1345,7 +1345,7 @@ function Get-HVPoolSummary { .PARAMETER HvServer Reference to Horizon View Server to query the pools from. If the value is not passed or null then - first element from global:DefaultHVServers would be considered inplace of hvServer + first element from global:DefaultHVServers would be considered in-place of hvServer .EXAMPLE Get-HVPoolSummary -PoolName 'mypool' -PoolType MANUAL -UserAssignment FLOATING -Enabled $true -ProvisioningEnabled $true @@ -1692,7 +1692,7 @@ function Get-HVQueryResult { .PARAMETER HvServer Reference to Horizon View Server to query the data from. If the value is not passed or null then - first element from global:DefaultHVServers would be considered inplace of hvServer + first element from global:DefaultHVServers would be considered in-place of hvServer .EXAMPLE Get-HVQueryResult DesktopSummaryView @@ -1939,7 +1939,7 @@ function New-HVFarm { Path of the JSON specification file. .PARAMETER HvServer - Reference to Horizon View Server to query the farms from. If the value is not passed or null then first element from global:DefaultHVServers would be considered inplace of hvServer. + Reference to Horizon View Server to query the farms from. If the value is not passed or null then first element from global:DefaultHVServers would be considered in-place of hvServer. .EXAMPLE New-HVFarm -LinkedClone -FarmName 'LCFarmTest' -ParentVM 'Win_Server_2012_R2' -SnapshotVM 'Snap_RDS' -VmFolder 'PoolVM' -HostOrCluster 'cls' -ResourcePool 'cls' -Datastores 'datastore1 (5)' -FarmDisplayName 'LC Farm Test' -Description 'created LC Farm from PS' -EnableProvisioning $true -StopOnProvisioningError $false -NamingPattern "LCFarmVM_PS" -MinReady 1 -MaximumCount 1 -SysPrepName "RDSH_Cust2" -NetBiosName "adviewdev" @@ -3389,7 +3389,7 @@ function New-HVPool { .PARAMETER HvServer Reference to Horizon View Server to query the pools from. If the value is not passed or null then - first element from global:DefaultHVServers would be considered inplace of hvServer. + first element from global:DefaultHVServers would be considered in-place of hvServer. .EXAMPLE C:\PS>New-HVPool -LinkedClone -PoolName 'vmwarepool' -UserAssignment FLOATING -ParentVM 'Agent_vmware' -SnapshotVM 'kb-hotfix' -VmFolder 'vmware' -HostOrCluster 'CS-1' -ResourcePool 'CS-1' -Datastores 'datastore1' -NamingMethod PATTERN -PoolDisplayName 'vmware linkedclone pool' -Description 'created linkedclone pool from ps' -EnableProvisioning $true -StopOnProvisioningError $false -NamingPattern "vmware2" -MinReady 0 -MaximumCount 1 -SpareCount 1 -ProvisioningTime UP_FRONT -SysPrepName vmwarecust -CustType SYS_PREP -NetBiosName adviewdev -DomainAdmin root @@ -5227,7 +5227,7 @@ function Remove-HVFarm { Object(s) of the farm to be deleted. Object(s) should be of type FarmSummaryView/FarmInfo. .PARAMETER HvServer - Reference to Horizon View Server to query the data from. If the value is not passed or null then first element from global:DefaultHVServers would be considered inplace of hvServer. + Reference to Horizon View Server to query the data from. If the value is not passed or null then first element from global:DefaultHVServers would be considered in-place of hvServer. .EXAMPLE Remove-HVFarm -FarmName 'Farm-01' -HvServer $hvServer -Confirm:$false @@ -5504,7 +5504,7 @@ function Set-HVFarm { Path of the JSON specification file containing key/value pair. .PARAMETER HvServer - Reference to Horizon View Server to query the data from. If the value is not passed or null then first element from global:DefaultHVServers would be considered inplace of hvServer. + Reference to Horizon View Server to query the data from. If the value is not passed or null then first element from global:DefaultHVServers would be considered in-place of hvServer. .EXAMPLE Set-HVFarm -FarmName 'Farm-01' -Spec 'C:\Edit-HVFarm\ManualEditFarm.json' -Confirm:$false @@ -5949,7 +5949,7 @@ function Start-HVFarm { This property has a default value of 1. This property has values 1-100. .PARAMETER HvServer - Reference to Horizon View Server to query the data from. If the value is not passed or null then first element from global:DefaultHVServers would be considered inplace of hvServer. + Reference to Horizon View Server to query the data from. If the value is not passed or null then first element from global:DefaultHVServers would be considered in-place of hvServer. .EXAMPLE Start-HVFarm -Recompose -Farm 'Farm-01' -LogoffSetting FORCE_LOGOFF -ParentVM 'View-Agent-Win8' -SnapshotVM 'Snap_USB' -Confirm:$false @@ -6772,7 +6772,7 @@ function Get-HVMachine { .PARAMETER HvServer Reference to Horizon View Server to query the virtual machines from. If the value is not passed or null then - first element from global:DefaultHVServers would be considered inplace of hvServer + first element from global:DefaultHVServers would be considered in-place of hvServer .EXAMPLE Get-HVDesktop -PoolName 'ManualPool' @@ -6893,7 +6893,7 @@ function Get-HVMachineSummary { .PARAMETER HvServer Reference to Horizon View Server to query the virtual machines from. If the value is not passed or null then - first element from global:DefaultHVServers would be considered inplace of hvServer + first element from global:DefaultHVServers would be considered in-place of hvServer .EXAMPLE Get-HVDesktopSummary -PoolName 'ManualPool' @@ -6984,7 +6984,7 @@ function Get-HVPoolSpec { .PARAMETER HvServer Reference to Horizon View Server to query the virtual machines from. If the value is not passed or null then - first element from global:DefaultHVServers would be considered inplace of hvServer + first element from global:DefaultHVServers would be considered in-place of hvServer .EXAMPLE Get-HVPoolSpec -DesktopInfo $DesktopInfoObj @@ -7220,7 +7220,7 @@ function Get-HVInternalName { .PARAMETER HvServer Reference to Horizon View Server to query the virtual machines from. If the value is not passed or null then - first element from global:DefaultHVServers would be considered inplace of hvServer + first element from global:DefaultHVServers would be considered in-place of hvServer .EXAMPLE Get-HVInternalName -EntityId $entityId @@ -7354,13 +7354,13 @@ function New-HVEntitlement { This represents a simple association between a single user/group and a resource that they can be assigned. .PARAMETER User - User prinicipal name of user or group + User principal name of user or group .PARAMETER ResourceName - The resource(Application, Pool etc.) name + The resource(Application, Desktop etc.) name .PARAMETER Resource - Object(s) of the resource(Application, Desktop etc) to entitle + Object(s) of the resource(Application, Desktop etc.) to entitle .PARAMETER ResourceType Type of Resource(Application, Desktop etc) @@ -7370,7 +7370,7 @@ function New-HVEntitlement { .PARAMETER HvServer Reference to Horizon View Server. If the value is not passed or null then - first element from global:DefaultHVServers would be considered inplace of hvServer + first element from global:DefaultHVServers would be considered in-place of hvServer .EXAMPLE New-HVEntitlement -User 'administrator@adviewdev.eng.vmware.com' -ResourceName 'InsClnPol' -Confirm:$false @@ -7599,23 +7599,23 @@ function Get-HVEntitlement { Provides entitlement Info between a single user/group and a resource that they can be assigned. .PARAMETER User - User prinicipal name of user or group + User principal name of user or group .PARAMETER ResourceName The resource(Application, Pool etc.) name .PARAMETER Resource - Object(s) of the resource(Application, Desktop etc) to entitle + Object(s) of the resource(Application, Desktop etc.) to entitle .PARAMETER ResourceType - Type of Resource(Application, Desktop etc) + Type of Resource(Application, Desktop etc.) .PARAMETER Type Whether or not this is a group or a user. .PARAMETER HvServer Reference to Horizon View Server. If the value is not passed or null then - first element from global:DefaultHVServers would be considered inplace of hvServer + first element from global:DefaultHVServers would be considered in-place of hvServer .EXAMPLE Get-HVEntitlement -ResourceType Application @@ -7811,13 +7811,13 @@ function Remove-HVEntitlement { Removes entitlement between a single user/group and a resource that already been assigned. .PARAMETER User - User prinicipal name of user or group + User principal name of user or group .PARAMETER ResourceName - The resource(Application, Pool etc.) name + The resource(Application, Desktop etc.) name .PARAMETER Resource - Object(s) of the resource(Application, Desktop etc) to entitle + Object(s) of the resource(Application, Desktop etc.) to entitle .PARAMETER ResourceType Type of Resource(Application, Desktop etc) @@ -7827,7 +7827,7 @@ function Remove-HVEntitlement { .PARAMETER HvServer Reference to Horizon View Server. If the value is not passed or null then - first element from global:DefaultHVServers would be considered inplace of hvServer + first element from global:DefaultHVServers would be considered in-place of hvServer .EXAMPLE Remove-HVEntitlement -User 'administrator@adviewdev' -ResourceName LnkClnJSon -Confirm:$false @@ -8066,7 +8066,7 @@ PARAMETER Key .PARAMETER HvServer Reference to Horizon View Server to query the virtual machines from. If the value is not passed or null then - first element from global:DefaultHVServers would be considered inplace of hvServer + first element from global:DefaultHVServers would be considered in-place of hvServer .EXAMPLE Set-HVMachine -MachineName 'Agent_Praveen' -Maintenance ENTER_MAINTENANCE_MODE @@ -8242,7 +8242,7 @@ function New-HVGlobalEntitlement { .PARAMETER HvServer Reference to Horizon View Server. If the value is not passed or null then - first element from global:DefaultHVServers would be considered inplace of hvServer + first element from global:DefaultHVServers would be considered in-place of hvServer .EXAMPLE New-HVGlobalEntitlement -DisplayName 'GE_APP' -Type APPLICATION_ENTITLEMENT @@ -8473,7 +8473,7 @@ function Get-HVGlobalEntitlement { .PARAMETER HvServer Reference to Horizon View Server. If the value is not passed or null then - first element from global:DefaultHVServers would be considered inplace of hvServer + first element from global:DefaultHVServers would be considered in-place of hvServer .EXAMPLE Get-HVGlobalEntitlement -DisplayName 'GEAPP' From 60f4948ea7612f9e9a5e4188922298dce1601a5b Mon Sep 17 00:00:00 2001 From: praveenmathamsetty Date: Fri, 10 Feb 2017 16:06:49 +0530 Subject: [PATCH 022/112] Changing Information message Changing Information message in entitlements. --- Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 index 009cb47..0c8450d 100644 --- a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 +++ b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 @@ -7576,13 +7576,13 @@ function New-HVEntitlement { } $base = New-Object VMware.HV.UserEntitlementBase $base.UserOrGroup = $results.id + Write-host $ResourceObjs.Length " resource(s) will be entitled with UserOrGroup: " $User foreach ($ResourceObj in $ResourceObjs) { $base.Resource = $ResourceObj.id if (!$confirmFlag -OR $pscmdlet.ShouldProcess($User)) { $id = $services.UserEntitlement.UserEntitlement_Create($base) } } - Write-host $ResourceObjs.Length " resource(s) entitled with User or group: " $User } end { [System.gc]::collect() @@ -7916,10 +7916,10 @@ function Remove-HVEntitlement { if ($results) { foreach ($result in $Results) { $userEntitlements = $result.localData.desktopUserEntitlements + Write-Host $userEntitlements.Length " desktopUserEntitlement(s) will be removed for UserOrGroup " $user if (!$confirmFlag -OR $pscmdlet.ShouldProcess($User)) { $services.UserEntitlement.UserEntitlement_DeleteUserEntitlements($userEntitlements) } - Write-Host $userEntitlements.Length " desktopUserEntitlement(s) are removed for UserOrGroup " $user } } } @@ -7936,10 +7936,10 @@ function Remove-HVEntitlement { if ($results) { foreach ($result in $Results) { $userEntitlements = $result.localData.applicationUserEntitlements + Write-Host $userEntitlements.Length " applicationUserEntitlement(s) will be removed for UserOrGroup " $user if (!$confirmFlag -OR $pscmdlet.ShouldProcess($User)) { $services.UserEntitlement.UserEntitlement_DeleteUserEntitlements($userEntitlements) } - Write-Host $userEntitlements.Length " applicationUserEntitlement(s) are removed for UserOrGroup " $user } } } @@ -7966,16 +7966,17 @@ function Remove-HVEntitlement { foreach ($result in $Results) { if ($result.GetType().Name -eq 'EntitledUserOrGroupLocalSummaryView') { $userEntitlements = $result.localData.urlRedirectionUserEntitlements + Write-Host $userEntitlements.Length " urlRedirectionUserEntitlement(s) will be removed for UserOrGroup " $user if (!$confirmFlag -OR $pscmdlet.ShouldProcess($User)) { $services.UserEntitlement.UserEntitlement_DeleteUserEntitlements($userEntitlements) } } else { $userEntitlements = $result.globalData.urlRedirectionUserEntitlements + Write-Host $userEntitlements.Length " urlRedirectionUserEntitlement(s) will be removed for UserOrGroup " $user if (!$confirmFlag -OR $pscmdlet.ShouldProcess($User)) { $services.UserEntitlement.UserEntitlement_DeleteUserEntitlements($userEntitlements) } } - Write-Host $userEntitlements.Length " urlRedirectionUserEntitlement(s) are removed for UserOrGroup " $user } } } @@ -7996,10 +7997,10 @@ function Remove-HVEntitlement { if ($results) { foreach ($result in $Results) { $userEntitlements = $result.globalData.globalUserApplicationEntitlements + Write-Host $userEntitlements.Length " GlobalApplicationEntitlement(s) will be removed for UserOrGroup " $user if (!$confirmFlag -OR $pscmdlet.ShouldProcess($User)) { $services.UserEntitlement.UserEntitlement_DeleteUserEntitlements($userEntitlements) } - Write-Host $userEntitlements.Length " GlobalApplicationEntitlement(s) are removed for UserOrGroup " $user } } } @@ -8020,10 +8021,10 @@ function Remove-HVEntitlement { if ($results) { foreach ($result in $Results) { $userEntitlements = $result.globalData.globalUserEntitlements + Write-Host $userEntitlements.Length " GlobalEntitlement(s) will be removed for UserOrGroup " $user if (!$confirmFlag -OR $pscmdlet.ShouldProcess($User)) { $services.UserEntitlement.UserEntitlement_DeleteUserEntitlements($userEntitlements) } - Write-Host $userEntitlements.Length " GlobalEntitlement(s) are removed for UserOrGroup " $user } } From b885a9a39427635cc9872750932093868cb1d7a1 Mon Sep 17 00:00:00 2001 From: praveenmathamsetty Date: Wed, 15 Feb 2017 20:25:48 +0530 Subject: [PATCH 023/112] Updating Resource Name Get Help for AF HVEntitlement. Updating Resource Name for Get Help for Advanced function HVEntitlement. --- Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 index 0c8450d..7d405c1 100644 --- a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 +++ b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 @@ -7357,7 +7357,8 @@ function New-HVEntitlement { User principal name of user or group .PARAMETER ResourceName - The resource(Application, Desktop etc.) name + The resource(Application, Desktop etc.) name. + Supports only wildcard character '*' when resource type is desktop. .PARAMETER Resource Object(s) of the resource(Application, Desktop etc.) to entitle @@ -7602,7 +7603,8 @@ function Get-HVEntitlement { User principal name of user or group .PARAMETER ResourceName - The resource(Application, Pool etc.) name + The resource(Application, Desktop etc.) name. + Supports only wildcard character '*' when resource type is desktop. .PARAMETER Resource Object(s) of the resource(Application, Desktop etc.) to entitle @@ -7814,7 +7816,8 @@ function Remove-HVEntitlement { User principal name of user or group .PARAMETER ResourceName - The resource(Application, Desktop etc.) name + The resource(Application, Desktop etc.) name. + Supports only wildcard character '*' when resource type is desktop. .PARAMETER Resource Object(s) of the resource(Application, Desktop etc.) to entitle From f6aebe0c7b8f0f48e0c633c2e271856adb93523a Mon Sep 17 00:00:00 2001 From: praveenmathamsetty Date: Fri, 17 Feb 2017 18:07:18 +0530 Subject: [PATCH 024/112] MaintenanceSettings fix for farm MaintenanceSettings fix for instant clone farm --- Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 index 7d405c1..97ffe68 100644 --- a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 +++ b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 @@ -6163,6 +6163,8 @@ function Start-HVFarm { if ($startTime) { $spec.ScheduledTime = $StartTime } + $spec.LogoffSetting = $LogoffSetting + $spec.StopOnFirstError = $StopOnFirstError if ($MaintenanceMode -eq "RECURRING") { $spec.RecurringMaintenanceSettings = New-Object VMware.Hv.FarmRecurringMaintenanceSettings $spec.RecurringMaintenanceSettings.MaintenancePeriod = $MaintenancePeriod @@ -6185,8 +6187,6 @@ function Start-HVFarm { #image settings are specified if ($ParentVM -and $SnapshotVM) { $spec.ImageMaintenanceSettings = New-Object VMware.Hv.FarmImageMaintenanceSettings - $spec.ImageMaintenanceSettings.LogoffSetting = $LogoffSetting - $spec.ImageMaintenanceSettings.StopOnFirstError = $StopOnFirstError $vcId = Get-VcenterID -services $services -vCenter $Vcenter if ($null -eq $vcId) { Write-Error "VCenter is required if you specify ParentVM name." From 373579a2914be917bb7475aae321288fb582c238 Mon Sep 17 00:00:00 2001 From: praveenmathamsetty Date: Tue, 21 Feb 2017 00:22:19 +0530 Subject: [PATCH 025/112] Suppresstext info Suppress text info, when no result found with given search parameters. --- .../VMware.Hv.Helper/VMware.HV.Helper.psm1 | 113 +++++++++++++----- 1 file changed, 84 insertions(+), 29 deletions(-) diff --git a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 index 97ffe68..9383c24 100644 --- a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 +++ b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 @@ -423,7 +423,7 @@ function Add-HVRDSServer { process { $confirmFlag = Get-HVConfirmFlag -keys $PsBoundParameters.Keys try { - $farmSpecObj = Get-HVFarmSummary -farmName $farmName -hvServer $hvServer + $farmSpecObj = Get-HVFarmSummary -farmName $farmName -hvServer $hvServer -suppressInfo $true } catch { Write-Error "Make sure Get-HVFarmSummary advanced function is loaded, $_" break @@ -924,8 +924,8 @@ function Get-HVFarm { .PARAMETER Enabled search for farms which are enabled -.PARAMETER Full - Switch to get list of FarmSummaryView or FarmInfo objects in the result. If it is true a list of FarmInfo objects is returned ohterwise a list of FarmSummaryView objects is returned. +.PARAMETER SuppressInfo + Suppress text info, when no farm found with given search parameters .PARAMETER HvServer Reference to Horizon View Server to query the data from. If the value is not passed or null then first element from global:DefaultHVServers would be considered in-place of hvServer. @@ -987,6 +987,10 @@ function Get-HVFarm { [boolean] $Enabled, + [Parameter(Mandatory = $false)] + [boolean] + $SuppressInfo = $false, + [Parameter(Mandatory = $false)] $HvServer = $null ) @@ -998,8 +1002,10 @@ function Get-HVFarm { } $farmList = Find-HVFarm -Param $PSBoundParameters if (! $farmList) { - Write-Host "Get-HVFarm: No Farm Found with given search parameters" - break + if (! $SuppressInfo) { + Write-Host "Get-HVFarm: No Farm Found with given search parameters" + } + return $farmList } $farm_service_helper = New-Object VMware.Hv.FarmService $queryResults = @() @@ -1020,18 +1026,21 @@ function Get-HVFarmSummary { This function queries the specified Connection Server for farms which are configured on the server. If no farm is configured on the specified connection server or no farm matches the given search criteria, it will return null. .PARAMETER FarmName - farmName to be searched + FarmName to be searched .PARAMETER FarmDisplayName - farmDisplayName to be searched + FarmDisplayName to be searched .PARAMETER FarmType - farmType to be searched. It can take following values: + FarmType to be searched. It can take following values: "AUTOMATED" - search for automated farms only 'MANUAL' - search for manual farms only .PARAMETER Enabled - search for farms which are enabled + Search for farms which are enabled + +.PARAMETER SuppressInfo + Suppress text info, when no farm found with given search parameters .PARAMETER HvServer Reference to Horizon View Server to query the data from. If the value is not passed or null then first element from global:DefaultHVServers would be considered in-place of hvServer. @@ -1093,6 +1102,10 @@ function Get-HVFarmSummary { [boolean] $Enabled, + [Parameter(Mandatory = $false)] + [boolean] + $SuppressInfo = $false, + [Parameter(Mandatory = $false)] $HvServer = $null ) @@ -1102,7 +1115,10 @@ function Get-HVFarmSummary { Write-Error "Could not retrieve ViewApi services from connection object" break } - Return Find-HVFarm -Param $PSBoundParameters + $farmList = Find-HVFarm -Param $PSBoundParameters + if (!$farmList -and !$SuppressInfo) { + Write-Host "Get-HVFarmSummary: No Farm Found with given search parameters" + } } function Find-HVFarm { @@ -1213,6 +1229,9 @@ function Get-HVPool { If the value is true then only pools which are enabled would be returned. If the value is false then only pools which are disabled would be returned. +.PARAMETER SuppressInfo + Suppress text info, when no pool found with given search parameters + .PARAMETER HvServer Reference to Horizon View Server to query the pools from. If the value is not passed or null then first element from global:DefaultHVServers would be considered in-place of hvServer @@ -1279,6 +1298,10 @@ function Get-HVPool { [boolean] $ProvisioningEnabled, + [Parameter(Mandatory = $false)] + [boolean] + $SuppressInfo = $false, + [Parameter(Mandatory = $false)] $HvServer = $null ) @@ -1290,8 +1313,10 @@ function Get-HVPool { } $poolList = Find-HVPool -Param $PSBoundParameters if (! $poolList) { - Write-Host "Get-HVPool: No Pool Found with given search parameters" - break + if (! $SuppressInfo) { + Write-Host "Get-HVPool: No Pool Found with given search parameters" + } + return $poolList } $queryResults = @() $desktop_helper = New-Object VMware.Hv.DesktopService @@ -1343,6 +1368,9 @@ function Get-HVPoolSummary { If the value is true then only pools which are enabled would be returned. If the value is false then only pools which are disabled would be returned. +.PARAMETER SuppressInfo + Suppress text info, when no pool found with given search parameters + .PARAMETER HvServer Reference to Horizon View Server to query the pools from. If the value is not passed or null then first element from global:DefaultHVServers would be considered in-place of hvServer @@ -1409,6 +1437,10 @@ function Get-HVPoolSummary { [boolean] $ProvisioningEnabled, + [Parameter(Mandatory = $false)] + [boolean] + $SuppressInfo = $false, + [Parameter(Mandatory = $false)] $HvServer = $null ) @@ -1418,7 +1450,11 @@ function Get-HVPoolSummary { Write-Error "Could not retrieve ViewApi services from connection object" break } - Return Find-HVPool -Param $psboundparameters + $pool_list = Find-HVPool -Param $psboundparameters + if (!$pool_list -and !$suppressInfo) { + Write-Host "Get-HVPoolSummary: No Pool Found with given search parameters" + } + Return $pool_list } function Find-HVPool { @@ -2343,7 +2379,7 @@ function New-HVFarm { $confirmFlag = Get-HVConfirmFlag -keys $PsBoundParameters.Keys if ($farmName) { try { - $sourceFarm = Get-HVFarmSummary -farmName $farmName -hvServer $hvServer + $sourceFarm = Get-HVFarmSummary -farmName $farmName -hvServer $hvServer -suppressInfo $true } catch { Write-Error "Make sure Get-HVFarmSummary advanced function is loaded, $_" break @@ -4073,7 +4109,7 @@ function New-HVPool { $confirmFlag = Get-HVConfirmFlag -keys $PsBoundParameters.Keys if ($poolName) { try { - $sourcePool = Get-HVPoolSummary -poolName $poolName -hvServer $hvServer + $sourcePool = Get-HVPoolSummary -poolName $poolName -suppressInfo $true -hvServer $hvServer } catch { Write-Error "Make sure Get-HVPoolSummary advanced function is loaded, $_" break @@ -5285,7 +5321,7 @@ function Remove-HVFarm { $farmList = @() if ($farmName) { try { - $farmSpecObj = Get-HVFarm -farmName $farmName -hvServer $hvServer + $farmSpecObj = Get-HVFarm -farmName $farmName -hvServer $hvServer -SuppressInfo $true } catch { Write-Error "Make sure Get-HVFarm advanced function is loaded, $_" break @@ -5408,7 +5444,7 @@ function Remove-HVPool { $poolList = @() if ($poolName) { try { - $myPools = Get-HVPoolSummary -poolName $poolName -hvServer $hvServer + $myPools = Get-HVPoolSummary -poolName $poolName -suppressInfo $true -hvServer $hvServer } catch { Write-Error "Make sure Get-HVPoolSummary advanced function is loaded, $_" break @@ -5589,7 +5625,7 @@ function Set-HVFarm { $farmList = @{} if ($farmName) { try { - $farmSpecObj = Get-HVFarmSummary -farmName $farmName -hvServer $hvServer + $farmSpecObj = Get-HVFarmSummary -farmName $farmName -hvServer $hvServer -suppressInfo $true } catch { Write-Error "Make sure Get-HVFarmSummary advanced function is loaded, $_" break @@ -5798,7 +5834,7 @@ function Set-HVPool { $poolList = @{} if ($poolName) { try { - $desktopPools = Get-HVPoolSummary -poolName $poolName -hvServer $hvServer + $desktopPools = Get-HVPoolSummary -poolName $poolName -suppressInfo $true -hvServer $hvServer } catch { Write-Error "Make sure Get-HVPoolSummary advanced function is loaded, $_" break @@ -5811,6 +5847,9 @@ function Set-HVPool { } $poolList.add($desktopObj.id, $desktopObj.DesktopSummaryData.Name) } + } else { + Write-Error "No desktopsummarydata found with pool name: [$poolName]" + break } } elseif ($PSCmdlet.MyInvocation.ExpectingInput -or $Pool) { foreach ($item in $pool) { @@ -6085,7 +6124,7 @@ function Start-HVFarm { } elseif ($farm.GetType().name -eq 'String') { try { - $farmSpecObj = Get-HVFarm -farmName $farm -hvServer $hvServer + $farmSpecObj = Get-HVFarm -farmName $farm -hvServer $hvServer -SuppressInfo $true } catch { Write-Error "Make sure Get-HVFarm advanced function is loaded, $_" break @@ -6458,7 +6497,7 @@ function Start-HVPool { $type = $item.desktopsummarydata.type } elseif ($item.GetType().name -eq 'String') { try { - $poolObj = Get-HVPoolSummary -poolName $item -hvServer $hvServer + $poolObj = Get-HVPoolSummary -poolName $item -suppressInfo $true -hvServer $hvServer } catch { Write-Error "Make sure Get-HVPoolSummary advanced function is loaded, $_" break @@ -6661,7 +6700,7 @@ function Find-HVMachine { try { if ($params['PoolName']) { - $poolObj = Get-HVPoolSummary -poolName $params['PoolName'] -hvServer $params['HvServer'] + $poolObj = Get-HVPoolSummary -poolName $params['PoolName'] -suppressInfo $true -hvServer $params['HvServer'] if ($poolObj.Length -ne 1) { Write-Host "Failed to retrieve specific pool object with given PoolName : " $params['PoolName'] break; @@ -6891,6 +6930,9 @@ function Get-HVMachineSummary { If the value is null or not provided then filter will not be applied, otherwise the virtual machines which has display name same as value will be returned. +.PARAMETER SuppressInfo + Suppress text info, when no machine found with given search parameters + .PARAMETER HvServer Reference to Horizon View Server to query the virtual machines from. If the value is not passed or null then first element from global:DefaultHVServers would be considered in-place of hvServer @@ -6957,6 +6999,10 @@ function Get-HVMachineSummary { [string] $JsonFilePath, + [Parameter(Mandatory = $false)] + [boolean] + $SuppressInfo = $false, + [Parameter(Mandatory = $false)] $HvServer = $null ) @@ -6968,6 +7014,9 @@ function Get-HVMachineSummary { } $machineList = Find-HVMachine -Param $PSBoundParameters + if (!$machineList -and !$SuppressInfo) { + Write-Host "Get-HVMachineSummary: No machine(s) found with given search parameters" + } return $machineList } @@ -7467,7 +7516,7 @@ function New-HVEntitlement { switch($ResourceType){ "Desktop" { if ($ResourceName) { - $ResourceObjs = Get-HVPool -PoolName $ResourceName + $ResourceObjs = Get-HVPool -PoolName $ResourceName -suppressInfo $true -HvServer $HvServer if (! $ResourceObjs) { Write-Host "No pool found with given resourceName: " $ResourceName return @@ -7704,7 +7753,7 @@ function Get-HVEntitlement { switch($ResourceType) { "Desktop" { if ($ResourceName) { - $ResourceObjs = Get-HVPool -PoolName $ResourceName -HvServer $HvServer + $ResourceObjs = Get-HVPool -PoolName $ResourceName -suppressInfo $true -HvServer $HvServer if (! $ResourceObjs) { Write-Host "No pool found with given resourceName: " $ResourceName return @@ -7908,7 +7957,7 @@ function Remove-HVEntitlement { $info = $services.PodFederation.PodFederation_get() switch($ResourceType) { "Desktop" { - $ResourceObjs = Get-HVPool -PoolName $ResourceName -HvServer $HvServer + $ResourceObjs = Get-HVPool -PoolName $ResourceName -suppressInfo $true -HvServer $HvServer if (! $ResourceObjs) { Write-Host "No pool found with given resourceName: " $ResourceName return @@ -8140,7 +8189,7 @@ PARAMETER Key $machineList = @{} if ($machineName) { try { - $machines = Get-HVMachineSummary -MachineName $machineName -hvServer $hvServer + $machines = Get-HVMachineSummary -MachineName $machineName -suppressInfo $true -hvServer $hvServer } catch { Write-Error "Make sure Get-HVMachineSummary advanced function is loaded, $_" break @@ -8475,6 +8524,9 @@ function Get-HVGlobalEntitlement { .PARAMETER Description Description of Global Entitlement. +.PARAMETER SuppressInfo + Suppress text info, when no global entitlement(s) found with given search parameters + .PARAMETER HvServer Reference to Horizon View Server. If the value is not passed or null then first element from global:DefaultHVServers would be considered in-place of hvServer @@ -8510,6 +8562,10 @@ function Get-HVGlobalEntitlement { [String] $Description, + [Parameter(Mandatory = $false)] + [boolean] + $SuppressInfo = $false, + [Parameter(Mandatory = $false)] $HvServer = $null ) @@ -8529,9 +8585,8 @@ function Get-HVGlobalEntitlement { $result = @() $result += Find-HVGlobalEntitlement -Param $psboundparameters -Type 'GlobalEntitlementSummaryView' $result += Find-HVGlobalEntitlement -Param $psboundparameters -Type 'GlobalApplicationEntitlementInfo' - if (! $result) { + if (!$result -and !$SuppressInfo) { Write-Host "Get-HVGlobalEntitlement: No global entitlement Found with given search parameters" - break } return $result } @@ -8611,7 +8666,7 @@ function Remove-HVGlobalEntitlement { $GeList = @() if ($DisplayName) { try { - $GeList = Get-HVGlobalEntitlement -DisplayName $DisplayName -hvServer $hvServer + $GeList = Get-HVGlobalEntitlement -DisplayName $DisplayName -suppressInfo $true -hvServer $hvServer } catch { Write-Error "Make sure Get-HVGlobalEntitlement advanced function is loaded, $_" break From 10b540c8c843d3fe4cad8a33a360725cecc4f293 Mon Sep 17 00:00:00 2001 From: praveenmathamsetty Date: Tue, 21 Feb 2017 00:27:38 +0530 Subject: [PATCH 026/112] suppress text Info bug fix suppress text Info bug fix --- Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 | 1 + 1 file changed, 1 insertion(+) diff --git a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 index 9383c24..2756357 100644 --- a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 +++ b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 @@ -1119,6 +1119,7 @@ function Get-HVFarmSummary { if (!$farmList -and !$SuppressInfo) { Write-Host "Get-HVFarmSummary: No Farm Found with given search parameters" } + Return $farmList } function Find-HVFarm { From 94f8f19d840c7b621a2bf41af8a6cdf149b92fdb Mon Sep 17 00:00:00 2001 From: praveenmathamsetty Date: Wed, 22 Feb 2017 18:48:24 +0530 Subject: [PATCH 027/112] Adding prefix to output message Adding prefix to output message in AF Get-HVMachine --- Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 index 2756357..a4a46d4 100644 --- a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 +++ b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 @@ -6888,7 +6888,7 @@ function Get-HVMachine { $machineList = Find-HVMachine -Param $PSBoundParameters if (!$machineList) { - Write-Host "No Virtual Machine(s) Found with given search parameters" + Write-Host "Get-HVMachine: No Virtual Machine(s) Found with given search parameters" break } $queryResults = @() From a8dbe6392916ba3563de75bb2792d5e39908eec1 Mon Sep 17 00:00:00 2001 From: praveenmathamsetty Date: Fri, 31 Mar 2017 17:00:57 +0530 Subject: [PATCH 028/112] Update VMware.HV.Helper.psm1 Updated AF Version and Horizon build no. --- .../VMware.Hv.Helper/VMware.HV.Helper.psm1 | 108 +++++++++--------- 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 index a4a46d4..723b781 100644 --- a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 +++ b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 @@ -1,7 +1,7 @@ #Script Module : VMware.Hv.Helper -#Version : 1.0 +#Version : 1.1 -#Copyright © 2016 VMware, Inc. All Rights Reserved. +#Copyright © 2016 VMware, Inc. All Rights Reserved. #Permission is hereby granted, free of charge, to any person obtaining a copy of #this software and associated documentation files (the "Software"), to deal in @@ -204,11 +204,11 @@ The Add-HVDesktop adds virtual machines to already exiting pools by using view A .NOTES Author : Praveen Mathamsetty. Author email : pmathamsetty@vmware.com - Version : 1.0 + Version : 1.1 Dependencies : Make sure pool already exists before adding VMs to it. ===Tested Against Environment==== - Horizon View Server Version : 7.0.2 + Horizon View Server Version : 7.0.2, 7.1.0 PowerCLI Version : PowerCLI 6.5 PowerShell Version : 5.0 @@ -386,13 +386,13 @@ function Add-HVRDSServer { None .NOTES - Author : Ankit Gupta. + Author : Ankit Gupta, Praveen Mathamsetty Author email : guptaa@vmware.com - Version : 1.0 + Version : 1.1 Dependencies : Make sure farm already exists before adding RDSServers to it. ===Tested Against Environment==== - Horizon View Server Version : 7.0.2 + Horizon View Server Version : 7.0.2, 7.1.0 PowerCLI Version : PowerCLI 6.5 PowerShell Version : 5.0 #> @@ -505,10 +505,10 @@ function Connect-HVEvent { .NOTES Author : Paramesh Oddepally. Author email : poddepally@vmware.com - Version : 1.0 + Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2 + Horizon View Server Version : 7.0.2, 7.1.0 PowerCLI Version : PowerCLI 6.5 PowerShell Version : 5.0 #> @@ -623,10 +623,10 @@ function Disconnect-HVEvent { .NOTES Author : Paramesh Oddepally. Author email : poddepally@vmware.com - Version : 1.0 + Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2 + Horizon View Server Version : 7.0.2, 7.1.0 PowerCLI Version : PowerCLI 6.5 PowerShell Version : 5.0 #> @@ -720,10 +720,10 @@ function Get-HVEvent { .NOTES Author : Paramesh Oddepally. Author email : poddepally@vmware.com - Version : 1.0 + Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2 + Horizon View Server Version : 7.0.2, 7.1.0 PowerCLI Version : PowerCLI 6.5 PowerShell Version : 5.0 #> @@ -954,12 +954,12 @@ function Get-HVFarm { Returns the list of FarmInfo object matching the query criteria. .NOTES - Author : Ankit Gupta. + Author : Ankit Gupta, Praveen Mathamsetty Author email : guptaa@vmware.com - Version : 1.0 + Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2 + Horizon View Server Version : 7.0.2, 7.1.0 PowerCLI Version : PowerCLI 6.5 PowerShell Version : 5.0 #> @@ -1071,10 +1071,10 @@ function Get-HVFarmSummary { .NOTES Author : Praveen Mathamsetty. Author email : pmathamsetty@vmware.com - Version : 1.0 + Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2 + Horizon View Server Version : 7.0.2, 7.1.0 PowerCLI Version : PowerCLI 6.5 PowerShell Version : 5.0 #> @@ -1259,10 +1259,10 @@ function Get-HVPool { .NOTES Author : Praveen Mathamsetty. Author email : pmathamsetty@vmware.com - Version : 1.0 + Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2 + Horizon View Server Version : 7.0.2, 7.1.0 PowerCLI Version : PowerCLI 6.5 PowerShell Version : 5.0 #> @@ -1398,10 +1398,10 @@ function Get-HVPoolSummary { .NOTES Author : Praveen Mathamsetty. Author email : pmathamsetty@vmware.com - Version : 1.0 + Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2 + Horizon View Server Version : 7.0.2, 7.1.0 PowerCLI Version : PowerCLI 6.5 PowerShell Version : 5.0 #> @@ -1614,10 +1614,10 @@ function Get-HVQueryFilter { .NOTES Author : Kummara Ramamohan. Author email : kramamohan@vmware.com - Version : 1.0 + Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2 + Horizon View Server Version : 7.0.2, 7.1.0 PowerCLI Version : PowerCLI 6.5 PowerShell Version : 5.0 #> @@ -1758,10 +1758,10 @@ function Get-HVQueryResult { .NOTES Author : Kummara Ramamohan. Author email : kramamohan@vmware.com - Version : 1.0 + Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2 + Horizon View Server Version : 7.0.2, 7.1.0 PowerCLI Version : PowerCLI 6.5 PowerShell Version : 5.0 #> @@ -2006,12 +2006,12 @@ function New-HVFarm { None .NOTES - Author : Ankit Gupta. + Author : Ankit Gupta, Praveen Mathamsetty Author email : guptaa@vmware.com - Version : 1.0 + Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2 + Horizon View Server Version : 7.0.2, 7.1.0 PowerCLI Version : PowerCLI 6.5 PowerShell Version : 5.0 #> @@ -3468,10 +3468,10 @@ function New-HVPool { .NOTES Author : Praveen Mathamsetty. Author email : pmathamsetty@vmware.com - Version : 1.0 + Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2 + Horizon View Server Version : 7.0.2, 7.1.0 PowerCLI Version : PowerCLI 6.5 PowerShell Version : 5.0 #> @@ -5283,12 +5283,12 @@ function Remove-HVFarm { None .NOTES - Author : Ankit Gupta. + Author : Ankit Gupta, Praveen Mathamsetty Author email : guptaa@vmware.com - Version : 1.0 + Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2 + Horizon View Server Version : 7.0.2, 7.1.0 PowerCLI Version : PowerCLI 6.5 PowerShell Version : 5.0 #> @@ -5405,7 +5405,7 @@ function Remove-HVPool { Version : 1.0 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2 + Horizon View Server Version : 7.0.2, 7.1.0 PowerCLI Version : PowerCLI 6.5 PowerShell Version : 5.0 #> @@ -5567,12 +5567,12 @@ function Set-HVFarm { None .NOTES - Author : Ankit Gupta. + Author : Ankit Gupta, Praveen Mathamsetty Author email : guptaa@vmware.com - Version : 1.0 + Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2 + Horizon View Server Version : 7.0.2, 7.1.0 PowerCLI Version : PowerCLI 6.5 PowerShell Version : 5.0 #> @@ -5776,10 +5776,10 @@ function Set-HVPool { .NOTES Author : Praveen Mathamsetty. Author email : pmathamsetty@vmware.com - Version : 1.0 + Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2 + Horizon View Server Version : 7.0.2, 7.1.0 PowerCLI Version : PowerCLI 6.5 PowerShell Version : 5.0 #> @@ -6016,12 +6016,12 @@ function Start-HVFarm { None .NOTES - Author : Ankit Gupta. + Author : Ankit Gupta, Praveen Mathamsetty Author email : guptaa@vmware.com - Version : 1.0 + Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2 + Horizon View Server Version : 7.0.2, 7.1.0 PowerCLI Version : PowerCLI 6.5 PowerShell Version : 5.0 #> @@ -6398,10 +6398,10 @@ function Start-HVPool { .NOTES Author : Praveen Mathamsetty. Author email : pmathamsetty@vmware.com - Version : 1.0 + Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2 + Horizon View Server Version : 7.0.2, 7.1.0 PowerCLI Version : PowerCLI 6.5 PowerShell Version : 5.0 #> @@ -6839,7 +6839,7 @@ function Get-HVMachine { Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2, 7.0.3 + Horizon View Server Version : 7.0.2, 7.1.0 PowerCLI Version : PowerCLI 6.5 PowerShell Version : 5.0 #> @@ -7053,7 +7053,7 @@ function Get-HVPoolSpec { Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2, 7.0.3 + Horizon View Server Version : 7.0.2, 7.1.0 PowerCLI Version : PowerCLI 6.5 PowerShell Version : 5.0 #> @@ -7285,7 +7285,7 @@ function Get-HVInternalName { Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2, 7.0.3 + Horizon View Server Version : 7.0.2, 7.1.0 PowerCLI Version : PowerCLI 6.5 PowerShell Version : 5.0 #> @@ -7454,7 +7454,7 @@ function New-HVEntitlement { Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2, 7.0.3 + Horizon View Server Version : 7.0.2, 7.1.0 PowerCLI Version : PowerCLI 6.5 PowerShell Version : 5.0 #> @@ -7691,7 +7691,7 @@ function Get-HVEntitlement { Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2, 7.0.3 + Horizon View Server Version : 7.0.2, 7.1.0 PowerCLI Version : PowerCLI 6.5 PowerShell Version : 5.0 #> @@ -7900,7 +7900,7 @@ function Remove-HVEntitlement { Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2, 7.0.3 + Horizon View Server Version : 7.0.2, 7.1.0 PowerCLI Version : PowerCLI 6.5 PowerShell Version : 5.0 #> @@ -8143,7 +8143,7 @@ PARAMETER Key Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2, 7.0.3 + Horizon View Server Version : 7.0.2, 7.1.0 PowerCLI Version : PowerCLI 6.5 PowerShell Version : 5.0 #> @@ -8313,7 +8313,7 @@ function New-HVGlobalEntitlement { Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2, 7.0.3 + Horizon View Server Version : 7.0.2, 7.1.0 PowerCLI Version : PowerCLI 6.5 PowerShell Version : 5.0 #> @@ -8543,7 +8543,7 @@ function Get-HVGlobalEntitlement { Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2, 7.0.3 + Horizon View Server Version : 7.0.2, 7.1.0 PowerCLI Version : PowerCLI 6.5 PowerShell Version : 5.0 #> From 720595989a9af1fc2aed1cff6c46923483a5044b Mon Sep 17 00:00:00 2001 From: praveenmathamsetty Date: Fri, 21 Apr 2017 20:58:12 +0530 Subject: [PATCH 029/112] Add readme file Add readme file, versions for "VMware.Hv.Helper" module. --- Modules/VMware.Hv.Helper/README.md | 20 ++ .../VMware.Hv.Helper/VMware.HV.Helper.psm1 | 198 +++++++++--------- 2 files changed, 119 insertions(+), 99 deletions(-) create mode 100644 Modules/VMware.Hv.Helper/README.md diff --git a/Modules/VMware.Hv.Helper/README.md b/Modules/VMware.Hv.Helper/README.md new file mode 100644 index 0000000..ae6e144 --- /dev/null +++ b/Modules/VMware.Hv.Helper/README.md @@ -0,0 +1,20 @@ +Prerequisites/Steps to use this module: + +1. This module only works for Horizon product E.g. Horizon 7.0.2 and later. +2. Install the latest version of Powershell, PowerCLI(6.5) or (later version via psgallary). +3. Import HorizonView module by running: Import-Module VMware.VimAutomation.HorizonView. +4. Import "VMware.Hv.Helper" module by running: Import-Module -Name "location of this module" or Get-Module -ListAvailable 'VMware.Hv.Helper' | Import-Module. +5. Get-Command -Module "This module Name" to list all available functions or Get-Command -Module 'VMware.Hv.Helper'. + +# Example script to connect view API service of Connection Server: + +Import-Module VMware.VimAutomation.HorizonView +# Connection to view API service +$hvServer = Connect-HVServer -server +$hvServices = $hvserver.ExtensionData +$csList = $hvServices.ConnectionServer.ConnectionServer_List() +# Load this module +Get-Module -ListAvailable 'VMware.Hv.Helper' | Import-Module +Get-Command -Module 'VMware.Hv.Helper' +# Use advanced functions of this module +New-HVPool -spec 'path to InstantClone.json file' diff --git a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 index a4a46d4..2e4118f 100644 --- a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 +++ b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 @@ -204,12 +204,12 @@ The Add-HVDesktop adds virtual machines to already exiting pools by using view A .NOTES Author : Praveen Mathamsetty. Author email : pmathamsetty@vmware.com - Version : 1.0 + Version : 1.1 Dependencies : Make sure pool already exists before adding VMs to it. ===Tested Against Environment==== - Horizon View Server Version : 7.0.2 - PowerCLI Version : PowerCLI 6.5 + Horizon View Server Version : 7.0.2, 7.1.0 + PowerCLI Version : PowerCLI 6.5, PowerCLI 6.5.1 PowerShell Version : 5.0 #> @@ -386,14 +386,14 @@ function Add-HVRDSServer { None .NOTES - Author : Ankit Gupta. - Author email : guptaa@vmware.com - Version : 1.0 + Author : praveen mathamsetty. + Author email : pmathamsetty@vmware.com + Version : 1.1 Dependencies : Make sure farm already exists before adding RDSServers to it. ===Tested Against Environment==== - Horizon View Server Version : 7.0.2 - PowerCLI Version : PowerCLI 6.5 + Horizon View Server Version : 7.0.2, 7.1.0 + PowerCLI Version : PowerCLI 6.5, PowerCLI 6.5.1 PowerShell Version : 5.0 #> [CmdletBinding( @@ -505,11 +505,11 @@ function Connect-HVEvent { .NOTES Author : Paramesh Oddepally. Author email : poddepally@vmware.com - Version : 1.0 + Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2 - PowerCLI Version : PowerCLI 6.5 + Horizon View Server Version : 7.0.2, 7.1.0 + PowerCLI Version : PowerCLI 6.5, PowerCLI 6.5.1 PowerShell Version : 5.0 #> [CmdletBinding()] @@ -623,11 +623,11 @@ function Disconnect-HVEvent { .NOTES Author : Paramesh Oddepally. Author email : poddepally@vmware.com - Version : 1.0 + Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2 - PowerCLI Version : PowerCLI 6.5 + Horizon View Server Version : 7.0.2, 7.1.0 + PowerCLI Version : PowerCLI 6.5, PowerCLI 6.5.1 PowerShell Version : 5.0 #> @@ -720,11 +720,11 @@ function Get-HVEvent { .NOTES Author : Paramesh Oddepally. Author email : poddepally@vmware.com - Version : 1.0 + Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2 - PowerCLI Version : PowerCLI 6.5 + Horizon View Server Version : 7.0.2, 7.1.0 + PowerCLI Version : PowerCLI 6.5, PowerCLI 6.5.1 PowerShell Version : 5.0 #> @@ -954,13 +954,13 @@ function Get-HVFarm { Returns the list of FarmInfo object matching the query criteria. .NOTES - Author : Ankit Gupta. - Author email : guptaa@vmware.com - Version : 1.0 + Author : praveen mathamsetty. + Author email : pmathamsetty@vmware.com + Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2 - PowerCLI Version : PowerCLI 6.5 + Horizon View Server Version : 7.0.2, 7.1.0 + PowerCLI Version : PowerCLI 6.5, PowerCLI 6.5.1 PowerShell Version : 5.0 #> @@ -1071,11 +1071,11 @@ function Get-HVFarmSummary { .NOTES Author : Praveen Mathamsetty. Author email : pmathamsetty@vmware.com - Version : 1.0 + Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2 - PowerCLI Version : PowerCLI 6.5 + Horizon View Server Version : 7.0.2, 7.1.0 + PowerCLI Version : PowerCLI 6.5, PowerCLI 6.5.1 PowerShell Version : 5.0 #> @@ -1259,11 +1259,11 @@ function Get-HVPool { .NOTES Author : Praveen Mathamsetty. Author email : pmathamsetty@vmware.com - Version : 1.0 + Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2 - PowerCLI Version : PowerCLI 6.5 + Horizon View Server Version : 7.0.2, 7.1.0 + PowerCLI Version : PowerCLI 6.5, PowerCLI 6.5.1 PowerShell Version : 5.0 #> @@ -1398,11 +1398,11 @@ function Get-HVPoolSummary { .NOTES Author : Praveen Mathamsetty. Author email : pmathamsetty@vmware.com - Version : 1.0 + Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2 - PowerCLI Version : PowerCLI 6.5 + Horizon View Server Version : 7.0.2, 7.1.0 + PowerCLI Version : PowerCLI 6.5, PowerCLI 6.5.1 PowerShell Version : 5.0 #> @@ -1614,11 +1614,11 @@ function Get-HVQueryFilter { .NOTES Author : Kummara Ramamohan. Author email : kramamohan@vmware.com - Version : 1.0 + Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2 - PowerCLI Version : PowerCLI 6.5 + Horizon View Server Version : 7.0.2, 7.1.0 + PowerCLI Version : PowerCLI 6.5, PowerCLI 6.5.1 PowerShell Version : 5.0 #> [CmdletBinding()] @@ -1758,11 +1758,11 @@ function Get-HVQueryResult { .NOTES Author : Kummara Ramamohan. Author email : kramamohan@vmware.com - Version : 1.0 + Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2 - PowerCLI Version : PowerCLI 6.5 + Horizon View Server Version : 7.0.2, 7.1.0 + PowerCLI Version : PowerCLI 6.5, PowerCLI 6.5.1 PowerShell Version : 5.0 #> @@ -2006,13 +2006,13 @@ function New-HVFarm { None .NOTES - Author : Ankit Gupta. - Author email : guptaa@vmware.com - Version : 1.0 + Author : praveen mathamsetty. + Author email : pmathamsetty@vmware.com + Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2 - PowerCLI Version : PowerCLI 6.5 + Horizon View Server Version : 7.0.2, 7.1.0 + PowerCLI Version : PowerCLI 6.5, PowerCLI 6.5.1 PowerShell Version : 5.0 #> @@ -3468,11 +3468,11 @@ function New-HVPool { .NOTES Author : Praveen Mathamsetty. Author email : pmathamsetty@vmware.com - Version : 1.0 + Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2 - PowerCLI Version : PowerCLI 6.5 + Horizon View Server Version : 7.0.2, 7.1.0 + PowerCLI Version : PowerCLI 6.5, PowerCLI 6.5.1 PowerShell Version : 5.0 #> @@ -5283,13 +5283,13 @@ function Remove-HVFarm { None .NOTES - Author : Ankit Gupta. - Author email : guptaa@vmware.com - Version : 1.0 + Author : praveen mathamsetty. + Author email : pmathamsetty@vmware.com + Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2 - PowerCLI Version : PowerCLI 6.5 + Horizon View Server Version : 7.0.2, 7.1.0 + PowerCLI Version : PowerCLI 6.5, PowerCLI 6.5.1 PowerShell Version : 5.0 #> @@ -5402,11 +5402,11 @@ function Remove-HVPool { .NOTES Author : Praveen Mathamsetty. Author email : pmathamsetty@vmware.com - Version : 1.0 + Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2 - PowerCLI Version : PowerCLI 6.5 + Horizon View Server Version : 7.0.2, 7.1.0 + PowerCLI Version : PowerCLI 6.5, PowerCLI 6.5.1 PowerShell Version : 5.0 #> @@ -5567,13 +5567,13 @@ function Set-HVFarm { None .NOTES - Author : Ankit Gupta. - Author email : guptaa@vmware.com - Version : 1.0 + Author : praveen mathamsetty. + Author email : pmathamsetty@vmware.com + Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2 - PowerCLI Version : PowerCLI 6.5 + Horizon View Server Version : 7.0.2, 7.1.0 + PowerCLI Version : PowerCLI 6.5, PowerCLI 6.5.1 PowerShell Version : 5.0 #> @@ -5776,11 +5776,11 @@ function Set-HVPool { .NOTES Author : Praveen Mathamsetty. Author email : pmathamsetty@vmware.com - Version : 1.0 + Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2 - PowerCLI Version : PowerCLI 6.5 + Horizon View Server Version : 7.0.2, 7.1.0 + PowerCLI Version : PowerCLI 6.5, PowerCLI 6.5.1 PowerShell Version : 5.0 #> @@ -6016,13 +6016,13 @@ function Start-HVFarm { None .NOTES - Author : Ankit Gupta. - Author email : guptaa@vmware.com - Version : 1.0 + Author : praveen mathamsetty. + Author email : pmathamsetty@vmware.com + Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2 - PowerCLI Version : PowerCLI 6.5 + Horizon View Server Version : 7.0.2, 7.1.0 + PowerCLI Version : PowerCLI 6.5, PowerCLI 6.5.1 PowerShell Version : 5.0 #> @@ -6398,11 +6398,11 @@ function Start-HVPool { .NOTES Author : Praveen Mathamsetty. Author email : pmathamsetty@vmware.com - Version : 1.0 + Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2 - PowerCLI Version : PowerCLI 6.5 + Horizon View Server Version : 7.0.2, 7.1.0 + PowerCLI Version : PowerCLI 6.5, PowerCLI 6.5.1 PowerShell Version : 5.0 #> @@ -6815,19 +6815,19 @@ function Get-HVMachine { first element from global:DefaultHVServers would be considered in-place of hvServer .EXAMPLE - Get-HVDesktop -PoolName 'ManualPool' + Get-HVMachine -PoolName 'ManualPool' Queries VM(s) with given parameter poolName .EXAMPLE - Get-HVDesktop -MachineName 'PowerCLIVM' + Get-HVMachine -MachineName 'PowerCLIVM' Queries VM(s) with given parameter machineName .EXAMPLE - Get-HVDesktop -State CUSTOMIZING + Get-HVMachine -State CUSTOMIZING Queries VM(s) with given parameter vm state .EXAMPLE - Get-HVDesktop -DnsName 'powercli-*' + Get-HVMachine -DnsName 'powercli-*' Queries VM(s) with given parameter dnsName with wildcard character * .OUTPUTS @@ -6839,8 +6839,8 @@ function Get-HVMachine { Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2, 7.0.3 - PowerCLI Version : PowerCLI 6.5 + Horizon View Server Version : 7.0.2, 7.1.0 + PowerCLI Version : PowerCLI 6.5, PowerCLI 6.5.1 PowerShell Version : 5.0 #> @@ -6939,19 +6939,19 @@ function Get-HVMachineSummary { first element from global:DefaultHVServers would be considered in-place of hvServer .EXAMPLE - Get-HVDesktopSummary -PoolName 'ManualPool' + Get-HVMachineSummary -PoolName 'ManualPool' Queries VM(s) with given parameter poolName .EXAMPLE - Get-HVDesktopSummary -MachineName 'PowerCLIVM' + Get-HVMachineSummary -MachineName 'PowerCLIVM' Queries VM(s) with given parameter machineName .EXAMPLE - Get-HVDesktopSummary -State CUSTOMIZING + Get-HVMachineSummary -State CUSTOMIZING Queries VM(s) with given parameter vm state .EXAMPLE - Get-HVDesktopSummary -DnsName 'powercli-*' + Get-HVMachineSummary -DnsName 'powercli-*' Queries VM(s) with given parameter dnsName with wildcard character * .OUTPUTS @@ -6963,8 +6963,8 @@ function Get-HVMachineSummary { Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2, 7.0.3 - PowerCLI Version : PowerCLI 6.5 + Horizon View Server Version : 7.0.2, 7.1.0 + PowerCLI Version : PowerCLI 6.5, PowerCLI 6.5.1 PowerShell Version : 5.0 #> @@ -7053,8 +7053,8 @@ function Get-HVPoolSpec { Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2, 7.0.3 - PowerCLI Version : PowerCLI 6.5 + Horizon View Server Version : 7.0.2, 7.1.0 + PowerCLI Version : PowerCLI 6.5, PowerCLI 6.5.1 PowerShell Version : 5.0 #> [CmdletBinding( @@ -7285,8 +7285,8 @@ function Get-HVInternalName { Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2, 7.0.3 - PowerCLI Version : PowerCLI 6.5 + Horizon View Server Version : 7.0.2, 7.1.0 + PowerCLI Version : PowerCLI 6.5, PowerCLI 6.5.1 PowerShell Version : 5.0 #> [CmdletBinding( @@ -7454,8 +7454,8 @@ function New-HVEntitlement { Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2, 7.0.3 - PowerCLI Version : PowerCLI 6.5 + Horizon View Server Version : 7.0.2, 7.1.0 + PowerCLI Version : PowerCLI 6.5, PowerCLI 6.5.1 PowerShell Version : 5.0 #> [CmdletBinding( @@ -7691,8 +7691,8 @@ function Get-HVEntitlement { Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2, 7.0.3 - PowerCLI Version : PowerCLI 6.5 + Horizon View Server Version : 7.0.2, 7.1.0 + PowerCLI Version : PowerCLI 6.5, PowerCLI 6.5.1 PowerShell Version : 5.0 #> @@ -7900,8 +7900,8 @@ function Remove-HVEntitlement { Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2, 7.0.3 - PowerCLI Version : PowerCLI 6.5 + Horizon View Server Version : 7.0.2, 7.1.0 + PowerCLI Version : PowerCLI 6.5, PowerCLI 6.5.1 PowerShell Version : 5.0 #> @@ -8143,8 +8143,8 @@ PARAMETER Key Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2, 7.0.3 - PowerCLI Version : PowerCLI 6.5 + Horizon View Server Version : 7.0.2, 7.1.0 + PowerCLI Version : PowerCLI 6.5, PowerCLI 6.5.1 PowerShell Version : 5.0 #> @@ -8313,8 +8313,8 @@ function New-HVGlobalEntitlement { Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2, 7.0.3 - PowerCLI Version : PowerCLI 6.5 + Horizon View Server Version : 7.0.2, 7.1.0 + PowerCLI Version : PowerCLI 6.5, PowerCLI 6.5.1 PowerShell Version : 5.0 #> @@ -8543,8 +8543,8 @@ function Get-HVGlobalEntitlement { Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2, 7.0.3 - PowerCLI Version : PowerCLI 6.5 + Horizon View Server Version : 7.0.2, 7.1.0 + PowerCLI Version : PowerCLI 6.5, PowerCLI 6.5.1 PowerShell Version : 5.0 #> @@ -8629,8 +8629,8 @@ function Remove-HVGlobalEntitlement { Version : 1.1 ===Tested Against Environment==== - Horizon View Server Version : 7.0.2, 7.0.3 - PowerCLI Version : PowerCLI 6.5 + Horizon View Server Version : 7.0.2, 7.1.0 + PowerCLI Version : PowerCLI 6.5, PowerCLI 6.5.1 PowerShell Version : 5.0 #> From 2769f885f3c62cbbe8b13af645ea6b523837002a Mon Sep 17 00:00:00 2001 From: praveenmathamsetty Date: Fri, 21 Apr 2017 21:17:27 +0530 Subject: [PATCH 030/112] update readme file update readme file --- Modules/VMware.Hv.Helper/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/VMware.Hv.Helper/README.md b/Modules/VMware.Hv.Helper/README.md index ae6e144..fe87153 100644 --- a/Modules/VMware.Hv.Helper/README.md +++ b/Modules/VMware.Hv.Helper/README.md @@ -1,7 +1,7 @@ Prerequisites/Steps to use this module: 1. This module only works for Horizon product E.g. Horizon 7.0.2 and later. -2. Install the latest version of Powershell, PowerCLI(6.5) or (later version via psgallary). +2. Install the latest version of Powershell, PowerCLI(6.5) or (later version via psgallery). 3. Import HorizonView module by running: Import-Module VMware.VimAutomation.HorizonView. 4. Import "VMware.Hv.Helper" module by running: Import-Module -Name "location of this module" or Get-Module -ListAvailable 'VMware.Hv.Helper' | Import-Module. 5. Get-Command -Module "This module Name" to list all available functions or Get-Command -Module 'VMware.Hv.Helper'. From 6580d1b08530d4b3e129101b7fe5faee94182f40 Mon Sep 17 00:00:00 2001 From: Kevin Kirkpatrick Date: Mon, 24 Apr 2017 12:06:39 -0400 Subject: [PATCH 031/112] Added .git files to root of repo Signed-off-by: Kevin Kirkpatrick --- .gitattributes | 0 .gitignore | 51 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..e69de29 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4ac9d8d --- /dev/null +++ b/.gitignore @@ -0,0 +1,51 @@ +# PowerShell Studio Files +*.temppoint.* +*.psproj.psbuild +*.psbuild + +#VS Code Files +*.vscode + +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# ========================= +# Operating System Files +# ========================= + +# OSX +# ========================= + +.DS_Store +.AppleDouble +.LSOverride + +# Thumbnails +._* + +# Files that might appear on external disk +.Spotlight-V100 +.Trashes + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk \ No newline at end of file From e808a91fc5e45423727b4e6e7c1ed7d30f1e4b82 Mon Sep 17 00:00:00 2001 From: Kevin Kirkpatrick Date: Mon, 24 Apr 2017 12:07:25 -0400 Subject: [PATCH 032/112] Added folders for all .psm1 Files in /Modules Signed-off-by: Kevin Kirkpatrick --- Modules/Backup-VCSA/Backup-VCSA.psm1 | 202 +++++ Modules/Get-NICDetails/Get-NICDetails.psm1 | 93 +++ .../Get-NewAndRemovedVMs.psm1 | 131 ++++ Modules/Get-VMmaxIOPS/Get-VMmaxIOPS.psm1 | 114 +++ Modules/Konfig-ESXi/Konfig-ESXi.psm1 | 234 ++++++ Modules/PSvLIMessage/PSvLIMessage.psm1 | 123 +++ Modules/ProactiveHA/ProactiveHA.psm1 | 468 ++++++++++++ .../Recommend-Sizing/Recommend-Sizing.psm1 | 227 ++++++ Modules/Set-CBT/Set-CBT.psm1 | 111 +++ Modules/Start-UNMAP/Start-UNMAP.psm1 | 99 +++ Modules/VAMI/VAMI.psm1 | 716 ++++++++++++++++++ Modules/VCHA/VCHA.psm1 | 413 ++++++++++ Modules/VMCPFunctions/VMCPFunctions.psm1 | 322 ++++++++ Modules/apply-hardening/apply-hardening.psm1 | 93 +++ .../vSphere_Hardening_Assess_VM_v1a.psm1 | 372 +++++++++ 15 files changed, 3718 insertions(+) create mode 100644 Modules/Backup-VCSA/Backup-VCSA.psm1 create mode 100644 Modules/Get-NICDetails/Get-NICDetails.psm1 create mode 100644 Modules/Get-NewAndRemovedVMs/Get-NewAndRemovedVMs.psm1 create mode 100644 Modules/Get-VMmaxIOPS/Get-VMmaxIOPS.psm1 create mode 100644 Modules/Konfig-ESXi/Konfig-ESXi.psm1 create mode 100644 Modules/PSvLIMessage/PSvLIMessage.psm1 create mode 100644 Modules/ProactiveHA/ProactiveHA.psm1 create mode 100644 Modules/Recommend-Sizing/Recommend-Sizing.psm1 create mode 100644 Modules/Set-CBT/Set-CBT.psm1 create mode 100644 Modules/Start-UNMAP/Start-UNMAP.psm1 create mode 100755 Modules/VAMI/VAMI.psm1 create mode 100644 Modules/VCHA/VCHA.psm1 create mode 100644 Modules/VMCPFunctions/VMCPFunctions.psm1 create mode 100644 Modules/apply-hardening/apply-hardening.psm1 create mode 100644 Modules/vSphere_Hardening_Assess_VM_v1a/vSphere_Hardening_Assess_VM_v1a.psm1 diff --git a/Modules/Backup-VCSA/Backup-VCSA.psm1 b/Modules/Backup-VCSA/Backup-VCSA.psm1 new file mode 100644 index 0000000..271a7ff --- /dev/null +++ b/Modules/Backup-VCSA/Backup-VCSA.psm1 @@ -0,0 +1,202 @@ +Function Backup-VCSAToFile { +<# + .NOTES + =========================================================================== + Created by: Brian Graf + Date: October 30, 2016 + Organization: VMware + Blog: www.vtagion.com + Twitter: @vBrianGraf + =========================================================================== + + .SYNOPSIS + This function will allow you to create a full or partial backup of your + VCSA appliance. (vSphere 6.5 and higher) + + .DESCRIPTION + Use this function to backup your VCSA to a remote location + + .EXAMPLE + [VMware.VimAutomation.Cis.Core.Types.V1.Secret]$BackupPassword = "VMw@re123" + $Comment = "First API Backup" + $LocationType = "FTP" + $location = "10.144.99.5/vcsabackup-$((Get-Date).ToString('yyyy-MM-dd-hh-mm'))" + $LocationUser = "admin" + [VMware.VimAutomation.Cis.Core.Types.V1.Secret]$locationPassword = "VMw@re123" + PS C:\> Backup-VCSAToFile -BackupPassword $BackupPassword -LocationType $LocationType -Location $location -LocationUser $LocationUser -LocationPassword $locationPassword -Comment "This is a demo" -ShowProgress -FullBackup + + + .NOTES + Credit goes to @AlanRenouf for sharing the base of this function with me which I was able to take and make more robust as well as add in progress indicators + You must be connected to the CisService for this to work, if you are not connected, the function will prompt you for your credentials + If a -LocationType is not chosen, the function will default to FTP. + The destination location for a backup must be an empty folder (easiest to use the get-date cmdlet in the location) + -ShowProgress will give you a progressbar as well as updates in the console + -SeatBackup will only backup the config whereas -Fullbackup grabs the historical data as well +#> + param ( + [Parameter(ParameterSetName=’FullBackup’)] + [switch]$FullBackup, + [Parameter(ParameterSetName=’SeatBackup’)] + [switch]$SeatBackup, + [ValidateSet('FTPS', 'HTTP', 'SCP', 'HTTPS', 'FTP')] + $LocationType = "FTP", + $Location, + $LocationUser, + [VMware.VimAutomation.Cis.Core.Types.V1.Secret]$LocationPassword, + [VMware.VimAutomation.Cis.Core.Types.V1.Secret]$BackupPassword, + $Comment = "Backup job", + [switch]$ShowProgress + ) + Begin { + if (!($global:DefaultCisServers)){ + [System.Windows.Forms.MessageBox]::Show("It appears you have not created a connection to the CisServer. You will now be prompted to enter your vCenter credentials to continue" , "Connect to CisServer") | out-null + $Connection = Connect-CisServer $global:DefaultVIServer + } else { + $Connection = $global:DefaultCisServers + } + if ($FullBackup) {$parts = @("common","seat")} + if ($SeatBackup) {$parts = @("seat")} + } + Process{ + $BackupAPI = Get-CisService com.vmware.appliance.recovery.backup.job + $CreateSpec = $BackupAPI.Help.create.piece.CreateExample() + $CreateSpec.parts = $parts + $CreateSpec.backup_password = $BackupPassword + $CreateSpec.location_type = $LocationType + $CreateSpec.location = $Location + $CreateSpec.location_user = $LocationUser + $CreateSpec.location_password = $LocationPassword + $CreateSpec.comment = $Comment + try { + $BackupJob = $BackupAPI.create($CreateSpec) + } + catch { + Write-Error $Error[0].exception.Message + } + + + If ($ShowProgress){ + do { + $BackupAPI.get("$($BackupJob.ID)") | select id, progress, state + $progress = ($BackupAPI.get("$($BackupJob.ID)").progress) + Write-Progress -Activity "Backing up VCSA" -Status $BackupAPI.get("$($BackupJob.ID)").state -PercentComplete ($BackupAPI.get("$($BackupJob.ID)").progress) -CurrentOperation "$progress% Complete" + start-sleep -seconds 5 + } until ($BackupAPI.get("$($BackupJob.ID)").progress -eq 100 -or $BackupAPI.get("$($BackupJob.ID)").state -ne "INPROGRESS") + + $BackupAPI.get("$($BackupJob.ID)") | select id, progress, state + } + Else { + $BackupJob | select id, progress, state + } + } + End {} +} + +Function Get-VCSABackupJobs { +<# + .NOTES + =========================================================================== + Created by: Brian Graf + Date: October 30, 2016 + Organization: VMware + Blog: www.vtagion.com + Twitter: @vBrianGraf + =========================================================================== + + .SYNOPSIS + Get-VCSABackupJobs returns a list of all backup jobs VCSA has ever performed (vSphere 6.5 and higher) + + .DESCRIPTION + Get-VCSABackupJobs returns a list of all backup jobs VCSA has ever performed + + .EXAMPLE + PS C:\> Get-VCSABackupJobs + + .NOTES + The values returned are read as follows: + YYYYMMDD-hhmmss-vcsabuildnumber + You can pipe the results of this function into the Get-VCSABackupStatus function + Get-VCSABackupJobs | select -First 1 | Get-VCSABackupStatus <- Most recent backup +#> + param ( + [switch]$ShowNewest + ) + Begin { + if (!($global:DefaultCisServers)){ + [System.Windows.Forms.MessageBox]::Show("It appears you have not created a connection to the CisServer. You will now be prompted to enter your vCenter credentials to continue" , "Connect to CisServer") | out-null + $Connection = Connect-CisServer $global:DefaultVIServer + } else { + $Connection = $global:DefaultCisServers + } + } + Process{ + + $BackupAPI = Get-CisService com.vmware.appliance.recovery.backup.job + + try { + if ($ShowNewest) { + $results = $BackupAPI.list() + $results[0] + } else { + $BackupAPI.list() + } + } + catch { + Write-Error $Error[0].exception.Message + } + + } + + End {} +} + +Function Get-VCSABackupStatus { +<# + .NOTES + =========================================================================== + Created by: Brian Graf + Date: October 30, 2016 + Organization: VMware + Blog: www.vtagion.com + Twitter: @vBrianGraf + =========================================================================== + + .SYNOPSIS + Returns the ID, Progress, and State of a VCSA backup (vSphere 6.5 and higher) + + .DESCRIPTION + Returns the ID, Progress, and State of a VCSA backup + + .EXAMPLE + PS C:\> $backups = Get-VCSABackupJobs + $backups[0] | Get-VCSABackupStatus + + .NOTES + The BackupID can be piped in from the Get-VCSABackupJobs function and can return multiple job statuses +#> + Param ( + [parameter(ValueFromPipeline=$True)] + [string[]]$BackupID + ) + Begin { + if (!($global:DefaultCisServers)){ + [System.Windows.Forms.MessageBox]::Show("It appears you have not created a connection to the CisServer. You will now be prompted to enter your vCenter credentials to continue" , "Connect to CisServer") | out-null + $Connection = Connect-CisServer $global:DefaultVIServer + } else { + $Connection = $global:DefaultCisServers + } + + $BackupAPI = Get-CisService com.vmware.appliance.recovery.backup.job + } + Process{ + + foreach ($id in $BackupID) { + $BackupAPI.get("$id") | select id, progress, state + } + + + } + + End {} +} diff --git a/Modules/Get-NICDetails/Get-NICDetails.psm1 b/Modules/Get-NICDetails/Get-NICDetails.psm1 new file mode 100644 index 0000000..30f1440 --- /dev/null +++ b/Modules/Get-NICDetails/Get-NICDetails.psm1 @@ -0,0 +1,93 @@ +function Get-NICDetails { +<# + .NOTES + =========================================================================== + Created by: Markus Kraus + Twitter: @VMarkus_K + Private Blog: mycloudrevolution.com + =========================================================================== + Changelog: + 2017.02 ver 1.0 Base Release + =========================================================================== + External Code Sources: + - + =========================================================================== + Tested Against Environment: + vSphere Version: ESXi 6.0 U2, ESXi 6.5 + PowerCLI Version: PowerCLI 6.3 R1, PowerCLI 6.5 R1 + PowerShell Version: 4.0, 5.0 + OS Version: Windows 8.1, Server 2008 R2, Server 2012 R2 + Keyword: ESXi, NIC, vmnic, Driver, Firmware + =========================================================================== + + .DESCRIPTION + Reports Firmware and Driver Details for your ESXi vmnics. + + .Example + Get-NICDetails -Clustername * + + .PARAMETER Clustername + Name or Wildcard of your vSphere Cluster Name to process. + + +#Requires PS -Version 4.0 +#Requires -Modules VMware.VimAutomation.Core, @{ModuleName="VMware.VimAutomation.Core";ModuleVersion="6.3.0.0"} +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory=$True, ValueFromPipeline=$False, Position=0)] + [ValidateNotNullorEmpty()] + [String] $Clustername + +) + +Begin { + $Validate = $True + + if (($myCluster = Get-Cluster -Name $Clustername).count -lt 1) { + $Validate = $False + thow "No Cluster '$myCluster' found!" + } + +} + +Process { + + $MyView = @() + if ($Validate -eq $True) { + + foreach ($myVMhost in ($myCluster | Get-VMHost)) { + + $esxcli2 = Get-ESXCLI -VMHost $myVMhost -V2 + $niclist = $esxcli2.network.nic.list.invoke() + + $nicdetails = @() + foreach ($nic in $niclist) { + + $args = $esxcli2.network.nic.get.createargs() + $args.nicname = $nic.name + $nicdetail = $esxcli2.network.nic.get.Invoke($args) + $nicdetails += $nicdetail + + } + ForEach ($nicdetail in $nicdetails){ + $NICReport = [PSCustomObject] @{ + Host = $myVMhost.Name + vmnic = $nicdetail.Name + LinkStatus = $nicdetail.LinkStatus + BusInfo = $nicdetail.driverinfo.BusInfo + Driver = $nicdetail.driverinfo.Driver + FirmwareVersion = $nicdetail.driverinfo.FirmwareVersion + DriverVersion = $nicdetail.driverinfo.Version + } + $MyView += $NICReport + } + + } + + $MyView + + } +} +} \ No newline at end of file diff --git a/Modules/Get-NewAndRemovedVMs/Get-NewAndRemovedVMs.psm1 b/Modules/Get-NewAndRemovedVMs/Get-NewAndRemovedVMs.psm1 new file mode 100644 index 0000000..4a2e3ba --- /dev/null +++ b/Modules/Get-NewAndRemovedVMs/Get-NewAndRemovedVMs.psm1 @@ -0,0 +1,131 @@ +function Get-NewAndRemovedVMs { +<# + .NOTES + =========================================================================== + Created by: Markus Kraus + Twitter: @VMarkus_K + Private Blog: mycloudrevolution.com + =========================================================================== + Changelog: + 2016.12 ver 1.0 Base Release + =========================================================================== + External Code Sources: + https://github.com/alanrenouf/vCheck-vSphere + =========================================================================== + Tested Against Environment: + vSphere Version: 5.5 U2 + PowerCLI Version: PowerCLI 6.3 R1, PowerCLI 6.5 R1 + PowerShell Version: 4.0, 5.0 + OS Version: Windows 8.1, Server 2012 R2 + =========================================================================== + Keywords vSphere, VM + =========================================================================== + + .DESCRIPTION + This Function report newly created and deleted VMs by Cluster. + + .Example + Get-NewAndRemovedVMs -ClusterName Cluster* | ft -AutoSize + + .Example + Get-NewAndRemovedVMs -ClusterName Cluster01 -Days 90 + + .PARAMETER ClusterName + Name or Wildcard of your vSphere Cluster Name(s) to report. + + .PARAMETER Day + Range in Days to report. + + +#Requires PS -Version 4.0 +#Requires -Modules VMware.VimAutomation.Core, @{ModuleName="VMware.VimAutomation.Core";ModuleVersion="6.3.0.0"} +#> + +param( + [Parameter(Mandatory=$True, ValueFromPipeline=$False, Position=0, HelpMessage = "Name or Wildcard of your vSphere Cluster Name to report")] + [ValidateNotNullorEmpty()] + [String]$ClusterName, + [Parameter(Mandatory=$False, ValueFromPipeline=$False, Position=1, HelpMessage = "Range in Days to report")] + [ValidateNotNullorEmpty()] + [String]$Days = "30" +) +Begin { + function Get-VIEventPlus { + + param( + [VMware.VimAutomation.ViCore.Impl.V1.Inventory.InventoryItemImpl[]]$Entity, + [string[]]$EventType, + [DateTime]$Start, + [DateTime]$Finish = (Get-Date), + [switch]$Recurse, + [string[]]$User, + [Switch]$System, + [string]$ScheduledTask, + [switch]$FullMessage = $false, + [switch]$UseUTC = $false + ) + + process { + $eventnumber = 100 + $events = @() + $eventMgr = Get-View EventManager + $eventFilter = New-Object VMware.Vim.EventFilterSpec + $eventFilter.disableFullMessage = ! $FullMessage + $eventFilter.entity = New-Object VMware.Vim.EventFilterSpecByEntity + $eventFilter.entity.recursion = &{if($Recurse){"all"}else{"self"}} + $eventFilter.eventTypeId = $EventType + if($Start -or $Finish){ + $eventFilter.time = New-Object VMware.Vim.EventFilterSpecByTime + if($Start){ + $eventFilter.time.beginTime = $Start + } + if($Finish){ + $eventFilter.time.endTime = $Finish + } + } + if($User -or $System){ + $eventFilter.UserName = New-Object VMware.Vim.EventFilterSpecByUsername + if($User){ + $eventFilter.UserName.userList = $User + } + if($System){ + $eventFilter.UserName.systemUser = $System + } + } + if($ScheduledTask){ + $si = Get-View ServiceInstance + $schTskMgr = Get-View $si.Content.ScheduledTaskManager + $eventFilter.ScheduledTask = Get-View $schTskMgr.ScheduledTask | + where {$_.Info.Name -match $ScheduledTask} | + Select -First 1 | + Select -ExpandProperty MoRef + } + if(!$Entity){ + $Entity = @(Get-Folder -NoRecursion) + } + $entity | %{ + $eventFilter.entity.entity = $_.ExtensionData.MoRef + $eventCollector = Get-View ($eventMgr.CreateCollectorForEvents($eventFilter)) + $eventsBuffer = $eventCollector.ReadNextEvents($eventnumber) + while($eventsBuffer){ + $events += $eventsBuffer + $eventsBuffer = $eventCollector.ReadNextEvents($eventnumber) + } + $eventCollector.DestroyCollector() + } + if (-not $UseUTC) + { + $events | % { $_.createdTime = $_.createdTime.ToLocalTime() } + } + + $events + } +} +} + +process { + $result = Get-VIEventPlus -Start ((get-date).adddays(-$Days)) -EventType @("VmCreatedEvent", "VmBeingClonedEvent", "VmBeingDeployedEvent","VmRemovedEvent") + $sortedResult = $result | Select CreatedTime, @{N='Cluster';E={$_.ComputeResource.Name}}, @{Name="VMName";Expression={$_.vm.name}}, UserName, @{N='Type';E={$_.GetType().Name}}, FullFormattedMessage | Sort CreatedTime + $sortedResult | where {$_.Cluster -like $ClusterName} +} +} \ No newline at end of file diff --git a/Modules/Get-VMmaxIOPS/Get-VMmaxIOPS.psm1 b/Modules/Get-VMmaxIOPS/Get-VMmaxIOPS.psm1 new file mode 100644 index 0000000..27af1ad --- /dev/null +++ b/Modules/Get-VMmaxIOPS/Get-VMmaxIOPS.psm1 @@ -0,0 +1,114 @@ +function Get-VMmaxIOPS { +<# + .NOTES + =========================================================================== + Created by: Markus Kraus + Twitter: @VMarkus_K + Private Blog: mycloudrevolution.com + =========================================================================== + Changelog: + 2016.10 ver 1.0 Base Release + 2016.11 ver 1.1 Added vSphere 6.5 Support, New Counters, More Error Handling + =========================================================================== + External Code Sources: + http://www.lucd.info/2011/04/22/get-the-maximum-iops/ + https://communities.vmware.com/thread/485386 + =========================================================================== + Tested Against Environment: + vSphere Version: 5.5 U2, 6.5 + PowerCLI Version: PowerCLI 6.3 R1, 6.5 R1 + PowerShell Version: 4.0, 5.0 + OS Version: Windows 8.1, Windows Server 2012 R2 + =========================================================================== + Keywords vSphere, ESXi, VM, Storage + =========================================================================== + + .DESCRIPTION + This Function will Create a VM Disk IOPS Report + + .Example + Get-VM TST* | Get-VMmaxIOPS -Minutes 60 | FT -Autosize + + .Example + $SampleVMs = Get-VM "TST*" + Get-VMmaxIOPS -VMs $SampleVMs -Minutes 60 + + .PARAMETER VMs + Specify the VMs + + .PARAMETER Minutes + Specify the Minutes to report (10080 is one Week) + +#Requires PS -Version 4.0 +#Requires -Modules VMware.VimAutomation.Core, @{ModuleName="VMware.VimAutomation.Core";ModuleVersion="6.3.0.0"} +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory=$true, ValueFromPipeline=$True, Position=0)] + [ValidateNotNullorEmpty()] + [VMware.VimAutomation.ViCore.Impl.V1.Inventory.InventoryItemImpl[]] $VMs, + [Parameter(Mandatory=$false, Position=1, HelpMessage = "Specify the Minutes to report (10080 is one Week)")] + [ValidateNotNullorEmpty()] + [int] $Minutes = 30 +) +Begin { + # none + } +Process { + if ($_.PowerState -eq "PoweredOn") { + #region: Global Definitions + [int]$TimeRange = "-" + $Minutes + #endregion + + #region: Creating VM Stats + Write-Verbose "$(Get-Date -Format G) Create VM Stats..." + $VMMetrics = "virtualdisk.numberwriteaveraged.average","virtualdisk.numberreadaveraged.average" + $Start = (Get-Date).AddMinutes($TimeRange) + $stats = Get-Stat -Realtime -Stat $VMMetrics -Entity $VMs -Start $Start -Verbose:$False + Write-Verbose "$(Get-Date -Format G) Create VM Stats completed" + #endregion + + #region: Creating HD-Tab + Write-Verbose "$(Get-Date -Format G) Create HD Tab..." + $hdTab = @{} + foreach($hd in (Get-Harddisk -VM $VMs)){ + $controllerKey = $hd.Extensiondata.ControllerKey + $controller = $hd.Parent.Extensiondata.Config.Hardware.Device | where{$_.Key -eq $controllerKey} + $hdTab[$hd.Parent.Name + "/scsi" + $controller.BusNumber + ":" + $hd.Extensiondata.UnitNumber] = $hd.FileName.Split(']')[0].TrimStart('[') + } + Write-Verbose "$(Get-Date -Format G) Create HD Tab completed" + #endregion + + #region: Creating Reports + Write-Verbose "$(Get-Date -Format G) Create Report..." + $reportPerf = @() + $reportPerf = $stats | Group-Object -Property {$_.Entity.Name},Instance | %{ + New-Object PSObject -Property @{ + VM = $_.Values[0] + Disk = $_.Values[1] + IOPSWriteAvg = [math]::round( ($_.Group | ` + where{$_.MetricId -eq "virtualdisk.numberwriteaveraged.average"} | ` + Measure-Object -Property Value -Average).Average,2) + IOPSReadAvg = [math]::round( ($_.Group | ` + where{$_.MetricId -eq "virtualdisk.numberreadaveraged.average"} | ` + Measure-Object -Property Value -Average).Average,2) + Datastore = $hdTab[$_.Values[0] + "/"+ $_.Values[1]] + } + } + Write-Verbose "$(Get-Date -Format G) Create Report completed" + #endregion + + + } + Else { + Write-Error "VM $($_.Name) is Powered Off! Processing Skipped" + } + $reportPerf | Select-Object VM, Disk, Datastore, IOPSWriteAvg, IOPSReadAvg + } + +End { + # none + } + +} \ No newline at end of file diff --git a/Modules/Konfig-ESXi/Konfig-ESXi.psm1 b/Modules/Konfig-ESXi/Konfig-ESXi.psm1 new file mode 100644 index 0000000..f14386a --- /dev/null +++ b/Modules/Konfig-ESXi/Konfig-ESXi.psm1 @@ -0,0 +1,234 @@ +function Konfig-ESXi { +<# + .NOTES + =========================================================================== + Created by: Markus Kraus + Twitter: @VMarkus_K + Private Blog: mycloudrevolution.com + =========================================================================== + Changelog: + 2016.12 ver 1.0 Base Release + 2016.12 ver 1.1 ESXi 6.5 Tests, Minor enhancements + =========================================================================== + External Code Sources: + Function My-Logger : http://www.virtuallyghetto.com/ + =========================================================================== + Tested Against Environment: + vSphere Version: ESXi 5.5 U2, ESXi 6.5 + PowerCLI Version: PowerCLI 6.3 R1, PowerCLI 6.5 R1 + PowerShell Version: 4.0, 5.0 + OS Version: Windows 8.1, Server 2012 R2 + Keyword: ESXi, NTP, SSH, Syslog, SATP, + =========================================================================== + + .DESCRIPTION + This Function sets the Basic settings for a new ESXi. + + * NTP + * SSH + * Syslog + * Power Management + * HP 3PAR SATP/PSP Rule + * ... + + .Example + Konfig-ESXi -VMHost myesxi.lan.local -NTP 192.168.2.1, 192.168.2.2 -syslog "udp://loginsight.lan.local:514" + + .PARAMETER VMHost + Host to configure. + + .PARAMETER NTP + NTP Server(s) to set. + + .PARAMETER Syslog + Syslog Server to set, e.g. "udp://loginsight.lan.local:514" + + DNS Name must be resolvable! + + +#Requires PS -Version 4.0 +#Requires -Modules VMware.VimAutomation.Core, @{ModuleName="VMware.VimAutomation.Core";ModuleVersion="6.3.0.0"} +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory=$True, ValueFromPipeline=$False, Position=0)] + [String] $VMHost, + [Parameter(Mandatory=$true, ValueFromPipeline=$False, Position=1)] + [array]$NTP, + [Parameter(Mandatory=$true, ValueFromPipeline=$False, Position=2)] + [String] $syslog + +) + +Begin { + Function My-Logger { + param( + [Parameter(Mandatory=$true)] + [String]$message + ) + + $timeStamp = Get-Date -Format "MM-dd-yyyy_hh-mm-ss" + + Write-Host -NoNewline -ForegroundColor White "[$timestamp]" + Write-Host -ForegroundColor Green " $message" + } + function Set-MyESXiOption { + [CmdletBinding()] + param( + [Parameter(Mandatory=$True, ValueFromPipeline=$False, Position=0)] + [String] $Name, + [Parameter(Mandatory=$False, ValueFromPipeline=$False, Position=1)] + [String] $Value + ) + process { + $myESXiOption = Get-AdvancedSetting -Entity $ESXiHost -Name $Name + if ($myESXiOption.Value -ne $Value) { + My-Logger " Setting ESXi Option $Name to Value $Value" + $myESXiOption | Set-AdvancedSetting -Value $Value -Confirm:$false | Out-Null + } + else { + My-Logger " ESXi Option $Name already has Value $Value" + } + } + } +} + +Process { + $Validate = $True + + #region: Start vCenter Connection + My-Logger "Starting to Process ESXi Server Connection to $VMHost ..." + if (($global:DefaultVIServers).count -gt 0) { + Disconnect-VIServer -Force -Confirm:$False -ErrorAction SilentlyContinue + } + $VIConnection = Connect-VIServer -Server $VMHost + if (-not $VIConnection.IsConnected) { + Write-Error "ESXi Connection Failed." + $Validate = $False + } + elseif ($VIConnection.ProductLine -ne "EmbeddedEsx") { + Write-Error "Connencted System is not an ESXi." + $Validate = $False + } + else { + $ESXiHost = Get-VMHost + My-Logger "Connected ESXi Version: $($ESXiHost.Version) $($ESXiHost.Build) " + } + #endregion + + if ($Validate -eq $True) { + + #region: Enable SSH and disable SSH Warning + $SSHService = $ESXiHost | Get-VMHostService | where {$_.Key -eq 'TSM-SSH'} + My-Logger "Starting SSH Service..." + if($SSHService.Running -ne $True){ + Start-VMHostService -HostService $SSHService -Confirm:$false | Out-Null + } + else { + My-Logger " SSH Service is already running" + } + My-Logger "Setting SSH Service to Automatic Start..." + if($SSHService.Policy -ne "automatic"){ + Set-VMHostService -HostService $SSHService -Policy "Automatic" | Out-Null + } + else { + My-Logger " SSH Service is already set to Automatic Start" + } + My-Logger "Disabling SSH Warning..." + Set-MyESXiOption -Name "UserVars.SuppressShellWarning" -Value "1" + #endregion + + #region: Config NTP + My-Logger "Removing existing NTP Server..." + try { + $ESXiHost | Remove-VMHostNtpServer -NtpServer (Get-VMHostNtpServer) -Confirm:$false + } + catch [System.Exception] { + Write-Warning "Error during removing existing NTP Servers." + } + My-Logger "Setting new NTP Servers..." + foreach ($myNTP in $NTP) { + $ESXiHost | Add-VMHostNtpServer -ntpserver $myNTP -confirm:$False | Out-Null + } + + My-Logger "Configure NTP Service..." + $NTPService = $ESXiHost | Get-VMHostService| Where-Object {$_.key -eq "ntpd"} + if($NTPService.Running -eq $True){ + Stop-VMHostService -HostService $NTPService -Confirm:$false | Out-Null + } + if($NTPService.Policy -ne "on"){ + Set-VMHostService -HostService $NTPService -Policy "on" -confirm:$False | Out-Null + } + + My-Logger "Configure Local Time..." + $HostTimeSystem = Get-View $ESXiHost.ExtensionData.ConfigManager.DateTimeSystem + $HostTimeSystem.UpdateDateTime([DateTime]::UtcNow) + + My-Logger "Start NTP Service..." + Start-VMHostService -HostService $NTPService -confirm:$False | Out-Null + #endregion + + #region: Remove default PG + My-Logger "Checking for Default Port Group ..." + if ($defaultPG = $ESXiHost | Get-VirtualSwitch -Name vSwitch0 | Get-VirtualPortGroup -Name "VM Network" -ErrorAction SilentlyContinue ){ + Remove-VirtualPortGroup -VirtualPortGroup $defaultPG -confirm:$False | Out-Null + My-Logger " Default PG Removed" + } + else { + My-Logger " No Default PG found" + } + #endregion + + #region: Configure Static HighPower + My-Logger "Setting PowerProfile to Static HighPower..." + try { + $HostView = ($ESXiHost | Get-View) + (Get-View $HostView.ConfigManager.PowerSystem).ConfigurePowerPolicy(1) + } + catch [System.Exception] { + Write-Warning "Error during Configure Static HighPower. See latest errors..." + } + #endregion + + #region: Conf Syslog + My-Logger "Setting Syslog Firewall Rule ..." + $SyslogFW = ($ESXiHost | Get-VMHostFirewallException | where {$_.Name -eq 'syslog'}) + if ($SyslogFW.Enabled -eq $False ){ + $SyslogFW | Set-VMHostFirewallException -Enabled:$true -Confirm:$false | Out-Null + My-Logger " Syslog Firewall Rule enabled" + } + else { + My-Logger " Syslog Firewall Rule already enabled" + } + My-Logger "Setting Syslog Server..." + Set-MyESXiOption -Name "Syslog.global.logHost" -Value $syslog + #endregion + + #region: Change Disk Scheduler + My-Logger "Changing Disk Scheduler..." + Set-MyESXiOption -Name "Disk.SchedulerWithReservation" -Value "0" + #endregion + + #region: Configure HP 3PAR SATP/PSP Rule + My-Logger "Configure HP 3PAR SATP/PSP Rule" + $esxcli2 = Get-ESXCLI -VMHost $ESXiHost -V2 + $arguments = $esxcli2.storage.nmp.satp.rule.add.CreateArgs() + $arguments.satp = "VMW_SATP_ALUA" + $arguments.psp = "VMW_PSP_RR" + $arguments.pspoption = "iops=100" + $arguments.claimoption = "tpgs_on" + $arguments.vendor = "3PARdata" + $arguments.model = "VV" + $arguments.description = "HP 3PAR custom SATP Claimrule" + try { + $esxcli2.storage.nmp.satp.rule.add.Invoke($arguments) + } + catch { + Write-Warning "Error during Configure HP 3PAR SATP/PSP Rule. See latest errors..." + } + #endregion + + } + } +} diff --git a/Modules/PSvLIMessage/PSvLIMessage.psm1 b/Modules/PSvLIMessage/PSvLIMessage.psm1 new file mode 100644 index 0000000..9cab209 --- /dev/null +++ b/Modules/PSvLIMessage/PSvLIMessage.psm1 @@ -0,0 +1,123 @@ +add-type @" + using System.Net; + using System.Security.Cryptography.X509Certificates; + public class TrustAllCertsPolicy : ICertificatePolicy { + public bool CheckValidationResult( + ServicePoint srvPoint, X509Certificate certificate, + WebRequest request, int certificateProblem) { + return true; + } + } +"@ +[System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy + +<# + .NOTES + =========================================================================== + Created by: Markus Kraus + Organization: Private + Personal Blog: mycloudrevolution.com + Twitter: @vMarkus_K + =========================================================================== + Tested Against Environment: + vRealize Log Insight 3.3.1 + PowerShell Version: 4.0, 5.0 + OS Version: Windows 8.1, Server 2012 R2 + Keyword: vRealize, RestAPI + + Dependencies: + PowerCLI Version: PowerCLI 6.3 R1 + + .SYNOPSIS + Push Messages to VMware vRealize Log Insight. + + .DESCRIPTION + Creates a Messages in VMware vRealize Log Insight via the Ingestion API + + .EXAMPLE + Push-vLIMessage -vLIServer "loginsight.lan.local" -vLIAgentID "12862842-5A6D-679C-0E38-0E2BE888BB28" -Text "My Test" + + .EXAMPLE + Push-vLIMessage -vLIServer "loginsight.lan.local" -vLIAgentID "12862842-5A6D-679C-0E38-0E2BE888BB28" -Text "My Test" -Hostname MyTEST -FieldName myTest -FieldContent myTest + + .PARAMETER vLIServer + Specify the FQDN of your vRealize Log Insight Appliance + + .PARAMETER vLIAgentID + Specify the vRealize Log Insight Agent ID, e.g. "12862842-5A6D-679C-0E38-0E2BE888BB28" + + .PARAMETER Text + Specify the Event Text + + .PARAMETER Hostname + Specify the Hostanme displayed in vRealize Log Insight + + .PARAMETER FieldName + Specify the a Optional Field Name for vRealize Log Insight + + .PARAMETER FieldContent + Specify the a Optional FieldContent for the Field in -FieldName for vRealize Log Insight + If FielName is missing and FieldContent is given, it will be ignored + + #Requires PS -Version 3.0 + + #> +function Push-vLIMessage { + + [cmdletbinding()] + param ( + [parameter(Mandatory=$true)] + [string]$Text, + [parameter(Mandatory=$true)] + [string]$vLIServer, + [parameter(Mandatory=$true)] + [string]$vLIAgentID, + [parameter(Mandatory=$false)] + [string]$Hostname = $env:computername, + [parameter(Mandatory=$false)] + [string]$FieldName, + [parameter(Mandatory=$false)] + [string]$FieldContent = "" + ) + Process { + $Field_vLI = [ordered]@{ + name = "PS_vLIMessage" + content = "true" + } + $Field_HostName = [ordered]@{ + name = "hostname" + content = $Hostname + } + + $Fields = @($Field_vLI, $Field_HostName) + + if ($FieldName) { + $Field_Custom = [ordered]@{ + name = $FieldName + content = $FieldContent + } + $Fields += @($Field_Custom) + } + + $Restcall = @{ + messages = ([Object[]]([ordered]@{ + text = ($Text) + fields = ([Object[]]$Fields) + })) + } | convertto-json -Depth 4 + + $Resturl = ("http://" + $vLIServer + ":9000/api/v1/messages/ingest/" + $vLIAgentID) + try + { + $Response = Invoke-RestMethod $Resturl -Method Post -Body $Restcall -ContentType 'application/json' -ErrorAction stop + Write-Information "REST Call to Log Insight server successful" + Write-Verbose $Response + } + catch + { + Write-Error "REST Call failed to Log Insight server" + Write-Verbose $error[0] + Write-Verbose $Resturl + } + } +} \ No newline at end of file diff --git a/Modules/ProactiveHA/ProactiveHA.psm1 b/Modules/ProactiveHA/ProactiveHA.psm1 new file mode 100644 index 0000000..ea4e92f --- /dev/null +++ b/Modules/ProactiveHA/ProactiveHA.psm1 @@ -0,0 +1,468 @@ +Function New-PHAProvider { +<# + .NOTES + =========================================================================== + Created by: William Lam + Organization: VMware + Blog: www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + .DESCRIPTION + Function to register a new Proactive HA Provider with vCenter Server + .PARAMETER ProviderName + Name of ProactiveHA Provider + .PARAMETER ComponentType + Name of a supported ComponentType that ProactiveHA supports (Fan, Memory, Network, Power or Storage) + .PARAMETER ComponentDescription + Description of the health check for the given component + .PARAMETER ComponentId + Unique identifier for the given component within a ProactiveHA Provider + .EXAMPLE + New-PHAProvider -ProviderName "virtuallyGhetto" -ComponentType Power -ComponentDescription "Simulated ProactiveHA Provider" -ComponentId "Power" +#> + param( + [Parameter(Mandatory=$true)][String]$ProviderName, + [Parameter(Mandatory=$true)][ValidateSet("Fan","Memory","Network","Power","Storage")][String]$ComponentType, + [Parameter(Mandatory=$true)][String]$ComponentDescription, + [Parameter(Mandatory=$true)][String]$ComponentId + ) + Write-Host -ForegroundColor Red "`n******************** DISCLAIMER ********************" + Write-Host -ForegroundColor Red "**** THIS IS NOT INTENDED FOR PRODUCTION USE ****" + Write-Host -ForegroundColor Red "**** LEARNING PURPOSES ONLY ****" + Write-Host -ForegroundColor Red "******************** DISCLAIMER ********************`n" + + $healthManager = Get-View $global:DefaultVIServer.ExtensionData.Content.HealthUpdateManager + + $healthInfo = [VMware.Vim.HealthUpdateInfo] @{ + ComponentType = $ComponentType + description = $ComponentDescription + Id = $ComponentId + } + + try { + Write-Host "`nRegistering new Proactive HA Provider $ProviderName ..." + $providerId = $healthManager.RegisterHealthUpdateProvider($ProviderName,$healthInfo) + } catch { + Write-host -ForegroundColor Red $Error[0].Exception + } +} + +Function Get-PHAProvider { +<# + .NOTES + =========================================================================== + Created by: William Lam + Organization: VMware + Blog: www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + .DESCRIPTION + Function to return list of all Proactive HA Providers registered with vCenter Server + .EXAMPLE + Get-PHAProvider +#> + $healthManager = Get-View $global:DefaultVIServer.ExtensionData.Content.HealthUpdateManager + + $healthProviderResults = @() + $hpIDs = $healthManager.QueryProviderList() + + foreach ($hpID in $hpIDs) { + $hpName = $healthManager.QueryProviderName($hpID) + $hpConfig = $healthManager.QueryHealthUpdateInfos($hpID) + + $hp = [pscustomobject] @{ + ProviderName = $hpName + ProviderID = $hpID + ComponentType = $hpConfig.componentType + ComponentID = $hpConfig.id + Description = $hpConfig.description + } + $healthProviderResults+=$hp + } + $healthProviderResults +} + +Function Remove-PHAProvider { +<# + .NOTES + =========================================================================== + Created by: William Lam + Organization: VMware + Blog: www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + .DESCRIPTION + Function to remove a registered Proactive HA Provider from vCenter Server + .PARAMETER ProviderId + The ProactiveHA provider ID (retrieved from Get-PHAProvider) to unregister + .EXAMPLE + Remove-PHAProvider -ProviderID "52 85 22 c2 f2 6a e7 b9-fc ff 63 9e 10 81 00 79" +#> + param( + [Parameter(Mandatory=$true)][String]$ProviderId + ) + + Write-Host -ForegroundColor Red "`n******************** DISCLAIMER ********************" + Write-Host -ForegroundColor Red "**** THIS IS NOT INTENDED FOR PRODUCTION USE ****" + Write-Host -ForegroundColor Red "**** LEARNING PURPOSES ONLY ****" + Write-Host -ForegroundColor Red "******************** DISCLAIMER ********************`n" + + $healthManager = Get-View $global:DefaultVIServer.ExtensionData.Content.HealthUpdateManager + + try { + Write-Host "`nUnregistering Proactive HA Provider $ProviderId ... " + $healthManager.UnregisterHealthUpdateProvider($providerId) + } catch { + if($Error[0].Exception.InnerException.MethodFault.getType().Name -eq "InvalidState") { + Write-host -ForegroundColor Red "The Proactive HA Provider is still in use, please disable it before unregistering" + } else { + Write-host -ForegroundColor Red $Error[0].Exception + } + } +} + +Function Set-PHAConfig { +<# + .NOTES + =========================================================================== + Created by: William Lam + Organization: VMware + Blog: www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + .DESCRIPTION + Function to enable/disable Proactive HA for vSphere Cluster + .PARAMETER Cluster + Name of the vSphere Cluster to enable Proactive HA + .PARAMETER ProviderId + Proactive HA Provider ID to enable in vSphere Cluster + .PARAMETER ClusterMode + Whether Proactive HA should be "Automated" or "Manual" for actions it will take + .PARAMETER ModerateRemediation + Type of operation (Maintenance Mode or Quaratine Mode) to perform when a Moderate issue is observed + .PARAMETER SevereRemediation + Type of operation (Maintenance Mode or Quaratine Mode) to perform when a Severe issue is observed + .EXAMPLE + Set-PHAConfig -Cluster VSAN-Cluster -Enabled -ClusterMode Automated -ModerateRemediation QuarantineMode -SevereRemediation QuarantineMode -ProviderID "52 85 22 c2 f2 6a e7 b9-fc ff 63 9e 10 81 00 79" + .EXAMPLE + Set-PHAConfig -Cluster VSAN-Cluster -Disabled -ProviderID "52 85 22 c2 f2 6a e7 b9-fc ff 63 9e 10 81 00 79" +#> + param( + [Parameter(Mandatory=$true)][String]$ProviderId, + [Parameter(Mandatory=$true)][String]$Cluster, + [Parameter(Mandatory=$false)][ValidateSet("Automated","Manual")]$ClusterMode="Manual", + [Parameter(Mandatory=$false)][ValidateSet("MaintenanceMode","QuarantineMode")]$ModerateRemediation="QuarantineMode", + [Parameter(Mandatory=$false)][ValidateSet("MaintenanceMode","QuarantineMode")]$SevereRemediation="QuarantineMode", + [Switch]$Enabled, + [Switch]$Disabled + ) + + $ClusterView = Get-View -ViewType ClusterComputeResource -Property Name,Host,ConfigurationEx -Filter @{"Name" = $Cluster} + + if($ClusterView -eq $null) { + Write-Host -ForegroundColor Red "Unable to find vSphere Cluster $cluster ..." + break + } + + $vmhosts = $ClusterView.host + + $healthManager = Get-View $global:DefaultVIServer.ExtensionData.Content.HealthUpdateManager + + if($Enabled) { + try { + $entities = @() + foreach ($vmhost in $vmhosts) { + if(-not $healthManager.HasMonitoredEntity($ProviderId,$vmhost)) { + $entities += $vmhost + } + } + + Write-Host "Enabling Proactive HA monitoring for all ESXi hosts in cluster ..." + $healthManager.AddMonitoredEntities($ProviderId,$entities) + } catch { + Write-host -ForegroundColor Red $Error[0].Exception + } + + try { + $healthProviders = @() + + # Make sure not to remove existing ProactiveHA providers + if($ClusterView.ConfigurationEx.InfraUpdateHaConfig.Providers -ne $null) { + $currentHPs = $ClusterView.ConfigurationEx.infraUpdateHaConfig.providers + foreach ($currentHP in $currentHPs) { + $healthProviders+=$currentHP + } + if(-not ($healthProviders -contains $ProviderID)) { + $healthProviders+=$ProviderId + } + } else { + $healthProviders+=$ProviderId + } + + $PHASpec = [VMware.Vim.ClusterInfraUpdateHaConfigInfo] @{ + enabled = $true + behavior = $ClusterMode + moderateRemediation = $ModerateRemediation + severeRemediation = $SevereRemediation + providers = $healthProviders + } + + $spec = [VMware.Vim.ClusterConfigSpecEx] @{ + infraUpdateHaConfig = $PHASpec + } + + Write-Host "Enabling Proactive HA Provider $ProviderId on $Cluster ..." + $task = $ClusterView.ReconfigureComputeResource_Task($spec,$True) + $task1 = Get-Task -Id ("Task-$($task.value)") + $task1 | Wait-Task | Out-Null + } catch { + Write-host -ForegroundColor Red $Error[0].Exception + } + } + + if($Disabled) { + foreach ($vmhost in $vmhosts) { + if($vmhost.runtime.inQuarantineMode) { + Write-Host -ForegroundColor Red $vmhost.name " is currently still in Quaratine Mode, please remediate this before disabling Proactive HA" + break + } + } + + try { + $healthProviders = @() + + # Make sure not to remove existing ProactiveHA providers + if($ClusterView.ConfigurationEx.InfraUpdateHaConfig.Providers -ne $null) { + $currentHPs = $ClusterView.ConfigurationEx.infraUpdateHaConfig.providers + foreach ($currentHP in $currentHPs) { + if($currentHP -ne $ProviderId) { + $healthProviders+=$currentHP + } + } + } + + $PHASpec = [VMware.Vim.ClusterInfraUpdateHaConfigInfo] @{ + enabled = $true + behavior = $ClusterMode + moderateRemediation = $ModerateRemediation + severeRemediation = $SevereRemediation + providers = $healthProviders + } + + $spec = [VMware.Vim.ClusterConfigSpecEx] @{ + infraUpdateHaConfig = $PHASpec + } + + Write-Host "Disabling Proactive HA Provider $ProviderId on $Cluster ..." + $task = $ClusterView.ReconfigureComputeResource_Task($spec,$True) + $task1 = Get-Task -Id ("Task-$($task.value)") + $task1 | Wait-Task | Out-Null + } catch { + Write-host -ForegroundColor Red $Error[0].Exception + } + + $ClusterView.UpdateViewData() + + try { + $entities = @() + foreach ($vmhost in $vmhosts) { + if($healthManager.HasMonitoredEntity($ProviderId,$vmhost)) { + $entities += $vmhost + } + } + + Write-Host "Disabling Proactive HA monitoring for all ESXi hosts in cluster ..." + $healthManager.RemoveMonitoredEntities($ProviderId,$entities) + } catch { + Write-host -ForegroundColor Red $Error[0].Exception + } + } +} + +Function Get-PHAConfig { +<# + .NOTES + =========================================================================== + Created by: William Lam + Organization: VMware + Blog: www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + .DESCRIPTION + Function to retrieve Proactive HA configuration for a vSphere Cluster + .PARAMETER Cluster + Name of the vSphere Cluster to check Proactive HA configuration + .EXAMPLE + Get-PHAConfig -Cluster VSAN-Cluster +#> + param( + [Parameter(Mandatory=$true)][String]$Cluster + ) + + $ClusterView = Get-View -ViewType ClusterComputeResource -Property Name,ConfigurationEx -Filter @{"Name" = $Cluster} + + if($ClusterView -eq $null) { + Write-Host -ForegroundColor Red "Unable to find vSphere Cluster $cluster ..." + break + } + + if($ClusterView.ConfigurationEx.InfraUpdateHaConfig.Providers -ne $null) { + $healthManager = Get-View $global:DefaultVIServer.ExtensionData.Content.HealthUpdateManager + + $phSettings = $ClusterView.ConfigurationEx.InfraUpdateHaConfig + $providers = $ClusterView.ConfigurationEx.InfraUpdateHaConfig.Providers + $healthProviders = @() + foreach ($provider in $providers) { + $providerName = $healthManager.QueryProviderName($provider) + $healthProviders+=$providerName + } + + $pHAConfig = [pscustomobject] @{ + Enabled = $phSettings.Enabled + ClusterMode = $phSettings.behavior + ModerateRemediation = $phSettings.ModerateRemediation + SevereRemediation = $phSettings.SevereRemediation + HealthProviders = $healthProviders + } + $pHAConfig + } else { + Write-Host "Proactive HA has not been configured on this vSphere Cluster" + } +} + +Function Get-PHAHealth { +<# + .NOTES + =========================================================================== + Created by: William Lam + Organization: VMware + Blog: www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + .DESCRIPTION + Function to retrieve the Proactive HA health info for all ESXi hosts in vSphere Cluster + .PARAMETER Cluster + Name of the vSphere Cluster to check Proactive HA health information + .EXAMPLE + Get-PHAHealth -Cluster VSAN-Cluster +#> + param( + [Parameter(Mandatory=$true)][String]$Cluster + ) + + $ClusterView = Get-View -ViewType ClusterComputeResource -Property Name,ConfigurationEx -Filter @{"Name" = $Cluster} + + if($ClusterView -eq $null) { + Write-Host -ForegroundColor Red "Unable to find vSphere Cluster $cluster ..." + break + } + + if($ClusterView.ConfigurationEx.InfraUpdateHaConfig.Providers -ne $null) { + $healthManager = Get-View $global:DefaultVIServer.ExtensionData.Content.HealthUpdateManager + + $providers = $ClusterView.ConfigurationEx.InfraUpdateHaConfig.Providers + + foreach ($provider in $providers) { + $providerName = $healthManager.QueryProviderName($provider) + $healthUpdates = $healthManager.QueryHealthUpdates($provider) + + $healthResults = @() + Write-Host -NoNewline -ForegroundColor Magenta "Health summary for Proactive HA Provider $providerName`:`n" + foreach ($healthUpdate in $healthUpdates) { + $vmhost = Get-View $healthUpdate.Entity + + $hr = [PSCustomObject] @{ + Entity = $vmhost.name + Status = $healthUpdate.status + HealthComponentId = $healthUpdate.HealthUpdateInfoId + HealthUpdateId = $healthUpdate.Id + Remediation = $healthUpdate.Remediation + } + $healthResults+=$hr + } + $healthResults + } + } else { + Write-Host "Proactive HA has not been configured on this vSphere Cluster" + } +} + +Function New-PHASimulation { +<# + .NOTES + =========================================================================== + Created by: William Lam + Organization: VMware + Blog: www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + .DESCRIPTION + Function to return VCHA Configuration + .PARAMETER ProviderId + The Proactive HA Provider ID that you like to simulate a health update from + .PARAMETER EsxiHost + The name of ESXi host to update the health on + .PARAMETER Component + The name of the matching component ID from Proactive HA Provider to simulate a health update from + .PARAMETER HealthStatus + The health value (green, yellow or red) for the given simulated health Update + .PARAMETER Remediation + The remediation message associated with simulated health update + .EXAMPLE + New-PHASimulation -EsxiHost vesxi65-4.primp-industries.com -Component Power -HealthStatus green -Remediation "" -ProviderId "52 85 22 c2 f2 6a e7 b9-fc ff 63 9e 10 81 00 79" + .EXAMPLE + New-PHASimulation -EsxiHost vesxi65-4.primp-industries.com -Component Power -HealthStatus red -Remediation "Please replace my virtual PSU" -ProviderId "52 85 22 c2 f2 6a e7 b9-fc ff 63 9e 10 81 00 79" +#> + param( + [Parameter(Mandatory=$true)][String]$ProviderId, + [Parameter(Mandatory=$true)][String]$EsxiHost, + [Parameter(Mandatory=$true)][String]$Component, + [Parameter(Mandatory=$true)][ValidateSet("green","red","yellow")][String]$HealthStatus, + [Parameter(Mandatory=$false)][String]$Remediation + ) + + Write-Host -ForegroundColor Red "`n******************** DISCLAIMER ********************" + Write-Host -ForegroundColor Red "**** THIS IS NOT INTENDED FOR PRODUCTION USE ****" + Write-Host -ForegroundColor Red "**** LEARNING PURPOSES ONLY ****" + Write-Host -ForegroundColor Red "******************** DISCLAIMER ********************`n" + + $vmhost = Get-View -ViewType HostSystem -Property Name -Filter @{"name" = $EsxiHost} + + if($vmhost -eq $null) { + Write-Host -ForegroundColor Red "`nUnable to find ESXi host $EsxiHost ..." + break + } + + $healthManager = Get-View $global:DefaultVIServer.ExtensionData.Content.HealthUpdateManager + + # Randomly generating an ID for Health Update + # In general, you would want to generate a specific ID + # which can be referenced between ProactiveHA Provider + # and VMware logs for troubleshooting purposes + $HealthUpdateID = "vghetto-" + (Get-Random -Minimum 1 -Maximum 100000) + + # All other Health Status can have a remediation message + # but for green, it must be an empty string or API call will fail + if($HealthStatus -eq "green") { + $Remediation = "" + } + + $healthUpdate = [VMware.Vim.HealthUpdate] @{ + Entity = $vmhost.moref + HealthUpdateInfoId = $Component + Id = $HealthUpdateId + Status = $HealthStatus + Remediation = $Remediation + } + + try { + Write-Host "`nSimulating Proactive HA Health Update to ..." + Write-Host "`tHost: $EsxiHost " + Write-Host -NoNewline "`tStatus: " + Write-Host -ForegroundColor $HealthStatus "$HealthStatus" + Write-Host "`tRemediation Messsage: $Remediation" + $healthManager.PostHealthUpdates($providerId,$healthUpdate) + } catch { + Write-host -ForegroundColor Red $Error[0].Exception + } +} \ No newline at end of file diff --git a/Modules/Recommend-Sizing/Recommend-Sizing.psm1 b/Modules/Recommend-Sizing/Recommend-Sizing.psm1 new file mode 100644 index 0000000..0075e43 --- /dev/null +++ b/Modules/Recommend-Sizing/Recommend-Sizing.psm1 @@ -0,0 +1,227 @@ +function Recommend-Sizing { +<# + .NOTES + =========================================================================== + Created by: Markus Kraus + Twitter: @VMarkus_K + Private Blog: mycloudrevolution.com + =========================================================================== + Changelog: + 2016.11 ver 1.0 Base Release + 2016.11 ver 1.1 Optional Stats Collection + 2016.11 ver 1.2 VM Stats from Realtime Data and new Counters + =========================================================================== + External Code Sources: + http://www.lucd.info/2011/04/22/get-the-maximum-iops/ + https://communities.vmware.com/thread/485386 + =========================================================================== + Tested Against Environment: + vSphere Version: 5.5 U2, 6.0 + PowerCLI Version: PowerCLI 6.3 R1, PowerCLI 6.5 R1 + PowerShell Version: 4.0, 5.0 + OS Version: Windows 8.1, Server 2012 R2 + =========================================================================== + Keywords vSphere, ESXi, VM, Storage, Sizing + =========================================================================== + + .DESCRIPTION + This Function collects Basic vSphere Informations for a Hardware Sizing Recommandation. Focus is in Compute Ressources. + + .Example + Recommend-Sizing -ClusterNames Cluster01, Cluster02 -Stats -StatsRange 60 -Verbose + + .Example + Recommend-Sizing -ClusterNames Cluster01, Cluster02 + + .Example + Recommend-Sizing -ClusterNames Cluster01 + + .PARAMETER ClusterNames + List of your vSphere Cluser Names to process. + + .PARAMETER Stats + Enables Stats Collection. + + Warning: At the moment this is only fully tested with vSphere 5.5 and vSphere 6.5! + + .PARAMETER StatsRange + Time Range in Minutes for the Stats Collection. + Default is 24h. + +#Requires PS -Version 4.0 +#Requires -Modules VMware.VimAutomation.Core, @{ModuleName="VMware.VimAutomation.Core";ModuleVersion="6.3.0.0"} +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory=$True, ValueFromPipeline=$False, Position=0)] + [Array] $ClusterNames, + [Parameter(Mandatory=$False, ValueFromPipeline=$False, Position=1, ParameterSetName = "Stats")] + [switch] $Stats, + [Parameter(Mandatory=$False, ValueFromPipeline=$False, Position=2, ParameterSetName = "Stats")] + [int] $StatsRange = 1440 + +) +Begin { + if ($Stats) { + Write-Warning "Stats Collection requested.`nAt the moment this is only fully tested with vSphere 5.5 and vSphere 6.5" + [int]$TimeRange = "-" + $StatsRange + } + + $Validate = $True + #region: Check Clusters + Write-Verbose "$(Get-Date -Format G) Starting Cluster Validation..." + foreach ($ClusterName in $ClusterNames) { + $TestCluster = Get-Cluster -Name $ClusterName -ErrorAction SilentlyContinue -Verbose:$False + if(!($TestCluster)){ + Write-Warning "No Custer found wth Name $ClusterName!" + $Validate = $False + } + elseif ($TestCluster.count -gt 1) { + Write-Warning "Multiple Custers found wth Name $ClusterName!`nUse a List of explicit Cluster Names: Recommend-Sizing -ClusterNames Cluster01, Cluster02 " + $Validate = $False + } + } + Write-Verbose "$(Get-Date -Format G) Cluster Validation completed" + #endregion +} + +Process { + $MyView = @() + if ($Validate -eq $True) { + foreach ($ClusterName in $ClusterNames) { + #region: Get Cluster Objects + Write-Verbose "$(Get-Date -Format G) Collect $ClusterName Cluster Objects..." + $Cluster = Get-Cluster -Name $ClusterName -Verbose:$False + $ClusterVMs = $Cluster | Get-VM -Verbose:$False + $ClusterVMsPoweredOn = $ClusterVMs | where {$_.PowerState -eq "PoweredOn"} + $ClusterDatastores = $Cluster | Get-Datastore -Verbose:$False + $ClusterHosts = $Cluster | Get-VMHost -Verbose:$False + $HostsAverageMemoryUsageGB = [math]::round( ($ClusterHosts | Measure-Object -Average -Property MemoryUsageGB).Average,1 ) + $HostsAverageMemoryUsage = $([math]::round( (($ClusterHosts | Measure-Object -Average -Property MemoryUsageGB).Average / ($ClusterHosts | Measure-Object -Average -Property MemoryTotalGB).Average) * 100,1 )) + $HostsAverageCpuUsageMhz = [math]::round( ($ClusterHosts | Measure-Object -Average -Property CpuUsageMhz).Average,1 ) + $HostsAverageCpuUsage = $([math]::round( (($ClusterHosts | Measure-Object -Average -Property CpuUsageMhz).Average / ($ClusterHosts | Measure-Object -Average -Property CpuTotalMhz).Average) * 100,1 )) + Write-Verbose "$(Get-Date -Format G) Collect $($Cluster.name) Cluster Objects completed" + #endregion + + #region: CPU Calculation + Write-Verbose "$(Get-Date -Format G) Collect $($Cluster.name) CPU Details..." + $VMvCPUs = ($ClusterVMs | Measure-Object -Sum -Property NumCpu).sum + $LogicalThreads = $Cluster.ExtensionData.Summary.NumCpuThreads + $CpuCores = $Cluster.ExtensionData.Summary.NumCpuCores + $vCPUpCPUratio = [math]::round( $VMvCPUs / $LogicalThreads,1 ) + Write-Verbose "$(Get-Date -Format G) Collect $($Cluster.name) CPU Details completed." + #endregion + + #region: Memory Calculation + Write-Verbose "$(Get-Date -Format G) Collect $($Cluster.name) Memory Details..." + $AllocatedVMMemoryGB = [math]::round( ($ClusterVMs | Measure-Object -Sum -Property MemoryGB).sum ) + $PhysicalMemory = [math]::round( $Cluster.ExtensionData.Summary.TotalMemory / 1073741824,1 ) + $MemoryUsage = [math]::round( ($AllocatedVMMemoryGB / $PhysicalMemory) * 100 ,1 ) + Write-Verbose "$(Get-Date -Format G) Collect $($Cluster.name) Memory Details completed" + #endregion + + if ($Stats) { + #region: Creating VM Stats + Write-Verbose "$(Get-Date -Format G) Create $($Cluster.name) VM Stats..." + $VMMetrics = "disk.numberwrite.summation","disk.numberread.summation","cpu.usage.average", "mem.usage.average" + $Start = (Get-Date).AddMinutes($TimeRange) + $VMStats = Get-Stat -Realtime -Stat $VMMetrics -Entity $ClusterVMsPoweredOn -Start $Start -Verbose:$False + Write-Verbose "$(Get-Date -Format G) Create $($Cluster.name) VM Stats completed" + #endregion + + #region: Creating VM Stats Report + Write-Verbose "$(Get-Date -Format G) Process $($Cluster.name) VM Stats Report..." + $ReportVMPerf = @() + $ReportVMPerf = $VMStats | Group-Object -Property {$_.Entity.Name},Instance | %{ + New-Object PSObject -Property @{ + IOPSWriteAvg = ($_.Group | ` + where{$_.MetricId -eq "disk.numberwrite.summation"} | ` + Measure-Object -Property Value -Average).Average + IOPSReadAvg = ($_.Group | ` + where{$_.MetricId -eq "disk.numberread.summation"} | ` + Measure-Object -Property Value -Average).Average + CPUUsageAvg = ($_.Group | ` + where{$_.MetricId -eq "cpu.usage.average"} | ` + Measure-Object -Property Value -Average).Average + MEMUsageAvg = ($_.Group | ` + where{$_.MetricId -eq "mem.usage.average"} | ` + Measure-Object -Property Value -Average).Average + } + } + Write-Verbose "$(Get-Date -Format G) Process $($Cluster.name) VM Stats Report completed" + #endregion + } + else { + Write-Verbose "$(Get-Date -Format G) Stats Collection skipped..." + } + + #region: Create VM Disk Space Report + Write-Verbose "$(Get-Date -Format G) Process $($Cluster.name) VM Disk Space Report..." + $reportDiskSpace = @() + foreach ($ClusterVM in $ClusterVMs){ + $VMDKs = $ClusterVM | get-HardDisk -Verbose:$False + foreach ($VMDK in $VMDKs) { + if ($VMDK -ne $null){ + [int]$CapacityGB = $VMDK.CapacityKB/1024/1024 + $Report = [PSCustomObject] @{ + CapacityGB = $CapacityGB + } + $reportDiskSpace += $Report + } + } + } + Write-Verbose "$(Get-Date -Format G) Process $($Cluster.name) VM Disk Space Report completed" + #endregion + + #region: Create Datastore Space Report + Write-Verbose "$(Get-Date -Format G) Process $($Cluster.name) Datastore Space Report..." + $DatastoreReport = @($ClusterDatastores | Select-Object @{N="CapacityGB";E={[math]::Round($_.CapacityGB,2)}}, @{N="FreeSpaceGB";E={[math]::Round($_.FreeSpaceGB,2)}}, @{N="UsedSpaceGB";E={[math]::Round($_.CapacityGB - $_.FreeSpaceGB,2)}}) + Write-Verbose "$(Get-Date -Format G) Process $($Cluster.name) Datastore Space Report completed" + #endregion + + #region: Create Global Report + Write-Verbose "$(Get-Date -Format G) Process Global Report..." + $SizingReport = [PSCustomObject] @{ + Cluster = $Cluster.name + HAEnabled = $Cluster.HAEnabled + DrsEnabled = $Cluster.DrsEnabled + Hosts = $Cluster.ExtensionData.Summary.NumHosts + HostsAverageMemoryUsageGB = $HostsAverageMemoryUsageGB + HostsAverageMemoryUsage = "$HostsAverageMemoryUsage %" + HostsAverageCpuUsageMhz = $HostsAverageCpuUsageMhz + HostsAverageCpuUsage = "$HostsAverageCpuUsage %" + PhysicalCPUCores = $CpuCores + LogicalCPUThreads = $LogicalThreads + VMs = $ClusterVMs.count + ActiveVMs = $ClusterVMsPoweredOn.count + VMvCPUs = $VMvCPUs + vCPUpCPUratio = "$vCPUpCPUratio : 1" + PhysicalMemoryGB = $PhysicalMemory + AllocatedVMMemoryGB = $AllocatedVMMemoryGB + ClusterMemoryUsage = "$MemoryUsage %" + SumVMDiskSpaceGB = [math]::round( ($reportDiskSpace | Measure-Object -Sum -Property CapacityGB).sum, 1 ) + SumDatastoreSpaceGB = [math]::round( ($DatastoreReport | Measure-Object -Sum -Property CapacityGB).sum, 1 ) + SumDatastoreUsedSpaceGB = [math]::round( ($DatastoreReport | Measure-Object -Sum -Property UsedSpaceGB).sum, 1 ) + AverageVMIOPSWriteAvg = [math]::round( ($ReportVMPerf | Measure-Object -Average -Property IOPSWriteAvg).Average,1 ) + AverageVMIOPSReadAvg = [math]::round( ($ReportVMPerf | Measure-Object -Average -Property IOPSReadAvg).Average,1 ) + AverageVMCPUUsageAvg = "$([math]::round( ($ReportVMPerf | Measure-Object -Average -Property CPUUsageAvg).Average,1 )) %" + AverageVMMEMUsageAvg = "$([math]::round( ($ReportVMPerf | Measure-Object -Average -Property MEMUsageAvg).Average,1 )) %" + } + $MyView += $SizingReport + Write-Verbose "$(Get-Date -Format G) Process Global Report completed" + #endregion + } + + } + Else { + Write-Error "Validation Failed! Processing Skipped" + } + + } + + End { + $MyView + } + +} \ No newline at end of file diff --git a/Modules/Set-CBT/Set-CBT.psm1 b/Modules/Set-CBT/Set-CBT.psm1 new file mode 100644 index 0000000..784aebb --- /dev/null +++ b/Modules/Set-CBT/Set-CBT.psm1 @@ -0,0 +1,111 @@ +function Set-CBT { +<# + .NOTES + =========================================================================== + Created by: Markus Kraus + Twitter: @VMarkus_K + Private Blog: mycloudrevolution.com + =========================================================================== + Changelog: + 2016.11 ver 1.0 Base Release + =========================================================================== + External Code Sources: + http://wahlnetwork.com/2015/12/01/change-block-tracking-cbt-powercli/ + =========================================================================== + Tested Against Environment: + vSphere Version: 5.5 U2 + PowerCLI Version: PowerCLI 6.3 R1 + PowerShell Version: 4.0 + OS Version: Windows Server 2012 R2 + =========================================================================== + Keywords vSphere, ESXi, VM, Storage, CBT, Backup + =========================================================================== + + .DESCRIPTION + This Function enables or disables CBT. + + .Example + Get-VN TST* | Set-CBT -DisableCBT + + .Example + Get-VN TST* | Set-CBT -EnableCBT + + .PARAMETER DisableCBT + Disables CBT for any VMs found with it enabled + + .PARAMETER EnableCBT + Enables CBT for any VMs found with it disabled + +#Requires PS -Version 4.0 +#Requires -Modules VMware.VimAutomation.Core, @{ModuleName="VMware.VimAutomation.Core";ModuleVersion="6.3.0.0"} +#> + + [CmdletBinding()] + param( + [Parameter(Mandatory=$True, ValueFromPipeline=$True, Position=0, HelpMessage = "VMs to process")] + [ValidateNotNullorEmpty()] + [VMware.VimAutomation.ViCore.Impl.V1.Inventory.InventoryItemImpl[]] $myVMs, + [Parameter(Mandatory = $False,ValueFromPipeline=$False, Position = 1, HelpMessage = "Enables CBT for any VMs found with it disabled", ParameterSetName = "EnableCBT")] + [ValidateNotNullorEmpty()] + [Switch]$EnableCBT, + [Parameter(Mandatory = $False,ValueFromPipeline=$False, Position = 1, HelpMessage = "Disables CBT for any VMs found with it enabled", ParameterSetName = "DisableCBT")] + [ValidateNotNullorEmpty()] + [Switch]$DisableCBT + ) +Process { + + $vmconfigspec = New-Object -TypeName VMware.Vim.VirtualMachineConfigSpec + Write-Verbose -Message "Walking through given VMs" + foreach($myVM in $myVMs) + { + if ($DisableCBT -and $myVM.ExtensionData.Config.ChangeTrackingEnabled -eq $true -and $myVM.ExtensionData.Snapshot -eq $null) + { + try + { + Write-Verbose -Message "Reconfiguring $($myVM.name) to disable CBT" -Verbose + $vmconfigspec.ChangeTrackingEnabled = $false + $myVM.ExtensionData.ReconfigVM($vmconfigspec) + + if ($myVM.PowerState -eq "PoweredOn" ) { + Write-Verbose -Message "Creating a snapshot on $($myVM.name) to clear CBT file" -Verbose + $SnapShot = New-Snapshot -VM $myVM -Name "CBT Cleanup" + + Write-Verbose -Message "Removing snapshot on $($myVM.name)" -Verbose + $SnapShot| Remove-Snapshot -Confirm:$false + } + + } + catch + { + throw $myVM + } + } + elseif ($EnableCBT -and $myVM.ExtensionData.Config.ChangeTrackingEnabled -eq $false -and $myVM.ExtensionData.Snapshot -eq $null) + { + Write-Verbose -Message "Reconfiguring $($myVM.name) to enable CBT" -Verbose + $vmconfigspec.ChangeTrackingEnabled = $true + $myVM.ExtensionData.ReconfigVM($vmconfigspec) + + if ($myVM.PowerState -eq "PoweredOn" ) { + Write-Verbose -Message "Creating a snapshot on $($myVM.name) to Create CBT file" -Verbose + $SnapShot = New-Snapshot -VM $myVM -Name "CBT Cleanup" + + Write-Verbose -Message "Removing snapshot on $($myVM.name)" -Verbose + $SnapShot | Remove-Snapshot -Confirm:$false + } + } + else + { + if ($myVM.ExtensionData.Snapshot -ne $null -and $EnableCBT) + { + Write-Warning -Message "Skipping $($myVM.name) - Snapshots found" + } + elseif ($myVM.ExtensionData.Snapshot -ne $null -and $DisableCBT) + { + Write-Warning -Message "Skipping $($myVM.name) - Snapshots found" + } + } + } + + } +} diff --git a/Modules/Start-UNMAP/Start-UNMAP.psm1 b/Modules/Start-UNMAP/Start-UNMAP.psm1 new file mode 100644 index 0000000..a8e9896 --- /dev/null +++ b/Modules/Start-UNMAP/Start-UNMAP.psm1 @@ -0,0 +1,99 @@ +function Start-UNMAP { +<# + .SYNOPSIS + Process SCSI UNMAP on VMware Datastores + + .DESCRIPTION + This Function will process SCSI UNMAP on VMware Datastores via ESXCLI -V2 + + .Example + Start-UNMAP -ClusterName myCluster -DSWildcard *RAID5* + + .Example + Start-UNMAP -ClusterName myCluster -DSWildcard *RAID5* -Verbose -WhatIf + + .Notes + NAME: Start-UNMAP.psm1 + AUTHOR: Markus Kraus + LASTEDIT: 23.09.2016 + VERSION: 1.0 + KEYWORDS: VMware, vSphere, ESXi, SCSI, VAAI, UNMAP + + .Link + http://mycloudrevolution.com/ + + #Requires PS -Version 4.0 + #Requires -Modules VMware.VimAutomation.Core, @{ModuleName="VMware.VimAutomation.Core";ModuleVersion="6.3.0.0"} + #> + + [CmdletBinding(SupportsShouldProcess = $true,ConfirmImpact='High')] + param( + [Parameter(Mandatory=$true, Position=0)] + [String]$ClusterName, + [Parameter(Mandatory=$true, Position=1)] + [String]$DSWildcard + ) + Process { + $Validate = $true + #region: PowerCLI Session Timeout + Write-Verbose "Set Session Timeout ..." + $initialTimeout = (Get-PowerCLIConfiguration -Scope Session).WebOperationTimeoutSeconds + Set-PowerCLIConfiguration -Scope Session -WebOperationTimeoutSeconds -1 -Confirm:$False | Out-Null + #endregion + + #region: Get Cluster + $Cluster = Get-Cluster -Name $ClusterName -ErrorAction SilentlyContinue + Write-Verbose "vSphere Cluster: $Cluster" + if (!$Cluster){Write-Error "No Cluster found!"; $Validate = $false} + #endregion + + #region: Get Hosts + $ClusterHosts = $Cluster | Get-VMHost -ErrorAction SilentlyContinue | where {$_.ConnectionState -eq "Connected" -and $_.PowerState -eq "PoweredOn"} + Write-Verbose "vSphere Cluster Hosts: $ClusterHosts" + if (!$ClusterHosts){Write-Error "No Hosts found!"; $Validate = $false} + #endregion + + #region: Get Datastores + $ClusterDataStores = $Cluster | Get-Datastore -ErrorAction SilentlyContinue | where {$_.Name -like $DSWildcard -and $_.State -eq "Available" -and $_.Accessible -eq "True"} + Write-Verbose "vSphere Cluster Datastores: $ClusterDataStores" + if (!$ClusterDataStores){Write-Error "No Datastores found!"; $Validate = $false} + #endregion + + #region: Process Datastores + if ($Validate -eq $true) { + Write-Verbose "Starting Loop..." + foreach ($ClusterDataStore in $ClusterDataStores) { + Write-Verbose "vSphere Datastore to Process: $ClusterDataStore" + $myHost = $ClusterHosts[(Get-Random -Maximum ($ClusterHosts).count)] + Write-Verbose "vSphere Host to Process: $myHost" + $esxcli2 = $myHost | Get-ESXCLI -V2 + $arguments = $esxcli2.storage.vmfs.unmap.CreateArgs() + $arguments.volumelabel = $ClusterDataStore + $arguments.reclaimunit = "256" + if ($PSCmdlet.ShouldProcess( $ClusterDataStore,"Starting UNMAP on $myHost")) { + $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() + try { + Write-Output "Starting UNMAP for $ClusterDataStore on $myHost..." + $esxcli2.storage.vmfs.unmap.Invoke($arguments) + } + catch { + Write-Output "A Error occured: " "" $error[0] "" + } + $stopwatch.Stop() + Write-Output "UNMAP duration: $($stopwatch.Elapsed.Minutes)" + } + + } + } + else { + Write-Error "Validation Failed. Processing Loop Skipped!" + } + #endregion + + #region: Revert PowerCLI Session Timeout + Write-Verbose "Revert Session Timeout ..." + Set-PowerCLIConfiguration -Scope Session -WebOperationTimeoutSeconds $initialTimeout -Confirm:$False | Out-Null + #endregion + } + +} diff --git a/Modules/VAMI/VAMI.psm1 b/Modules/VAMI/VAMI.psm1 new file mode 100755 index 0000000..92c5d5f --- /dev/null +++ b/Modules/VAMI/VAMI.psm1 @@ -0,0 +1,716 @@ +Function Get-VAMISummary { +<# + .NOTES + =========================================================================== + Created by: William Lam + Organization: VMware + Blog: www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + .SYNOPSIS + This function retrieves some basic information from VAMI interface (5480) + for a VCSA node which can be an Embedded VCSA, External PSC or External VCSA. + .DESCRIPTION + Function to return basic VAMI summary info + .EXAMPLE + Connect-CisServer -Server 192.168.1.51 -User administrator@vsphere.local -Password VMware1! + Get-VAMISummary +#> + $systemVersionAPI = Get-CisService -Name 'com.vmware.appliance.system.version' + $results = $systemVersionAPI.get() | select product, type, version, build, install_time + + $systemUptimeAPI = Get-CisService -Name 'com.vmware.appliance.system.uptime' + $ts = [timespan]::fromseconds($systemUptimeAPI.get().toString()) + $uptime = $ts.ToString("hh\:mm\:ss\,fff") + + $summaryResult = [pscustomobject] @{ + Product = $results.product; + Type = $results.type; + Version = $results.version; + Build = $results.build; + InstallTime = $results.install_time; + Uptime = $uptime + } + $summaryResult +} + +Function Get-VAMIHealth { +<# + .NOTES + =========================================================================== + Created by: William Lam + Organization: VMware + Blog: www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + .SYNOPSIS + This function retrieves health information from VAMI interface (5480) + for a VCSA node which can be an Embedded VCSA, External PSC or External VCSA. + .DESCRIPTION + Function to return VAMI health + .EXAMPLE + Connect-CisServer -Server 192.168.1.51 -User administrator@vsphere.local -Password VMware1! + Get-VAMIHealth +#> + $healthOverall = (Get-CisService -Name 'com.vmware.appliance.health.system').get() + $healthLastCheck = (Get-CisService -Name 'com.vmware.appliance.health.system').lastcheck() + $healthCPU = (Get-CisService -Name 'com.vmware.appliance.health.load').get() + $healthMem = (Get-CisService -Name 'com.vmware.appliance.health.mem').get() + $healthSwap = (Get-CisService -Name 'com.vmware.appliance.health.swap').get() + $healthStorage = (Get-CisService -Name 'com.vmware.appliance.health.storage').get() + + # DB health only applicable for Embedded/External VCSA Node + $vami = (Get-CisService -Name 'com.vmware.appliance.system.version').get() + + if($vami.type -eq "vCenter Server with an embedded Platform Services Controller" -or $vami.type -eq "vCenter Server with an external Platform Services Controller") { + $healthVCDB = (Get-CisService -Name 'com.vmware.appliance.health.databasestorage').get() + } else { + $healthVCDB = "N/A" + } + $healthSoftwareUpdates = (Get-CisService -Name 'com.vmware.appliance.health.softwarepackages').get() + + $healthResult = [pscustomobject] @{ + HealthOverall = $healthOverall; + HealthLastCheck = $healthLastCheck; + HealthCPU = $healthCPU; + HealthMem = $healthMem; + HealthSwap = $healthSwap; + HealthStorage = $healthStorage; + HealthVCDB = $healthVCDB; + HealthSoftware = $healthSoftwareUpdates + } + $healthResult +} + +Function Get-VAMIAccess { +<# + .NOTES + =========================================================================== + Created by: William Lam + Organization: VMware + Blog: www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + .SYNOPSIS + This function retrieves access information from VAMI interface (5480) + for a VCSA node which can be an Embedded VCSA, External PSC or External VCSA. + .DESCRIPTION + Function to return VAMI access interfaces (Console,DCUI,Bash Shell & SSH) + .EXAMPLE + Connect-CisServer -Server 192.168.1.51 -User administrator@vsphere.local -Password VMware1! + Get-VAMIAccess +#> + $consoleAccess = (Get-CisService -Name 'com.vmware.appliance.access.consolecli').get() + $dcuiAccess = (Get-CisService -Name 'com.vmware.appliance.access.dcui').get() + $shellAccess = (Get-CisService -Name 'com.vmware.appliance.access.shell').get() + $sshAccess = (Get-CisService -Name 'com.vmware.appliance.access.ssh').get() + + $accessResult = New-Object PSObject -Property @{ + Console = $consoleAccess; + DCUI = $dcuiAccess; + BashShell = $shellAccess.enabled; + SSH = $sshAccess + } + $accessResult +} + +Function Get-VAMITime { +<# + .NOTES + =========================================================================== + Created by: William Lam + Organization: VMware + Blog: www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + .SYNOPSIS + This function retrieves the time and NTP info from VAMI interface (5480) + for a VCSA node which can be an Embedded VCSA, External PSC or External VCSA. + .DESCRIPTION + Function to return current Time and NTP information + .EXAMPLE + Connect-CisServer -Server 192.168.1.51 -User administrator@vsphere.local -Password VMware1! + Get-VAMITime +#> + $systemTimeAPI = Get-CisService -Name 'com.vmware.appliance.system.time' + $timeResults = $systemTimeAPI.get() + + $timeSync = (Get-CisService -Name 'com.vmware.appliance.techpreview.timesync').get() + $timeSyncMode = $timeSync.mode + + $timeResult = [pscustomobject] @{ + Timezone = $timeResults.timezone; + Date = $timeResults.date; + CurrentTime = $timeResults.time; + Mode = $timeSyncMode; + NTPServers = "N/A"; + NTPStatus = "N/A"; + } + + if($timeSyncMode -eq "NTP") { + $ntpServers = (Get-CisService -Name 'com.vmware.appliance.techpreview.ntp').get() + $timeResult.NTPServers = $ntpServers.servers + $timeResult.NTPStatus = $ntpServers.status + } + $timeResult +} + +Function Get-VAMINetwork { +<# + .NOTES + =========================================================================== + Created by: William Lam + Organization: VMware + Blog: www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + .SYNOPSIS + This function retrieves network information from VAMI interface (5480) + for a VCSA node which can be an Embedded VCSA, External PSC or External VCSA. + .DESCRIPTION + Function to return networking information including details for each interface + .EXAMPLE + Connect-CisServer -Server 192.168.1.51 -User administrator@vsphere.local -Password VMware1! + Get-VAMINetwork +#> + $netResults = @() + + $Hostname = (Get-CisService -Name 'com.vmware.appliance.networking.dns.hostname').get() + $dns = (Get-CisService -Name 'com.vmware.appliance.networking.dns.servers').get() + + Write-Host "Hostname: " $hostname + Write-Host "DNS Servers: " $dns.servers + + $interfaces = (Get-CisService -Name 'com.vmware.appliance.networking.interfaces').list() + foreach ($interface in $interfaces) { + $ipv4API = (Get-CisService -Name 'com.vmware.appliance.techpreview.networking.ipv4') + $spec = $ipv4API.Help.get.interfaces.CreateExample() + $spec+= $interface.name + $ipv4result = $ipv4API.get($spec) + + $interfaceResult = [pscustomobject] @{ + Inteface = $interface.name; + MAC = $interface.mac; + Status = $interface.status; + Mode = $ipv4result.mode; + IP = $ipv4result.address; + Prefix = $ipv4result.prefix; + Gateway = $ipv4result.default_gateway; + Updateable = $ipv4result.updateable + } + $netResults += $interfaceResult + } + $netResults +} + +Function Get-VAMIDisks { +<# + .NOTES + =========================================================================== + Created by: William Lam + Organization: VMware + Blog: www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + .SYNOPSIS + This function retrieves VMDK disk number to partition mapping VAMI interface (5480) + for a VCSA node which can be an Embedded VCSA, External PSC or External VCSA. + .DESCRIPTION + Function to return VMDK disk number to OS partition mapping + .EXAMPLE + Connect-CisServer -Server 192.168.1.51 -User administrator@vsphere.local -Password VMware1! + Get-VAMIDisks +#> + $storageAPI = Get-CisService -Name 'com.vmware.appliance.system.storage' + $disks = $storageAPI.list() + + foreach ($disk in $disks | sort {[int]$_.disk.toString()}) { + $disk | Select Disk, Partition + } +} + +Function Start-VAMIDiskResize { +<# + .NOTES + =========================================================================== + Created by: William Lam + Organization: VMware + Blog: www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + .SYNOPSIS + This function triggers an OS partition resize after adding additional disk capacity + for a VCSA node which can be an Embedded VCSA, External PSC or External VCSA. + .DESCRIPTION + Function triggers OS partition resize operation + .EXAMPLE + Connect-CisServer -Server 192.168.1.51 -User administrator@vsphere.local -Password VMware1! + Start-VAMIDiskResize +#> + $storageAPI = Get-CisService -Name 'com.vmware.appliance.system.storage' + Write-Host "Initiated OS partition resize operation ..." + $storageAPI.resize() +} + +Function Get-VAMIStatsList { +<# + .NOTES + =========================================================================== + Created by: William Lam + Organization: VMware + Blog: www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + .SYNOPSIS + This function retrieves list avialable monitoring metrics in VAMI interface (5480) + for a VCSA node which can be an Embedded VCSA, External PSC or External VCSA. + .DESCRIPTION + Function to return list of available monitoring metrics that can be queried + .EXAMPLE + Connect-CisServer -Server 192.168.1.51 -User administrator@vsphere.local -Password VMware1! + Get-VAMIStatsList +#> + $monitoringAPI = Get-CisService -Name 'com.vmware.appliance.monitoring' + $ids = $monitoringAPI.list() | Select id | Sort-Object -Property id + + foreach ($id in $ids) { + $id + } +} + +Function Get-VAMIStorageUsed { +<# + .NOTES + =========================================================================== + Created by: William Lam + Organization: VMware + Blog: www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + .SYNOPSIS + This function retrieves the individaul OS partition storage utilization + for a VCSA node which can be an Embedded VCSA, External PSC or External VCSA. + .DESCRIPTION + Function to return individual OS partition storage utilization + .EXAMPLE + Connect-CisServer -Server 192.168.1.51 -User administrator@vsphere.local -Password VMware1! + Get-VAMIStorageUsed +#> + $monitoringAPI = Get-CisService 'com.vmware.appliance.monitoring' + $querySpec = $monitoringAPI.help.query.item.CreateExample() + + # List of IDs from Get-VAMIStatsList to query + $querySpec.Names = @( + "storage.used.filesystem.autodeploy", + "storage.used.filesystem.boot", + "storage.used.filesystem.coredump", + "storage.used.filesystem.imagebuilder", + "storage.used.filesystem.invsvc", + "storage.used.filesystem.log", + "storage.used.filesystem.netdump", + "storage.used.filesystem.root", + "storage.used.filesystem.updatemgr", + "storage.used.filesystem.vcdb_core_inventory", + "storage.used.filesystem.vcdb_seat", + "storage.used.filesystem.vcdb_transaction_log", + "storage.totalsize.filesystem.autodeploy", + "storage.totalsize.filesystem.boot", + "storage.totalsize.filesystem.coredump", + "storage.totalsize.filesystem.imagebuilder", + "storage.totalsize.filesystem.invsvc", + "storage.totalsize.filesystem.log", + "storage.totalsize.filesystem.netdump", + "storage.totalsize.filesystem.root", + "storage.totalsize.filesystem.updatemgr", + "storage.totalsize.filesystem.vcdb_core_inventory", + "storage.totalsize.filesystem.vcdb_seat", + "storage.totalsize.filesystem.vcdb_transaction_log" + ) + + # Tuple (Filesystem Name, Used, Total) to store results + $storageStats = @{ + "autodeploy"=@{"name"="/storage/autodeploy";"used"=0;"total"=0}; + "boot"=@{"name"="/boot";"used"=0;"total"=0}; + "coredump"=@{"name"="/storage/core";"used"=0;"total"=0}; + "imagebuilder"=@{"name"="/storage/imagebuilder";"used"=0;"total"=0}; + "invsvc"=@{"name"="/storage/invsvc";"used"=0;"total"=0}; + "log"=@{"name"="/storage/log";"used"=0;"total"=0}; + "netdump"=@{"name"="/storage/netdump";"used"=0;"total"=0}; + "root"=@{"name"="/";"used"=0;"total"=0}; + "updatemgr"=@{"name"="/storage/updatemgr";"used"=0;"total"=0}; + "vcdb_core_inventory"=@{"name"="/storage/db";"used"=0;"total"=0}; + "vcdb_seat"=@{"name"="/storage/seat";"used"=0;"total"=0}; + "vcdb_transaction_log"=@{"name"="/storage/dblog";"used"=0;"total"=0} + } + + $querySpec.interval = "DAY1" + $querySpec.function = "MAX" + $querySpec.start_time = ((get-date).AddDays(-1)) + $querySpec.end_time = (Get-Date) + $queryResults = $monitoringAPI.query($querySpec) | Select * -ExcludeProperty Help + + foreach ($queryResult in $queryResults) { + # Update hash if its used storage results + if($queryResult.name -match "used") { + $key = (($queryResult.name).toString()).split(".")[-1] + $value = [Math]::Round([int]($queryResult.data[1]).toString()/1MB,2) + $storageStats[$key]["used"] = $value + # Update hash if its total storage results + } else { + $key = (($queryResult.name).toString()).split(".")[-1] + $value = [Math]::Round([int]($queryResult.data[1]).toString()/1MB,2) + $storageStats[$key]["total"] = $value + } + } + + $storageResults = @() + foreach ($key in $storageStats.keys | Sort-Object -Property name) { + $statResult = [pscustomobject] @{ + Filesystem = $storageStats[$key].name; + Used = $storageStats[$key].used; + Total = $storageStats[$key].total + } + $storageResults += $statResult + } + $storageResults +} + +Function Get-VAMIService { +<# + .NOTES + =========================================================================== + Created by: William Lam + Organization: VMware + Blog: www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + .SYNOPSIS + This function retrieves list of services in VAMI interface (5480) + for a VCSA node which can be an Embedded VCSA, External PSC or External VCSA. + .DESCRIPTION + Function to return list of services and their description + .EXAMPLE + Connect-CisServer -Server 192.168.1.51 -User administrator@vsphere.local -Password VMware1! + Get-VAMIService + .EXAMPLE + Get-VAMIService -Name rbd +#> + param( + [Parameter( + Mandatory=$false, + ValueFromPipeline=$true, + ValueFromPipelineByPropertyName=$true) + ] + [String]$Name + ) + + if($Name -ne "") { + $vMonAPI = Get-CisService 'com.vmware.appliance.vmon.service' + + try { + $serviceStatus = $vMonAPI.get($name,0) + $serviceString = [pscustomobject] @{ + Name = $name; + State = $serviceStatus.state; + Health = ""; + Startup = $serviceStatus.startup_type + } + if($serviceStatus.health -eq $null) { $serviceString.Health = "N/A"} else { $serviceString.Health = $serviceStatus.health } + $serviceString + } catch { + Write-Error $Error[0].exception.Message + } + } else { + $vMonAPI = Get-CisService 'com.vmware.appliance.vmon.service' + $services = $vMonAPI.list_details() + + $serviceResult = @() + foreach ($key in $services.keys | Sort-Object -Property Value) { + $serviceString = [pscustomobject] @{ + Name = $key; + State = $services[$key].state; + Health = "N/A"; + Startup = $services[$key].Startup_type + } + if($services[$key].health -eq $null) { $serviceString.Health = "N/A"} else { $serviceString.Health = $services[$key].health } + + $serviceResult += $serviceString + } + $serviceResult + } +} + +Function Start-VAMIService { +<# + .NOTES + =========================================================================== + Created by: William Lam + Organization: VMware + Blog: www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + .SYNOPSIS + This function retrieves list of services in VAMI interface (5480) + for a VCSA node which can be an Embedded VCSA, External PSC or External VCSA. + .DESCRIPTION + Function to return list of services and their description + .EXAMPLE + Connect-CisServer -Server 192.168.1.51 -User administrator@vsphere.local -Password VMware1! + Start-VAMIService -Name rbd +#> + param( + [Parameter( + Mandatory=$true, + ValueFromPipeline=$true, + ValueFromPipelineByPropertyName=$true) + ] + [String]$Name + ) + + $vMonAPI = Get-CisService 'com.vmware.appliance.vmon.service' + + try { + Write-Host "Starting $name service ..." + $vMonAPI.start($name) + } catch { + Write-Error $Error[0].exception.Message + } +} + +Function Stop-VAMIService { +<# + .NOTES + =========================================================================== + Created by: William Lam + Organization: VMware + Blog: www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + .SYNOPSIS + This function retrieves list of services in VAMI interface (5480) + for a VCSA node which can be an Embedded VCSA, External PSC or External VCSA. + .DESCRIPTION + Function to return list of services and their description + .EXAMPLE + Connect-CisServer -Server 192.168.1.51 -User administrator@vsphere.local -Password VMware1! + Stop-VAMIService -Name rbd +#> + param( + [Parameter( + Mandatory=$true, + ValueFromPipeline=$true, + ValueFromPipelineByPropertyName=$true) + ] + [String]$Name + ) + + $vMonAPI = Get-CisService 'com.vmware.appliance.vmon.service' + + try { + Write-Host "Stopping $name service ..." + $vMonAPI.stop($name) + } catch { + Write-Error $Error[0].exception.Message + } +} + +Function Get-VAMIBackupSize { +<# + .NOTES + =========================================================================== + Created by: William Lam + Organization: VMware + Blog: www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + .SYNOPSIS + This function retrieves the backup size of the VCSA from VAMI interface (5480) + for a VCSA node which can be an Embedded VCSA, External PSC or External VCSA. + .DESCRIPTION + Function to return the current backup size of the VCSA (common and core data) + .EXAMPLE + Connect-CisServer -Server 192.168.1.51 -User administrator@vsphere.local -Password VMware1! + Get-VAMIBackupSize +#> + $recoveryAPI = Get-CisService 'com.vmware.appliance.recovery.backup.parts' + $backupParts = $recoveryAPI.list() | select id + + $estimateBackupSize = 0 + $backupPartSizes = "" + foreach ($backupPart in $backupParts) { + $partId = $backupPart.id.value + $partSize = $recoveryAPI.get($partId) + $estimateBackupSize += $partSize + $backupPartSizes += $partId + " data is " + $partSize + " MB`n" + } + + Write-Host "Estimated Backup Size: $estimateBackupSize MB" + Write-Host $backupPartSizes +} + +Function Get-VAMIUser { +<# + .NOTES + =========================================================================== + Created by: William Lam + Organization: VMware + Blog: www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + .SYNOPSIS + This function retrieves VAMI local users using VAMI interface (5480) + for a VCSA node which can be an Embedded VCSA, External PSC or External VCSA. + .DESCRIPTION + Function to retrieve VAMI local users + .EXAMPLE + Connect-CisServer -Server 192.168.1.51 -User administrator@vsphere.local -Password VMware1! + Get-VAMIUser +#> + param( + [Parameter( + Mandatory=$false, + ValueFromPipeline=$true, + ValueFromPipelineByPropertyName=$true) + ] + [String]$Name + ) + + $userAPI = Get-CisService 'com.vmware.appliance.techpreview.localaccounts.user' + + $userResults = @() + + if($Name -ne "") { + try { + $user = $userAPI.get($name) + + $userString = [pscustomobject] @{ + User = $user.username + Name = $user.fullname + Email = $user.email + Status = $user.status + PasswordStatus = $user.passwordstatus + Role = $user.role + } + $userResults += $userString + } catch { + Write-Error $Error[0].exception.Message + } + } else { + $users = $userAPI.list() + + foreach ($user in $users) { + $userString = [pscustomobject] @{ + User = $user.username + Name = $user.fullname + Email = $user.email + Status = $user.status + PasswordStatus = $user.passwordstatus + Role = $user.role + } + $userResults += $userString + } + } + $userResults +} + +Function New-VAMIUser { +<# + .NOTES + =========================================================================== + Created by: William Lam + Organization: VMware + Blog: www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + .SYNOPSIS + This function to create new VAMI local user using VAMI interface (5480) + for a VCSA node which can be an Embedded VCSA, External PSC or External VCSA. + .DESCRIPTION + Function to create a new VAMI local user + .EXAMPLE + Connect-CisServer -Server 192.168.1.51 -User administrator@vsphere.local -Password VMware1! + New-VAMIUser -name lamw -fullname "William Lam" -role "operator" -email "lamw@virtuallyghetto.com" -password "VMware1!" +#> + param( + [Parameter( + Mandatory=$true) + ] + [String]$name, + [Parameter( + Mandatory=$true) + ] + [String]$fullname, + [Parameter( + Mandatory=$true) + ] + [ValidateSet("admin","operator","superAdmin")][String]$role, + [Parameter( + Mandatory=$false) + ] + [String]$email="", + [Parameter( + Mandatory=$true) + ] + [String]$password + ) + + $userAPI = Get-CisService 'com.vmware.appliance.techpreview.localaccounts.user' + $createSpec = $userAPI.Help.add.config.CreateExample() + + $createSpec.username = $name + $createSpec.fullname = $fullname + $createSpec.role = $role + $createSpec.email = $email + $createSpec.password = [VMware.VimAutomation.Cis.Core.Types.V1.Secret]$password + + try { + Write-Host "Creating new user $name ..." + $userAPI.add($createSpec) + } catch { + Write-Error $Error[0].exception.Message + } +} + +Function Remove-VAMIUser { +<# + .NOTES + =========================================================================== + Created by: William Lam + Organization: VMware + Blog: www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + .SYNOPSIS + This function to remove VAMI local user using VAMI interface (5480) + for a VCSA node which can be an Embedded VCSA, External PSC or External VCSA. + .DESCRIPTION + Function to remove VAMI local user + .EXAMPLE + Connect-CisServer -Server 192.168.1.51 -User administrator@vsphere.local -Password VMware1! + Get-VAMIAccess +#> + param( + [Parameter( + Mandatory=$true) + ] + [String]$name, + [Parameter( + Mandatory=$false) + ] + [boolean]$confirm=$false + ) + + if(!$confirm) { + $answer = Read-Host -Prompt "Do you want to delete user $name (Y or N)" + if($answer -eq "Y" -or $answer -eq "y") { + $userAPI = Get-CisService 'com.vmware.appliance.techpreview.localaccounts.user' + + try { + Write-Host "Deleting user $name ..." + $userAPI.delete($name) + } catch { + Write-Error $Error[0].exception.Message + } + } + } +} \ No newline at end of file diff --git a/Modules/VCHA/VCHA.psm1 b/Modules/VCHA/VCHA.psm1 new file mode 100644 index 0000000..160f0e7 --- /dev/null +++ b/Modules/VCHA/VCHA.psm1 @@ -0,0 +1,413 @@ +Function Get-VCHAConfig { +<# + .NOTES + =========================================================================== + Created by: William Lam + Date: Nov 20, 2016 + Organization: VMware + Blog: www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + .SYNOPSIS + This function retrieves the VCHA Configuration which provides you with + the current state, mode as well as the IP Addresses of the Active, + Passive & Witness Node. This is only available on VCSA 6.5 (vSphere 6.5 or greater) + .DESCRIPTION + Function to return VCHA Configuration + .EXAMPLE + Get-VCHAConfig +#> + $vcHAClusterConfig = Get-View failoverClusterConfigurator + $vcHAConfig = $vcHAClusterConfig.getVchaConfig() + + $vcHAState = $vcHAConfig.State + switch($vcHAState) { + configured { + $activeIp = $vcHAConfig.FailoverNodeInfo1.ClusterIpSettings.Ip.IpAddress + $passiveIp = $vcHAConfig.FailoverNodeInfo2.ClusterIpSettings.Ip.IpAddress + $witnessIp = $vcHAConfig.WitnessNodeInfo.IpSettings.Ip.IpAddress + + $vcHAClusterManager = Get-View failoverClusterManager + $vcHAMode = $vcHAClusterManager.getClusterMode() + + Write-Host "" + Write-Host -NoNewline -ForegroundColor Green "VCHA State: " + Write-Host -ForegroundColor White "$vcHAState" + Write-Host -NoNewline -ForegroundColor Green " VCHA Mode: " + Write-Host -ForegroundColor White "$vcHAMode" + Write-Host -NoNewline -ForegroundColor Green " ActiveIP: " + Write-Host -ForegroundColor White "$activeIp" + Write-Host -NoNewline -ForegroundColor Green " PassiveIP: " + Write-Host -ForegroundColor White "$passiveIp" + Write-Host -NoNewline -ForegroundColor Green " WitnessIP: " + Write-Host -ForegroundColor White "$witnessIp`n" + ;break + } + invalid { Write-Host -ForegroundColor Red "VCHA State is in invalid state ...";break} + notConfigured { Write-Host "VCHA is not configured";break} + prepared { Write-Host "VCHA is being prepared, please try again in a little bit ...";break} + } +} + +Function Get-VCHAClusterHealth { +<# + .NOTES + =========================================================================== + Created by: William Lam + Date: Nov 20, 2016 + Organization: VMware + Blog: www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + .SYNOPSIS + This function retrieves the VCHA Cluster Health which provides more info + on each of the individual. This is only available on VCSA 6.5 (vSphere 6.5 or greater) + .DESCRIPTION + Function to return VCHA Cluster Health + .EXAMPLE + Get-VCHAClusterHealth +#> + $vcHAClusterConfig = Get-View failoverClusterConfigurator + $vcHAConfig = $vcHAClusterConfig.getVchaConfig() + $vcHAState = $vcHAConfig.State + + switch($vcHAState) { + invalid { Write-Host -ForegroundColor Red "VCHA State is in invalid state ...";break} + notConfigured { Write-Host "VCHA is not configured";break} + prepared { Write-Host "VCHA is being prepared ...";break} + configured { + $vcHAClusterManager = Get-View failoverClusterManager + $healthInfo = $vcHAClusterManager.GetVchaClusterHealth() + + $vcClusterState = $healthInfo.RuntimeInfo.ClusterState + $nodeState = $healthInfo.RuntimeInfo.NodeInfo + + Write-Host "" + Write-Host -NoNewline -ForegroundColor Green "VCHA Cluster State: " + Write-Host -ForegroundColor White "$vcClusterState" + Write-Host -NoNewline -ForegroundColor Green "VCHA Node Information: " + $nodeState | Select NodeIp, NodeRole, NodeState + ;break + } + } +} + +Function Set-VCHAClusterMode { +<# + .NOTES + =========================================================================== + Created by: William Lam + Date: Nov 20, 2016 + Organization: VMware + Blog: www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + .SYNOPSIS + This function allows you to set the mode of the VCHA Cluster whether + that is Enabled, Disabled or in Maintenance Mode. This is only available on VCSA 6.5 (vSphere 6.5 or greater) + .DESCRIPTION + Function to set VCHA Cluster Mode + .EXAMPLE + Set-VCHAClusterMode -Enabled $true + .EXAMPLE + Set-VCHAClusterMode -Disabled $true + .EXAMPLE + Set-VCHAClusterMode -Maintenance $true +#> + param( + [Switch]$Enabled, + [Switch]$Disabled, + [Switch]$Maintenance + ) + + $vcHAClusterManager = Get-View failoverClusterManager + + if($Enabled) { + Write-Host "Setting VCHA Cluster to Enabled ..." + $task = $vcHAClusterManager.setClusterMode_Task("enabled") + $task1 = Get-Task -Id ("Task-$($task.value)") + $task1 | Wait-Task + } elseIf($Maintenance) { + Write-Host "Setting VCHA Cluster to Maintenance ..." + $task = $vcHAClusterManager.setClusterMode_Task("maintenance") + $task1 = Get-Task -Id ("Task-$($task.value)") + $task1 | Wait-Task + } elseIf($Disabled) { + Write-Host "`nSetting VCHA Cluster to Disabled ...`n" + $task = $vcHAClusterManager.setClusterMode_Task("disabled") + $task1 = Get-Task -Id ("Task-$($task.value)") + $task1 | Wait-Task + } +} + +Function New-VCHABasicConfig { +<# + .NOTES + =========================================================================== + Created by: William Lam + Date: Nov 20, 2016 + Organization: VMware + Blog: www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + .SYNOPSIS + This function allows you create a new "Basic" VCHA Cluster, it does not + cover the "Advanced" use case. You will need to ensure that you have a + "Self Managed" vCenter Server before attempting this workflow. + This is only available on VCSA 6.5 (vSphere 6.5 or greater) + .DESCRIPTION + Function to create "Basic" VCHA Cluster + .PARAMETER VCSAVM + The name of the vCenter Server Appliance (VCSA) in which you wish to enable VCHA on (must be self-managed) + .PARAMETER HANetwork + The name of the Virtual Portgroup or Distributed Portgroup used for the HA Network + .PARAMETER ActiveHAIp + The IP Address for the Active VCSA node + .PARAMETER ActiveNetmask + The Netmask for the Active VCSA node + .PARAMETER PassiveHAIp + The IP Address for the Passive VCSA node + .PARAMETER PassiveNetmask + The Netmask for the Passive VCSA node + .PARAMETER WitnessHAIp + The IP Address for the Witness VCSA node + .PARAMETER WitnessNetmask + The Netmask for the Witness VCSA node + .PARAMETER PassiveDatastore + The name of the datastore to deploy the Passive node to + .PARAMETER WitnessDatastore + The name of the datastore to deploy the Witness node to + .PARAMETER VCUsername + The VCSA username (e.g. administrator@vghetto.local) + .PARAMETER VCPassword + The VCSA password + .EXAMPLE + New-VCHABasicConfig -VCSAVM "vcenter65-1" -HANetwork "DVPG-VCHA-Network" ` + -ActiveHAIp 192.168.1.70 ` + -ActiveNetmask 255.255.255.0 ` + -PassiveHAIp 192.168.1.71 ` + -PassiveNetmask 255.255.255.0 ` + -WitnessHAIp 192.168.1.72 ` + -WitnessNetmask 255.255.255.0 ` + -PassiveDatastore "vsanDatastore" ` + -WitnessDatastore "vsanDatastore" ` + -VCUsername "administrator@vghetto.local" ` + -VCPassword "VMware1!" +#> + param( + [Parameter( + Mandatory=$true, + ValueFromPipeline=$true, + ValueFromPipelineByPropertyName=$true) + ] + [String]$VCSAVM, + [String]$HANetwork, + [String]$ActiveHAIp, + [String]$ActiveNetmask, + [String]$PassiveHAIp, + [String]$PassiveNetmask, + [String]$PassiveDatastore, + [String]$WitnessHAIp, + [String]$WitnessNetmask, + [String]$WitnessDatastore, + # Crappy Implementation but need to research more into using PSH Credential + [String]$VCUsername, + [String]$VCPassword + ) + + $VCSAVMView = Get-View -ViewType VirtualMachine -Filter @{"name"=$VCSAVM} + if($VCSAVMView -eq $null) { + Write-Host -ForegroundColor Red "Error: Unable to find Virtual Machine $VCSAVM" + return + } + + $HANetworkView = Get-View -ViewType Network -Filter @{"name"=$HANetwork} + if($HANetworkView -eq $null) { + Write-Host -ForegroundColor Red "Error: Unable to find Network $HANetwork" + return + } + + $PassiveDatastoreView = Get-View -ViewType Datastore -Filter @{"name"=$PassiveDatastore} + if($PassiveDatastoreView -eq $null) { + Write-Host -ForegroundColor Red "Error: Unable to find Passive Datastore $PassiveDatastore" + return + } + + $WitnessDatastoreView = Get-View -ViewType Datastore -Filter @{"name"=$WitnessDatastore} + if($WitnessDatastoreView -eq $null) { + Write-Host -ForegroundColor Red "Error: Unable to find Witness Datastore $WitnessDatastore" + return + } + + $vcIP = $VCSAVMView.Guest.IpAddress + if($vcIP -eq $null) { + Write-Host -ForegroundColor Red "Error: Unable to automatically retrieve the IP Address of $VCSAVM which is needed to use this function" + return + } + + # Retrieve Source VC SSL Thumbprint + $vcurl = "https://$vcIP" +add-type @" + using System.Net; + using System.Security.Cryptography.X509Certificates; + + public class IDontCarePolicy : ICertificatePolicy { + public IDontCarePolicy() {} + public bool CheckValidationResult( + ServicePoint sPoint, X509Certificate cert, + WebRequest wRequest, int certProb) { + return true; + } + } +"@ + [System.Net.ServicePointManager]::CertificatePolicy = new-object IDontCarePolicy + # Need to do simple GET connection for this method to work + Invoke-RestMethod -Uri $VCURL -Method Get | Out-Null + + $endpoint_request = [System.Net.Webrequest]::Create("$vcurl") + # Get Thumbprint + add colons for a valid Thumbprint + $vcSSLThumbprint = ($endpoint_request.ServicePoint.Certificate.GetCertHashString()) -replace '(..(?!$))','$1:' + + $vcHAClusterConfig = Get-View failoverClusterConfigurator + $spec = New-Object VMware.Vim.VchaClusterDeploymentSpec + + $activeNetworkConfig = New-Object VMware.Vim.ClusterNetworkConfigSpec + $activeNetworkConfig.NetworkPortGroup = $HANetworkView.MoRef + $ipSettings = New-Object Vmware.Vim.CustomizationIPSettings + $ipSettings.SubnetMask = $ActiveNetmask + $activeIpSpec = New-Object VMware.Vim.CustomizationFixedIp + $activeIpSpec.IpAddress = $ActiveHAIp + $ipSettings.Ip = $activeIpSpec + $activeNetworkConfig.IpSettings = $ipSettings + $spec.ActiveVcNetworkConfig = $activeNetworkConfig + + $activeVCConfig = New-Object Vmware.Vim.SourceNodeSpec + $activeVCConfig.ActiveVc = $VCSAVMView.MoRef + $serviceLocator = New-Object Vmware.Vim.ServiceLocator + $credential = New-Object VMware.Vim.ServiceLocatorNamePassword + $credential.username = $VCUsername + $credential.password = $VCPassword + $serviceLocator.Credential = $credential + $serviceLocator.InstanceUuid = $global:DefaultVIServer.InstanceUuid + $serviceLocator.Url = $vcurl + $serviceLocator.SslThumbprint = $vcSSLThumbprint + $activeVCConfig.ManagementVc = $serviceLocator + $spec.ActiveVcSpec = $activeVCConfig + + $passiveSpec = New-Object VMware.Vim.PassiveNodeDeploymentSpec + $passiveSpec.Folder = (Get-View (Get-Folder vm)).MoRef + $passiveIpSettings = New-object Vmware.Vim.CustomizationIPSettings + $passiveIpSettings.SubnetMask = $passiveNetmask + $passiveIpSpec = New-Object VMware.Vim.CustomizationFixedIp + $passiveIpSpec.IpAddress = $passiveHAIp + $passiveIpSettings.Ip = $passiveIpSpec + $passiveSpec.IpSettings = $passiveIpSettings + $passiveSpec.NodeName = $VCSAVMView.Name + "-Passive" + $passiveSpec.datastore = $PassiveDatastoreView.MoRef + $spec.PassiveDeploymentSpec = $passiveSpec + + $witnessSpec = New-Object VMware.Vim.NodeDeploymentSpec + $witnessSpec.Folder = (Get-View (Get-Folder vm)).MoRef + $witnessSpec.NodeName = $VCSAVMView.Name + "-Witness" + $witnessIpSettings = New-object Vmware.Vim.CustomizationIPSettings + $witnessIpSettings.SubnetMask = $witnessNetmask + $witnessIpSpec = New-Object VMware.Vim.CustomizationFixedIp + $witnessIpSpec.IpAddress = $witnessHAIp + $witnessIpSettings.Ip = $witnessIpSpec + $witnessSpec.IpSettings = $witnessIpSettings + $witnessSpec.datastore = $WitnessDatastoreView.MoRef + $spec.WitnessDeploymentSpec = $witnessSpec + + Write-Host "`nDeploying VCHA Cluster ...`n" + $task = $vcHAClusterConfig.deployVcha_Task($spec) + $task1 = Get-Task -Id ("Task-$($task.value)") + $task1 | Wait-Task -Verbose +} + +Function Remove-VCHAConfig { +<# + .NOTES + =========================================================================== + Created by: William Lam + Date: Nov 20, 2016 + Organization: VMware + Blog: www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + .SYNOPSIS + This function allows you destroy a VCHA Cluster. In addition, you have + the option to specify whether you would like both the Passive & Witness + Virtual Machines be deleted after the VCHA Cluster has been destroyed. + This is only available on VCSA 6.5 (vSphere 6.5 or greater) + .DESCRIPTION + Function to destroy a VCHA Cluster Mode + .EXAMPLE + Remove-VCHAConfig + .EXAMPLE + Remove-VCHAConfig -Confirm:$false + .EXAMPLE + Remove-VCHAConfig -DeleteVM $true -Confirm:$false + .NOTES + Before you can destroy a VCHA Cluster, you must make sure it is first + disabled. Run the Set-VCHAClusterMode -Disabled $true to do so +#> + param( + [Boolean]$Confirm=$true, + [Switch]$DeleteVM=$false + ) + + $Verified = $false + if($Confirm -eq $true) { + Write-Host -ForegroundColor Yellow "`nDo you want to destroy VCHA Cluster?" + $answer = Read-Host -Prompt "Do you accept (Y or N)" + if($answer -eq "Y" -or $answer -eq "y") { + $Verified = $true + } + } else { + $Verified = $true + } + + if($Verified) { + $vcHAClusterManager = Get-View failoverClusterManager + $vcHAMode = $vcHAClusterManager.getClusterMode() + + if($vcHAMode -ne "disabled") { + Write-Host -ForegroundColor Yellow "To destroy VCHA Cluster, you must first set the VCHA Cluster Mode to `"Disabled`"" + Exit + } + + # Query BIOS UUID of the Passive/Witness to be able to delete + if($DeleteVM) { + $vcHAClusterConfig = Get-View failoverClusterConfigurator + $vcHAConfig = $vcHAClusterConfig.getVchaConfig() + $passiveBiosUUID = $vcHAConfig.FailoverNodeInfo2.biosUuid + $witnessBiosUUID = $vcHAConfig.WitnessNodeInfo.biosUuid + } + + $vcHAClusterConfig = Get-View failoverClusterConfigurator + + Write-Host "Destroying VCHA Cluster ..." + $task = $vcHAClusterConfig.destroyVcha_Task() + $task1 = Get-Task -Id ("Task-$($task.value)") + $task1 | Wait-Task + + # After VCHA Cluster has been destroyed, we can now delete the VMs we had queried earlier + if($DeleteVM) { + if($passiveBiosUUID -ne $null -and $witnessBiosUUID -ne $null) { + $searchIndex = Get-View searchIndex + + $passiveVM = $searchIndex.FindByUuid($null,$passiveBiosUUID,$true,$null) + $witnessVM = $searchIndex.FindByUuid($null,$witnessBiosUUID,$true,$null) + + if($passiveVM -ne $null -and $witnessVM -ne $null) { + Write-Host "Powering off & deleting Passive VM ..." + Stop-VM -VM (Get-View $passiveVM).Name -Confirm:$false | Out-Null + Remove-VM (Get-View $passiveVM).Name -DeletePermanently -Confirm:$false + Write-Host "Powering off & deleting Witness VM ..." + Stop-VM -VM (Get-View $witnessVM).Name -Confirm:$false | Out-Null + Remove-VM (Get-View $witnessVM).Name -DeletePermanently -Confirm:$false + } + } + } + } +} diff --git a/Modules/VMCPFunctions/VMCPFunctions.psm1 b/Modules/VMCPFunctions/VMCPFunctions.psm1 new file mode 100644 index 0000000..4f9b16e --- /dev/null +++ b/Modules/VMCPFunctions/VMCPFunctions.psm1 @@ -0,0 +1,322 @@ +function Get-VMCPSettings { +<# + .NOTES + =========================================================================== + Created on: 10/27/2015 9:25 PM + Created by: Brian Graf + Twitter: @vBrianGraf + VMware Blog: blogs.vmware.com/powercli + Personal Blog: www.vtagion.com + + Modified on: 10/11/2016 + Modified by: Erwan Quélin + Twitter: @erwanquelin + Github: https://github.com/equelin + =========================================================================== + .DESCRIPTION + This function will allow users to view the VMCP settings for their clusters + + .PARAMETER Cluster + Cluster Name or Object + + .PARAMETER Server + vCenter server object + + .EXAMPLE + Get-VMCPSettings + + This will show you the VMCP settings for all the clusters + + .EXAMPLE + Get-VMCPSettings -cluster LAB-CL + + This will show you the VMCP settings of your cluster + + .EXAMPLE + Get-VMCPSettings -cluster (Get-Cluster Lab-CL) + + This will show you the VMCP settings of your cluster + + .EXAMPLE + Get-Cluster | Get-VMCPSettings + + This will show you the VMCP settings for all the clusters +#> + [CmdletBinding()] + param + ( + [Parameter(Mandatory=$False, + ValueFromPipeline=$True, + ValueFromPipelineByPropertyName=$True, + HelpMessage='What is the Cluster Name?')] + $cluster = (Get-Cluster -Server $Server), + + [Parameter(Mandatory=$False)] + [VMware.VimAutomation.Types.VIServer[]]$Server = $global:DefaultVIServers + ) + + Process { + + Foreach ($Clus in $Cluster) { + + Write-Verbose "Processing Cluster $($Clus.Name)" + + # Determine input and convert to ClusterImpl object + Switch ($Clus.GetType().Name) + { + "string" {$CL = Get-Cluster $Clus -Server $Server -ErrorAction SilentlyContinue} + "ClusterImpl" {$CL = $Clus} + } + + If ($CL) { + # Work with the Cluster View + $ClusterMod = Get-View -Id "ClusterComputeResource-$($CL.ExtensionData.MoRef.Value)" -Server $Server + + # Create Hashtable with desired properties to return + $properties = [ordered]@{ + 'Cluster' = $ClusterMod.Name; + 'VMCP Status' = $clustermod.Configuration.DasConfig.VmComponentProtecting; + 'Protection For APD' = $clustermod.Configuration.DasConfig.DefaultVmSettings.VmComponentProtectionSettings.VmStorageProtectionForAPD; + 'APD Timeout Enabled' = $clustermod.Configuration.DasConfig.DefaultVmSettings.VmComponentProtectionSettings.EnableAPDTimeoutForHosts; + 'APD Timeout (Seconds)' = $clustermod.Configuration.DasConfig.DefaultVmSettings.VmComponentProtectionSettings.VmTerminateDelayForAPDSec; + 'Reaction on APD Cleared' = $clustermod.Configuration.DasConfig.DefaultVmSettings.VmComponentProtectionSettings.VmReactionOnAPDCleared; + 'Protection For PDL' = $clustermod.Configuration.DasConfig.DefaultVmSettings.VmComponentProtectionSettings.VmStorageProtectionForPDL + } + + # Create PSObject with the Hashtable + $object = New-Object -TypeName PSObject -Prop $properties + + # Show object + $object + } + } + } +} + +function Set-VMCPSettings { +<# + .NOTES + =========================================================================== + Created on: 10/27/2015 9:25 PM + Created by: Brian Graf + Twitter: @vBrianGraf + VMware Blog: blogs.vmware.com/powercli + Personal Blog: www.vtagion.com + + Modified on: 10/11/2016 + Modified by: Erwan Quélin + Twitter: @erwanquelin + Github: https://github.com/equelin + =========================================================================== + .DESCRIPTION + This function will allow users to enable/disable VMCP and also allow + them to configure the additional VMCP settings + For each parameter, users should use the 'Tab' button to auto-fill the + possible values. + + .PARAMETER Cluster + Cluster Name or Object + + .PARAMETER enableVMCP + Enable or disable VMCP + + .PARAMETER VmStorageProtectionForPDL + VM Storage Protection for PDL settings. Might be: + - disabled + - warning + - restartAggressive + + .PARAMETER VmStorageProtectionForAPD + VM Storage Protection for APD settings. Might be: + - disabled + - restartConservative + - restartAggressive + - warning + + .PARAMETER VmTerminateDelayForAPDSec + VM Terminate Delay for APD (seconds). + + .PARAMETER VmReactionOnAPDCleared + VM reaction on APD Cleared. Might be: + - reset + - none + + .PARAMETER Server + vCenter server object + + .EXAMPLE + Set-VMCPSettings -cluster LAB-CL -enableVMCP:$True -VmStorageProtectionForPDL ` + restartAggressive -VmStorageProtectionForAPD restartAggressive ` + -VmTerminateDelayForAPDSec 2000 -VmReactionOnAPDCleared reset + + This will enable VMCP and configure the Settings on cluster LAB-CL + + .EXAMPLE + Set-VMCPSettings -cluster LAB-CL -enableVMCP:$False -VmStorageProtectionForPDL ` + disabled -VmStorageProtectionForAPD disabled ` + -VmTerminateDelayForAPDSec 600 -VmReactionOnAPDCleared none + + This will disable VMCP and configure the Settings on cluster LAB-CL + + .EXAMPLE + Set-VMCPSettings -enableVMCP:$False -VmStorageProtectionForPDL ` + disabled -VmStorageProtectionForAPD disabled ` + -VmTerminateDelayForAPDSec 600 -VmReactionOnAPDCleared none + + This will disable VMCP and configure the Settings on all clusters available +#> + [CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact="High")] + param + ( + [Parameter(Mandatory=$true, + ValueFromPipeline=$True, + ValueFromPipelineByPropertyName=$True, + HelpMessage='What is the Cluster Name?')] + $cluster, + + [Parameter(Mandatory=$False, + ValueFromPipeline=$False, + HelpMessage='$True=Enabled $False=Disabled')] + [bool]$enableVMCP, + + [Parameter(Mandatory=$False, + ValueFromPipeline=$False, + HelpMessage='Actions that can be taken in response to a PDL event')] + [ValidateSet("disabled","warning","restartAggressive")] + [string]$VmStorageProtectionForPDL, + + [Parameter(Mandatory=$False, + ValueFromPipeline=$False, + HelpMessage='Options available for an APD response')] + [ValidateSet("disabled","restartConservative","restartAggressive","warning")] + [string]$VmStorageProtectionForAPD, + + [Parameter(Mandatory=$False, + ValueFromPipeline=$False, + HelpMessage='Value in seconds')] + [Int]$VmTerminateDelayForAPDSec, + + [Parameter(Mandatory=$False, + ValueFromPipeline=$False, + HelpMessage='This setting will instruct vSphere HA to take a certain action if an APD event is cleared')] + [ValidateSet("reset","none")] + [string]$VmReactionOnAPDCleared, + + [Parameter(Mandatory=$False)] + [VMware.VimAutomation.Types.VIServer[]]$Server = $global:DefaultVIServers + ) + + Process { + + Foreach ($Clus in $Cluster) { + + Write-Verbose "Processing Cluster $Clus" + + # Determine input and convert to ClusterImpl object + Switch ($Clus.GetType().Name) + { + "string" {$CL = Get-Cluster $Clus -Server $Server -ErrorAction SilentlyContinue} + "ClusterImpl" {$CL = $Clus} + default {Throw 'Please provide a cluster name or object'} + } + + If ($CL) { + + # Get the actual configuration of the Cluster + $ActualSettings = Get-VMCPSettings -Cluster $CL -Server $Server + + # Show actual settings in the verbose mode + Write-Verbose "[$($CL.Name)] Actual VMCP settings " + Write-Verbose $ActualSettings + + # Create the object we will configure + $settings = New-Object VMware.Vim.ClusterConfigSpecEx + $settings.dasConfig = New-Object VMware.Vim.ClusterDasConfigInfo + + # Based on $enableVMCP switch + if ($enableVMCP -eq $false) { + $settings.dasConfig.vmComponentProtecting = "disabled" + } + elseif ($enableVMCP -eq $true) { + $settings.dasConfig.vmComponentProtecting = "enabled" + } + + #Create the VMCP object to work with + $settings.dasConfig.defaultVmSettings = New-Object VMware.Vim.ClusterDasVmSettings + $settings.dasConfig.defaultVmSettings.vmComponentProtectionSettings = New-Object VMware.Vim.ClusterVmComponentProtectionSettings + + #Storage Protection For PDL + If ($PSBoundParameters.ContainsKey('VmStorageProtectionForPDL')) { + $settings.dasConfig.defaultVmSettings.vmComponentProtectionSettings.vmStorageProtectionForPDL = $VmStorageProtectionForPDL + } else { + $settings.dasConfig.defaultVmSettings.vmComponentProtectionSettings.vmStorageProtectionForPDL = $ActualSettings.'Protection For PDL' + } + + #Storage Protection for APD + If ($PSBoundParameters.ContainsKey('VmStorageProtectionForAPD')) { + $settings.dasConfig.defaultVmSettings.vmComponentProtectionSettings.vmStorageProtectionForAPD = $VmStorageProtectionForAPD + } else { + $settings.dasConfig.defaultVmSettings.vmComponentProtectionSettings.vmStorageProtectionForAPD = $ActualSettings.'Protection For APD' + } + + #Storage Protection for APD + If ($PSBoundParameters.ContainsKey('VmStorageProtectionForAPD')) { + switch ($VmStorageProtectionForAPD) { + "disabled" { + # If Disabled, there is no need to set enable Timeout Value + $settings.dasConfig.defaultVmSettings.vmComponentProtectionSettings.vmStorageProtectionForAPD = 'disabled' + $settings.dasConfig.defaultVmSettings.vmComponentProtectionSettings.enableAPDTimeoutForHosts = $false + } + + "restartConservative" { + $settings.dasConfig.defaultVmSettings.vmComponentProtectionSettings.vmStorageProtectionForAPD = 'restartConservative' + $settings.dasConfig.defaultVmSettings.vmComponentProtectionSettings.enableAPDTimeoutForHosts = $true + } + + "restartAggressive" { + $settings.dasConfig.defaultVmSettings.vmComponentProtectionSettings.vmStorageProtectionForAPD = 'restartAggressive' + $settings.dasConfig.defaultVmSettings.vmComponentProtectionSettings.enableAPDTimeoutForHosts = $true + } + + "warning" { + # If Warning, there is no need to enable the Timeout Value + $settings.dasConfig.defaultVmSettings.vmComponentProtectionSettings.vmStorageProtectionForAPD = 'warning' + $settings.dasConfig.defaultVmSettings.vmComponentProtectionSettings.enableAPDTimeoutForHosts = $false + } + } + } else { + $settings.dasConfig.defaultVmSettings.vmComponentProtectionSettings.vmStorageProtectionForAPD = $ActualSettings.'Protection For APD' + $settings.dasConfig.defaultVmSettings.vmComponentProtectionSettings.enableAPDTimeoutForHosts = $ActualSettings.'APD Timeout Enabled' + } + + #APD Timeout Enabled + If ($PSBoundParameters.ContainsKey('VmTerminateDelayForAPDSec')) { + $settings.dasConfig.defaultVmSettings.vmComponentProtectionSettings.vmTerminateDelayForAPDSec = $VmTerminateDelayForAPDSec + } else { + $settings.dasConfig.defaultVmSettings.vmComponentProtectionSettings.vmTerminateDelayForAPDSec = $ActualSettings.'APD Timeout (Seconds)' + } + + # Reaction On APD Cleared + If ($PSBoundParameters.ContainsKey('VmReactionOnAPDCleared')) { + $settings.dasConfig.defaultVmSettings.vmComponentProtectionSettings.vmReactionOnAPDCleared = "$VmReactionOnAPDCleared" + } else { + $settings.dasConfig.defaultVmSettings.vmComponentProtectionSettings.vmReactionOnAPDCleared = $ActualSettings.'Reaction on APD Cleared' + } + + # Execute API Call + If ($pscmdlet.ShouldProcess($CL.Name,"Modify VMCP configuration")) { + $modify = $true + $ClusterMod = Get-View -Id "ClusterComputeResource-$($CL.ExtensionData.MoRef.Value)" -Server $Server + $Task = $ClusterMod.ReconfigureComputeResource_Task($settings, $modify) + } + + # Wait for the reconfiguration task to finish to show the result + If ($Task) { + $TaskID = "Task-" + $($Task.Value) + Get-Task -Id $TaskID -Server $Server | Wait-Task | Out-Null + Get-VMCPSettings -Cluster $CL -Server $Server + } + } + } + } +} diff --git a/Modules/apply-hardening/apply-hardening.psm1 b/Modules/apply-hardening/apply-hardening.psm1 new file mode 100644 index 0000000..94b1279 --- /dev/null +++ b/Modules/apply-hardening/apply-hardening.psm1 @@ -0,0 +1,93 @@ +function Apply-Hardening { +<# + .NOTES + =========================================================================== + Created by: Markus Kraus + Twitter: @VMarkus_K + Private Blog: mycloudrevolution.com + =========================================================================== + Changelog: + 2016.11 ver 2.0 Base Release + =========================================================================== + External Code Sources: + + =========================================================================== + Tested Against Environment: + vSphere Version: 5.5 U2 + PowerCLI Version: PowerCLI 6.3 R1, PowerCLI 6.5 R1 + PowerShell Version: 4.0, 5.0 + OS Version: Windows 8.1, Server 2012 R2 + Keyword: VM, Hardening, Security + =========================================================================== + + .DESCRIPTION + Applys a set of Hardening options to your VMs + + .Example + Get-VM TST* | Apply-Hardening + + .Example + $SampleVMs = Get-VM "TST*" + Apply-Hardening -VMs $SampleVMs + + .PARAMETER VMs + Specify the VMs + + +#Requires PS -Version 4.0 +#Requires -Modules VMware.VimAutomation.Core, @{ModuleName="VMware.VimAutomation.Core";ModuleVersion="6.3.0.0"} +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory=$true, + ValueFromPipeline=$True, + Position=0)] + [VMware.VimAutomation.ViCore.Impl.V1.Inventory.InventoryItemImpl[]] + $VMs +) + +Process { +#region: Create Options + $ExtraOptions = @{ + "isolation.tools.diskShrink.disable"="true"; + "isolation.tools.diskWiper.disable"="true"; + "isolation.tools.copy.disable"="true"; + "isolation.tools.paste.disable"="true"; + "isolation.tools.dnd.disable"="true"; + "isolation.tools.setGUIOptions.enable"="false"; + "log.keepOld"="10"; + "log.rotateSize"="100000" + "RemoteDisplay.maxConnections"="2"; + "RemoteDisplay.vnc.enabled"="false"; + + } + if ($DebugPreference -eq "Inquire") { + Write-Output "VM Hardening Options:" + $ExtraOptions | Format-Table -AutoSize + } + + $VMConfigSpec = New-Object VMware.Vim.VirtualMachineConfigSpec + + Foreach ($Option in $ExtraOptions.GetEnumerator()) { + $OptionValue = New-Object VMware.Vim.optionvalue + $OptionValue.Key = $Option.Key + $OptionValue.Value = $Option.Value + $VMConfigSpec.extraconfig += $OptionValue + } +#endregion + +#region: Apply Options + ForEach ($VM in $VMs){ + $VMv = Get-VM $VM | Get-View + $state = $VMv.Summary.Runtime.PowerState + Write-Output "...Starting Reconfiguring VM: $VM " + $TaskConf = ($VMv).ReconfigVM_Task($VMConfigSpec) + if ($state -eq "poweredOn") { + Write-Output "...Migrating VM: $VM " + $TaskMig = $VMv.MigrateVM_Task($null, $_.Runtime.Host, 'highPriority', $null) + } + } + } +#endregion +} \ No newline at end of file diff --git a/Modules/vSphere_Hardening_Assess_VM_v1a/vSphere_Hardening_Assess_VM_v1a.psm1 b/Modules/vSphere_Hardening_Assess_VM_v1a/vSphere_Hardening_Assess_VM_v1a.psm1 new file mode 100644 index 0000000..ad6227c --- /dev/null +++ b/Modules/vSphere_Hardening_Assess_VM_v1a/vSphere_Hardening_Assess_VM_v1a.psm1 @@ -0,0 +1,372 @@ +<# + .NOTES + =========================================================================== + Created on: 5/27/2015 3:24 PM + Created by: Brian Graf + Twitter: @vBrianGraf + Blog: http://www.vtagion.com + =========================================================================== +#> + +#Encoded Hardening Guide +$Global:Base64 = "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" + +Function Import-Xls { + +<# +.SYNOPSIS +Import an Excel file. + +.DESCRIPTION +Import an excel file. Since Excel files can have multiple worksheets, you can specify the worksheet you want to import. You can specify it by number (1, 2, 3) or by name (Sheet1, Sheet2, Sheet3). Imports Worksheet 1 by default. + +.PARAMETER Path +Specifies the path to the Excel file to import. You can also pipe a path to Import-Xls. + +.PARAMETER Worksheet +Specifies the worksheet to import in the Excel file. You can specify it by name or by number. The default is 1. +Note: Charts don't count as worksheets, so they don't affect the Worksheet numbers. + +.INPUTS +System.String + +.OUTPUTS +Object + +.EXAMPLE +".\employees.xlsx" | Import-Xls -Worksheet 1 +Import Worksheet 1 from employees.xlsx + +.EXAMPLE +".\employees.xlsx" | Import-Xls -Worksheet "Sheet2" +Import Worksheet "Sheet2" from employees.xlsx + +.EXAMPLE +".\deptA.xslx", ".\deptB.xlsx" | Import-Xls -Worksheet 3 +Import Worksheet 3 from deptA.xlsx and deptB.xlsx. +Make sure that the worksheets have the same headers, or have some headers in common, or that it works the way you expect. + +.EXAMPLE +Get-ChildItem *.xlsx | Import-Xls -Worksheet "Employees" +Import Worksheet "Employees" from all .xlsx files in the current directory. +Make sure that the worksheets have the same headers, or have some headers in common, or that it works the way you expect. + +.LINK +Import-Xls +http://gallery.technet.microsoft.com/scriptcenter/17bcabe7-322a-43d3-9a27-f3f96618c74b +Export-Xls +http://gallery.technet.microsoft.com/scriptcenter/d41565f1-37ef-43cb-9462-a08cd5a610e2 +Import-Csv +Export-Csv + +.NOTES +Author: Francis de la Cerna +Created: 2011-03-27 +Modified: 2011-04-09 +#Requires –Version 2.0 + +#> + + [CmdletBinding(SupportsShouldProcess = $true)] + Param ( + [parameter( + mandatory = $true, + position = 1, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true)] + [String[]]$Path, + [parameter(mandatory = $false)] + $Worksheet = 1, + [parameter(mandatory = $false)] + [switch]$Force + ) + + Begin { + function GetTempFileName($extension) { + $temp = [io.path]::GetTempFileName(); + $params = @{ + Path = $temp; + Destination = $temp + $extension; + Confirm = $false; + Verbose = $VerbosePreference; + } + Move-Item @params; + $temp += $extension; + return $temp; + } + + # since an extension like .xls can have multiple formats, this + # will need to be changed + # + $xlFileFormats = @{ + # single worksheet formats + '.csv' = 6; # 6, 22, 23, 24 + '.dbf' = 11; # 7, 8, 11 + '.dif' = 9; # + '.prn' = 36; # + '.slk' = 2; # 2, 10 + '.wk1' = 31; # 5, 30, 31 + '.wk3' = 32; # 15, 32 + '.wk4' = 38; # + '.wks' = 4; # + '.xlw' = 35; # + + # multiple worksheet formats + '.xls' = -4143; # -4143, 1, 16, 18, 29, 33, 39, 43 + '.xlsb' = 50; # + '.xlsm' = 52; # + '.xlsx' = 51; # + '.xml' = 46; # + '.ods' = 60; # + } + + $xl = New-Object -ComObject Excel.Application; + $xl.DisplayAlerts = $false; + $xl.Visible = $false; + } + + Process { + $Path | ForEach-Object { + + if ($Force -or $psCmdlet.ShouldProcess($_)) { + + $fileExist = Test-Path $_ + + if (-not $fileExist) { + Write-Error "Error: $_ does not exist" -Category ResourceUnavailable; + } else { + # create temporary .csv file from excel file and import .csv + # + $_ = (Resolve-Path $_).toString(); + $wb = $xl.Workbooks.Add($_); + if ($?) { + $csvTemp = GetTempFileName(".csv"); + $ws = $wb.Worksheets.Item($Worksheet); + $ws.SaveAs($csvTemp, $xlFileFormats[".csv"]); + $wb.Close($false); + Remove-Variable -Name ('ws', 'wb') -Confirm:$false; + Import-Csv $csvTemp; + Remove-Item $csvTemp -Confirm:$false -Verbose:$VerbosePreference; + } + } + } + } + } + + End { + $xl.Quit(); + Remove-Variable -name xl -Confirm:$false; + [gc]::Collect(); + } +} +Function Show-HardeningGuide { + $Global:XLS = Import-XLS $env:temp\HG.XLSX + $ShowGuide = $XLS | Out-GridView +} +Function Assess-VM { +<# + .DESCRIPTION + This cmdlet will allow you to assess the security of the VM's in your environment + .EXAMPLE + # Specify Virtual Machines to Assess + $VM = Get-VM MGMT-AD, MGMT-VCO, WebCommander, Exchange01 + + # Assess against Risk Profile 2 + Assess-VM -VM $VM -RiskProfile 2 + + .EXAMPLE + # Assess all VM's against specific ID's + $VM = Get-VM + Assess-VM -VM $VM -ID 9,10,12,22,33,49 + + .EXAMPLE + # Assess All VM's against All ID's + Assess-VM -VM (Get-VM) +#> + [CmdletBinding( + DefaultParameterSetName = â€ID†+ )] + Param ( + #Object receiving the hardening + [Parameter( + Position = 0, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true, + HelpMessage = 'What object are you running this against?')] + #[string]$VMs, + [PSObject[]] + $VM, + + #Hardening Guide Profile + [Parameter(Mandatory = $false, + ValueFromPipeline = $false, + HelpMessage = "You must choose which Hardening Guide Risk Profile to use (1-3).", + ParameterSetName = 'RiskProfile')] + #[ValidateRange(1,3)] + [int]$RiskProfile, + + #Hardening Guide Guideline ID + [Parameter(Mandatory = $false, + ValueFromPipeline = $false, + ParameterSetName = 'ID')] + [string[]]$ID, + + [parameter(ParameterSetName = "ID", + ValueFromPipeline = $false)] + $AllIDs = "1" + ) + + BEGIN { + # Validate VM Parameter + # This ensures the object being passed is an object rather than a string + [int]$Err = 0 + foreach ($object in $vm) { if (($object.GetType().FullName) -ne "VMware.VimAutomation.ViCore.Impl.V1.Inventory.VirtualMachineImpl") { $Err += 1 } } + if ($Err -gt 0) { + Write-Error "One or more objects specified in the `$VM parameter is not a VM object" + Break + } + + + # Get the Date, used for folder creation and report + $Date = (Get-Date -Format MM.dd.yy) + + # Create array of ID's + if (-not $ID) { + $IDs = $null + } else { + $IDs = $ID.Replace(' ', '').split(',') + } + + # Manage Risk Profile Parameter value + $RiskArray = @("1","2","3") + + + ### HTML CSS CODE ### + $head = @' + +'@ + ##################### + + #C heck to see if HG exists in the $Env:Temp folder, if not, place it there + if (!(Test-Path -Path "$env:Temp\HG.XLSX")) { + + # Decode Hardening Guide from script + $Content = [System.Convert]::FromBase64String($Global:Base64) + Set-Content -Path $env:temp\HG.XLSX -Value $Content -Encoding Byte + } + + #Save Imported Hardening Guide to $XLS + $Global:XLS = Import-XLS $env:temp\HG.XLSX + + } + + PROCESS { + + # If risk profile is provided + if ($RiskArray -contains $RiskProfile) { + + $Guidelines = ($XLS | ?{ $_."Guideline ID" -like "VM*" -and $_."Risk Profile" -match $RiskProfile }) + foreach ($Guideline in $Guidelines) { + + $ID = $Guideline.ID + $GuidelineID = $Guideline."Guideline ID" + $Name = ($GuidelineID.Split("."))[1] + $Assessment = $Guideline."PowerCLI Command Assessment" + $Assessment = $Assessment -replace "Get-VM", "`$VM" + $Description = $Guideline."Description" + + Write-Host "Processing: $ID - $Name" -ForegroundColor CYAN + if ($assessment -ne "") { + $run = iex ($Assessment) + if ($run -eq $null) { + $run = New-Object System.object + $run | Add-Member -type NoteProperty -name Name -value "Clear" + $run | Add-Member -type NoteProperty -name Description -value "NO SECURITY HARDENING NEEDED FOR THIS GUIDELINE" + } + $Task = $run | ConvertTo-Html -Fragment -PreContent "

$ID - $Name

$Description

" -PostContent "
" | Out-String + $Tasks += $task + } + } + } elseif ($AllIDs -eq "1" -and $ID -eq $null) { + + $Guidelines = ($XLS | ?{ $_."Guideline ID" -like "VM*" }) + foreach ($Guideline in $Guidelines) { + $ID = $Guideline.ID + $GuidelineID = $Guideline."Guideline ID" + $Name = ($GuidelineID.Split("."))[1] + $Assessment = $Guideline."PowerCLI Command Assessment" + $Assessment = $Assessment -replace "Get-VM", "`$VM" + $Description = $Guideline."Description" + Write-Host "Processing: $ID - $Name" -ForegroundColor CYAN + if ($assessment -ne "") { + $run = iex ($Assessment) + if ($run -eq $null) { + $run = New-Object System.object + $run | Add-Member -type NoteProperty -name Name -value "Clear" + $run | Add-Member -type NoteProperty -name Description -value "NO SECURITY HARDENING NEEDED FOR THIS GUIDELINE" + } + $Task = $run | ConvertTo-Html -Fragment -PreContent "

$ID - $Name


$Description

" | Out-String + $Tasks += $task + } + } + + } else { + # If Guideline IDs are provided + foreach ($line in $ID) { + $Guideline = ($XLS | ?{ $_."Guideline ID" -like "VM*" -and $_."ID" -eq $Line }) + $GuidelineID = $Guideline."Guideline ID" + if ($GuidelineID -eq $null) { + Write-Host "$line is an invalid ID for this object... moving to next ID" -ForegroundColor 'Red' + Continue + } + $Name = ($GuidelineID.Split("."))[1] + + $Description = $Guideline."Description" + $Assessment = $Guideline."PowerCLI Command Assessment" + $Assessment = $Assessment -replace "Get-VM", "`$VM" + Write-Host "Processing: $line - $Name" -ForegroundColor CYAN + if ($assessment -ne "") { + $run = iex ($Assessment) + if ($run -eq $null) { + $run = New-Object System.object + $run| Add-Member -type NoteProperty -name Name -value "Clear" + $run | Add-Member -type NoteProperty -name Description -value "NO SECURITY HARDENING NEEDED FOR THIS GUIDELINE" + } + $Task = $run | ConvertTo-Html -Fragment -PreContent "

$line - $Name


$Description

" -PostContent "
" | Out-String + $Tasks += $task + } + } + } + } + + END { + # if no tasks were generated, end function and do NOT create empty report + if ($tasks -eq $null) {Break } + + # Get's end time Hours/Minutes for creating the HTML report + $time = (get-date -Format HH-mm) + + # Checks to see if a folder has been created for that date. If not, it will create one. + if (!(Test-Path c:\Temp\$Date)) { + Write-Host "Folder does not exist... Creating" -ForegroundColor 'Yellow' + New-item "C:\Temp\$date" -type Directory + } + + # HTML Report is then generated + $HTML = ConvertTo-HTML -head $head -PostContent "
$Tasks
" -PreContent “

vSphere 6 VM Hardening Report

†| Out-File "c:\Temp\$Date\$Time-VMAssessment.html" -Force + + # Report is opened for user to see + invoke-item "c:\Temp\$Date\$Time-VMAssessment.html" + } +} + From 81f0299bce6ca2975d22812a879e3fa3087eaec1 Mon Sep 17 00:00:00 2001 From: Kevin Kirkpatrick Date: Mon, 24 Apr 2017 12:09:00 -0400 Subject: [PATCH 033/112] Removed standalone .psm1 files from /Modules Signed-off-by: Kevin Kirkpatrick --- Modules/Backup-VCSA.psm1 | 202 ------ Modules/Get-NICDetails.psm1 | 93 --- Modules/Get-NewAndRemovedVMs.psm1 | 131 ---- Modules/Get-VMmaxIOPS.psm1 | 114 --- Modules/Konfig-ESXi.psm1 | 234 ------ Modules/PSvLIMessage.psm1 | 123 ---- Modules/ProactiveHA.psm1 | 468 ------------ Modules/Recommend-Sizing.psm1 | 227 ------ Modules/Set-CBT.psm1 | 111 --- Modules/Start-UNMAP.psm1 | 99 --- Modules/VAMI.psm1 | 716 ------------------- Modules/VCHA.psm1 | 413 ----------- Modules/VMCPFunctions.psm1 | 322 --------- Modules/apply-hardening.psm1 | 93 --- Modules/vSphere_Hardening_Assess_VM_v1a.psm1 | 372 ---------- 15 files changed, 3718 deletions(-) delete mode 100644 Modules/Backup-VCSA.psm1 delete mode 100644 Modules/Get-NICDetails.psm1 delete mode 100644 Modules/Get-NewAndRemovedVMs.psm1 delete mode 100644 Modules/Get-VMmaxIOPS.psm1 delete mode 100644 Modules/Konfig-ESXi.psm1 delete mode 100644 Modules/PSvLIMessage.psm1 delete mode 100644 Modules/ProactiveHA.psm1 delete mode 100644 Modules/Recommend-Sizing.psm1 delete mode 100644 Modules/Set-CBT.psm1 delete mode 100644 Modules/Start-UNMAP.psm1 delete mode 100755 Modules/VAMI.psm1 delete mode 100644 Modules/VCHA.psm1 delete mode 100644 Modules/VMCPFunctions.psm1 delete mode 100644 Modules/apply-hardening.psm1 delete mode 100644 Modules/vSphere_Hardening_Assess_VM_v1a.psm1 diff --git a/Modules/Backup-VCSA.psm1 b/Modules/Backup-VCSA.psm1 deleted file mode 100644 index 271a7ff..0000000 --- a/Modules/Backup-VCSA.psm1 +++ /dev/null @@ -1,202 +0,0 @@ -Function Backup-VCSAToFile { -<# - .NOTES - =========================================================================== - Created by: Brian Graf - Date: October 30, 2016 - Organization: VMware - Blog: www.vtagion.com - Twitter: @vBrianGraf - =========================================================================== - - .SYNOPSIS - This function will allow you to create a full or partial backup of your - VCSA appliance. (vSphere 6.5 and higher) - - .DESCRIPTION - Use this function to backup your VCSA to a remote location - - .EXAMPLE - [VMware.VimAutomation.Cis.Core.Types.V1.Secret]$BackupPassword = "VMw@re123" - $Comment = "First API Backup" - $LocationType = "FTP" - $location = "10.144.99.5/vcsabackup-$((Get-Date).ToString('yyyy-MM-dd-hh-mm'))" - $LocationUser = "admin" - [VMware.VimAutomation.Cis.Core.Types.V1.Secret]$locationPassword = "VMw@re123" - PS C:\> Backup-VCSAToFile -BackupPassword $BackupPassword -LocationType $LocationType -Location $location -LocationUser $LocationUser -LocationPassword $locationPassword -Comment "This is a demo" -ShowProgress -FullBackup - - - .NOTES - Credit goes to @AlanRenouf for sharing the base of this function with me which I was able to take and make more robust as well as add in progress indicators - You must be connected to the CisService for this to work, if you are not connected, the function will prompt you for your credentials - If a -LocationType is not chosen, the function will default to FTP. - The destination location for a backup must be an empty folder (easiest to use the get-date cmdlet in the location) - -ShowProgress will give you a progressbar as well as updates in the console - -SeatBackup will only backup the config whereas -Fullbackup grabs the historical data as well -#> - param ( - [Parameter(ParameterSetName=’FullBackup’)] - [switch]$FullBackup, - [Parameter(ParameterSetName=’SeatBackup’)] - [switch]$SeatBackup, - [ValidateSet('FTPS', 'HTTP', 'SCP', 'HTTPS', 'FTP')] - $LocationType = "FTP", - $Location, - $LocationUser, - [VMware.VimAutomation.Cis.Core.Types.V1.Secret]$LocationPassword, - [VMware.VimAutomation.Cis.Core.Types.V1.Secret]$BackupPassword, - $Comment = "Backup job", - [switch]$ShowProgress - ) - Begin { - if (!($global:DefaultCisServers)){ - [System.Windows.Forms.MessageBox]::Show("It appears you have not created a connection to the CisServer. You will now be prompted to enter your vCenter credentials to continue" , "Connect to CisServer") | out-null - $Connection = Connect-CisServer $global:DefaultVIServer - } else { - $Connection = $global:DefaultCisServers - } - if ($FullBackup) {$parts = @("common","seat")} - if ($SeatBackup) {$parts = @("seat")} - } - Process{ - $BackupAPI = Get-CisService com.vmware.appliance.recovery.backup.job - $CreateSpec = $BackupAPI.Help.create.piece.CreateExample() - $CreateSpec.parts = $parts - $CreateSpec.backup_password = $BackupPassword - $CreateSpec.location_type = $LocationType - $CreateSpec.location = $Location - $CreateSpec.location_user = $LocationUser - $CreateSpec.location_password = $LocationPassword - $CreateSpec.comment = $Comment - try { - $BackupJob = $BackupAPI.create($CreateSpec) - } - catch { - Write-Error $Error[0].exception.Message - } - - - If ($ShowProgress){ - do { - $BackupAPI.get("$($BackupJob.ID)") | select id, progress, state - $progress = ($BackupAPI.get("$($BackupJob.ID)").progress) - Write-Progress -Activity "Backing up VCSA" -Status $BackupAPI.get("$($BackupJob.ID)").state -PercentComplete ($BackupAPI.get("$($BackupJob.ID)").progress) -CurrentOperation "$progress% Complete" - start-sleep -seconds 5 - } until ($BackupAPI.get("$($BackupJob.ID)").progress -eq 100 -or $BackupAPI.get("$($BackupJob.ID)").state -ne "INPROGRESS") - - $BackupAPI.get("$($BackupJob.ID)") | select id, progress, state - } - Else { - $BackupJob | select id, progress, state - } - } - End {} -} - -Function Get-VCSABackupJobs { -<# - .NOTES - =========================================================================== - Created by: Brian Graf - Date: October 30, 2016 - Organization: VMware - Blog: www.vtagion.com - Twitter: @vBrianGraf - =========================================================================== - - .SYNOPSIS - Get-VCSABackupJobs returns a list of all backup jobs VCSA has ever performed (vSphere 6.5 and higher) - - .DESCRIPTION - Get-VCSABackupJobs returns a list of all backup jobs VCSA has ever performed - - .EXAMPLE - PS C:\> Get-VCSABackupJobs - - .NOTES - The values returned are read as follows: - YYYYMMDD-hhmmss-vcsabuildnumber - You can pipe the results of this function into the Get-VCSABackupStatus function - Get-VCSABackupJobs | select -First 1 | Get-VCSABackupStatus <- Most recent backup -#> - param ( - [switch]$ShowNewest - ) - Begin { - if (!($global:DefaultCisServers)){ - [System.Windows.Forms.MessageBox]::Show("It appears you have not created a connection to the CisServer. You will now be prompted to enter your vCenter credentials to continue" , "Connect to CisServer") | out-null - $Connection = Connect-CisServer $global:DefaultVIServer - } else { - $Connection = $global:DefaultCisServers - } - } - Process{ - - $BackupAPI = Get-CisService com.vmware.appliance.recovery.backup.job - - try { - if ($ShowNewest) { - $results = $BackupAPI.list() - $results[0] - } else { - $BackupAPI.list() - } - } - catch { - Write-Error $Error[0].exception.Message - } - - } - - End {} -} - -Function Get-VCSABackupStatus { -<# - .NOTES - =========================================================================== - Created by: Brian Graf - Date: October 30, 2016 - Organization: VMware - Blog: www.vtagion.com - Twitter: @vBrianGraf - =========================================================================== - - .SYNOPSIS - Returns the ID, Progress, and State of a VCSA backup (vSphere 6.5 and higher) - - .DESCRIPTION - Returns the ID, Progress, and State of a VCSA backup - - .EXAMPLE - PS C:\> $backups = Get-VCSABackupJobs - $backups[0] | Get-VCSABackupStatus - - .NOTES - The BackupID can be piped in from the Get-VCSABackupJobs function and can return multiple job statuses -#> - Param ( - [parameter(ValueFromPipeline=$True)] - [string[]]$BackupID - ) - Begin { - if (!($global:DefaultCisServers)){ - [System.Windows.Forms.MessageBox]::Show("It appears you have not created a connection to the CisServer. You will now be prompted to enter your vCenter credentials to continue" , "Connect to CisServer") | out-null - $Connection = Connect-CisServer $global:DefaultVIServer - } else { - $Connection = $global:DefaultCisServers - } - - $BackupAPI = Get-CisService com.vmware.appliance.recovery.backup.job - } - Process{ - - foreach ($id in $BackupID) { - $BackupAPI.get("$id") | select id, progress, state - } - - - } - - End {} -} diff --git a/Modules/Get-NICDetails.psm1 b/Modules/Get-NICDetails.psm1 deleted file mode 100644 index 30f1440..0000000 --- a/Modules/Get-NICDetails.psm1 +++ /dev/null @@ -1,93 +0,0 @@ -function Get-NICDetails { -<# - .NOTES - =========================================================================== - Created by: Markus Kraus - Twitter: @VMarkus_K - Private Blog: mycloudrevolution.com - =========================================================================== - Changelog: - 2017.02 ver 1.0 Base Release - =========================================================================== - External Code Sources: - - - =========================================================================== - Tested Against Environment: - vSphere Version: ESXi 6.0 U2, ESXi 6.5 - PowerCLI Version: PowerCLI 6.3 R1, PowerCLI 6.5 R1 - PowerShell Version: 4.0, 5.0 - OS Version: Windows 8.1, Server 2008 R2, Server 2012 R2 - Keyword: ESXi, NIC, vmnic, Driver, Firmware - =========================================================================== - - .DESCRIPTION - Reports Firmware and Driver Details for your ESXi vmnics. - - .Example - Get-NICDetails -Clustername * - - .PARAMETER Clustername - Name or Wildcard of your vSphere Cluster Name to process. - - -#Requires PS -Version 4.0 -#Requires -Modules VMware.VimAutomation.Core, @{ModuleName="VMware.VimAutomation.Core";ModuleVersion="6.3.0.0"} -#> - -[CmdletBinding()] -param( - [Parameter(Mandatory=$True, ValueFromPipeline=$False, Position=0)] - [ValidateNotNullorEmpty()] - [String] $Clustername - -) - -Begin { - $Validate = $True - - if (($myCluster = Get-Cluster -Name $Clustername).count -lt 1) { - $Validate = $False - thow "No Cluster '$myCluster' found!" - } - -} - -Process { - - $MyView = @() - if ($Validate -eq $True) { - - foreach ($myVMhost in ($myCluster | Get-VMHost)) { - - $esxcli2 = Get-ESXCLI -VMHost $myVMhost -V2 - $niclist = $esxcli2.network.nic.list.invoke() - - $nicdetails = @() - foreach ($nic in $niclist) { - - $args = $esxcli2.network.nic.get.createargs() - $args.nicname = $nic.name - $nicdetail = $esxcli2.network.nic.get.Invoke($args) - $nicdetails += $nicdetail - - } - ForEach ($nicdetail in $nicdetails){ - $NICReport = [PSCustomObject] @{ - Host = $myVMhost.Name - vmnic = $nicdetail.Name - LinkStatus = $nicdetail.LinkStatus - BusInfo = $nicdetail.driverinfo.BusInfo - Driver = $nicdetail.driverinfo.Driver - FirmwareVersion = $nicdetail.driverinfo.FirmwareVersion - DriverVersion = $nicdetail.driverinfo.Version - } - $MyView += $NICReport - } - - } - - $MyView - - } -} -} \ No newline at end of file diff --git a/Modules/Get-NewAndRemovedVMs.psm1 b/Modules/Get-NewAndRemovedVMs.psm1 deleted file mode 100644 index 4a2e3ba..0000000 --- a/Modules/Get-NewAndRemovedVMs.psm1 +++ /dev/null @@ -1,131 +0,0 @@ -function Get-NewAndRemovedVMs { -<# - .NOTES - =========================================================================== - Created by: Markus Kraus - Twitter: @VMarkus_K - Private Blog: mycloudrevolution.com - =========================================================================== - Changelog: - 2016.12 ver 1.0 Base Release - =========================================================================== - External Code Sources: - https://github.com/alanrenouf/vCheck-vSphere - =========================================================================== - Tested Against Environment: - vSphere Version: 5.5 U2 - PowerCLI Version: PowerCLI 6.3 R1, PowerCLI 6.5 R1 - PowerShell Version: 4.0, 5.0 - OS Version: Windows 8.1, Server 2012 R2 - =========================================================================== - Keywords vSphere, VM - =========================================================================== - - .DESCRIPTION - This Function report newly created and deleted VMs by Cluster. - - .Example - Get-NewAndRemovedVMs -ClusterName Cluster* | ft -AutoSize - - .Example - Get-NewAndRemovedVMs -ClusterName Cluster01 -Days 90 - - .PARAMETER ClusterName - Name or Wildcard of your vSphere Cluster Name(s) to report. - - .PARAMETER Day - Range in Days to report. - - -#Requires PS -Version 4.0 -#Requires -Modules VMware.VimAutomation.Core, @{ModuleName="VMware.VimAutomation.Core";ModuleVersion="6.3.0.0"} -#> - -param( - [Parameter(Mandatory=$True, ValueFromPipeline=$False, Position=0, HelpMessage = "Name or Wildcard of your vSphere Cluster Name to report")] - [ValidateNotNullorEmpty()] - [String]$ClusterName, - [Parameter(Mandatory=$False, ValueFromPipeline=$False, Position=1, HelpMessage = "Range in Days to report")] - [ValidateNotNullorEmpty()] - [String]$Days = "30" -) -Begin { - function Get-VIEventPlus { - - param( - [VMware.VimAutomation.ViCore.Impl.V1.Inventory.InventoryItemImpl[]]$Entity, - [string[]]$EventType, - [DateTime]$Start, - [DateTime]$Finish = (Get-Date), - [switch]$Recurse, - [string[]]$User, - [Switch]$System, - [string]$ScheduledTask, - [switch]$FullMessage = $false, - [switch]$UseUTC = $false - ) - - process { - $eventnumber = 100 - $events = @() - $eventMgr = Get-View EventManager - $eventFilter = New-Object VMware.Vim.EventFilterSpec - $eventFilter.disableFullMessage = ! $FullMessage - $eventFilter.entity = New-Object VMware.Vim.EventFilterSpecByEntity - $eventFilter.entity.recursion = &{if($Recurse){"all"}else{"self"}} - $eventFilter.eventTypeId = $EventType - if($Start -or $Finish){ - $eventFilter.time = New-Object VMware.Vim.EventFilterSpecByTime - if($Start){ - $eventFilter.time.beginTime = $Start - } - if($Finish){ - $eventFilter.time.endTime = $Finish - } - } - if($User -or $System){ - $eventFilter.UserName = New-Object VMware.Vim.EventFilterSpecByUsername - if($User){ - $eventFilter.UserName.userList = $User - } - if($System){ - $eventFilter.UserName.systemUser = $System - } - } - if($ScheduledTask){ - $si = Get-View ServiceInstance - $schTskMgr = Get-View $si.Content.ScheduledTaskManager - $eventFilter.ScheduledTask = Get-View $schTskMgr.ScheduledTask | - where {$_.Info.Name -match $ScheduledTask} | - Select -First 1 | - Select -ExpandProperty MoRef - } - if(!$Entity){ - $Entity = @(Get-Folder -NoRecursion) - } - $entity | %{ - $eventFilter.entity.entity = $_.ExtensionData.MoRef - $eventCollector = Get-View ($eventMgr.CreateCollectorForEvents($eventFilter)) - $eventsBuffer = $eventCollector.ReadNextEvents($eventnumber) - while($eventsBuffer){ - $events += $eventsBuffer - $eventsBuffer = $eventCollector.ReadNextEvents($eventnumber) - } - $eventCollector.DestroyCollector() - } - if (-not $UseUTC) - { - $events | % { $_.createdTime = $_.createdTime.ToLocalTime() } - } - - $events - } -} -} - -process { - $result = Get-VIEventPlus -Start ((get-date).adddays(-$Days)) -EventType @("VmCreatedEvent", "VmBeingClonedEvent", "VmBeingDeployedEvent","VmRemovedEvent") - $sortedResult = $result | Select CreatedTime, @{N='Cluster';E={$_.ComputeResource.Name}}, @{Name="VMName";Expression={$_.vm.name}}, UserName, @{N='Type';E={$_.GetType().Name}}, FullFormattedMessage | Sort CreatedTime - $sortedResult | where {$_.Cluster -like $ClusterName} -} -} \ No newline at end of file diff --git a/Modules/Get-VMmaxIOPS.psm1 b/Modules/Get-VMmaxIOPS.psm1 deleted file mode 100644 index 27af1ad..0000000 --- a/Modules/Get-VMmaxIOPS.psm1 +++ /dev/null @@ -1,114 +0,0 @@ -function Get-VMmaxIOPS { -<# - .NOTES - =========================================================================== - Created by: Markus Kraus - Twitter: @VMarkus_K - Private Blog: mycloudrevolution.com - =========================================================================== - Changelog: - 2016.10 ver 1.0 Base Release - 2016.11 ver 1.1 Added vSphere 6.5 Support, New Counters, More Error Handling - =========================================================================== - External Code Sources: - http://www.lucd.info/2011/04/22/get-the-maximum-iops/ - https://communities.vmware.com/thread/485386 - =========================================================================== - Tested Against Environment: - vSphere Version: 5.5 U2, 6.5 - PowerCLI Version: PowerCLI 6.3 R1, 6.5 R1 - PowerShell Version: 4.0, 5.0 - OS Version: Windows 8.1, Windows Server 2012 R2 - =========================================================================== - Keywords vSphere, ESXi, VM, Storage - =========================================================================== - - .DESCRIPTION - This Function will Create a VM Disk IOPS Report - - .Example - Get-VM TST* | Get-VMmaxIOPS -Minutes 60 | FT -Autosize - - .Example - $SampleVMs = Get-VM "TST*" - Get-VMmaxIOPS -VMs $SampleVMs -Minutes 60 - - .PARAMETER VMs - Specify the VMs - - .PARAMETER Minutes - Specify the Minutes to report (10080 is one Week) - -#Requires PS -Version 4.0 -#Requires -Modules VMware.VimAutomation.Core, @{ModuleName="VMware.VimAutomation.Core";ModuleVersion="6.3.0.0"} -#> - -[CmdletBinding()] -param( - [Parameter(Mandatory=$true, ValueFromPipeline=$True, Position=0)] - [ValidateNotNullorEmpty()] - [VMware.VimAutomation.ViCore.Impl.V1.Inventory.InventoryItemImpl[]] $VMs, - [Parameter(Mandatory=$false, Position=1, HelpMessage = "Specify the Minutes to report (10080 is one Week)")] - [ValidateNotNullorEmpty()] - [int] $Minutes = 30 -) -Begin { - # none - } -Process { - if ($_.PowerState -eq "PoweredOn") { - #region: Global Definitions - [int]$TimeRange = "-" + $Minutes - #endregion - - #region: Creating VM Stats - Write-Verbose "$(Get-Date -Format G) Create VM Stats..." - $VMMetrics = "virtualdisk.numberwriteaveraged.average","virtualdisk.numberreadaveraged.average" - $Start = (Get-Date).AddMinutes($TimeRange) - $stats = Get-Stat -Realtime -Stat $VMMetrics -Entity $VMs -Start $Start -Verbose:$False - Write-Verbose "$(Get-Date -Format G) Create VM Stats completed" - #endregion - - #region: Creating HD-Tab - Write-Verbose "$(Get-Date -Format G) Create HD Tab..." - $hdTab = @{} - foreach($hd in (Get-Harddisk -VM $VMs)){ - $controllerKey = $hd.Extensiondata.ControllerKey - $controller = $hd.Parent.Extensiondata.Config.Hardware.Device | where{$_.Key -eq $controllerKey} - $hdTab[$hd.Parent.Name + "/scsi" + $controller.BusNumber + ":" + $hd.Extensiondata.UnitNumber] = $hd.FileName.Split(']')[0].TrimStart('[') - } - Write-Verbose "$(Get-Date -Format G) Create HD Tab completed" - #endregion - - #region: Creating Reports - Write-Verbose "$(Get-Date -Format G) Create Report..." - $reportPerf = @() - $reportPerf = $stats | Group-Object -Property {$_.Entity.Name},Instance | %{ - New-Object PSObject -Property @{ - VM = $_.Values[0] - Disk = $_.Values[1] - IOPSWriteAvg = [math]::round( ($_.Group | ` - where{$_.MetricId -eq "virtualdisk.numberwriteaveraged.average"} | ` - Measure-Object -Property Value -Average).Average,2) - IOPSReadAvg = [math]::round( ($_.Group | ` - where{$_.MetricId -eq "virtualdisk.numberreadaveraged.average"} | ` - Measure-Object -Property Value -Average).Average,2) - Datastore = $hdTab[$_.Values[0] + "/"+ $_.Values[1]] - } - } - Write-Verbose "$(Get-Date -Format G) Create Report completed" - #endregion - - - } - Else { - Write-Error "VM $($_.Name) is Powered Off! Processing Skipped" - } - $reportPerf | Select-Object VM, Disk, Datastore, IOPSWriteAvg, IOPSReadAvg - } - -End { - # none - } - -} \ No newline at end of file diff --git a/Modules/Konfig-ESXi.psm1 b/Modules/Konfig-ESXi.psm1 deleted file mode 100644 index f14386a..0000000 --- a/Modules/Konfig-ESXi.psm1 +++ /dev/null @@ -1,234 +0,0 @@ -function Konfig-ESXi { -<# - .NOTES - =========================================================================== - Created by: Markus Kraus - Twitter: @VMarkus_K - Private Blog: mycloudrevolution.com - =========================================================================== - Changelog: - 2016.12 ver 1.0 Base Release - 2016.12 ver 1.1 ESXi 6.5 Tests, Minor enhancements - =========================================================================== - External Code Sources: - Function My-Logger : http://www.virtuallyghetto.com/ - =========================================================================== - Tested Against Environment: - vSphere Version: ESXi 5.5 U2, ESXi 6.5 - PowerCLI Version: PowerCLI 6.3 R1, PowerCLI 6.5 R1 - PowerShell Version: 4.0, 5.0 - OS Version: Windows 8.1, Server 2012 R2 - Keyword: ESXi, NTP, SSH, Syslog, SATP, - =========================================================================== - - .DESCRIPTION - This Function sets the Basic settings for a new ESXi. - - * NTP - * SSH - * Syslog - * Power Management - * HP 3PAR SATP/PSP Rule - * ... - - .Example - Konfig-ESXi -VMHost myesxi.lan.local -NTP 192.168.2.1, 192.168.2.2 -syslog "udp://loginsight.lan.local:514" - - .PARAMETER VMHost - Host to configure. - - .PARAMETER NTP - NTP Server(s) to set. - - .PARAMETER Syslog - Syslog Server to set, e.g. "udp://loginsight.lan.local:514" - - DNS Name must be resolvable! - - -#Requires PS -Version 4.0 -#Requires -Modules VMware.VimAutomation.Core, @{ModuleName="VMware.VimAutomation.Core";ModuleVersion="6.3.0.0"} -#> - -[CmdletBinding()] -param( - [Parameter(Mandatory=$True, ValueFromPipeline=$False, Position=0)] - [String] $VMHost, - [Parameter(Mandatory=$true, ValueFromPipeline=$False, Position=1)] - [array]$NTP, - [Parameter(Mandatory=$true, ValueFromPipeline=$False, Position=2)] - [String] $syslog - -) - -Begin { - Function My-Logger { - param( - [Parameter(Mandatory=$true)] - [String]$message - ) - - $timeStamp = Get-Date -Format "MM-dd-yyyy_hh-mm-ss" - - Write-Host -NoNewline -ForegroundColor White "[$timestamp]" - Write-Host -ForegroundColor Green " $message" - } - function Set-MyESXiOption { - [CmdletBinding()] - param( - [Parameter(Mandatory=$True, ValueFromPipeline=$False, Position=0)] - [String] $Name, - [Parameter(Mandatory=$False, ValueFromPipeline=$False, Position=1)] - [String] $Value - ) - process { - $myESXiOption = Get-AdvancedSetting -Entity $ESXiHost -Name $Name - if ($myESXiOption.Value -ne $Value) { - My-Logger " Setting ESXi Option $Name to Value $Value" - $myESXiOption | Set-AdvancedSetting -Value $Value -Confirm:$false | Out-Null - } - else { - My-Logger " ESXi Option $Name already has Value $Value" - } - } - } -} - -Process { - $Validate = $True - - #region: Start vCenter Connection - My-Logger "Starting to Process ESXi Server Connection to $VMHost ..." - if (($global:DefaultVIServers).count -gt 0) { - Disconnect-VIServer -Force -Confirm:$False -ErrorAction SilentlyContinue - } - $VIConnection = Connect-VIServer -Server $VMHost - if (-not $VIConnection.IsConnected) { - Write-Error "ESXi Connection Failed." - $Validate = $False - } - elseif ($VIConnection.ProductLine -ne "EmbeddedEsx") { - Write-Error "Connencted System is not an ESXi." - $Validate = $False - } - else { - $ESXiHost = Get-VMHost - My-Logger "Connected ESXi Version: $($ESXiHost.Version) $($ESXiHost.Build) " - } - #endregion - - if ($Validate -eq $True) { - - #region: Enable SSH and disable SSH Warning - $SSHService = $ESXiHost | Get-VMHostService | where {$_.Key -eq 'TSM-SSH'} - My-Logger "Starting SSH Service..." - if($SSHService.Running -ne $True){ - Start-VMHostService -HostService $SSHService -Confirm:$false | Out-Null - } - else { - My-Logger " SSH Service is already running" - } - My-Logger "Setting SSH Service to Automatic Start..." - if($SSHService.Policy -ne "automatic"){ - Set-VMHostService -HostService $SSHService -Policy "Automatic" | Out-Null - } - else { - My-Logger " SSH Service is already set to Automatic Start" - } - My-Logger "Disabling SSH Warning..." - Set-MyESXiOption -Name "UserVars.SuppressShellWarning" -Value "1" - #endregion - - #region: Config NTP - My-Logger "Removing existing NTP Server..." - try { - $ESXiHost | Remove-VMHostNtpServer -NtpServer (Get-VMHostNtpServer) -Confirm:$false - } - catch [System.Exception] { - Write-Warning "Error during removing existing NTP Servers." - } - My-Logger "Setting new NTP Servers..." - foreach ($myNTP in $NTP) { - $ESXiHost | Add-VMHostNtpServer -ntpserver $myNTP -confirm:$False | Out-Null - } - - My-Logger "Configure NTP Service..." - $NTPService = $ESXiHost | Get-VMHostService| Where-Object {$_.key -eq "ntpd"} - if($NTPService.Running -eq $True){ - Stop-VMHostService -HostService $NTPService -Confirm:$false | Out-Null - } - if($NTPService.Policy -ne "on"){ - Set-VMHostService -HostService $NTPService -Policy "on" -confirm:$False | Out-Null - } - - My-Logger "Configure Local Time..." - $HostTimeSystem = Get-View $ESXiHost.ExtensionData.ConfigManager.DateTimeSystem - $HostTimeSystem.UpdateDateTime([DateTime]::UtcNow) - - My-Logger "Start NTP Service..." - Start-VMHostService -HostService $NTPService -confirm:$False | Out-Null - #endregion - - #region: Remove default PG - My-Logger "Checking for Default Port Group ..." - if ($defaultPG = $ESXiHost | Get-VirtualSwitch -Name vSwitch0 | Get-VirtualPortGroup -Name "VM Network" -ErrorAction SilentlyContinue ){ - Remove-VirtualPortGroup -VirtualPortGroup $defaultPG -confirm:$False | Out-Null - My-Logger " Default PG Removed" - } - else { - My-Logger " No Default PG found" - } - #endregion - - #region: Configure Static HighPower - My-Logger "Setting PowerProfile to Static HighPower..." - try { - $HostView = ($ESXiHost | Get-View) - (Get-View $HostView.ConfigManager.PowerSystem).ConfigurePowerPolicy(1) - } - catch [System.Exception] { - Write-Warning "Error during Configure Static HighPower. See latest errors..." - } - #endregion - - #region: Conf Syslog - My-Logger "Setting Syslog Firewall Rule ..." - $SyslogFW = ($ESXiHost | Get-VMHostFirewallException | where {$_.Name -eq 'syslog'}) - if ($SyslogFW.Enabled -eq $False ){ - $SyslogFW | Set-VMHostFirewallException -Enabled:$true -Confirm:$false | Out-Null - My-Logger " Syslog Firewall Rule enabled" - } - else { - My-Logger " Syslog Firewall Rule already enabled" - } - My-Logger "Setting Syslog Server..." - Set-MyESXiOption -Name "Syslog.global.logHost" -Value $syslog - #endregion - - #region: Change Disk Scheduler - My-Logger "Changing Disk Scheduler..." - Set-MyESXiOption -Name "Disk.SchedulerWithReservation" -Value "0" - #endregion - - #region: Configure HP 3PAR SATP/PSP Rule - My-Logger "Configure HP 3PAR SATP/PSP Rule" - $esxcli2 = Get-ESXCLI -VMHost $ESXiHost -V2 - $arguments = $esxcli2.storage.nmp.satp.rule.add.CreateArgs() - $arguments.satp = "VMW_SATP_ALUA" - $arguments.psp = "VMW_PSP_RR" - $arguments.pspoption = "iops=100" - $arguments.claimoption = "tpgs_on" - $arguments.vendor = "3PARdata" - $arguments.model = "VV" - $arguments.description = "HP 3PAR custom SATP Claimrule" - try { - $esxcli2.storage.nmp.satp.rule.add.Invoke($arguments) - } - catch { - Write-Warning "Error during Configure HP 3PAR SATP/PSP Rule. See latest errors..." - } - #endregion - - } - } -} diff --git a/Modules/PSvLIMessage.psm1 b/Modules/PSvLIMessage.psm1 deleted file mode 100644 index 9cab209..0000000 --- a/Modules/PSvLIMessage.psm1 +++ /dev/null @@ -1,123 +0,0 @@ -add-type @" - using System.Net; - using System.Security.Cryptography.X509Certificates; - public class TrustAllCertsPolicy : ICertificatePolicy { - public bool CheckValidationResult( - ServicePoint srvPoint, X509Certificate certificate, - WebRequest request, int certificateProblem) { - return true; - } - } -"@ -[System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy - -<# - .NOTES - =========================================================================== - Created by: Markus Kraus - Organization: Private - Personal Blog: mycloudrevolution.com - Twitter: @vMarkus_K - =========================================================================== - Tested Against Environment: - vRealize Log Insight 3.3.1 - PowerShell Version: 4.0, 5.0 - OS Version: Windows 8.1, Server 2012 R2 - Keyword: vRealize, RestAPI - - Dependencies: - PowerCLI Version: PowerCLI 6.3 R1 - - .SYNOPSIS - Push Messages to VMware vRealize Log Insight. - - .DESCRIPTION - Creates a Messages in VMware vRealize Log Insight via the Ingestion API - - .EXAMPLE - Push-vLIMessage -vLIServer "loginsight.lan.local" -vLIAgentID "12862842-5A6D-679C-0E38-0E2BE888BB28" -Text "My Test" - - .EXAMPLE - Push-vLIMessage -vLIServer "loginsight.lan.local" -vLIAgentID "12862842-5A6D-679C-0E38-0E2BE888BB28" -Text "My Test" -Hostname MyTEST -FieldName myTest -FieldContent myTest - - .PARAMETER vLIServer - Specify the FQDN of your vRealize Log Insight Appliance - - .PARAMETER vLIAgentID - Specify the vRealize Log Insight Agent ID, e.g. "12862842-5A6D-679C-0E38-0E2BE888BB28" - - .PARAMETER Text - Specify the Event Text - - .PARAMETER Hostname - Specify the Hostanme displayed in vRealize Log Insight - - .PARAMETER FieldName - Specify the a Optional Field Name for vRealize Log Insight - - .PARAMETER FieldContent - Specify the a Optional FieldContent for the Field in -FieldName for vRealize Log Insight - If FielName is missing and FieldContent is given, it will be ignored - - #Requires PS -Version 3.0 - - #> -function Push-vLIMessage { - - [cmdletbinding()] - param ( - [parameter(Mandatory=$true)] - [string]$Text, - [parameter(Mandatory=$true)] - [string]$vLIServer, - [parameter(Mandatory=$true)] - [string]$vLIAgentID, - [parameter(Mandatory=$false)] - [string]$Hostname = $env:computername, - [parameter(Mandatory=$false)] - [string]$FieldName, - [parameter(Mandatory=$false)] - [string]$FieldContent = "" - ) - Process { - $Field_vLI = [ordered]@{ - name = "PS_vLIMessage" - content = "true" - } - $Field_HostName = [ordered]@{ - name = "hostname" - content = $Hostname - } - - $Fields = @($Field_vLI, $Field_HostName) - - if ($FieldName) { - $Field_Custom = [ordered]@{ - name = $FieldName - content = $FieldContent - } - $Fields += @($Field_Custom) - } - - $Restcall = @{ - messages = ([Object[]]([ordered]@{ - text = ($Text) - fields = ([Object[]]$Fields) - })) - } | convertto-json -Depth 4 - - $Resturl = ("http://" + $vLIServer + ":9000/api/v1/messages/ingest/" + $vLIAgentID) - try - { - $Response = Invoke-RestMethod $Resturl -Method Post -Body $Restcall -ContentType 'application/json' -ErrorAction stop - Write-Information "REST Call to Log Insight server successful" - Write-Verbose $Response - } - catch - { - Write-Error "REST Call failed to Log Insight server" - Write-Verbose $error[0] - Write-Verbose $Resturl - } - } -} \ No newline at end of file diff --git a/Modules/ProactiveHA.psm1 b/Modules/ProactiveHA.psm1 deleted file mode 100644 index ea4e92f..0000000 --- a/Modules/ProactiveHA.psm1 +++ /dev/null @@ -1,468 +0,0 @@ -Function New-PHAProvider { -<# - .NOTES - =========================================================================== - Created by: William Lam - Organization: VMware - Blog: www.virtuallyghetto.com - Twitter: @lamw - =========================================================================== - .DESCRIPTION - Function to register a new Proactive HA Provider with vCenter Server - .PARAMETER ProviderName - Name of ProactiveHA Provider - .PARAMETER ComponentType - Name of a supported ComponentType that ProactiveHA supports (Fan, Memory, Network, Power or Storage) - .PARAMETER ComponentDescription - Description of the health check for the given component - .PARAMETER ComponentId - Unique identifier for the given component within a ProactiveHA Provider - .EXAMPLE - New-PHAProvider -ProviderName "virtuallyGhetto" -ComponentType Power -ComponentDescription "Simulated ProactiveHA Provider" -ComponentId "Power" -#> - param( - [Parameter(Mandatory=$true)][String]$ProviderName, - [Parameter(Mandatory=$true)][ValidateSet("Fan","Memory","Network","Power","Storage")][String]$ComponentType, - [Parameter(Mandatory=$true)][String]$ComponentDescription, - [Parameter(Mandatory=$true)][String]$ComponentId - ) - Write-Host -ForegroundColor Red "`n******************** DISCLAIMER ********************" - Write-Host -ForegroundColor Red "**** THIS IS NOT INTENDED FOR PRODUCTION USE ****" - Write-Host -ForegroundColor Red "**** LEARNING PURPOSES ONLY ****" - Write-Host -ForegroundColor Red "******************** DISCLAIMER ********************`n" - - $healthManager = Get-View $global:DefaultVIServer.ExtensionData.Content.HealthUpdateManager - - $healthInfo = [VMware.Vim.HealthUpdateInfo] @{ - ComponentType = $ComponentType - description = $ComponentDescription - Id = $ComponentId - } - - try { - Write-Host "`nRegistering new Proactive HA Provider $ProviderName ..." - $providerId = $healthManager.RegisterHealthUpdateProvider($ProviderName,$healthInfo) - } catch { - Write-host -ForegroundColor Red $Error[0].Exception - } -} - -Function Get-PHAProvider { -<# - .NOTES - =========================================================================== - Created by: William Lam - Organization: VMware - Blog: www.virtuallyghetto.com - Twitter: @lamw - =========================================================================== - .DESCRIPTION - Function to return list of all Proactive HA Providers registered with vCenter Server - .EXAMPLE - Get-PHAProvider -#> - $healthManager = Get-View $global:DefaultVIServer.ExtensionData.Content.HealthUpdateManager - - $healthProviderResults = @() - $hpIDs = $healthManager.QueryProviderList() - - foreach ($hpID in $hpIDs) { - $hpName = $healthManager.QueryProviderName($hpID) - $hpConfig = $healthManager.QueryHealthUpdateInfos($hpID) - - $hp = [pscustomobject] @{ - ProviderName = $hpName - ProviderID = $hpID - ComponentType = $hpConfig.componentType - ComponentID = $hpConfig.id - Description = $hpConfig.description - } - $healthProviderResults+=$hp - } - $healthProviderResults -} - -Function Remove-PHAProvider { -<# - .NOTES - =========================================================================== - Created by: William Lam - Organization: VMware - Blog: www.virtuallyghetto.com - Twitter: @lamw - =========================================================================== - .DESCRIPTION - Function to remove a registered Proactive HA Provider from vCenter Server - .PARAMETER ProviderId - The ProactiveHA provider ID (retrieved from Get-PHAProvider) to unregister - .EXAMPLE - Remove-PHAProvider -ProviderID "52 85 22 c2 f2 6a e7 b9-fc ff 63 9e 10 81 00 79" -#> - param( - [Parameter(Mandatory=$true)][String]$ProviderId - ) - - Write-Host -ForegroundColor Red "`n******************** DISCLAIMER ********************" - Write-Host -ForegroundColor Red "**** THIS IS NOT INTENDED FOR PRODUCTION USE ****" - Write-Host -ForegroundColor Red "**** LEARNING PURPOSES ONLY ****" - Write-Host -ForegroundColor Red "******************** DISCLAIMER ********************`n" - - $healthManager = Get-View $global:DefaultVIServer.ExtensionData.Content.HealthUpdateManager - - try { - Write-Host "`nUnregistering Proactive HA Provider $ProviderId ... " - $healthManager.UnregisterHealthUpdateProvider($providerId) - } catch { - if($Error[0].Exception.InnerException.MethodFault.getType().Name -eq "InvalidState") { - Write-host -ForegroundColor Red "The Proactive HA Provider is still in use, please disable it before unregistering" - } else { - Write-host -ForegroundColor Red $Error[0].Exception - } - } -} - -Function Set-PHAConfig { -<# - .NOTES - =========================================================================== - Created by: William Lam - Organization: VMware - Blog: www.virtuallyghetto.com - Twitter: @lamw - =========================================================================== - .DESCRIPTION - Function to enable/disable Proactive HA for vSphere Cluster - .PARAMETER Cluster - Name of the vSphere Cluster to enable Proactive HA - .PARAMETER ProviderId - Proactive HA Provider ID to enable in vSphere Cluster - .PARAMETER ClusterMode - Whether Proactive HA should be "Automated" or "Manual" for actions it will take - .PARAMETER ModerateRemediation - Type of operation (Maintenance Mode or Quaratine Mode) to perform when a Moderate issue is observed - .PARAMETER SevereRemediation - Type of operation (Maintenance Mode or Quaratine Mode) to perform when a Severe issue is observed - .EXAMPLE - Set-PHAConfig -Cluster VSAN-Cluster -Enabled -ClusterMode Automated -ModerateRemediation QuarantineMode -SevereRemediation QuarantineMode -ProviderID "52 85 22 c2 f2 6a e7 b9-fc ff 63 9e 10 81 00 79" - .EXAMPLE - Set-PHAConfig -Cluster VSAN-Cluster -Disabled -ProviderID "52 85 22 c2 f2 6a e7 b9-fc ff 63 9e 10 81 00 79" -#> - param( - [Parameter(Mandatory=$true)][String]$ProviderId, - [Parameter(Mandatory=$true)][String]$Cluster, - [Parameter(Mandatory=$false)][ValidateSet("Automated","Manual")]$ClusterMode="Manual", - [Parameter(Mandatory=$false)][ValidateSet("MaintenanceMode","QuarantineMode")]$ModerateRemediation="QuarantineMode", - [Parameter(Mandatory=$false)][ValidateSet("MaintenanceMode","QuarantineMode")]$SevereRemediation="QuarantineMode", - [Switch]$Enabled, - [Switch]$Disabled - ) - - $ClusterView = Get-View -ViewType ClusterComputeResource -Property Name,Host,ConfigurationEx -Filter @{"Name" = $Cluster} - - if($ClusterView -eq $null) { - Write-Host -ForegroundColor Red "Unable to find vSphere Cluster $cluster ..." - break - } - - $vmhosts = $ClusterView.host - - $healthManager = Get-View $global:DefaultVIServer.ExtensionData.Content.HealthUpdateManager - - if($Enabled) { - try { - $entities = @() - foreach ($vmhost in $vmhosts) { - if(-not $healthManager.HasMonitoredEntity($ProviderId,$vmhost)) { - $entities += $vmhost - } - } - - Write-Host "Enabling Proactive HA monitoring for all ESXi hosts in cluster ..." - $healthManager.AddMonitoredEntities($ProviderId,$entities) - } catch { - Write-host -ForegroundColor Red $Error[0].Exception - } - - try { - $healthProviders = @() - - # Make sure not to remove existing ProactiveHA providers - if($ClusterView.ConfigurationEx.InfraUpdateHaConfig.Providers -ne $null) { - $currentHPs = $ClusterView.ConfigurationEx.infraUpdateHaConfig.providers - foreach ($currentHP in $currentHPs) { - $healthProviders+=$currentHP - } - if(-not ($healthProviders -contains $ProviderID)) { - $healthProviders+=$ProviderId - } - } else { - $healthProviders+=$ProviderId - } - - $PHASpec = [VMware.Vim.ClusterInfraUpdateHaConfigInfo] @{ - enabled = $true - behavior = $ClusterMode - moderateRemediation = $ModerateRemediation - severeRemediation = $SevereRemediation - providers = $healthProviders - } - - $spec = [VMware.Vim.ClusterConfigSpecEx] @{ - infraUpdateHaConfig = $PHASpec - } - - Write-Host "Enabling Proactive HA Provider $ProviderId on $Cluster ..." - $task = $ClusterView.ReconfigureComputeResource_Task($spec,$True) - $task1 = Get-Task -Id ("Task-$($task.value)") - $task1 | Wait-Task | Out-Null - } catch { - Write-host -ForegroundColor Red $Error[0].Exception - } - } - - if($Disabled) { - foreach ($vmhost in $vmhosts) { - if($vmhost.runtime.inQuarantineMode) { - Write-Host -ForegroundColor Red $vmhost.name " is currently still in Quaratine Mode, please remediate this before disabling Proactive HA" - break - } - } - - try { - $healthProviders = @() - - # Make sure not to remove existing ProactiveHA providers - if($ClusterView.ConfigurationEx.InfraUpdateHaConfig.Providers -ne $null) { - $currentHPs = $ClusterView.ConfigurationEx.infraUpdateHaConfig.providers - foreach ($currentHP in $currentHPs) { - if($currentHP -ne $ProviderId) { - $healthProviders+=$currentHP - } - } - } - - $PHASpec = [VMware.Vim.ClusterInfraUpdateHaConfigInfo] @{ - enabled = $true - behavior = $ClusterMode - moderateRemediation = $ModerateRemediation - severeRemediation = $SevereRemediation - providers = $healthProviders - } - - $spec = [VMware.Vim.ClusterConfigSpecEx] @{ - infraUpdateHaConfig = $PHASpec - } - - Write-Host "Disabling Proactive HA Provider $ProviderId on $Cluster ..." - $task = $ClusterView.ReconfigureComputeResource_Task($spec,$True) - $task1 = Get-Task -Id ("Task-$($task.value)") - $task1 | Wait-Task | Out-Null - } catch { - Write-host -ForegroundColor Red $Error[0].Exception - } - - $ClusterView.UpdateViewData() - - try { - $entities = @() - foreach ($vmhost in $vmhosts) { - if($healthManager.HasMonitoredEntity($ProviderId,$vmhost)) { - $entities += $vmhost - } - } - - Write-Host "Disabling Proactive HA monitoring for all ESXi hosts in cluster ..." - $healthManager.RemoveMonitoredEntities($ProviderId,$entities) - } catch { - Write-host -ForegroundColor Red $Error[0].Exception - } - } -} - -Function Get-PHAConfig { -<# - .NOTES - =========================================================================== - Created by: William Lam - Organization: VMware - Blog: www.virtuallyghetto.com - Twitter: @lamw - =========================================================================== - .DESCRIPTION - Function to retrieve Proactive HA configuration for a vSphere Cluster - .PARAMETER Cluster - Name of the vSphere Cluster to check Proactive HA configuration - .EXAMPLE - Get-PHAConfig -Cluster VSAN-Cluster -#> - param( - [Parameter(Mandatory=$true)][String]$Cluster - ) - - $ClusterView = Get-View -ViewType ClusterComputeResource -Property Name,ConfigurationEx -Filter @{"Name" = $Cluster} - - if($ClusterView -eq $null) { - Write-Host -ForegroundColor Red "Unable to find vSphere Cluster $cluster ..." - break - } - - if($ClusterView.ConfigurationEx.InfraUpdateHaConfig.Providers -ne $null) { - $healthManager = Get-View $global:DefaultVIServer.ExtensionData.Content.HealthUpdateManager - - $phSettings = $ClusterView.ConfigurationEx.InfraUpdateHaConfig - $providers = $ClusterView.ConfigurationEx.InfraUpdateHaConfig.Providers - $healthProviders = @() - foreach ($provider in $providers) { - $providerName = $healthManager.QueryProviderName($provider) - $healthProviders+=$providerName - } - - $pHAConfig = [pscustomobject] @{ - Enabled = $phSettings.Enabled - ClusterMode = $phSettings.behavior - ModerateRemediation = $phSettings.ModerateRemediation - SevereRemediation = $phSettings.SevereRemediation - HealthProviders = $healthProviders - } - $pHAConfig - } else { - Write-Host "Proactive HA has not been configured on this vSphere Cluster" - } -} - -Function Get-PHAHealth { -<# - .NOTES - =========================================================================== - Created by: William Lam - Organization: VMware - Blog: www.virtuallyghetto.com - Twitter: @lamw - =========================================================================== - .DESCRIPTION - Function to retrieve the Proactive HA health info for all ESXi hosts in vSphere Cluster - .PARAMETER Cluster - Name of the vSphere Cluster to check Proactive HA health information - .EXAMPLE - Get-PHAHealth -Cluster VSAN-Cluster -#> - param( - [Parameter(Mandatory=$true)][String]$Cluster - ) - - $ClusterView = Get-View -ViewType ClusterComputeResource -Property Name,ConfigurationEx -Filter @{"Name" = $Cluster} - - if($ClusterView -eq $null) { - Write-Host -ForegroundColor Red "Unable to find vSphere Cluster $cluster ..." - break - } - - if($ClusterView.ConfigurationEx.InfraUpdateHaConfig.Providers -ne $null) { - $healthManager = Get-View $global:DefaultVIServer.ExtensionData.Content.HealthUpdateManager - - $providers = $ClusterView.ConfigurationEx.InfraUpdateHaConfig.Providers - - foreach ($provider in $providers) { - $providerName = $healthManager.QueryProviderName($provider) - $healthUpdates = $healthManager.QueryHealthUpdates($provider) - - $healthResults = @() - Write-Host -NoNewline -ForegroundColor Magenta "Health summary for Proactive HA Provider $providerName`:`n" - foreach ($healthUpdate in $healthUpdates) { - $vmhost = Get-View $healthUpdate.Entity - - $hr = [PSCustomObject] @{ - Entity = $vmhost.name - Status = $healthUpdate.status - HealthComponentId = $healthUpdate.HealthUpdateInfoId - HealthUpdateId = $healthUpdate.Id - Remediation = $healthUpdate.Remediation - } - $healthResults+=$hr - } - $healthResults - } - } else { - Write-Host "Proactive HA has not been configured on this vSphere Cluster" - } -} - -Function New-PHASimulation { -<# - .NOTES - =========================================================================== - Created by: William Lam - Organization: VMware - Blog: www.virtuallyghetto.com - Twitter: @lamw - =========================================================================== - .DESCRIPTION - Function to return VCHA Configuration - .PARAMETER ProviderId - The Proactive HA Provider ID that you like to simulate a health update from - .PARAMETER EsxiHost - The name of ESXi host to update the health on - .PARAMETER Component - The name of the matching component ID from Proactive HA Provider to simulate a health update from - .PARAMETER HealthStatus - The health value (green, yellow or red) for the given simulated health Update - .PARAMETER Remediation - The remediation message associated with simulated health update - .EXAMPLE - New-PHASimulation -EsxiHost vesxi65-4.primp-industries.com -Component Power -HealthStatus green -Remediation "" -ProviderId "52 85 22 c2 f2 6a e7 b9-fc ff 63 9e 10 81 00 79" - .EXAMPLE - New-PHASimulation -EsxiHost vesxi65-4.primp-industries.com -Component Power -HealthStatus red -Remediation "Please replace my virtual PSU" -ProviderId "52 85 22 c2 f2 6a e7 b9-fc ff 63 9e 10 81 00 79" -#> - param( - [Parameter(Mandatory=$true)][String]$ProviderId, - [Parameter(Mandatory=$true)][String]$EsxiHost, - [Parameter(Mandatory=$true)][String]$Component, - [Parameter(Mandatory=$true)][ValidateSet("green","red","yellow")][String]$HealthStatus, - [Parameter(Mandatory=$false)][String]$Remediation - ) - - Write-Host -ForegroundColor Red "`n******************** DISCLAIMER ********************" - Write-Host -ForegroundColor Red "**** THIS IS NOT INTENDED FOR PRODUCTION USE ****" - Write-Host -ForegroundColor Red "**** LEARNING PURPOSES ONLY ****" - Write-Host -ForegroundColor Red "******************** DISCLAIMER ********************`n" - - $vmhost = Get-View -ViewType HostSystem -Property Name -Filter @{"name" = $EsxiHost} - - if($vmhost -eq $null) { - Write-Host -ForegroundColor Red "`nUnable to find ESXi host $EsxiHost ..." - break - } - - $healthManager = Get-View $global:DefaultVIServer.ExtensionData.Content.HealthUpdateManager - - # Randomly generating an ID for Health Update - # In general, you would want to generate a specific ID - # which can be referenced between ProactiveHA Provider - # and VMware logs for troubleshooting purposes - $HealthUpdateID = "vghetto-" + (Get-Random -Minimum 1 -Maximum 100000) - - # All other Health Status can have a remediation message - # but for green, it must be an empty string or API call will fail - if($HealthStatus -eq "green") { - $Remediation = "" - } - - $healthUpdate = [VMware.Vim.HealthUpdate] @{ - Entity = $vmhost.moref - HealthUpdateInfoId = $Component - Id = $HealthUpdateId - Status = $HealthStatus - Remediation = $Remediation - } - - try { - Write-Host "`nSimulating Proactive HA Health Update to ..." - Write-Host "`tHost: $EsxiHost " - Write-Host -NoNewline "`tStatus: " - Write-Host -ForegroundColor $HealthStatus "$HealthStatus" - Write-Host "`tRemediation Messsage: $Remediation" - $healthManager.PostHealthUpdates($providerId,$healthUpdate) - } catch { - Write-host -ForegroundColor Red $Error[0].Exception - } -} \ No newline at end of file diff --git a/Modules/Recommend-Sizing.psm1 b/Modules/Recommend-Sizing.psm1 deleted file mode 100644 index 0075e43..0000000 --- a/Modules/Recommend-Sizing.psm1 +++ /dev/null @@ -1,227 +0,0 @@ -function Recommend-Sizing { -<# - .NOTES - =========================================================================== - Created by: Markus Kraus - Twitter: @VMarkus_K - Private Blog: mycloudrevolution.com - =========================================================================== - Changelog: - 2016.11 ver 1.0 Base Release - 2016.11 ver 1.1 Optional Stats Collection - 2016.11 ver 1.2 VM Stats from Realtime Data and new Counters - =========================================================================== - External Code Sources: - http://www.lucd.info/2011/04/22/get-the-maximum-iops/ - https://communities.vmware.com/thread/485386 - =========================================================================== - Tested Against Environment: - vSphere Version: 5.5 U2, 6.0 - PowerCLI Version: PowerCLI 6.3 R1, PowerCLI 6.5 R1 - PowerShell Version: 4.0, 5.0 - OS Version: Windows 8.1, Server 2012 R2 - =========================================================================== - Keywords vSphere, ESXi, VM, Storage, Sizing - =========================================================================== - - .DESCRIPTION - This Function collects Basic vSphere Informations for a Hardware Sizing Recommandation. Focus is in Compute Ressources. - - .Example - Recommend-Sizing -ClusterNames Cluster01, Cluster02 -Stats -StatsRange 60 -Verbose - - .Example - Recommend-Sizing -ClusterNames Cluster01, Cluster02 - - .Example - Recommend-Sizing -ClusterNames Cluster01 - - .PARAMETER ClusterNames - List of your vSphere Cluser Names to process. - - .PARAMETER Stats - Enables Stats Collection. - - Warning: At the moment this is only fully tested with vSphere 5.5 and vSphere 6.5! - - .PARAMETER StatsRange - Time Range in Minutes for the Stats Collection. - Default is 24h. - -#Requires PS -Version 4.0 -#Requires -Modules VMware.VimAutomation.Core, @{ModuleName="VMware.VimAutomation.Core";ModuleVersion="6.3.0.0"} -#> - -[CmdletBinding()] -param( - [Parameter(Mandatory=$True, ValueFromPipeline=$False, Position=0)] - [Array] $ClusterNames, - [Parameter(Mandatory=$False, ValueFromPipeline=$False, Position=1, ParameterSetName = "Stats")] - [switch] $Stats, - [Parameter(Mandatory=$False, ValueFromPipeline=$False, Position=2, ParameterSetName = "Stats")] - [int] $StatsRange = 1440 - -) -Begin { - if ($Stats) { - Write-Warning "Stats Collection requested.`nAt the moment this is only fully tested with vSphere 5.5 and vSphere 6.5" - [int]$TimeRange = "-" + $StatsRange - } - - $Validate = $True - #region: Check Clusters - Write-Verbose "$(Get-Date -Format G) Starting Cluster Validation..." - foreach ($ClusterName in $ClusterNames) { - $TestCluster = Get-Cluster -Name $ClusterName -ErrorAction SilentlyContinue -Verbose:$False - if(!($TestCluster)){ - Write-Warning "No Custer found wth Name $ClusterName!" - $Validate = $False - } - elseif ($TestCluster.count -gt 1) { - Write-Warning "Multiple Custers found wth Name $ClusterName!`nUse a List of explicit Cluster Names: Recommend-Sizing -ClusterNames Cluster01, Cluster02 " - $Validate = $False - } - } - Write-Verbose "$(Get-Date -Format G) Cluster Validation completed" - #endregion -} - -Process { - $MyView = @() - if ($Validate -eq $True) { - foreach ($ClusterName in $ClusterNames) { - #region: Get Cluster Objects - Write-Verbose "$(Get-Date -Format G) Collect $ClusterName Cluster Objects..." - $Cluster = Get-Cluster -Name $ClusterName -Verbose:$False - $ClusterVMs = $Cluster | Get-VM -Verbose:$False - $ClusterVMsPoweredOn = $ClusterVMs | where {$_.PowerState -eq "PoweredOn"} - $ClusterDatastores = $Cluster | Get-Datastore -Verbose:$False - $ClusterHosts = $Cluster | Get-VMHost -Verbose:$False - $HostsAverageMemoryUsageGB = [math]::round( ($ClusterHosts | Measure-Object -Average -Property MemoryUsageGB).Average,1 ) - $HostsAverageMemoryUsage = $([math]::round( (($ClusterHosts | Measure-Object -Average -Property MemoryUsageGB).Average / ($ClusterHosts | Measure-Object -Average -Property MemoryTotalGB).Average) * 100,1 )) - $HostsAverageCpuUsageMhz = [math]::round( ($ClusterHosts | Measure-Object -Average -Property CpuUsageMhz).Average,1 ) - $HostsAverageCpuUsage = $([math]::round( (($ClusterHosts | Measure-Object -Average -Property CpuUsageMhz).Average / ($ClusterHosts | Measure-Object -Average -Property CpuTotalMhz).Average) * 100,1 )) - Write-Verbose "$(Get-Date -Format G) Collect $($Cluster.name) Cluster Objects completed" - #endregion - - #region: CPU Calculation - Write-Verbose "$(Get-Date -Format G) Collect $($Cluster.name) CPU Details..." - $VMvCPUs = ($ClusterVMs | Measure-Object -Sum -Property NumCpu).sum - $LogicalThreads = $Cluster.ExtensionData.Summary.NumCpuThreads - $CpuCores = $Cluster.ExtensionData.Summary.NumCpuCores - $vCPUpCPUratio = [math]::round( $VMvCPUs / $LogicalThreads,1 ) - Write-Verbose "$(Get-Date -Format G) Collect $($Cluster.name) CPU Details completed." - #endregion - - #region: Memory Calculation - Write-Verbose "$(Get-Date -Format G) Collect $($Cluster.name) Memory Details..." - $AllocatedVMMemoryGB = [math]::round( ($ClusterVMs | Measure-Object -Sum -Property MemoryGB).sum ) - $PhysicalMemory = [math]::round( $Cluster.ExtensionData.Summary.TotalMemory / 1073741824,1 ) - $MemoryUsage = [math]::round( ($AllocatedVMMemoryGB / $PhysicalMemory) * 100 ,1 ) - Write-Verbose "$(Get-Date -Format G) Collect $($Cluster.name) Memory Details completed" - #endregion - - if ($Stats) { - #region: Creating VM Stats - Write-Verbose "$(Get-Date -Format G) Create $($Cluster.name) VM Stats..." - $VMMetrics = "disk.numberwrite.summation","disk.numberread.summation","cpu.usage.average", "mem.usage.average" - $Start = (Get-Date).AddMinutes($TimeRange) - $VMStats = Get-Stat -Realtime -Stat $VMMetrics -Entity $ClusterVMsPoweredOn -Start $Start -Verbose:$False - Write-Verbose "$(Get-Date -Format G) Create $($Cluster.name) VM Stats completed" - #endregion - - #region: Creating VM Stats Report - Write-Verbose "$(Get-Date -Format G) Process $($Cluster.name) VM Stats Report..." - $ReportVMPerf = @() - $ReportVMPerf = $VMStats | Group-Object -Property {$_.Entity.Name},Instance | %{ - New-Object PSObject -Property @{ - IOPSWriteAvg = ($_.Group | ` - where{$_.MetricId -eq "disk.numberwrite.summation"} | ` - Measure-Object -Property Value -Average).Average - IOPSReadAvg = ($_.Group | ` - where{$_.MetricId -eq "disk.numberread.summation"} | ` - Measure-Object -Property Value -Average).Average - CPUUsageAvg = ($_.Group | ` - where{$_.MetricId -eq "cpu.usage.average"} | ` - Measure-Object -Property Value -Average).Average - MEMUsageAvg = ($_.Group | ` - where{$_.MetricId -eq "mem.usage.average"} | ` - Measure-Object -Property Value -Average).Average - } - } - Write-Verbose "$(Get-Date -Format G) Process $($Cluster.name) VM Stats Report completed" - #endregion - } - else { - Write-Verbose "$(Get-Date -Format G) Stats Collection skipped..." - } - - #region: Create VM Disk Space Report - Write-Verbose "$(Get-Date -Format G) Process $($Cluster.name) VM Disk Space Report..." - $reportDiskSpace = @() - foreach ($ClusterVM in $ClusterVMs){ - $VMDKs = $ClusterVM | get-HardDisk -Verbose:$False - foreach ($VMDK in $VMDKs) { - if ($VMDK -ne $null){ - [int]$CapacityGB = $VMDK.CapacityKB/1024/1024 - $Report = [PSCustomObject] @{ - CapacityGB = $CapacityGB - } - $reportDiskSpace += $Report - } - } - } - Write-Verbose "$(Get-Date -Format G) Process $($Cluster.name) VM Disk Space Report completed" - #endregion - - #region: Create Datastore Space Report - Write-Verbose "$(Get-Date -Format G) Process $($Cluster.name) Datastore Space Report..." - $DatastoreReport = @($ClusterDatastores | Select-Object @{N="CapacityGB";E={[math]::Round($_.CapacityGB,2)}}, @{N="FreeSpaceGB";E={[math]::Round($_.FreeSpaceGB,2)}}, @{N="UsedSpaceGB";E={[math]::Round($_.CapacityGB - $_.FreeSpaceGB,2)}}) - Write-Verbose "$(Get-Date -Format G) Process $($Cluster.name) Datastore Space Report completed" - #endregion - - #region: Create Global Report - Write-Verbose "$(Get-Date -Format G) Process Global Report..." - $SizingReport = [PSCustomObject] @{ - Cluster = $Cluster.name - HAEnabled = $Cluster.HAEnabled - DrsEnabled = $Cluster.DrsEnabled - Hosts = $Cluster.ExtensionData.Summary.NumHosts - HostsAverageMemoryUsageGB = $HostsAverageMemoryUsageGB - HostsAverageMemoryUsage = "$HostsAverageMemoryUsage %" - HostsAverageCpuUsageMhz = $HostsAverageCpuUsageMhz - HostsAverageCpuUsage = "$HostsAverageCpuUsage %" - PhysicalCPUCores = $CpuCores - LogicalCPUThreads = $LogicalThreads - VMs = $ClusterVMs.count - ActiveVMs = $ClusterVMsPoweredOn.count - VMvCPUs = $VMvCPUs - vCPUpCPUratio = "$vCPUpCPUratio : 1" - PhysicalMemoryGB = $PhysicalMemory - AllocatedVMMemoryGB = $AllocatedVMMemoryGB - ClusterMemoryUsage = "$MemoryUsage %" - SumVMDiskSpaceGB = [math]::round( ($reportDiskSpace | Measure-Object -Sum -Property CapacityGB).sum, 1 ) - SumDatastoreSpaceGB = [math]::round( ($DatastoreReport | Measure-Object -Sum -Property CapacityGB).sum, 1 ) - SumDatastoreUsedSpaceGB = [math]::round( ($DatastoreReport | Measure-Object -Sum -Property UsedSpaceGB).sum, 1 ) - AverageVMIOPSWriteAvg = [math]::round( ($ReportVMPerf | Measure-Object -Average -Property IOPSWriteAvg).Average,1 ) - AverageVMIOPSReadAvg = [math]::round( ($ReportVMPerf | Measure-Object -Average -Property IOPSReadAvg).Average,1 ) - AverageVMCPUUsageAvg = "$([math]::round( ($ReportVMPerf | Measure-Object -Average -Property CPUUsageAvg).Average,1 )) %" - AverageVMMEMUsageAvg = "$([math]::round( ($ReportVMPerf | Measure-Object -Average -Property MEMUsageAvg).Average,1 )) %" - } - $MyView += $SizingReport - Write-Verbose "$(Get-Date -Format G) Process Global Report completed" - #endregion - } - - } - Else { - Write-Error "Validation Failed! Processing Skipped" - } - - } - - End { - $MyView - } - -} \ No newline at end of file diff --git a/Modules/Set-CBT.psm1 b/Modules/Set-CBT.psm1 deleted file mode 100644 index 784aebb..0000000 --- a/Modules/Set-CBT.psm1 +++ /dev/null @@ -1,111 +0,0 @@ -function Set-CBT { -<# - .NOTES - =========================================================================== - Created by: Markus Kraus - Twitter: @VMarkus_K - Private Blog: mycloudrevolution.com - =========================================================================== - Changelog: - 2016.11 ver 1.0 Base Release - =========================================================================== - External Code Sources: - http://wahlnetwork.com/2015/12/01/change-block-tracking-cbt-powercli/ - =========================================================================== - Tested Against Environment: - vSphere Version: 5.5 U2 - PowerCLI Version: PowerCLI 6.3 R1 - PowerShell Version: 4.0 - OS Version: Windows Server 2012 R2 - =========================================================================== - Keywords vSphere, ESXi, VM, Storage, CBT, Backup - =========================================================================== - - .DESCRIPTION - This Function enables or disables CBT. - - .Example - Get-VN TST* | Set-CBT -DisableCBT - - .Example - Get-VN TST* | Set-CBT -EnableCBT - - .PARAMETER DisableCBT - Disables CBT for any VMs found with it enabled - - .PARAMETER EnableCBT - Enables CBT for any VMs found with it disabled - -#Requires PS -Version 4.0 -#Requires -Modules VMware.VimAutomation.Core, @{ModuleName="VMware.VimAutomation.Core";ModuleVersion="6.3.0.0"} -#> - - [CmdletBinding()] - param( - [Parameter(Mandatory=$True, ValueFromPipeline=$True, Position=0, HelpMessage = "VMs to process")] - [ValidateNotNullorEmpty()] - [VMware.VimAutomation.ViCore.Impl.V1.Inventory.InventoryItemImpl[]] $myVMs, - [Parameter(Mandatory = $False,ValueFromPipeline=$False, Position = 1, HelpMessage = "Enables CBT for any VMs found with it disabled", ParameterSetName = "EnableCBT")] - [ValidateNotNullorEmpty()] - [Switch]$EnableCBT, - [Parameter(Mandatory = $False,ValueFromPipeline=$False, Position = 1, HelpMessage = "Disables CBT for any VMs found with it enabled", ParameterSetName = "DisableCBT")] - [ValidateNotNullorEmpty()] - [Switch]$DisableCBT - ) -Process { - - $vmconfigspec = New-Object -TypeName VMware.Vim.VirtualMachineConfigSpec - Write-Verbose -Message "Walking through given VMs" - foreach($myVM in $myVMs) - { - if ($DisableCBT -and $myVM.ExtensionData.Config.ChangeTrackingEnabled -eq $true -and $myVM.ExtensionData.Snapshot -eq $null) - { - try - { - Write-Verbose -Message "Reconfiguring $($myVM.name) to disable CBT" -Verbose - $vmconfigspec.ChangeTrackingEnabled = $false - $myVM.ExtensionData.ReconfigVM($vmconfigspec) - - if ($myVM.PowerState -eq "PoweredOn" ) { - Write-Verbose -Message "Creating a snapshot on $($myVM.name) to clear CBT file" -Verbose - $SnapShot = New-Snapshot -VM $myVM -Name "CBT Cleanup" - - Write-Verbose -Message "Removing snapshot on $($myVM.name)" -Verbose - $SnapShot| Remove-Snapshot -Confirm:$false - } - - } - catch - { - throw $myVM - } - } - elseif ($EnableCBT -and $myVM.ExtensionData.Config.ChangeTrackingEnabled -eq $false -and $myVM.ExtensionData.Snapshot -eq $null) - { - Write-Verbose -Message "Reconfiguring $($myVM.name) to enable CBT" -Verbose - $vmconfigspec.ChangeTrackingEnabled = $true - $myVM.ExtensionData.ReconfigVM($vmconfigspec) - - if ($myVM.PowerState -eq "PoweredOn" ) { - Write-Verbose -Message "Creating a snapshot on $($myVM.name) to Create CBT file" -Verbose - $SnapShot = New-Snapshot -VM $myVM -Name "CBT Cleanup" - - Write-Verbose -Message "Removing snapshot on $($myVM.name)" -Verbose - $SnapShot | Remove-Snapshot -Confirm:$false - } - } - else - { - if ($myVM.ExtensionData.Snapshot -ne $null -and $EnableCBT) - { - Write-Warning -Message "Skipping $($myVM.name) - Snapshots found" - } - elseif ($myVM.ExtensionData.Snapshot -ne $null -and $DisableCBT) - { - Write-Warning -Message "Skipping $($myVM.name) - Snapshots found" - } - } - } - - } -} diff --git a/Modules/Start-UNMAP.psm1 b/Modules/Start-UNMAP.psm1 deleted file mode 100644 index a8e9896..0000000 --- a/Modules/Start-UNMAP.psm1 +++ /dev/null @@ -1,99 +0,0 @@ -function Start-UNMAP { -<# - .SYNOPSIS - Process SCSI UNMAP on VMware Datastores - - .DESCRIPTION - This Function will process SCSI UNMAP on VMware Datastores via ESXCLI -V2 - - .Example - Start-UNMAP -ClusterName myCluster -DSWildcard *RAID5* - - .Example - Start-UNMAP -ClusterName myCluster -DSWildcard *RAID5* -Verbose -WhatIf - - .Notes - NAME: Start-UNMAP.psm1 - AUTHOR: Markus Kraus - LASTEDIT: 23.09.2016 - VERSION: 1.0 - KEYWORDS: VMware, vSphere, ESXi, SCSI, VAAI, UNMAP - - .Link - http://mycloudrevolution.com/ - - #Requires PS -Version 4.0 - #Requires -Modules VMware.VimAutomation.Core, @{ModuleName="VMware.VimAutomation.Core";ModuleVersion="6.3.0.0"} - #> - - [CmdletBinding(SupportsShouldProcess = $true,ConfirmImpact='High')] - param( - [Parameter(Mandatory=$true, Position=0)] - [String]$ClusterName, - [Parameter(Mandatory=$true, Position=1)] - [String]$DSWildcard - ) - Process { - $Validate = $true - #region: PowerCLI Session Timeout - Write-Verbose "Set Session Timeout ..." - $initialTimeout = (Get-PowerCLIConfiguration -Scope Session).WebOperationTimeoutSeconds - Set-PowerCLIConfiguration -Scope Session -WebOperationTimeoutSeconds -1 -Confirm:$False | Out-Null - #endregion - - #region: Get Cluster - $Cluster = Get-Cluster -Name $ClusterName -ErrorAction SilentlyContinue - Write-Verbose "vSphere Cluster: $Cluster" - if (!$Cluster){Write-Error "No Cluster found!"; $Validate = $false} - #endregion - - #region: Get Hosts - $ClusterHosts = $Cluster | Get-VMHost -ErrorAction SilentlyContinue | where {$_.ConnectionState -eq "Connected" -and $_.PowerState -eq "PoweredOn"} - Write-Verbose "vSphere Cluster Hosts: $ClusterHosts" - if (!$ClusterHosts){Write-Error "No Hosts found!"; $Validate = $false} - #endregion - - #region: Get Datastores - $ClusterDataStores = $Cluster | Get-Datastore -ErrorAction SilentlyContinue | where {$_.Name -like $DSWildcard -and $_.State -eq "Available" -and $_.Accessible -eq "True"} - Write-Verbose "vSphere Cluster Datastores: $ClusterDataStores" - if (!$ClusterDataStores){Write-Error "No Datastores found!"; $Validate = $false} - #endregion - - #region: Process Datastores - if ($Validate -eq $true) { - Write-Verbose "Starting Loop..." - foreach ($ClusterDataStore in $ClusterDataStores) { - Write-Verbose "vSphere Datastore to Process: $ClusterDataStore" - $myHost = $ClusterHosts[(Get-Random -Maximum ($ClusterHosts).count)] - Write-Verbose "vSphere Host to Process: $myHost" - $esxcli2 = $myHost | Get-ESXCLI -V2 - $arguments = $esxcli2.storage.vmfs.unmap.CreateArgs() - $arguments.volumelabel = $ClusterDataStore - $arguments.reclaimunit = "256" - if ($PSCmdlet.ShouldProcess( $ClusterDataStore,"Starting UNMAP on $myHost")) { - $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() - try { - Write-Output "Starting UNMAP for $ClusterDataStore on $myHost..." - $esxcli2.storage.vmfs.unmap.Invoke($arguments) - } - catch { - Write-Output "A Error occured: " "" $error[0] "" - } - $stopwatch.Stop() - Write-Output "UNMAP duration: $($stopwatch.Elapsed.Minutes)" - } - - } - } - else { - Write-Error "Validation Failed. Processing Loop Skipped!" - } - #endregion - - #region: Revert PowerCLI Session Timeout - Write-Verbose "Revert Session Timeout ..." - Set-PowerCLIConfiguration -Scope Session -WebOperationTimeoutSeconds $initialTimeout -Confirm:$False | Out-Null - #endregion - } - -} diff --git a/Modules/VAMI.psm1 b/Modules/VAMI.psm1 deleted file mode 100755 index 92c5d5f..0000000 --- a/Modules/VAMI.psm1 +++ /dev/null @@ -1,716 +0,0 @@ -Function Get-VAMISummary { -<# - .NOTES - =========================================================================== - Created by: William Lam - Organization: VMware - Blog: www.virtuallyghetto.com - Twitter: @lamw - =========================================================================== - .SYNOPSIS - This function retrieves some basic information from VAMI interface (5480) - for a VCSA node which can be an Embedded VCSA, External PSC or External VCSA. - .DESCRIPTION - Function to return basic VAMI summary info - .EXAMPLE - Connect-CisServer -Server 192.168.1.51 -User administrator@vsphere.local -Password VMware1! - Get-VAMISummary -#> - $systemVersionAPI = Get-CisService -Name 'com.vmware.appliance.system.version' - $results = $systemVersionAPI.get() | select product, type, version, build, install_time - - $systemUptimeAPI = Get-CisService -Name 'com.vmware.appliance.system.uptime' - $ts = [timespan]::fromseconds($systemUptimeAPI.get().toString()) - $uptime = $ts.ToString("hh\:mm\:ss\,fff") - - $summaryResult = [pscustomobject] @{ - Product = $results.product; - Type = $results.type; - Version = $results.version; - Build = $results.build; - InstallTime = $results.install_time; - Uptime = $uptime - } - $summaryResult -} - -Function Get-VAMIHealth { -<# - .NOTES - =========================================================================== - Created by: William Lam - Organization: VMware - Blog: www.virtuallyghetto.com - Twitter: @lamw - =========================================================================== - .SYNOPSIS - This function retrieves health information from VAMI interface (5480) - for a VCSA node which can be an Embedded VCSA, External PSC or External VCSA. - .DESCRIPTION - Function to return VAMI health - .EXAMPLE - Connect-CisServer -Server 192.168.1.51 -User administrator@vsphere.local -Password VMware1! - Get-VAMIHealth -#> - $healthOverall = (Get-CisService -Name 'com.vmware.appliance.health.system').get() - $healthLastCheck = (Get-CisService -Name 'com.vmware.appliance.health.system').lastcheck() - $healthCPU = (Get-CisService -Name 'com.vmware.appliance.health.load').get() - $healthMem = (Get-CisService -Name 'com.vmware.appliance.health.mem').get() - $healthSwap = (Get-CisService -Name 'com.vmware.appliance.health.swap').get() - $healthStorage = (Get-CisService -Name 'com.vmware.appliance.health.storage').get() - - # DB health only applicable for Embedded/External VCSA Node - $vami = (Get-CisService -Name 'com.vmware.appliance.system.version').get() - - if($vami.type -eq "vCenter Server with an embedded Platform Services Controller" -or $vami.type -eq "vCenter Server with an external Platform Services Controller") { - $healthVCDB = (Get-CisService -Name 'com.vmware.appliance.health.databasestorage').get() - } else { - $healthVCDB = "N/A" - } - $healthSoftwareUpdates = (Get-CisService -Name 'com.vmware.appliance.health.softwarepackages').get() - - $healthResult = [pscustomobject] @{ - HealthOverall = $healthOverall; - HealthLastCheck = $healthLastCheck; - HealthCPU = $healthCPU; - HealthMem = $healthMem; - HealthSwap = $healthSwap; - HealthStorage = $healthStorage; - HealthVCDB = $healthVCDB; - HealthSoftware = $healthSoftwareUpdates - } - $healthResult -} - -Function Get-VAMIAccess { -<# - .NOTES - =========================================================================== - Created by: William Lam - Organization: VMware - Blog: www.virtuallyghetto.com - Twitter: @lamw - =========================================================================== - .SYNOPSIS - This function retrieves access information from VAMI interface (5480) - for a VCSA node which can be an Embedded VCSA, External PSC or External VCSA. - .DESCRIPTION - Function to return VAMI access interfaces (Console,DCUI,Bash Shell & SSH) - .EXAMPLE - Connect-CisServer -Server 192.168.1.51 -User administrator@vsphere.local -Password VMware1! - Get-VAMIAccess -#> - $consoleAccess = (Get-CisService -Name 'com.vmware.appliance.access.consolecli').get() - $dcuiAccess = (Get-CisService -Name 'com.vmware.appliance.access.dcui').get() - $shellAccess = (Get-CisService -Name 'com.vmware.appliance.access.shell').get() - $sshAccess = (Get-CisService -Name 'com.vmware.appliance.access.ssh').get() - - $accessResult = New-Object PSObject -Property @{ - Console = $consoleAccess; - DCUI = $dcuiAccess; - BashShell = $shellAccess.enabled; - SSH = $sshAccess - } - $accessResult -} - -Function Get-VAMITime { -<# - .NOTES - =========================================================================== - Created by: William Lam - Organization: VMware - Blog: www.virtuallyghetto.com - Twitter: @lamw - =========================================================================== - .SYNOPSIS - This function retrieves the time and NTP info from VAMI interface (5480) - for a VCSA node which can be an Embedded VCSA, External PSC or External VCSA. - .DESCRIPTION - Function to return current Time and NTP information - .EXAMPLE - Connect-CisServer -Server 192.168.1.51 -User administrator@vsphere.local -Password VMware1! - Get-VAMITime -#> - $systemTimeAPI = Get-CisService -Name 'com.vmware.appliance.system.time' - $timeResults = $systemTimeAPI.get() - - $timeSync = (Get-CisService -Name 'com.vmware.appliance.techpreview.timesync').get() - $timeSyncMode = $timeSync.mode - - $timeResult = [pscustomobject] @{ - Timezone = $timeResults.timezone; - Date = $timeResults.date; - CurrentTime = $timeResults.time; - Mode = $timeSyncMode; - NTPServers = "N/A"; - NTPStatus = "N/A"; - } - - if($timeSyncMode -eq "NTP") { - $ntpServers = (Get-CisService -Name 'com.vmware.appliance.techpreview.ntp').get() - $timeResult.NTPServers = $ntpServers.servers - $timeResult.NTPStatus = $ntpServers.status - } - $timeResult -} - -Function Get-VAMINetwork { -<# - .NOTES - =========================================================================== - Created by: William Lam - Organization: VMware - Blog: www.virtuallyghetto.com - Twitter: @lamw - =========================================================================== - .SYNOPSIS - This function retrieves network information from VAMI interface (5480) - for a VCSA node which can be an Embedded VCSA, External PSC or External VCSA. - .DESCRIPTION - Function to return networking information including details for each interface - .EXAMPLE - Connect-CisServer -Server 192.168.1.51 -User administrator@vsphere.local -Password VMware1! - Get-VAMINetwork -#> - $netResults = @() - - $Hostname = (Get-CisService -Name 'com.vmware.appliance.networking.dns.hostname').get() - $dns = (Get-CisService -Name 'com.vmware.appliance.networking.dns.servers').get() - - Write-Host "Hostname: " $hostname - Write-Host "DNS Servers: " $dns.servers - - $interfaces = (Get-CisService -Name 'com.vmware.appliance.networking.interfaces').list() - foreach ($interface in $interfaces) { - $ipv4API = (Get-CisService -Name 'com.vmware.appliance.techpreview.networking.ipv4') - $spec = $ipv4API.Help.get.interfaces.CreateExample() - $spec+= $interface.name - $ipv4result = $ipv4API.get($spec) - - $interfaceResult = [pscustomobject] @{ - Inteface = $interface.name; - MAC = $interface.mac; - Status = $interface.status; - Mode = $ipv4result.mode; - IP = $ipv4result.address; - Prefix = $ipv4result.prefix; - Gateway = $ipv4result.default_gateway; - Updateable = $ipv4result.updateable - } - $netResults += $interfaceResult - } - $netResults -} - -Function Get-VAMIDisks { -<# - .NOTES - =========================================================================== - Created by: William Lam - Organization: VMware - Blog: www.virtuallyghetto.com - Twitter: @lamw - =========================================================================== - .SYNOPSIS - This function retrieves VMDK disk number to partition mapping VAMI interface (5480) - for a VCSA node which can be an Embedded VCSA, External PSC or External VCSA. - .DESCRIPTION - Function to return VMDK disk number to OS partition mapping - .EXAMPLE - Connect-CisServer -Server 192.168.1.51 -User administrator@vsphere.local -Password VMware1! - Get-VAMIDisks -#> - $storageAPI = Get-CisService -Name 'com.vmware.appliance.system.storage' - $disks = $storageAPI.list() - - foreach ($disk in $disks | sort {[int]$_.disk.toString()}) { - $disk | Select Disk, Partition - } -} - -Function Start-VAMIDiskResize { -<# - .NOTES - =========================================================================== - Created by: William Lam - Organization: VMware - Blog: www.virtuallyghetto.com - Twitter: @lamw - =========================================================================== - .SYNOPSIS - This function triggers an OS partition resize after adding additional disk capacity - for a VCSA node which can be an Embedded VCSA, External PSC or External VCSA. - .DESCRIPTION - Function triggers OS partition resize operation - .EXAMPLE - Connect-CisServer -Server 192.168.1.51 -User administrator@vsphere.local -Password VMware1! - Start-VAMIDiskResize -#> - $storageAPI = Get-CisService -Name 'com.vmware.appliance.system.storage' - Write-Host "Initiated OS partition resize operation ..." - $storageAPI.resize() -} - -Function Get-VAMIStatsList { -<# - .NOTES - =========================================================================== - Created by: William Lam - Organization: VMware - Blog: www.virtuallyghetto.com - Twitter: @lamw - =========================================================================== - .SYNOPSIS - This function retrieves list avialable monitoring metrics in VAMI interface (5480) - for a VCSA node which can be an Embedded VCSA, External PSC or External VCSA. - .DESCRIPTION - Function to return list of available monitoring metrics that can be queried - .EXAMPLE - Connect-CisServer -Server 192.168.1.51 -User administrator@vsphere.local -Password VMware1! - Get-VAMIStatsList -#> - $monitoringAPI = Get-CisService -Name 'com.vmware.appliance.monitoring' - $ids = $monitoringAPI.list() | Select id | Sort-Object -Property id - - foreach ($id in $ids) { - $id - } -} - -Function Get-VAMIStorageUsed { -<# - .NOTES - =========================================================================== - Created by: William Lam - Organization: VMware - Blog: www.virtuallyghetto.com - Twitter: @lamw - =========================================================================== - .SYNOPSIS - This function retrieves the individaul OS partition storage utilization - for a VCSA node which can be an Embedded VCSA, External PSC or External VCSA. - .DESCRIPTION - Function to return individual OS partition storage utilization - .EXAMPLE - Connect-CisServer -Server 192.168.1.51 -User administrator@vsphere.local -Password VMware1! - Get-VAMIStorageUsed -#> - $monitoringAPI = Get-CisService 'com.vmware.appliance.monitoring' - $querySpec = $monitoringAPI.help.query.item.CreateExample() - - # List of IDs from Get-VAMIStatsList to query - $querySpec.Names = @( - "storage.used.filesystem.autodeploy", - "storage.used.filesystem.boot", - "storage.used.filesystem.coredump", - "storage.used.filesystem.imagebuilder", - "storage.used.filesystem.invsvc", - "storage.used.filesystem.log", - "storage.used.filesystem.netdump", - "storage.used.filesystem.root", - "storage.used.filesystem.updatemgr", - "storage.used.filesystem.vcdb_core_inventory", - "storage.used.filesystem.vcdb_seat", - "storage.used.filesystem.vcdb_transaction_log", - "storage.totalsize.filesystem.autodeploy", - "storage.totalsize.filesystem.boot", - "storage.totalsize.filesystem.coredump", - "storage.totalsize.filesystem.imagebuilder", - "storage.totalsize.filesystem.invsvc", - "storage.totalsize.filesystem.log", - "storage.totalsize.filesystem.netdump", - "storage.totalsize.filesystem.root", - "storage.totalsize.filesystem.updatemgr", - "storage.totalsize.filesystem.vcdb_core_inventory", - "storage.totalsize.filesystem.vcdb_seat", - "storage.totalsize.filesystem.vcdb_transaction_log" - ) - - # Tuple (Filesystem Name, Used, Total) to store results - $storageStats = @{ - "autodeploy"=@{"name"="/storage/autodeploy";"used"=0;"total"=0}; - "boot"=@{"name"="/boot";"used"=0;"total"=0}; - "coredump"=@{"name"="/storage/core";"used"=0;"total"=0}; - "imagebuilder"=@{"name"="/storage/imagebuilder";"used"=0;"total"=0}; - "invsvc"=@{"name"="/storage/invsvc";"used"=0;"total"=0}; - "log"=@{"name"="/storage/log";"used"=0;"total"=0}; - "netdump"=@{"name"="/storage/netdump";"used"=0;"total"=0}; - "root"=@{"name"="/";"used"=0;"total"=0}; - "updatemgr"=@{"name"="/storage/updatemgr";"used"=0;"total"=0}; - "vcdb_core_inventory"=@{"name"="/storage/db";"used"=0;"total"=0}; - "vcdb_seat"=@{"name"="/storage/seat";"used"=0;"total"=0}; - "vcdb_transaction_log"=@{"name"="/storage/dblog";"used"=0;"total"=0} - } - - $querySpec.interval = "DAY1" - $querySpec.function = "MAX" - $querySpec.start_time = ((get-date).AddDays(-1)) - $querySpec.end_time = (Get-Date) - $queryResults = $monitoringAPI.query($querySpec) | Select * -ExcludeProperty Help - - foreach ($queryResult in $queryResults) { - # Update hash if its used storage results - if($queryResult.name -match "used") { - $key = (($queryResult.name).toString()).split(".")[-1] - $value = [Math]::Round([int]($queryResult.data[1]).toString()/1MB,2) - $storageStats[$key]["used"] = $value - # Update hash if its total storage results - } else { - $key = (($queryResult.name).toString()).split(".")[-1] - $value = [Math]::Round([int]($queryResult.data[1]).toString()/1MB,2) - $storageStats[$key]["total"] = $value - } - } - - $storageResults = @() - foreach ($key in $storageStats.keys | Sort-Object -Property name) { - $statResult = [pscustomobject] @{ - Filesystem = $storageStats[$key].name; - Used = $storageStats[$key].used; - Total = $storageStats[$key].total - } - $storageResults += $statResult - } - $storageResults -} - -Function Get-VAMIService { -<# - .NOTES - =========================================================================== - Created by: William Lam - Organization: VMware - Blog: www.virtuallyghetto.com - Twitter: @lamw - =========================================================================== - .SYNOPSIS - This function retrieves list of services in VAMI interface (5480) - for a VCSA node which can be an Embedded VCSA, External PSC or External VCSA. - .DESCRIPTION - Function to return list of services and their description - .EXAMPLE - Connect-CisServer -Server 192.168.1.51 -User administrator@vsphere.local -Password VMware1! - Get-VAMIService - .EXAMPLE - Get-VAMIService -Name rbd -#> - param( - [Parameter( - Mandatory=$false, - ValueFromPipeline=$true, - ValueFromPipelineByPropertyName=$true) - ] - [String]$Name - ) - - if($Name -ne "") { - $vMonAPI = Get-CisService 'com.vmware.appliance.vmon.service' - - try { - $serviceStatus = $vMonAPI.get($name,0) - $serviceString = [pscustomobject] @{ - Name = $name; - State = $serviceStatus.state; - Health = ""; - Startup = $serviceStatus.startup_type - } - if($serviceStatus.health -eq $null) { $serviceString.Health = "N/A"} else { $serviceString.Health = $serviceStatus.health } - $serviceString - } catch { - Write-Error $Error[0].exception.Message - } - } else { - $vMonAPI = Get-CisService 'com.vmware.appliance.vmon.service' - $services = $vMonAPI.list_details() - - $serviceResult = @() - foreach ($key in $services.keys | Sort-Object -Property Value) { - $serviceString = [pscustomobject] @{ - Name = $key; - State = $services[$key].state; - Health = "N/A"; - Startup = $services[$key].Startup_type - } - if($services[$key].health -eq $null) { $serviceString.Health = "N/A"} else { $serviceString.Health = $services[$key].health } - - $serviceResult += $serviceString - } - $serviceResult - } -} - -Function Start-VAMIService { -<# - .NOTES - =========================================================================== - Created by: William Lam - Organization: VMware - Blog: www.virtuallyghetto.com - Twitter: @lamw - =========================================================================== - .SYNOPSIS - This function retrieves list of services in VAMI interface (5480) - for a VCSA node which can be an Embedded VCSA, External PSC or External VCSA. - .DESCRIPTION - Function to return list of services and their description - .EXAMPLE - Connect-CisServer -Server 192.168.1.51 -User administrator@vsphere.local -Password VMware1! - Start-VAMIService -Name rbd -#> - param( - [Parameter( - Mandatory=$true, - ValueFromPipeline=$true, - ValueFromPipelineByPropertyName=$true) - ] - [String]$Name - ) - - $vMonAPI = Get-CisService 'com.vmware.appliance.vmon.service' - - try { - Write-Host "Starting $name service ..." - $vMonAPI.start($name) - } catch { - Write-Error $Error[0].exception.Message - } -} - -Function Stop-VAMIService { -<# - .NOTES - =========================================================================== - Created by: William Lam - Organization: VMware - Blog: www.virtuallyghetto.com - Twitter: @lamw - =========================================================================== - .SYNOPSIS - This function retrieves list of services in VAMI interface (5480) - for a VCSA node which can be an Embedded VCSA, External PSC or External VCSA. - .DESCRIPTION - Function to return list of services and their description - .EXAMPLE - Connect-CisServer -Server 192.168.1.51 -User administrator@vsphere.local -Password VMware1! - Stop-VAMIService -Name rbd -#> - param( - [Parameter( - Mandatory=$true, - ValueFromPipeline=$true, - ValueFromPipelineByPropertyName=$true) - ] - [String]$Name - ) - - $vMonAPI = Get-CisService 'com.vmware.appliance.vmon.service' - - try { - Write-Host "Stopping $name service ..." - $vMonAPI.stop($name) - } catch { - Write-Error $Error[0].exception.Message - } -} - -Function Get-VAMIBackupSize { -<# - .NOTES - =========================================================================== - Created by: William Lam - Organization: VMware - Blog: www.virtuallyghetto.com - Twitter: @lamw - =========================================================================== - .SYNOPSIS - This function retrieves the backup size of the VCSA from VAMI interface (5480) - for a VCSA node which can be an Embedded VCSA, External PSC or External VCSA. - .DESCRIPTION - Function to return the current backup size of the VCSA (common and core data) - .EXAMPLE - Connect-CisServer -Server 192.168.1.51 -User administrator@vsphere.local -Password VMware1! - Get-VAMIBackupSize -#> - $recoveryAPI = Get-CisService 'com.vmware.appliance.recovery.backup.parts' - $backupParts = $recoveryAPI.list() | select id - - $estimateBackupSize = 0 - $backupPartSizes = "" - foreach ($backupPart in $backupParts) { - $partId = $backupPart.id.value - $partSize = $recoveryAPI.get($partId) - $estimateBackupSize += $partSize - $backupPartSizes += $partId + " data is " + $partSize + " MB`n" - } - - Write-Host "Estimated Backup Size: $estimateBackupSize MB" - Write-Host $backupPartSizes -} - -Function Get-VAMIUser { -<# - .NOTES - =========================================================================== - Created by: William Lam - Organization: VMware - Blog: www.virtuallyghetto.com - Twitter: @lamw - =========================================================================== - .SYNOPSIS - This function retrieves VAMI local users using VAMI interface (5480) - for a VCSA node which can be an Embedded VCSA, External PSC or External VCSA. - .DESCRIPTION - Function to retrieve VAMI local users - .EXAMPLE - Connect-CisServer -Server 192.168.1.51 -User administrator@vsphere.local -Password VMware1! - Get-VAMIUser -#> - param( - [Parameter( - Mandatory=$false, - ValueFromPipeline=$true, - ValueFromPipelineByPropertyName=$true) - ] - [String]$Name - ) - - $userAPI = Get-CisService 'com.vmware.appliance.techpreview.localaccounts.user' - - $userResults = @() - - if($Name -ne "") { - try { - $user = $userAPI.get($name) - - $userString = [pscustomobject] @{ - User = $user.username - Name = $user.fullname - Email = $user.email - Status = $user.status - PasswordStatus = $user.passwordstatus - Role = $user.role - } - $userResults += $userString - } catch { - Write-Error $Error[0].exception.Message - } - } else { - $users = $userAPI.list() - - foreach ($user in $users) { - $userString = [pscustomobject] @{ - User = $user.username - Name = $user.fullname - Email = $user.email - Status = $user.status - PasswordStatus = $user.passwordstatus - Role = $user.role - } - $userResults += $userString - } - } - $userResults -} - -Function New-VAMIUser { -<# - .NOTES - =========================================================================== - Created by: William Lam - Organization: VMware - Blog: www.virtuallyghetto.com - Twitter: @lamw - =========================================================================== - .SYNOPSIS - This function to create new VAMI local user using VAMI interface (5480) - for a VCSA node which can be an Embedded VCSA, External PSC or External VCSA. - .DESCRIPTION - Function to create a new VAMI local user - .EXAMPLE - Connect-CisServer -Server 192.168.1.51 -User administrator@vsphere.local -Password VMware1! - New-VAMIUser -name lamw -fullname "William Lam" -role "operator" -email "lamw@virtuallyghetto.com" -password "VMware1!" -#> - param( - [Parameter( - Mandatory=$true) - ] - [String]$name, - [Parameter( - Mandatory=$true) - ] - [String]$fullname, - [Parameter( - Mandatory=$true) - ] - [ValidateSet("admin","operator","superAdmin")][String]$role, - [Parameter( - Mandatory=$false) - ] - [String]$email="", - [Parameter( - Mandatory=$true) - ] - [String]$password - ) - - $userAPI = Get-CisService 'com.vmware.appliance.techpreview.localaccounts.user' - $createSpec = $userAPI.Help.add.config.CreateExample() - - $createSpec.username = $name - $createSpec.fullname = $fullname - $createSpec.role = $role - $createSpec.email = $email - $createSpec.password = [VMware.VimAutomation.Cis.Core.Types.V1.Secret]$password - - try { - Write-Host "Creating new user $name ..." - $userAPI.add($createSpec) - } catch { - Write-Error $Error[0].exception.Message - } -} - -Function Remove-VAMIUser { -<# - .NOTES - =========================================================================== - Created by: William Lam - Organization: VMware - Blog: www.virtuallyghetto.com - Twitter: @lamw - =========================================================================== - .SYNOPSIS - This function to remove VAMI local user using VAMI interface (5480) - for a VCSA node which can be an Embedded VCSA, External PSC or External VCSA. - .DESCRIPTION - Function to remove VAMI local user - .EXAMPLE - Connect-CisServer -Server 192.168.1.51 -User administrator@vsphere.local -Password VMware1! - Get-VAMIAccess -#> - param( - [Parameter( - Mandatory=$true) - ] - [String]$name, - [Parameter( - Mandatory=$false) - ] - [boolean]$confirm=$false - ) - - if(!$confirm) { - $answer = Read-Host -Prompt "Do you want to delete user $name (Y or N)" - if($answer -eq "Y" -or $answer -eq "y") { - $userAPI = Get-CisService 'com.vmware.appliance.techpreview.localaccounts.user' - - try { - Write-Host "Deleting user $name ..." - $userAPI.delete($name) - } catch { - Write-Error $Error[0].exception.Message - } - } - } -} \ No newline at end of file diff --git a/Modules/VCHA.psm1 b/Modules/VCHA.psm1 deleted file mode 100644 index 160f0e7..0000000 --- a/Modules/VCHA.psm1 +++ /dev/null @@ -1,413 +0,0 @@ -Function Get-VCHAConfig { -<# - .NOTES - =========================================================================== - Created by: William Lam - Date: Nov 20, 2016 - Organization: VMware - Blog: www.virtuallyghetto.com - Twitter: @lamw - =========================================================================== - .SYNOPSIS - This function retrieves the VCHA Configuration which provides you with - the current state, mode as well as the IP Addresses of the Active, - Passive & Witness Node. This is only available on VCSA 6.5 (vSphere 6.5 or greater) - .DESCRIPTION - Function to return VCHA Configuration - .EXAMPLE - Get-VCHAConfig -#> - $vcHAClusterConfig = Get-View failoverClusterConfigurator - $vcHAConfig = $vcHAClusterConfig.getVchaConfig() - - $vcHAState = $vcHAConfig.State - switch($vcHAState) { - configured { - $activeIp = $vcHAConfig.FailoverNodeInfo1.ClusterIpSettings.Ip.IpAddress - $passiveIp = $vcHAConfig.FailoverNodeInfo2.ClusterIpSettings.Ip.IpAddress - $witnessIp = $vcHAConfig.WitnessNodeInfo.IpSettings.Ip.IpAddress - - $vcHAClusterManager = Get-View failoverClusterManager - $vcHAMode = $vcHAClusterManager.getClusterMode() - - Write-Host "" - Write-Host -NoNewline -ForegroundColor Green "VCHA State: " - Write-Host -ForegroundColor White "$vcHAState" - Write-Host -NoNewline -ForegroundColor Green " VCHA Mode: " - Write-Host -ForegroundColor White "$vcHAMode" - Write-Host -NoNewline -ForegroundColor Green " ActiveIP: " - Write-Host -ForegroundColor White "$activeIp" - Write-Host -NoNewline -ForegroundColor Green " PassiveIP: " - Write-Host -ForegroundColor White "$passiveIp" - Write-Host -NoNewline -ForegroundColor Green " WitnessIP: " - Write-Host -ForegroundColor White "$witnessIp`n" - ;break - } - invalid { Write-Host -ForegroundColor Red "VCHA State is in invalid state ...";break} - notConfigured { Write-Host "VCHA is not configured";break} - prepared { Write-Host "VCHA is being prepared, please try again in a little bit ...";break} - } -} - -Function Get-VCHAClusterHealth { -<# - .NOTES - =========================================================================== - Created by: William Lam - Date: Nov 20, 2016 - Organization: VMware - Blog: www.virtuallyghetto.com - Twitter: @lamw - =========================================================================== - .SYNOPSIS - This function retrieves the VCHA Cluster Health which provides more info - on each of the individual. This is only available on VCSA 6.5 (vSphere 6.5 or greater) - .DESCRIPTION - Function to return VCHA Cluster Health - .EXAMPLE - Get-VCHAClusterHealth -#> - $vcHAClusterConfig = Get-View failoverClusterConfigurator - $vcHAConfig = $vcHAClusterConfig.getVchaConfig() - $vcHAState = $vcHAConfig.State - - switch($vcHAState) { - invalid { Write-Host -ForegroundColor Red "VCHA State is in invalid state ...";break} - notConfigured { Write-Host "VCHA is not configured";break} - prepared { Write-Host "VCHA is being prepared ...";break} - configured { - $vcHAClusterManager = Get-View failoverClusterManager - $healthInfo = $vcHAClusterManager.GetVchaClusterHealth() - - $vcClusterState = $healthInfo.RuntimeInfo.ClusterState - $nodeState = $healthInfo.RuntimeInfo.NodeInfo - - Write-Host "" - Write-Host -NoNewline -ForegroundColor Green "VCHA Cluster State: " - Write-Host -ForegroundColor White "$vcClusterState" - Write-Host -NoNewline -ForegroundColor Green "VCHA Node Information: " - $nodeState | Select NodeIp, NodeRole, NodeState - ;break - } - } -} - -Function Set-VCHAClusterMode { -<# - .NOTES - =========================================================================== - Created by: William Lam - Date: Nov 20, 2016 - Organization: VMware - Blog: www.virtuallyghetto.com - Twitter: @lamw - =========================================================================== - .SYNOPSIS - This function allows you to set the mode of the VCHA Cluster whether - that is Enabled, Disabled or in Maintenance Mode. This is only available on VCSA 6.5 (vSphere 6.5 or greater) - .DESCRIPTION - Function to set VCHA Cluster Mode - .EXAMPLE - Set-VCHAClusterMode -Enabled $true - .EXAMPLE - Set-VCHAClusterMode -Disabled $true - .EXAMPLE - Set-VCHAClusterMode -Maintenance $true -#> - param( - [Switch]$Enabled, - [Switch]$Disabled, - [Switch]$Maintenance - ) - - $vcHAClusterManager = Get-View failoverClusterManager - - if($Enabled) { - Write-Host "Setting VCHA Cluster to Enabled ..." - $task = $vcHAClusterManager.setClusterMode_Task("enabled") - $task1 = Get-Task -Id ("Task-$($task.value)") - $task1 | Wait-Task - } elseIf($Maintenance) { - Write-Host "Setting VCHA Cluster to Maintenance ..." - $task = $vcHAClusterManager.setClusterMode_Task("maintenance") - $task1 = Get-Task -Id ("Task-$($task.value)") - $task1 | Wait-Task - } elseIf($Disabled) { - Write-Host "`nSetting VCHA Cluster to Disabled ...`n" - $task = $vcHAClusterManager.setClusterMode_Task("disabled") - $task1 = Get-Task -Id ("Task-$($task.value)") - $task1 | Wait-Task - } -} - -Function New-VCHABasicConfig { -<# - .NOTES - =========================================================================== - Created by: William Lam - Date: Nov 20, 2016 - Organization: VMware - Blog: www.virtuallyghetto.com - Twitter: @lamw - =========================================================================== - .SYNOPSIS - This function allows you create a new "Basic" VCHA Cluster, it does not - cover the "Advanced" use case. You will need to ensure that you have a - "Self Managed" vCenter Server before attempting this workflow. - This is only available on VCSA 6.5 (vSphere 6.5 or greater) - .DESCRIPTION - Function to create "Basic" VCHA Cluster - .PARAMETER VCSAVM - The name of the vCenter Server Appliance (VCSA) in which you wish to enable VCHA on (must be self-managed) - .PARAMETER HANetwork - The name of the Virtual Portgroup or Distributed Portgroup used for the HA Network - .PARAMETER ActiveHAIp - The IP Address for the Active VCSA node - .PARAMETER ActiveNetmask - The Netmask for the Active VCSA node - .PARAMETER PassiveHAIp - The IP Address for the Passive VCSA node - .PARAMETER PassiveNetmask - The Netmask for the Passive VCSA node - .PARAMETER WitnessHAIp - The IP Address for the Witness VCSA node - .PARAMETER WitnessNetmask - The Netmask for the Witness VCSA node - .PARAMETER PassiveDatastore - The name of the datastore to deploy the Passive node to - .PARAMETER WitnessDatastore - The name of the datastore to deploy the Witness node to - .PARAMETER VCUsername - The VCSA username (e.g. administrator@vghetto.local) - .PARAMETER VCPassword - The VCSA password - .EXAMPLE - New-VCHABasicConfig -VCSAVM "vcenter65-1" -HANetwork "DVPG-VCHA-Network" ` - -ActiveHAIp 192.168.1.70 ` - -ActiveNetmask 255.255.255.0 ` - -PassiveHAIp 192.168.1.71 ` - -PassiveNetmask 255.255.255.0 ` - -WitnessHAIp 192.168.1.72 ` - -WitnessNetmask 255.255.255.0 ` - -PassiveDatastore "vsanDatastore" ` - -WitnessDatastore "vsanDatastore" ` - -VCUsername "administrator@vghetto.local" ` - -VCPassword "VMware1!" -#> - param( - [Parameter( - Mandatory=$true, - ValueFromPipeline=$true, - ValueFromPipelineByPropertyName=$true) - ] - [String]$VCSAVM, - [String]$HANetwork, - [String]$ActiveHAIp, - [String]$ActiveNetmask, - [String]$PassiveHAIp, - [String]$PassiveNetmask, - [String]$PassiveDatastore, - [String]$WitnessHAIp, - [String]$WitnessNetmask, - [String]$WitnessDatastore, - # Crappy Implementation but need to research more into using PSH Credential - [String]$VCUsername, - [String]$VCPassword - ) - - $VCSAVMView = Get-View -ViewType VirtualMachine -Filter @{"name"=$VCSAVM} - if($VCSAVMView -eq $null) { - Write-Host -ForegroundColor Red "Error: Unable to find Virtual Machine $VCSAVM" - return - } - - $HANetworkView = Get-View -ViewType Network -Filter @{"name"=$HANetwork} - if($HANetworkView -eq $null) { - Write-Host -ForegroundColor Red "Error: Unable to find Network $HANetwork" - return - } - - $PassiveDatastoreView = Get-View -ViewType Datastore -Filter @{"name"=$PassiveDatastore} - if($PassiveDatastoreView -eq $null) { - Write-Host -ForegroundColor Red "Error: Unable to find Passive Datastore $PassiveDatastore" - return - } - - $WitnessDatastoreView = Get-View -ViewType Datastore -Filter @{"name"=$WitnessDatastore} - if($WitnessDatastoreView -eq $null) { - Write-Host -ForegroundColor Red "Error: Unable to find Witness Datastore $WitnessDatastore" - return - } - - $vcIP = $VCSAVMView.Guest.IpAddress - if($vcIP -eq $null) { - Write-Host -ForegroundColor Red "Error: Unable to automatically retrieve the IP Address of $VCSAVM which is needed to use this function" - return - } - - # Retrieve Source VC SSL Thumbprint - $vcurl = "https://$vcIP" -add-type @" - using System.Net; - using System.Security.Cryptography.X509Certificates; - - public class IDontCarePolicy : ICertificatePolicy { - public IDontCarePolicy() {} - public bool CheckValidationResult( - ServicePoint sPoint, X509Certificate cert, - WebRequest wRequest, int certProb) { - return true; - } - } -"@ - [System.Net.ServicePointManager]::CertificatePolicy = new-object IDontCarePolicy - # Need to do simple GET connection for this method to work - Invoke-RestMethod -Uri $VCURL -Method Get | Out-Null - - $endpoint_request = [System.Net.Webrequest]::Create("$vcurl") - # Get Thumbprint + add colons for a valid Thumbprint - $vcSSLThumbprint = ($endpoint_request.ServicePoint.Certificate.GetCertHashString()) -replace '(..(?!$))','$1:' - - $vcHAClusterConfig = Get-View failoverClusterConfigurator - $spec = New-Object VMware.Vim.VchaClusterDeploymentSpec - - $activeNetworkConfig = New-Object VMware.Vim.ClusterNetworkConfigSpec - $activeNetworkConfig.NetworkPortGroup = $HANetworkView.MoRef - $ipSettings = New-Object Vmware.Vim.CustomizationIPSettings - $ipSettings.SubnetMask = $ActiveNetmask - $activeIpSpec = New-Object VMware.Vim.CustomizationFixedIp - $activeIpSpec.IpAddress = $ActiveHAIp - $ipSettings.Ip = $activeIpSpec - $activeNetworkConfig.IpSettings = $ipSettings - $spec.ActiveVcNetworkConfig = $activeNetworkConfig - - $activeVCConfig = New-Object Vmware.Vim.SourceNodeSpec - $activeVCConfig.ActiveVc = $VCSAVMView.MoRef - $serviceLocator = New-Object Vmware.Vim.ServiceLocator - $credential = New-Object VMware.Vim.ServiceLocatorNamePassword - $credential.username = $VCUsername - $credential.password = $VCPassword - $serviceLocator.Credential = $credential - $serviceLocator.InstanceUuid = $global:DefaultVIServer.InstanceUuid - $serviceLocator.Url = $vcurl - $serviceLocator.SslThumbprint = $vcSSLThumbprint - $activeVCConfig.ManagementVc = $serviceLocator - $spec.ActiveVcSpec = $activeVCConfig - - $passiveSpec = New-Object VMware.Vim.PassiveNodeDeploymentSpec - $passiveSpec.Folder = (Get-View (Get-Folder vm)).MoRef - $passiveIpSettings = New-object Vmware.Vim.CustomizationIPSettings - $passiveIpSettings.SubnetMask = $passiveNetmask - $passiveIpSpec = New-Object VMware.Vim.CustomizationFixedIp - $passiveIpSpec.IpAddress = $passiveHAIp - $passiveIpSettings.Ip = $passiveIpSpec - $passiveSpec.IpSettings = $passiveIpSettings - $passiveSpec.NodeName = $VCSAVMView.Name + "-Passive" - $passiveSpec.datastore = $PassiveDatastoreView.MoRef - $spec.PassiveDeploymentSpec = $passiveSpec - - $witnessSpec = New-Object VMware.Vim.NodeDeploymentSpec - $witnessSpec.Folder = (Get-View (Get-Folder vm)).MoRef - $witnessSpec.NodeName = $VCSAVMView.Name + "-Witness" - $witnessIpSettings = New-object Vmware.Vim.CustomizationIPSettings - $witnessIpSettings.SubnetMask = $witnessNetmask - $witnessIpSpec = New-Object VMware.Vim.CustomizationFixedIp - $witnessIpSpec.IpAddress = $witnessHAIp - $witnessIpSettings.Ip = $witnessIpSpec - $witnessSpec.IpSettings = $witnessIpSettings - $witnessSpec.datastore = $WitnessDatastoreView.MoRef - $spec.WitnessDeploymentSpec = $witnessSpec - - Write-Host "`nDeploying VCHA Cluster ...`n" - $task = $vcHAClusterConfig.deployVcha_Task($spec) - $task1 = Get-Task -Id ("Task-$($task.value)") - $task1 | Wait-Task -Verbose -} - -Function Remove-VCHAConfig { -<# - .NOTES - =========================================================================== - Created by: William Lam - Date: Nov 20, 2016 - Organization: VMware - Blog: www.virtuallyghetto.com - Twitter: @lamw - =========================================================================== - .SYNOPSIS - This function allows you destroy a VCHA Cluster. In addition, you have - the option to specify whether you would like both the Passive & Witness - Virtual Machines be deleted after the VCHA Cluster has been destroyed. - This is only available on VCSA 6.5 (vSphere 6.5 or greater) - .DESCRIPTION - Function to destroy a VCHA Cluster Mode - .EXAMPLE - Remove-VCHAConfig - .EXAMPLE - Remove-VCHAConfig -Confirm:$false - .EXAMPLE - Remove-VCHAConfig -DeleteVM $true -Confirm:$false - .NOTES - Before you can destroy a VCHA Cluster, you must make sure it is first - disabled. Run the Set-VCHAClusterMode -Disabled $true to do so -#> - param( - [Boolean]$Confirm=$true, - [Switch]$DeleteVM=$false - ) - - $Verified = $false - if($Confirm -eq $true) { - Write-Host -ForegroundColor Yellow "`nDo you want to destroy VCHA Cluster?" - $answer = Read-Host -Prompt "Do you accept (Y or N)" - if($answer -eq "Y" -or $answer -eq "y") { - $Verified = $true - } - } else { - $Verified = $true - } - - if($Verified) { - $vcHAClusterManager = Get-View failoverClusterManager - $vcHAMode = $vcHAClusterManager.getClusterMode() - - if($vcHAMode -ne "disabled") { - Write-Host -ForegroundColor Yellow "To destroy VCHA Cluster, you must first set the VCHA Cluster Mode to `"Disabled`"" - Exit - } - - # Query BIOS UUID of the Passive/Witness to be able to delete - if($DeleteVM) { - $vcHAClusterConfig = Get-View failoverClusterConfigurator - $vcHAConfig = $vcHAClusterConfig.getVchaConfig() - $passiveBiosUUID = $vcHAConfig.FailoverNodeInfo2.biosUuid - $witnessBiosUUID = $vcHAConfig.WitnessNodeInfo.biosUuid - } - - $vcHAClusterConfig = Get-View failoverClusterConfigurator - - Write-Host "Destroying VCHA Cluster ..." - $task = $vcHAClusterConfig.destroyVcha_Task() - $task1 = Get-Task -Id ("Task-$($task.value)") - $task1 | Wait-Task - - # After VCHA Cluster has been destroyed, we can now delete the VMs we had queried earlier - if($DeleteVM) { - if($passiveBiosUUID -ne $null -and $witnessBiosUUID -ne $null) { - $searchIndex = Get-View searchIndex - - $passiveVM = $searchIndex.FindByUuid($null,$passiveBiosUUID,$true,$null) - $witnessVM = $searchIndex.FindByUuid($null,$witnessBiosUUID,$true,$null) - - if($passiveVM -ne $null -and $witnessVM -ne $null) { - Write-Host "Powering off & deleting Passive VM ..." - Stop-VM -VM (Get-View $passiveVM).Name -Confirm:$false | Out-Null - Remove-VM (Get-View $passiveVM).Name -DeletePermanently -Confirm:$false - Write-Host "Powering off & deleting Witness VM ..." - Stop-VM -VM (Get-View $witnessVM).Name -Confirm:$false | Out-Null - Remove-VM (Get-View $witnessVM).Name -DeletePermanently -Confirm:$false - } - } - } - } -} diff --git a/Modules/VMCPFunctions.psm1 b/Modules/VMCPFunctions.psm1 deleted file mode 100644 index 4f9b16e..0000000 --- a/Modules/VMCPFunctions.psm1 +++ /dev/null @@ -1,322 +0,0 @@ -function Get-VMCPSettings { -<# - .NOTES - =========================================================================== - Created on: 10/27/2015 9:25 PM - Created by: Brian Graf - Twitter: @vBrianGraf - VMware Blog: blogs.vmware.com/powercli - Personal Blog: www.vtagion.com - - Modified on: 10/11/2016 - Modified by: Erwan Quélin - Twitter: @erwanquelin - Github: https://github.com/equelin - =========================================================================== - .DESCRIPTION - This function will allow users to view the VMCP settings for their clusters - - .PARAMETER Cluster - Cluster Name or Object - - .PARAMETER Server - vCenter server object - - .EXAMPLE - Get-VMCPSettings - - This will show you the VMCP settings for all the clusters - - .EXAMPLE - Get-VMCPSettings -cluster LAB-CL - - This will show you the VMCP settings of your cluster - - .EXAMPLE - Get-VMCPSettings -cluster (Get-Cluster Lab-CL) - - This will show you the VMCP settings of your cluster - - .EXAMPLE - Get-Cluster | Get-VMCPSettings - - This will show you the VMCP settings for all the clusters -#> - [CmdletBinding()] - param - ( - [Parameter(Mandatory=$False, - ValueFromPipeline=$True, - ValueFromPipelineByPropertyName=$True, - HelpMessage='What is the Cluster Name?')] - $cluster = (Get-Cluster -Server $Server), - - [Parameter(Mandatory=$False)] - [VMware.VimAutomation.Types.VIServer[]]$Server = $global:DefaultVIServers - ) - - Process { - - Foreach ($Clus in $Cluster) { - - Write-Verbose "Processing Cluster $($Clus.Name)" - - # Determine input and convert to ClusterImpl object - Switch ($Clus.GetType().Name) - { - "string" {$CL = Get-Cluster $Clus -Server $Server -ErrorAction SilentlyContinue} - "ClusterImpl" {$CL = $Clus} - } - - If ($CL) { - # Work with the Cluster View - $ClusterMod = Get-View -Id "ClusterComputeResource-$($CL.ExtensionData.MoRef.Value)" -Server $Server - - # Create Hashtable with desired properties to return - $properties = [ordered]@{ - 'Cluster' = $ClusterMod.Name; - 'VMCP Status' = $clustermod.Configuration.DasConfig.VmComponentProtecting; - 'Protection For APD' = $clustermod.Configuration.DasConfig.DefaultVmSettings.VmComponentProtectionSettings.VmStorageProtectionForAPD; - 'APD Timeout Enabled' = $clustermod.Configuration.DasConfig.DefaultVmSettings.VmComponentProtectionSettings.EnableAPDTimeoutForHosts; - 'APD Timeout (Seconds)' = $clustermod.Configuration.DasConfig.DefaultVmSettings.VmComponentProtectionSettings.VmTerminateDelayForAPDSec; - 'Reaction on APD Cleared' = $clustermod.Configuration.DasConfig.DefaultVmSettings.VmComponentProtectionSettings.VmReactionOnAPDCleared; - 'Protection For PDL' = $clustermod.Configuration.DasConfig.DefaultVmSettings.VmComponentProtectionSettings.VmStorageProtectionForPDL - } - - # Create PSObject with the Hashtable - $object = New-Object -TypeName PSObject -Prop $properties - - # Show object - $object - } - } - } -} - -function Set-VMCPSettings { -<# - .NOTES - =========================================================================== - Created on: 10/27/2015 9:25 PM - Created by: Brian Graf - Twitter: @vBrianGraf - VMware Blog: blogs.vmware.com/powercli - Personal Blog: www.vtagion.com - - Modified on: 10/11/2016 - Modified by: Erwan Quélin - Twitter: @erwanquelin - Github: https://github.com/equelin - =========================================================================== - .DESCRIPTION - This function will allow users to enable/disable VMCP and also allow - them to configure the additional VMCP settings - For each parameter, users should use the 'Tab' button to auto-fill the - possible values. - - .PARAMETER Cluster - Cluster Name or Object - - .PARAMETER enableVMCP - Enable or disable VMCP - - .PARAMETER VmStorageProtectionForPDL - VM Storage Protection for PDL settings. Might be: - - disabled - - warning - - restartAggressive - - .PARAMETER VmStorageProtectionForAPD - VM Storage Protection for APD settings. Might be: - - disabled - - restartConservative - - restartAggressive - - warning - - .PARAMETER VmTerminateDelayForAPDSec - VM Terminate Delay for APD (seconds). - - .PARAMETER VmReactionOnAPDCleared - VM reaction on APD Cleared. Might be: - - reset - - none - - .PARAMETER Server - vCenter server object - - .EXAMPLE - Set-VMCPSettings -cluster LAB-CL -enableVMCP:$True -VmStorageProtectionForPDL ` - restartAggressive -VmStorageProtectionForAPD restartAggressive ` - -VmTerminateDelayForAPDSec 2000 -VmReactionOnAPDCleared reset - - This will enable VMCP and configure the Settings on cluster LAB-CL - - .EXAMPLE - Set-VMCPSettings -cluster LAB-CL -enableVMCP:$False -VmStorageProtectionForPDL ` - disabled -VmStorageProtectionForAPD disabled ` - -VmTerminateDelayForAPDSec 600 -VmReactionOnAPDCleared none - - This will disable VMCP and configure the Settings on cluster LAB-CL - - .EXAMPLE - Set-VMCPSettings -enableVMCP:$False -VmStorageProtectionForPDL ` - disabled -VmStorageProtectionForAPD disabled ` - -VmTerminateDelayForAPDSec 600 -VmReactionOnAPDCleared none - - This will disable VMCP and configure the Settings on all clusters available -#> - [CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact="High")] - param - ( - [Parameter(Mandatory=$true, - ValueFromPipeline=$True, - ValueFromPipelineByPropertyName=$True, - HelpMessage='What is the Cluster Name?')] - $cluster, - - [Parameter(Mandatory=$False, - ValueFromPipeline=$False, - HelpMessage='$True=Enabled $False=Disabled')] - [bool]$enableVMCP, - - [Parameter(Mandatory=$False, - ValueFromPipeline=$False, - HelpMessage='Actions that can be taken in response to a PDL event')] - [ValidateSet("disabled","warning","restartAggressive")] - [string]$VmStorageProtectionForPDL, - - [Parameter(Mandatory=$False, - ValueFromPipeline=$False, - HelpMessage='Options available for an APD response')] - [ValidateSet("disabled","restartConservative","restartAggressive","warning")] - [string]$VmStorageProtectionForAPD, - - [Parameter(Mandatory=$False, - ValueFromPipeline=$False, - HelpMessage='Value in seconds')] - [Int]$VmTerminateDelayForAPDSec, - - [Parameter(Mandatory=$False, - ValueFromPipeline=$False, - HelpMessage='This setting will instruct vSphere HA to take a certain action if an APD event is cleared')] - [ValidateSet("reset","none")] - [string]$VmReactionOnAPDCleared, - - [Parameter(Mandatory=$False)] - [VMware.VimAutomation.Types.VIServer[]]$Server = $global:DefaultVIServers - ) - - Process { - - Foreach ($Clus in $Cluster) { - - Write-Verbose "Processing Cluster $Clus" - - # Determine input and convert to ClusterImpl object - Switch ($Clus.GetType().Name) - { - "string" {$CL = Get-Cluster $Clus -Server $Server -ErrorAction SilentlyContinue} - "ClusterImpl" {$CL = $Clus} - default {Throw 'Please provide a cluster name or object'} - } - - If ($CL) { - - # Get the actual configuration of the Cluster - $ActualSettings = Get-VMCPSettings -Cluster $CL -Server $Server - - # Show actual settings in the verbose mode - Write-Verbose "[$($CL.Name)] Actual VMCP settings " - Write-Verbose $ActualSettings - - # Create the object we will configure - $settings = New-Object VMware.Vim.ClusterConfigSpecEx - $settings.dasConfig = New-Object VMware.Vim.ClusterDasConfigInfo - - # Based on $enableVMCP switch - if ($enableVMCP -eq $false) { - $settings.dasConfig.vmComponentProtecting = "disabled" - } - elseif ($enableVMCP -eq $true) { - $settings.dasConfig.vmComponentProtecting = "enabled" - } - - #Create the VMCP object to work with - $settings.dasConfig.defaultVmSettings = New-Object VMware.Vim.ClusterDasVmSettings - $settings.dasConfig.defaultVmSettings.vmComponentProtectionSettings = New-Object VMware.Vim.ClusterVmComponentProtectionSettings - - #Storage Protection For PDL - If ($PSBoundParameters.ContainsKey('VmStorageProtectionForPDL')) { - $settings.dasConfig.defaultVmSettings.vmComponentProtectionSettings.vmStorageProtectionForPDL = $VmStorageProtectionForPDL - } else { - $settings.dasConfig.defaultVmSettings.vmComponentProtectionSettings.vmStorageProtectionForPDL = $ActualSettings.'Protection For PDL' - } - - #Storage Protection for APD - If ($PSBoundParameters.ContainsKey('VmStorageProtectionForAPD')) { - $settings.dasConfig.defaultVmSettings.vmComponentProtectionSettings.vmStorageProtectionForAPD = $VmStorageProtectionForAPD - } else { - $settings.dasConfig.defaultVmSettings.vmComponentProtectionSettings.vmStorageProtectionForAPD = $ActualSettings.'Protection For APD' - } - - #Storage Protection for APD - If ($PSBoundParameters.ContainsKey('VmStorageProtectionForAPD')) { - switch ($VmStorageProtectionForAPD) { - "disabled" { - # If Disabled, there is no need to set enable Timeout Value - $settings.dasConfig.defaultVmSettings.vmComponentProtectionSettings.vmStorageProtectionForAPD = 'disabled' - $settings.dasConfig.defaultVmSettings.vmComponentProtectionSettings.enableAPDTimeoutForHosts = $false - } - - "restartConservative" { - $settings.dasConfig.defaultVmSettings.vmComponentProtectionSettings.vmStorageProtectionForAPD = 'restartConservative' - $settings.dasConfig.defaultVmSettings.vmComponentProtectionSettings.enableAPDTimeoutForHosts = $true - } - - "restartAggressive" { - $settings.dasConfig.defaultVmSettings.vmComponentProtectionSettings.vmStorageProtectionForAPD = 'restartAggressive' - $settings.dasConfig.defaultVmSettings.vmComponentProtectionSettings.enableAPDTimeoutForHosts = $true - } - - "warning" { - # If Warning, there is no need to enable the Timeout Value - $settings.dasConfig.defaultVmSettings.vmComponentProtectionSettings.vmStorageProtectionForAPD = 'warning' - $settings.dasConfig.defaultVmSettings.vmComponentProtectionSettings.enableAPDTimeoutForHosts = $false - } - } - } else { - $settings.dasConfig.defaultVmSettings.vmComponentProtectionSettings.vmStorageProtectionForAPD = $ActualSettings.'Protection For APD' - $settings.dasConfig.defaultVmSettings.vmComponentProtectionSettings.enableAPDTimeoutForHosts = $ActualSettings.'APD Timeout Enabled' - } - - #APD Timeout Enabled - If ($PSBoundParameters.ContainsKey('VmTerminateDelayForAPDSec')) { - $settings.dasConfig.defaultVmSettings.vmComponentProtectionSettings.vmTerminateDelayForAPDSec = $VmTerminateDelayForAPDSec - } else { - $settings.dasConfig.defaultVmSettings.vmComponentProtectionSettings.vmTerminateDelayForAPDSec = $ActualSettings.'APD Timeout (Seconds)' - } - - # Reaction On APD Cleared - If ($PSBoundParameters.ContainsKey('VmReactionOnAPDCleared')) { - $settings.dasConfig.defaultVmSettings.vmComponentProtectionSettings.vmReactionOnAPDCleared = "$VmReactionOnAPDCleared" - } else { - $settings.dasConfig.defaultVmSettings.vmComponentProtectionSettings.vmReactionOnAPDCleared = $ActualSettings.'Reaction on APD Cleared' - } - - # Execute API Call - If ($pscmdlet.ShouldProcess($CL.Name,"Modify VMCP configuration")) { - $modify = $true - $ClusterMod = Get-View -Id "ClusterComputeResource-$($CL.ExtensionData.MoRef.Value)" -Server $Server - $Task = $ClusterMod.ReconfigureComputeResource_Task($settings, $modify) - } - - # Wait for the reconfiguration task to finish to show the result - If ($Task) { - $TaskID = "Task-" + $($Task.Value) - Get-Task -Id $TaskID -Server $Server | Wait-Task | Out-Null - Get-VMCPSettings -Cluster $CL -Server $Server - } - } - } - } -} diff --git a/Modules/apply-hardening.psm1 b/Modules/apply-hardening.psm1 deleted file mode 100644 index 94b1279..0000000 --- a/Modules/apply-hardening.psm1 +++ /dev/null @@ -1,93 +0,0 @@ -function Apply-Hardening { -<# - .NOTES - =========================================================================== - Created by: Markus Kraus - Twitter: @VMarkus_K - Private Blog: mycloudrevolution.com - =========================================================================== - Changelog: - 2016.11 ver 2.0 Base Release - =========================================================================== - External Code Sources: - - =========================================================================== - Tested Against Environment: - vSphere Version: 5.5 U2 - PowerCLI Version: PowerCLI 6.3 R1, PowerCLI 6.5 R1 - PowerShell Version: 4.0, 5.0 - OS Version: Windows 8.1, Server 2012 R2 - Keyword: VM, Hardening, Security - =========================================================================== - - .DESCRIPTION - Applys a set of Hardening options to your VMs - - .Example - Get-VM TST* | Apply-Hardening - - .Example - $SampleVMs = Get-VM "TST*" - Apply-Hardening -VMs $SampleVMs - - .PARAMETER VMs - Specify the VMs - - -#Requires PS -Version 4.0 -#Requires -Modules VMware.VimAutomation.Core, @{ModuleName="VMware.VimAutomation.Core";ModuleVersion="6.3.0.0"} -#> - -[CmdletBinding()] -param( - [Parameter(Mandatory=$true, - ValueFromPipeline=$True, - Position=0)] - [VMware.VimAutomation.ViCore.Impl.V1.Inventory.InventoryItemImpl[]] - $VMs -) - -Process { -#region: Create Options - $ExtraOptions = @{ - "isolation.tools.diskShrink.disable"="true"; - "isolation.tools.diskWiper.disable"="true"; - "isolation.tools.copy.disable"="true"; - "isolation.tools.paste.disable"="true"; - "isolation.tools.dnd.disable"="true"; - "isolation.tools.setGUIOptions.enable"="false"; - "log.keepOld"="10"; - "log.rotateSize"="100000" - "RemoteDisplay.maxConnections"="2"; - "RemoteDisplay.vnc.enabled"="false"; - - } - if ($DebugPreference -eq "Inquire") { - Write-Output "VM Hardening Options:" - $ExtraOptions | Format-Table -AutoSize - } - - $VMConfigSpec = New-Object VMware.Vim.VirtualMachineConfigSpec - - Foreach ($Option in $ExtraOptions.GetEnumerator()) { - $OptionValue = New-Object VMware.Vim.optionvalue - $OptionValue.Key = $Option.Key - $OptionValue.Value = $Option.Value - $VMConfigSpec.extraconfig += $OptionValue - } -#endregion - -#region: Apply Options - ForEach ($VM in $VMs){ - $VMv = Get-VM $VM | Get-View - $state = $VMv.Summary.Runtime.PowerState - Write-Output "...Starting Reconfiguring VM: $VM " - $TaskConf = ($VMv).ReconfigVM_Task($VMConfigSpec) - if ($state -eq "poweredOn") { - Write-Output "...Migrating VM: $VM " - $TaskMig = $VMv.MigrateVM_Task($null, $_.Runtime.Host, 'highPriority', $null) - } - } - } -#endregion -} \ No newline at end of file diff --git a/Modules/vSphere_Hardening_Assess_VM_v1a.psm1 b/Modules/vSphere_Hardening_Assess_VM_v1a.psm1 deleted file mode 100644 index ad6227c..0000000 --- a/Modules/vSphere_Hardening_Assess_VM_v1a.psm1 +++ /dev/null @@ -1,372 +0,0 @@ -<# - .NOTES - =========================================================================== - Created on: 5/27/2015 3:24 PM - Created by: Brian Graf - Twitter: @vBrianGraf - Blog: http://www.vtagion.com - =========================================================================== -#> - -#Encoded Hardening Guide -$Global:Base64 = "UEsDBBQABgAIAAAAIQByzFKQpAEAANcGAAATAAgCW0NvbnRlbnRfVHlwZXNdLnhtbCCiBAIooAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACslctOwzAQRfdI/EPkLUpcWCCEmnbBYwlIlA8w9qSx6he2C+3fM3bpg6o0Qs0mTmLPvWec8WQ4XmhVfIIP0pqaXFYDUoDhVkgzrcnb5LG8IUWIzAimrIGaLCGQ8ej8bDhZOggFRptQkzZGd0tp4C1oFirrwOBMY71mER/9lDrGZ2wK9GowuKbcmggmljFpkNHwHho2V7F4WODrFcmXbkhxt1qXrGoidYpflGmGHozxoMJeEHNOSc4iZkc/jdgjK3+oKozMa0IrXbhA9D8c0sxvql2Dn7hn3E4vBRQvzMcnppGdLhT9sn72bu2sOi5ygNI2jeQgLJ9r3LUqOA9MhBYgalXlsdJMmjX3Ef+8ONA8XPYMkvLLwh0cEWsEaL6ejpBlOgyx2gzwVAKh55R3lDsYQlwq6Nt+Jdrl3DIP4jV6PNG9A+xqd3129o47QGMa+i69LNrh/zEHv5ysILb3fZNslbuKkil+1+Kp7bsk17rH/LGVvHjrAjZiD/8HWHfNFF06FAIfJWz65qH+s3HEfnlyxpB+EwLEAW+af0ujbwAAAP//AwBQSwMEFAAGAAgAAAAhAOT5JVMGAQAA3AIAAAsACAJfcmVscy8ucmVscyCiBAIooAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACs0t1qwyAUB/D7wd5Bzn1j2o0xRpPejEHvxsge4FRPEkn0iNo1ffvJYB+BrgzWS/X496fH9Wayo3ijEA27CpZFCYKcYm1cV8Fr87S4BxETOo0jO6rgSBE29fXV+oVGTHlT7I2PIqe4WEGfkn+QMqqeLMaCPbm80nKwmPIwdNKjGrAjuSrLOxl+ZkA9yxRbXUHY6hsQzdHnk/+TLS0l1JhQKg608CHLQjL5LqLB0FGqQLN6ztPxo6LIapCnQavLglK/tzuHZjxB+VorDrb9zbP8u4fb1ih6ZLW35NKJHsh5xTdpGuWBw7BjHs69ze0lLTQlcpr0+Xah958iOfuT9TsAAAD//wMAUEsDBBQABgAIAAAAIQBQ+Z9uEwEAAMgDAAAaAAgBeGwvX3JlbHMvd29ya2Jvb2sueG1sLnJlbHMgogQBKKAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACsk8tqwzAQRfeF/oOYfS07bUMpkbNoKWTbph8g5LFlYo+MRn347ysMiW0I7sYbwdxBdw7z2O1/20Z8o+fakYIsSUEgGVfUVCn4PL7dPYHgoKnQjSNU0CPDPr+92b1jo0P8xLbuWEQXYgU2hO5ZSjYWW82J65BipnS+1SGGvpKdNiddodyk6Vb6qQfkM09xKBT4Q3EP4th3sfL/3q4sa4Ovzny1SOFKCWkcEZoBO9pqX2FQMBGTSAvyOshmTZAQG4QjwhDK4c2WGLI1GX6cP7FFDCPHRWI5ZBZhtqtORjfmxeqaJnM5S0sdeVwTgq32WHwEH7efR5CZvATzsCpM6Jt4bJc15SE+l5ez+8v/AAAA//8DAFBLAwQUAAYACAAAACEAH5G1vY0CAAAtBQAADwAAAHhsL3dvcmtib29rLnhtbKyU32/TMBDH35H4H4zV1zY/1q5dlGRa18ImIZi2sb1UQm58baw6drCdtRPif+ecrNCxlyF4ic928rm7790lPd1VkjyAsUKrjEaDkBJQheZCrTP65fZ9f0KJdUxxJrWCjD6Cpaf52zfpVpvNUusNQYCyGS2dq5MgsEUJFbMDXYPCm5U2FXO4NevA1gYYtyWAq2QQh+FxUDGhaEdIzGsYerUSBcx00VSgXAcxIJnD8G0parunVcVrcBUzm6buF7qqEbEUUrjHFkpJVSSXa6UNW0pMexeN9mQ0X6ArURht9coNEBV0Qb7INwqDKOpSztOVkHDXyU5YXX9ilfciKZHMujkXDnhGj3Grt/DswDT1tBESb6PhMA5pkP8qxZUhHFaske4Wi7DH44ujoziO/ZuY1Jl0YBRzcK6VQw2f1P9XvVr2eamxOuQavjXCADaFly1P8cmKhC3tFXMlaYzM6GLxUG2ZgX6prVvclGhy8l5LjpIsPiuYGfEA08YKBdYuLpjhoLAlyYdGcLCL40G4OKgHe1nsv6gIK7w0AWrTxd/Zf+qUp77b7wRs7W/F/Zbs7oXieptRnJ3HA3vbHt8L7sqMxtFkjPfd2QWIdekyOhmjPt73AbqdD3TRrkS1fXHjZybCQfTrpS89JSYRaJhLHrWE/WfYAKgZ9/2EkIPdE2q+a8svZ8yxr4iRumCy5XsuRlgKjlp7F3nn9l3vrBclvfveOEqDAyCG/dwZggpsQb+0MY7iUdQGBzv30bo8xRWrLzL6PRqGZ+PwZNgP50ej/nByEvcnw6O4fz6cxfPReD6bT0c//u/AYRMm+3+WjxJbzt0aVmywra5hNWUWB7DTEuPE5PZRB/uv8p8AAAD//wMAUEsDBBQABgAIAAAAIQCpTAfKqgMAAJQUAAANAAAAeGwvc3R5bGVzLnhtbOxYbW/bNhD+PmD/gdB3RS+xPNuQ1NVxBBTIumHxgH2lJcrmwheBpFO7w/777iTZVtpsbZMMyNp8kcgj+fDu9NxRvPTVTgpyy4zlWmVedBZ6hKlSV1ytM++3ZeFPPGIdVRUVWrHM2zPrvcq//y61bi/Y9YYxRwBC2czbONfMgsCWGyapPdMNUzBSayOpg65ZB7YxjFYWF0kRxGE4DiTlyusQZrL8HBBJzc228UstG+r4igvu9i2WR2Q5e7NW2tCVAFV30YiWB+y28xG85KXRVtfuDOACXde8ZB9rOQ2mASDlaa2Vs6TUW+UyLwZo3GF2o/Q7VeAQOLCflaf2PbmlAiSRF+RpqYU2xIFnQLFWoqhk3YwLKvjKcJxWU8nFvhPHKGid2c+THExDYYB6dNo8cJ8eoX1ZQOJCDOzqBHkK/nXMqAJGSd9e7hswQAEVOkVg6JOz14buozgZLAjaDfN0pU0F1Dt4FJ3XifJUsNqBqYavN/h2uoHnSjsH3ylPK07XWlGBzjis6BtgTsmEuEZ6/l7fwd7VRG1lId2bKvOA6OjGQxMM6ZsdXtdB/CFahz2AHYPKXw5LdvUR/xGrCW0asX8t+FpJhpREBwKZui55Z2izZLtWjobs6n/WNQJv3O+Co67dbshz3Oixe+OGn3T/B3u/3coVM0WbUPoouuO90fQJQO8xDJz39PscqAVkGjD2Dl+PzCOYLDLvLVouIO307CGrLReOq3u4CpjV7sT+OAZmgGBIj402/D18AkxSa6aYQWg4CRwvUdTFmkccMOhX7SDV4gEBUTOkFeGqapkH8j+21vF6f0Wtu+J4VIDMbgxXN0tdcCAN9DH9w8nyM4Y9CtAJrVrtowvOQYyet0GK58eFrgDxxw8WnLj+/Ix50e1hJHjx29fnt88L7Kf88pA4njQ1vbDyW2XlF59Pz5nG//9AfEkl/+Uf2HNOc98Kd7/uhNPe+05/9IMrANoN1xaHxZv29n68C8FhXrGaboVbHgcz79T+iVV8K6Ee08/6hd9q10Jk3ql9haWEaIxXCLjSXFmooMCbbA3PvD8v5z9MF5dF7E/C+cQfnbPEnybzhZ+MLuaLRTEN4/Dir0Et6RGVpLbiBTfKaDSzAupNpje2V/76JMu8QadTv70AgdpD3afxOHydRKFfnIeRPxrTiT8Znyd+kUTxYjyaXyZFMtA9eZjuURhEUVeuQ+WTmeOSCfgruKv+ciiFjwTdfzEiOHyJ4FROzP8GAAD//wMAUEsDBBQABgAIAAAAIQConPUAvAAAACUBAAAjAAAAeGwvd29ya3NoZWV0cy9fcmVscy9zaGVldDEueG1sLnJlbHOEj8EKwjAQRO+C/xD2btJ6EJGmvYjQq+gHrOm2DbZJyEbRvzfgRUHwNOwO+2anah7zJO4U2XqnoZQFCHLGd9YNGs6nw2oLghO6DifvSMOTGJp6uaiONGHKRzzawCJTHGsYUwo7pdiMNCNLH8hlp/dxxpTHOKiA5ooDqXVRbFT8ZED9xRRtpyG2XQni9Aw5+T/b9701tPfmNpNLPyJUwstEGYhxoKRByveG31LK/CyoulJf5eoXAAAA//8DAFBLAwQUAAYACAAAACEAi4JuWJMGAACOGgAAEwAAAHhsL3RoZW1lL3RoZW1lMS54bWzsWc+LGzcUvhf6Pwxzd/xrZmwv8QZ7bGfb7CYh66TkqLVlj7KakRnJuzEhUJJjoVCall4KvfVQ2gYS6CX9a7ZNaVPIv9AnzdgjreVumm4gLVnDMqP59PTpvTffkzQXL92NqXOEU05Y0narFyqug5MRG5Nk2nZvDgelputwgZIxoizBbXeBuXtp+/33LqItEeEYO9A/4Vuo7UZCzLbKZT6CZsQvsBlO4NmEpTEScJtOy+MUHYPdmJZrlUpQjhFJXCdBMZi9NpmQEXaG0qS7vTTep3CbCC4bRjTdl6ax0UNhx4dVieALHtLUOUK07cI4Y3Y8xHeF61DEBTxouxX155a3L5bRVt6Jig19tX4D9Zf3yzuMD2tqzHR6sBrU83wv6KzsKwAV67h+ox/0g5U9BUCjEcw046Lb9Lutbs/PsRoou7TY7jV69aqB1+zX1zh3fPkz8AqU2ffW8INBCF408AqU4X2LTxq10DPwCpThgzV8o9LpeQ0Dr0ARJcnhGrriB/VwOdsVZMLojhXe8r1Bo5YbL1CQDavskkNMWCI25VqM7rB0AAAJpEiQxBGLGZ6gEWRxiCg5SImzS6YRJN4MJYxDc6VWGVTq8F/+PHWlPIK2MNJ6S17AhK81ST4OH6VkJtruh2DV1SAvn33/8tkT5+WzxycPnp48+Onk4cOTBz9mtoyOOyiZ6h1ffPvZn19/7Pzx5JsXj76w47mO//WHT375+XM7ECZbeOH5l49/e/r4+Vef/v7dIwu8k6IDHT4kMebOVXzs3GAxzE15wWSOD9J/1mMYIWL0QBHYtpjui8gAXl0gasN1sem8WykIjA14eX7H4LofpXNBLCNfiWIDuMcY7bLU6oArcizNw8N5MrUPns513A2EjmxjhygxQtufz0BZic1kGGGD5nWKEoGmOMHCkc/YIcaW2d0mxPDrHhmljLOJcG4Tp4uI1SVDcmAkUtFph8QQl4WNIITa8M3eLafLqG3WPXxkIuGFQNRCfoip4cbLaC5QbDM5RDHVHb6LRGQjub9IRzquzwVEeoopc/pjzLmtz7UU5qsF/QqIiz3se3QRm8hUkEObzV3EmI7sscMwQvHMypkkkY79gB9CiiLnOhM2+B4z3xB5D3FAycZw3yLYCPfZQnATdFWnVCSIfDJPLbG8jJn5Pi7oBGGlMiD7hprHJDlT2k+Juv9O1LOqdFrUOymxvlo7p6R8E+4/KOA9NE+uY3hn1gvYO/1+p9/u/16/N73L56/ahVCDhherdbV2jzcu3SeE0n2xoHiXq9U7h/I0HkCj2laoveVqKzeL4DLfKBi4aYpUHydl4iMiov0IzWCJX1Ub0SnPTU+5M2McVv6qWW2J8Snbav8wj/fYONuxVqtyd5qJB0eiaK/4q3bYbYgMHTSKXdjKvNrXTtVueUlA9v0nJLTBTBJ1C4nGshGi8Hck1MzOhUXLwqIpzS9DtYziyhVAbRUVWD85sOpqu76XnQTApgpRPJZxyg4FltGVwTnXSG9yJtUzABYTywwoIt2SXDdOT84uS7VXiLRBQks3k4SWhhEa4zw79aOT84x1qwipQU+6Yvk2FDQazTcRaykip7SBJrpS0MQ5brtB3YfTsRGatd0J7PzhMp5B7nC57kV0CsdnI5FmL/zrKMss5aKHeJQ5XIlOpgYxETh1KInbrpz+KhtoojREcavWQBDeWnItkJW3jRwE3QwynkzwSOhh11qkp7NbUPhMK6xPVffXB8uebA7h3o/Gx84Bnac3EKSY36hKB44JhwOgaubNMYETzZWQFfl3qjDlsqsfKaocytoRnUUoryi6mGdwJaIrOupu5QPtLp8zOHTdhQdTWWD/ddU9u1RLz2miWdRMQ1Vk1bSL6Zsr8hqroogarDLpVtsGXmhda6l1kKjWKnFG1X2FgqBRKwYzqEnG6zIsNTtvNamd44JA80SwwW+rGmH1xOtWfuh3OmtlgViuK1Xiq08f+tcJdnAHxKMH58BzKrgKJXx7SBEs+rKT5Ew24BW5K/I1Ilw585S03XsVv+OFNT8sVZp+v+TVvUqp6XfqpY7v16t9v1rpdWv3obCIKK762WeXAZxH0UX+8UW1r32AiZdHbhdGLC4z9YGlrIirDzDV2uYPMA4B0bkX1AateqsblFr1zqDk9brNUisMuqVeEDZ6g17oN1uD+65zpMBepx56Qb9ZCqphWPKCiqTfbJUaXq3W8RqdZt/r3M+XMTDzTD5yX4B7Fa/tvwAAAP//AwBQSwMEFAAGAAgAAAAhACNPrliBDQAACE4AABYAAABkb2NQcm9wcy90aHVtYm5haWwud21m7JzbjxPXHcdnZr2+rtfr9WZZSBoBjRi/rK3Sf6C0tFVeqki0Uqo8NBQ2yVa50AC5VJXSRk1VVWqUvvTBTmT3pS+pVKS+9sUmj5X6AgEEJEDCtVyW3Ei4bb/f35mxZ73j+ZmcJKoUzPzWXx/P+cyZi893zjlzcJ2c44ylfMcpOyXPwSuFGHOzzjjeC5JCNeG95b3hjkHdL2v47vJy3Kf7sMYNb8rJEOX8S/K7nrdx24u79yw85Ty/b8sOp3iy/PxvNt73vIs1dn40fX8h2Oo8EritXG+rrre8vCzb81xP3mtYhyVMuUUni/frsq5sTNLnXVe2fRPpzGvWng9y90vN7Rh6f4ss82xqv/c6GO7AXn7x63NrLOWbb77ZK+VYsP9Mi+4dP3Ov592UrDHjzTjrRFW8/d663ro8onyZdcedCejv5DY4S0H6PzzXYc5w29G107HHLeOYs2z+rjxCpoQ8MuZYm61me5yrV6/2ypILypR1JpH2wBhzVRAzONIM6kpvHUPKyOclN9cjLi1xT8y3q4mkkOZJUOvEy5cvJxA9EEgbk6DWiZcuXUogjoFAWkqCWidevHgxgZgSwoyTBjEtWideuHAhgUgKaRkJap14/vz5BGIGBNKyEtQ68dy5cwnELAik5SSodeKZM2cSiDkQSMtLUOvE06dPJxDzIJBWkKDWie+//34CsQACaRMS1DrxvffeSyBOgEBaUYJaJ546dSqBWASBtEkJap148uTJBOIkCKSVJKh14okTJxKIJRBIm5Kg1onvvvtuAnEKBNLKEtQ68fjx4wnEMgikTUtQ68Rjx44lEKdBIK0iQa0Tjx49mkCsgECa+Wc+hS4wrA4/cuRIAtGUbsaZBXM2KKlGPHz4cAKRFNLWSFDre33o0KEE4hoQSJuToNaJBw8eTCDOgUDaWgnq4cTQ2/nO+5no+7ybDtwxybfv1Kdxc+hU4aYMat/ap32heSB6oJGqnWPNp32QqnBTBrVO1HzaF1oKxBRopGpl1HyahCrclEGtEzWf9oWWATEDmqHyLjHbuwpY5ugdlObTPkhVuCmDWi+j5tO+0HIg5kAjVTuOmk/7IFXhpgxqnaj5tC+0AogF0EjVyqj5tA9SFW7KoNaJmk/7QiuCWASNVK2Mmk/7IFXhpgxqnaj5tC+0Eogl0EjVyqj5tA9SFW7KoNaJmk/7QiuDWAaNVK2Mmk/7IFXhpgxqnaj5tC+0CogV0EjVyqj5tA9SFe7CoNaJmk/7QpsFcRY0Um192gepCjdlUOtEzad9oc2BOAcaqVoZNZ/2QarCpxnUw4lfvU+3UNe34aYM6pa1T7eE5oHogUaqrU+3QGrDTRnUOlHz6ZbQUiCmQCPV1qdJaMNNGdQ6UfPpltAyIGZAM1Q7n26B1IabMqj1Mmo+3RJaDsQcaKTa+nQLpDbclEGtEzWfbgmtAGIBNFJtfboFUhtuyqDWiZpPt4RWBLEIGqm2Pt0CqQ03ZVDrRM2nW0IrgVgCjVRbn26B1IabMqh1oubTLaGVQSyDRqqtT7dAasNNGdQ6UfPpltAqIFZAI9XWp1sgteGmDGqdqPl0S2izIM6CRqqtT7dAasNNGdQ6UfPpltDmQJwDjVRbn26B1IZHM6iHE796n+6g17+LnngGdcfT9lbr9+4IzQPRA41UW5/ugNT1xiSodaLm0x2hpUBMgTY2AlFrT7NMXS8tQa2XUfPpjtAyIGZAS49A1NrTHZC6XlaCWi+j5tMdoeVAzIGWHYGotac7IHW9vAS1XkbNpztCK4BYAC0/AlFrT3dA6noTEtR6GTWf7gitCGIRtIkRiFp7ugNS15uUoNbLqPl0R2glEEugTY5A1NrTHZC6GJ9lUOtl1Hy6I7QyiGXQSLX16Q5IXW9aglonaj7dEVoFxApo0yMQtfZ0B6QuRmEZ1HoZNZ/uCG0WxFnQSLX16Q5IXW+NBLVO1Hy6I7Q5EOdAWzMCUWtPd0DqemslqIeXUfNp8+TBhxzyx+u1cCh8/baFx59ZWP+TB0366r+GmwtG3rdu3Sr3Hh6eAcCTEc7m/CzaGYR9C60sk/JqkLK5l7IPdRzX+TZSDC8cpY6O6POb8DmGuJH8Ged1jO+zXbh65N51XnY5cm++/zy9/FeuXAHb5M/37q/C0fi1+G4t9mGdBHW47mBfcr43Gh/tk48juqB4IDKozTMVq3un+8ToPUQc0QNlDDQGtXkmJIkYvYeII46BkpLjSirp8b+6fhmj9xBxxBQo4/J0Bs8W6Rox6vhxxHFQ0iAyqMdV4tmzZxPPdRqUjDwHQirpWhmjjh9XxgwoWRAZ1OHvYPjVE3X8OGIWlBxoDOqQNJwYdfw4Yg6UPGgM6v4TJGbvTZmX3P65jvpzHDEPSgE0BnV/nWHEqD/HEQugTIDGoC6oZ+add95JPNcToBRBY1BPqMRof3dcGYugTILGoC6qxKibxhEnQSmBxqCeVIlRN40jlkCZAo1BXVKJ0VHkOOIUKGXQGNRTKjHqpnHEMijToDGoyyrx7bffTjzX06BUQGNQT6vEAwcOJBIroNB3GNTDxqXD/TN10xczLp3sWDWUuwZPqUtQ2zoWaTV4Sl2C2taxSKvBU+oS1LaORVoNZ7UuQW3rWKTV4Cl1CWpbxyKtBk+pS1DbOhZpNXhKXYLa1rFIq8FT6hLUto5FWg2eUpegtnUs0mrwlLoEta1jkVaDp9QlqG0di7QaPKUuQW3rWKTV4Cl1CWpbxyKtBk+pS1DbOhZpNXhKXYLa1rFIq8FT6hLUto5FWg2eUpegtnUs0mrwlLoEta1jkVaDp9QlqG0di7Qa3KouQf3/41gNNBEbaFc2JahtHYu0Bp5tb0pQ2zoWaQ0859+UoLZ1LNIaaNc2JahtHYu0BtrITQlqW8cirYFn55oS1LaORVrDzYDIoLZ1LNIabhY0BrWtY5HWwNP1TQlqW8cirYH2U1OC2taxSGu4BRAZ1LaORVrDnQCNQW3rWKQ1MB+lKUFt61ikNdxJEBnUto5FWsMtgcagtnUs0hruFGgMalvHIq3hlkFjUNs6FmkNdxo0BrWtY5HWcCugMahtHYu0Bvr2mhLUto4V9mWyr3MckfNW92q6zgbpsdyCu1TTq+k6pyTlu5E+zHCeUHQmVT5gRmdmDc4zYkuRc6iGpYe9I9G5RUxLw79MWhqfNqU3ob1kHC0V1HZcg2tmgjqf73vxeUvhB85juK8rcqflPSXPdFKbMPnk69g/3B8z1jF8nWHfbJTXei94d76sz/2jgmMlR8id4gS4aN/4fORIRY9i3JwvU/NxLl2UTFoKtSKvnU1p3DbKeXxgLIWR+XFEGff4ZdHTsoY3tQ7vLjwzfIWd7/xsaNEt9O9ReH3EccP9id9fL1LakGW8kFdJNA9nqG1Kl5D24x899FBaete//Cvo85195vryr6DwbPCdvTT9q8hFTRA9Wvxlxc0wGF679GcfmLM+fLYl66P+XMJ7nHt7YwqPNA/2rueJoKZhGq8t16MarFPucf4jIyPh9X38v9d6BOOaS/BjUw/yO0OiGiS9CFJVekmdFbMm/4T0LmZZmhGmq8EIk5nbik2t/972Jxd//uxi7+ofFOZqDsvAesaUoV+DuqirzFjTN1DjOc6DW1PSQ23SNktJf7h3cefCk4tPL6x/cGte+hay8jeFv1npBYqemZVlfa03GjZaWSd7I12bxBM49rVRSviqsxltNMf5/raHF2vbd+168sX5Xdv37HhiYXcG6/D+MSXvVBnsJ2eDjuP3xzKmA+o+z1A5fsZeu835fd5mHPeQunfn4p75hRd2LOzas/jM0/N7dy88G0MfD+iGzft1boNbZAq3Gede4T3K4BVqzlF4T1SMnCFz5fCc9q/Y8BPn70Z7UM39yhLqU5OL35lzTcVtLOFXFZ3ZG6ZzPDCs9QavS1z50v9qvtfWY8+gmSmczGN7LDqj+K9oM7OsFa/rhYzV45Ab8HQR1xzMy7aYyRtyh+Vl28XMfG52+/MxwyPGtJW/jsEjxjVWbr0/F/ot/EqH7fW+YO7zYN57g72ecd5QRl/vHdhuFfWW2euqkpdrrtxuu5e3reTlmivzdr1wu13MyR+2vxwx5prme9P65JVpPg+7/vvH3lzBPNora+qQx/8tYEthvfNv3AuUpcrj+yz+f4AqPjGFc7pdPHvH1zHe1g283vjLn8+ePD76gvUdxJ0uzj///rc7XZjn2gdXRl+wvuT5cOnayEuQ59OPro6+hHk+/uDTkZcgz2cffzj6EuS5/snHq5dHfvpwbHqY59on11cuz+15dvHxhR2P/mwgHR+DPDc+uxZdXnn5pZd+/asXntv9y2ee+sUTjw18G+a5/umNcPnDKy+b5Xe/7efsfQsR5Ll54/roS5Dn1o0boy9hnps3b428BHlu37o1+hLmuX0HryDP8vLt0ZcgD97uaFlR6/C+cFht8Ufccd2tLUapM+7WFndrC1YqX+fa4tDd2mLE24u7tcXd2uLrUVt8vpYMmyJxPaJhO5Tfx7XXw/T+87tMMa1AJ/H53delD2ZlS66fdz9aU8NacvvRcu1v17Tk2Lf2TSRuKfzeuSx97VyD/3cZ33lPRWHCtMBIj3uZhtn/AAAA//8DAFBLAwQUAAYACAAAACEA4JqCgIskAAB7AAEAGAAAAHhsL3dvcmtzaGVldHMvc2hlZXQxLnhtbKSdWXPbSpaE3ydi/oNCTzMR05a4Sw7bHU1apNhXC8XlMvpRV5avFW1bDkl3+/cDEEUSlfmRKKhfvBCJU+ecqsQpVCWAd3//89vXg9/vn54fHr+/P2y8OT48uP9+9/jp4fuv7w8X8+HfTg4Pnl9uv3+6/fr4/f794V/3z4d///Df//Xuj8enfz9/ub9/OcgsfH9+f/jl5eXH26Oj57sv999un988/rj/nh35/Pj07fYl++/Tr0fPP57ubz+tTvr29ah5fNw9+nb78P2wsPD2KcXG4+fPD3f3Hx/vfvt2//2lMPJ0//X2JfP/+cvDj+e1tW93Kea+3T79+7cff7t7/PYjM/HLw9eHl79WRg8Pvt29Hf/6/fHp9pevWdx/Ntq3d2vbq/+Y+W8Pd0+Pz4+fX95k5o4KRz3m06PTo8zSh3efHrII8rQfPN1/fn/4j8bbZa9xePTh3SpBPz/c//Fc+vfBy+0vs/uv93cv95+yfjo8yPP/y+Pjv3PgOPvpOD/1yM4drvI/eTr4dP/59revL9PHP87vH3798pIZ6WTx5GG9/fTXx/vnuyyfmZk3zU5u6e7xa9Z89ufBt4d8YGT5uP2zaPjh08uX94etw4Nf7p9fhg+5pcODu9+eXx6/LYtjqzA25zbDudnffxTHO9k/E0/Omlk1nP0dTm5krSWe3A4nZ4GGk08abxrt424WY6qNbrCR/R1stHtv2s1O76RRw0ovWMn+frUnGRNXqcj+XnvSeHPS6bS7J730eE6DlezvYKXZfdNrHJ+2ahhpZJeJYjxk/1j3S6e+mc2w+k96qLHuovwfwZneSX1n1n3U3MZUf7g01yHl/1gnuFd30DU3nNmO+2Rfjgruri4FH29fbj+8e3r84yC7umYePf+4za/VjbfNzDCTP2N9Dv5Hhs6o/ZxdqX7/cPzu6Pfs6nIXjvXLxxrxsUH5WDM+9rF8rBUfOysfa8fHhuVjnfjYqHysGx87j/yUIMaRo+LpP6MzJcSfygd7cZMX5WMn8bHL8rHT+NhV1KB4cx0dlMRNooOSuZvooKRuGh2U3M2igxLlPDooYS6igxLnz1HWpUuW0cFt1o+yAbwZxRk1olGcl7LTjLL7R3N+Vl608tEs3dkvjmXFYjPem5LjQQHJrlJbiGT6I1iRfJ8VkChIyfoQIJL7ETQkkPNqyDiCvDxlifm8Cu7L7dP9p8NiTjJuvh23sgvF80M+v/gwHv7PqPn+vPl/h//KZoH/d3j1ePi/744+5ynNfojH8z89Yy2lEcQqQ+kCIDKgLh3SkmF15eloKcPAivIMIMo2gCjnAKLMA4jyrxqygA6Q7P7sVron4swSWtr2QETMfKCUy0tOzPbx6Zud08x1pclPXHNT+qVfHCtzsy29Oygge7kJVmQwnhWQMjfb4ssQIDJGRtCQjJFzgEjvjiOIc7OgYzZJ//3D1aPQzpPRll7/CcKQQXrhkKbSDqzI0Llah5HfTuRTiuv1OesfJvrDjf4w1R9m0K64NtdzFpAU6bif3Ww2q45zu3RMZzsWIypk49Wo0DiuJEJ+2poI4mC/OFYmQkdnXgVkLxHAiozyswJSJkJHfBk6pK1EgIaUCA5pKxEiSD0ieDI6Yv0niFSJ4BAjAlhRIkAyhJTX1SmdVENuwBchx9QhXbmgzgAiQ20OEBlHC++ArpIOrJxIYpaA2Q6kiHTZwH8N6fLT1qSTIdovjmUubKZ9XRkhgwKyl3RgRUbIWQEpk64ro3UIEMnVyBvqKOkc0pO+HUeQeqTzZBjpIAwZohcOMdI5pKeTPohUJ31gRSd9ANFJH0B00gcQnfQBRCd9ANFbL+8AIx2NI+mBJbS0Y9KXLbk46VrZ+ftvx/LT1qSTZPWLY2XSneiUr4Bkrey+HQMrOuUrINENu075HGKVDhqSQXQOEOn+cQSpRzpPhpEOIpVRfOEQIx1Ykb67gkhlFF+DFRnFE4DICL1xyKkMkilAZATMACIjYA4Q6d6Fd4CRzq101d8ltLSj0mUrlUa6buUSSH7WmnMy/PrFsTLnTrXQFZC9nAMrWugKSLQQpoUOIFrooCEZIOcOaRxLx40jzF7S+fqGp8NutDyQxrEM0gvCyCi9JIwM0ysPVy9U12RGrgITwshAuHGMNjWthsyoJRksc8LIaFl4Pxj53Ez3VHK8pKa2oy6aZ+Y7orrO0TippF9+2pp+EkS/OFamX7YhI+vqBWYv/8BMQwbcWYGJlmgbkoyhY6zqeVM21XSILi6NI0i9qufpaDRkNP9EsQpxLhxjdY/MyKXkilIvxLmuTuukGnJD3ghxpoSRMTcjjFxn54SRcbnwjjACupnuqfTDEppq7lhdybYuX3Ojl5+2JqAE2i+ORQRs6vJKgdlLQDKj6ysFJiKgbjcMCSODe+RtGQPJHV1iiTD1KOgJaTR1sZEi0UUWxxgFyYyuslC0MlivwY7uFkwII2PhhjDS0VPCyGVjRhjp6DlhJIUL74qWLva7me6puLN0TGvHfV+uADASNvNfK3aV8/PWLJTe6a+MZpsGpfu6Rku6eRBAZR42WjKqP6IlychZAEVULMW7WrMeEki3IEbUXFvGzDmArCDGmHp0hMTYjSBGoyURQTJULhEkQ/cK86JlES1Jr08QJL1+swGtNxim9ssMDWndQ5AWPsi3Vb6AyTbRtiuIp5KlJbW2a2chVz+9ZsFldd5mA9z0HLlVIZ5tLwQTewtgwEQM7mgFDKCId7bHgCCtgdCcFUF0SatgDKrJuyJ50QXJ6iCGo4UQQFYJ0ZCWQoi5Z5qSwu1yL/R09RMbE7LcAKhnShNvrNERQs2oNd2cmCNILrULGKpOTXepq4seS2xtm8novrCRb5XrjWGzV10Siy32oE1RccrKqlBTYxkEUDQEu0KWj2hJJSoBFHGzqyIVAnlNdNVFw2uig7wmRpia3HQVhNdElzg0dPflAvMiTLhEkIzyK+qGttVE8KltNZFAVhPXoG1N1F9m5Lfup8wRZMTzfOvSwc9gqHuqshNsbRfx8o35V6zINIoN/UA8uT72w9GolOkFchBA+2viWgexnQM0ekLOs2Ao4p1eRYcAsoUZcNtLInkk43QcG6pJO1dcOO1cTtHoyTC4wLTIEL9EkHDzirpT95WuydKJjPEJgmT03CR01TQBM8PGpEzPEwwtYKR6SQQVzKmqYNCjHRsVjXz33piZC6wr7hKLXf/ATJXCrKxKSTzR7YoA2s/MopmI4lp+zoKhiJkqDRoiSHct0G/dtyCQLpaNY1BNbroww7npqouG7lddQMw+WyVDQqgrCNlnq2tDG+3Ypvn1Lzf2y9R+mdkvc/tlAUPHqeKBdU9Vu0KjorTmEs8e8z13pUr1pl7+EM9W2Kz6lXA0GuB6+zkIoP1MKZqJDdnc0WUDDS3sw9BaRCe9xozQb8nuOYCax3IdHsegmkwp4iknxplCMaugBWLOnn7ThwPcUvPYqOL94FRZG9pSRX+52Xi0nRUqZmaYuf2ygLHjVPHAuqeqOMFhsb3wx1TJd8qtqlTLK/Png7ZcUdlJOFoe4k3dMR4E0H6uuAiieWzzPdBbHItLw9BamSvNY5kijdBvKdrnBNKtwXEMqskVF0H4ZgDGI7S+IJCMlks0ZDda3g/OFegG3Q6dUGsNm/C5JdsWB0OKmW0wa2bO7ZcFjEJnnfvT02vOEhzaueCf75CbvD8jQsVMrthYDzM5lZ00XLbQbMh0bxBA+zlHhoxzvsnfbBjnCGSco+aMcwmgcZyBmhoUyIyJUAImuoY0jHMQtHGOEmOc85idc2tD2/qkv9xsvN7WJ8XMDDO3XxaQIWeKB9bTwruENDZ2TuXyvWylSv50bQVVii3wQBWViDRcYdAsbZCvNqsGAbSfKmRIpZHBUDRqmiqORJA+CIB+CzPPEWRr9JHjNcuTqxR8KucShGbTlugBZEwhQ7ZE791gii1IsGImCZibBMw0ATPDDre9M4re9s68P2zDGlrr6fx+CaCdFSzf3X7FHVaxKR5oqcKRhqsOmqooGATQflqSIaMlSCX0UcZhaC3ibstoCWqJE1uLIJ9kII/jDNSkpesWnJYUs6qXKWajJRkSfl9Bf3oBI0NytZpgL9jWWYKlKVqyvTOwpFsyc7KkWzILGKxOTW+tp/dKS2htFzXzFUGjZsJD3WUxSUNS0l8ZjZcJm/pM5yCA9lITDQmhzgIoYl1bhukQQTIER9Bcw6hJPqlUZhyD6lETEmPUxHB0CZ9ASk00JIy6gpCNmmhIWDdBkLh0k2JpSqCScmM1KZshSK7scwTJhGsBfWLUBEM9XX1YYms7VlvyF5m8RnGyOm/zWLcqTsLRaLVF96kGAbSfnMU2fmxIVyaDoYicHd3VRpAMwhH6rSuTKaBxDKp55weZsTs/jEeXJjeg9Y3WJZ2mKoyrjfObZ7vN0MR+ubFfpvbLDJs3trh2o9k1thSg8uixez9orafLcEsANUpXnvjtJCQCaXQrb/7yN/5sX4KgIpBwNBrkJgIJoIgtHRmaH9GSLpQEUEQXffB1CCDbjMbWhHfnBFL1wTgG1SxlCSIQDFnIf4EgFYEgSNdJMGbp9WuypBVvgiCZnNwgSHp9mmJphiC53M4RJN2+CKDycPVi5gqXni6AL6G1nfNMVIoksLOsFNF1kH7+Li1RTzZ7MnYGAVQOt2ESLbKkoLMAitjZk1E4BJCzk/zWe0AMTl+lFYNqsjNBK0Ihq3jjAkHCqUsESeW4ophPhFPX2A1iaYIgY6fLLpoqEJqmWJphdPo8AVoydnqvODvdb2Cng3ayE9UiCewsq0X0WZp+00UeTVOLBFDETt3O/0iWnJ0uDGiqpmQYLJUp7Owkv3WPgVzy2hlZqsnO4txyYnyiSSHrFgOE3NS72ksCqe7kCjvU2Ak+ee0kkLGTQFY7EyzNMDqb2pIlY6f3irPTDQE7HbSTnfk2vu26V7/qIX9L6nZmqwqVcDRzY/sqB5U7DQJo/31g0UxsyBZpClBUOlUPMwytxSBbpKHmbGbrIHvFUZyAmuQs7EdXLXvyAMOxmS0lxma2BLKZ7Trm7Z2hn2a7GOCkYm4SMNMEzAwwLd0ZnyNI1c4wLp2GHn3vWDWV2NoOtXOT1C/VQrHVaZvlGBW/hKNl9rRM/BJA+2nooouWiV+CoTLDWiZ+QZBuxKPfViTdJ6dhhKlJwxTtC4ZjRdJFGy19EcAlWVItzxUlRoUt12jJprDVwpYbNCQX3ymATP2ChmwCC1lSHccCBqs9iQCt9Y5VxIkubcdhvPpDIpkEbpZFMk0VyTRdU9Fq2O1lAdrPTTDk3HQ1REv1IsPgUkRg3X8Zgd9NlYOeA8i5Gfldk5ueF5+/Qsg6DC4gZFM7Y1rsZbTQCyrkuCZL+kKCCYC8aEJsuoAxTTA0Q4/0QQQECX0XAVQeqs5M0uQYMym2bWWLmUmanARmljU5TdXkNF3D0VL6DgJoPzPBkDPThRUtdWkYWouYqVPCEfptCz8gUFFJTmyoJjNdAuKKUQrHqZmgySFDXjSpP4W/12TJ3iEBIKcm9Ke+I3uaYGiGHkkZnyNI+LuAserUXLu9nuAv0fSOJ4KarxPhrE7bzF5VhBOORrPXlj4QFED7eeiCF5i9uvihpVvvw9BaxEN9S8WI/NZ7g3MAeYWM/K7JQ9fgAA8hZuchaFBspx8MOQ+hG3zySt1gW/0Och6SIfF7Cv3pk1cwZBocGhimwYGx6jwkDY6VSIptBzVbJMJJeLHZ6rwNN1WFE45G3DQVTgDt5SYa0gWeAIpoZyocBOkCDzVn3ASQcTPG1OMm5MW5SeEYNzFmudO4JJCRkxJj81dsTiVyADJyoiEZ5dMEQzM0JIN1TiDV8yygU4ycYKinN/JLbG27xhR/XAFVONVvHWwVIohCvarTlH44GpFTX9cyCKD95HQVTkuTchYMReS0974gSG4hRuS3k9N9cnJGmJrkLM7dv/pK4Tg5C0vxVcvICSAnJ/SDVU5MsVz/JgBycpLfRk4HWeVEjyQBcwTJNWUBg9XJ6R5lH0DTjz1AbCU1TUxOEv1U31y2ypofnfn3w9GYm3LBGgRQNAZNVUCWVNF1FkDRGOzKgvcQQXKnMaLmnJz+4hcn53/w4hdKjG2NUDhOTtegtFR7dUmWnJwec8vJSc3J9W8CzTk5yZCwfJpgaIadbuSk1oycCW+HgdZ6mqQlurS98MTkRM1P9dPCqw9fbb7Gom+HCUcjdnaNnSCi0N35j2RJv8lwFkARO3U7e4ggffADm9PnsRCkD37EoJq10zMDE1uQjjg9AaQvzrmkzDg9XQ4F9KTmbGLrIKcnGbLaWW1ohr1uE1toTQVUi2Bp78IstNbTZ6GX5FLpNToxPXMFQv2nslqFcCHMa2WQ98PRiJ0nQuFBAFXUzqKd2JLddYKK4kSf/QjNRRRWHdKIHPfa6T557YwwTs7VN4fHrfbbca6vyr8o1ig+8Ndqvz9vteETf/6psaKJivkvpMY57CD/3hgYcgpDb3mFpd6Si9sEesspTIbkWjBNMDTDoSHXgjmChOcLGNM+/XW3e/rQ/ZJaa+zY9cw1QK+hcKGMCBRWYdDKaPz0VkulYIMAqqBw0U4FhQtQxE79SMAwNBeDbPoLzTmFHeQUjjBcX1df2qbPABbnVnATYnZuOsi5CYacm5AX5yZ1g1xuJ9ANzk0ypKq9BEMz7HMVvCNIRXswWJ2b7jZwk9K9i5uvUwu1ColF4KaqhcLRiFH6DqdBAFVwE+RCqrQ+C5YquAmiEFXwjshx52aCXCg2VJObKXIhitm56TE7N0kso5oEyotzkzIsBWgCfjs3QVGkArxpgqEZYfTNHXME6bNiMFidm+42cJPSvYubr1MLtcpqId147IejZW62TckXQBXcBKGKcxM0GF43HdRWecMIHG85N90nr5sJcqGdddPlQnBfmqIXCuGUL1rOTTDkdRO6wblJGbYlXQCp2h3cbusTV1MCiaEZGpLJ8RxBMvFdwGB1boJeyOe0lO5d3CS9UMInfFtlwZBu+PfD0ZictmoEwhhf0wWpipPTFSZtHV/D4FN5oLZ1fI3AcSBngmIoNlSzcKYohiCclhfOBMUQGXJyQjc4OakbVGZL3WDkJEMqs00wNMM+V5ktguSasgigciVxcrrbUDhBDbXzhvN1IqJWIYYIk1oVEYWjETdLDhQv2AqgisIJ8hXnJigzvHCCfEWlvyNwHLjpPnnhjDA1uVmcW3HDCTE7NylmfRQlxBzdEqilK0qMk5Oas1ltgowIXGqrLmJKIKuc0Jhqf+fYms1qvVOcnOvWNnI+yu0uJrZJM1S987k6bbO3opKhcDRioj7WOQig/UwESy1jYgDtv70EULupsgRqzqawFJ3qamNMPSZSYmznk2I2JlLM+ijAJVmyMkmJMSZiinUOSyAtk2hIVmOmCYZmaEjnsAjSOSx0ijFxY2jDRMztjglrO9cr1N9HWZ22ZqKKFfvhaMxEna8GUAUTQZjiTATNhdXE0Fw0X1Vh0wgc95pI0RkTI79rMrE4d39NhHB8vkoxOxMhe85E6Adnoltqqz5lgv0Qi2huECNVappgaIYYffoEQfr0CYxWZ+I6/i0TKbe7mPg6NVC7rAZSRXA/HI2YaBL3AKpgIqhQnIkg53AmOqit61EjcByYmKAGig3VZKILT3xZJzSwd055AaC2MxGy50xMUQNRc7qqMEGQMpH6SuUGCYZmiFG5AYG0uCxgtDoT125vmUi53cVEkv4kzE4LsURxn6ha9X7bZSJtpesggCqYCIITZyK9P0V1eaG5qCaq4yNwHJgI0VlNjDA1mZgi/IFwoCaCNsaZCNlzJqYIfzDFujNJIJudgt/6iMI0wdAMPdKdSQTpziSMVmfi2u0tEym3u5iYaw5eMTstpAqBiaryabvao62LGoMAqmAi6EaciSD38JrooLY+cDwCx4GJEJ0xMcLUZGJxbsXsFGL2+0SI2ZkIlpyJ0A8+O6UU64pNSHF8l6A1EQzp0x7TBEMzwujnc+cI0hUbGK3OxLXbWyZSbncxMRcPvIKJheYgMFHFOu3iaObG5i0+bX2yYxBAFUx0S7BiU4AqVmwc1NY3zI7AcWAiRGdMjDA1mVicW8FEiNmZCDE7E8GSMxH6wZlIKbYVGwBZTSRDtmJTbWgW+jOivb62eo4gW7HxTnEmrj3aMpFyu4uJr5PmtMvSHP1wbD8cjZloKzagQLEdRrAETCRNiM1OQe+hz5+MqDlfO02Q5sSGajIxRZoTGqi4T0yQ5pAhJ6KH7Kp0sNTWh3cmBDIiUlfZgg2AdBMDPbIFG2rNFmy8T5yIa0NbItbQ4bRfp8NZnbZZOpWrVT8cjYio71AeBFB05XcipuhwgqWKkggCEP249wgch5KYoMOJDdUkYooOh2L2kggxe0lMEeJQYrwkUor1+RBw3ERyhOnagk21omeGhmzBBgzpk0oLGK3OxLWhLRNrqG7apLpJWLApi270DrC/Mhorydv6gNUggCqYmCK6CZYqmAhKDn2eawSOAxMTRDexoZpMTBHdUMzORFKv6MY+WfKamKK6AUtt/X77hEBWE6mvVEqeYGiGHtmCDbVmCzbeKc7EtaEtE0Fis70QRE9ltV+nsFmdtimJqrAJR6OSaO9JD6AKIqYobIKlCiKC2EJfpj4Cx4GICQqb2FBNIqYobChmJ2KKwoYsORGhH7wkUoptvSZBYQMutfUhximBbHIKjZWeT1xJvebYmq3XJChsNoa2RPT2W7sUNp3XKWxWp22YqAqbcDRioq53DgJoPxPBkt8lBtB+JgKorQ9Rjqg5u0uk6HS9JsbUYyIlxhQ2FLMxkWK2ySlZMiZSYoyJmGJdryGQlkQ0pOs1CYZmaEgVNgjS9RroFCuJG0MbJmJud6zXdHINQP2V09Vpaybq8nI/HI2ZqOs1AVTBRFB22B5GsFTBRJB/6KOXI3DcayJFZ0yM/K7JxOLcKDHORA/HdxMhMb6vT9lzJkI/OBMpxbJuNiGfjIlkSBdsEgzNCFP6wHFRExGkCzYwWp2Ja7e3TIRe2lkTSWHTqP4UQacssdENmn44GlHxVN8iGUAVVAQ5i775+CxYKlOxrQ9RDhEkl74ROA5UTJDYxIZqUjFFYgPhABVBqnIql8RLsuRUTJHYgKWOPo04oX4wKrrfHX0acZpgaIYe6ftcESQuLWC0OhXd7V5DOL3EdO+qk6S6qf6Ya6esutGX0PXD0TI5O/oI4SCAyuRs2ft20JLQ/CyAyuTs6GNwQwRJeRtBc0DOBNVNbKgmOVNUNxAOkNNFHx2d1l6SJSdniuoGU6z3jgCy5VQypC5NEwzN0JB+xAdBeu8Io9XJaaobNL3lfbSK08l1ATpjTWBiISco9vp1G7+/Mhovp3b0ejEIoDIT9WmJj2jIiOjSho4+lTUMliK22mNS2JyU0nMA2WNSMaYmEYtwKiasIOfwW0cH2fPFkJeW87AwVL6y+gYjZth46C45D6E/7SkpaE0Nzcgje0oKQcZD7xPnobvd00+OLDHdu4pkLhR4xc1koS8I1FQZTqc4GhVJe3AqgPZTkwxJa2fBUMS6pvB3iCBZKRiB3y0dzOcAcmpGftekZnFuOS/2pRCIBkpkYaicF2emY4CZ3gvATLfU0a9CTMBvZyYZ0g3HBEMz7HLdcCSQPsWzgKHqzFy7vb2ThEBKT6/EFZI0OAnvPO+URTgdfT9OOBrxsKWvnwug/Tx08UenJZP8s2Ao4mFL3z6HIPF7BH7DXDVBgxMbqsnDFA0OhANEdA1IR59PuSRLXiNTRDiYYt36B5Azkfw2JiaIcNAjYyK0Zs9qwGB1Jrqhnk7VluRS6emJmJyky0khZ6E4CEVShTkdl6507IMEAbSfnGDIyenqh46TE0Ca3hH4DeRM0OXEhmqSM0WXExqIFpl9/kqJkb66JEtOTo8ZyiSlWF+rDM05OcmQvlY5wdAMMJ22JGCOIH2tMgxWJ6e73dNbpCW2tr3wxOQkqU7KGmxZq6PS3H7HlR2dtm2HuPoBlnnAku6+nIXmotKp68JDBOlbH8FxXbM8B4zPYCO3a5ITVCFSNn6CYKBwulCko+sAl2TJuZmi1MEEGzdBF2MrsOS3cbPa0Aw9Mm5Sa8bNBKUOtNbTXeAlurSLm6TeaVR/SL1T6BJC4VT5TjgazWo7xk0XRPjCj6tGOvY9gtBaRE37HgGCjJrUnAydcwjOuRkZqslNz0unK4PlJwxHiHCBIBnklwiSAXy1iXnzqWY8TcbBBEBeJV3h0tHX408TDM3II13SnyNIP9YcQOUpnX2sGQz19LumS2xtO+iiKplrV/2j6bnq4M9G+/bu7ae/Pt4/391/zx6bOn6TfQX6w7vVRus/VudtRAMq3wlHIyaqtHEQQHunsGhI13kCKGKivmF/iCBd58HmhATnADImxph6TIS82DoPRiM8uECQ7lMCyBd6IGSfwVJzqn6bAMi4iYaELdMEQzM0pPuUG9D6IrOgDpDL8c9guqfLxEvK7Q49azdXF+hya0JFXJ235qHWqH44GvHQZHQBtJ+HrhnpqPbtLBiKeKgfEBgiyHjozTX0sZNzCK6pqyfjGFSTiIUX5cTo/cBPGI5cMS4AZAuuaEgYfQUhqyj/Gg3pTgiBVEF1gyDjoctiOqfCsdnG0ppjc/tlAcPQbhLBo57uqSzR7R0bkF0U6lR/e2d13oZ2kpN+OBrR7lSq1iCA9tPO9SEdVfycBUMR7VTMMwSQLeCA2yfCzPMEzDjG7OXcv+6f42fe/wlp8eoHQhZVJl1AxE46MmSko04QQl0n5HeSgLlJwEwTMLMNZss4fVPNglJtdQ7EN/p59iU41C0plOIJJ4lvGqcZDSomnGX1TVcuMP2uazW6Km0aBNB+xpEhae0sGCozrquvMB8CyBlHrcmF4hxjk0E6jkF7OWffyYG8eJ1zWU33WG7qLjAvumSKIKmYVxizcQ580gWeCTWnatcbBKlIFUH65AaCpEfnG9B2yun6Jy9+Hm5PN8+X2P62n2Iq5poBm3NWf7K1W2gNilUY/dJgPxwtF7+uvaY4gPZT0XUf3WOjouseurpsPAytlfnqVKTWjIoJoHGcgLrlD7QeukQK4XR1Oe4CQXbzB9nTu5gr6lCddF0npHiSgLlJwEwTMDMKX+vWfAPa8hDSbyXRk9bTJcQltr9rEprrAV5z71foCAIPVWuz+mxaLIPrqpuDANrPw6KZiNAqozkLhqKS2JRL/xBAzkNoTXW05xSbbuAV343bJKBmSSy82H/rV2DikO3Wj0BC6EtMnnD1imJWQco1WfKSCD7p21Nv0JKVRLJkN38AUj3mfNPcloreBV4S3XRPh+aSIiltg8clEeU21Q9udMtyG32LRj8cjRike+yDANpPRZd5dJ2KLnPo6ptYh6G1/SURWnMqJoDGcQKcisXHHrvdt+PTfBs4+9hjs/jYY7f7/rzbhY89wo2jy3L8xpFSI1epC0hNV19peokgu3OE3LRsFlutk5kk9NZNAmaagJlRZCq3mW9AW7pC+q1yggBHi8QS29+usMR0RQFOwu5FWYCjL5jpd12v0TUBTgDtpysZUnVcMBSVEX3d5hBAXjmhNadrAmgcJ4ArZ/Mw6/rfPwAPXYHjPHSxR1ffFHaBibG7SbJkd5MQtJdOsOSlk5oT2t+Q4/oJ4ilGJ9eGGVqyu8m1T1suQhcYF0Fvo7LcJbVf0qjEXES9TcLCTllvozvO/a7rM7oqgBkE0H4ugiGl0FkwFHFRn/QaAsi5CK05FxNA4zgBdbmYILjBmGW0XCBIBuIlgmSN6Ao7VFh9nZDiSQLmJgEzTcDMMDJ9C8AGtOVhgrYGTPe8JrqSp9vZoa3pkram+pmq1WmbHQ2V1oSj0QzWpDUBtJ+GrnXpmrQmGIpoaNIaADkNU5Q1EJvv5ycoa0JF9OVVV9b4u/0xZLmTugCQb2m4sKWra3RXEHJH1YDXCQmeJGBuEjDTBMwMU2TFkMKXa9ACBqrfWrqhnr65YUku7bq17JHSppqXq9M2vFShTTga8VLns4MA2stLNCRXuLMAinkpl4ohgIyX0JrtNCZgxjGmZnGEtNhEFSOWTrggkErALhEkG8dXELPzMiHBkwTMTQJmmoCZbTCbrUb7ZUG51hkptNXTy9sSQKU76mhC2kNJTfXe/uq8NeO0H/vhaMQ41WAMAmg/41zj0lWN3FkwFDGup49OIUgfnQK/tcqdJ2DGMaYu5VxR45RzQUm3p2s0GLJcri4RpGs0ELNJatCQSmoIpPK3GwSppAZBuqqKIKHUHEEyfBYwWE13CoZ6ege9xNZ07/Ho+cv9/cvH25fbD+9+3P56f3n79OvD9+eDr/efVxrT7Lb06eHXL+t/vzz+yJWnvYxKvzy+vDx+W//vy/3tp/un/H/Ztujnx8eX9X+yq9DL7S9f7ye3Ty/PB3ePv+Xa1UYmXN38evD09uHT+8On8afVV7aPtvAP747+eHz698rFD/8vAAAAAP//AwBQSwMEFAAGAAgAAAAhAJ9uqiOnaQAAK9MBABQAAAB4bC9zaGFyZWRTdHJpbmdzLnhtbOx963IbR5Luf0XoHWphxZqcIEBL3vHs0Via5VCSzbOixBEkzW44HD5NoEn2CEBjugFSHI8i/A57/mzE7sv5Sc73ZWZVX6oBgrrY8h7vOsYm0KiuS1Zev8z88g+vpxN3nhZlls/u9W4PPuu5dDbKx9ns9F7vxfNH/X/uuXKRzMbJJJ+l93qXadn7w/2bN74sy4XDb2flvd7ZYjG/u7tbjs7SaVIO8nk6wzcneTFNFvizON0t50WajMuzNF1MJ7t3Pvvsi91pks16bpQvZwu895/ufNFzy1n212W6rx/97vPbvftfltn9Lxf3Dx58ubu4/+Uu/9JPvlpm43SSzVIXf/csK1+5oyI/ySZp+3cP0nJUZPMFVtv+6uVyMkuL5DibZItL9yArR8uSu9J+bj+fnWSnyyLhIO4oKZJpukiLjjdlRTp2L5PJMprGXlmmZTlNZwu3LLHV7s/psdufZPigPc6T9BRvOk/do+VsxFcmE3cwnSej6Mln6TQdZzqtq0Z9kJ4ky8lixezkPe755Tya+PlwfpYWqds7OmhP9OHw3zI3PEsnE7efT6cgGVctc4OHa9NvP32+//hgg0Ebj60Z7ii/SIvNhoweXTPs3vEkdYvclak/1a9zXJIVlPgsPcFGzkbRFu+fJbPT1D3OT9vbcFC6cVoKVZ2TqtziLMUncpJ/6NrhQTKfTy7782SBq1m2n7i9c2fn8/aH/5qmcydHWV6Wi3Tq5gXuczG5dDrKuP2DP16SP1ySiJdzLn+cLFKHmyGD2Kt33HntdmVp6bKZzP4MNFacZ2VeuFEyc8epm2aLDBSfjgdub+bS8XLEP1yyWCSjV6k+lr6eT/Js4V7N8otZNPTFWTrj8+kU9xzTwpz4a7wxGY1w7Rxelk7Sc85zXmTnYBOnmBGmjBnIrM9wboPoIu61P3lU5FNZhb8VF7jFI7nFIIJJOlroGkkFvA74avTK9YZL3I7isjdwD1/P+XmvwVJ68ixYcnZy6XqYz67M6aXyaP22dzBNQCNGWuTQBVZaDtxzTD31f7qLfDkZuwWv5GW+lMmMlgWobuEyGcAYv8tPwkwxq1m5xBVfnCU6/+ajWSmPTrB7WBZkgjsFd8LRXbqXhxdJkcYbh4ci6pziBC9dQvKSM8EhvQqkx/0v6+R0kS3OjACFdHCQeTFOcHucfJXNxkvswWVfRVUxLmUTsxkYMxnmqRcX2CKdpXsxF0I9TGbYycJhWTj9ZLnIIbRAb4s8n+gWkC5PIb4WuAMJJAJWLe9cnGXVaMmkzN18eTzJIORKtzcWmiahg6zKFLtOoRJuA089PwEDwFvdRXIpbGN5TNmEK4C9gCDNJi6ZpAU2ApKUez4d3LxBUVtC1l5cXAzOp7Ldo3y6Wy7n87xY7M5zkBjeuutf+V2RlvN8VkYHoMtvH4tJciykrA9/XgrX73/x2e4in2ejXbzTf39RluNXYDRgSyeDcT7aPc+mA7lAR2Q7tr+D4SJZLMvBGeR/+6WfuLR8jbvhyvxkwSWRKCi83Sk46a6Lvz/Pjvnd5gMt9bBXjaVfrxzuHyeL34/y2ey7XBSH8h9PF7+/crLrf3TVCuqvi18VL6f7eb5l1dq6r0El76bjSQraM668LOVOOMgRsDCSvjvBlQCfiNjk0/YnEJn5OX6evl4UiSuhuqRyO8H5s3zstgbb7oScFLLN62YRDyELHCTLcbbop69HqZxDH5MqIqm2x4eURfGqgrPJY+7iDGIAtIX7SLH50I/iXnAU95gP81pCeuB7uXDuLIHelYyn2QzfQuHDPaTAEHkRCWcIIlOOvhh8JkORRxY7wnq5jcl4bHPBBe+aA5jHQreC3/rRKtWQ7D0Fe9cFjXM3yxduQoaP5zPMLS2mmeisJZdhEpbSBxyfi5zko1djisxpPk4HWPkymYCrUTZMwYQuEggGSktMFJtynvGgwGmhopeuXI7OwP/Ar44hiCHqwTb18VVLAatVIRYkiSwwOpH0NSU1CESoAOuh0K6vBcSGLYdeQNYsO4t5zNJ0jN/gOc4fEn12nuXFjDo1V8bl8shtl4r0r0twKFBfOsogEOrDl2ciJrmZYL7t2dR3LKL13UgnGGaLtC9vOclG11AYdrzGQNZZ0xZArj3loT3XF8bTG6YL6jVl9YFoarXvTdx49WDghqMihxIgZ4/ThGjpPfa0cAhagC6y6qx6rZvSo6isjiO6qWuUop99jTSxqIE9BItQRQq0OzO9jDvdXit0UNwFXHsYmzDy7OaB+gLZlThvZ9RF2itj9Wc5iUSeMqmns8lli0RoUt8VFnmvB5uZtzDt3f9kmEBfSmEtj0U7UC6cJriRuNIgZ3KEtgJ788YnySm0XpATb6jcPejA5/vCDD4tQenGSPXnfADrpSIj45cYQKyZ1GWqHtbedUZO0M0YoaV8gn+onJ7DSJeXc34z2MnK96FFXUB9I28mW1QOw+nLRQ4aeMmRGsPIEPiVPGcLcUPuUXHzxi1oEeE991yvyPNFTz+e+zfiY5V7t/+BX9kQOoLDl+ejMhlMkuOBLBWPYAamDoNTYv9nM2j1MMKxenCTMWyjOSyVmzfoLtAv+y8PbLz+o7wAB+3LIRbuN64ven4xvXvrBPpiKqO7fzfmS35G5gvd5+pta2yAjX+cTvKLmzf8NKCAysa45ip1QzLVsO9Ri+qfT/mXzObmDeiaQldb3E55jKZa+KPcvnnj+5s3/gx1Nu2LeduT/83Ku65XPeafEGbWvlXK523veQzTS3nunvsKs3l5KANWr/+7fpylWNytIMHsYXzq7PcDtaK8wslR9sTes0/w68cPVHTec2GgwZ+WaXHpmWG4/+UWFqqrlLn5n+qZ4U2kA1HUeQkDyXqzdjSBOUIlIgdvgG2WQo8IlxAEXN0kEnl9NwNb5kxlT3XSN284/F9tRjQahRjFwm3tPp+172v0aHRZba1oUHJq4eL0j/xd4WP+4siyH+c5DPyzIl+enjVW09alxNZVRZHeEM8/Lk2f8hpFPiOHoS5Gz5FRK38r7AbGp+cMtNypPtX1MPsOu1dRrO6U0KudNA6RWwGKjfePxKakb2OBFPnwLR64fSQMp35L/JSc6z8sirzYU1fZENbKDAYi9hyiGZ4ZZwSjQ17IddHLsN/aGGHd6Wsw39ImgOVvtScxeALuaaupkUvnY/pOWzf/VVGO3li5ZgdHQVu098ris1L2Gcyw/ZTrH8HJMMrmkDe2uis24e/uz+Is/P7Wd4NnOeTXjz/8R/pX/O9/7fEsf/zhv9+4arJ12pbvHX8jhG2TkkGqH8g+1b9yfYzek9/2tt339X3ypCFWfoshqTmvrwzqLrWBip+9qd7q/7OT4/sbdjrJj5PJXfOytiVCLAi49foKDI9/4F9vOry79YJnKQlUxIZoFG3b6OaNZ2n/osihovCe1Zzf5EzH0CPBo2C3m6sK/oybN9QxMHbBEgQ/MF/1zRvqm4Q3Tsl+Ae8dR6rUGQwQuXpoukEqwWneny3m7a+97yt1T54fOej4UNIvZyOwmVn2N3Fmt38Bj2NKF5X49uikomhWbyWcRmoNuZLaRpHCBKPvXIeFmQB5vAUinsBIJN/EpuAZuAJhyQhPtlfSOQM37vaOKCzBfukczyxkOKTkaz0O8is4jdJxfwn3i71fwijQe7a8LbWfQw3KZuJwejHDRIsSl+s59uDHH/7vi+f7eD9tM75hmrwCH124MqMmKJIFFimUWWG2/t3kk3B6FctxWvz4w3+C2k1bEXOwSM8hLzsWfuroboE36wChHyyDok0OwiyNxgzG2QlMGwYPcPJQL2FRmcESZjHJbUTxB0Nx5hPiwC11R8OKxKTnjLALI5gt2IpIgaZJ5YY03K5nUq3zwbZMKm45BWYV2FHzQD22JBOxGQaDQc8dLxeLfDaIFNPdgyNqtKKakZRVoGHBkCaF6uD+YzGs6RBWVz/Hl4dgVYsHT/yBPbjM8Duebwmvm3IpyhAYa09wpaFxmZOYv+fQJ7BwL3gZ6AtUDZWxOBwr/L0lKB1uO9goNJup2IdbptxBpPCICpa+KvE0QPVaXaG0/GBv0FY3NwrCPPADSCBrli6g1b9yOEP1n4AWx5jRiI5T8Zg2R6S/XV2mIxjz8ls6OOhfVtqT95JSxEirT9dGE3/rCO4xuddeM4GLCRqXvBVcZ0HDF/P1s6tUeD8nv2ttuutmuQfqVYmdM8G45WmIqLcQh7dTshnc5HAhXcIc1+CAEqGZ9d6q5yUUxjTU+MtQDY4wfEyqNW8BDdswfA9eEPfETkV+BpNskY/g2N56OEsYrSLVaMACrIZxK09K3v3TJMsddSTJU6B2+IlgokI6+PhBjezbFMYjs7svNrgSNyzrZwgjgM4Hrs3m4ZdABKT96Xv1UD8AKXNjdKtXOKfhBjsRyeU6Xbr9Pg3n2Hdc/azhm+336V07OIp/IB5Iv/+eGsT3zyutBtvNGzUT6e+ejqgb7rh/+f7JvR5OwH7a+/3De1C8nJlPYlU9WcxVQ3nzJn4/fheOv/v1sKBkfF7MkubyHEGSAWQ6Y/y9Hf37TvigMVX4Ufovp9Q/wyRcbbT2dP794bD90cGnEOTqSRZvsLLrPmmOHknKPXWrniXFOJ1RqEjYB6wKDOg4HSXUDbIFPB/cZfmU4SuR7ibEYXyfIp4PP2MGtwoim+lil3Eu2Pjr1Bp4gxgXwvXuQ/RFLulKxakeBCM9PeUcGdzhAQcbsutF3g2vahS87srBwU3g2AnCViOY2aw/hce9uBShborRwClrzSlkW95hbEgP4A3w7dFZz1g2f05un81e6bt6u4vpPDw1oGZPNoVn8M+Y+ygbCuaLRYGzjJNLjAuBgMCdGLynKpAgyjh7am0ziDlcPmF4uBTw72IB1WouMuwKHLJFCvf7Ak7b7G/4oahT4qEo0mN4ePzCxFlGPzVm4MNtBYEgcFSJiUVN6JxBOG58JcCUUzNMx+PWyTFynBdJkTHix5AAZzKDe7h2fAk8qggl6ySgOen2iptfPGg51DTQEP3VDEjwpDmUV+OERkWLm0LPZWABLgKZPh4CiON0Rrc+nPlLqmU1B4MnG3NaJxNED0tuU5M0IFkDTTIin8jSYtXqssSAAzVX4PQ6fZBFQJa3jHJf5bRGiJTx27EzOYerpl7tAaAP+Su5Fz18F82vhzCqYW5c8CmKQWB3GVoyaJLnJRItd71vvnWedHcxXo8eXXvkeJLMXm0m+t1tkCo4y4KBebKQsK8M7J4x5ILrSIXdB1LmE8a67GrtqB6D91MxUFbVFeO5eeMOFLzknCgIgy8ElUKUQVUeQlSg/ll7U6tQgSkjttnzZJZOBjA5P2e4n1Zga6uBPgEpwNevugjMAzwEQoTIUSmx4nlbl4epNHcIv795g6rrXec1nNrp6ivdFJ4z0jPPjrxR7jpvKX7dZo2tc/1QuoKGfQdPJero/YvrI9mqu8EoJT/Tq7k+Yt31A24B1IvOWxmC4t1x5uu+v6GjmMzgv2rTXz8b0V66rmvzEK9UYDpvfIcq4ym9YbSRl0T8AhqQ6dt9xdtACwZzhCQQaF65She69mLg7hdi/Z6XxPzY3dMMTu7vXJ/KW/e8+zI/13uSXsArLK6QnosUt0fZazBR0VoIY5m5Csd4SRsVLBb6wlr1pZxNI7eMRT0U/eWGTw5hKNTt40g7O9GHPFuF0AYgDKiBHfFaaIi1IKyFVmFJ6wMIr4MTfovfHNeeJ7ez98LFMScuAIxQcY0m9hryToaRGdrLA2Stkok70BxFzqqvQbGp1DdM8Sstlp1AQhNCky9LNeKCdihGqHqXqPTMqiEYtIEUER+1ODnAqYzLiVL328HtCg6AOLpAdUrZrvPPITUyRBAs6oMwOwxWwP+KSovBDGb68G3KLS70/A52NXixXqVQU5acH7VW0aIoE4AtLC6FZUWHz6Dl2wSpr+FRqSRPiFpHUemanCd9qWlCGDC8VxBIikBAmBsKHVdFPwY3iEAiVxEC/CQSheMPGdnm2pczuLNKkcWM0ykalmTOCxLBBVq03G34M05nYIJAVirFOHcFQxgurBu7QWtcgU47wbm6wxMlvdcVAb9wN3zwr7FZDL+zXJ4PahgPsVVrjWJDbAVJwa3tRGRVtg9BwUB4g+8OBa1484aHfZmw4hgqZOin4qPEz32z//Tw8MWTg+f//i3DwdV4cuOfJwVeu3IgeBbwNQZ5vvfsq4fPv/2Xb46ePnv+7W5rUPOGcMSVQ8HVLvjeIkZzbyaL32WLGuLZgLlCzm+3X2Hrrxj3+tu36chrdzN4QuSImxKemAHlzFtmaYoFx4Uol/BgjDGiZQ0PRPUHqTv2fxggNOjaHS/f9JXi55DfU/fwuoB80FdaG9+9BS89wAHPkBdBHEh1Oz4Fu0PQ+NONuBLcKpD+NNNpUsgbXn7elNQrIjImhPvT/Li9F8ZinKq6Y/f0+C/04P+xyC/gF3Rbh0//uN3+zXMwMXUGwzeszx/Xn68kXAC8CopbgMZwHuhPiEsTNMuYeGIexctDgL5hrHB5Or58zEP/PXUHJSTgFOvKCTw8DG+Zfx3eBngiAII297Kggk9omUGQT1MDveFxqisE+AGdTdOf1sc4PV6qn6bOpYUxw01bw/sFV4ixZ87foPntrVKCHghAgRi6wXyyhE1YDsp8kg90QYfxqTwiZqQ91lP492VDKpTgjld3OWNvVXsMsVyd8ARipU0jvJLaalUmBXQT7oNB1K+cNEHtHj4OqSZZCthlmbvbsg2hEG5gJLc7MeP+uOBfijd1M5P9o9seDeeYDW1JHLltD+LZogzPPl04g+mDCmXrYjVAEERtagD0uj+ajtVZfD7aTcbnsF2ByAZK5i2pbsWQhkDecFAHTx1ucIforHFoGrK81gJyVK+1J08jSzOW3nIlH/BV7u/k8tFs9Xx7clS99ln5OPvm+V3i9Vb20Ke+34fR8DoKkIhjlu5U2F8VlE6i9PSAAgUgMTk6eH2sPCR1NY0IGI+vJRKJ90gI16OBIgc3zDiJUxsPFidYYfZT7v6SK4CHWJnG+8fItcBXqp0LNMUQq+qhj56HWBT3GxihwO9KwLOBFqrhnH3iALkXFX/Yj7MSGUUSsdQwZloFG91zYJ3P8wwXBu42zRTimG7tm+199fkqSsAPFl7KIdcOBVi8qv8+OrnRkUTMErIXaxXj1yeTiNNZ/fO6vbXUJPAZRXzPFP49wq0TGIccVNB/IEITWDw8IqA6bt7wh7zyaTtOCD6J9Z+Jz1upwM6lgjOEyLXlGIkS50+P4NLjlMBcT1AS7iC5Csl7rBgFE+fsEQ0yCKatE8EYlOEToCIxGv8XERokz3AtX8H9Cr/w1/k0rQf6Cfpp0GEwRg+D96Bmju6f5Qa+d0i0g1epQPalBX/rlAhFR9K8EMrVPCD+VZm3ZiDvNW9fQ+0N2O6InJqPxTDu3v/GBj3Qi3aIzIZ8zAWIq50U09xhiRhfxQ0Yv6wTiQ7ei6iSBjO9KzCaKdZwSy9ykmfp+i5FNBikL4D/SnOp4AI42PMscY2zGDg42KDH+RS7OOAuUBiJLm+Cmm/tdziPgaHEOQY3z9kCDQoEsrTcPuwWKU/ZpHdeFMk4g+7Zwog0yLaKnNMDD8SVDCNr9VSDjGKc1A6+PgB+m+FBhfkoQEXxGq35yyuwR6tzEncQLpGoglz2FqHHuxnurF9uK6JDrIGekD1ZS2hcT+nh28hJtPYOVMCGmKk2oDt80KIUtQN0nvx58+mMuRahg4qVrSi/wJIDSrs6T4+DaHnj9GjaCoBlFvwEMAfdriD3m5u8FvXAhSiLWAV+4BOS+A+p+QDRClNcda9iI1tREbVhGx6IZPxd42+AJVrDk92bnNF/fffoTw+exO8xlK2H4vh7BY6n4nDF2es9peRrMTkJ9wkXXRfB+D8KwuBVBlcgpq4LhWFfvXmz4+wHpNK9B+YZkJ9sVcgNcSbYb7YHD18DZsCyBMCuJAZ5H+wxzdseGbSOt3X4A3tL6+3K4+yGMNyyVZuBYEfe2wxWx2OOmL16ie8r2PDBWEG9lRTTOeqjvTfbA/0vC9EdjN+8gY9QINYhcOgZNzLkjHSAjzhGePwsg8NR3MkrHEUtdhQCSQpk2THJ4P99GEbVZNeYKoUbkZSgmB8o77SAqe5/g7a+fjp8frsB3+mYTogzNZWHvkl8U9803aAPplcgoakOo6/g/oL7/wzo1H611+0VaKwJiM8JPE6n5Ha9NiuWkHHCuh5MWMITEgPXdGyEfyQRtHdrVh7Pf98j9FhrIMRZU3VjBzDJuXCC9nzMb3ucKc5Qi2Psf713BKQZURhaHwDa5OFyAV3X2VfNvaIWmQ33hwe0Fghtjaxqc/NADMMBFjwpVEexvubLW5yf3+eABvDM7R3ikRaS1KC2KJHczRkAC6CGlJEl+KaA4PGJ/IxNtQYOAFoErggUMQOMaepAnMDzNBbwSYV7xCJfPt57IrA8n9qpZQH86sdAByMVf+CAr+Zc6m/E9NavQ8ANaoQAegJzEaEYbLiAm9xh9vzQ8L/UAzXixYCZr+kwzU7PWJKAmBUUOMESTEEsmUBmxQlqLl5sE3R5jE98ASbcOP+unbKaEmoQERYkMcPqVOAN1LIvgmfKmbhr6ahw0KEoDOA/hqM0GpGoJh6WMJOvbaHlP6w4hQTiIqWYegdA9PMdIDwI2oMqBF9vdM/WKXGmjpkXfL3OC2ARIlNVZqj+DR6QIOG2KKGxiQZKxfrh3v7XRqP2NVRCTRsVB2CT+QQryEfr7DfIMUCljUkVtPNJqxAfDLojotNzCyTYMZUZma1mkVSIniqux9CGV9XiGx7bG5sknv7SNy3KXw32W4sPkWW2eEaHjxWJG/WwcYeG2v7ovcJvD6Q8R7oYjsrs6+OkpblUFLO+agQyL8sMCpuQtKyZfv8V4UgL7636DWgultr2m06gzdu/vKX4tmffORMJjB3Ieg8EE0ngIDkABZtwFEoCeNlRGGu8MvyFrW7oWKzzpFqWDN17U2lomowJBUdkw46wLj5uaGO+lnzMtNzVB0iWxwfX4Y75TByv0NUad3nHJXlliRvwCWsEhHJKgf8A9QGNJNIzmMJkntcZy5pNgMrSwgCIn42j5zvqKnnAvfwaCEcrK8Dke80S1aQFK1HUqehQSWhUcPBxEQZIJGnH6hvRfqlVMrKIaMNLhmiXj9ZRK0OJIPjKKHWaWdUe1+oxvqKfWHUgIHORn6eanb0YNM18C8BjmKTErBD1z/pB6ZI1xKqpOXDTEq4BxVRKjVDvOL6kn5kq05QePA94FXi38W+fVkJAIrQVKkzibIRCI8E9n56WO8E7YyxNWm0uTnQWJyBhqBglfIGEMQNpNIZ3khoM4b9SOIqDW8UoCRCGklFSc4NIINncpHzFeRPbjIVyLbAntXiW+GO5GWWoJQRn9hLgrAosFIiCJAVTBXsrehiNO9JIrZAJYXnBc06FJy8EHQ0oEBQ4CQHQxUsVlXpW4wGFhVOmM0PewNENObBTfaOOdl2KIJS9XqQnXnPxCvZlOPxagMyt8iMCmGpXH6Hr64mmJmLHlASYSlG59B/svzgYaA63li9o4EFlk0UnV4Q6nekF9cXmHSF9NTC3MqrqPaAdH5KkLqglA3RHMGV+JLBwwY+vn1okXBE3KxmBw8YCcqy3/bus/E65SFRFzazyt1EExet9PdenQnvWFAzh2WxQM6RRNKRVUaSG0sZeemXO1um2nggj3I7VOA1m8ALxRjreH1GlmUhnJWh8ARqcLNgNim6l0BpzgqV5pUFMvtpLxBa93xqXDuF7/IULMkqYwdTkyIYKDHFnyVXzaQFIJkmQ7iHGja/0Yvk5oS6NTxyFt7hI+3bo1a0AHh8JK3S/k1EyvcQDo69iApvckx0zsSoIgqEFsU7uQLj3EdX+XMVbNqW3FfVbNA7TU5qKSeoKpfW3v92wqhn9emtRcYgnamkWaGHNg8RZGxHchcX1CfneXU0ZCcklzKulNwSGMwxgJGdfMASmaxIpSdjcaMGiKAjFmYHMe+KpS442LZmZ63Nneoy5SxkTH8fvv3Dj0TJDeOVa/OkTxkrtOjSXxvVsNDil2ncodKCVRoQRUHyY3tNk2hLLRP6GYmXB30HK9hIpCoPJ6Kr9/jR2+5pTohUSa/2bnOUaX7Bk4/lZef04OG/Fb2E4Bqm34FGdkYK8oiAQUDYCR2xuW0M+tiYHG9/Q6ajGIDkffnbUQbe23Zt2ekX3qxG24LE9yEdLKnjKmwHplPJ+UYZG3Y2nimZfswpaG979Lh/cZBIYldRG0lwN3NA+PVacqz1NfdiNMFk6cqg6+WgzQzghYwxGjT0jqpQkTAmPlxfhZ+oaQ1YOTARugYzC4SR5HqpC4z3ybcjepzMvhdSAJucTvqqsTktnNjwjK08OpCxQyCZD8Ck5RTkB1kUFK0BmIuVPBdES7TPH0ixvSgQlEGkoEkbs26hWZsDS8Jm/PQWnyOil5VSxOCTgSEaiLEOS6FpbwrVamRaU+Z2jChYqCtA5yS8WKFGl+yLgC6mHqUEy7jCKR5/2cc+mmg4nuegFdF/cMvO9cWmtU7YShRZrU9nlAzeanQFUE7K7RzCDr05tI/feiOoiIViLULfKuoJktKJYKzZaw9Wxhm6Ic6paQLsoyrjCHnKGUcqVBjN8VUJkN2pSjpCXKlaMIuOfjdY2BATC3Ub+lcWVd42PNMfdWrXL9RoC2+I8lAHvYMBr7JoGsz/0xq1KVQsbbaEX7mSjsJjtsFK6huhVXqv+5tETSY1cLeFfUlkk8tCutdCdNaepXaj8gbLncIsClAJxjW2VmmDMg6vcexTuYWKaKdlOj6NHVcjCnqs5NQInaSTOaOYBC2HDGPT1XiQlQYOkMB1R59sKBHSk3z3Jkb0vnNZuo2AYlChXutE8ej3KLcNmcPYE+bdA+PooqqvkSWQ4hYcb/rTVGWxv+5bu/DZZ8Dqpa5lntQz9KK1Mbv3bZ7jJFN5XituV66m0iArX/rY5bvKyzZLcvpKkfACGkOymgg9CmYQrH3Qnt5m/TvXL9f66253+NqTlicbdcijgmqLqhX6jXpCNOG9QeH96Fx6tKyms8qsb70O68cSI7aIYlu9R7YW+J1GhEGkdAABwAf0JFYINEuDbJwg8EJ40K8KiPjjgXlKA/QmA1YIKjVK+jOA1CQtkCmYPFp7CrFTkbSiMgKQHOnFZzqVZ6l1UXe3h0DkJkW1V+qZVm7dEPS2J2QHV58YoLNnkpNRwb0x3xxyAK6ozS/FSEWJawd/7capqya2Ze48vHZJavdSqD/KXTQgEvdi+nJGIT65cfwPvUOMMzNK2xARzeuNSzVEKImN2GNVhOma7jFoo2KnBjBUB4o/b19rhezk9ztl/FjxQ5o+csjqw6uZwjWlWZJzgs0YXe1fU5LVdh6DDqNbwO/gNlSXHfsOV1Zq6jsI7yOhmQGoEDqxWuluvY/2m0lPHO8xSr0xZ5iE10QjNwIJUTuFB+RONyzwYWptmEUyB5Bwxc9rzNVYgpIiqNXCCArQG5KY94nNbN+EZ3jPEeURFOmGRWqRQFmWmKl39FG80WON18pPWWlWfU2S31EsX+ciywzRwK4frx+ra7qTP9X5GpcHYzyhOsisdgB/CaST1WdVDB7yVZs5JFGlNuV+orVdV+pXKqT9Zed56GVzUP1z7z3+wbrCXIAPNpXmJNCeiIlm6N3xn6eL1ir91v9eK4ruyd5BFtSLIIYbRGhoTue7E2zUcVvj4ni2twpLlSPiLh3qVyIdk5N/Ke0PIQztAdaMxQW5mRO0rFoxKdn3BlmnJHZJh7qFC3EXfMjytgctLdPBo/w5m2VE+XxIZpYXzbBZSy1iaCtzd8ufhnZk7/gN1Yoc/jYfX5tDzT9rV+p9MtnVq8YltzWABWRIqVPN86lSuxVcbXlt9qE7vv7S74GK/832fGtfwMZuLObLUCM3wJRx9oO2azuUgSkXmhiqasaFXQ0+i9NdyJplHprLKb9uzezHzpiJrnpnuyprY0lyjMUYDsUFVFzhCZhcZ5AGTyZcLwUN6F64gJ2a14L6+YACnDJEFGupjCTQET7wjyYNARF2ur1Srt0mylM1T4CC10Q0BGUN1NwkaBoW9ClS/e1sLSr162wpxxEhcJPQvQdRYgKuolpILSIPvx0aeAnEvhZl8PX94Zn3RGVso67k+k1Zs4tGimxmHuMWVfA6YNMpoLqfQ9BvNQ3p7k0kvzmZmgn+oUGz5gxgdQFgAbwB52N3bfxxidlpmApHEqsh8oBdvHlUF09/Oo71ZFtX7OSGapqJoeG00rIbntdnpCH5g9QElJ3SoI5YjpdF4sjwIZPxVWjrC9YQ3sehgla+D/pNMFTjOX8cJ1og9NXo9eKRJxzDtW/9eQZKWl7Yu7qy9i1gE2/gKA6BAYe2mi9EuiycOxgw6oxJKsCtVLeJztYd2vxk+fPbyYP/ht5YWETtxu58ePn+6qqCp1Ob27E7h4Kqg1FIu4kwHW7R2WwjlQALhGIK8FvvmtRbro1gSk2aGTROigXNHOjBb/THCVTkSbNgrki8eWXHlyo6q56l4XEufE9nq04pimLWRrCMkmY6pWZUD/HFwtC2l3j/R5lPi4fc71b1CxAE58sez0I3WGAlFacY2RiLumjaoLF2/up0pGq6sa2AqUWfEafpWzp/JI3kfRaEAxOqz5mh7Sj5fUCpPKzexExCvmgpv9sGi3c7BrEIs2LqOmrYDE906/aEPufo+EopMO4FtD39clVsi1ev9M8ilQRdH8zGxki1EFfxbSPVAAXDGPhE2kv6bdPkVSyjoBESq/LdoC57R8rcsjAsbgXhHJTXkSgqqSttlauGU7jEqZP4kQ0q9gjK4o+S3EKmib/g56x6KgakYQU4YI0zgQZFkclZeK+HvkHKZssDQfQzba7taHxIZFwP4THW7G+9i+JzvrzUIYZ0BBYFWZa3lbMmB9GR53fxstS6d9kejj8er5dC9kJniUEBPy6pVv+7+pTXcHLMfsIXnWSuZwp/SSNpv8b31U9JUX6uRK/F35NtHOAqfMks4JOnwhdAya1e3Sfl/ffbZla6W6we0o0zdlh9wVcC7Ki9que/xAqKId7sTmZbR4N5LkZ0qV6TCFWLRV2PZPu5Vq8sJeVtW8r9RI6Zj8bgkXau+fSc6/icP/+y+enHw4OHjgycPO+2nOqvkjcZtWsUhhfpIrdPkdTZdTgHfQJqrh9ha112Wb6B7qXFNcVocG892DP8rY4wrjLfvPC3/RyYp2scT9XF+DyCW937n6wt4H7f+865KKIGpa/NjSMuK6oQn6+ceItwpNKggdggaSbZBBDGZs14Km1TovawECRXLDu3gF8Gb1p7OW3Cn+HRuR6wpKGrEpFJbQ9XOLs5TzwTgHotxB7CMZAsoJJoOks4MgI0s5KYDzqJuIa9Gw4JqMSjhqB+FmR5Mp0YZOemDYijw7kSaEO1FbNGcOj68g/7ntZTTriwVbcrVYKGKooKjaEbkn8SRWgXyQlynqoGq2UvNUHGtQ60UQ5JZcYG1i6OtGgPkUmH2mI53gsGp4V240lwWbhfpniAVzNunp8kcGJLaFXNbGgfJrq30f63J56gZb5rNQeiTROwMOFybVncotk1ktSm2vxLzk+uvsmiE4j5GLau2jCsZbLMZL0kr9NiVjkGm7wMii+smHkRk9iKdgUlDAvGEKwW57dB0cfLWg1R77kqaV7VbK2/mR8kW122hwKaZtDFllwkqojSDJD3IDIJVG2AUKzTqQQI0gDqgHDSP9kFsqGZwFzBrsfi1csKaxA2eSpsg11UDXEPv6yv+1e8yrxbYxDcv4DYafrtSsxTeTnHZofdp5zjZFntCi0Z23U/a3thrJOZDEIfOUo38wIpBiZ85KdQwpnUoPLzWroopOAA9k3NkaHXecBWCYmVKvhcBgcvWpo3SR7gkPYmoUQworzTUWpZItPelCuTHSMmDOac9HNRpiBt2AvAOi76p3c/GeixsHXlIYJy/TIpy8ABikUbe01gh/+LjtPNYY6Q99feh62G5HyW72Gi9b6E9Yb0M8UoVEB9DglejG+pI+80XZ+xLBznUWYjuI3y+x2D66OKO3DxrwWzt5nijwmealIZEnc2aU8udw71UhNs8mX4nI43/OkJBW5R/WZ72GfJg1AsJsf7F4B3p7BSlUeSiShoc38jEPHGyIYMYvYOsNi/zWEPxSdU9fIatJVWcok8OWYMibiwDt+pU2FiflvSA30y8XeKswe0m7tqF5rcrpgd9R1KefUpuaNwkGkG9KD8ZsnAXpJgqD0eJQM3jjaqXqQIrLRotFBlwcqx3pemtoRYjWBbjgs0jZKV0zQUT358kTO09WONH8iv9EyAD2HdEO5i5HenebL24tpu9hzZ1RJQCmA6L8k3WrxtSWtMl6JH0pBFhECzl7lUp8QU8kzR5xKaKX6mu6sBWJI0iY8ZnBavraZN7KPb2L38z6AiHBwq3lZR4/W3osd315b3PHW7UPQ9e3In/43c7v4sq4loc5KrDlCgvu25HcaSuhnhXjNZVn21FaxnvwFxBY9eF4K9K7OMJXHML2ESb9QxZbVvZgMAHkJA1SZEu2ShuhmjsWzepuWIHPJS/TgL/vMP//93OF+hc050cSNlVIp1sAikBPUvatKUSpFnhhdQuohJwaHtbgp6mKl0nmlk0rq4Kxokq8iAvaRmBK8C0Nr5HC1ChfJwic2UERWq2NEeN4ftoAnuvEpjFciwM9F+KfMD2S71P3q9moSxfgQJvJrCLlei10EQdOlIBKkV15RIFry21dHTzTGFUL4k2QYn8Uq2dkgMA89PNWa2Prnldpzn8UTDEoKKt2yzRTzYWEXjatzj7JYqId9+RDo22LjZKBMtNwG6kxUaOwZDW1e/jQiLaCoK9NyrPUWFU/4a+i5ygeydZOhmX93pHyMrdwR3Qnl6oTmY9ZXwDa9R00W6D4vX4O4uwzN2u34fda9B1lJ0WvYI6RD/fbHTXz9oXpzujbeWC3vlt4t/aiCKuJ3k3GXJz8bvRBN8yDW7T64B4GxNFQuL2+xOtG83A5CtnEXWBC450FaUrvC3ULRriUyL5UKQuxIO8UR5SreIOvKxt4rW2sFLh1Oe3QH2upbR4O0WaWMl2WqOAM7Qxk7oVGLYhNlmEql2oXxSegAcSv4r3+fJCeESniEaTh1LLSidW/9iwQkxSv2Bt4QsR9wzu1HEgASK6IjlKJt4M/cCaqPxGG4jUFX6ej8ewiInUaxr/nwvPjbbhPUvMn0tArqDSd5GKfvs+oChc94oV8s//5F2Fno3zLpLOT+UtxRtz9IGHWnt9f0rRFg5D+5t2yjNLAbjr1kAEkbnCFslY2qPlTDpEIgp1MJ3DCmJdAkBpFLWlxQxq6J8GdhC1LVVb1dlswf1YId04N2Rb5rNxuY22FT6JGpA3oFhO6P+HM79kIWKDI4a6D/pqZhaimRi7iR6jH+cZOouhRODW/sEQNTUPhnvoBLl/sOO+Pjja20M9jWG+gxgBi+FVhTa0eQbLHcsry0SgfuzgGRxzGsVYpK8Xrtd2GAPiDCBzjvI84qC1SnuIV0PYmneVhZ+Z1iNBCm/WoINFCPJKbsAiv0iKMcVk+xVo+Y6qe5iYo9PatqvbUf2c/XiguSOJ+QjxkiHaoCOCRMu/SFYkb9y3fmJ+hyHeJajMe6m/e3nonh8NtZxawTqYbaVkb8RC4EApnUpNSh+lwaNSWXuUjNNpNmLaYyoNz6SGHNNvWDy5dLVJw0GMKI9N223htdv0lYm5jhzNKuZZ5Xqw1JMVSR6hrCQNewt7W7VMoqdIYahRnQNJQA/22IorlWh/h11Pf/zhP6XYDjCcfChB7yZL4eZaQngAsThuBCZUSwc7zwqpfT6Fywy+ZOmKU+vDtpxbYoQ4S1BjA+4JFgHf4/Ez6Ria0I5Ng210MmyLFbkUDwaq7yGkIDUb1TBjU134+0aTXFLLx2hyiAlxXr6kWTQjvAbufnH9MJCMHWJtFfzXRV4A+Yq890l+yWJT0ofo5aG93/zqmnTK8igA848HqO0/mJcgrHSAiwnnjWRJi2MXqALcBFxvesrxHHtgae2lEMrG4BHloq7/gEeewnc2SocYFDTbprE714ADhBzsWjnqD+4j71xFs1+QtmIW/Tl3d7rDbXEq88e0mpVKVrSabvfrLS14XLp7ruE1rRpSVGkhLHlmdZPtw3TcA6CfwEe6p7dsMNKzH3fbfV83Gq+onKKTsa53P/7wX51H2AOeGm9tkd+K5S1Qh+Kew1B3fvzhv5Fb+FMsF0bnZs3QN1yumcG3sJauld8XU1j7iwmibI4o4ijtSzqlfRCh9e8b5vhgSv5urU6EFb88+CN6uI1SHQUlHJCUGXmPA2LZ7N7mMGB/tey6MjtlRA/jgsu6PbS/QzsX/xHYHmqqsRk5c/4hsokvGbEcsCXUEz0rncYD0qFm1kL54fRRL40V6apO4yf5EnD0ahGyFSWTf7Zubxtv32dxfnjT0P9D5ubQl4DoFvi5dRaUSzZRQLtULmGAO34A3aTo98SBJV5+QPIv0E6B7bGuGPbzbYhaeXgYakBuNDEW+vMr8fXj7K2Y7T9to2KctaGOBhaxL9KEUkIa0Nnaw4Il67q1GhxArbV1mC0PONSBrgpZqniv0ngQ1gVMBLUSZHNRRBFyS/wdOEVpuqhpQbXGcQyTnBbsow3ZL73MAy0gcGmxZgFIBsraiheNY+MEQZvwUzSIS4ZkbC2WhLahFa1AB7PP/PH/HGGA0E/MLi7aWsTFi5loUqYo8ixlp5o3tH3BMUA7CQGNbppMoNdd4Li1HT1STK+9b11VjzPklPJ+4G7JweitqyjP7k5FU94RV8s/9uXaNWMIOa+TJTVz1lHW7i5ujJgSLBZU8j84JFCTcAYtSqhQPvS96mPAMj9ZUPHEgLJdIyBtkT/7t7pBpZ/pbA12aVNarzl8IKTAhyCCqAyI9y2upQbcKX/i/lbEB27cDdlXcwJdYom0Pon0i882rFnMizwQatdiqlqosljfUsMff11oMHuUWaShOJ+RCOryHgugd8M4hv9ZTR5JCEMF9MpBOrtvvKd5Vv3m/eSqyy4Cv1lp8eYNH0Svq3g1fRFanrsFpgzyMUXyYfmaf5ge5NXB8O8u4EJrCuo/0kGBq7LrWW3iAOeztf3mDVRBw1Go1c6CPdkxYakZdFKPmoIbwEg08PI6pwpkG2kN73Ht7bVgmgNG51CKOGTWQon+TjoAqPqlh9FHl4Zo9nDPSOrt+ufDwlDuOIqhfBIqhg5/ajpYc65Qn7fi1XYHgFZpvUElal+vX6ji+qvmqjLD6+q/aq5iFl1Dc/1VTaV65yruH+lsKO7bsp+CRdOhecJMpNdRFc5ZPoOkkxdcwFABQn5T9RMux6AG/6qDphMYIu+sg6454/fbzY3Oh02UzF/VLbhZ0Ea65ZD5qVWrTTWrDk1prYst+Dr+hygbH6uXjN3ht371k/0S/GQhgBacpPAKXekB+CmQPx+z62ynw3Em7rRo52L3yq9eq95DRJQhZei2N0TclV6rDTa2VqzqJ7eSLUD08XpL6Otthy1+fkH/4Xwo610u0U50OV2s2MHAXBezk1G/LKM8NOtSMxw+FlfcE2vGjRQwJu7OL9FP8NH+dlvhwGf4ov7sPp7dJlKEURXf9IX/PU3RM3iWlVPNMIYxg57kTO8mwwGWAS4nRrEChmFxkbta2EVyemVMfZu0xrl54ze/+U3VYhItvzEfeZLrwCR8QcStDM3ges/Zqm1bAAmCgPCRdthUCANa0IzuPKb5eAyLR8PQAb+cTAZ4o+SPA6fBGKIWo+bEOn8lwShDLmHJDHspWMpvNDo6CaoDAVQiMkoCVLQ0vg3o36vQqOMU4bNMio3NGFfinp5qoTDpNOCrj/pZnyHVTuJs7JmejTJ2XLW4Y8W44E2XrbGq+4K0lSRHa0wjQSxmiGLSkRWrdU8HIKsBHsETbRrh0O3PtDVkqAlfIcfQG4c5PdzPKjHR4/99NM6X7yA6OS1gwLHEpN+1XjShnkPTVhR7hedTwT/YX+TN6n0wCJZkxBouQ1v6oa4WWw2w8BkKlNVz9UjcWBOCOktCithhb1RkcxIDssJCtquie/xBiK/YDpbTscpa1bEAEIS+bwI+QjEIJj/hkWsdRTci4YXkQpHciEjTlriuTnUsvCHYPvaMOM8n51wRLlLVDRS3Ui8o0zTl0jKlPko2e5+HGoiz6zhxkbSNk3UK4H6GZr56zjicMXpMSJ5lREhV+TkeqxJHuB3iemEJwtbZSkKSws2t8J+R5bsf3P1PjIQjTioMDdeqlKvnng5bnTWOfAKbkQo4wixNx4phEGed3hEro0TKZdlbbYlKNnbL36V7rnd4aX88+tODJ6z+jjvNK3HPIfLR9/k9nqABJlwwRB5G6Au3j24fxtF63JKsbGNuhqX54HNDGATL6XsCQSBArnNfWTlpzPVlQcX07q0TtGZJ404goZR2vVwmt7n3BOJC1cPRBChMgX6Q4F4uJ4BoJMcZ08IdSrkjzttZpOLl4cDyejkLVDBK8e95u2bA/YcVa/f9Q/nYLvLXKV1De982E6aolnvWflCCWKEvStXjttZPB5yM4f6aVPGvFr51AmQLcBbWitBgmMofvcCUkvvKBWXT8Z+jXMClkYjR4k3kOezcWA64Or8zkbR59iKqwLdJ+WwBlIHRXaN6Np5+KtDHEk5EHAJ/2ouYTQWV4Nn7kkA9deP5spdQ5BKUqQRuQAmm3TXIY4b1dvlfSYKeVMLE5hmqSeGWKq5qOYIdddyICVU1rQ38hXA8CXhv0qjPtxWkDCZ6JRKzA4Hwrhtc99IO3FcQAQICCEeA9pjHcnqgycZJBA8vUBdisgFMNJMKBYK1aHwNEeye5Rd6CAIYpRip7+aqExCU69Xb/r78wdaZSOlPcPWdeANJwuxnrrfuCvXcNy8P/y2qLnQ+ZXyhP5oCsNY3MSLlqx8++xafQCoVwvGlONGTvUPUtO6HGinum6O94fDPT589+Nbtnk9Pyl1oFmhnW+5+82Dv+R7KWT97+O0u3qv/MzifvqaUYZERcIMTRDqkYNtmd75mMRO6rBVKUmSTA9NFIDyZPHkM0IyKIQDf539EfJ9NBK/aq2Abo1QV7gZsDvwI7ifaDDG2ggSl+DIVLSBaJjdimrW5sD/IW83FBNUtSqqNRKo2uBgjI+MkYRdrzcjApCirIoWuQ/6MAdjvz/rjIp+/szhhViRTAAhgjASJGjJgUF6UQJcTbdi6Uot+RlQ/i6qoWiNWgARqRQbVDDcvWLRNX7vqRrCwvPxXWL4qmeSRf2GjUGZDyEuvFE/j2XiVdHpffdKvef3bl6k2w1/C3V+zoR/46tfe3APTsF6jP8vNb0xlzcW//wQJtuB64EpgPQwxR0pk7RLTEgQam13zPkX/peUp0nGk510UWoqHOUVBUSXECK59bdXyp+cF7QW27wjYB0o6m4I3UK4T6T17j4eRtull7iYD/hJu3yYb8X6vYSyCu+bQ+3nvY+eUnL+YYixGEtKcdZ3Sd4MLJ3baO4vdHX/XUCxnIyvOHF2VsKWb6z2YcbKcVZJy1S1q/OiXcH3WrvIDi6/Gu3/m+9KcS7goHarr/ZrIglfhFYpXIYeVTTDsv9pXwLrhAYariZj8EXIQ7Uftp4f+C8QbGr9AtuokyabMC4Jxg6Ae8pyZuQE3oub/hiGZAMRC2vgGDZFOfLPTRfIKnjq0h8TPajkDO+Z1VWO7zP4WYgucKMQuc4kka5SWQjKVLijwyuoyZBI2NUkCFXviIptLGhJn4Tumwt/CkMd0Dos9zF1MUrzGHMp+4uoH1hlVb9RfSTCjvpX5aLScs/5hvRGb5DkMnDYetN7CtLzsDWmJ7pKijGdIf6CxqIZZ+FocFaz9JeWOqVyzzjd89IgJsVZm8GxiHBrXTPRuJ9qGxNRRMvf+NEHJneev6KQVtwhOCu1FkGixh/DLrC+v43zorZ8hHiNbyTUz/wNr1O6zDcqQdfh69hxAofRUnqaZVfSqek2Ll1+9K2OGtZBxJole6tUnWrxGpuSlWqS80YIWYHIE3tSOCfSKMY/T/8fete42ciXn/wb8Dg16s7GNaap5J22ME17HA6zGA0uWNwgSh5KoETMSyeVFmoHXwP7PKyRAniWPsk+S76s6p+9NNiVKlHYH2MtI6svpc+rUqfqq6is6Rga+Z+2Y33uagZ2hxpZQ8ZRsQgN0Xlobs1qaVdmYgNEEQ5lOWPeLNYruGm2fuwSuPoF3C0RIqnu0FVsa1MeeePqZ8QfxC6+xM+LbLdRxF42bTcOb0ELia4OWvBI7mwCE9mWErAQqVqbdm2AHm50z7AXVAFknz2v0t1NoFi/VLR+XPPlEabFNuSHbtCnlx8xjerW3L9hyIE1KbicbKzG2THSGSxEd5LPAaDjsn6GY5llT+9DeWnLa9uyzpazjOtftcIzuAWBOG4L+fL7GIIyditRgicvvdsyZpOJj4uxkyNADkZCVORSZFi8sCvaAotY0xApfLqTRFcldwbGBw0oOtq80xgAtCDhRThy0tMFn+qdp0YHQCN4TOj/Dm9rGMBhpFdY5chiLmv505BGez3/k7frM+3ToPZtDb61iXnsSRe7MOIgeQbNHh7F/xR6blt1i8ck4c8gpunx3kUDcrA/0/avBkehdpEiACegCdncCMTAkOYZMj8kCfowYECCLGP0WjIIh8ixYzXAsIQxJmwuWHHMIYBmyXaNm61x+xDNugHnRrr4i446Wgb9iRMeRNDLb0E5bmzjaZJl2uOm+iCqWJVonsuMjuz8JNw5BeP0M4yWhkJ5cSLS7xOBDTsQmJI/TdSSpQgD4M+PGfrqNtnGUQP3hH31PzyRv4QTDdwuwKk1KQFulrE68XqLT9g7jcDHpRtyUITIzDJkWqHjIoAVvaouJj39l5pZN/dyC8xywklwr9cCqJn369qtuMpY0tymZ8DxCyoT02Ui5g8e4RFPdCXbxAk44fooLXOlFgoGpLX1a1OkMPYd+dvAcdZbiTzsmnZShO4evuUCqjYTQk7dKtytNVUDOCPQTMY+L1ZWvQOC7KscUCD6Y2QTLcrWUTqBQRvPRKfz1F8xbu0TuJehHmW9quv2wwwz0Cx6oaYDYxOCrGrHrMeOSBu4wvF7AgOAPDi9G71agaXOGpCSDahM9NReEBdgG2sLTPE5CFQHjnckypI6LTZJ86bfMXAvlAYrrLERfgkIIATmNPoOJqJ7i9LBND9QuP41U6Er8ansoSTPa0TzI6UN7pY9omQqLWjQjJ2X0p9UYmU9U6j8LT+vCPQVV2blxcNGOnR3ap0i4/Nn2gw1/AYn5MC8mnfBG6FPIM3ZyyGS8ELaDXm6yGBOZZBLHoXZ+hA9h+1EsgKhLSVo1RwQeOsdv2dkyIcmLs8X4j9/8SxFMG4nw+w8AXwycc4HBT8E+++4b52tgVmiToiRAzpcm+UVCyG994Sc0fno1nLz/CteHEqzM4oUu/BqHCi3w16F9FPw5LvY/TyegmiP3jWO70MsCUxow1Kj085sstmjSEhHsvjrFTPCNQ4dzByoRocmS5iYq7TYJGLs8/v7skfnnyb9zSv/Vc1v/9vU3+n8yuRmGX6kIWb9Y3DNFxEU/+VBiCGTmAINHUkiB8Usmh5SLyijOZJbtRviFXw15cgiwNcgOEcBrCbslnhfyPfY37Kn3QcQX+VnYFjbXg9YM46tgmMRVx3gCWCat6Jwls0DaV0TbDMRo8rz8xpa+ZDrItCE1JVEnU06CDDMZB1+BZt4mecVkr0RGuXTtj5uCTkHOR7YshI6H1QSpGeiGc+6aRIyFtPqG5pgmqKmtDWpZGP17bRJHsvMkiXOQfBQw/ZBgafKPINGWXj7UpEczaAZ0dB6pYWYQigQWDCbD99R5p6DZ8W/iYjPvFErM3GAZdoDSL5l4z6C3Wrw/4wHcSlTfvG+Ac41pzon0SrO9sGPtlEQA/mjLiZtQsichdPsZSGcJY/CT1fUpT5wLcIZ+FGTRdkdXK5cg6vDiAvpX5Eg4ZzbZve8ux0Xa+LJWm+JsiQyx1LufhxmZ68Mf2IxMn779mpEZS7rOjFyvCU7H08Xp6WPsaqsF/k43NCe6iJneYhfHb3nqW3fTJz7Yfk1M1L42aXLF7r4zkUuLMq/zxNaMJyXhui6v2/54iN34TE6GDZ/70IdCfNL2fB4k1vDuAnc1RMXAJULJqzxmSXB1USsYc6NbPMMSdz+L6GjqyOPf/QhWScrs7d8sSRnU3WURxOOLi2Sb+84GD4J0TSy0EapgeGHpHgUq7dBKjxD5/RwLaW/HEin8V/hDY26QoD3whQQuUG9IXfzVBD2R0TbPd41IcnwVqlON1u68Yz00+ZCRZQ8XVPqa2mIjm+RhXDw+EyXXSCqJV5QZl+fatt8DzKUY/FpnBwNTl3CDxxQ4PzcCMBGNm0vZbYoTBLIrZDhpzPMCNRcsnKZrzdVQntXlJbo7CO4+upoR6JH8jBBwyM+bInVrY7IIBOmIlPcDBKaPpNkjKcm3Pyw3POd5nJ1bTsYDa7KNU7pfnbZxeHfXbqTjmQKMRTIx8KJE3kfCwsOBGbulyAqrLIvviJBMEBrUmFYosU921hl7WHAPWl2z1fm9bjjP5ijfZk4f4VRfP6f7P+DXj+/uu0G68Sl/QbbhSdNLIDO5mt3BAeyZLMYckrvu9icur9t8+YNJ6fr525dsrh/V3SVyOUWOHGJpyHvY5ApBBGfHuPxHvXxrJzzj/ueSLJrn2x8+sShtFHuulchc13tI5Xy4uESs6RxxKsYLN4kmwuKLy4FcL214tpbNrAc8C+HM+/UPLJ2Zc7jf8zx7ae8lnx/HKOfOgx7h/XLtnWJa8ZufjcG56asfwchMzN3+7crkct5dBtFscplg14n7U3LR1pIXuetZiNza73xgWYvO1n6FLLZy95QuNuIcza+mZ+83KTp572t79Q82Q/dukpf9nOcji/nn4jGkc82MPgF5Xbfe95Tg2WpxmUt2i7yyuJqhIm50R32Z8oRC/N1Pi6ZItUWOL38UDZo2f3umMcmcoELekvCUjDR5qIvCy/enybq59APcXHxHwYzd/YwO9A3f/ShimZi9p3DCJwZ135NehFLStTc623IpYcmbO2rK6L3PRxzXf/NjCGN85p6AKMaHZAUxjUh5fX6e6kWkvaMaglRpLPTPd3brPSBKlHvuqCXTH/KMlGW+WXgUnZk1l09BdWaN7e6Cyw6qiNEgO22TuKI2oDfpocser0eC/NaimvWAZyGmeb/+gUU0cw73K53ZS3v3s91IJio0NiKVjAAFsrmm9jSzlFNy/1Mf8TxQy9wz8NDYZeZA9hzoWbfAOWX0ZvzBBa/uAq3hMovCT17/Ef039JqgXPnkMC7CLMrkte23r5kaNgRd9el8OP8oxUpsNBhudmByQshWIsVz1+juMVuhrCZgODKVk8pQRQIhVF9OUHwXLnnzm1xLrbVwn/AtKCsX/voQR5FkzMmvUeEpNeNkRUc1+Jwlpiin43229sgvzUGSiml6IAVYqI2UKnP5pFhxjjT3ZmFeQP+DwtjUmWPui80641SR50lLDUNlQXjeG7xPa0uDIUYY0qVvwZsfjlEiSdXCws1QnY9Nr5GEvwtZm1DJuKQITufobj+RPiqrZbQ2Xnib7ISYuimuWiXUAfJqpXQuhuKKxfcocNeq1hAZUzDhkAHU6pr0SNMxRGQFZcQoAcVA0HeBsOUF6mpRPQU2JVmYOCcvKWBMN3mnhPkTdqwpOKlCta4oz017qhQDrySpcXpxETDzOrPVnLVqmxP7sGkOdT9k2QvKWyCMw5Mpamthi4CoOKjaXEQk42aMOlfdOLkzoZJjeB4mx8ape2hjIzGAPSvxtIXMqbxPDiW/TkoqWU2OKt+4/NiiSpOIh3o+bK/UK5W0ial79gppSANSPdBqQA2xhBB/vEaJ45lfFx9TS8rnq2m+NnNW33wxn14b+jnzfmx0/3lC26EngqTb+gQieJU5BcwrF0WycKO/OzjfJqsPpsTcaDWf4ekFeP3RfQcFsPpV0DC30PTUjjfD8ZXUTpOlBP12SBk4PhU1Zh4Gdo33OA7RGMieD0pRICoIbRYup0LggQMLV+kxE7xI51bKQIVY0RxbvPdFGgmefyfnAuEdWRrT5EleCJpkDHw8GW1MN+YTXusD1tKUmEUyCWw8fMnQEl54JEIjR11/b4ZkOsQoZeKUqgzfv5J1w5DnUNv5+elSBvosdFeOCd6t8koyGadN3X69pNTFXKe/0NMAW9/0tMGBz4JpFq0rebhaO7K3sX1RcK9kPiPZ0Gha7Ro15W9j2zkD/Rr0rvCOTmMuuSJXJDpSSKMIl3yqcZ0pbJJQgtJZTVtqrbXiWHpBrro0ezja8INXCREyyD+CqgaSAZgZmw3HYDolHYdhtBSiBkv6WjrsGJMJ/4J2hqYiT0aIHGR1cYE2beTgoIEkjJ1nyLWG7oGKE/2j7Mts+6ZWmjYrAl/FZERmFJjrRedfYGsryQY6hrOtXPxaNCTC/cr8KpynxgwPzZlUpijfqBBa0qwTmzc5I0qPa9vIyJdzCvCN34oK9H/DOha0ZhZGEZJ+6AM5O7pkkY429BTYLydgvRUmCC4BLOufJqb5A7mmZH45YZa2SdcJx8PVaCjKPsGIavvFBYSdbDYxJnVVQui07RAk7jUT5/3hxsWl5FWbtUY90wLMeIyqzt+eVmAz5yfv2NbLnKHHVZGZwzCbPGOhAcaT5uYa4uw37Dp502WrnPjiElkHl4u0KAePNJQEm+xpky9QVcCbeNMlk5hUeqS66cHl2ptgIcoBD8JToAmkQoq7wT6UvDxxWiIwwE8MgzA4R4Ra+PYSJEtDKB5YjdhP5MJwZpcfF2OwulmuoFtxvqG0bo0ys++gH3gFrmBjoxEIGOqWDH1N0H1vNVMNxj7tmAS5VVsQgijDtm/BRRfj+eiWBs18RaJsjEmaSUl3UENfxC+NUYKYWrUVWNYmiR39o7AkwbYGD8jH4s3kzHR4OM/cvJl3PEnLJ/f37XgDr5mlx93CawZibZvUHg09FIACLzLNVwfs+ZcHYMDWN+6Sez66GZ+NNNFLC1OoDLgl47L11pQmggl7BadkDr2h/F040l/YfWzpaEAGhRaCxtwi+a68JgH5geY7ZZvfh39dnEirVKASYJ7Zn8wYAlaxiWkFPDwfzqA+lG2p23N//OHQEcJ42DH0EoUTnB9EgIfT5bfoKjo/ibGS3M5GIYDhHvRoVE1kjgw1crO/F8DvGlWvYHuzzTDnlr74EtRObGtmp8938W6H5PCe+taqGZawvKnfxzFbl3go1OPCJGfKcf2OAGrSxHWReM9m2mA3Ctt6+Eex9TgfNCrJ3y6TIneFiN6UCyjuKxedcAvkIQj1cZ0O0FLqZXHrU/8nxIWr843z+Weg9sIxJWcJRuSvOmQ0vKKynkaDAxRbAGzA1IftSEujh3UDojy68DlDRSI+/wzUXlDEVqbIk4nezyBzMkIkpb7SDU4LfVOnFtW89j7LmGZAyQkoRQnN8qCAoH3+WaXoHKro+RIlp5zOeXZUR/+O5pMyJUQgspz0nq6eoKCqAWgNnzLrU2zZBIOpgQsMRCvWRw4YMXtAT9wZzz+TDwYlrpu7fbnj68a0xh9Pb/vclZazHw8MmTyNpqy+s6lcw2tOM+rfjduEF20RizffHr7reQjxuu98aOmNztaexTY6mHX4kRFNOTg0Pdi2PIhK6cbmyKDwBPR7tnRZ2wuD62KaAM813ijBGuk/HT6XcNrLgZqwoXCcik+lZzEcM14JSka0dgf2LCrcR5YQLWSPD4ktWjfJUlGYmB4DqGs4rmND0h9RWD8XRlSlQI203inibP78MzRAMGSkzAsTnlUxq6anGKS0oc891qLzNSO/Fr9RQMmiKRFIRu1hIlE40FilCO+JYYCFNBAN9VWfOnopLkNQjri93z8ziNJ9rf04cY119gwQRuQHAPUxejnDcIPPx1i0NJo4Q4B5Tl/ZdOMhVQjuD88hX2Qa5AxJDmJcWSxmBq4jQoBIt3EBv4cwEeeJy9JPjGRK5HM+gtghzBF5TQQ582VChE7MuDhxeZj/ZTlGdN7iYfgcbQpEE448x7IS4EPB2zE/Uwbhs6F6hS4yPulJqtS1I44vwq7h+fXTtQ/UKXPp1rqthoS4vcKx6/yAYDr3pG4NiZJpu1wJzaVB6sck2Wd/lsnyLWJiR3CTIGXqwxKzlyj+KAGOdKXlLuVTMSh2khJbewjG23OF/5W1miRH+lAA8ddTaMRZWsLKz4SEF7hbUkDg6smeO3e+PBxdFzmqEQKHZ+DikSteIm9g7pS/ohcD6mly7LJjE907qgsA3KdwJUZEmW+nSUpuYYrlyywnsAD75hOUrRePuGbzXyijBTB+9hCTGwzmTrXk+6BUDmQqNtE5mZUhqGVj/oqC89oUy8bfZqMzusJ8Z/B4v79U7AEIqfA1gnD7Xccno9vwyxdkLSpiposzmY8iH2v6xDFbRdrWW/YnS4Vt4P3s15kEHMHa2O6aS31zVlwhwBl+OV7ATnjwLKPt5WXScEYgJGE8OXMvbkCmMhgTuH4C7cXGgOAHgQLtWX389ghk4DOEh3kqUtKh4d/pCUyZwmIxXUOiMyTJDp0bcdfxS9RJzYcQ8K8kKI0jdQhTZArIkV8W7zZHuSLSyYwjSDc6Fy7oqC4v0QQcu45H3RTN4MwhoXIoUh6MEBF0RHu0G7l20cDYwyIlpyRDtzg4knlUzpfiU8qA45oxdcnjFx2NlyP3SMRtfBb7Y7ofMWDYX1bc8DD/PDp1ugjikA4b6UpEmmXJcCYLsnM1Bjt54VBi66jp+f275bdOAepJGrD7v8DVprVuQRiWeWvB9kBHn2VzOaTljJTnMgLGgrgwBaASF+N3KzQKJDgGbm7DqV2QEYQziDhwUrpxvaJgUcSssTvA3072HhVZY27RHpMWi4Ao0EhwwpbpqgiUUxzAua8X2BxEw6JQdpIBRhg6saLcUakLLWF6u9gORd6owmK8S3v6sv1oYCn5IqkGhNCoTrR62NhasoWg36RVodWXCdn3tbhIvatinxjLdwlpuYW0nO1WWrgaEEBKWR+nHeSHv3mFfLypUwjJFZTQqQrEh1lEvMwd+gwskjyGUFcgVIlLEPN2fpzeqoRBEBCC4EmbR74yhCq+NQlY34BIXrGpTPgndZs/Sbsul0LasYucPju7M+N+h3OOR6nu85dO4fdXy2+FvdEcYPIH6ryCT9f/Z+fN6Na1us2oNsd9A6XlFDKW09p6obfl0taamtGbnq1oiql+JH3LCim4cZFiY4XRHHaHawBXFyYF8jbj12FjJEKVAUOj7IAbvdNHbpE/HHeq09VUG0bCEv1I38N203wFNpbCkTPG6cfWITwvkS3Mw9zmkZzfoP8CGyrgLYJQy7Zlx13YAqpOTw4lpGrvYDqzKDCGA8IhRzn1j+lA2jOZzldwxAPxLwQmdQFwgU3livQfBIIu5pA5RsRM0pYnfqKyGMpUGH6Gq1oMaJmCpNklBhKfdmmIgpWZFHVyJ0VmmTgvafjh6/lD/JY3PJGMv6wZb+dmZeJXPoaeRt+E53Cqi5Rh2Yk5LFYz6XXBZcKRyPg7FgcyAymEIantffzcPpyZVvAhYWBNDaUuc1sEnT3M40OGNuMVNuvoG8eutWfWumTX+vzGbMoSkmVgR/z1L//tX/vXv/yPdkUaJSImfv51vJuxTcU0jZnj3/mCL1D5KoUf7zenkM/S3WcfoRlCbJitoT08IRh1+CGUXtONJ2SSM/0Gdgl6dkAHTEZX7IGEKL/uV5rUugYm14rdYMzWo3Lgxs1YsuRyyPYxA0i8VVUA9r5sx6uPyVxNpBuZv/mWpA4q8Kfo1Nkmb9BFps1blI6XA5B0TZk/aqiwPvvHZOp60JfHrrxpyqPrZH7gx2VYAXvp0HPHse4a3rHD+NpsrK9lY31d2IoE4eaNBiQNPuiezs5XGWel4iRO523vJ6OnbRMxSVYRNBC6xYaRde+YuKBpFR0k28BwBHzAhrvM3cUOeYt/XqC7tFi18o5X0orMuAZxJR+6goo4uFv8WPonEFKLsJggbfztGKxyMHPnB98AmT+HU3W2ZNdxDYjyZI2U2hwdv+XfYGO8E5JmJEEj1Ya7d6gTxDRtoOqy9ZjvSE8zGYw2A4vMXwRtX+g8Ie3IeniiigCiLs6G59yjnGAzu5IWjU9ZIQ9x8j4oVAl1cQzexP08PTtboePZcbBo2pgSHZmozcyjX+gXGcPEwLexqY18wfl8OsMyfgzPhO3ujmiFn70VWw9NtDwb3qChvN8Sz1b4HB39wTl5+8YsGFWLaePmnM7H5wKIhCp6OMir0TuE61EQQIAeqzSCSz2KDElfGE0fMI6jGo+qqNU1JkDCYhn/qdGvsy+Q4zThYSbWeTYfM6GTBpMpIRKB1jnWDtChX3A27IQLkEWxGi9XYgPzMDDP+HH0n4QsACOyWZ7gnsgm9uMrdlhGqLj5DCKErnzBacSEAdP/7whWjqQCH89HI8jDdJbU4VAgxQ5FRaqKOOpUg8w2uUmBWCjxoj0UaoD/Ca86D9Bim64aICaAWKSRtp92A4fXCC8RqOR4BVnF93EcwTZTuxd7GqaTVuQRKNUeZmHXWGGUjd98FsOVnvZHK34gMAAXhZ+tBv7mLx8tPmD5TDNzfw0YLmTE6lyynh136hxgIQ7WC86mR3FY6U9y0ErPS3gdOjK6tdTrvyhSvBAUT3uv72S44WdnT8N2Y8/wMKVY4TziEVP158TR/LNfiw6sp3w5AghwCZAdWzA+hcYGOOkd+XZ5+HI5dZGtz7oi8UeTyRTG3nC+D71FTlwWuao3YsDwImI9ANON4lPHM/wyPWLFp5WocCwtixrlixfOzfniC7Ez8G/TcSHkl2u27wVIRExAUvY7xsOGn9fAGVgNYtxrTdwKjwAKgb5BWhDTdGsICjm1ICAa6aRex7vHYO5O5uXnB6exGNyk7xQl3IBO67w7srxQjCCX0OgMztzCyR/ab+RRh8c/ORL/UkywcDwaIvkbyXtsHYjQO9tu+BfgYJRoU8FU850XEg7G43xLGDiVoXKP3e2jCLdmf87lcjn75uBgtkLTMqVoQ+bc9cHNQro5unXvgA2DPxT/czH7JzBmj89e/kN5gCvsxbeLxfn74nAGK/OiiO2Lv96Mr4t4oVb7jc5PND5zJOZf8XJ5nSidVDTsxxGaoIsFyn2PBFTNOTkH4h2GyciKDJgsvpt9BTAXw8HF8QjDwZWu5DAckps/KF/mOQBvOG5q0CeeTXF6w0MU4EntZpol8afBVuZD4mGreAIoJJXlQBptO2x32UEYuTH4VN6dvBqWn2TEXDBwYnwL/A7+KirQp6wlx3E2XSHQ6oSeBqROLFYmK5i8f9OUlmay8lP69ZtiLywZgr0e4lPHU7RkiaWymORhTUG1OZ8AWoJhaP5NPB00lCutLW2oiEbjG15trtWQgJl4s1SIzAHYC015G1UHaCQTRBb1LoEDr0eIh0fm3hiEUq5PqIg4Je1dzrCZK+78kfToZAZsaOZQkC4xCnEiBFLyrcfw/IY+fDrRCs6oOcqqCDWzJDykosgG1pMFAFP9PmN9qZlbdNLNXZtgwouxqiJjuo+oEw5MHRUdRo2GXiFx4CoIXFO1waPAh6vrJX8O2QX6xPjtceHWoW1EJgNj2IYbI2ZhuhlsTlCIRBBxNOFc/c7RghYvExfCIXrzPVxIP3wfcgH8TGxG+szj9BZk64s5LklwjolFHZn25npAhN2lQlwnFEJyWdCJSZwQ6faNiKvQFFhQgeC3pptTKxjgDMWVqaJpNrI8wPBOSI2e1hmYtugm10qR8wichYQIRMI+SGsk6fwNu4ElfqZM4Wr83hQeQtMdjhHKXUyR/t29QuQchyrz4NXe8LO51MYU9N4fL3xTm9QR2i6RkUvRkbbHRVUSUATmpSE1RD1ftpYHMq46CrrLTzvTzw7dnP3t6JduwuYRnUgxwecy7x1ZWTobVCsMTAcu0fUQm0XSuKAtAoEKus4z2hHMG1Slphow915wlZT4wBp74XlvFNFgmKeUTUKTI2N3fKfqPK5ONpshYnwcrLE8Dmh3SPcYo1XeyuFdtNvb/JhmgHzhGPfMnmE3FqxaQsegFMUaAgujKiTN1L1x/vXk6OfXx93v/y3+Pds/UZyoG3TEFl1ddlwQprCqKfPJqb7f7j8g4gJunJecX5GuIw2qTOzPTANOUgOkQDHahfTTTkzoNmxfOi7CqLpkfjT55JCporYp/H98/pnzz7++eYnUl7Ou2mOFb/svf4Wr9+Xvfin2P4B4iEd0DwGPIlNwijE5KgY3fuX8isCj2CcF5zenzyI0/MaI/m/Ob7+9cPz3gUQHp//ZCiYWcgdGW740dvdWb9ZTzMfQtnxz7O7Nb07GSF/BgL2GnleLXsto/X7rkfRxWe43o3eAA2GbDdCr1NACvIYZh/jrlma/e35DNS7WUezWdBH8ZMnjUL6zJS9poTT7oiY9YVs/k9EYvrprXmisImGt57DUxTgg2p801GMGOhEcouj2wMoaHE4ta45DDJRPxkA10B/Wk1B3I8Xt0iCRdThNNJqTkV4NqKEi/j3V4BMzhIB33KmUajuTQgugF2oLz2DyBYJQfszVOJZQn+eWyQ1/1o8J4hsMbujnKP6UkSdm4e5wjljE7Fbnw1jdYqj71CD0UNX5yAfpFETZjsXs5wpHMJ34whWS/vnzMsr5hXs0zLPt6xzGeeDQKJWAWvWPYZif3/gu6/0t87RYzsMLtxxzxiU2yZGpzmjcxrbaZeGL/jpTe1dG9fnNonhyzXLsTFhPS2L9dcm2tTdafrq6r5jdnd/444vlFscNDTHIHmASH0yx/IafZpkWe5rLzseb38S8icAWLJ4wtyyXXabDyG0P5h1L++pqeht66tYjym0n5h1R7IF5B5QwINOF5idpWYLyFd+UJEcTS9RXArIFDKpAkFMSK+OQMZKuXIPQ5oCLCXEAuCZ+i/CB4LqPiRgjhKk2DTGwHeHDGhYFwL43OPjeKBki3H9fGNn+juE74WMW6ObuMbvmSYDcaxJt/4bg7HAUw2i6PIj2d5+2peHLyg1dP7NtuTG6E/h+fyOgdepWuIcxXasdPC2EOoHEXm/Ak+8J9KY/fl2yj2R2CuhiAv9J/GGNibYG2vvu3ijeJ+vqAiGtWHhc8ok+WVcIxYdDgIaQ/08rZMAAO91V9BHlBndBOdbmbj42gBcB71IVbiK/JjNs+MkEeT4myLMX3a3huXXC/WgI3eFahC5XGPwPLN1n/PUTChcPBf/doXC54qUWg8tXyhw357SCWELiLuoJE7kO/VilJrI0Q/gmS/BHW2BuEp0MvZIljMITYetRiPsHhU02yYzZCHGKkUgtFWN4QRKdNm6Q/CHbwgj1R+B2lxIn7i2b7D1knpWJYJiQ6wuw7qA9EzIkTZV0CrdI7N3IcfNfXmSnHptk4H+e6T8h5TyWnCWo4nmNdIo5kt5MlrewdGlQMJw3buqui87301skGeJqfDUMHi0GR/I4GUNtkRK/Fvmpfk0QJllp8s9Hp2g+IPlkAduXwzTGWDK5PgCYrZ/zYbNQ9YVBplusDRR+DM2w8IWaVBJUIkmobUPW1x2MLmHn9nmNJNdWorfgBLtymYUL+QHdjN+sivKCQA4bCGjVYeoSMhNXBNRUqe01B/RvNoktku0Z1y25sLGEhEYcAq51pE9ZtoAqcM2cZZMAydwGpGuzQlnzy/OqP1yXlX33vBeSodJietquHylNLuI6A2wHeXrgvV+bp3dPXCXj8ahhgd40rLGbsqLi5+w67CTnKRmy8aMp8LaXYLTeWGrtAo8WxGBI7+IvZ36jv4c+JcMe9aeD8t4H5Z2cvF0claEXS3sZSV5H0FKzPv3EUVC3B0mkyj/H+HjEQqIBaQsrQo+FKYM6iqcLpSRVXH4c5QkcV5YHLlRL4Z9YRdjzT2jitwYCMpfmMUILkqdzXxRgrwk62+RmP2iKjh3IU0rSsWN6Omk6yUxvLeVELaiWcsI0SKnr3lzCafjC4RVfQGejMRNSUtbl5LCIG1UfvNh3rcj3Q+YU0xrNkHWEqhLPQFiJKAfMqNdvE9zix6znNFSafDq9sNEHyXmwL0phkjZenTj2PhVS76joD04foYVDNPVR7zr/OGOyMOscpGUcaNxIDJ7OU22RCJuibTjX4F/D5R4jLZgYBpzxw9fH4PkUyjZDHjX6gNoVqRhdSRsneqegeSHPi/0g831SMi7xDFBFhR0kutaRmWZzuiVmD1S3WgmlJGlrFXiYNMNPtLx7vjDGPsCqx8u/u6G1tWW2pgA8+BNXE78EjBFgF+n13muzRHcx+NR6b9rHIFENpHTTl2AZZnQa5+wjm/iSV6Ole9ILkCq/QEazIyN12i+Yo4ikyZcFIxyGWRq1Ix9Ij8YKmZe/hutlSNVVNDrZoEyhtMnx7GL8wTxD0wB/+w3pmXA25iP3h1PheuHjom9z3NGfnAJ58wuJtntfMNNPmN55D/a9onehD/z8s9+Fvvalw4IpSy7mV4WQMcPUEYUu1hs1o/TP9kXu21dmeGv8LaO5+HxXzEjXVuHmY1r80fRKkPJDfUKMEdd/nnRhgGqKj4bTvuFO6ayk0xeGBZM0FdAFpnic04vGR6LYQSNjC+EMMCg1PYZURrshsT8duX6g7NFYnLQQ1tKW8encmoJlKFsGT1lMyR6aZNjHvyONBKBiLOhnKDzhVwdEsKbvpnA+heo5tTuelMkb6mc5EoTpQlgsguNAVWEwuUqStLRtCZQ27yO5gW8v0WYCKhpTow3xYNsJO6GQfxEIBaorEzQSdRkZqOkwAfaBuZSfrhYk9/owu5oKTS0m7XQ+RRkqkGh5eHwXr2F/ftxUdsGmllqpEQ708pAo/GArz+XAFO+EV8oSh8goEqJrKa+1+Fbr1YMlUYYTSBmbhGbRjOxvFhI63JS8WC4nnFFWZa2bpOwaNz2LAd4opaB7CmqOxOb3e9WRXFI4X8gwxkZ0OdhfmTnDwicr/MpOiiPlfHWGWiSBNmFyCFeMJYlMe26obCraKsTuBWWhE3YLa8rE+qtwR/n1TGSGDYjDGKqI89HC1hldgyYCe9EESHRP4ucxaj8lWgEKCR9uwq7DZ92MNSrj72MeuXZAmLMp78NAYn1daDTZadIpClhH/TnSjspCZImhK7ct+MDCBhXfJmxhl6DgP2UDhND8ZHNc4hQq9k6UB6wDGXg9M4nouT1nQwg/dAKysofmKUsd8A7oyiR8BLogTOXrt9ZAsrIZMofIQxol243QICd2XTLhBM31/J2HY+rcv98/DCUxSJrZyBloKRjgCWy7Mo/LIXeHtcnPqvZQC+RD9y7JflD/CKqKl2eLG8f+7ErvgJcX49HV+eJl4S3Ia1+AYt+RQp3CBla3PwNbH82U2S3XXvOHk0m/FqZJS59xN6HO/aemVvs/sS+PRDp2PA2SmZKp+AIaGG4cS+nOsv94DZiU/mc+R0rjf/cLbhJnSYgD7PGtvo3tZpE9FN+tcpVZGxASonioTBK5W/yW4skcwYvWvgJC6Zi5HaWVBYPasLH4oYvIl37/w9FxCUMHPiRFuL+SKAHtJDgN6R9h/ujgi5XnPvurXBm9UwDPQa7EEAWChOB6gsaeYCGaLNEXdwSO1lz336F20XCWFZVl4JdraW0yJynZwaufXvfc/qDe6ferXbdaa1bcarvZdZulTsutdbxGtdsu98uNsvCTsS6W71+AGO329jbMiyY01vPlgTVpD6zu/wVO8QzEVWhcmuv7Pv/sr3/5r3t8pH1v8Hn1bq/ZLfcrbqNVrrnVhld3m+VGyR1Uy4NypzsYtGo983nmveh0+i6N9e2g7JVqB14lYIFzPZe0qOfT24kk6rijDyaPgk3T54tUWredfl25Vqt0m/U+vq5ZcquDRsvttAZdt13v9rvtWnlQ73bXjeL9aXgZ358elLxKpd6q27Vez4GXSO23spZchtag3uh1amXMe70FUSsN3E6923R77X6z16q1GvV+I22g6Y7dPeYw1E/P3wLN/qBS7zV6bq3VqbvVbqPqdkrtNjZDq1dqtjv1cs2LysgaYsAtJqVa8Vq9frnptgd8b6fcc9vNftttV5q9Sh07sd6vm/cevTl865xUYqCH6+x4Jsr9ascblNtuqdRrutV6teW2u/Wu22oNmq1uv1XxynZESZR7p5Lt9QfNSr/dcMuNBnST53UgMK2G61U7vW6r1MDfKg+/v7xmtdPAGzGANrRHu9rBCpW7rtdttUrlXq3p9dburzvJCbB3APuBCsMK1HqNasNtlJsDt1ryWm6r3sOYWp1qpdKHHqvbRdnpEvQG2Am1Qcft9noQzyYEtdmqQix6Xt+r1Ut9LNHDL0Gz2SlX66WK228OoDm8XtntNCtNt1yuDWqDQbXpDQYyiv/7Xz2foLNS1JrX9BqNuL9xj+m6AWsaTGth1vIVCeak0S9jBw9qFajjer3mtjBOt9bstJvYQ7Vau5Zfy+3+MNxiLv/vf3Me2DsVuSZ3Va9adqH6oH/LLYh6p+S5tUGl3cd51q17207grg0KaJ/6YNDrYI0HZbda6cNe6jYqrldrd3GawZiqqmLCi/GfvRk+24wz9yB3a5o1Gq2B14JCaQ76brVfxs4ulerQsJVapVdqDQa1B9FruXcBraDc+0CXO/+K71ZV5xXKBz61W9WB161VB9gNMLmrfaxm0yvj1C43vU6r3yt5XiviTKQp67LnVRvV+g6VdYo9mn+gO12oTqVEK73jljs8wXt1nKleq+4OMDGdZqPd7VRUe4Qs8MRplj5B6aayf4jQbUt5Eo7FSm3TRa1GrVXZeFGp3ixz2Lk3zT1mFnGmJfz9wEiq1eueh1G6fXh7MJJw6sKG91yvXsNMl+rdQdn6eb6ZsBsrvlGD01QdVNxapdLAknbK9DDxP712F9rN67e7bSPze/KBO7Vmu9aoeW67VKMtXSu5nV4TY20NKp1at1Evd/ZqllTgG3rlLkbVgF9YbXcrbrvt1dxmDd5oCZ5QvZ7qHH63063Zqpe8QR8mW6XdwJnUamIJu7B5ax2sbr9Trlc7ehztwzC66+B2O0Xw0gf1UhXqqu1hk9UbA4h6feB2W7VKs1Qrl5rdvQpSqQ8Mq1fvufAZId4eNEGr5TWgYjuNQbUBr3qwe3fNxPxDkJPXb1YasFqb5RacxlK77La6rZ6LofU7nXqzUe4qopZTkOjYwK54FOVxc/2LtEsKfU2z1iiXB223hSMK9m4LaEAJFjqAkVa/0a214O88BDjSKnWAAFT7brfTgp3tlarYjgPgAcAhqt6g0m+mAwBZx+B9fL0nMymDJjT5oA+kqlbuuNUqPOI2jl24S+UewJl6r9NL1VG7VQNPYxTlLsbh9dpuuVrBNgM667YApHFC2qVSq4Z9pyf+Fvr6cYykB7Vsuz2v0ax0Wy4wsipAxVYbeEm55ML4brQbjY5X9taKSIqNWGqUaP6ZHZRyAVRrGabfTo/jAU79UrvvuV2vg+WtwTvEmgIhrTRaXq/Tb3id6j6xFGg+nCo9ejkwEqoIJrjYm9CQ3V6n1a3D3GzvQC+arAvS8/soUxXofqlfAu5Qa+CUA+bgNqt4c7/faVUBl7YAURqNvB8Up9Hr1qtN2AU14LbQUj2YCe162a2XG1i8Zrs8aO4AT0+bGuBHpWoDnhVgURgotTZ0QgeGb6NRanQr1Wql19sizHAvib7X8Ha8l3LL6p4cFIhtp1dBxKVRKeFYE9u/BK+4jm3UqXYGJfjFz2Kv72v+ysBHO4ypVbvQlQM4v22PBmiz7PUaiF5VTABrH65LbkUu2Nk9zpC0HVerDRAJhQGJKA50c782QECrC6XUb5e6lfqg0mqoln7SM7NzRdTrDWrdHqHXWhU4ewPnasurAcSGGHWAxjbrvdI2G26nJ39uC+Ze0zL6sPxlMb1asZPCIuy8lSr9Ug3zUe7iBGk1PJgdFUTUu4Dm6u1eu9m2OFIgMgeLxfK7/xcAAAD//wMAUEsDBBQABgAIAAAAIQCP62UapQIAAEwIAAAeAAAAeGwvcXVlcnlUYWJsZXMvcXVlcnlUYWJsZTEueG1sjFXLbtswELwX6D8sdG9sKe8iTuDacWMgCIwkTXoraGktEaFIlQ83/vuuJEtxQDPt0bue0exyhry4ei0FrFEbruQoig+GEaBMVcZlPop+PM6+nEVgLJMZE0riKNqgia4uP3+6+O1Qbx7ZUiAQhTSjqLC2+joYmLTAkpkDVaGkzkrpkln6qfOBqTSyzBSIthSDZDg8GZSMywgkK4n7+tWilkxMmWW/4giWLH3JtXIyu8eVRlOMIpKXKikxtaR3npHiCJizatZ8pSmcUKWqxObOlUvUbYPUEbIpf1M621OeKWm9/y6YrRV59bHguSxxD+KZZ7a4QZ4Xu2yXO9vajgISX22tNzmKdtszjiIzNKSTlpqHfhN4O3a7s/k0AlufwkQJV25XMvAYG1DSLfq74xkKLhH2wJMoAD/s4PfcvMBCqxUX6H39MAQ/6uBTNKnmVX2CHvoohD7u0E9OSNRsyQW3G5hykzpTm9ejOg5RkUHa3U2UXPHcaVZLgQXT5EI6b4/pJMR0ujMS15jBExPOX8lpCB+Te1spU1wxJ2yA4CxEQP7YEswNZGgaEetaBNgCqdKwXnkTnQcV9R4ZNxGDx03lDxQPQ3i6LdqBxsagMXVGwBm6TOAZlzARnAq+YeMQ3XlHd4c5ndIaYeZkI4wJmJcVS/ewBQ1Msrfq7rHEjLfn/m95QUvH/frXD1WBGmG8mPvjBU0d95m4fvjJ4aFAIWCiypLuW3jboM8Y9Hbc52QP487UPmXQ5HGfl/Xkdv5f8sKG7xPzjutDYUHzx73ZFuoP6v8VF/Z+7zaP7yOBSTAM1OnSUD+TVoHBLg43ytjgHZoEA0Gd3sIrMpxM/XQmwQBQp7v6CiZzhFuVe06gO4UegMHb+94+Se9K20fsXe3yLwAAAP//AwBQSwMEFAAGAAgAAAAhAPzFE/6/AAAANAEAAB8AAAB4bC90YWJsZXMvX3JlbHMvdGFibGUxLnhtbC5yZWxzhI/NCsIwEITvgu8Q9m7SehCRpr2I4FXqA6zp9gfbJGaj2Lc3xwqCt9kd9pudonpPo3hR4MFZDbnMQJA1rhlsp+FanzZ7EBzRNjg6SxpmYqjK9aq40IgxHXE/eBaJYllDH6M/KMWmpwlZOk82Oa0LE8Y0hk55NHfsSG2zbKfCkgHlF1OcGw3h3OQg6tmn5P9s17aDoaMzz4ls/BGhHk8Kc423kRIVQ0dRg5SLNS90LtPvoMpCfXUtPwAAAP//AwBQSwMEFAAGAAgAAAAhACmidYbPAwAAYg0AABQAAAB4bC90YWJsZXMvdGFibGUxLnhtbJRXUXPaOBB+v5n7Dxq/X8GkSdpOSIaD0mOathlI23tjFLwGTWWJWjIJ//7WBtta3yalL8yw0ur79Gn1aX1185RpsYPcKWuGUfyqHwkwK5sosx5GX++nf72JhPPSJFJbA8NoDy66uf7zjysvHzQIzDZuGG28377r9dxqA5l0r+wWDI6kNs+kx7/5uue2OcjEbQB8pnuDfv+il0llIqEShI2EkRmufl8uiv8S5bZa7j+TYA7pMBrF775f4owK/n6/xaSfBeT7KhPD1kvt5vZxsbGPuKF+dH0lC2+nSnvIRbhE7/qwh7HVRWacWNnC+GE0OMOUavXDQE2wMAqBDoyWVkPysPz7y5ePn0bzj8ua/mwSiZbNVIFOZoftJdLLyVNa/hvEEYWuEAaRCBHulddQL/uhUAloZUA8A4DJIUCfAzijAHPlfiyf+v3ztPwd9Jd3uU1Vi1mOiybGbArXCzDjtxzma2ZTzVlPwK1ytfVYeKxqmBwCvOEAzinAt0IbyOWD0srvye4myq0KVxZ5LSqZK8JxZrOIE3K55LhcUC5ja1K1LnJZbpAqLXOsayzHmgqZKu7aYYYJooRMLjgml5QJCq1ySAiHb1IXTX0dJ4hjkEHFFUPUcw4VjSKs4AmkstD+JdRqwguocekE7cWJX3OwbynszNFz77I4S9vb6kRykEbsSjmE3wBGKlY3bEmiNxBCZxyhGC00FGK8kWYNhFXpW/Xhj1ZlgYhDiDMPernjAYuJSoWYI+fAuQwM1R/v8wqSIm/Bm3micGj54js8iLFWmMgKgGccHgjrZMiQcPkMa7wDO6rAtDDVvqUmwtSi1DminSdm2VaueFpYAyEt1v/irgFCBolibmdXo3k78TSRygII6LDOiLVMRNotthvIqUaju1mtx3FYVCGuSGhhsl4Zd8zy/eJfRcRfbEDT4xjbLMOnf/lU8yhzRDVPHMdEW2r820dtnHXOuGOdv8MMtT60Dgy14OR4btTWWS+NO2a6G9/OiGqNRsFbetSkOT3MOVkvavCs08Ydq2U5de/+UQyW0y+Fov7P+jB2AKSi7+wj5CeKVZOqc04Wi/oRa8eDjh2fzAtFeZbXLwWjjsR6NvaB1LOx9SWV5S3564CaeeGaJ6TsxL0VOOPoUP9Y51/s3kpVQstkW9KOkc8hRYsyq+b1CAKMJz3T53ZsmHkdb+266Yyqt1NUEQ6Cvo2V7/eC3t0dO/mF32uYmdSGnxlV8BO+AUWGyzj8Xpiq3PlD119+OVSxW/m/UPl14bFzBfzwwTMsMw9JTTQgcv0fAAAA//8DAFBLAwQUAAYACAAAACEA+eW9EOwBAAAGAwAAEgAAAHhsL2Nvbm5lY3Rpb25zLnhtbGxS227iMBB9X2n/IfIDb4m5hctCqBISWiRaKi7tIzKJA9bGdtY22aLV/vuOS0OptC+W54znnJkzHt+98cKpqNJMigC1vCZyqEhlxsQhQNvNzB0gRxsiMlJIQQN0phrdTb5/G6dSCJoaKNMOcAgdoKMx5Q+MdXqknGhPllRAJpeKEwOhOmBdKkoyfaTU8AK3m80e5oQJNLmhc1gGjSDnJ6VlWLAKRCEShMNlehVFjjmXgPjIUTRXFDizl3oMADkTqwtO9gW9ZjowDaloTAyxtJNxtn9WzucsAXpWsmIZVcEjS5XUMjfeMs9ZSr0F03BfJHHkgU8jy+Gs5UmlNGj8OkkzupyjsCwLlhJrzZPtOnlLaYFAhHPwMUCNwowW8/WmcYASuL/Mk9f77Ty28Z9B1I5bszB2/Xg2dLv9VuiGLX/o9pOoMxhG/WnYC//aKnxbVlM+hY/JO01vMPPboR+58bSfuF3fb7pRFLZdv9PrdsO41Y87wwuN7aQuq2lek8iy2H1qWCgsvuK/XX729JEoWkomjAfj4BIMl4IUmOeyoOcdvIL8zqYeiMqogF90fwI3Nd5Vhu32TLy3bjU/NGrJ9Ta6QbB98RVaLZeb2XIRJ6vaN/wfqPb1avfm45fgyRh/rvlLoCf/AAAA//8DAFBLAwQUAAYACAAAACEA3kEW2YoBAAARAwAAEAAIAWRvY1Byb3BzL2FwcC54bWwgogQBKKAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACckkFv2zAMhe8D9h8M3Rs53ToMgaxiSDf0sGIBknZnTaZjobIkiKyR7NePttHUaXvajeR7ePpESV0fOl/0kNHFUInlohQFBBtrF/aVuN/9uPgqCiQTauNjgEocAcW1/vhBbXJMkMkBFhwRsBItUVpJibaFzuCC5cBKE3NniNu8l7FpnIWbaJ86CCQvy/KLhANBqKG+SKdAMSWuevrf0DragQ8fdsfEwFp9S8k7a4hvqe+czRFjQ8X3gwWv5FxUTLcF+5QdHXWp5LxVW2s8rDlYN8YjKPkyULdghqVtjMuoVU+rHizFXKD7y2u7FMUfgzDgVKI32ZlAjDXYpmasfULK+nfMj9gCECrJhmk4lnPvvHaf9XI0cHFuHAImEBbOEXeOPOCvZmMyvUO8nBOPDBPvhLMd+KYz53zjlfmkV9nr2CUTjiycqp8uPOJ92sUbQ/C8zvOh2rYmQ80vcFr3aaBueZPZDyHr1oQ91M+et8Lw+A/TD9fLq0X5qeR3nc2UfPnL+h8AAAD//wMAUEsDBBQABgAIAAAAIQDnU/L2QgEAAGUCAAARAAgBZG9jUHJvcHMvY29yZS54bWwgogQBKKAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACMkstOwzAURPdI/EPkfeI8VB5WkkqAuqISEkUgdpZ921qNH7INaf8eJ2lDqrJg6Tvj45krl/O9bKJvsE5oVaEsSVEEimku1KZCb6tFfIci56nitNEKKnQAh+b19VXJDGHawovVBqwX4KJAUo4wU6Gt94Zg7NgWJHVJcKggrrWV1Iej3WBD2Y5uAOdpeoMleMqpp7gDxmYkoiOSsxFpvmzTAzjD0IAE5R3Okgz/ej1Y6f680CsTpxT+YEKnY9wpm7NBHN17J0Zj27ZJW/QxQv4MfyyfX/uqsVDdrhiguuSMMAvUa1svxQ6ihW7gUOLJuFthQ51fhm2vBfCHw5nzUg3MvsIABh6FUGSocFLei8en1QLVeZrN4nQW57erbEbSe5IXn93jZ/e7kMNAHiP8m5ilpJgST4C6xBcfo/4BAAD//wMAUEsDBBQABgAIAAAAIQDvkv7xQAEAAHwFAAAQAAAAeGwvY2FsY0NoYWluLnhtbGTU3UqEQBiA4fOgexjmvNz53Yp19yAIOq8LGNxpFXRcHIm6+6ZopXxPBF/l8xnR2R0+hl68xyl3Y6qlut1IEVMzHrt0quXry9PNnRR5DukY+jHFWn7GLA/766tdE/rmsQ1dEmVCyrVs5/n8UFW5aeMQ8u14jqlceRunIczldDpV+TzFcMxtjPPQV3qz8dVQBsj9rhFTLZ+1FF0xSNF/H6tLNr95CXYd3Dr4ddiuQ1nWz7OWoffroMqr+H+LKrRVuZCXMQpaBa6CVwGsIFYgK5g1zBpmDbOGWcOsYdYwa5g1zBpmA7OB2cBsYDYwG5gNzAZmA7OB2cJsYbYwW37JMFuYLcwWZguzhdnB7GB2MDuYHX8/mB3MDmYHs4PZw+xh9jB7mD3MnnsGzB5mD7OHeQvz9o+5WvbN/RcAAAD//wMAUEsBAi0AFAAGAAgAAAAhAHLMUpCkAQAA1wYAABMAAAAAAAAAAAAAAAAAAAAAAFtDb250ZW50X1R5cGVzXS54bWxQSwECLQAUAAYACAAAACEA5PklUwYBAADcAgAACwAAAAAAAAAAAAAAAADdAwAAX3JlbHMvLnJlbHNQSwECLQAUAAYACAAAACEAUPmfbhMBAADIAwAAGgAAAAAAAAAAAAAAAAAUBwAAeGwvX3JlbHMvd29ya2Jvb2sueG1sLnJlbHNQSwECLQAUAAYACAAAACEAH5G1vY0CAAAtBQAADwAAAAAAAAAAAAAAAABnCQAAeGwvd29ya2Jvb2sueG1sUEsBAi0AFAAGAAgAAAAhAKlMB8qqAwAAlBQAAA0AAAAAAAAAAAAAAAAAIQwAAHhsL3N0eWxlcy54bWxQSwECLQAUAAYACAAAACEAqJz1ALwAAAAlAQAAIwAAAAAAAAAAAAAAAAD2DwAAeGwvd29ya3NoZWV0cy9fcmVscy9zaGVldDEueG1sLnJlbHNQSwECLQAUAAYACAAAACEAi4JuWJMGAACOGgAAEwAAAAAAAAAAAAAAAADzEAAAeGwvdGhlbWUvdGhlbWUxLnhtbFBLAQItABQABgAIAAAAIQAjT65YgQ0AAAhOAAAWAAAAAAAAAAAAAAAAALcXAABkb2NQcm9wcy90aHVtYm5haWwud21mUEsBAi0AFAAGAAgAAAAhAOCagoCLJAAAewABABgAAAAAAAAAAAAAAAAAbCUAAHhsL3dvcmtzaGVldHMvc2hlZXQxLnhtbFBLAQItABQABgAIAAAAIQCfbqojp2kAACvTAQAUAAAAAAAAAAAAAAAAAC1KAAB4bC9zaGFyZWRTdHJpbmdzLnhtbFBLAQItABQABgAIAAAAIQCP62UapQIAAEwIAAAeAAAAAAAAAAAAAAAAAAa0AAB4bC9xdWVyeVRhYmxlcy9xdWVyeVRhYmxlMS54bWxQSwECLQAUAAYACAAAACEA/MUT/r8AAAA0AQAAHwAAAAAAAAAAAAAAAADntgAAeGwvdGFibGVzL19yZWxzL3RhYmxlMS54bWwucmVsc1BLAQItABQABgAIAAAAIQAponWGzwMAAGINAAAUAAAAAAAAAAAAAAAAAOO3AAB4bC90YWJsZXMvdGFibGUxLnhtbFBLAQItABQABgAIAAAAIQD55b0Q7AEAAAYDAAASAAAAAAAAAAAAAAAAAOS7AAB4bC9jb25uZWN0aW9ucy54bWxQSwECLQAUAAYACAAAACEA3kEW2YoBAAARAwAAEAAAAAAAAAAAAAAAAAAAvgAAZG9jUHJvcHMvYXBwLnhtbFBLAQItABQABgAIAAAAIQDnU/L2QgEAAGUCAAARAAAAAAAAAAAAAAAAAMDAAABkb2NQcm9wcy9jb3JlLnhtbFBLAQItABQABgAIAAAAIQDvkv7xQAEAAHwFAAAQAAAAAAAAAAAAAAAAADnDAAB4bC9jYWxjQ2hhaW4ueG1sUEsFBgAAAAARABEAbgQAAKfEAAAAAA==" - -Function Import-Xls { - -<# -.SYNOPSIS -Import an Excel file. - -.DESCRIPTION -Import an excel file. Since Excel files can have multiple worksheets, you can specify the worksheet you want to import. You can specify it by number (1, 2, 3) or by name (Sheet1, Sheet2, Sheet3). Imports Worksheet 1 by default. - -.PARAMETER Path -Specifies the path to the Excel file to import. You can also pipe a path to Import-Xls. - -.PARAMETER Worksheet -Specifies the worksheet to import in the Excel file. You can specify it by name or by number. The default is 1. -Note: Charts don't count as worksheets, so they don't affect the Worksheet numbers. - -.INPUTS -System.String - -.OUTPUTS -Object - -.EXAMPLE -".\employees.xlsx" | Import-Xls -Worksheet 1 -Import Worksheet 1 from employees.xlsx - -.EXAMPLE -".\employees.xlsx" | Import-Xls -Worksheet "Sheet2" -Import Worksheet "Sheet2" from employees.xlsx - -.EXAMPLE -".\deptA.xslx", ".\deptB.xlsx" | Import-Xls -Worksheet 3 -Import Worksheet 3 from deptA.xlsx and deptB.xlsx. -Make sure that the worksheets have the same headers, or have some headers in common, or that it works the way you expect. - -.EXAMPLE -Get-ChildItem *.xlsx | Import-Xls -Worksheet "Employees" -Import Worksheet "Employees" from all .xlsx files in the current directory. -Make sure that the worksheets have the same headers, or have some headers in common, or that it works the way you expect. - -.LINK -Import-Xls -http://gallery.technet.microsoft.com/scriptcenter/17bcabe7-322a-43d3-9a27-f3f96618c74b -Export-Xls -http://gallery.technet.microsoft.com/scriptcenter/d41565f1-37ef-43cb-9462-a08cd5a610e2 -Import-Csv -Export-Csv - -.NOTES -Author: Francis de la Cerna -Created: 2011-03-27 -Modified: 2011-04-09 -#Requires –Version 2.0 - -#> - - [CmdletBinding(SupportsShouldProcess = $true)] - Param ( - [parameter( - mandatory = $true, - position = 1, - ValueFromPipeline = $true, - ValueFromPipelineByPropertyName = $true)] - [String[]]$Path, - [parameter(mandatory = $false)] - $Worksheet = 1, - [parameter(mandatory = $false)] - [switch]$Force - ) - - Begin { - function GetTempFileName($extension) { - $temp = [io.path]::GetTempFileName(); - $params = @{ - Path = $temp; - Destination = $temp + $extension; - Confirm = $false; - Verbose = $VerbosePreference; - } - Move-Item @params; - $temp += $extension; - return $temp; - } - - # since an extension like .xls can have multiple formats, this - # will need to be changed - # - $xlFileFormats = @{ - # single worksheet formats - '.csv' = 6; # 6, 22, 23, 24 - '.dbf' = 11; # 7, 8, 11 - '.dif' = 9; # - '.prn' = 36; # - '.slk' = 2; # 2, 10 - '.wk1' = 31; # 5, 30, 31 - '.wk3' = 32; # 15, 32 - '.wk4' = 38; # - '.wks' = 4; # - '.xlw' = 35; # - - # multiple worksheet formats - '.xls' = -4143; # -4143, 1, 16, 18, 29, 33, 39, 43 - '.xlsb' = 50; # - '.xlsm' = 52; # - '.xlsx' = 51; # - '.xml' = 46; # - '.ods' = 60; # - } - - $xl = New-Object -ComObject Excel.Application; - $xl.DisplayAlerts = $false; - $xl.Visible = $false; - } - - Process { - $Path | ForEach-Object { - - if ($Force -or $psCmdlet.ShouldProcess($_)) { - - $fileExist = Test-Path $_ - - if (-not $fileExist) { - Write-Error "Error: $_ does not exist" -Category ResourceUnavailable; - } else { - # create temporary .csv file from excel file and import .csv - # - $_ = (Resolve-Path $_).toString(); - $wb = $xl.Workbooks.Add($_); - if ($?) { - $csvTemp = GetTempFileName(".csv"); - $ws = $wb.Worksheets.Item($Worksheet); - $ws.SaveAs($csvTemp, $xlFileFormats[".csv"]); - $wb.Close($false); - Remove-Variable -Name ('ws', 'wb') -Confirm:$false; - Import-Csv $csvTemp; - Remove-Item $csvTemp -Confirm:$false -Verbose:$VerbosePreference; - } - } - } - } - } - - End { - $xl.Quit(); - Remove-Variable -name xl -Confirm:$false; - [gc]::Collect(); - } -} -Function Show-HardeningGuide { - $Global:XLS = Import-XLS $env:temp\HG.XLSX - $ShowGuide = $XLS | Out-GridView -} -Function Assess-VM { -<# - .DESCRIPTION - This cmdlet will allow you to assess the security of the VM's in your environment - .EXAMPLE - # Specify Virtual Machines to Assess - $VM = Get-VM MGMT-AD, MGMT-VCO, WebCommander, Exchange01 - - # Assess against Risk Profile 2 - Assess-VM -VM $VM -RiskProfile 2 - - .EXAMPLE - # Assess all VM's against specific ID's - $VM = Get-VM - Assess-VM -VM $VM -ID 9,10,12,22,33,49 - - .EXAMPLE - # Assess All VM's against All ID's - Assess-VM -VM (Get-VM) -#> - [CmdletBinding( - DefaultParameterSetName = â€ID†- )] - Param ( - #Object receiving the hardening - [Parameter( - Position = 0, - ValueFromPipeline = $true, - ValueFromPipelineByPropertyName = $true, - HelpMessage = 'What object are you running this against?')] - #[string]$VMs, - [PSObject[]] - $VM, - - #Hardening Guide Profile - [Parameter(Mandatory = $false, - ValueFromPipeline = $false, - HelpMessage = "You must choose which Hardening Guide Risk Profile to use (1-3).", - ParameterSetName = 'RiskProfile')] - #[ValidateRange(1,3)] - [int]$RiskProfile, - - #Hardening Guide Guideline ID - [Parameter(Mandatory = $false, - ValueFromPipeline = $false, - ParameterSetName = 'ID')] - [string[]]$ID, - - [parameter(ParameterSetName = "ID", - ValueFromPipeline = $false)] - $AllIDs = "1" - ) - - BEGIN { - # Validate VM Parameter - # This ensures the object being passed is an object rather than a string - [int]$Err = 0 - foreach ($object in $vm) { if (($object.GetType().FullName) -ne "VMware.VimAutomation.ViCore.Impl.V1.Inventory.VirtualMachineImpl") { $Err += 1 } } - if ($Err -gt 0) { - Write-Error "One or more objects specified in the `$VM parameter is not a VM object" - Break - } - - - # Get the Date, used for folder creation and report - $Date = (Get-Date -Format MM.dd.yy) - - # Create array of ID's - if (-not $ID) { - $IDs = $null - } else { - $IDs = $ID.Replace(' ', '').split(',') - } - - # Manage Risk Profile Parameter value - $RiskArray = @("1","2","3") - - - ### HTML CSS CODE ### - $head = @' - -'@ - ##################### - - #C heck to see if HG exists in the $Env:Temp folder, if not, place it there - if (!(Test-Path -Path "$env:Temp\HG.XLSX")) { - - # Decode Hardening Guide from script - $Content = [System.Convert]::FromBase64String($Global:Base64) - Set-Content -Path $env:temp\HG.XLSX -Value $Content -Encoding Byte - } - - #Save Imported Hardening Guide to $XLS - $Global:XLS = Import-XLS $env:temp\HG.XLSX - - } - - PROCESS { - - # If risk profile is provided - if ($RiskArray -contains $RiskProfile) { - - $Guidelines = ($XLS | ?{ $_."Guideline ID" -like "VM*" -and $_."Risk Profile" -match $RiskProfile }) - foreach ($Guideline in $Guidelines) { - - $ID = $Guideline.ID - $GuidelineID = $Guideline."Guideline ID" - $Name = ($GuidelineID.Split("."))[1] - $Assessment = $Guideline."PowerCLI Command Assessment" - $Assessment = $Assessment -replace "Get-VM", "`$VM" - $Description = $Guideline."Description" - - Write-Host "Processing: $ID - $Name" -ForegroundColor CYAN - if ($assessment -ne "") { - $run = iex ($Assessment) - if ($run -eq $null) { - $run = New-Object System.object - $run | Add-Member -type NoteProperty -name Name -value "Clear" - $run | Add-Member -type NoteProperty -name Description -value "NO SECURITY HARDENING NEEDED FOR THIS GUIDELINE" - } - $Task = $run | ConvertTo-Html -Fragment -PreContent "

$ID - $Name

$Description

" -PostContent "
" | Out-String - $Tasks += $task - } - } - } elseif ($AllIDs -eq "1" -and $ID -eq $null) { - - $Guidelines = ($XLS | ?{ $_."Guideline ID" -like "VM*" }) - foreach ($Guideline in $Guidelines) { - $ID = $Guideline.ID - $GuidelineID = $Guideline."Guideline ID" - $Name = ($GuidelineID.Split("."))[1] - $Assessment = $Guideline."PowerCLI Command Assessment" - $Assessment = $Assessment -replace "Get-VM", "`$VM" - $Description = $Guideline."Description" - Write-Host "Processing: $ID - $Name" -ForegroundColor CYAN - if ($assessment -ne "") { - $run = iex ($Assessment) - if ($run -eq $null) { - $run = New-Object System.object - $run | Add-Member -type NoteProperty -name Name -value "Clear" - $run | Add-Member -type NoteProperty -name Description -value "NO SECURITY HARDENING NEEDED FOR THIS GUIDELINE" - } - $Task = $run | ConvertTo-Html -Fragment -PreContent "

$ID - $Name


$Description

" | Out-String - $Tasks += $task - } - } - - } else { - # If Guideline IDs are provided - foreach ($line in $ID) { - $Guideline = ($XLS | ?{ $_."Guideline ID" -like "VM*" -and $_."ID" -eq $Line }) - $GuidelineID = $Guideline."Guideline ID" - if ($GuidelineID -eq $null) { - Write-Host "$line is an invalid ID for this object... moving to next ID" -ForegroundColor 'Red' - Continue - } - $Name = ($GuidelineID.Split("."))[1] - - $Description = $Guideline."Description" - $Assessment = $Guideline."PowerCLI Command Assessment" - $Assessment = $Assessment -replace "Get-VM", "`$VM" - Write-Host "Processing: $line - $Name" -ForegroundColor CYAN - if ($assessment -ne "") { - $run = iex ($Assessment) - if ($run -eq $null) { - $run = New-Object System.object - $run| Add-Member -type NoteProperty -name Name -value "Clear" - $run | Add-Member -type NoteProperty -name Description -value "NO SECURITY HARDENING NEEDED FOR THIS GUIDELINE" - } - $Task = $run | ConvertTo-Html -Fragment -PreContent "

$line - $Name


$Description

" -PostContent "
" | Out-String - $Tasks += $task - } - } - } - } - - END { - # if no tasks were generated, end function and do NOT create empty report - if ($tasks -eq $null) {Break } - - # Get's end time Hours/Minutes for creating the HTML report - $time = (get-date -Format HH-mm) - - # Checks to see if a folder has been created for that date. If not, it will create one. - if (!(Test-Path c:\Temp\$Date)) { - Write-Host "Folder does not exist... Creating" -ForegroundColor 'Yellow' - New-item "C:\Temp\$date" -type Directory - } - - # HTML Report is then generated - $HTML = ConvertTo-HTML -head $head -PostContent "
$Tasks
" -PreContent “

vSphere 6 VM Hardening Report

†| Out-File "c:\Temp\$Date\$Time-VMAssessment.html" -Force - - # Report is opened for user to see - invoke-item "c:\Temp\$Date\$Time-VMAssessment.html" - } -} - From ae429792ed5b2e2f0855f8c70c428f4052449c1b Mon Sep 17 00:00:00 2001 From: Wouter Kursten Date: Wed, 26 Apr 2017 18:58:32 +0200 Subject: [PATCH 034/112] removed brake for issue #74 --- Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 index 7a3f909..c92eb14 100644 --- a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 +++ b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 @@ -5138,7 +5138,7 @@ function Get-HVMachine { $machineList = Find-HVMachine -Param $PSBoundParameters if (!$machineList) { Write-Host "No Virtual Machine(s) Found with given search parameters" - break + } $queryResults = @() $desktop_helper = New-Object VMware.Hv.MachineService From 70f7be027070edf229c4f7005ec94689c15da813 Mon Sep 17 00:00:00 2001 From: Alessio Rocchi Date: Wed, 3 May 2017 08:24:43 +0200 Subject: [PATCH 035/112] Add a script to set multiple datastore Tag. This example uses new powershell 5 features, classes and two different Design Patterns (Singleton and Disposable). --- SetDatastoreTag.ps1 | 198 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100755 SetDatastoreTag.ps1 diff --git a/SetDatastoreTag.ps1 b/SetDatastoreTag.ps1 new file mode 100755 index 0000000..23708e5 --- /dev/null +++ b/SetDatastoreTag.ps1 @@ -0,0 +1,198 @@ +<# + .SYNOPSIS + A brief description of the file. + + .DESCRIPTION + Given a list of Datastore Names, this script will assign a Tag to them + + .PARAMETER csvFile + String representing the full path of the file + The file must be structured like this: + ----------------------------- + Tag1,Tag2,Tag3,Tag4 + IPv4-iSCSI-SiteA,Tag1,Tag3 + IPv4-NFS-SiteA,Tag2,Tag4 + ... + ----------------------------- + + .NOTES + =========================================================================== + Created on: 31/03/2017 11:16 + Created by: Alessio Rocchi + Organization: VMware + Filename: SetDatastoreTag.ps1 + =========================================================================== +#> +[CmdletBinding()] +param +( + [Parameter(Mandatory = $true, + ValueFromPipeline = $true)] + [ValidateNotNullOrEmpty()] + [System.String]$csvFile, + [Parameter(Mandatory = $true, + ValueFromPipeline = $true)] + [ValidateNotNullOrEmpty()] + [String]$vCenter, + [Parameter(ValueFromPipeline = $true, + Position = 2)] + [AllowNull()] + [String]$Username, + [Parameter(Position = 3)] + [AllowNull()] + [String]$Password +) + +Import-Module -Name VMware.VimAutomation.Core -ErrorAction SilentlyContinue | Out-Null + +class vcConnector : System.IDisposable +{ + [String]$Username + [String]$Password + [String]$vCenter + [PSObject]$server + + static [vcConnector]$instance + + vcConnector($Username, $Password, $vCenter) + { + Import-Module -Name VMware.VimAutomation.Core -ErrorAction SilentlyContinue | Out-Null + + $this.Username = $Username + $this.Password = $Password + $this.vCenter = $vCenter + $this.connect() + } + + vcConnector($vcCredential, $vCenter) + { + Import-Module -Name VMware.VimAutomation.Core -ErrorAction SilentlyContinue | Out-Null + + $this.vcCredential = $vcCredential + $this.vCenter = $vCenter + $this.connect() + } + + [void] hidden connect() + { + try + { + if ([String]::IsNullOrEmpty($this.Username) -or [String]::IsNullOrEmpty($this.Password)) + { + $vcCredential = Get-Credential + Connect-VIServer -Server $this.vCenter -Credential $this.vcCredential -WarningAction SilentlyContinue -ErrorAction Stop | Out-Null + } + else + { + Connect-VIServer -Server $this.vCenter -User $this.Username -Password $this.Password -WarningAction SilentlyContinue -ErrorAction Stop + } + Write-Debug("Connected to vCenter: {0}" -f $this.vCenter) + } + catch + { + Write-Error($Error[0].Exception.Message) + exit + } + } + + + [void] Dispose() + { + Write-Debug("Called Dispose Method of Instance: {0}" -f ($this)) + Disconnect-VIServer -WarningAction SilentlyContinue -Server $this.vCenter -Force -Confirm:$false | Out-Null + } + + static [vcConnector] GetInstance() + { + if ([vcConnector]::instance -eq $null) + { + [vcConnector]::instance = [vcConnector]::new() + } + + return [vcConnector]::instance + } +} + +class Content{ + [System.Collections.Generic.List[System.String]]$availableTags + [System.Collections.Generic.List[System.String]]$elements + + Content() + { + } + + Content([String]$filePath) + { + if ((Test-Path -Path $filePath) -eq $false) + { + throw ("Cannot find file: {0}" -f ($filePath)) + } + try + { + # Cast the Get-Content return type to Generic List of Strings in order to avoid fixed-size array + $this.elements = [System.Collections.Generic.List[System.String]](Get-Content -Path $filePath -ea SilentlyContinue -wa SilentlyContinue) + $this.availableTags = $this.elements[0].split(',') + # Delete the first element aka availableTags + $this.elements.RemoveAt(0) + } + catch + { + throw ("Error reading the file: {0}" -f ($filePath)) + } + } +} + +try +{ + $vc = [vcConnector]::new($Username, $Password, $vCenter) + $csvContent = [Content]::new($csvFile) + + Write-Host("Available Tags: {0}" -f ($csvContent.availableTags)) + + foreach ($element in $csvContent.elements) + { + [System.Collections.Generic.List[System.String]]$splittedList = $element.split(',') + # Get the Datastore Name + [System.String]$datastoreName = $splittedList[0] + # Removing Datastore Name + $splittedList.RemoveAt(0) + # Create a List of Tags which will be assigned to the Datastore + [System.Collections.Generic.List[PSObject]]$tagsToAssign = $splittedList | ForEach-Object { Get-Tag -Name $_ } + Write-Host("Tags to assign to Datastore: {0} are: {1}" -f ($datastoreName, $tagsToAssign)) + # Get Datastore object by the given Datastore Name, first field of the the line + $datastore = Get-Datastore -Name $datastoreName -ea Stop + # Iterate the assigned Datastore Tags + foreach ($tag in ($datastore | Get-TagAssignment)) + { + # Check if the current tag is one of the available ones. + if ($tag.Tag.Name -in $csvContent.availableTags) + { + # Remove the current assigned Tag + Write-Host("Removing Tag: {0}" -f ($tag)) + Remove-TagAssignment -TagAssignment $tag -Confirm:$false + } + } + # Finally add the new set of tags to the Datastore + foreach ($tag in $tagsToAssign) + { + Write-Host("Trying to assign Tag: {0} to Datastore: {1}" -f ($tag.Name, $datastoreName)) + # Assign the Tag + New-TagAssignment -Entity $datastore -Tag $tag + } + } +} +catch [VMware.VimAutomation.Sdk.Types.V1.ErrorHandling.VimException.VimException] +{ + Write-Error("VIException: {0}" -f ($Error[0].Exception.Message)) + exit +} +catch +{ + Write-Error $Error[0].Exception.Message + exit +} +finally +{ + # Let be assured that the vc connection will be disposed. + $vc.Dispose() +} From 3d0c65b8022ffa637dea3838efd93069f108d214 Mon Sep 17 00:00:00 2001 From: praveenmathamsetty Date: Mon, 15 May 2017 17:25:20 +0530 Subject: [PATCH 036/112] merge changes related to Get-HVPodSessions pull request https://github.com/vmware/PowerCLI-Example-Scripts/pull/71 --- .../VMware.Hv.Helper/VMware.HV.Helper.psm1 | 61 ++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 index 56ad921..d9bd7b9 100644 --- a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 +++ b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 @@ -8699,4 +8699,63 @@ function Remove-HVGlobalEntitlement { } -Export-ModuleMember Add-HVDesktop,Add-HVRDSServer,Connect-HVEvent,Disconnect-HVEvent,Get-HVPoolSpec,Get-HVInternalName, Get-HVEvent,Get-HVFarm,Get-HVFarmSummary,Get-HVPool,Get-HVPoolSummary,Get-HVMachine,Get-HVMachineSummary,Get-HVQueryResult,Get-HVQueryFilter,New-HVFarm,New-HVPool,Remove-HVFarm,Remove-HVPool,Set-HVFarm,Set-HVPool,Start-HVFarm,Start-HVPool,New-HVEntitlement,Get-HVEntitlement,Remove-HVEntitlement, Set-HVMachine, New-HVGlobalEntitlement, Remove-HVGlobalEntitlement, Get-HVGlobalEntitlement +function Get-HVPodSession { +<# +.Synopsis + Gets the total amount of sessions for all Pods in a Federation +.DESCRIPTION + Gets the total amout of current sessions (connected and disconnected) for all Pods in a Federation (CPA) + based on the global query service. + The default object response is used which contains both success and fault information as well as the + session count per pod and the ID of each pod. +.PARAMETER HvServer + Reference to Horizon View Server to query the virtual machines from. If the value is not passed or null then + first element from global:DefaultHVServers would be considered inplace of hvServer +.EXAMPLE + Get-HVPodSession +.OUTPUTS + Returns list of objects of type GlobalSessionPodSessionCounter +.NOTES + Author : Rasmus Sjoerslev + Author email : rasmus.sjorslev@vmware.com + Version : 1.0 + ===Tested Against Environment==== + Horizon View Server Version : 7.0.2 + PowerCLI Version : PowerCLI 6.5 + PowerShell Version : 5.0 +#> + + [CmdletBinding( + SupportsShouldProcess = $true, + ConfirmImpact = 'High' + )] + + param( + [Parameter(Mandatory = $false)] + $HvServer = $null + ) + + $services = Get-ViewAPIService -hvServer $hvServer + if ($null -eq $services) { + Write-Error "Could not retrieve ViewApi services from connection object" + break + } + + $query_service_helper = New-Object VMware.Hv.GlobalSessionQueryServiceService + $count_spec = New-Object VMware.Hv.GlobalSessionQueryServiceCountSpec + $queryResults = @() + + foreach ($pod in $services.Pod.Pod_List()) { + $count_spec.Pod = $pod.Id + $info = $query_service_helper.GlobalSessionQueryService_GetCountWithSpec($services,$count_spec) + + foreach ($res in $info) { + if ($pod.Id.Id -eq $res.Id.Id) { + $queryResults += $res + } + } + } + return $queryResults +} + +Export-ModuleMember Add-HVDesktop,Add-HVRDSServer,Connect-HVEvent,Disconnect-HVEvent,Get-HVPoolSpec,Get-HVInternalName, Get-HVEvent,Get-HVFarm,Get-HVFarmSummary,Get-HVPool,Get-HVPoolSummary,Get-HVMachine,Get-HVMachineSummary,Get-HVQueryResult,Get-HVQueryFilter,New-HVFarm,New-HVPool,Remove-HVFarm,Remove-HVPool,Set-HVFarm,Set-HVPool,Start-HVFarm,Start-HVPool,New-HVEntitlement,Get-HVEntitlement,Remove-HVEntitlement, Set-HVMachine, New-HVGlobalEntitlement, Remove-HVGlobalEntitlement, Get-HVGlobalEntitlement, Get-HVPodSession From 8337b986330eb1aa6f798ac9c1b07769bbec9af9 Mon Sep 17 00:00:00 2001 From: PARAMESHO Date: Tue, 16 May 2017 14:48:36 +0530 Subject: [PATCH 037/112] Advanced functions for customizing Application icons in Horizon This changes adds two advanced functions: Set-HVApplicationIcon -> Used to create/update an icon association for a given application. Remove-HVApplicationIcon -> Used to remove a customized icon association for a given application. --- .../VMware.Hv.Helper/VMware.HV.Helper.psm1 | 221 +++++++++++++++++- 1 file changed, 220 insertions(+), 1 deletion(-) diff --git a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 index d9bd7b9..85cad3a 100644 --- a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 +++ b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 @@ -8758,4 +8758,223 @@ function Get-HVPodSession { return $queryResults } -Export-ModuleMember Add-HVDesktop,Add-HVRDSServer,Connect-HVEvent,Disconnect-HVEvent,Get-HVPoolSpec,Get-HVInternalName, Get-HVEvent,Get-HVFarm,Get-HVFarmSummary,Get-HVPool,Get-HVPoolSummary,Get-HVMachine,Get-HVMachineSummary,Get-HVQueryResult,Get-HVQueryFilter,New-HVFarm,New-HVPool,Remove-HVFarm,Remove-HVPool,Set-HVFarm,Set-HVPool,Start-HVFarm,Start-HVPool,New-HVEntitlement,Get-HVEntitlement,Remove-HVEntitlement, Set-HVMachine, New-HVGlobalEntitlement, Remove-HVGlobalEntitlement, Get-HVGlobalEntitlement, Get-HVPodSession +function Set-HVApplicationIcon { +<# +.SYNOPSIS + Used to create/update an icon association for a given application. + +.DESCRIPTION + This function is used to create an application icon and associate it with the given application. If the specified icon already exists in the LDAP, it will just updates the icon association to the application. Any of the existing customized icon association to the given application will be overwritten. + +.PARAMETER ApplicationName + Name of the application to which the association to be made. + +.PARAMETER IconPath + Path of the icon. + +.PARAMETER HvServer + View API service object of Connect-HVServer cmdlet. + +.EXAMPLE + Creating the icon I1 and associating with application A1. Same command is used for update icon also. + Set-HVApplicationIcon -ApplicationName A1 -IconPath C:\I1.ico -HvServer $hvServer + +.OUTPUTS + None + +.NOTES + Author : Paramesh Oddepally. + Author email : poddepally@vmware.com + Version : 1.1 + + ===Tested Against Environment==== + Horizon View Server Version : 7.1 + PowerShell Version : 5.0 +#> + + [CmdletBinding( + SupportsShouldProcess = $true, + ConfirmImpact = 'High' + )] + + param( + [Parameter(Mandatory = $true)] + [string] $ApplicationName, + + [Parameter(Mandatory = $true)] + $IconPath, + + [Parameter(Mandatory = $false)] + $HvServer = $null + ) + + begin { + $services = Get-ViewAPIService -HvServer $HvServer + if ($null -eq $services) { + Write-Error "Could not retrieve ViewApi services from connection object." + break + } + Add-Type -AssemblyName System.Drawing + } + + process { + try { + $appInfo = Get-HVQueryResult -EntityType ApplicationInfo -Filter (Get-HVQueryFilter data.name -Eq $ApplicationName) -HvServer $HvServer + } catch { + # EntityNotFound, InsufficientPermission, InvalidArgument, InvalidType, UnexpectedFault + Write-Error "Error in querying the ApplicationInfo for Application:[$ApplicationName] $_" + break + } + + if ($null -eq $appInfo) { + Write-Error "No application found with specified name:[$ApplicationName]." + break + } + + if (!(Test-Path $IconPath)) { + Write-Error "File:[$IconPath] does not exists" + break + } + + $spec = New-Object VMware.Hv.ApplicationIconSpec + $base = New-Object VMware.Hv.ApplicationIconBase + + try { + $fileHash = Get-FileHash -Path $IconPath -Algorithm MD5 + $base.IconHash = $fileHash.Hash + $base.Data = (Get-Content $iconPath -Encoding byte) + $bitMap = [System.Drawing.Bitmap]::FromFile($iconPath) + $base.Width = $bitMap.Width + $base.Height = $bitMap.Height + $base.IconSource = "broker" + $base.Applications = @($appInfo.Id) + $spec.ExecutionData = $base + } catch { + Write-Error "Error in reading the icon parameters: $_" + break + } + + if ($base.Height -gt 256 -or $base.Width -gt 256) { + Write-Error "Invalid image resolution. Maximum resolution for an icon should be 256*256." + break + } + + $ApplicationIconHelper = New-Object VMware.Hv.ApplicationIconService + try { + $ApplicationIconId = $ApplicationIconHelper.ApplicationIcon_CreateAndAssociate($services, $spec) + } catch { + if ($_.Exception.InnerException.MethodFault.GetType().name.Equals('EntityAlreadyExists')) { + # This icon is already part of LDAP and associated with some other application(s). + # In this case, call updateAssociations + $applicationIconId = $_.Exception.InnerException.MethodFault.Id + Write-Host "Some application(s) already have an association for the specified icon." + $ApplicationIconHelper.ApplicationIcon_UpdateAssociations($services, $applicationIconId, @($appInfo.Id)) + Write-Host "Successfully updated customized icon association for Application:[$ApplicationName]." + break + } + Write-Host "Error in associating customized icon for Application:[$ApplicationName] $_" + break + } + Write-Host "Successfully associated customized icon for Application:[$ApplicationName]." + } + + end { + [System.gc]::collect() + } +} + +Function Remove-HVApplicationIcon { +<# +.SYNOPSIS + Used to remove a customized icon association for a given application. + +.DESCRIPTION + This function is used to remove an application association to the given application. It will never remove the RDS system icons. If application doesnot have any customized icon, an error will be thrown. + +.PARAMETER ApplicationName + Name of the application to which customized icon needs to be removed. + +.PARAMETER HvServer + View API service object of Connect-HVServer cmdlet. + +.EXAMPLE + Removing the icon for an application A1. + Remove-HVApplicationIcon -ApplicationName A1 -HvServer $hvServer + +.OUTPUTS + None + +.NOTES + Author : Paramesh Oddepally. + Author email : poddepally@vmware.com + Version : 1.1 + + ===Tested Against Environment==== + Horizon View Server Version : 7.1 + PowerShell Version : 5.0 +#> + + [CmdletBinding( + SupportsShouldProcess = $true, + ConfirmImpact = 'High' + )] + param( + [Parameter(Mandatory = $true)] + [string] $ApplicationName, + + [Parameter(Mandatory = $false)] + $HvServer = $null + ) + + begin { + $services = Get-ViewAPIService -HvServer $HvServer + if ($null -eq $services) { + Write-Error "Could not retrieve ViewApi services from connection object." + break + } + } + + process { + try { + $appInfo = Get-HVQueryResult -EntityType ApplicationInfo -Filter (Get-HVQueryFilter data.name -Eq $ApplicationName) -HvServer $HvServer + } catch { + # EntityNotFound, InsufficientPermission, InvalidArgument, InvalidType, UnexpectedFault + Write-Error "Error in querying the ApplicationInfo for Application:[$ApplicationName] $_" + break + } + + if ($null -eq $appInfo) { + Write-Error "No application found with specified name:[$ApplicationName]" + break + } + + [VMware.Hv.ApplicationIconId[]] $icons = $appInfo.Icons + [VMware.Hv.ApplicationIconId] $brokerIcon = $null + $ApplicationIconHelper = New-Object VMware.Hv.ApplicationIconService + Foreach ($icon in $icons) { + $applicationIconInfo = $ApplicationIconHelper.ApplicationIcon_Get($services, $icon) + if ($applicationIconInfo.Base.IconSource -eq "broker") { + $brokerIcon = $icon + } + } + + if ($null -eq $brokerIcon) { + Write-Error "There is no customized icon for the Application:[$ApplicationName]." + break + } + + try { + $ApplicationIconHelper.ApplicationIcon_RemoveAssociations($services, $brokerIcon, @($appInfo.Id)) + } catch { + Write-Error "Error in removing the customized icon association for Application:[$ApplicationName] $_ " + break + } + Write-Host "Successfully removed customized icon association for Application:[$ApplicationName]." + } + + end { + [System.gc]::collect() + } +} + +Export-ModuleMember Add-HVDesktop,Add-HVRDSServer,Connect-HVEvent,Disconnect-HVEvent,Get-HVPoolSpec,Get-HVInternalName, Get-HVEvent,Get-HVFarm,Get-HVFarmSummary,Get-HVPool,Get-HVPoolSummary,Get-HVMachine,Get-HVMachineSummary,Get-HVQueryResult,Get-HVQueryFilter,New-HVFarm,New-HVPool,Remove-HVFarm,Remove-HVPool,Set-HVFarm,Set-HVPool,Start-HVFarm,Start-HVPool,New-HVEntitlement,Get-HVEntitlement,Remove-HVEntitlement, Set-HVMachine, New-HVGlobalEntitlement, Remove-HVGlobalEntitlement, Get-HVGlobalEntitlement, Get-HVPodSession, Set-HVApplicationIcon, Remove-HVApplicationIcon From 4936b79a5cea87401471138b4c853566bb7a7e40 Mon Sep 17 00:00:00 2001 From: PARAMESHO Date: Tue, 16 May 2017 14:56:21 +0530 Subject: [PATCH 038/112] Updating the description of the advanced functions for ApplicationIcon Updating the description of the advanced functions for ApplicationIcon --- Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 index 85cad3a..5b68b9f 100644 --- a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 +++ b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 @@ -8789,6 +8789,7 @@ function Set-HVApplicationIcon { ===Tested Against Environment==== Horizon View Server Version : 7.1 + PowerCLI Version : PowerCLI 6.5.1 PowerShell Version : 5.0 #> @@ -8911,6 +8912,7 @@ Function Remove-HVApplicationIcon { ===Tested Against Environment==== Horizon View Server Version : 7.1 + PowerCLI Version : PowerCLI 6.5.1 PowerShell Version : 5.0 #> From acb0383f1683a05de967a415de76247b558c1e3d Mon Sep 17 00:00:00 2001 From: NamedJason Date: Tue, 16 May 2017 08:19:00 -0700 Subject: [PATCH 039/112] Create DatastoreFunctions.psm1 --- Modules/DatastoreFunctions.psm1 | 184 ++++++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 Modules/DatastoreFunctions.psm1 diff --git a/Modules/DatastoreFunctions.psm1 b/Modules/DatastoreFunctions.psm1 new file mode 100644 index 0000000..e417112 --- /dev/null +++ b/Modules/DatastoreFunctions.psm1 @@ -0,0 +1,184 @@ +#Created by Alan Renouf, published at https://communities.vmware.com/docs/DOC-18008 +Function Get-DatastoreMountInfo { + [CmdletBinding()] + Param ( + [Parameter(ValueFromPipeline=$true)] + $Datastore + ) + Process { + $AllInfo = @() + if (-not $Datastore) { + $Datastore = Get-Datastore + } + Foreach ($ds in $Datastore) { + if ($ds.ExtensionData.info.Vmfs) { + $hostviewDSDiskName = $ds.ExtensionData.Info.vmfs.extent[0].diskname + if ($ds.ExtensionData.Host) { + $attachedHosts = $ds.ExtensionData.Host + Foreach ($VMHost in $attachedHosts) { + $hostview = Get-View $VMHost.Key + $hostviewDSState = $VMHost.MountInfo.Mounted + $StorageSys = Get-View $HostView.ConfigManager.StorageSystem + $devices = $StorageSys.StorageDeviceInfo.ScsiLun + Foreach ($device in $devices) { + $Info = "" | Select Datastore, VMHost, Lun, Mounted, State + if ($device.canonicalName -eq $hostviewDSDiskName) { + $hostviewDSAttachState = "" + if ($device.operationalState[0] -eq "ok") { + $hostviewDSAttachState = "Attached" + } elseif ($device.operationalState[0] -eq "off") { + $hostviewDSAttachState = "Detached" + } else { + $hostviewDSAttachState = $device.operationalstate[0] + } + $Info.Datastore = $ds.Name + $Info.Lun = $hostviewDSDiskName + $Info.VMHost = $hostview.Name + $Info.Mounted = $HostViewDSState + $Info.State = $hostviewDSAttachState + $AllInfo += $Info + } + } + + } + } + } + } + $AllInfo + } +} + +Function Detach-Datastore { + [CmdletBinding()] + Param ( + [Parameter(ValueFromPipeline=$true)] + $Datastore + ) + Process { + if (-not $Datastore) { + Write-Host "No Datastore defined as input" + Exit + } + Foreach ($ds in $Datastore) { + $hostviewDSDiskName = $ds.ExtensionData.Info.vmfs.extent[0].Diskname + if ($ds.ExtensionData.Host) { + $attachedHosts = $ds.ExtensionData.Host + Foreach ($VMHost in $attachedHosts) { + $hostview = Get-View $VMHost.Key + $StorageSys = Get-View $HostView.ConfigManager.StorageSystem + $devices = $StorageSys.StorageDeviceInfo.ScsiLun + Foreach ($device in $devices) { + if ($device.canonicalName -eq $hostviewDSDiskName) { + $LunUUID = $Device.Uuid + Write-Host "Detaching LUN $($Device.CanonicalName) from host $($hostview.Name)..." + $StorageSys.DetachScsiLun($LunUUID); + } + } + } + } + } + } +} + +Function Unmount-Datastore { + [CmdletBinding()] + Param ( + [Parameter(ValueFromPipeline=$true)] + $Datastore + ) + Process { + if (-not $Datastore) { + Write-Host "No Datastore defined as input" + Exit + } + Foreach ($ds in $Datastore) { + $hostviewDSDiskName = $ds.ExtensionData.Info.vmfs.extent[0].Diskname + if ($ds.ExtensionData.Host) { + $attachedHosts = $ds.ExtensionData.Host + Foreach ($VMHost in $attachedHosts) { + $hostview = Get-View $VMHost.Key + $StorageSys = Get-View $HostView.ConfigManager.StorageSystem + Write-Host "Unmounting VMFS Datastore $($DS.Name) from host $($hostview.Name)..." + $StorageSys.UnmountVmfsVolume($DS.ExtensionData.Info.vmfs.uuid); + } + } + } + } +} + +Function Mount-Datastore { + [CmdletBinding()] + Param ( + [Parameter(ValueFromPipeline=$true)] + $Datastore + ) + Process { + if (-not $Datastore) { + Write-Host "No Datastore defined as input" + Exit + } + Foreach ($ds in $Datastore) { + $hostviewDSDiskName = $ds.ExtensionData.Info.vmfs.extent[0].Diskname + if ($ds.ExtensionData.Host) { + $attachedHosts = $ds.ExtensionData.Host + Foreach ($VMHost in $attachedHosts) { + $hostview = Get-View $VMHost.Key + $StorageSys = Get-View $HostView.ConfigManager.StorageSystem + Write-Host "Mounting VMFS Datastore $($DS.Name) on host $($hostview.Name)..." + $StorageSys.MountVmfsVolume($DS.ExtensionData.Info.vmfs.uuid); + } + } + } + } +} + +Function Attach-Datastore { + [CmdletBinding()] + Param ( + [Parameter(ValueFromPipeline=$true)] + $Datastore + ) + Process { + if (-not $Datastore) { + Write-Host "No Datastore defined as input" + Exit + } + Foreach ($ds in $Datastore) { + $hostviewDSDiskName = $ds.ExtensionData.Info.vmfs.extent[0].Diskname + if ($ds.ExtensionData.Host) { + $attachedHosts = $ds.ExtensionData.Host + Foreach ($VMHost in $attachedHosts) { + $hostview = Get-View $VMHost.Key + $StorageSys = Get-View $HostView.ConfigManager.StorageSystem + $devices = $StorageSys.StorageDeviceInfo.ScsiLun + Foreach ($device in $devices) { + if ($device.canonicalName -eq $hostviewDSDiskName) { + $LunUUID = $Device.Uuid + Write-Host "Attaching LUN $($Device.CanonicalName) to host $($hostview.Name)..." + $StorageSys.AttachScsiLun($LunUUID); + } + } + } + } + } + } +} +# +#Get-Datastore | Get-DatastoreMountInfo | Sort Datastore, VMHost | FT -AutoSize +# +#Get-Datastore IX2ISCSI01 | Unmount-Datastore +# +#Get-Datastore IX2ISCSI01 | Get-DatastoreMountInfo | Sort Datastore, VMHost | FT -AutoSize +# +#Get-Datastore IX2iSCSI01 | Mount-Datastore +# +#Get-Datastore IX2iSCSI01 | Get-DatastoreMountInfo | Sort Datastore, VMHost | FT -AutoSize +# +#Get-Datastore IX2iSCSI01 | Detach-Datastore +# +#Get-Datastore IX2iSCSI01 | Get-DatastoreMountInfo | Sort Datastore, VMHost | FT -AutoSize +# +#Get-Datastore IX2iSCSI01 | Attach-datastore +# +#Get-Datastore IX2iSCSI01 | Get-DatastoreMountInfo | Sort Datastore, VMHost | FT -AutoSize +# From 6dab3ef94d4ce7487e178d84ce09b76240b6c7be Mon Sep 17 00:00:00 2001 From: Eric Gray Date: Thu, 25 May 2017 12:55:16 -0700 Subject: [PATCH 040/112] Initial commit of esxi-image scripts --- Scripts/esxi-image-comparator.ps1 | 97 +++++++++++++++++++++++++++ Scripts/esxi-image-creator.ps1 | 108 ++++++++++++++++++++++++++++++ 2 files changed, 205 insertions(+) create mode 100644 Scripts/esxi-image-comparator.ps1 create mode 100644 Scripts/esxi-image-creator.ps1 diff --git a/Scripts/esxi-image-comparator.ps1 b/Scripts/esxi-image-comparator.ps1 new file mode 100644 index 0000000..a9be557 --- /dev/null +++ b/Scripts/esxi-image-comparator.ps1 @@ -0,0 +1,97 @@ +<# +Script name: esxi-image-comparator.ps1 +Last update: 24 May 2017 +Author: Eric Gray, @eric_gray +Description: Compare contents (VIBs) of multiple VMware ESXi image profiles. +Dependencies: PowerCLI Image Builder (VMware.ImageBuilder,VMware.VimAutomation.Core) +#> + +param( + [switch]$ShowAllVIBs=$false, + [switch]$HideDates=$false, + [switch]$Interactive=$false, + [switch]$Grid=$false, + [string]$ProfileInclude, + [string]$ProfileExclude +) + +$profileList = Get-EsxImageProfile | sort -Property Name + +if ($ProfileInclude) { + $profileList = $profileList | ? Name -Match $ProfileInclude +} + +if ($ProfileExclude) { + $profileList = $profileList | ? Name -NotMatch $ProfileExclude +} + +if ($profileList.Count -eq 0) { + Write-Host "No ESXi image profiles available in current session." + Write-Host "Use Add-EsxSoftwareDepot for each depot zip bundle you would like to compare." + exit 1 +} + +if ($Interactive) { + $keep = @() + Write-Host "Found the following profiles:" -ForegroundColor Yellow + $profileList | % { write-host $_.Name } + + if ($profileList.Count -gt 7) { + Write-Host "Found $($profileList.Count) profiles!" -ForegroundColor Yellow + Write-Host "Note: List filtering is possible through -ProfileInclude / -ProfileExclude" -ForegroundColor DarkGreen + } + + write-host "`nType 'y' next to each profile to compare..." -ForegroundColor Yellow + + foreach ($profile in $profileList) { + $want = Read-Host -Prompt $profile.Name + if ($want.StartsWith("y") ) { + $keep += $profile + } + + } + $profileList = $keep + +} + +# go thru each profile and build a hash of the vib name and hash of profile name + version +$diffResults = @{} +foreach ($profile in $profileList ) { + foreach ($vib in $profile.VibList) { + $vibValue = $vib.Version + if (! $HideDates) { + $vibValue += " "+ $vib.CreationDate.ToShortDateString() + } + $diffResults.($vib.name) += @{$profile.name = $vibValue} + + } + +} + +# create an object that will neatly output as CSV or table +$outputTable=@() +foreach ($row in $diffResults.keys | sort) { + $vibRow = new-object PSObject + $vibRow | add-member -membertype NoteProperty -name "VIB" -Value $row + $valueCounter = @{} + + foreach ($profileName in $profileList.name) { + #populate this hash to decide if all profiles have same version of VIB + $valueCounter.($diffResults.$row.$profileName) = 1 + $vibRow | add-member -membertype NoteProperty -name $profileName -Value $diffResults.$row.$profileName + } + + if ($valueCounter.Count -gt 1 -or $ShowAllVIBs) { + $outputTable += $vibRow + } +} + +# useful for debugging +#$diffResults | ConvertTo-Json +#$outputTable|Export-Csv -Path .\image-diff-results.csv -NoTypeInformation + +if ($Grid) { + $outputTable | Out-GridView -Title "VMware ESXi Image Profile Comparator" +} else { + $outputTable +} \ No newline at end of file diff --git a/Scripts/esxi-image-creator.ps1 b/Scripts/esxi-image-creator.ps1 new file mode 100644 index 0000000..322c6e6 --- /dev/null +++ b/Scripts/esxi-image-creator.ps1 @@ -0,0 +1,108 @@ +<# +Script name: esxi-image-creator.ps1 +Last update: 24 May 2017 +Author: Eric Gray, @eric_gray +Description: Create a VMware ESXi image profile based on + one or more depots and offline driver bundles. +Dependencies: PowerCLI Image Builder (VMware.ImageBuilder,VMware.VimAutomation.Core) +#> + +param( + [switch]$NewestDate = $false, + [switch]$WriteZip = $false, + [switch]$WriteISO = $false, + [switch]$LeaveCurrentDepotsMounted = $false, + [string]$NewProfileName = "Custom Image $(Get-Date -Format "yyyyMMddhhmm")", + [ValidateNotNullOrEmpty()] + [ValidateSet('VMwareCertified','VMwareAccepted','PartnerSupported','CommunitySupported')] + [string]$Acceptance = "VMwareCertified", + [string[]]$Files = "*.zip" +) + +#### Specify optional image fine-tuning here #### +# comma-separated list (array) of VIBs to exclude +$removeVibs = @("tools-light") + +# force specific VIB version to be included, when more than one version is present +# e.g. "net-enic"="2.1.2.71-1OEM.550.0.0.1331820" +$overrideVibs = @{ + # "net-enic"="2.1.2.71-1OEM.550.0.0.1331820", +} + +#### end of optional fine-tuning #### + +# may be desirable to manually mount an online depot in advance, such as for HPE +# e.g. Add-EsxSoftwareDepot http://vibsdepot.hpe.com/index-ecli-650.xml +if (! $LeaveCurrentDepotsMounted) { + Get-EsxSoftwareDepot | Remove-EsxSoftwareDepot +} + +foreach ($depot in Get-ChildItem $Files) { + if ($depot.Name.EndsWith(".zip") ) { + Add-EsxSoftwareDepot $depot.FullName + } else { + Write-Host "Not a zip depot:" $depot.Name + } +} + +if ((Get-EsxImageProfile).count -eq 0) { + write-host "No image profiles found in the selected files" + exit 1 +} + +# either use the native -Newest switch, or try to find latest VIBs by date (NewestDate) +if ($NewestDate) { + $pkgsAll = Get-EsxSoftwarePackage | sort -Property Name,CreationDate -Descending + $pkgsNewestDate=@() + + foreach ($pkg in $pkgsAll) { + if ($pkgsNewestDate.GetEnumerator().name -notcontains $pkg.Name ) { + $pkgsNewestDate += $pkg + } + } + $pkgs = $pkgsNewestDate + +} else { + $pkgs = Get-ESXSoftwarePackage -Newest +} + +# rebuild the package array according to manual fine-tuning +if ($removeVibs) { + Write-Host "`nThe following VIBs will not be included in ${NewProfileName}:" -ForegroundColor Yellow + $removeVibs + $pkgs = $pkgs | ? name -NotIn $removeVibs +} + +foreach ($override in $overrideVibs.keys) { + # check that the override exists, then remove existing and add override + $tmpOver = Get-EsxSoftwarePackage -Name $override -Version $overrideVibs.$override + if ($tmpOver) { + $pkgs = $pkgs | ? name -NotIn $tmpOver.name + $pkgs += $tmpOver + } else { + Write-host "Did not find:" $override $overrideVibs.$override -ForegroundColor Yellow + } +} + +try { + New-EsxImageProfile -NewProfile $NewProfileName -SoftwarePackage $pkgs ` + -Vendor Custom -AcceptanceLevel $Acceptance -Description "Made with esxi-image-creator.ps1" ` + -ErrorAction Stop -ErrorVariable CreationError | Out-Null +} +catch { + Write-Host "Custom image profile $NewProfileName not created." -ForegroundColor Yellow + $CreationError + exit 1 +} + +Write-Host "`nFinished creating $NewProfileName" -ForegroundColor Yellow + +if ($WriteZip) { + Write-Host "Creating zip bundle..." -ForegroundColor Green + Export-EsxImageProfile -ImageProfile $NewProfileName -ExportToBundle -FilePath .\${NewProfileName}.zip -Force +} + +if ($WriteISO) { + Write-Host "Creating ISO image..." -ForegroundColor Green + Export-EsxImageProfile -ImageProfile $NewProfileName -ExportToIso -FilePath .\${NewProfileName}.iso -Force +} From 390ce91bb4f2b6fb03bbed56e0ebb4d58bacf7ea Mon Sep 17 00:00:00 2001 From: Kyle Ruddy Date: Fri, 26 May 2017 15:00:00 -0400 Subject: [PATCH 041/112] Update folder structure Update the folder structure to include moving the DatastoreFcuntions module into its own folder. --- Modules/{ => DatastoreFunctions}/DatastoreFunctions.psm1 | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Modules/{ => DatastoreFunctions}/DatastoreFunctions.psm1 (100%) diff --git a/Modules/DatastoreFunctions.psm1 b/Modules/DatastoreFunctions/DatastoreFunctions.psm1 similarity index 100% rename from Modules/DatastoreFunctions.psm1 rename to Modules/DatastoreFunctions/DatastoreFunctions.psm1 From dc0213773d60620a23885d61430f409896d33804 Mon Sep 17 00:00:00 2001 From: NamedJason Date: Wed, 31 May 2017 07:27:38 -0700 Subject: [PATCH 042/112] Improved performance and user feedback I changed the structure of the functions to improve performance when working with large numbers of datastores and hosts. I also added more verbose progress bars so that the user would be more aware of what the scripts were doing at any given time. --- .../DatastoreFunctions.psm1 | 339 +++++++++++------- 1 file changed, 200 insertions(+), 139 deletions(-) diff --git a/Modules/DatastoreFunctions/DatastoreFunctions.psm1 b/Modules/DatastoreFunctions/DatastoreFunctions.psm1 index e417112..b908a40 100644 --- a/Modules/DatastoreFunctions/DatastoreFunctions.psm1 +++ b/Modules/DatastoreFunctions/DatastoreFunctions.psm1 @@ -1,50 +1,112 @@ -#Created by Alan Renouf, published at https://communities.vmware.com/docs/DOC-18008 +<# +.SYNOPSIS Datastore Functions +.DESCRIPTION A collection of functions to manipulate datastore Mount + Attach status +.EXAMPLE Get-Datastore | Get-DatastoreMountInfo | Sort Datastore, VMHost | FT -AutoSize +.EXAMPLE Get-Datastore IX2ISCSI01 | Unmount-Datastore +.EXAMPLE Get-Datastore IX2ISCSI01 | Get-DatastoreMountInfo | Sort Datastore, VMHost | FT -AutoSize +.EXAMPLE Get-Datastore IX2iSCSI01 | Mount-Datastore +.EXAMPLE Get-Datastore IX2iSCSI01 | Get-DatastoreMountInfo | Sort Datastore, VMHost | FT -AutoSize +.EXAMPLE Get-Datastore IX2iSCSI01 | Detach-Datastore +.EXAMPLE Get-Datastore IX2iSCSI01 | Get-DatastoreMountInfo | Sort Datastore, VMHost | FT -AutoSize +.EXAMPLE Get-Datastore IX2iSCSI01 | Attach-datastore +.EXAMPLE Get-Datastore IX2iSCSI01 | Get-DatastoreMountInfo | Sort Datastore, VMHost | FT -AutoSize +.NOTES Written by Alan Renouf, originally published at https://blogs.vmware.com/vsphere/2012/01/automating-datastore-storage-device-detachment-in-vsphere-5.html +.NOTES May 2017: Modified by Jason Coleman (virtuallyjason.blogspot.com), to improve performance when dealing with a large number of hosts and datastores +#> +Function Get-HostViews { + [CmdletBinding()] + Param ( + $Datastore + ) + Begin{ + $allDatastores = @() + } + Process { + $allDatastores += $Datastore + } + End { + #Build the array of Datastore Objects + if (-not $Datastore) { + $allDatastores = Get-Datastore + } + $allDatastores = $allDatastores | ? {$_.pstypenames -contains "VMware.VimAutomation.ViCore.Impl.V1.DatastoreManagement.DatastoreImpl"} + if (-not $allDatastores){ + Throw "No Datastores found.`nIs ""$Datastore"" a Datastore Object?" + } + $allHosts = @() + $DShostsKeys = $allDatastores.extensiondata.host.key.value | sort | get-unique -asstring + $DShosts = foreach ($thisKey in $DShostsKeys) {($allDatastores.extensiondata.host | ? {$_.key.value -eq $thisKey})[0]} + $i = 1 + foreach ($DSHost in $DSHosts){ + write-progress -activity "Collecting ESXi Host Views" -status "Querying $($dshost.key)..." -percentComplete ($i++/$DSHosts.count*100) + $hostObj = "" | select keyValue,hostView,storageSys + $hostObj.hostView = get-view $DSHost.key + $hostObj.keyValue = $DSHost.key.value + $hostObj.storageSys = Get-View $hostObj.hostView.ConfigManager.StorageSystem + $allHosts += $hostObj + } + write-progress -activity "Collecting ESXi Host Views" -completed + $allHosts + } +} + Function Get-DatastoreMountInfo { [CmdletBinding()] Param ( [Parameter(ValueFromPipeline=$true)] $Datastore ) + #Roll back up an unrolled array from a pipeline + Begin{ + $allDatastores = @() + } Process { + $allDatastores += $Datastore + } + End { $AllInfo = @() + #Build the array of Datastore Objects if (-not $Datastore) { - $Datastore = Get-Datastore + $allDatastores = Get-Datastore } - Foreach ($ds in $Datastore) { - if ($ds.ExtensionData.info.Vmfs) { - $hostviewDSDiskName = $ds.ExtensionData.Info.vmfs.extent[0].diskname - if ($ds.ExtensionData.Host) { - $attachedHosts = $ds.ExtensionData.Host - Foreach ($VMHost in $attachedHosts) { - $hostview = Get-View $VMHost.Key - $hostviewDSState = $VMHost.MountInfo.Mounted - $StorageSys = Get-View $HostView.ConfigManager.StorageSystem - $devices = $StorageSys.StorageDeviceInfo.ScsiLun - Foreach ($device in $devices) { - $Info = "" | Select Datastore, VMHost, Lun, Mounted, State - if ($device.canonicalName -eq $hostviewDSDiskName) { - $hostviewDSAttachState = "" - if ($device.operationalState[0] -eq "ok") { - $hostviewDSAttachState = "Attached" - } elseif ($device.operationalState[0] -eq "off") { - $hostviewDSAttachState = "Detached" - } else { - $hostviewDSAttachState = $device.operationalstate[0] - } - $Info.Datastore = $ds.Name - $Info.Lun = $hostviewDSDiskName - $Info.VMHost = $hostview.Name - $Info.Mounted = $HostViewDSState - $Info.State = $hostviewDSAttachState - $AllInfo += $Info - } - } - + $allDatastores = $allDatastores | ? {$_.pstypenames -contains "VMware.VimAutomation.ViCore.Impl.V1.DatastoreManagement.DatastoreImpl"} + if (-not $allDatastores){ + Throw "No Datastores found.`nIs ""$Datastore"" a Datastore Object?" + } + $allDatastoreNAAs = foreach ($ds in $allDatastores) {$ds.ExtensionData.Info.vmfs.extent[0].diskname} + + #Build the array of custom Host Objects + $allHosts = Get-HostViews -datastore $allDatastores + $output = @() + $i = 1 + foreach ($dsHost in $allHosts){ + write-progress -activity "Checking Datastore access" -status "Checking $($dshost.hostview.name)..." -percentComplete ($i++ / $allHosts.count * 100) + #Get all devices on the host that match the list of $allDatastoreNAAs + $devices = $dsHost.storagesys.StorageDeviceInfo.ScsiLun + foreach ($device in $devices){ + if ($allDatastoreNAAs -contains $device.canonicalName){ + #Record information about this device/host combo + $thisDatastore = $alldatastores | ? {$_.ExtensionData.Info.vmfs.extent[0].diskname -eq $device.canonicalName} + $hostviewDSAttachState = "" + if ($device.operationalState[0] -eq "ok") { + $hostviewDSAttachState = "Attached" + } elseif ($device.operationalState[0] -eq "off") { + $hostviewDSAttachState = "Detached" + } else { + $hostviewDSAttachState = $device.operationalstate[0] } + $Info = "" | Select Datastore, VMHost, Lun, Mounted, State + $Info.VMHost = $dsHost.hostview.name + $Info.Datastore = $thisDatastore.name + $Info.Lun = $device.canonicalName + $Info.mounted = ($thisDatastore.extensiondata.host | ? {$_.key.value -eq $dshost.keyvalue}).mountinfo.mounted + $Info.state = $hostviewDSAttachState + $output += $info } } } - $AllInfo + write-progress -activity "Checking Datastore access" -completed + $output } } @@ -54,81 +116,32 @@ Function Detach-Datastore { [Parameter(ValueFromPipeline=$true)] $Datastore ) - Process { - if (-not $Datastore) { - Write-Host "No Datastore defined as input" - Exit - } - Foreach ($ds in $Datastore) { - $hostviewDSDiskName = $ds.ExtensionData.Info.vmfs.extent[0].Diskname - if ($ds.ExtensionData.Host) { - $attachedHosts = $ds.ExtensionData.Host - Foreach ($VMHost in $attachedHosts) { - $hostview = Get-View $VMHost.Key - $StorageSys = Get-View $HostView.ConfigManager.StorageSystem - $devices = $StorageSys.StorageDeviceInfo.ScsiLun - Foreach ($device in $devices) { - if ($device.canonicalName -eq $hostviewDSDiskName) { - $LunUUID = $Device.Uuid - Write-Host "Detaching LUN $($Device.CanonicalName) from host $($hostview.Name)..." - $StorageSys.DetachScsiLun($LunUUID); - } - } - } - } - } + Begin{ + $allDatastores = @() } -} - -Function Unmount-Datastore { - [CmdletBinding()] - Param ( - [Parameter(ValueFromPipeline=$true)] - $Datastore - ) Process { - if (-not $Datastore) { - Write-Host "No Datastore defined as input" - Exit - } - Foreach ($ds in $Datastore) { - $hostviewDSDiskName = $ds.ExtensionData.Info.vmfs.extent[0].Diskname - if ($ds.ExtensionData.Host) { - $attachedHosts = $ds.ExtensionData.Host - Foreach ($VMHost in $attachedHosts) { - $hostview = Get-View $VMHost.Key - $StorageSys = Get-View $HostView.ConfigManager.StorageSystem - Write-Host "Unmounting VMFS Datastore $($DS.Name) from host $($hostview.Name)..." - $StorageSys.UnmountVmfsVolume($DS.ExtensionData.Info.vmfs.uuid); - } - } - } + $allDatastores += $Datastore } -} - -Function Mount-Datastore { - [CmdletBinding()] - Param ( - [Parameter(ValueFromPipeline=$true)] - $Datastore - ) - Process { - if (-not $Datastore) { - Write-Host "No Datastore defined as input" - Exit + End { + $allDatastores = $allDatastores | ? {$_.pstypenames -contains "VMware.VimAutomation.ViCore.Impl.V1.DatastoreManagement.DatastoreImpl"} + if (-not $allDatastores){ + Throw "No Datastores found.`nIs ""$Datastore"" a Datastore Object?" } - Foreach ($ds in $Datastore) { - $hostviewDSDiskName = $ds.ExtensionData.Info.vmfs.extent[0].Diskname - if ($ds.ExtensionData.Host) { - $attachedHosts = $ds.ExtensionData.Host - Foreach ($VMHost in $attachedHosts) { - $hostview = Get-View $VMHost.Key - $StorageSys = Get-View $HostView.ConfigManager.StorageSystem - Write-Host "Mounting VMFS Datastore $($DS.Name) on host $($hostview.Name)..." - $StorageSys.MountVmfsVolume($DS.ExtensionData.Info.vmfs.uuid); - } + $allDatastoreNAAs = foreach ($ds in $allDatastores) {$ds.ExtensionData.Info.vmfs.extent[0].diskname} + $allHosts = Get-HostViews -datastore $allDatastores + $j = 1 + foreach ($dsHost in $allHosts){ + #Get all devices on the host that match the list of $allDatastoreNAAs + write-progress -id 1 -activity "Detaching Datastores" -status "Removing device(s) from $($dsHost.hostview.name)" -percentComplete ($j++ / $allHosts.count * 100) + $devices = $dsHost.storagesys.StorageDeviceInfo.ScsiLun | ? {$allDatastoreNAAs -contains $_.canonicalName} + $i = 1 + foreach ($device in $devices){ + write-progress -parentid 1 -activity "Detaching Datastores" -status "Removing device: $(($allDatastores | ? {$_.ExtensionData.Info.vmfs.extent[0].diskname -eq $device.canonicalName}).name)" -percentComplete ($i++ / $allDatastoreNAAs.count * 100) + $LunUUID = $Device.Uuid + $dsHost.storageSys.DetachScsiLun($LunUUID); } } + write-progress -activity "Detaching Datastores" -completed } } @@ -138,47 +151,95 @@ Function Attach-Datastore { [Parameter(ValueFromPipeline=$true)] $Datastore ) + Begin{ + $allDatastores = @() + } Process { - if (-not $Datastore) { - Write-Host "No Datastore defined as input" - Exit + $allDatastores += $Datastore + } + End { + $allDatastores = $allDatastores | ? {$_.pstypenames -contains "VMware.VimAutomation.ViCore.Impl.V1.DatastoreManagement.DatastoreImpl"} + if (-not $allDatastores){ + Throw "No Datastores found.`nIs ""$Datastore"" a Datastore Object?" } - Foreach ($ds in $Datastore) { - $hostviewDSDiskName = $ds.ExtensionData.Info.vmfs.extent[0].Diskname - if ($ds.ExtensionData.Host) { - $attachedHosts = $ds.ExtensionData.Host - Foreach ($VMHost in $attachedHosts) { - $hostview = Get-View $VMHost.Key - $StorageSys = Get-View $HostView.ConfigManager.StorageSystem - $devices = $StorageSys.StorageDeviceInfo.ScsiLun - Foreach ($device in $devices) { - if ($device.canonicalName -eq $hostviewDSDiskName) { - $LunUUID = $Device.Uuid - Write-Host "Attaching LUN $($Device.CanonicalName) to host $($hostview.Name)..." - $StorageSys.AttachScsiLun($LunUUID); - } - } + $allDatastoreNAAs = foreach ($ds in $allDatastores) {$ds.ExtensionData.Info.vmfs.extent[0].diskname} + $allHosts = Get-HostViews -datastore $allDatastores + $j = 1 + foreach ($dsHost in $allHosts){ + #Get all devices on the host that match the list of $allDatastoreNAAs + write-progress -id 1 -activity "Attaching Datastores" -status "Attaching devices to $($dsHost.hostview.name)" -percentComplete ($j++ / $allHosts.count * 100) + $devices = $dsHost.storagesys.StorageDeviceInfo.ScsiLun + $i = 1 + foreach ($device in $devices){ + write-progress -parentid 1 -activity "Attaching Datastores" -status "Attaching device: $($Device.Uuid)" -percentComplete ($i++ / $devices.count * 100) + if ($allDatastoreNAAs -contains $device.canonicalName){ + $LunUUID = $Device.Uuid + $dsHost.storageSys.AttachScsiLun($LunUUID); } } } + write-progress -activity "Attaching Datastores" -completed + } +} + +Function Unmount-Datastore { + [CmdletBinding()] + Param ( + [Parameter(ValueFromPipeline=$true)] + $Datastore + ) + Begin{ + $allDatastores = @() + } + Process { + $allDatastores += $Datastore + } + End { + $allDatastores = $allDatastores | ? {$_.pstypenames -contains "VMware.VimAutomation.ViCore.Impl.V1.DatastoreManagement.DatastoreImpl"} + if (-not $allDatastores){ + Throw "No Datastores found.`nIs ""$Datastore"" a Datastore Object?" + } + $allHosts = Get-HostViews -datastore $allDatastores + $j = 1 + foreach ($dsHost in $allHosts){ + write-progress -id 1 -activity "Unmounting Datastores" -status "Unmounting devices from $($dsHost.hostview.name)" -percentComplete ($j++ / $allHosts.count * 100) + $i = 1 + foreach ($ds in $allDatastores){ + write-progress -parentid 1 -activity "Unmounting Datastores" -status "Unmounting device: $($ds.name)" -percentComplete ($i++ / $allDatastores.count * 100) + $dsHost.storageSys.UnmountVmfsVolume($DS.ExtensionData.Info.vmfs.uuid); + } + } + write-progress -activity "Unmounting Datastores" -completed + } +} + +Function Mount-Datastore { + [CmdletBinding()] + Param ( + [Parameter(ValueFromPipeline=$true)] + $Datastore + ) + Begin{ + $allDatastores = @() + } + Process { + $allDatastores += $Datastore + } + End { + $allDatastores = $allDatastores | ? {$_.pstypenames -contains "VMware.VimAutomation.ViCore.Impl.V1.DatastoreManagement.DatastoreImpl"} + if (-not $allDatastores){ + Throw "No Datastores found.`nIs ""$Datastore"" a Datastore Object?" + } + $allHosts = Get-HostViews -datastore $allDatastores + $j = 0 + foreach ($dsHost in $allHosts){ + write-progress -activity "Mounting Datastores" -status "Mounting devices to $($dsHost.hostview.name)" -percentComplete ($j++ / $allHosts.count * 100) + $i = 1 + foreach ($ds in $allDatastores){ + write-progress -activity "Mounting Datastores" -status "Mounting device: $($DS.ExtensionData.Info.vmfs.uuid)" -percentComplete ($i++ / $allDatastores.count * 100) + $dsHost.storageSys.MountVmfsVolume($DS.ExtensionData.Info.vmfs.uuid); + } + } + write-progress -activity "Mounting Datastores" -completed } } -# -#Get-Datastore | Get-DatastoreMountInfo | Sort Datastore, VMHost | FT -AutoSize -# -#Get-Datastore IX2ISCSI01 | Unmount-Datastore -# -#Get-Datastore IX2ISCSI01 | Get-DatastoreMountInfo | Sort Datastore, VMHost | FT -AutoSize -# -#Get-Datastore IX2iSCSI01 | Mount-Datastore -# -#Get-Datastore IX2iSCSI01 | Get-DatastoreMountInfo | Sort Datastore, VMHost | FT -AutoSize -# -#Get-Datastore IX2iSCSI01 | Detach-Datastore -# -#Get-Datastore IX2iSCSI01 | Get-DatastoreMountInfo | Sort Datastore, VMHost | FT -AutoSize -# -#Get-Datastore IX2iSCSI01 | Attach-datastore -# -#Get-Datastore IX2iSCSI01 | Get-DatastoreMountInfo | Sort Datastore, VMHost | FT -AutoSize -# From e1bc6912fa3c6397ac5b4b63459894f9ddc80dd5 Mon Sep 17 00:00:00 2001 From: mycloudrevolution Date: Tue, 13 Jun 2017 22:51:12 +0200 Subject: [PATCH 043/112] VMware-vCD-Module Create new Org, OrgUser, OrgVDC --- Modules/VMware-vCD-Module/README.md | 23 ++ .../VMware-vCD-Module/VMware-vCD-Module.psd1 | 126 +++++++++ .../examples/OnBoarding.json | 24 ++ .../functions/Invoke-MyOnBoarding.psm1 | 172 ++++++++++++ .../functions/New-MyOrg.psm1 | 107 ++++++++ .../functions/New-MyOrgAdmin.psm1 | 91 +++++++ .../functions/New-MyOrgVdc.psm1 | 252 ++++++++++++++++++ .../media/Invoke-MyOnBoarding.png | Bin 0 -> 40988 bytes .../media/VSCode-Pester-Tests.png | Bin 0 -> 80733 bytes .../tests/VMware-vCD-Module.Tests.ps1 | 35 +++ 10 files changed, 830 insertions(+) create mode 100644 Modules/VMware-vCD-Module/README.md create mode 100644 Modules/VMware-vCD-Module/VMware-vCD-Module.psd1 create mode 100644 Modules/VMware-vCD-Module/examples/OnBoarding.json create mode 100644 Modules/VMware-vCD-Module/functions/Invoke-MyOnBoarding.psm1 create mode 100644 Modules/VMware-vCD-Module/functions/New-MyOrg.psm1 create mode 100644 Modules/VMware-vCD-Module/functions/New-MyOrgAdmin.psm1 create mode 100644 Modules/VMware-vCD-Module/functions/New-MyOrgVdc.psm1 create mode 100644 Modules/VMware-vCD-Module/media/Invoke-MyOnBoarding.png create mode 100644 Modules/VMware-vCD-Module/media/VSCode-Pester-Tests.png create mode 100644 Modules/VMware-vCD-Module/tests/VMware-vCD-Module.Tests.ps1 diff --git a/Modules/VMware-vCD-Module/README.md b/Modules/VMware-vCD-Module/README.md new file mode 100644 index 0000000..a691421 --- /dev/null +++ b/Modules/VMware-vCD-Module/README.md @@ -0,0 +1,23 @@ +VMware-vCD-Module PowerShell Module +=================================== + +![Invoke-MyOnBoarding](/media/Invoke-MyOnBoarding.png) + +# About + +## Project Owner: + +Markus Kraus [@vMarkus_K](https://twitter.com/vMarkus_K) + +## Project WebSite: +[mycloudrevolution.com](http://mycloudrevolution.com/) + +## Project Documentation: + +[Read the Docs](http://vmware-vcd-module.readthedocs.io/) + +## Project Description: + +The 'VMware-vCD-Module' PowerShell Module is focused on the initial craation of VMware vCloud Director Objects like Org, Org User, Org VDC. + + diff --git a/Modules/VMware-vCD-Module/VMware-vCD-Module.psd1 b/Modules/VMware-vCD-Module/VMware-vCD-Module.psd1 new file mode 100644 index 0000000..59a02de --- /dev/null +++ b/Modules/VMware-vCD-Module/VMware-vCD-Module.psd1 @@ -0,0 +1,126 @@ +# +# Modulmanifest für das Modul "PSGet_VMware-vCD-Module" +# +# Generiert von: Markus +# +# Generiert am: 6/11/2017 +# + +@{ + +# Die diesem Manifest zugeordnete Skript- oder Binärmoduldatei. +# RootModule = '' + +# Die Versionsnummer dieses Moduls +ModuleVersion = '0.2.0' + +# ID zur eindeutigen Kennzeichnung dieses Moduls +GUID = '1ef8a2de-ca22-4c88-8cdb-e00f35007d2a' + +# Autor dieses Moduls +Author = 'Markus' + +# Unternehmen oder Hersteller dieses Moduls +CompanyName = 'Unbekannt' + +# Urheberrechtserklärung für dieses Modul +Copyright = '(c) 2017 Markus. Alle Rechte vorbehalten.' + +# Beschreibung der von diesem Modul bereitgestellten Funktionen +# Description = '' + +# Die für dieses Modul mindestens erforderliche Version des Windows PowerShell-Moduls +# PowerShellVersion = '' + +# Der Name des für dieses Modul erforderlichen Windows PowerShell-Hosts +# PowerShellHostName = '' + +# Die für dieses Modul mindestens erforderliche Version des Windows PowerShell-Hosts +# PowerShellHostVersion = '' + +# Die für dieses Modul mindestens erforderliche Microsoft .NET Framework-Version +# DotNetFrameworkVersion = '' + +# Die für dieses Modul mindestens erforderliche Version der CLR (Common Language Runtime) +# CLRVersion = '' + +# Die für dieses Modul erforderliche Prozessorarchitektur ("Keine", "X86", "Amd64"). +# ProcessorArchitecture = '' + +# Die Module, die vor dem Importieren dieses Moduls in die globale Umgebung geladen werden müssen +# RequiredModules = @() + +# Die Assemblys, die vor dem Importieren dieses Moduls geladen werden müssen +# RequiredAssemblies = @() + +# Die Skriptdateien (PS1-Dateien), die vor dem Importieren dieses Moduls in der Umgebung des Aufrufers ausgeführt werden. +# ScriptsToProcess = @() + +# Die Typdateien (.ps1xml), die beim Importieren dieses Moduls geladen werden sollen +# TypesToProcess = @() + +# Die Formatdateien (.ps1xml), die beim Importieren dieses Moduls geladen werden sollen +# FormatsToProcess = @() + +# Die Module, die als geschachtelte Module des in "RootModule/ModuleToProcess" angegebenen Moduls importiert werden sollen. +NestedModules = @('functions\Invoke-MyOnBoarding.psm1', + 'functions\New-MyOrg.psm1', + 'functions\New-MyOrgAdmin.psm1', + 'functions\New-MyOrgVdc.psm1') + +# Aus diesem Modul zu exportierende Funktionen +FunctionsToExport = 'Invoke-MyOnBoarding', 'New-MyOrg', 'New-MyOrgAdmin', 'New-MyOrgVdc' + +# Aus diesem Modul zu exportierende Cmdlets +CmdletsToExport = '*' + +# Die aus diesem Modul zu exportierenden Variablen +VariablesToExport = '*' + +# Aus diesem Modul zu exportierende Aliase +AliasesToExport = '*' + +# Aus diesem Modul zu exportierende DSC-Ressourcen +# DscResourcesToExport = @() + +# Liste aller Module in diesem Modulpaket +# ModuleList = @() + +# Liste aller Dateien in diesem Modulpaket +# FileList = @() + +# Die privaten Daten, die an das in "RootModule/ModuleToProcess" angegebene Modul übergeben werden sollen. Diese können auch eine PSData-Hashtabelle mit zusätzlichen von PowerShell verwendeten Modulmetadaten enthalten. +PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + # Tags = @() + + # A URL to the license for this module. + # LicenseUri = '' + + # A URL to the main website for this project. + # ProjectUri = '' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + # ReleaseNotes = '' + + # External dependent modules of this module + # ExternalModuleDependencies = '' + + } # End of PSData hashtable + +} # End of PrivateData hashtable + +# HelpInfo-URI dieses Moduls +# HelpInfoURI = '' + +# Standardpräfix für Befehle, die aus diesem Modul exportiert werden. Das Standardpräfix kann mit "Import-Module -Prefix" überschrieben werden. +# DefaultCommandPrefix = '' + +} + diff --git a/Modules/VMware-vCD-Module/examples/OnBoarding.json b/Modules/VMware-vCD-Module/examples/OnBoarding.json new file mode 100644 index 0000000..ecd1f88 --- /dev/null +++ b/Modules/VMware-vCD-Module/examples/OnBoarding.json @@ -0,0 +1,24 @@ +{ +"Org": { + "Name":"TestOrg", + "FullName": "Test Org", + "Description":"Automation Test Org" + }, +"OrgAdmin": { + "Name":"TestOrgAdmin", + "Pasword": "myPassword1!", + "FullName":"Test OrgAdmin", + "EmailAddress":"test@admin.org" + }, +"OrgVdc": { + "Name":"TestOrgVdc", + "FixedSize": "M", + "CPULimit": "1000", + "MEMLimit":"1024", + "StorageLimit":"1024", + "StorageProfile":"Standard-DC01", + "ProviderVDC":"Provider-VDC-DC01", + "NetworkPool":"Provider-VDC-DC01-NetPool", + "ExternalNetwork": "External-OrgVdcNet" + } +} \ No newline at end of file diff --git a/Modules/VMware-vCD-Module/functions/Invoke-MyOnBoarding.psm1 b/Modules/VMware-vCD-Module/functions/Invoke-MyOnBoarding.psm1 new file mode 100644 index 0000000..4b5e209 --- /dev/null +++ b/Modules/VMware-vCD-Module/functions/Invoke-MyOnBoarding.psm1 @@ -0,0 +1,172 @@ +#Requires -Version 4 +#Requires -Modules VMware.VimAutomation.Cloud, @{ModuleName="VMware.VimAutomation.Cloud";ModuleVersion="6.3.0.0"} +Function Invoke-MyOnBoarding { +<# +.SYNOPSIS + Creates all vCD Objecst for a new IAAS Customer + +.DESCRIPTION + Creates all vCD Objects for a new IAAS Customer + + All Objects are: + * Org + * Default Org Admin + * Org VDC + ** Private Catalog + ** Optional Bridged Network + + JSON Config Example: + + { + "Org": { + "Name":"TestOrg", + "FullName": "Test Org", + "Description":"Automation Test Org" + }, + "OrgAdmin": { + "Name":"TestOrgAdmin", + "Pasword": "myPassword1!", + "FullName":"Test OrgAdmin", + "EmailAddress":"test@admin.org" + }, + "OrgVdc": { + "Name":"TestOrgVdc", + "FixedSize": "M", + "CPULimit": "1000", + "MEMLimit":"1000", + "StorageLimit":"1000", + "StorageProfile":"Standard-DC01", + "ProviderVDC":"Provider-VDC-DC01", + "NetworkPool":"Provider-VDC-DC01-NetPool", + "ExternalNetwork": "External_OrgVdcNet" + } + } + +.NOTES + File Name : Invoke-MyOnBoarding.ps1 + Author : Markus Kraus + Version : 1.2 + State : Ready + +.LINK + https://mycloudrevolution.com/ + +.EXAMPLE + Invoke-MyOnBoarding -ConfigFile ".\OnBoarding.json" -Enabled:$true + +.EXAMPLE + Invoke-MyOnBoarding -ConfigFile ".\OnBoarding.json" -Enabled:$false + +.PARAMETER ConfigFile + Full Path to the JSON Config File + +.PARAMETER Enabled + Should the Customer be enabled after creation + + Default: $False + +#> + Param ( + [Parameter(Mandatory=$True, ValueFromPipeline=$False, HelpMessage="Full Path to the JSON Config File")] + [ValidateNotNullorEmpty()] + [String] $ConfigFile, + [Parameter(Mandatory=$False, ValueFromPipeline=$False, HelpMessage="Should the Customer be enabled after creation")] + [ValidateNotNullorEmpty()] + [Switch]$Enabled + ) + Process { + + $Valid = $true + + Write-Verbose "## Import JSON Config" + Write-Host "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") Importing JSON Config...`n" + $Configs = Get-Content -Raw -Path $ConfigFile -ErrorAction Continue | ConvertFrom-Json -ErrorAction Continue + + if (!($Configs)) { + $Valid = $false + Write-Host "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") Importing JSON Config Failed" -ForegroundColor Red + } + else { + Write-Host "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") Importing JSON Config OK" -ForegroundColor Green + } + + if ($Valid) { + try{ + Write-Verbose "## Create Org" + Write-Host "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") Creating new Org...`n" -ForegroundColor Yellow + $Trash = New-MyOrg -Name $Configs.Org.Name -FullName $Configs.Org.Fullname -Description $Configs.Org.Description -Enabled:$Enabled + Write-Host "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") Creating new Org OK" -ForegroundColor Green + Get-Org -Name $Configs.Org.Name | Select-Object Name, FullName, Enabled | Format-Table -AutoSize + } + catch { + $Valid = $false + Write-Host "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") Creating new Org Failed" -ForegroundColor Red + } + } + + if ($Valid) { + try{ + Write-Verbose "## Create OrgAdmin" + Write-Host "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") Creating new OrgAdmin...`n" -ForegroundColor Yellow + $Trash = New-MyOrgAdmin -Name $Configs.OrgAdmin.Name -Pasword $Configs.OrgAdmin.Pasword -FullName $Configs.OrgAdmin.FullName -EmailAddress $Configs.OrgAdmin.EmailAddress -Org $Configs.Org.Name -Enabled:$Enabled + Write-Host "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") Creating new OrgAdmin OK" -ForegroundColor Green + Get-CIUser -Org $Configs.Org.Name -Name $Configs.OrgAdmin.Name | Select-Object Name, FullName, Email | Format-Table -AutoSize + } + catch { + $Valid = $false + Write-Host "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") Creating new OrgAdmin Failed" -ForegroundColor Red + } + } + if ($Valid) { + try{ + Write-Verbose "## Create OrgVdc" + Write-Host "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") Creating new OrgVdc...`n" -ForegroundColor Yellow + + if ($Configs.OrgVdc.FixedSize){ + + Write-Host "Fixed Size (T-Shirt Size) '$($Configs.OrgVdc.FixedSize)' Org VDC Requested!" + + switch ($Configs.OrgVdc.FixedSize) { + M { + [String]$CPULimit = 36000 + [String]$MEMLimit = 122880 + [String]$StorageLimit = 1048576 + } + L { + [String]$CPULimit = 36000 + [String]$MEMLimit = 245760 + [String]$StorageLimit = 1048576 + } + default {throw "Invalid T-Shirt Size!"} + } + + } + else{ + Write-Host "Custom Org VDC Size Requested!" + + $CPULimit = $Configs.OrgVdc.CPULimit + $MEMLimit = $Configs.OrgVdc.MEMLimit + $StorageLimit = $Configs.OrgVdc.StorageLimit + + } + + if ($Configs.OrgVdc.ExternalNetwork){ + $Trash = New-MyOrgVdc -Name $Configs.OrgVdc.Name -CPULimit $CPULimit -MEMLimit $MEMLimit -StorageLimit $StorageLimit -Networkpool $Configs.OrgVdc.NetworkPool -StorageProfile $Configs.OrgVdc.StorageProfile -ProviderVDC $Configs.OrgVdc.ProviderVDC -ExternalNetwork $Configs.OrgVdc.ExternalNetwork -Org $Configs.Org.Name -Enabled:$Enabled + } + else { + $Trash = New-MyOrgVdc -Name $Configs.OrgVdc.Name -CPULimit $CPULimit -MEMLimit $MEMLimit -StorageLimit $StorageLimit -Networkpool $Configs.OrgVdc.NetworkPool -StorageProfile $Configs.OrgVdc.StorageProfile -ProviderVDC $Configs.OrgVdc.ProviderVDC -Org $Configs.Org.Name -Enabled:$Enabled + } + Write-Host "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") Creating new OrgVdc OK" -ForegroundColor Green + Get-OrgVdc -Org $Configs.Org.Name -Name $Configs.OrgVdc.Name | Select-Object Name, Enabled, CpuAllocationGhz, MemoryLimitGB, StorageLimitGB, AllocationModel, ThinProvisioned, UseFastProvisioning, ` + @{N="StorageProfile";E={$_.ExtensionData.VdcStorageProfiles.VdcStorageProfile.Name}}, ` + @{N='VCpuInMhz';E={$_.ExtensionData.VCpuInMhz}} | Format-Table -AutoSize + } + catch { + $Valid = $false + Write-Host "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") Creating new OrgVdc Failed" -ForegroundColor Red + } + } + + Write-Output "Overall Execution was Valid: $Valid" + } +} diff --git a/Modules/VMware-vCD-Module/functions/New-MyOrg.psm1 b/Modules/VMware-vCD-Module/functions/New-MyOrg.psm1 new file mode 100644 index 0000000..69565bb --- /dev/null +++ b/Modules/VMware-vCD-Module/functions/New-MyOrg.psm1 @@ -0,0 +1,107 @@ +#Requires -Version 4 +#Requires -Modules VMware.VimAutomation.Cloud, @{ModuleName="VMware.VimAutomation.Cloud";ModuleVersion="6.3.0.0"} +Function New-MyOrg { +<# +.SYNOPSIS + Creates a new vCD Org with Default Parameters + +.DESCRIPTION + Creates a new vCD Org with Default Parameters. + + Default Parameters are: + * Catalog Publishing + * Catalog Subscription + * VM Quota + * Stored VM Quota + * VM Lease Time + * Stored VM Lease Time + * Password Policy Settings + +.NOTES + File Name : New-MyOrg.ps1 + Author : Markus Kraus + Version : 1.1 + State : Ready + +.LINK + https://mycloudrevolution.com/ + +.EXAMPLE + New-MyOrg -Name "TestOrg" -FullName "Test Org" -Description "PowerCLI Test Org" + +.PARAMETER Name + Name of the New Org as String + +.PARAMETER FullName + Full Name of the New Org as String + +.PARAMETER Description + Description of the New Org as String + +.PARAMETER Enabled + Should the New Org be enabled after creation + + Default:$false + +#> + Param ( + [Parameter(Mandatory=$True, ValueFromPipeline=$False, HelpMessage="Name of the New Org as string")] + [ValidateNotNullorEmpty()] + [String] $Name, + [Parameter(Mandatory=$True, ValueFromPipeline=$False, HelpMessage="Full Name of the New Org as string")] + [ValidateNotNullorEmpty()] + [String] $FullName, + [Parameter(Mandatory=$False, ValueFromPipeline=$False, HelpMessage="Description of the New Org as string")] + [ValidateNotNullorEmpty()] + [String] $Description, + [Parameter(Mandatory=$False, ValueFromPipeline=$False, HelpMessage="Should the New Org be enabled after creation")] + [ValidateNotNullorEmpty()] + [Switch]$Enabled + ) + Process { + $vcloud = $DefaultCIServers[0].ExtensionData + + ## Create Objects + $AdminOrg = New-Object VMware.VimAutomation.Cloud.Views.AdminOrg + $orgGeneralSettings = New-Object VMware.VimAutomation.Cloud.Views.OrgGeneralSettings + $orgOrgLeaseSettings = New-Object VMware.VimAutomation.Cloud.Views.OrgLeaseSettings + $orgOrgVAppTemplateLeaseSettings = New-Object VMware.VimAutomation.Cloud.Views.OrgVAppTemplateLeaseSettings + $orgOrgPasswordPolicySettings = New-Object VMware.VimAutomation.Cloud.Views.OrgPasswordPolicySettings + $orgSettings = New-Object VMware.VimAutomation.Cloud.Views.OrgSettings + + ## Admin Settings + $adminOrg.Name = $name + $adminOrg.FullName = $FullName + $adminOrg.Description = $description + $adminOrg.IsEnabled = $Enabled + + ## Org Setting + ### General Org Settings + $orgGeneralSettings.CanPublishCatalogs = $False + $orgGeneralSettings.CanPublishExternally = $False + $orgGeneralSettings.CanSubscribe = $True + $orgGeneralSettings.DeployedVMQuota = 0 + $orgGeneralSettings.StoredVmQuota = 0 + $orgSettings.OrgGeneralSettings = $orgGeneralSettings + ### vApp Org Setting + $orgOrgLeaseSettings.DeleteOnStorageLeaseExpiration = $false + $orgOrgLeaseSettings.DeploymentLeaseSeconds = 0 + $orgOrgLeaseSettings.StorageLeaseSeconds = 0 + $orgSettings.VAppLeaseSettings = $orgOrgLeaseSettings + ### vApp Template Org Setting + $orgOrgVAppTemplateLeaseSettings.DeleteOnStorageLeaseExpiration = $false + $orgOrgVAppTemplateLeaseSettings.StorageLeaseSeconds = 0 + $orgSettings.VAppTemplateLeaseSettings = $orgOrgVAppTemplateLeaseSettings + ### PasswordPolicySettings Org Setting + $orgOrgPasswordPolicySettings.AccountLockoutEnabled = $True + $orgOrgPasswordPolicySettings.InvalidLoginsBeforeLockout = 5 + $orgOrgPasswordPolicySettings.AccountLockoutIntervalMinutes = 30 + $orgSettings.OrgPasswordPolicySettings = $orgOrgPasswordPolicySettings + + $adminOrg.Settings = $orgSettings + + $CreateOrg = $vcloud.CreateOrg($adminOrg) + + Get-Org -Name $name | Format-Table -AutoSize + } +} diff --git a/Modules/VMware-vCD-Module/functions/New-MyOrgAdmin.psm1 b/Modules/VMware-vCD-Module/functions/New-MyOrgAdmin.psm1 new file mode 100644 index 0000000..26087c0 --- /dev/null +++ b/Modules/VMware-vCD-Module/functions/New-MyOrgAdmin.psm1 @@ -0,0 +1,91 @@ +#Requires -Version 4 +#Requires -Modules VMware.VimAutomation.Cloud, @{ModuleName="VMware.VimAutomation.Cloud";ModuleVersion="6.3.0.0"} +Function New-MyOrgAdmin { +<# +.SYNOPSIS + Creates a new vCD Org Admin with Default Parameters + +.DESCRIPTION + Creates a new vCD Org Admin with Default Parameters + + Default Parameters are: + * User Role + +.NOTES + File Name : New-MyOrgAdmin.ps1 + Author : Markus Kraus + Version : 1.1 + State : Ready + +.LINK + https://mycloudrevolution.com/ + +.EXAMPLE + New-MyOrgAdmin -Name "OrgAdmin" -Pasword "Anfang!!" -FullName "Org Admin" -EmailAddress "OrgAdmin@TestOrg.local" -Org "TestOrg" + +.PARAMETER Name + Name of the New Org Admin as String + +.PARAMETER FullName + Full Name of the New Org Admin as String + +.PARAMETER Password + Password of the New Org Admin as String + +.PARAMETER EmailAddress + EmailAddress of the New Org Admin as String + +.PARAMETER Enabled + Should the New Org be enabled after creation + + Default:$false + +.PARAMETER Org + Org where the new Org Admin should be created as string + +#> + Param ( + [Parameter(Mandatory=$True, ValueFromPipeline=$False, HelpMessage="Name of the New Org Admin as String")] + [ValidateNotNullorEmpty()] + [String] $Name, + [Parameter(Mandatory=$True, ValueFromPipeline=$False, HelpMessage="Password of the New Org Admin as String")] + [ValidateNotNullorEmpty()] + [String] $Pasword, + [Parameter(Mandatory=$True, ValueFromPipeline=$False, HelpMessage="Full Name of the New Org Admin as String")] + [ValidateNotNullorEmpty()] + [String] $FullName, + [Parameter(Mandatory=$True, ValueFromPipeline=$False, HelpMessage="EmailAddress of the New Org Admin as String")] + [ValidateNotNullorEmpty()] + [String] $EmailAddress, + [Parameter(Mandatory=$True, ValueFromPipeline=$False, HelpMessage="Org where the new Org Admin should be created as string")] + [ValidateNotNullorEmpty()] + [String] $Org, + [Parameter(Mandatory=$False, ValueFromPipeline=$False, HelpMessage="Should the New Org be enabled after creation")] + [ValidateNotNullorEmpty()] + [Switch]$Enabled + ) + Process { + + ## Create Objects + $OrgED = (Get-Org $Org).ExtensionData + $orgAdminUser = New-Object VMware.VimAutomation.Cloud.Views.User + + ## Settings + $orgAdminUser.Name = $Name + $orgAdminUser.FullName = $FullName + $orgAdminUser.EmailAddress = $EmailAddress + $orgAdminUser.Password = $Pasword + $orgAdminUser.IsEnabled = $Enabled + + $vcloud = $DefaultCIServers[0].ExtensionData + + ## Find Role + $orgAdminRole = $vcloud.RoleReferences.RoleReference | Where-Object {$_.Name -eq "Organization Administrator"} + $orgAdminUser.Role = $orgAdminRole + + ## Create User + $user = $orgED.CreateUser($orgAdminUser) + + Get-CIUser -Org $Org -Name $Name | Format-Table -AutoSize + } +} diff --git a/Modules/VMware-vCD-Module/functions/New-MyOrgVdc.psm1 b/Modules/VMware-vCD-Module/functions/New-MyOrgVdc.psm1 new file mode 100644 index 0000000..783ad5e --- /dev/null +++ b/Modules/VMware-vCD-Module/functions/New-MyOrgVdc.psm1 @@ -0,0 +1,252 @@ +#Requires -Version 4 +#Requires -Modules VMware.VimAutomation.Cloud, @{ModuleName="VMware.VimAutomation.Cloud";ModuleVersion="6.3.0.0"} +Function New-MyOrgVdc { +<# +.SYNOPSIS + Creates a new vCD Org VDC with Default Parameters + +.DESCRIPTION + Creates a new vCD Org VDC with Default Parameters + + Default Parameters are: + * Allocation Model + * Network Quota + * VM Quota + * 'vCpu In Mhz' + * Fast Provisioning + * Thin Provisioning + * private Catalog + +.NOTES + File Name : New-MyOrgVdc.ps1 + Author : Markus Kraus + Version : 1.2 + State : Ready + +.LINK + https://mycloudrevolution.com/ + +.EXAMPLE + New-MyOrgVdc -Name "TestVdc" -CPULimit 1000 -MEMLimit 1000 -StorageLimit 1000 -StorageProfile "Standard-DC01" -NetworkPool "NetworkPool-DC01" -ProviderVDC "Provider-VDC-DC01" -Org "TestOrg" -ExternalNetwork "External_OrgVdcNet" + +.EXAMPLE + New-MyOrgVdc -Name "TestVdc" -CPULimit 1000 -MEMLimit 1000 -StorageLimit 1000 -StorageProfile "Standard-DC01" -NetworkPool "NetworkPool-DC01" -ProviderVDC "Provider-VDC-DC01" -Org "TestOrg" + +.PARAMETER Name + Name of the New Org VDC as String + +.PARAMETER CPULimit + CPU Limit (MHz) of the New Org VDC as String + +.PARAMETER MEMLimit + Memory Limit (MB) of the New Org VDC as String + +.PARAMETER StorageLimit + Storage Limit (MB) of the New Org VDC as String + +.PARAMETER StorageProfile + Storage Profile of the New Org VDC as String + +.PARAMETER NetworkPool + Network Pool of the New Org VDC as String + +.PARAMETER ExternalNetwork + Optional External Network of the New Org VDC as String + +.PARAMETER Enabled + Should the New Org VDC be enabled after creation + + Default:$false + + Note: If an External Network is requested the Org VDC will be enabled during External Network Configuration + +.PARAMETER ProviderVDC + ProviderVDC where the new Org VDC should be created as string + +.PARAMETER Org + Org where the new Org VDC should be created as string + +.PARAMETER Timeout + Timeout for teh Org VDC to get Ready + + Default: 120s + +#> + Param ( + [Parameter(Mandatory=$True, ValueFromPipeline=$False, HelpMessage="Name of the New Org VDC as String")] + [ValidateNotNullorEmpty()] + [String] $Name, + [Parameter(Mandatory=$True, ValueFromPipeline=$False, HelpMessage="CPU Limit (MHz) of the New Org VDC as String")] + [ValidateNotNullorEmpty()] + [int] $CPULimit, + [Parameter(Mandatory=$True, ValueFromPipeline=$False, HelpMessage="Memory Limit (MB) of the New Org VDC as String")] + [ValidateNotNullorEmpty()] + [int] $MEMLimit, + [Parameter(Mandatory=$True, ValueFromPipeline=$False, HelpMessage="Storage Limit (MB) of the New Org VDC as String")] + [ValidateNotNullorEmpty()] + [int] $StorageLimit, + [Parameter(Mandatory=$True, ValueFromPipeline=$False, HelpMessage="Storage Profile of the New Org VDC as String")] + [ValidateNotNullorEmpty()] + [String] $StorageProfile, + [Parameter(Mandatory=$True, ValueFromPipeline=$False, HelpMessage="Network Pool of the New Org VDC as String")] + [ValidateNotNullorEmpty()] + [String] $NetworkPool, + [Parameter(Mandatory=$False, ValueFromPipeline=$False, HelpMessage="Optional External Network of the New Org VDC as String")] + [ValidateNotNullorEmpty()] + [String] $ExternalNetwork, + [Parameter(Mandatory=$False, ValueFromPipeline=$False, HelpMessage="Should the New Org VDC be enabled after creation")] + [ValidateNotNullorEmpty()] + [Switch]$Enabled, + [Parameter(Mandatory=$True, ValueFromPipeline=$False, HelpMessage="ProviderVDC where the new Org VDC should be created as string")] + [ValidateNotNullorEmpty()] + [String] $ProviderVDC, + [Parameter(Mandatory=$True, ValueFromPipeline=$False, HelpMessage="Org where the new Org VDC should be created as string")] + [ValidateNotNullorEmpty()] + [String] $Org, + [Parameter(Mandatory=$False, ValueFromPipeline=$False,HelpMessage="Timeout for teh Org VDC to get Ready")] + [ValidateNotNullorEmpty()] + [int] $Timeout = 120 + ) + Process { + ## Create Objects and all Settings + Write-Verbose "Create Objects and all Settings" + $adminVdc = New-Object VMware.VimAutomation.Cloud.Views.AdminVdc + $adminVdc.Name = $name + $adminVdc.IsEnabled = $Enabled + $OrgVdcproviderVdc = Get-ProviderVdc $ProviderVDC + $providerVdcRef = New-Object VMware.VimAutomation.Cloud.Views.Reference + $providerVdcRef.Href = $OrgVdcproviderVdc.Href + $adminVdc.ProviderVdcReference = $providerVdcRef + $adminVdc.AllocationModel = "AllocationPool" + $adminVdc.ComputeCapacity = New-Object VMware.VimAutomation.Cloud.Views.ComputeCapacity + $adminVdc.ComputeCapacity.Cpu = New-Object VMware.VimAutomation.Cloud.Views.CapacityWithUsage + $adminVdc.ComputeCapacity.Cpu.Units = "MHz" + $adminVdc.ComputeCapacity.Cpu.Limit = $CPULimit + $adminVdc.ComputeCapacity.Cpu.Allocated = $CPULimit + $adminVdc.ComputeCapacity.Memory = New-Object VMware.VimAutomation.Cloud.Views.CapacityWithUsage + $adminVdc.ComputeCapacity.Memory.Units = "MB" + $adminVdc.ComputeCapacity.Memory.Limit = $MEMLimit + $adminVdc.ComputeCapacity.Memory.Allocated = $MEMLimit + $adminVdc.StorageCapacity = New-Object VMware.VimAutomation.Cloud.Views.CapacityWithUsage + $adminVdc.StorageCapacity.Units = "MB" + $adminVdc.StorageCapacity.Limit = $StorageLimit + $adminVdc.NetworkQuota = 10 + $adminVdc.VmQuota = 0 + $adminVdc.VCpuInMhz = 1000 + $adminVdc.VCpuInMhz2 = 1000 + $adminVdc.UsesFastProvisioning = $false + $adminVdc.IsThinProvision = $true + + ## Create Org vDC + Write-Verbose "Create Org vDC" + $OrgED = (Get-Org $Org).ExtensionData + $orgVdc = $orgED.CreateVdc($adminVdc) + + ## Wait for getting Ready + Write-Verbose "Wait for getting Ready" + $i = 0 + while(($orgVdc = Get-OrgVdc -Name $Name).Status -eq "NotReady"){ + $i++ + Start-Sleep 2 + if($i -gt $Timeout) { Write-Error "Creating Org Failed."; break} + Write-Progress -Activity "Creating Org" -Status "Wait for Org to become Ready..." + } + Write-Progress -Activity "Creating Org" -Completed + Start-Sleep 2 + + ## Search given Storage Profile + Write-Verbose "Search given Storage Profile" + $ProVdcStorageProfile = search-cloud -QueryType ProviderVdcStorageProfile -Name $StorageProfile | Get-CIView + + ## Create Storage Profile Object with Settings + Write-Verbose "Create Storage Profile Object with Settings" + $spParams = new-object VMware.VimAutomation.Cloud.Views.VdcStorageProfileParams + $spParams.Limit = $StorageLimit + $spParams.Units = "MB" + $spParams.ProviderVdcStorageProfile = $ProVdcStorageProfile.href + $spParams.Enabled = $true + $spParams.Default = $true + $UpdateParams = new-object VMware.VimAutomation.Cloud.Views.UpdateVdcStorageProfiles + $UpdateParams.AddStorageProfile = $spParams + + ## Update Org vDC + $orgVdc = Get-OrgVdc -Name $name + $orgVdc.ExtensionData.CreateVdcStorageProfile($UpdateParams) + + ## Wait for getting Ready + Write-Verbose "Wait for getting Ready" + while(($orgVdc = Get-OrgVdc -Name $name).Status -eq "NotReady"){ + $i++ + Start-Sleep 1 + if($i -gt $Timeout) { Write-Error "Update Org Failed."; break} + Write-Progress -Activity "Updating Org" -Status "Wait for Org to become Ready..." + } + Write-Progress -Activity "Updating Org" -Completed + Start-Sleep 1 + + ## Search Any-StorageProfile + Write-Verbose "Search Any-StorageProfile" + $orgvDCAnyProfile = search-cloud -querytype AdminOrgVdcStorageProfile | Where-Object {($_.Name -match '\*') -and ($_.VdcName -eq $orgVdc.Name)} | Get-CIView + + ## Disable Any-StorageProfile + Write-Verbose "Disable Any-StorageProfile" + $orgvDCAnyProfile.Enabled = $False + $return = $orgvDCAnyProfile.UpdateServerData() + + ## Remove Any-StorageProfile + Write-Verbose "Remove Any-StorageProfile" + $ProfileUpdateParams = new-object VMware.VimAutomation.Cloud.Views.UpdateVdcStorageProfiles + $ProfileUpdateParams.RemoveStorageProfile = $orgvDCAnyProfile.href + $remove = $orgvdc.extensiondata.CreatevDCStorageProfile($ProfileUpdateParams) + + ## Wait for getting Ready + Write-Verbose "Wait for getting Ready" + while(($orgVdc = Get-OrgVdc -Name $name).Status -eq "NotReady"){ + $i++ + Start-Sleep 1 + if($i -gt $Timeout) { Write-Error "Update Org Failed."; break} + Write-Progress -Activity "Updating Org" -Status "Wait for Org to become Ready..." + } + Write-Progress -Activity "Updating Org" -Completed + Start-Sleep 1 + + ## Set NetworkPool for correct location + Write-Verbose "Set NetworkPool for correct location" + $orgVdc = Get-OrgVdc -Name $name + $ProVdcNetworkPool = Get-NetworkPool -ProviderVdc $ProviderVDC -Name $NetworkPool + $set = Set-OrgVdc -OrgVdc $orgVdc -NetworkPool $ProVdcNetworkPool -NetworkMaxCount "10" + + ## Create private Catalog + Write-Verbose "Create private Catalog Object" + $OrgCatalog = New-Object VMware.VimAutomation.Cloud.Views.AdminCatalog + $OrgCatalog.name = "$Org Private Catalog" + if (!(Get-Org $org | Get-Catalog -Name $OrgCatalog.name -ErrorAction SilentlyContinue)) { + Write-Verbose "Create private Catalog" + $CreateCatalog = (Get-Org $org | Get-CIView).CreateCatalog($OrgCatalog) + $AccessControlRule = New-CIAccessControlRule -Entity $CreateCatalog.name -EveryoneInOrg -AccessLevel ReadWrite -Confirm:$False + } + else { + Write-Output "Catalog '$($OrgCatalog.name)' aleady exists!" + } + + ## Create a direct connect network + if ($ExternalNetwork) { + Write-Verbose "Create a direct connect network" + Write-Output "Org VDC '$Name' needs to be enabled to add an external Network!" + $EnableOrgVdc = Set-OrgVdc -OrgVdc $Name -Enabled:$True + $orgVdcView = Get-OrgVdc $Name | Get-CIView + $extNetwork = $_.externalnetwork + $extNetwork = Get-ExternalNetwork | Get-CIView | Where-Object {$_.name -eq $ExternalNetwork} + $orgNetwork = new-object vmware.vimautomation.cloud.views.orgvdcnetwork + $orgNetwork.name = $ExternalNetwork + $orgNetwork.Configuration = New-Object VMware.VimAutomation.Cloud.Views.NetworkConfiguration + $orgNetwork.Configuration.FenceMode = 'bridged' + $orgNetwork.configuration.ParentNetwork = New-Object vmware.vimautomation.cloud.views.reference + $orgNetwork.configuration.ParentNetwork.href = $extNetwork.href + + $result = $orgVdcView.CreateNetwork($orgNetwork) + } + + Get-OrgVdc -Name $name | Format-Table -AutoSize + } +} diff --git a/Modules/VMware-vCD-Module/media/Invoke-MyOnBoarding.png b/Modules/VMware-vCD-Module/media/Invoke-MyOnBoarding.png new file mode 100644 index 0000000000000000000000000000000000000000..9f60a93508c54e47418fa493ab0b730f8b003a3f GIT binary patch literal 40988 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 %IBM`Y0F&%$%@Mz>h~Xp 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 Y0@TidQEK`NO#t%qyOtiTT*EfvACt(Yp#T5? literal 0 HcmV?d00001 diff --git a/Modules/VMware-vCD-Module/media/VSCode-Pester-Tests.png b/Modules/VMware-vCD-Module/media/VSCode-Pester-Tests.png new file mode 100644 index 0000000000000000000000000000000000000000..2669d07b371a24bdfc72894208ab301fa52d75a6 GIT binary patch literal 80733 zcmdqIWl&t*x(0|_f?Ejg9w4~8I|L6J2n2V6HWu7HxVyW%2X}`cK^u4L+4;`7=iK=< zH8t~NYE~89)m^ol?zLWd8mCM>8{9Cks1g0}UkL5=gMDgy^U51}7cJo_IrvuP;6^TfUJPZ9ihBJ_L%0 ziD8l9AtU!Qz+oAM47r~0aJg=WvPU)bU&y`X+5n4u{P~kA-kav_4m5gkqiw`M)bX^iw?5 ze-{&tnqu7lx;W+k;^KiSS*6UD`!YjFp`AU4Mc32P+S<#N;qxlCvNyMdA%?)s*xl<> z7i6vLOlC@mmWfz@U3E-+tgj8Xb`+8z%mF5Z;Y>T*b?AG=4vin0i;I=A+{PcYa zao6)ah|BI6eFgQM|KSznbrS-7&Qn)8ZxT}sQp?8oV_H$qfB!6B0?tt8=jRu&9q4JR zPkFWRY#~QJTHxUr@;tr?dA*aH@h`nmob-lJ$3FIvLaxuhD8mzbm6v>bIgHGj-L%7@ zHD#Dy=SN)fCD5+LGsEq=Z-S`QK3&&MLUu?YDTuUh)Z%dtN=-S;8>8!Q#WUM9by6P9eY*!NCsOd#_h%U-)_FX zB%c{Pb?Y8n)n!4pN88TCA;33&;?D9)l4?gB1XIzarq-B4NNqN*0IhH|hGK}}S9YDPZ+E;%|N+?X~T#TW;zu$F#_ap8D`QK#> zjaed_kc&#nJHA~6mjM=jIqWic{e7GSgST~D##M&^Sp>b@{jd}0;o7g&Yex~fm~#WY zhJz>>Ym>-y-nyOjZ=bz zm(?Q4akYeqfrh(d5-zq1MjQuZYj5$qZ?K}x;NdsnCt{yqD5|Rb>duM%BQ57)dH*`1 z&c{6NlJG|GT9H1{pJ>qIEbv8MR|wCZPwznA?^ayilS`5+)aPG{UCWL@f5{t~*xubS zXHm@>o3FB!R*=$JN`=#UxUS3ErV{mpwx576mx3#1{-g-ABtpRvpSRP)0F9R+68$wcVs%&fx=}?C#-*iy-)J`3-UY}UiG|WM>U;;bxxX! z!gJw<9+mmHSLIW|&-MCvlrpyVbNc-=_!mUM^?tGI1$^X5T3Wuc(FO@86wdqc8mIcw zljybyIp3Ka9;^RFPS@@U4%qL6%wz^gZ$h@d9C6IBpxvO7yq;VUVnC~?w-z!5PJCiv zMP?6C;7_+8D_fqMXskuu+MjYanJ8tvp8UW|u!=#R7>GpV^L&V(q>>v*+V%TGn0UHy z2H{?yIOH)L1Ze_6sL{?RH$eoeI=L-(TWk&oo3+`H$TY~~ZK74gFsWI zLxUjWRtLU`^JRSQWuKddCVP_7wKJbH$-0K~aoVhc>wo3E1u}p?OFh6!DUqrMz4J9#gFIA+EVf#)EQRspWGW1oD&ypW9!VL&E$Ry3B_~IuUCxfXec82Rdh< z@f8o*Ij_eieYR!kc>JQn%TV|%N3nJE?x5qC^jo1skuu3tzylX;s{T7Ihqqqmzl%zH zrjRj}KlEftNq>8rkM}YydWug?F;VBh^iR6{Ml()I-Ms_de{q9gl#yA@*!euQg+s2H zlMIeYe|)5>*wm4*!VurDAtnBz=bI|f z-!dfZc>UIUxI1AgM!R|tG5I(ANBPH9Gnt^THa^d*n>p|OE}bU3EZnhV_h&aBghnLJ zGBImOIW3uKIo)@_ZoCrIB1uw7P_!W;d@`@lWw+Px<~ohgcBAT7{WxJR^HUp7q+QRr zrt;~}kF;8w(h5BJv4OBkpJUH&+gj-=)dT5hW2j-J&a+;g@#j<_*W*Rg%+lnqXOIiV zgYMr;BlPbzek46Yh)W1?^vO%ix(m7c()$GjZ&*!~1QR?X*Y*ur>C36762YUE3%2=l z&`kuwo}qkr#M`__urO$tKerW$sZtwrf^zAsNS_wiO}{?}GZtdm2C4Ajv+~^-*vv)& z^)GJ9?S{A?q=FXFLqcp6y89Hb+*NG1{*br0sp*-yFD?3`drDKfCvMa(sLxN2@0WWc z#=+?GC?8x%7&0aiEShRIPvD=>(BK2e^P8${==GmD`rA`kbsVyyTkJimB9Rhr>In5a zT=p2ySoEQF>7brlG~Xh8)PUv6mMW3zky^0DPQQmzT$dXd9ecCp3ocHfrl_RJA@fsy zhOU$l;PbxpB^hf%uHd2KM9*WN7J4};;_4Y7uX*F2OyaKg;%%g8C>0|uk#4?|l9BRH zN35WQ+G*Kig~|k|q|WNE}X0)RRqHnqYz?^^*6(E%7L}RcD+m z1{vBWUbiOoh~Z;V%%GyaSAsX3Q^_ombd&Tb^?z!A8}ow zN8&+#h9QnHI{llCPNcz8tjzUJIF`d^lk~A@aSPR=pkKDKneW(Gy5O9!u=zqpK;P~m zWl`(-!8E!p(z{*x%v~XScl&H||neEv+g?QRz9`jA(QlqfTgq|E`U=5vC z_pzOU@uIMtfuspB6)2yHYRZ(*noil{zNSb<8*&j1vXG!g)%_Le1k1cT1$v&%vi^xr zR)Fo7+ajkI(TX~YF#kr> z#BgNd$9E0BJm(MiJPh>ZKWq%O8VR+>W#IGfXZ(D{MGA5}9h8Nio~s~rCt_G$lRbIg z-;A(vkk>wSFX40hAoOS%dcHBIl%-~)4le+0Zv7?%O7`MRz>nvi9ElKy7%7)JVOjFf zQN`IvD1_ovI=^?*3`E0^j>vwM5-M-WvIUWFs_{cY#qRAS%Js7Cx%#2ZaqkuEIbveF zvK0nI;MjQ5-4soT9C5zZN9L(1s|Bl|(bK?X__5HSo~S;h=eI2Jb`D*67Z@3zf_A@ID#u6~o`+08FN z_ueD4>v{sZYlWs?LL^dlD0%VQ3+n3 zRhHCby4k2H{@xR}mq<3_72`1m83T>&Z8+!&JB%-_{Ko1&*E);jopa9h_tD=-4QqufO{oyBv< z`Vws3=@veCa3;t}FZ?BsDIDRahIC zBWvmDN=h*x`t4{Z>mdBwxr>=*0GWggoC_nu^e~r7Mk8b=xr;(H2VG26ot(4q;+qsN z8$1QE|7>nPhX32^$22XOE-#AC-ikUj2fhFx)2r5@>cWtHK0oTe0w@uU@r;yLw$`lQ z@5H3d2uP}_UFVYbDV6^E)Uh7$FkCKLc@;DdFQmm&`Fh3h!k1_~a8!@Zu-m9M>Vr!m z-E;oEBQ2W`Bc62FT0#F^U(O$#Pi%;j4%$!epLrRLNbIHuXGrT+8`%s7e!s#S26BSf*4Nq>|3pu2@#c zxejFcYwz8IviCFvA!m?RbWP6O=+47r7;}0$e&c=s3(CL0TyWHlC^OIU^uvCb z82p4iUd1TesF%OB!?0j6&qd}9)OUe_(P**O1*gPJRIS}sk z_e=A^6%E9Rg-|vd{4|p&7hd%-7vYlHsCu25PwzL?Th3JKVe@$?GbAMv+|yGBQJ_^% z130qu83VODR0^l8do#mhxWKAfn)z|#CR!r@%l;MsqZuR}a7f=c8g4luofAlLP@BH= z%!xq<;V?L|B3|EoMS8K;r<_rWnxL=$1xH~reDw^Y@?8a%wS|tVe3~S;5Bi)Sf^koT z{a8x}Cq%Y9J4FIAm2)7;=OUv0fDXxeowx(rQmr~mu>%DnhEJM8TnFYzc_ClF-^}Dy z0c?@j%l{M*M)!O_m=ywt^=)C59>4Q~CqPYESN@X%W-QVe%czp4!3O}ybWgn=1Z}Z( ziD>!tLMwDWOlVx0x5y>Pkoo+pm}U${q?%6mSibxPfD2FA-l`&4=>CHR0n`O${J{k* zf3*5B{znmTLz6F~k^Lvl|9QACCI5e-7nsN{QdwE*3P0c1K_0RoH-_f!`$jCndy@tX zD9>6yd=3qw0OnG&`Eq*qcrR{G_fPQseK4jsl2Xidn@O%an=!mDxPKEh@+SHfpojMt zgj)Wt^HiXhW5XAg^tErrA1|!1L=$DHIYWgkF}=PyRBB^Th;WYlMeu;m(3A5o8u<6I zz*k05ZNz*L5ASg}xpThZp!p@0D!MnlnoI)gKPyRyp;Y!SRfgOT32$_`z5sOjOz`^& zI_NpRQ|bgHo(j25JO=axmd?vB4Oc}bUoLy|Aa@|hz8wglVvl{2JE-n+4SY8dS_aBnVOEM!YxDqcxjyK-kOwWsu2SUT=|NgSJh%iemwcmg` z;r}Vc`8EJu*zC*E_Lq9lmEL5WLf6G@n{)HyM)2QFyWzCD{JH`y(KTzrM z#(K|-GY4$x>gMoa3FGT?au=X3C?=l*43pE=_Vjgm5_AFdUfb0PZz-y9K%x+1gY^V( zGM18$$Id=*z%B<|R#+GXF-45SGZU)E^b(;0FDF-@Oz1Rk6sU(+K8+5ApbI|tQ$`!Q zejeb+`j5h7@5cJ;OqNMckjLW)*x*?}oGF65Y(hLAf~#i5bQJ{8H$ksg00cZy68!Xm zxAiI52NEp&)#(wwT(-0Mf?eva&b3u%1#CBok|kphj$yCn%k)D)zs7rS{f?ep=;?W| z=Hki+8Q&~mCVb+rtkZlRNn31F-T{@4hG|Hy_Nu22U8tmcitr%R<-UTsjZrfIFT*z>aP2f?oGQhTjjRTM@C$zBoDE zHQWh5TOEp40osDy9DAK6&HMLi7@y?*$o~_USf@e^6|!i6EHRt)8h-LH33_;Q$mD*N zn!50p=Q*SKL{4$@$W4;{MDSrKCxlNz0bPxlLvEY8j#Z%Ek93(=$Y%Q7x;tKXz1~#) zjcuQ+)y>GPj{H|uB0^#1TgZd59hdETqT)*(v)JD+>=t(EY*uWua&h9s@_d&CQ6Qxk zK6Ay+Mr*kqjwb?CEl!^IzO&>erac0dakJ{^kUii>-5SCq{pWOJBqcdV7AgfH7rzXF z3V-VH_0F)#g(O+gJ*YIm;a=OX9i@Jd@YUh-4*@OLq z=_d#KQcXy0lAS6ROouuT9}Ht^!xx<@Q9B#|`oD|9YClsKExBI7U^d#r5Fmp9Y;(%o z{{H@NC<;GcC?iTM!=EkIrYF`v0g7EbTbVvRi?h%5@)Ics}dN7s*v%wp+?wn`G<|4%gq%ITItvF!Ei8^Jb>hR(yE z`>%U|P;vZ3>P6%%|KDn_?ca=ViFFR>qpI--V2!`pPGx)AQ(feL{$XGs^$*Yd|5JyE z`+rqY{(mG#|9|G1 z@rH(4r1hKLpWJM~9!}e@%;Ta@{@E87RoekRO%eW5jb2hh|z$xjNt zpJjq0WLmFQb|1ahUFme=t$CJaA4ZW4_nmGS`6Mcm_F~lVk}QYZ^nP8g1f4QsH)A_| z+n*nKI)48xfqy+?ii5V?+IIPN`@)Y9Uv!l{xT~{K=K?8!1zuM(izXZ0E9@DYH zg0AHUIX1>E@6Wvkd>K2)_EvIWLU*F@kKbb8iD*z^%2_JC<+9S@UYft!_~de@nQrqG ztwc|@NOVZ)v+cKF6PoQIExnkAeI67__<`6+(}e#4ITA_DsXSVRAl*zwpG}yJu|yV5 zJJn5b+q2WyCule*yUn-ZW!1Avd0g*rvwk2~i(h*g=)5$$l#d)8Fb!RKoc!{MUxV1D zT%pX(Hctro5sqxy`e_0aMDPVVMg6o{W!M0+@x8d|?y;p}GCFosU}~;6j7QjM=jZf^ zYbD*(e623k-}%lTk~*znxQr6 z7?RyyTQWdgy7CF+Q++o3MyiM>wY=Eo;QQ~T?#OUx??B6D=k5bKdqx@|p4PIvN1UR1 z0bYvb-@^QO`3IjpEuntAU2B)HpoFK#U=MSFUG)^^%UXm+o3^>RC1`;mKtH;{Mq0X+ zymBjB(KW`p-P+Y`&RQS`zh)5RTlrXr}u2iz(9vzcT{@P!w+46jzPOf7UOd@+qt>A*3K-aQRY-d)`_*OaHwY{SAFu zo@G4yvH~s6*Le*f{j3#z`fgJUa}0S zt~6JOGlZ_~9u~;w_7Gl9{Be}>;Mf3f4yMarSEPp~Ths)hwN`&?jL*rx4EA*H`JQ!7 z1;6B-K$&uqxZBIqBMx{)BlqD(m-|@NXZDLYT?_c~cT$nmGZc>nu=j}%2B!WQ^OuzM-;WcV$V>fL*;Zb?fY2G~CT3U$Al>5h~ywBMm(MVZ>oUDq`-=)ov1tQAMv!;i!oazhUP z;x19En(I4Ut8q$WfZ+pQNaeS^kQk-*5N2mVvi!mk!j59w$92_ob4llI&wC~HyR*SX zMO~J?2;)-ESB-S$A4H!XhNN8(3xv~OUjy>K%obYFBkgbCCqye(|fFp28rasK=b3iy5vAY259k*p90}pXfmj$NQam zVED|6i`ljFG-O+0FE(E|D2gXm=^Iq&+_^g%w~y1Mdqh?AXIF9qAn0?iaIo z^XHqU4lx4;J5<>l+7FXD=eeArc;9ww@s@4_8o-vS`D5Ld3j{F?#wMnwu@)ZA!+2Qv z$4|R3C6nIA$Bm!Ys%`}+U?`mC@brU%hLf(IETe83gam7C?ULPL3eegxMAI|xiV`Xv zLbiumrcQ~`Ia=Q2T1wAHM4dGVy9J_#(j-q|l?i%Yt>G59kJ3mh;PgD?RzR}_w;H%7*e7`>MrBcYUnQ!iQL`?!soN$r&5Y8%xK7p z1TZ2oc`JB-sJ_-1`Z0O;Ul6Fs&sF*U#C=E4SG`+z%Tl%MUu~PYa-~avwtjO~7H_I+ z%~yjDCq7$Amb+3u2)9}tHb;T1Eq2E;*n?nQ4pF>l=lzT5uU|T3@uIY64^q9wJl~a z%7C?<=ONGKcA)Lkz)tQ-8WSsu(Q33Li!_NL$r14mT{mV-Zj2KocQ_&N zM~qmp)#)2qBsBUzx^$-QnsZBz##uj@ztbE%{glIlqf{exYniS}m79dU$G?Y1y7l%D zRSSn!e}e?mgnf0yjN6jOLNrSUZ-Rid{r!iL@z-Upo(z&pD=a*D;rZ;?lhMo{+RJm> z5gM(De$Kbup`jSmCiZV4++NyzELkL$_CUYX^U^0^zGWJ16@T^@r-HjZ@Nc-vnomgY zzEIcee8t!9r*Ah!``(Qzmz+*1uT)dHWWRm3g0`Qe0d?Is^_|D(H1jFz@XOyov`t=Y z{fK~4*mrPEpTz@a=ZQmk=fbY>?hh5s{%F;fC_Y(()l1zqChdWEI4MPUIcF%YSgY>m zS2I;)=wKO}#V$MQRF@BVy0eSR&;2<-d!POgZ6c9e!rhRqF)T*3c^>*kDT`WsSj08D ziAfinBrq8*#x9u`W6>)A4s4T4RE*Z0V;JvS9)#LrvcTCJH$c6P-K{a4^UY29EMdj_ zN7@KX9+Zb*%&x1^Wee)&?^yJekD~AzsY|0i&V5 zqs}FPv%jOJjg8&YgfWWJV8K^$=7T_9c%9`ar%GNUVxEg?C^=F?<>h$OHEHtihY{<%i&trxUR!r!QQ7heW2df#?)u zJ=BCw`b;4OOxa|w2*J&lH8G=ES%aP4hlG<)h2eN9Q0DAEg6O zh$7UkLqN{8xil|QYTo7e&gJeiKfj8HDbm#GVCpwA(a5hbMwyc!Pv?Go0$YAVL0M#p zTzWJ~Diz`_9Hn~97h8^%kQwF76h9M16y4m;9&$`meja={^BO_{k}vxxDX?8L)X3!7 z*+~Kv?y3~>{W+uSqzCSHmRm*mO&#C8%nloK$lJfEqkJ^o{biLdwksyJkRFUGf)+MX zDj)q#H6baJj?k^;>9d%MUy6>^%<#dv2y*{mV{%luxW(fKW24W8SI(@-eYH{oM$!6) z3Y*tH++7(E>u_i{52a${C>SL+E;xt<2^rRKqBIpt6ucO6f8+^DCZZvRA!I;;rvM8Y zXry=;i3?Qava2A03AnEXi|Fzqt98L(u_{sW$x8nBA@G3$Fr=#m#~QRY+14B!aR0>0 zLOJ0Hg@)N4<(r&7xtC#6u4<9EDHYMf(xM&vN5n!QAXK)7__xB5r=s;0fVS8j6d(v7 zHM=_k2E<<^;gu#r(_Obkx`|a+kKu)d7qEd4|3@js7 zE(AwES^tfe1Z-}irO*m6)}Ob@#f~hNSL4w3>q&ORy>>TzlmS(YN_X)Sk8hsVY+_oa zhysLeiL${nT~8V8t_{k=J8+H5)p<1f&W}(xi>}!y>Z*8&Aq4lhW8HCQOFECsLiP6m zMuRG2Rmc@nLYjF3iOprRvCnav)hXusbg$;AEu%!gS8)D1nU)htpjSfH5F!5Z|AE z|DEzDl`~1*>#Q(xRO{|wecrJGM#rn$P0LvfE{HpyY>xHEYNt~QpTznf9RIJl>~DDb zKg{#7gcsRQ9I4Cm@*iOWuRp_`5M7b2C#&*xx2W`bYZ4S2daDhgTP=I%4Fq)>kpVZ z6hhBC0OmQW*#woap~4}FRpo2z3j_9Oisr;JLE@moxT~nL@-a!-?m_U#F8NVVa62c0 zQ7Br?g^_}!99GnAq6ieHie%|T|1spdWD;&IR+Dkc@;c}BRvaxcQ3W`kcG&7sm=>RE zLY=kQtHq-7Kt>F9U+k-fi;uKF?`#J^%4#s%7#AgoYg{esZRW>C2sKfe%#DQj2u-5~ zX`gacK(-hfT5gSqCjKcaHdV+ zc8YS3_O{CQJ9K^Ar&}o&fYYG(3dQ5Rcu7J!$W|Up5$0zUAbOFs)uC5ky@IuQ0=>oL zI8bgUM*bZsVR_Jetyx3qLGVnOEj)SAS`;~=Qm4Cj@nz!YQxnJU^nMDE0ioimOFzgl zDUnW({x{)R$m=8vLKzfmOTC_m#PDE^HPR)ts9q1eL_qw)ue?9H=hfhWof)n_>NH;r}J>JC?q70{f`M-i|#LV z%D+tC^}w2Bv*NYU-|X5{hR>e6!ow6z@1`m*9T2Rv@6~huV)Caa(_Mr}4FjSt0?&zDe78|GS}K)_ z1b;w=KQn#c6r!^}qdgi5TlRB!*yWen>@ekapFSm%B*fRBAQp3E>jJSmRWz?v5*wy_ z`lFUc+Iu-+qQzQprK9(EG&X(%o{dN6hf}!gflT$SL}WFoyf3W!>qlNY6!cMYayL3% zRcW6Y7-{f&#>pngub$~>FV*57lbm^HrC&JZvQfB(B{>cR`c2dF1L3qtHtv>x0DM#y zjm&BRXUt%!_%N3I20u8$q8tXq&DVn|s)RJ9eO;jm6wJxPF82|v@p56-VgWx6F?CsZ z;|G)KjXVtxd0fg|y2H``v2Ag2PNgdkxsynR=sPZD&V*S9W@Iv|_j)_Y2rgC03d7|K znh->Tc{S!WJ1uH2V%i?Ia9($1uD*z?@oZ-+csLFx>S_JruHs>UhOc`M0i!y0E7+0M zNJTc;t`2T%CxnEe_tL4pyMkpy)%CVrucyZicBc8I75=S!&Deu$M9?{8SoTF~dt$mJ zC|k!R5njo^Y>lDCPcRqnNeJ1g;wreP)LbZ#fJ|9|E5*=JjBZnRrO+JCC?GQx9!D!e z(p-$FAXXxXsed4XY?Q)Q6n*JaX*&(HD#i2S2e2rSIIk$uC2u%g~wsl_9<7HwU5i@eQlG;^OWqw0WlX&e&2d8=q zo^(LZQk$=}HBbA|#7FY&#h&EVPTz7{dAR>dItwoi)ou?;BcO&+M|tMZOs93ObnzEH zL3s>HomqE0X|GD5FA$vfgRQA8V3KkxA$^RC#wH5w1ipV?jN(=TdGFvI@8s>3#BQKZRvcNQubdH-FE@0YT?M;Bo(Vfh z71lSWaELncn!7r`?JG|-3cHyuAC33q48VJc5FWsdXJ>6$)W|zO|Ed`T@x9~F3@LY_ zVG_k)40+cd2`J4%z7VKq@_&?OKy0qAP5~+AO6ptP0$STD$oZ&}`b9pUMyIi`@wHco zF3&Gxk!JDi7>#euEXG}(#H+o7>5EZ-Fv!n@+s7 zz?}FxKzF|~1H{8)BdzpwW7OG5E-W~e(i@hW1$Tcl-2aG}Sl>>mo6zM;UjeKF9V4uT zL`>=zZ^JT_$&Yk!nO&yR{OPkZ)n%2;9j@aa0&e{76m&t~y$|~04)O$mcz6w;kdNnoFdp)TP zy%{34usrN^EPMhZYgK--vGo2UbB4H|q^nrl2zsiFp5XqxqD+5z6M3UIah5kG4o)5B z00h{>r`>i&`H%Z3A;NZSjBf4!V{fd%Gbt#-XwGrO5LqOHqwu3p*P4-5GIc6?8ts9&9xW?&~FTA%my4ijfqj*2Oqn!sp{JEBE#{(Ajq zDELVT6f!}@*V-<(o#I^_>cIcM)KY9Mu7(2)c&W*Tx@}e-#_m=>a0h8dG)L1sPQVx~8eo-kWX>K-WSCxVzu0eJXyE+vv#<==BV}{Ao@$u0}v1>b9rO>EYDk-6Eqvar$QY%!3^;hxMDaeE5WdL9o znC?=I`g|pzLUDEt-Fh>Wv}mo<(A z8UQ?Dzr`JSJR2|MuAN&JPm*8u zcET+q!jwxf8m;FY&M1+O*T%Gty1ZJ$%|jkl_MP1~+s8lWu;a`aRm7WLBm%AZaN&t% zO6cGHr4STRI5+sW( z%^oPZRde=u372-+(~GFCE{41TVoNk-l?2BDABiZU&f<~6B|7(!{3?0eR^^9Epd7@S zUbY=5XU@-iTs+&-Ih*Xj?x4}ITo@a~iBNsH_AlsN*fn=yui6|cj#oVE$ zN@dmRnn+>sbem?eGQwa_nc0y=ax`Ik_>>gaN-vkb9+Jj96ADTtB=5J6BJpjs;+`9D zldv0-#A4=+Wh<+$27-F)4Y+-xn(BRs`5f@|$49Hj`?+!m?k0bD!C~W(`gV#OK7AUP zr-IntQFWDE#RJUR@kXrHdgc}y45Zr@`b!bGX$<)f)_{%SI z*hf_CDSHv$iomH|FS@va;~48|+8e#}@wP`{ywQNnjrr+7!FTBO5Gd%mvm%@A$v5zO z0YsvXFxf&;X$wQtI>CMh1$X!f=d{1fyg+-4eah8W5h0kNVM|#gXGw~QN?vB1>^vJ%A5XtV zSQ2|lL)Wni0!$a=n-ZcW|3h)930D9ND6plYIH=5(R)7I#Qi1bVMBbV|qg4L_53T{C zR=T`FCUd)@!hH@+O;OQYCU;Lm8oZ!tf|RIhP>}ewlyQ0k|FyRki^|7gZ1%}--=q!3 z6*n(<22K)*r-e~2X}|oeR(Cz-CN$3P6xM_rkuE#`>M~(acsRVg0eZ66M>ln5Ee*?dWt3;N2zSBD0T($J{A`dQ+J}d1Q?xy8`y5IB7W6DA~8nzC}xn7m0f|HYr0e;A6$(eMkKiF zoQaRE<2z2S3AUuMu`E>%sh=E7+FNquHBbnB{p$edc)nn$zWBTRq*_$lnxOPpaRkR7 zMSH4It|JpTY79c4#eYD{(UDaYL-g!NBYuD3E9mOY<~{JZ{_Of%$BeLcIEyeEc0vlD zZeoq}I{j~d|G~J&63eaQc7^JvwEYNgxS5qDpo_2HT(}Ez^uFJGnILF8wLroOhgy~( zALcsw1B-O|R+t!CDl9edwsZ1BjCo8J?iv*|Yc$fl{+`v?pf8AdAsE=RZuHL&=+{q3 zJRI2u4AlCLTokSxX-blq=}VVjaaYIhqZYft4wvhvXc?AS>=-}3eNWi-dXV{Xs2DpOy_!@1O%@OM5Dp~jt$bEm6U^pQDT zS%;Tg6cB{YSrTVKmu`6uoB>bpcN$hw^~bunTr`KR958=y ziTE6|GO(Ll7>Sx^2TQLzYRDYk?6aBu?5wBEv`mS5okuXcp+EQQ>DFu&4q*a` zCYu(4MsV6#L0b#L*;PFM_ja!K^v+S4ie*$~u3bFdhovHXlx&nGTE2aHvi~;T0Dzdr z_1W7q#YV4VmD@Y2Wt?RUprX^LlC6`Ml=9alTZ=ywVB~9Ve?CNFRWnukB>l?5fwC3S z&ZQJo#AH8v;p5;5I$Uo?MJIiWMQ=zX3|%42J?!dY-j(Po}TLQJ8)nD?N~Fibg}xeTL*k^fc2 z)ykJ!uDNwYR0FL^dFiNCpfg++)W}J%xaj?&P9{`1x=!+eaCOe&y;j zj7q!4y(iYz2gG*I#^Va)mxB*ype^)Tvw)3_N_PLuE`(q_Y-P8`Z(BJyI?*aA>Q zYLOg*LMQ3wt$*qHF4ow~Uv|zRPry$>eu0|@$>etaQl-S4=pGR!r3qRMMa2W-U@2ng z=z6<$lGGn&lb^1ey~)*teHx6Ei|Ts9<|%zdHF z<$tib8_4(zkeEC7_gmmn)D>Z9alVDvb#+aqVl^5|xzRc!HK=>UHUrzPaPJ^s>J^%F z0>^>?FS|*L|IDr2&BZqqyrd3)J!N_kp8TM4t2O^ahu)NI1U z_k~i9GW4_2_`!cSdaR445=JFqPCK zQo8|o>K=IE5MmahBI*MtbR$vAIK-l?h?Sd*hjH^eFXpSXuzzPO@PzVzk0-9pr}l$7 z3Olsxpsq{Wuf&Ws%sqjTA0Tv)SI%gzkwYSble*D|P_Gnj*BLB(J>{Xthi{xmYvHiy6cTp0|hKf16esNsNy(*$reWTc8>^ zbegg)ZWjL0sT0vcHsoXyQ~bC{R3GPCYpuw9nCgwU<63x~ZW-73E12-ZDf;`X>e?1b zLl-EUNg}jPOoHs&U6CR5s0UJ^RP+=Q)YFwc4KMd?Wcz?{QOml48bD2W?ilB$fAJB zjN85?+M1e`k~l{N!=w@C_?!2BGD5juA+NNf;qEcPyVL^OVKeB_h9zT(2kT*Kmbg?l z*Mxx}w=~%|CA+(CAEkJl{#~tZi=A4cP|2LPnaRzBjc9OzZ)xMgX%nrjv^;LB88uzz zn}}X*(Y$E*UDL{t%iF{X*+2oS2}|j|MPS(J?v&SOyZ>?Ja55pW+ERRHDrXv2oYJ{^ zaAnX&dN4{+K_P;Kq^xAq5{xX5eJ7(R_HoYhWo>0+0^U$PT=juTcX>rlH!mTUv&7YF zGU!Dz{NhC~wQVE4__UStN4_B8hougu5Ce#9b3JH(zV4BUn>;8nvEQoFIH9rP2kKpJK+=6jv_V)@@49M`Q>btY`kUHG=n{b$|n z_!sFq;=kr}E)Y|7TdUH^4;}X8<_j0hqpp4IPj3Z{$yFOlerKU!X)K_g7^RgHH;}c_ zORt>%!$kd^iu~@c@ah~leG7Cb^Ho5`S$lgz3%7RWd|C&=PGWvdYrLid52qH0b3o`0^b*B$o4q;2k&$7;dD-h7A=5E1K07;6SmVPPyNJu|9~w@9GlW{2b_!- zG`>Ug{$%{R#~l8{Wr81t$??uA&LvGZ2%UIinw{14x_O@JS1A{#-MTF2%&PFgB9-;Y z`=zGJMwjJL#nskFsc0f^BXF`vjiDJ+Vrt-EdsH7W(q)6cR!l5y!G_$s`8mk zwm)aJVbyO}hqe^^fEhSG10KB3qKlyoMF=aIf)Tnh1dG?LZ(Q6Zps2I-%sPh1-zCFY z0V8k(Ccl1omyhlmm>Sv3hYen${{VEBhp=%5A}8QJ8qIout+7JE>ndpZ%$240iDEJ_T!>6|h{=Uyh3se>`=VKpXHtvP91mU8r(bvYfrRL|z+mFD344IVK`XyE!0bGzsvE zWlDR_VW>9i!I7rdDQ~fx0IGm}fgU2K^ixF<#zCo(K|pbe0v?*G0>yijeg%MAhuic^ zaD?*O?_lA^TH|vDx(4L=Q+~Xt7f^Yc&cf58@^|G5*}XsO!b*%=Hd1v;)c=Tma(!^}~igp-Lc6@`na1F4g*E?vKU< zY{!%6x*Sk`72YyhDzj_t1!Xhd=%L8)TnC}P#ads)zYR0v$LT%JPgvt27-}3 zM7%*p0g^3yC71_K;ns6k-r;ba`${9)rjj;&0KIa;e*Q!KH4OE=WN3W+9`5s1u;t)r ze8(l7sB*%$ycEvmpt6d|WS$lWOv~#}q&+lIBKyVr*jL9ih|^fXk9GzOnr(lwZ5tk( zSoyqR5c*p_V3`SKNtJl?z1l7hQ;Gea!6niyk0~&Ma=YOVXvcu7UBH3SyqelrZY#+N zu8eWC&do)kbg;iGc@>SZVu33Dt#7?~-$UCm!Wvc8 zQS(jUTMQj6O)=aAH$di?;EO$9ydM^wn|WP(S4J<&EGI#yf0}Vl8tjWx6mM`%S^X2_f9MU4Y(kA5xG{e{R`)204*4B*}9AP_}P zB!-XJa@cW69GYh&>8m$){R&Dzh)C$CmHj&!#F=4IB#(x{!)zaIr=O)3jtyR+TAlJk z!U2*dt}4&VVE|Nc0<#{|)EXJz4-aL2Y+Vt~O%E?>-L zp4~rRhwh?BYJOrW%={S&!kFdjgROj{_>7TeOR}k`M8A$=O$dP>W``@Pv!$2-c*6sW zntP@lo-zONJHN@iEE^VL7W(mZqK(^ahb%BtKeFFS#TK>nkNpx6<;DvG`)D?NX z)ZvyFc>8jA1n2bz_|8Lo_Ht((gmG++mawn*_%yH$LV;DIEGwB9PcU)hg)!nSX_%Kv z0xn{|A{0E>DBtg0DNc$PAo!A!ZlBjWqRDXQt}C)mtBS6k1RTT&jVSktS$w9*mi5jX zZ^V_1#D$ehZ33T%45wbR*s^bS532mQ%Ep>IGv~e=OHXLxo(jK>qY#ERp`S~Cz&eR# zLO^`>)h;wc=%r}%S5XQ*zTzBApgXBsG_S%(j9q#>&rHw(-6|SeE--&!c`AWFg-?m5 zgSHj#4sw+b43IiffbR@5RFH!{GIlMG|Co5E1zYp2^iuXg6QXX;A4yV1tQxh<;Hq(g8{v&Jmco&r zkVf^t^@K}HBHJHGNWRs+POIT$aN*Ob9as8YEWqt<-jN4YSKH;;WsB(@?(C}gHIo)Y zlhrKe;L#S25OT<`-VO(;&JIJ+L;xWdqXno;O}0p|Nc_9RqU03bIPa|HNBkCwDToso9rhW|Wh* zMa9B~0a+}V3#o2-YS6$gaj)XodV{OlNmrAJYxwXZn|yL>$@tdCUIwiNl#>?Cq!?np z)++Z2N>kP9AuhOpVLjYkiSEkr@hc!LSv_8)x3)KeUZQ)wYIcqi_kaj8#mG;O6ARIv z<^UrrIsOh;d-cSrpWEX2mTt<>FP=L|ya=6NDkmS0q)V?bohFHGGE zz_7>iUWRFMS7a2uA|UWc{|*cW)4MP#k-hC}))L>rBWZ=4bjO8g1?n&-e1SBF&dv>Hs98U5 zsKXW2X|Xi0luDB!_)+dc+2Jl+4y|Wl@0mA~NiS0p-KgDF*|;j{`BCdi=#_wjz-D}N z?LkY1I+GmkqKjz&p_d3z8(c+rq2}37_5(aoq?U)Bd#&R`TuZ8xSMvyr#Np;I!Vo-m z@q4V<+FLo<%}dM%)4--S8BYIPNh8c_HYmwOR%-28?s^fP0mL8AKMeqyVp(Bplh$)Y z?U0@V-8kpSbQ~L1ZKJIu>-S zdc8ver#8sqi0vR2BG4r^tTm&}g0>W9z?rc1D(Om|WzP+8R_YiUO4TF2U>O)0x+T84$iHSB>} zQd!*`rM-G8p@<8*{aOcu{tD%)&8ony93pCPvh>#K;Z4k+&s=jfl{U?3W58Y=C~UA( zSc5E?X{jG2ke6M|e!3mH0sq8^Mw{RJpN&e|cz}=?7e$D=nUb#B#9 z6#5S=;zw#Ta@H9TOpVi^AaD5FNHAk8X|Xo&cUbB;iy>_i9fk$hb&Vzqm}lqm%v^~C zbZPL?<$Ti%{+0uUzk+8V8hzK@CcqNv%QHf1#-bP+1R9J`6@i(E`{>jzKe-k!%(Vc%|bc@ zD@&yOwM|-;IJ-981SPgCl4uok)Bt~~%!7*3NGJcrVm1N)Amn>to1YQRZDDRRv`{?| zPha-*BZd(8`kik3EilX2$lJ!~MhxY^{S$3Ae4#NxWO}*8WL?vr&XGMXm&M2kzOUww>3#m*)UW$P8hmt&D4)y3tMv41%_HZW{ zlKFDP;y?_6%huC^-z)#?L{%o#JE$lqY`zIe=O5EAmq+kKm zyai)a)v1W5jS$ng!dIol%3heZ7g6y%wQkO@hPvNVFI(YmLxLe19$RZ4$$+00_(c~G zp(|?zCbD+Qh_vK;-T`?(z>;gv#Ey^ERm^!%`$CzJ#k?gXJyIe@>=G?C8`@xgA;l9` z=^0};=23qV?w=B2_pEQAi1`g97jE?w#jVSP8rp6@01>^ATZ};9q>aUn*uPFfXqkxx z4S!~KS{E_=$)4{(Wg)Xv+k)B?+s;SeQMg5PC>&%y?JI(=`_A{|mQwt=Ma;L8{u72v zpAhTkrer0N5~m>c3sqHOYT&iPw15L&NqQ&w^ea|EC<~S=J%9R&PRCxH3|-Qx?Z@Lu z2!xi#R#x#3w2aE^UHH~V<>r(2fH*ElxyVva)m2u8h(uM20`hB>DsNfxT)q=#%sC!D z@_6KYWu0Pz?5Wa6!}36<*6V01?8wLY1uhY`6XN*suX2}|jw4f5U*WN3Z&#Pg?*w$L zu#z0m{jVudypCs9FggrlB<#Rrhzzx~kj z5oaUev>+VTdYSo5j3hj~nd?C`_AauxAB-!UOK$9HZU$2+(9J&XU1_|;k&)OhIrq)n zvjh$vylAxV6_BRE2;s$p7h6lfLw-#I0S&g27$R)XBk0GPZbq9m`{n4ulppmelDO$E zD>B4nh?{swbWM94Du9Y@3a&La%Emg-u@aC#;Nh0ROE(SQlhzIBk!~|@?bHyR=~6uF z^+t^Le^p$M%%Efqb)uUI%sIdr^b;^_lW!&qaYHA26y`gx%Z8R|YIhg6BZuF(SmqV~ zP9cw)TqgY!zTNzLLvm_0aFFlRGCba2dvgGBfT%%#M@-%Y;|ArzjodcOfP88Vc0!fg z&4AzO(q$7qMrQSp$M7wUPZnhU>F_Stmj+()Qcy(213ODLhQ3NJN4Wzb8Wy z8V(DDMQJjh0kzuM_yXMJKFgg6pM_;eT6jb!G|YUFSU*2~(yzcay&{Je1jXeEKP{Yf zA>A^@PEf8?!jl_~QPOT~dY;35x=2O&qP920euXJYNGXk>vPd11Say_wC%$yd=;-qP z3f-mg^A|x?Q>3VTCo_hbCXs`g+cpI@Q~$dv4H?V+B-k4_`#K#(K;Shrw*4j|ZRd^bk* zd>sK&v%uIeV~DK{(AX*=M9a#L`mC5a=w+v(AI|rA#tLiZRLemxFz;k5C^iVRsm#m` zQCeK5h!mJM*LoFEMN|I4w?$q~XTBQ9d6mQSuB`#uuxT{Ph7FPYEk<6Uu_#3SK|Cxj z<~xH%7Z0^!1RHsg(qv|m{d>*SI8-i4@bnMKeU*g)ALDZj2h?kk@h9Cm`q`0Gm2Spl z!U4cD3^SVtmwhRfG`$_$V=4h3=v<%XX4z0iMyl|JjRM5}*-d0OI{-zEv#Rw`e0`#K zlVgSH`2L3t`qT!YfYSq58r1J1!r)j?E8PD5R0|y&pVaKzVCm5DHEn88)sM~IzR-?j z>B5SHuZkI%FVT3xFg>>>5@+%qAoeRq6{r(%VVKNTXd=2yPBz3Zj;Fc81ikmC}OK%039UI9KE>=ugRiZSCP4V)&SE4}1(W ztDvr)T>aX%KwS>bZd^fMogY>XN(*KpoMV%QLhj&hGS?zweL9@zKAfZfe5*mpJ*~vE zF}Ek;C_n=x_2pA(g`kSy+xLMq$~gtCc~yC>v^*&<8urd0PYxm@A3xJxkM=^5sntW#P2@eU|R{`H^KJ7GMU(&_McYi+F&1maZ$H6S_V+*uN>0wm$EFfm*s$q(x_m*$J>)$q@8GFjMvq}Bca z4gXvC?C6$~46uZ61Kw8wKbO2`N3(%ZBOILKrsjF&KzbL`{4}ujvzZC6#9pdU@5o4k zov~lFL(pRD9LEYnHs^4X#v^!nE)Q!NDUz~pKLlT+PcXhxgEoQF=OQnBAO_5y+CJ%* zufn*m4gVgR7@hN-3r#^^LD2AaXNKo`bsvSYZ{7I!ux$$^6JBae`;|5?q(5b4X%d@@ zl`xitiG=-!$MrHU1oxEcW}VOsi>fN)NCakmb%oXvF09pYmact`q`!^c^oW%2Og;qs z)>j?M=u*EdzfR|TuQ0hCH(l%cbt=+v^U(vG;@8Hv`uyuT9T#?#YzzeKJnBdn+SFY{ zYhOTjWcrTf<>565q#_t%B2^{8B;7U$BJVXhSBChRjCM0w7LiETq`#zk=%Gu#JqgS* zT{F?1@Kv8%(l@lKmR+1x_i|(QxpB zepNxsSs|wP`pm`gdhW)bblTnYC-cQOH8hNYiLr?ibOhA5f<$Pu$XUv_O-v}K=fAhN zzwLk}S%NbJL!5r+=fO!$;v*Yb(QcteE11ukiwpl(P>}m_hAmcEesx&0W4lp(KhK`V z>ak>GXweTMrNFStyHEeO|{S!p@mxjLiJO1Z}Z z{=RKxqhmNHFbR%@TBH15I+U$QYGD2a790im_M|XqX6ohgO+R+G6gSWUX({IoYLO;K zlQ%0Cr)AJfihR?ae@MOPSprCXxlQRQk1&jy&9VyKh~%EslK`8*1sBJI{X#a-_i|%dG`8na=aqRo5X@q`sUXc#Lyyd5{S=!-g@qN zNESBtjTLeGr%b#*A+#W%moq&anHg!^dMdwJ*3c%oua^m~-h=e�~x1eXgp4{f}++ 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 XP`J?L89Dp>J>Y{dMVh>XyT$zvC#L89 literal 0 HcmV?d00001 diff --git a/Modules/VMware-vCD-Module/tests/VMware-vCD-Module.Tests.ps1 b/Modules/VMware-vCD-Module/tests/VMware-vCD-Module.Tests.ps1 new file mode 100644 index 0000000..ada710e --- /dev/null +++ b/Modules/VMware-vCD-Module/tests/VMware-vCD-Module.Tests.ps1 @@ -0,0 +1,35 @@ +$moduleRoot = Resolve-Path "$PSScriptRoot\.." +$moduleName = "VMware-vCD-Module" +$ConfigFile = "$moduleRoot\examples\OnBoarding.json" + +Describe "General project validation: $moduleName" { + + $scripts = Get-ChildItem $moduleRoot -Include *.ps1, *.psm1, *.psd1 -Recurse + + # TestCases are splatted to the script so we need hashtables + $testCase = $scripts | Foreach-Object {@{file = $_}} + It "Script should be valid powershell" -TestCases $testCase { + param($file) + + $file.fullname | Should Exist + + $contents = Get-Content -Path $file.fullname -ErrorAction Stop + $errors = $null + $null = [System.Management.Automation.PSParser]::Tokenize($contents, [ref]$errors) + $errors.Count | Should Be 0 + } + + It "Module '$moduleName' prerequirements are met" { + {Import-Module VMware.VimAutomation.Cloud -Force} | Should Not Throw + } + + It "Module '$moduleName' can import cleanly" { + {Import-Module (Join-Path $moduleRoot "$moduleName.psd1") -force } | Should Not Throw + } + + It "Module '$moduleName' JSON example is valid" { + {Get-Content -Raw -Path $ConfigFile | ConvertFrom-Json} | Should Not Throw + } + + +} From 704251045ddafdf2336730b65e8b08c92c2fbd6b Mon Sep 17 00:00:00 2001 From: Matt Frey Date: Tue, 13 Jun 2017 23:13:15 -0500 Subject: [PATCH 044/112] Add Get-HVGlobalSettings and Set-HVGlobalSettings 2 new functions to Get and Set VMware.Hv.GlobalSettingsInfo --- .../Set-HVGlobalSettings.json | 22 + .../VMware.Hv.Helper/VMware.HV.Helper.psm1 | 391 +++++++++++++++++- 2 files changed, 412 insertions(+), 1 deletion(-) create mode 100644 Modules/VMware.Hv.Helper/Set-HVGlobalSettings/Set-HVGlobalSettings.json diff --git a/Modules/VMware.Hv.Helper/Set-HVGlobalSettings/Set-HVGlobalSettings.json b/Modules/VMware.Hv.Helper/Set-HVGlobalSettings/Set-HVGlobalSettings.json new file mode 100644 index 0000000..269d034 --- /dev/null +++ b/Modules/VMware.Hv.Helper/Set-HVGlobalSettings/Set-HVGlobalSettings.json @@ -0,0 +1,22 @@ +{ + "generalData.clientMaxSessionTimePolicy": "TIMEOUT_AFTER", + "generalData.clientMaxSessionTimeMinutes": 600, + "generalData.clientIdleSessionTimeoutPolicy": "NEVER", + "generalData.clientIdleSessionTimeoutMinutes": null, + "generalData.clientSessionTimeoutMinutes": 1200, + "generalData.desktopSSOTimeoutPolicy": "DISABLE_AFTER", + "generalData.desktopSSOTimeoutMinutes": 15, + "generalData.applicationSSOTimeoutPolicy": "ALWAYS_ENABLED", + "generalData.applicationSSOTimeoutMinutes": null, + "generalData.viewAPISessionTimeoutMinutes": 10, + "generalData.preLoginMessage": null, + "generalData.displayWarningBeforeForcedLogoff": true, + "generalData.forcedLogoffTimeoutMinutes": 5, + "generalData.forcedLogoffMessage": "Your desktop is scheduled for an important update and will shut down in 5 minutes. Please save any unsaved work now", + "generalData.enableServerInSingleUserMode": false, + "generalData.storeCALOnBroker": false, + "generalData.storeCALOnClient": false, + "securityData.reauthSecureTunnelAfterInterruption": true, + "securityData.messageSecurityMode": "ENHANCED", + "securityData.enableIPSecForSecurityServerPairing": true +} \ No newline at end of file diff --git a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 index aa7697f..e335ce2 100644 --- a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 +++ b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 @@ -8982,4 +8982,393 @@ Function Remove-HVApplicationIcon { } } -Export-ModuleMember Add-HVDesktop,Add-HVRDSServer,Connect-HVEvent,Disconnect-HVEvent,Get-HVPoolSpec,Get-HVInternalName, Get-HVEvent,Get-HVFarm,Get-HVFarmSummary,Get-HVPool,Get-HVPoolSummary,Get-HVMachine,Get-HVMachineSummary,Get-HVQueryResult,Get-HVQueryFilter,New-HVFarm,New-HVPool,Remove-HVFarm,Remove-HVPool,Set-HVFarm,Set-HVPool,Start-HVFarm,Start-HVPool,New-HVEntitlement,Get-HVEntitlement,Remove-HVEntitlement, Set-HVMachine, New-HVGlobalEntitlement, Remove-HVGlobalEntitlement, Get-HVGlobalEntitlement, Get-HVPodSession, Set-HVApplicationIcon, Remove-HVApplicationIcon +function Get-HVGlobalSettings { +<# +.Synopsis + Gets a list of Global Settings + +.DESCRIPTION + Queries and returns the Global Settings for the pod of the specified HVServer. + +.PARAMETER HvServer + Reference to Horizon View Server to query the virtual machines from. If the value is not passed or null then + first element from global:DefaultHVServers would be considered inplace of hvServer + +.EXAMPLE + Get-HVGlobalSettings + +.OUTPUTS + Returns list of object type VMware.Hv.GlobalSettingsInfo + +.NOTES + Author : Matt Frey. + Author email : mfrey@vmware.com + Version : 1.0 + + ===Tested Against Environment==== + Horizon View Server Version : 7.1 + PowerCLI Version : PowerCLI 6.5.1 + PowerShell Version : 5.0 +#> + + [CmdletBinding( + SupportsShouldProcess = $true, + ConfirmImpact = 'High' + )] + + param( + [Parameter(Mandatory = $false)] + $HvServer = $null + ) + + begin { + $services = Get-ViewAPIService -hvServer $hvServer + + if ($null -eq $services) { + Write-Error "Could not retrieve ViewApi services from connection object" + break + } + } + + process { + + $globalSettings = $services.GlobalSettings.GlobalSettings_Get() + + } + + end { + + Return $globalSettings + + } +} + +function Set-HVGlobalSettings { +<# +.SYNOPSIS + Sets the Global Settings of the Connection Server Pod + +.DESCRIPTION + This cmdlet allows user to set Global Settings by passing key/value pair or by passing specific parameters. Optionally, user can pass a JSON spec file. + +.PARAMETER Key + Property names path separated by . (dot) from the root of global settings spec. + +.PARAMETER Value + Property value corresponds to above key name. + +.PARAMETER HvServer + View API service object of Connect-HVServer cmdlet. + +.PARAMETER Spec + Path of the JSON specification file containing key/value pair. + +.PARAMETER clientMaxSessionTimePolicy + Client max session lifetime policy. + "TIMEOUT_AFTER" Indicates that the client session times out after a configurable session length (in minutes) + "NEVER" Indicates no absolute client session length (sessions only end due to inactivity) + +.PARAMETER clientMaxSessionTimeMinutes + Determines how long a user can keep a session open after logging in to View Connection Server. The value is set in minutes. When a session times out, the session is terminated and the View client is disconnected from the resource. + Default value is 600. + Minimum value is 5. + Maximum value is 600. + This property is required if clientMaxSessionTimePolicy is set to "TIMEOUT_AFTER" + +.PARAMETER clientIdleSessionTimeoutPolicy + Specifies the policy for the maximum time that a that a user can be idle before the broker takes measure to protect the session. + "TIMEOUT_AFTER" Indicates that the user session can be idle for a configurable max time (in minutes) before the broker takes measure to protect the session. + "NEVER" Indicates that the client session is never locked. + +.PARAMETER clientIdleSessionTimeoutMinutes + Determines how long a that a user can be idle before the broker takes measure to protect the session. The value is set in minutes. + Default value is 15 + This property is required if -clientIdleSessionTimeoutPolicy is set to "TIMEOUT_AFTER" + +.PARAMETER clientSessionTimeoutMinutes + Determines the maximum length of time that a Broker session will be kept active if there is no traffic between a client and the Broker. The value is set in minutes. + Default value is 1200 + Minimum value is 5 + +.PARAMETER desktopSSOTimeoutPolicy + The single sign on setting for when a user connects to View Connection Server. + "DISABLE_AFTER" SSO is disabled the specified number of minutes after a user connects to View Connection Server. + "DISABLED" Single sign on is always disabled. + "ALWAYS_ENABLED" Single sign on is always enabled. + +.PARAMETER desktopSSOTimeoutMinutes + SSO is disabled the specified number of minutes after a user connects to View Connection Server. + Minimum value is 1 + Maximum value is 999 + +.PARAMETER applicationSSOTimeoutPolicy + The single sign on timeout policy for application sessions. + "DISABLE_AFTER" SSO is disabled the specified number of minutes after a user connects to View Connection Server. + "DISABLED" Single sign on is always disabled. + "ALWAYS_ENABLED" Single sign on is always enabled. + +.PARAMETER applicationSSOTimeoutMinutes + SSO is disabled the specified number of minutes after a user connects to View Connection Server. + Minimum value is 1 + Maximum value is 999 + +.PARAMETER viewAPISessionTimeoutMinutes + Determines how long (in minutes) an idle View API session continues before the session times out. Setting the View API session timeout to a high number of minutes increases the risk of unauthorized use of View API. Use caution when you allow an idle session to persist a long time. + Default value is 10 + Minimum value is 1 + Maximum value is 4320 + +.PARAMETER preLoginMessage + Displays a disclaimer or another message to View Client users when they log in. No message will be displayed if this is null. + +.PARAMETER displayWarningBeforeForcedLogoff + Displays a warning message when users are forced to log off because a scheduled or immediate update such as a machine-refresh operation is about to start. + $TRUE or $FALSE + +.PARAMETER forcedLogoffMinutes + The number of minutes to wait after the warning is displayed and before logging off the user. + Default value is 5 + Minimum value is 1 + Maximum value is 999999 + This property is required if displayWarningBeforeForcedLogoff is $true + +.PARAMETER forcedLogoffMessage + The warning to be displayed before logging off the user. + +.PARAMETER enableServerInSingleUserMode + Permits certain RDSServer operating systems to be used for non-RDS Desktops. + +.PARAMETER storeCALOnBroker + Used for configuring whether or not to store the RDS Per Device CAL on Broker. + $TRUE or $FALSE + +.PARAMETER storeCALOnClient + Used for configuring whether or not to store the RDS Per Device CAL on client devices. This value can be true only if the storeCALOnBroker is true. + $TRUE or $FALSE + +.PARAMETER reauthSecureTunnelAfterInterruption + Reauthenticate secure tunnel connections after network interruption Determines if user credentials must be reauthenticated after a network interruption when View clients use secure tunnel connections to View resources. When you select this setting, if a secure tunnel connection ends during a session, View Client requires the user to reauthenticate before reconnecting. This setting offers increased security. For example, if a laptop is stolen and moved to a different network, the user cannot automatically gain access to the remote resource because the network connection was temporarily interrupted. When this setting is not selected, the client reconnects to the resource without requiring the user to reauthenticate. This setting has no effect when you use direct connection. + +.PARAMETER messageSecurityMode + Determines if signing and verification of the JMS messages passed between View Manager components takes place. + "DISABLED" Message security mode is disabled. + "MIXED" Message security mode is enabled but not enforced. You can use this mode to detect components in your View environment that predate View Manager 3.0. The log files generated by View Connection Server contain references to these components. + "ENABLED" Message security mode is enabled. Unsigned messages are rejected by View components. Message security mode is enabled by default. Note: View components that predate View Manager 3.0 are not allowed to communicate with other View components. + "ENHANCED" Message Security mode is Enhanced. Message signing and validation is performed based on the current Security Level and desktop Message Security mode. + +.PARAMETER enableIPSecForSecurityServerPairing + Determines whether to use Internet Protocol Security (IPSec) for connections between security servers and View Connection Server instances. By default, secure connections (using IPSec) for security server connections is enabled. + $TRUE or $FALSE + +.EXAMPLE + Set-HVGlobalSettings 'ManualPool' -Spec 'C:\Set-HVGlobalSettings\Set-GlobalSettings.json' + +.EXAMPLE + Set-HVGlobalSettings -Key 'generalData.clientMaxSessionTimePolicy' -Value 'NEVER' + +.EXAMPLE + Set-HVGlobalSettings -clientMaxSessionTimePolicy "TIMEOUT_AFTER" -clientMaxSessionTimeMinutes 1200 + +.OUTPUTS + None + +.NOTES + Author : Matt Frey. + Author email : mfrey@vmware.com + Version : 1.0 + + ===Tested Against Environment==== + Horizon View Server Version : 7.1 + PowerCLI Version : PowerCLI 6.5.1 + PowerShell Version : 5.0 +#> + + [CmdletBinding( + SupportsShouldProcess = $true, + ConfirmImpact = 'High' + )] + + param( + [Parameter(Mandatory = $false)] + [string]$Key, + + [Parameter(Mandatory = $false)] + $Value, + + [Parameter(Mandatory = $false)] + [string]$Spec, + + [Parameter(Mandatory = $false)] + [ValidateSet('TIMEOUT_AFTER','NEVER')] + [string]$clientMaxSessionTimePolicy, + + [Parameter(Mandatory = $false)] + [ValidateRange(5,600)] + [Int]$clientMaxSessionTimeMinutes, + + [Parameter(Mandatory = $false)] + [ValidateSet('TIMEOUT_AFTER','NEVER')] + [string]$clientIdleSessionTimeoutPolicy, + + [Parameter(Mandatory = $false)] + [Int]$clientIdleSessionTimeoutMinutes, + + [Parameter(Mandatory = $false)] + [Int]$clientSessionTimeoutMinutes, + + [Parameter(Mandatory = $false)] + [ValidateSet('DISABLE_AFTER','DISABLED','ALWAYS_ENABLED')] + [string]$desktopSSOTimeoutPolicy, + + [Parameter(Mandatory = $false)] + [ValidateRange(1,999)] + [Int]$desktopSSOTimeoutMinutes, + + [Parameter(Mandatory = $false)] + [ValidateSet('DISABLE_AFTER','DISABLED','ALWAYS_ENABLED')] + [string]$applicationSSOTimeoutPolicy, + + [Parameter(Mandatory = $false)] + [ValidateRange(1,999)] + [Int]$applicationSSOTimeoutMinutes, + + [Parameter(Mandatory = $false)] + [ValidateRange(1,4320)] + [Int]$viewAPISessionTimeoutMinutes, + + [Parameter(Mandatory = $false)] + [string]$preLoginMessage, + + [Parameter(Mandatory = $false)] + [boolean]$displayWarningBeforeForcedLogoff, + + [Parameter(Mandatory = $false)] + [ValidateRange(1,999999)] + [Int]$forcedLogoffTimeoutMinutes, + + [Parameter(Mandatory = $false)] + [string]$forcedLogoffMessage, + + [Parameter(Mandatory = $false)] + [boolean]$enableServerInSingleUserMode, + + [Parameter(Mandatory = $false)] + [boolean]$storeCALOnBroker, + + [Parameter(Mandatory = $false)] + [boolean]$storeCALOnClient, + + [Parameter(Mandatory = $false)] + [boolean]$reauthSecureTunnelAfterInterruption, + + [Parameter(Mandatory = $false)] + [ValidateSet('DISABLED','MIXED','ENABLED','ENHANCED')] + [string]$messageSecurityMode, + + [Parameter(Mandatory = $false)] + [boolean]$enableIPSecForSecurityServerPairing, + + [Parameter(Mandatory = $false)] + $HvServer = $null + ) + + begin { + $services = Get-ViewAPIService -hvServer $hvServer + if ($null -eq $services) { + Write-Error "Could not retrieve ViewApi services from connection object" + break + } + } + + process { + + $updates = @() + if ($key -and $value) { + $updates += Get-MapEntry -key $key -value $value + } elseif ($key -or $value) { + Write-Error "Both key:[$key] and value:[$value] needs to be specified" + } + if ($spec) { + try { + $specObject = Get-JsonObject -specFile $spec + } catch { + Write-Error "Json file exception, $_" + return + } + foreach ($member in ($specObject.PSObject.Members | Where-Object { $_.MemberType -eq 'NoteProperty' })) { + $updates += Get-MapEntry -key $member.name -value $member.value + } + } + if ($clientMaxSessionTimePolicy) { + $updates += Get-MapEntry -key 'generalData.clientMaxSessionTimePolicy' -Value $clientMaxSessionTimePolicy + } + if ($clientMaxSessionTimeMinutes) { + $updates += Get-MapEntry -key 'generalData.clientMaxSessionTimeMinutes' -Value $clientMaxSessionTimeMinutes + } + if ($clientIdleSessionTimeoutPolicy) { + $updates += Get-MapEntry -key 'generalData.clientIdleSessionTimeoutPolicy' -Value $clientIdleSessionTimeoutPolicy + } + if ($clientIdleSessionTimeoutMinutes) { + $updates += Get-MapEntry -key 'generalData.clientIdleSessionTimeoutMinutes' -Value $clientIdleSessionTimeoutMinutes + } + if ($clientSessionTimeoutMinutes) { + $updates += Get-MapEntry -key 'generalData.clientSessionTimeoutMinutes' -Value $clientSessionTimeoutMinutes + } + if ($desktopSSOTimeoutPolicy) { + $updates += Get-MapEntry -key 'generalData.desktopSSOTimeoutPolicy' -Value $desktopSSOTimeoutPolicy + } + if ($desktopSSOTimeoutMinutes) { + $updates += Get-MapEntry -key 'generalData.desktopSSOTimeoutMinutes' -Value $desktopSSOTimeoutMinutes + } + if ($applicationSSOTimeoutPolicy) { + $updates += Get-MapEntry -key 'generalData.applicationSSOTimeoutPolicy' -Value $applicationSSOTimeoutPolicy + } + if ($applicationSSOTimeoutMinutes) { + $updates += Get-MapEntry -key 'generalData.applicationSSOTimeoutMinutes' -Value $applicationSSOTimeoutMinutes + } + if ($viewAPISessionTimeoutMinutes) { + $updates += Get-MapEntry -key 'generalData.viewAPISessionTimeoutMinutes' -Value $viewAPISessionTimeoutMinutes + } + if ($preLoginMessage) { + $updates += Get-MapEntry -key 'generalData.preLoginMessage' -Value $preLoginMessage + } + if ($displayWarningBeforeForcedLogoff) { + $updates += Get-MapEntry -key 'generalData.displayWarningBeforeForcedLogoff' -Value $displayWarningBeforeForcedLogoff + } + if ($forcedLogoffTimeoutMinutes) { + $updates += Get-MapEntry -key 'generalData.forcedLogoffTimeoutMinutes' -Value $forcedLogoffTimeoutMinutes + } + if ($forcedLogoffMessage) { + $updates += Get-MapEntry -key 'generalData.forcedLogoffMessage' -Value $forcedLogoffMessage + } + if ($enableServerInSingleUserMode) { + $updates += Get-MapEntry -key 'generalData.enableServerInSingleUserMode' -Value $enableServerInSingleUserMode + } + if ($storeCALOnBroker) { + $updates += Get-MapEntry -key 'generalData.storeCALOnBroker' -Value $storeCALOnBroker + } + if ($storeCALOnClient) { + $updates += Get-MapEntry -key 'generalData.storeCALOnClient' -Value $storeCALOnClient + } + if ($reauthSecureTunnelAfterInterruption) { + $updates += Get-MapEntry -key 'securityData.reauthSecureTunnelAfterInterruption' -Value $reauthSecureTunnelAfterInterruption + } + if ($messageSecurityMode) { + $updates += Get-MapEntry -key 'securityData.messageSecurityMode' -Value $messageSecurityMode + } + if ($enableIPSecForSecurityServerPairing) { + $updates += Get-MapEntry -key 'securityData.enableIPSecForSecurityServerPairing' -Value $enableIPSecForSecurityServerPairing + } + + $global_settings_helper = New-Object VMware.Hv.GlobalSettingsService + + $global_settings_helper.GlobalSettings_Update($services,$updates) + + } + + end { + [System.gc]::collect() + } +} + +Export-ModuleMember Add-HVDesktop,Add-HVRDSServer,Connect-HVEvent,Disconnect-HVEvent,Get-HVPoolSpec,Get-HVInternalName, Get-HVEvent,Get-HVFarm,Get-HVFarmSummary,Get-HVPool,Get-HVPoolSummary,Get-HVMachine,Get-HVMachineSummary,Get-HVQueryResult,Get-HVQueryFilter,New-HVFarm,New-HVPool,Remove-HVFarm,Remove-HVPool,Set-HVFarm,Set-HVPool,Start-HVFarm,Start-HVPool,New-HVEntitlement,Get-HVEntitlement,Remove-HVEntitlement, Set-HVMachine, New-HVGlobalEntitlement, Remove-HVGlobalEntitlement, Get-HVGlobalEntitlement, Get-HVPodSession, Set-HVApplicationIcon, Remove-HVApplicationIcon, Get-HVGlobalSettings, Set-HVGlobalSettings From 6ca366e8c4f46d5e412ed17024e12e9bb7e59a69 Mon Sep 17 00:00:00 2001 From: Ryan Bolger Date: Sat, 24 Jun 2017 23:55:58 -0700 Subject: [PATCH 045/112] bugfixes for issues #96 and #97 --- Modules/Backup-VCSA/Backup-VCSA.psm1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/Backup-VCSA/Backup-VCSA.psm1 b/Modules/Backup-VCSA/Backup-VCSA.psm1 index 271a7ff..f5731c0 100644 --- a/Modules/Backup-VCSA/Backup-VCSA.psm1 +++ b/Modules/Backup-VCSA/Backup-VCSA.psm1 @@ -72,7 +72,7 @@ $BackupJob = $BackupAPI.create($CreateSpec) } catch { - Write-Error $Error[0].exception.Message + throw $_.Exception.Message } @@ -84,6 +84,7 @@ start-sleep -seconds 5 } until ($BackupAPI.get("$($BackupJob.ID)").progress -eq 100 -or $BackupAPI.get("$($BackupJob.ID)").state -ne "INPROGRESS") + Write-Progress -Activity "Backing up VCSA" -Completed $BackupAPI.get("$($BackupJob.ID)") | select id, progress, state } Else { From 0beda7c30dc3a53a04d6dc3d7614f3b6f4c8109e Mon Sep 17 00:00:00 2001 From: William Lam Date: Wed, 5 Jul 2017 05:31:22 -0700 Subject: [PATCH 046/112] Adding Module for vSphere Content Library --- Modules/ContentLibrary/ContentLibrary.psm1 | 197 +++++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 Modules/ContentLibrary/ContentLibrary.psm1 diff --git a/Modules/ContentLibrary/ContentLibrary.psm1 b/Modules/ContentLibrary/ContentLibrary.psm1 new file mode 100644 index 0000000..824cb16 --- /dev/null +++ b/Modules/ContentLibrary/ContentLibrary.psm1 @@ -0,0 +1,197 @@ +Function Get-ContentLibrary { +<# + .NOTES + =========================================================================== + Created by: William Lam + Organization: VMware + Blog: www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + .DESCRIPTION + This function lists all available vSphere Content Libaries + .PARAMETER LibraryName + The name of a vSphere Content Library + .EXAMPLE + Get-ContentLibrary + .EXAMPLE + Get-ContentLibrary -LibraryName Test +#> + param( + [Parameter(Mandatory=$false)][String]$LibraryName + ) + + $contentLibaryService = Get-CisService com.vmware.content.library + $libaryIDs = $contentLibaryService.list() + + $results = @() + foreach($libraryID in $libaryIDs) { + $library = $contentLibaryService.get($libraryId) + + # Use vCenter REST API to retrieve name of Datastore that is backing the Content Library + $datastoreService = Get-CisService com.vmware.vcenter.datastore + $datastore = $datastoreService.get($library.storage_backings.datastore_id) + + if(!$LibraryName) { + $libraryResult = [pscustomobject] @{ + Id = $library.Id; + Name = $library.Name; + Type = $library.Type; + Description = $library.Description; + Datastore = $datastore.name; + CreationTime = $library.Creation_Time; + } + $results+=$libraryResult + } else { + if($LibraryName -eq $library.name) { + $libraryResult = [pscustomobject] @{ + Name = $library.Name; + Id = $library.Id; + Type = $library.Type; + Description = $library.Description; + Datastore = $datastore.name; + CreationTime = $library.Creation_Time; + } + $results+=$libraryResult + } + } + } + $results +} + +Function Get-ContentLibraryItems { +<# + .NOTES + =========================================================================== + Created by: William Lam + Organization: VMware + Blog: www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + .DESCRIPTION + This function lists all items within a given vSphere Content Library + .PARAMETER LibraryName + The name of a vSphere Content Library + .PARAMETER LibraryItemName + The name of a vSphere Content Library Item + .EXAMPLE + Get-ContentLibraryItems -LibraryName Test + .EXAMPLE + Get-ContentLibraryItems -LibraryName Test -LibraryItemName TinyPhotonVM +#> + param( + [Parameter(Mandatory=$true)][String]$LibraryName, + [Parameter(Mandatory=$false)][String]$LibraryItemName + ) + + $contentLibaryService = Get-CisService com.vmware.content.library + $libaryIDs = $contentLibaryService.list() + + $results = @() + foreach($libraryID in $libaryIDs) { + $library = $contentLibaryService.get($libraryId) + if($library.name -eq $LibraryName) { + $contentLibaryItemService = Get-CisService com.vmware.content.library.item + $itemIds = $contentLibaryItemService.list($libraryID) + + foreach($itemId in $itemIds) { + $item = $contentLibaryItemService.get($itemId) + + if(!$LibraryItemName) { + $itemResult = [pscustomobject] @{ + Name = $item.name; + Id = $item.id; + Description = $item.description; + Size = $item.size + Type = $item.type; + Version = $item.version; + MetadataVersion = $item.metadata_version; + ContentVersion = $item.content_version; + } + $results+=$itemResult + } else { + if($LibraryItemName -eq $item.name) { + $itemResult = [pscustomobject] @{ + Name = $item.name; + Id = $item.id; + Description = $item.description; + Size = $item.size + Type = $item.type; + Version = $item.version; + MetadataVersion = $item.metadata_version; + ContentVersion = $item.content_version; + } + $results+=$itemResult + } + } + } + } + } + $results +} + +Function Get-ContentLibraryItemFiles { +<# + .NOTES + =========================================================================== + Created by: William Lam + Organization: VMware + Blog: www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + .DESCRIPTION + This function lists all item files within a given vSphere Content Library + .PARAMETER LibraryName + The name of a vSphere Content Library + .PARAMETER LibraryItemName + The name of a vSphere Content Library Item + .EXAMPLE + Get-ContentLibraryItemFiles -LibraryName Test + .EXAMPLE + Get-ContentLibraryItemFiles -LibraryName Test -LibraryItemName TinyPhotonVM +#> + param( + [Parameter(Mandatory=$true)][String]$LibraryName, + [Parameter(Mandatory=$false)][String]$LibraryItemName + ) + + $contentLibaryService = Get-CisService com.vmware.content.library + $libaryIDs = $contentLibaryService.list() + + $results = @() + foreach($libraryID in $libaryIDs) { + $library = $contentLibaryService.get($libraryId) + if($library.name -eq $LibraryName) { + $contentLibaryItemService = Get-CisService com.vmware.content.library.item + $itemIds = $contentLibaryItemService.list($libraryID) + + foreach($itemId in $itemIds) { + $itemName = ($contentLibaryItemService.get($itemId)).name + $contenLibraryItemFileSerice = Get-CisService com.vmware.content.library.item.file + $files = $contenLibraryItemFileSerice.list($itemId) + + foreach($file in $files) { + if(!$LibraryItemName) { + $fileResult = [pscustomobject] @{ + Name = $file.name; + Version = $file.version; + Size = $file.size; + Stored = $file.cached; + } + $results+=$fileResult + } else { + if($itemName -eq $LibraryItemName) { + $fileResult = [pscustomobject] @{ + Name = $file.name; + Version = $file.version; + Size = $file.size; + Stored = $file.cached; + } + $results+=$fileResult + } + } + } + } + } + } + $results +} \ No newline at end of file From 23b36d60a3cad40aae6922f8d15ac458bee96daa Mon Sep 17 00:00:00 2001 From: yomurakami Date: Thu, 20 Jul 2017 01:45:05 +0900 Subject: [PATCH 047/112] load an assembly that is not loaed by default --- Modules/Backup-VCSA/Backup-VCSA.psm1 | 1 + 1 file changed, 1 insertion(+) diff --git a/Modules/Backup-VCSA/Backup-VCSA.psm1 b/Modules/Backup-VCSA/Backup-VCSA.psm1 index f5731c0..7eab203 100644 --- a/Modules/Backup-VCSA/Backup-VCSA.psm1 +++ b/Modules/Backup-VCSA/Backup-VCSA.psm1 @@ -50,6 +50,7 @@ ) Begin { if (!($global:DefaultCisServers)){ + Add-Type -Assembly System.Windows.Forms [System.Windows.Forms.MessageBox]::Show("It appears you have not created a connection to the CisServer. You will now be prompted to enter your vCenter credentials to continue" , "Connect to CisServer") | out-null $Connection = Connect-CisServer $global:DefaultVIServer } else { From d61cfc1b67d1580a49d59b28b564e15ef0711be9 Mon Sep 17 00:00:00 2001 From: yomurakami Date: Thu, 20 Jul 2017 02:08:51 +0900 Subject: [PATCH 048/112] seat only backup is impossible. so change to common only backup fix notes --- Modules/Backup-VCSA/Backup-VCSA.psm1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Modules/Backup-VCSA/Backup-VCSA.psm1 b/Modules/Backup-VCSA/Backup-VCSA.psm1 index 7eab203..7b790d0 100644 --- a/Modules/Backup-VCSA/Backup-VCSA.psm1 +++ b/Modules/Backup-VCSA/Backup-VCSA.psm1 @@ -32,13 +32,13 @@ If a -LocationType is not chosen, the function will default to FTP. The destination location for a backup must be an empty folder (easiest to use the get-date cmdlet in the location) -ShowProgress will give you a progressbar as well as updates in the console - -SeatBackup will only backup the config whereas -Fullbackup grabs the historical data as well + -CommonBackup will only backup the config whereas -Fullbackup grabs the historical data as well #> param ( [Parameter(ParameterSetName=’FullBackup’)] [switch]$FullBackup, - [Parameter(ParameterSetName=’SeatBackup’)] - [switch]$SeatBackup, + [Parameter(ParameterSetName=’CommonBackup’)] + [switch]$CommonBackup, [ValidateSet('FTPS', 'HTTP', 'SCP', 'HTTPS', 'FTP')] $LocationType = "FTP", $Location, @@ -57,7 +57,7 @@ $Connection = $global:DefaultCisServers } if ($FullBackup) {$parts = @("common","seat")} - if ($SeatBackup) {$parts = @("seat")} + if ($CommonBackup) {$parts = @("common")} } Process{ $BackupAPI = Get-CisService com.vmware.appliance.recovery.backup.job From 1172ebb2d3bff7ed9f1fe6c0088dbc19804232f3 Mon Sep 17 00:00:00 2001 From: Alessio Rocchi Date: Thu, 27 Jul 2017 11:35:01 +0200 Subject: [PATCH 049/112] Moved script in the correct directory. --- SetDatastoreTag.ps1 | 198 -------------------------------------------- 1 file changed, 198 deletions(-) delete mode 100755 SetDatastoreTag.ps1 diff --git a/SetDatastoreTag.ps1 b/SetDatastoreTag.ps1 deleted file mode 100755 index 23708e5..0000000 --- a/SetDatastoreTag.ps1 +++ /dev/null @@ -1,198 +0,0 @@ -<# - .SYNOPSIS - A brief description of the file. - - .DESCRIPTION - Given a list of Datastore Names, this script will assign a Tag to them - - .PARAMETER csvFile - String representing the full path of the file - The file must be structured like this: - ----------------------------- - Tag1,Tag2,Tag3,Tag4 - IPv4-iSCSI-SiteA,Tag1,Tag3 - IPv4-NFS-SiteA,Tag2,Tag4 - ... - ----------------------------- - - .NOTES - =========================================================================== - Created on: 31/03/2017 11:16 - Created by: Alessio Rocchi - Organization: VMware - Filename: SetDatastoreTag.ps1 - =========================================================================== -#> -[CmdletBinding()] -param -( - [Parameter(Mandatory = $true, - ValueFromPipeline = $true)] - [ValidateNotNullOrEmpty()] - [System.String]$csvFile, - [Parameter(Mandatory = $true, - ValueFromPipeline = $true)] - [ValidateNotNullOrEmpty()] - [String]$vCenter, - [Parameter(ValueFromPipeline = $true, - Position = 2)] - [AllowNull()] - [String]$Username, - [Parameter(Position = 3)] - [AllowNull()] - [String]$Password -) - -Import-Module -Name VMware.VimAutomation.Core -ErrorAction SilentlyContinue | Out-Null - -class vcConnector : System.IDisposable -{ - [String]$Username - [String]$Password - [String]$vCenter - [PSObject]$server - - static [vcConnector]$instance - - vcConnector($Username, $Password, $vCenter) - { - Import-Module -Name VMware.VimAutomation.Core -ErrorAction SilentlyContinue | Out-Null - - $this.Username = $Username - $this.Password = $Password - $this.vCenter = $vCenter - $this.connect() - } - - vcConnector($vcCredential, $vCenter) - { - Import-Module -Name VMware.VimAutomation.Core -ErrorAction SilentlyContinue | Out-Null - - $this.vcCredential = $vcCredential - $this.vCenter = $vCenter - $this.connect() - } - - [void] hidden connect() - { - try - { - if ([String]::IsNullOrEmpty($this.Username) -or [String]::IsNullOrEmpty($this.Password)) - { - $vcCredential = Get-Credential - Connect-VIServer -Server $this.vCenter -Credential $this.vcCredential -WarningAction SilentlyContinue -ErrorAction Stop | Out-Null - } - else - { - Connect-VIServer -Server $this.vCenter -User $this.Username -Password $this.Password -WarningAction SilentlyContinue -ErrorAction Stop - } - Write-Debug("Connected to vCenter: {0}" -f $this.vCenter) - } - catch - { - Write-Error($Error[0].Exception.Message) - exit - } - } - - - [void] Dispose() - { - Write-Debug("Called Dispose Method of Instance: {0}" -f ($this)) - Disconnect-VIServer -WarningAction SilentlyContinue -Server $this.vCenter -Force -Confirm:$false | Out-Null - } - - static [vcConnector] GetInstance() - { - if ([vcConnector]::instance -eq $null) - { - [vcConnector]::instance = [vcConnector]::new() - } - - return [vcConnector]::instance - } -} - -class Content{ - [System.Collections.Generic.List[System.String]]$availableTags - [System.Collections.Generic.List[System.String]]$elements - - Content() - { - } - - Content([String]$filePath) - { - if ((Test-Path -Path $filePath) -eq $false) - { - throw ("Cannot find file: {0}" -f ($filePath)) - } - try - { - # Cast the Get-Content return type to Generic List of Strings in order to avoid fixed-size array - $this.elements = [System.Collections.Generic.List[System.String]](Get-Content -Path $filePath -ea SilentlyContinue -wa SilentlyContinue) - $this.availableTags = $this.elements[0].split(',') - # Delete the first element aka availableTags - $this.elements.RemoveAt(0) - } - catch - { - throw ("Error reading the file: {0}" -f ($filePath)) - } - } -} - -try -{ - $vc = [vcConnector]::new($Username, $Password, $vCenter) - $csvContent = [Content]::new($csvFile) - - Write-Host("Available Tags: {0}" -f ($csvContent.availableTags)) - - foreach ($element in $csvContent.elements) - { - [System.Collections.Generic.List[System.String]]$splittedList = $element.split(',') - # Get the Datastore Name - [System.String]$datastoreName = $splittedList[0] - # Removing Datastore Name - $splittedList.RemoveAt(0) - # Create a List of Tags which will be assigned to the Datastore - [System.Collections.Generic.List[PSObject]]$tagsToAssign = $splittedList | ForEach-Object { Get-Tag -Name $_ } - Write-Host("Tags to assign to Datastore: {0} are: {1}" -f ($datastoreName, $tagsToAssign)) - # Get Datastore object by the given Datastore Name, first field of the the line - $datastore = Get-Datastore -Name $datastoreName -ea Stop - # Iterate the assigned Datastore Tags - foreach ($tag in ($datastore | Get-TagAssignment)) - { - # Check if the current tag is one of the available ones. - if ($tag.Tag.Name -in $csvContent.availableTags) - { - # Remove the current assigned Tag - Write-Host("Removing Tag: {0}" -f ($tag)) - Remove-TagAssignment -TagAssignment $tag -Confirm:$false - } - } - # Finally add the new set of tags to the Datastore - foreach ($tag in $tagsToAssign) - { - Write-Host("Trying to assign Tag: {0} to Datastore: {1}" -f ($tag.Name, $datastoreName)) - # Assign the Tag - New-TagAssignment -Entity $datastore -Tag $tag - } - } -} -catch [VMware.VimAutomation.Sdk.Types.V1.ErrorHandling.VimException.VimException] -{ - Write-Error("VIException: {0}" -f ($Error[0].Exception.Message)) - exit -} -catch -{ - Write-Error $Error[0].Exception.Message - exit -} -finally -{ - # Let be assured that the vc connection will be disposed. - $vc.Dispose() -} From c0d0e9e4412bc22c760658de851d6f2fd8e9cdaa Mon Sep 17 00:00:00 2001 From: Alessio Rocchi Date: Thu, 27 Jul 2017 11:38:29 +0200 Subject: [PATCH 050/112] Added the script in the correct path. --- Scripts/SetDatastoreTag.ps1 | 198 ++++++++++++++++++++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100755 Scripts/SetDatastoreTag.ps1 diff --git a/Scripts/SetDatastoreTag.ps1 b/Scripts/SetDatastoreTag.ps1 new file mode 100755 index 0000000..23708e5 --- /dev/null +++ b/Scripts/SetDatastoreTag.ps1 @@ -0,0 +1,198 @@ +<# + .SYNOPSIS + A brief description of the file. + + .DESCRIPTION + Given a list of Datastore Names, this script will assign a Tag to them + + .PARAMETER csvFile + String representing the full path of the file + The file must be structured like this: + ----------------------------- + Tag1,Tag2,Tag3,Tag4 + IPv4-iSCSI-SiteA,Tag1,Tag3 + IPv4-NFS-SiteA,Tag2,Tag4 + ... + ----------------------------- + + .NOTES + =========================================================================== + Created on: 31/03/2017 11:16 + Created by: Alessio Rocchi + Organization: VMware + Filename: SetDatastoreTag.ps1 + =========================================================================== +#> +[CmdletBinding()] +param +( + [Parameter(Mandatory = $true, + ValueFromPipeline = $true)] + [ValidateNotNullOrEmpty()] + [System.String]$csvFile, + [Parameter(Mandatory = $true, + ValueFromPipeline = $true)] + [ValidateNotNullOrEmpty()] + [String]$vCenter, + [Parameter(ValueFromPipeline = $true, + Position = 2)] + [AllowNull()] + [String]$Username, + [Parameter(Position = 3)] + [AllowNull()] + [String]$Password +) + +Import-Module -Name VMware.VimAutomation.Core -ErrorAction SilentlyContinue | Out-Null + +class vcConnector : System.IDisposable +{ + [String]$Username + [String]$Password + [String]$vCenter + [PSObject]$server + + static [vcConnector]$instance + + vcConnector($Username, $Password, $vCenter) + { + Import-Module -Name VMware.VimAutomation.Core -ErrorAction SilentlyContinue | Out-Null + + $this.Username = $Username + $this.Password = $Password + $this.vCenter = $vCenter + $this.connect() + } + + vcConnector($vcCredential, $vCenter) + { + Import-Module -Name VMware.VimAutomation.Core -ErrorAction SilentlyContinue | Out-Null + + $this.vcCredential = $vcCredential + $this.vCenter = $vCenter + $this.connect() + } + + [void] hidden connect() + { + try + { + if ([String]::IsNullOrEmpty($this.Username) -or [String]::IsNullOrEmpty($this.Password)) + { + $vcCredential = Get-Credential + Connect-VIServer -Server $this.vCenter -Credential $this.vcCredential -WarningAction SilentlyContinue -ErrorAction Stop | Out-Null + } + else + { + Connect-VIServer -Server $this.vCenter -User $this.Username -Password $this.Password -WarningAction SilentlyContinue -ErrorAction Stop + } + Write-Debug("Connected to vCenter: {0}" -f $this.vCenter) + } + catch + { + Write-Error($Error[0].Exception.Message) + exit + } + } + + + [void] Dispose() + { + Write-Debug("Called Dispose Method of Instance: {0}" -f ($this)) + Disconnect-VIServer -WarningAction SilentlyContinue -Server $this.vCenter -Force -Confirm:$false | Out-Null + } + + static [vcConnector] GetInstance() + { + if ([vcConnector]::instance -eq $null) + { + [vcConnector]::instance = [vcConnector]::new() + } + + return [vcConnector]::instance + } +} + +class Content{ + [System.Collections.Generic.List[System.String]]$availableTags + [System.Collections.Generic.List[System.String]]$elements + + Content() + { + } + + Content([String]$filePath) + { + if ((Test-Path -Path $filePath) -eq $false) + { + throw ("Cannot find file: {0}" -f ($filePath)) + } + try + { + # Cast the Get-Content return type to Generic List of Strings in order to avoid fixed-size array + $this.elements = [System.Collections.Generic.List[System.String]](Get-Content -Path $filePath -ea SilentlyContinue -wa SilentlyContinue) + $this.availableTags = $this.elements[0].split(',') + # Delete the first element aka availableTags + $this.elements.RemoveAt(0) + } + catch + { + throw ("Error reading the file: {0}" -f ($filePath)) + } + } +} + +try +{ + $vc = [vcConnector]::new($Username, $Password, $vCenter) + $csvContent = [Content]::new($csvFile) + + Write-Host("Available Tags: {0}" -f ($csvContent.availableTags)) + + foreach ($element in $csvContent.elements) + { + [System.Collections.Generic.List[System.String]]$splittedList = $element.split(',') + # Get the Datastore Name + [System.String]$datastoreName = $splittedList[0] + # Removing Datastore Name + $splittedList.RemoveAt(0) + # Create a List of Tags which will be assigned to the Datastore + [System.Collections.Generic.List[PSObject]]$tagsToAssign = $splittedList | ForEach-Object { Get-Tag -Name $_ } + Write-Host("Tags to assign to Datastore: {0} are: {1}" -f ($datastoreName, $tagsToAssign)) + # Get Datastore object by the given Datastore Name, first field of the the line + $datastore = Get-Datastore -Name $datastoreName -ea Stop + # Iterate the assigned Datastore Tags + foreach ($tag in ($datastore | Get-TagAssignment)) + { + # Check if the current tag is one of the available ones. + if ($tag.Tag.Name -in $csvContent.availableTags) + { + # Remove the current assigned Tag + Write-Host("Removing Tag: {0}" -f ($tag)) + Remove-TagAssignment -TagAssignment $tag -Confirm:$false + } + } + # Finally add the new set of tags to the Datastore + foreach ($tag in $tagsToAssign) + { + Write-Host("Trying to assign Tag: {0} to Datastore: {1}" -f ($tag.Name, $datastoreName)) + # Assign the Tag + New-TagAssignment -Entity $datastore -Tag $tag + } + } +} +catch [VMware.VimAutomation.Sdk.Types.V1.ErrorHandling.VimException.VimException] +{ + Write-Error("VIException: {0}" -f ($Error[0].Exception.Message)) + exit +} +catch +{ + Write-Error $Error[0].Exception.Message + exit +} +finally +{ + # Let be assured that the vc connection will be disposed. + $vc.Dispose() +} From 11330a67ac7d426c92f650aa5c7395ad8c73146e Mon Sep 17 00:00:00 2001 From: simonfangyingzhang Date: Tue, 22 Aug 2017 07:44:26 +0100 Subject: [PATCH 051/112] New-VIProperty KMSserver per Mike Foley New-VIProperty KMSserver per Mike Foley --- Modules/VMware.VMEncryption/VMware.VMEncryption.psm1 | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Modules/VMware.VMEncryption/VMware.VMEncryption.psm1 b/Modules/VMware.VMEncryption/VMware.VMEncryption.psm1 index c350955..8d19b50 100644 --- a/Modules/VMware.VMEncryption/VMware.VMEncryption.psm1 +++ b/Modules/VMware.VMEncryption/VMware.VMEncryption.psm1 @@ -83,6 +83,13 @@ New-VIProperty -Name EncryptionKeyId -ObjectType HardDisk -Value { } } -BasedOnExtensionProperty 'Backing.KeyId' -Force | Out-Null +New-VIProperty -Name KMSserver -ObjectType VMHost -Value { + Param ($VMHost) + if ($VMHost.CryptoSafe) { + $VMHost.ExtensionData.Runtime.CryptoKeyId.ProviderId.Id + } +} -BasedOnExtensionProperty 'Runtime.CryptoKeyId.ProviderId.Id' -Force | Out-Null + Function Enable-VMHostCryptoSafe { <# .SYNOPSIS From e70e50600fcbc7a674943881e2e123812ce394e5 Mon Sep 17 00:00:00 2001 From: Jim Jones Date: Wed, 30 Aug 2017 12:34:03 -0700 Subject: [PATCH 052/112] Create Get-VMToolsParts.ps1 Outputs the VMware Tool component installation state for all running Windows VMs in your environment. --- Scripts/Get-VMToolsParts.ps1 | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 Scripts/Get-VMToolsParts.ps1 diff --git a/Scripts/Get-VMToolsParts.ps1 b/Scripts/Get-VMToolsParts.ps1 new file mode 100644 index 0000000..c4a3d51 --- /dev/null +++ b/Scripts/Get-VMToolsParts.ps1 @@ -0,0 +1,11 @@ +$vms = Get-VM | where {$_.PowerState -eq "PoweredOn" -and $_.GuestId -match "Windows"} + +ForEach ($vm in $vms){ + Write-Host $vm + $namespace = "root\CIMV2" + $componentPattern = "hcmon|vmci|vmdebug|vmhgfs|VMMEMCTL|vmmouse|vmrawdsk|vmxnet|vmx_svga" + (Get-WmiObject -class Win32_SystemDriver -computername $vm -namespace $namespace | + where-object { $_.Name -match $componentPattern } | + Format-Table -Auto Name,State,StartMode,DisplayName + ) +} From b8d0d1071655eb1c2904384a48673b4a3176353c Mon Sep 17 00:00:00 2001 From: = Date: Wed, 30 Aug 2017 14:48:03 -0700 Subject: [PATCH 053/112] Added CIVM Data gathering function for vCD export/Migration --- Scripts/Get-CIVMData.ps1 | 245 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 245 insertions(+) create mode 100644 Scripts/Get-CIVMData.ps1 diff --git a/Scripts/Get-CIVMData.ps1 b/Scripts/Get-CIVMData.ps1 new file mode 100644 index 0000000..f7e83b3 --- /dev/null +++ b/Scripts/Get-CIVMData.ps1 @@ -0,0 +1,245 @@ +Function Get-CIVMData +{ +<# + .SYNOPSIS + Gathers information about a target CIVM + + .DESCRIPTION + This function gathers CIVM Name, Parent vApp (obj), Parent vApp Name, All network adapters + (including IP, NIC index, and network), and vCenter VMX path details returning the resulting + ordered list. + + .PARAMETER CIVM + The target vCloud VM from which information will be gathered + + .NOTES + Author: Brian Marsh + Version: 1.0 +#> + + [CmdletBinding()] + Param ( + [Parameter( + Position=0, + Mandatory=$true, + ValueFromPipeline=$true, + ValueFromPipelineByPropertyName=$true) + ] + [VMware.VimAutomation.Cloud.Types.V1.CIVM] $CIVM + ) + BEGIN + { + + } + + PROCESS + { + $NewObj = [Ordered]@{} + $NewObj.GetCIVMData = @{} + $NewObj.GetCIVMData.Successful = $true + + # Get the vCenter VM from the vCloud VM object + $vm = $civm | Get-VM -Debug:$False -Verbose:$False + + Write-Verbose "Storing CIVM Name: $($CIVM.Name)/ Status: $($CIVM.Status)" + $NewObj.Name = $CIVM.Name + $NewObj.Status = $CIVM.Status + + # Do some logic here, if the VM name matches "tmpl" or "tpl", it's a template. Grab the vApp name and set "New Name" + If ($CIVM.Name -match "tmpl" -or $CIVM.Name -match "tpl") + { + $NewObj.NewName = $CIVM.VApp.Name + } + Else + { + $NewObj.NewName = $CIVM.Name + } + + Write-Verbose "Recording Reservations" + $NewObj.Reservations = @{} + $NewObj.Reservations.CPU = @{} + $NewObj.Reservations.Memory = @{} + + $NewObj.Reservations.CPU.Reservation = $vm.ExtensionData.ResourceConfig.CpuAllocation.Reservation + $NewObj.Reservations.CPU.Limit = $vm.ExtensionData.ResourceConfig.CpuAllocation.Limit + $NewObj.Reservations.Memory.Reservation = $vm.ExtensionData.ResourceConfig.MemoryAllocation.Reservation + $NewObj.Reservations.Memory.Limit = $vm.ExtensionData.ResourceConfig.MemoryAllocation.Limit + + # Get the UUid from the Id, split out the UUID and pass it along + # Sample Id: urn:vcloud:vm:d9ca710d-cdf2-44eb-a274-26e1dcfd01bb + Write-Verbose "Storing CIVM UUID: $(($CIVM.Id).Split(':')[3])" + $NewObj.Uuid = ($CIVM.Id).Split(':')[3] + + Write-Verbose "Gathering Network details" + $vAppNetworkAdapters = @() + $NetworkAdapters = Get-CINetworkAdapter -VM $civm -Debug:$False -Verbose:$False + + foreach ($networkAdapter in $networkAdapters) + { + # Remove any existing VMNIC variables + Remove-Variable -Name VMNic -ErrorAction SilentlyContinue + + $vAppNicInfo = [Ordered]@{} + $vAppNicInfo.NIC = ("NIC" + $networkAdapter.Index) + $vAppNicInfo.Index = $networkAdapter.Index + $vAppNicInfo.Connected = $networkAdapter.Connected + $vAppNicInfo.ExternalIP = $networkAdapter.IpAddress + $vAppNicInfo.InternalIP = $networkAdapter.ExternalIpAddress + $vAppNicInfo.MacAddress = $networkAdapter.MACAddress + + $vAppNicInfo.vAppNetwork = [Ordered]@{} + $vAppNicInfo.vAppNetwork.Name = $networkAdapter.VAppNetwork.Name + + <# + There is a chance that the vApp Network Name may not match a PortGroup which causes issues upon importing the VM after migration. + To fix this issue, we'll try to find get the PortGroup in this data gathering stage. If it is not found, we'll move on to attempted + remediation: + 1) Get the vCenter VM network adapter that corresponds to this vCloud Director VM network adapter (where MAC Addresses match) + 2) If the vCenter VM network adapter's network name doesn't match 'none' (indicating the VM is powered off) and the vCenter Network + name does not match the vCloud Director network name, set this target object's vAppNetwork Name to the vCenter PortGroup + 3) If the vCenter VM network adapter's network name is 'none' then this VM is probably powered off and the network information is + not defined in vCenter. In this case, we mark the get-data as unsuccessful, set an error message and return. + #> + try + { + $vm | Get-VMHost -Debug:$false -Verbose:$false | Get-VDSwitch -Debug:$false -Verbose:$false -ErrorAction Stop | ` + Get-VDPortgroup -name $networkAdapter.vAppNetwork.Name -Debug:$false -Verbose:$false -ErrorAction Stop | Out-Null + } + catch + { + Write-Debug "Portgroup not found by name $($networkAdapter.vAppNetwork.Name), Debug?" + Write-Verbose "Portgroup not found by name $($networkAdapter.vAppNetwork.Name), attempting fall back." + # Get VIVM network adapter where adapter mac matches vappnicinfo MacAddress + $VMNic = $vm | Get-NetworkAdapter -Debug:$false -Verbose:$false | Where-Object { $_.MacAddress -eq $vAppNicInfo.MacAddress } + + # If VMNic Network Name doesn't match 'none' and doesn't match the vAppNetworkName, set vAppNetwork name to VMNic Network name + If ( ($VMNic.NetworkName -notlike 'none') -and ($VMNic.NetworkName -ne $vAppNicInfo.vAppNetwork.Name)) + { + $vAppNicInfo.vAppNetwork.Name = $VMNic.NetworkName + } + else + { + Write-Debug "Tried to recover from missing network port group. Failed. Debug?" + $ErrorMessage = "VM [ $($CIVM.Name) ] has vAppNetwork connection that doesn't exist in vCenter [ $($vAppNicInfo.vAppNetwork.Name) ]" + $NewObj.GetCIVMData.Successful = $False + $NewObj.GetCIVMData.Error = $ErrorMessage + Write-Error $ErrorMessage + + #Return whatever object we have at this point + $NewObj + + Return + } + + } + + $vAppNetworkAdapters += $vAppNicInfo + } + + Write-Verbose "Checking for Duplicate name upon Import" + Try + { + $DupeVM = Get-VM -Name $NewObj.NewName -Debug:$false -Verbose:$false -ErrorAction Stop -ErrorVariable DupeVM + If ($DupeVM) + { + $NewObj.GetCIVMData.Successful = $False + $NewObj.GetCIVMData.Error = "VM with name $($NewObj.NewName) already exists in vCenter" + Write-Error "VM with name $($NewObj.NewName) already exists in vCenter" + + #Return whatever object we have at this point + $NewObj + + return + } + } + Catch + { + Write-Verbose "No Duplicate Name Found!" + } + + $NewObj.vAppNetworkAdapters = $vAppNetworkAdapters + + Write-Verbose "Setting VIVIM object, parent vApp details, and CIVM object" + try + { + $NewObj.VIVM = $vm + $NewObj.ToolsStatus = $vm.ExtensionData.Guest.ToolsStatus + $NewObj.ToolsRunningStatus = $vm.ExtensionData.Guest.ToolsRunningStatus + $NewObj.HasSnapshots = ($vm | Get-Snapshot -Debug:$false -Verbose:$false -ErrorAction Stop | Select-Object Name, Description,VMId) + $NewObj.NeedsConsolidation = $vm.ExtensionData.Runtime.ConsolidationNeeded + $NewObj.OldMoref = $vm.Id + $NewObj.VmPathName = $vm.ExtensionData.Config.Files.VmPathName + $NewObj.ParentVApp = $CIVM.VApp.Name + $NewObj.StorageReservation = ($vm |Get-DatastoreCluster -Debug:$false -Verbose:$false -ErrorAction Stop | Select-Object -ExpandProperty Name) + $NewObj.CIVMId = $CIVM.Id + } + catch + { + $NewObj.GetCIVMData.Successful = $False + $NewObj.GetCIVMData.Error = "VM [ $($CIVM.Name) ] something went wrong while gathering details: $_" + Write-Debug "VM [ $($CIVM.Name) ] something went wrong while gathering details: $_, Debug" + Write-Error "VM [ $($CIVM.Name) ] something went wrong while gathering details: $_. " + + #Return whatever object we have at this point + $NewObj + + Return + } + + # If ToolsStatus is not 'toolsOk' and status is not "PoweredOn", bomb out. We won't be able to power this VM off later. + If ($NewObj.ToolsRunningStatus -ne 'guestToolsRunning' -and $NewObj.status -eq "PoweredOn") + { + $NewObj.GetCIVMData.Successful = $False + $NewObj.GetCIVMData.Error = "VM [ $($CIVM.Name) ] tools are not running but the VM is powered On. Fix and try again." + Write-Debug "VM [ $($CIVM.Name) ] tools are not running but the VM is powered On, Debug" + Write-Error "VM [ $($CIVM.Name) ] tools are not running but the VM is powered On. " + + #Return whatever object we have at this point + $NewObj + + Return + } + + If ($NewObj.HasSnapshots) + { + $NewObj.GetCIVMData.Successful = $False + $NewObj.GetCIVMData.Error = "VM [ $($CIVM.Name) ] has snapshots. Remove before trying again." + Write-Debug "VM [ $($CIVM.Name) ] has snapshots. Remove before trying again, Debug" + Write-Error "VM [ $($CIVM.Name) ] has snapshots. Remove before trying again." + + #Return whatever object we have at this point + $NewObj + + Return + } + + Write-Verbose "Determining the VMX Path for this VM" + + # Get this VM's path on disk + $vmPathName = $vm.ExtensionData.Config.Files.VmPathName + + # Determine in which Datacenter this VM resides + $datacenter = $vm | get-Datacenter -Debug:$False -Verbose:$False | Select-Object -expand name + + # Split out the datastore from the path name + $datastore = $vmPathName.Split("]")[0].split("[")[1] + + # Split out the folder from the path name + $vmFolderPath = $vmPathName.Split("/")[0].split("]")[1].trim() + + # Re-combine into a valid folder path + $vmxPath = "vmstore:\$($datacenter)\$($datastore)\$vmFolderPath" + + Write-Verbose "VMXPath $vmxPath" + $NewObj.vmxPath = $vmxPath + + $NewObj + + } + + END + { + Write-Debug "About to exit Get-CIVMData, anything else?" + Write-Verbose "Exited Get-CIVMData" + } +} From 070de5d0bdf3d7e401598de85035a1fd7b6fb513 Mon Sep 17 00:00:00 2001 From: Derek-Charlekston Date: Wed, 30 Aug 2017 17:26:53 -0500 Subject: [PATCH 054/112] Create get-ping.ps1 --- get-ping.ps1 | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 get-ping.ps1 diff --git a/get-ping.ps1 b/get-ping.ps1 new file mode 100644 index 0000000..77e3e59 --- /dev/null +++ b/get-ping.ps1 @@ -0,0 +1,53 @@ +Function Get-PingStatus + { + param( + [Parameter(ValueFromPipeline=$true)] + [string]$device, + + [validateSet("Online","Offline","ObjectTable")] + [String]$getObject + ) + +begin{ + $hash = @() + + } +process{ + + $device| foreach { + if (Test-Connection $_ -Count 1 -Quiet) { + + if(-not($GetObject)){write-host -ForegroundColor green "Online: $_ "} + + $Hash = $Hash += @{Online="$_"} + }else{ + + if(-not($GetObject)){write-host -ForegroundColor Red "Offline: $_ "} + + $Hash = $Hash += @{Offline="$_"} + } + } + } + +end { + if($GetObject) { + + $Global:Objects = $Hash | foreach { [PSCustomObject]@{ + + DeviceName = $_.Values| foreach { "$_" } + Online = $_.Keys| where {$_ -eq "Online"} + offline = $_.Keys| where {$_ -eq "Offline"} + } + } + + Switch -Exact ($GetObject) + { + + 'Online' { $Global:Objects| where 'online'| select -ExpandProperty DeviceName } + 'Offline' { $Global:Objects| where 'offline'| select -ExpandProperty DeviceName } + 'ObjectTable' { return $Global:Objects } + } + + } + } +} From 98997718b8634f3408d818f98352378251118e51 Mon Sep 17 00:00:00 2001 From: = Date: Wed, 30 Aug 2017 15:29:02 -0700 Subject: [PATCH 055/112] updated code for clarity --- Scripts/Get-CIVMData.ps1 | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/Scripts/Get-CIVMData.ps1 b/Scripts/Get-CIVMData.ps1 index f7e83b3..7215cfc 100644 --- a/Scripts/Get-CIVMData.ps1 +++ b/Scripts/Get-CIVMData.ps1 @@ -44,16 +44,6 @@ Function Get-CIVMData Write-Verbose "Storing CIVM Name: $($CIVM.Name)/ Status: $($CIVM.Status)" $NewObj.Name = $CIVM.Name $NewObj.Status = $CIVM.Status - - # Do some logic here, if the VM name matches "tmpl" or "tpl", it's a template. Grab the vApp name and set "New Name" - If ($CIVM.Name -match "tmpl" -or $CIVM.Name -match "tpl") - { - $NewObj.NewName = $CIVM.VApp.Name - } - Else - { - $NewObj.NewName = $CIVM.Name - } Write-Verbose "Recording Reservations" $NewObj.Reservations = @{} From cd98f8672dfb7e52ebfdc00e456fcae7122ce6ab Mon Sep 17 00:00:00 2001 From: VTsnowboarder42 <31492211+VTsnowboarder42@users.noreply.github.com> Date: Wed, 30 Aug 2017 18:43:43 -0400 Subject: [PATCH 056/112] Create Get-BasicVMCapacityReport Create a basic VM capacity report --- Scripts/Get-BasicVMCapacityReport | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 Scripts/Get-BasicVMCapacityReport diff --git a/Scripts/Get-BasicVMCapacityReport b/Scripts/Get-BasicVMCapacityReport new file mode 100644 index 0000000..edbe9c7 --- /dev/null +++ b/Scripts/Get-BasicVMCapacityReport @@ -0,0 +1,30 @@ + +$myCol = @() +$start = (Get-Date).AddDays(-30) +$finish = Get-Date +$cluster= "Demo" + +$objServers = Get-Cluster $cluster | Get-VM +foreach ($server in $objServers) { + if ($server.guest.osfullname -ne $NULL){ + if ($server.guest.osfullname.contains("Windows")){ + $stats = get-stat -Entity $server -Stat "cpu.usage.average","mem.usage.average" -Start $start -Finish $finish + + $ServerInfo = "" | Select-Object vName, OS, Mem, AvgMem, MaxMem, CPU, AvgCPU, MaxCPU, pDisk, Host + $ServerInfo.vName = $server.name + $ServerInfo.OS = $server.guest.osfullname + $ServerInfo.Host = $server.vmhost.name + $ServerInfo.Mem = $server.memoryGB + $ServerInfo.AvgMem = $("{0:N2}" -f ($stats | Where-Object {$_.MetricId -eq "mem.usage.average"} | Measure-Object -Property Value -Average).Average) + $ServerInfo.MaxMem = $("{0:N2}" -f ($stats | Where-Object {$_.MetricId -eq "mem.usage.average"} | Measure-Object -Property Value -Maximum).Maximum) + $ServerInfo.CPU = $server.numcpu + $ServerInfo.AvgCPU = $("{0:N2}" -f ($stats | Where-Object {$_.MetricId -eq "cpu.usage.average"} | Measure-Object -Property Value -Average).Average) + $ServerInfo.MaxCPU = $("{0:N2}" -f ($stats | Where-Object {$_.MetricId -eq "cpu.usage.average"} | Measure-Object -Property Value -Maximum).Maximum) + $ServerInfo.pDisk = [Math]::Round($server.ProvisionedSpaceGB,2) + + $mycol += $ServerInfo + } + } +} + +$myCol | Sort-Object vName | Export-Csv "VM_report.csv" -NoTypeInformation From b21d2079caf574c8625f0bf76d021aede94dfd58 Mon Sep 17 00:00:00 2001 From: kdmhorn Date: Wed, 30 Aug 2017 18:57:55 -0400 Subject: [PATCH 057/112] Create vmCreationNotes.ps1 --- Scripts/vmCreationNotes.ps1 | 191 ++++++++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 Scripts/vmCreationNotes.ps1 diff --git a/Scripts/vmCreationNotes.ps1 b/Scripts/vmCreationNotes.ps1 new file mode 100644 index 0000000..2a805ca --- /dev/null +++ b/Scripts/vmCreationNotes.ps1 @@ -0,0 +1,191 @@ +<# +.SYNOPSIS + VM_CreationNotes to replace the Notes on newly deployed virtual machines with + information regarding there deployment, including date, time, user and method + of deployment. + +.DESCRIPTION + VM_CreationNotes is run daily as a scheduled task requiring no interaction. + The script will take in vCenter events for the latest 24 hour period filtering + for vm creation, clone or vapp deployment and parse the data. + Utilizes GET-VIEventsPlus by Luc Dekens for faster event gathering +.NOTES + File Name : VM_CreationNotes.ps1 + Author : KWH + Version : 1.03 + License : GNU GPL 3.0 www.gnu.org/licenses/gpl-3.0.en.html + +.INPUTS + No inputs required +.OUTPUTS + No Output is produced + +.PARAMETER config + No Parameters + +.PARAMETER Outputpath + No Parameters + +.PARAMETER job + No Parameters +.CHANGE LOG + #20170301 KWH - Removed canned VM Initialize script in favor of get-module + #20170303 KWH - Change VIEvent Call to date range rather than maxSamples + # KWH - Optimized event call where to array match + # KWH - Updated Synopsis and Description + # KWH - Changed vcenter list to text file input + # KWH - Added Register Events to list + # KWH - Included Get-VIEventPlus by LucD + #20170321 KWH - Added event $VIEvent array declaration/reset and $VM reset on loops + # KWH - Converted returned events to Local Time +#> + +<# + .SYNOPSIS Function GET-VIEventPlus Returns vSphere events + .DESCRIPTION The function will return vSphere events. With + the available parameters, the execution time can be + improved, compered to the original Get-VIEvent cmdlet. + .NOTES Author: Luc Dekens + .PARAMETER Entity + When specified the function returns events for the + specific vSphere entity. By default events for all + vSphere entities are returned. + .PARAMETER EventType + This parameter limits the returned events to those + specified on this parameter. + .PARAMETER Start + The start date of the events to retrieve + .PARAMETER Finish + The end date of the events to retrieve. + .PARAMETER Recurse + A switch indicating if the events for the children of + the Entity will also be returned + .PARAMETER User + The list of usernames for which events will be returned + .PARAMETER System + A switch that allows the selection of all system events. + .PARAMETER ScheduledTask + The name of a scheduled task for which the events + will be returned + .PARAMETER FullMessage + A switch indicating if the full message shall be compiled. + This switch can improve the execution speed if the full + message is not needed. + .EXAMPLE + PS> Get-VIEventPlus -Entity $vm + .EXAMPLE + PS> Get-VIEventPlus -Entity $cluster -Recurse:$true + #> + function Get-VIEventPlus { + + param( + [VMware.VimAutomation.ViCore.Impl.V1.Inventory.InventoryItemImpl[]]$Entity, + [string[]]$EventType, + [DateTime]$Start, + [DateTime]$Finish = (Get-Date), + [switch]$Recurse, + [string[]]$User, + [Switch]$System, + [string]$ScheduledTask, + [switch]$FullMessage = $false + ) + + process { + $eventnumber = 100 + $events = @() + $eventMgr = Get-View EventManager + $eventFilter = New-Object VMware.Vim.EventFilterSpec + $eventFilter.disableFullMessage = ! $FullMessage + $eventFilter.entity = New-Object VMware.Vim.EventFilterSpecByEntity + $eventFilter.entity.recursion = &{if($Recurse){"all"}else{"self"}} + $eventFilter.eventTypeId = $EventType + if($Start -or $Finish){ + $eventFilter.time = New-Object VMware.Vim.EventFilterSpecByTime + if($Start){ + $eventFilter.time.beginTime = $Start + } + if($Finish){ + $eventFilter.time.endTime = $Finish + } + } + if($User -or $System){ + $eventFilter.UserName = New-Object VMware.Vim.EventFilterSpecByUsername + if($User){ + $eventFilter.UserName.userList = $User + } + if($System){ + $eventFilter.UserName.systemUser = $System + } + } + if($ScheduledTask){ + $si = Get-View ServiceInstance + $schTskMgr = Get-View $si.Content.ScheduledTaskManager + $eventFilter.ScheduledTask = Get-View $schTskMgr.ScheduledTask | + where {$_.Info.Name -match $ScheduledTask} | + Select -First 1 | + Select -ExpandProperty MoRef + } + if(!$Entity){ + $Entity = @(Get-Folder -Name Datacenters) + } + $entity | %{ + $eventFilter.entity.entity = $_.ExtensionData.MoRef + $eventCollector = Get-View ($eventMgr.CreateCollectorForEvents($eventFilter)) + $eventsBuffer = $eventCollector.ReadNextEvents($eventnumber) + while($eventsBuffer){ + $events += $eventsBuffer + $eventsBuffer = $eventCollector.ReadNextEvents($eventnumber) + } + $eventCollector.DestroyCollector() + } + $events + } + } + +#Run parameters - Change below if username or vcenter list source changes +$dayBtwnRuns = 1 +$AdminName = "username" +$credfile = "c:\Scripts\common\credentials\runtime-cred.txt" +$vcfile = "c:\Scripts\Common\inputlists\vcenterlist.txt" + +$vmCreationTypes = @() #Remark out any event types not desired below +$vmCreationTypes += "VmCreatedEvent" +$vmCreationTypes += "VmBeingClonedEvent" +$vmCreationTypes += "VmBeingDeployedEvent" +$vmCreationTypes += "VmRegisteredEvent" +$newline = "`r`n" + +#Convert Password and username to credential object +$password = Get-Content $CredFile | ConvertTo-SecureString +$Cred = New-Object -Typename System.Management.Automation.PSCredential -argumentlist $AdminName,$password + +#Load vCenter List +$vCenterServers = Get-Content $vcfile + +If ($daysBtwnRuns -gt 0) {$daysBtwnRuns = -$daysBtwnRuns} +$Today = Get-Date +$StartDate = ($Today).AddDays($dayBtwnRuns) + +ForEach ($vcenter in $vCenterServers){ + Connect-VIServer $vcenter -Credential $Cred -WarningAction SilentlyContinue -ErrorAction SilentlyContinue + $TargetVM = $null + $VIEvent = @() + $Today = Get-Date + $StartDate = ($Today).AddDays($dayBtwnRuns) + $VIEvent = Get-VIEventPlus -Start $StartDate -Finish $Today -EventType $vmCreationTypes + + $VIEvent|%{ + $NewNote = "" + $VM = $null + $VM = Get-View -Id $_.VM.Vm -Server $vcenter -Property Name,Config + + If ($VM){ + $NewNote = $VM.Config.GuestFullName+$newline + $NewNote += "Deployed: "+$_.CreatedTime.ToLocaltime().DateTime+$newline + $NewNote += "Deployed by "+$_.UserName+$newline + $NewNote += $_.FullFormattedMessage + Set-VM -VM $VM.Name -Notes $NewNote -Confirm:$false + } + } + +Disconnect-VIServer -Confirm:$false} From e63f4178ab1532ae0127859441232bee53b7a0a6 Mon Sep 17 00:00:00 2001 From: mycloudrevolution Date: Thu, 31 Aug 2017 14:58:04 +0200 Subject: [PATCH 058/112] Add New Edge Gateway Abbility to add a Edge Gateway --- Modules/VMware-vCD-Module/README.md | 23 +- .../VMware-vCD-Module/VMware-vCD-Module.psd1 | 7 +- .../examples/OnBoarding.json | 8 +- .../functions/Invoke-MyOnBoarding.psm1 | 363 +++++++++--------- .../functions/New-MyEdgeGateway.psm1 | 161 ++++++++ .../functions/New-MyOrgVdc.psm1 | 12 +- .../media/Invoke-MyOnBoarding.png | Bin 40988 -> 90987 bytes 7 files changed, 389 insertions(+), 185 deletions(-) create mode 100644 Modules/VMware-vCD-Module/functions/New-MyEdgeGateway.psm1 diff --git a/Modules/VMware-vCD-Module/README.md b/Modules/VMware-vCD-Module/README.md index a691421..3d7bc48 100644 --- a/Modules/VMware-vCD-Module/README.md +++ b/Modules/VMware-vCD-Module/README.md @@ -9,15 +9,32 @@ VMware-vCD-Module PowerShell Module Markus Kraus [@vMarkus_K](https://twitter.com/vMarkus_K) +MY CLOUD-(R)EVOLUTION [mycloudrevolution.com](http://mycloudrevolution.com/) + + ## Project WebSite: -[mycloudrevolution.com](http://mycloudrevolution.com/) +[PowerCLI vCloud Director Customer Provisioning](https://mycloudrevolution.com/2017/06/13/powercli-vcloud-director-customer-provisioning/) + +[PowerCLI – Create vCloud Director Edge Gateway](https://mycloudrevolution.com/2017/06/27/powercli-create-vcloud-director-edge-gateway/) + ## Project Documentation: -[Read the Docs](http://vmware-vcd-module.readthedocs.io/) +[Read the Docs - VMware-vCD-Module](http://vmware-vcd-module.readthedocs.io/) ## Project Description: -The 'VMware-vCD-Module' PowerShell Module is focused on the initial craation of VMware vCloud Director Objects like Org, Org User, Org VDC. +The 'VMware-vCD-Module' PowerShell Module is focused on the initial creation of VMware vCloud Director Objects like Org, Org User, Org VDC with External Networks or Edge Gateway. + +All Functions in this Module can be used as standalone Cmdlet but also the ``Invoke-My OnBoarding`` Functions to process a JSON File and create all Objects at once. + +### Fully tested Versions: + +Powershell: v4, v5 + +PowerCLI: 6.5.1 + +VMware vCloud Director: 8.10.1 + diff --git a/Modules/VMware-vCD-Module/VMware-vCD-Module.psd1 b/Modules/VMware-vCD-Module/VMware-vCD-Module.psd1 index 59a02de..27584d1 100644 --- a/Modules/VMware-vCD-Module/VMware-vCD-Module.psd1 +++ b/Modules/VMware-vCD-Module/VMware-vCD-Module.psd1 @@ -12,7 +12,7 @@ # RootModule = '' # Die Versionsnummer dieses Moduls -ModuleVersion = '0.2.0' +ModuleVersion = '1.0.0' # ID zur eindeutigen Kennzeichnung dieses Moduls GUID = '1ef8a2de-ca22-4c88-8cdb-e00f35007d2a' @@ -21,7 +21,7 @@ GUID = '1ef8a2de-ca22-4c88-8cdb-e00f35007d2a' Author = 'Markus' # Unternehmen oder Hersteller dieses Moduls -CompanyName = 'Unbekannt' +CompanyName = 'mycloudrevolution.com' # Urheberrechtserklärung für dieses Modul Copyright = '(c) 2017 Markus. Alle Rechte vorbehalten.' @@ -64,12 +64,13 @@ Copyright = '(c) 2017 Markus. Alle Rechte vorbehalten.' # Die Module, die als geschachtelte Module des in "RootModule/ModuleToProcess" angegebenen Moduls importiert werden sollen. NestedModules = @('functions\Invoke-MyOnBoarding.psm1', + 'functions\New-MyEdgeGateway.psm1', 'functions\New-MyOrg.psm1', 'functions\New-MyOrgAdmin.psm1', 'functions\New-MyOrgVdc.psm1') # Aus diesem Modul zu exportierende Funktionen -FunctionsToExport = 'Invoke-MyOnBoarding', 'New-MyOrg', 'New-MyOrgAdmin', 'New-MyOrgVdc' +FunctionsToExport = 'Invoke-MyOnBoarding', 'New-MyEdgeGateway', 'New-MyOrg', 'New-MyOrgAdmin', 'New-MyOrgVdc' # Aus diesem Modul zu exportierende Cmdlets CmdletsToExport = '*' diff --git a/Modules/VMware-vCD-Module/examples/OnBoarding.json b/Modules/VMware-vCD-Module/examples/OnBoarding.json index ecd1f88..9fe4217 100644 --- a/Modules/VMware-vCD-Module/examples/OnBoarding.json +++ b/Modules/VMware-vCD-Module/examples/OnBoarding.json @@ -19,6 +19,12 @@ "StorageProfile":"Standard-DC01", "ProviderVDC":"Provider-VDC-DC01", "NetworkPool":"Provider-VDC-DC01-NetPool", - "ExternalNetwork": "External-OrgVdcNet" + "ExternalNetwork": "External-OrgVdcNet", + "EdgeGateway": "Yes", + "IPAddress":"192.168.100.1", + "SubnetMask":"255.255.255.0", + "Gateway":"192.168.100.254", + "IPRangeStart":"192.168.100.2", + "IPRangeEnd":"192.168.100.3" } } \ No newline at end of file diff --git a/Modules/VMware-vCD-Module/functions/Invoke-MyOnBoarding.psm1 b/Modules/VMware-vCD-Module/functions/Invoke-MyOnBoarding.psm1 index 4b5e209..97e00ed 100644 --- a/Modules/VMware-vCD-Module/functions/Invoke-MyOnBoarding.psm1 +++ b/Modules/VMware-vCD-Module/functions/Invoke-MyOnBoarding.psm1 @@ -1,172 +1,191 @@ -#Requires -Version 4 -#Requires -Modules VMware.VimAutomation.Cloud, @{ModuleName="VMware.VimAutomation.Cloud";ModuleVersion="6.3.0.0"} -Function Invoke-MyOnBoarding { -<# -.SYNOPSIS - Creates all vCD Objecst for a new IAAS Customer - -.DESCRIPTION - Creates all vCD Objects for a new IAAS Customer - - All Objects are: - * Org - * Default Org Admin - * Org VDC - ** Private Catalog - ** Optional Bridged Network - - JSON Config Example: - - { - "Org": { - "Name":"TestOrg", - "FullName": "Test Org", - "Description":"Automation Test Org" - }, - "OrgAdmin": { - "Name":"TestOrgAdmin", - "Pasword": "myPassword1!", - "FullName":"Test OrgAdmin", - "EmailAddress":"test@admin.org" - }, - "OrgVdc": { - "Name":"TestOrgVdc", - "FixedSize": "M", - "CPULimit": "1000", - "MEMLimit":"1000", - "StorageLimit":"1000", - "StorageProfile":"Standard-DC01", - "ProviderVDC":"Provider-VDC-DC01", - "NetworkPool":"Provider-VDC-DC01-NetPool", - "ExternalNetwork": "External_OrgVdcNet" - } - } - -.NOTES - File Name : Invoke-MyOnBoarding.ps1 - Author : Markus Kraus - Version : 1.2 - State : Ready - -.LINK - https://mycloudrevolution.com/ - -.EXAMPLE - Invoke-MyOnBoarding -ConfigFile ".\OnBoarding.json" -Enabled:$true - -.EXAMPLE - Invoke-MyOnBoarding -ConfigFile ".\OnBoarding.json" -Enabled:$false - -.PARAMETER ConfigFile - Full Path to the JSON Config File - -.PARAMETER Enabled - Should the Customer be enabled after creation - - Default: $False - -#> - Param ( - [Parameter(Mandatory=$True, ValueFromPipeline=$False, HelpMessage="Full Path to the JSON Config File")] - [ValidateNotNullorEmpty()] - [String] $ConfigFile, - [Parameter(Mandatory=$False, ValueFromPipeline=$False, HelpMessage="Should the Customer be enabled after creation")] - [ValidateNotNullorEmpty()] - [Switch]$Enabled - ) - Process { - - $Valid = $true - - Write-Verbose "## Import JSON Config" - Write-Host "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") Importing JSON Config...`n" - $Configs = Get-Content -Raw -Path $ConfigFile -ErrorAction Continue | ConvertFrom-Json -ErrorAction Continue - - if (!($Configs)) { - $Valid = $false - Write-Host "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") Importing JSON Config Failed" -ForegroundColor Red - } - else { - Write-Host "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") Importing JSON Config OK" -ForegroundColor Green - } - - if ($Valid) { - try{ - Write-Verbose "## Create Org" - Write-Host "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") Creating new Org...`n" -ForegroundColor Yellow - $Trash = New-MyOrg -Name $Configs.Org.Name -FullName $Configs.Org.Fullname -Description $Configs.Org.Description -Enabled:$Enabled - Write-Host "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") Creating new Org OK" -ForegroundColor Green - Get-Org -Name $Configs.Org.Name | Select-Object Name, FullName, Enabled | Format-Table -AutoSize - } - catch { - $Valid = $false - Write-Host "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") Creating new Org Failed" -ForegroundColor Red - } - } - - if ($Valid) { - try{ - Write-Verbose "## Create OrgAdmin" - Write-Host "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") Creating new OrgAdmin...`n" -ForegroundColor Yellow - $Trash = New-MyOrgAdmin -Name $Configs.OrgAdmin.Name -Pasword $Configs.OrgAdmin.Pasword -FullName $Configs.OrgAdmin.FullName -EmailAddress $Configs.OrgAdmin.EmailAddress -Org $Configs.Org.Name -Enabled:$Enabled - Write-Host "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") Creating new OrgAdmin OK" -ForegroundColor Green - Get-CIUser -Org $Configs.Org.Name -Name $Configs.OrgAdmin.Name | Select-Object Name, FullName, Email | Format-Table -AutoSize - } - catch { - $Valid = $false - Write-Host "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") Creating new OrgAdmin Failed" -ForegroundColor Red - } - } - if ($Valid) { - try{ - Write-Verbose "## Create OrgVdc" - Write-Host "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") Creating new OrgVdc...`n" -ForegroundColor Yellow - - if ($Configs.OrgVdc.FixedSize){ - - Write-Host "Fixed Size (T-Shirt Size) '$($Configs.OrgVdc.FixedSize)' Org VDC Requested!" - - switch ($Configs.OrgVdc.FixedSize) { - M { - [String]$CPULimit = 36000 - [String]$MEMLimit = 122880 - [String]$StorageLimit = 1048576 - } - L { - [String]$CPULimit = 36000 - [String]$MEMLimit = 245760 - [String]$StorageLimit = 1048576 - } - default {throw "Invalid T-Shirt Size!"} - } - - } - else{ - Write-Host "Custom Org VDC Size Requested!" - - $CPULimit = $Configs.OrgVdc.CPULimit - $MEMLimit = $Configs.OrgVdc.MEMLimit - $StorageLimit = $Configs.OrgVdc.StorageLimit - - } - - if ($Configs.OrgVdc.ExternalNetwork){ - $Trash = New-MyOrgVdc -Name $Configs.OrgVdc.Name -CPULimit $CPULimit -MEMLimit $MEMLimit -StorageLimit $StorageLimit -Networkpool $Configs.OrgVdc.NetworkPool -StorageProfile $Configs.OrgVdc.StorageProfile -ProviderVDC $Configs.OrgVdc.ProviderVDC -ExternalNetwork $Configs.OrgVdc.ExternalNetwork -Org $Configs.Org.Name -Enabled:$Enabled - } - else { - $Trash = New-MyOrgVdc -Name $Configs.OrgVdc.Name -CPULimit $CPULimit -MEMLimit $MEMLimit -StorageLimit $StorageLimit -Networkpool $Configs.OrgVdc.NetworkPool -StorageProfile $Configs.OrgVdc.StorageProfile -ProviderVDC $Configs.OrgVdc.ProviderVDC -Org $Configs.Org.Name -Enabled:$Enabled - } - Write-Host "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") Creating new OrgVdc OK" -ForegroundColor Green - Get-OrgVdc -Org $Configs.Org.Name -Name $Configs.OrgVdc.Name | Select-Object Name, Enabled, CpuAllocationGhz, MemoryLimitGB, StorageLimitGB, AllocationModel, ThinProvisioned, UseFastProvisioning, ` - @{N="StorageProfile";E={$_.ExtensionData.VdcStorageProfiles.VdcStorageProfile.Name}}, ` - @{N='VCpuInMhz';E={$_.ExtensionData.VCpuInMhz}} | Format-Table -AutoSize - } - catch { - $Valid = $false - Write-Host "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") Creating new OrgVdc Failed" -ForegroundColor Red - } - } - - Write-Output "Overall Execution was Valid: $Valid" - } -} +#Requires -Version 4 +#Requires -Modules VMware.VimAutomation.Cloud, @{ModuleName="VMware.VimAutomation.Cloud";ModuleVersion="6.3.0.0"} +Function Invoke-MyOnBoarding { +<# +.SYNOPSIS + Creates all vCD Objecst for a new IAAS Customer + +.DESCRIPTION + Creates all vCD Objects for a new IAAS Customer + + All Objects are: + * Org + * Default Org Admin + * Org VDC + ** Private Catalog + ** Optional Bridged Network + + JSON Config Example: + + { + "Org": { + "Name":"TestOrg", + "FullName": "Test Org", + "Description":"Automation Test Org" + }, + "OrgAdmin": { + "Name":"TestOrgAdmin", + "Pasword": "myPassword1!", + "FullName":"Test OrgAdmin", + "EmailAddress":"test@admin.org" + }, + "OrgVdc": { + "Name":"TestOrgVdc", + "FixedSize": "M", + "CPULimit": "1000", + "MEMLimit":"1000", + "StorageLimit":"1000", + "StorageProfile":"Standard-DC01", + "ProviderVDC":"Provider-VDC-DC01", + "NetworkPool":"Provider-VDC-DC01-NetPool", + "ExternalNetwork": "External_OrgVdcNet", + "EdgeGateway": "Yes", + "IPAddress":"192.168.100.1", + "SubnetMask":"255.255.255.0", + "Gateway":"192.168.100.254", + "IPRangeStart":"192.168.100.2", + "IPRangeEnd":"192.168.100.3" + } + } + +.NOTES + File Name : Invoke-MyOnBoarding.ps1 + Author : Markus Kraus + Version : 1.3 + State : Ready + +.LINK + https://mycloudrevolution.com/ + +.EXAMPLE + Invoke-MyOnBoarding -ConfigFile ".\OnBoarding.json" -Enabled:$true + +.EXAMPLE + Invoke-MyOnBoarding -ConfigFile ".\OnBoarding.json" -Enabled:$false + +.PARAMETER ConfigFile + Full Path to the JSON Config File + +.PARAMETER Enabled + Should the Customer be enabled after creation + + Default: $False + +#> + Param ( + [Parameter(Mandatory=$True, ValueFromPipeline=$False, HelpMessage="Full Path to the JSON Config File")] + [ValidateNotNullorEmpty()] + [String] $ConfigFile, + [Parameter(Mandatory=$False, ValueFromPipeline=$False, HelpMessage="Should the Customer be enabled after creation")] + [ValidateNotNullorEmpty()] + [Switch]$Enabled + ) + Process { + + $Valid = $true + + Write-Verbose "## Import JSON Config" + Write-Host "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") Importing JSON Config...`n" + $Configs = Get-Content -Raw -Path $ConfigFile -ErrorAction Continue | ConvertFrom-Json -ErrorAction Continue + + if (!($Configs)) { + $Valid = $false + Write-Host "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") Importing JSON Config Failed" -ForegroundColor Red + } + else { + Write-Host "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") Importing JSON Config OK" -ForegroundColor Green + } + + if ($Valid) { + try{ + Write-Verbose "## Create Org" + Write-Host "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") Creating new Org...`n" -ForegroundColor Yellow + $Trash = New-MyOrg -Name $Configs.Org.Name -FullName $Configs.Org.Fullname -Description $Configs.Org.Description -Enabled:$Enabled + Write-Host "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") Creating new Org OK" -ForegroundColor Green + Get-Org -Name $Configs.Org.Name | Select-Object Name, FullName, Enabled | Format-Table -AutoSize + } + catch { + $Valid = $false + Write-Host "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") Creating new Org Failed" -ForegroundColor Red + } + } + + if ($Valid) { + try{ + Write-Verbose "## Create OrgAdmin" + Write-Host "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") Creating new OrgAdmin...`n" -ForegroundColor Yellow + $Trash = New-MyOrgAdmin -Name $Configs.OrgAdmin.Name -Pasword $Configs.OrgAdmin.Pasword -FullName $Configs.OrgAdmin.FullName -EmailAddress $Configs.OrgAdmin.EmailAddress -Org $Configs.Org.Name -Enabled:$Enabled + Write-Host "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") Creating new OrgAdmin OK" -ForegroundColor Green + Get-CIUser -Org $Configs.Org.Name -Name $Configs.OrgAdmin.Name | Select-Object Name, FullName, Email | Format-Table -AutoSize + } + catch { + $Valid = $false + Write-Host "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") Creating new OrgAdmin Failed" -ForegroundColor Red + } + } + if ($Valid) { + try{ + Write-Verbose "## Create OrgVdc" + Write-Host "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") Creating new OrgVdc...`n" -ForegroundColor Yellow + + if ($Configs.OrgVdc.FixedSize){ + + Write-Host "Fixed Size (T-Shirt Size) '$($Configs.OrgVdc.FixedSize)' Org VDC Requested!" + + switch ($Configs.OrgVdc.FixedSize) { + M { + [String]$CPULimit = 36000 + [String]$MEMLimit = 122880 + [String]$StorageLimit = 1048576 + } + L { + [String]$CPULimit = 36000 + [String]$MEMLimit = 245760 + [String]$StorageLimit = 1048576 + } + default {throw "Invalid T-Shirt Size!"} + } + + } + else{ + Write-Host "Custom Org VDC Size Requested!" + + $CPULimit = $Configs.OrgVdc.CPULimit + $MEMLimit = $Configs.OrgVdc.MEMLimit + $StorageLimit = $Configs.OrgVdc.StorageLimit + + } + + if ($Configs.OrgVdc.ExternalNetwork -and $Configs.OrgVdc.EdgeGateway -like "Yes"){ + Write-Host "Edge Gateway for Org VDC '$($Configs.OrgVdc.Name)' Requested!" + $Trash = New-MyOrgVdc -Name $Configs.OrgVdc.Name -CPULimit $CPULimit -MEMLimit $MEMLimit -StorageLimit $StorageLimit -Networkpool $Configs.OrgVdc.NetworkPool ` -StorageProfile $Configs.OrgVdc.StorageProfile -ProviderVDC $Configs.OrgVdc.ProviderVDC -Org $Configs.Org.Name -Enabled:$Enabled + + $EdgeName = $Configs.Org.Name + "-ESG01" + $Trash = New-MyEdgeGateway -Name $EdgeName -OrgVDCName $Configs.OrgVdc.Name -Orgname $Configs.Org.Name -ExternalNetwork $Configs.OrgVdc.ExternalNetwork ` -IPAddress $Configs.OrgVdc.IPAddress -SubnetMask $Configs.OrgVdc.SubnetMask -Gateway $Configs.OrgVdc.Gateway -IPRangeStart $Configs.OrgVdc.IPRangeStart -IPRangeEnd $Configs.OrgVdc.IPRangeEnd + } + elseif ($Configs.OrgVdc.ExternalNetwork -and $Configs.OrgVdc.EdgeGateway -like "No"){ + Write-Host "External Network for Org VDC '$($Configs.OrgVdc.Name)' Requested!" + $Trash = New-MyOrgVdc -Name $Configs.OrgVdc.Name -CPULimit $CPULimit -MEMLimit $MEMLimit -StorageLimit $StorageLimit -Networkpool $Configs.OrgVdc.NetworkPool ` -StorageProfile $Configs.OrgVdc.StorageProfile -ProviderVDC $Configs.OrgVdc.ProviderVDC -ExternalNetwork $Configs.OrgVdc.ExternalNetwork -Org $Configs.Org.Name -Enabled:$Enabled + } + else { + Write-Host "No external Connection for Org VDC '$($Configs.OrgVdc.Name)' Requested!" + $Trash = New-PecOrgVdc -Name $Configs.OrgVdc.Name -CPULimit $CPULimit -MEMLimit $MEMLimit -StorageLimit $StorageLimit -Networkpool $ProVdcNetworkPool.Name ` -StorageProfile $Configs.OrgVdc.StorageProfile -ProviderVDC $Configs.OrgVdc.ProviderVDC -Org $Configs.Org.Name -Enabled:$Enabled + } + Write-Host "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") Creating new OrgVdc OK" -ForegroundColor Green + Get-OrgVdc -Org $Configs.Org.Name -Name $Configs.OrgVdc.Name | Select-Object Name, Enabled, CpuAllocationGhz, MemoryLimitGB, StorageLimitGB, AllocationModel, ThinProvisioned, UseFastProvisioning, ` + @{N="StorageProfile";E={$_.ExtensionData.VdcStorageProfiles.VdcStorageProfile.Name}}, ` + @{N='VCpuInMhz';E={$_.ExtensionData.VCpuInMhz}} | Format-Table -AutoSize + + if ($Configs.OrgVdc.EdgeGateway -like "Yes"){ + Search-Cloud -QueryType EdgeGateway -Name $EdgeName | Select Name, IsBusy, GatewayStatus, HaStatus | ft -AutoSize + } + } + catch { + $Valid = $false + Write-Host "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") Creating new OrgVdc Failed" -ForegroundColor Red + } + } + + Write-Output "Overall Execution was Valid: $Valid" + } +} diff --git a/Modules/VMware-vCD-Module/functions/New-MyEdgeGateway.psm1 b/Modules/VMware-vCD-Module/functions/New-MyEdgeGateway.psm1 new file mode 100644 index 0000000..205c635 --- /dev/null +++ b/Modules/VMware-vCD-Module/functions/New-MyEdgeGateway.psm1 @@ -0,0 +1,161 @@ +#Requires -Version 4 +#Requires -Modules VMware.VimAutomation.Cloud, @{ModuleName="VMware.VimAutomation.Cloud";ModuleVersion="6.3.0.0"} +Function New-MyEdgeGateway { +<# +.SYNOPSIS + Creates a new Edge Gateway with Default Parameters + +.DESCRIPTION + Creates a new Edge Gateway with Default Parameters + + Default Parameters are: + * Size + * HA State + * DNS Relay + + +.NOTES + File Name : New-MyEdgeGateway.ps1 + Author : Markus Kraus + Version : 1.0 + State : Ready + +.LINK + https://mycloudrevolution.com/ + +.EXAMPLE + New-MyEdgeGateway -Name "TestEdge" -OrgVDCName "TestVDC" -OrgName "TestOrg" -ExternalNetwork "ExternalNetwork" -IPAddress "192.168.100.1" -SubnetMask "255.255.255.0" -Gateway "192.168.100.254" -IPRangeStart ""192.168.100.2" -IPRangeEnd ""192.168.100.3" -Verbose + +.PARAMETER Name + Name of the New Edge Gateway as String + +.PARAMETER OrgVDCName + OrgVDC where the new Edge Gateway should be created as string + +.PARAMETER OrgName + Org where the new Edge Gateway should be created as string + +.PARAMETER ExternalNetwork + External Network of the new Edge Gateway as String + +.PARAMETER IPAddress + IP Address of the New Edge Gateway as IP Address + +.PARAMETER SubnetMask + Subnet Mask of the New Edge Gateway as IP Address + +.PARAMETER Gateway + Gateway of the New Edge Gateway as IP Address + +.PARAMETER IPRangeStart + Sub Allocation IP Range Start of the New Edge Gateway as IP Address + +.PARAMETER IPRangeEnd + Sub Allocation IP Range End of the New Edge Gateway as IP Address + +.PARAMETER Timeout + Timeout for the Edge Gateway to get Ready + + Default: 120s + +#> + Param ( + [Parameter(Mandatory=$True, ValueFromPipeline=$False, HelpMessage="Name of the New Edge Gateway as String")] + [ValidateNotNullorEmpty()] + [String] $Name, + [Parameter(Mandatory=$True, ValueFromPipeline=$False, HelpMessage="OrgVDC where the new Edge Gateway should be created as string")] + [ValidateNotNullorEmpty()] + [String] $OrgVdcName, + [Parameter(Mandatory=$True, ValueFromPipeline=$False, HelpMessage="Org where the new Edge Gateway should be created as string")] + [ValidateNotNullorEmpty()] + [String] $OrgName, + [Parameter(Mandatory=$True, ValueFromPipeline=$False, HelpMessage="External Network of the New Edge Gateway as String")] + [ValidateNotNullorEmpty()] + [String] $ExternalNetwork, + [Parameter(Mandatory=$True, ValueFromPipeline=$False, HelpMessage="IP Address of the New Edge Gateway as IP Address")] + [ValidateNotNullorEmpty()] + [IPAddress] $IPAddress, + [Parameter(Mandatory=$True, ValueFromPipeline=$False, HelpMessage="Subnet Mask of the New Edge Gateway as IP Address")] + [ValidateNotNullorEmpty()] + [IPAddress] $SubnetMask, + [Parameter(Mandatory=$True, ValueFromPipeline=$False, HelpMessage="Gateway of the New Edge Gateway as IP Address")] + [ValidateNotNullorEmpty()] + [IPAddress] $Gateway, + [Parameter(Mandatory=$True, ValueFromPipeline=$False, HelpMessage="Sub Allocation IP Range Start the New Edge Gateway as IP Address")] + [ValidateNotNullorEmpty()] + [IPAddress] $IPRangeStart, + [Parameter(Mandatory=$True, ValueFromPipeline=$False, HelpMessage="Sub Allocation IP Range End the New Edge Gateway as IP Address")] + [ValidateNotNullorEmpty()] + [IPAddress] $IPRangeEnd, + [Parameter(Mandatory=$False, ValueFromPipeline=$False,HelpMessage="Timeout for the Edge Gateway to get Ready")] + [ValidateNotNullorEmpty()] + [int] $Timeout = 120 + ) + Process { + + ## Get Org vDC + Write-Verbose "Get Org vDC" + [Array] $orgVdc = Get-Org -Name $OrgName | Get-OrgVdc -Name $OrgVdcName + + if ( $orgVdc.Count -gt 1) { + throw "Multiple OrgVdcs found!" + } + elseif ( $orgVdc.Count -lt 1) { + throw "No OrgVdc found!" + } + ## Get External Network + Write-Verbose "Get External Network" + $extNetwork = Get-ExternalNetwork | Get-CIView -Verbose:$False | Where-Object {$_.name -eq $ExternalNetwork} + + ## Build EdgeGatway Configuration + Write-Verbose "Build EdgeGatway Configuration" + $EdgeGateway = New-Object VMware.VimAutomation.Cloud.Views.Gateway + $EdgeGateway.Name = $Name + $EdgeGateway.Configuration = New-Object VMware.VimAutomation.Cloud.Views.GatewayConfiguration + #$EdgeGateway.Configuration.BackwardCompatibilityMode = $false + $EdgeGateway.Configuration.GatewayBackingConfig = "compact" + $EdgeGateway.Configuration.UseDefaultRouteForDnsRelay = $false + $EdgeGateway.Configuration.HaEnabled = $false + + $EdgeGateway.Configuration.EdgeGatewayServiceConfiguration = New-Object VMware.VimAutomation.Cloud.Views.GatewayFeatures + $EdgeGateway.Configuration.GatewayInterfaces = New-Object VMware.VimAutomation.Cloud.Views.GatewayInterfaces + + $EdgeGateway.Configuration.GatewayInterfaces.GatewayInterface = New-Object VMware.VimAutomation.Cloud.Views.GatewayInterface + $EdgeGateway.Configuration.GatewayInterfaces.GatewayInterface[0].name = $extNetwork.Name + $EdgeGateway.Configuration.GatewayInterfaces.GatewayInterface[0].DisplayName = $extNetwork.Name + $EdgeGateway.Configuration.GatewayInterfaces.GatewayInterface[0].Network = $extNetwork.Href + $EdgeGateway.Configuration.GatewayInterfaces.GatewayInterface[0].InterfaceType = "uplink" + $EdgeGateway.Configuration.GatewayInterfaces.GatewayInterface[0].UseForDefaultRoute = $true + $EdgeGateway.Configuration.GatewayInterfaces.GatewayInterface[0].ApplyRateLimit = $false + + $ExNetexternalSubnet = New-Object VMware.VimAutomation.Cloud.Views.SubnetParticipation + $ExNetexternalSubnet.Gateway = $Gateway.IPAddressToString + $ExNetexternalSubnet.Netmask = $SubnetMask.IPAddressToString + $ExNetexternalSubnet.IpAddress = $IPAddress.IPAddressToString + $ExNetexternalSubnet.IpRanges = New-Object VMware.VimAutomation.Cloud.Views.IpRanges + $ExNetexternalSubnet.IpRanges.IpRange = New-Object VMware.VimAutomation.Cloud.Views.IpRange + $ExNetexternalSubnet.IpRanges.IpRange[0].StartAddress = $IPRangeStart.IPAddressToString + $ExNetexternalSubnet.IpRanges.IpRange[0].EndAddress = $IPRangeEnd.IPAddressToString + + $EdgeGateway.Configuration.GatewayInterfaces.GatewayInterface[0].SubnetParticipation = $ExNetexternalSubnet + + ## Create EdgeGatway + Write-Verbose "Create EdgeGatway" + $CreateEdgeGateway = $orgVdc.ExtensionData.CreateEdgeGateway($EdgeGateway) + + ## Wait for EdgeGatway to become Ready + Write-Verbose "Wait for EdgeGatway to become Ready" + while((Search-Cloud -QueryType EdgeGateway -Name $Name -Verbose:$False).IsBusy -eq $True){ + $i++ + Start-Sleep 5 + if($i -gt $Timeout) { Write-Error "Creating Edge Gateway."; break} + Write-Progress -Activity "Creating Edge Gateway" -Status "Wait for Edge to become Ready..." + } + Write-Progress -Activity "Creating Edge Gateway" -Completed + Start-Sleep 1 + + Search-Cloud -QueryType EdgeGateway -Name $Name | Select-Object Name, IsBusy, GatewayStatus, HaStatus | Format-Table -AutoSize + + + } +} diff --git a/Modules/VMware-vCD-Module/functions/New-MyOrgVdc.psm1 b/Modules/VMware-vCD-Module/functions/New-MyOrgVdc.psm1 index 783ad5e..26258e7 100644 --- a/Modules/VMware-vCD-Module/functions/New-MyOrgVdc.psm1 +++ b/Modules/VMware-vCD-Module/functions/New-MyOrgVdc.psm1 @@ -67,7 +67,7 @@ Function New-MyOrgVdc { Org where the new Org VDC should be created as string .PARAMETER Timeout - Timeout for teh Org VDC to get Ready + Timeout for the Org VDC to get Ready Default: 120s @@ -103,7 +103,7 @@ Function New-MyOrgVdc { [Parameter(Mandatory=$True, ValueFromPipeline=$False, HelpMessage="Org where the new Org VDC should be created as string")] [ValidateNotNullorEmpty()] [String] $Org, - [Parameter(Mandatory=$False, ValueFromPipeline=$False,HelpMessage="Timeout for teh Org VDC to get Ready")] + [Parameter(Mandatory=$False, ValueFromPipeline=$False,HelpMessage="Timeout for the Org VDC to get Ready")] [ValidateNotNullorEmpty()] [int] $Timeout = 120 ) @@ -145,7 +145,7 @@ Function New-MyOrgVdc { ## Wait for getting Ready Write-Verbose "Wait for getting Ready" $i = 0 - while(($orgVdc = Get-OrgVdc -Name $Name).Status -eq "NotReady"){ + while(($orgVdc = Get-OrgVdc -Name $Name -Verbose:$false).Status -eq "NotReady"){ $i++ Start-Sleep 2 if($i -gt $Timeout) { Write-Error "Creating Org Failed."; break} @@ -175,7 +175,7 @@ Function New-MyOrgVdc { ## Wait for getting Ready Write-Verbose "Wait for getting Ready" - while(($orgVdc = Get-OrgVdc -Name $name).Status -eq "NotReady"){ + while(($orgVdc = Get-OrgVdc -Name $name -Verbose:$false).Status -eq "NotReady"){ $i++ Start-Sleep 1 if($i -gt $Timeout) { Write-Error "Update Org Failed."; break} @@ -201,7 +201,7 @@ Function New-MyOrgVdc { ## Wait for getting Ready Write-Verbose "Wait for getting Ready" - while(($orgVdc = Get-OrgVdc -Name $name).Status -eq "NotReady"){ + while(($orgVdc = Get-OrgVdc -Name $name -Verbose:$false).Status -eq "NotReady"){ $i++ Start-Sleep 1 if($i -gt $Timeout) { Write-Error "Update Org Failed."; break} @@ -236,7 +236,7 @@ Function New-MyOrgVdc { $EnableOrgVdc = Set-OrgVdc -OrgVdc $Name -Enabled:$True $orgVdcView = Get-OrgVdc $Name | Get-CIView $extNetwork = $_.externalnetwork - $extNetwork = Get-ExternalNetwork | Get-CIView | Where-Object {$_.name -eq $ExternalNetwork} + $extNetwork = Get-ExternalNetwork | Get-CIView -Verbose:$false | Where-Object {$_.name -eq $ExternalNetwork} $orgNetwork = new-object vmware.vimautomation.cloud.views.orgvdcnetwork $orgNetwork.name = $ExternalNetwork $orgNetwork.Configuration = New-Object VMware.VimAutomation.Cloud.Views.NetworkConfiguration diff --git a/Modules/VMware-vCD-Module/media/Invoke-MyOnBoarding.png b/Modules/VMware-vCD-Module/media/Invoke-MyOnBoarding.png index 9f60a93508c54e47418fa493ab0b730f8b003a3f..c09f0fe830d30617944f0edefc64716c0ec8ca60 100644 GIT binary patch literal 90987 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 HVETUmkdju5 literal 40988 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 %IBM`Y0F&%$%@Mz>h~Xp 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 Y0@TidQEK`NO#t%qyOtiTT*EfvACt(Yp#T5? From fc90f81d88d29d2d04b5c10fef3438d5342aa50f Mon Sep 17 00:00:00 2001 From: haberstrohr Date: Thu, 31 Aug 2017 10:02:54 -0500 Subject: [PATCH 059/112] Get-VMHostWWPN Returns WWPN in Base16 format, similar to what is displayed in the GUI. --- Scripts/Get-VMHostWWPN.ps1 | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 Scripts/Get-VMHostWWPN.ps1 diff --git a/Scripts/Get-VMHostWWPN.ps1 b/Scripts/Get-VMHostWWPN.ps1 new file mode 100644 index 0000000..921b5bb --- /dev/null +++ b/Scripts/Get-VMHostWWPN.ps1 @@ -0,0 +1,17 @@ +function Get-VMHostWWPN { +<# +Script name: Get-VMHostWWPN.ps1 +Created on: 08/31/2017 +Author: Robin Haberstroh, @strohland +Description: This script returns the WWPN of the hosts FiberChannel HBA in a readable format that corresponds to what storage team expects +Dependencies: None known +#> + param( + [string]$cluster + ) + + Get-Cluster $cluster | get-vmhost | get-vmhosthba -type FibreChannel | + format-table VMHost, Device, @{ + n='WorldWidePortName';e={[convert]::ToString($_.PortWorldWideName, 16)} + } +} \ No newline at end of file From 1ed68eeab2f4d2022e3caf37e971792037d448f3 Mon Sep 17 00:00:00 2001 From: mycloudrevolution Date: Fri, 1 Sep 2017 15:45:18 +0200 Subject: [PATCH 060/112] New Module VMware-vCD-TenantReport This Module Creates a nice HTML Report for vCD Customers. Report Contains Objects like Users, Catalogs, VDCs, vApps, VMs, Edge Gateways --- Modules/VMware-vCD-TenantReport/README.md | 30 +++ .../VMware-vCD-TenantReport.psd1 | 122 +++++++++ .../functions/Get-VcdTenantReport.psm1 | 251 ++++++++++++++++++ .../media/Get-VcdTenantReport.png | Bin 0 -> 6197 bytes .../PowerStartHTML/PowerStartHTML.psd1 | Bin 0 -> 7874 bytes .../PowerStartHTML/PowerStartHTML.psm1 | 237 +++++++++++++++++ .../tests/VMware-vCD-TenantReport.Tests.ps1 | 24 ++ 7 files changed, 664 insertions(+) create mode 100644 Modules/VMware-vCD-TenantReport/README.md create mode 100644 Modules/VMware-vCD-TenantReport/VMware-vCD-TenantReport.psd1 create mode 100644 Modules/VMware-vCD-TenantReport/functions/Get-VcdTenantReport.psm1 create mode 100644 Modules/VMware-vCD-TenantReport/media/Get-VcdTenantReport.png create mode 100644 Modules/VMware-vCD-TenantReport/modules/PowerStartHTML/PowerStartHTML.psd1 create mode 100644 Modules/VMware-vCD-TenantReport/modules/PowerStartHTML/PowerStartHTML.psm1 create mode 100644 Modules/VMware-vCD-TenantReport/tests/VMware-vCD-TenantReport.Tests.ps1 diff --git a/Modules/VMware-vCD-TenantReport/README.md b/Modules/VMware-vCD-TenantReport/README.md new file mode 100644 index 0000000..ac9b9f8 --- /dev/null +++ b/Modules/VMware-vCD-TenantReport/README.md @@ -0,0 +1,30 @@ +VMware-vCD-TenantReport PowerShell Module +============= + +# About + +## Project Owner: + +Markus Kraus [@vMarkus_K](https://twitter.com/vMarkus_K) + +MY CLOUD-(R)EVOLUTION [mycloudrevolution.com](http://mycloudrevolution.com/) + +## Project WebSite: + +[mycloudrevolution.com](http://mycloudrevolution.com/) + +## Project Documentation: + +[Read the Docs](http://readthedocs.io/) + +## Project Description: + +The 'VMware-vCD-TenantReport' PowerShell Module creates with the Fuction 'Get-VcdTenantReport' a HTML Report of your vCloud Director Objects. + +![Get-VcdTenantReport](/media/Get-VcdTenantReport.png) + +Big thanks to [Timothy Dewin](https://twitter.com/tdewin) for his great [PowerStartHTML](https://github.com/tdewin/randomsamples/tree/master/powerstarthtml) PowerShell Module which is used to generate the Report for this Module. + + + + diff --git a/Modules/VMware-vCD-TenantReport/VMware-vCD-TenantReport.psd1 b/Modules/VMware-vCD-TenantReport/VMware-vCD-TenantReport.psd1 new file mode 100644 index 0000000..32afbb5 --- /dev/null +++ b/Modules/VMware-vCD-TenantReport/VMware-vCD-TenantReport.psd1 @@ -0,0 +1,122 @@ +# +# Modulmanifest für das Modul "VMware-vCD-TenantReport" +# +# Generiert von: Markus Kraus +# +# Generiert am: 8/22/2017 +# + +@{ + +# Die diesem Manifest zugeordnete Skript- oder Binärmoduldatei. +# RootModule = 'VMware-vCD-TenantReport.psm1' + +# Die Versionsnummer dieses Moduls +ModuleVersion = '1.0.2' + +# ID zur eindeutigen Kennzeichnung dieses Moduls +GUID = '21a71eaa-d259-48c5-8482-643ba152af76' + +# Autor dieses Moduls +Author = 'Markus' + +# Unternehmen oder Hersteller dieses Moduls +CompanyName = 'mycloudrevolution.com' + +# Urheberrechtserklärung für dieses Modul +Copyright = '(c) 2017 Markus Kraus. Alle Rechte vorbehalten.' + +# Beschreibung der von diesem Modul bereitgestellten Funktionen +# Description = '' + +# Die für dieses Modul mindestens erforderliche Version des Windows PowerShell-Moduls +# PowerShellVersion = '' + +# Der Name des für dieses Modul erforderlichen Windows PowerShell-Hosts +# PowerShellHostName = '' + +# Die für dieses Modul mindestens erforderliche Version des Windows PowerShell-Hosts +# PowerShellHostVersion = '' + +# Die für dieses Modul mindestens erforderliche Microsoft .NET Framework-Version +# DotNetFrameworkVersion = '' + +# Die für dieses Modul mindestens erforderliche Version der CLR (Common Language Runtime) +# CLRVersion = '' + +# Die für dieses Modul erforderliche Prozessorarchitektur ("Keine", "X86", "Amd64"). +# ProcessorArchitecture = '' + +# Die Module, die vor dem Importieren dieses Moduls in die globale Umgebung geladen werden müssen +# RequiredModules = @() + +# Die Assemblys, die vor dem Importieren dieses Moduls geladen werden müssen +# RequiredAssemblies = @() + +# Die Skriptdateien (PS1-Dateien), die vor dem Importieren dieses Moduls in der Umgebung des Aufrufers ausgeführt werden. +# ScriptsToProcess = @() + +# Die Typdateien (.ps1xml), die beim Importieren dieses Moduls geladen werden sollen +# TypesToProcess = @() + +# Die Formatdateien (.ps1xml), die beim Importieren dieses Moduls geladen werden sollen +# FormatsToProcess = @() + +# Die Module, die als geschachtelte Module des in "RootModule/ModuleToProcess" angegebenen Moduls importiert werden sollen. +NestedModules = @( "modules/PowerStartHTML/PowerStartHTML.psd1", + "functions/Get-VcdTenantReport.psm1" ) + +# Aus diesem Modul zu exportierende Funktionen +FunctionsToExport = '*' + +# Aus diesem Modul zu exportierende Cmdlets +CmdletsToExport = '*' + +# Die aus diesem Modul zu exportierenden Variablen +VariablesToExport = '*' + +# Aus diesem Modul zu exportierende Aliase +AliasesToExport = '*' + +# Aus diesem Modul zu exportierende DSC-Ressourcen +# DscResourcesToExport = @() + +# Liste aller Module in diesem Modulpaket +# ModuleList = @() + +# Liste aller Dateien in diesem Modulpaket +# FileList = @() + +# Die privaten Daten, die an das in "RootModule/ModuleToProcess" angegebene Modul übergeben werden sollen. Diese können auch eine PSData-Hashtabelle mit zusätzlichen von PowerShell verwendeten Modulmetadaten enthalten. +PrivateData = @{ + + PSData = @{ + + # 'Tags' wurde auf das Modul angewendet und unterstützt die Modulermittlung in Onlinekatalogen. + # Tags = @() + + # Eine URL zur Lizenz für dieses Modul. + # LicenseUri = '' + + # Eine URL zur Hauptwebsite für dieses Projekt. + # ProjectUri = '' + + # Eine URL zu einem Symbol, das das Modul darstellt. + # IconUri = '' + + # 'ReleaseNotes' des Moduls + # ReleaseNotes = '' + + } # Ende der PSData-Hashtabelle + +} # Ende der PrivateData-Hashtabelle + +# HelpInfo-URI dieses Moduls +# HelpInfoURI = '' + +# Standardpräfix für Befehle, die aus diesem Modul exportiert werden. Das Standardpräfix kann mit "Import-Module -Prefix" überschrieben werden. +# DefaultCommandPrefix = '' + +} + + diff --git a/Modules/VMware-vCD-TenantReport/functions/Get-VcdTenantReport.psm1 b/Modules/VMware-vCD-TenantReport/functions/Get-VcdTenantReport.psm1 new file mode 100644 index 0000000..ee1296e --- /dev/null +++ b/Modules/VMware-vCD-TenantReport/functions/Get-VcdTenantReport.psm1 @@ -0,0 +1,251 @@ +function Get-VcdTenantReport { +<# + .NOTES + =========================================================================== + Created by: Markus Kraus + Twitter: @VMarkus_K + Private Blog: mycloudrevolution.com + =========================================================================== + Changelog: + 1.0.0 - Inital Release + 1.0.1 - Removed "Test-IP" Module + 1.0.2 - More Detailed Console Log + =========================================================================== + External Code Sources: + Examle Usage of BOOTSTRAP with PowerShell + https://github.com/tdewin/randomsamples/tree/master/powershell-veeamallstat + BOOTSTRAP with PowerShell + https://github.com/tdewin/randomsamples/tree/master/powerstarthtml + =========================================================================== + Tested Against Environment: + vCD Version: 8.20 + PowerCLI Version: PowerCLI 6.5.1 + PowerShell Version: 5.0 + OS Version: Windows 8.1 + Keyword: VMware, vCD, Report, HTML + =========================================================================== + + .DESCRIPTION + This Function creates a HTML Report for your vCloud Director Organization. + + This Function is fully tested as Organization Administrator. + With lower permissions a unexpected behavior is possible. + + .Example + Get-VcdTenantReport -Server $ServerFQDN -Org $OrgName -Credential $MyCedential + + .Example + Get-VcdTenantReport -Server $ServerFQDN -Org $OrgName -Path "C:\Temp\Report.html" + + .PARAMETER Server + The FQDN of your vCloud Director Endpoint. + + .PARAMETER Org + The Organization Name. + + .PARAMETER Credential + PowerShell Credentials to access the Eénvironment. + + .PARAMETER Path + The Path of the exported HTML Report. + +#> +#Requires -Version 5 +#Requires -Modules VMware.VimAutomation.Cloud, @{ModuleName="VMware.VimAutomation.Cloud";ModuleVersion="6.5.1.0"} + +[CmdletBinding()] +param( + [Parameter(Mandatory=$True, ValueFromPipeline=$False)] + [ValidateNotNullorEmpty()] + [String] $Server, + [Parameter(Mandatory=$True, ValueFromPipeline=$False)] + [ValidateNotNullorEmpty()] + [String] $Org, + [Parameter(Mandatory=$False, ValueFromPipeline=$False)] + [ValidateNotNullorEmpty()] + [PSCredential] $Credential, + [Parameter(Mandatory=$false, ValueFromPipeline=$False)] + [ValidateNotNullorEmpty()] + [String] $Path = ".\Report.html" + +) + +Process { + + # Start Connection to vCD + + if ($global:DefaultCIServers) { + "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") - Disconnect existing vCD Server ..." + $Trash = Disconnect-CIServer -Server * -Force:$true -Confirm:$false -ErrorAction SilentlyContinue + } + + "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") - Connect vCD Server ..." + if ($Credential) { + $Trash = Connect-CIServer -Server $Server -Org $Org -Credential $Credential -ErrorAction Stop + } + else { + $Trash = Connect-CIServer -Server $Server -Org $Org -ErrorAction Stop + } + "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") - Create HTML Report..." + + # Init HTML Report + $ps = New-PowerStartHTML -title "vCD Tenant Report" + + #Set CSS Style + $ps.cssStyles['.bgtitle'] = "background-color:grey" + $ps.cssStyles['.bgsubsection'] = "background-color:#eee;" + + # Processing Data + ## Get Main Objects + [Array] $OrgVdcs = Get-OrgVdc + [Array] $Catalogs = Get-Catalog + [Array] $Users = Get-CIUser + + ## Add Header to Report + $ps.Main().Add("div","jumbotron").N() + $ps.Append("h1","display-3",("vCD Tenant Report" -f $OrgVdcs.Count)).Append("p","lead","Organization User Count: {0}" -f $Users.Count).Append("p","lead","Organization Catalog Count: {0}" -f $Catalogs.Count).Append("p","lead","Organization VDC Count: {0}" -f $OrgVdcs.Count).Append("hr","my-4").Append("p","font-italic","This Report lists the most important objects in your vCD Environmet. For more details contact your Service Provider").N() + + ## add Org Users to Report + $ps.Main().Append("h2",$null,"Org Users").N() + + $ps.Add('table','table').Add("tr","bgtitle text-white").Append("th",$null,"User Name").Append("th",$null,"Locked").Append("th",$null,"DeployedVMCount").Append("th",$null,"StoredVMCount").N() + $ps.Add("tr").N() + + foreach ($User in $Users) { + $ps.Append("td",$null,$User.Name).N() + $ps.Append("td",$null,$User.Locked).N() + $ps.Append("td",$null,$User.DeployedVMCount).N() + $ps.Append("td",$null,$User.StoredVMCount).N() + $ps.Up().N() + + } + $ps.Up().N() + + ## add Org Catalogs to Report + $ps.Main().Append("h2",$null,"Org Catalogs").N() + + foreach ($Catalog in $Catalogs) { + $ps.Add('table','table').Add("tr","bgtitle text-white").Append("th",$null,"Catalog Name").N() + $ps.Add("tr").N() + $ps.Append("td",$null,$Catalog.Name).Up().N() + + $ps.Add("td","bgsubsection").N() + $ps.Add("table","table bgcolorsub").N() + $ps.Add("tr").N() + + $headers = @("Item") + foreach ($h in $headers) { + $ps.Append("th",$null,$h).N() + } + $ps.Up().N() + + ### add Itens of the Catalog to the Report + [Array] $Items = $Catalog.ExtensionData.CatalogItems.CatalogItem + + foreach ($Item in $Items) { + $ps.Add("tr").N() + $ps.Append("td",$null,$Item.Name).N() + + $ps.Up().N() + + } + + $ps.Up().Up().N() + } + $ps.Up().N() + + ## add Org VDC`s to the Report + $ps.Main().Append("h2",$null,"Org VDCs").N() + + foreach ($OrgVdc in $OrgVdcs) { + $ps.Main().Add('table','table table-striped table-inverse').Add("tr").Append("th",$null,"VDC Name").Append("th",$null,"Enabled").Append("th",$null,"CpuUsedGHz").Append("th",$null,"MemoryUsedGB").Append("th",$null,"StorageUsedGB").Up().N() + $ps.Add("tr").N() + $ps.Append("td",$null,$OrgVdc.Name).Append("td",$null,$OrgVdc.Enabled).Append("td",$null,$OrgVdc.CpuUsedGHz).Append("td",$null,$OrgVdc.MemoryUsedGB).Append("td",$null,[Math]::Round($OrgVdc.StorageUsedGB,2)).Up().N() + + ### add Edge Gateways of this Org VDC to Report + $ps.Main().Append("h3",$null,"Org VDC Edge Gateways").N() + [Array] $Edges = Search-Cloud -QueryType EdgeGateway -Filter "Vdc==$($OrgVdc.Id)" + + foreach ($Edge in $Edges) { + $ps.Add('table','table').Add("tr","bgtitle text-white").Append("th",$null,"Edge Name").N() + $ps.Add("tr").N() + $ps.Append("td",$null,$Edge.Name).Up().N() + + $ps.Add("td","bgsubsection").N() + $ps.Add("table","table bgcolorsub").N() + $ps.Append("tr").Append("td","font-weight-bold","HaStatus").Append("td",$null,($Edge.HaStatus)).N() + $ps.Append("td","font-weight-bold","AdvancedNetworkingEnabled").Append("td",$null,$Edge.AdvancedNetworkingEnabled).N() + $ps.Append("tr").Append("td","font-weight-bold","NumberOfExtNetworks").Append("td",$null,($Edge.NumberOfExtNetworks)).N() + $ps.Append("td","font-weight-bold","NumberOfOrgNetworks").Append("td",$null,$Edge.NumberOfOrgNetworks).N() + + $ps.Up().Up().N() + } + $ps.Up().N() + + ### add Org Networks of this Org VDC to Report + $ps.Main().Append("h3",$null,"Org VDC Networks").N() + [Array] $Networks = $OrgVdc | Get-OrgVdcNetwork + + foreach ($Network in $Networks) { + $ps.Add('table','table').Add("tr","bgtitle text-white").Append("th",$null,"Network Name").N() + $ps.Add("tr").N() + $ps.Append("td",$null,$Network.Name).Up().N() + + $ps.Add("td","bgsubsection").N() + $ps.Add("table","table bgcolorsub").N() + $ps.Append("tr").Append("td","font-weight-bold","DefaultGateway").Append("td",$null,($Network.DefaultGateway)).N() + $ps.Append("td","font-weight-bold","Netmask").Append("td",$null,$Network.Netmask).N() + $ps.Append("tr").Append("td","font-weight-bold","NetworkType").Append("td",$null,($Network.NetworkType)).N() + $ps.Append("td","font-weight-bold","StaticIPPool").Append("td",$null,$Network.StaticIPPool).N() + + $ps.Up().Up().N() + } + $ps.Up().N() + + ### add vApps of this Org VDC to Report + $ps.Main().Append("h3",$null,"Org VDC vApps").N() + + [Array] $Vapps = $OrgVdc | Get-CIVApp + + foreach ($Vapp in $Vapps) { + $ps.Add('table','table').Add("tr","bgtitle text-white").Append("th",$null,"vApp Name").Append("th",$null,"Owner").Up().N() + $ps.Add("tr").N() + $ps.Append("td",$null,$Vapp.Name).Append("td",$null,$Vapp.Owner).Up().N() + + #### add VMs of this vApp to Report + $ps.Add("td","bgsubsection").N() + $ps.Add("table","table bgcolorsub").N() + $ps.Add("tr").N() + + $headers = @("Name","Status","GuestOSFullName","CpuCount","MemoryGB") + foreach ($h in $headers) { + $ps.Append("th",$null,$h).N() + } + $ps.Up().N() + + [Array] $VMs = $Vapp | Get-CIVM + + foreach ($VM in $VMs) { + $ps.Add("tr").N() + $ps.Append("td",$null,$VM.Name).N() + $ps.Append("td",$null,$VM.Status).N() + $ps.Append("td",$null,$VM.GuestOSFullName).N() + $ps.Append("td",$null,$VM.CpuCount).N() + $ps.Append("td",$null,$VM.MemoryGB).N() + + $ps.Up().N() + + } + $ps.Up().Up().N() + + } + $ps.Up().N() + + } + $ps.save($Path) + + "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") - Open HTML Report..." + Start-Process $Path + +} +} diff --git a/Modules/VMware-vCD-TenantReport/media/Get-VcdTenantReport.png b/Modules/VMware-vCD-TenantReport/media/Get-VcdTenantReport.png new file mode 100644 index 0000000000000000000000000000000000000000..c42dacfdb845605ada5fa72b3bb898333356c5ed GIT binary patch literal 6197 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

" + [xml]$xmlDocument = $null + $onLoadJS = $null + $cssStyles= @{} + $lastEl = $null + $newEl = $null + $indentedOutput = $false + $bootstrapAtCompile = $false + PowerStartHTML([string]$title) { + $this.xmlDocument = $this.PowerStartHtmlTemplate + $this.xmlDocument.html.head.title = $title + $this.lastEl = $this.xmlDocument.html.body.ChildNodes[0] + $this.onLoadJS = New-Object System.Collections.Generic.List[System.String] + } + [string] GetHtml() { + $xmlclone = $this.xmlDocument.Clone() + $csb = [System.Text.StringBuilder]::new() + foreach ($cssStyle in $this.cssStyles.GetEnumerator()) { + $null = $csb.AppendFormat("{0} {{ {1} }}",$cssStyle.Name,$cssStyle.Value) + } + $this.xmlDocument.html.head.style = $csb.toString() + $this.AddBootStrapAtCompile() + if($this.onLoadJS.Count -gt 0) { + $this.onLoadJs.Insert(0,"`r`n`$(document).ready(function() {") + $this.onLoadJs.Add("})`r`n") + $el = $this.xmlDocument.CreateElement("script") + $el.AppendChild($this.xmlDocument.CreateTextNode([System.String]::Join("`r`n",$this.onLoadJs))) + $this.xmlDocument.html.body.AppendChild($el) + } + $ms = [System.IO.MemoryStream]::new() + $xmlWriter = [System.Xml.XmlTextWriter]::new($ms,[System.Text.Encoding]::UTF8) + if($this.indentedOutput) { + $xmlWriter.Formatting = [System.Xml.Formatting]::Indented + } + $this.xmlDocument.WriteContentTo($xmlWriter) + $xmlWriter.Flush() + $ms.Flush() + #make sure that everytime we do gethtml we keep it clean + $this.xmlDocument = $xmlclone + $ms.Position = 0; + $sr = [System.IO.StreamReader]::new($ms); + return ("{0}`r`n" -f $sr.ReadToEnd()) + } + Save($path) { + $this.GetHtml() | Set-Content -path $path -Encoding UTF8 + } + + AddAttr($el,$name,$value) { + $attr = $this.xmlDocument.CreateAttribute($name) + $attr.Value = $value + $el.Attributes.Append($attr) + } + + AddAttrs($el,$dict) { + foreach($a in $dict.GetEnumerator()) { + $this.AddAttr($el,$a.Name,$a.Value) + } + } + [PowerStartHTML] AddBootStrap() { + $this.bootstrapAtCompile = $true + return $this + } + AddJSScript($href,$integrity) { + $el = $this.xmlDocument.CreateElement("script") + $attrs = @{ + "src"="$href"; + "integrity"="$integrity"; + "crossorigin"="anonymous" + } + $this.AddAttrs($el,$attrs) + $el.AppendChild($this.xmlDocument.CreateTextNode("")) + $this.xmlDocument.html.body.AppendChild($el) + } + AddBootStrapAtCompile() { #Bootstrap script needs to be added at the end + if($this.bootstrapAtCompile) { + $el = $this.xmlDocument.CreateElement("link") + $attrs = @{ + "rel"="stylesheet"; + "href"='https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css'; + "integrity"="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M"; + "crossorigin"="anonymous" + } + $this.AddAttrs($el,$attrs) + $el.AppendChild($this.xmlDocument.CreateTextNode("")) + $this.xmlDocument.html.head.AppendChild($el) + $this.AddJSScript('https://code.jquery.com/jquery-3.2.1.slim.min.js',"sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN") + $this.AddJSScript('https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.11.0/umd/popper.min.js',"sha384-b/U6ypiBEHpOf/4+1nzFpr53nxSS+GLCkfwBdFNTxtclqqenISfwAzpKaMNFNmj4") + $this.AddJSScript('https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/js/bootstrap.min.js',"sha384-h0AbiXch4ZDo7tp9hKZ4TsHbi047NrKGLO3SEJAg45jXxnGIfYzk4Si90RDIqNm1") + } + } + [PowerStartHTML] AddContainerAttrToMain() { + $this.AddAttr($this.xmlDocument.html.body.ChildNodes[0],"class","container") + return $this + } + [PowerStartHTML] Append($elType = "table",$className=$null,[string]$text=$null) { + $el = $this.xmlDocument.CreateElement($elType) + if($text -ne $null) { + $el.AppendChild($this.xmlDocument.CreateTextNode($text)) + } + if($className -ne $null) { + $this.AddAttr($el,"class",$className) + } + $this.lastEl.AppendChild($el) + $this.newEl = $el + + return $this + } + [PowerStartHTML] Append($elType = "table",$className=$null) { return $this.Append($elType,$className,$null) } + [PowerStartHTML] Append($elType = "table") { return $this.Append($elType,$null,$null) } + [PowerStartHTML] Add($elType = "table",$className=$null,[string]$text=$null) { + $this.Append($elType,$className,$text) + $this.lastEl = $this.newEl + return $this + } + [PowerStartHTML] Add($elType = "table",$className=$null) { return $this.Add($elType,$className,$null) } + [PowerStartHTML] Add($elType = "table") { return $this.Add($elType,$null,$null) } + [PowerStartHTML] Main() { + $this.lastEl = $this.xmlDocument.html.body.ChildNodes[0]; + return $this + } + [PowerStartHTML] Up() { + $this.lastEl = $this.lastEl.ParentNode; + return $this + } + N() {} +} +class PowerStartHTMLPassThroughLine { + $object;$cells + PowerStartHTMLPassThroughLine($object) { + $this.object = $object; + $this.cells = new-object System.Collections.HashTable; + } +} +class PowerStartHTMLPassThroughElement { + $name;$text;$element;$id + PowerStartHTMLPassThroughElement($name,$text,$element,$id) { + $this.name = $name; $this.text = $text; $this.element = $element;$this.id = $id + } +} +function New-PowerStartHTML { + param( + [Parameter(Mandatory=$true)][string]$title, + [switch]$nobootstrap=$false + ) + $pshtml = (new-object PowerStartHTML($title)) + if(-not $nobootstrap) { + $pshtml.AddBootStrap().AddContainerAttrToMain().N() + } + return $pshtml +} +function Add-PowerStartHTMLTable { + param( + [Parameter(Mandatory=$True,ValueFromPipeline=$True)]$object, + [PowerStartHTML]$psHtml, + [string]$tableTitle = $null, + [string]$tableClass = $null, + [string]$idOverride = $(if($tableTitle -ne $null) {($tableTitle.toLower() -replace "[^a-z0-9]","-") }), + [switch]$passthroughTable = $false, + [switch]$noheaders = $false + ) + begin { + if($tableTitle -ne $null) { + $psHtml.Main().Append("h1",$null,$tableTitle).N() + if($idOverride -ne $null) { + $psHtml.AddAttr($psHtml.newEl,"id","header-$idOverride") + } + } + $psHtml.Main().Add("table").N() + [int]$r = 0 + [int]$c = 0 + if($idOverride -ne $null) { + $psHtml.AddAttr($psHtml.newEl,"id","table-$idOverride") + } + if($tableClass -ne $null) { + $psHtml.AddAttr($psHtml.newEl,"class",$tableClass) + } + [bool]$isFirst = $true + } + process { + $c = 0 + + $props = $object | Get-Member -Type Properties + if(-not $noheaders -and $isFirst) { + $psHtml.Add("tr").N() + if($idOverride -ne $null) { + $psHtml.AddAttr($psHtml.newEl,"id","table-$idOverride-trh") + } + $props | % { + $n = $_.Name; + $psHtml.Append("th",$null,$n).N() + if($idOverride -ne $null) { + $cellid = "table-$idOverride-td-$r-$c" + $psHtml.AddAttr($psHtml.newEl,"id",$cellid) + } + $c++ + } + $c = 0 + $psHtml.Up().N() + } + + $psHtml.Add("tr").N() + if($idOverride -ne $null) { + $psHtml.AddAttr($psHtml.newEl,"id","table-$idOverride-tr-$r") + } + $pstableln = [PowerStartHTMLPassThroughLine]::new($object) + + $props | % { + $n = $_.Name; + $psHtml.Append("td",$null,$object."$n").N() + $cellid = $null + if($idOverride -ne $null) { + $cellid = "table-$idOverride-td-$r-$c" + $psHtml.AddAttr($psHtml.newEl,"id",$cellid) + } + if($passthroughTable) { + $pstableln.cells.Add($n,[PowerStartHTMLPassThroughElement]::new($n,($object."$n"),$psHtml.newEl,$cellid)) + } + + $c++ + } + if($passthroughTable) { + $pstableln + } + $psHtml.Up().N() + $isFirst = $false + $r++ + } + end { + } +} + + +Export-ModuleMember -Function @('New-PowerStartHTML','Add-PowerStartHTMLTable') + + + diff --git a/Modules/VMware-vCD-TenantReport/tests/VMware-vCD-TenantReport.Tests.ps1 b/Modules/VMware-vCD-TenantReport/tests/VMware-vCD-TenantReport.Tests.ps1 new file mode 100644 index 0000000..2effa50 --- /dev/null +++ b/Modules/VMware-vCD-TenantReport/tests/VMware-vCD-TenantReport.Tests.ps1 @@ -0,0 +1,24 @@ +$moduleRoot = Resolve-Path "$PSScriptRoot\.." +$moduleName = "VMware-vCD-TenantReport" + +Describe "General project validation: $moduleName" { + + $scripts = Get-ChildItem $moduleRoot -Include *.ps1, *.psm1, *.psd1 -Recurse + + # TestCases are splatted to the script so we need hashtables + $testCase = $scripts | Foreach-Object {@{file = $_}} + It "Script should be valid powershell" -TestCases $testCase { + param($file) + + $file.fullname | Should Exist + + $contents = Get-Content -Path $file.fullname -ErrorAction Stop + $errors = $null + $null = [System.Management.Automation.PSParser]::Tokenize($contents, [ref]$errors) + $errors.Count | Should Be 0 + } + + It "Module '$moduleName' can import cleanly" { + {Import-Module (Join-Path $moduleRoot "$moduleName.psd1") -force } | Should Not Throw + } +} From b89bd7ab5a013c8ec4df082dc7aa4b1fbf2902c1 Mon Sep 17 00:00:00 2001 From: Sam McGeown Date: Fri, 8 Sep 2017 17:40:08 +0100 Subject: [PATCH 061/112] Added HA vCenter Deploy script Added HA vCenter Deploy script, configuration JSON file and note with link to my blog post detailing how to use it --- Scripts/ha-vcenter-deploy-template.json | 58 ++++ Scripts/ha-vcenter-deploy.md | 4 + Scripts/ha-vcenter-deploy.ps1 | 381 ++++++++++++++++++++++++ 3 files changed, 443 insertions(+) create mode 100644 Scripts/ha-vcenter-deploy-template.json create mode 100644 Scripts/ha-vcenter-deploy.md create mode 100644 Scripts/ha-vcenter-deploy.ps1 diff --git a/Scripts/ha-vcenter-deploy-template.json b/Scripts/ha-vcenter-deploy-template.json new file mode 100644 index 0000000..3ac0079 --- /dev/null +++ b/Scripts/ha-vcenter-deploy-template.json @@ -0,0 +1,58 @@ +{ + "__version": "0.1", + "__comments": "Configuration for ha-vcenter-deploy.ps1 - www.definit.co.uk", + "target": { + "server": "vcsa.definit.local", + "user": "administrator@vsphere.local", + "password": "VMware1!", + "datacenter": "Lab", + "cluster": "Workload", + "datastore": "vsanDatastore", + "folder": "Nested Labs/HA-vCenter", + "portgroup": "HA-vCenter-Management", + "ha-portgroup": "HA-vCenter-Heartbeat", + "network": { + "netmask": "255.255.255.0", + "gateway": "10.0.11.1", + "prefix": "24", + "dns": "192.168.1.20", + "domain": "definit.local", + "ntp": "192.168.1.1" + } + }, + "sources": { + "VCSAInstaller": "e:\\Pod-Deploy\\vSphere\\VMware-VCSA-all-6.5.0-4944578" + }, + "active": { + "deploymentSize": "small", + "name": "ha-vc-active", + "ip": "10.0.11.10", + "ha-ip": "172.16.1.1", + "hostname": "ha-vc.definit.local", + "rootPassword": "VMware1!", + "sso": { + "domain": "vsphere.local", + "site": "Default-First-Site", + "password": "VMware1!" + }, + "datacenter": "HA-vCenter-Datacenter", + "cluster": "HA-vCenter-Cluster-1", + "distributedSwitch": "HA-vCenter-VDS", + "portgroup": "HA-vCenter-PortGroup" + }, + "cluster": { + "passive-ip": "172.16.1.2", + "passive-name": "ha-vc-passive", + "witness-ip": "172.16.1.3", + "witness-name": "ha-vc-witness", + "ha-mask": "255.255.255.248" + }, + "general": { + "syslog": "192.168.1.26", + "ssh": true, + "log": "ha-vcenter-deploy.log" + }, + "license": { + "vcenter": "7H23H-11111-22222-33333-90ZQN" + } +} \ No newline at end of file diff --git a/Scripts/ha-vcenter-deploy.md b/Scripts/ha-vcenter-deploy.md new file mode 100644 index 0000000..91271ca --- /dev/null +++ b/Scripts/ha-vcenter-deploy.md @@ -0,0 +1,4 @@ +# HA-vCenter-Deploy +PowerShell script to deploy a highly available vCenter Server + +See https://www.definit.co.uk/2017/06/powershell-deploying-vcenter-high-availability-in-advanced-mode/ for details diff --git a/Scripts/ha-vcenter-deploy.ps1 b/Scripts/ha-vcenter-deploy.ps1 new file mode 100644 index 0000000..7a9984a --- /dev/null +++ b/Scripts/ha-vcenter-deploy.ps1 @@ -0,0 +1,381 @@ +param( + [Parameter(Mandatory=$true)] [String]$configFile, + [switch]$deployActive, + [switch]$licenseVCSA, + [switch]$addSecondaryNic, + [switch]$prepareVCHA, + [switch]$clonePassiveVM, + [switch]$cloneWitnessVM, + [switch]$configureVCHA, + [switch]$resizeWitness, + [switch]$createDRSRule +) + +if($psboundparameters.count -eq 1) { + # Only the configFile is passed, set all steps to true + $deployActive = $true + $licenseVCSA = $true + $addSecondaryNic = $true + $prepareVCHA = $true + $clonePassiveVM = $true + $cloneWitnessVM = $true + $configureVCHA = $true + $resizeWitness = $true + $createDRSRule = $true +} + +# Import the PowerCLI and DNS modules +Get-Module -ListAvailable VMware*,DnsServer | Import-Module +if ( !(Get-Module -Name VMware.VimAutomation.Core -ErrorAction SilentlyContinue) ) { + throw "PowerCLI must be installed" +} +# Written by Sam McGeown @sammcgeown - www.definit.co.uk +# Hat tips and thanks go to... +# William Lam http://www.virtuallyghetto.com/2016/11/vghetto-automated-vsphere-lab-deployment-for-vsphere-6-0u2-vsphere-6-5.html +# http://www.virtuallyghetto.com/2017/01/exploring-new-vcsa-vami-api-wpowercli-part-1.html + +# Get the folder location +$ScriptLocation = Split-Path -Parent $PSCommandPath + +# Import the JSON Config File +$podConfig = (get-content $($configFile) -Raw) | ConvertFrom-Json + +# Path to VCSA Install Sources +$VCSAInstaller = "$($podConfig.sources.VCSAInstaller)" + +# Log File +$verboseLogFile = $podConfig.general.log + +$StartTime = Get-Date + +Function Write-Log { + param( + [Parameter(Mandatory=$true)] + [String]$message, + [switch]$Warning, + [switch]$Info + ) + $timeStamp = Get-Date -Format "dd-MM-yyyy hh:mm:ss" + Write-Host -NoNewline -ForegroundColor White "[$timestamp]" + if($Warning){ + Write-Host -ForegroundColor Yellow " WARNING: $message" + } elseif($Info) { + Write-Host -ForegroundColor White " $message" + }else { + Write-Host -ForegroundColor Green " $message" + } + $logMessage = "[$timeStamp] $message" | Out-File -Append -LiteralPath $verboseLogFile +} + +function Get-VCSAConnection { + param( + [string]$vcsaName, + [string]$vcsaUser, + [string]$vcsaPassword + ) + $existingConnection = $global:DefaultVIServers | where-object -Property Name -eq -Value $vcsaName + if($existingConnection -ne $null) { + return $existingConnection; + } else { + $connection = Connect-VIServer -Server $vcsaName -User $vcsaUser -Password $vcsaPassword -WarningAction SilentlyContinue; + if($connection -ne $null) { + return $connection; + } else { + throw "Unable to connect to $($vcsaName)..." + } + } +} + +function Close-VCSAConnection { + param( + [string]$vcsaName + ) + if($vcsaName.Length -le 0) { + if($Global:DefaultVIServers -ne $null) { + Disconnect-VIServer -Server $Global:DefaultVIServers -Confirm:$false -ErrorAction SilentlyContinue + } + } else { + $existingConnection = $global:DefaultVIServers | where-object -Property Name -eq -Value $vcsaName + if($existingConnection -ne $null) { + Disconnect-VIServer -Server $existingConnection -Confirm:$false; + } else { + Write-Warning -Message "Could not find an existing connection named $($vcsaName)" + } + } +} + +function Get-PodFolder { + param( + $vcsaConnection, + [string]$folderPath + ) + $folderArray = $folderPath.split("/") + $parentFolder = Get-Folder -Server $vcsaConnection -Name vm + foreach($folder in $folderArray) { + $folderExists = Get-Folder -Server $vcsaConnection | Where-Object -Property Name -eq -Value $folder + if($folderExists -ne $null) { + $parentFolder = $folderExists + } else { + $parentFolder = New-Folder -Name $folder -Location $parentFolder + } + } + return $parentFolder +} + + +Close-VCSAConnection + +if($deployActive) { + Write-Log "#### Deploying Active VCSA ####" + $pVCSA = Get-VCSAConnection -vcsaName $podConfig.target.server -vcsaUser $podConfig.target.user -vcsaPassword $podConfig.target.password + $pCluster = Get-Cluster -Name $podConfig.target.cluster -Server $pVCSA + $pDatastore = Get-Datastore -Name $podConfig.target.datastore -Server $pVCSA + $pPortGroup = Get-VDPortgroup -Name $podConfig.target.portgroup -Server $pVCSA + $pFolder = Get-PodFolder -vcsaConnection $pVCSA -folderPath $podConfig.target.folder + + Write-Log "Disabling DRS on $($podConfig.target.cluster)" + $pCluster | Set-Cluster -DrsEnabled:$true -DrsAutomationLevel:PartiallyAutomated -Confirm:$false | Out-File -Append -LiteralPath $verboseLogFile + + + Write-Log "Creating DNS Record" + Add-DnsServerResourceRecordA -Name $podConfig.active.name -ZoneName $podConfig.target.network.domain -AllowUpdateAny -IPv4Address $podConfig.active.ip -ComputerName "192.168.1.20" -CreatePtr -ErrorAction SilentlyContinue + + Write-Log "Deploying VCSA" + $config = (Get-Content -Raw "$($VCSAInstaller)\vcsa-cli-installer\templates\install\embedded_vCSA_on_VC.json") | convertfrom-json + $config.'new.vcsa'.vc.hostname = $podConfig.target.server + $config.'new.vcsa'.vc.username = $podConfig.target.user + $config.'new.vcsa'.vc.password = $podConfig.target.password + $config.'new.vcsa'.vc.datacenter = @($podConfig.target.datacenter) + $config.'new.vcsa'.vc.datastore = $podConfig.target.datastore + $config.'new.vcsa'.vc.target = @($podConfig.target.cluster) + $config.'new.vcsa'.vc.'deployment.network' = $podConfig.target.portgroup + $config.'new.vcsa'.os.'ssh.enable' = $podConfig.general.ssh + $config.'new.vcsa'.os.password = $podConfig.active.rootPassword + $config.'new.vcsa'.appliance.'thin.disk.mode' = $true + $config.'new.vcsa'.appliance.'deployment.option' = $podConfig.active.deploymentSize + $config.'new.vcsa'.appliance.name = $podConfig.active.name + $config.'new.vcsa'.network.'system.name' = $podConfig.active.hostname + $config.'new.vcsa'.network.'ip.family' = "ipv4" + $config.'new.vcsa'.network.mode = "static" + $config.'new.vcsa'.network.ip = $podConfig.active.ip + $config.'new.vcsa'.network.'dns.servers'[0] = $podConfig.target.network.dns + $config.'new.vcsa'.network.prefix = $podConfig.target.network.prefix + $config.'new.vcsa'.network.gateway = $podConfig.target.network.gateway + $config.'new.vcsa'.sso.password = $podConfig.active.sso.password + $config.'new.vcsa'.sso.'domain-name' = $podConfig.active.sso.domain + $config.'new.vcsa'.sso.'site-name' = $podConfig.active.sso.site + + Write-Log "Creating VCSA JSON Configuration file for deployment" + + $config | ConvertTo-Json | Set-Content -Path "$($ENV:Temp)\active.json" + if((Get-VM | Where-Object -Property Name -eq -Value $podConfig.active.name) -eq $null) { + Write-Log "Deploying OVF, this may take a while..." + Invoke-Expression "$($VCSAInstaller)\vcsa-cli-installer\win32\vcsa-deploy.exe install --no-esx-ssl-verify --accept-eula --acknowledge-ceip $($ENV:Temp)\active.json"| Out-File -Append -LiteralPath $verboseLogFile + $vcsaDeployOutput | Out-File -Append -LiteralPath $verboseLogFile + Write-Log "Moving $($podConfig.active.name) to $($podConfig.target.folder)" + if((Get-VM | where {$_.name -eq $podConfig.active.name}) -eq $null) { + throw "Could not find VCSA VM. The script was unable to find the deployed VCSA" + } + Get-VM -Name $podConfig.active.name | Move-VM -Destination $pFolder | Out-File -Append -LiteralPath $verboseLogFile + } else { + Write-Log "VCSA exists, skipping" -Warning + } + Close-VCSAConnection +} + + +if($licenseVCSA) { + Write-Log "#### Configuring VCSA ####" + Write-Log "Getting connection to the new VCSA" + $nVCSA = Get-VCSAConnection -vcsaName $podConfig.active.ip -vcsaUser "administrator@$($podConfig.active.sso.domain)" -vcsaPassword $podConfig.active.sso.password + + Write-Log "Installing vCenter License" + $serviceInstance = Get-View ServiceInstance -Server $nVCSA + $licenseManagerRef=$serviceInstance.Content.LicenseManager + $licenseManager=Get-View $licenseManagerRef + $licenseManager.AddLicense($podConfig.license.vcenter,$null) | Out-File -Append -LiteralPath $verboseLogFile + $licenseAssignmentManager = Get-View $licenseManager.LicenseAssignmentManager + Write-Log "Assigning vCenter Server License" + try { + $licenseAssignmentManager.UpdateAssignedLicense($nVCSA.InstanceUuid, $podConfig.license.vcenter, $null) | Out-File -Append -LiteralPath $verboseLogFile + } + catch { + $ErrorMessage = $_.Exception.Message + Write-Log $ErrorMessage -Warning + } + Close-VCSAConnection -vcsaName $podConfig.active.ip +} + +if($addSecondaryNic) { + Write-Log "#### Adding HA Network Adapter ####" + $pVCSA = Get-VCSAConnection -vcsaName $podConfig.target.server -vcsaUser $podConfig.target.user -vcsaPassword $podConfig.target.password + + if((Get-VM -Server $pVCSA -Name $podConfig.active.name | Get-NetworkAdapter).count -le 1) { + Write-Log "Adding HA interface" + Get-VM -Server $pVCSA -Name $podConfig.active.name | New-NetworkAdapter -Portgroup (Get-VDPortgroup -Name $podConfig.target."ha-portgroup") -Type Vmxnet3 -StartConnected | Out-File -Append -LiteralPath $verboseLogFile + } + Close-VCSAConnection + + $nVCSA = Get-VCSAConnection -vcsaName $podConfig.active.ip -vcsaUser "administrator@$($podConfig.active.sso.domain)" -vcsaPassword $podConfig.active.sso.password + + Write-Log "Configuring HA interface" + $CisServer = Connect-CisServer -Server $podConfig.active.ip -User "administrator@$($podConfig.active.sso.domain)" -Password $podConfig.active.sso.password + + $ipv4API = (Get-CisService -Name 'com.vmware.appliance.techpreview.networking.ipv4') + $specList = $ipv4API.Help.set.config.CreateExample() + $createSpec = [pscustomobject] @{ + address = $podConfig.active."ha-ip"; + default_gateway = ""; + interface_name = "nic1"; + mode = "is_static"; + prefix = "29"; + } + $specList += $createSpec + $ipv4API.set($specList) + Close-VCSAConnection +} + +if($prepareVCHA) { + Write-Log "#### Preparing vCenter HA mode ####" + + $nVCSA = Get-VCSAConnection -vcsaName $podConfig.active.ip -vcsaUser "administrator@$($podConfig.active.sso.domain)" -vcsaPassword $podConfig.active.sso.password + + Write-Log "Preparing vCenter HA" + $ClusterConfig = Get-View failoverClusterConfigurator + + $PassiveIpSpec = New-Object VMware.Vim.CustomizationFixedIp + $PassiveIpSpec.IpAddress = $podConfig.cluster."passive-ip" + + $PassiveNetwork = New-object VMware.Vim.CustomizationIPSettings + $PassiveNetwork.Ip = $PassiveIpSpec + $PassiveNetwork.SubnetMask = $podConfig.cluster."ha-mask" + + $PassiveNetworkSpec = New-Object Vmware.Vim.PassiveNodeNetworkSpec + $PassiveNetworkSpec.IpSettings = $PassiveNetwork + + $WitnessIpSpec = New-Object VMware.Vim.CustomizationFixedIp + $WitnessIpSpec.IpAddress = $podConfig.cluster."witness-ip" + + $WitnessNetwork = New-object VMware.Vim.CustomizationIPSettings + $WitnessNetwork.Ip = $WitnessIpSpec + $WitnessNetwork.SubnetMask = $podConfig.cluster."ha-mask" + + $WitnessNetworkSpec = New-Object VMware.Vim.NodeNetworkSpec + $WitnessNetworkSpec.IpSettings = $WitnessNetwork + + $ClusterNetworkSpec = New-Object VMware.Vim.VchaClusterNetworkSpec + $ClusterNetworkSpec.WitnessNetworkSpec = $WitnessNetworkSpec + $ClusterNetworkSpec.PassiveNetworkSpec = $PassiveNetworkSpec + + $PrepareTask = $ClusterConfig.prepareVcha_task($ClusterNetworkSpec) + + Close-VCSAConnection +} + +if($clonePassiveVM) { + Write-Log "#### Cloning VCSA for Passive Node ####" + + $pVCSA = Get-VCSAConnection -vcsaName $podConfig.target.server -vcsaUser $podConfig.target.user -vcsaPassword $podConfig.target.password + $pVMHost = Get-Random (Get-VMhost -Location $podConfig.target.cluster) + $pFolder = Get-PodFolder -vcsaConnection $pVCSA -folderPath $podConfig.target.folder + + $activeVM = Get-VM -Name $podConfig.active.name + $CloneSpecName = "vCHA_ClonePassive" + + Write-Log "Creating customization spec" + # Clean up any old spec + Get-OSCustomizationSpec -Name $CloneSpecName -ErrorAction SilentlyContinue | Remove-OSCustomizationSpec -Confirm:$false -ErrorAction SilentlyContinue | Out-File -Append -LiteralPath $verboseLogFile + New-OSCustomizationSpec -Name $CloneSpecName -OSType Linux -Domain $podConfig.target.network.domain -NamingScheme fixed -DnsSuffix $podConfig.target.network.domain -NamingPrefix $podConfig.active.hostname -DnsServer $podConfig.target.network.dns -Type NonPersistent | Out-File -Append -LiteralPath $verboseLogFile + Get-OSCustomizationNicMapping -OSCustomizationSpec $CloneSpecName | Set-OSCustomizationNicMapping -IpMode UseStaticIP -IpAddress $podConfig.active.ip -SubnetMask $podConfig.target.network.netmask -DefaultGateway $podConfig.target.network.gateway | Out-File -Append -LiteralPath $verboseLogFile + New-OSCustomizationNicMapping -OSCustomizationSpec $CloneSpecName -IpMode UseStaticIP -IpAddress $podConfig.cluster."passive-ip" -SubnetMask $podConfig.cluster."ha-mask" -DefaultGateway $podConfig.target.network.gateway | Out-File -Append -LiteralPath $verboseLogFile + + Write-Log "Cloning Active VCSA to Passive VCSA" + $passiveVM = New-VM -Name $podConfig.cluster."passive-name" -VM $activeVM -OSCustomizationSpec $CloneSpecName -VMhost $pVMHost -Server $pVCSA -Location $pFolder | Start-VM | Out-File -Append -LiteralPath $verboseLogFile + + # Ensure the network adapters are connected + $passiveVM | Get-NetworkAdapter | Set-NetworkAdapter -Connected:$true -Confirm:$false + + Write-Log "Waiting for VMware Tools" + $passiveVM | Wait-Tools + + Close-VCSAConnection +} + +if($cloneWitnessVM) { + Write-Log "#### Cloning VCSA for Witness Node ####" + + $pVCSA = Get-VCSAConnection -vcsaName $podConfig.target.server -vcsaUser $podConfig.target.user -vcsaPassword $podConfig.target.password + $pVMHost = Get-Random (Get-VMhost -Location $podConfig.target.cluster) + $pFolder = Get-PodFolder -vcsaConnection $pVCSA -folderPath $podConfig.target.folder + + $activeVM = Get-VM -Name $podConfig.active.name + $CloneSpecName = "vCHA_CloneWitness" + + Write-Log "Creating customization spec" + # Clean up any old spec + Get-OSCustomizationSpec -Name $CloneSpecName -ErrorAction SilentlyContinue | Remove-OSCustomizationSpec -Confirm:$false -ErrorAction SilentlyContinue | Out-File -Append -LiteralPath $verboseLogFile + New-OSCustomizationSpec -Name $CloneSpecName -OSType Linux -Domain $podConfig.target.network.domain -NamingScheme fixed -DnsSuffix $podConfig.target.network.domain -NamingPrefix $podConfig.active.hostname -DnsServer $podConfig.target.network.dns -Type NonPersistent | Out-File -Append -LiteralPath $verboseLogFile + New-OSCustomizationNicMapping -OSCustomizationSpec $CloneSpecName -IpMode UseStaticIP -IpAddress $podConfig.cluster."witness-ip" -SubnetMask $podConfig.cluster."ha-mask" -DefaultGateway $podConfig.target.network.gateway | Out-File -Append -LiteralPath $verboseLogFile + + Write-Log "Cloning Active VCSA to Witness VCSA" + $witnessVM = New-VM -Name $podConfig.cluster."witness-name" -VM $activeVM -OSCustomizationSpec $CloneSpecName -VMhost $pVMHost -Server $pVCSA -Location $pFolder | Start-VM | Out-File -Append -LiteralPath $verboseLogFile + + # Ensure the network adapters are connected + $witnessVM | Get-NetworkAdapter | Set-NetworkAdapter -Connected:$true -Confirm:$false + + Write-Log "Waiting for VMware Tools" + $witnessVM | Wait-Tools + + Close-VCSAConnection +} + +if($configureVCHA) { + Write-Log "#### Configuring vCenter HA mode ####" + + $nVCSA = Get-VCSAConnection -vcsaName $podConfig.active.ip -vcsaUser "administrator@$($podConfig.active.sso.domain)" -vcsaPassword $podConfig.active.sso.password + + $ClusterConfig = Get-View failoverClusterConfigurator + $ClusterConfigSpec = New-Object VMware.Vim.VchaClusterConfigSpec + $ClusterConfigSpec.PassiveIp = $podConfig.cluster."passive-ip" + $ClusterConfigSpec.WitnessIp = $podConfig.cluster."witness-ip" + $ConfigureTask = $ClusterConfig.configureVcha_task($ClusterConfigSpec) + Write-Log "Waiting for cluster configuration task" + Start-Sleep -Seconds 30 + + Close-VCSAConnection -vcsaName $podConfig.active.ip +} + +if($resizeWitness) { + Write-Log "#### Resizing Witness Node ####" + $pVCSA = Get-VCSAConnection -vcsaName $podConfig.target.server -vcsaUser $podConfig.target.user -vcsaPassword $podConfig.target.password + + $witnessVM = Get-VM -Name $podConfig.cluster."witness-name" + Write-Log "Waiting for Witness node to shut down" + $witnessVM | Stop-VMGuest -Confirm:$false | Out-File -Append -LiteralPath $verboseLogFile + do { + Start-Sleep -Seconds 3 + $witnessVM = Get-VM -Name $podConfig.cluster."witness-name" + } until($witnessVM.PowerState -eq "Poweredoff") + Write-Log "Setting CPU and Memory" + $witnessVM | Set-VM -MemoryGB 1 -NumCpu 1 -Confirm:$false | Out-File -Append -LiteralPath $verboseLogFile + Write-Log "Starting Witness VM" + $witnessVM | Start-VM | Out-File -Append -LiteralPath $verboseLogFile + Close-VCSAConnection +} + +if($createDRSRule) { + Write-Log "#### Creating DRS Rule ####" + $pVCSA = Get-VCSAConnection -vcsaName $podConfig.target.server -vcsaUser $podConfig.target.user -vcsaPassword $podConfig.target.password + $pCluster = Get-Cluster $podConfig.target.cluster + $vCHA = Get-VM -Name $podConfig.active.name,$podConfig.cluster."passive-name",$podConfig.cluster."witness-name" + New-DRSRule -Name "vCenter HA" -Cluster $pCluster -VM $vCHA -KeepTogether $false | Out-File -Append -LiteralPath $verboseLogFile + Write-Log "Enabling DRS on $($podConfig.target.cluster)" + $pCluster | Set-Cluster -DrsEnabled:$true -DrsAutomationLevel:FullyAutomated -Confirm:$false | Out-File -Append -LiteralPath $verboseLogFile + Close-VCSAConnection +} + + +$EndTime = Get-Date +$duration = [math]::Round((New-TimeSpan -Start $StartTime -End $EndTime).TotalMinutes,2) + +Write-Log "Pod Deployment Completed in $($duration) minutes" \ No newline at end of file From b8055b7ed80061617e004852ab0aa29a562f7279 Mon Sep 17 00:00:00 2001 From: Sam McGeown Date: Fri, 8 Sep 2017 17:44:16 +0100 Subject: [PATCH 062/112] Added script description removed extra .md file --- Scripts/ha-vcenter-deploy.md | 4 ---- Scripts/ha-vcenter-deploy.ps1 | 7 +++++++ 2 files changed, 7 insertions(+), 4 deletions(-) delete mode 100644 Scripts/ha-vcenter-deploy.md diff --git a/Scripts/ha-vcenter-deploy.md b/Scripts/ha-vcenter-deploy.md deleted file mode 100644 index 91271ca..0000000 --- a/Scripts/ha-vcenter-deploy.md +++ /dev/null @@ -1,4 +0,0 @@ -# HA-vCenter-Deploy -PowerShell script to deploy a highly available vCenter Server - -See https://www.definit.co.uk/2017/06/powershell-deploying-vcenter-high-availability-in-advanced-mode/ for details diff --git a/Scripts/ha-vcenter-deploy.ps1 b/Scripts/ha-vcenter-deploy.ps1 index 7a9984a..af215dc 100644 --- a/Scripts/ha-vcenter-deploy.ps1 +++ b/Scripts/ha-vcenter-deploy.ps1 @@ -1,3 +1,10 @@ +<# +Script name: ha-vcenter-deploy.ps1 +Created on: 30/06/2017 +Author: Sam McGeown, @sammcgeown +Description: The purpose of the script is to deploy vCenter in High Availability mode, using the advanced method. See https://www.definit.co.uk/2017/06/powershell-deploying-vcenter-high-availability-in-advanced-mode/ +Dependencies: None known +#> param( [Parameter(Mandatory=$true)] [String]$configFile, [switch]$deployActive, From 2b9aa80b36a54befe058cf911a15d5dca2062f41 Mon Sep 17 00:00:00 2001 From: soggychipsnz <31921723+soggychipsnz@users.noreply.github.com> Date: Wed, 13 Sep 2017 11:23:36 +0100 Subject: [PATCH 063/112] get-peakvms This script will interrogate vcenter to find the peak users of network or storage usage. This was quite handy for me to quickly identify the source of issues such as an inbound DDOS, outbound DOS or a server pummiling storage due to swapping/etc. --- get-peakvms.ps1 | 181 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 get-peakvms.ps1 diff --git a/get-peakvms.ps1 b/get-peakvms.ps1 new file mode 100644 index 0000000..79d7eb3 --- /dev/null +++ b/get-peakvms.ps1 @@ -0,0 +1,181 @@ +<# +Script name: get-peakvms.ps1 +Created on: 09/06/2016 +Author: Scott White, @soggychipsnz, https://github.com/LatexGolem +Description: This script will interrogate vcenter to find the peak users of network or storage usage. +This was quite handy for me to quickly identify the source of issues such as an inbound DDOS, outbound DOS or a server pummiling storage due to swapping/etc. +I would suggest that when you first run the script you run it in host mode, which will identify where the hotspot is. Then once the hot host has been identified, run it in VM mode. +For each statistic the number of operations/packets is collectioned along ith the throughput split into reads or writes/sent or recieved - giving you peak and 95th percentiles +Really need to get around to tidying this up :) +Results will be outputted to OGV and into a timestamped CSV in the current working directory +Dependencies: Nothing special. Assumes vsphere logging has 20 second samples for the last 60 minutes and 5 minute samples in the past 24 hours. +#> + +$clusterfilter = "*" + +#Gather basic stuff +do{ + $modes = @("host","vm") + $mode = $modes | OGV -PassThru -Title "Select mode of operation" + $stats = @("Disk Throughput","Network Throughput") + $stat = $stats | OGV -PassThru -Title "Select type of Stat to check" + if (($mode.count -eq 2) -or ($stat.count -eq 2) ){write-host "Please select only a single mode of operation and statistic type."} +}while ($mode.count -eq 2 -and $stat.count -eq 2) + +$hour = 1..24 +$hour = $hour | OGV -PassThru -Title "Select number of hours to go back in time" + + +#Helper Stuff +$start = (Get-Date).addHours(-$hour) +$finish = (Get-Date) +$duration = $finish - $start | %{[int]$_.TotalSeconds} +#If Start is within the last hour, use 20 second sampling otherwise 5 minute avg +if (((Get-Date) - $start ).TotalSeconds -gt 3700) {$interval=300} else {$interval=20} + +function getstats($starttime,$endtime,$sample,$stat,$entity){ + (Get-Stat -Entity $entity -Stat $stat -IntervalSecs $sample -Start $starttime -Finish $endtime).value | measure -Sum | %{$_.sum} +} + + +if ($mode -eq "host"){ + $clusters = get-cluster $clusterfilter + $vmhosts = $clusters | OGV -PassThru -Title "Select Cluster(s) to target, to get all member Hosts"| get-vmhost + + if ($stat -eq "Network Throughput"){ + $master = @() + $vmhosts | %{ + $metric ="net.packetsRx.summation" + $pktrx = getstats $start $finish $interval $metric $_ + + $metric ="net.packetsTx.summation" + $pkttx = getstats $start $finish $interval $metric $_ + + $metric ="net.bytesRx.average" + $bytesrx = getstats $start $finish $interval $metric $_ + + $metric ="net.bytesTx.average" + $bytestx = getstats $start $finish $interval $metric $_ + + $row = "" | select name,pktrx,pkttx,bytesrx,bytestx + $row.name = $_.name + $row.pktrx = $pktrx + $row.pkttx = $pkttx + $row.bytesrx = $bytesrx + $row.bytestx = $bytestx + $master += $row + } + $master | ogv #sort -Property name | Format-Table + } + if ($stat -eq "Disk Throughput"){ + #Target the datastore, just one. + $datastore = Get-Datastore -vmhost $vmhost[0]| OGV -PassThru -Title "Select target datastore" + #Yes this is fugly. + $instance = ($datastore | Get-View).Info.Url.Split("/") | select -last 2 | select -First 1 + + $master = @() + $vmhosts | %{ + $metric ="datastore.datastoreReadIops.latest" + $rop = (Get-Stat -Entity $_ -Stat $metric -IntervalSecs $interval -Start $start -Finish $finish | ?{$_.Instance -match $instance}).value | measure -Sum | %{$_.sum} + + $metric ="datastore.datastoreWriteIops.latest" + $wrop = (Get-Stat -Entity $_ -Stat $metric -IntervalSecs $interval -Start $start -Finish $finish | ?{$_.Instance -match $instance}).value | measure -Sum | %{$_.sum} + + $metric ="datastore.datastoreReadBytes.latest" + $rbytes = (Get-Stat -Entity $_ -Stat $metric -IntervalSecs $interval -Start $start -Finish $finish | ?{$_.Instance -match $instance}).value | measure -Sum | %{$_.sum} + + $metric ="datastore.datastoreWriteBytes.latest" + $wrbytes = (Get-Stat -Entity $_ -Stat $metric -IntervalSecs $interval -Start $start -Finish $finish | ?{$_.Instance -match $instance}).value | measure -Sum | %{$_.sum} + + $row = "" | select name,rop,wrop,rbytes,wrbytes + $row.name = $_.name + $row.rop = $rop + $row.wrop = $wrop + $row.rbytes = $rbytes + $row.wrbytes = $wrbytes + $master += $row + } + $master | ogv + } + +}if ($mode -eq "vm"){ + Write-Host "Do note doing things on a vmbasis take quite some time (please isolate on a host basis first)" + #Currently only works on a cluster basis + $clusters = get-cluster $clusterfilter + $vms = $clusters |get-vmhost | OGV -PassThru -Title "Select Hosts(s) to target, to get all member VMs"| get-vm | ?{$_.powerstate -match "poweredOn"} + + if ($stat -eq "Network Throughput"){ + $master = new-object system.collections.arraylist + $vms | %{ + $metric ="net.packetsRx.summation" + $pktrx = getstats $start $finish $interval $metric $_ + + $metric ="net.packetsTx.summation" + $pkttx = getstats $start $finish $interval $metric $_ + + $metric ="net.bytesRx.average" + $bytesrx = getstats $start $finish $interval $metric $_ + + $metric ="net.bytesTx.average" + $bytestx = getstats $start $finish $interval $metric $_ + + $row = "" | select name,pktrx,pkttx,bytesrx,bytestx + $row.name = $_.name + $row.pktrx = $pktrx + $row.pkttx = $pkttx + $row.bytesrx = $bytesrx + $row.bytestx = $bytestx + $master += $row + } + $master | ogv + } + if ($stat -eq "Disk Throughput"){ + $master = new-object system.collections.arraylist + + $vms = $vms |?{$_.PowerState -eq "PoweredOn"} + foreach ($vm in $vms){ + $row = ""| select name,iops,riops,riops95,rpeak,wiops,wiops95,wpeak,throughputGBpduration,wMBps,rMBps,datastore,used,prov,iops95 + + $metric = "datastore.numberReadAveraged.average" + $rawdata = Get-Stat -Entity $vm -Stat $metric -IntervalSecs $interval -Start $start -Finish $finish + $tmp = $rawdata.value | measure -average -Maximum + $row.riops = [int] $tmp.average + $row.rpeak = [int] $tmp.Maximum + $row.riops95 = ($rawdata.value | sort)[[math]::Round(($rawdata.count-1) * .95)] + + $metric = "datastore.numberwriteaveraged.average" + $rawdata = Get-Stat -Entity $vm -Stat $metric -IntervalSecs $interval -Start $start -Finish $finish + $tmp = $rawdata.value | measure -average -Maximum + $row.wiops = [int] $tmp.average + $row.wpeak = [int] $tmp.Maximum + $row.wiops95 = ($rawdata.value | sort)[[math]::Round(($rawdata.count-1) * .95)] + + $row.iops = ($row.wiops + $row.riops) + + $metric = "datastore.write.average" + $rawdatawr = Get-Stat -Entity $vm -Stat $metric -IntervalSecs $interval -Start $start -Finish $finish + + $metric = "datastore.read.average" + $rawdatar = Get-Stat -Entity $vm -Stat $metric -IntervalSecs $interval -Start $start -Finish $finish + $reads = $rawdatar.value | measure -Sum | %{$_.sum}| %{$_ /($duration/$interval)/1024/1024} + $writes = $rawdatawr.value | measure -Sum | %{$_.sum}| %{$_ /($duration/$interval)/1024/1024} + $total = $reads * $duration + $writes * $duration + + + $row.name = $vm.name + $row.throughputGBpduration = [decimal]::round($total,2) + $row.wMBps = [decimal]::round($writes*1024,2) + $row.rMBps = [decimal]::round($reads*1024,2) + $row.datastore = ($vm.DatastoreIdList[0] | Get-VIObjectByVIView).name #(($vm.DatastoreIdList| select -First 1 | Get-VIObjectByVIView).Name) + + $row.used = [System.Math]::Round(($vm.UsedSpaceGB)) + $row.prov = [System.Math]::Round(($vm.ProvisionedSpaceGB)) + $row.iops95 = $row.riops95 + $row.wiops95 + + $master += $row + } + $master | ogv + } + +} +$master | Export-Csv "$mode$stat-$(get-date -Format HHmm).csv" From 0d461ab72b06a3e6da4d9d6d547dfc9143458590 Mon Sep 17 00:00:00 2001 From: Alan Comstock <31929022+Mr-Uptime@users.noreply.github.com> Date: Wed, 13 Sep 2017 10:29:57 -0500 Subject: [PATCH 064/112] Create SetMultiPathToRoundRobin --- Scripts/SetMultiPathToRoundRobin | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 Scripts/SetMultiPathToRoundRobin diff --git a/Scripts/SetMultiPathToRoundRobin b/Scripts/SetMultiPathToRoundRobin new file mode 100644 index 0000000..382cbd3 --- /dev/null +++ b/Scripts/SetMultiPathToRoundRobin @@ -0,0 +1,6 @@ +#Check a host for any Fibre Channel devices that are not set to Round Robin. Modify to check clusters if needed. +Get-VMhost VMHOSTNAME | Get-VMHostHba -Type "FibreChannel" | Get-ScsiLun -LunType disk | Where { $_.MultipathPolicy -notlike "RoundRobin" } | Select CanonicalName,MultipathPolicy + +#Set the Multipathing Policy on a host to Round Robin for any Fibre Channel devices that are not Round Robin +$scsilun = Get-VMhost VMHOSTNAME | Get-VMHostHba -Type "FibreChannel" | Get-ScsiLun -LunType disk | Where { $_.MultipathPolicy -notlike "RoundRobin" } +Set-ScsiLun -ScsiLun $scsilun -MultipathPolicy RoundRobin From b10e5606150bfc47ce3dca8d947c4ed124306cce Mon Sep 17 00:00:00 2001 From: Alan Comstock <31929022+Mr-Uptime@users.noreply.github.com> Date: Wed, 13 Sep 2017 12:37:15 -0500 Subject: [PATCH 065/112] Update SetMultiPathToRoundRobin --- Scripts/SetMultiPathToRoundRobin | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/Scripts/SetMultiPathToRoundRobin b/Scripts/SetMultiPathToRoundRobin index 382cbd3..c1a6253 100644 --- a/Scripts/SetMultiPathToRoundRobin +++ b/Scripts/SetMultiPathToRoundRobin @@ -1,6 +1,17 @@ +<# +Script name: SetMultiPathToRoundRobin.ps1 +Created on: 09/13/2017 +Author: Alan Comstock, @Mr_Uptime +Description: Set the MultiPath policy for FC devices to RoundRobin +Dependencies: None known +PowerCLI Version: VMware PowerCLI 6.5 Release 1 build 4624819 +PowerShell Version: 5.1.14393.1532 +OS Version: Windows 10 +#> + #Check a host for any Fibre Channel devices that are not set to Round Robin. Modify to check clusters if needed. -Get-VMhost VMHOSTNAME | Get-VMHostHba -Type "FibreChannel" | Get-ScsiLun -LunType disk | Where { $_.MultipathPolicy -notlike "RoundRobin" } | Select CanonicalName,MultipathPolicy +Get-VMhost HOSTNAME | Get-VMHostHba -Type "FibreChannel" | Get-ScsiLun -LunType disk | Where { $_.MultipathPolicy -notlike "RoundRobin" } | Select CanonicalName,MultipathPolicy #Set the Multipathing Policy on a host to Round Robin for any Fibre Channel devices that are not Round Robin -$scsilun = Get-VMhost VMHOSTNAME | Get-VMHostHba -Type "FibreChannel" | Get-ScsiLun -LunType disk | Where { $_.MultipathPolicy -notlike "RoundRobin" } +$scsilun = Get-VMhost HOSTNAME | Get-VMHostHba -Type "FibreChannel" | Get-ScsiLun -LunType disk | Where { $_.MultipathPolicy -notlike "RoundRobin" } Set-ScsiLun -ScsiLun $scsilun -MultipathPolicy RoundRobin From fee53a0565d319b82817880871056716d0d343da Mon Sep 17 00:00:00 2001 From: Alan Comstock <31929022+Mr-Uptime@users.noreply.github.com> Date: Thu, 14 Sep 2017 10:42:04 -0500 Subject: [PATCH 066/112] Create SetClusterMultiPathToRoundRobin.ps1 --- Scripts/SetClusterMultiPathToRoundRobin.ps1 | 29 +++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 Scripts/SetClusterMultiPathToRoundRobin.ps1 diff --git a/Scripts/SetClusterMultiPathToRoundRobin.ps1 b/Scripts/SetClusterMultiPathToRoundRobin.ps1 new file mode 100644 index 0000000..97bf311 --- /dev/null +++ b/Scripts/SetClusterMultiPathToRoundRobin.ps1 @@ -0,0 +1,29 @@ +<# + Script name: SetClusterMultiPathToRoundRobin.ps1 + Created on: 09/14/2017 + Author: Alan Comstock, @Mr_Uptime + Description: Set the MultiPath policy for FC devices to RoundRobin for all hosts in a cluster. + Dependencies: None known + PowerCLI Version: VMware PowerCLI 6.5 Release 1 build 4624819 + PowerShell Version: 5.1.14393.1532 + OS Version: Windows 10 +#> + +#Check for any Fibre Channel devices that are not set to Round Robin in a cluster. +#Get-Cluster -Name CLUSTERNAME | Get-VMhost | Get-VMHostHba -Type "FibreChannel" | Get-ScsiLun -LunType disk | Where { $_.MultipathPolicy -notlike "RoundRobin" } | Select CanonicalName,MultipathPolicy + +#Set the Multipathing Policy to Round Robin for any Fibre Channel devices that are not Round Robin in a cluster +$cluster = Get-Cluster CLUSTERNAME +$hostlist = Get-VMHost -Location $cluster | Sort Name +$TotalHostCount = $hostlist.count +$hostincrement = 0 +while ($hostincrement -lt $TotalHostCount){ #Host Loop + $currenthost = $hostlist[$hostincrement].Name + Write-Host "Working on" $currenthost + $scsilun = Get-VMhost $currenthost | Get-VMHostHba -Type "FibreChannel" | Get-ScsiLun -LunType disk | Where { $_.MultipathPolicy -notlike "RoundRobin" } + if ($scsilun -ne $null){ + Set-ScsiLun -ScsiLun $scsilun -MultipathPolicy RoundRobin + } + $hostincrement++ #bump the host increment +} +#The End From 434d8c2b9b32bc17fde46024528fdc849444a5fe Mon Sep 17 00:00:00 2001 From: Chris Date: Wed, 20 Sep 2017 19:34:38 +0100 Subject: [PATCH 067/112] Created Get-TotalDiskUsage --- Scripts/Get-TotalDiskUsage | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 Scripts/Get-TotalDiskUsage diff --git a/Scripts/Get-TotalDiskUsage b/Scripts/Get-TotalDiskUsage new file mode 100644 index 0000000..b14e7d5 --- /dev/null +++ b/Scripts/Get-TotalDiskUsage @@ -0,0 +1,4 @@ +#Script returns total disk usage by all Powered On VMs in the environment in Gigabytes +#Author: Chris Bradshaw via https://isjw.uk/using-powercli-to-measure-vm-disk-space-usage/ + +[math]::Round(((get-vm | Where-object{$_.PowerState -eq "PoweredOn" }).UsedSpaceGB | measure-Object -Sum).Sum) From da2cf62c1b835f9e46b7f10e4e13c091f6cb303e Mon Sep 17 00:00:00 2001 From: Chris Date: Wed, 20 Sep 2017 19:37:36 +0100 Subject: [PATCH 068/112] Create Get-TotalMemoryAllocation.ps1 --- Scripts/Get-TotalMemoryAllocation.ps1 | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 Scripts/Get-TotalMemoryAllocation.ps1 diff --git a/Scripts/Get-TotalMemoryAllocation.ps1 b/Scripts/Get-TotalMemoryAllocation.ps1 new file mode 100644 index 0000000..6e6736d --- /dev/null +++ b/Scripts/Get-TotalMemoryAllocation.ps1 @@ -0,0 +1,6 @@ +#Script gets total memory allocation in GB of all powered on VMs in the environment +#Author: Chris Bradshaw via https://isjw.uk/powercli-snippet-total-memory-allocation/ + +[System.Math]::Round(((get-vm | + where-object{$_.PowerState -eq "PoweredOn" }).MemoryGB | + Measure-Object -Sum).Sum ,0) From 78d606ab44228ce6404a4b4ad247cd102fa5ff11 Mon Sep 17 00:00:00 2001 From: Chris Date: Wed, 20 Sep 2017 19:38:14 +0100 Subject: [PATCH 069/112] Rename Get-TotalDiskUsage to Get-TotalDiskUsage.ps1 --- Scripts/{Get-TotalDiskUsage => Get-TotalDiskUsage.ps1} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Scripts/{Get-TotalDiskUsage => Get-TotalDiskUsage.ps1} (100%) diff --git a/Scripts/Get-TotalDiskUsage b/Scripts/Get-TotalDiskUsage.ps1 similarity index 100% rename from Scripts/Get-TotalDiskUsage rename to Scripts/Get-TotalDiskUsage.ps1 From 3b3197b04acfa3eaa9e25cace1cfe686db06640a Mon Sep 17 00:00:00 2001 From: Lukas Winn <30748942+lukaswinn@users.noreply.github.com> Date: Thu, 21 Sep 2017 16:46:41 +0100 Subject: [PATCH 070/112] Create vCenterSnapshot.ps1 Script to retrieve snapshot information for all VM's in a given vCenter --- Scripts/vCenterSnapshot.ps1 | 40 +++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 Scripts/vCenterSnapshot.ps1 diff --git a/Scripts/vCenterSnapshot.ps1 b/Scripts/vCenterSnapshot.ps1 new file mode 100644 index 0000000..415aca8 --- /dev/null +++ b/Scripts/vCenterSnapshot.ps1 @@ -0,0 +1,40 @@ +<# + .NOTES + Script name: vCenterSnapshot.ps1 + Created on: 20/09/2017 + Author: Lukas Winn, @lukaswinn + Dependencies: Password is set to VMware123 in my test environment but this can be changed. + + .DESCRIPTION + Script to retrieve snapshot information for all VM's in a given vCenter + +#> +Write-Host "`nGet VM Snapshot Information!" +Write-Host "Copyright 2017 Lukas Winn / @lukaswinn" +Write-Host "Version 1.0" "`n" + +$vCenter = Read-Host -prompt 'Enter FQDN / IP address of vCenter' + +if ($vCenter) { + $vcUser = Read-Host -prompt 'Username' + +Write-Host 'vCenter:' $vCenter '' + +# Connect to vCenter with $vCenter variable value +Connect-VIServer -Server $vCenter -User $vcUser -Password VMware123 + + Write-Host "`nConnected to vCenter: " $vCenter + Write-Host 'Retrieving snapshot information...' + Write-Progress -Activity 'Working...' + + # Get VM snapshot information and output in table format + $getSnap = Get-VM | Get-Snapshot | sort SizeGB -descending | Select VM, Name, Created, @{Label="Size";Expression={"{0:N2} GB" -f ($_.SizeGB)}}, Id + $getSnap | Format-Table | Out-Default + +# Close connection to active vCenter +Disconnect-VIServer $vCenter -Confirm:$false + Write-Host 'Connection closed to' $vCenter +} +else { + Write-Warning "Error: No data entered for vCenter!" +} From e8896d388fdf8e2b751d91d60b07da0711d7b55e Mon Sep 17 00:00:00 2001 From: jrob24 Date: Thu, 21 Sep 2017 23:30:31 -0500 Subject: [PATCH 071/112] Add files via upload --- Modules/vCenter.Alarms/New-vCenterAlarms.ps1 | 79 ++ Modules/vCenter.Alarms/vCenter.Alarms.psm1 | 736 +++++++++++++++++++ 2 files changed, 815 insertions(+) create mode 100644 Modules/vCenter.Alarms/New-vCenterAlarms.ps1 create mode 100644 Modules/vCenter.Alarms/vCenter.Alarms.psm1 diff --git a/Modules/vCenter.Alarms/New-vCenterAlarms.ps1 b/Modules/vCenter.Alarms/New-vCenterAlarms.ps1 new file mode 100644 index 0000000..e3ba5c8 --- /dev/null +++ b/Modules/vCenter.Alarms/New-vCenterAlarms.ps1 @@ -0,0 +1,79 @@ +<# + + =========================================================================== + Created by: Jason Robinson + Created on: 05/2017 + Twitter: @jrob24 + Filename: New-vCenterAlarms.ps1 + =========================================================================== + .DESCRIPTION + Examples of creating alarms using vCenter.Alarm module +#> + +Import-Module -Name vCenter.Alarms + +Write-Verbose -Message "Example 1 : Creating new Host CPU Usage alarm (Metric based alarm)" +Write-Verbose -Message "Finding the metric id for 'cpu.usage.average'" +$MetricId = (Get-MetricId -MetricGroup CPU | Where-Object -FilterScript { $_.Name -eq 'cpu.usage.average' }).Key +Write-Verbose -Message "Creating an alarm trigger for cpu.usage.average of 90% for 15mins (Warning) & 95% for 10mins (Alert) on the HostSystem object type" +$Trigger = New-AlarmTrigger -MetricId $MetricId -MetricOperator isAbove -ObjectType HostSystem -Yellow 90 -YellowInterval 15 -Red 95 -RedInterval 10 +Write-Verbose -Message "Creates a new alarm called 'Host CPU Usage' at the root level of vCenter" +New-AlarmDefinition -Name "Host CPU Usage" -Description "Alarm on 95%" -Entity Datacenters -Trigger $Trigger -ActionRepeatMinutes 10 +Write-Verbose -Message "Configures the alarm to send snmp traps" +Get-AlarmDefinition -Name "Host CPU Usage" | vSphere.Alarms\New-AlarmAction -Snmp -GreenToYellow Once -YellowToRed Repeat + +Write-Verbose -Message "Example 2 : Creating new HA Disabled alarm (Event based alarm)" +Write-Verbose -Message "Finding the event type for 'HA disabled for cluster'" +$EventType = (Get-EventId | Where-Object -FilterScript { $_.Description -match 'HA disabled for cluster' }).EventType +Write-Verbose -Message "Creating an alarm trigger for 'DasDisabledEvent' on the ClusterComputeResource object type" +$Trigger = New-AlarmTrigger -EventType $EventType -Status Red -ObjectType ClusterComputeResource +Write-Verbose -Message "Creates a new alarm called 'HA Disabled' at the root level of vCenter" +New-AlarmDefinition -Name "HA Disabled" -Description "Alarm on HA" -Entity Datacenters -Trigger $Trigger -ActionRepeatMinutes 30 +Write-Verbose -Message "Configures the alarm to send an email every 30mins" +$EmailParams = @{ + Email = $true + To = 'helpdesk@company.com' + Subject = 'HA Disabled' +} +Get-AlarmDefinition -Name "HA Disabled" | vCenter.Alarms\New-AlarmAction @EmailParams -YellowToRed Repeat + +Write-Verbose -Message "Example 3 : Creating new Host Connection State alarm (State based alarm)" +Write-Verbose -Message "Creating an alarm trigger for StateType of 'runtime.connectionState' on the HostSystem object type" +$Trigger = New-AlarmTrigger -StateType runtime.connectionState -StateOperator isEqual -YellowStateCondition disconnected -RedStateCondition notResponding -ObjectType HostSystem +Write-Verbose -Message "Creates a new alarm called 'Host Connection State' at the root level of vCenter" +New-AlarmDefinition -Name "Host Connection State" -Description "Connection State" -Entity Datacenters -Trigger $Trigger +Write-Verbose -Message "Configures the alarm to send an email once" +$EmailParams = @{ + Email = $true + To = 'helpdesk@company.com' + Subject = 'Host Connection Lost' +} +Get-AlarmDefinition -Name "Host Connection State" | vCenter.Alarms\New-AlarmAction @EmailParams -YellowToRed Once + +Write-Verbose -Message "Example 4 : Creating new Lost Storage Connectivity (Event based alarm)" +Write-Verbose -Message "Find the event type for 'Lost Storage Connectivity'" +Get-EventId | Where-Object -FilterScript { $_.Description -match 'Lost Storage Connectivity' } +Write-Verbose -Message "Two results returned, we want esx not vprob" + <# + EventType : EventEx + EventTypeId : esx.problem.storage.connectivity.lost + Category : error + Description : Lost Storage Connectivity + FullFormat : Lost connectivity to storage device { 1 }. Path { 2 } is down. Affected datastores: { 3 }. + vCenter : vCenter01 + + EventType : EventEx + EventTypeId : vprob.storage.connectivity.lost + Category : error + Description : Lost Storage Connectivity + FullFormat : Lost connectivity to storage device { 1 }. Path { 2 } is down. Affected datastores: { 3 }. + vCenter : vCenter01 + #> +Write-Verbose -Message "Since the event type is EventEx, we need both the EventType & EventTypeId to create the trigger" +$EventType = Get-EventId | Where-Object -FilterScript { $_.EventTypeId -eq 'esx.problem.storage.connectivity.lost' } +Write-Verbose -Message "Creating an alarm trigger for 'DasDisabledEvent' on the ClusterComputeResource object type" +$Trigger = New-AlarmTrigger -EventType $EventType.EventType -EventTypeId $EventType.EventTypeId -Status Red -ObjectType HostSystem +Write-Verbose -Message "Creates a new alarm called 'Lost Storage Connectivity' at the root level of vCenter" +New-AlarmDefinition -Name "Lost Storage Connectivity" -Description "Lost Storage" -Entity Datacenters -Trigger $Trigger -ActionRepeatMinutes 5 +Write-Verbose -Message "Configures the alarm to send an snmp every 5mins" +Get-AlarmDefinition -Name "Lost Storage Connectivity" | vCenter.Alarms\New-AlarmAction -Snmp -YellowToRed Repeat \ No newline at end of file diff --git a/Modules/vCenter.Alarms/vCenter.Alarms.psm1 b/Modules/vCenter.Alarms/vCenter.Alarms.psm1 new file mode 100644 index 0000000..80d625f --- /dev/null +++ b/Modules/vCenter.Alarms/vCenter.Alarms.psm1 @@ -0,0 +1,736 @@ +<# + =========================================================================== + Created by: Jason Robinson + Created on: 05/2017 + Twitter: @jrob24 + =========================================================================== + .DESCRIPTION + PowerShell Module to help with creation of vCenter Alarms + .NOTES + See New-vCenterAlarms.ps1 for examples of alarm creation + + * Tested against PowerShell 5.0 + * Tested against PowerCLI 6.5.1 build 5377412 + * Tested against vCenter 6.0 + * Tested against ESXi 5.5/6.0 +#> + +function New-AlarmDefinition { +<# + .SYNOPSIS + This cmdlet creates a new alarm defintion on the specified entity in vCenter. + .DESCRIPTION + This cmdlet creates a new alarm defintion on the specified entity in vCenter. + An alarm trigger is required in order to create a new alarm definition. + They can be created by using the New-AlarmTrigger cmdlet. + + After the alarm definition is created, if alarm actions are required use + the cmdlet New-AlarmAction to create actions for the alarm. + .PARAMETER Name + Specifies the name of the alarm you want to create. + .PARAMETER Description + Specifies the description for the alarm. + .PARAMETER Entity + Specifies where to create the alarm. To create the alarm at the root + level of vCenter use the entity 'Datacenters', otherwise specify any + object name. + .PARAMETER Trigger + Specifies the alarm event, state, or metric trigger(s). The alarm + trigger(s) are created with the New-AlarmTrigger cmdlet. For more + information about triggers, run Get-Help New-AlarmTrigger. + .PARAMETER Enabled + Specifies if the alarm is enabled when it is created. If unset, the + default value is true. + .PARAMETER ActionRepeatMinutes + Specifies the frequency how often the actions should repeat when an alarm + does not change state. + .PARAMETER ReportingFrequency + Specifies how often the alarm is triggered, measured in minutes. A zero + value means the alarm is allowed to trigger as often as possible. A + nonzero value means that any subsequent triggers are suppressed for a + period of minutes following a reported trigger. + + If unset, the default value is 0. Allowed range is 0 - 60. + .PARAMETER ToleranceRange + Specifies the tolerance range for the metric triggers, measure in + percentage. A zero value means that the alarm triggers whenever the metric + value is above or below the specified value. A nonzero means that the + alarm triggers only after reaching a certain percentage above or below + the nominal trigger value. + + If unset, the default value is 0. Allowed range is 0 - 100. + .PARAMETER Server + Specifies the vCenter Server system on which you want to run the cmdlet. + If no value is passed to this parameter, the command runs on the default + server, $DefaultVIServer. For more information about default servers, + see the description of Connect-VIServer. + .OUTPUTS + VMware.Vim.ManagedObjectReference + .NOTES + This cmdlet requires a connection to vCenter to create the alarm action. + .LINKS + http://pubs.vmware.com/vsphere-6-0/topic/com.vmware.wssdk.apiref.doc/vim.alarm.AlarmSpec.html + .EXAMPLE + PS C:\> $trigger = New-AlarmTrigger -StateType runtime.connectionState -StateOperator isEqual -YellowStateCondition disconnected -RedStateCondition notResponding -ObjectType HostSystem + PS C:\> New-AlarmDefinition -Name 'Host Connection' -Description 'Host Connection State Alarm -Entity Datacenters -Trigger $trigger -ActionRepeatMinutes 10 + + Type Value + ---- ----- + Alarm alarm-1801 + + This will create a host connection state alarm trigger and store it in + the variable $trigger. Then it will create a new alarm 'Host Connection' + on the root level of vCenter and set the action to repeat every 10 mins. +#> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [Alias('AlarmName')] + [string]$Name, + + [string]$Description, + + [Parameter(Mandatory = $true)] + [string]$Entity, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [VMware.Vim.AlarmExpression[]]$Trigger, + + [boolean]$Enabled = $true, + + [ValidateRange(0, 60)] + [int32]$ActionRepeatMinutes, + + [ValidateRange(0, 60)] + [int32]$ReportingFrequency = 0, + + [ValidateRange(0, 100)] + [int32]$ToleranceRange = 0, + + [string]$Server + ) + BEGIN { + Write-Verbose -Message "Adding parameters with default values to PSBoundParameters" + foreach ($Key in $MyInvocation.MyCommand.Parameters.Keys) { + $Value = Get-Variable $Key -ValueOnly -ErrorAction SilentlyContinue + if ($Value -and !$PSBoundParameters.ContainsKey($Key)) { + $PSBoundParameters[$Key] = $Value + } + } + } + PROCESS { + try { + if ($PSBoundParameters.ContainsKey('Server')) { + $Object = Get-Inventory -Name $PSBoundParameters['Entity'] -ErrorAction Stop -Server $PSBoundParameters['Server'] + $AlarmMgr = Get-View AlarmManager -ErrorAction Stop -Server $PSBoundParameters['Server'] + } else { + $Object = Get-Inventory -Name $PSBoundParameters['Entity'] -ErrorAction Stop -Server $global:DefaultVIServer + $AlarmMgr = Get-View AlarmManager -ErrorAction Stop -Server $global:DefaultVIServer + } + + if ($PSCmdlet.ShouldProcess($global:DefaultVIServer, "Create alarm $($PSBoundParameters['Name'])")) { + $Alarm = New-Object -TypeName VMware.Vim.AlarmSpec + $Alarm.Name = $PSBoundParameters['Name'] + $Alarm.Description = $PSBoundParameters['Description'] + $Alarm.Enabled = $PSBoundParameters['Enabled'] + $Alarm.Expression = New-Object -TypeName VMware.Vim.OrAlarmExpression + $Alarm.Expression.Expression += $PSBoundParameters['Trigger'] + $Alarm.Setting = New-Object -TypeName VMware.Vim.AlarmSetting + $Alarm.Setting.ReportingFrequency = $PSBoundParameters['ReportingFrequency'] * 60 + $Alarm.Setting.ToleranceRange = $PSBoundParameters['ToleranceRange'] * 100 + $Alarm.ActionFrequency = $PSBoundParameters['ActionRepeatMinutes'] * 60 + $AlarmMgr.CreateAlarm($Object.Id, $Alarm) + } + } catch { + $PSCmdlet.ThrowTerminatingError($_) + } + } +} #End of New-AlarmDefinition function + +function New-AlarmAction { +<# + .SYNOPSIS + This cmdlet creates an alarm action on the specified alarm definition. + .DESCRIPTION + This cmdlet creates an alarm action on the specified alarm definition. + This cmdlet differs from the VMware PowerCLI New-AlarmAction cmdlet as it + will create the transitions of the alarm state. It requires an alarm + action and at least one transition to be specified. + + The transition indicates when the action executes and if it repeats. + There are only four acceptable transitions: green to yellow, yellow to + red, red to yellow, and yellow to green. At least one pair must be + specified or the results will be an invalid. + + If an alarm action already exists on the alarm definition, it will be + overwritten if the same alarm action is specified. For example if the + alarm definition already has an alarm action of Snmp on the transition + of green to yellow and the cmdlet is used to create a new action of + Snmp on the transition of yellow to red, it will overwrite the existing + action and transition. The end result will be one Snmp action on the + transition of yellow to red. If you want the old to transition to remain + both should be specified during the usage of the cmdlet. + .PARAMETER AlarmDefinition + Specifies the alarm definition for which you want to configure actions. + The alarm definition can be retreived by using the Get-AlarmDefinition + cmdlet. + .PARAMETER Snmp + Indicates that a SNMP message is sent when the alarm is activated. + .PARAMETER Email + Indicates that when the alarm is activated, the system sends an email + message to the specified address. Use the Subject, To, CC, and Body + parameters to customize the alarm message. + .PARAMETER To + Specifies the email address to which you want to send a message. + .PARAMETER Cc + Specifies the email address you want to add to the CC field of the email + message. + .PARAMETER Subject + Specifies a subject for the email address message you want to send. + .PARAMETER Body + Specifies the text of the email message. + .PARAMETER GreenToYellow + Specifies the alarm action for the green to yellow transition. Allowed + values are 'Once' and 'Repeat'. If parameter is not set transition will + remain unset. + .PARAMETER YellowToRed + Specifies the alarm action for the yellow to red transition. Allowed + values are 'Once' and 'Repeat'. If parameter is not set transition will + remain unset. + .PARAMETER RedToYellow + Specifies the alarm action for the red to yellow transition. Allowed + values are 'Once' and 'Repeat'. If parameter is not set transition will + remain unset. + .PARAMETER YellowToGreen + Specifies the alarm action for the yellow to green transition. Allowed + values are 'Once' and 'Repeat'. If parameter is not set transition will + remain unset. + .NOTES + This cmdlet requires a connection to vCenter to create the alarm action. + + When using this cmdlet specify the Module-Qualified cmdlet name to avoid + using the New-AlarmAction cmdlet with VMware PowerCLI. + .EXAMPLE + PS C:\> vCenter.Alarms\New-AlarmAction -AlarmDefinition (Get-AlarmDefintion "Host CPU Usage") -Snmp -YellowToRed Repeat + + This will create an Snmp alarm action on the "Host CPU Usage" alarm + transition of yellow to red. The alarm action will also repeat, as per + the action frequency defined on the alarm. + .EXAMPLE + PS C:\> Get-AlarmDefintion "Cluster HA Status" | vCenter.Alarms\New-AlarmAction -Email -To helpdesk@company.com -GreenToYellow Once -YellowToRed Once + + This will create an Email alarm action on the "Cluster HA Status" alarm + transition of green to yellow and yellow to red. The alarm action will + send an email to vmwaresupport@wellsfarg.com one time per transition. +#> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low')] + param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [VMware.VimAutomation.ViCore.Types.V1.Alarm.AlarmDefinition]$AlarmDefinition, + + [Parameter(Mandatory = $true, ParameterSetName = 'Snmp')] + [switch]$Snmp, + + [Parameter(Mandatory = $true, ParameterSetName = 'Email')] + [switch]$Email, + + [Parameter(Mandatory = $true, ParameterSetName = 'Email')] + [string[]]$To, + + [Parameter(ParameterSetName = 'Email')] + [string[]]$Cc, + + [Parameter(ParameterSetName = 'Email')] + [string]$Subject, + + [Parameter(ParameterSetName = 'Email')] + [string]$Body, + + [ValidateSet('Once', 'Repeat')] + [string]$GreenToYellow, + + [ValidateSet('Once', 'Repeat')] + [string]$YellowToRed, + + [ValidateSet('Once', 'Repeat')] + [string]$RedToYellow, + + [ValidateSet('Once', 'Repeat')] + [string]$YellowToGreen + ) + + BEGIN { + } + PROCESS { + try { + $AlarmView = Get-View -Id $PSBoundParameters['AlarmDefinition'].Id -Server ($PSBoundParameters['AlarmDefinition'].Uid.Split('@:')[1]) + $Alarm = New-Object -TypeName VMware.Vim.AlarmSpec + $Alarm.Name = $AlarmView.Info.Name + $Alarm.Description = $AlarmView.Info.Description + $Alarm.Enabled = $AlarmView.Info.Enabled + $Alarm.ActionFrequency = $AlarmView.Info.ActionFrequency + $Alarm.Action = New-Object VMware.Vim.GroupAlarmAction + $Trigger = New-Object VMware.Vim.AlarmTriggeringAction + + Write-Verbose -Message "Defining alarm actions" + if ($PSCmdlet.ParameterSetName -eq 'Snmp') { + $Trigger.Action = New-Object -TypeName VMware.Vim.SendSNMPAction + } elseif ($PSCmdlet.ParameterSetName -eq 'Email') { + $Trigger.Action = New-Object -TypeName VMware.Vim.SendEmailAction + $Trigger.Action.ToList = $PSBoundParameters['To'].GetEnumerator() | ForEach-Object -Process { + "$_;" + } + if ($PSBoundParameters.ContainsKey('Cc')) { + $Trigger.Action.CcList = $PSBoundParameters['Cc'].GetEnumerator() | ForEach-Object -Process { + "$_;" + } + } else { + $Trigger.Action.CcList = $null + } + $Trigger.Action.Subject = $PSBoundParameters['Subject'] + $Trigger.Action.Body = $PSBoundParameters['Body'] + } + + Write-Verbose -Message "Defining alarm transitions" + if ($PSBoundParameters.ContainsKey('GreenToYellow')) { + $Trans1 = New-Object -TypeName VMware.Vim.AlarmTriggeringActionTransitionSpec + $Trans1.StartState = 'green' + $Trans1.FinalState = 'yellow' + if ($PSBoundParameters['GreenToYellow'] -eq 'Repeat') { + $Trans1.Repeats = $true + } + $Trigger.TransitionSpecs += $Trans1 + } + + if ($PSBoundParameters.ContainsKey('YellowToRed')) { + $Trans2 = New-Object -TypeName VMware.Vim.AlarmTriggeringActionTransitionSpec + $Trans2.StartState = 'yellow' + $Trans2.FinalState = 'red' + if ($PSBoundParameters['YellowToRed'] -eq 'Repeat') { + $Trans2.Repeats = $true + } else { + $Trans2.Repeats = $false + } + $Trigger.TransitionSpecs += $Trans2 + } + + if ($PSBoundParameters.ContainsKey('RedToYellow')) { + $Trans3 = New-Object -TypeName VMware.Vim.AlarmTriggeringActionTransitionSpec + $Trans3.StartState = 'red' + $Trans3.FinalState = 'yellow' + if ($PSBoundParameters['RedToYellow'] -eq 'Repeat') { + $Trans3.Repeats = $true + } else { + $Trans3.Repeats = $false + } + $Trigger.TransitionSpecs += $Trans3 + } + + if ($PSBoundParameters.ContainsKey('YellowToGreen')) { + $Trans4 = New-Object -TypeName VMware.Vim.AlarmTriggeringActionTransitionSpec + $Trans4.StartState = 'yellow' + $Trans4.FinalState = 'green' + if ($PSBoundParameters['YellowToGreen'] -eq 'Repeat') { + $Trans4.Repeats = $true + } else { + $Trans4.Repeats = $false + } + $Trigger.TransitionSpecs += $Trans4 + } + + $Alarm.Action.Action += $Trigger + $Alarm.Expression = New-Object -TypeName VMware.Vim.OrAlarmExpression + $Alarm.Expression.Expression += $AlarmView.Info.Expression.Expression + $Alarm.Setting += $AlarmView.Info.Setting + $AlarmView.ReconfigureAlarm($Alarm) + } catch { + $PSCmdlet.ThrowTerminatingError($_) + } + } +} #End of New-AlarmAction function + +function New-AlarmTrigger { +<# + .SYNOPSIS + This cmdlet creates a vCenter event, state, or metric alarm trigger. + .DESCRIPTION + This cmdlet creates a vCenter event, state, or metric alarm trigger. + The trigger is used with the New-AlarmDefinition cmdlet to create a new + alarm in vCenter. This cmdlet will only create one alarm trigger. If more + triggers are required store the triggers in an array. + .PARAMETER EventType + Specifies the type of the event to trigger on. The event types can be + discovered by using the Get-EventId cmdlet. If the the event type is + 'EventEx' or 'ExtendedEvent' the EventTypeId parameter is required. + .PARAMETER EventTypeId + Specifies the id of the event type. Only used when the event type is an + 'EventEx' or 'ExtendedEvent'. + .PARAMETER Status + Specifies the status of the event. Allowed values are green, yellow, or + red. + .PARAMETER StateType + Specifies the state type to trigger on. Allowed values are + runtime.powerstate (HostSystem), summary.quickStats.guestHeartbeatStatus + (VirtualMachine), or runtime.connectionState (VirtualMachine). + .PARAMETER StateOperator + Specifies the operator condition on the target state. Allowed values are + 'isEqual' or 'isUnequal'. + .PARAMETER YellowStateCondition + Specifies the yellow state condition. When creating a state alarm + trigger at least one condition must be specified for a valid trigger to + be created. If the parameter is not set, the yellow condition is unset. + .PARAMETER RedStateCondition + Specifies the red state condition. When creating a state alarm trigger + at least one condition must be specified for a valid trigger to be + created. If the parameter is not set, the red condition is unset. + .PARAMETER MetricId + Specifies the id of the metric to trigger on. The metric ids can be + discovered by using the Get-MetricId cmdlet. + .PARAMETER MetricOperator + Specifies the operator condition on the target metric. Allowed values + are 'isAbove' or 'isBelow'. + .PARAMETER Yellow + Specifies the threshold value that triggers a yellow status. Allowed + range is 1% - 100%. + .PARAMETER YellowInterval + Specifies the time interval in minutes for which the yellow condition + must be true before the yellow status is triggered. If unset, the yellow + status is triggered immediately when the yellow condition becomes true. + .PARAMETER Red + Specifies the threshold value that triggers a red status. Allowed range + is 1% - 100%. + .PARAMETER RedInterval + Specifies the time interval in minutes for which the red condition must + be true before the red status is triggered. If unset, the red status is + triggered immediately when the red condition becomes true. + .PARAMETER ObjectType + Specifies the type of object on which the event is logged, the object + type containing the state condition or the type of object containing the + metric. + + When creating a state alarm trigger the only acceptable values are + 'HostSystem' or 'VirtualMachine'. The supported state types for each object + are as follows: + VirtualMachine type: runtime.powerState or summary.quickStats.guestHeartbeatStatus + HostSystem type: runtime.connectionState + .OUTPUTS + (Event|State|Metric)AlarmExpression + .NOTES + This cmdlet requires the PowerCLI module to be imported. + .LINK + Event Alarm Trigger + http://pubs.vmware.com/vsphere-6-0/topic/com.vmware.wssdk.apiref.doc/vim.alarm.EventAlarmExpression.html + + State Alarm Trigger + http://pubs.vmware.com/vsphere-6-0/topic/com.vmware.wssdk.apiref.doc/vim.alarm.StateAlarmExpression.html + + Metric Alarm Trigger + http://pubs.vmware.com/vsphere-6-0/topic/com.vmware.wssdk.apiref.doc/vim.alarm.MetricAlarmExpression.html + .EXAMPLE + PS C:\> New-AlarmTrigger -EventType "DasDisabledEvent" -Status Red -ObjectType ClusterComputeResource + + Comparisons : + EventType : DasDisabledEvent + ObjectType : ClusterComputeResource + Status : red + + Creates an event trigger on 'DasDisabledEvent' (HA Disabled) with a + status on 'Red'. The object type is a ClusterComputerResource because + this event occurs at a cluster level. + .EXAMPLE + PS C:\> New-AlarmTrigger -MetricId (Get-MetricId | Where Name -EQ 'cpu.usage.average').Key -Operator isAbove -Yellow 90 -YellowInterval 30 -Red 98 -RedInterval 15 -ObjectType HostSytem + + Operator : isAbove + Type : HostSytem + Metric : VMware.Vim.PerfMetricId + Yellow : 9000 + YellowInterval : 30 + Red : 9800 + RedInterval : 15 + + Creates a trigger on the 'cpu.usage.average' metric where the warning + condition must be above 90% for 30mins and the alert condition must be + above 98% for 15mins. The object type is a HostSystem. + .EXAMPLE + PS C:\temp> New-AlarmTrigger -StateType runtime.connectionState -StateOperator isEqual -YellowStateCondition Disconnected -RedStateCondition notResponding -ObjectType HostSystem + + Operator : isEqual + Type : HostSystem + StatePath : runtime.connectionState + Yellow : Disconnected + Red : notResponding + + Creates a trigger on the 'runtime.connectionState' condition where the + warning condition is 'disconnected' and the alert condition is + 'notResponding'. The object type is a HostSystem. +#> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low')] + param ( + [Parameter(Mandatory = $true, ParameterSetName = 'Event')] + [string]$EventType, + + [Parameter(ParameterSetName = 'Event')] + [string]$EventTypeId, + + [Parameter(Mandatory = $true, ParameterSetName = 'Event')] + [ValidateSet('Green', 'Yellow', 'Red')] + [string]$Status, + + [Parameter(Mandatory = $true, ParameterSetName = 'State')] + [ValidateSet('runtime.powerState', 'summary.quickStats.guestHeartbeatStatus', 'runtime.connectionState')] + [string]$StateType, + + [Parameter(Mandatory = $true, ParameterSetName = 'State')] + [VMware.Vim.StateAlarmOperator]$StateOperator, + + [Parameter(ParameterSetName = 'State')] + [ValidateSet('disconnected', 'notResponding', 'connected', 'noHeartbeat', 'intermittentHeartbeat', 'poweredOn', 'poweredOff', 'suspended')] + [string]$YellowStateCondition, + + [Parameter(ParameterSetName = 'State')] + [ValidateSet('disconnected', 'notResponding', 'connected', 'noHeartbeat', 'intermittentHeartbeat', 'poweredOn', 'poweredOff', 'suspended')] + [string]$RedStateCondition, + + [Parameter(Mandatory = $true, ParameterSetName = 'Metric')] + [string]$MetricId, + + [Parameter(Mandatory = $true, ParameterSetName = 'Metric')] + [VMware.Vim.MetricAlarmOperator]$MetricOperator, + + [Parameter(ParameterSetName = 'Metric')] + [ValidateRange(1, 100)] + [int32]$Yellow, + + [Parameter(ParameterSetName = 'Metric')] + [ValidateRange(1, 90)] + [int32]$YellowInterval, + + [Parameter(ParameterSetName = 'Metric')] + [ValidateRange(1, 100)] + [int32]$Red, + + [Parameter(ParameterSetName = 'Metric')] + [ValidateRange(1, 90)] + [int32]$RedInterval, + + [Parameter(Mandatory = $true)] + [ValidateSet('ClusterComputeResource', 'Datacenter', 'Datastore', 'DistributedVirtualSwitch', 'HostSystem', 'Network', 'ResourcePool', 'VirtualMachine')] + [string]$ObjectType + ) + try { + if ($PSCmdlet.ShouldProcess("vCenter alarm", "Create $($PSCmdlet.ParameterSetName) trigger")) { + if ($PSCmdlet.ParameterSetName -eq 'Event') { + $Expression = New-Object -TypeName VMware.Vim.EventAlarmExpression + $Expression.EventType = $PSBoundParameters['EventType'] + if ($PSBoundParameters.ContainsKey('EventTypeId')) { + $Expression.EventTypeId = $PSBoundParameters['EventTypeId'] + } + $Expression.ObjectType = $PSBoundParameters['ObjectType'] + $Expression.Status = $PSBoundParameters['Status'] + $Expression + } elseif ($PSCmdlet.ParameterSetName -eq 'Metric') { + $Expression = New-Object -TypeName VMware.Vim.MetricAlarmExpression + $Expression.Metric = New-Object -TypeName VMware.Vim.PerfMetricId + $Expression.Metric.CounterId = $PSBoundParameters['MetricId'] + $Expression.Metric.Instance = "" + $Expression.Operator = $PSBoundParameters['MetricOperator'] + $Expression.Red = ($PSBoundParameters['Red'] * 100) + $Expression.RedInterval = ($PSBoundParameters['RedInterval'] * 60) + $Expression.Yellow = ($PSBoundParameters['Yellow'] * 100) + $Expression.YellowInterval = ($PSBoundParameters['YellowInterval'] * 60) + $Expression.Type = $PSBoundParameters['ObjectType'] + $Expression + } elseif ($PSCmdlet.ParameterSetName -eq 'State') { + $Expression = New-Object -TypeName VMware.Vim.StateAlarmExpression + $Expression.Operator = $PSBoundParameters['StateOperator'] + $Expression.Type = $PSBoundParameters['ObjectType'] + $Expression.StatePath = $PSBoundParameters['StateType'] + + if ($PSBoundParameters.ContainsKey('RedStateCondition')) { + if ($PSBoundParameters['RedStateCondition'] -eq 'intermittentHeartbeat') { + $Expression.Red = 'yellow' + } elseif ($PSBoundParameters['RedStateCondition'] -eq 'noHeartbeat') { + $Expression.Red = 'red' + } else { + $Expression.Red = $PSBoundParameters['RedStateCondition'] + } + } + + if ($PSBoundParameters.ContainsKey('YellowStateCondition')) { + if ($PSBoundParameters['YellowStateCondition'] -eq 'intermittentHeartbeat') { + $Expression.Yellow = 'yellow' + } elseif ($PSBoundParameters['YellowStateCondition'] -eq 'noHeartbeat') { + $Expression.Yellow = 'red' + } else { + $Expression.Yellow = $PSBoundParameters['YellowStateCondition'] + } + } + $Expression + } + } + } catch { + $PSCmdlet.ThrowTerminatingError($_) + } +} #End of New-AlarmTrigger function + +function Get-MetricId { +<# + .SYNOPSIS + This cmdlet collects all of the available metrics from vCenter. + .DESCRIPTION + This cmdlet collects all of the available metrics from vCenter. It will + provide the metric name, key, stats level, and summary of the metric. + The information can be used to identify the available metrics on vCenter + as well as gathering the metric key needed for configuring an alarm. + + The metric keys are unique across vCenters. If you are connected to + more than one vCenter metrics from each vCenter will be generated. A + vCenter property is available to help determine the correct metric key + on a given vCenter. This is extrememly useful when trying to create + a metric based vCenter alarm. + .PARAMETER MetricGroup + Specifies the name of the metric group you would like to see. Allowed + values are 'CPU', 'Mem', 'Disk', 'Net', and 'Datastore'. + .OUTPUTS + System.Management.Automation.PSCustomObject + .NOTES + This cmdlet requires a connection to vCenter to collect metric data. + .EXAMPLE + PS C:\> Get-MetricId -MetricGroup Mem + + Name : mem.usage.none + Key : 23 + Level : 4 + Summary : Memory usage as percentage of total configured or available memory + vCenter : vCenter01 + + Name : mem.usage.average + Key : 24 + Level : 1 + Summary : Memory usage as percentage of total configured or available memory + vCenter : vCenter01 + + Name : mem.usage.minimum + Key : 25 + Level : 4 + Summary : Memory usage as percentage of total configured or available memory + vCenter : vCenter01 + ..... + + Collects all of the available memory metrics on the connected vCenter. +#> + [CmdletBinding()] + param ( + [ValidateSet('CPU', 'Mem', 'Disk', 'Net', 'Datastore')] + [string]$MetricGroup + ) + + foreach ($Mgr in (Get-View PerformanceManager-PerfMgr)) { + $vCenter = $Mgr.Client.ServiceUrl.Split('/')[2] + if ($PSBoundParameters.ContainsKey('MetricGroup')) { + $Metrics += $Mgr.PerfCounter | Where-Object -FilterScript { + $_.GroupInfo.Key -eq $PSBoundParameters['MetricGroup'] + } + } else { + $Metrics += $Mgr.PerfCounter + } + + $Metrics | ForEach-Object -Process { + [pscustomobject] @{ + Name = $_.GroupInfo.Key + "." + $_.NameInfo.key + "." + $_.RollupType + Key = $_.Key + Level = $_.Level + Summary = $_.NameInfo.Summary + vCenter = $vCenter + } + } + } +} #End of Get-MetricId function + +function Get-EventId { +<# + .SYNOPSIS + This cmdlet collects all of the available events from vCenter. + .DESCRIPTION + This cmdlet collects all of the available events from vCenter. It will + provide the event type, event type id (if applicable), category, + description, and summary of the event. The information can be used to + identify the available events on vCenter as well as gathering the event + type and event type id (if applicable) required for configuring an alarm. + + If the event type is 'EventEx' or 'ExtendedEvent' both the event type + and event type id will be required to create a new event based vCenter + alarm. + + The event types can be unique across vCenters. If you are connected to + more than one vCenter events from each vCenter will be generated. A + vCenter property is available to help determine the correct event type + on a given vCenter. This is extrememly useful when trying to create + a event based vCenter alarm. + .PARAMETER Category + Specifies the name of the event category you would like to see. Allowed + values are 'info', 'warning', 'error', and 'user'. + .OUTPUTS + System.Management.Automation.PSCustomObject + .NOTES + This cmdlet requires a connection to vCenter to collect event data. + .EXAMPLE + PS C:\> Get-EventId -Category Error + + EventType : ExtendedEvent + EventTypeId : ad.event.ImportCertFailedEvent + Category : error + Description : Import certificate failure + FullFormat : Import certificate failed. + vCenter : vCenter01 + + EventType : ExtendedEvent + EventTypeId : ad.event.JoinDomainFailedEvent + Category : error + Description : Join domain failure + FullFormat : Join domain failed. + vCenter : vCenter01 + + EventType : ExtendedEvent + EventTypeId : ad.event.LeaveDomainFailedEvent + Category : error + Description : Leave domain failure + FullFormat : Leave domain failed. + vCenter : vCenter01 + ..... +#> + [CmdletBinding()] + param ( + [VMware.Vim.EventCategory]$Category + ) + + foreach ($Mgr in (Get-View EventManager)) { + $vCenter = $Mgr.Client.ServiceUrl.Split('/')[2] + if ($PSBoundParameters.ContainsKey('Category')) { + $Events += $Mgr.Description.EventInfo | Where-Object -FilterScript { + $_.Category -eq $PSBoundParameters['Category'] + } + } else { + $Events += $Mgr.Description.EventInfo + } + + $Events | ForEach-Object -Process { + $Hash = [ordered]@{} + $Hash.Add('EventType', $_.Key) + if ($_.Key -eq 'ExtendedEvent' -or $_.Key -eq 'EventEx') { + $Hash.Add('EventTypeId', $_.FullFormat.Split('|')[0]) + } + $Hash.Add('Category', $_.Category) + $Hash.Add('Description', $_.Description) + if ($Hash['EventType'] -eq 'ExtendedEvent' -or $Hash['EventType'] -eq 'EventEx') { + $Hash.Add('FullFormat', $_.FullFormat.Split('|')[1]) + } else { + $Hash.Add('FullFormat', $_.FullFormat) + } + $Hash.Add('vCenter', $vCenter) + New-Object -TypeName System.Management.Automation.PSObject -Property $Hash + } + } +} #End of Get-EventId function \ No newline at end of file From 16530f256d10f242b1daa0d61b2b5ce32e534ce7 Mon Sep 17 00:00:00 2001 From: jrob24 Date: Thu, 21 Sep 2017 23:31:44 -0500 Subject: [PATCH 072/112] Delete vCenter.Alarms.psm1 --- Modules/vCenter.Alarms/vCenter.Alarms.psm1 | 736 --------------------- 1 file changed, 736 deletions(-) delete mode 100644 Modules/vCenter.Alarms/vCenter.Alarms.psm1 diff --git a/Modules/vCenter.Alarms/vCenter.Alarms.psm1 b/Modules/vCenter.Alarms/vCenter.Alarms.psm1 deleted file mode 100644 index 80d625f..0000000 --- a/Modules/vCenter.Alarms/vCenter.Alarms.psm1 +++ /dev/null @@ -1,736 +0,0 @@ -<# - =========================================================================== - Created by: Jason Robinson - Created on: 05/2017 - Twitter: @jrob24 - =========================================================================== - .DESCRIPTION - PowerShell Module to help with creation of vCenter Alarms - .NOTES - See New-vCenterAlarms.ps1 for examples of alarm creation - - * Tested against PowerShell 5.0 - * Tested against PowerCLI 6.5.1 build 5377412 - * Tested against vCenter 6.0 - * Tested against ESXi 5.5/6.0 -#> - -function New-AlarmDefinition { -<# - .SYNOPSIS - This cmdlet creates a new alarm defintion on the specified entity in vCenter. - .DESCRIPTION - This cmdlet creates a new alarm defintion on the specified entity in vCenter. - An alarm trigger is required in order to create a new alarm definition. - They can be created by using the New-AlarmTrigger cmdlet. - - After the alarm definition is created, if alarm actions are required use - the cmdlet New-AlarmAction to create actions for the alarm. - .PARAMETER Name - Specifies the name of the alarm you want to create. - .PARAMETER Description - Specifies the description for the alarm. - .PARAMETER Entity - Specifies where to create the alarm. To create the alarm at the root - level of vCenter use the entity 'Datacenters', otherwise specify any - object name. - .PARAMETER Trigger - Specifies the alarm event, state, or metric trigger(s). The alarm - trigger(s) are created with the New-AlarmTrigger cmdlet. For more - information about triggers, run Get-Help New-AlarmTrigger. - .PARAMETER Enabled - Specifies if the alarm is enabled when it is created. If unset, the - default value is true. - .PARAMETER ActionRepeatMinutes - Specifies the frequency how often the actions should repeat when an alarm - does not change state. - .PARAMETER ReportingFrequency - Specifies how often the alarm is triggered, measured in minutes. A zero - value means the alarm is allowed to trigger as often as possible. A - nonzero value means that any subsequent triggers are suppressed for a - period of minutes following a reported trigger. - - If unset, the default value is 0. Allowed range is 0 - 60. - .PARAMETER ToleranceRange - Specifies the tolerance range for the metric triggers, measure in - percentage. A zero value means that the alarm triggers whenever the metric - value is above or below the specified value. A nonzero means that the - alarm triggers only after reaching a certain percentage above or below - the nominal trigger value. - - If unset, the default value is 0. Allowed range is 0 - 100. - .PARAMETER Server - Specifies the vCenter Server system on which you want to run the cmdlet. - If no value is passed to this parameter, the command runs on the default - server, $DefaultVIServer. For more information about default servers, - see the description of Connect-VIServer. - .OUTPUTS - VMware.Vim.ManagedObjectReference - .NOTES - This cmdlet requires a connection to vCenter to create the alarm action. - .LINKS - http://pubs.vmware.com/vsphere-6-0/topic/com.vmware.wssdk.apiref.doc/vim.alarm.AlarmSpec.html - .EXAMPLE - PS C:\> $trigger = New-AlarmTrigger -StateType runtime.connectionState -StateOperator isEqual -YellowStateCondition disconnected -RedStateCondition notResponding -ObjectType HostSystem - PS C:\> New-AlarmDefinition -Name 'Host Connection' -Description 'Host Connection State Alarm -Entity Datacenters -Trigger $trigger -ActionRepeatMinutes 10 - - Type Value - ---- ----- - Alarm alarm-1801 - - This will create a host connection state alarm trigger and store it in - the variable $trigger. Then it will create a new alarm 'Host Connection' - on the root level of vCenter and set the action to repeat every 10 mins. -#> - [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] - param ( - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [Alias('AlarmName')] - [string]$Name, - - [string]$Description, - - [Parameter(Mandatory = $true)] - [string]$Entity, - - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [VMware.Vim.AlarmExpression[]]$Trigger, - - [boolean]$Enabled = $true, - - [ValidateRange(0, 60)] - [int32]$ActionRepeatMinutes, - - [ValidateRange(0, 60)] - [int32]$ReportingFrequency = 0, - - [ValidateRange(0, 100)] - [int32]$ToleranceRange = 0, - - [string]$Server - ) - BEGIN { - Write-Verbose -Message "Adding parameters with default values to PSBoundParameters" - foreach ($Key in $MyInvocation.MyCommand.Parameters.Keys) { - $Value = Get-Variable $Key -ValueOnly -ErrorAction SilentlyContinue - if ($Value -and !$PSBoundParameters.ContainsKey($Key)) { - $PSBoundParameters[$Key] = $Value - } - } - } - PROCESS { - try { - if ($PSBoundParameters.ContainsKey('Server')) { - $Object = Get-Inventory -Name $PSBoundParameters['Entity'] -ErrorAction Stop -Server $PSBoundParameters['Server'] - $AlarmMgr = Get-View AlarmManager -ErrorAction Stop -Server $PSBoundParameters['Server'] - } else { - $Object = Get-Inventory -Name $PSBoundParameters['Entity'] -ErrorAction Stop -Server $global:DefaultVIServer - $AlarmMgr = Get-View AlarmManager -ErrorAction Stop -Server $global:DefaultVIServer - } - - if ($PSCmdlet.ShouldProcess($global:DefaultVIServer, "Create alarm $($PSBoundParameters['Name'])")) { - $Alarm = New-Object -TypeName VMware.Vim.AlarmSpec - $Alarm.Name = $PSBoundParameters['Name'] - $Alarm.Description = $PSBoundParameters['Description'] - $Alarm.Enabled = $PSBoundParameters['Enabled'] - $Alarm.Expression = New-Object -TypeName VMware.Vim.OrAlarmExpression - $Alarm.Expression.Expression += $PSBoundParameters['Trigger'] - $Alarm.Setting = New-Object -TypeName VMware.Vim.AlarmSetting - $Alarm.Setting.ReportingFrequency = $PSBoundParameters['ReportingFrequency'] * 60 - $Alarm.Setting.ToleranceRange = $PSBoundParameters['ToleranceRange'] * 100 - $Alarm.ActionFrequency = $PSBoundParameters['ActionRepeatMinutes'] * 60 - $AlarmMgr.CreateAlarm($Object.Id, $Alarm) - } - } catch { - $PSCmdlet.ThrowTerminatingError($_) - } - } -} #End of New-AlarmDefinition function - -function New-AlarmAction { -<# - .SYNOPSIS - This cmdlet creates an alarm action on the specified alarm definition. - .DESCRIPTION - This cmdlet creates an alarm action on the specified alarm definition. - This cmdlet differs from the VMware PowerCLI New-AlarmAction cmdlet as it - will create the transitions of the alarm state. It requires an alarm - action and at least one transition to be specified. - - The transition indicates when the action executes and if it repeats. - There are only four acceptable transitions: green to yellow, yellow to - red, red to yellow, and yellow to green. At least one pair must be - specified or the results will be an invalid. - - If an alarm action already exists on the alarm definition, it will be - overwritten if the same alarm action is specified. For example if the - alarm definition already has an alarm action of Snmp on the transition - of green to yellow and the cmdlet is used to create a new action of - Snmp on the transition of yellow to red, it will overwrite the existing - action and transition. The end result will be one Snmp action on the - transition of yellow to red. If you want the old to transition to remain - both should be specified during the usage of the cmdlet. - .PARAMETER AlarmDefinition - Specifies the alarm definition for which you want to configure actions. - The alarm definition can be retreived by using the Get-AlarmDefinition - cmdlet. - .PARAMETER Snmp - Indicates that a SNMP message is sent when the alarm is activated. - .PARAMETER Email - Indicates that when the alarm is activated, the system sends an email - message to the specified address. Use the Subject, To, CC, and Body - parameters to customize the alarm message. - .PARAMETER To - Specifies the email address to which you want to send a message. - .PARAMETER Cc - Specifies the email address you want to add to the CC field of the email - message. - .PARAMETER Subject - Specifies a subject for the email address message you want to send. - .PARAMETER Body - Specifies the text of the email message. - .PARAMETER GreenToYellow - Specifies the alarm action for the green to yellow transition. Allowed - values are 'Once' and 'Repeat'. If parameter is not set transition will - remain unset. - .PARAMETER YellowToRed - Specifies the alarm action for the yellow to red transition. Allowed - values are 'Once' and 'Repeat'. If parameter is not set transition will - remain unset. - .PARAMETER RedToYellow - Specifies the alarm action for the red to yellow transition. Allowed - values are 'Once' and 'Repeat'. If parameter is not set transition will - remain unset. - .PARAMETER YellowToGreen - Specifies the alarm action for the yellow to green transition. Allowed - values are 'Once' and 'Repeat'. If parameter is not set transition will - remain unset. - .NOTES - This cmdlet requires a connection to vCenter to create the alarm action. - - When using this cmdlet specify the Module-Qualified cmdlet name to avoid - using the New-AlarmAction cmdlet with VMware PowerCLI. - .EXAMPLE - PS C:\> vCenter.Alarms\New-AlarmAction -AlarmDefinition (Get-AlarmDefintion "Host CPU Usage") -Snmp -YellowToRed Repeat - - This will create an Snmp alarm action on the "Host CPU Usage" alarm - transition of yellow to red. The alarm action will also repeat, as per - the action frequency defined on the alarm. - .EXAMPLE - PS C:\> Get-AlarmDefintion "Cluster HA Status" | vCenter.Alarms\New-AlarmAction -Email -To helpdesk@company.com -GreenToYellow Once -YellowToRed Once - - This will create an Email alarm action on the "Cluster HA Status" alarm - transition of green to yellow and yellow to red. The alarm action will - send an email to vmwaresupport@wellsfarg.com one time per transition. -#> - [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low')] - param ( - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] - [VMware.VimAutomation.ViCore.Types.V1.Alarm.AlarmDefinition]$AlarmDefinition, - - [Parameter(Mandatory = $true, ParameterSetName = 'Snmp')] - [switch]$Snmp, - - [Parameter(Mandatory = $true, ParameterSetName = 'Email')] - [switch]$Email, - - [Parameter(Mandatory = $true, ParameterSetName = 'Email')] - [string[]]$To, - - [Parameter(ParameterSetName = 'Email')] - [string[]]$Cc, - - [Parameter(ParameterSetName = 'Email')] - [string]$Subject, - - [Parameter(ParameterSetName = 'Email')] - [string]$Body, - - [ValidateSet('Once', 'Repeat')] - [string]$GreenToYellow, - - [ValidateSet('Once', 'Repeat')] - [string]$YellowToRed, - - [ValidateSet('Once', 'Repeat')] - [string]$RedToYellow, - - [ValidateSet('Once', 'Repeat')] - [string]$YellowToGreen - ) - - BEGIN { - } - PROCESS { - try { - $AlarmView = Get-View -Id $PSBoundParameters['AlarmDefinition'].Id -Server ($PSBoundParameters['AlarmDefinition'].Uid.Split('@:')[1]) - $Alarm = New-Object -TypeName VMware.Vim.AlarmSpec - $Alarm.Name = $AlarmView.Info.Name - $Alarm.Description = $AlarmView.Info.Description - $Alarm.Enabled = $AlarmView.Info.Enabled - $Alarm.ActionFrequency = $AlarmView.Info.ActionFrequency - $Alarm.Action = New-Object VMware.Vim.GroupAlarmAction - $Trigger = New-Object VMware.Vim.AlarmTriggeringAction - - Write-Verbose -Message "Defining alarm actions" - if ($PSCmdlet.ParameterSetName -eq 'Snmp') { - $Trigger.Action = New-Object -TypeName VMware.Vim.SendSNMPAction - } elseif ($PSCmdlet.ParameterSetName -eq 'Email') { - $Trigger.Action = New-Object -TypeName VMware.Vim.SendEmailAction - $Trigger.Action.ToList = $PSBoundParameters['To'].GetEnumerator() | ForEach-Object -Process { - "$_;" - } - if ($PSBoundParameters.ContainsKey('Cc')) { - $Trigger.Action.CcList = $PSBoundParameters['Cc'].GetEnumerator() | ForEach-Object -Process { - "$_;" - } - } else { - $Trigger.Action.CcList = $null - } - $Trigger.Action.Subject = $PSBoundParameters['Subject'] - $Trigger.Action.Body = $PSBoundParameters['Body'] - } - - Write-Verbose -Message "Defining alarm transitions" - if ($PSBoundParameters.ContainsKey('GreenToYellow')) { - $Trans1 = New-Object -TypeName VMware.Vim.AlarmTriggeringActionTransitionSpec - $Trans1.StartState = 'green' - $Trans1.FinalState = 'yellow' - if ($PSBoundParameters['GreenToYellow'] -eq 'Repeat') { - $Trans1.Repeats = $true - } - $Trigger.TransitionSpecs += $Trans1 - } - - if ($PSBoundParameters.ContainsKey('YellowToRed')) { - $Trans2 = New-Object -TypeName VMware.Vim.AlarmTriggeringActionTransitionSpec - $Trans2.StartState = 'yellow' - $Trans2.FinalState = 'red' - if ($PSBoundParameters['YellowToRed'] -eq 'Repeat') { - $Trans2.Repeats = $true - } else { - $Trans2.Repeats = $false - } - $Trigger.TransitionSpecs += $Trans2 - } - - if ($PSBoundParameters.ContainsKey('RedToYellow')) { - $Trans3 = New-Object -TypeName VMware.Vim.AlarmTriggeringActionTransitionSpec - $Trans3.StartState = 'red' - $Trans3.FinalState = 'yellow' - if ($PSBoundParameters['RedToYellow'] -eq 'Repeat') { - $Trans3.Repeats = $true - } else { - $Trans3.Repeats = $false - } - $Trigger.TransitionSpecs += $Trans3 - } - - if ($PSBoundParameters.ContainsKey('YellowToGreen')) { - $Trans4 = New-Object -TypeName VMware.Vim.AlarmTriggeringActionTransitionSpec - $Trans4.StartState = 'yellow' - $Trans4.FinalState = 'green' - if ($PSBoundParameters['YellowToGreen'] -eq 'Repeat') { - $Trans4.Repeats = $true - } else { - $Trans4.Repeats = $false - } - $Trigger.TransitionSpecs += $Trans4 - } - - $Alarm.Action.Action += $Trigger - $Alarm.Expression = New-Object -TypeName VMware.Vim.OrAlarmExpression - $Alarm.Expression.Expression += $AlarmView.Info.Expression.Expression - $Alarm.Setting += $AlarmView.Info.Setting - $AlarmView.ReconfigureAlarm($Alarm) - } catch { - $PSCmdlet.ThrowTerminatingError($_) - } - } -} #End of New-AlarmAction function - -function New-AlarmTrigger { -<# - .SYNOPSIS - This cmdlet creates a vCenter event, state, or metric alarm trigger. - .DESCRIPTION - This cmdlet creates a vCenter event, state, or metric alarm trigger. - The trigger is used with the New-AlarmDefinition cmdlet to create a new - alarm in vCenter. This cmdlet will only create one alarm trigger. If more - triggers are required store the triggers in an array. - .PARAMETER EventType - Specifies the type of the event to trigger on. The event types can be - discovered by using the Get-EventId cmdlet. If the the event type is - 'EventEx' or 'ExtendedEvent' the EventTypeId parameter is required. - .PARAMETER EventTypeId - Specifies the id of the event type. Only used when the event type is an - 'EventEx' or 'ExtendedEvent'. - .PARAMETER Status - Specifies the status of the event. Allowed values are green, yellow, or - red. - .PARAMETER StateType - Specifies the state type to trigger on. Allowed values are - runtime.powerstate (HostSystem), summary.quickStats.guestHeartbeatStatus - (VirtualMachine), or runtime.connectionState (VirtualMachine). - .PARAMETER StateOperator - Specifies the operator condition on the target state. Allowed values are - 'isEqual' or 'isUnequal'. - .PARAMETER YellowStateCondition - Specifies the yellow state condition. When creating a state alarm - trigger at least one condition must be specified for a valid trigger to - be created. If the parameter is not set, the yellow condition is unset. - .PARAMETER RedStateCondition - Specifies the red state condition. When creating a state alarm trigger - at least one condition must be specified for a valid trigger to be - created. If the parameter is not set, the red condition is unset. - .PARAMETER MetricId - Specifies the id of the metric to trigger on. The metric ids can be - discovered by using the Get-MetricId cmdlet. - .PARAMETER MetricOperator - Specifies the operator condition on the target metric. Allowed values - are 'isAbove' or 'isBelow'. - .PARAMETER Yellow - Specifies the threshold value that triggers a yellow status. Allowed - range is 1% - 100%. - .PARAMETER YellowInterval - Specifies the time interval in minutes for which the yellow condition - must be true before the yellow status is triggered. If unset, the yellow - status is triggered immediately when the yellow condition becomes true. - .PARAMETER Red - Specifies the threshold value that triggers a red status. Allowed range - is 1% - 100%. - .PARAMETER RedInterval - Specifies the time interval in minutes for which the red condition must - be true before the red status is triggered. If unset, the red status is - triggered immediately when the red condition becomes true. - .PARAMETER ObjectType - Specifies the type of object on which the event is logged, the object - type containing the state condition or the type of object containing the - metric. - - When creating a state alarm trigger the only acceptable values are - 'HostSystem' or 'VirtualMachine'. The supported state types for each object - are as follows: - VirtualMachine type: runtime.powerState or summary.quickStats.guestHeartbeatStatus - HostSystem type: runtime.connectionState - .OUTPUTS - (Event|State|Metric)AlarmExpression - .NOTES - This cmdlet requires the PowerCLI module to be imported. - .LINK - Event Alarm Trigger - http://pubs.vmware.com/vsphere-6-0/topic/com.vmware.wssdk.apiref.doc/vim.alarm.EventAlarmExpression.html - - State Alarm Trigger - http://pubs.vmware.com/vsphere-6-0/topic/com.vmware.wssdk.apiref.doc/vim.alarm.StateAlarmExpression.html - - Metric Alarm Trigger - http://pubs.vmware.com/vsphere-6-0/topic/com.vmware.wssdk.apiref.doc/vim.alarm.MetricAlarmExpression.html - .EXAMPLE - PS C:\> New-AlarmTrigger -EventType "DasDisabledEvent" -Status Red -ObjectType ClusterComputeResource - - Comparisons : - EventType : DasDisabledEvent - ObjectType : ClusterComputeResource - Status : red - - Creates an event trigger on 'DasDisabledEvent' (HA Disabled) with a - status on 'Red'. The object type is a ClusterComputerResource because - this event occurs at a cluster level. - .EXAMPLE - PS C:\> New-AlarmTrigger -MetricId (Get-MetricId | Where Name -EQ 'cpu.usage.average').Key -Operator isAbove -Yellow 90 -YellowInterval 30 -Red 98 -RedInterval 15 -ObjectType HostSytem - - Operator : isAbove - Type : HostSytem - Metric : VMware.Vim.PerfMetricId - Yellow : 9000 - YellowInterval : 30 - Red : 9800 - RedInterval : 15 - - Creates a trigger on the 'cpu.usage.average' metric where the warning - condition must be above 90% for 30mins and the alert condition must be - above 98% for 15mins. The object type is a HostSystem. - .EXAMPLE - PS C:\temp> New-AlarmTrigger -StateType runtime.connectionState -StateOperator isEqual -YellowStateCondition Disconnected -RedStateCondition notResponding -ObjectType HostSystem - - Operator : isEqual - Type : HostSystem - StatePath : runtime.connectionState - Yellow : Disconnected - Red : notResponding - - Creates a trigger on the 'runtime.connectionState' condition where the - warning condition is 'disconnected' and the alert condition is - 'notResponding'. The object type is a HostSystem. -#> - [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low')] - param ( - [Parameter(Mandatory = $true, ParameterSetName = 'Event')] - [string]$EventType, - - [Parameter(ParameterSetName = 'Event')] - [string]$EventTypeId, - - [Parameter(Mandatory = $true, ParameterSetName = 'Event')] - [ValidateSet('Green', 'Yellow', 'Red')] - [string]$Status, - - [Parameter(Mandatory = $true, ParameterSetName = 'State')] - [ValidateSet('runtime.powerState', 'summary.quickStats.guestHeartbeatStatus', 'runtime.connectionState')] - [string]$StateType, - - [Parameter(Mandatory = $true, ParameterSetName = 'State')] - [VMware.Vim.StateAlarmOperator]$StateOperator, - - [Parameter(ParameterSetName = 'State')] - [ValidateSet('disconnected', 'notResponding', 'connected', 'noHeartbeat', 'intermittentHeartbeat', 'poweredOn', 'poweredOff', 'suspended')] - [string]$YellowStateCondition, - - [Parameter(ParameterSetName = 'State')] - [ValidateSet('disconnected', 'notResponding', 'connected', 'noHeartbeat', 'intermittentHeartbeat', 'poweredOn', 'poweredOff', 'suspended')] - [string]$RedStateCondition, - - [Parameter(Mandatory = $true, ParameterSetName = 'Metric')] - [string]$MetricId, - - [Parameter(Mandatory = $true, ParameterSetName = 'Metric')] - [VMware.Vim.MetricAlarmOperator]$MetricOperator, - - [Parameter(ParameterSetName = 'Metric')] - [ValidateRange(1, 100)] - [int32]$Yellow, - - [Parameter(ParameterSetName = 'Metric')] - [ValidateRange(1, 90)] - [int32]$YellowInterval, - - [Parameter(ParameterSetName = 'Metric')] - [ValidateRange(1, 100)] - [int32]$Red, - - [Parameter(ParameterSetName = 'Metric')] - [ValidateRange(1, 90)] - [int32]$RedInterval, - - [Parameter(Mandatory = $true)] - [ValidateSet('ClusterComputeResource', 'Datacenter', 'Datastore', 'DistributedVirtualSwitch', 'HostSystem', 'Network', 'ResourcePool', 'VirtualMachine')] - [string]$ObjectType - ) - try { - if ($PSCmdlet.ShouldProcess("vCenter alarm", "Create $($PSCmdlet.ParameterSetName) trigger")) { - if ($PSCmdlet.ParameterSetName -eq 'Event') { - $Expression = New-Object -TypeName VMware.Vim.EventAlarmExpression - $Expression.EventType = $PSBoundParameters['EventType'] - if ($PSBoundParameters.ContainsKey('EventTypeId')) { - $Expression.EventTypeId = $PSBoundParameters['EventTypeId'] - } - $Expression.ObjectType = $PSBoundParameters['ObjectType'] - $Expression.Status = $PSBoundParameters['Status'] - $Expression - } elseif ($PSCmdlet.ParameterSetName -eq 'Metric') { - $Expression = New-Object -TypeName VMware.Vim.MetricAlarmExpression - $Expression.Metric = New-Object -TypeName VMware.Vim.PerfMetricId - $Expression.Metric.CounterId = $PSBoundParameters['MetricId'] - $Expression.Metric.Instance = "" - $Expression.Operator = $PSBoundParameters['MetricOperator'] - $Expression.Red = ($PSBoundParameters['Red'] * 100) - $Expression.RedInterval = ($PSBoundParameters['RedInterval'] * 60) - $Expression.Yellow = ($PSBoundParameters['Yellow'] * 100) - $Expression.YellowInterval = ($PSBoundParameters['YellowInterval'] * 60) - $Expression.Type = $PSBoundParameters['ObjectType'] - $Expression - } elseif ($PSCmdlet.ParameterSetName -eq 'State') { - $Expression = New-Object -TypeName VMware.Vim.StateAlarmExpression - $Expression.Operator = $PSBoundParameters['StateOperator'] - $Expression.Type = $PSBoundParameters['ObjectType'] - $Expression.StatePath = $PSBoundParameters['StateType'] - - if ($PSBoundParameters.ContainsKey('RedStateCondition')) { - if ($PSBoundParameters['RedStateCondition'] -eq 'intermittentHeartbeat') { - $Expression.Red = 'yellow' - } elseif ($PSBoundParameters['RedStateCondition'] -eq 'noHeartbeat') { - $Expression.Red = 'red' - } else { - $Expression.Red = $PSBoundParameters['RedStateCondition'] - } - } - - if ($PSBoundParameters.ContainsKey('YellowStateCondition')) { - if ($PSBoundParameters['YellowStateCondition'] -eq 'intermittentHeartbeat') { - $Expression.Yellow = 'yellow' - } elseif ($PSBoundParameters['YellowStateCondition'] -eq 'noHeartbeat') { - $Expression.Yellow = 'red' - } else { - $Expression.Yellow = $PSBoundParameters['YellowStateCondition'] - } - } - $Expression - } - } - } catch { - $PSCmdlet.ThrowTerminatingError($_) - } -} #End of New-AlarmTrigger function - -function Get-MetricId { -<# - .SYNOPSIS - This cmdlet collects all of the available metrics from vCenter. - .DESCRIPTION - This cmdlet collects all of the available metrics from vCenter. It will - provide the metric name, key, stats level, and summary of the metric. - The information can be used to identify the available metrics on vCenter - as well as gathering the metric key needed for configuring an alarm. - - The metric keys are unique across vCenters. If you are connected to - more than one vCenter metrics from each vCenter will be generated. A - vCenter property is available to help determine the correct metric key - on a given vCenter. This is extrememly useful when trying to create - a metric based vCenter alarm. - .PARAMETER MetricGroup - Specifies the name of the metric group you would like to see. Allowed - values are 'CPU', 'Mem', 'Disk', 'Net', and 'Datastore'. - .OUTPUTS - System.Management.Automation.PSCustomObject - .NOTES - This cmdlet requires a connection to vCenter to collect metric data. - .EXAMPLE - PS C:\> Get-MetricId -MetricGroup Mem - - Name : mem.usage.none - Key : 23 - Level : 4 - Summary : Memory usage as percentage of total configured or available memory - vCenter : vCenter01 - - Name : mem.usage.average - Key : 24 - Level : 1 - Summary : Memory usage as percentage of total configured or available memory - vCenter : vCenter01 - - Name : mem.usage.minimum - Key : 25 - Level : 4 - Summary : Memory usage as percentage of total configured or available memory - vCenter : vCenter01 - ..... - - Collects all of the available memory metrics on the connected vCenter. -#> - [CmdletBinding()] - param ( - [ValidateSet('CPU', 'Mem', 'Disk', 'Net', 'Datastore')] - [string]$MetricGroup - ) - - foreach ($Mgr in (Get-View PerformanceManager-PerfMgr)) { - $vCenter = $Mgr.Client.ServiceUrl.Split('/')[2] - if ($PSBoundParameters.ContainsKey('MetricGroup')) { - $Metrics += $Mgr.PerfCounter | Where-Object -FilterScript { - $_.GroupInfo.Key -eq $PSBoundParameters['MetricGroup'] - } - } else { - $Metrics += $Mgr.PerfCounter - } - - $Metrics | ForEach-Object -Process { - [pscustomobject] @{ - Name = $_.GroupInfo.Key + "." + $_.NameInfo.key + "." + $_.RollupType - Key = $_.Key - Level = $_.Level - Summary = $_.NameInfo.Summary - vCenter = $vCenter - } - } - } -} #End of Get-MetricId function - -function Get-EventId { -<# - .SYNOPSIS - This cmdlet collects all of the available events from vCenter. - .DESCRIPTION - This cmdlet collects all of the available events from vCenter. It will - provide the event type, event type id (if applicable), category, - description, and summary of the event. The information can be used to - identify the available events on vCenter as well as gathering the event - type and event type id (if applicable) required for configuring an alarm. - - If the event type is 'EventEx' or 'ExtendedEvent' both the event type - and event type id will be required to create a new event based vCenter - alarm. - - The event types can be unique across vCenters. If you are connected to - more than one vCenter events from each vCenter will be generated. A - vCenter property is available to help determine the correct event type - on a given vCenter. This is extrememly useful when trying to create - a event based vCenter alarm. - .PARAMETER Category - Specifies the name of the event category you would like to see. Allowed - values are 'info', 'warning', 'error', and 'user'. - .OUTPUTS - System.Management.Automation.PSCustomObject - .NOTES - This cmdlet requires a connection to vCenter to collect event data. - .EXAMPLE - PS C:\> Get-EventId -Category Error - - EventType : ExtendedEvent - EventTypeId : ad.event.ImportCertFailedEvent - Category : error - Description : Import certificate failure - FullFormat : Import certificate failed. - vCenter : vCenter01 - - EventType : ExtendedEvent - EventTypeId : ad.event.JoinDomainFailedEvent - Category : error - Description : Join domain failure - FullFormat : Join domain failed. - vCenter : vCenter01 - - EventType : ExtendedEvent - EventTypeId : ad.event.LeaveDomainFailedEvent - Category : error - Description : Leave domain failure - FullFormat : Leave domain failed. - vCenter : vCenter01 - ..... -#> - [CmdletBinding()] - param ( - [VMware.Vim.EventCategory]$Category - ) - - foreach ($Mgr in (Get-View EventManager)) { - $vCenter = $Mgr.Client.ServiceUrl.Split('/')[2] - if ($PSBoundParameters.ContainsKey('Category')) { - $Events += $Mgr.Description.EventInfo | Where-Object -FilterScript { - $_.Category -eq $PSBoundParameters['Category'] - } - } else { - $Events += $Mgr.Description.EventInfo - } - - $Events | ForEach-Object -Process { - $Hash = [ordered]@{} - $Hash.Add('EventType', $_.Key) - if ($_.Key -eq 'ExtendedEvent' -or $_.Key -eq 'EventEx') { - $Hash.Add('EventTypeId', $_.FullFormat.Split('|')[0]) - } - $Hash.Add('Category', $_.Category) - $Hash.Add('Description', $_.Description) - if ($Hash['EventType'] -eq 'ExtendedEvent' -or $Hash['EventType'] -eq 'EventEx') { - $Hash.Add('FullFormat', $_.FullFormat.Split('|')[1]) - } else { - $Hash.Add('FullFormat', $_.FullFormat) - } - $Hash.Add('vCenter', $vCenter) - New-Object -TypeName System.Management.Automation.PSObject -Property $Hash - } - } -} #End of Get-EventId function \ No newline at end of file From 6365ed0bd179574bf9e8f43ceeecb1fec66040a0 Mon Sep 17 00:00:00 2001 From: jrob24 Date: Thu, 21 Sep 2017 23:32:58 -0500 Subject: [PATCH 073/112] Delete New-vCenterAlarms.ps1 --- Modules/vCenter.Alarms/New-vCenterAlarms.ps1 | 79 -------------------- 1 file changed, 79 deletions(-) delete mode 100644 Modules/vCenter.Alarms/New-vCenterAlarms.ps1 diff --git a/Modules/vCenter.Alarms/New-vCenterAlarms.ps1 b/Modules/vCenter.Alarms/New-vCenterAlarms.ps1 deleted file mode 100644 index e3ba5c8..0000000 --- a/Modules/vCenter.Alarms/New-vCenterAlarms.ps1 +++ /dev/null @@ -1,79 +0,0 @@ -<# - - =========================================================================== - Created by: Jason Robinson - Created on: 05/2017 - Twitter: @jrob24 - Filename: New-vCenterAlarms.ps1 - =========================================================================== - .DESCRIPTION - Examples of creating alarms using vCenter.Alarm module -#> - -Import-Module -Name vCenter.Alarms - -Write-Verbose -Message "Example 1 : Creating new Host CPU Usage alarm (Metric based alarm)" -Write-Verbose -Message "Finding the metric id for 'cpu.usage.average'" -$MetricId = (Get-MetricId -MetricGroup CPU | Where-Object -FilterScript { $_.Name -eq 'cpu.usage.average' }).Key -Write-Verbose -Message "Creating an alarm trigger for cpu.usage.average of 90% for 15mins (Warning) & 95% for 10mins (Alert) on the HostSystem object type" -$Trigger = New-AlarmTrigger -MetricId $MetricId -MetricOperator isAbove -ObjectType HostSystem -Yellow 90 -YellowInterval 15 -Red 95 -RedInterval 10 -Write-Verbose -Message "Creates a new alarm called 'Host CPU Usage' at the root level of vCenter" -New-AlarmDefinition -Name "Host CPU Usage" -Description "Alarm on 95%" -Entity Datacenters -Trigger $Trigger -ActionRepeatMinutes 10 -Write-Verbose -Message "Configures the alarm to send snmp traps" -Get-AlarmDefinition -Name "Host CPU Usage" | vSphere.Alarms\New-AlarmAction -Snmp -GreenToYellow Once -YellowToRed Repeat - -Write-Verbose -Message "Example 2 : Creating new HA Disabled alarm (Event based alarm)" -Write-Verbose -Message "Finding the event type for 'HA disabled for cluster'" -$EventType = (Get-EventId | Where-Object -FilterScript { $_.Description -match 'HA disabled for cluster' }).EventType -Write-Verbose -Message "Creating an alarm trigger for 'DasDisabledEvent' on the ClusterComputeResource object type" -$Trigger = New-AlarmTrigger -EventType $EventType -Status Red -ObjectType ClusterComputeResource -Write-Verbose -Message "Creates a new alarm called 'HA Disabled' at the root level of vCenter" -New-AlarmDefinition -Name "HA Disabled" -Description "Alarm on HA" -Entity Datacenters -Trigger $Trigger -ActionRepeatMinutes 30 -Write-Verbose -Message "Configures the alarm to send an email every 30mins" -$EmailParams = @{ - Email = $true - To = 'helpdesk@company.com' - Subject = 'HA Disabled' -} -Get-AlarmDefinition -Name "HA Disabled" | vCenter.Alarms\New-AlarmAction @EmailParams -YellowToRed Repeat - -Write-Verbose -Message "Example 3 : Creating new Host Connection State alarm (State based alarm)" -Write-Verbose -Message "Creating an alarm trigger for StateType of 'runtime.connectionState' on the HostSystem object type" -$Trigger = New-AlarmTrigger -StateType runtime.connectionState -StateOperator isEqual -YellowStateCondition disconnected -RedStateCondition notResponding -ObjectType HostSystem -Write-Verbose -Message "Creates a new alarm called 'Host Connection State' at the root level of vCenter" -New-AlarmDefinition -Name "Host Connection State" -Description "Connection State" -Entity Datacenters -Trigger $Trigger -Write-Verbose -Message "Configures the alarm to send an email once" -$EmailParams = @{ - Email = $true - To = 'helpdesk@company.com' - Subject = 'Host Connection Lost' -} -Get-AlarmDefinition -Name "Host Connection State" | vCenter.Alarms\New-AlarmAction @EmailParams -YellowToRed Once - -Write-Verbose -Message "Example 4 : Creating new Lost Storage Connectivity (Event based alarm)" -Write-Verbose -Message "Find the event type for 'Lost Storage Connectivity'" -Get-EventId | Where-Object -FilterScript { $_.Description -match 'Lost Storage Connectivity' } -Write-Verbose -Message "Two results returned, we want esx not vprob" - <# - EventType : EventEx - EventTypeId : esx.problem.storage.connectivity.lost - Category : error - Description : Lost Storage Connectivity - FullFormat : Lost connectivity to storage device { 1 }. Path { 2 } is down. Affected datastores: { 3 }. - vCenter : vCenter01 - - EventType : EventEx - EventTypeId : vprob.storage.connectivity.lost - Category : error - Description : Lost Storage Connectivity - FullFormat : Lost connectivity to storage device { 1 }. Path { 2 } is down. Affected datastores: { 3 }. - vCenter : vCenter01 - #> -Write-Verbose -Message "Since the event type is EventEx, we need both the EventType & EventTypeId to create the trigger" -$EventType = Get-EventId | Where-Object -FilterScript { $_.EventTypeId -eq 'esx.problem.storage.connectivity.lost' } -Write-Verbose -Message "Creating an alarm trigger for 'DasDisabledEvent' on the ClusterComputeResource object type" -$Trigger = New-AlarmTrigger -EventType $EventType.EventType -EventTypeId $EventType.EventTypeId -Status Red -ObjectType HostSystem -Write-Verbose -Message "Creates a new alarm called 'Lost Storage Connectivity' at the root level of vCenter" -New-AlarmDefinition -Name "Lost Storage Connectivity" -Description "Lost Storage" -Entity Datacenters -Trigger $Trigger -ActionRepeatMinutes 5 -Write-Verbose -Message "Configures the alarm to send an snmp every 5mins" -Get-AlarmDefinition -Name "Lost Storage Connectivity" | vCenter.Alarms\New-AlarmAction -Snmp -YellowToRed Repeat \ No newline at end of file From 76556f26c2699f8b2f98944e14bed05854e2dbe9 Mon Sep 17 00:00:00 2001 From: jrob24 Date: Thu, 21 Sep 2017 23:33:30 -0500 Subject: [PATCH 074/112] Initial commit --- Modules/vCenter.Alarms/New-vCenterAlarms.ps1 | 79 ++ Modules/vCenter.Alarms/vCenter.Alarms.psm1 | 736 +++++++++++++++++++ 2 files changed, 815 insertions(+) create mode 100644 Modules/vCenter.Alarms/New-vCenterAlarms.ps1 create mode 100644 Modules/vCenter.Alarms/vCenter.Alarms.psm1 diff --git a/Modules/vCenter.Alarms/New-vCenterAlarms.ps1 b/Modules/vCenter.Alarms/New-vCenterAlarms.ps1 new file mode 100644 index 0000000..e3ba5c8 --- /dev/null +++ b/Modules/vCenter.Alarms/New-vCenterAlarms.ps1 @@ -0,0 +1,79 @@ +<# + + =========================================================================== + Created by: Jason Robinson + Created on: 05/2017 + Twitter: @jrob24 + Filename: New-vCenterAlarms.ps1 + =========================================================================== + .DESCRIPTION + Examples of creating alarms using vCenter.Alarm module +#> + +Import-Module -Name vCenter.Alarms + +Write-Verbose -Message "Example 1 : Creating new Host CPU Usage alarm (Metric based alarm)" +Write-Verbose -Message "Finding the metric id for 'cpu.usage.average'" +$MetricId = (Get-MetricId -MetricGroup CPU | Where-Object -FilterScript { $_.Name -eq 'cpu.usage.average' }).Key +Write-Verbose -Message "Creating an alarm trigger for cpu.usage.average of 90% for 15mins (Warning) & 95% for 10mins (Alert) on the HostSystem object type" +$Trigger = New-AlarmTrigger -MetricId $MetricId -MetricOperator isAbove -ObjectType HostSystem -Yellow 90 -YellowInterval 15 -Red 95 -RedInterval 10 +Write-Verbose -Message "Creates a new alarm called 'Host CPU Usage' at the root level of vCenter" +New-AlarmDefinition -Name "Host CPU Usage" -Description "Alarm on 95%" -Entity Datacenters -Trigger $Trigger -ActionRepeatMinutes 10 +Write-Verbose -Message "Configures the alarm to send snmp traps" +Get-AlarmDefinition -Name "Host CPU Usage" | vSphere.Alarms\New-AlarmAction -Snmp -GreenToYellow Once -YellowToRed Repeat + +Write-Verbose -Message "Example 2 : Creating new HA Disabled alarm (Event based alarm)" +Write-Verbose -Message "Finding the event type for 'HA disabled for cluster'" +$EventType = (Get-EventId | Where-Object -FilterScript { $_.Description -match 'HA disabled for cluster' }).EventType +Write-Verbose -Message "Creating an alarm trigger for 'DasDisabledEvent' on the ClusterComputeResource object type" +$Trigger = New-AlarmTrigger -EventType $EventType -Status Red -ObjectType ClusterComputeResource +Write-Verbose -Message "Creates a new alarm called 'HA Disabled' at the root level of vCenter" +New-AlarmDefinition -Name "HA Disabled" -Description "Alarm on HA" -Entity Datacenters -Trigger $Trigger -ActionRepeatMinutes 30 +Write-Verbose -Message "Configures the alarm to send an email every 30mins" +$EmailParams = @{ + Email = $true + To = 'helpdesk@company.com' + Subject = 'HA Disabled' +} +Get-AlarmDefinition -Name "HA Disabled" | vCenter.Alarms\New-AlarmAction @EmailParams -YellowToRed Repeat + +Write-Verbose -Message "Example 3 : Creating new Host Connection State alarm (State based alarm)" +Write-Verbose -Message "Creating an alarm trigger for StateType of 'runtime.connectionState' on the HostSystem object type" +$Trigger = New-AlarmTrigger -StateType runtime.connectionState -StateOperator isEqual -YellowStateCondition disconnected -RedStateCondition notResponding -ObjectType HostSystem +Write-Verbose -Message "Creates a new alarm called 'Host Connection State' at the root level of vCenter" +New-AlarmDefinition -Name "Host Connection State" -Description "Connection State" -Entity Datacenters -Trigger $Trigger +Write-Verbose -Message "Configures the alarm to send an email once" +$EmailParams = @{ + Email = $true + To = 'helpdesk@company.com' + Subject = 'Host Connection Lost' +} +Get-AlarmDefinition -Name "Host Connection State" | vCenter.Alarms\New-AlarmAction @EmailParams -YellowToRed Once + +Write-Verbose -Message "Example 4 : Creating new Lost Storage Connectivity (Event based alarm)" +Write-Verbose -Message "Find the event type for 'Lost Storage Connectivity'" +Get-EventId | Where-Object -FilterScript { $_.Description -match 'Lost Storage Connectivity' } +Write-Verbose -Message "Two results returned, we want esx not vprob" + <# + EventType : EventEx + EventTypeId : esx.problem.storage.connectivity.lost + Category : error + Description : Lost Storage Connectivity + FullFormat : Lost connectivity to storage device { 1 }. Path { 2 } is down. Affected datastores: { 3 }. + vCenter : vCenter01 + + EventType : EventEx + EventTypeId : vprob.storage.connectivity.lost + Category : error + Description : Lost Storage Connectivity + FullFormat : Lost connectivity to storage device { 1 }. Path { 2 } is down. Affected datastores: { 3 }. + vCenter : vCenter01 + #> +Write-Verbose -Message "Since the event type is EventEx, we need both the EventType & EventTypeId to create the trigger" +$EventType = Get-EventId | Where-Object -FilterScript { $_.EventTypeId -eq 'esx.problem.storage.connectivity.lost' } +Write-Verbose -Message "Creating an alarm trigger for 'DasDisabledEvent' on the ClusterComputeResource object type" +$Trigger = New-AlarmTrigger -EventType $EventType.EventType -EventTypeId $EventType.EventTypeId -Status Red -ObjectType HostSystem +Write-Verbose -Message "Creates a new alarm called 'Lost Storage Connectivity' at the root level of vCenter" +New-AlarmDefinition -Name "Lost Storage Connectivity" -Description "Lost Storage" -Entity Datacenters -Trigger $Trigger -ActionRepeatMinutes 5 +Write-Verbose -Message "Configures the alarm to send an snmp every 5mins" +Get-AlarmDefinition -Name "Lost Storage Connectivity" | vCenter.Alarms\New-AlarmAction -Snmp -YellowToRed Repeat \ No newline at end of file diff --git a/Modules/vCenter.Alarms/vCenter.Alarms.psm1 b/Modules/vCenter.Alarms/vCenter.Alarms.psm1 new file mode 100644 index 0000000..5f13f3f --- /dev/null +++ b/Modules/vCenter.Alarms/vCenter.Alarms.psm1 @@ -0,0 +1,736 @@ +<# + =========================================================================== + Created by: Jason Robinson + Created on: 05/2017 + Twitter: @jrob24 + =========================================================================== + .DESCRIPTION + PowerShell Module to help with creation of vCenter Alarms + .NOTES + See New-vCenterAlarms.ps1 for examples of alarm creation + + * Tested against PowerShell 5.0 + * Tested against PowerCLI 6.5.1 build 5377412 + * Tested against vCenter 6.0 + * Tested against ESXi 5.5/6.0 +#> + +function New-AlarmDefinition { +<# + .SYNOPSIS + This cmdlet creates a new alarm defintion on the specified entity in vCenter. + .DESCRIPTION + This cmdlet creates a new alarm defintion on the specified entity in vCenter. + An alarm trigger is required in order to create a new alarm definition. + They can be created by using the New-AlarmTrigger cmdlet. + + After the alarm definition is created, if alarm actions are required use + the cmdlet New-AlarmAction to create actions for the alarm. + .PARAMETER Name + Specifies the name of the alarm you want to create. + .PARAMETER Description + Specifies the description for the alarm. + .PARAMETER Entity + Specifies where to create the alarm. To create the alarm at the root + level of vCenter use the entity 'Datacenters', otherwise specify any + object name. + .PARAMETER Trigger + Specifies the alarm event, state, or metric trigger(s). The alarm + trigger(s) are created with the New-AlarmTrigger cmdlet. For more + information about triggers, run Get-Help New-AlarmTrigger. + .PARAMETER Enabled + Specifies if the alarm is enabled when it is created. If unset, the + default value is true. + .PARAMETER ActionRepeatMinutes + Specifies the frequency how often the actions should repeat when an alarm + does not change state. + .PARAMETER ReportingFrequency + Specifies how often the alarm is triggered, measured in minutes. A zero + value means the alarm is allowed to trigger as often as possible. A + nonzero value means that any subsequent triggers are suppressed for a + period of minutes following a reported trigger. + + If unset, the default value is 0. Allowed range is 0 - 60. + .PARAMETER ToleranceRange + Specifies the tolerance range for the metric triggers, measure in + percentage. A zero value means that the alarm triggers whenever the metric + value is above or below the specified value. A nonzero means that the + alarm triggers only after reaching a certain percentage above or below + the nominal trigger value. + + If unset, the default value is 0. Allowed range is 0 - 100. + .PARAMETER Server + Specifies the vCenter Server system on which you want to run the cmdlet. + If no value is passed to this parameter, the command runs on the default + server, $DefaultVIServer. For more information about default servers, + see the description of Connect-VIServer. + .OUTPUTS + VMware.Vim.ManagedObjectReference + .NOTES + This cmdlet requires a connection to vCenter to create the alarm action. + .LINKS + http://pubs.vmware.com/vsphere-6-0/topic/com.vmware.wssdk.apiref.doc/vim.alarm.AlarmSpec.html + .EXAMPLE + PS C:\> $trigger = New-AlarmTrigger -StateType runtime.connectionState -StateOperator isEqual -YellowStateCondition disconnected -RedStateCondition notResponding -ObjectType HostSystem + PS C:\> New-AlarmDefinition -Name 'Host Connection' -Description 'Host Connection State Alarm -Entity Datacenters -Trigger $trigger -ActionRepeatMinutes 10 + + Type Value + ---- ----- + Alarm alarm-1801 + + This will create a host connection state alarm trigger and store it in + the variable $trigger. Then it will create a new alarm 'Host Connection' + on the root level of vCenter and set the action to repeat every 10 mins. +#> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [Alias('AlarmName')] + [string]$Name, + + [string]$Description, + + [Parameter(Mandatory = $true)] + [string]$Entity, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [VMware.Vim.AlarmExpression[]]$Trigger, + + [boolean]$Enabled = $true, + + [ValidateRange(0, 60)] + [int32]$ActionRepeatMinutes, + + [ValidateRange(0, 60)] + [int32]$ReportingFrequency = 0, + + [ValidateRange(0, 100)] + [int32]$ToleranceRange = 0, + + [string]$Server + ) + BEGIN { + Write-Verbose -Message "Adding parameters with default values to PSBoundParameters" + foreach ($Key in $MyInvocation.MyCommand.Parameters.Keys) { + $Value = Get-Variable $Key -ValueOnly -ErrorAction SilentlyContinue + if ($Value -and !$PSBoundParameters.ContainsKey($Key)) { + $PSBoundParameters[$Key] = $Value + } + } + } + PROCESS { + try { + if ($PSBoundParameters.ContainsKey('Server')) { + $Object = Get-Inventory -Name $PSBoundParameters['Entity'] -ErrorAction Stop -Server $PSBoundParameters['Server'] + $AlarmMgr = Get-View AlarmManager -ErrorAction Stop -Server $PSBoundParameters['Server'] + } else { + $Object = Get-Inventory -Name $PSBoundParameters['Entity'] -ErrorAction Stop -Server $global:DefaultVIServer + $AlarmMgr = Get-View AlarmManager -ErrorAction Stop -Server $global:DefaultVIServer + } + + if ($PSCmdlet.ShouldProcess($global:DefaultVIServer, "Create alarm $($PSBoundParameters['Name'])")) { + $Alarm = New-Object -TypeName VMware.Vim.AlarmSpec + $Alarm.Name = $PSBoundParameters['Name'] + $Alarm.Description = $PSBoundParameters['Description'] + $Alarm.Enabled = $PSBoundParameters['Enabled'] + $Alarm.Expression = New-Object -TypeName VMware.Vim.OrAlarmExpression + $Alarm.Expression.Expression += $PSBoundParameters['Trigger'] + $Alarm.Setting = New-Object -TypeName VMware.Vim.AlarmSetting + $Alarm.Setting.ReportingFrequency = $PSBoundParameters['ReportingFrequency'] * 60 + $Alarm.Setting.ToleranceRange = $PSBoundParameters['ToleranceRange'] * 100 + $Alarm.ActionFrequency = $PSBoundParameters['ActionRepeatMinutes'] * 60 + $AlarmMgr.CreateAlarm($Object.Id, $Alarm) + } + } catch { + $PSCmdlet.ThrowTerminatingError($_) + } + } +} #End of New-AlarmDefinition function + +function New-AlarmAction { +<# + .SYNOPSIS + This cmdlet creates an alarm action on the specified alarm definition. + .DESCRIPTION + This cmdlet creates an alarm action on the specified alarm definition. + This cmdlet differs from the VMware PowerCLI New-AlarmAction cmdlet as it + will create the transitions of the alarm state. It requires an alarm + action and at least one transition to be specified. + + The transition indicates when the action executes and if it repeats. + There are only four acceptable transitions: green to yellow, yellow to + red, red to yellow, and yellow to green. At least one pair must be + specified or the results will be an invalid. + + If an alarm action already exists on the alarm definition, it will be + overwritten if the same alarm action is specified. For example if the + alarm definition already has an alarm action of Snmp on the transition + of green to yellow and the cmdlet is used to create a new action of + Snmp on the transition of yellow to red, it will overwrite the existing + action and transition. The end result will be one Snmp action on the + transition of yellow to red. If you want the old to transition to remain + both should be specified during the usage of the cmdlet. + .PARAMETER AlarmDefinition + Specifies the alarm definition for which you want to configure actions. + The alarm definition can be retreived by using the Get-AlarmDefinition + cmdlet. + .PARAMETER Snmp + Indicates that a SNMP message is sent when the alarm is activated. + .PARAMETER Email + Indicates that when the alarm is activated, the system sends an email + message to the specified address. Use the Subject, To, CC, and Body + parameters to customize the alarm message. + .PARAMETER To + Specifies the email address to which you want to send a message. + .PARAMETER Cc + Specifies the email address you want to add to the CC field of the email + message. + .PARAMETER Subject + Specifies a subject for the email address message you want to send. + .PARAMETER Body + Specifies the text of the email message. + .PARAMETER GreenToYellow + Specifies the alarm action for the green to yellow transition. Allowed + values are 'Once' and 'Repeat'. If parameter is not set transition will + remain unset. + .PARAMETER YellowToRed + Specifies the alarm action for the yellow to red transition. Allowed + values are 'Once' and 'Repeat'. If parameter is not set transition will + remain unset. + .PARAMETER RedToYellow + Specifies the alarm action for the red to yellow transition. Allowed + values are 'Once' and 'Repeat'. If parameter is not set transition will + remain unset. + .PARAMETER YellowToGreen + Specifies the alarm action for the yellow to green transition. Allowed + values are 'Once' and 'Repeat'. If parameter is not set transition will + remain unset. + .NOTES + This cmdlet requires a connection to vCenter to create the alarm action. + + When using this cmdlet specify the Module-Qualified cmdlet name to avoid + using the New-AlarmAction cmdlet with VMware PowerCLI. + .EXAMPLE + PS C:\> vCenter.Alarms\New-AlarmAction -AlarmDefinition (Get-AlarmDefintion "Host CPU Usage") -Snmp -YellowToRed Repeat + + This will create an Snmp alarm action on the "Host CPU Usage" alarm + transition of yellow to red. The alarm action will also repeat, as per + the action frequency defined on the alarm. + .EXAMPLE + PS C:\> Get-AlarmDefintion "Cluster HA Status" | vCenter.Alarms\New-AlarmAction -Email -To helpdesk@company.com -GreenToYellow Once -YellowToRed Once + + This will create an Email alarm action on the "Cluster HA Status" alarm + transition of green to yellow and yellow to red. The alarm action will + send an email to helpdesk@company.com one time per transition. +#> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low')] + param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [VMware.VimAutomation.ViCore.Types.V1.Alarm.AlarmDefinition]$AlarmDefinition, + + [Parameter(Mandatory = $true, ParameterSetName = 'Snmp')] + [switch]$Snmp, + + [Parameter(Mandatory = $true, ParameterSetName = 'Email')] + [switch]$Email, + + [Parameter(Mandatory = $true, ParameterSetName = 'Email')] + [string[]]$To, + + [Parameter(ParameterSetName = 'Email')] + [string[]]$Cc, + + [Parameter(ParameterSetName = 'Email')] + [string]$Subject, + + [Parameter(ParameterSetName = 'Email')] + [string]$Body, + + [ValidateSet('Once', 'Repeat')] + [string]$GreenToYellow, + + [ValidateSet('Once', 'Repeat')] + [string]$YellowToRed, + + [ValidateSet('Once', 'Repeat')] + [string]$RedToYellow, + + [ValidateSet('Once', 'Repeat')] + [string]$YellowToGreen + ) + + BEGIN { + } + PROCESS { + try { + $AlarmView = Get-View -Id $PSBoundParameters['AlarmDefinition'].Id -Server ($PSBoundParameters['AlarmDefinition'].Uid.Split('@:')[1]) + $Alarm = New-Object -TypeName VMware.Vim.AlarmSpec + $Alarm.Name = $AlarmView.Info.Name + $Alarm.Description = $AlarmView.Info.Description + $Alarm.Enabled = $AlarmView.Info.Enabled + $Alarm.ActionFrequency = $AlarmView.Info.ActionFrequency + $Alarm.Action = New-Object VMware.Vim.GroupAlarmAction + $Trigger = New-Object VMware.Vim.AlarmTriggeringAction + + Write-Verbose -Message "Defining alarm actions" + if ($PSCmdlet.ParameterSetName -eq 'Snmp') { + $Trigger.Action = New-Object -TypeName VMware.Vim.SendSNMPAction + } elseif ($PSCmdlet.ParameterSetName -eq 'Email') { + $Trigger.Action = New-Object -TypeName VMware.Vim.SendEmailAction + $Trigger.Action.ToList = $PSBoundParameters['To'].GetEnumerator() | ForEach-Object -Process { + "$_;" + } + if ($PSBoundParameters.ContainsKey('Cc')) { + $Trigger.Action.CcList = $PSBoundParameters['Cc'].GetEnumerator() | ForEach-Object -Process { + "$_;" + } + } else { + $Trigger.Action.CcList = $null + } + $Trigger.Action.Subject = $PSBoundParameters['Subject'] + $Trigger.Action.Body = $PSBoundParameters['Body'] + } + + Write-Verbose -Message "Defining alarm transitions" + if ($PSBoundParameters.ContainsKey('GreenToYellow')) { + $Trans1 = New-Object -TypeName VMware.Vim.AlarmTriggeringActionTransitionSpec + $Trans1.StartState = 'green' + $Trans1.FinalState = 'yellow' + if ($PSBoundParameters['GreenToYellow'] -eq 'Repeat') { + $Trans1.Repeats = $true + } + $Trigger.TransitionSpecs += $Trans1 + } + + if ($PSBoundParameters.ContainsKey('YellowToRed')) { + $Trans2 = New-Object -TypeName VMware.Vim.AlarmTriggeringActionTransitionSpec + $Trans2.StartState = 'yellow' + $Trans2.FinalState = 'red' + if ($PSBoundParameters['YellowToRed'] -eq 'Repeat') { + $Trans2.Repeats = $true + } else { + $Trans2.Repeats = $false + } + $Trigger.TransitionSpecs += $Trans2 + } + + if ($PSBoundParameters.ContainsKey('RedToYellow')) { + $Trans3 = New-Object -TypeName VMware.Vim.AlarmTriggeringActionTransitionSpec + $Trans3.StartState = 'red' + $Trans3.FinalState = 'yellow' + if ($PSBoundParameters['RedToYellow'] -eq 'Repeat') { + $Trans3.Repeats = $true + } else { + $Trans3.Repeats = $false + } + $Trigger.TransitionSpecs += $Trans3 + } + + if ($PSBoundParameters.ContainsKey('YellowToGreen')) { + $Trans4 = New-Object -TypeName VMware.Vim.AlarmTriggeringActionTransitionSpec + $Trans4.StartState = 'yellow' + $Trans4.FinalState = 'green' + if ($PSBoundParameters['YellowToGreen'] -eq 'Repeat') { + $Trans4.Repeats = $true + } else { + $Trans4.Repeats = $false + } + $Trigger.TransitionSpecs += $Trans4 + } + + $Alarm.Action.Action += $Trigger + $Alarm.Expression = New-Object -TypeName VMware.Vim.OrAlarmExpression + $Alarm.Expression.Expression += $AlarmView.Info.Expression.Expression + $Alarm.Setting += $AlarmView.Info.Setting + $AlarmView.ReconfigureAlarm($Alarm) + } catch { + $PSCmdlet.ThrowTerminatingError($_) + } + } +} #End of New-AlarmAction function + +function New-AlarmTrigger { +<# + .SYNOPSIS + This cmdlet creates a vCenter event, state, or metric alarm trigger. + .DESCRIPTION + This cmdlet creates a vCenter event, state, or metric alarm trigger. + The trigger is used with the New-AlarmDefinition cmdlet to create a new + alarm in vCenter. This cmdlet will only create one alarm trigger. If more + triggers are required store the triggers in an array. + .PARAMETER EventType + Specifies the type of the event to trigger on. The event types can be + discovered by using the Get-EventId cmdlet. If the the event type is + 'EventEx' or 'ExtendedEvent' the EventTypeId parameter is required. + .PARAMETER EventTypeId + Specifies the id of the event type. Only used when the event type is an + 'EventEx' or 'ExtendedEvent'. + .PARAMETER Status + Specifies the status of the event. Allowed values are green, yellow, or + red. + .PARAMETER StateType + Specifies the state type to trigger on. Allowed values are + runtime.powerstate (HostSystem), summary.quickStats.guestHeartbeatStatus + (VirtualMachine), or runtime.connectionState (VirtualMachine). + .PARAMETER StateOperator + Specifies the operator condition on the target state. Allowed values are + 'isEqual' or 'isUnequal'. + .PARAMETER YellowStateCondition + Specifies the yellow state condition. When creating a state alarm + trigger at least one condition must be specified for a valid trigger to + be created. If the parameter is not set, the yellow condition is unset. + .PARAMETER RedStateCondition + Specifies the red state condition. When creating a state alarm trigger + at least one condition must be specified for a valid trigger to be + created. If the parameter is not set, the red condition is unset. + .PARAMETER MetricId + Specifies the id of the metric to trigger on. The metric ids can be + discovered by using the Get-MetricId cmdlet. + .PARAMETER MetricOperator + Specifies the operator condition on the target metric. Allowed values + are 'isAbove' or 'isBelow'. + .PARAMETER Yellow + Specifies the threshold value that triggers a yellow status. Allowed + range is 1% - 100%. + .PARAMETER YellowInterval + Specifies the time interval in minutes for which the yellow condition + must be true before the yellow status is triggered. If unset, the yellow + status is triggered immediately when the yellow condition becomes true. + .PARAMETER Red + Specifies the threshold value that triggers a red status. Allowed range + is 1% - 100%. + .PARAMETER RedInterval + Specifies the time interval in minutes for which the red condition must + be true before the red status is triggered. If unset, the red status is + triggered immediately when the red condition becomes true. + .PARAMETER ObjectType + Specifies the type of object on which the event is logged, the object + type containing the state condition or the type of object containing the + metric. + + When creating a state alarm trigger the only acceptable values are + 'HostSystem' or 'VirtualMachine'. The supported state types for each object + are as follows: + VirtualMachine type: runtime.powerState or summary.quickStats.guestHeartbeatStatus + HostSystem type: runtime.connectionState + .OUTPUTS + (Event|State|Metric)AlarmExpression + .NOTES + This cmdlet requires the PowerCLI module to be imported. + .LINK + Event Alarm Trigger + http://pubs.vmware.com/vsphere-6-0/topic/com.vmware.wssdk.apiref.doc/vim.alarm.EventAlarmExpression.html + + State Alarm Trigger + http://pubs.vmware.com/vsphere-6-0/topic/com.vmware.wssdk.apiref.doc/vim.alarm.StateAlarmExpression.html + + Metric Alarm Trigger + http://pubs.vmware.com/vsphere-6-0/topic/com.vmware.wssdk.apiref.doc/vim.alarm.MetricAlarmExpression.html + .EXAMPLE + PS C:\> New-AlarmTrigger -EventType "DasDisabledEvent" -Status Red -ObjectType ClusterComputeResource + + Comparisons : + EventType : DasDisabledEvent + ObjectType : ClusterComputeResource + Status : red + + Creates an event trigger on 'DasDisabledEvent' (HA Disabled) with a + status on 'Red'. The object type is a ClusterComputerResource because + this event occurs at a cluster level. + .EXAMPLE + PS C:\> New-AlarmTrigger -MetricId (Get-MetricId | Where Name -EQ 'cpu.usage.average').Key -Operator isAbove -Yellow 90 -YellowInterval 30 -Red 98 -RedInterval 15 -ObjectType HostSytem + + Operator : isAbove + Type : HostSytem + Metric : VMware.Vim.PerfMetricId + Yellow : 9000 + YellowInterval : 30 + Red : 9800 + RedInterval : 15 + + Creates a trigger on the 'cpu.usage.average' metric where the warning + condition must be above 90% for 30mins and the alert condition must be + above 98% for 15mins. The object type is a HostSystem. + .EXAMPLE + PS C:\temp> New-AlarmTrigger -StateType runtime.connectionState -StateOperator isEqual -YellowStateCondition Disconnected -RedStateCondition notResponding -ObjectType HostSystem + + Operator : isEqual + Type : HostSystem + StatePath : runtime.connectionState + Yellow : Disconnected + Red : notResponding + + Creates a trigger on the 'runtime.connectionState' condition where the + warning condition is 'disconnected' and the alert condition is + 'notResponding'. The object type is a HostSystem. +#> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low')] + param ( + [Parameter(Mandatory = $true, ParameterSetName = 'Event')] + [string]$EventType, + + [Parameter(ParameterSetName = 'Event')] + [string]$EventTypeId, + + [Parameter(Mandatory = $true, ParameterSetName = 'Event')] + [ValidateSet('Green', 'Yellow', 'Red')] + [string]$Status, + + [Parameter(Mandatory = $true, ParameterSetName = 'State')] + [ValidateSet('runtime.powerState', 'summary.quickStats.guestHeartbeatStatus', 'runtime.connectionState')] + [string]$StateType, + + [Parameter(Mandatory = $true, ParameterSetName = 'State')] + [VMware.Vim.StateAlarmOperator]$StateOperator, + + [Parameter(ParameterSetName = 'State')] + [ValidateSet('disconnected', 'notResponding', 'connected', 'noHeartbeat', 'intermittentHeartbeat', 'poweredOn', 'poweredOff', 'suspended')] + [string]$YellowStateCondition, + + [Parameter(ParameterSetName = 'State')] + [ValidateSet('disconnected', 'notResponding', 'connected', 'noHeartbeat', 'intermittentHeartbeat', 'poweredOn', 'poweredOff', 'suspended')] + [string]$RedStateCondition, + + [Parameter(Mandatory = $true, ParameterSetName = 'Metric')] + [string]$MetricId, + + [Parameter(Mandatory = $true, ParameterSetName = 'Metric')] + [VMware.Vim.MetricAlarmOperator]$MetricOperator, + + [Parameter(ParameterSetName = 'Metric')] + [ValidateRange(1, 100)] + [int32]$Yellow, + + [Parameter(ParameterSetName = 'Metric')] + [ValidateRange(1, 90)] + [int32]$YellowInterval, + + [Parameter(ParameterSetName = 'Metric')] + [ValidateRange(1, 100)] + [int32]$Red, + + [Parameter(ParameterSetName = 'Metric')] + [ValidateRange(1, 90)] + [int32]$RedInterval, + + [Parameter(Mandatory = $true)] + [ValidateSet('ClusterComputeResource', 'Datacenter', 'Datastore', 'DistributedVirtualSwitch', 'HostSystem', 'Network', 'ResourcePool', 'VirtualMachine')] + [string]$ObjectType + ) + try { + if ($PSCmdlet.ShouldProcess("vCenter alarm", "Create $($PSCmdlet.ParameterSetName) trigger")) { + if ($PSCmdlet.ParameterSetName -eq 'Event') { + $Expression = New-Object -TypeName VMware.Vim.EventAlarmExpression + $Expression.EventType = $PSBoundParameters['EventType'] + if ($PSBoundParameters.ContainsKey('EventTypeId')) { + $Expression.EventTypeId = $PSBoundParameters['EventTypeId'] + } + $Expression.ObjectType = $PSBoundParameters['ObjectType'] + $Expression.Status = $PSBoundParameters['Status'] + $Expression + } elseif ($PSCmdlet.ParameterSetName -eq 'Metric') { + $Expression = New-Object -TypeName VMware.Vim.MetricAlarmExpression + $Expression.Metric = New-Object -TypeName VMware.Vim.PerfMetricId + $Expression.Metric.CounterId = $PSBoundParameters['MetricId'] + $Expression.Metric.Instance = "" + $Expression.Operator = $PSBoundParameters['MetricOperator'] + $Expression.Red = ($PSBoundParameters['Red'] * 100) + $Expression.RedInterval = ($PSBoundParameters['RedInterval'] * 60) + $Expression.Yellow = ($PSBoundParameters['Yellow'] * 100) + $Expression.YellowInterval = ($PSBoundParameters['YellowInterval'] * 60) + $Expression.Type = $PSBoundParameters['ObjectType'] + $Expression + } elseif ($PSCmdlet.ParameterSetName -eq 'State') { + $Expression = New-Object -TypeName VMware.Vim.StateAlarmExpression + $Expression.Operator = $PSBoundParameters['StateOperator'] + $Expression.Type = $PSBoundParameters['ObjectType'] + $Expression.StatePath = $PSBoundParameters['StateType'] + + if ($PSBoundParameters.ContainsKey('RedStateCondition')) { + if ($PSBoundParameters['RedStateCondition'] -eq 'intermittentHeartbeat') { + $Expression.Red = 'yellow' + } elseif ($PSBoundParameters['RedStateCondition'] -eq 'noHeartbeat') { + $Expression.Red = 'red' + } else { + $Expression.Red = $PSBoundParameters['RedStateCondition'] + } + } + + if ($PSBoundParameters.ContainsKey('YellowStateCondition')) { + if ($PSBoundParameters['YellowStateCondition'] -eq 'intermittentHeartbeat') { + $Expression.Yellow = 'yellow' + } elseif ($PSBoundParameters['YellowStateCondition'] -eq 'noHeartbeat') { + $Expression.Yellow = 'red' + } else { + $Expression.Yellow = $PSBoundParameters['YellowStateCondition'] + } + } + $Expression + } + } + } catch { + $PSCmdlet.ThrowTerminatingError($_) + } +} #End of New-AlarmTrigger function + +function Get-MetricId { +<# + .SYNOPSIS + This cmdlet collects all of the available metrics from vCenter. + .DESCRIPTION + This cmdlet collects all of the available metrics from vCenter. It will + provide the metric name, key, stats level, and summary of the metric. + The information can be used to identify the available metrics on vCenter + as well as gathering the metric key needed for configuring an alarm. + + The metric keys are unique across vCenters. If you are connected to + more than one vCenter metrics from each vCenter will be generated. A + vCenter property is available to help determine the correct metric key + on a given vCenter. This is extrememly useful when trying to create + a metric based vCenter alarm. + .PARAMETER MetricGroup + Specifies the name of the metric group you would like to see. Allowed + values are 'CPU', 'Mem', 'Disk', 'Net', and 'Datastore'. + .OUTPUTS + System.Management.Automation.PSCustomObject + .NOTES + This cmdlet requires a connection to vCenter to collect metric data. + .EXAMPLE + PS C:\> Get-MetricId -MetricGroup Mem + + Name : mem.usage.none + Key : 23 + Level : 4 + Summary : Memory usage as percentage of total configured or available memory + vCenter : vCenter01 + + Name : mem.usage.average + Key : 24 + Level : 1 + Summary : Memory usage as percentage of total configured or available memory + vCenter : vCenter01 + + Name : mem.usage.minimum + Key : 25 + Level : 4 + Summary : Memory usage as percentage of total configured or available memory + vCenter : vCenter01 + ..... + + Collects all of the available memory metrics on the connected vCenter. +#> + [CmdletBinding()] + param ( + [ValidateSet('CPU', 'Mem', 'Disk', 'Net', 'Datastore')] + [string]$MetricGroup + ) + + foreach ($Mgr in (Get-View PerformanceManager-PerfMgr)) { + $vCenter = $Mgr.Client.ServiceUrl.Split('/')[2] + if ($PSBoundParameters.ContainsKey('MetricGroup')) { + $Metrics += $Mgr.PerfCounter | Where-Object -FilterScript { + $_.GroupInfo.Key -eq $PSBoundParameters['MetricGroup'] + } + } else { + $Metrics += $Mgr.PerfCounter + } + + $Metrics | ForEach-Object -Process { + [pscustomobject] @{ + Name = $_.GroupInfo.Key + "." + $_.NameInfo.key + "." + $_.RollupType + Key = $_.Key + Level = $_.Level + Summary = $_.NameInfo.Summary + vCenter = $vCenter + } + } + } +} #End of Get-MetricId function + +function Get-EventId { +<# + .SYNOPSIS + This cmdlet collects all of the available events from vCenter. + .DESCRIPTION + This cmdlet collects all of the available events from vCenter. It will + provide the event type, event type id (if applicable), category, + description, and summary of the event. The information can be used to + identify the available events on vCenter as well as gathering the event + type and event type id (if applicable) required for configuring an alarm. + + If the event type is 'EventEx' or 'ExtendedEvent' both the event type + and event type id will be required to create a new event based vCenter + alarm. + + The event types can be unique across vCenters. If you are connected to + more than one vCenter events from each vCenter will be generated. A + vCenter property is available to help determine the correct event type + on a given vCenter. This is extrememly useful when trying to create + a event based vCenter alarm. + .PARAMETER Category + Specifies the name of the event category you would like to see. Allowed + values are 'info', 'warning', 'error', and 'user'. + .OUTPUTS + System.Management.Automation.PSCustomObject + .NOTES + This cmdlet requires a connection to vCenter to collect event data. + .EXAMPLE + PS C:\> Get-EventId -Category Error + + EventType : ExtendedEvent + EventTypeId : ad.event.ImportCertFailedEvent + Category : error + Description : Import certificate failure + FullFormat : Import certificate failed. + vCenter : vCenter01 + + EventType : ExtendedEvent + EventTypeId : ad.event.JoinDomainFailedEvent + Category : error + Description : Join domain failure + FullFormat : Join domain failed. + vCenter : vCenter01 + + EventType : ExtendedEvent + EventTypeId : ad.event.LeaveDomainFailedEvent + Category : error + Description : Leave domain failure + FullFormat : Leave domain failed. + vCenter : vCenter01 + ..... +#> + [CmdletBinding()] + param ( + [VMware.Vim.EventCategory]$Category + ) + + foreach ($Mgr in (Get-View EventManager)) { + $vCenter = $Mgr.Client.ServiceUrl.Split('/')[2] + if ($PSBoundParameters.ContainsKey('Category')) { + $Events += $Mgr.Description.EventInfo | Where-Object -FilterScript { + $_.Category -eq $PSBoundParameters['Category'] + } + } else { + $Events += $Mgr.Description.EventInfo + } + + $Events | ForEach-Object -Process { + $Hash = [ordered]@{} + $Hash.Add('EventType', $_.Key) + if ($_.Key -eq 'ExtendedEvent' -or $_.Key -eq 'EventEx') { + $Hash.Add('EventTypeId', $_.FullFormat.Split('|')[0]) + } + $Hash.Add('Category', $_.Category) + $Hash.Add('Description', $_.Description) + if ($Hash['EventType'] -eq 'ExtendedEvent' -or $Hash['EventType'] -eq 'EventEx') { + $Hash.Add('FullFormat', $_.FullFormat.Split('|')[1]) + } else { + $Hash.Add('FullFormat', $_.FullFormat) + } + $Hash.Add('vCenter', $vCenter) + New-Object -TypeName System.Management.Automation.PSObject -Property $Hash + } + } +} #End of Get-EventId function \ No newline at end of file From 038742d872909068ff1384489d06c0679303f180 Mon Sep 17 00:00:00 2001 From: Chris Date: Tue, 26 Sep 2017 14:53:08 +0100 Subject: [PATCH 075/112] Update VMware.VMEncryption.psm1 Example for Get-Help was using $SetVMEncryptionKey rather than SetVMDiskEncryptionKey ($ sign and other command used) --- Modules/VMware.VMEncryption/VMware.VMEncryption.psm1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/VMware.VMEncryption/VMware.VMEncryption.psm1 b/Modules/VMware.VMEncryption/VMware.VMEncryption.psm1 index 8d19b50..e91b0c5 100644 --- a/Modules/VMware.VMEncryption/VMware.VMEncryption.psm1 +++ b/Modules/VMware.VMEncryption/VMware.VMEncryption.psm1 @@ -1034,7 +1034,7 @@ Function Set-VMDiskEncryptionKey { C:\PS>$KMSCluster = Get-KMSCluster | select -last 1 C:\PS>$VM = Get-VM -Name win2012 C:\PS>$HardDisk = get-vm $vm|Get-HardDisk - C:\PS>$HardDisk|$Set-VMEncryptionKey -VM $VM -KMSClusterId $KMSCluster.Id -Deep + C:\PS>$HardDisk| Set-VMDiskEncryptionKey -VM $VM -KMSClusterId $KMSCluster.Id -Deep Deep rekeys all the disks of the $VM using a new key. The key is generted from the KMS whose clusterId is $KMSCluster.Id. From 677d0c211d0006751b407e233d2f4dac34c6c2a2 Mon Sep 17 00:00:00 2001 From: William Lam Date: Wed, 4 Oct 2017 14:26:05 -0700 Subject: [PATCH 076/112] Adding VMware Fusion Module --- Modules/VMware.Hosted/VMware.Hosted.psd1 | 18 + Modules/VMware.Hosted/VMware.Hosted.psm1 | 501 +++++++++++++++++++++++ 2 files changed, 519 insertions(+) create mode 100644 Modules/VMware.Hosted/VMware.Hosted.psd1 create mode 100644 Modules/VMware.Hosted/VMware.Hosted.psm1 diff --git a/Modules/VMware.Hosted/VMware.Hosted.psd1 b/Modules/VMware.Hosted/VMware.Hosted.psd1 new file mode 100644 index 0000000..59d5b53 --- /dev/null +++ b/Modules/VMware.Hosted/VMware.Hosted.psd1 @@ -0,0 +1,18 @@ +@{ + ModuleToProcess = 'VMware.Hosted.psm1' + ModuleVersion = '1.0.0.0' + GUID = '11393D09-D6B8-4E79-B9BC-247F1BE66683' + Author = 'William Lam' + CompanyName = 'primp-industries.com' + Copyright = '(c) 2017. All rights reserved.' + Description = 'Powershell Module for VMware Fusion 10 REST API' + PowerShellVersion = '5.0' + FunctionsToExport = 'Get-HostedCommand','Connect-HostedServer','Disconnect-HostedServer','Get-HostedVM','Start-HostedVM','Stop-HostedVM','Suspend-HostedVM','Resume-HostedVM','New-HostedVM','Remove-HostedVM','Get-HostedVMSharedFolder','New-HostedVMSharedFolder','Remove-HostedVMSharedFolder','Get-HostedVMNic','Get-HostedNetworks' + PrivateData = @{ + PSData = @{ + Tags = @('Fusion','REST','vmrest') + LicenseUri = 'https://www.tldrlegal.com/l/mit' + ProjectUri = 'https://github.com/lamw/PowerCLI-Example-Scripts/tree/master/Modules/VMware.Hosted' + } + } +} \ No newline at end of file diff --git a/Modules/VMware.Hosted/VMware.Hosted.psm1 b/Modules/VMware.Hosted/VMware.Hosted.psm1 new file mode 100644 index 0000000..4997dee --- /dev/null +++ b/Modules/VMware.Hosted/VMware.Hosted.psm1 @@ -0,0 +1,501 @@ +Function Get-Confirmation { + $choice = "" + while ($choice -notmatch "[y|n]"){ + $choice = read-host "Do you want to continue? (Y/N)" + } + if($choice -ne 'y') { + break + } +} + +Function Connect-HostedServer { + Param ( + [parameter(Mandatory=$true,ValueFromPipeline=$true)][string]$Server, + [parameter(Mandatory=$true,ValueFromPipeline=$true)][string]$Username, + [parameter(Mandatory=$true,ValueFromPipeline=$true)][string]$Password, + [parameter(Mandatory=$false,ValueFromPipeline=$true)][string]$Protocol = "http", + [parameter(Mandatory=$false,ValueFromPipeline=$true)][int]$Port = 8697 + ) + $pair = $Username+":"+$Password + $bytes = [System.Text.Encoding]::ASCII.GetBytes($pair) + $base64 = [System.Convert]::ToBase64String($bytes) + $basicAuthValue = "Basic $base64" + $headers = @{Authorization = $basicAuthValue} + + $Global:DefaultHostedServer = [pscustomobject] @{ + Server=$Protocol + "://" + $server + ":$Port/api"; + Protcol=$Protocol + Headers=$headers + } + + if($DefaultHostedServer.Protcol -eq "https") { + # PowerShell Core has a nice -SkipCertificateCheck but looks like Windows does NOT :( + if($PSVersionTable.PSEdition -eq "Core") { + $Global:fusionCommand = "Invoke-Webrequest -SkipCertificateCheck " + } else { + # Needed for Windows PowerShell to handle HTTPS scenario + # https://stackoverflow.com/a/15627483 + $Provider = New-Object Microsoft.CSharp.CSharpCodeProvider + $Compiler = $Provider.CreateCompiler() + $Params = New-Object System.CodeDom.Compiler.CompilerParameters + $Params.GenerateExecutable = $false + $Params.GenerateInMemory = $true + $Params.IncludeDebugInformation = $false + $Params.ReferencedAssemblies.Add("System.DLL") > $null + $TASource=@' + namespace Local.ToolkitExtensions.Net.CertificatePolicy + { + public class TrustAll : System.Net.ICertificatePolicy + { + public bool CheckValidationResult(System.Net.ServicePoint sp,System.Security.Cryptography.X509Certificates.X509Certificate cert, System.Net.WebRequest req, int problem) + { + return true; + } + } + } +'@ + $TAResults=$Provider.CompileAssemblyFromSource($Params,$TASource) + $TAAssembly=$TAResults.CompiledAssembly + ## We create an instance of TrustAll and attach it to the ServicePointManager + $TrustAll = $TAAssembly.CreateInstance("Local.ToolkitExtensions.Net.CertificatePolicy.TrustAll") + [System.Net.ServicePointManager]::CertificatePolicy = $TrustAll + $Global:fusionCommand = "Invoke-Webrequest " + } + } else { + $Global:fusionCommand = "Invoke-Webrequest " + } + $Global:DefaultHostedServer +} + +Function Disconnect-HostedServer { + Param ( + [parameter(Mandatory=$true,ValueFromPipeline=$true)][string]$Server + ) + + $Global:DefaultHostedServer = $null +} + +Function Get-HostedVM { + Param ( + [parameter(Mandatory=$false,ValueFromPipeline=$true)][string]$Id + ) + + if(!$Global:DefaultHostedServer) { + Write-Host -ForegroundColor Red "You are not connected to Hosted Server, please run Connect-HostedServer" + exit + } + + if($Id) { + $vmUri = $Global:DefaultHostedServer.Server + "/vms/" + $Id + try { + $params = "-Headers `$Global:DefaultHostedServer.Headers -Uri $vmUri -Method GET -ContentType `"application/vnd.vmware.vmw.rest-v1+json`"" + $command = $Global:fusionCommand + $params + $vm = Invoke-Expression -Command $command | ConvertFrom-Json -ErrorAction Stop + } catch { + Write-host -ForegroundColor Red "Invalid VM Id $Id" + break + } + + $vmIPUri = $Global:DefaultHostedServer.Server + "/vms/" + $Id + "/ip" + try { + $params = "-UseBasicParsing -Headers `$Global:DefaultHostedServer.Headers -Uri $vmIPUri -Method GET -ContentType `"application/vnd.vmware.vmw.rest-v1+json`"" + $command = $Global:fusionCommand + $params + $vmIPResults = Invoke-Expression -Command $command | ConvertFrom-Json -ErrorAction Stop + $vmIP = $vmIPResults.ip + } catch { + $vmIP = "N/A" + } + + $vmPowerUri = $Global:DefaultHostedServer.Server + "/vms/" + $Id + "/power" + try { + $params = "-UseBasicParsing -Headers `$Global:DefaultHostedServer.Headers -Uri $vmPowerUri -Method GET -ContentType `"application/vnd.vmware.vmw.rest-v1+json`"" + $command = $Global:fusionCommand + $params + $vmPower = Invoke-Expression -Command $command | ConvertFrom-Json -ErrorAction Stop + } catch { + $vmPower = "N/A" + } + + $results = [pscustomobject] @{ + Id = $vm.Id; + CPU = $vm.Cpu.processors; + Memory = $vm.Memory; + PowerState = $vmPower.power_state; + IPAddress = $vmIP; + } + $results + } else { + $uri = $Global:DefaultHostedServer.Server + "/vms" + $params = "-UseBasicParsing -Headers `$Global:DefaultHostedServer.Headers -Uri $uri -Method GET -ContentType `"application/vnd.vmware.vmw.rest-v1+json`"" + $command = $Global:fusionCommand + $params + try { + Invoke-Expression -Command $command | ConvertFrom-Json -ErrorAction Stop + } catch { + Write-host -ForegroundColor Red "Failed to list VMs" + } + } +} + +Function Start-HostedVM { + Param ( + [parameter(Mandatory=$true,ValueFromPipeline=$true)][string]$Id + ) + + if(!$Global:DefaultHostedServer) { + Write-Host -ForegroundColor Red "You are not connected to Hosted Server, please run Connect-HostedServer" + exit + } + + $vmPowerUri = $Global:DefaultHostedServer.Server + "/vms/" + $Id + "/power" + try { + $params = "-UseBasicParsing -Headers `$Global:DefaultHostedServer.Headers -Uri $vmPowerUri -Method GET -ContentType `"application/vnd.vmware.vmw.rest-v1+json`"" + $command = $Global:fusionCommand + $params + $vmPower = Invoke-Expression -Command $command | ConvertFrom-Json -ErrorAction Stop + } catch { + Write-host -ForegroundColor Red "Invalid VM Id $Id" + break + } + + if($vmPower.power_state -eq "poweredOff" -or $vmPower.power_state -eq "suspended") { + try { + Write-Host "Powering on VM $Id ..." + $params = "-UseBasicParsing -Headers `$Global:DefaultHostedServer.Headers -Uri $vmPowerUri -Method PUT -ContentType `"application/vnd.vmware.vmw.rest-v1+json`" -Body `"on`"" + $command = $Global:fusionCommand + $params + $vm = Invoke-Expression -Command $command | ConvertFrom-Json -ErrorAction Stop + } catch { + Write-host -ForegroundColor Red "Unable to Power On VM $Id" + break + } + } else { + Write-Host "VM $Id is already Powered On" + } +} + +Function Stop-HostedVM { + Param ( + [parameter(Mandatory=$true,ValueFromPipeline=$true)][string]$Id, + [parameter(Mandatory=$false,ValueFromPipeline=$true)][boolean]$Soft, + [parameter(Mandatory=$false,ValueFromPipeline=$true)][boolean]$Confirm = $true + ) + + if(!$Global:DefaultHostedServer) { + Write-Host -ForegroundColor Red "You are not connected to Hosted Server, please run Connect-HostedServer" + exit + } + + if($Confirm) { + Get-Confirmation + } + + $vmPowerUri = $Global:DefaultHostedServer.Server + "/vms/" + $Id + "/power" + try { + $params += "-UseBasicParsing -Headers `$Global:DefaultHostedServer.Headers -Uri $vmPowerUri -Method GET -ContentType `"application/vnd.vmware.vmw.rest-v1+json`"" + $command = $Global:fusionCommand + $params + $vmPower = Invoke-Expression -Command $command | ConvertFrom-Json -ErrorAction Stop + } catch { + Write-host -ForegroundColor Red "Invalid VM Id $Id" + break + } + + if($vmPower.power_state -eq "poweredOn") { + if($Soft) { + try { + Write-Host "Shutting down VM $Id ..." + $params = "-UseBasicParsing -Headers `$Global:DefaultHostedServer.Headers -Uri $vmPowerUri -Method PUT -ContentType `"application/vnd.vmware.vmw.rest-v1+json`" -Body `"shutdown`"" + $command = $Global:fusionCommand + $params + $vm = Invoke-Expression -Command $command | ConvertFrom-Json -ErrorAction Stop + } catch { + Write-host -ForegroundColor Red "Unable to Shutdown VM $Id" + break + } + } else { + try { + Write-Host "Powering off VM $Id ..." + $params = "-UseBasicParsing -Headers `$Global:DefaultHostedServer.Headers -Uri $vmPowerUri -Method PUT -ContentType `"application/vnd.vmware.vmw.rest-v1+json`" -Body `"off`"" + $command = $Global:fusionCommand + $params + $vm = Invoke-Expression -Command $command | ConvertFrom-Json -ErrorAction Stop + } catch { + Write-host -ForegroundColor Red "Unable to Power Off VM $Id" + break + } + } + } else { + Write-Host "VM $Id is already Powered Off" + } +} + +Function Suspend-HostedVM { + Param ( + [parameter(Mandatory=$true,ValueFromPipeline=$true)][string]$Id, + [parameter(Mandatory=$false,ValueFromPipeline=$true)][boolean]$Confirm + ) + + if(!$Global:DefaultHostedServer) { + Write-Host -ForegroundColor Red "You are not connected to Hosted Server, please run Connect-HostedServer" + exit + } + + if($Confirm) { + Get-Confirmation + } + + $vmPowerUri = $Global:DefaultHostedServer.Server + "/vms/" + $Id + "/power" + try { + $params = "-UseBasicParsing -Headers `$Global:DefaultHostedServer.Headers -Uri $vmPowerUri -Method GET -ContentType `"application/vnd.vmware.vmw.rest-v1+json`"" + $command = $Global:fusionCommand + $params + $vmPower = Invoke-Expression -Command $command | ConvertFrom-Json -ErrorAction Stop + } catch { + Write-host -ForegroundColor Red "Invalid VM Id $Id" + break + } + + if($vmPower.power_state -eq "poweredOn") { + try { + Write-Host "Suspending VM $Id ..." + $params = "-UseBasicParsing -Headers `$Global:DefaultHostedServer.Headers -Uri $vmPowerUri -Method PUT -ContentType `"application/vnd.vmware.vmw.rest-v1+json`" -Body `"suspend`"" + $command = $Global:fusionCommand + $params + $vm = Invoke-Expression -Command $command | ConvertFrom-Json -ErrorAction Stop + } catch { + Write-host -ForegroundColor Red "Unable to suspend VM $Id" + break + } + } else { + Write-Host "VM $Id can not be suspended because it is either Powered Off or Suspended" + } +} + +Function Resume-HostedVM { + Param ( + [parameter(Mandatory=$true,ValueFromPipeline=$true)][string]$Id + ) + + if(!$Global:DefaultHostedServer) { + Write-Host -ForegroundColor Red "You are not connected to Hosted Server, please run Connect-HostedServer" + exit + } + + $vmPowerUri = $Global:DefaultHostedServer.Server + "/vms/" + $Id + "/power" + try { + $params = "-UseBasicParsing -Headers `$Global:DefaultHostedServer.Headers -Uri $vmPowerUri -Method GET -ContentType `"application/vnd.vmware.vmw.rest-v1+json`"" + $command = $Global:fusionCommand + $params + $vmPower = Invoke-Expression -Command $command | ConvertFrom-Json -ErrorAction Stop + } catch { + Write-host -ForegroundColor Red "Invalid VM Id $Id" + break + } + + if($vmPower.power_state -eq "suspended") { + try { + Start-HostedVM -Id $Id + } catch { + Write-host -ForegroundColor Red "Unable to Resume VM $Id" + break + } + } else { + Write-Host "VM $Id is not Suspended" + } +} + +Function New-HostedVM { + Param ( + [parameter(Mandatory=$true,ValueFromPipeline=$true)][string]$ParentId, + [parameter(Mandatory=$true,ValueFromPipeline=$true)][string]$Name + ) + + if(!$Global:DefaultHostedServer) { + Write-Host -ForegroundColor Red "You are not connected to Hosted Server, please run Connect-HostedServer" + exit + } + + $vm = Get-HostedVM -Id $ParentId + + if($vm -match "Invalid VM Id") { + Write-host -ForegroundColor Red "Unable to find existing VM Id $ParentId" + break + } + + $vmUri = $Global:DefaultHostedServer.Server + "/vms" + $body = @{"ParentId"="$ParentId";"Name"=$Name} + $body = $body | ConvertTo-Json + + try { + Write-Host "Cloning VM $ParentId to $Name ..." + $params = "-UseBasicParsing -Headers `$Global:DefaultHostedServer.Headers -Uri $vmUri -Method POST -ContentType `"application/vnd.vmware.vmw.rest-v1+json`" -Body `$body" + $command = $Global:fusionCommand + $params + Invoke-Expression -Command $command | ConvertFrom-Json -ErrorAction Stop + } catch { + Write-host -ForegroundColor Red "Failed to Clone VM Id $ParentId" + break + } +} + +Function Remove-HostedVM { + Param ( + [parameter(Mandatory=$true,ValueFromPipeline=$true)][string]$Id, + [parameter(Mandatory=$false,ValueFromPipeline=$true)][boolean]$Confirm = $true + ) + + if(!$Global:DefaultHostedServer) { + Write-Host -ForegroundColor Red "You are not connected to Hosted Server, please run Connect-HostedServer" + exit + } + + $vm = Get-HostedVM -Id $Id + + if($vm -match "Invalid VM Id") { + Write-host -ForegroundColor Red "Unable to find existing VM Id $Id" + break + } + + if($Confirm) { + Get-Confirmation + } + + $vmUri = $Global:DefaultHostedServer.Server + "/vms/" + $Id + try { + Write-Host "Deleting VM Id $Id ..." + $params = "-UseBasicParsing -Headers `$Global:DefaultHostedServer.Headers -Uri $vmUri -Method DELETE -ContentType `"application/vnd.vmware.vmw.rest-v1+json`"" + $command = $Global:fusionCommand + $params + Invoke-Expression -Command $command | ConvertFrom-Json -ErrorAction Stop + } catch { + Write-host -ForegroundColor Red "Failed to Delete VM Id $Id" + break + } +} + +Function Get-HostedVMSharedFolder { + Param ( + [parameter(Mandatory=$true,ValueFromPipeline=$true)][string]$Id + ) + + if(!$Global:DefaultHostedServer) { + Write-Host -ForegroundColor Red "You are not connected to Hosted Server, please run Connect-HostedServer" + exit + } + + $folderUri = $Global:DefaultHostedServer.Server + "/vms/" + $Id + "/sharedfolders" + try { + $params = "-UseBasicParsing -Headers `$Global:DefaultHostedServer.Headers -Uri $folderUri -Method GET -ContentType `"application/vnd.vmware.vmw.rest-v1+json`"" + $command = $Global:fusionCommand + $params + Invoke-Expression -Command $command | ConvertFrom-Json -ErrorAction Stop + } catch { + Write-host -ForegroundColor Red "Invalid VM Id $Id" + break + } +} + +Function New-HostedVMSharedFolder { + Param ( + [parameter(Mandatory=$true,ValueFromPipeline=$true)][string]$Id, + [parameter(Mandatory=$true,ValueFromPipeline=$true)][string]$FolderName, + [parameter(Mandatory=$true,ValueFromPipeline=$true)][string]$HostPath + ) + + if(!$Global:DefaultHostedServer) { + Write-Host -ForegroundColor Red "You are not connected to Hosted Server, please run Connect-HostedServer" + exit + } + + $body = @{"folder_id"="$FolderName";"host_path"=$HostPath;"flags"=4} + $body = $body | ConvertTo-Json + + $folderUri = $Global:DefaultHostedServer.Server + "/vms/" + $Id + "/sharedfolders" + try { + Write-Host "Creating new Shared Folder $FolderName to $HostPath for VM Id $Id ..." + $params += "-UseBasicParsing -Headers `$Global:DefaultHostedServer.Headers -Uri $folderUri -Method POST -ContentType `"application/vnd.vmware.vmw.rest-v1+json`" -Body `$body" + $command = $Global:fusionCommand + $params + Invoke-Expression -Command $command | ConvertFrom-Json -ErrorAction Stop + } catch { + Write-host -ForegroundColor Red "Failed to create Shared Folder for VM Id $Id" + break + } +} + +Function Remove-HostedVMSharedFolder { + Param ( + [parameter(Mandatory=$true,ValueFromPipeline=$true)][string]$Id, + [parameter(Mandatory=$true,ValueFromPipeline=$true)][string]$FolderName, + [parameter(Mandatory=$false,ValueFromPipeline=$true)][boolean]$Confirm = $true + ) + + if(!$Global:DefaultHostedServer) { + Write-Host -ForegroundColor Red "You are not connected to Hosted Server, please run Connect-HostedServer" + exit + } + + if($Confirm) { + Get-Confirmation + } + + $folderUri = $Global:DefaultHostedServer.Server + "/vms/" + $Id + "/sharedfolders/" + $FolderName + try { + Write-Host "Removing Shared Folder $FolderName for VM Id $Id ..." + $params += "-UseBasicParsing -Headers `$Global:DefaultHostedServer.Headers -Uri $folderUri -Method DELETE -ContentType `"application/vnd.vmware.vmw.rest-v1+json`"" + $command = $Global:fusionCommand + $params + Invoke-Expression -Command $command | ConvertFrom-Json -ErrorAction Stop + } catch { + Write-host -ForegroundColor Red "Failed to remove Shared Folder for VM Id $Id" + break + } +} + +Function Get-HostedVMNic { + Param ( + [parameter(Mandatory=$true,ValueFromPipeline=$true)][string]$Id + ) + + if(!$Global:DefaultHostedServer) { + Write-Host -ForegroundColor Red "You are not connected to Hosted Server, please run Connect-HostedServer" + exit + } + + $vmNicUri = $Global:DefaultHostedServer.Server + "/vms/" + $Id + "/nic" + try { + $params += "-UseBasicParsing -Headers `$Global:DefaultHostedServer.Headers -Uri $vmNicUri -Method GET -ContentType `"application/vnd.vmware.vmw.rest-v1+json`"" + $command = $Global:fusionCommand + $params + $vmNics = Invoke-Expression -Command $command | ConvertFrom-Json -ErrorAction Stop + } catch { + Write-host -ForegroundColor Red "Invalid VM Id $Id" + break + } + + $results = @() + foreach ($vmNic in $vmNics.nics) { + $tmp = [pscustomobject] @{ + Index = $vmNic.index; + Type = $vmNic.Type; + VMnet = $vmNic.Vmnet; + } + $results+=$tmp + } + $results +} + +Function Get-HostedNetworks { + if(!$Global:DefaultHostedServer) { + Write-Host -ForegroundColor Red "You are not connected to Hosted Server, please run Connect-HostedServer" + exit + } + + $networksUri = $Global:DefaultHostedServer.Server + "/vmnet" + try { + $params = "-UseBasicParsing -Headers `$Global:DefaultHostedServer.Headers -Uri $networksUri -Method GET -ContentType `"application/vnd.vmware.vmw.rest-v1+json`"" + $command = $Global:fusionCommand + $params + $networks = Invoke-Expression -Command $command | ConvertFrom-Json -ErrorAction Stop + } catch { + Write-host -ForegroundColor Red "Unable to retrieve Networks" + break + } + + $results = @() + foreach ($network in $networks.vmnets) { + $tmp = [pscustomobject] @{ + Name = $network.Name; + Type = $network.Type; + DHCP = $network.Dhcp; + Network = $network.subnet; + Netmask = $network.mask; + } + $results+=$tmp + } + $results +} \ No newline at end of file From 04aabda1bab5e19d7d27f4ccbd61c40d3ef73ca0 Mon Sep 17 00:00:00 2001 From: mtelvers Date: Mon, 9 Oct 2017 12:10:33 +0100 Subject: [PATCH 077/112] Updated Set-HVPool and add function Set-HVGlobalEntitlement Add -globalEntitlement parameter to Set-HVPool to allow pool to be associated with a globalentitlement. Previously this was only possible through the New-HVPool cmdlet. Added cmdlet Set-HVGlobalEntitlement to allow global entitlements to be updated --- .../VMware.Hv.Helper/VMware.HV.Helper.psm1 | 227 +++++++++++++++++- 1 file changed, 224 insertions(+), 3 deletions(-) diff --git a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 index e335ce2..058f979 100644 --- a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 +++ b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 @@ -1,5 +1,5 @@ #Script Module : VMware.Hv.Helper -#Version : 1.1 +#Version : 1.2 #Copyright © 2016 VMware, Inc. All Rights Reserved. @@ -5776,7 +5776,8 @@ function Set-HVPool { .NOTES Author : Praveen Mathamsetty. Author email : pmathamsetty@vmware.com - Version : 1.1 + Version : 1.2 + Updated : Mark Elvers ===Tested Against Environment==== Horizon View Server Version : 7.0.2, 7.1.0 @@ -5818,6 +5819,16 @@ function Set-HVPool { [Parameter(Mandatory = $false)] [string]$Spec, + [Parameter(Mandatory = $false)] + [string] + $globalEntitlement, + + [Parameter(Mandatory = $false)] + [boolean]$allowUsersToChooseProtocol, + + [Parameter(Mandatory = $false)] + [boolean]$enableHTMLAccess, + [Parameter(Mandatory = $false)] $HvServer = $null ) @@ -5906,6 +5917,36 @@ function Set-HVPool { $updates += Get-MapEntry -key 'automatedDesktopData.virtualCenterProvisioningSettings.enableProvisioning' ` -value $false } + + if ($PSBoundParameters.ContainsKey("allowUsersToChooseProtocol")) { + $updates += Get-MapEntry -key 'desktopSettings.displayProtocolSettings.allowUsersToChooseProtocol' -value $allowUsersToChooseProtocol + } + + if ($PSBoundParameters.ContainsKey("enableHTMLAccess")) { + $updates += Get-MapEntry -key 'desktopSettings.displayProtocolSettings.enableHTMLAccess' -value $enableHTMLAccess + } + + $info = $services.PodFederation.PodFederation_get() + if ($globalEntitlement -and ("ENABLED" -eq $info.localPodStatus.status)) { + $QueryFilterEquals = New-Object VMware.Hv.QueryFilterEquals + $QueryFilterEquals.memberName = 'base.displayName' + $QueryFilterEquals.value = $globalEntitlement + $defn = New-Object VMware.Hv.QueryDefinition + $defn.queryEntityType = 'GlobalEntitlementSummaryView' + $defn.Filter = $QueryFilterEquals + $query_service_helper = New-Object VMware.Hv.QueryServiceService + try { + $queryResults = $query_service_helper.QueryService_Query($services,$defn) + $globalEntitlementid = $queryResults.Results.id + if ($globalEntitlementid.length -eq 1) { + $updates += Get-MapEntry -key 'globalEntitlementData.globalEntitlement' -value $globalEntitlementid + } + } + catch { + Write-Host "GlobalEntitlement " $_ + } + } + $desktop_helper = New-Object VMware.Hv.DesktopService foreach ($item in $poolList.Keys) { Write-Host "Updating the Pool: " $poolList.$item @@ -8597,6 +8638,185 @@ function Get-HVGlobalEntitlement { } +function Set-HVGlobalEntitlement { +<# +.SYNOPSIS + Sets the existing pool properties. + +.DESCRIPTION + This cmdlet allows user to edit global entitlements. + +.PARAMETER DisplayName + Display Name of Global Entitlement. + +.PARAMETER Description + Description of Global Entitlement. + +.PARAMETER EnableHTMLAccess + If set to true, the desktops that are associated with this GlobalEntitlement must also have HTML Access enabled. + +.PARAMETER Key + Property names path separated by . (dot) from the root of desktop spec. + +.PARAMETER Value + Property value corresponds to above key name. + +.PARAMETER HvServer + View API service object of Connect-HVServer cmdlet. + +.PARAMETER Spec + Path of the JSON specification file containing key/value pair. + +.EXAMPLE + Set-HVGlobalEntitlement -DisplayName 'MyGlobalEntitlement' -Spec 'C:\Edit-HVPool\EditPool.json' -Confirm:$false + Updates pool configuration by using json file + +.EXAMPLE + Set-HVGlobalEntitlement -DisplayName 'MyGlobalEntitlement' -Key 'base.description' -Value 'update description' + Updates pool configuration with given parameters key and value + +.EXAMPLE + Set-HVGlobalEntitlement -DisplayName 'MyGlobalEntitlement' -enableHTMLAccess $true + Set Allow HTML Access on a global entitlement. Note that it must also be enabled on the Pool and as of 7.3.0 Allow User to Choose Protocol must be enabled (which is unfortunately read-only) + +.EXAMPLE + Get-HVGlobalEntitlement | Set-HVGlobalEntitlement -Disable + Disable all global entitlements + +.OUTPUTS + None + +.NOTES + Author : Mark Elvers + Author email : mark.elvers@tunbury.org + Version : 1.0 + + ===Tested Against Environment==== + Horizon View Server Version : 7.3.0, 7.3.1 + PowerCLI Version : PowerCLI 6.5.1 + PowerShell Version : 5.0 +#> + + [CmdletBinding( + SupportsShouldProcess = $true, + ConfirmImpact = 'High' + )] + + param( + [Parameter(Mandatory = $true,ParameterSetName = 'option')] + [string] $displayName, + + [Parameter(ValueFromPipeline = $true,ParameterSetName = 'pipeline')] + $GlobalEntitlements, + + [Parameter(Mandatory = $false)] + [string]$Key, + + [Parameter(Mandatory = $false)] + $Value, + + [Parameter(Mandatory = $false)] + [string]$Spec, + + [Parameter(Mandatory = $false)] + [switch]$Enable, + + [Parameter(Mandatory = $false)] + [switch]$Disable, + + [Parameter(Mandatory = $false)] + [boolean]$enableHTMLAccess, + + [Parameter(Mandatory = $false)] + $HvServer = $null + ) + + begin { + $services = Get-ViewAPIService -hvServer $hvServer + if ($null -eq $services) { + Write-Error "Could not retrieve ViewApi services from connection object" + break + } + } + + process { + $info = $services.PodFederation.PodFederation_get() + if ("ENABLED" -ne $info.localPodStatus.status) { + Write-Host "Multi-DataCenter-View/CPA is not enabled" + return + } + + $confirmFlag = Get-HVConfirmFlag -keys $PsBoundParameters.Keys + $geList = @{} + if ($displayName) { + try { + $ge = Get-HVGlobalEntitlement -displayName $displayName -suppressInfo $true -hvServer $hvServer + } catch { + Write-Error "Make sure Get-HVGlobalEntitlement advanced function is loaded, $_" + break + } + if ($ge) { + $geList.add($ge.id, $ge.base.DisplayName) + } else { + Write-Error "No globalentitlement found with name: [$displayName]" + break + } + } elseif ($PSCmdlet.MyInvocation.ExpectingInput -or $GlobalEntitlements) { + foreach ($item in $GlobalEntitlements) { + if ($item.GetType().name -eq 'GlobalEntitlementSummaryView') { + $geList.add($item.id, $item.Base.DisplayName) + } else { + Write-Error "In pipeline did not get object of expected type GlobalEntitlementSummaryView" + [System.gc]::collect() + return + } + } + } + + $updates = @() + if ($key -and $value) { + $updates += Get-MapEntry -key $key -value $value + } elseif ($key -or $value) { + Write-Error "Both key:[$key] and value:[$value] needs to be specified" + } + if ($spec) { + try { + $specObject = Get-JsonObject -specFile $spec + } catch { + Write-Error "Json file exception, $_" + return + } + foreach ($member in ($specObject.PSObject.Members | Where-Object { $_.MemberType -eq 'NoteProperty' })) { + $updates += Get-MapEntry -key $member.name -value $member.value + } + } + + if ($Enable) { + $updates += Get-MapEntry -key 'base.enabled' -value $true + } + elseif ($Disable) { + $updates += Get-MapEntry -key 'base.enabled' -value $false + } + + if ($PSBoundParameters.ContainsKey("enableHTMLAccess")) { + $updates += Get-MapEntry -key 'base.enableHTMLAccess' -value $enableHTMLAccess + } + + $ge_helper = New-Object VMware.HV.GlobalEntitlementService + foreach ($item in $geList.Keys) { + Write-Host "Updating the Entitlement: " $geList.$item + if (!$confirmFlag -OR $pscmdlet.ShouldProcess($geList.$item)) { + $ge_helper.GlobalEntitlement_Update($services, $item, $updates) + } + } + } + + end { + [System.gc]::collect() + } +} + + function Remove-HVGlobalEntitlement { <# @@ -9371,4 +9591,5 @@ function Set-HVGlobalSettings { } } -Export-ModuleMember Add-HVDesktop,Add-HVRDSServer,Connect-HVEvent,Disconnect-HVEvent,Get-HVPoolSpec,Get-HVInternalName, Get-HVEvent,Get-HVFarm,Get-HVFarmSummary,Get-HVPool,Get-HVPoolSummary,Get-HVMachine,Get-HVMachineSummary,Get-HVQueryResult,Get-HVQueryFilter,New-HVFarm,New-HVPool,Remove-HVFarm,Remove-HVPool,Set-HVFarm,Set-HVPool,Start-HVFarm,Start-HVPool,New-HVEntitlement,Get-HVEntitlement,Remove-HVEntitlement, Set-HVMachine, New-HVGlobalEntitlement, Remove-HVGlobalEntitlement, Get-HVGlobalEntitlement, Get-HVPodSession, Set-HVApplicationIcon, Remove-HVApplicationIcon, Get-HVGlobalSettings, Set-HVGlobalSettings +Export-ModuleMember Add-HVDesktop,Add-HVRDSServer,Connect-HVEvent,Disconnect-HVEvent,Get-HVPoolSpec,Get-HVInternalName, Get-HVEvent,Get-HVFarm,Get-HVFarmSummary,Get-HVPool,Get-HVPoolSummary,Get-HVMachine,Get-HVMachineSummary,Get-HVQueryResult,Get-HVQueryFilter,New-HVFarm,New-HVPool,Remove-HVFarm,Remove-HVPool,Set-HVFarm,Set-HVPool,Start-HVFarm,Start-HVPool,New-HVEntitlement,Get-HVEntitlement,Remove-HVEntitlement, Set-HVMachine, New-HVGlobalEntitlement, Remove-HVGlobalEntitlement, Get-HVGlobalEntitlement, Get-HVPodSession, Set-HVApplicationIcon, Remove-HVApplicationIcon, Get-HVGlobalSettings, Set-HVGlobalSettings, Set-HVGlobalEntitlement + From accbd83effe97f1a20c2ed2a1412e325947c9335 Mon Sep 17 00:00:00 2001 From: Kyle Ruddy Date: Sat, 14 Oct 2017 15:36:06 -0400 Subject: [PATCH 078/112] Clean Up Moving script resources to the scripting folder. --- get-peakvms.ps1 => Scripts/get-peakvms.ps1 | 0 get-ping.ps1 => Scripts/get-ping.ps1 | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename get-peakvms.ps1 => Scripts/get-peakvms.ps1 (100%) rename get-ping.ps1 => Scripts/get-ping.ps1 (100%) diff --git a/get-peakvms.ps1 b/Scripts/get-peakvms.ps1 similarity index 100% rename from get-peakvms.ps1 rename to Scripts/get-peakvms.ps1 diff --git a/get-ping.ps1 b/Scripts/get-ping.ps1 similarity index 100% rename from get-ping.ps1 rename to Scripts/get-ping.ps1 From 795bcccad815dc26ec2a53c7d3d756e5d6616540 Mon Sep 17 00:00:00 2001 From: Kyle Ruddy Date: Sat, 14 Oct 2017 15:36:58 -0400 Subject: [PATCH 079/112] Get-VMNetworkPortId This script can be used to find the network Port-ID on the lost host. --- Scripts/Get-VMNetworkPortId.ps1 | 56 +++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 Scripts/Get-VMNetworkPortId.ps1 diff --git a/Scripts/Get-VMNetworkPortId.ps1 b/Scripts/Get-VMNetworkPortId.ps1 new file mode 100644 index 0000000..ed130a6 --- /dev/null +++ b/Scripts/Get-VMNetworkPortId.ps1 @@ -0,0 +1,56 @@ +<# +.SYNOPSIS + Finds the local ESXi network Port-ID where a VM is assigned +.DESCRIPTION + Reports back a VM's Port-ID according to the local ESXi host. This correlates to the Port-ID which is displayed via ESXTop +.NOTES + Author: Kyle Ruddy, @kmruddy, thatcouldbeaproblem.com +.PARAMETER vm + The name of the desired VM +.EXAMPLE + PS> .\Get-VMNetworkPortId.ps1 -vm vmname +.EXAMPLE + PS> Get-VM -Name vmname | .\Get-VMNetworkPortId.ps1 +#> +[CmdletBinding(SupportsShouldProcess=$True)] + param( + [Parameter(Mandatory=$true,Position=0,ValueFromPipelineByPropertyName=$true)] + [Alias('Name')] + [String[]]$vm + ) + + Begin { + #Create an array to store output prior to return + $output = @() + + } + + Process { + #Loop through each of the input values + foreach ($v in $vm) { + #Validate the input is a valid VM + $vmobj = Get-VM -Name $v -erroraction silentlycontinue + if (!$vmobj) {Write-Verbose "No VM found by the name $vm."} + else { + #Create a temporary object to store individual ouput + $tempout = "" | select VM,PortId + #Start an ESXCLI session with the host where the VM resides + $esxcli = Get-EsxCli -VMHost $vmobj.VMHost -v2 + #ESXCLI call to obtain information about the VM, specifically its WorldID + $vmNetInfo = $esxcli.network.vm.list.Invoke() | ?{$_.Name -eq $vmobj.Name} + #Create spec to poll the host for the network information of the VM + $portArgs = $esxcli.network.vm.port.list.CreateArgs() + $portArgs.worldid = $vmNetInfo.WorldID + #Output the values to the temporary object + $tempout.VM = $vmobj.Name + $tempout.PortId = $esxcli.network.vm.port.list.Invoke($portArgs).PortId + $output += $tempout + } + } + } + + End { + + return $output + + } From 754e9300ebdf49c417d67de8732f2baa6919804c Mon Sep 17 00:00:00 2001 From: mtelvers Date: Mon, 16 Oct 2017 10:03:30 +0100 Subject: [PATCH 080/112] Updates to New-HVPool Removed parameter -Deleting which isn't used and is a read-only property anyway Corrected automatic Logoff Minutes validation range which is 1.. rather than 1..120 Updated the validation set for delete or refresh machines after log off. From the API, valid options are Never, Delete, Refresh not Never, Delete, After Allowed selecting the AD Container when creating Linked Clones --- Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 index 058f979..25c64a5 100644 --- a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 +++ b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 @@ -3568,10 +3568,6 @@ function New-HVPool { [string[]] $ConnectionServerRestrictions, - #desktopSpec.desktopSettings.deleting - [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] - [boolean]$Deleting = $false, - #desktopSpec.desktopSettings.logoffSettings.powerPloicy [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] [ValidateSet('TAKE_NO_POWER_ACTION', 'ALWAYS_POWERED_ON', 'SUSPEND', 'POWER_OFF')] @@ -3584,7 +3580,7 @@ function New-HVPool { #desktopSpec.desktopSettings.logoffSettings.automaticLogoffMinutes [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] - [ValidateRange(1,120)] + [ValidateRange(1,[int]::MaxValue)] [int]$AutomaticLogoffMinutes = 120, #desktopSpec.desktopSettings.logoffSettings.allowUsersToResetMachines @@ -3597,7 +3593,7 @@ function New-HVPool { #desktopSpec.desktopSettings.logoffSettings.deleteOrRefreshMachineAfterLogoff [Parameter(Mandatory = $false,ParameterSetName = "LINKED_CLONE")] - [ValidateSet('NEVER', 'DELETE', 'AFTER')] + [ValidateSet('NEVER', 'DELETE', 'REFRESH')] [string]$deleteOrRefreshMachineAfterLogoff = 'NEVER', #desktopSpec.desktopSettings.logoffSettings.refreshOsDiskAfterLogoff @@ -3961,8 +3957,9 @@ function New-HVPool { [int] $NumUnassignedMachinesKeptPoweredOn = 1, - #desktopSpec.automatedDesktopSpec.customizationSettings.cloneprepCustomizationSettings.instantCloneEngineDomainAdministrator if INSTANT_CLONE + #desktopSpec.automatedDesktopSpec.customizationSettings.AdContainer [Parameter(Mandatory = $false,ParameterSetName = 'INSTANT_CLONE')] + [Parameter(Mandatory = $false,ParameterSetName = 'LINKED_CLONE')] $AdContainer = 'CN=Computers', [Parameter(Mandatory = $true,ParameterSetName = 'INSTANT_CLONE')] From ba61e6bb0f9349ccd9c610af2ca134e59650d98e Mon Sep 17 00:00:00 2001 From: mtelvers Date: Mon, 16 Oct 2017 19:11:49 +0100 Subject: [PATCH 081/112] Updated Find-HVMachine Updated Find-HVMachine to use the iterative query service, QueryService_Create() rather than the quick QueryService_Query() --- .../VMware.Hv.Helper/VMware.HV.Helper.psm1 | 38 ++++++++++++------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 index 25c64a5..6953824 100644 --- a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 +++ b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 @@ -6794,25 +6794,35 @@ function Find-HVMachine { $andFilter.Filters = $filterset $query.Filter = $andFilter } - $queryResults = $query_service_helper.QueryService_Query($services,$query) - $machineList = $queryResults.results + $machineList = @() + $queryResults = $query_service_helper.QueryService_Create($services, $query) + do { + if ($machineList.length -ne 0) { $queryResults = $query_service_helper.QueryService_GetNext($services, $queryResults.id) } + $machineList += $queryResults.results + } while ($queryResults.remainingCount -gt 0) + $query_service_helper.QueryService_Delete($services, $queryResults.id) } if ($wildcard -or [string]::IsNullOrEmpty($machineList)) { $query.Filter = $null - $queryResults = $query_service_helper.QueryService_Query($services,$query) - $strFilterSet = @() - foreach ($setting in $machineSelectors.Keys) { - if ($null -ne $params[$setting]) { - if ($wildcard -and (($setting -eq 'MachineName') -or ($setting -eq 'DnsName')) ) { - $strFilterSet += '($_.' + $machineSelectors[$setting] + ' -like "' + $params[$setting] + '")' - } else { - $strFilterSet += '($_.' + $machineSelectors[$setting] + ' -eq "' + $params[$setting] + '")' + $machineList = @() + $queryResults = $query_service_helper.QueryService_Create($services,$query) + do { + if ($machineList.length -ne 0) { $queryResults = $query_service_helper.QueryService_GetNext($services, $queryResults.id) } + $strFilterSet = @() + foreach ($setting in $machineSelectors.Keys) { + if ($null -ne $params[$setting]) { + if ($wildcard -and (($setting -eq 'MachineName') -or ($setting -eq 'DnsName')) ) { + $strFilterSet += '($_.' + $machineSelectors[$setting] + ' -like "' + $params[$setting] + '")' + } else { + $strFilterSet += '($_.' + $machineSelectors[$setting] + ' -eq "' + $params[$setting] + '")' + } } } - } - $whereClause = [string]::Join(' -and ', $strFilterSet) - $scriptBlock = [Scriptblock]::Create($whereClause) - $machineList = $queryResults.results | where $scriptBlock + $whereClause = [string]::Join(' -and ', $strFilterSet) + $scriptBlock = [Scriptblock]::Create($whereClause) + $machineList += $queryResults.results | where $scriptBlock + } while ($queryResults.remainingCount -gt 0) + $query_service_helper.QueryService_Delete($services, $queryResults.id) } return $machineList } From 1edf24432c3d4257351641d07b40afa4a4050bd7 Mon Sep 17 00:00:00 2001 From: mtelvers Date: Wed, 18 Oct 2017 08:54:42 +0100 Subject: [PATCH 082/112] Improved searching for Host and Clusters, Resource Pools and Access Groups Add three new internal functions Get-HVHostOrClusterID, Get-HVResourcePoolID and Get-HVAccessGroupID to search the tree results of HostOrCluster_GetHostOrClusterTree(), ResourcePool_GetResourcePoolTree() and AccessGroup_List() respectively. Previously only the top level of the result tree from these calls where considered which was raised as issue #88. The solutions provided in the discussions were point fixes as they changed the level of the tree which was searched which missed matches at the other and the original levels. --- .../VMware.Hv.Helper/VMware.HV.Helper.psm1 | 162 +++++++++++++++--- 1 file changed, 138 insertions(+), 24 deletions(-) diff --git a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 index 6953824..dd7a66c 100644 --- a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 +++ b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 @@ -2646,8 +2646,7 @@ function New-HVFarm { $farmData = $farmSpecObj.data $AccessGroup_service_helper = New-Object VMware.Hv.AccessGroupService - $ag = $AccessGroup_service_helper.AccessGroup_List($services) | Where-Object { $_.base.name -eq $accessGroup } - $farmData.AccessGroup = $ag.id + $farmData.AccessGroup = Get-HVAccessGroupID $AccessGroup_service_helper.AccessGroup_List($services) $farmData.name = $farmName $farmData.DisplayName = $farmDisplayName @@ -2851,21 +2850,17 @@ function Get-HVFarmProvisioningData { } if ($hostOrCluster) { $HostOrCluster_service_helper = New-Object VMware.Hv.HostOrClusterService - $hostClusterList = ($HostOrCluster_service_helper.HostOrCluster_GetHostOrClusterTree($services, $vmobject.datacenter)).treeContainer.children.info - $HostClusterObj = $hostClusterList | Where-Object { $_.name -eq $hostOrCluster } - if ($null -eq $HostClusterObj) { - throw "No host or cluster found with name: [$hostOrCluster]" + $vmObject.HostOrCluster = Get-HVHostOrClusterID $HostOrCluster_service_helper.HostOrCluster_GetHostOrClusterTree($services,$vmobject.datacenter) + if ($null -eq $vmObject.HostOrCluster) { + throw "No hostOrCluster found with Name: [$hostOrCluster]" } - $vmObject.HostOrCluster = $HostClusterObj.id } if ($resourcePool) { $ResourcePool_service_helper = New-Object VMware.Hv.ResourcePoolService - $resourcePoolList = $ResourcePool_service_helper.ResourcePool_GetResourcePoolTree($services, $vmobject.HostOrCluster) - $resourcePoolObj = $resourcePoolList | Where-Object { $_.resourcepooldata.name -eq $resourcePool } - if ($null -eq $resourcePoolObj) { - throw "No resource pool found with name: [$resourcePool]" + $vmObject.ResourcePool = Get-HVResourcePoolID $ResourcePool_service_helper.ResourcePool_GetResourcePoolTree($services,$vmobject.HostOrCluster) + if ($null -eq $vmObject.ResourcePool) { + throw "No Resource Pool found with Name: [$resourcePool]" } - $vmObject.ResourcePool = $resourcePoolObj.id } return $vmObject } @@ -4620,8 +4615,7 @@ function New-HVPool { } if (!$desktopBase) { $accessGroup_client = New-Object VMware.Hv.AccessGroupService - $ag = $accessGroup_client.AccessGroup_List($services) | Where-Object { $_.base.name -eq $accessGroup } - $desktopSpecObj.base.AccessGroup = $ag.id + $desktopSpecObj.base.AccessGroup = Get-HVAccessGroupID $accessGroup_client.AccessGroup_List($services) } else { $desktopSpecObj.base = $desktopBase } @@ -4811,25 +4805,146 @@ function Get-HVPoolProvisioningData { } if ($hostOrCluster) { $vmFolder_helper = New-Object VMware.Hv.HostOrClusterService - $hostClusterList = ($vmFolder_helper.HostOrCluster_GetHostOrClusterTree($services,$vmobject.datacenter)).treeContainer.children.info - $hostClusterObj = $hostClusterList | Where-Object { ($_.path -eq $hostOrCluster) -or ($_.name -eq $hostOrCluster) } - if ($null -eq $hostClusterObj) { + $vmObject.HostOrCluster = Get-HVHostOrClusterID $vmFolder_helper.HostOrCluster_GetHostOrClusterTree($services,$vmobject.datacenter) + if ($null -eq $vmObject.HostOrCluster) { throw "No hostOrCluster found with Name: [$hostOrCluster]" } - $vmObject.HostOrCluster = $hostClusterObj.id } if ($resourcePool) { $resourcePool_helper = New-Object VMware.Hv.ResourcePoolService - $resourcePoolList = $resourcePool_helper.ResourcePool_GetResourcePoolTree($services,$vmobject.HostOrCluster) - $resourcePoolObj = $resourcePoolList | Where-Object { ($_.resourcepooldata.path -eq $resourcePool) -or ($_.resourcepooldata.name -eq $resourcePool) } - if ($null -eq $resourcePoolObj) { - throw "No hostOrCluster found with Name: [$resourcePool]" + $vmObject.ResourcePool = Get-HVResourcePoolID $resourcePool_helper.ResourcePool_GetResourcePoolTree($services,$vmobject.HostOrCluster) + if ($null -eq $vmObject.ResourcePool) { + throw "No Resource Pool found with Name: [$resourcePool]" } - $vmObject.ResourcePool = $resourcePoolObj.id } return $vmObject } + +function Get-HVHostOrClusterID { +<# +.Synopsis + Recursive search for a Host or Cluster name within the results tree from HostOrCluster_GetHostOrClusterTree() and returns the ID + +.NOTES + HostOrCluster_GetHostOrClusterTree() returns a HostOrClusterTreeNode as below + + HostOrClusterTreeNode.container $true if this is a container + HostOrClusterTreeNode.treecontainer HostOrClusterTreeContainer + HostOrClusterTreeNode.treecontainer.name Container name + HostOrClusterTreeNode.treecontainer.path Path to this container + HostOrClusterTreeNode.treecontainer.type DATACENTER, FOLDER or OTHER + HostOrClusterTreeNode.treecontainer.children HostOrClusterTreeNode[] list of child nodes with potentially more child nodes + HostOrClusterTreeNode.info HostOrClusterInfo + HostOrClusterTreeNode.info.id Host or cluster ID + HostOrClusterTreeNode.info.cluster Is this a cluster + HostOrClusterTreeNode.info.name Host or cluster name + HostOrClusterTreeNode.info.path Path to host or cluster name + HostOrClusterTreeNode.info.virtualCenter + HostOrClusterTreeNode.info.datacenter + HostOrClusterTreeNode.info.vGPUTypes + HostOrClusterTreeNode.info.incompatibileReasons + + Author : Mark Elvers +#> + param( + [Parameter(Mandatory = $true)] + [VMware.Hv.HostOrClusterTreeNode]$hoctn + ) + if ($hoctn.container) { + foreach ($node in $hoctn.treeContainer.children) { + $id = Get-HVHostOrClusterID $node + if ($id -ne $null) { + return $id + } + } + } else { + if ($hoctn.info.path -eq $hostOrCluster -or $hoctn.info.name -eq $hostOrCluster) { + return $hoctn.info.id + } + } + return $null +} + +function Get-HVResourcePoolID { +<# +.Synopsis + Recursive search for a Resource Pool within the results tree from ResourcePool_GetResourcePoolTree() and returns the ID + +.NOTES + ResourcePool_GetResourcePoolTree() returns ResourcePoolInfo as below + + ResourcePoolInfo.id Resource pool ID + ResourcePoolInfo.resourcePoolData + ResourcePoolInfo.resourcePoolData.name Resource pool name + ResourcePoolInfo.resourcePoolData.path Resource pool path + ResourcePoolInfo.resourcePoolData.type HOST_OR_CLUSTER, RESOURCE_POOL or OTHER + ResourcePoolInfo.children ResourcePoolInfo[] list of child nodes with potentially further child nodes + + Author : Mark Elvers +#> + param( + [Parameter(Mandatory = $true)] + [VMware.Hv.ResourcePoolInfo]$rpi + ) + if ($rpi.resourcePoolData.path -eq $resourcePool -or $rpi.resourcePoolData.name -eq $resourcePool) { + return $rpi.id + } + foreach ($child in $rpi.children) { + $id = Get-HVResourcePoolID $child + if ($id -ne $null) { + return $id + } + } + return $null +} + +function Get-HVAccessGroupID { +<# +.Synopsis + Recursive search for an Acess Group within the results tree from AccessGroup_List() and returns the ID + +.NOTES + AccessGroup_List() returns AccessGroupInfo[] (a list of structures) + + Iterate through the list of structures + AccessGroupInfo.id Access Group ID + AccessGroupInfo.base + AccessGroupInfo.base.name Access Group name + AccessGroupInfo.base.description Access Group description + AccessGroupInfo.base.parent Access Group parent ID + AccessGroupInfo.data + AccessGroupInfo.data.permissions PermissionID[] + AccessGroupInfo.children AccessGroupInfo[] list of child nodes with potentially further child nodes + + I couldn't create a child node of a child node via the Horizon View Administrator GUI, but the this code allows that if it occurs + Furthermore, unless you are using the Root access group you must iterate over the children + + Root -\ + +- Access Group 1 + +- Access Group 2 + \- Access Group 3 + + Author : Mark Elvers +#> + param( + [Parameter(Mandatory = $true)] + [VMware.Hv.AccessGroupInfo[]]$agi + ) + foreach ($element in $agi) { + if ($element.base.name -eq $accessGroup) { + return $element.id + } + foreach ($child in $element.children) { + $id = Get-HVAccessGroupID $child + if ($id -ne $null) { + return $id + } + } + } + return $null +} + function Get-HVPoolStorageObject { param( [Parameter(Mandatory = $true)] @@ -9599,4 +9714,3 @@ function Set-HVGlobalSettings { } Export-ModuleMember Add-HVDesktop,Add-HVRDSServer,Connect-HVEvent,Disconnect-HVEvent,Get-HVPoolSpec,Get-HVInternalName, Get-HVEvent,Get-HVFarm,Get-HVFarmSummary,Get-HVPool,Get-HVPoolSummary,Get-HVMachine,Get-HVMachineSummary,Get-HVQueryResult,Get-HVQueryFilter,New-HVFarm,New-HVPool,Remove-HVFarm,Remove-HVPool,Set-HVFarm,Set-HVPool,Start-HVFarm,Start-HVPool,New-HVEntitlement,Get-HVEntitlement,Remove-HVEntitlement, Set-HVMachine, New-HVGlobalEntitlement, Remove-HVGlobalEntitlement, Get-HVGlobalEntitlement, Get-HVPodSession, Set-HVApplicationIcon, Remove-HVApplicationIcon, Get-HVGlobalSettings, Set-HVGlobalSettings, Set-HVGlobalEntitlement - From dfd5a4e37edcf654dfde78c03a43defcef5edceb Mon Sep 17 00:00:00 2001 From: mtelvers Date: Wed, 25 Oct 2017 17:22:20 +0100 Subject: [PATCH 083/112] Fix for Find-HVMachine if no match is found in the first 1000 results If no match was found in the first 1000 results then the QueryService_GetNext() was never called as it used the length of the result array to detect the second pass through the loop --- Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 index dd7a66c..9c0cdb8 100644 --- a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 +++ b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 @@ -6910,19 +6910,22 @@ function Find-HVMachine { $query.Filter = $andFilter } $machineList = @() + $GetNext = $false $queryResults = $query_service_helper.QueryService_Create($services, $query) do { - if ($machineList.length -ne 0) { $queryResults = $query_service_helper.QueryService_GetNext($services, $queryResults.id) } + if ($GetNext) { $queryResults = $query_service_helper.QueryService_GetNext($services, $queryResults.id) } $machineList += $queryResults.results + $GetNext = $true } while ($queryResults.remainingCount -gt 0) $query_service_helper.QueryService_Delete($services, $queryResults.id) } if ($wildcard -or [string]::IsNullOrEmpty($machineList)) { $query.Filter = $null $machineList = @() + $GetNext = $false $queryResults = $query_service_helper.QueryService_Create($services,$query) do { - if ($machineList.length -ne 0) { $queryResults = $query_service_helper.QueryService_GetNext($services, $queryResults.id) } + if ($GetNext) { $queryResults = $query_service_helper.QueryService_GetNext($services, $queryResults.id) } $strFilterSet = @() foreach ($setting in $machineSelectors.Keys) { if ($null -ne $params[$setting]) { @@ -6936,6 +6939,7 @@ function Find-HVMachine { $whereClause = [string]::Join(' -and ', $strFilterSet) $scriptBlock = [Scriptblock]::Create($whereClause) $machineList += $queryResults.results | where $scriptBlock + $GetNext = $true } while ($queryResults.remainingCount -gt 0) $query_service_helper.QueryService_Delete($services, $queryResults.id) } From c2f137c812b70ad067afe277dc89ad93838269fd Mon Sep 17 00:00:00 2001 From: mtelvers Date: Fri, 27 Oct 2017 08:52:44 +0100 Subject: [PATCH 084/112] Updates to Set-HVPool Updated Set-HVPool to address issue #132. The -Start and -Stop parameter tested the wrong variable in the loop. $item is undefined. Furthermore, in general if the key/value pair specified $false as the value then the parameter wasn't seen in simple logic test such as if ($key -and $value) {}. I've updated to use $PSBoundParameters instead. --- Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 index 9c0cdb8..94f9939 100644 --- a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 +++ b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 @@ -5965,8 +5965,8 @@ function Set-HVPool { } if ($desktopPools) { foreach ($desktopObj in $desktopPools) { - if (($Start -or $Stop) -and ("AUTOMATED" -ne $item.DesktopSummaryData.Type)) { - Write-Error "Start/Stop operation is not supported for Poll with name : [$item.DesktopSummaryData.Name]" + if (($Start -or $Stop) -and ("AUTOMATED" -ne $desktopObj.DesktopSummaryData.Type)) { + Write-Error "Start/Stop operation is not supported for Pool with name : [$desktopObj.DesktopSummaryData.Name]" return } $poolList.add($desktopObj.id, $desktopObj.DesktopSummaryData.Name) @@ -5999,9 +5999,9 @@ function Set-HVPool { } } $updates = @() - if ($key -and $value) { + if ($PSBoundParameters.ContainsKey("key") -and $PSBoundParameters.ContainsKey("value")) { $updates += Get-MapEntry -key $key -value $value - } elseif ($key -or $value) { + } elseif ($PSBoundParameters.ContainsKey("key") -or $PSBoundParameters.ContainsKey("value")) { Write-Error "Both key:[$key] and value:[$value] needs to be specified" } if ($spec) { From 76820423a424f108dd5c3b79be5e4132c415677c Mon Sep 17 00:00:00 2001 From: Alan Comstock <31929022+Mr-Uptime@users.noreply.github.com> Date: Fri, 27 Oct 2017 09:16:39 -0500 Subject: [PATCH 085/112] Multipath to Round Robin on all FC disks in cluster This will configure multipath to round robin and IOPS to 1 on all disks in a cluster based upon the vendor type. This script is configured for 3par LUNs. Change $vendor for other storage types/vendors. --- Scripts/SetClusterMultiPathToRoundRobin.ps1 | 24 ++++++++------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/Scripts/SetClusterMultiPathToRoundRobin.ps1 b/Scripts/SetClusterMultiPathToRoundRobin.ps1 index 97bf311..396b038 100644 --- a/Scripts/SetClusterMultiPathToRoundRobin.ps1 +++ b/Scripts/SetClusterMultiPathToRoundRobin.ps1 @@ -2,28 +2,22 @@ Script name: SetClusterMultiPathToRoundRobin.ps1 Created on: 09/14/2017 Author: Alan Comstock, @Mr_Uptime - Description: Set the MultiPath policy for FC devices to RoundRobin for all hosts in a cluster. + Description: Set the MultiPath policy for FC devices to RoundRobin and IOPS to 1 for all hosts in a cluster based upon the vendor tag. Dependencies: None known PowerCLI Version: VMware PowerCLI 6.5 Release 1 build 4624819 PowerShell Version: 5.1.14393.1532 OS Version: Windows 10 #> -#Check for any Fibre Channel devices that are not set to Round Robin in a cluster. -#Get-Cluster -Name CLUSTERNAME | Get-VMhost | Get-VMHostHba -Type "FibreChannel" | Get-ScsiLun -LunType disk | Where { $_.MultipathPolicy -notlike "RoundRobin" } | Select CanonicalName,MultipathPolicy - -#Set the Multipathing Policy to Round Robin for any Fibre Channel devices that are not Round Robin in a cluster -$cluster = Get-Cluster CLUSTERNAME -$hostlist = Get-VMHost -Location $cluster | Sort Name -$TotalHostCount = $hostlist.count -$hostincrement = 0 -while ($hostincrement -lt $TotalHostCount){ #Host Loop - $currenthost = $hostlist[$hostincrement].Name - Write-Host "Working on" $currenthost - $scsilun = Get-VMhost $currenthost | Get-VMHostHba -Type "FibreChannel" | Get-ScsiLun -LunType disk | Where { $_.MultipathPolicy -notlike "RoundRobin" } +$pathpolicy="RoundRobin" +$iops="1" +$vendor="3PARdata" +$AllESXHosts = Get-VMHost -Location CLUSTERNAME | Sort Name +Foreach ($esxhost in $AllESXHosts) { + Write-Host "Working on" $esxhost + $scsilun = Get-VMhost $esxhost | Get-VMHostHba -Type "FibreChannel" | Get-ScsiLun -LunType disk | Where-Object {$_.Vendor -like $vendor -and ($_.MultipathPolicy -notlike $pathpolicy -or $_.CommandsToSwitchPath -ne $iops)} if ($scsilun -ne $null){ - Set-ScsiLun -ScsiLun $scsilun -MultipathPolicy RoundRobin + Set-ScsiLun -ScsiLun $scsilun -MultipathPolicy $pathpolicy -CommandsToSwitchPath $iops } - $hostincrement++ #bump the host increment } #The End From dc523953cacedeeb0d00e22e6ea38b8ea9682111 Mon Sep 17 00:00:00 2001 From: Alan Comstock <31929022+Mr-Uptime@users.noreply.github.com> Date: Fri, 27 Oct 2017 09:25:47 -0500 Subject: [PATCH 086/112] Adds VLANs to an existing standard switch This script will add VLANs to an existing standard switch on a host. Information needed: $esxhost - Name of host $vswitch - Name of existing standard switch $vlanlist - List of all the VLANs you want to add to the switch --- Scripts/CreateVLANonStandardSwitch | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 Scripts/CreateVLANonStandardSwitch diff --git a/Scripts/CreateVLANonStandardSwitch b/Scripts/CreateVLANonStandardSwitch new file mode 100644 index 0000000..d64b221 --- /dev/null +++ b/Scripts/CreateVLANonStandardSwitch @@ -0,0 +1,19 @@ +<# + Script name: CreateVLANonStandardSwitch.ps1 + Created on: 10/26/2017 + Author: Alan Comstock, @Mr_Uptime + Description: Adds VLANs to an existing standard switch + Dependencies: None known + PowerCLI Version: VMware PowerCLI 6.5 Release 1 build 4624819 + PowerShell Version: 5.1.14393.1532 + OS Version: Windows 10 +#> + +$esxhost="HOSTNAME" +$vswitch="vSwitch0" +$vlanlist=10,20,30,40,50 +Foreach ($vlan in $vlanlist) { + $portgroupname="VLAN " + $vlan + Get-VMHost $esxhost | Get-VirtualSwitch -name $vswitch | New-VirtualPortGroup -Name $portgroupname -VLanId $vlan +} +#The End From 77adef309d9774ebc3a558895c99650feffb9a76 Mon Sep 17 00:00:00 2001 From: William Lam Date: Sun, 29 Oct 2017 13:45:29 -0700 Subject: [PATCH 087/112] Adding NSXT Module --- Modules/NSXT/NSXT.psd1 | 18 +++ Modules/NSXT/NSXT.psm1 | 260 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 278 insertions(+) create mode 100644 Modules/NSXT/NSXT.psd1 create mode 100644 Modules/NSXT/NSXT.psm1 diff --git a/Modules/NSXT/NSXT.psd1 b/Modules/NSXT/NSXT.psd1 new file mode 100644 index 0000000..c3371b3 --- /dev/null +++ b/Modules/NSXT/NSXT.psd1 @@ -0,0 +1,18 @@ +@{ + ModuleToProcess = 'NSXT.psm1' + ModuleVersion = '1.0.0.0' + GUID = 'c72f4e3d-5d1d-498f-ba86-6fa03e4ae6dd' + Author = 'William Lam' + CompanyName = 'primp-industries.com' + Copyright = '(c) 2017. All rights reserved.' + Description = 'Powershell Module for NSX-T REST API Functions' + PowerShellVersion = '5.0' + FunctionsToExport = 'Get-NSXTComputeManager','Get-NSXTFabricNode','Get-NSXTFirewallRule','Get-NSXTIPPool','Get-NSXTLogicalSwitch','Get-NSXTManager','Get-NSXTTransportZone','Get-NSXTController' + PrivateData = @{ + PSData = @{ + Tags = @('NSX-T','REST') + LicenseUri = 'https://www.tldrlegal.com/l/mit' + ProjectUri = 'https://github.com/lamw/PowerCLI-Example-Scripts/tree/master/Modules/NSXT' + } + } +} \ No newline at end of file diff --git a/Modules/NSXT/NSXT.psm1 b/Modules/NSXT/NSXT.psm1 new file mode 100644 index 0000000..f8dcb49 --- /dev/null +++ b/Modules/NSXT/NSXT.psm1 @@ -0,0 +1,260 @@ +Function Get-NSXTController { + Param ( + [parameter(Mandatory=$false,ValueFromPipeline=$true)][string]$Id + ) + + $clusterNodeService = Get-NsxtService -Name "com.vmware.nsx.cluster.nodes" + $clusterNodeStatusService = Get-NsxtService -Name "com.vmware.nsx.cluster.nodes.status" + if($Id) { + $nodes = $clusterNodeService.get($Id) + } else { + $nodes = $clusterNodeService.list().results | where { $_.manager_role -eq $null } + } + + $results = @() + foreach ($node in $nodes) { + $nodeId = $node.id + $nodeName = $node.controller_role.control_plane_listen_addr.ip_address + $nodeStatusResults = $clusterNodeStatusService.get($nodeId) + + $tmp = [pscustomobject] @{ + Id = $nodeId; + Name = $nodeName; + ClusterStatus = $nodeStatusResults.control_cluster_status.control_cluster_status; + Version = $nodeStatusResults.version; + + } + $results+=$tmp + } + $results +} + +Function Get-NSXTFabricNode { + Param ( + [parameter(Mandatory=$false,ValueFromPipeline=$true)][string]$Id, + [Switch]$ESXi, + [Switch]$Edge + ) + + $fabricNodeService = Get-NsxtService -Name "com.vmware.nsx.fabric.nodes" + $fabricNodeStatusService = Get-NsxtService -Name "com.vmware.nsx.fabric.nodes.status" + if($Id) { + $nodes = $fabricNodeService.get($Id) + } else { + if($ESXi) { + $nodes = $fabricNodeService.list().results | where { $_.resource_type -eq "HostNode" } + } elseif ($Edge) { + $nodes = $fabricNodeService.list().results | where { $_.resource_type -eq "EdgeNode" } + } else { + $nodes = $fabricNodeService.list().results + } + } + + $results = @() + foreach ($node in $nodes) { + $nodeStatusResult = $fabricNodeStatusService.get($node.id) + + $tmp = [pscustomobject] @{ + Id = $node.id; + Name = $node.display_name; + Type = $node.resource_type; + Address = $node.ip_addresses; + NSXVersion = $nodeStatusResult.software_version + OS = $node.os_type; + Version = $node.os_version; + Status = $nodeStatusResult.host_node_deployment_status + ManagerStatus = $nodeStatusResult.mpa_connectivity_status + ControllerStatus = $nodeStatusResult.lcp_connectivity_status + } + $results+=$tmp + } + $results +} + +Function Get-NSXTIPPool { + Param ( + [parameter(Mandatory=$false,ValueFromPipeline=$true)][string]$Id + ) + + $ipPoolService = Get-NsxtService -Name "com.vmware.nsx.pools.ip_pools" + + if($Id) { + $ipPools = $ipPoolService.get($Id) + } else { + $ipPools = $ipPoolService.list().results + } + + $results = @() + foreach ($ipPool in $ipPools) { + $tmp = [pscustomobject] @{ + Id = $ipPool.Id; + Name = $ipPool.Display_Name; + Total = $ipPool.pool_usage.total_ids; + Free = $ipPool.pool_usage.free_ids; + Network = $ipPool.subnets.cidr; + Gateway = $ipPool.subnets.gateway_ip; + DNS = $ipPool.subnets.dns_nameservers; + RangeStart = $ipPool.subnets.allocation_ranges.start; + RangeEnd = $ipPool.subnets.allocation_ranges.end + } + $results+=$tmp + } + $results +} + +Function Get-NSXTTransportZone { + Param ( + [parameter(Mandatory=$false,ValueFromPipeline=$true)][string]$Id + ) + + $transportZoneService = Get-NsxtService -Name "com.vmware.nsx.transport_zones" + + if($Id) { + $transportZones = $transportZoneService.get($Id) + } else { + $transportZones = $transportZoneService.list().results + } + + $results = @() + foreach ($transportZone in $transportZones) { + $tmp = [pscustomobject] @{ + Id = $transportZone.Id; + Name = $transportZone.display_name; + Type = $transportZone.transport_type; + HostSwitchName = $transportZone.host_switch_name; + } + $results+=$tmp + } + $results +} + +Function Get-NSXTComputeManager { + Param ( + [parameter(Mandatory=$false,ValueFromPipeline=$true)][string]$Id + ) + + $computeManagerSerivce = Get-NsxtService -Name "com.vmware.nsx.fabric.compute_managers" + $computeManagerStatusService = Get-NsxtService -Name "com.vmware.nsx.fabric.compute_managers.status" + + if($Id) { + $computeManagers = $computeManagerSerivce.get($id) + } else { + $computeManagers = $computeManagerSerivce.list().results + } + + $results = @() + foreach ($computeManager in $computeManagers) { + $computeManagerStatus = $computeManagerStatusService.get($computeManager.Id) + + $tmp = [pscustomobject] @{ + Id = $computeManager.Id; + Name = $computeManager.display_name; + Server = $computeManager.server + Type = $computeManager.origin_type; + Version = $computeManagerStatus.Version; + Registration = $computeManagerStatus.registration_status; + Connection = $computeManagerStatus.connection_status; + } + $results+=$tmp + } + $results +} + +Function Get-NSXTLogicalSwitch { + Param ( + [parameter(Mandatory=$false,ValueFromPipeline=$true)][string]$Id + ) + + $logicalSwitchService = Get-NsxtService -Name "com.vmware.nsx.logical_switches" + $logicalSwitchSummaryService = Get-NsxtService -Name "com.vmware.nsx.logical_switches.summary" + + if($Id) { + $logicalSwitches = $logicalSwitchService.get($Id) + } else { + $logicalSwitches = $logicalSwitchService.list().results + } + + $results = @() + foreach ($logicalSwitch in $logicalSwitches) { + $transportZone = (Get-NSXTTransportZone -Id $logicalSwitch.transport_zone_id | Select Name | ft -HideTableHeaders | Out-String).trim() + $ports = $logicalSwitchSummaryService.get($logicalSwitch.id).num_logical_ports + + $tmp = [pscustomobject] @{ + Id = $logicalSwitch.Id; + Name = $logicalSwitch.display_name; + VLAN = $logicalSwitch.vlan; + AdminStatus = $logicalSwitch.admin_state; + Ports = $ports; + TransportZone = $transportZone; + } + $results+=$tmp + } + $results +} + +Function Get-NSXTFirewallRule { + Param ( + [parameter(Mandatory=$false,ValueFromPipeline=$true)][string]$Id + ) + + $firewallService = Get-NsxtService -Name "com.vmware.nsx.firewall.sections" + $firewallRuleService = Get-NsxtService -Name "com.vmware.nsx.firewall.sections.rules" + + if($Id) { + $firewallRuleSections = $firewallService.get($Id) + } else { + $firewallRuleSections = $firewallService.list().results + } + + $sectionResults = @() + foreach ($firewallRuleSection in $firewallRuleSections) { + $tmp = [pscustomobject] @{ + Id = $firewallRuleSection.Id; + Name = $firewallRuleSection.display_name; + Type = $firewallRuleSection.section_type; + Stateful = $firewallRuleSection.stateful; + RuleCount = $firewallRuleSection.rule_count; + } + $sectionResults+=$tmp + } + $sectionResults + + $firewallResults = @() + if($id) { + $firewallRules = $firewallRuleService.list($id).results + foreach ($firewallRule in $firewallRules) { + $tmp = [pscustomobject] @{ + Id = $firewallRule.id; + Name = $firewallRule.display_name; + Sources = if($firewallRule.sources -eq $null) { "ANY" } else { $firewallRule.sources}; + Destination = if($firewallRule.destinations -eq $null) { "ANY" } else { $firewallRule.destinations }; + Services = if($firewallRule.services -eq $null) { "ANY" } else { $firewallRule.services } ; + Action = $firewallRule.action; + AppliedTo = if($firewallRule.applied_tos -eq $null) { "ANY" } else { $firewallRule.applied_tos }; + Log = $firewallRule.logged; + } + $firewallResults+=$tmp + } + } + $firewallResults +} + +Function Get-NSXTManager { + $clusterNodeService = Get-NsxtService -Name "com.vmware.nsx.cluster.nodes" + + $nodes = $clusterNodeService.list().results + + $results = @() + foreach ($node in $nodes) { + if($node.manager_role -ne $null) { + $tmp = [pscustomobject] @{ + Id = $node.id; + Name = $node.display_name; + Address = $node.appliance_mgmt_listen_addr; + SHA256Thumbprint = $node.manager_role.api_listen_addr.certificate_sha256_thumbprint; + } + $results+=$tmp + } + } + $results +} \ No newline at end of file From df32c591484a3e90ef78086b19a73f9703253764 Mon Sep 17 00:00:00 2001 From: Unknown Date: Tue, 31 Oct 2017 11:17:36 -0400 Subject: [PATCH 088/112] Use the item in foreach rather than collection Error message should use $v rather than $vm --- Scripts/Get-VMNetworkPortId.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Scripts/Get-VMNetworkPortId.ps1 b/Scripts/Get-VMNetworkPortId.ps1 index ed130a6..eeb2e62 100644 --- a/Scripts/Get-VMNetworkPortId.ps1 +++ b/Scripts/Get-VMNetworkPortId.ps1 @@ -30,7 +30,7 @@ foreach ($v in $vm) { #Validate the input is a valid VM $vmobj = Get-VM -Name $v -erroraction silentlycontinue - if (!$vmobj) {Write-Verbose "No VM found by the name $vm."} + if (!$vmobj) {Write-Verbose "No VM found by the name $v."} else { #Create a temporary object to store individual ouput $tempout = "" | select VM,PortId From 5879c10003763c57ba61cc8530571555cf024977 Mon Sep 17 00:00:00 2001 From: mtelvers Date: Thu, 2 Nov 2017 22:18:41 +0000 Subject: [PATCH 089/112] Fixes to Get-HVDatastore() when multiple datastores are specified `$Datastores` needs to be initialized as an empty array `@()` rather than `$null`. Furthermore, if `$DsStroageOvercommit` is empty it is only initialised with a single element where there should be one element per datastore. I've moved the test into the loop instead. --- Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 index 94f9939..127c056 100644 --- a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 +++ b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 @@ -5039,15 +5039,16 @@ function Get-HVDatastore { foreach ($ds in $datastoreNames) { $datastoresSelected += ($datastoreInfoList | Where-Object { ($_.DatastoreData.Path -eq $ds) -or ($_.datastoredata.name -eq $ds) }).id } - $Datastores = $null - if (! $DsStorageOvercommit) { - $DsStorageOvercommit += 'UNBOUNDED' - } + $Datastores = @() $StorageOvercommitCnt = 0 foreach ($ds in $datastoresSelected) { $myDatastores = New-Object VMware.Hv.DesktopVirtualCenterDatastoreSettings $myDatastores.Datastore = $ds - $mydatastores.StorageOvercommit = $DsStorageOvercommit[$StorageOvercommitCnt] + if (! $DsStorageOvercommit) { + $mydatastores.StorageOvercommit = 'UNBOUNDED' + } else { + $mydatastores.StorageOvercommit = $DsStorageOvercommit[$StorageOvercommitCnt] + } $Datastores += $myDatastores $StorageOvercommitCnt++ } From d406e54b19381dd6e7d6e6727fd9a0079fa97a67 Mon Sep 17 00:00:00 2001 From: mtelvers Date: Fri, 3 Nov 2017 11:17:27 +0000 Subject: [PATCH 090/112] Fix to New-HVPool when persistent datastores are specified Function Get_Datastore should read Get-HVDatastore --- Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 index 127c056..e05be97 100644 --- a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 +++ b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 @@ -3239,6 +3239,10 @@ function New-HVPool { Datastore names to store the VM Applicable to Full, Linked, Instant Clone Pools. +.PARAMETER StorageOvercommit + Storage overcommit determines how View places new VMs on the selected datastores. + Supported values are 'UNBOUNDED','AGGRESSIVE','MODERATE','CONSERVATIVE','NONE' and are case sensitive. + .PARAMETER UseVSAN Whether to use vSphere VSAN. This is applicable for vSphere 5.5 or later. Applicable to Full, Linked, Instant Clone Pools. @@ -3269,6 +3273,7 @@ function New-HVPool { .PARAMETER PersistentDiskStorageOvercommit Storage overcommit determines how view places new VMs on the selected datastores. + Supported values are 'UNBOUNDED','AGGRESSIVE','MODERATE','CONSERVATIVE','NONE' and are case sensitive. .PARAMETER DiskSizeMB Size of the persistent disk in MB. @@ -4986,7 +4991,7 @@ function Get-HVPoolStorageObject { if ($persistentDiskStorageOvercommit -and ($persistentDiskDatastores.Length -ne $persistentDiskStorageOvercommit.Length) ) { throw "Parameters persistentDiskDatastores length: [$persistentDiskDatastores.Length] and persistentDiskStorageOvercommit length: [$persistentDiskStorageOvercommit.Length] should be of same size" } - $desktopPersistentDiskSettings.PersistentDiskDatastores = Get_Datastore -DatastoreInfoList $datastoreList -DatastoreNames $PersistentDiskDatastores -DsStorageOvercommit $persistentDiskStorageOvercommit + $desktopPersistentDiskSettings.PersistentDiskDatastores = Get-HVDatastore -DatastoreInfoList $datastoreList -DatastoreNames $PersistentDiskDatastores -DsStorageOvercommit $persistentDiskStorageOvercommit } $desktopNonPersistentDiskSettings.RedirectDisposableFiles = $redirectDisposableFiles $desktopNonPersistentDiskSettings.DiskSizeMB = $nonPersistentDiskSizeMB From 9e588e39c9515e12595431659d60802d929ed4e2 Mon Sep 17 00:00:00 2001 From: mtelvers Date: Fri, 3 Nov 2017 17:13:44 +0000 Subject: [PATCH 091/112] Added -ResourcePool to Set-HVPool Update Set-HVPool to allow you to set a new resource pool within the same host/cluster --- .../.VMware.HV.Helper.psm1.swp | Bin 0 -> 458752 bytes Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 | 13 +++++++++++++ 2 files changed, 13 insertions(+) create mode 100644 Modules/VMware.Hv.Helper/.VMware.HV.Helper.psm1.swp diff --git a/Modules/VMware.Hv.Helper/.VMware.HV.Helper.psm1.swp b/Modules/VMware.Hv.Helper/.VMware.HV.Helper.psm1.swp new file mode 100644 index 0000000000000000000000000000000000000000..803f4193b5e2339e38b9b7e6408b61ce5a3d27d1 GIT binary patch literal 458752 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 z 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 A_W%F@ literal 0 HcmV?d00001 diff --git a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 index e05be97..9d07050 100644 --- a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 +++ b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 @@ -5941,6 +5941,10 @@ function Set-HVPool { [string] $globalEntitlement, + [Parameter(Mandatory = $false)] + [string] + $ResourcePool, + [Parameter(Mandatory = $false)] [boolean]$allowUsersToChooseProtocol, @@ -6044,6 +6048,15 @@ function Set-HVPool { $updates += Get-MapEntry -key 'desktopSettings.displayProtocolSettings.enableHTMLAccess' -value $enableHTMLAccess } + if ($PSBoundParameters.ContainsKey("ResourcePool")) { + foreach ($item in $poolList.Keys) { + $pool = Get-HVPool -PoolName $poolList.$item + $ResourcePool_service_helper = New-Object VMware.Hv.ResourcePoolService + $ResourcePoolID = Get-HVResourcePoolID $ResourcePool_service_helper.ResourcePool_GetResourcePoolTree($services, $pool.AutomatedDesktopData.VirtualCenterProvisioningSettings.VirtualCenterProvisioningData.HostOrCluster) + $updates += Get-MapEntry -key 'automatedDesktopData.virtualCenterProvisioningSettings.virtualCenterProvisioningData.resourcePool' -value $ResourcePoolID + } + } + $info = $services.PodFederation.PodFederation_get() if ($globalEntitlement -and ("ENABLED" -eq $info.localPodStatus.status)) { $QueryFilterEquals = New-Object VMware.Hv.QueryFilterEquals From 6b4bbf562d5effaaa08eebd512891fb325836aa4 Mon Sep 17 00:00:00 2001 From: mtelvers Date: Fri, 3 Nov 2017 17:18:55 +0000 Subject: [PATCH 092/112] Delete .VMware.HV.Helper.psm1.swp --- .../.VMware.HV.Helper.psm1.swp | Bin 458752 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 Modules/VMware.Hv.Helper/.VMware.HV.Helper.psm1.swp diff --git a/Modules/VMware.Hv.Helper/.VMware.HV.Helper.psm1.swp b/Modules/VMware.Hv.Helper/.VMware.HV.Helper.psm1.swp deleted file mode 100644 index 803f4193b5e2339e38b9b7e6408b61ce5a3d27d1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 458752 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 z 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 A_W%F@ From 77d44734d46f9029f2ed35a9606b86fd9a92f670 Mon Sep 17 00:00:00 2001 From: mtelvers Date: Mon, 6 Nov 2017 11:15:26 +0000 Subject: [PATCH 093/112] Add Get-HVResourceStructure to display the Resouce Pool structure Display the resource pools available to the Horizon View server --- .../VMware.Hv.Helper/VMware.HV.Helper.psm1 | 88 ++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 index 9d07050..d90aaf1 100644 --- a/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 +++ b/Modules/VMware.Hv.Helper/VMware.HV.Helper.psm1 @@ -4740,6 +4740,92 @@ function New-HVPool { } } +function Get-HVResourceStructure { +<# +.Synopsis + Output the structure of the resource pools available to a HV. Primarily this is for debugging + + PS> Get-HVResourceStructure + vCenter vc.domain.local + Container DC path /DC/host + HostOrCluster Servers path /DC/host/Servers + HostOrCluster VDI path /DC/host/VDI + ResourcePool Servers path /DC/host/Servers/Resources + ResourcePool VDI path /DC/host/VDI/Resources + ResourcePool RP1 path /DC/host/VDI/Resources/RP1 + ResourcePool RP2 path /DC/host/VDI/Resources/RP1/RP2 + + Author : Mark Elvers +#> + param( + [Parameter(Mandatory = $false)] + $HvServer = $null + ) + begin { + $services = Get-ViewAPIService -hvServer $HvServer + if ($null -eq $services) { + Write-Error "Could not retrieve ViewApi services from connection object" + break + } + } + process { + $vc_service_helper = New-Object VMware.Hv.VirtualCenterService + $vcList = $vc_service_helper.VirtualCenter_List($services) + foreach ($vc in $vcList) { + Write-Host vCenter $vc.ServerSpec.ServerName + $datacenterList = @{} + $BaseImage_service_helper = New-Object VMware.Hv.BaseImageVmService + $parentList = $BaseImage_service_helper.BaseImageVm_List($services, $vc.id) + foreach ($possibleParent in $parentList) { + if (-not $datacenterList.ContainsKey($possibleParent.datacenter.id)) { + $datacenterList.Add($possibleParent.datacenter.id, $possibleParent.datacenter) + } + if (0) { + Write-Host "$($possibleParent.name): " -NoNewLine + if ($possibleParent.incompatibleReasons.inUseByDesktop) { Write-Host "inUseByDesktop, " -NoNewLine } + if ($possibleParent.incompatibleReasons.viewComposerReplica) { Write-Host "viewComposerReplica, " -NoNewLine } + if ($possibleParent.incompatibleReasons.inUseByLinkedCloneDesktop) { Write-Host "inUseByLinkedCloneDesktop, " -NoNewLine } + if ($possibleParent.incompatibleReasons.unsupportedOSForLinkedCloneFarm) { Write-Host "unsupportedOSForLinkedCloneFarm, " -NoNewLine } + if ($possibleParent.incompatibleReasons.unsupportedOS) { Write-Host "unsupportedOS, " -NoNewLine } + if ($possibleParent.incompatibleReasons.noSnapshots) { Write-Host "noSnapshots, " -NoNewLine } + Write-Host + } + } + $hcNodes = @() + $index = 0 + foreach ($datacenter in $datacenterList.keys) { + $HostOrCluster_service_helper = New-Object VMware.Hv.HostOrClusterService + $hcNodes += $HostOrCluster_service_helper.HostOrCluster_GetHostOrClusterTree($services, $datacenterList.$datacenter) + while ($index -lt $hcNodes.length) { + if ($hcNodes[$index].container) { + Write-Host "Container" $hcNodes[$index].treecontainer.name "path" $hcNodes[$index].treecontainer.path + if ($hcNodes[$index].treecontainer.children.Length) { $hcNodes += $hcNodes[$index].treecontainer.children } + } else { + Write-Host "HostOrCluster" $hcNodes[$index].info.name "path" $hcNodes[$index].info.path + } + $index++ + } + } + $rpNodes = @() + $index = 0 + foreach ($hostOrCluster in $hcNodes) { + if (-not $hostOrCluster.container) { + $ResourcePool_service_helper = New-Object VMware.Hv.ResourcePoolService + $rpNodes += $ResourcePool_service_helper.ResourcePool_GetResourcePoolTree($services, $hostOrCluster.info.id) + while ($index -lt $rpNodes.length) { + Write-Host "ResourcePool" $rpNodes[$index].resourcePoolData.name "path" $rpNodes[$index].resourcePoolData.path + if ($rpNodes[$index].children.Length) { $rpNodes += $rpNodes[$index].children } + $index++ + } + } + } + } + } + end { + [System.gc]::collect() + } +} + function Get-HVPoolProvisioningData { param( [Parameter(Mandatory = $false)] @@ -9736,4 +9822,4 @@ function Set-HVGlobalSettings { } } -Export-ModuleMember Add-HVDesktop,Add-HVRDSServer,Connect-HVEvent,Disconnect-HVEvent,Get-HVPoolSpec,Get-HVInternalName, Get-HVEvent,Get-HVFarm,Get-HVFarmSummary,Get-HVPool,Get-HVPoolSummary,Get-HVMachine,Get-HVMachineSummary,Get-HVQueryResult,Get-HVQueryFilter,New-HVFarm,New-HVPool,Remove-HVFarm,Remove-HVPool,Set-HVFarm,Set-HVPool,Start-HVFarm,Start-HVPool,New-HVEntitlement,Get-HVEntitlement,Remove-HVEntitlement, Set-HVMachine, New-HVGlobalEntitlement, Remove-HVGlobalEntitlement, Get-HVGlobalEntitlement, Get-HVPodSession, Set-HVApplicationIcon, Remove-HVApplicationIcon, Get-HVGlobalSettings, Set-HVGlobalSettings, Set-HVGlobalEntitlement +Export-ModuleMember Add-HVDesktop,Add-HVRDSServer,Connect-HVEvent,Disconnect-HVEvent,Get-HVPoolSpec,Get-HVInternalName, Get-HVEvent,Get-HVFarm,Get-HVFarmSummary,Get-HVPool,Get-HVPoolSummary,Get-HVMachine,Get-HVMachineSummary,Get-HVQueryResult,Get-HVQueryFilter,New-HVFarm,New-HVPool,Remove-HVFarm,Remove-HVPool,Set-HVFarm,Set-HVPool,Start-HVFarm,Start-HVPool,New-HVEntitlement,Get-HVEntitlement,Remove-HVEntitlement, Set-HVMachine, New-HVGlobalEntitlement, Remove-HVGlobalEntitlement, Get-HVGlobalEntitlement, Get-HVPodSession, Set-HVApplicationIcon, Remove-HVApplicationIcon, Get-HVGlobalSettings, Set-HVGlobalSettings, Set-HVGlobalEntitlement, Get-HVResourceStructure From 14256a32a87ee2bb607feba0bed4e3d6da3f6d77 Mon Sep 17 00:00:00 2001 From: daoyuanw Date: Tue, 14 Nov 2017 14:43:47 +0800 Subject: [PATCH 094/112] Add VMToolsManagment for VMTools upgrade, guest info query .etc --- .../VMToolsManagement/VMToolsManagement.psm1 | 1149 +++++++++++++++++ 1 file changed, 1149 insertions(+) create mode 100644 Modules/VMToolsManagement/VMToolsManagement.psm1 diff --git a/Modules/VMToolsManagement/VMToolsManagement.psm1 b/Modules/VMToolsManagement/VMToolsManagement.psm1 new file mode 100644 index 0000000..bbfdf5e --- /dev/null +++ b/Modules/VMToolsManagement/VMToolsManagement.psm1 @@ -0,0 +1,1149 @@ +# Script Module : VMToolsManagement +# Version : 1.0 + +# Copyright © 2017 VMware, Inc. All Rights Reserved. + +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +# of the Software, and to permit persons to whom the Software is furnished to do +# so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +New-VIProperty -Name ToolsBuildNumber -Object VirtualMachine -Value { + Param ($VM) + + foreach ($item in $VM.ExtensionData.Config.ExtraConfig.GetEnumerator()) { + if ($item.Key -eq "guestinfo.vmtools.buildNumber") { + $toolsBuildNumber = $item.value + break + } + } + + return $toolsBuildNumber +} -BasedOnExtensionProperty 'Config.ExtraConfig' -Force | Out-Null + +Function Get-VMToolsInfo { +<# +.SYNOPSIS + This cmdlet retrieves the VMTools info of specified virtual machines. + +.DESCRIPTION + This cmdlet retrieves the VMTools version and build number info of specified virtual machines. + +.PARAMETER VM + Specifies the virtual machines which you want to get the VMTools info of. + +.EXAMPLE + C:\PS> Import-Module .\VMToolsManagement.psm1 + C:\PS> $VCServer = Connect-VIServer -Server -User -Password + C:\PS> Get-VM -Server $VCServer | Get-VMToolsInfo + + Retrieves VMTools info of all virtual machines which run in the $VCServer vCetner Server. + +.EXAMPLE + C:\PS> Get-VM "*rhel*" | Get-VMToolsInfo + + Name ToolsVersion ToolsBuildNumber + ------ ------------ ---------------- + 111394-RHEL-6.8-0 10.2.0 6090153 + 111394-RHEL-6.8-1 9.0.15 + 111393-RHEL-Server-7.2 10.1.0 + + Retrieves VMTools info of virtual machines with name containing "rhel". + +.EXAMPLE + C:\PS> Get-VM -Location "MyClusterName" | Get-VMToolsInfo + + Retrieves VMTools info from virtual machines which run in the "MyClusterName" cluster. + +.EXAMPLE + C:\PS> Get-VMHost "MyESXiHostName" | Get-VM | Get-VMToolsInfo + + Retrieves VMTools info of virtual machines which run on the "MyESXiHostName" ESXi host. + +.NOTES + This cmdlet assumes that you are connected to at least one vCenter Server system. + The tools build number is not supported in VMTools before 10.2.0 + +.NOTES + Author : Daoyuan Wang + Author email : daoyuanw@vmware.com + Version : 1.0 + ==========Tested Against Environment========== + VMware vSphere Hypervisor(ESXi) Version : 6.5 (build 4564106) + VMware vCenter Server Version : 6.5 (build 4602587) + PowerCLI Version : PowerCLI 6.5 (build 4624819) + PowerShell Version : 5.1 +#> + + [CmdletBinding()] + + Param ( + [Parameter(Mandatory=$true, + ValueFromPipeLine = $true, + ValueFromPipelinebyPropertyName=$True, + Position = 0)] + [ValidateNotNullOrEmpty()] + [VMware.VimAutomation.ViCore.Types.V1.Inventory.VirtualMachine[]] $VM + ) + + Process { + Get-VM $VM | Select Name, @{Name="ToolsVersion"; Expression={$_.Guest.ToolsVersion}}, ToolsBuildNumber + } +} + +Function Get-VMToolsInstallLastError { +<# +.SYNOPSIS + This cmdlet retrieves the error code of last VMTools installation. + +.DESCRIPTION + This cmdlet retrieves the error code of last VMTools installation on specified virtual machines. + +.PARAMETER VM + Specifies the virtual machines which you want to get the error code of. + +.EXAMPLE + C:\PS> Import-Module .\VMToolsManagement.psm1 + C:\PS> $VCServer = Connect-VIServer -Server -User -Password + C:\PS> Get-VM -Server $VCServer | Get-VMToolsInstallLastError + + Retrieves the last VMTools installation error code of all virtual machines which run in the $VCServer vCetner Server. + +.EXAMPLE + C:\PS> Get-VM "*win*" | Get-VMToolsInstallLastError + + Name LastToolsInstallErrCode + ------ ----------------------- + 111167-Win-7-Sp1-64-Enterprise-NoTools + 111323-Windows-8.1U3-32-Enterprise-Tools + 111305-Windows-Server2016 1641 + + Retrieves the last VMTools installation error code of virtual machines with name containing "win". + +.EXAMPLE + C:\PS> Get-VM -Location "MyClusterName" | Get-VMToolsInstallLastError + + Retrieves the last VMTools installation error code of virtual machines which run in the "MyClusterName" cluster. + +.EXAMPLE + C:\PS> Get-VMHost "MyESXiHostName" | Get-VM | Get-VMToolsInstallLastError + + Retrieves the last VMTools installation error code of virtual machines which run on the "MyESXiHostName" ESXi host. + +.NOTES + This cmdlet assumes that you are connected to at least one vCenter Server system. + +.NOTES + Author : Daoyuan Wang + Author email : daoyuanw@vmware.com + Version : 1.0 + ==========Tested Against Environment========== + VMware vSphere Hypervisor(ESXi) Version : 6.5 (build 4564106) + VMware vCenter Server Version : 6.5 (build 4602587) + PowerCLI Version : PowerCLI 6.5 (build 4624819)(build 4624819) + PowerShell Version : 5.1 +#> + + [CmdletBinding()] + + Param ( + [Parameter(Mandatory=$true, + ValueFromPipeLine = $true, + ValueFromPipelinebyPropertyName=$True, + Position = 0)] + [ValidateNotNullOrEmpty()] + [VMware.VimAutomation.ViCore.Types.V1.Inventory.VirtualMachine[]] $VM + ) + + Process { + $result = @() + foreach ($_ in $VM) { + $errorCodeInfo = $_.ExtensionData.Config.ExtraConfig.GetEnumerator() | where {$_.Key -eq "guestinfo.toolsInstallErrCode"} + + $info = New-Object PSObject + $info | Add-Member -type NoteProperty -name VmName -value $_.Name + $info | Add-Member -type NoteProperty -name LastToolsInstallErrCode -value $errorCodeInfo.Value + + $result += $info + } + $result + } +} + +Function Get-VMToolsGuestInfo { +<# +.SYNOPSIS + This cmdlet retrieves the guest info of specified virtual machines. + +.DESCRIPTION + This cmdlet retrieves the guest info such as HostName, IP, ToolsStatus, ToolsVersion, + ToolsInstallType and GuestFamily of specified virtual machines. + +.PARAMETER VM + Specifies the virtual machines which you want to get the guest info of. + +.EXAMPLE + C:\PS> Import-Module .\VMToolsManagement.psm1 + C:\PS> $VCServer = Connect-VIServer -Server -User -Password + C:\PS> Get-VM -Server $VCServer | Get-VMToolsGuestInfo + + Retrieves guest info of all virtual machines which run in the $VCServer vCetner Server. + +.EXAMPLE + C:\PS> Get-VM "*win*" | Get-VMToolsGuestInfo + + Name : 111323-Windows-8.1U3-32-Enterprise-Tools + HostName : win81u3 + IP : + ToolsStatus : toolsNotRunning + ToolsVersion : 10.2.0 + ToolsInstallType : guestToolsTypeMSI + GuestFamily : windowsGuest + VMPowerState : PoweredOff + + Name : 111305-Windows-Server2016 + HostName : WIN-ULETOOSSB7U + IP : 10.160.59.99 + ToolsStatus : toolsOk + ToolsVersion : 10.1.0 + ToolsInstallType : guestToolsTypeMSI + GuestFamily : windowsGuest + VMPowerState : PoweredOn + + Retrieves guest info of virtual machines with name containing "win". + +.EXAMPLE + C:\PS> Get-VM -Location "MyClusterName" | Get-VMToolsGuestInfo + + Retrieves guest info of virtual machines which run in the "MyClusterName" cluster. + +.EXAMPLE + C:\PS> Get-VMHost "MyESXiHostName" | Get-VM | Get-VMToolsGuestInfo + + Retrieves guest info of virtual machines which run on the "MyESXiHostName" ESXi host. + +.NOTES + This cmdlet assumes that you are connected to at least one vCenter Server system. + +.NOTES + Author : Daoyuan Wang + Author email : daoyuanw@vmware.com + Version : 1.0 + ==========Tested Against Environment========== + VMware vSphere Hypervisor(ESXi) Version : 6.5 (build 4564106) + VMware vCenter Server Version : 6.5 (build 4602587) + PowerCLI Version : PowerCLI 6.5 (build 4624819)(build 4624819) + PowerShell Version : 5.1 +#> + + [CmdletBinding()] + + Param ( + [Parameter(Mandatory=$true, + ValueFromPipeLine = $true, + ValueFromPipelinebyPropertyName=$True, + Position = 0)] + [ValidateNotNullOrEmpty()] + [VMware.VimAutomation.ViCore.Types.V1.Inventory.VirtualMachine[]] $VM + ) + + Process { + Get-VM $VM | Select Name, @{Name="HostName"; Expression={$_.Guest.HostName}}, + @{Name="IP"; Expression={$_.Guest.ExtensionData.IpAddress}}, + @{Name="ToolsStatus"; Expression={$_.Guest.ExtensionData.ToolsStatus}}, + @{Name="ToolsVersion"; Expression={$_.Guest.ToolsVersion}}, + @{Name="ToolsInstallType"; Expression={$_.Guest.ExtensionData.ToolsInstallType}}, + @{Name="GuestFamily"; Expression={$_.Guest.GuestFamily}}, + PowerState + } +} + +Function Get-VMByToolsInfo { +<# +.SYNOPSIS + This cmdlet retrieves the virtual machines with specified VMTools info. + +.DESCRIPTION + This cmdlet retrieves the virtual machines with specified VMTools version, + running status or version status. + +.PARAMETER VM + Specifies the virtual machines which you want to query VMTools status of. + +.PARAMETER ToolsVersion + Specifies the VMTools version. + +.PARAMETER ToolsRunningStatus + Specifies the VMTools running status. + +.PARAMETER ToolsVersionStatus + Specifies the VMTools version status. + +.EXAMPLE + C:\PS> Import-Module .\VMToolsManagement.psm1 + C:\PS> $VCServer = Connect-VIServer -Server -User -Password + C:\PS> Get-VM -Server $VCServer | Get-VMByToolsInfo + + Retrieves the virtual machines with VMTools not running in vCetner Server $VCServer. + +.EXAMPLE + C:\PS> Get-VM | Get-VMByToolsInfo -ToolsRunningStatus guestToolsNotRunning + + Name PowerState Num CPUs MemoryGB + ---- ---------- -------- -------- + 111394-RHEL-6.8-1 PoweredOff 4 2.000 + + Retrieves all the virtual machines with VMTools not running. + +.EXAMPLE + C:\PS> Get-VM | Get-VMByToolsInfo -ToolsVersion '10.1.0' + + Name PowerState Num CPUs MemoryGB + ---- ---------- -------- -------- + 111394-RHEL-6.8-1 PoweredOff 4 2.000 + + Retrieves the virtual machines with VMTools version 10.1.0. + +.EXAMPLE + C:\PS> Get-VM -Location "MyClusterName" | Get-VMByToolsInfo -ToolsVersionStatus guestToolsNeedUpgrade + + Retrieves the virtual machines with VMTools that need to upgrade in the "MyClusterName" cluster. + +.EXAMPLE + C:\PS> Get-VMHost "MyESXiHostName" | Get-VM | Get-VMByToolsInfo -ToolsRunningStatus guestToolsRunning -ToolsVersionStatus guestToolsNeedUpgrade + + Retrieves the virtual machines with VMTools that need to upgrade on the "MyESXiHostName" ESXi host. + +.NOTES + This cmdlet assumes that you are connected to at least one vCenter Server system. + +.NOTES + Author : Daoyuan Wang + Author email : daoyuanw@vmware.com + Version : 1.0 + ==========Tested Against Environment========== + VMware vSphere Hypervisor(ESXi) Version : 6.5 (build 4564106) + VMware vCenter Server Version : 6.5 (build 4602587)(build 4602587) + PowerCLI Version : PowerCLI 6.5 (build 4624819) + PowerShell Version : 5.1 +#> + + [CmdletBinding()] + + Param ( + [Parameter(Mandatory=$true, + ValueFromPipeLine = $true, + ValueFromPipelinebyPropertyName=$True, + Position = 0)] + [ValidateNotNullOrEmpty()] + [VMware.VimAutomation.ViCore.Types.V1.Inventory.VirtualMachine[]] $VM, + + [Parameter(Mandatory=$false)] + [String] $ToolsVersion, + + [Parameter(Mandatory=$false)] + [ValidateSet("guestToolsRunning", + "guestToolsNotRunning", + "guestToolsExecutingScripts")] + [String] $ToolsRunningStatus, + + [Parameter(Mandatory=$false)] + [ValidateSet("guestToolsNotInstalled", + "guestToolsNeedUpgrade", + "guestToolsCurrent", + "guestToolsUnmanaged")] + [String] $ToolsVersionStatus + ) + + Process { + $vmList = Get-VM $VM + + if ((-not $ToolsVersion) -and (-not $ToolsRunningStatus) -and (-not $ToolsVersionStatus)) { + Throw "Please specify at lease one parameter: ToolsVersion, ToolsRunningStatus or ToolsVersionStatus" + } + + if ($ToolsVersion) { + $vmList = $vmList | Where {$_.Guest.ToolsVersion -like $ToolsVersion} + } + + if ($ToolsRunningStatus) { + $vmList = $vmList | Where {$_.Guest.ExtensionData.ToolsRunningStatus -eq $ToolsRunningStatus} + } + + if ($ToolsVersionStatus) { + $vmList = $vmList | Where {$_.Guest.ExtensionData.ToolsVersionStatus -eq $ToolsVersionStatus} + } + + $vmList + } +} + +Function Set-VMToolsUpgradePolicy { +<# +.SYNOPSIS + This cmdlet sets the VMTool's upgrade policy to "upgradeAtPowerCycle". + +.DESCRIPTION + This cmdlet sets the VMTool's upgrade policy to "upgradeAtPowerCycle" of specified virtual machines. + +.PARAMETER VM + Specifies the virtual machines which you want to set the VMTool's upgrade policy of. + +.EXAMPLE + C:\PS> Import-Module .\VMToolsManagement.psm1 + C:\PS> $VCServer = Connect-VIServer -Server -User -Password + C:\PS> Get-VM -Server $VCServer | Set-VMToolsUpgradePolicy + + Sets VMTool's upgrade policy to "upgradeAtPowerCycle" of all virtual machines in the $VCServer vCetner Server. + +.EXAMPLE + C:\PS> Get-VM "*win*" | Set-VMToolsUpgradePolicy + + Sets VMTool's upgrade policy to "upgradeAtPowerCycle" of virtual machines with name containing "win". + +.EXAMPLE + C:\PS> Get-VM -Location "MyClusterName" | Set-VMToolsUpgradePolicy + + Sets VMTool's upgrade policy to "upgradeAtPowerCycle" of virtual machines in the "MyClusterName" cluster. + +.EXAMPLE + C:\PS> Get-VMHost "MyESXiHostName" | Get-VM | Set-VMToolsUpgradePolicy + + Sets VMTool's upgrade policy to "upgradeAtPowerCycle" of virtual machines on the "MyESXiHostName" ESXi host. + +.NOTES + This cmdlet assumes that you are connected to at least one vCenter Server system. + +.NOTES + Author : Daoyuan Wang + Author email : daoyuanw@vmware.com + Version : 1.0 + ==========Tested Against Environment========== + VMware vSphere Hypervisor(ESXi) Version : 6.5 (build 4564106) + VMware vCenter Server Version : 6.5 (build 4602587) + PowerCLI Version : PowerCLI 6.5 (build 4624819) + PowerShell Version : 5.1 +#> + + [CmdletBinding()] + + Param ( + [Parameter(Mandatory=$true, + ValueFromPipeLine = $true, + ValueFromPipelinebyPropertyName=$True, + Position = 0)] + [ValidateNotNullOrEmpty()] + [VMware.VimAutomation.ViCore.Types.V1.Inventory.VirtualMachine[]] $VM + ) + Begin { + $UpgradePolicy = "upgradeAtPowerCycle" + $vmConfigSpec = New-Object VMware.Vim.VirtualMachineConfigSpec + $vmConfigSpec.Tools = New-Object VMware.Vim.ToolsConfigInfo + $vmConfigSpec.Tools.ToolsUpgradePolicy = $UpgradePolicy + } + + Process { + foreach ($_ in $VM) { + # Get current setting + $vmView = Get-View $_ -Property Config.Tools.ToolsUpgradePolicy + # Change if VMTools upgrade policy is not "upgradeAtPowerCycle" + if ($vmView.Config.Tools.ToolsUpgradePolicy -ne $UpgradePolicy) { + Write-Verbose "Applying 'upgradeAtPowerCycle' setting to $($_.Name)..." + $vmView.ReconfigVM($vmConfigSpec) + } + } + } +} + +Function Invoke-VMToolsListProcessInVM { +<# +.Synopsis + This cmdlet lists the processes in the virtual machine. + +.Description + This cmdlet lists the running processes in the virtual machine. + +.PARAMETER VM + Specifies the virtual machine which you want to list the processes of. + +.Parameter GuestUser + Specifies the user name you want to use for authenticating with the guest OS. + +.Parameter GuestPassword + Specifies the password you want to use for authenticating with the guest OS. + +.Example + C:\PS> Import-Module .\VMToolsManagement.psm1 + C:\PS> $SampleVM = get-vm "MyVMName" + C:\PS> Invoke-VMToolsListProcessInVM -VM $SampleVM -GuestUser -GuestPassword + + ScriptOutput + ----------------------------------------------------------------------------------------------------------------------- + | USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND + | root 1 0.0 0.0 19360 1556 ? Ss Jul20 0:02 /sbin/init + | root 2 0.0 0.0 0 0 ? S Jul20 0:00 [kthreadd] + | root 3 0.0 0.0 0 0 ? S Jul20 0:06 [migration/0] + | root 4 0.0 0.0 0 0 ? S Jul20 0:00 [ksoftirqd/0] + | root 5 0.0 0.0 0 0 ? S Jul20 0:00 [stopper/0] + ...... + + List the processes in the "MyVMName" VM. + +.NOTES + This cmdlet lists processes in the guest OS of virtual machine. + A VMTools should already be running in the guest OS. + +.NOTES + Author : Daoyuan Wang + Author email : daoyuanw@vmware.com + Version : 1.0 + ==========Tested Against Environment========== + VMware vSphere Hypervisor(ESXi) Version : 6.5 (build 4564106) + VMware vCenter Server Version : 6.5 (build 4602587) + PowerCLI Version : PowerCLI 6.5 (build 4624819) + PowerShell Version : 5.1 + Guest OS : RHEL6.8, Windows7 +#> + + [CmdletBinding()] + + Param ( + [Parameter(Mandatory=$true, + ValueFromPipeLine = $true, + ValueFromPipelinebyPropertyName=$True, + Position = 0)] + [ValidateNotNullOrEmpty()] + [VMware.VimAutomation.ViCore.Types.V1.Inventory.VirtualMachine] $VM, + + [Parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [String] $GuestUser, + + [Parameter(Mandatory=$true)] + [AllowEmptyString()] + [String] $GuestPassword + ) + + Process { + $vmView = Get-VM $VM | Get-View -Property Guest + if ($vmView.Guest.State -eq 'NotRunning') { + Write-Error "$VM is Not Running, unable to list the processes!" + return + } + + if ($vmView.Guest.GuestFamily -match 'windows') { + $command = 'Get-Process' + } elseif ($vmView.Guest.GuestFamily -match 'linux') { + $command = 'ps aux' + } else { + $command = 'ps' + } + + Invoke-VMScript -VM $VM -ScriptText $command -GuestUser $GuestUser -GuestPassword $GuestPassword + } +} + +Function Update-VMToolsImageLocation { +<# +.Synopsis + This cmdlet updates the link /productLocker in ESXi host. + +.Description + This cmdlet updates the link /productLocker in ESXi host directly to avoid host reboot. + +.Parameter VMHost + Specifies the ESXi host on which you want to update the /productLocker link. + +.Parameter HostUser + Specifies the user name you want to use for authenticating with the ESXi host. + +.Parameter HostPassword + Specifies the password you want to use for authenticating with the ESXi host. + +.Parameter ImageLocation + Specifies the new image location where you want /producterLocker to link. + +.Example + C:\PS> Import-Module .\VMToolsManagement.psm1 + C:\PS> $SampleHost = get-vmhost + C:\PS> Update-VMToolsImageLocation -VmHost $SampleHost -HostUser 'root' -HostPassword -ImageLocation '/locker/packages/6.5.0/' + + Update link /productLocker successfully. + + Update the link /producterLocker on $SampleHost to point to '/locker/packages/6.5.0/'. + +.NOTES + This cmdlet connects to ESXi host to execute shell command directly. + Make sure the SSH service on ESXi host is enabled, and a SSH library(Posh-SSH or SSH-Sessions etc.) + for powershell is already installed on client where you call This cmdlet. + You can instal Posh-SSH by executing: + iex (New-Object Net.WebClient).DownloadString("https://gist.github.com/darkoperator/6152630/raw/c67de4f7cd780ba367cccbc2593f38d18ce6df89/instposhsshdev") + For SSH-Sessions installation and usage, please refer to + http://www.powershelladmin.com/wiki/SSH_from_PowerShell_using_the_SSH.NET_library + + +.NOTES + Author : Daoyuan Wang + Author email : daoyuanw@vmware.com + Version : 1.0 + ==========Tested Against Environment========== + VMware vSphere Hypervisor(ESXi) Version : 6.5 (build 4564106) + VMware vCenter Server Version : 6.5 (build 4602587) + PowerCLI Version : PowerCLI 6.5 (build 4624819) + PowerShell Version : 5.1 +#> + + [CmdletBinding()] + + Param ( + [Parameter(Mandatory=$true, + ValueFromPipeLine = $true, + ValueFromPipelinebyPropertyName=$True, + Position = 0)] + [VMware.VimAutomation.ViCore.Types.V1.Inventory.VMHost] $VMHost, + + [Parameter(Mandatory=$true)] + [String] $HostUser, + + [Parameter(Mandatory=$true)] + [AllowEmptyString()] + [String] $HostPassword, + + [Parameter(Mandatory=$true)] + [String] $ImageLocation + ) + + Process { + if (-not (Get-Command New-SSHSession)) { + Throw "This cmdlet depends on SSH library. Please ensure a SSH library is already installed!" + } + + $password = new-object System.Security.SecureString + if ($HostPassword) { + $password = ConvertTo-SecureString -AsPlainText $HostPassword -Force + } + + $crendential = New-Object System.Management.Automation.PSCredential -ArgumentList $HostUser, $password + $sshSession = New-SSHSession -ComputerName $VMHost -Credential $crendential -Force + + $result = Invoke-SshCommand -SSHSession $sshSession -Command "readlink /productLocker" -EnsureConnection:$false + Write-Verbose "The link /productLocker before change: $($result.Output)" + + $command = "rm /productLocker && ln -s $ImageLocation /productLocker" + Write-Verbose "Updating /productLocker on $VMHost..." + $result = Invoke-SshCommand -SSHSession $sshSession -Command $command -EnsureConnection:$false + if ($result.ExitStatus -eq 0) { + Write-Host "Update link /productLocker successfully." -ForegroundColor Green + } else { + Write-Error "Failed to update link /productLocker: $($result.Error)" + } + + $result = Invoke-SshCommand -SSHSession $sshSession -Command "readlink /productLocker" -EnsureConnection:$false + Write-Verbose "The link /productLocker after change: $($result.Output)" + } +} + +Function Update-VMToolsConfInVM { +<# +.Synopsis + This cmdlet updates the tools.conf content in guest OS. + +.Description + This cmdlet copies the tools.conf in gueset OS of virtual machine to localhost, + then updates it locally by setting "vmtoolsd.level = info" and copies it back to the guest OS. + +.PARAMETER VM + Specifies the virtual machine to update. + +.Parameter GuestUser + Specifies the user name you want to use for authenticating with the guest OS. + +.Parameter GuestPassword + Specifies the password you want to use for authenticating with the guest OS. + +.Example + C:\PS> Import-Module .\VMToolsManagement.psm1 + C:\PS> $SampleVM = get-vm "MyVMName" + C:\PS> Update-VMToolsConfInVM -VM $SampleVM -GuestUser -GuestPassword + + Update tools.conf of 111394-RHEL-6.8-0 successfully. + + Updates the tools.conf in $SampleVM, changes the vmtoolsd log level to info ("vmtoolsd.level = info") for example. + +.NOTES + This cmdlet updates the tools.conf in guest OS. A VMTools should already be running in the guest OS. + +.NOTES + Author : Daoyuan Wang + Author email : daoyuanw@vmware.com + Version : 1.0 + ==========Tested Against Environment========== + VMware vSphere Hypervisor(ESXi) Version : 6.5 (build 4564106) + VMware vCenter Server Version : 6.5 (build 4602587) + PowerCLI Version : PowerCLI 6.5 (build 4624819) + PowerShell Version : 5.1 +#> + + [CmdletBinding()] + + Param ( + [Parameter(Mandatory=$true, + ValueFromPipeLine = $true, + ValueFromPipelinebyPropertyName=$True, + Position = 0)] + [ValidateNotNullOrEmpty()] + [VMware.VimAutomation.ViCore.Types.V1.Inventory.VirtualMachine] $VM, + + [Parameter(Mandatory=$true)] + [String] $GuestUser, + + [Parameter(Mandatory=$true)] + [AllowEmptyString()] + [String] $GuestPassword + ) + + Process { + $vmGuest = Get-VMGuest $VM + $OsName = $vmGuest.OSFullName + $guestToolsConfFile = "" + $localToolsConfFile = ".\tools.conf" + + # Determine the tools.conf path in guest OS + if (($OsName -match "Linux") ` + -or ($OsName -match "FreeBSD") ` + -or ($OsName -match "Solaris")) { + $guestToolsConfFile = '/etc/vmware-tools/tools.conf' + } elseif (($OsName -match "Windows Server 2003") ` + -or ($OsName -match "Windows Server 2000") ` + -or ($OsName -match "Windows XP")) { + $guestToolsConfFile = 'C:\Documents and Settings\All Users\Application Data\VMware\VMware Tools\tools.conf' + } elseif ($OsName -match "Windows") { + $guestToolsConfFile = 'C:\ProgramData\VMware\VMware Tools\tools.conf' + } elseif ($OsName -match "Mac") { + $guestToolsConfFile = '/Library/Application Support/VMware Tools/tools.conf' + } else { + Throw "Unknown tools.conf path on OS: $OsName" + } + + # Get the tools.conf from guest OS to localhost, ignore the error if tools.conf was not found in guest OS + Write-Verbose "Copy tools.conf from $VM to localhost..." + $lastError = $Error[0] + Copy-VMGuestFile -Source $guestToolsConfFile -Destination $localToolsConfFile -VM $VM -GuestToLocal ` + -GuestUser $GuestUser -GuestPassword $GuestPassword -Force -ErrorAction:SilentlyContinue + + # The tools.conf doesn't exist in guest OS, create an empty one locally + if (($Error[0] -ne $lastError) -and ($Error[0] -notmatch 'tools.conf was not found')) { + Write-Error "Failed to copy tools.conf from $VM" + return + } elseif (-not (Test-Path $localToolsConfFile)) { + Set-Content $localToolsConfFile $null + } + + ############################################################################# + # Updates tools.conf by setting vmtoolsd.level = info, just for example. + ############################################################################# + $confContent = Get-Content $localToolsConfFile + $updatedContent = "vmtoolsd.level = info" + + Write-Verbose "Editing tools.conf (set 'vmtoolsd.level = info' for example)..." + if ($confContent -match "vmtoolsd\.level") { + $confContent -replace "vmtoolsd\.level.*", $updatedContent | Set-Content $localToolsConfFile + } elseif ($confContent -match "logging") { + Add-Content $localToolsConfFile $updatedContent + } else { + Add-Content $localToolsConfFile "[logging]`nlog=true" + Add-Content $localToolsConfFile $updatedContent + } + + # Upload the changed tools.conf to guest OS + try { + Write-Verbose "Copy local tools.conf to $VM..." + Copy-VMGuestFile -Source $localToolsConfFile -Destination $guestToolsConfFile -VM $VM -LocalToGuest ` + -GuestUser $GuestUser -GuestPassword $GuestPassword -Force -ErrorAction:Stop + } catch { + Write-Error "Failed to update tools.conf of $VM" + return + } + Write-Host "The tools.conf updated in $VM successfully." -ForegroundColor Green + } +} + +Function Invoke-VMToolsVIBInstall { +<# +.SYNOPSIS + This cmdlet installs VMTool VIB in ESXi hosts. + +.DESCRIPTION + This cmdlet installs VMTool VIB in specified ESXi hosts. + +.PARAMETER VMHost + Specifies the ESXi hosts which you want to install VMTool VIB in. + +.PARAMETER ToolsVibUrl + Specifies the URL of VMTools VIB package which you want to install in ESXi hosts. + +.EXAMPLE + C:\PS> Import-Module .\VMToolsManagement.psm1 + C:\PS> $VCServer = Connect-VIServer -Server -User -Password . + C:\PS> $viBurl = "http:///VMware_locker_tools-light_6.5.0-10.2.0.6085460.vib" + C:\PS> Get-VMHost -Server $VCServer | Invoke-VMToolsVIBInstall -ToolsVibUrl $viBurl + + Install VMTool VIB in $VCServer. + +.EXAMPLE + C:\PS> Invoke-VMToolsVIBInstall -VMHost "MyESXiHostName" -ToolsVibUrl $viBurl + + Installs VMTools VIB package successfully. + + Installs VMTool VIB in the "MyESXiHostName" ESXi host. + +.EXAMPLE + C:\PS> Get-VMHost -Location "MyClusterName" | Invoke-VMToolsVIBInstall -ToolsVibUrl $vib + + Installs VMTool VIB in ESXi host of the "MyClusterName" cluster. + +.NOTES + This cmdlet assumes that you are connected to at least one vCenter Server system. + +.NOTES + Author : Daoyuan Wang + Author email : daoyuanw@vmware.com + Version : 1.0 + ==========Tested Against Environment========== + VMware vSphere Hypervisor(ESXi) Version : 6.5 (build 4564106) + VMware vCenter Server Version : 6.5 (build 4602587) + PowerCLI Version : PowerCLI 6.5 (build 4624819) + PowerShell Version : 5.1 +#> + + [CmdletBinding()] + + Param ( + [Parameter(Mandatory=$true, + ValueFromPipeLine = $true, + ValueFromPipelinebyPropertyName=$True, + Position = 0)] + [VMware.VimAutomation.ViCore.Types.V1.Inventory.VMHost[]] $VMHost, + + [Parameter(Mandatory=$true)] + [String] $ToolsVibUrl + ) + + Process { + foreach ($_ in $VMHost) { + $esxcli = Get-EsxCLI -VMHost $_ -V2 + + $result = $esxcli.software.vib.list.Invoke() | where {$_.name -match 'tools'} + Write-Verbose "Existing tools VIB on $_ before installing: $($result.Name)_$($result.Version)" + + # Install VIBs + Write-Verbose "Installing $ToolsVibUrl on $($_.Name)..." + $Error.Clear() + $cliArgs = $esxcli.software.vib.install.CreateArgs() + $cliArgs.viburl = $ToolsVibUrl + $cliArgs.nosigcheck = $true + $cliArgs.force = $true + $result = $esxcli.software.vib.install.Invoke($cliArgs) + if ($Error) { + Write-Error "Failed to install VMTools VIB package!" + } else { + Write-Verbose $result.Message + $result = $esxcli.software.vib.list.Invoke() | where {$_.name -match 'tools'} + Write-Verbose "Tools VIB on $_ after installing: $($result.Name)_$($result.Version)" + Write-Host "VMTools VIB package installed on $_ successfully." -ForegroundColor Green + } + } + } +} + +Function Invoke-VMToolsUpgradeInVMs { +<# +.SYNOPSIS + This cmdlet upgrades VMTools to the version bundled by ESXi host. + +.DESCRIPTION + This cmdlet upgrades VMTools of specified virtual machines to the version + bundled by ESXi host. You can also specify the number of virtual machines + to upgrade in parallel. + +.PARAMETER VM + Specifies the virtual machines you want to upgrade VMTools of. + +.PARAMETER GuestOSType + Specifies the guest OS type of the virtual machines. + +.PARAMETER VersionToUpgrade + Specifies the current running VMTools version of virtual machines. + +.PARAMETER MaxParallelUpgrades + Specifies the max virtual machine numbers to upgrade in parallel. + +.EXAMPLE + C:\PS> Import-Module .\VMToolsManagement.psm1 + C:\PS> $VCServer = Connect-VIServer -Server -User -Password + C:\PS> Get-VM -Server $VCServer | Invoke-VMToolsUpgradeInVMs -MaxParallelUpgrades 5 + + Upgrades VMTools of all virtual machines in the $VCServer vCetner Server, 5 at a time in parallel. + +.EXAMPLE + C:\PS> Get-VM | Invoke-VMToolsUpgradeInVMs -GuestOSType windows -MaxParallelUpgrades 1 | ft -Autosize + + Upgrade result: + + VmName UpgradeResult ToolsVersion ToolsVersionStatus TotalSeconds Message + ------ ------------- ------------ ------------------ ------------ ------- + 111167-Win-7-Sp1-64-Enterprise-NoTools-2 Completed 10.1.0 guestToolsCurrent 102 Upgrade VMTools successfully + 111393-RHEL-Server-7.2 Skipped 10.0.0 guestToolsNeedUpgrade 0 Guest OS type does not meet condtion 'windows' + 111305-Windows-Server2016 Completed 10.1.0 guestToolsCurrent 144 Upgrade VMTools successfully + + Upgrades VMTools of windows virtual machines one by one. + +.EXAMPLE + C:\PS> Get-VM -Location "MyClusterName" | Invoke-VMToolsUpgradeInVMs -MaxParallelUpgrades 2 | ft -Autosize + + Upgrade result: + + VmName UpgradeResult ToolsVersion ToolsVersionStatus TotalSeconds Message + ------ ------------- ------------ ------------------ ------------ ------- + 111167-Win-7-Sp1-64-Enterprise-NoTools-2 Failed 10.0.0 guestToolsNeedUpgrade 0 The required VMware Tools ISO image does not exist or is inaccessible. + 111393-RHEL-Server-7.2 Completed 10.1.0 guestToolsCurrent 100 Upgrade VMTools successfully + + Upgrades VMTools of virtual machines in the "MyClusterName" cluster, 2 at a time. + +.EXAMPLE + C:\PS> Get-VMHost "MyESXiHostName" | Get-VM | Invoke-VMToolsUpgradeInVMs -MaxParallelUpgrades 5 + + Upgrades VMTools of virtual machines on the "MyESXiHostName" ESXi host, 5 at a time. + +.NOTES + This cmdlet assumes an old VMTools is already running in the virtual machine. + +.NOTES + Author : Daoyuan Wang + Author email : daoyuanw@vmware.com + Version : 1.0 + ==========Tested Against Environment========== + VMware vSphere Hypervisor(ESXi) Version : 6.5 (build 4564106) + VMware vCenter Server Version : 6.5 (build 4602587) + PowerCLI Version : PowerCLI 6.5 (build 4624819) + PowerShell Version : 5.1 +#> + + [CmdletBinding()] + + Param ( + [Parameter(Mandatory=$true, + ValueFromPipeLine = $true, + ValueFromPipelinebyPropertyName=$True, + Position = 0)] + [ValidateNotNullOrEmpty()] + [VMware.VimAutomation.ViCore.Types.V1.Inventory.VirtualMachine[]] $VM, + + [Parameter(Mandatory=$false)] + [ValidateSet("linux", "windows")] + [String] $GuestOSType, + + [Parameter(Mandatory=$false)] + [String] $VersionToUpgrade, + + [Parameter(Mandatory=$false)] + [ValidateRange(1, 5)] + [Int] $MaxParallelUpgrades = 1 + ) + + Begin { + $RunspacePool = [runspacefactory]::CreateRunspacePool( + 1, #Min Runspaces + $MaxParallelUpgrades #Max Runspaces + ) + + $RunspacePool.Open() + + $jobs = New-Object System.Collections.ArrayList + $result = @() + } + + Process { + foreach ($_ in $VM) { + $vmView = Get-View $_ -Property Guest + $toolsVersion = $_.Guest.ToolsVersion + $toolsVersionStatus = $vmView.Guest.ToolsVersionStatus + + # Skip if VMTools doesn't need to upgrade + if ($toolsVersionStatus -ne "guestToolsNeedUpgrade") { + Write-Host "No VMTools need to upgrade!`nVM: '$_', ToolsVersionStatus: '$toolsVersionStatus'" + $result += [pscustomobject]@{ + VmName = $_.Name + UpgradeResult = "Skipped" + ToolsVersion = $toolsVersion + ToolsVersionStatus = $toolsVersionStatus + TotalSeconds = 0 + Message = "No VMTools need to upgrade!" + } + continue + } + + # Skip if current VMTools doesn't meet to specified version + if ($VersionToUpgrade -and ($toolsVersion -notmatch $VersionToUpgrade)) { + Write-Host "Current ToolsVersion in $_ is: $toolsVersion,"` + "does not meet condtion `'$VersionToUpgrade`', skipping it..." -ForegroundColor Yellow + $result += [pscustomobject]@{ + VmName = $_.Name + UpgradeResult = "Skipped" + ToolsVersion = $toolsVersion + ToolsVersionStatus = $toolsVersionStatus + TotalSeconds = 0 + Message = "Current VMTools version does not meet condtion `'$VersionToUpgrade`'" + } + continue + } + + # Create a thread to upgrade VMTools for each virtual machine + $PSThread = [powershell]::Create() + $PSThread.RunspacePool = $RunspacePool + + # Script content to upgrade VMTools + $PSThread.AddScript({ + Param ( + $vcServer, + $session, + $vmId, + $GuestOSType + ) + # Load PowerCLI module and connect to VCServer, as child thread environment is independent with parent + if(-not $global:DefaultVIServer) { + $moduleName = "vmware.vimautomation.core" + if(-not (Get-Module | Where {$_.name -eq $moduleName})) { + try { + Import-Module $moduleName -ErrorAction SilentlyContinue | Out-Null + } + catch { + Throw "Failed to load PowerCLI module('$moduleName')" + } + } + try { + $server = Connect-VIServer -Server $vcserver -session $session -Force + } + catch { + Throw "Failed to connect to VI server: $vcserver" + } + } + + # Retrieves VM + $vm = Get-VM -Id $vmId + + $ThreadID = [appdomain]::GetCurrentThreadId() + Write-Verbose “Thread[$ThreadID]: Beginning Update-Tools for $vm†+ + if ($vm.PowerState -ne 'PoweredOn') { + Write-Host "Powering on VM: $vm..." + Start-VM $vm | Out-Null + $vm = Get-VM $vm + } + + # Wait for OS and VMTools starting up + $timeOut = 60*10 #seconds + $refreshInterval = 5 #seconds + $count = $timeOut/$refreshInterval + while (($vm.Guest.ExtensionData.ToolsRunningStatus -ne "guestToolsRunning") ` + -or (-not $vm.Guest.GuestFamily)) { + $count -= 1 + if ($count -lt 0) { + Write-Error "VMTools doesn't start up in $timeOut seconds, please check if $vm is hung!" + break + } + Write-Verbose "Waiting for VMTools running in $vm before upgrading..." + Start-Sleep -Seconds $refreshInterval + } + + # Skip if virtual machine doesn't meet specified guest OS type + if ($GuestOSType -and ($vm.Guest.GuestFamily -notmatch $GuestOSType)) { + Write-Host "GuestFamily of $vm is: $($vm.Guest.GuestFamily),"` + "does not meet condition `'$GuestOSType`', skipping it..." -ForegroundColor Yellow + # upgrade result + [pscustomobject]@{ + VmName = $vm.Name + UpgradeResult = "Skipped" + ToolsVersion = $vm.Guest.ToolsVersion + ToolsVersionStatus = $vm.Guest.ExtensionData.ToolsVersionStatus + TotalSeconds = 0 + Message = "Guest OS type does not meet condtion `'$GuestOSType`'" + } + Disconnect-VIServer $server -Confirm:$false + return + } + + # Upgrade VMTools and check the tools version status + Write-Host "Upgrading VMTools for VM: $vm..." + $task = Update-Tools -VM $vm -RunAsync + $task | Wait-Task + $task = Get-Task -Id $task.Id + + if ($task.State -eq "Success") { + $upgradeResult = "Completed" + $message = "Upgrade VMTools successfully" + Write-Host "Upgrade VMTools successfully for VM: $vm" -ForegroundColor Green + } else { + $upgradeResult = "Failed" + $message = $task.ExtensionData.Info.Error.LocalizedMessage + Write-Error "Failed to upgrade VMTools for VM: $vm" + } + $vm = Get-VM $vm + # Upgrade result to return + [pscustomobject]@{ + VmName = $vm.Name + UpgradeResult = $upgradeResult + ToolsVersion = $vm.Guest.ToolsVersion + ToolsVersionStatus = $vm.Guest.ExtensionData.ToolsVersionStatus + TotalSeconds = [math]::Floor(($task.FinishTime).Subtract($task.StartTime).TotalSeconds) + Message = $message + } + Write-Verbose “Thread[$ThreadID]: Ending Update-Tools for $vm†+ }) | Out-Null + $vc = $Global:DefaultVIServer.ServiceUri.Host + $vcSession = $Global:DefaultVIServer.SessionSecret + $PSThread.AddArgument($vc).AddArgument($vcSession).AddArgument($_.Id).AddArgument($GuestOSType) | Out-Null + + # Start thread + $Handle = $PSThread.BeginInvoke() + $job = New-Object System.Object + $job | Add-Member -type NoteProperty -name Thread -value $PSThread + $job | Add-Member -type NoteProperty -name Handle -value $Handle + $jobs.Add($job) | Out-Null + + Write-Verbose (“Available Runspaces in RunspacePool: {0}†-f $RunspacePool.GetAvailableRunspaces()) + } + } + + End { + #Verify all threads completed + while (($jobs | Where {$_.Handle.iscompleted -ne ‘Completed’}).Count -gt 0) { + Start-Sleep -Seconds 5 + } + + $upgradeResult = $jobs | foreach { + $_.Thread.EndInvoke($_.Handle) + $_.Thread.Dispose() + } + $result += $upgradeResult + $result + + $RunspacePool.Close() + $RunspacePool.Dispose() + } +} + +Export-ModuleMember *-* \ No newline at end of file From 9f2fc3ec065cb451317a2cbb9f9e66691b7fe55e Mon Sep 17 00:00:00 2001 From: William Lam Date: Tue, 14 Nov 2017 06:25:50 -0800 Subject: [PATCH 095/112] Adding several new Content Library functions --- Modules/ContentLibrary/ContentLibrary.psm1 | 386 ++++++++++++++++++++- 1 file changed, 385 insertions(+), 1 deletion(-) diff --git a/Modules/ContentLibrary/ContentLibrary.psm1 b/Modules/ContentLibrary/ContentLibrary.psm1 index 824cb16..da83745 100644 --- a/Modules/ContentLibrary/ContentLibrary.psm1 +++ b/Modules/ContentLibrary/ContentLibrary.psm1 @@ -25,12 +25,29 @@ $results = @() foreach($libraryID in $libaryIDs) { - $library = $contentLibaryService.get($libraryId) + $library = $contentLibaryService.get($libraryID) # Use vCenter REST API to retrieve name of Datastore that is backing the Content Library $datastoreService = Get-CisService com.vmware.vcenter.datastore $datastore = $datastoreService.get($library.storage_backings.datastore_id) + if($library.publish_info.published) { + $published = $library.publish_info.published + $publishedURL = $library.publish_info.publish_url + $externalReplication = $library.publish_info.persist_json_enabled + } else { + $published = $library.publish_info.published + $publishedURL = "N/A" + $externalReplication = "N/A" + } + + if($library.subscription_info) { + $subscribeURL = $library.subscription_info.subscription_url + $published = "N/A" + } else { + $subscribeURL = "N/A" + } + if(!$LibraryName) { $libraryResult = [pscustomobject] @{ Id = $library.Id; @@ -38,6 +55,10 @@ Type = $library.Type; Description = $library.Description; Datastore = $datastore.name; + Published = $published; + PublishedURL = $publishedURL; + JSONPersistence = $externalReplication; + SubscribedURL = $subscribeURL; CreationTime = $library.Creation_Time; } $results+=$libraryResult @@ -49,6 +70,10 @@ Type = $library.Type; Description = $library.Description; Datastore = $datastore.name; + Published = $published; + PublishedURL = $publishedURL; + JSONPersistence = $externalReplication; + SubscribedURL = $subscribeURL; CreationTime = $library.Creation_Time; } $results+=$libraryResult @@ -194,4 +219,363 @@ Function Get-ContentLibraryItemFiles { } } $results +} + +Function Set-ContentLibrary { +<# + .NOTES + =========================================================================== + Created by: William Lam + Organization: VMware + Blog: www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + .DESCRIPTION + This function updates the JSON Persistence property for a given Content Library + .PARAMETER LibraryName + The name of a vSphere Content Library + .EXAMPLE + Set-ContentLibraryItems -LibraryName Test -JSONPersistenceEnabled + .EXAMPLE + Set-ContentLibraryItems -LibraryName Test -JSONPersistenceDisabled +#> + param( + [Parameter(Mandatory=$true)][String]$LibraryName, + [Parameter(Mandatory=$false)][Switch]$JSONPersistenceEnabled, + [Parameter(Mandatory=$false)][Switch]$JSONPersistenceDisabled + ) + + $contentLibaryService = Get-CisService com.vmware.content.library + $libaryIDs = $contentLibaryService.list() + + $found = $false + foreach($libraryID in $libaryIDs) { + $library = $contentLibaryService.get($libraryId) + if($library.name -eq $LibraryName) { + $found = $true + break + } + } + + if($found) { + $localLibraryService = Get-CisService -Name "com.vmware.content.local_library" + + if($JSONPersistenceEnabled) { + $jsonPersist = $true + } else { + $jsonPersist = $false + } + + $updateSpec = $localLibraryService.Help.update.update_spec.Create() + $updateSpec.type = $library.type + $updateSpec.publish_info.authentication_method = $library.publish_info.authentication_method + $updateSpec.publish_info.persist_json_enabled = $jsonPersist + Write-Host "Updating JSON Persistence configuration setting for $LibraryName ..." + $localLibraryService.update($library.id,$updateSpec) + } else { + Write-Host "Unable to find Content Library $Libraryname" + } +} + +Function New-ExtReplicatedContentLibrary { +<# + .NOTES + =========================================================================== + Created by: William Lam + Organization: VMware + Blog: www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + .DESCRIPTION + This function creates a new Subscriber Content Library from a JSON Persisted + Content Library that has been externally replicated + .PARAMETER LibraryName + The name of the new vSphere Content Library + .PARAMETER DatastoreName + The name of the vSphere Datastore which contains JSON Persisted configuration file + .PARAMETER SubscribeLibraryName + The name fo the root directroy of the externally replicated Content Library residing on vSphere Datastore + .PARAMETER AutoSync + Whether or not to Automatically sync content + .PARAMETER OnDemand + Only sync content when requested + .EXAMPLE + New-ExtReplicatedContentLibrary -LibraryName Bar -DatastoreName iSCSI-02 -SubscribeLibraryName myExtReplicatedLibrary + .EXAMPLE + New-ExtReplicatedContentLibrary -LibraryName Bar -DatastoreName iSCSI-02 -SubscribeLibraryName myExtReplicatedLibrary -AutoSync $false -OnDemand $true +#> + param( + [Parameter(Mandatory=$true)][String]$LibraryName, + [Parameter(Mandatory=$true)][String]$DatastoreName, + [Parameter(Mandatory=$true)][String]$SubscribeLibraryName, + [Parameter(Mandatory=$false)][Boolean]$AutoSync=$false, + [Parameter(Mandatory=$false)][Boolean]$OnDemand=$true + ) + + $datastore = Get-Datastore -Name $DatastoreName + + if($datastore) { + $datastoreId = $datastore.ExtensionData.MoRef.Value + $datastoreUrl = $datastore.ExtensionData.Info.Url + $subscribeUrl = $datastoreUrl + $SubscribeLibraryName + "/lib.json" + + $subscribeLibraryService = Get-CisService -Name "com.vmware.content.subscribed_library" + + $StorageSpec = [pscustomobject] @{ + datastore_id = $datastoreId; + type = "DATASTORE"; + } + + $UniqueChangeId = [guid]::NewGuid().tostring() + + $createSpec = $subscribeLibraryService.Help.create.create_spec.Create() + $createSpec.name = $LibraryName + $addResults = $createSpec.storage_backings.Add($StorageSpec) + $createSpec.subscription_info.automatic_sync_enabled = $false + $createSpec.subscription_info.on_demand = $true + $createSpec.subscription_info.subscription_url = $subscribeUrl + $createSpec.subscription_info.authentication_method = "NONE" + $createSpec.type = "SUBSCRIBED" + Write-Host "Creating new Externally Replicated Content Library called $LibraryName ..." + $library = $subscribeLibraryService.create($UniqueChangeId,$createSpec) + } +} + +Function Remove-SubscribedContentLibrary { +<# + .NOTES + =========================================================================== + Created by: William Lam + Organization: VMware + Blog: www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + .DESCRIPTION + This function deletes a Subscriber Content Library + .PARAMETER LibraryName + The name of the new vSphere Content Library to delete + .EXAMPLE + Remove-SubscribedContentLibrary -LibraryName Bar +#> + param( + [Parameter(Mandatory=$true)][String]$LibraryName + ) + + $contentLibaryService = Get-CisService com.vmware.content.library + $libaryIDs = $contentLibaryService.list() + + $found = $false + foreach($libraryID in $libaryIDs) { + $library = $contentLibaryService.get($libraryId) + if($library.name -eq $LibraryName) { + $found = $true + break + } + } + + if($found) { + $subscribeLibraryService = Get-CisService -Name "com.vmware.content.subscribed_library" + + Write-Host "Deleting Subscribed Content Library $LibraryName ..." + $subscribeLibraryService.delete($library.id) + } else { + Write-Host "Unable to find Content Library $LibraryName" + } +} + +Function New-LocalContentLibrary { +<# + .NOTES + =========================================================================== + Created by: William Lam + Organization: VMware + Blog: www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + .DESCRIPTION + This function creates a new Subscriber Content Library from a JSON Persisted + Content Library that has been externally replicated + .PARAMETER LibraryName + The name of the new vSphere Content Library + .PARAMETER DatastoreName + The name of the vSphere Datastore to store the Content Library + .PARAMETER Publish + Whther or not to publish the Content Library, this is required for JSON Peristence + .PARAMETER JSONPersistence + Whether or not to enable JSON Persistence which enables external replication of Content Library + .EXAMPLE + New-LocalContentLibrary -LibraryName Foo -DatastoreName iSCSI-01 -Publish $true + .EXAMPLE + New-LocalContentLibrary -LibraryName Foo -DatastoreName iSCSI-01 -Publish $true -JSONPersistence $true +#> + param( + [Parameter(Mandatory=$true)][String]$LibraryName, + [Parameter(Mandatory=$true)][String]$DatastoreName, + [Parameter(Mandatory=$false)][Boolean]$Publish=$true, + [Parameter(Mandatory=$false)][Boolean]$JSONPersistence=$false + ) + + $datastore = Get-Datastore -Name $DatastoreName + + if($datastore) { + $datastoreId = $datastore.ExtensionData.MoRef.Value + $localLibraryService = Get-CisService -Name "com.vmware.content.local_library" + + $StorageSpec = [pscustomobject] @{ + datastore_id = $datastoreId; + type = "DATASTORE"; + } + + $UniqueChangeId = [guid]::NewGuid().tostring() + + $createSpec = $localLibraryService.Help.create.create_spec.Create() + $createSpec.name = $LibraryName + $addResults = $createSpec.storage_backings.Add($StorageSpec) + $createSpec.publish_info.authentication_method = "NONE" + $createSpec.publish_info.persist_json_enabled = $JSONPersistence + $createSpec.publish_info.published = $Publish + $createSpec.type = "LOCAL" + Write-Host "Creating new Local Content Library called $LibraryName ..." + $library = $localLibraryService.create($UniqueChangeId,$createSpec) + } +} + +Function Remove-LocalContentLibrary { +<# + .NOTES + =========================================================================== + Created by: William Lam + Organization: VMware + Blog: www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + .DESCRIPTION + This function deletes a Local Content Library + .PARAMETER LibraryName + The name of the new vSphere Content Library to delete + .EXAMPLE + Remove-LocalContentLibrary -LibraryName Bar +#> + param( + [Parameter(Mandatory=$true)][String]$LibraryName + ) + + $contentLibaryService = Get-CisService com.vmware.content.library + $libaryIDs = $contentLibaryService.list() + + $found = $false + foreach($libraryID in $libaryIDs) { + $library = $contentLibaryService.get($libraryId) + if($library.name -eq $LibraryName) { + $found = $true + break + } + } + + if($found) { + $localLibraryService = Get-CisService -Name "com.vmware.content.local_library" + + Write-Host "Deleting Local Content Library $LibraryName ..." + $localLibraryService.delete($library.id) + } else { + Write-Host "Unable to find Content Library $LibraryName" + } +} + +Function Copy-ContentLibrary { +<# + .NOTES + =========================================================================== + Created by: William Lam + Organization: VMware + Blog: www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + .DESCRIPTION + This function copies all library items from one Content Library to another + .PARAMETER SourceLibaryName + The name of the source Content Library to copy from + .PARAMETER DestinationLibaryName + The name of the desintation Content Library to copy to + .PARAMETER DeleteSourceFile + Whther or not to delete library item from the source Content Library after copy + .EXAMPLE + Copy-ContentLibrary -SourceLibaryName Foo -DestinationLibaryName Bar + .EXAMPLE + Copy-ContentLibrary -SourceLibaryName Foo -DestinationLibaryName Bar -DeleteSourceFile $true +#> + param( + [Parameter(Mandatory=$true)][String]$SourceLibaryName, + [Parameter(Mandatory=$true)][String]$DestinationLibaryName, + [Parameter(Mandatory=$false)][Boolean]$DeleteSourceFile=$false + ) + + $sourceLibraryId = (Get-ContentLibrary -LibraryName $SourceLibaryName).Id + if($sourceLibraryId -eq $null) { + Write-Host -ForegroundColor red "Unable to find Source Content Library named $SourceLibaryName" + exit + } + $destinationLibraryId = (Get-ContentLibrary -LibraryName $DestinationLibaryName).Id + if($destinationLibraryId -eq $null) { + Write-Host -ForegroundColor Red "Unable to find Destination Content Library named $DestinationLibaryName" + break + } + + $sourceItemFiles = Get-ContentLibraryItems -LibraryName $SourceLibaryName + if($sourceItemFiles -eq $null) { + Write-Host -ForegroundColor red "Unable to retrieve Content Library Items from $SourceLibaryName" + break + } + + $contentLibaryItemService = Get-CisService com.vmware.content.library.item + + foreach ($sourceItemFile in $sourceItemFiles) { + # Check to see if file already exists in destination Content Library + $result = Get-ContentLibraryItems -LibraryName $DestinationLibaryName -LibraryItemName $sourceItemFile.Name + + if($result -eq $null) { + # Create CopySpec + $copySpec = $contentLibaryItemService.Help.copy.destination_create_spec.Create() + $copySpec.library_id = $destinationLibraryId + $copySpec.name = $sourceItemFile.Name + $copySpec.description = $sourceItemFile.Description + # Create random Unique Copy Id + $UniqueChangeId = [guid]::NewGuid().tostring() + + # Perform Copy + try { + Write-Host -ForegroundColor Cyan "Copying" $sourceItemFile.Name "..." + $copyResult = $contentLibaryItemService.copy($UniqueChangeId, $sourceItemFile.Id, $copySpec) + } catch { + Write-Host -ForegroundColor Red "Failed to copy" $sourceItemFile.Name + $Error[0] + break + } + + # Delete source file if set to true + if($DeleteSourceFile) { + try { + Write-Host -ForegroundColor Magenta "Deleteing" $sourceItemFile.Name "..." + #$deleteResult = $contentLibaryItemService.delete($sourceItemFile.Id) + } catch { + Write-Host -ForegroundColor Red "Failed to delete" $sourceItemFile.Name + $Error[0] + break + } + } + } else { + Write-Host -ForegroundColor Yellow "Skipping" $sourceItemFile.Name "already exists" + + # Delete source file if set to true + if($DeleteSourceFile) { + try { + Write-Host -ForegroundColor Magenta "Deleteing" $sourceItemFile.Name "..." + #$deleteResult = $contentLibaryItemService.delete($sourceItemFile.Id) + } catch { + Write-Host -ForegroundColor Red "Failed to delete" $sourceItemFile.Name + break + } + } + } + } } \ No newline at end of file From 2d429c62717c4fac6590625738ce6e2bf9cd7a4e Mon Sep 17 00:00:00 2001 From: William Lam Date: Tue, 14 Nov 2017 06:45:58 -0800 Subject: [PATCH 096/112] Forgot to un-comment the delete code --- Modules/ContentLibrary/ContentLibrary.psm1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/ContentLibrary/ContentLibrary.psm1 b/Modules/ContentLibrary/ContentLibrary.psm1 index da83745..e2fef41 100644 --- a/Modules/ContentLibrary/ContentLibrary.psm1 +++ b/Modules/ContentLibrary/ContentLibrary.psm1 @@ -556,7 +556,7 @@ Function Copy-ContentLibrary { if($DeleteSourceFile) { try { Write-Host -ForegroundColor Magenta "Deleteing" $sourceItemFile.Name "..." - #$deleteResult = $contentLibaryItemService.delete($sourceItemFile.Id) + $deleteResult = $contentLibaryItemService.delete($sourceItemFile.Id) } catch { Write-Host -ForegroundColor Red "Failed to delete" $sourceItemFile.Name $Error[0] @@ -570,7 +570,7 @@ Function Copy-ContentLibrary { if($DeleteSourceFile) { try { Write-Host -ForegroundColor Magenta "Deleteing" $sourceItemFile.Name "..." - #$deleteResult = $contentLibaryItemService.delete($sourceItemFile.Id) + $deleteResult = $contentLibaryItemService.delete($sourceItemFile.Id) } catch { Write-Host -ForegroundColor Red "Failed to delete" $sourceItemFile.Name break From a47369a29572e0b0458a3d81e1c31621688c1b56 Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 20 Nov 2017 15:22:21 +0000 Subject: [PATCH 097/112] Minor Spelling corrections generted->generated Minor Spelling corrections generted->generated in Get-Help examples --- Modules/VMware.VMEncryption/VMware.VMEncryption.psm1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/VMware.VMEncryption/VMware.VMEncryption.psm1 b/Modules/VMware.VMEncryption/VMware.VMEncryption.psm1 index e91b0c5..023087c 100644 --- a/Modules/VMware.VMEncryption/VMware.VMEncryption.psm1 +++ b/Modules/VMware.VMEncryption/VMware.VMEncryption.psm1 @@ -882,7 +882,7 @@ Function Set-VMEncryptionKey { C:\PS>$VM|Set-VMEncryptionKey -KMSClusterId $KMSCluster.Id -Deep Deep rekeys the VM Home and all its disks using a new key. - The key is generted from the KMS whose clusterId is $KMSCluster.Id. + The key is generated from the KMS whose clusterId is $KMSCluster.Id. .NOTES This cmdlet assumes there is already a KMS in vCenter Server. If VM is not encrypted, the cmdlet quits. @@ -1037,7 +1037,7 @@ Function Set-VMDiskEncryptionKey { C:\PS>$HardDisk| Set-VMDiskEncryptionKey -VM $VM -KMSClusterId $KMSCluster.Id -Deep Deep rekeys all the disks of the $VM using a new key. - The key is generted from the KMS whose clusterId is $KMSCluster.Id. + The key is generated from the KMS whose clusterId is $KMSCluster.Id. .NOTES This cmdlet assumes there is already a KMS in vCenter Server. From dc6b02a95ae5f1f3ee7d53d6f7747682190e5345 Mon Sep 17 00:00:00 2001 From: William Lam Date: Tue, 21 Nov 2017 07:39:22 -0800 Subject: [PATCH 098/112] Export/Import functions for manual vCenter Server Migrations --- Modules/vCenterManualMigration.psm1 | 475 ++++++++++++++++++++++++++++ 1 file changed, 475 insertions(+) create mode 100644 Modules/vCenterManualMigration.psm1 diff --git a/Modules/vCenterManualMigration.psm1 b/Modules/vCenterManualMigration.psm1 new file mode 100644 index 0000000..a7750b9 --- /dev/null +++ b/Modules/vCenterManualMigration.psm1 @@ -0,0 +1,475 @@ +Function Export-DRSRules { +<# + .NOTES + =========================================================================== + Created by: William Lam + Date: 11/21/2017 + Blog: https://www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + + .SYNOPSIS + Export DRS Rules to JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg + .DESCRIPTION + Export DRS Rules to JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg + .EXAMPLE + Export-DRSRules -Path C:\Users\primp\Desktop\VMworld2017 -Cluster Windows-Cluster +#> + param( + [Parameter(Mandatory=$false)][String]$Path, + [Parameter(Mandatory=$true)][String]$Cluster + ) + + $rules = Get-Cluster -Name $Cluster | Get-DrsRule + + $results = @() + foreach ($rule in $rules) { + $vmNames = @() + $vmIds = $rule.VMIds + + # Reconstruct MoRef ID to VM Object to get Name + foreach ($vmId in $vmIds) { + $vm = New-Object VMware.Vim.ManagedObjectReference + $vm.Type = "VirtualMachine" + $vm.Value = ($vmId -replace "VirtualMachine-","") + $vmView = Get-View $vm + $vmNames += $vmView.name + } + + $rulesObject = [pscustomobject] @{ + Name = $rule.ExtensionData.Name; + Type = $rule.Type; #VMAffinity = 1, VMAntiAffinity = 0 + Enabled = $rule.Enabled; + Mandatory = $rule.ExtensionData.Mandatory + VM = $vmNames + } + $results+=$rulesObject + } + if($Path) { + $fullPath = $Path + "\DRSRules.json" + Write-Host -ForegroundColor Green "Exporting DRS Rules to $fullpath ..." + $results | ConvertTo-Json | Out-File $fullPath + } else { + $results | ConvertTo-Json + } +} + +Function Import-DRSRules { +<# + .NOTES + =========================================================================== + Created by: William Lam + Date: 11/21/2017 + Blog: https://www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + + .SYNOPSIS + Import DRS Rules from JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg + .DESCRIPTION + Import DRS Rules from JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg + .EXAMPLE + Import-DRSRules -Path C:\Users\primp\Desktop\VMworld2017 -Cluster Windows-Cluster +#> + param( + [Parameter(Mandatory=$true)][String]$Path, + [Parameter(Mandatory=$true)][String]$Cluster + ) + + Get-DrsRule -Cluster $cluster | Remove-DrsRule -Confirm:$false | Out-Null + + $DRSRulesFilename = "/DRSRules.json" + $fullPath = $Path + $DRSRulesFilename + $json = Get-Content -Raw $fullPath | ConvertFrom-Json + + foreach ($line in $json) { + $vmArr = @() + $vmNames = $line.vm + foreach ($vmName in $vmNames) { + $vmView = Get-VM -Name $vmName + $vmArr+=$vmView + } + New-DrsRule -Name $line.name -Enabled $line.Enabled -Cluster (Get-Cluster -Name $Cluster) -KeepTogether $line.Type -VM $vmArr + } +} + +Function Export-DRSClusterGroup { +<# + .NOTES + =========================================================================== + Created by: William Lam + Date: 11/21/2017 + Blog: https://www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + + .SYNOPSIS + Export DRS Cluster Group Rules to JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg + .DESCRIPTION + Export DRS Cluster Group Rules to JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg + .EXAMPLE + Export-DRSClusterGroup -Path C:\Users\primp\Desktop\VMworld2017 -Cluster Windows-Cluster +#> + param( + [Parameter(Mandatory=$false)][String]$Path, + [Parameter(Mandatory=$true)][String]$Cluster + ) + + $rules = Get-Cluster -Name $Cluster | Get-DrsClusterGroup + + $results = @() + foreach ($rule in $rules) { + $rulesObject = [pscustomobject] @{ + Name = $rule.ExtensionData.Name; + Type = $rule.GroupType; #VMType = 1, HostType = 0 + Member = $rule.Member + } + $results+=$rulesObject + } + if($Path) { + $fullPath = $Path + "\DRSClusterGroupRules.json" + Write-Host -ForegroundColor Green "Exporting DRS Cluster Group Rules to $fullpath ..." + $results | ConvertTo-Json | Out-File $fullPath + } else { + $results | ConvertTo-Json + } +} + +Function Import-DRSClusterClusterGroup { +<# + .NOTES + =========================================================================== + Created by: William Lam + Date: 11/21/2017 + Blog: https://www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + + .SYNOPSIS + Import DRS Cluster Group Rules from JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg + .DESCRIPTION + Import DRS Cluster Group Rules from JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg + .EXAMPLE + Import-DRSClusterClusterGroup -Path C:\Users\primp\Desktop\VMworld2017 -Cluster Windows-Cluster +#> + param( + [Parameter(Mandatory=$true)][String]$Path, + [Parameter(Mandatory=$true)][String]$Cluster + ) + + $DRSClusterGroupRulesFilename = "\DRSClusterGroupRules.json" + $fullPath = $Path + $DRSClusterGroupRulesFilename + $json = Get-Content -Raw $fullPath | ConvertFrom-Json + + foreach ($line in $json) { + $memberArr = @() + $members = $line.member + + # VMHost Group + if($line.Type -eq 0) { + foreach ($member in $members) { + $memberView = Get-VMhost -Name $member + $memberArr+=$memberView + } + New-DrsClusterGroup -Name $line.name -Cluster (Get-Cluster -Name $Cluster) -VMhost $memberArr + # VM Group + } else { + foreach ($member in $members) { + $memberView = Get-VM -Name $member + $memberArr+=$memberView + } + New-DrsClusterGroup -Name $line.name -Cluster (Get-Cluster -Name $Cluster) -VM $memberArr + } + } +} + +Function Export-Tag { +<# + .NOTES + =========================================================================== + Created by: William Lam + Date: 11/21/2017 + Blog: https://www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + + .SYNOPSIS + Export vSphere Tags and VM Assocations to JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg + .DESCRIPTION + Export vSphere Tags and VM Assocations to JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg + .EXAMPLE + Export-Tag -Path C:\Users\primp\Desktop\VMworld2017 +#> + param( + [Parameter(Mandatory=$false)][String]$Path + ) + + # Export Tag Categories + $tagCatagorys = Get-TagCategory + + $tagCatresults = @() + foreach ($tagCategory in $tagCatagorys) { + $tagCatObj = [pscustomobject] @{ + Name = $tagCategory.Name; + Cardinality = $tagCategory.Cardinality; + Description = $tagCategory.Description; + Type = $tagCategory.EntityType + } + $tagCatresults+=$tagCatObj + } + if($Path) { + $fullPath = $Path + "\AllTagCategory.json" + Write-Host -ForegroundColor Green "Exporting vSphere Tag Category to $fullpath ..." + $tagCatresults | ConvertTo-Json | Out-File $fullPath + } else { + $tagCatresults | ConvertTo-Json + } + + # Export Tags + $tags = Get-Tag + + $tagResults = @() + foreach ($tag in $tags) { + $tagObj = [pscustomobject] @{ + Name = $tag.Name; + Description = $tag.Description; + Category = $tag.Category.Name + } + $tagResults+=$tagObj + } + if($Path) { + $fullPath = $Path + "\AllTag.json" + Write-Host -ForegroundColor Green "Exporting vSphere Tag to $fullpath ..." + $tagResults | ConvertTo-Json | Out-File $fullPath + } else { + $tagResults | ConvertTo-Json + } + + # Export VM to Tag Mappings + $vms = Get-VM + + $vmResults = @() + foreach ($vm in $vms) { + $tagAssignments = $vm | Get-TagAssignment + $tags = @() + foreach ($tagAssignment in $tagAssignments) { + $tag = $tagAssignment.Tag + $tagName = $tag -split "/" + $tags+=$tagName + } + $vmObj = [pscustomobject] @{ + Name = $vm.name; + Tag = $tags + } + $vmResults+=$vmObj + } + if($Path) { + $fullPath = $Path + "\AllTagAssocations.json" + Write-Host -ForegroundColor Green "Exporting VM to vSphere Tag Assignment to $fullpath ..." + $vmResults | ConvertTo-Json | Out-File $fullPath + } else { + $vmResults | ConvertTo-Json + } +} + +Function Import-Tag { +<# + .NOTES + =========================================================================== + Created by: William Lam + Date: 11/21/2017 + Blog: https://www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + + .SYNOPSIS + Import vSphere Tags and VM Assocations from JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg + .DESCRIPTION + Import vSphere Tags and VM Assocations from JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg + .EXAMPLE + Import-Tag -Path C:\Users\primp\Desktop\VMworld2017 +#> + param( + [Parameter(Mandatory=$true)][String]$Path + ) + + $tagCatFilename = "\AllTagCategory.json" + $fullPath = $Path + $tagCatFilename + $tagCategoryJson = Get-Content -Raw $fullPath | ConvertFrom-Json + + $tagFilename = "\AllTag.json" + $fullPath = $Path + $tagFilename + $tagJson = Get-Content -Raw $fullPath | ConvertFrom-Json + + $vmTagFilename = "\AllTagAssocations.json" + $fullPath = $Path + $vmTagFilename + $vmTagJson = Get-Content -Raw $fullPath | ConvertFrom-Json + + # Re-Create Tag Category + foreach ($category in $tagCategoryJson) { + if($category.Cardinality -eq 0) { + $cardinality = "Single" + } else { + $cardinality = "Multiple" + } + New-TagCategory -Name $category.Name -Cardinality $cardinality -Description $category.Description -EntityType $category.Type + } + + # Re-Create Tags + foreach ($tag in $tagJson) { + New-Tag -Name $tag.Name -Description $tag.Description -Category (Get-TagCategory -Name $tag.Category) + } + + # Re-Create VM to Tag Mappings + foreach ($vmTag in $vmTagJson) { + $vm = Get-VM -Name $vmTag.name + $tags = $vmTag.Tag + foreach ($tag in $tags) { + New-TagAssignment -Entity $vm -Tag (Get-Tag -Name $tag) + } + } +} + +Function Export-VMFolder { +<# + .NOTES + =========================================================================== + Created by: William Lam + Date: 11/21/2017 + Blog: https://www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + + .SYNOPSIS + Export vSphere Folder to JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg + .DESCRIPTION + Export vSphere Folder to JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg + .EXAMPLE + Export-VMFolder -Path C:\Users\primp\Desktop\VMworld2017 +#> + param( + [Parameter(Mandatory=$false)][String]$Path + ) + $vms = Get-VM + + $vmFolderResults = @() + foreach ($vm in $vms) { + $vmFolderObj = [pscustomobject] @{ + Name = $vm.name; + Folder = $vm.Folder.Name; + } + $vmFolderResults+=$vmFolderObj + } + if($Path) { + $fullPath = $Path + "\AllVMFolder.json" + Write-Host -ForegroundColor Green "Exporting VM Folders to $fullpath ..." + $vmFolderResults | ConvertTo-Json | Out-File $fullPath + } else { + $vmFolderResults | ConvertTo-Json + } +} + +Function Import-VMFolder { +<# + .NOTES + =========================================================================== + Created by: William Lam + Date: 11/21/2017 + Blog: https://www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + + .SYNOPSIS + Import vSphere Folder from JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg + .DESCRIPTION + Import vSphere Folder from JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg + .EXAMPLE + Import-VMFolder -Path C:\Users\primp\Desktop\VMworld2017 +#> + param( + [Parameter(Mandatory=$true)][String]$Path + ) + + $vmFolderFilename = "\AllVMFolder.json" + $fullPath = $Path + $vmFolderFilename + $vmFolderJson = Get-Content -Raw $fullPath | ConvertFrom-Json + + # Root vm Folder + $rootVMFolder = Get-Folder -Type VM -Name vm + + $folders = $vmFolderJson | Select Folder | Sort-Object -Property Folder -Unique + foreach ($folder in $folders) { + $rootVMFolder | New-Folder -Name $folder.folder + } + + foreach ($vmFolder in $vmFolderJson) { + $vm = Get-VM -Name $vmFolder.Name + $folder = Get-Folder -Name $vmFolder.Folder + Move-VM -VM $vm -Destination $folder + } +} + +Function Export-VMStoragePolicy { +<# + .NOTES + =========================================================================== + Created by: William Lam + Date: 11/21/2017 + Blog: https://www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + + .SYNOPSIS + Export VM Storage Policies to JSON file + .DESCRIPTION + Export VM Storage Policies to JSON file + .EXAMPLE + Export-VMStoragePolicy -Path C:\Users\primp\Desktop\VMworld2017 +#> + param( + [Parameter(Mandatory=$false)][String]$Path + ) + + foreach ($policy in Get-SpbmStoragePolicy) { + $policyName = $policy.Name + if($Path) { + Write-Host -ForegroundColor Green "Exporting Policy $policyName to $Path\$policyName.xml ..." + $policy | Export-SpbmStoragePolicy -FilePath $Path\$policyName.xml -Force | Out-Null + } else { + $policy + } + } +} + +Function Import-VMStoragePolicy { +<# + .NOTES + =========================================================================== + Created by: William Lam + Date: 11/21/2017 + Blog: https://www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + + .SYNOPSIS + Import VM Storage Policies from JSON file + .DESCRIPTION + Import VM Storage Policies from JSON file + .EXAMPLE + Import-VMStoragePolicy -Path C:\Users\primp\Desktop\VMworld2017 +#> + param( + [Parameter(Mandatory=$false)][String]$Path + ) + + foreach ($file in Get-ChildItem -Path $Path -Filter *.xml) { + $policyName = $file.name + $policyName = $policyName.replace(".xml","") + if(Get-SpbmStoragePolicy -Name $policyName -ErrorAction SilentlyContinue) { + Continue + } else { + Write-Host "Importing Policy $policyname ..." + Import-SpbmStoragePolicy -FilePath $Path\$file -Name $policyName + } + } +} \ No newline at end of file From cdfd510dd9cfb7e61f3cf23f7fc13e3fbfd55585 Mon Sep 17 00:00:00 2001 From: William Lam Date: Tue, 21 Nov 2017 09:37:27 -0800 Subject: [PATCH 099/112] Whoops, missed folder for module --- Modules/vCenterManualMigration.psm1 | 475 ---------------------------- 1 file changed, 475 deletions(-) delete mode 100644 Modules/vCenterManualMigration.psm1 diff --git a/Modules/vCenterManualMigration.psm1 b/Modules/vCenterManualMigration.psm1 deleted file mode 100644 index a7750b9..0000000 --- a/Modules/vCenterManualMigration.psm1 +++ /dev/null @@ -1,475 +0,0 @@ -Function Export-DRSRules { -<# - .NOTES - =========================================================================== - Created by: William Lam - Date: 11/21/2017 - Blog: https://www.virtuallyghetto.com - Twitter: @lamw - =========================================================================== - - .SYNOPSIS - Export DRS Rules to JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg - .DESCRIPTION - Export DRS Rules to JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg - .EXAMPLE - Export-DRSRules -Path C:\Users\primp\Desktop\VMworld2017 -Cluster Windows-Cluster -#> - param( - [Parameter(Mandatory=$false)][String]$Path, - [Parameter(Mandatory=$true)][String]$Cluster - ) - - $rules = Get-Cluster -Name $Cluster | Get-DrsRule - - $results = @() - foreach ($rule in $rules) { - $vmNames = @() - $vmIds = $rule.VMIds - - # Reconstruct MoRef ID to VM Object to get Name - foreach ($vmId in $vmIds) { - $vm = New-Object VMware.Vim.ManagedObjectReference - $vm.Type = "VirtualMachine" - $vm.Value = ($vmId -replace "VirtualMachine-","") - $vmView = Get-View $vm - $vmNames += $vmView.name - } - - $rulesObject = [pscustomobject] @{ - Name = $rule.ExtensionData.Name; - Type = $rule.Type; #VMAffinity = 1, VMAntiAffinity = 0 - Enabled = $rule.Enabled; - Mandatory = $rule.ExtensionData.Mandatory - VM = $vmNames - } - $results+=$rulesObject - } - if($Path) { - $fullPath = $Path + "\DRSRules.json" - Write-Host -ForegroundColor Green "Exporting DRS Rules to $fullpath ..." - $results | ConvertTo-Json | Out-File $fullPath - } else { - $results | ConvertTo-Json - } -} - -Function Import-DRSRules { -<# - .NOTES - =========================================================================== - Created by: William Lam - Date: 11/21/2017 - Blog: https://www.virtuallyghetto.com - Twitter: @lamw - =========================================================================== - - .SYNOPSIS - Import DRS Rules from JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg - .DESCRIPTION - Import DRS Rules from JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg - .EXAMPLE - Import-DRSRules -Path C:\Users\primp\Desktop\VMworld2017 -Cluster Windows-Cluster -#> - param( - [Parameter(Mandatory=$true)][String]$Path, - [Parameter(Mandatory=$true)][String]$Cluster - ) - - Get-DrsRule -Cluster $cluster | Remove-DrsRule -Confirm:$false | Out-Null - - $DRSRulesFilename = "/DRSRules.json" - $fullPath = $Path + $DRSRulesFilename - $json = Get-Content -Raw $fullPath | ConvertFrom-Json - - foreach ($line in $json) { - $vmArr = @() - $vmNames = $line.vm - foreach ($vmName in $vmNames) { - $vmView = Get-VM -Name $vmName - $vmArr+=$vmView - } - New-DrsRule -Name $line.name -Enabled $line.Enabled -Cluster (Get-Cluster -Name $Cluster) -KeepTogether $line.Type -VM $vmArr - } -} - -Function Export-DRSClusterGroup { -<# - .NOTES - =========================================================================== - Created by: William Lam - Date: 11/21/2017 - Blog: https://www.virtuallyghetto.com - Twitter: @lamw - =========================================================================== - - .SYNOPSIS - Export DRS Cluster Group Rules to JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg - .DESCRIPTION - Export DRS Cluster Group Rules to JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg - .EXAMPLE - Export-DRSClusterGroup -Path C:\Users\primp\Desktop\VMworld2017 -Cluster Windows-Cluster -#> - param( - [Parameter(Mandatory=$false)][String]$Path, - [Parameter(Mandatory=$true)][String]$Cluster - ) - - $rules = Get-Cluster -Name $Cluster | Get-DrsClusterGroup - - $results = @() - foreach ($rule in $rules) { - $rulesObject = [pscustomobject] @{ - Name = $rule.ExtensionData.Name; - Type = $rule.GroupType; #VMType = 1, HostType = 0 - Member = $rule.Member - } - $results+=$rulesObject - } - if($Path) { - $fullPath = $Path + "\DRSClusterGroupRules.json" - Write-Host -ForegroundColor Green "Exporting DRS Cluster Group Rules to $fullpath ..." - $results | ConvertTo-Json | Out-File $fullPath - } else { - $results | ConvertTo-Json - } -} - -Function Import-DRSClusterClusterGroup { -<# - .NOTES - =========================================================================== - Created by: William Lam - Date: 11/21/2017 - Blog: https://www.virtuallyghetto.com - Twitter: @lamw - =========================================================================== - - .SYNOPSIS - Import DRS Cluster Group Rules from JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg - .DESCRIPTION - Import DRS Cluster Group Rules from JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg - .EXAMPLE - Import-DRSClusterClusterGroup -Path C:\Users\primp\Desktop\VMworld2017 -Cluster Windows-Cluster -#> - param( - [Parameter(Mandatory=$true)][String]$Path, - [Parameter(Mandatory=$true)][String]$Cluster - ) - - $DRSClusterGroupRulesFilename = "\DRSClusterGroupRules.json" - $fullPath = $Path + $DRSClusterGroupRulesFilename - $json = Get-Content -Raw $fullPath | ConvertFrom-Json - - foreach ($line in $json) { - $memberArr = @() - $members = $line.member - - # VMHost Group - if($line.Type -eq 0) { - foreach ($member in $members) { - $memberView = Get-VMhost -Name $member - $memberArr+=$memberView - } - New-DrsClusterGroup -Name $line.name -Cluster (Get-Cluster -Name $Cluster) -VMhost $memberArr - # VM Group - } else { - foreach ($member in $members) { - $memberView = Get-VM -Name $member - $memberArr+=$memberView - } - New-DrsClusterGroup -Name $line.name -Cluster (Get-Cluster -Name $Cluster) -VM $memberArr - } - } -} - -Function Export-Tag { -<# - .NOTES - =========================================================================== - Created by: William Lam - Date: 11/21/2017 - Blog: https://www.virtuallyghetto.com - Twitter: @lamw - =========================================================================== - - .SYNOPSIS - Export vSphere Tags and VM Assocations to JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg - .DESCRIPTION - Export vSphere Tags and VM Assocations to JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg - .EXAMPLE - Export-Tag -Path C:\Users\primp\Desktop\VMworld2017 -#> - param( - [Parameter(Mandatory=$false)][String]$Path - ) - - # Export Tag Categories - $tagCatagorys = Get-TagCategory - - $tagCatresults = @() - foreach ($tagCategory in $tagCatagorys) { - $tagCatObj = [pscustomobject] @{ - Name = $tagCategory.Name; - Cardinality = $tagCategory.Cardinality; - Description = $tagCategory.Description; - Type = $tagCategory.EntityType - } - $tagCatresults+=$tagCatObj - } - if($Path) { - $fullPath = $Path + "\AllTagCategory.json" - Write-Host -ForegroundColor Green "Exporting vSphere Tag Category to $fullpath ..." - $tagCatresults | ConvertTo-Json | Out-File $fullPath - } else { - $tagCatresults | ConvertTo-Json - } - - # Export Tags - $tags = Get-Tag - - $tagResults = @() - foreach ($tag in $tags) { - $tagObj = [pscustomobject] @{ - Name = $tag.Name; - Description = $tag.Description; - Category = $tag.Category.Name - } - $tagResults+=$tagObj - } - if($Path) { - $fullPath = $Path + "\AllTag.json" - Write-Host -ForegroundColor Green "Exporting vSphere Tag to $fullpath ..." - $tagResults | ConvertTo-Json | Out-File $fullPath - } else { - $tagResults | ConvertTo-Json - } - - # Export VM to Tag Mappings - $vms = Get-VM - - $vmResults = @() - foreach ($vm in $vms) { - $tagAssignments = $vm | Get-TagAssignment - $tags = @() - foreach ($tagAssignment in $tagAssignments) { - $tag = $tagAssignment.Tag - $tagName = $tag -split "/" - $tags+=$tagName - } - $vmObj = [pscustomobject] @{ - Name = $vm.name; - Tag = $tags - } - $vmResults+=$vmObj - } - if($Path) { - $fullPath = $Path + "\AllTagAssocations.json" - Write-Host -ForegroundColor Green "Exporting VM to vSphere Tag Assignment to $fullpath ..." - $vmResults | ConvertTo-Json | Out-File $fullPath - } else { - $vmResults | ConvertTo-Json - } -} - -Function Import-Tag { -<# - .NOTES - =========================================================================== - Created by: William Lam - Date: 11/21/2017 - Blog: https://www.virtuallyghetto.com - Twitter: @lamw - =========================================================================== - - .SYNOPSIS - Import vSphere Tags and VM Assocations from JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg - .DESCRIPTION - Import vSphere Tags and VM Assocations from JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg - .EXAMPLE - Import-Tag -Path C:\Users\primp\Desktop\VMworld2017 -#> - param( - [Parameter(Mandatory=$true)][String]$Path - ) - - $tagCatFilename = "\AllTagCategory.json" - $fullPath = $Path + $tagCatFilename - $tagCategoryJson = Get-Content -Raw $fullPath | ConvertFrom-Json - - $tagFilename = "\AllTag.json" - $fullPath = $Path + $tagFilename - $tagJson = Get-Content -Raw $fullPath | ConvertFrom-Json - - $vmTagFilename = "\AllTagAssocations.json" - $fullPath = $Path + $vmTagFilename - $vmTagJson = Get-Content -Raw $fullPath | ConvertFrom-Json - - # Re-Create Tag Category - foreach ($category in $tagCategoryJson) { - if($category.Cardinality -eq 0) { - $cardinality = "Single" - } else { - $cardinality = "Multiple" - } - New-TagCategory -Name $category.Name -Cardinality $cardinality -Description $category.Description -EntityType $category.Type - } - - # Re-Create Tags - foreach ($tag in $tagJson) { - New-Tag -Name $tag.Name -Description $tag.Description -Category (Get-TagCategory -Name $tag.Category) - } - - # Re-Create VM to Tag Mappings - foreach ($vmTag in $vmTagJson) { - $vm = Get-VM -Name $vmTag.name - $tags = $vmTag.Tag - foreach ($tag in $tags) { - New-TagAssignment -Entity $vm -Tag (Get-Tag -Name $tag) - } - } -} - -Function Export-VMFolder { -<# - .NOTES - =========================================================================== - Created by: William Lam - Date: 11/21/2017 - Blog: https://www.virtuallyghetto.com - Twitter: @lamw - =========================================================================== - - .SYNOPSIS - Export vSphere Folder to JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg - .DESCRIPTION - Export vSphere Folder to JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg - .EXAMPLE - Export-VMFolder -Path C:\Users\primp\Desktop\VMworld2017 -#> - param( - [Parameter(Mandatory=$false)][String]$Path - ) - $vms = Get-VM - - $vmFolderResults = @() - foreach ($vm in $vms) { - $vmFolderObj = [pscustomobject] @{ - Name = $vm.name; - Folder = $vm.Folder.Name; - } - $vmFolderResults+=$vmFolderObj - } - if($Path) { - $fullPath = $Path + "\AllVMFolder.json" - Write-Host -ForegroundColor Green "Exporting VM Folders to $fullpath ..." - $vmFolderResults | ConvertTo-Json | Out-File $fullPath - } else { - $vmFolderResults | ConvertTo-Json - } -} - -Function Import-VMFolder { -<# - .NOTES - =========================================================================== - Created by: William Lam - Date: 11/21/2017 - Blog: https://www.virtuallyghetto.com - Twitter: @lamw - =========================================================================== - - .SYNOPSIS - Import vSphere Folder from JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg - .DESCRIPTION - Import vSphere Folder from JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg - .EXAMPLE - Import-VMFolder -Path C:\Users\primp\Desktop\VMworld2017 -#> - param( - [Parameter(Mandatory=$true)][String]$Path - ) - - $vmFolderFilename = "\AllVMFolder.json" - $fullPath = $Path + $vmFolderFilename - $vmFolderJson = Get-Content -Raw $fullPath | ConvertFrom-Json - - # Root vm Folder - $rootVMFolder = Get-Folder -Type VM -Name vm - - $folders = $vmFolderJson | Select Folder | Sort-Object -Property Folder -Unique - foreach ($folder in $folders) { - $rootVMFolder | New-Folder -Name $folder.folder - } - - foreach ($vmFolder in $vmFolderJson) { - $vm = Get-VM -Name $vmFolder.Name - $folder = Get-Folder -Name $vmFolder.Folder - Move-VM -VM $vm -Destination $folder - } -} - -Function Export-VMStoragePolicy { -<# - .NOTES - =========================================================================== - Created by: William Lam - Date: 11/21/2017 - Blog: https://www.virtuallyghetto.com - Twitter: @lamw - =========================================================================== - - .SYNOPSIS - Export VM Storage Policies to JSON file - .DESCRIPTION - Export VM Storage Policies to JSON file - .EXAMPLE - Export-VMStoragePolicy -Path C:\Users\primp\Desktop\VMworld2017 -#> - param( - [Parameter(Mandatory=$false)][String]$Path - ) - - foreach ($policy in Get-SpbmStoragePolicy) { - $policyName = $policy.Name - if($Path) { - Write-Host -ForegroundColor Green "Exporting Policy $policyName to $Path\$policyName.xml ..." - $policy | Export-SpbmStoragePolicy -FilePath $Path\$policyName.xml -Force | Out-Null - } else { - $policy - } - } -} - -Function Import-VMStoragePolicy { -<# - .NOTES - =========================================================================== - Created by: William Lam - Date: 11/21/2017 - Blog: https://www.virtuallyghetto.com - Twitter: @lamw - =========================================================================== - - .SYNOPSIS - Import VM Storage Policies from JSON file - .DESCRIPTION - Import VM Storage Policies from JSON file - .EXAMPLE - Import-VMStoragePolicy -Path C:\Users\primp\Desktop\VMworld2017 -#> - param( - [Parameter(Mandatory=$false)][String]$Path - ) - - foreach ($file in Get-ChildItem -Path $Path -Filter *.xml) { - $policyName = $file.name - $policyName = $policyName.replace(".xml","") - if(Get-SpbmStoragePolicy -Name $policyName -ErrorAction SilentlyContinue) { - Continue - } else { - Write-Host "Importing Policy $policyname ..." - Import-SpbmStoragePolicy -FilePath $Path\$file -Name $policyName - } - } -} \ No newline at end of file From d104a293934819d792ce2ab1af449c5af51ce236 Mon Sep 17 00:00:00 2001 From: William Lam Date: Tue, 21 Nov 2017 09:38:24 -0800 Subject: [PATCH 100/112] Might be useful if I actually added directory --- .../vCenterManualMigration.psm1 | 475 ++++++++++++++++++ 1 file changed, 475 insertions(+) create mode 100644 Modules/vCenterManualMigration/vCenterManualMigration.psm1 diff --git a/Modules/vCenterManualMigration/vCenterManualMigration.psm1 b/Modules/vCenterManualMigration/vCenterManualMigration.psm1 new file mode 100644 index 0000000..a7750b9 --- /dev/null +++ b/Modules/vCenterManualMigration/vCenterManualMigration.psm1 @@ -0,0 +1,475 @@ +Function Export-DRSRules { +<# + .NOTES + =========================================================================== + Created by: William Lam + Date: 11/21/2017 + Blog: https://www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + + .SYNOPSIS + Export DRS Rules to JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg + .DESCRIPTION + Export DRS Rules to JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg + .EXAMPLE + Export-DRSRules -Path C:\Users\primp\Desktop\VMworld2017 -Cluster Windows-Cluster +#> + param( + [Parameter(Mandatory=$false)][String]$Path, + [Parameter(Mandatory=$true)][String]$Cluster + ) + + $rules = Get-Cluster -Name $Cluster | Get-DrsRule + + $results = @() + foreach ($rule in $rules) { + $vmNames = @() + $vmIds = $rule.VMIds + + # Reconstruct MoRef ID to VM Object to get Name + foreach ($vmId in $vmIds) { + $vm = New-Object VMware.Vim.ManagedObjectReference + $vm.Type = "VirtualMachine" + $vm.Value = ($vmId -replace "VirtualMachine-","") + $vmView = Get-View $vm + $vmNames += $vmView.name + } + + $rulesObject = [pscustomobject] @{ + Name = $rule.ExtensionData.Name; + Type = $rule.Type; #VMAffinity = 1, VMAntiAffinity = 0 + Enabled = $rule.Enabled; + Mandatory = $rule.ExtensionData.Mandatory + VM = $vmNames + } + $results+=$rulesObject + } + if($Path) { + $fullPath = $Path + "\DRSRules.json" + Write-Host -ForegroundColor Green "Exporting DRS Rules to $fullpath ..." + $results | ConvertTo-Json | Out-File $fullPath + } else { + $results | ConvertTo-Json + } +} + +Function Import-DRSRules { +<# + .NOTES + =========================================================================== + Created by: William Lam + Date: 11/21/2017 + Blog: https://www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + + .SYNOPSIS + Import DRS Rules from JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg + .DESCRIPTION + Import DRS Rules from JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg + .EXAMPLE + Import-DRSRules -Path C:\Users\primp\Desktop\VMworld2017 -Cluster Windows-Cluster +#> + param( + [Parameter(Mandatory=$true)][String]$Path, + [Parameter(Mandatory=$true)][String]$Cluster + ) + + Get-DrsRule -Cluster $cluster | Remove-DrsRule -Confirm:$false | Out-Null + + $DRSRulesFilename = "/DRSRules.json" + $fullPath = $Path + $DRSRulesFilename + $json = Get-Content -Raw $fullPath | ConvertFrom-Json + + foreach ($line in $json) { + $vmArr = @() + $vmNames = $line.vm + foreach ($vmName in $vmNames) { + $vmView = Get-VM -Name $vmName + $vmArr+=$vmView + } + New-DrsRule -Name $line.name -Enabled $line.Enabled -Cluster (Get-Cluster -Name $Cluster) -KeepTogether $line.Type -VM $vmArr + } +} + +Function Export-DRSClusterGroup { +<# + .NOTES + =========================================================================== + Created by: William Lam + Date: 11/21/2017 + Blog: https://www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + + .SYNOPSIS + Export DRS Cluster Group Rules to JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg + .DESCRIPTION + Export DRS Cluster Group Rules to JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg + .EXAMPLE + Export-DRSClusterGroup -Path C:\Users\primp\Desktop\VMworld2017 -Cluster Windows-Cluster +#> + param( + [Parameter(Mandatory=$false)][String]$Path, + [Parameter(Mandatory=$true)][String]$Cluster + ) + + $rules = Get-Cluster -Name $Cluster | Get-DrsClusterGroup + + $results = @() + foreach ($rule in $rules) { + $rulesObject = [pscustomobject] @{ + Name = $rule.ExtensionData.Name; + Type = $rule.GroupType; #VMType = 1, HostType = 0 + Member = $rule.Member + } + $results+=$rulesObject + } + if($Path) { + $fullPath = $Path + "\DRSClusterGroupRules.json" + Write-Host -ForegroundColor Green "Exporting DRS Cluster Group Rules to $fullpath ..." + $results | ConvertTo-Json | Out-File $fullPath + } else { + $results | ConvertTo-Json + } +} + +Function Import-DRSClusterClusterGroup { +<# + .NOTES + =========================================================================== + Created by: William Lam + Date: 11/21/2017 + Blog: https://www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + + .SYNOPSIS + Import DRS Cluster Group Rules from JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg + .DESCRIPTION + Import DRS Cluster Group Rules from JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg + .EXAMPLE + Import-DRSClusterClusterGroup -Path C:\Users\primp\Desktop\VMworld2017 -Cluster Windows-Cluster +#> + param( + [Parameter(Mandatory=$true)][String]$Path, + [Parameter(Mandatory=$true)][String]$Cluster + ) + + $DRSClusterGroupRulesFilename = "\DRSClusterGroupRules.json" + $fullPath = $Path + $DRSClusterGroupRulesFilename + $json = Get-Content -Raw $fullPath | ConvertFrom-Json + + foreach ($line in $json) { + $memberArr = @() + $members = $line.member + + # VMHost Group + if($line.Type -eq 0) { + foreach ($member in $members) { + $memberView = Get-VMhost -Name $member + $memberArr+=$memberView + } + New-DrsClusterGroup -Name $line.name -Cluster (Get-Cluster -Name $Cluster) -VMhost $memberArr + # VM Group + } else { + foreach ($member in $members) { + $memberView = Get-VM -Name $member + $memberArr+=$memberView + } + New-DrsClusterGroup -Name $line.name -Cluster (Get-Cluster -Name $Cluster) -VM $memberArr + } + } +} + +Function Export-Tag { +<# + .NOTES + =========================================================================== + Created by: William Lam + Date: 11/21/2017 + Blog: https://www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + + .SYNOPSIS + Export vSphere Tags and VM Assocations to JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg + .DESCRIPTION + Export vSphere Tags and VM Assocations to JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg + .EXAMPLE + Export-Tag -Path C:\Users\primp\Desktop\VMworld2017 +#> + param( + [Parameter(Mandatory=$false)][String]$Path + ) + + # Export Tag Categories + $tagCatagorys = Get-TagCategory + + $tagCatresults = @() + foreach ($tagCategory in $tagCatagorys) { + $tagCatObj = [pscustomobject] @{ + Name = $tagCategory.Name; + Cardinality = $tagCategory.Cardinality; + Description = $tagCategory.Description; + Type = $tagCategory.EntityType + } + $tagCatresults+=$tagCatObj + } + if($Path) { + $fullPath = $Path + "\AllTagCategory.json" + Write-Host -ForegroundColor Green "Exporting vSphere Tag Category to $fullpath ..." + $tagCatresults | ConvertTo-Json | Out-File $fullPath + } else { + $tagCatresults | ConvertTo-Json + } + + # Export Tags + $tags = Get-Tag + + $tagResults = @() + foreach ($tag in $tags) { + $tagObj = [pscustomobject] @{ + Name = $tag.Name; + Description = $tag.Description; + Category = $tag.Category.Name + } + $tagResults+=$tagObj + } + if($Path) { + $fullPath = $Path + "\AllTag.json" + Write-Host -ForegroundColor Green "Exporting vSphere Tag to $fullpath ..." + $tagResults | ConvertTo-Json | Out-File $fullPath + } else { + $tagResults | ConvertTo-Json + } + + # Export VM to Tag Mappings + $vms = Get-VM + + $vmResults = @() + foreach ($vm in $vms) { + $tagAssignments = $vm | Get-TagAssignment + $tags = @() + foreach ($tagAssignment in $tagAssignments) { + $tag = $tagAssignment.Tag + $tagName = $tag -split "/" + $tags+=$tagName + } + $vmObj = [pscustomobject] @{ + Name = $vm.name; + Tag = $tags + } + $vmResults+=$vmObj + } + if($Path) { + $fullPath = $Path + "\AllTagAssocations.json" + Write-Host -ForegroundColor Green "Exporting VM to vSphere Tag Assignment to $fullpath ..." + $vmResults | ConvertTo-Json | Out-File $fullPath + } else { + $vmResults | ConvertTo-Json + } +} + +Function Import-Tag { +<# + .NOTES + =========================================================================== + Created by: William Lam + Date: 11/21/2017 + Blog: https://www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + + .SYNOPSIS + Import vSphere Tags and VM Assocations from JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg + .DESCRIPTION + Import vSphere Tags and VM Assocations from JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg + .EXAMPLE + Import-Tag -Path C:\Users\primp\Desktop\VMworld2017 +#> + param( + [Parameter(Mandatory=$true)][String]$Path + ) + + $tagCatFilename = "\AllTagCategory.json" + $fullPath = $Path + $tagCatFilename + $tagCategoryJson = Get-Content -Raw $fullPath | ConvertFrom-Json + + $tagFilename = "\AllTag.json" + $fullPath = $Path + $tagFilename + $tagJson = Get-Content -Raw $fullPath | ConvertFrom-Json + + $vmTagFilename = "\AllTagAssocations.json" + $fullPath = $Path + $vmTagFilename + $vmTagJson = Get-Content -Raw $fullPath | ConvertFrom-Json + + # Re-Create Tag Category + foreach ($category in $tagCategoryJson) { + if($category.Cardinality -eq 0) { + $cardinality = "Single" + } else { + $cardinality = "Multiple" + } + New-TagCategory -Name $category.Name -Cardinality $cardinality -Description $category.Description -EntityType $category.Type + } + + # Re-Create Tags + foreach ($tag in $tagJson) { + New-Tag -Name $tag.Name -Description $tag.Description -Category (Get-TagCategory -Name $tag.Category) + } + + # Re-Create VM to Tag Mappings + foreach ($vmTag in $vmTagJson) { + $vm = Get-VM -Name $vmTag.name + $tags = $vmTag.Tag + foreach ($tag in $tags) { + New-TagAssignment -Entity $vm -Tag (Get-Tag -Name $tag) + } + } +} + +Function Export-VMFolder { +<# + .NOTES + =========================================================================== + Created by: William Lam + Date: 11/21/2017 + Blog: https://www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + + .SYNOPSIS + Export vSphere Folder to JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg + .DESCRIPTION + Export vSphere Folder to JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg + .EXAMPLE + Export-VMFolder -Path C:\Users\primp\Desktop\VMworld2017 +#> + param( + [Parameter(Mandatory=$false)][String]$Path + ) + $vms = Get-VM + + $vmFolderResults = @() + foreach ($vm in $vms) { + $vmFolderObj = [pscustomobject] @{ + Name = $vm.name; + Folder = $vm.Folder.Name; + } + $vmFolderResults+=$vmFolderObj + } + if($Path) { + $fullPath = $Path + "\AllVMFolder.json" + Write-Host -ForegroundColor Green "Exporting VM Folders to $fullpath ..." + $vmFolderResults | ConvertTo-Json | Out-File $fullPath + } else { + $vmFolderResults | ConvertTo-Json + } +} + +Function Import-VMFolder { +<# + .NOTES + =========================================================================== + Created by: William Lam + Date: 11/21/2017 + Blog: https://www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + + .SYNOPSIS + Import vSphere Folder from JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg + .DESCRIPTION + Import vSphere Folder from JSON file based on VMworld Demo https://youtu.be/MagjfbIL4kg + .EXAMPLE + Import-VMFolder -Path C:\Users\primp\Desktop\VMworld2017 +#> + param( + [Parameter(Mandatory=$true)][String]$Path + ) + + $vmFolderFilename = "\AllVMFolder.json" + $fullPath = $Path + $vmFolderFilename + $vmFolderJson = Get-Content -Raw $fullPath | ConvertFrom-Json + + # Root vm Folder + $rootVMFolder = Get-Folder -Type VM -Name vm + + $folders = $vmFolderJson | Select Folder | Sort-Object -Property Folder -Unique + foreach ($folder in $folders) { + $rootVMFolder | New-Folder -Name $folder.folder + } + + foreach ($vmFolder in $vmFolderJson) { + $vm = Get-VM -Name $vmFolder.Name + $folder = Get-Folder -Name $vmFolder.Folder + Move-VM -VM $vm -Destination $folder + } +} + +Function Export-VMStoragePolicy { +<# + .NOTES + =========================================================================== + Created by: William Lam + Date: 11/21/2017 + Blog: https://www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + + .SYNOPSIS + Export VM Storage Policies to JSON file + .DESCRIPTION + Export VM Storage Policies to JSON file + .EXAMPLE + Export-VMStoragePolicy -Path C:\Users\primp\Desktop\VMworld2017 +#> + param( + [Parameter(Mandatory=$false)][String]$Path + ) + + foreach ($policy in Get-SpbmStoragePolicy) { + $policyName = $policy.Name + if($Path) { + Write-Host -ForegroundColor Green "Exporting Policy $policyName to $Path\$policyName.xml ..." + $policy | Export-SpbmStoragePolicy -FilePath $Path\$policyName.xml -Force | Out-Null + } else { + $policy + } + } +} + +Function Import-VMStoragePolicy { +<# + .NOTES + =========================================================================== + Created by: William Lam + Date: 11/21/2017 + Blog: https://www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + + .SYNOPSIS + Import VM Storage Policies from JSON file + .DESCRIPTION + Import VM Storage Policies from JSON file + .EXAMPLE + Import-VMStoragePolicy -Path C:\Users\primp\Desktop\VMworld2017 +#> + param( + [Parameter(Mandatory=$false)][String]$Path + ) + + foreach ($file in Get-ChildItem -Path $Path -Filter *.xml) { + $policyName = $file.name + $policyName = $policyName.replace(".xml","") + if(Get-SpbmStoragePolicy -Name $policyName -ErrorAction SilentlyContinue) { + Continue + } else { + Write-Host "Importing Policy $policyname ..." + Import-SpbmStoragePolicy -FilePath $Path\$file -Name $policyName + } + } +} \ No newline at end of file From d6861d38fb4e8eac4d2168220d0e5f8047e62f4b Mon Sep 17 00:00:00 2001 From: alanrenouf Date: Tue, 28 Nov 2017 22:48:08 -0800 Subject: [PATCH 101/112] New VMware Cloud on AWS functions module --- Modules/VMware.VMC/VMware.VMC.psd1 | Bin 0 -> 7976 bytes Modules/VMware.VMC/VMware.VMC.psm1 | 323 +++++++++++++++++++++++++++++ 2 files changed, 323 insertions(+) create mode 100755 Modules/VMware.VMC/VMware.VMC.psd1 create mode 100644 Modules/VMware.VMC/VMware.VMC.psm1 diff --git a/Modules/VMware.VMC/VMware.VMC.psd1 b/Modules/VMware.VMC/VMware.VMC.psd1 new file mode 100755 index 0000000000000000000000000000000000000000..74256ef01bb8a2e4a146aa91e45b35b01949b08f GIT binary patch literal 7976 zcmeI1T~Av_5Qg`fUvVTZOsa;IG)tQXdha$WT=b;}i!f6=j?kse}C=A1~?xta;C&zj} ziEqkut#GK7m!h7Cax>m_LM!q#zR&c0q`!%1X8P`n$5Hqce$h8;M&f%R?%nVte9*p$ zS5I%wmT)@PdN^(Ab1&QvkMw^l{5)T^CZ2!k+K}vro^~9Dag^sQlhKI$#ki^S@>P+_&*dyU5lGS@~I{Plr^7ZUJK8{+hANxzQ?lNPg?U>QvN6&cXj_zDDUg)fi(SCobL)d*N=477gaw# zhh`wkhjB&HPFuLx-Tde}3#^5l))2KN6A@R)KT=F2{z;#sD5>r9A=36&+BN6!O1qtD zwdp_iynNTO_9JReBprExY+zo`dbFQ%!y^5&ca@LHy&*l`5r_M-1aVtb@<}0Wo$5ob za12BinFIfrC}-d$o2BU3j_*>edz2)rAop?fsGjiK`>uAukMXRr_@0Zid6;LcJhIFF zwP;6^aZkK1Bi4QC5bs5vaug;FzKmYbmA1)5C!#Co*CKSZmV5~&i~V+ZrzlJ|dl^>_ zWnFSY5%n{Xu9~&*E>hz`m!c^YqfJ<`vmrur=$<$n#;fJpY2w+OcOF!7PV&;+cB`Q# z8m)HS6!H23O+24-Ot{R#UswBIPmX3Q+*BGoj%Y*M*olm=8D6O}9O!CCZ!Fhb>g^=F zzp)2xYQ@1-O(iyod-p*+n|ee&z=J<%HzFCo^qz&h1Vxi47OFPnlK0|5mMU~tu4oo# zR2*t2p0O>undBSje_gsSqfa(W6m|{lh>}|S-$(K zXm_F?GqDmW{{`YQf>lvhxN-E%Zm8S1gK6C)sX*&sGa2jSyYF2id0B>if0&LLlWHOJd5DW{X?y?Nz9^y&8t-uhZPGERgz zjhcy<^)IG<&RkBHtl!g0t1&&vv$ud6?RZDujMnkwT}bMWJFFYY} zVD(&w&05c`zOB5}k&Vd(bo%u4*7x78qWCN~6&HHhoaZWtUdRiq4p|LG@>3x!*I@jE zQ_!B~OPkrKHO*(msjeHga&=96)oW~em*XgRLbt`R6D#IGGo{N~-=BVR80T)QmVHUe z8KK*Sy_wGZ-m7U@PhYxM$){O*18OlA!p?>4!8`;?>xV$g`SiA)7L8vlm2t-rU5>KFP>a~ z99Km~hNd64y22?AxrUsQ=AWjM3fI4XV~54O@T}In())_fdd8}!@)>fs?UzSgJy*~x zELvI4mMONId9d^IDEnBQ>?qDU=G(hhSei9&HKe+hs97vPq6y@_L>X&;*kfQEDf&+@;RHii7+~obbEIu5kES*xhV$Ej0Py W|Bjy+sms{OWGU0<+WD;2YvEtFqc#)( literal 0 HcmV?d00001 diff --git a/Modules/VMware.VMC/VMware.VMC.psm1 b/Modules/VMware.VMC/VMware.VMC.psm1 new file mode 100644 index 0000000..674668b --- /dev/null +++ b/Modules/VMware.VMC/VMware.VMC.psm1 @@ -0,0 +1,323 @@ +Function Get-VMCCommand { +<# + .NOTES + =========================================================================== + Created by: VMware + Date: 11/17/2017 + Organization: VMware + Blog: http://vmware.com/go/powercli + Twitter: @powercli + =========================================================================== + + .SYNOPSIS + Returns all cmdlets for VMware Cloud on AWS + .DESCRIPTION + This cmdlet will allow you to return all cmdlets included in the VMC module + .EXAMPLE + Get-VMCCommand + .EXAMPLE + Get-Command -Module VMware.VMC + .NOTES + You can either use this cmdlet or the Get-Command cmdlet as seen in Example 2 +#> + Get-command -Module VMware.VimAutomation.Vmc + Get-Command -Module VMware.VMC + +} +Function Connect-VMCVIServer { +<# + .NOTES + =========================================================================== + Created by: VMware + Date: 11/17/2017 + Organization: VMware + Blog: http://vmware.com/go/powercli + Twitter: @powercli + =========================================================================== + + .SYNOPSIS + Cmdlet to connect to your VMC vCenter Server + .DESCRIPTION + This will connect you to both the VMC ViServer as well as the CiSServer at the same time. + .EXAMPLE + Connect-VMCVIServer -Server -User -Password + .NOTES + Easiest way is to pipe through your credentials from Get-VMCSDDCDefaultCredential +#> + Param ( + [Parameter(Mandatory=$true)]$Org, + [Parameter(Mandatory=$true)]$Sddc, + [switch]$Autologin + ) + + If (-Not $global:DefaultVMCServers) { Write-error "No VMC Connection found, please use the Connect-VMC to connect" } Else { + $creds = Get-VMCSDDCDefaultCredential -Org $Org -Sddc $Sddc + Write-Host "Connecting to VMC vCenter Server" $creds.vc_public_ip + Connect-VIServer -Server $creds.vc_public_ip -User $creds.cloud_username -Password $creds.cloud_password | Add-Member -MemberType Noteproperty -Name Location -Value "VMC" + Write-Host "Connecting to VMC CIS Endpoint" $creds.vc_public_ip + Connect-CisServer -Server $creds.vc_public_ip -User $creds.cloud_username -Password $creds.cloud_password | Add-Member -MemberType Noteproperty -Name Location -Value "VMC" + } +} +Function Get-VMCOrg { +<# + .NOTES + =========================================================================== + Created by: VMware + Date: 11/17/2017 + Organization: VMware + Blog: http://vmware.com/go/powercli + Twitter: @powercli + =========================================================================== + + .SYNOPSIS + Return the Orgs that you are a part of + .DESCRIPTION + Depending on what you've purchased, you may be a part of one or more VMC Orgs. This will return your orgs + .EXAMPLE + Get-VMCOrg + .EXAMPLE + Get-VMCOrg -Name + .NOTES + Return all the info about the orgs you are a part of +#> + Param ( + [Parameter(Mandatory=$false)]$Name + ) + + If (-Not $global:DefaultVMCServers) { Write-error "No VMC Connection found, please use Connect-VMC to connect" } Else { + $orgService = Get-VMCService com.vmware.vmc.orgs + if ($PSBoundParameters.ContainsKey("Name")){ + $orgs = $orgService.list() | Where {$_.display_name -match $Name} + } Else { + $orgs = $orgService.list() + } + $Orgs | Select display_name, name, user_name, created, id + } +} +Function Get-VMCSDDC { +<# + .NOTES + =========================================================================== + Created by: VMware + Date: 11/17/2017 + Organization: VMware + Blog: http://vmware.com/go/powercli + Twitter: @powercli + =========================================================================== + + .SYNOPSIS + Returns all of the SDDCs you are associated to + .DESCRIPTION + Returns all of the SDDCs ayou are associated to + .EXAMPLE + Get-VMCSDDC -Org + .EXAMPLE + Get-VMCSDDC -Name -Org +#> + Param ( + [Parameter(Mandatory=$True)]$Org, + [Parameter(Mandatory=$false)]$Name + ) + + If (-Not $global:DefaultVMCServers) { Write-error "No VMC Connection found, please use the Connect-VMC to connect" } Else { + if ($PSBoundParameters.ContainsKey("Org")){ + $orgs = Get-VMCOrg -Name $Org + } else { + $orgs = Get-VMCOrg + } + + foreach ($org in $orgs) { + $orgID = $org.ID + $sddcService = Get-VMCService com.vmware.vmc.orgs.sddcs + if ($PSBoundParameters.ContainsKey("Name")){ + $sddcService.list($OrgID) | Where {$_.name -match $Name} + } Else { + $sddcService.list($OrgID) + } + } + } +} +Function Get-VMCTask { +<# + .NOTES + =========================================================================== + Created by: VMware + Date: 11/17/2017 + Organization: VMware + Blog: http://vmware.com/go/powercli + Twitter: @powercli + =========================================================================== + + .SYNOPSIS + Returns all of the VMC Tasks + .DESCRIPTION + Returns all of the VMC Tasks that have either occurred or are in process + .EXAMPLE + Get-VMCTask +#> + Param ( + [Parameter(Mandatory=$True)]$Org + ) + + If (-Not $global:DefaultVMCServers) { Write-error "No VMC Connection found, please use the Connect-VMC to connect" } Else { + if ($PSBoundParameters.ContainsKey("Org")){ + $orgs = Get-VMCOrg -Name $Org + } else { + $orgs = Get-VMCOrg + } + + foreach ($org in $orgs) { + $orgID = $org.ID + $taskService = Get-VMCService com.vmware.vmc.orgs.tasks + $taskService.list($OrgID) | Select * -ExcludeProperty Help + } + } +} +Function Get-VMCSDDCDefaultCredential { +<# + .NOTES + =========================================================================== + Created by: VMware + Date: 11/17/2017 + Organization: VMware + Blog: http://vmware.com/go/powercli + Twitter: @powercli + =========================================================================== + + .SYNOPSIS + Returns the default credential for the SDDC + .DESCRIPTION + Returns the default credential for the sddc + .EXAMPLE + Get-VMCSDDCDefaultCredential -Org + .EXAMPLE + Get-VMCSDDCDefaultCredential -Sddc -Org +#> + Param ( + [Parameter(Mandatory=$true)]$Org, + [Parameter(Mandatory=$false)]$Sddc + ) + + If (-Not $global:DefaultVMCServers) { Write-error "No VMC Connection found, please use the Connect-VMC to connect" } Else { + if ($PSBoundParameters.ContainsKey("Sddc")){ + $sddcs = Get-VMCSDDC -Name $Sddc -Org $Org + } else { + $sddcs = Get-VMCSDDC -Org $Org + } + + foreach ($sddc in $sddcs) { + $sddc.resource_config | Select-object vc_url, vc_management_ip, vc_public_ip, cloud_username, cloud_password + } + } +} +Function Get-VMCSDDCPublicIP { +<# + .NOTES + =========================================================================== + Created by: VMware + Date: 11/17/2017 + Organization: VMware + Blog: http://vmware.com/go/powercli + Twitter: @powercli + =========================================================================== + + .SYNOPSIS + Returns your Public IPs + .DESCRIPTION + Returns your Public IPs + .EXAMPLE + Get-VMCSDDCPublicIP -Org + .EXAMPLE + Get-VMCSDDCPublicIP -Sddc -Org + .NOTES + Return your Public IPs that you have assigned to your account +#> + Param ( + [Parameter(Mandatory=$true)]$Org, + [Parameter(Mandatory=$false)]$Sddc + ) + + If (-Not $global:DefaultVMCServers) { Write-error "No VMC Connection found, please use the Connect-VMC to connect" } Else { + if ($PSBoundParameters.ContainsKey("Sddc")){ + $sddcs = Get-VMCSDDC -Name $Sddc -Org $Org + } else { + $sddcs = Get-VMCSDDC -Org $Org + } + + foreach ($sddc in $sddcs) { + $sddc.resource_config.Public_ip_pool + } + } +} +Function Get-VMCVMHost { + Param ( + [Parameter(Mandatory=$false)]$Sddc, + [Parameter(Mandatory=$true)]$Org + ) + + If (-Not $global:DefaultVMCServers) { Write-error "No VMC Connection found, please use the Connect-VMC to connect" } Else { + if ($PSBoundParameters.ContainsKey("Sddc")){ + $sddcs = Get-VMCSDDC -Name $Sddc -Org $Org + } else { + $sddcs = Get-VMCSDDC -Org $Org + } + + $results = @() + foreach ($sddc in $sddcs) { + foreach ($vmhost in $sddc.resource_config.esx_hosts) { + $tmp = [pscustomobject] @{ + esx_id = $vmhost.esx_id; + name = $vmhost.name; + hostname = $vmhost.hostname; + esx_state = $vmhost.esx_state; + sddc_id = $sddc.id; + org_id = $sddc.org_id; + } + $results += $tmp + } + $results + } + } +} +Function Get-VMCSDDCVersion { +<# + .NOTES + =========================================================================== + Created by: VMware + Date: 11/17/2017 + Organization: VMware + Blog: http://vmware.com/go/powercli + Twitter: @powercli + =========================================================================== + + .SYNOPSIS + Returns SDDC Version + .DESCRIPTION + Returns Version of the SDDC + .EXAMPLE + Get-VMCSDDCVersion -Name -Org +#> + Param ( + [Parameter(Mandatory=$True)]$Org, + [Parameter(Mandatory=$false)]$Name + ) + + If (-Not $global:DefaultVMCServers) { Write-error "No VMC Connection found, please use the Connect-VMC to connect" } Else { + if ($PSBoundParameters.ContainsKey("Org")){ + $orgs = Get-VMCOrg -Name $Org + } else { + $orgs = Get-VMCOrg + } + + foreach ($org in $orgs) { + $orgID = $org.ID + $sddcService = Get-VMCService com.vmware.vmc.orgs.sddcs + if ($PSBoundParameters.ContainsKey("Name")){ + ($sddcService.list($OrgID) | Where {$_.name -match $Name}).resource_config.sddc_manifest | Select *version + } Else { + ($sddcService.list($OrgID)).resource_config.sddc_manifest | Select *version + } + } + } +} +Export-ModuleMember -Function 'Get-VMCCommand', 'Connect-VMCVIServer', 'Get-VMCOrg', 'Get-VMCSDDC', 'Get-VMCTask', 'Get-VMCSDDCDefaultCredential', 'Get-VMCSDDCPublicIP', 'Get-VMCVMHost', 'Get-VMCSDDCVersion' \ No newline at end of file From 2c2b16457cf5dcb47e99821f6a10f8bb5fec5ffc Mon Sep 17 00:00:00 2001 From: lucdekens Date: Fri, 1 Dec 2017 23:08:04 +0100 Subject: [PATCH 102/112] Add module rCisTag Module rCisTag provides CRUD functions for Tags, Tag Categories and Tag Assignments. The functions use the Cis REST API functions. This is a beta release (0.9.0), feedback welcome. --- Modules/rCisTag/MITLicense.txt | 21 + Modules/rCisTag/README.md | 15 + Modules/rCisTag/en-US/about_rCISTag.Help.txt | 26 + Modules/rCisTag/en-US/rCISTag-help.xml | 1793 ++++++++++++++++++ Modules/rCisTag/rCISTag.psd1 | Bin 0 -> 5838 bytes Modules/rCisTag/rCISTag.psm1 | 821 ++++++++ 6 files changed, 2676 insertions(+) create mode 100644 Modules/rCisTag/MITLicense.txt create mode 100644 Modules/rCisTag/README.md create mode 100644 Modules/rCisTag/en-US/about_rCISTag.Help.txt create mode 100644 Modules/rCisTag/en-US/rCISTag-help.xml create mode 100644 Modules/rCisTag/rCISTag.psd1 create mode 100644 Modules/rCisTag/rCISTag.psm1 diff --git a/Modules/rCisTag/MITLicense.txt b/Modules/rCisTag/MITLicense.txt new file mode 100644 index 0000000..b4eaf48 --- /dev/null +++ b/Modules/rCisTag/MITLicense.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) since 2015 Luc Dekens, Matt Boren + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/Modules/rCisTag/README.md b/Modules/rCisTag/README.md new file mode 100644 index 0000000..4160fdc --- /dev/null +++ b/Modules/rCisTag/README.md @@ -0,0 +1,15 @@ +# rCisTag + +A module with cmdlets to provide CRUD functions to +* Tags +* Tag Categories +* Tag Assignments + +The cmdlets use the Cis REST API + +## History + +* Author : **Luc Dekens** +* Release : + * **0.9.0** First draft + \ No newline at end of file diff --git a/Modules/rCisTag/en-US/about_rCISTag.Help.txt b/Modules/rCisTag/en-US/about_rCISTag.Help.txt new file mode 100644 index 0000000..f3b393f --- /dev/null +++ b/Modules/rCisTag/en-US/about_rCISTag.Help.txt @@ -0,0 +1,26 @@ +TOPIC + about_rCISTag + +SHORT DESCRIPTION + The rCisTag module provides CRUD functions to work with vSphere Tags + +LONG DESCRIPTION + The CisTag module provides CRUD functions to work with vSphere Tags. + The functions in the module are based on the REST API + +NOTE + The module requires PowerShell 5.0 + +TROUBLESHOOTING NOTE + + + +EXAMPLES + Get-rCisTag + +KEYWORDS + + + +SEE ALSO + Place related topics here. \ No newline at end of file diff --git a/Modules/rCisTag/en-US/rCISTag-help.xml b/Modules/rCisTag/en-US/rCISTag-help.xml new file mode 100644 index 0000000..023f48c --- /dev/null +++ b/Modules/rCisTag/en-US/rCISTag-help.xml @@ -0,0 +1,1793 @@ + + + + + Connect-rCisServer + Connect + rCisServer + + Make a connection to the CIS service on the vCenter + + + + Make a connection to the CIS service on the vCenter. Credentials need to be provided. + + + + Connect-rCisServer + + Confirm + + Prompts you for confirmation before running the cmdlet. + + + SwitchParameter + + + False + + + Credential + + A PSCredential object with a user and password that is allowed to connect to the CIS service. + + PSCredential + + PSCredential + + + None + + + Fiddler + + Connects to a Fiddler instance on the station where the cmdlet is executed. Used for debugging purposes. + + + SwitchParameter + + + False + + + Proxy + + When the vCenter is located behind a proxy, specify the proxy. + + String + + String + + + None + + + Server + + The hostname of the vCenter + + String + + String + + + None + + + WhatIf + + Shows what would happen if the cmdlet runs. The cmdlet is not run. + + + SwitchParameter + + + False + + + + Connect-rCisServer + + Confirm + + Prompts you for confirmation before running the cmdlet. + + + SwitchParameter + + + False + + + Fiddler + + Connects to a Fiddler instance on the station where the cmdlet is executed. Used for debugging purposes. + + + SwitchParameter + + + False + + + Password + + The user's password + + String + + String + + + None + + + Proxy + + When the vCenter is located behind a proxy, specify the proxy. + + String + + String + + + None + + + Server + + The hostname of the vCenter + + String + + String + + + None + + + User + + The user to connect to the CIS service + + String + + String + + + None + + + WhatIf + + Shows what would happen if the cmdlet runs. The cmdlet is not run. + + + SwitchParameter + + + False + + + + + + Confirm + + Prompts you for confirmation before running the cmdlet. + + SwitchParameter + + SwitchParameter + + + False + + + Credential + + A PSCredential object with a user and password that is allowed to connect to the CIS service. + + PSCredential + + PSCredential + + + None + + + Fiddler + + Connects to a Fiddler instance on the station where the cmdlet is executed. Used for debugging purposes. + + SwitchParameter + + SwitchParameter + + + False + + + Password + + The user's password + + String + + String + + + None + + + Proxy + + When the vCenter is located behind a proxy, specify the proxy. + + String + + String + + + None + + + Server + + The hostname of the vCenter + + String + + String + + + None + + + User + + The user to connect to the CIS service + + String + + String + + + None + + + WhatIf + + Shows what would happen if the cmdlet runs. The cmdlet is not run. + + SwitchParameter + + SwitchParameter + + + False + + + + + + System.Management.Automation.PSCredential + + + + + + + + + + System.Object + + + + + + + + + + + + + + Example 1 + PS C:\> Connect-rCisServer -Server 'vcsa.domain.org' -User 'administrator@vsphere.local' -Password 'vSphere1!' + + Connect to the CIS service on vCenter vcsa.domain.org with user administrator@vsphere.local + + + + Example 2 + PS C:\> Connect-rCisServer -Server 'vcsa.domain.org' -Credential $cred + + Connect to the CIS service on vCenter vcsa.domain.org with the PSCredential stored in $cred + + + + + + + + Disconnect-rCisServer + Disconnect + rCisServer + + Closes the open connection to the CIS REST API + + + + Closes the open connection to the Cis REST API on the host passed on the Server parameter. + + + + Disconnect-rCisServer + + Server + + The name of the server on which the Cis REST API was opened. + + String + + String + + + None + + + Confirm + + Prompts you for confirmation before running the cmdlet. + + + SwitchParameter + + + False + + + WhatIf + + Shows what would happen if the cmdlet runs. The cmdlet is not run. + + + SwitchParameter + + + False + + + + + + Confirm + + Prompts you for confirmation before running the cmdlet. + + SwitchParameter + + SwitchParameter + + + False + + + Server + + The name of the server on which the Cis REST API was opened. + + String + + String + + + None + + + WhatIf + + Shows what would happen if the cmdlet runs. The cmdlet is not run. + + SwitchParameter + + SwitchParameter + + + False + + + + + + None + + + + + + + + + + System.Object + + + + + + + + + + + + + + Example 1 + PS C:\> Disconnect-rCisServer -Server 'vcsa.domain.org' + + Closes the connection to the server vcsa.domain.org + + + + + + + + Get-rCisTag + Get + rCisTag + + Retrieves Tags from vCenter + + + + Return Tags found on the vCenter. + + + + Get-rCisTag + + Category + + The returned tags are restricted to the category with name passed on this parameter + + PSObject[] + + PSObject[] + + + None + + + Confirm + + Prompts you for confirmation before running the cmdlet. + + + SwitchParameter + + + False + + + Name + + Return the tag(s) with this/these name(s) + + String[] + + String[] + + + None + + + WhatIf + + Shows what would happen if the cmdlet runs. The cmdlet is not run. + + + SwitchParameter + + + False + + + + Get-rCisTag + + Confirm + + Prompts you for confirmation before running the cmdlet. + + + SwitchParameter + + + False + + + Id + + Return the tag with the specific ID + + String[] + + String[] + + + None + + + WhatIf + + Shows what would happen if the cmdlet runs. The cmdlet is not run. + + + SwitchParameter + + + False + + + + + + Category + + The returned tags are restricted to the category with name passed on this parameter + + PSObject[] + + PSObject[] + + + None + + + Confirm + + Prompts you for confirmation before running the cmdlet. + + SwitchParameter + + SwitchParameter + + + False + + + Id + + Return the tag with the specific ID + + String[] + + String[] + + + None + + + Name + + Return the tag(s) with this/these name(s) + + String[] + + String[] + + + None + + + WhatIf + + Shows what would happen if the cmdlet runs. The cmdlet is not run. + + SwitchParameter + + SwitchParameter + + + False + + + + + + System.Management.Automation.PSObject[] + + + + + + + + + + System.Object + + + + + + + + + + + + + + Example 1 + PS C:\> Get-rCisTag -Name MyTag + + Return the tags that are named MyTag + + + + Example 2 + PS C:\> Get-rCisTag -Name MyTag -Category MyCategory + + Return the tags that are named MyTag in the category MyCategory + + + + Example 3 + PS C:\> Get-rCisTag -Id urn:vmomi:InventoryServiceTag:385c90a7-e4ad-49af-a0a5-66fc5fedb254:GLOBAL + + Return the tag with the Id urn:vmomi:InventoryServiceTag:385c90a7-e4ad-49af-a0a5-66fc5fedb254:GLOBAL + + + + Example 4 + PS C:\> Get-rCisTagCategory -Name MyCat | Get-rCisTag + + Return all the Tags in TagCategory MyCat + + + + Example 5 + PS C:\> Get-rCisTag -Name MyTag1,MyTag2 + + Return all the Tags named MyTag1 and MyTag2 + + + + + + + + Get-rCisTagAssignment + Get + rCisTagAssignment + + Return Tag assignments + + + + Return information about all or specific Tag assignments + + + + Get-rCisTagAssignment + + Entity + + Return Tag assignments made to this entity + + PSObject[] + + PSObject[] + + + None + + + Tag + + Return Tag assignments for this Tag + + PSObject[] + + PSObject[] + + + None + + + Category + + Return only Tag assignments for Tags from this specific Category + + PSObject[] + + PSObject[] + + + None + + + Confirm + + Prompts you for confirmation before running the cmdlet. + + + SwitchParameter + + + False + + + WhatIf + + Shows what would happen if the cmdlet runs. The cmdlet is not run. + + + SwitchParameter + + + False + + + + + + Category + + Return only Tag assignments for Tags from this specific Category + + PSObject[] + + PSObject[] + + + None + + + Confirm + + Prompts you for confirmation before running the cmdlet. + + SwitchParameter + + SwitchParameter + + + False + + + Entity + + Return Tag assignments made to this entity + + PSObject[] + + PSObject[] + + + None + + + Tag + + Return Tag assignments for this Tag + + PSObject[] + + PSObject[] + + + None + + + WhatIf + + Shows what would happen if the cmdlet runs. The cmdlet is not run. + + SwitchParameter + + SwitchParameter + + + False + + + + + + System.Management.Automation.PSObject[] + + + + + + + + + + System.Object + + + + + + + + + + + + + + Example 1 + PS C:\> Get-rCisTagAssignment + + Return all Tag assignments + + + + Example 2 + PS C:\> Get-rCisTagAssignment -Entity MyVM + + Return all Tag assignments to entity MyVM + + + + Example 3 + PS C:\> Get-rCisTagAssignment -Entity MyVM -Tag MyTag + + Return the Tag assignment to entity MyVM for a Tag with the name MyTag + + + + Example 4 + PS C:\> Get-rCisTagAssignment -Entity MyVM -Category MyCat + + Return all Tag assignments to entity MyVM with Tags from the Tag Category named MyCat + + + + Example 5 + PS C:\> Get-rCisTagAssignment -Category MyCat + + Return all Tag assignments for all Tags from the Tag Category MyCat + + + + Example 6 + PS C:\> Get-VM MyVM | Get-rCisTagAssignment + + Return all Tag assignments made to entity MyVM + + + + + + + + Get-rCisTagCategory + Get + rCisTagCategory + + Return one or more Tag Categories + + + + Return specific Tag Categories defined on the vCenter. The returned Tag Categories can be filtered by the parameters. + + + + Get-rCisTagCategory + + Confirm + + Prompts you for confirmation before running the cmdlet. + + + SwitchParameter + + + False + + + Id + + The Tag Category Id + + String[] + + String[] + + + None + + + WhatIf + + Shows what would happen if the cmdlet runs. The cmdlet is not run. + + + SwitchParameter + + + False + + + + Get-rCisTagCategory + + Confirm + + Prompts you for confirmation before running the cmdlet. + + + SwitchParameter + + + False + + + Name + + The name of the Tag Category + + String[] + + String[] + + + None + + + WhatIf + + Shows what would happen if the cmdlet runs. The cmdlet is not run. + + + SwitchParameter + + + False + + + + + + Confirm + + Prompts you for confirmation before running the cmdlet. + + SwitchParameter + + SwitchParameter + + + False + + + Id + + The Tag Category Id + + String[] + + String[] + + + None + + + Name + + The name of the Tag Category + + String[] + + String[] + + + None + + + WhatIf + + Shows what would happen if the cmdlet runs. The cmdlet is not run. + + SwitchParameter + + SwitchParameter + + + False + + + + + + None + + + + + + + + + + System.Object + + + + + + + + + + + + + + Example 1 + PS C:\> Get-rCisTagCategory + + Return all Tag Categories + + + + Example 2 + PS C:\> Get-rCisTagCategory -Name Cat1,Cat2 + + Return the Tag Categories named Cat1 and Cat2 + + + + Example 3 + PS C:\> Get-rCisTagCategory -Id urn:vmomi:InventoryServiceCategory:925d8ae1-6b6e-403a-9a12-e78e14b1cdd4:GLOBAL + + Return the Tag Category with Id urn:vmomi:InventoryServiceCategory:925d8ae1-6b6e-403a-9a12-e78e14b1cdd4:GLOBAL + + + + + + + + New-rCisTag + New + rCisTag + + Create a Tag + + + + Create a Tag in one or more Categories, optionally with a Description + + + + New-rCisTag + + Name + + The Name of the new Tag + + String[] + + String[] + + + None + + + Category + + The name of the Category in which to create the Tag + + PSObject + + PSObject + + + None + + + Description + + An optional Description for the Tag + + String + + String + + + None + + + Confirm + + Prompts you for confirmation before running the cmdlet. + + + SwitchParameter + + + False + + + WhatIf + + Shows what would happen if the cmdlet runs. The cmdlet is not run. + + + SwitchParameter + + + False + + + + + + Category + + The name of the Category in which to create the Tag + + PSObject + + PSObject + + + None + + + Confirm + + Prompts you for confirmation before running the cmdlet. + + SwitchParameter + + SwitchParameter + + + False + + + Description + + An optional Description for the Tag + + String + + String + + + None + + + Name + + The Name of the new Tag + + String[] + + String[] + + + None + + + WhatIf + + Shows what would happen if the cmdlet runs. The cmdlet is not run. + + SwitchParameter + + SwitchParameter + + + False + + + + + + System.Management.Automation.PSObject + + + + + + + + + + System.Object + + + + + + + + + + + + + + Example 1 + PS C:\> New-rCisTag -Name MyTag -Category MyCategory + + Creates a Tag named MyTag in the Category MyCategory + + + + Example 2 + PS C:\> New-rCisTag -Name MyTag + + Creates a Tag named MyTag in all Categories + + + + Example 3 + PS C:\> Get-rCisCategory -Name MyCategory | New-rCisTag -Name MyTag -Description 'Text' + + Creates a Tag named MyTag in the Category MyCategory. And add the Description 'Text' to the Tag. + + + + + + + + New-rCisTagAssignment + New + rCisTagAssignment + + Create a new Tag assignment to an Entity + + + + Create a Tag assignment to one more Entities + + + + New-rCisTagAssignment + + Tag + + The name of the Tag to assign to the entity + + String[] + + String[] + + + None + + + Entity + + The name of the Entity or the PowerCLI object for the entity + + PSObject[] + + PSObject[] + + + None + + + Confirm + + Prompts you for confirmation before running the cmdlet. + + + SwitchParameter + + + False + + + WhatIf + + Shows what would happen if the cmdlet runs. The cmdlet is not run. + + + SwitchParameter + + + False + + + + + + Confirm + + Prompts you for confirmation before running the cmdlet. + + SwitchParameter + + SwitchParameter + + + False + + + Entity + + The name of the Entity or the PowerCLI object for the entity + + PSObject[] + + PSObject[] + + + None + + + Tag + + The name of the Tag to assign to the entity + + String[] + + String[] + + + None + + + WhatIf + + Shows what would happen if the cmdlet runs. The cmdlet is not run. + + SwitchParameter + + SwitchParameter + + + False + + + + + + System.Management.Automation.PSObject[] + + + + + + + + + + System.Object + + + + + + + + + + + + + + Example 1 + PS C:\> New-rCisTagAssignment -Tag MyTag -Entity MyVM + + Assign the Tag MyTag to the entity MyVM + + + + Example 2 + PS C:\> Get-VM | New-rCisTagAssignment -Tag MyTag + + Assign the Tag MyTag to all VMs + + + + + + + + New-rCisTagCategory + New + rCisTagCategory + + Create a new Tag Category + + + + Creates a new Tag Category. The user can define the Cardinality of the Tag Catgeory (default Single), add a description to the Tag Category and specify for which entity types the Tag Category can be used. + + + + New-rCisTagCategory + + Name + + The name of the new Tag Category + + String[] + + String[] + + + None + + + Cardinality + + Defines the Cardinality of the Tags in this Tag Category. Possible values are Single (default) and Multiple. + + + Single + Multiple + + String + + String + + + None + + + Description + + A description that is attached to the Tag Category + + String + + String + + + None + + + EntityType + + The type of Entity/Entities to which Tags in this Tag Category can be attached. + + String[] + + String[] + + + None + + + Confirm + + Prompts you for confirmation before running the cmdlet. + + + SwitchParameter + + + False + + + WhatIf + + Shows what would happen if the cmdlet runs. The cmdlet is not run. + + + SwitchParameter + + + False + + + + + + Cardinality + + Defines the Cardinality of the Tags in this Tag Category. Possible values are Single (default) and Multiple. + + String + + String + + + None + + + Confirm + + Prompts you for confirmation before running the cmdlet. + + SwitchParameter + + SwitchParameter + + + False + + + Description + + A description that is attached to the Tag Category + + String + + String + + + None + + + EntityType + + The type of Entity/Entities to which Tags in this Tag Category can be attached. + + String[] + + String[] + + + None + + + Name + + The name of the new Tag Category + + String[] + + String[] + + + None + + + WhatIf + + Shows what would happen if the cmdlet runs. The cmdlet is not run. + + SwitchParameter + + SwitchParameter + + + False + + + + + + None + + + + + + + + + + System.Object + + + + + + + + + + + + + + Example 1 + PS C:\> New-rCisTagCategory -Name MyCat + + Create a new Tag Category named MyCat + + + + Example 2 + PS C:\> New-rCisTagCategory -Name MyCat -Cardinality Multiple -Description 'My Category' + + Create a new Tag Category named MyCat, with a Cardinality of Multiple and a description. + + + + + + + + Remove-rCisTag + Remove + rCisTag + + Remove a Tag + + + + Remove one or more Tags from the environment + + + + Remove-rCisTag + + Tag + + {{Fill Tag Description}} + + PSObject[] + + PSObject[] + + + None + + + Confirm + + Prompts you for confirmation before running the cmdlet. + + + SwitchParameter + + + False + + + WhatIf + + Shows what would happen if the cmdlet runs. The cmdlet is not run. + + + SwitchParameter + + + False + + + + Remove-rCisTag + + Confirm + + Prompts you for confirmation before running the cmdlet. + + + SwitchParameter + + + False + + + WhatIf + + Shows what would happen if the cmdlet runs. The cmdlet is not run. + + + SwitchParameter + + + False + + + Id + + {{Fill Id Description}} + + String[] + + String[] + + + None + + + + + + Confirm + + Prompts you for confirmation before running the cmdlet. + + SwitchParameter + + SwitchParameter + + + False + + + Tag + + {{Fill Tag Description}} + + PSObject[] + + PSObject[] + + + None + + + WhatIf + + Shows what would happen if the cmdlet runs. The cmdlet is not run. + + SwitchParameter + + SwitchParameter + + + False + + + Id + + {{Fill Id Description}} + + String[] + + String[] + + + None + + + + + + System.Management.Automation.PSObject[] + + + + + + + + + + System.Object + + + + + + + + + + + + + + Example 1 + PS C:\> Remove-rCisTag -Name MyTag + + Remove the Tag named MyTag + + + + Example 2 + PS C:\> Remove-rCisTag -Name MyTag + + Remove the Tag named MyTag + + + + + + \ No newline at end of file diff --git a/Modules/rCisTag/rCISTag.psd1 b/Modules/rCisTag/rCISTag.psd1 new file mode 100644 index 0000000000000000000000000000000000000000..a9099d9b4397c79fbc4db9a0aa39ad2317b2deba GIT binary patch literal 5838 zcmcJTT~8ZF6o%&-ssCXmE({g8Aqf$=s1(bD1_@{cg!b0>tE>8}*kGcnf4%Md%<(Y0 z_O4@_Aj|g7&dfRQ`93rM{rkcF?Y8vUc8R-kO?T%~H*yn=`fli2?%wrWTce?75_hZF zfotgb>%xkT=4@@ljrAFdTB@}Z?Qd!IRHKReLr?ssS^20dr1ML||g1&I`7AZRV&GL^t;GN|; zPstFx64&?ozRK`Cb;qK5FRmu(M0^+{*{U5r0|J(3SZ#~!ddUxxN+&gJzUkG1o?w$W0X>_Ew2ihC;R1+?^n_ZXb zWy|H)8lp4pZwlYBxc}*;8fU22MYk{OU|r-iE)RZPYbBm}3Kyc(*Y2Q?3(**89OYsC zNWAdhuH?dl@hI~@_M-Ph_BSkLnlGYb5}(T=*P?JH4oy!Fdnay3FBt$2Av@G(mg;)@ zq+V;$y60ZUseFKFPNa=|l;^ein_rEeO@%oT8lKp3Zo8r~i$D+6ABd;rur;=I(kb}rT zmT7kslV^(R-3?{4-QSYov)3{C)OrT0H2pc4cH(=b6t}OGDU4HlVj;ES1 zw^Z-mZj5#jN39gaVtmA$O1?qk)E)X8x&of)K#nL{?hP|20bhqZud3#NLbw%!Vd=bQf`>JS9ysw%6(Ph}lBX6IVMVv)Y zj=GZR1m|OHF>1+t$UsLz*09qA89SVVn9X+$*;zVXY{wi#8B`ba^I=4~DBX{d_jc@UsWhBll zF}p8YpYM~uN}fHp9!BgrEx}kkSB|lG{qr%BxAE);{TFL<*@|+!%XU2KjjGbSSCf-R^4= zc|K1K>1v`pV4CKH5XPaidina7M|S*#%QO}x_#}Py(C|j|MYTUL~ z%UE8}NtjH7_G|ol~pHl3gF}0?h*d$OHceY3 + + Set-StrictMode -Version 2 + + # You have already run this function + if ([System.Net.ServicePointManager]::CertificatePolicy.ToString() -eq 'IgnoreCerts') { Return } + + $Domain = [AppDomain]::CurrentDomain + $DynAssembly = New-Object System.Reflection.AssemblyName('IgnoreCerts') + $AssemblyBuilder = $Domain.DefineDynamicAssembly($DynAssembly, [System.Reflection.Emit.AssemblyBuilderAccess]::Run) + $ModuleBuilder = $AssemblyBuilder.DefineDynamicModule('IgnoreCerts', $false) + $TypeBuilder = $ModuleBuilder.DefineType('IgnoreCerts', 'AutoLayout, AnsiClass, Class, Public, BeforeFieldInit', [System.Object], [System.Net.ICertificatePolicy]) + $TypeBuilder.DefineDefaultConstructor('PrivateScope, Public, HideBySig, SpecialName, RTSpecialName') | Out-Null + $MethodInfo = [System.Net.ICertificatePolicy].GetMethod('CheckValidationResult') + $MethodBuilder = $TypeBuilder.DefineMethod($MethodInfo.Name, 'PrivateScope, Public, Virtual, HideBySig, VtableLayoutMask', $MethodInfo.CallingConvention, $MethodInfo.ReturnType, ([Type[]] ($MethodInfo.GetParameters() | % {$_.ParameterType}))) + $ILGen = $MethodBuilder.GetILGenerator() + $ILGen.Emit([Reflection.Emit.Opcodes]::Ldc_I4_1) + $ILGen.Emit([Reflection.Emit.Opcodes]::Ret) + $TypeBuilder.CreateType() | Out-Null + + # Disable SSL certificate validation + [System.Net.ServicePointManager]::CertificatePolicy = New-Object IgnoreCerts +} + +function Invoke-vCisRest{ + param ( + [String]$Method, + [String]$Request, + [PSObject]$Body + ) + + Process + { + Write-Verbose -Message "$($MyInvocation.MyCommand.Name)" + Write-Verbose -Message "`t$($PSCmdlet.ParameterSetName)" + Write-Verbose -Message "`tCalled from $($stack = Get-PSCallStack; $stack[1].Command) at $($stack[1].Location)" + + Disable-SSLValidation + + $sRest = @{ + Uri = "https:/",$Script:CisServer.Server,'rest',$Request -join '/' + Method = $Method +# Body = &{if($Body){$Body}} + Body = &{if($Body){$Body | ConvertTo-Json -Depth 32}} + ContentType = 'application/json' + Headers = &{ + if($Script:CisServer.ContainsKey('vmware-api-session-id')){ + @{ + 'vmware-api-session-id' = "$($Script:CisServer.'vmware-api-session-id')" + } + } + else{ + @{ + Authorization = "$($Script:CisServer.AuthHeader)" + } + } + } + } + Try + { +# $result = Invoke-WebRequest @sRest + $result = Invoke-RestMethod @sRest + } + Catch + { + + } + $result + } +} + +function Connect-rCisServer{ + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Low')] + param ( + [Parameter(Mandatory, Position = 1)] + [String]$Server, + [Parameter(Mandatory = $True,ValueFromPipeline = $True, Position = 2, ParameterSetName = 'Credential')] + [System.Management.Automation.PSCredential]$Credential, + [Parameter(Mandatory = $True, Position = 2, ParameterSetName = 'PlainText')] + [String]$User, + [Parameter(Mandatory = $True, Position = 3, ParameterSetName = 'PlainText')] + [String]$Password, + [string]$Proxy, + [Parameter(DontShow)] + [switch]$Fiddler = $false + ) + + Process + { + if ($Proxy) + { + if ($PSDefaultParameterValues.ContainsKey('*:Proxy')) + { + $PSDefaultParameterValues['*:Proxy'] = $Proxy + } + else + { + $PSDefaultParameterValues.Add('*:Proxy', $Proxy) + } + if ($PSDefaultParameterValues.ContainsKey('*:ProxyUseDefaultCredentials')) + { + $PSDefaultParameterValues['*:ProxyUseDefaultCredentials'] = $True + } + else + { + $PSDefaultParameterValues.Add('*:ProxyUseDefaultCredentials', $True) + } + } + if ($PSCmdlet.ParameterSetName -eq 'PlainText') + { + $sPswd = ConvertTo-SecureString -String $Password -AsPlainText -Force + $CisCredential = New-Object System.Management.Automation.PSCredential -ArgumentList ($User, $sPswd) + } + if ($PSCmdlet.ParameterSetName -eq 'Credential') + { + $CisCredential = $Credential + } + if ($Fiddler) + { + if (Get-Process -Name fiddler -ErrorAction SilentlyContinue) + { + if ($PSDefaultParameterValues.ContainsKey('Invoke-RestMethod:Proxy')) + { + $PSDefaultParameterValues['Invoke-RestMethod:Proxy'] = 'http://127.0.0.1:8888' + } + else + { + $PSDefaultParameterValues.Add('Invoke-RestMethod:Proxy', 'http://127.0.0.1:8888') + } + } + } + $Script:CisServer = @{ + Server = $Server + AuthHeader = &{ + $User = $CisCredential.UserName + $Password = $CisCredential.GetNetworkCredential().password + + $Encoded = [System.Text.Encoding]::UTF8.GetBytes(($User, $Password -Join ':')) + $EncodedPassword = [System.Convert]::ToBase64String($Encoded) + "Basic $($EncodedPassword)" + } + } + $sRest = @{ + Method = 'Post' + Request = 'com/vmware/cis/session' + } + If($PSCmdlet.ShouldProcess("CisServer $($Server)")) + { + $result = Invoke-vCisRest @sRest + + $Script:CisServer.Add('vmware-api-session-id',$result.value) + $Script:CisServer.Remove('AuthHeader') + } + } +} + +function Disconnect-rCisServer{ + [CmdletBinding(SupportsShouldProcess = $True, ConfirmImpact = 'High')] + param ( + [Parameter(Mandatory = $True, Position = 1)] + [String]$Server + ) + + Process + { + if($Server -ne $Script:CisServer.Server){ + Write-Warning "You are not connected to server $($Server)" + } + + $sRest = @{ + Method = 'Delete' + Request = 'com/vmware/cis/session' + } + If($PSCmdlet.ShouldProcess("CisServer $($Server)")) + { + $result = Invoke-vCisRest @sRest + $Script:CisServer.Remove('vmware-api-session-id') + } + } +} + +function Get-rCisTag{` + [CmdletBinding(SupportsShouldProcess = $True, ConfirmImpact = 'Low', DefaultParameterSetName='Name')] + param ( + [Parameter(Position = 1, ParameterSetName='Name')] + [String[]]$Name, + [Parameter(Position = 2, ParameterSetName='Name',ValueFromPipeline = $true)] + [PSObject[]]$Category, + [Parameter(Mandatory = $True, Position = 1, ParameterSetName='Id')] + [String[]]$Id + ) + + Process + { + if($PSCmdlet.ParameterSetName -eq 'Name'){ + if($Category){ + $tagIds = $Category | %{ + $categoryIds = &{if($_ -is [string]){ + (Get-rCisTagCategory -Name $_).Id + } + else{ + $_.Id + }} + $categoryIds | %{ + # Get all tags in categories + $sRest = @{ + Method = 'Post' + Request = "com/vmware/cis/tagging/tag/id:$([uri]::EscapeDataString($_))?~action=list-tags-for-category" + } + (Invoke-vCisRest @sRest).value + } + } + } + else{ + $sRest = @{ + Method = 'Get' + Request = 'com/vmware/cis/tagging/tag' + } + $tagIds = (Invoke-vCisRest @sRest).value + } + } + else{ + $tagIds = $Id + } + + # Get category details + $out = @() + $tagIds | where{($PSCmdlet.ParameterSetName -eq 'Id' -and $Id -contains $_) -or $PSCmdlet.ParameterSetName -eq 'Name'} | %{ + $sRest = @{ + Method = 'Get' + Request = "com/vmware/cis/tagging/tag/id:$([uri]::EscapeDataString($_))" + } + $result = Invoke-vCisRest @sRest + + if($PSCmdlet.ParameterSetName -eq 'Id' -or ($PSCmdlet.ParameterSetName -eq 'Name' -and ($Name -eq $null -or $Name -contains $result.value.name))){ + $out += New-Object PSObject -Property @{ + Description = $result.value.description + Id = $result.value.id + Name = $result.value.name + Category = (Get-rCisTagCategory -Id $result.value.category_id).Name + Uid = "$($global:defaultviserver.Id)Tag=$($result.value.id)/" + Client = $global:defaultviserver.Client + } + } + } + $out | Select-Object Category,Description,Id,Name,Uid,Client + } + +} + +function Get-rCisTagCategory{ + [CmdletBinding(SupportsShouldProcess = $True, ConfirmImpact = 'Low', DefaultParameterSetName='Name')] + param ( + [Parameter(Position = 1, ParameterSetName='Name')] + [String[]]$Name, + [Parameter(Mandatory = $True, Position = 1, ParameterSetName='Id')] + [String[]]$Id + ) + + Begin + { + $txtInfo = (Get-Culture).TextInfo + $entityTab = @{ + 'ClusterComputeResource' = 'Cluster' + 'DistributedVirtualSwitch' = 'DistributedSwitch' + 'VmwareDistributedVirtualSwitch' = 'DistributedSwitch' + 'HostSystem' = 'VMHost' + 'DistributedVirtualPortGroup' = 'DistributedPortGroup' + 'VirtualApp' = 'VApp' + 'StoragePod' = 'DatastoreCluster' + 'Network' = 'VirtualPortGroup' + } + } + + Process + { + if($PSCmdlet.ParameterSetName -eq 'Name'){ + # Get all categories + $sRest = @{ + Method = 'Get' + Request = 'com/vmware/cis/tagging/category' + } + $tagCategoryIds = (Invoke-vCisRest @sRest).value + } + else{ + $tagCategoryIds = $Id + } + + # Get category details + $out = @() + $tagCategoryids | where{($PSCmdlet.ParameterSetName -eq 'Id' -and $Id -contains $_) -or $PSCmdlet.ParameterSetName -eq 'Name'} | %{ + $sRest = @{ + Method = 'Get' + Request = "com/vmware/cis/tagging/category/id:$([uri]::EscapeDataString($_))" + } + $result = Invoke-vCisRest @sRest + if($PSCmdlet.ParameterSetName -eq 'Id' -or ($PSCmdlet.ParameterSetName -eq 'Name' -and ($Name -eq $null -or $Name -contains $result.value.name))){ + $out += New-Object PSObject -Property @{ + Description = $result.value.description + Cardinality = $txtInfo.ToTitleCase($result.value.cardinality.ToLower()) + EntityType = @(&{ + if($result.value.associable_types.Count -eq 0){'All'} + else{ + $result.value.associable_types | %{ + if($entityTab.ContainsKey($_)){ + $entityTab.Item($_) + } + else{$_} + } + }} | Sort-Object -Unique) + Id = $result.value.id + Name = $result.value.name + Uid = "$($global:defaultviserver.Id)TagCategory=$($result.value.id)/" + Client = $global:defaultviserver.Client + } + } + } + $out | Select-Object Description,Cardinality,EntityType,Id,Name,Uid,Client + } +} + +function Get-rCisTagAssignment{ + [CmdletBinding(SupportsShouldProcess = $True, ConfirmImpact = 'Low')] + param ( + [parameter(Position = 1, ValueFromPipeline = $true)] + [PSObject[]]$Entity, + [parameter(Position = 2)] + [PSObject[]]$Tag, + [parameter(Position = 3)] + [PSObject[]]$Category + ) + + Begin + { + if($Category.Count -ne 0 -or $Tag.Count -ne 0){ + $tagIds = @((Get-rCisTag -Name $Tag -Category $Category).Id) + } + else{ + $tagIds = @((Get-rCisTag).Id) + } + $out = @() + } + + Process + { + foreach($ent in $Entity){ + if($ent -is [string]){ + $ent = Get-Inventory -Name $ent -ErrorAction SilentlyContinue + } + + $entMoRef = New-Object PSObject -Property @{ + type = $ent.ExtensionData.MoRef.Type + id = $ent.ExtensionData.MoRef.Value + } + $sRest = @{ + Method = 'Post' + Request = 'com/vmware/cis/tagging/tag-association?~action=list-attached-tags-on-objects' + Body = @{ + object_ids = @($entMoRef) + } + } + $tagObj = (Invoke-vCisRest @sRest).value + foreach($obj in @($tagObj)){ + foreach($tag in ($obj.tag_ids | where{$tagIds -contains $_})){ + $sMoRef = "$($obj.object_id.type)-$($obj.object_id.id)" + $out += New-Object PSObject -Property @{ + Entity = (Get-View -id $sMoRef -Property Name).Name + Tag = (Get-rCisTag -Id $tag).Name + Id = 'com.vmware.cis.tagging.TagAssociationModel' + Name = 'com.vmware.cis.tagging.TagAssociationModel' + Uid = "$($global:defaultviserver.Id)VirtualMachine=$($sMoRef)/TagAssignment=/Tag=$($tag.tag_id)/" + Client = $global:defaultviserver.Client + } + } + } + } + } + + End + { + if($out.Count -eq 0) + { + $sRest = @{ + Method = 'Post' + Request = 'com/vmware/cis/tagging/tag-association?~action=list-attached-objects-on-tags' + Body = @{ + tag_ids = $tagIds + } + } + $tagObj = (Invoke-vCisRest @sRest).value + $out = foreach($tag in @(($tagObj | where{$tagIds -contains $_.tag_id}))){ + foreach($obj in $tag.object_ids){ + $sMoRef = "$($obj.type)-$($obj.id)" + New-Object PSObject -Property @{ + Entity = (Get-View -id $sMoRef -Property Name).Name + Tag = (Get-rCisTag -Id $tag.tag_id).Name + Id = 'com.vmware.cis.tagging.TagAssociationModel' + Name = 'com.vmware.cis.tagging.TagAssociationModel' + Uid = "$($global:defaultviserver.Id)VirtualMachine=$($sMoRef)/TagAssignment=/Tag=$($tag.tag_id)/" + Client = $global:defaultviserver.Client + } + } + } + } + + $out | Select-Object Uid,Tag,Entity,Id,Name,Client + } +} + +function New-rCisTag{ + [CmdletBinding(SupportsShouldProcess = $True, ConfirmImpact = 'High')] + param ( + [Parameter(Mandatory=$true, Position = 1)] + [String[]]$Name, + [Parameter(Mandatory=$true, Position = 2,ValueFromPipeline = $true)] + [PSObject]$Category, + [Parameter(Position = 3)] + [string]$Description + ) + + Process + { + $out = @() + if($Category -is [String]){ + $Category = Get-rCisTagCategory -Name $Category + } + $Name | %{ + $sRest = @{ + Method = 'Post' + Request = 'com/vmware/cis/tagging/tag' + Body = @{ + create_spec = @{ + category_id = $Category.Id + name = $_ + description = $Description + } + } + } + $tagId = (Invoke-vCisRest @sRest).value + $out += New-Object PSObject -Property @{ + Category = $Category.Name + Description = $Description + Id = $tagId + Name = $_ + Uid = "$($global:defaultviserver.Id)Tag=$($tagId)/" + Client = $global:defaultviserver.Client + } + } + $out | Select-Object Category,Description,Id,Name,Uid,Client + } +} + +function New-rCisTagCategory{ + [CmdletBinding(SupportsShouldProcess = $True, ConfirmImpact = 'High')] + param ( + [Parameter(Mandatory=$true, Position = 1)] + [String[]]$Name, + [Parameter(Position = 2)] + [ValidateSet('Single','Multiple')] + [string]$Cardinality = 'Single', + [Parameter(Position = 3)] + [string]$Description, + [Parameter(Position = 4)] + [string[]]$EntityType + ) + + Process + { + $out = @() + $Name | %{ + $sRest = @{ + Method = 'Post' + Request = 'com/vmware/cis/tagging/category' + Body = @{ + create_spec = @{ + cardinality = $Cardinality.ToUpper() + associable_types = @($EntityType) + name = $_ + description = $Description + } + } + } + $categoryId = (Invoke-vCisRest @sRest).value + $out += New-Object PSObject -Property @{ + Description = $Description + Cardinality = $Cardinality + EntityType = @($EntityType) + Id = $categoryId + Name = $_ + Uid = "$($global:defaultviserver.Id)TagCategory=$($categoryId)/" + Client = $global:defaultviserver.Client + } + } + $out | Select-Object Description,Cardinality,EntityType,Id,Name,Uid,Client + } +} + +function New-rCisTagAssignment{ + [CmdletBinding(SupportsShouldProcess = $True, ConfirmImpact = 'High')] + param ( + [Parameter(Mandatory=$true, Position = 1)] + [String[]]$Tag, + [Parameter(Mandatory=$true,ValueFromPipeline = $true, Position = 2)] + [PSObject[]]$Entity + ) + + Process + { + $tagIds = @((Get-rCisTag -Name $Tag).Id) + $Entity = foreach($ent in $Entity){ + if($ent -is [string]){ + $ent = Get-Inventory -Name $ent -ErrorAction SilentlyContinue + } + $entMoRef = New-Object PSObject -Property @{ + type = $ent.ExtensionData.MoRef.Type + id = $ent.ExtensionData.MoRef.Value + } + foreach($tagId in $tagIds){ + $sRest = @{ + Method = 'Post' + Request = "com/vmware/cis/tagging/tag-association/id:$($tagId)?~action=attach" + Body = @{ + object_id = $entMoRef + } + } + Invoke-vCisRest @sRest + } + } + } + +# foreach($ent in +# if($Tag.Count -eq 1) +# { +# $tagId = (Get-rCisTag -Name $Tag).Id +# } +# elseif($Tag.Count -gt 1) +# { +# $tagIds = (Get-rCisTag -Name $Tag).Id +# } +# $Entity = foreach($ent in $Entity){ +# if($ent -is [string]){ +# Get-Inventory -Name $ent -ErrorAction SilentlyContinue +# } +# else{$ent} +# } +# +# if($Entity.Count -eq 1) +# { +# $entMoRef = New-Object PSObject -Property @{ +# type = $Entity[0].ExtensionData.MoRef.Type +# id = $Entity[0].ExtensionData.MoRef.Value +# } +# if($tag.Count -eq 1){ +# $sRest = @{ +# Method = 'Post' +# Request = "com/vmware/cis/tagging/tag-association/id:$($tagId)?~action=attach" +# Body = @{ +# object_id = $entMoRef +# } +# } +# Invoke-vCisRest @sRest +# } +# elseif($Tag.Count -gt 1){ +# $sRest = @{ +# Method = 'Post' +# Request = 'com/vmware/cis/tagging/tagassociation?~action=attach-multiple-tags-to-object' +# Body = @{ +# object_id = $entMoRef +# tag_ids = @($tagIds) +# } +# } +# Invoke-vCisRest @sRest +# } +# } +# elseif($Entity.Count -gt 1) +# { +# $entMorefs = $Entity | %{ +# New-Object PSObject -Property @{ +# type = $_.ExtensionData.MoRef.Type +# id = $_.ExtensionData.MoRef.Value +# } +# } +# if($tag.Count -eq 1){ +# $sRest = @{ +# Method = 'Post' +# Request = 'com/vmware/cis/tagging/tagassociation/id:$($tagId)?~action=attach-tag-to-multiple-objects' +# Body = @{ +# objects_ids = @($entMoRefs) +# tag_id = $tagId +# } +# } +# Invoke-vCisRest @sRest +# } +# elseif($Tag.Count -gt 1){ +# $tagIds | %{ +# $sRest = @{ +# Method = 'Post' +# Request = 'com/vmware/cis/tagging/tagassociation/id:$($tagId)?~action=attach-tag-to-multiple-objects' +# Body = @{ +# objects_ids = @($entMoRefs) +# tag_id = $_ +# } +# } +# Invoke-vCisRest @sRest +# } +# } +# } +# } +} + +function Remove-rCisTag{ + [CmdletBinding(SupportsShouldProcess = $True, ConfirmImpact = 'High', DefaultParameterSetName='Name')] + param ( + [Parameter(Mandatory=$true, Position = 1, ValueFromPipeline = $true,ParameterSetName='Name')] + [PSObject[]]$Tag, + [Parameter(Mandatory=$true, Position = 1, ValueFromPipelineByPropertyName = $true,ParameterSetName='Id')] + [String[]]$Id + ) + + Process + { + if($PSCmdlet.ParameterSetName -eq 'Name'){ + foreach($tagObj in $Tag){ + if($tagObj -is [string]){ + $tagObj = Get-rCisTag -Name $tagObj + } + $sRest = @{ + Method = 'Delete' + Request = "com/vmware/cis/tagging/tag/id:$($tagObj.Id)" + } + Invoke-vCisRest @sRest + } + } + else{ + foreach($tagId in $Id){ + $sRest = @{ + Method = 'Delete' + Request = "com/vmware/cis/tagging/tag/id:$($tagId)" + } + Invoke-vCisRest @sRest + } + } + } +} + +function Remove-rCisTagCategory{ + [CmdletBinding(SupportsShouldProcess = $True, ConfirmImpact = 'High', DefaultParameterSetName='Name')] + param ( + [Parameter(Mandatory=$true,Position = 1, ValueFromPipeline = $true,ParameterSetName='Name')] + [PSObject[]]$Category, + [Parameter(Mandatory=$true,Position = 1, ValueFromPipelineByPropertyName = $true,ParameterSetName='Id')] + [String[]]$Id + ) + + Process + { + if($PSCmdlet.ParameterSetName -eq 'Name'){ + foreach($catObj in $Category){ + if($catObj -is [string]){ + $catObj = Get-rCisTagCategory -Name $catObj + } + $sRest = @{ + Method = 'Delete' + Request = "com/vmware/cis/tagging/category/id:$($catObj.Id)" + } + Invoke-vCisRest @sRest + } + } + else{ + foreach($catId in $Id){ + $sRest = @{ + Method = 'Delete' + Request = "com/vmware/cis/tagging/category/id:$($catId)" + } + Invoke-vCisRest @sRest + } + } + } +} + +function Remove-rCisTagAssignment{ + [CmdletBinding(SupportsShouldProcess = $True, ConfirmImpact = 'High',DefaultParameterSetName='Assignment')] + param ( + [Parameter(Mandatory=$true, Position = 1, ValueFromPipeline = $true,ParameterSetName='Assignment')] + [PSObject[]]$TagAssignment, + [Parameter(Mandatory=$true,Position = 1, ValueFromPipeline = $true,ParameterSetName='Name')] + [string[]]$Tag, + [Parameter(Position = 2, ParameterSetName='Name')] + [string[]]$Category, + [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName = $true,ParameterSetName='Id')] + [string[]]$TagId, + [Parameter(ParameterSetName='Name')] + [Parameter(ParameterSetName='Id')] + [PSObject[]]$Entity + ) + + Process + { + + switch ($PSCmdlet.ParameterSetName){ + 'Name' { + $TagAssignment = Get-rCisTagAssignment -Entity $Entity -Tag $Tag -Category $Category + } + 'Id' { + $tags = Get-rCisTag -Id $TagId + $TagAssignment = Get-rCisTagAssignment -Tag $tags.Name -Entity $Entity + } + } + if($TagAssignment){ + $entMoRefs = @(Get-Inventory -Name $TagAssignment.Entity -ErrorAction SilentlyContinue | %{ + New-Object PSObject -Property @{ + type = $_.ExtensionData.MoRef.Type + id = $_.ExtensionData.MoRef.Value + } + }) + $tagIds = @((Get-rCisTag -Name $TagAssignment.Tag).Id) + } + + foreach($entMoRef in $entMoRefs){ + foreach($tId in $tagIds){ + $sRest = @{ + Method = 'Post' + Request = "com/vmware/cis/tagging/tag-association/id:$($tId)?~action=detach" + Body = @{ + object_id = $entMoRef + } + } + Invoke-vCisRest @sRest + } + } + } +} + +function Set-rCisTag{ + [CmdletBinding(SupportsShouldProcess = $True, ConfirmImpact = 'High')] + param ( + [Parameter(Mandatory=$true, Position = 1, ValueFromPipeline = $true)] + [PSObject[]]$Tag, + [Parameter(Position = 2)] + [string]$Name, + [Parameter(Position = 3)] + [string]$Description + ) + + Process + { + foreach($tagObj in $Tag){ + if($tagObj -is [string]){ + $tagObj = Get-rCisTag -Name $tagObj + } + $sRest = @{ + Method = 'Patch' + Request = "com/vmware/cis/tagging/tag/id:$($tagObj.Id)" + Body = @{ + update_spec = @{ + name = $Name + description = $Description + } + } + } + Invoke-vCisRest @sRest + } + } +} + +function Set-rCisTagCategory{ + [CmdletBinding(SupportsShouldProcess = $True, ConfirmImpact = 'High')] + param ( + [Parameter(Mandatory=$true, Position = 1, ValueFromPipeline = $true)] + [PSObject[]]$Category, + [Parameter(Position = 2)] + [string]$Name, + [Parameter(Position = 3)] + [ValidateSet('Single','Multiple')] + [string]$Cardinality, # Only SINGLE to MULTIPLE +# [string[]]$AddEntityType, # Does not work + [string]$Description + ) + + Process + { + foreach($catObj in $Category){ + if($catObj -is [string]){ + $catObj = Get-rCisTagCategory -Name $catObj + } + $sRest = @{ + Method = 'Patch' + Request = "com/vmware/cis/tagging/category/id:$($catObj.Id)" + Body = @{ + update_spec = @{ + } + } + } + if($Name){ + $sRest.Body.update_spec.Add('name',$Name) + } + if($Description){ + $sRest.Body.update_spec.Add('description',$Description) + } + if($Cardinality -and $catObj.Cardinality -eq 'SINGLE'){ + $sRest.Body.update_spec.Add('cardinality',$Cardinality.ToUpper()) + } + if($Name -or $Description -or $Cardinality){ + Invoke-vCisRest @sRest + } + } + } +} From 6c50bef798a2e3a23c6f3af75ddd1e637f19db7d Mon Sep 17 00:00:00 2001 From: LucD Date: Sat, 2 Dec 2017 23:25:35 +0100 Subject: [PATCH 103/112] rCisTag-Examples Add examples to rCisTag module --- Modules/rCisTag/Examples/01-Get.ps1 | 17 ++++++++++++++++ Modules/rCisTag/Examples/02-New.ps1 | 14 +++++++++++++ Modules/rCisTag/Examples/03-Set.ps1 | 11 ++++++++++ Modules/rCisTag/Examples/04-Remove.ps1 | 13 ++++++++++++ Modules/rCisTag/Examples/05-Tag-Datastore.ps1 | 20 +++++++++++++++++++ Modules/rCisTag/Examples/CisConfig.ps1 | 3 +++ 6 files changed, 78 insertions(+) create mode 100644 Modules/rCisTag/Examples/01-Get.ps1 create mode 100644 Modules/rCisTag/Examples/02-New.ps1 create mode 100644 Modules/rCisTag/Examples/03-Set.ps1 create mode 100644 Modules/rCisTag/Examples/04-Remove.ps1 create mode 100644 Modules/rCisTag/Examples/05-Tag-Datastore.ps1 create mode 100644 Modules/rCisTag/Examples/CisConfig.ps1 diff --git a/Modules/rCisTag/Examples/01-Get.ps1 b/Modules/rCisTag/Examples/01-Get.ps1 new file mode 100644 index 0000000..52cf4de --- /dev/null +++ b/Modules/rCisTag/Examples/01-Get.ps1 @@ -0,0 +1,17 @@ +# Fetch Cis Server hostname and credentials +.\CisConfig.ps1 + + +Connect-rCisServer -Server $cisServer -User $cisUser -Password $cisPswd + +# Get Tag information +Get-rCisTag + +# Get Tag Category information +Get-rCisTagCategory + +# Get Tag Assignment information +Get-rCisTagAssignment + +Disconnect-rCisServer -Server $cisServer -Confirm:$false + \ No newline at end of file diff --git a/Modules/rCisTag/Examples/02-New.ps1 b/Modules/rCisTag/Examples/02-New.ps1 new file mode 100644 index 0000000..7fe9af2 --- /dev/null +++ b/Modules/rCisTag/Examples/02-New.ps1 @@ -0,0 +1,14 @@ +# Fetch Cis Server hostname and credentials +.\CisConfig.ps1 + +Connect-rCisServer -Server $cisServer -User $cisUser -Password $cisPswd + +New-rCisTagCategory -Name MyCat1 -Cardinality Single -Description 'Test Tag Category' -EntityType 'VirtualMachine' +New-rCisTag -Name MyTag1 -Category MyCat1 -Description 'Test Tag' +$vm = Get-VM | Get-Random +New-rCisTagAssignment -Entity $vm -Tag MyTag1 + +Get-rCisTagAssignment -Tag MyTag1 + +Disconnect-rCisServer -Server $cisServer -Confirm:$false + \ No newline at end of file diff --git a/Modules/rCisTag/Examples/03-Set.ps1 b/Modules/rCisTag/Examples/03-Set.ps1 new file mode 100644 index 0000000..35d26b0 --- /dev/null +++ b/Modules/rCisTag/Examples/03-Set.ps1 @@ -0,0 +1,11 @@ +# Fetch Cis Server hostname and credentials +.\CisConfig.ps1 + +Connect-rCisServer -Server $cisServer -User $cisUser -Password $cisPswd + +Get-rCisTag -Name MyTag1 | Set-rCisTag -Name MyNewTag1 -Description 'Name changed' + +Get-rCisTagCategory -Name MyCat1 | Set-rCisTagCategory -Cardinality Multiple -Name MyNewCat1 -Description 'Name changed' + +Disconnect-rCisServer -Server $cisServer -Confirm:$false + \ No newline at end of file diff --git a/Modules/rCisTag/Examples/04-Remove.ps1 b/Modules/rCisTag/Examples/04-Remove.ps1 new file mode 100644 index 0000000..2f684f9 --- /dev/null +++ b/Modules/rCisTag/Examples/04-Remove.ps1 @@ -0,0 +1,13 @@ +# Fetch Cis Server hostname and credentials +.\CisConfig.ps1 + +Connect-rCisServer -Server $cisServer -User $cisUser -Password $cisPswd + +Get-rCisTagAssignment -Tag MyNewTag1 | Remove-rCisTagAssignment -Confirm:$false + +Get-rCisTag -Name MyNewTag1 | Remove-rCisTag -Confirm:$false + +Get-rCisTagCategory -Name MyNewCat1 | Remove-rCisTagCategory -Confirm:$false + +Disconnect-rCisServer -Server $cisServer -Confirm:$false + \ No newline at end of file diff --git a/Modules/rCisTag/Examples/05-Tag-Datastore.ps1 b/Modules/rCisTag/Examples/05-Tag-Datastore.ps1 new file mode 100644 index 0000000..f0ad343 --- /dev/null +++ b/Modules/rCisTag/Examples/05-Tag-Datastore.ps1 @@ -0,0 +1,20 @@ +# Fetch Cis Server hostname and credentials +.\CisConfig.ps1 + +Connect-rCisServer -Server $cisServer -User $cisUser -Password $cisPswd + +$catName = 'Homelab' + +# Clean up +Get-rCisTagCategory -Name $catName | Remove-rCisTagCategory -Confirm:$false + +# Tag all datastores with their type +New-rCisTagCategory -Name HomeLab -Description 'Homelab datastores' -Cardinality Single -EntityType 'Datastore' | +New-rCisTag -Name 'VMFS','NFS' -Description 'Datastore type' + +Get-Cluster -Name Cluster1 | Get-Datastore | %{ + New-rCisTagAssignment -Entity $_ -Tag "$($_.Type)" +} + +Disconnect-rCisServer -Server $cisServer -Confirm:$false + \ No newline at end of file diff --git a/Modules/rCisTag/Examples/CisConfig.ps1 b/Modules/rCisTag/Examples/CisConfig.ps1 new file mode 100644 index 0000000..de72021 --- /dev/null +++ b/Modules/rCisTag/Examples/CisConfig.ps1 @@ -0,0 +1,3 @@ +$cisServer = 'vcsa.my.domain' +$cisUser = 'administrator@vsphere.local' +$cisPswd = 'VMware1!' From b5800f0eff9eb4e16a1fe93b8ea1827d709f072f Mon Sep 17 00:00:00 2001 From: William Lam Date: Wed, 20 Dec 2017 08:37:38 -0800 Subject: [PATCH 104/112] PowerCLI Module using Cross vCenter Migration Utility Fling API --- Modules/CrossvCentervmotion/XVM.psm1 | 290 +++++++++++++++++++++++++++ 1 file changed, 290 insertions(+) create mode 100644 Modules/CrossvCentervmotion/XVM.psm1 diff --git a/Modules/CrossvCentervmotion/XVM.psm1 b/Modules/CrossvCentervmotion/XVM.psm1 new file mode 100644 index 0000000..aef65c8 --- /dev/null +++ b/Modules/CrossvCentervmotion/XVM.psm1 @@ -0,0 +1,290 @@ +Function Get-XVCMStatus { +<# + .NOTES + =========================================================================== + Created by: William Lam + Organization: VMware + Blog: www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + .DESCRIPTION + This function returns whether Cross vCenter Workload Migration Utility is running or not + .EXAMPLE + Get-XVCMStatus +#> + $Uri = "http://localhost:8080/api/ping" + + $results = Invoke-WebRequest -Uri $Uri -Method GET -TimeoutSec 5 + + if($results.StatusCode -eq 200) { + Write-Host -ForegroundColor Green $results.Content + } else { Write-Host -ForegroundColor Red "Cross vCenter Workload Migration Utility is probably not running" } +} + +Function Get-XVCMSite { +<# + .NOTES + =========================================================================== + Created by: William Lam + Organization: VMware + Blog: www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + .DESCRIPTION + This function returns all registered vCenter Servers + .EXAMPLE + Get-XVCMSite +#> + $Uri = "http://localhost:8080/api/sites" + + $results = Invoke-WebRequest -Uri $Uri -Method GET + + if($results.StatusCode -eq 200) { + ($results.Content | ConvertFrom-Json)|select sitename,hostname,username + } else { Write-Host -ForegroundColor Red "Failed to retrieve VC Site Registration details" } +} + +Function New-XVCMSite { +<# + .NOTES + =========================================================================== + Created by: William Lam + Organization: VMware + Blog: www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + .DESCRIPTION + This function registers a new vCenter Server endpoint + .PARAMETER SiteName + The display name for the particular vCenter Server to be registered + .PARAMETER VCHostname + The Hostname/IP Address of vCenter Server + .PARAMETER VCUsername + The VC Username of vCenter Server + .PARAMETER VCPassword + The VC Password of vCenter Server + .PARAMETER Insecure + Flag to disable SSL Verification checking, useful for lab environments + .EXAMPLE + New-XVCMSite -SiteName "SiteA" -VCHostname "vcenter65-1.primp-industries.com" -VCUsername "administrator@vsphere.local" -VCPassword "VMware1!" -Insecure +#> + param( + [Parameter(Mandatory=$true)][String]$SiteName, + [Parameter(Mandatory=$true)][String]$VCHostname, + [Parameter(Mandatory=$true)][String]$VCUsername, + [Parameter(Mandatory=$true)][String]$VCPassword, + [Parameter(Mandatory=$false)][Switch]$Insecure + ) + + $Uri = "http://localhost:8080/api/sites" + + $insecureFlag = $false + if($Insecure) { + $insecureFlag = $true + } + + $body = @{ + "sitename"=$SiteName; + "hostname"=$VCHostname; + "username"=$VCUsername; + "password"=$VCPassword; + "insecure"=$insecureFlag; + } + + $body = $body | ConvertTo-Json + + Write-Host -ForegroundColor Cyan "Registering vCenter Server $VCHostname as $SiteName ..." + $results = Invoke-WebRequest -Uri $Uri -Method POST -Body $body -ContentType "application/json" + + if($results.StatusCode -eq 200) { + Write-Host -ForegroundColor Green "Successfully registered $SiteName" + } else { Write-Host -ForegroundColor Red "Failed to register $SiteName" } +} + +Function Remove-XVCMSite { +<# + .NOTES + =========================================================================== + Created by: William Lam + Organization: VMware + Blog: www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + .DESCRIPTION + This function removes vCenter Server endpoint + .PARAMETER SiteName + The name of the registered vCenter Server to remove + .EXAMPLE + Remove-XVCMSite -SiteName "SiteA" +#> + param( + [Parameter(Mandatory=$true)][String]$SiteName + ) + + $Uri = "http://localhost:8080/api/sites/$SiteName" + + Write-Host -ForegroundColor Cyan "Deleting vCenter Server Site Registerion $SiteName ..." + $results = Invoke-WebRequest -Uri $Uri -Method DELETE + + if($results.StatusCode -eq 200) { + Write-Host -ForegroundColor Green "Successfully deleted $SiteName" + } else { Write-Host -ForegroundColor Red "Failed to deleted $SiteName" } +} + +Function New-XVCMRequest { +<# + .NOTES + =========================================================================== + Created by: William Lam + Organization: VMware + Blog: www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + .DESCRIPTION + This function initiates a migration request + .PARAMETER SrcSite + The name of the source vCenter Server + .PARAMETER DstSite + The name of the destination vCenter Server + .PARAMETER SrcDatacenter + The name of the source vSphere Datacenter + .PARAMETER DstDatacenter + The name of the destination vSphere Datacenter + .PARAMETER SrcCluster + The name of the source vSphere Cluster + .PARAMETER DstCluster + The name of the destination vSphere Cluster + .PARAMETER DstDatastore + The name of the destination Datastore + .PARAMETER srcVMs + List of VMs to migrate + .PARAMETER NetworkMapping + Hash table of the VM network mappings between your source and destination vCenter Server + .EXAMPLE + New-XVCMRequest -SrcSite SiteA -DstSite SiteB ` + -SrcDatacenter Datacenter-SiteA -DstDatacenter Datacenter-SiteB ` + -SrcCluster Palo-Alto -DstCluster Santa-Barbara ` + -DstDatastore vsanDatastore ` + -srcVMs @("PhotonOS-01","PhotonOS-02","PhotonOS-03","PhotonOS-04") ` + -NetworkMapping @{"DVPG-VM Network 1"="DVPG-Internal Network";"DVPG-VM Network 2"="DVPG-External Network"} +#> + param( + [Parameter(Mandatory=$true)][String]$SrcSite, + [Parameter(Mandatory=$true)][String]$DstSite, + [Parameter(Mandatory=$true)][String]$SrcDatacenter, + [Parameter(Mandatory=$true)][String]$DstDatacenter, + [Parameter(Mandatory=$true)][String]$SrcCluster, + [Parameter(Mandatory=$true)][String]$DstCluster, + [Parameter(Mandatory=$true)][String]$DstDatastore, + [Parameter(Mandatory=$true)][String[]]$srcVMs, + [Parameter(Mandatory=$true)][Hashtable]$NetworkMapping + ) + + $Uri = "http://localhost:8080/api/tasks" + + $body = @{ + "sourceSite"=$SrcSite; + "targetSite"=$DstSite; + "sourceDatacenter"=$SrcDatacenter; + "targetDatacenter"=$dstDatacenter; + "sourceCluster"=$SrcCluster; + "targetCluster"=$DstCluster; + "targetDatastore"=$DstDatastore; + "networkMap"=$NetworkMapping; + "vmList"=$srcVMs; + } + + $body = $body | ConvertTo-Json + + Write-Host -ForegroundColor Cyan "Initiating migration request ..." + $results = Invoke-WebRequest -Uri $Uri -Method POST -Body $body -ContentType "application/json" + + if($results.StatusCode -eq 200) { + $taskId = ($results.Content | ConvertFrom-Json).requestId + Write-Host -ForegroundColor Green "Successfully issued migration with TaskID: $taskId" + } else { Write-Host -ForegroundColor Red "Failed to initiate migration request" } +} + +Function Get-XVCMTask { +<# + .NOTES + =========================================================================== + Created by: William Lam + Organization: VMware + Blog: www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + .DESCRIPTION + This function retrieves either all migration tasks and/or a specific migration task + .PARAMETER Id + The task ID returned from initiating a migration + .EXAMPLE + Get-XVCMTask -Id +#> + param( + [Parameter(Mandatory=$false)][String]$Id + ) + + $Uri = "http://localhost:8080/api/tasks" + + if($Id) { + $body = @{"requestId"=$Id} + + $results = Invoke-WebRequest -Uri $Uri -Method GET -Body $body -ContentType "application/json" + } else { + $results = Invoke-WebRequest -Uri $Uri -Method GET + } + + if($results.StatusCode -eq 200) { + $results.Content | ConvertFrom-Json + } else { Write-Host -ForegroundColor Red "Failed to retrieve tasks" } +} + +Function Get-VMNetwork { +<# + .NOTES + =========================================================================== + Created by: William Lam + Organization: VMware + Blog: www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + .DESCRIPTION + This function returns the list of all VM Networks attached to + given VMs to help with initiating migration + .PARAMETER srcVMs + List of VMs to query their current VM Networks + .EXAMPLE + Get-VMNetwork -srcVMs @("PhotonOS-01","PhotonOS-02","PhotonOS-03","PhotonOS-04") +#> + param( + [Parameter(Mandatory=$false)][String[]]$srcVMs + ) + + if (-not $global:DefaultVIServers) { Write-Host -ForegroundColor red "No vCenter Server Connection found, please connect to your source vCenter Server using Connect-VIServer"; break } + + $results = @() + if($srcVMs) { + foreach ($srcVM in $srcVMs) { + $vm = Get-VM -Name $srcVM + $networkDetails = $vm | Get-NetworkAdapter + $tmp = [pscustomobject] @{ + Name = $srcVM; + Adapter = $networkDetails.name; + Network = $networkDetails.NetworkName; + } + $results+=$tmp + } + } else { + foreach ($vm in Get-VM) { + $networkDetails = $vm | Get-NetworkAdapter + $tmp = [pscustomobject] @{ + Name = $vm.Name; + Adapter = $networkDetails.name; + Network = $networkDetails.NetworkName; + } + $results+=$tmp + } + } + $results +} \ No newline at end of file From 23f215be3596fbc3b89e25a963ddef69cbd0a7d7 Mon Sep 17 00:00:00 2001 From: Kyle Ruddy Date: Fri, 5 Jan 2018 16:13:39 -0500 Subject: [PATCH 105/112] Update VMToolsManagement.psm1 Added advanced function: Get-VMToolsUpgradePolicy Updated the following advanced functions to add functionatlity: - Set-VMToolsUpgradePolicy - Supports UpgradeAtPowerCycle and Manual - Update-VMToolsConfInVM - Supports all log levels now Misc other minor updates: - cmdlets changed to 'advanced function' - 'vcetner' changed to 'vcenter' --- .../VMToolsManagement/VMToolsManagement.psm1 | 257 ++++++++++++------ 1 file changed, 173 insertions(+), 84 deletions(-) diff --git a/Modules/VMToolsManagement/VMToolsManagement.psm1 b/Modules/VMToolsManagement/VMToolsManagement.psm1 index bbfdf5e..335476d 100644 --- a/Modules/VMToolsManagement/VMToolsManagement.psm1 +++ b/Modules/VMToolsManagement/VMToolsManagement.psm1 @@ -1,4 +1,4 @@ -# Script Module : VMToolsManagement +# Script Module : VMToolsManagement # Version : 1.0 # Copyright © 2017 VMware, Inc. All Rights Reserved. @@ -37,10 +37,10 @@ New-VIProperty -Name ToolsBuildNumber -Object VirtualMachine -Value { Function Get-VMToolsInfo { <# .SYNOPSIS - This cmdlet retrieves the VMTools info of specified virtual machines. + This advanced function retrieves the VMTools info of specified virtual machines. .DESCRIPTION - This cmdlet retrieves the VMTools version and build number info of specified virtual machines. + This advanced function retrieves the VMTools version and build number info of specified virtual machines. .PARAMETER VM Specifies the virtual machines which you want to get the VMTools info of. @@ -50,7 +50,7 @@ Function Get-VMToolsInfo { C:\PS> $VCServer = Connect-VIServer -Server -User -Password C:\PS> Get-VM -Server $VCServer | Get-VMToolsInfo - Retrieves VMTools info of all virtual machines which run in the $VCServer vCetner Server. + Retrieves VMTools info of all virtual machines which run in the $VCServer vCenter Server. .EXAMPLE C:\PS> Get-VM "*rhel*" | Get-VMToolsInfo @@ -74,7 +74,7 @@ Function Get-VMToolsInfo { Retrieves VMTools info of virtual machines which run on the "MyESXiHostName" ESXi host. .NOTES - This cmdlet assumes that you are connected to at least one vCenter Server system. + This advanced function assumes that you are connected to at least one vCenter Server system. The tools build number is not supported in VMTools before 10.2.0 .NOTES @@ -100,17 +100,17 @@ Function Get-VMToolsInfo { ) Process { - Get-VM $VM | Select Name, @{Name="ToolsVersion"; Expression={$_.Guest.ToolsVersion}}, ToolsBuildNumber + Get-VM $VM | Select-Object Name, @{Name="ToolsVersion"; Expression={$_.Guest.ToolsVersion}}, ToolsBuildNumber } } Function Get-VMToolsInstallLastError { <# .SYNOPSIS - This cmdlet retrieves the error code of last VMTools installation. + This advanced function retrieves the error code of last VMTools installation. .DESCRIPTION - This cmdlet retrieves the error code of last VMTools installation on specified virtual machines. + This advanced function retrieves the error code of last VMTools installation on specified virtual machines. .PARAMETER VM Specifies the virtual machines which you want to get the error code of. @@ -120,7 +120,7 @@ Function Get-VMToolsInstallLastError { C:\PS> $VCServer = Connect-VIServer -Server -User -Password C:\PS> Get-VM -Server $VCServer | Get-VMToolsInstallLastError - Retrieves the last VMTools installation error code of all virtual machines which run in the $VCServer vCetner Server. + Retrieves the last VMTools installation error code of all virtual machines which run in the $VCServer vCenter Server. .EXAMPLE C:\PS> Get-VM "*win*" | Get-VMToolsInstallLastError @@ -144,7 +144,7 @@ Function Get-VMToolsInstallLastError { Retrieves the last VMTools installation error code of virtual machines which run on the "MyESXiHostName" ESXi host. .NOTES - This cmdlet assumes that you are connected to at least one vCenter Server system. + This advanced function assumes that you are connected to at least one vCenter Server system. .NOTES Author : Daoyuan Wang @@ -171,7 +171,7 @@ Function Get-VMToolsInstallLastError { Process { $result = @() foreach ($_ in $VM) { - $errorCodeInfo = $_.ExtensionData.Config.ExtraConfig.GetEnumerator() | where {$_.Key -eq "guestinfo.toolsInstallErrCode"} + $errorCodeInfo = $_.ExtensionData.Config.ExtraConfig.GetEnumerator() | Where-Object {$_.Key -eq "guestinfo.toolsInstallErrCode"} $info = New-Object PSObject $info | Add-Member -type NoteProperty -name VmName -value $_.Name @@ -186,10 +186,10 @@ Function Get-VMToolsInstallLastError { Function Get-VMToolsGuestInfo { <# .SYNOPSIS - This cmdlet retrieves the guest info of specified virtual machines. + This advanced function retrieves the guest info of specified virtual machines. .DESCRIPTION - This cmdlet retrieves the guest info such as HostName, IP, ToolsStatus, ToolsVersion, + This advanced function retrieves the guest info such as HostName, IP, ToolsStatus, ToolsVersion, ToolsInstallType and GuestFamily of specified virtual machines. .PARAMETER VM @@ -200,7 +200,7 @@ Function Get-VMToolsGuestInfo { C:\PS> $VCServer = Connect-VIServer -Server -User -Password C:\PS> Get-VM -Server $VCServer | Get-VMToolsGuestInfo - Retrieves guest info of all virtual machines which run in the $VCServer vCetner Server. + Retrieves guest info of all virtual machines which run in the $VCServer vCenter Server. .EXAMPLE C:\PS> Get-VM "*win*" | Get-VMToolsGuestInfo @@ -236,7 +236,7 @@ Function Get-VMToolsGuestInfo { Retrieves guest info of virtual machines which run on the "MyESXiHostName" ESXi host. .NOTES - This cmdlet assumes that you are connected to at least one vCenter Server system. + This advanced function assumes that you are connected to at least one vCenter Server system. .NOTES Author : Daoyuan Wang @@ -261,7 +261,7 @@ Function Get-VMToolsGuestInfo { ) Process { - Get-VM $VM | Select Name, @{Name="HostName"; Expression={$_.Guest.HostName}}, + Get-VM $VM | Select-Object Name, @{Name="HostName"; Expression={$_.Guest.HostName}}, @{Name="IP"; Expression={$_.Guest.ExtensionData.IpAddress}}, @{Name="ToolsStatus"; Expression={$_.Guest.ExtensionData.ToolsStatus}}, @{Name="ToolsVersion"; Expression={$_.Guest.ToolsVersion}}, @@ -274,10 +274,10 @@ Function Get-VMToolsGuestInfo { Function Get-VMByToolsInfo { <# .SYNOPSIS - This cmdlet retrieves the virtual machines with specified VMTools info. + This advanced function retrieves the virtual machines with specified VMTools info. .DESCRIPTION - This cmdlet retrieves the virtual machines with specified VMTools version, + This advanced function retrieves the virtual machines with specified VMTools version, running status or version status. .PARAMETER VM @@ -297,7 +297,7 @@ Function Get-VMByToolsInfo { C:\PS> $VCServer = Connect-VIServer -Server -User -Password C:\PS> Get-VM -Server $VCServer | Get-VMByToolsInfo - Retrieves the virtual machines with VMTools not running in vCetner Server $VCServer. + Retrieves the virtual machines with VMTools not running in vCenter Server $VCServer. .EXAMPLE C:\PS> Get-VM | Get-VMByToolsInfo -ToolsRunningStatus guestToolsNotRunning @@ -328,7 +328,7 @@ Function Get-VMByToolsInfo { Retrieves the virtual machines with VMTools that need to upgrade on the "MyESXiHostName" ESXi host. .NOTES - This cmdlet assumes that you are connected to at least one vCenter Server system. + This advanced function assumes that you are connected to at least one vCenter Server system. .NOTES Author : Daoyuan Wang @@ -361,9 +361,9 @@ Function Get-VMByToolsInfo { [String] $ToolsRunningStatus, [Parameter(Mandatory=$false)] - [ValidateSet("guestToolsNotInstalled", - "guestToolsNeedUpgrade", - "guestToolsCurrent", + [ValidateSet("guestToolsNotInstalled", + "guestToolsNeedUpgrade", + "guestToolsCurrent", "guestToolsUnmanaged")] [String] $ToolsVersionStatus ) @@ -376,65 +376,60 @@ Function Get-VMByToolsInfo { } if ($ToolsVersion) { - $vmList = $vmList | Where {$_.Guest.ToolsVersion -like $ToolsVersion} + $vmList = $vmList | Where-Object {$_.Guest.ToolsVersion -like $ToolsVersion} } if ($ToolsRunningStatus) { - $vmList = $vmList | Where {$_.Guest.ExtensionData.ToolsRunningStatus -eq $ToolsRunningStatus} + $vmList = $vmList | Where-Object {$_.Guest.ExtensionData.ToolsRunningStatus -eq $ToolsRunningStatus} } if ($ToolsVersionStatus) { - $vmList = $vmList | Where {$_.Guest.ExtensionData.ToolsVersionStatus -eq $ToolsVersionStatus} + $vmList = $vmList | Where-Object {$_.Guest.ExtensionData.ToolsVersionStatus -eq $ToolsVersionStatus} } $vmList } } -Function Set-VMToolsUpgradePolicy { +Function Get-VMToolsUpgradePolicy { <# .SYNOPSIS - This cmdlet sets the VMTool's upgrade policy to "upgradeAtPowerCycle". + This advanced function retrieves the VMTools upgrade policy info of specified virtual machines. .DESCRIPTION - This cmdlet sets the VMTool's upgrade policy to "upgradeAtPowerCycle" of specified virtual machines. + This advanced function retrieves the VMTools upgrade policy info of specified virtual machines. .PARAMETER VM - Specifies the virtual machines which you want to set the VMTool's upgrade policy of. + Specifies the virtual machines which you want to query VMTools status of. .EXAMPLE - C:\PS> Import-Module .\VMToolsManagement.psm1 - C:\PS> $VCServer = Connect-VIServer -Server -User -Password - C:\PS> Get-VM -Server $VCServer | Set-VMToolsUpgradePolicy - - Sets VMTool's upgrade policy to "upgradeAtPowerCycle" of all virtual machines in the $VCServer vCetner Server. + C:\PS> Get-VM "*rhel*" | Get-VMToolsUpgradePolicy + Name VMToolsUpgradePolicy + ------ ---------------------- + 111394-RHEL-6.8-0 manual + 111394-RHEL-6.8-1 manual + 111393-RHEL-Server-7.2 upgradeAtPowerCycle + Retrieves VMTools upgrade policy info of virtual machines with name containing "rhel". .EXAMPLE - C:\PS> Get-VM "*win*" | Set-VMToolsUpgradePolicy - - Sets VMTool's upgrade policy to "upgradeAtPowerCycle" of virtual machines with name containing "win". + C:\PS> Get-VM -Location "MyClusterName" | Get-VMToolsUpgradePolicy + Retrieves VMTools upgrade policy info from virtual machines which run in the "MyClusterName" cluster. .EXAMPLE - C:\PS> Get-VM -Location "MyClusterName" | Set-VMToolsUpgradePolicy - - Sets VMTool's upgrade policy to "upgradeAtPowerCycle" of virtual machines in the "MyClusterName" cluster. - -.EXAMPLE - C:\PS> Get-VMHost "MyESXiHostName" | Get-VM | Set-VMToolsUpgradePolicy - - Sets VMTool's upgrade policy to "upgradeAtPowerCycle" of virtual machines on the "MyESXiHostName" ESXi host. + C:\PS> Get-VMHost "MyESXiHostName" | Get-VM | Get-VMToolsUpgradePolicy + Retrieves VMTools upgrade policyinfo of virtual machines which run on the "MyESXiHostName" ESXi host. .NOTES - This cmdlet assumes that you are connected to at least one vCenter Server system. + This advanced function assumes that you are connected to at least one vCenter Server system. .NOTES - Author : Daoyuan Wang - Author email : daoyuanw@vmware.com + Author : Kyle Ruddy + Author email : kmruddy@gmail.com Version : 1.0 ==========Tested Against Environment========== - VMware vSphere Hypervisor(ESXi) Version : 6.5 (build 4564106) - VMware vCenter Server Version : 6.5 (build 4602587) - PowerCLI Version : PowerCLI 6.5 (build 4624819) + VMware vSphere Hypervisor(ESXi) Version : 6.5 (build 7388607) + VMware vCenter Server Version : 6.5 (build 7312210) + PowerCLI Version : PowerCLI 6.5 (build 7155375) PowerShell Version : 5.1 #> @@ -448,8 +443,81 @@ Function Set-VMToolsUpgradePolicy { [ValidateNotNullOrEmpty()] [VMware.VimAutomation.ViCore.Types.V1.Inventory.VirtualMachine[]] $VM ) + + Process { + + Get-VM $VM | Select-Object Name, @{Name="VMToolsUpgradePolicy"; Expression={$_.ExtensionData.Config.Tools.ToolsUpgradePolicy}} + + } + +} + +Function Set-VMToolsUpgradePolicy { +<# +.SYNOPSIS + This advanced function sets the VMTool's upgrade policy to either "manual" or "upgradeAtPowerCycle". + +.DESCRIPTION + This advanced function sets the VMTool's upgrade policy to either "manual" or "upgradeAtPowerCycle" of specified virtual machines. + +.PARAMETER VM + Specifies the virtual machines which you want to set the VMTool's upgrade policy of. + +.EXAMPLE + C:\PS> Import-Module .\VMToolsManagement.psm1 + C:\PS> $VCServer = Connect-VIServer -Server -User -Password + C:\PS> Get-VM -Server $VCServer | Set-VMToolsUpgradePolicy -UpgradePolicy manual + + Sets VMTool's upgrade policy to "manual" of all virtual machines in the $VCServer vCenter Server. + +.EXAMPLE + C:\PS> Get-VM "*win*" | Set-VMToolsUpgradePolicy -UpgradePolicy upgradeAtPowerCycle + + Sets VMTool's upgrade policy to "upgradeAtPowerCycle" of virtual machines with name containing "win". + +.EXAMPLE + C:\PS> Get-VM -Location "MyClusterName" | Set-VMToolsUpgradePolicy -UpgradePolicy upgradeAtPowerCycle + + Sets VMTool's upgrade policy to "upgradeAtPowerCycle" of virtual machines in the "MyClusterName" cluster. + +.EXAMPLE + C:\PS> Get-VMHost "MyESXiHostName" | Get-VM | Set-VMToolsUpgradePolicy -UpgradePolicy manual + + Sets VMTool's upgrade policy to "manual" of virtual machines on the "MyESXiHostName" ESXi host. + +.NOTES + This advanced function assumes that you are connected to at least one vCenter Server system. + +.NOTES + Author : Daoyuan Wang + Author email : daoyuanw@vmware.com + Version : 1.1 + Update Author : Kyle Ruddy + Update email : kmruddy@gmail.com + ==========Tested Against Environment========== + VMware vSphere Hypervisor(ESXi) Version : 6.5 (build 4564106)(build 7388607) + VMware vCenter Server Version : 6.5 (build 4602587)(build 7312210) + PowerCLI Version : PowerCLI 6.5 (build 4624819)(build 7155375) + PowerShell Version : 5.1 +#> + + [CmdletBinding(SupportsShouldProcess)] + + Param ( + [Parameter(Mandatory=$true, + ValueFromPipeLine = $true, + ValueFromPipelinebyPropertyName=$True, + Position = 0)] + [ValidateNotNullOrEmpty()] + [VMware.VimAutomation.ViCore.Types.V1.Inventory.VirtualMachine[]] $VM, + + [Parameter(Mandatory=$false, + Position = 1)] + [ValidateSet("upgradeAtPowerCycle", + "manual")] + [String] $UpgradePolicy + ) Begin { - $UpgradePolicy = "upgradeAtPowerCycle" $vmConfigSpec = New-Object VMware.Vim.VirtualMachineConfigSpec $vmConfigSpec.Tools = New-Object VMware.Vim.ToolsConfigInfo $vmConfigSpec.Tools.ToolsUpgradePolicy = $UpgradePolicy @@ -463,6 +531,7 @@ Function Set-VMToolsUpgradePolicy { if ($vmView.Config.Tools.ToolsUpgradePolicy -ne $UpgradePolicy) { Write-Verbose "Applying 'upgradeAtPowerCycle' setting to $($_.Name)..." $vmView.ReconfigVM($vmConfigSpec) + Get-VMToolsUpgradePolicy -VM $_ } } } @@ -471,10 +540,10 @@ Function Set-VMToolsUpgradePolicy { Function Invoke-VMToolsListProcessInVM { <# .Synopsis - This cmdlet lists the processes in the virtual machine. + This advanced function lists the processes in the virtual machine. .Description - This cmdlet lists the running processes in the virtual machine. + This advanced function lists the running processes in the virtual machine. .PARAMETER VM Specifies the virtual machine which you want to list the processes of. @@ -503,7 +572,7 @@ Function Invoke-VMToolsListProcessInVM { List the processes in the "MyVMName" VM. .NOTES - This cmdlet lists processes in the guest OS of virtual machine. + This advanced function lists processes in the guest OS of virtual machine. A VMTools should already be running in the guest OS. .NOTES @@ -559,10 +628,10 @@ Function Invoke-VMToolsListProcessInVM { Function Update-VMToolsImageLocation { <# .Synopsis - This cmdlet updates the link /productLocker in ESXi host. + This advanced function updates the link /productLocker in ESXi host. .Description - This cmdlet updates the link /productLocker in ESXi host directly to avoid host reboot. + This advanced function updates the link /productLocker in ESXi host directly to avoid host reboot. .Parameter VMHost Specifies the ESXi host on which you want to update the /productLocker link. @@ -574,7 +643,7 @@ Function Update-VMToolsImageLocation { Specifies the password you want to use for authenticating with the ESXi host. .Parameter ImageLocation - Specifies the new image location where you want /producterLocker to link. + Specifies the new image location Where-Object you want /producterLocker to link. .Example C:\PS> Import-Module .\VMToolsManagement.psm1 @@ -586,9 +655,9 @@ Function Update-VMToolsImageLocation { Update the link /producterLocker on $SampleHost to point to '/locker/packages/6.5.0/'. .NOTES - This cmdlet connects to ESXi host to execute shell command directly. + This advanced function connects to ESXi host to execute shell command directly. Make sure the SSH service on ESXi host is enabled, and a SSH library(Posh-SSH or SSH-Sessions etc.) - for powershell is already installed on client where you call This cmdlet. + for powershell is already installed on client Where-Object you call this advanced function. You can instal Posh-SSH by executing: iex (New-Object Net.WebClient).DownloadString("https://gist.github.com/darkoperator/6152630/raw/c67de4f7cd780ba367cccbc2593f38d18ce6df89/instposhsshdev") For SSH-Sessions installation and usage, please refer to @@ -628,7 +697,7 @@ Function Update-VMToolsImageLocation { Process { if (-not (Get-Command New-SSHSession)) { - Throw "This cmdlet depends on SSH library. Please ensure a SSH library is already installed!" + Throw "This advanced function depends on SSH library. Please ensure a SSH library is already installed!" } $password = new-object System.Security.SecureString @@ -656,18 +725,21 @@ Function Update-VMToolsImageLocation { } } -Function Update-VMToolsConfInVM { +Function Set-VMToolsConfInVM { <# .Synopsis - This cmdlet updates the tools.conf content in guest OS. + This advanced function sets the tools.conf content in guest OS. .Description - This cmdlet copies the tools.conf in gueset OS of virtual machine to localhost, - then updates it locally by setting "vmtoolsd.level = info" and copies it back to the guest OS. + This advanced function copies the tools.conf in gueset OS of virtual machine to localhost, + then sets it locally by setting "vmtoolsd.level" to a valid level and copies it back to the guest OS. .PARAMETER VM Specifies the virtual machine to update. +.PARAMETER LogLevel + Specifies the desired log level to log. + .Parameter GuestUser Specifies the user name you want to use for authenticating with the guest OS. @@ -684,16 +756,18 @@ Function Update-VMToolsConfInVM { Updates the tools.conf in $SampleVM, changes the vmtoolsd log level to info ("vmtoolsd.level = info") for example. .NOTES - This cmdlet updates the tools.conf in guest OS. A VMTools should already be running in the guest OS. + This advanced function updates the tools.conf in guest OS. A VMTools should already be running in the guest OS. .NOTES Author : Daoyuan Wang Author email : daoyuanw@vmware.com - Version : 1.0 + Version : 1.1 + Update Author : Kyle Ruddy + Update email : kmruddy@gmail.com ==========Tested Against Environment========== - VMware vSphere Hypervisor(ESXi) Version : 6.5 (build 4564106) - VMware vCenter Server Version : 6.5 (build 4602587) - PowerCLI Version : PowerCLI 6.5 (build 4624819) + VMware vSphere Hypervisor(ESXi) Version : 6.5 (build 4564106)(build 7388607) + VMware vCenter Server Version : 6.5 (build 4602587)(build 7312210) + PowerCLI Version : PowerCLI 6.5 (build 4624819)(build 7155375) PowerShell Version : 5.1 #> @@ -707,6 +781,17 @@ Function Update-VMToolsConfInVM { [ValidateNotNullOrEmpty()] [VMware.VimAutomation.ViCore.Types.V1.Inventory.VirtualMachine] $VM, + [Parameter(Mandatory=$true, + Position = 1)] + [ValidateSet("none", + "critical", + "error", + "warning", + "message", + "info", + "debug")] + [String] $LogLevel, + [Parameter(Mandatory=$true)] [String] $GuestUser, @@ -756,7 +841,7 @@ Function Update-VMToolsConfInVM { # Updates tools.conf by setting vmtoolsd.level = info, just for example. ############################################################################# $confContent = Get-Content $localToolsConfFile - $updatedContent = "vmtoolsd.level = info" + $updatedContent = "vmtoolsd.level = $LogLevel" Write-Verbose "Editing tools.conf (set 'vmtoolsd.level = info' for example)..." if ($confContent -match "vmtoolsd\.level") { @@ -775,19 +860,23 @@ Function Update-VMToolsConfInVM { -GuestUser $GuestUser -GuestPassword $GuestPassword -Force -ErrorAction:Stop } catch { Write-Error "Failed to update tools.conf of $VM" + Write-Verbose "Removing the local tools configuration file" + Remove-Item $localToolsConfFile return } Write-Host "The tools.conf updated in $VM successfully." -ForegroundColor Green + Write-Verbose "Removing the local tools configuration file" + Remove-Item $localToolsConfFile } } Function Invoke-VMToolsVIBInstall { <# .SYNOPSIS - This cmdlet installs VMTool VIB in ESXi hosts. + This advanced function installs VMTool VIB in ESXi hosts. .DESCRIPTION - This cmdlet installs VMTool VIB in specified ESXi hosts. + This advanced function installs VMTool VIB in specified ESXi hosts. .PARAMETER VMHost Specifies the ESXi hosts which you want to install VMTool VIB in. @@ -816,7 +905,7 @@ Function Invoke-VMToolsVIBInstall { Installs VMTool VIB in ESXi host of the "MyClusterName" cluster. .NOTES - This cmdlet assumes that you are connected to at least one vCenter Server system. + This advanced function assumes that you are connected to at least one vCenter Server system. .NOTES Author : Daoyuan Wang @@ -846,7 +935,7 @@ Function Invoke-VMToolsVIBInstall { foreach ($_ in $VMHost) { $esxcli = Get-EsxCLI -VMHost $_ -V2 - $result = $esxcli.software.vib.list.Invoke() | where {$_.name -match 'tools'} + $result = $esxcli.software.vib.list.Invoke() | Where-Object {$_.name -match 'tools'} Write-Verbose "Existing tools VIB on $_ before installing: $($result.Name)_$($result.Version)" # Install VIBs @@ -861,7 +950,7 @@ Function Invoke-VMToolsVIBInstall { Write-Error "Failed to install VMTools VIB package!" } else { Write-Verbose $result.Message - $result = $esxcli.software.vib.list.Invoke() | where {$_.name -match 'tools'} + $result = $esxcli.software.vib.list.Invoke() | Where-Object {$_.name -match 'tools'} Write-Verbose "Tools VIB on $_ after installing: $($result.Name)_$($result.Version)" Write-Host "VMTools VIB package installed on $_ successfully." -ForegroundColor Green } @@ -872,10 +961,10 @@ Function Invoke-VMToolsVIBInstall { Function Invoke-VMToolsUpgradeInVMs { <# .SYNOPSIS - This cmdlet upgrades VMTools to the version bundled by ESXi host. + This advanced function upgrades VMTools to the version bundled by ESXi host. .DESCRIPTION - This cmdlet upgrades VMTools of specified virtual machines to the version + This advanced function upgrades VMTools of specified virtual machines to the version bundled by ESXi host. You can also specify the number of virtual machines to upgrade in parallel. @@ -896,7 +985,7 @@ Function Invoke-VMToolsUpgradeInVMs { C:\PS> $VCServer = Connect-VIServer -Server -User -Password C:\PS> Get-VM -Server $VCServer | Invoke-VMToolsUpgradeInVMs -MaxParallelUpgrades 5 - Upgrades VMTools of all virtual machines in the $VCServer vCetner Server, 5 at a time in parallel. + Upgrades VMTools of all virtual machines in the $VCServer vCenter Server, 5 at a time in parallel. .EXAMPLE C:\PS> Get-VM | Invoke-VMToolsUpgradeInVMs -GuestOSType windows -MaxParallelUpgrades 1 | ft -Autosize @@ -929,7 +1018,7 @@ Function Invoke-VMToolsUpgradeInVMs { Upgrades VMTools of virtual machines on the "MyESXiHostName" ESXi host, 5 at a time. .NOTES - This cmdlet assumes an old VMTools is already running in the virtual machine. + This advanced function assumes an old VMTools is already running in the virtual machine. .NOTES Author : Daoyuan Wang @@ -1026,7 +1115,7 @@ Function Invoke-VMToolsUpgradeInVMs { # Load PowerCLI module and connect to VCServer, as child thread environment is independent with parent if(-not $global:DefaultVIServer) { $moduleName = "vmware.vimautomation.core" - if(-not (Get-Module | Where {$_.name -eq $moduleName})) { + if(-not (Get-Module | Where-Object {$_.name -eq $moduleName})) { try { Import-Module $moduleName -ErrorAction SilentlyContinue | Out-Null } @@ -1130,7 +1219,7 @@ Function Invoke-VMToolsUpgradeInVMs { End { #Verify all threads completed - while (($jobs | Where {$_.Handle.iscompleted -ne ‘Completed’}).Count -gt 0) { + while (($jobs | Where-Object {$_.Handle.iscompleted -ne ‘Completed’}).Count -gt 0) { Start-Sleep -Seconds 5 } @@ -1146,4 +1235,4 @@ Function Invoke-VMToolsUpgradeInVMs { } } -Export-ModuleMember *-* \ No newline at end of file +Export-ModuleMember *-* From 02286dbe53f7196003c4ab0ad71c3cf139dd204c Mon Sep 17 00:00:00 2001 From: Kyle Ruddy Date: Tue, 9 Jan 2018 14:55:28 -0500 Subject: [PATCH 106/112] Update VMToolsManagement.psm1 Update the quotes on line 1222 so that the module import no longer errors out. --- Modules/VMToolsManagement/VMToolsManagement.psm1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/VMToolsManagement/VMToolsManagement.psm1 b/Modules/VMToolsManagement/VMToolsManagement.psm1 index 335476d..df6e2e8 100644 --- a/Modules/VMToolsManagement/VMToolsManagement.psm1 +++ b/Modules/VMToolsManagement/VMToolsManagement.psm1 @@ -1219,7 +1219,7 @@ Function Invoke-VMToolsUpgradeInVMs { End { #Verify all threads completed - while (($jobs | Where-Object {$_.Handle.iscompleted -ne ‘Completed’}).Count -gt 0) { + while (($jobs | Where-Object {$_.Handle.iscompleted -ne "Completed"}).Count -gt 0) { Start-Sleep -Seconds 5 } From 5bee9abf629f2e16eda72dda891fa8b2eac14f88 Mon Sep 17 00:00:00 2001 From: cloudcowboyco Date: Thu, 11 Jan 2018 15:57:17 -0500 Subject: [PATCH 107/112] Fixing spelling errors and grammar issues --- README.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 7d2825c..86afe3e 100644 --- a/README.md +++ b/README.md @@ -59,15 +59,15 @@ The repository has been provided to allow the community to share resources that 1. Browse to the appropriate section (example: Scripts) 2. Select the “Create new file†button 3. On the new page, enter a file name, enter the resource’s information -4. Within the “Commit new file†area, enter the title and description, then select “Create a new branch for this commit…†and enter a sensical branch name +4. Within the “Commit new file†area, enter the title and description, then select “Create a new branch for this commit…†and enter a sensible branch name 5. Click “Propose new file†6. On the “Open a pull request†page, click “Create pull request†#### GitHub - Upload files Option 1. Browse to the appropriate section (example: Modules) 2. Select the “Upload files†button -3. On the new page, drag or choose the files to add -4. Within the “Commit changes†area, enter the title and description, then select “Create a new branch for this commit…†and enter a sensical branch name +3. On the new page, drag or choose the files to add +4. Within the “Commit changes†area, enter the title and description, then select “Create a new branch for this commit…†and enter a sensible branch name 5. Click “Propose new file†6. On the “Open a pull request†page, click “Create pull request†@@ -88,7 +88,7 @@ The following information must be included with each submitted scripting resourc #### Note Placement Examples: Script: Top few lines Module: Module manifest - + #### Required Script Note Example: `<#` `Script name: script_name.ps1` @@ -126,7 +126,7 @@ The following information should be included when possible. Inclusion of informa This section describes guidelines put in place to maintain a standard of quality while also promoting broader contribution. ### General Best Practices ### Resource Naming -* Give the resource a name that is indicitive of the actions and/or results of its running +* Give the resource a name that is indicative of the actions and/or results of its running ### Fault Handling * Read and apply the following basic fault handling where applicable: Microsoft’s Hey, Scripting Guy! Blog: https://blogs.technet.microsoft.com/heyscriptingguy/2014/07/09/handling-errors-the-powershell-way/ @@ -138,7 +138,7 @@ This section describes guidelines put in place to maintain a standard of quality * Avoid changing any global variables ### Help Information -* All resources should have inline documentation. +* All resources should have inline documentation. ### Scripts * The script should be easy to read and understand @@ -149,27 +149,27 @@ This section describes guidelines put in place to maintain a standard of quality * Use only standard verbs ### Security -* Usage of PowerShell’s strict mode is preferred, but not required. +* Usage of PowerShell’s strict mode is preferred, but not required. * Remove any information related to one’s own environment (examples: Passwords, DNS/IP Addresses, custom user credentials, etc) ## Resource Maintenance ### Maintenance Ownership -Ownership of any and all submitted resources are maintained by the submitter. This ownership also includes maintenance of any and all submitted resources. +Ownership of any and all submitted resources are maintained by the submitter. This ownership also includes maintenance of any and all submitted resources. ### Filing Issues -Any bugs or other issues should be filed within GitHub by way of the repository’s Issue Tracker. -### Resolving Issues -Any community member can resolve issues within the repository, however only the owner or a board member can approve the update. Once approved, assuming the resolution involves a pull request, only a board member will be able to merge and close the request. +Any bugs or other issues should be filed within GitHub by way of the repository’s Issue Tracker. +### Resolving Issues +Any community member can resolve issues within the repository, however only the owner or a board member can approve the update. Once approved, assuming the resolution involves a pull request, only a board member will be able to merge and close the request. ## Additional Resources ### Discussions Join in on the discussion within the VMware Code Slack team's PowerCLI channel: ### VMware Sample Exchange -It is highly recommened to add any and all submitted resources to the VMware Sample Exchange: - +It is highly recommended to add any and all submitted resources to the VMware Sample Exchange: + Sample Exchange can be allowed to access your GitHub resources, by way of a linking process, where they can be indexed and searched by the community. There are VMware social media accounts which will advertise resources posted to the site and there's no additional accounts needed, as the VMware Sample Exchange uses MyVMware credentials. ## VMWARE TECHNOLOGY PREVIEW LICENSE AGREEMENT -The VMware Technnology Preview License Agreement: +The VMware Technology Preview License Agreement: # Repository Administrator Resources ## Table of Contents @@ -180,7 +180,7 @@ The VMware Technnology Preview License Agreement: Date: Thu, 18 Jan 2018 08:37:38 -0800 Subject: [PATCH 108/112] Add PowerCLI_FixNestedFolders.ps1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add script to help remove the nested version folders when moving PowerCLI module folders from systems with PoSh 5.x to 4.0/3.0 systems that can’t properly understand those versioned folders. More info: https://blogs.vmware.com/PowerCLI/2018/01/powercli-offline-installation- walkthrough.html --- Scripts/PowerCLI_FixNestedFolders.ps1 | 96 +++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 Scripts/PowerCLI_FixNestedFolders.ps1 diff --git a/Scripts/PowerCLI_FixNestedFolders.ps1 b/Scripts/PowerCLI_FixNestedFolders.ps1 new file mode 100644 index 0000000..a4e8afe --- /dev/null +++ b/Scripts/PowerCLI_FixNestedFolders.ps1 @@ -0,0 +1,96 @@ +<# +Script name: PowerCLI_FixNestedFolders.ps1 +Created on: 01/11/2018 +Author: Kyle Ruddy, @kmruddy, thatcouldbeaproblem.com +Description: The purpose of the script is to remove the nested Version based folders when using Powercli on systems using older versions of PowerShell +Dependencies: None known + +===Tested Against Environment==== +PowerCLI Version: PowerCLI 6.5.4 +PowerShell Version: 5.1, 4.0 +OS Version: Server 2016, Server 2012 R2 +#> + +# Variable used to store where the PowerCLI module folders exist +$pcliFolder = @() + +# Variable used to store the current PSModulePath locations +$downloadDir = $env:PSModulePath.Split(';') + +# Loop to detect PowerCLI module folders in any of the PSModulePath locations +foreach ($possPath in $downloadDir) { + + # Verifying the PSModulePath location exists + if ((Test-Path -Path $possPath) -eq $true) { + + # Searching for folders with the name of 'VMware.*' + $tempFolder = Get-ChildItem -Path $possPath -Name "VMware.*" + + # If a VMware.* module folder is found, the full path is added to the pcliFolder variable + if ($tempFolder) { + + foreach ($moduleName in $tempFolder) { + $pcliFolder += $possPath + "\" + $moduleName + } + } + + } +} + +# Verifying that there were PowerCLI module folders found +if ($pcliFolder) { + + # Looping through each of the found PowerCLI module folders + foreach ($dir in $pcliFolder) { + + # Variable to be used if there are several PowerCLI module versions available + $historicDir = $null + + # Varibale used to store the PowerCLI module version folder + $tempDir = Get-ChildItem -Path $dir + + # Verifying whether or not there are several PowerCLI module versions available by checking for a type of 'array' + if ($tempDir -is [array]) { + + # Variable used to store the current folder structure + $historicDir = $tempDir + # Updating the tempDir variable to only contain the newest PowerCLI module version folder + $tempDir = $tempDir | Sort-Object Name -Descending | select-Object -First 1 + + } + + # Verifying the child item is indeed a folder + if ($tempDir.GetType().Name -eq "DirectoryInfo") { + + # Obtaining the child objects of the PowerCLI module version folder and copying them to the parent folder + $tempDir | Get-ChildItem | Copy-Item -Destination $dir -ErrorAction Stop + + # Checking for any nested folders within the PowerCLI module version folder + if ($tempDir | Get-ChildItem -Directory) { + + # Obtaining and storing the child items to a variable, then copying the items to the parent folder's nested folder + $nestFolder = $tempDir | Get-ChildItem -Directory + foreach ($nestDir in $nestFolder) { + $nestDir | Get-ChildItem | Copy-Item -Destination ($dir + "\" + $nestDir.Name) -ErrorAction Stop + } + + } + + # Detecting whether the historicDir variable was used + if ($historicDir) { + + # Removing any of the former, no longer needed, directory structure + $historicDir | Remove-Item -Recurse -Force + + } + else { + + # Removing any of the former, no longer needed, directory structure + $tempDir | Remove-Item -Recurse -Force + + } + } + } + +} +else {Write-Host 'No PowerCLI module folders founds in the $PSModulePath directories.'} \ No newline at end of file From 61aead2685b8af74075cbdc3dc4b74ee9bb1350d Mon Sep 17 00:00:00 2001 From: Ben Meadowcroft Date: Tue, 30 Jan 2018 21:03:35 -0800 Subject: [PATCH 109/112] Initial commit to Examples of SRM-Cmdlets --- Modules/SRM/.gitattributes | 1 + Modules/SRM/.gitignore | 1 + Modules/SRM/Examples/ReportConfiguration.ps1 | 167 ++++++ Modules/SRM/Examples/SrmTagging.ps1 | 34 ++ Modules/SRM/LICENSE.txt | 74 +++ Modules/SRM/Meadowcroft.Srm.Protection.ps1 | 422 ++++++++++++++ Modules/SRM/Meadowcroft.Srm.Recovery.ps1 | 556 +++++++++++++++++++ Modules/SRM/Meadowcroft.Srm.Storage.ps1 | 24 + Modules/SRM/Meadowcroft.Srm.psd1 | 92 +++ Modules/SRM/Meadowcroft.Srm.psm1 | 148 +++++ Modules/SRM/NOTICE.txt | 7 + Modules/SRM/README.md | 81 +++ 12 files changed, 1607 insertions(+) create mode 100644 Modules/SRM/.gitattributes create mode 100644 Modules/SRM/.gitignore create mode 100644 Modules/SRM/Examples/ReportConfiguration.ps1 create mode 100644 Modules/SRM/Examples/SrmTagging.ps1 create mode 100644 Modules/SRM/LICENSE.txt create mode 100644 Modules/SRM/Meadowcroft.Srm.Protection.ps1 create mode 100644 Modules/SRM/Meadowcroft.Srm.Recovery.ps1 create mode 100644 Modules/SRM/Meadowcroft.Srm.Storage.ps1 create mode 100644 Modules/SRM/Meadowcroft.Srm.psd1 create mode 100644 Modules/SRM/Meadowcroft.Srm.psm1 create mode 100644 Modules/SRM/NOTICE.txt create mode 100644 Modules/SRM/README.md diff --git a/Modules/SRM/.gitattributes b/Modules/SRM/.gitattributes new file mode 100644 index 0000000..75f1064 --- /dev/null +++ b/Modules/SRM/.gitattributes @@ -0,0 +1 @@ +*.psd1 diff diff --git a/Modules/SRM/.gitignore b/Modules/SRM/.gitignore new file mode 100644 index 0000000..6f66c74 --- /dev/null +++ b/Modules/SRM/.gitignore @@ -0,0 +1 @@ +*.zip \ No newline at end of file diff --git a/Modules/SRM/Examples/ReportConfiguration.ps1 b/Modules/SRM/Examples/ReportConfiguration.ps1 new file mode 100644 index 0000000..f7a8036 --- /dev/null +++ b/Modules/SRM/Examples/ReportConfiguration.ps1 @@ -0,0 +1,167 @@ +# Depends on SRM Helper Methods - https://github.com/benmeadowcroft/SRM-Cmdlets +# It is assumed that the connection to VC and SRM Server have already been made + +Function Get-SrmConfigReportSite { + Param( + [VMware.VimAutomation.Srm.Types.V1.SrmServer] $SrmServer + ) + + Get-SrmServer $SrmServer | + Format-Table -Wrap -AutoSize @{Label="SRM Site Name"; Expression={$_.ExtensionData.GetSiteName()} }, + @{Label="SRM Host"; Expression={$_.Name} }, + @{Label="SRM Port"; Expression={$_.Port} }, + @{Label="Version"; Expression={$_.Version} }, + @{Label="Build"; Expression={$_.Build} }, + @{Label="SRM Peer Site Name"; Expression={$_.ExtensionData.GetPairedSite().Name} } +} + +Function Get-SrmConfigReportPlan { + Param( + [VMware.VimAutomation.Srm.Types.V1.SrmServer] $SrmServer + ) + + Get-SrmRecoveryPlan -SrmServer $SrmServer | %{ + $rp = $_ + $rpinfo = $rp.GetInfo() + $peerState = $rp.GetPeer().State + $pgs = Get-SrmProtectionGroup -RecoveryPlan $rp + $pgnames = $pgs | %{ $_.GetInfo().Name } + + $output = "" | select plan, state, peerState, groups + $output.plan = $rpinfo.Name + $output.state = $rpinfo.State + $output.peerState = $peerState + if ($pgnames) { + $output.groups = [string]::Join(",`r`n", $pgnames) + } else { + $output.groups = "NONE" + } + + $output + } | Format-Table -Wrap -AutoSize @{Label="Recovery Plan Name"; Expression={$_.plan} }, + @{Label="Recovery State"; Expression={$_.state} }, + @{Label="Peer Recovery State"; Expression={$_.peerState} }, + @{Label="Protection Groups"; Expression={$_.groups}} +} + + +Function Get-SrmConfigReportProtectionGroup { + Param( + [VMware.VimAutomation.Srm.Types.V1.SrmServer] $SrmServer + ) + + Get-SrmProtectionGroup -SrmServer $SrmServer | %{ + $pg = $_ + $pginfo = $pg.GetInfo() + $pgstate = $pg.GetProtectionState() + $peerState = $pg.GetPeer().State + $rps = Get-SrmRecoveryPlan -ProtectionGroup $pg + $rpnames = $rps | %{ $_.GetInfo().Name } + + $output = "" | select name, type, state, peerState, plans + $output.name = $pginfo.Name + $output.type = $pginfo.Type + $output.state = $pgstate + $output.peerState = $peerState + if ($rpnames) { + $output.plans = [string]::Join(",`r`n", $rpnames) + } else { + $output.plans = "NONE" + } + + $output + } | Format-Table -Wrap -AutoSize @{Label="Protection Group Name"; Expression={$_.name} }, + @{Label="Type"; Expression={$_.type} }, + @{Label="Protection State"; Expression={$_.state} }, + @{Label="Peer Protection State"; Expression={$_.peerState} }, + @{Label="Recovery Plans"; Expression={$_.plans} } +} + + +Function Get-SrmConfigReportProtectedDatastore { + Param( + [VMware.VimAutomation.Srm.Types.V1.SrmServer] $SrmServer + ) + + Get-SrmProtectionGroup -SrmServer $SrmServer -Type "san" | %{ + $pg = $_ + $pginfo = $pg.GetInfo() + $pds = Get-SrmProtectedDatastore -ProtectionGroup $pg + $pds | %{ + $pd = $_ + $output = "" | select datacenter, group, name, capacity, free + $output.datacenter = $pd.Datacenter.Name + $output.group = $pginfo.Name + $output.name = $pd.Name + $output.capacity = $pd.CapacityGB + $output.free = $pd.FreeSpaceGB + + $output + + } + } | Format-Table -Wrap -AutoSize -GroupBy "datacenter" @{Label="Datastore Name"; Expression={$_.name} }, + @{Label="Capacity GB"; Expression={$_.capacity} }, + @{Label="Free GB"; Expression={$_.free} }, + @{Label="Protection Group"; Expression={$_.group} } +} + + +Function Get-SrmConfigReportProtectedVm { + Param( + [VMware.VimAutomation.Srm.Types.V1.SrmServer] $SrmServer + ) + + $srmversion = Get-SrmServerVersion -SrmServer $SrmServer + $srmMajorVersion, $srmMinorVersion = $srmversion -split "\." + + Get-SrmProtectionGroup -SrmServer $SrmServer | %{ + $pg = $_ + $pginfo = $pg.GetInfo() + $pvms = Get-SrmProtectedVM -ProtectionGroup $pg + $rps = Get-SrmRecoveryPlan -ProtectionGroup $pg + $rpnames = $rps | %{ $_.GetInfo().Name } + $pvms | %{ + $pvm = $_ + if ($srmMajorVersion -ge 6 -or ($srmMajorVersion -eq 5 -and $srmMinorVersion -eq 8)) { + $rs = $rps | Select -First 1 | %{ $_.GetRecoverySettings($pvm.Vm.MoRef) } + } + $output = "" | select group, name, moRef, needsConfiguration, state, plans, priority, finalPowerState, preCallouts, postCallouts + $output.group = $pginfo.Name + $output.name = $pvm.Vm.Name + $output.moRef = $pvm.Vm.MoRef # this is necessary in case we can't retrieve the name when VC is unavailable + $output.needsConfiguration = $pvm.NeedsConfiguration + $output.state = $pvm.State + $output.plans = [string]::Join(",`r`n", $rpnames) + if ($rs) { + $output.priority = $rs.RecoveryPriority + $output.finalPowerState = $rs.FinalPowerState + $output.preCallouts = $rs.PrePowerOnCallouts.Count + $output.postCallouts = $rs.PostPowerOnCallouts.Count + } + $output + + } + } | Format-Table -Wrap -AutoSize @{Label="VM Name"; Expression={$_.name} }, + @{Label="VM MoRef"; Expression={$_.moRef} }, + @{Label="Needs Config"; Expression={$_.needsConfiguration} }, + @{Label="VM Protection State"; Expression={$_.state} }, + @{Label="Protection Group"; Expression={$_.group} }, + @{Label="Recovery Plans"; Expression={$_.plans} }, + @{Label="Recovery Priority"; Expression={$_.priority} }, + @{Label="Final Power State"; Expression={$_.finalPowerState} }, + @{Label="Pre-PowerOn Callouts"; Expression={$_.preCallouts} }, + @{Label="Post-PowerOn Callouts"; Expression={$_.postCallouts} } + +} + +Function Get-SrmConfigReport { + Param( + [VMware.VimAutomation.Srm.Types.V1.SrmServer] $SrmServer + ) + + Get-SrmConfigReportSite -SrmServer $SrmServer + Get-SrmConfigReportPlan -SrmServer $SrmServer + Get-SrmConfigReportProtectionGroup -SrmServer $SrmServer + Get-SrmConfigReportProtectedDatastore -SrmServer $SrmServer + Get-SrmConfigReportProtectedVm -SrmServer $SrmServer +} diff --git a/Modules/SRM/Examples/SrmTagging.ps1 b/Modules/SRM/Examples/SrmTagging.ps1 new file mode 100644 index 0000000..1e33659 --- /dev/null +++ b/Modules/SRM/Examples/SrmTagging.ps1 @@ -0,0 +1,34 @@ +# Depends on SRM Helper Methods - https://github.com/benmeadowcroft/SRM-Cmdlets +# It is assumed that the connections to active VC and SRM Server have already been made + +Import-Module Meadowcroft.SRM -Prefix Srm + +$TagCategoryName = 'Meadowcroft.SRM.VM' +$TagCategoryDescription = 'Tag category for tagging VMs with SRM state' + +# If the tag category doesn't exist, create it and the relevant tags +$TagCategory = Get-TagCategory -Name $TagCategoryName -ErrorAction SilentlyContinue +if (-Not $TagCategory) { + Write-Output "Creating Tag Category $TagCategoryName" + $TagCategory = New-TagCategory -Name $TagCategoryName -Description $TagCategoryDescription -EntityType 'VirtualMachine' + + Write-Output "Creating Tag SrmProtectedVm" + New-Tag -Name 'SrmProtectedVm' -Category $TagCategory -Description "VM protected by VMware SRM" + Write-Output "Creating Tag SrmTestVm" + New-Tag -Name 'SrmTestVm' -Category $TagCategory -Description "Test VM instantiated by VMware SRM" + Write-Output "Creating Tag SrmPlaceholderVm" + New-Tag -Name 'SrmPlaceholderVm' -Category $TagCategory -Description "Placeholder VM used by VMware SRM" +} + +$protectedVmTag = Get-Tag -Name 'SrmProtectedVm' -Category $TagCategory +$testVmTag = Get-Tag -Name 'SrmTestVm' -Category $TagCategory +$placeholderVmTag = Get-Tag -Name 'SrmPlaceholderVm' -Category $TagCategory + +# Assign protected tag to a VM, use ready state to get "local" protected VMs +Get-SrmProtectedVM -State Ready | %{ New-TagAssignment -Tag $protectedVmTag -Entity $(Get-VIObjectByVIView $_.Vm) | Out-Null } + +# Assign test tag to a VM +Get-SrmTestVM | %{ New-TagAssignment -Tag $testVmTag -Entity $_ | Out-Null } + +# Assign placeholder tag to a VM +Get-SrmPlaceholderVM | %{ New-TagAssignment -Tag $placeholderVmTag -Entity $_ | Out-Null } diff --git a/Modules/SRM/LICENSE.txt b/Modules/SRM/LICENSE.txt new file mode 100644 index 0000000..22896f0 --- /dev/null +++ b/Modules/SRM/LICENSE.txt @@ -0,0 +1,74 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: +(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and +(b) You must cause any modified files to carry prominent notices stating that You changed the files; and +(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and +(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + +You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied. + + See the License for the specific language governing permissions and + limitations under the License. + + diff --git a/Modules/SRM/Meadowcroft.Srm.Protection.ps1 b/Modules/SRM/Meadowcroft.Srm.Protection.ps1 new file mode 100644 index 0000000..d522cb6 --- /dev/null +++ b/Modules/SRM/Meadowcroft.Srm.Protection.ps1 @@ -0,0 +1,422 @@ +# SRM Helper Methods - https://github.com/benmeadowcroft/SRM-Cmdlets + +<# +.SYNOPSIS +Get the subset of protection groups matching the input criteria + +.PARAMETER Name +Return protection groups matching the specified name + +.PARAMETER Type +Return protection groups matching the specified protection group +type. For SRM 5.0-5.5 this is either 'san' for protection groups +consisting of a set of replicated datastores or 'vr' for vSphere +Replication based protection groups. + +.PARAMETER RecoveryPlan +Return protection groups associated with a particular recovery +plan + +.PARAMETER SrmServer +the SRM server to use for this operation. +#> +Function Get-ProtectionGroup { + [cmdletbinding()] + Param( + [Parameter(position=1)][string] $Name, + [string] $Type, + [Parameter (ValueFromPipeline=$true)][VMware.VimAutomation.Srm.Views.SrmRecoveryPlan[]] $RecoveryPlan, + [VMware.VimAutomation.Srm.Types.V1.SrmServer] $SrmServer + ) + begin { + $api = Get-ServerApiEndpoint -SrmServer $SrmServer + $pgs = @() + } + process { + if ($RecoveryPlan) { + foreach ($rp in $RecoveryPlan) { + $pgs += $RecoveryPlan.GetInfo().ProtectionGroups + } + $pgs = Select_UniqueByMoRef($pgs) + } else { + $pgs += $api.Protection.ListProtectionGroups() + } + } + end { + $pgs | ForEach-Object { + $pg = $_ + $pgi = $pg.GetInfo() + $selected = (-not $Name -or ($Name -eq $pgi.Name)) -and (-not $Type -or ($Type -eq $pgi.Type)) + if ($selected) { + Add-Member -InputObject $pg -MemberType NoteProperty -Name "Name" -Value $pgi.Name + $pg + } + } + } +} + +<# +.SYNOPSIS +Get the subset of protected VMs matching the input criteria + +.PARAMETER Name +Return protected VMs matching the specified name + +.PARAMETER State +Return protected VMs matching the specified state. For protected +VMs on the protected site this is usually 'ready', for +placeholder VMs this is 'shadowing' + +.PARAMETER ProtectionGroup +Return protected VMs associated with particular protection +groups +#> +Function Get-ProtectedVM { + [cmdletbinding()] + Param( + [Parameter(position=1)][string] $Name, + [VMware.VimAutomation.Srm.Views.SrmProtectionGroupProtectionState] $State, + [VMware.VimAutomation.Srm.Views.SrmProtectionGroupProtectionState] $PeerState, + [switch] $ConfiguredOnly, + [switch] $UnconfiguredOnly, + [Parameter (ValueFromPipeline=$true)][VMware.VimAutomation.Srm.Views.SrmProtectionGroup[]] $ProtectionGroup, + [Parameter (ValueFromPipeline=$true)][VMware.VimAutomation.Srm.Views.SrmRecoveryPlan[]] $RecoveryPlan, + [string] $ProtectionGroupName, + [VMware.VimAutomation.Srm.Types.V1.SrmServer] $SrmServer + ) + + if ($null -eq $ProtectionGroup) { + $ProtectionGroup = Get-ProtectionGroup -Name $ProtectionGroupName -RecoveryPlan $RecoveryPlan -SrmServer $SrmServer + } + $ProtectionGroup | ForEach-Object { + $pg = $_ + $pg.ListProtectedVms() | ForEach-Object { + # try and update the view data for the protected VM + try { + $_.Vm.UpdateViewData() + } catch { + Write-Error $_ + } finally { + $_ + } + } | Where-object { -not $Name -or ($Name -eq $_.Vm.Name) } | + where-object { -not $State -or ($State -eq $_.State) } | + where-object { -not $PeerState -or ($PeerState -eq $_.PeerState) } | + where-object { ($ConfiguredOnly -and $_.NeedsConfiguration -eq $false) -or ($UnconfiguredOnly -and $_.NeedsConfiguration -eq $true) -or (-not $ConfiguredOnly -and -not $UnconfiguredOnly) } + } +} + + +<# +.SYNOPSIS +Get the unprotected VMs that are associated with a protection group + +.PARAMETER ProtectionGroup +Return unprotected VMs associated with particular protection +groups. For VR protection groups this is VMs that are associated +with the PG but not configured, For ABR protection groups this is +VMs on replicated datastores associated with the group that are not +configured. +#> +Function Get-UnProtectedVM { + [cmdletbinding()] + Param( + [Parameter (ValueFromPipeline=$true)][VMware.VimAutomation.Srm.Views.SrmProtectionGroup[]] $ProtectionGroup, + [Parameter (ValueFromPipeline=$true)][VMware.VimAutomation.Srm.Views.SrmRecoveryPlan[]] $RecoveryPlan, + [string] $ProtectionGroupName, + [VMware.VimAutomation.Srm.Types.V1.SrmServer] $SrmServer + ) + + if ($null -eq $ProtectionGroup) { + $ProtectionGroup = Get-ProtectionGroup -Name $ProtectionGroupName -RecoveryPlan $RecoveryPlan -SrmServer $SrmServer + } + + $associatedVMs = @() + $protectedVmRefs = @() + + $ProtectionGroup | ForEach-Object { + $pg = $_ + # For VR listAssociatedVms to get list of VMs + if ($pg.GetInfo().Type -eq 'vr') { + $associatedVMs += @($pg.ListAssociatedVms() | Get-VIObjectByVIView) + } + # TODO test this: For ABR get VMs on GetProtectedDatastore + if ($pg.GetInfo().Type -eq 'san') { + $pds = @(Get-ProtectedDatastore -ProtectionGroup $pg) + $pds | ForEach-Object { + $ds = Get-Datastore -id $_.MoRef + $associatedVMs += @(Get-VM -Datastore $ds) + } + } + + # get protected VMs + $protectedVmRefs += @(Get-ProtectedVM -ProtectionGroup $pg | ForEach-Object { $_.Vm.MoRef } | Select-Object -Unique) + } + + # get associated but unprotected VMs + $associatedVMs | Where-Object { $protectedVmRefs -notcontains $_.ExtensionData.MoRef } +} + + +#Untested as I don't have ABR setup in my lab yet +<# +.SYNOPSIS +Get the subset of protected Datastores matching the input criteria + +.PARAMETER ProtectionGroup +Return protected datastores associated with particular protection +groups +#> +Function Get-ProtectedDatastore { + [cmdletbinding()] + Param( + [Parameter (ValueFromPipeline=$true)][VMware.VimAutomation.Srm.Views.SrmProtectionGroup[]] $ProtectionGroup, + [Parameter (ValueFromPipeline=$true)][VMware.VimAutomation.Srm.Views.SrmRecoveryPlan[]] $RecoveryPlan, + [string] $ProtectionGroupName, + [VMware.VimAutomation.Srm.Types.V1.SrmServer] $SrmServer + ) + + if (-not $ProtectionGroup) { + $ProtectionGroup = Get-ProtectionGroup -Name $ProtectionGroupName -RecoveryPlan $RecoveryPlan -SrmServer $SrmServer + } + $ProtectionGroup | ForEach-Object { + $pg = $_ + if ($pg.GetInfo().Type -eq 'san') { # only supported for array based replication datastores + $pg.ListProtectedDatastores() + } + } +} + + +#Untested as I don't have ABR setup in my lab yet +<# +.SYNOPSIS +Get the replicated datastores that aren't associated with a protection group. +#> +Function Get-ReplicatedDatastore { + [cmdletbinding()] + Param( + [VMware.VimAutomation.Srm.Types.V1.SrmServer] $SrmServer + ) + + $api = Get-ServerApiEndpoint -SrmServer $SrmServer + + $api.Protection.ListUnassignedReplicatedDatastores() +} + +<# +.SYNOPSIS +Protect a VM using SRM + +.PARAMETER ProtectionGroup +The protection group that this VM will belong to + +.PARAMETER Vm +The virtual machine to protect +#> +Function Protect-VM { + [cmdletbinding()] + Param( + [Parameter (Mandatory=$true)][VMware.VimAutomation.Srm.Views.SrmProtectionGroup] $ProtectionGroup, + [Parameter (ValueFromPipeline=$true)][VMware.VimAutomation.ViCore.Types.V1.Inventory.VirtualMachine] $Vm, + [Parameter (ValueFromPipeline=$true)][VMware.Vim.VirtualMachine] $VmView + ) + + $moRef = Get_MoRefFromVmObj -Vm $Vm -VmView $VmView + + $pgi = $ProtectionGroup.GetInfo() + #TODO query protection status first + + if ($moRef) { + if ($pgi.Type -eq 'vr') { + $ProtectionGroup.AssociateVms(@($moRef)) + } + $protectionSpec = New-Object VMware.VimAutomation.Srm.Views.SrmProtectionGroupVmProtectionSpec + $protectionSpec.Vm = $moRef + $protectTask = $ProtectionGroup.ProtectVms($protectionSpec) + while(-not $protectTask.IsComplete()) { Start-Sleep -Seconds 1 } + $protectTask.GetResult() + } else { + throw "Can't protect the VM, no MoRef found." + } +} + + +<# +.SYNOPSIS +Unprotect a VM using SRM + +.PARAMETER ProtectionGroup +The protection group that this VM will be removed from + +.PARAMETER Vm +The virtual machine to unprotect +#> +Function Unprotect-VM { + [cmdletbinding()] + Param( + [Parameter (Mandatory=$true)][VMware.VimAutomation.Srm.Views.SrmProtectionGroup] $ProtectionGroup, + [Parameter (ValueFromPipeline=$true)][VMware.VimAutomation.ViCore.Types.V1.Inventory.VirtualMachine] $Vm, + [Parameter (ValueFromPipeline=$true)][VMware.Vim.VirtualMachine] $VmView, + [Parameter (ValueFromPipeline=$true)][VMware.VimAutomation.Srm.Views.SrmProtectionGroupProtectedVm] $ProtectedVm + ) + + $moRef = Get_MoRefFromVmObj -Vm $Vm -VmView $VmView -ProtectedVm $ProtectedVm + + $pgi = $ProtectionGroup.GetInfo() + $protectTask = $ProtectionGroup.UnprotectVms($moRef) + while(-not $protectTask.IsComplete()) { Start-Sleep -Seconds 1 } + if ($pgi.Type -eq 'vr') { + $ProtectionGroup.UnassociateVms(@($moRef)) + } + $protectTask.GetResult() +} + +<# +.SYNOPSIS +Get a protection group folder + +.PARAMETER SrmServer +The SRM Server to query for the protection group folder +#> +Function Get-ProtectionGroupFolder { + [cmdletbinding()] + Param( + [VMware.VimAutomation.Srm.Types.V1.SrmServer] $SrmServer + ) + + $api = Get-ServerApiEndpoint -SrmServer $SrmServer + + $folder = $api.Protection.GetProtectionGroupRootFolder() + + return $folder +} + +<# +.SYNOPSIS +Create a new protection group + +.PARAMETER Name +The name of the protection group + +.PARAMETER Description +Description of the protection group + +.PARAMETER Folder +The protection group folder in which to create the new protection group + +.PARAMETER ArrayReplication +Set if protection group is for replicating VMs using Array based replication + +.PARAMETER vSphereReplication +Set if protection group is for replicating VMs with vSphere Replication + +.PARAMETER VMs +For vSphere Replication based protection, the VMs to add to the replication +group. These should already be replicated. + +.PARAMETER VMViews +For vSphere Replication based protection, the VMs to add to the replication +group. These should already be replicated. + +.PARAMETER SrmServer +The SRM Server to perform the operation against +#> +Function New-ProtectionGroup { + [cmdletbinding(DefaultParameterSetName="VR", SupportsShouldProcess=$True, ConfirmImpact="Medium")] + [OutputType([VMware.VimAutomation.Srm.Views.SrmProtectionGroup])] + Param( + [Parameter (Mandatory=$true)] $Name, + $Description, + [VMware.VimAutomation.Srm.Views.SrmProtectionGroupFolder] $Folder, + [Parameter (ParameterSetName="ABR", Mandatory=$true)][switch] $ArrayReplication, + [Parameter (ValueFromPipeline=$true, ParameterSetName="ABR")][VMware.VimAutomation.ViCore.Types.V1.DatastoreManagement.Datastore[]] $Datastores, + [Parameter (ValueFromPipeline=$true, ParameterSetName="ABR")][VMware.Vim.Datastore[]] $DatastoreViews, + [Parameter (ParameterSetName="VR", Mandatory=$true)][switch] $vSphereReplication, + [Parameter (ValueFromPipeline=$true, ParameterSetName="VR")][VMware.VimAutomation.ViCore.Types.V1.Inventory.VirtualMachine[]] $VMs, + [Parameter (ValueFromPipeline=$true, ParameterSetName="VR")][VMware.Vim.VirtualMachine[]] $VMViews, + [VMware.VimAutomation.Srm.Types.V1.SrmServer] $SrmServer + ) + + $api = Get-ServerApiEndpoint $SrmServer + [VMware.VimAutomation.Srm.Views.SrmCreateProtectionGroupTask] $task = $null + + #get root folder if this wasn't specified as a parameter + if(-not $Folder) { + $Folder = Get-ProtectionGroupFolder -SrmServer $SrmServer + } + + if ($vSphereReplication) { + #create list of managed object references from VM and/or VM view arrays + [VMware.Vim.ManagedObjectReference[]]$moRefs = @() + foreach ($vm in $VMs) { + $moRefs += Get_MoRefFromVmObj -Vm $Vm + } + foreach ($VmView in $VMViews) { + $moRefs += Get_MoRefFromVmObj -VmView $VmView + } + + if ($pscmdlet.ShouldProcess($Name, "New")) { + $task = $api.Protection.CreateHbrProtectionGroup($Folder.MoRef, $Name, $Description, $moRefs) + } + + } elseif ($ArrayReplication) { + #create list of managed object references from VM and/or VM view arrays + $moRefs = @() + foreach ($ds in $Datastores) { + $moRefs += $ds.ExtensionData.MoRef + } + foreach ($DsView in $DatastoreViews) { + $moRefs += $DsView.MoRef + } + + if ($pscmdlet.ShouldProcess($Name, "New")) { + $task = $api.Protection.CreateAbrProtectionGroup($Folder.MoRef, $Name, $Description, $moRefs) + } + + } else { + throw "Undetermined protection group type" + } + + # Complete task + while(-not $task.IsCreateProtectionGroupComplete()) { Start-Sleep -Seconds 1 } + + # Retrieve the protection group, and protect associated VMs + $pg = $task.GetNewProtectionGroup() + if ($pg) { + $unProtectedVMs = Get-UnProtectedVM -ProtectionGroup $pg + $unProtectedVMs | Protect-VM -ProtectionGroup $pg + } + + return $pg +} + + +<# +.SYNOPSIS +Delete a protection group + +.PARAMETER ProtectionGroup +The protection group to remove + +.PARAMETER SrmServer +The SRM Server to perform the operation against +#> +Function Remove-ProtectionGroup { + [cmdletbinding(SupportsShouldProcess=$True, ConfirmImpact="High")] + [OutputType([VMware.VimAutomation.Srm.Views.RemoveProtectionGroupTask])] + Param( + [Parameter (Mandatory=$true, ValueFromPipeline=$true)][VMware.VimAutomation.Srm.Views.SrmProtectionGroup] $ProtectionGroup, + [VMware.VimAutomation.Srm.Types.V1.SrmServer] $SrmServer + ) + + $api = Get-ServerApiEndpoint $SrmServer + [VMware.VimAutomation.Srm.Views.RemoveProtectionGroupTask] $task = $null + + $pginfo = $ProtectionGroup.GetInfo() + if ($pscmdlet.ShouldProcess($pginfo.Name, "Remove")) { + $task = $api.Protection.RemoveProtectionGroup($ProtectionGroup.MoRef) + } + + return $task +} diff --git a/Modules/SRM/Meadowcroft.Srm.Recovery.ps1 b/Modules/SRM/Meadowcroft.Srm.Recovery.ps1 new file mode 100644 index 0000000..1b2eaca --- /dev/null +++ b/Modules/SRM/Meadowcroft.Srm.Recovery.ps1 @@ -0,0 +1,556 @@ +# SRM Helper Methods - https://github.com/benmeadowcroft/SRM-Cmdlets + +<# +.SYNOPSIS +Get the subset of recovery plans matching the input criteria + +.PARAMETER Name +Return recovery plans matching the specified name + +.PARAMETER ProtectionGroup +Return recovery plans associated with particular protection +groups +#> +Function Get-RecoveryPlan { + [cmdletbinding()] + Param( + [Parameter(position=1)][string] $Name, + [Parameter (ValueFromPipeline=$true)][VMware.VimAutomation.Srm.Views.SrmProtectionGroup[]] $ProtectionGroup, + [VMware.VimAutomation.Srm.Types.V1.SrmServer] $SrmServer + ) + + begin { + $api = Get-ServerApiEndpoint -SrmServer $SrmServer + $rps = @() + } + process { + if ($ProtectionGroup) { + foreach ($pg in $ProtectionGroup) { + $rps += $pg.ListRecoveryPlans() + } + $rps = Select_UniqueByMoRef($rps) + } else { + $rps += $api.Recovery.ListPlans() + } + } + end { + $rps | ForEach-Object { + $rp = $_ + $rpi = $rp.GetInfo() + $selected = (-not $Name -or ($Name -eq $rpi.Name)) + if ($selected) { + Add-Member -InputObject $rp -MemberType NoteProperty -Name "Name" -Value $rpi.Name + $rp + } + } + } +} + +<# +.SYNOPSIS +Start a Recovery Plan action like test, recovery, cleanup, etc. + +.PARAMETER RecoveryPlan +The recovery plan to start + +.PARAMETER RecoveryMode +The recovery mode to invoke on the plan. May be one of "Test", "Cleanup", "Failover", "Migrate", "Reprotect" +#> +Function Start-RecoveryPlan { + [cmdletbinding(SupportsShouldProcess=$True, ConfirmImpact="High")] + Param( + [Parameter (Mandatory=$true, ValueFromPipeline=$true, Position=1)][VMware.VimAutomation.Srm.Views.SrmRecoveryPlan] $RecoveryPlan, + [VMware.VimAutomation.Srm.Views.SrmRecoveryPlanRecoveryMode] $RecoveryMode = [VMware.VimAutomation.Srm.Views.SrmRecoveryPlanRecoveryMode]::Test, + [bool] $SyncData = $True + ) + + # Validate with informative error messages + $rpinfo = $RecoveryPlan.GetInfo() + + # Create recovery options + $rpOpt = New-Object VMware.VimAutomation.Srm.Views.SrmRecoveryOptions + $rpOpt.SyncData = $SyncData + + # Prompt the user to confirm they want to execute the action + if ($pscmdlet.ShouldProcess($rpinfo.Name, $RecoveryMode)) { + if ($rpinfo.State -eq 'Protecting') { + throw "This recovery plan action needs to be initiated from the other SRM instance" + } + + $RecoveryPlan.Start($RecoveryMode, $rpOpt) + } +} + +<# +.SYNOPSIS +Stop a running Recovery Plan action. + +.PARAMETER RecoveryPlan +The recovery plan to stop +#> +Function Stop-RecoveryPlan { + [cmdletbinding(SupportsShouldProcess=$True,ConfirmImpact="High")] + Param( + [Parameter (Mandatory=$true, ValueFromPipeline=$true, Position=1)][VMware.VimAutomation.Srm.Views.SrmRecoveryPlan] $RecoveryPlan + ) + + # Validate with informative error messages + $rpinfo = $RecoveryPlan.GetInfo() + + # Prompt the user to confirm they want to cancel the running action + if ($pscmdlet.ShouldProcess($rpinfo.Name, 'Cancel')) { + + $RecoveryPlan.Cancel() + } +} + +<# +.SYNOPSIS +Retrieve the historical results of a recovery plan + +.PARAMETER RecoveryPlan +The recovery plan to retrieve the history for +#> +Function Get-RecoveryPlanResult { + [cmdletbinding()] + Param( + [Parameter (Mandatory=$true, ValueFromPipeline=$true, Position=1)][VMware.VimAutomation.Srm.Views.SrmRecoveryPlan] $RecoveryPlan, + [VMware.VimAutomation.Srm.Views.SrmRecoveryPlanRecoveryMode] $RecoveryMode, + [VMware.VimAutomation.Srm.Views.SrmRecoveryResultResultState] $ResultState, + [DateTime] $StartedAfter, + [DateTime] $startedBefore, + [VMware.VimAutomation.Srm.Types.V1.SrmServer] $SrmServer + ) + + $api = Get-ServerApiEndpoint -SrmServer $SrmServer + + # Get the history objects + $history = $api.Recovery.GetHistory($RecoveryPlan.MoRef) + $resultCount = $history.GetResultCount() + + if ($resultCount -gt 0) { + $results = $history.GetRecoveryResult($resultCount) + + $results | + Where-Object { -not $RecoveryMode -or $_.RunMode -eq $RecoveryMode } | + Where-Object { -not $ResultState -or $_.ResultState -eq $ResultState } | + Where-Object { $null -eq $StartedAfter -or $_.StartTime -gt $StartedAfter } | + Where-Object { $null -eq $StartedBefore -or $_.StartTime -lt $StartedBefore } + } +} + +<# +.SYNOPSIS +Exports a recovery plan result object to XML format + +.PARAMETER RecoveryPlanResult +The recovery plan result to export +#> +Function Export-RecoveryPlanResultAsXml { + [cmdletbinding()] + [OutputType([xml])] + Param( + [Parameter (Mandatory=$true, ValueFromPipeline=$true, Position=1)][VMware.VimAutomation.Srm.Views.SrmRecoveryResult] $RecoveryPlanResult, + [VMware.VimAutomation.Srm.Types.V1.SrmServer] $SrmServer + ) + + $api = Get-ServerApiEndpoint -SrmServer $SrmServer + + $RecoveryPlan = $RecoveryPlanResult.Plan + $history = $api.Recovery.GetHistory($RecoveryPlan.MoRef) + $lines = $history.GetResultLength($RecoveryPlanResult.Key) + [xml] $history.RetrieveStatus($RecoveryPlanResult.Key, 0, $lines) +} + +<# +.SYNOPSIS +Add a protection group to a recovery plan. This requires SRM 5.8 or later. + +.PARAMETER RecoveryPlan +The recovery plan the protection group will be associated with + +.PARAMETER ProtectionGroup +The protection group to associate with the recovery plan +#> +Function Add-ProtectionGroupToRecoveryPlan { + [cmdletbinding()] + Param( + [Parameter (Mandatory=$true, Position=1)][VMware.VimAutomation.Srm.Views.SrmRecoveryPlan] $RecoveryPlan, + [Parameter (Mandatory=$true, ValueFromPipeline=$true, Position=2)][VMware.VimAutomation.Srm.Views.SrmProtectionGroup] $ProtectionGroup + ) + + if ($RecoveryPlan -and $ProtectionGroup) { + foreach ($pg in $ProtectionGroup) { + try { + $RecoveryPlan.AddProtectionGroup($pg.MoRef) + } catch { + Write-Error $_ + } + } + } +} + +<# +.SYNOPSIS +Remove a protection group to a recovery plan. This requires SRM 6.5 or later. + +.PARAMETER RecoveryPlan +The recovery plan the protection group will be disassociated from + +.PARAMETER ProtectionGroup +The protection group to disassociate from the recovery plan +#> +Function Remove-ProtectionGroupFromRecoveryPlan { + [cmdletbinding()] + Param( + [Parameter (Mandatory=$true)][VMware.VimAutomation.Srm.Views.SrmRecoveryPlan] $RecoveryPlan, + [Parameter (Mandatory=$true)][VMware.VimAutomation.Srm.Views.SrmProtectionGroup] $ProtectionGroup + ) + + if ($RecoveryPlan -and $ProtectionGroup) { + foreach ($pg in $ProtectionGroup) { + try { + $RecoveryPlan.RemoveProtectionGroupFromRecoveryPlan($pg.MoRef) + } catch { + Write-Error $_ + } + } + } +} + +<# +.SYNOPSIS +Get the recovery settings of a protected VM. This requires SRM 5.8 or later. + +.PARAMETER RecoveryPlan +The recovery plan the settings will be retrieved from. + +.PARAMETER Vm +The virtual machine to retieve recovery settings for. + +#> +Function Get-RecoverySetting { + [cmdletbinding()] + Param( + [Parameter (Mandatory=$true)][VMware.VimAutomation.Srm.Views.SrmRecoveryPlan] $RecoveryPlan, + [Parameter (ValueFromPipeline=$true)][VMware.VimAutomation.ViCore.Types.V1.Inventory.VirtualMachine] $Vm, + [Parameter (ValueFromPipeline=$true)][VMware.Vim.VirtualMachine] $VmView, + [Parameter (ValueFromPipeline=$true)][VMware.VimAutomation.Srm.Views.SrmProtectionGroupProtectedVm] $ProtectedVm + ) + + $moRef = Get_MoRefFromVmObj -Vm $Vm -VmView $VmView -ProtectedVm $ProtectedVm + + if ($RecoveryPlan -and $moRef) { + $RecoveryPlan.GetRecoverySettings($moRef) + } +} + +<# +.SYNOPSIS +Get the recovery settings of a protected VM. This requires SRM 5.8 or later. + +.PARAMETER RecoveryPlan +The recovery plan the settings will be retrieved from. + +.PARAMETER Vm +The virtual machine to configure recovery settings on. + +.PARAMETER RecoverySettings +The recovery settings to configure. These should have been retrieved via a +call to Get-RecoverySettings + +#> +Function Set-RecoverySetting { + [cmdletbinding(SupportsShouldProcess=$true, ConfirmImpact="Medium")] + Param( + [Parameter (Mandatory=$true)][VMware.VimAutomation.Srm.Views.SrmRecoveryPlan] $RecoveryPlan, + [Parameter (ValueFromPipeline=$true)][VMware.VimAutomation.ViCore.Types.V1.Inventory.VirtualMachine] $Vm, + [Parameter (ValueFromPipeline=$true)][VMware.Vim.VirtualMachine] $VmView, + [Parameter (ValueFromPipeline=$true)][VMware.VimAutomation.Srm.Views.SrmProtectionGroupProtectedVm] $ProtectedVm, + [Parameter (Mandatory=$true, ValueFromPipeline=$true)][VMware.VimAutomation.Srm.Views.SrmRecoverySettings] $RecoverySettings + ) + + + $moRef = Get_MoRefFromVmObj -Vm $Vm -VmView $VmView -ProtectedVm $ProtectedVm + + if ($RecoveryPlan -and $moRef -and $RecoverySettings) { + if ($PSCmdlet.ShouldProcess("$moRef", "Set")) { + $RecoveryPlan.SetRecoverySettings($moRef, $RecoverySettings) + } + } +} + +<# +.SYNOPSIS +Create a new per-Vm command to add to the SRM Recovery Plan + +.PARAMETER Command +The command script to execute. + +.PARAMETER Description +The user friendly description of this script. + +.PARAMETER Timeout +The number of seconds this command has to execute before it will be timedout. + +.PARAMETER RunInRecoveredVm +For a post-power on command this flag determines whether it will run on the +recovered VM or on the SRM server. + +#> +Function New-Command { + [cmdletbinding(SupportsShouldProcess=$true, ConfirmImpact="None")] + Param( + [Parameter (Mandatory=$true)][string] $Command, + [Parameter (Mandatory=$true)][string] $Description, + [int] $Timeout = 300, + [switch] $RunInRecoveredVm = $false + ) + + if($PSCmdlet.ShouldProcess("Description", "New")) { + $srmWsdlCmd = New-Object VMware.VimAutomation.Srm.WsdlTypes.SrmCommand + $srmCmd = New-Object VMware.VimAutomation.Srm.Views.SrmCommand -ArgumentList $srmWsdlCmd + $srmCmd.Command = $Command + $srmCmd.Description = $Description + $srmCmd.RunInRecoveredVm = $RunInRecoveredVm + $srmCmd.Timeout = $Timeout + $srmCmd.Uuid = [guid]::NewGuid() + + return $srmCmd + } +} + +<# Internal function #> +Function Add_Command { + [cmdletbinding()] + Param( + [Parameter (Mandatory=$true, ValueFromPipeline=$true)][VMware.VimAutomation.Srm.Views.SrmRecoverySettings] $RecoverySettings, + [Parameter (Mandatory=$true)][VMware.VimAutomation.Srm.Views.SrmCommand] $SrmCommand, + [Parameter (Mandatory=$true)][bool] $PostRecovery + ) + + if ($PostRecovery) { + $commands = $RecoverySettings.PostPowerOnCallouts + } else { + $commands = $RecoverySettings.PrePowerOnCallouts + } + + if (-not $commands) { + $commands = New-Object System.Collections.Generic.List[VMware.VimAutomation.Srm.Views.SrmCallout] + } + $commands.Add($SrmCommand) + + if ($PostRecovery) { + $RecoverySettings.PostPowerOnCallouts = $commands + } else { + $RecoverySettings.PrePowerOnCallouts = $commands + } +} + +<# +.SYNOPSIS +Add an SRM command to the set of pre recovery callouts for a VM. + +.PARAMETER RecoverySettings +The recovery settings to update. These should have been retrieved via a +call to Get-RecoverySettings + +.PARAMETER SrmCommand +The command to add to the list. + +#> +Function Add-PreRecoveryCommand { + [cmdletbinding()] + [OutputType([VMware.VimAutomation.Srm.Views.SrmRecoverySettings])] + Param( + [Parameter (Mandatory=$true, ValueFromPipeline=$true)][VMware.VimAutomation.Srm.Views.SrmRecoverySettings] $RecoverySettings, + [Parameter (Mandatory=$true)][VMware.VimAutomation.Srm.Views.SrmCommand] $SrmCommand + ) + Add_Command -RecoverySettings $RecoverySettings -SrmCommand $SrmCommand -PostRecovery $false + return $RecoverySettings +} + +<# +.SYNOPSIS +Remove an SRM command from the set of pre recovery callouts for a VM. + +.PARAMETER RecoverySettings +The recovery settings to update. These should have been retrieved via a +call to Get-RecoverySettings + +.PARAMETER SrmCommand +The command to remove from the list. + +#> +Function Remove-PreRecoveryCommand { + [cmdletbinding(SupportsShouldProcess=$true, ConfirmImpact="Low")] + [OutputType([VMware.VimAutomation.Srm.Views.SrmRecoverySettings])] + Param( + [Parameter (Mandatory=$true, ValueFromPipeline=$true)][VMware.VimAutomation.Srm.Views.SrmRecoverySettings] $RecoverySettings, + [Parameter (Mandatory=$true)][VMware.VimAutomation.Srm.Views.SrmCommand] $SrmCommand + ) + + if ($pscmdlet.ShouldProcess($SrmCommand.Description, "Remove")) { + $RecoverySettings.PrePowerOnCallouts.Remove($SrmCommand) + } + + return $RecoverySettings +} + +<# +.SYNOPSIS +Add an SRM command to the set of post recovery callouts for a VM. + +.PARAMETER RecoverySettings +The recovery settings to update. These should have been retrieved via a +call to Get-RecoverySettings + +.PARAMETER SrmCommand +The command to add to the list. + +#> +Function Add-PostRecoveryCommand { + [cmdletbinding()] + [OutputType([VMware.VimAutomation.Srm.Views.SrmRecoverySettings])] + Param( + [Parameter (Mandatory=$true, ValueFromPipeline=$true)][VMware.VimAutomation.Srm.Views.SrmRecoverySettings] $RecoverySettings, + [Parameter (Mandatory=$true)][VMware.VimAutomation.Srm.Views.SrmCommand] $SrmCommand + ) + + Add_Command -RecoverySettings $RecoverySettings -SrmCommand $SrmCommand -PostRecovery $true + + return $RecoverySettings +} + + +<# +.SYNOPSIS +Remove an SRM command from the set of post recovery callouts for a VM. + +.PARAMETER RecoverySettings +The recovery settings to update. These should have been retrieved via a +call to Get-RecoverySettings + +.PARAMETER SrmCommand +The command to remove from the list. + +#> +Function Remove-PostRecoveryCommand { + [cmdletbinding(SupportsShouldProcess=$true, ConfirmImpact="Low")] + [OutputType([VMware.VimAutomation.Srm.Views.SrmRecoverySettings])] + Param( + [Parameter (Mandatory=$true, ValueFromPipeline=$true)][VMware.VimAutomation.Srm.Views.SrmRecoverySettings] $RecoverySettings, + [Parameter (Mandatory=$true)][VMware.VimAutomation.Srm.Views.SrmCommand] $SrmCommand + ) + + if ($pscmdlet.ShouldProcess($SrmCommand.Description, "Remove")) { + $RecoverySettings.PostPowerOnCallouts.Remove($SrmCommand) + } + + return $RecoverySettings +} + + +<# +.SYNOPSIS +Create a new recovery plan + +.PARAMETER Name +The name for this recovery plan + +.PARAMETER Description +A description of the recovery plan + +.PARAMETER Folder +The recovery plan folder in which to create this recovery plan. Will default to +the root recovery plan folder + +.PARAMETER ProtectionGroups +The protection groups to associate with this recovery plan + +.PARAMETER TestNetworkMappings +The test network mappings to configure as part of this recovery plan + +.PARAMETER SrmServer +The SRM Server to operate against +#> +Function New-RecoveryPlan { + [cmdletbinding(SupportsShouldProcess=$true, ConfirmImpact="Medium")] + Param( + [Parameter (Mandatory=$true)][string] $Name, + [string] $Description, + [VMware.VimAutomation.Srm.Views.SrmRecoveryPlanFolder] $Folder, + [VMware.VimAutomation.Srm.Views.SrmProtectionGroup[]] $ProtectionGroups, + [VMware.VimAutomation.Srm.Views.SrmRecoveryTestNetworkMapping[]] $TestNetworkMappings, + [VMware.VimAutomation.Srm.Types.V1.SrmServer] $SrmServer + ) + + $api = Get-ServerApiEndpoint -SrmServer $SrmServer + + if (-not $Folder) { + $Folder = Get-RecoveryPlanFolder -SrmServer $SrmServer + } + + $protectionGroupmRefs += @( $ProtectionGroups | ForEach-Object { $_.MoRef } | Select-Object -Unique) + + [VMware.VimAutomation.Srm.Views.CreateRecoveryPlanTask] $task = $null + + if ($PSCmdlet.ShouldProcess($Name, "New")) { + $task = $api.Recovery.CreateRecoveryPlan( + $Name, + $Folder.MoRef, + $protectionGroupmRefs, + $Description, + $TestNetworkMappings + ) + } + + while(-not $task.IsCreateRecoveryPlanComplete()) { Start-Sleep -Seconds 1 } + + $task.GetNewRecoveryPlan() +} + +<# +.SYNOPSIS +Remove a recovery plan permanently + +.PARAMETER RecoveryPlan +The recovery plan to remove + +.PARAMETER SrmServer +The SRM Server to operate against +#> +Function Remove-RecoveryPlan { + [cmdletbinding(SupportsShouldProcess=$True, ConfirmImpact="High")] + Param( + [Parameter (Mandatory=$true)][VMware.VimAutomation.Srm.Views.SrmRecoveryPlan] $RecoveryPlan, + [VMware.VimAutomation.Srm.Types.V1.SrmServer] $SrmServer + ) + + $api = Get-ServerApiEndpoint -SrmServer $SrmServer + + $rpinfo = $RecoveryPlan.GetInfo() + if ($pscmdlet.ShouldProcess($rpinfo.Name, "Remove")) { + $api.Recovery.DeleteRecoveryPlan($RecoveryPlan.MoRef) + } +} + +<# +.SYNOPSIS +Get a recovery plan folder + +.PARAMETER SrmServer +The SRM Server to query for the recovery plan folder +#> +Function Get-RecoveryPlanFolder { + [cmdletbinding()] + Param( + [VMware.VimAutomation.Srm.Types.V1.SrmServer] $SrmServer + ) + + $api = Get-ServerApiEndpoint -SrmServer $SrmServer + + $folder = $api.Recovery.GetRecoveryPlanRootFolder() + + return $folder +} diff --git a/Modules/SRM/Meadowcroft.Srm.Storage.ps1 b/Modules/SRM/Meadowcroft.Srm.Storage.ps1 new file mode 100644 index 0000000..8880f36 --- /dev/null +++ b/Modules/SRM/Meadowcroft.Srm.Storage.ps1 @@ -0,0 +1,24 @@ +# SRM Helper Methods - https://github.com/benmeadowcroft/SRM-Cmdlets + +<# +.SYNOPSIS +Trigger Discover Devices for Site Recovery Manager + +.OUTPUTS +Returns discover devices task +#> +Function Start-DiscoverDevice { + [cmdletbinding(SupportsShouldProcess=$True, ConfirmImpact="Medium")] + [OutputType([VMware.VimAutomation.Srm.Views.DiscoverDevicesTask])] + Param( + [VMware.VimAutomation.Srm.Types.V1.SrmServer] $SrmServer + ) + + $api = Get-ServerApiEndpoint -SrmServer $SrmServer + $name = $SrmServer.Name + [VMware.VimAutomation.Srm.Views.DiscoverDevicesTask] $task = $null + if ($pscmdlet.ShouldProcess($name, "Rescan Storage Devices")) { + $task = $api.Storage.DiscoverDevices() + } + return $task +} diff --git a/Modules/SRM/Meadowcroft.Srm.psd1 b/Modules/SRM/Meadowcroft.Srm.psd1 new file mode 100644 index 0000000..996a493 --- /dev/null +++ b/Modules/SRM/Meadowcroft.Srm.psd1 @@ -0,0 +1,92 @@ +# +# Module manifest for module 'Meadowcroft.Srm' +# + +@{ + +# Script module or binary module file associated with this manifest. +RootModule = 'Meadowcroft.Srm.psm1' + +# Version number of this module. +ModuleVersion = '0.2' + +# ID used to uniquely identify this module +GUID = 'f9247009-9168-4a21-831b-819f82884ffe' + +# Author of this module +Author = 'Ben Meadowcroft' + +# Company or vendor of this module +CompanyName = 'VMware, Inc' + +# Copyright statement for this module +Copyright = '(c) 2014 - 2017. All rights reserved.' + +# Description of the functionality provided by this module +# Description = '' + +# Minimum version of the Windows PowerShell engine required by this module +# PowerShellVersion = '' + +# Name of the Windows PowerShell host required by this module +# PowerShellHostName = '' + +# Minimum version of the Windows PowerShell host required by this module +# PowerShellHostVersion = '' + +# Minimum version of Microsoft .NET Framework required by this module +# DotNetFrameworkVersion = '' + +# Minimum version of the common language runtime (CLR) required by this module +# CLRVersion = '' + +# Processor architecture (None, X86, Amd64) required by this module +# ProcessorArchitecture = '' + +# Modules that must be imported into the global environment prior to importing this module +RequiredModules = @{ModuleName='VMware.VimAutomation.Srm'; ModuleVersion='6.5'} + +# Assemblies that must be loaded prior to importing this module +# RequiredAssemblies = @() + +# Script files (.ps1) that are run in the caller's environment prior to importing this module. +# ScriptsToProcess = @() + +# Type files (.ps1xml) to be loaded when importing this module +# TypesToProcess = @() + +# Format files (.ps1xml) to be loaded when importing this module +# FormatsToProcess = @() + +# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess +NestedModules = 'Meadowcroft.Srm.Recovery.ps1','Meadowcroft.Srm.Protection.ps1','Meadowcroft.Srm.Storage.ps1' +# NestedModules = @() + +# Functions to export from this module, note that internal functions use '_' not '-' as separator +FunctionsToExport = '*-*' + +# Cmdlets to export from this module +CmdletsToExport = '*' + +# Variables to export from this module +VariablesToExport = '*' + +# Aliases to export from this module +AliasesToExport = '*' + +# List of all modules packaged with this module +# ModuleList = @() + +# List of all files packaged with this module +# FileList = @() + +# Private data to pass to the module specified in RootModule/ModuleToProcess +# PrivateData = '' + +# HelpInfo URI of this module +# HelpInfoURI = '' + +# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. +DefaultCommandPrefix = 'Srm' + +} diff --git a/Modules/SRM/Meadowcroft.Srm.psm1 b/Modules/SRM/Meadowcroft.Srm.psm1 new file mode 100644 index 0000000..ed7c042 --- /dev/null +++ b/Modules/SRM/Meadowcroft.Srm.psm1 @@ -0,0 +1,148 @@ +# SRM Helper Methods - https://github.com/benmeadowcroft/SRM-Cmdlets + +<# +.SYNOPSIS +This is intended to be an "internal" function only. It filters a +pipelined input of objects and elimiates duplicates as identified +by the MoRef property on the object. + +.LINK +https://github.com/benmeadowcroft/SRM-Cmdlets/ +#> +Function Select_UniqueByMoRef { + + Param( + [Parameter (ValueFromPipeline=$true)] $in + ) + process { + $moref = New-Object System.Collections.ArrayList + $in | Sort-Object | Select-Object MoRef -Unique | ForEach-Object { $moref.Add($_.MoRef) } > $null + $in | ForEach-Object { + if ($_.MoRef -in $moref) { + $moref.Remove($_.MoRef) + $_ #output + } + } + } +} + +<# +.SYNOPSIS +This is intended to be an "internal" function only. It gets the +MoRef property of a VM from either a VM object, a VM view, or the +protected VM object. +#> +Function Get_MoRefFromVmObj { + Param( + [Parameter (ValueFromPipeline=$true)][VMware.VimAutomation.ViCore.Types.V1.Inventory.VirtualMachine] $Vm, + [Parameter (ValueFromPipeline=$true)][VMware.Vim.VirtualMachine] $VmView, + [Parameter (ValueFromPipeline=$true)][VMware.VimAutomation.Srm.Views.SrmProtectionGroupProtectedVm] $ProtectedVm + ) + + + $moRef = $null + if ($Vm.ExtensionData.MoRef) { # VM object + $moRef = $Vm.ExtensionData.MoRef + } elseif ($VmView.MoRef) { # VM view + $moRef = $VmView.MoRef + } elseif ($protectedVm) { + $moRef = $ProtectedVm.Vm.MoRef + } + + $moRef +} + +<# +.SYNOPSIS +Lookup the srm instance for a specific server. +#> +Function Get-Server { + [cmdletbinding()] + Param( + [string] $SrmServerAddress, + [VMware.VimAutomation.Srm.Types.V1.SrmServer] $SrmServer + ) + + $found = $null + + if ($SrmServer) { + $found = $SrmServer + } elseif ($SrmServerAddress) { + # search for server address in default servers + $global:DefaultSrmServers | ForEach-Object { + if ($_.Name -ieq $SrmServerAddress) { + $found = $_ + } + } + if (-not $found) { + throw "SRM server $SrmServerAddress not found. Connect-Server must be called first." + } + } + + if (-not $found) { + #default result + $found = $global:DefaultSrmServers[0] + } + + return $found; +} + +<# +.SYNOPSIS +Retrieve the SRM Server Version +#> +Function Get-ServerVersion { + [cmdletbinding()] + Param( + [VMware.VimAutomation.Srm.Types.V1.SrmServer] $SrmServer + ) + $srm = Get-Server $SrmServer + $srm.Version +} + +<# +.SYNOPSIS +Lookup the SRM API endpoint for a specific server. +#> +Function Get-ServerApiEndpoint { + [cmdletbinding()] + Param( + [string] $SrmServerAddress, + [VMware.VimAutomation.Srm.Types.V1.SrmServer] $SrmServer + ) + + [VMware.VimAutomation.Srm.Types.V1.SrmServer] $server = Get-Server -SrmServerAddress $SrmServerAddress -SrmServer $SrmServer + + return $server.ExtensionData +} + +<# +.SYNOPSIS +Get the placeholder VMs that are associated with SRM +#> +Function Get-PlaceholderVM { + [cmdletbinding()] + Param() + Get-VM @Args | Where-Object {$_.ExtensionData.Config.ManagedBy.extensionKey -like "com.vmware.vcDr*" -and $_.ExtensionData.Config.ManagedBy.Type -ieq 'placeholderVm'} +} + +<# +.SYNOPSIS +Get the test VMs that are associated with SRM +#> +Function Get-TestVM { + [cmdletbinding()] + Param() + Get-VM @Args | Where-Object {$_.ExtensionData.Config.ManagedBy.extensionKey -like "com.vmware.vcDr*" -and $_.ExtensionData.Config.ManagedBy.Type -ieq 'testVm'} +} + +<# +.SYNOPSIS +Get the VMs that are replicated using vSphere Replication. These may not be SRM +protected VMs. +#> +Function Get-ReplicatedVM { + [cmdletbinding()] + Param() + Get-VM @Args | Where-Object {($_.ExtensionData.Config.ExtraConfig | Where-Object { $_.Key -eq 'hbr_filter.destination' -and $_.Value } )} +} diff --git a/Modules/SRM/NOTICE.txt b/Modules/SRM/NOTICE.txt new file mode 100644 index 0000000..2eb2d48 --- /dev/null +++ b/Modules/SRM/NOTICE.txt @@ -0,0 +1,7 @@ + +Copyright (c) 2017 VMware, Inc. All Rights Reserved. + +This product is licensed to you under the Apache License version 2.0 (the "License"). You may not use this product except in compliance with the License. + +This product may include a number of subcomponents with separate copyright notices and license terms. Your use of these subcomponents is subject to the terms and conditions of the subcomponent's license, as noted in the LICENSE file. + diff --git a/Modules/SRM/README.md b/Modules/SRM/README.md new file mode 100644 index 0000000..00d775e --- /dev/null +++ b/Modules/SRM/README.md @@ -0,0 +1,81 @@ +# SRM PowerCLI Cmdlets + +Helper functions for working with VMware SRM 6.5 with PowerCLI 6.5.1 or later. PowerShell 5.0 and above is required. + +This module is provided for illustrative/educational purposes to explain how the PowerCLI access to the SRM public API can be used. + +## Getting Started + +### Getting the SRM cmdlets + +The latest version of the software can be cloned from the git repository: + + git clone https://github.com/benmeadowcroft/SRM-Cmdlets.git + +Or downloaded as a [zip file](https://github.com/benmeadowcroft/SRM-Cmdlets/archive/master.zip). + +Specific releases (compatible with earlier PowerCLI and SRM versions) can be downloaded via the [release page](https://github.com/benmeadowcroft/SRM-Cmdlets/releases). + +### Deploy SRM-Cmdlets module + +After cloning (or downloading and extracting) the PowerShell module, you can import the module into your current PowerShell session by by passing the path to `Meadowcroft.Srm.psd1` to the `Import-Module` cmdlet, e.g.: + + Import-Module -Name .\SRM-Cmdlets\Meadowcroft.Srm.psd1 + +You can also install the module into the PowerShell path so it can be loaded implicitly. See [Microsoft's Installing Modules instructions](http://msdn.microsoft.com/en-us/library/dd878350) for more details on how to do this. + +The module uses the default prefix of `Srm` for the custom functions it defines. This can be overridden when importing the module by setting the value of the `-Prefix` parameter when calling `Import-Module`. + +### Connecting to SRM + +After installing the module the next step is to connect to the SRM server. Details of how to do this are located in the [PowerCLI 6.5.1 User's Guide](http://pubs.vmware.com/vsphere-65/topic/com.vmware.powercli.ug.doc/GUID-A5F206CF-264D-4565-8CB9-4ED1C337053F.html) + + $credential = Get-Credential + Connect-VIServer -Server vc-a.example.com -Credential $credential + Connect-SrmServer -Credential $credential -RemoteCredential $credential + +At this point we've just been using the cmdlets provided by PowerCLI, the PowerCLI documentation also provides some examples of how to call the SRM API to perform various tasks. In the rest of this introduction we'll perform some of those tasks using the custom functions defined in this project. + +### Report the Protected Virtual Machines and Their Protection Groups + +Goal: Create a simple report listing the VMs protected by SRM and the protection group they belong to. + + Get-SrmProtectionGroup | %{ + $pg = $_ + Get-SrmProtectedVM -ProtectionGroup $pg } | %{ + $output = "" | select VmName, PgName + $output.VmName = $_.Vm.Name + $output.PgName = $pg.GetInfo().Name + $output + } | Format-Table @{Label="VM Name"; Expression={$_.VmName} }, + @{Label="Protection group name"; Expression={$_.PgName} + } + +### Report the Last Recovery Plan Test + +Goal: Create a simple report listing the state of the last test of a recovery plan + + Get-SrmRecoveryPlan | %{ $_ | + Get-SrmRecoveryPlanResult -RecoveryMode Test | select -First 1 + } | Select Name, StartTime, RunMode, ResultState | Format-Table + + +### Execute a Recovery Plan Test + +Goal: for a specific recovery plan, execute a test failover. Note the "local" SRM server we are connected to should be the recovery site in order for this to be successful. + + Get-SrmRecoveryPlan -Name "Name of Plan" | Start-SrmRecoveryPlan -RecoveryMode Test + +### Export the Detailed XML Report of the Last Recovery Plan Workflow + +Goal: get the XML report of the last recovery plan execution for a specific recovery plan. + + Get-SrmRecoveryPlan -Name "Name of Plan" | Get-SrmRecoveryPlanResult | + select -First 1 | Export-SrmRecoveryPlanResultAsXml + +### Protect a Replicated VM + +Goal: Take a VM replicated using vSphere Replication or Array Based Replication, add it to an appropriate protection group and configure it for protection + + $pg = Get-SrmProtectionGroup "Name of Protection Group" + Get-VM vm-01a | Protect-SrmVM -ProtectionGroup $pg From e0e4a568267859d0a845eb46ed413ea755746a00 Mon Sep 17 00:00:00 2001 From: alanrenouf Date: Wed, 31 Jan 2018 20:40:39 -0800 Subject: [PATCH 110/112] updated module with VMC Firewall functions --- Modules/VMware.VMC/VMware.VMC.psd1 | Bin 7976 -> 8176 bytes Modules/VMware.VMC/VMware.VMC.psm1 | 371 ++++++++++++++++++++++++++++- 2 files changed, 370 insertions(+), 1 deletion(-) diff --git a/Modules/VMware.VMC/VMware.VMC.psd1 b/Modules/VMware.VMC/VMware.VMC.psd1 index 74256ef01bb8a2e4a146aa91e45b35b01949b08f..d2f3b23eb599158b576193db188ce27bfb1746d0 100755 GIT binary patch delta 206 zcmZ2s_rZRH6ceN2W@#pA!O2QOBK2+znG8h?sSM=|i3~YF7{pKtBvTpG8FUyFfTSx! w1w#QtK2S{wgDyiDgD-V6ceMtW@#pAK}N34|Ajwt0{~8O2HOAt diff --git a/Modules/VMware.VMC/VMware.VMC.psm1 b/Modules/VMware.VMC/VMware.VMC.psm1 index 674668b..3af0ad0 100644 --- a/Modules/VMware.VMC/VMware.VMC.psm1 +++ b/Modules/VMware.VMC/VMware.VMC.psm1 @@ -320,4 +320,373 @@ Function Get-VMCSDDCVersion { } } } -Export-ModuleMember -Function 'Get-VMCCommand', 'Connect-VMCVIServer', 'Get-VMCOrg', 'Get-VMCSDDC', 'Get-VMCTask', 'Get-VMCSDDCDefaultCredential', 'Get-VMCSDDCPublicIP', 'Get-VMCVMHost', 'Get-VMCSDDCVersion' \ No newline at end of file + +Function Get-VMCFirewallRule { + <# + .NOTES + =========================================================================== + Created by: William Lam + Date: 11/19/2017 + Organization: VMware + Blog: https://www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + + .SYNOPSIS + Retruns VMC Firewall Rules for a given Gateway (MGW or CGW) + .DESCRIPTION + Retruns VMC Firewall Rules for a given Gateway (MGW or CGW) + .EXAMPLE + Get-VMCFirewallRule -OrgName -SDDCName -GatewayType + .EXAMPLE + Get-VMCFirewallRule -OrgName -SDDCName -GatewayType -ShowAll + #> + param( + [Parameter(Mandatory=$false)][String]$SDDCName, + [Parameter(Mandatory=$false)][String]$OrgName, + [Parameter(Mandatory=$false)][Switch]$ShowAll, + [Parameter(Mandatory=$true)][ValidateSet("MGW","CGW")][String]$GatewayType + ) + + if($GatewayType -eq "MGW") { + $EdgeId = "edge-1" + } else { + $EdgeId = "edge-2" + } + + $orgId = (Get-VMCOrg -Name $OrgName).Id + $sddcId = (Get-VMCSDDC -Name $SDDCName -Org $OrgName).Id + + $firewallConfigService = Get-VmcService com.vmware.vmc.orgs.sddcs.networks.edges.firewall.config + + $firewallRules = ($firewallConfigService.get($orgId, $sddcId, $EdgeId)).firewall_rules.firewall_rules + if(-not $ShowAll) { + $firewallRules = $firewallRules | where { $_.rule_type -ne "default_policy" -and $_.rule_type -ne "internal_high" -and $_.name -ne "vSphere Cluster HA" -and $_.name -ne "Outbound Access" } | Sort-Object -Property rule_tag + } else { + $firewallRules = $firewallRules | Sort-Object -Property rule_tag + } + + $results = @() + foreach ($firewallRule in $firewallRules) { + if($firewallRule.source.ip_address.Count -ne 0) { + $source = $firewallRule.source.ip_address + } else { $source = "ANY" } + + if($firewallRule.application.service.protocol -ne $null) { + $protocol = $firewallRule.application.service.protocol + } else { $protocol = "ANY" } + + if($firewallRule.application.service.port -ne $null) { + $port = $firewallRule.application.service.port + } else { $port = "ANY" } + + $tmp = [pscustomobject] @{ + ID = $firewallRule.rule_id; + Name = $firewallRule.name; + Type = $firewallRule.rule_type; + Action = $firewallRule.action; + Protocol = $protocol; + Port = $port; + SourceAddress = $source + DestinationAddress = $firewallRule.destination.ip_address; + } + $results+=$tmp + } + $results + } + + Function Export-VMCFirewallRule { + <# + .NOTES + =========================================================================== + Created by: William Lam + Date: 11/19/2017 + Organization: VMware + Blog: https://www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + + .SYNOPSIS + Exports all "customer" created VMC Firewall Rules to JSON file + .DESCRIPTION + Exports all "customer" created VMC Firewall Rules to JSON file + .EXAMPLE + Export-VMCFirewallRule -OrgName -SDDCName -GatewayType -Path "C:\Users\lamw\Desktop\VMCFirewallRules.json" + #> + param( + [Parameter(Mandatory=$false)][String]$SDDCName, + [Parameter(Mandatory=$false)][String]$OrgName, + [Parameter(Mandatory=$true)][ValidateSet("MGW","CGW")][String]$GatewayType, + [Parameter(Mandatory=$false)][String]$Path + ) + + if (-not $global:DefaultVMCServers) { Write-error "No VMC Connection found, please use the Connect-VMC to connect"; break } + + if($GatewayType -eq "MGW") { + $EdgeId = "edge-1" + } else { + $EdgeId = "edge-2" + } + + $orgId = (Get-VMCOrg -Name $OrgName).Id + $sddcId = (Get-VMCSDDC -Name $SDDCName -Org $OrgName).Id + + if(-not $orgId) { + Write-Host -ForegroundColor red "Unable to find Org $OrgName, please verify input" + break + } + if(-not $sddcId) { + Write-Host -ForegroundColor red "Unable to find SDDC $SDDCName, please verify input" + break + } + + $firewallConfigService = Get-VmcService com.vmware.vmc.orgs.sddcs.networks.edges.firewall.config + + $firewallRules = ($firewallConfigService.get($orgId, $sddcId, $EdgeId)).firewall_rules.firewall_rules + if(-not $ShowAll) { + $firewallRules = $firewallRules | where { $_.rule_type -ne "default_policy" -and $_.rule_type -ne "internal_high" -and $_.name -ne "vSphere Cluster HA" -and $_.name -ne "Outbound Access" } | Sort-Object -Property rule_tag + } else { + $firewallRules = $firewallRules | Sort-Object -Property rule_tag + } + + $results = @() + $count = 0 + foreach ($firewallRule in $firewallRules) { + if($firewallRule.source.ip_address.Count -ne 0) { + $source = $firewallRule.source.ip_address + } else { + $source = "ANY" + } + + $tmp = [pscustomobject] @{ + Name = $firewallRule.name; + Action = $firewallRule.action; + Protocol = $firewallRule.application.service.protocol; + Port = $firewallRule.application.service.port; + SourcePort = $firewallRule.application.service.source_port; + ICMPType = $firewallRule.application.service.icmp_type; + SourceAddress = $firewallRule.source.ip_address; + DestinationAddress = $firewallRule.destination.ip_address; + Enabled = $firewallRule.enabled; + Logging = $firewallRule.logging_enabled; + } + $count+=1 + $results+=$tmp + } + if($Path) { + Write-Host -ForegroundColor Green "Exporting $count VMC Firewall Rules to $Path ..." + $results | ConvertTo-Json | Out-File $Path + } else { + $results | ConvertTo-Json + } + } + + Function Import-VMCFirewallRule { + <# + .NOTES + =========================================================================== + Created by: William Lam + Date: 11/19/2017 + Organization: VMware + Blog: https://www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + + .SYNOPSIS + Imports VMC Firewall Rules from exported JSON configuration file + .DESCRIPTION + Imports VMC Firewall Rules from exported JSON configuration file + .EXAMPLE + Import-VMCFirewallRule -OrgName -SDDCName -GatewayType -Path "C:\Users\lamw\Desktop\VMCFirewallRules.json" + #> + param( + [Parameter(Mandatory=$false)][String]$SDDCName, + [Parameter(Mandatory=$false)][String]$OrgName, + [Parameter(Mandatory=$true)][ValidateSet("MGW","CGW")][String]$GatewayType, + [Parameter(Mandatory=$false)][String]$Path + ) + + if (-not $global:DefaultVMCServers) { Write-error "No VMC Connection found, please use the Connect-VMC to connect"; break } + + if($GatewayType -eq "MGW") { + $EdgeId = "edge-1" + } else { + $EdgeId = "edge-2" + } + + $orgId = (Get-VMCOrg -Name $OrgName).Id + $sddcId = (Get-VMCSDDC -Name $SDDCName -Org $OrgName).Id + + if(-not $orgId) { + Write-Host -ForegroundColor red "Unable to find Org $OrgName, please verify input" + break + } + if(-not $sddcId) { + Write-Host -ForegroundColor red "Unable to find SDDC $SDDCName, please verify input" + break + } + + $firewallService = Get-VmcService com.vmware.vmc.orgs.sddcs.networks.edges.firewall.config.rules + + $vmcFirewallRulesJSON = Get-Content -Raw $Path | ConvertFrom-Json + + # Create top level Firewall Rules Object + $firewallRules = $firewallService.Help.add.firewall_rules.Create() + # Create top top level Firewall Rule Spec which will be an array of individual Firewall rules as we process them in next section + $ruleSpec = $firewallService.Help.add.firewall_rules.firewall_rules.Create() + + foreach ($vmcFirewallRule in $vmcFirewallRulesJSON) { + # Create Individual Firewall Rule Element Spec + $ruleElementSpec = $firewallService.Help.add.firewall_rules.firewall_rules.Element.Create() + + # AppSpec + $appSpec = $firewallService.Help.add.firewall_rules.firewall_rules.Element.application.Create() + # ServiceSpec + $serviceSpec = $firewallService.Help.add.firewall_rules.firewall_rules.Element.application.service.Element.Create() + + $protocol = $null + if($vmcFirewallRule.Protocol -ne $null) { + $protocol = $vmcFirewallRule.Protocol + } + $serviceSpec.protocol = $protocol + + # Process ICMP Type from JSON + $icmpType = $null + if($vmcFirewallRule.ICMPType -ne $null) { + $icmpType = $vmcFirewallRule.ICMPType + } + $serviceSpec.icmp_type = $icmpType + + # Process Source Ports from JSON + $sourcePorts = @() + if($vmcFirewallRule.SourcePort -eq "any" -or $vmcFirewallRule.SourcePort -ne $null) { + foreach ($port in $vmcFirewallRule.SourcePort) { + $sourcePorts+=$port + } + } else { + $sourcePorts = @("any") + } + $serviceSpec.source_port = $sourcePorts + + # Process Ports from JSON + $ports = @() + if($vmcFirewallRule.Port -ne "null") { + foreach ($port in $vmcFirewallRule.Port) { + $ports+=$port + } + } + $serviceSpec.port = $ports + $addSpec = $appSpec.service.Add($serviceSpec) + + # Create Source Spec + $srcSpec = $firewallService.Help.add.firewall_rules.firewall_rules.Element.source.Create() + $srcSpec.exclude = $false + # Process Source Address from JSON + $sourceAddess = @() + if($vmcFirewallRule.SourceAddress -ne "null") { + foreach ($address in $vmcFirewallRule.SourceAddress) { + $sourceAddess+=$address + } + } + $srcSpec.ip_address = $sourceAddess; + + # Create Destination Spec + $destSpec = $firewallService.Help.add.firewall_rules.firewall_rules.Element.destination.Create() + $destSpec.exclude = $false + # Process Destination Address from JSON + $destinationAddess = @() + if($vmcFirewallRule.DestinationAddress -ne "null") { + foreach ($address in $vmcFirewallRule.DestinationAddress) { + $destinationAddess+=$address + } + } + $destSpec.ip_address = $destinationAddess + + # Add various specs + if($vmcFirewallRule.Protocol -ne $null -and $vmcFirewallRule.port -ne $null) { + $ruleElementSpec.application = $appSpec + } + + $ruleElementSpec.source = $srcSpec + $ruleElementSpec.destination = $destSpec + $ruleElementSpec.rule_type = "user" + + # Process Enabled from JSON + $fwEnabled = $false + if($vmcFirewallRule.Enabled -eq "true") { + $fwEnabled = $true + } + $ruleElementSpec.enabled = $fwEnabled + + # Process Logging from JSON + $loggingEnabled = $false + if($vmcFirewallRule.Logging -eq "true") { + $loggingEnabled = $true + } + $ruleElementSpec.logging_enabled = $loggingEnabled + + $ruleElementSpec.action = $vmcFirewallRule.Action + $ruleElementSpec.name = $vmcFirewallRule.Name + + # Add the individual FW rule spec into our overall firewall rules array + Write-host "Creating VMC Firewall Rule Spec:" $vmcFirewallRule.Name "..." + $ruleSpecAdd = $ruleSpec.Add($ruleElementSpec) + } + $firewallRules.firewall_rules = $ruleSpec + + Write-host "Adding VMC Firewall Rules ..." + $firewallRuleAdd = $firewallService.add($orgId,$sddcId,$EdgeId,$firewallRules) + } + + Function Remove-VMCFirewallRule { + <# + .NOTES + =========================================================================== + Created by: William Lam + Date: 11/19/2017 + Organization: VMware + Blog: https://www.virtuallyghetto.com + Twitter: @lamw + =========================================================================== + + .SYNOPSIS + Removes VMC Firewall Rule given Rule Id + .DESCRIPTION + Removes VMC Firewall Rule given Rule Id + .EXAMPLE + Import-VMCFirewallRule -OrgName -SDDCName -GatewayType -RuleId + #> + param( + [Parameter(Mandatory=$false)][String]$SDDCName, + [Parameter(Mandatory=$false)][String]$OrgName, + [Parameter(Mandatory=$true)][ValidateSet("MGW","CGW")][String]$GatewayType, + [Parameter(Mandatory=$false)][String]$RuleId + ) + + if (-not $global:DefaultVMCServers) { Write-error "No VMC Connection found, please use the Connect-VMC to connect"; break } + + if($GatewayType -eq "MGW") { + $EdgeId = "edge-1" + } else { + $EdgeId = "edge-2" + } + + $orgId = (Get-VMCOrg -Name $OrgName).Id + $sddcId = (Get-VMCSDDC -Name $SDDCName -Org $OrgName).Id + + if(-not $orgId) { + Write-Host -ForegroundColor red "Unable to find Org $OrgName, please verify input" + break + } + if(-not $sddcId) { + Write-Host -ForegroundColor red "Unable to find SDDC $SDDCName, please verify input" + break + } + + $firewallService = Get-VmcService com.vmware.vmc.orgs.sddcs.networks.edges.firewall.config.rules + Write-Host "Removing VMC Firewall Rule Id $RuleId ..." + $firewallService.delete($orgId,$sddcId,$EdgeId,$RuleId) + } + + +Export-ModuleMember -Function 'Get-VMCCommand', 'Connect-VMCVIServer', 'Get-VMCOrg', 'Get-VMCSDDC', 'Get-VMCTask', 'Get-VMCSDDCDefaultCredential', 'Get-VMCSDDCPublicIP', 'Get-VMCVMHost', 'Get-VMCSDDCVersion', 'Get-VMCFirewallRule', 'Export-VMCFirewallRule', 'Import-VMCFirewallRule', 'Remove-VMCFirewallRule' \ No newline at end of file From a9c83ab50a7288875120457d8990f50b9ca83cf2 Mon Sep 17 00:00:00 2001 From: alanrenouf Date: Wed, 31 Jan 2018 21:40:34 -0800 Subject: [PATCH 111/112] Added a VMC Firewall Rules example script --- .../Sample VMC firewall rules management.ps1 | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 Scripts/Sample VMC firewall rules management.ps1 diff --git a/Scripts/Sample VMC firewall rules management.ps1 b/Scripts/Sample VMC firewall rules management.ps1 new file mode 100644 index 0000000..b62360b --- /dev/null +++ b/Scripts/Sample VMC firewall rules management.ps1 @@ -0,0 +1,20 @@ +$MyRefreshToken = "XXXX-XXXX-XXXX-XXXX" +Connect-VMC -RefreshToken $MyRefreshToken + +#List the user firewall Rules for MGW +Get-VMCFirewallRule -SDDCName "vGhetto" -OrgName "BashFest - Red Team" -GatewayType MGW + +#List the firewall rules including system firewall rules for MGW +Get-VMCFirewallRule -SDDCName "vGhetto" -OrgName "BashFest - Red Team" -GatewayType MGW -ShowAll + +#Export Firewall Rules from original SDDC +Export-VMCFirewallRule -SDDCName "vGhetto" -OrgName "BashFest - Red Team" -GatewayType MGW -Path ~/Desktop/VMCFirewallRules.json + +#Import Firewall Rules to new SDDC +Import-VMCFirewallRule -SDDCName “Single-Host-SDDC†-OrgName "BashFest - Red Team" -GatewayType MGW -Path ~/Desktop/VMCFirewallRules.json + +#Remove the firewall Rules we just created for the SDDC +$Rules = Get-VMCFirewallRule -SDDCName "Single-Host-SDDC" -OrgName "BashFest - Red Team" -GatewayType MGW +Foreach ($rule in $rules){ + Remove-VMCFirewallRule -SDDCName “Single-Host-SDDC†-OrgName "BashFest - Red Team" -GatewayType MGW -RuleId $rule.id +} \ No newline at end of file From 8a4abe7dd4c71d8997591455d87f300ff04a736e Mon Sep 17 00:00:00 2001 From: alanrenouf Date: Wed, 31 Jan 2018 22:18:58 -0800 Subject: [PATCH 112/112] Added a VMC example script --- Scripts/VMC Example Script.ps1 | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100755 Scripts/VMC Example Script.ps1 diff --git a/Scripts/VMC Example Script.ps1 b/Scripts/VMC Example Script.ps1 new file mode 100755 index 0000000..d1f1fae --- /dev/null +++ b/Scripts/VMC Example Script.ps1 @@ -0,0 +1,34 @@ +#List the commands available for the VMC module +Get-VMCCommand + +#Connect to VMC +$MyRefreshToken = "XXXX-XXXX-XXXX-XXXX" +Connect-VMC -RefreshToken $MyRefreshToken + +#List the Orgs available to us +Get-VMCOrg + +#List the SDDCs +Get-VMCSDDC -Org BashFest* + +#List the Tasks for a particular Org +Get-VMCTask -Org BashFest* | Select-Object task_type, Sub_Status, start_time, End_time, user_name | Sort-Object Start_Time | Format-Table + +#Get the Public IPs for a SDDC +Get-VMCSDDCPublicIPPool -org bashfest* + +#Get all ESXi hosts for given SDDC +Get-VMCVMHost -org bashfest* -Sddc virtu-al + +#Get the credentials of a SDDC so we can login via vSphere cmdlets +Get-VMCSDDCDefaultCredential -org bashfest* -Sddc virtu-al + +#Connect to your VMC vCenter with default creds +Connect-VmcVIServer -org bashfest* -Sddc virtu-al + +#Run some vSphere cmdlets + +#List all VMs from On-Premises and VMC SDDC +Get-VM | Select vCenterServer, Name, PowerState, VMHost + +