I'm trying to write something that
1) verifies a VM is powered off (for more that 30 days)
2) queries an external DB to make sure it's safe to delete the VM
3) output the results to a different DB (don't have this DB yet so outputting to Excel for now)
I found Luc Deken's "Get-VMLog" code and, it has formed the basis for what i'm trying to do.
To keep the code understandable, I've broken it into various functions, i'm hoping this will also allow me add to it in time as needed.
I'm having trouble with scoped of Variables as a result and, don't understand scopes well enough to correct the issues i'm having.
any help would be appreciated. Here's what i've come up with so far
<#
.SYNOPSIS
Retrieve the virtual machine logs to identify how long they are Powered down
Query external DB to verify safe to delete VM
.DESCRIPTION
The function retrieves the logs from one or more virtual machines and stores them in a local folder
Date of last Log entry is captured
.NOTES
Credits: Luc Dekens
#>
Set-PowerCLIConfiguration -DefaultVIServerMode Single -InvalidCertificateAction Ignore -Scope Session -Confirm:$False
function Install-PowerCLI {
If (!(Get-Module -Name VMware.VimAutomation.Cis.Core)) {
find-Module -Name VMware.VimAutomation.Cis.Core | Install-Module -Scope CurrentUser -Confirm:$false
}
}
function Login-VCSA {
Disconnect-VIServer * -Confirm:$False -ErrorAction SilentlyContinue | Out-Null
$targetVC = Read-Host "Enter the FQDN of the vCenter you want to connect to"
Connect-VIServer $targetVC | Out-Null -ErrorAction SilentlyContinue
if($DefaultVIServer -eq $null) {
Write-Host -ForegroundColor Red "Error: You need to connect to your vSphere environment before running this. Please do so now and try again"
exit
}
}
# Create folder to store results
function Create-Directory {
$Global:directory = $null
$Global:directory = Read-Host "Enter local directory to store results"
if ( !(Test-Path -LiteralPath $directory)) {
try {
New-Item -Path $directory -ItemType Directory -ErrorAction Stop | Out-Null
}
catch {
Write-Error -Message "Unable to create directory '$directory'. Error was: $_" -ErrorAction Stop
}
Write-Host -ForegroundColor Green "Successfully created directory '$directory'."
} else {
Write-Host -ForegroundColor Yellow "Directory '$directory' already exists - not creating it again"
}
}
# Create text file that contains VM Names
function Create-File {
$Global:file = $null
$Global:file = $directory + "\" + "vmList.txt"
if ( !(Test-Path -LiteralPath $file -PathType Leaf)) {
Set-Content -Encoding UTF8 -LiteralPath $file -Value ""
Write-Host -ForegroundColor Green "Successfully created '$file'."
} else {
Write-Host -ForegroundColor Yellow "File '$file' already exists - not creating it again"
}
Write-Host "Enter VM Names into the text file and save it"
start-sleep -Seconds 3
ii $file
}
# install ImportExcel module if not already installed and prepare layout
function global:Prepare-Excel {
If (!(Get-module -ListAvailable "ImportExcel")) {
Find-Module -Name ImportExcel | Install-Module -Scope CurrentUser
}
$ContainsBlanks = New-ConditionalText -ConditionalType ContainsBlanks
$excelHash = @{
Path = $directory + "\DecomList-" + (Get-Date -Format yyyy-MMM-dd-HHmm) + ".xlsx"
Show = $true;
AutoSize = $true;
AutoFilter = $true;
ConditionalText = $ContainsBlanks
ShowPercent = $true;
HideSheet = "Sheet1";
}
}
function Get-CIStatus {
# Placeholder for when read-only access to CMDB is available
# $ciStatus = identify CI Status from Asset Management SQL DB
}
function Get-Details {
Read-Host "When you have saved the text file, press ENTER to continue..."
$global:decomReport = @()
$global:dir = $directory
$VMList = get-vm (Get-Content $file)
foreach($vm in $VMList){
$powerState = $vm.PowerState
$vmProperty = [ordered] @{
'vCenter' = $global:DefaultVIServer.Name
'VM Name' = $vm.Name
'PowerState' = $powerState
}
# If VM Powered off Get the vmware.log file for the VM
if ($powerState -eq "PoweredOff") {
$logPath = $vm.Extensiondata.Config.Files.LogDirectory
$dsName = $logPath.Split(']')[0].Trim('[')
$vmPath = $logPath.Split(']')[1].Trim(' ')
$ds = Get-Datastore -Name $dsName
$drvName = "LogCollect-" + (Get-Random)
New-PSDrive -Location $ds -Name $drvName -PSProvider VimDatastore -Root '\' | Out-Null
Copy-DatastoreItem -Item ($drvName + ":" + $vmPath + "vmware.log") -Destination ($dir + "\" + $vm.Name + "\") -Force:$true
$lastLine = Get-Content ($dir + "\" + $vm.Name + "\" + "vmware.log") -Tail 1
[datetime]$LastLogDate = $lastLine.Split("|")[0]
$vmProperty.Add("Date of last Log entry",$LastLogDate)
$vmProperty.Add("Can be deleted","")
$vmProperty.Add("Removed from Inventory",(Get-Date -Format yyyy-MMM-dd-HHmm))
}
else {
$vmProperty.Add("Date of last Log entry",$LastLogDate)
$vmProperty.Add("Can be deleted","NO")
$vmProperty.Add("Removed from Inventory","NO - Notify Decom Team - Not Powered Off")
}
Remove-PSDrive -Name $drvName -Confirm:$false
$decomReport += New-Object -TypeName psobject -Property $vmProperty
}
$decomReport |
Sort-Object -Property 'VM Name' |
Export-Excel @excelHash
#Export-Csv ($directory + "\TempCSV.csv") -NoTypeInformation
}
function Remove-Decom {
# Placeholder to delete Decommissioned VM's once pre-req's are met
<#
if ($ciStatus -eq "Decommissioned") {}
#>
<#
# AND if (Has the VM been powered down for more than 30 days) {
$target = (Get-Date).AddDays(-30)
if ($LastLogDate -le $target) {
Write-Host -ForegroundColor Red "'$vm.Name' has neen powered for more than 30 days"
}
else {
Write-Host "$vm not powered down long enough, staying"
}
#>
# Delete VM From Inventory
}
Install-PowerCLI
Login-VCSA
Create-Directory
Create-File
Prepare-Excel
Get-CIStatus
Get-Details
Looks like your New-PSDrive fails (sometimes), and then the subsequent Remove-PSDrive gives an error because the drive doesn't exist.
Try removing the pipe to Out-Null and perhaps add the Verbose switch on the New-PSdrive.
An alternative could be to use a try-catch
New-PSDrive ... -ErrorAction Stop
}
catch{
throw "Error in creating the PSDrive"
}
Blog: lucd.info Twitter: @LucD22 Co-author PowerCLI Reference
The ideal way to pass information between functions is to use parameters and function output.
Another option is to use variables with a Scope higher than Local.
Since this all happens in 1 script, you don't even need to Global scope.
The Script scope will suffice.
Note that a variable $Script:var is not the same a local variable $var in a function.
Also when referencing a variable, you have to specify the Scope (exception is the implicit Local Scope).
I think I made most required changes to use Script scope variables where needed.
Give it a try
.SYNOPSIS
Retrieve the virtual machine logs to identify how long they are Powered down
Query external DB to verify safe to delete VM
.DESCRIPTION
The function retrieves the logs from one or more virtual machines and stores them in a local folder
Date of last Log entry is captured
.NOTES
Credits: Luc Dekens
#>
Set-PowerCLIConfiguration -DefaultVIServerMode Single -InvalidCertificateAction Ignore -Scope Session -Confirm:$False
function Install-PowerCLI {
If (!(Get-Module -Name VMware.VimAutomation.Cis.Core)) {
find-Module -Name VMware.VimAutomation.Cis.Core | Install-Module -Scope CurrentUser -Confirm:$false
}
}
function Login-VCSA {
Disconnect-VIServer * -Confirm:$False -ErrorAction SilentlyContinue | Out-Null
$targetVC = Read-Host "Enter the FQDN of the vCenter you want to connect to"
Connect-VIServer $targetVC | Out-Null -ErrorAction SilentlyContinue
if($DefaultVIServer -eq $null) {
Write-Host -ForegroundColor Red "Error: You need to connect to your vSphere environment before running this. Please do so now and try again"
exit
}
}
# Create folder to store results
function Create-Directory {
$Script:directory = $null
$Script:directory = Read-Host "Enter local directory to store results"
if ( !(Test-Path -LiteralPath $Script:directory)) {
try {
New-Item -Path $Script:directory -ItemType Directory -ErrorAction Stop | Out-Null
}
catch {
Write-Error -Message "Unable to create directory '$Script:directory'. Error was: $_" -ErrorAction Stop
}
Write-Host -ForegroundColor Green "Successfully created directory '$Script:directory'."
} else {
Write-Host -ForegroundColor Yellow "Directory '$Script:directory' already exists - not creating it again"
}
}
# Create text file that contains VM Names
function Create-File {
$Script:file = $null
$Script::file = $Script:directory + "\" + "vmList.txt"
if ( !(Test-Path -LiteralPath $Script:file -PathType Leaf)) {
Set-Content -Encoding UTF8 -LiteralPath $Script:file -Value ""
Write-Host -ForegroundColor Green "Successfully created '$Script:file'."
} else {
Write-Host -ForegroundColor Yellow "File '$Script:file' already exists - not creating it again"
}
Write-Host "Enter VM Names into the text file and save it"
start-sleep -Seconds 3
Invoke-Item $Script:file
}
# install ImportExcel module if not already installed and prepare layout
function global:Prepare-Excel {
If (!(Get-module -ListAvailable "ImportExcel")) {
Find-Module -Name ImportExcel | Install-Module -Scope CurrentUser
}
$ContainsBlanks = New-ConditionalText -ConditionalType ContainsBlanks
$excelHash = @{
Path = $Script:directory + "\DecomList-" + (Get-Date -Format yyyy-MMM-dd-HHmm) + ".xlsx"
Show = $true;
AutoSize = $true;
AutoFilter = $true;
ConditionalText = $ContainsBlanks
ShowPercent = $true;
HideSheet = "Sheet1";
}
}
function Get-CIStatus {
# Placeholder for when read-only access to CMDB is available
# $ciStatus = identify CI Status from Asset Management SQL DB
}
function Get-Details {
Read-Host "When you have saved the text file, press ENTER to continue..."
$Script:decomReport = @()
$Script:dir = $Script:directory
$VMList = Get-VM (Get-Content $Script:file)
foreach($vm in $VMList){
$powerState = $vm.PowerState
$vmProperty = [ordered] @{
'vCenter' = $global:DefaultVIServer.Name
'VM Name' = $vm.Name
'PowerState' = $powerState
}
# If VM Powered off Get the vmware.log file for the VM
if ($powerState -eq "PoweredOff") {
$logPath = $vm.Extensiondata.Config.Files.LogDirectory
$dsName = $logPath.Split(']')[0].Trim('[')
$vmPath = $logPath.Split(']')[1].Trim(' ')
$ds = Get-Datastore -Name $dsName
$drvName = "LogCollect-" + (Get-Random)
New-PSDrive -Location $ds -Name $drvName -PSProvider VimDatastore -Root '\' | Out-Null
Copy-DatastoreItem -Item ($drvName + ":" + $vmPath + "vmware.log") -Destination ($dir + "\" + $vm.Name + "\") -Force:$true
$lastLine = Get-Content ($dir + "\" + $vm.Name + "\" + "vmware.log") -Tail 1
[datetime]$LastLogDate = $lastLine.Split("|")[0]
$vmProperty.Add("Date of last Log entry",$LastLogDate)
$vmProperty.Add("Can be deleted","")
$vmProperty.Add("Removed from Inventory",(Get-Date -Format yyyy-MMM-dd-HHmm))
}
else {
$vmProperty.Add("Date of last Log entry",$LastLogDate)
$vmProperty.Add("Can be deleted","NO")
$vmProperty.Add("Removed from Inventory","NO - Notify Decom Team - Not Powered Off")
}
Remove-PSDrive -Name $drvName -Confirm:$false
$decomReport += New-Object -TypeName psobject -Property $vmProperty
}
$decomReport |
Sort-Object -Property 'VM Name' |
Export-Excel @excelHash
#Export-Csv ($directory + "\TempCSV.csv") -NoTypeInformation
}
function Remove-Decom {
# Placeholder to delete Decommissioned VM's once pre-req's are met
<#
if ($ciStatus -eq "Decommissioned") {}
#>
<#
# AND if (Has the VM been powered down for more than 30 days) {
$target = (Get-Date).AddDays(-30)
if ($LastLogDate -le $target) {
Write-Host -ForegroundColor Red "'$vm.Name' has neen powered for more than 30 days"
}
else {
Write-Host "$vm not powered down long enough, staying"
}
#>
# Delete VM From Inventory
}
Install-PowerCLI
Login-VCSA
Create-Directory
Create-File
Prepare-Excel
Get-CIStatus
Get-Details
Blog: lucd.info Twitter: @LucD22 Co-author PowerCLI Reference
Thank you Luc,
i'll give this a go when back in the office tomorrow.
Regards,
Jason
Thanks Luc,
That was a great help. I added in a few more VM Properties and a progress bar.
I needed to change some more variables but, it's working as expected now (but for one error that doesn't seem to impact the results)
sometimes, i get an error about removing a PSDrive, but the script still works.
the error is
Remove-PSDrive : Cannot find drive. A drive with the name 'LogCollect-103617767' does not exist.
At C:\Temp\Remove-VMs.ps1:140 char:9
+ Remove-PSDrive -Name $drvName -Confirm:$false | Out-Null
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (LogCollect-103617767:String) [Remove-PSDrive], DriveNotFoundException
+ FullyQualifiedErrorId : DriveNotFound,Microsoft.PowerShell.Commands.RemovePSDriveCommand
The updated code is below
<#
.SYNOPSIS
Retrieve the virtual machine logs to identify how long they are Powered down
Query external DB to verify safe to delete VM
.DESCRIPTION
The function retrieves the logs from one or more virtual machines and stores them in a local folder
Date of last Log entry is captured
.NOTES
Credits: Luc Dekens
#>
Set-PowerCLIConfiguration -DefaultVIServerMode Single -InvalidCertificateAction Ignore -Scope Session -Confirm:$False | Out-Null
function Install-PowerCLI {
If (!(Get-Module -Name VMware.VimAutomation.Cis.Core)) {
find-Module -Name VMware.VimAutomation.Cis.Core | Install-Module -Scope CurrentUser -Confirm:$false
}
}
function Login-VCSA {
Disconnect-VIServer * -Confirm:$False -WarningAction SilentlyContinue
$targetVC = Read-Host "Enter the FQDN of the vCenter you want to connect to"
Connect-VIServer $targetVC | Out-Null -ErrorAction SilentlyContinue
if($DefaultVIServer -eq $null) {
Write-Host -ForegroundColor Red "Error: You need to connect to your vSphere environment before running this. Please do so now and try again"
exit
}
}
# Create folder to store results
function Create-Directory {
$Script:directory = $null
$Script:directory = Read-Host "Enter local directory to store results"
if ( !(Test-Path -LiteralPath $Script:directory)) {
try {
New-Item -Path $Script:directory -ItemType Directory -ErrorAction Stop | Out-Null
}
catch {
Write-Error -Message "Unable to create directory '$Script:directory'. Error was: $_" -ErrorAction Stop
}
Write-Host -ForegroundColor Green "Successfully created directory '$Script:directory'."
} else {
Write-Host -ForegroundColor Yellow "Directory '$Script:directory' already exists - not creating it again"
}
}
# Create text file that contains VM Names
function Create-File {
$Script:file = $null
$Script:file = $Script:directory + "\" + "vmList.txt"
if ( !(Test-Path -LiteralPath $Script:file -PathType Leaf)) {
Set-Content -Encoding UTF8 -LiteralPath $Script:file -Value ""
Write-Host -ForegroundColor Green "Successfully created '$Script:file'."
} else {
Write-Host -ForegroundColor Yellow "File '$Script:file' already exists - not creating it again"
}
Write-Host "Enter VM Names into the text file and save it. Opening File for you now."
start-sleep -Seconds 3
Invoke-Item $Script:file
}
# install ImportExcel module if not already installed and prepare layout
#function global:Prepare-Excel {
function script:Prepare-Excel {
If (!(Get-module -ListAvailable "ImportExcel")) {
Find-Module -Name ImportExcel | Install-Module -Scope CurrentUser
}
$cannotBeDeleted = New-ConditionalText -Text PoweredOn -BackgroundColor Yellow -ConditionalTextColor Red
$Script:excelHash = @{
Path = $Script:directory + "\DecomList-" + (Get-Date -Format yyyy-MMM-dd-HHmm) + ".xlsx"
Show = $true;
AutoSize = $true;
AutoFilter = $true;
ConditionalText = $cannotBeDeleted
ShowPercent = $true;
HideSheet = "Sheet1";
}
}
function Get-CIStatus {
# Placeholder for when read-only access to CMDB is available
# $ciStatus = identify CI Status from Asset Management SQL DB
}
function Get-Details {
Start-Sleep -Seconds 5
Read-Host "When you have saved the text file, press ENTER to continue..."
$Script:decomReport = @()
$dir = $Script:directory
#$Script:dir = $Script:directory
$VMList = Get-VM (Get-Content $Script:file) | Sort-Object
$count = $VMList.count
$i = 1
foreach($vm in $VMList){
Write-Progress -Activity "Collecting details on provided VMs" -Status "Working on $vm" -PercentComplete (($i*100)/$count)
$ip = $vm.guest.IPAddress[0]
$powerState = $vm.PowerState
$memory = $vm.memoryGB
$hddSize = [math]::Round(((Get-HardDisk -VM $vm).CapacityGB | Measure-Object -Sum).Sum)
$vmProperty = [ordered] @{
'vCenter' = $global:DefaultVIServer.Name
'VM Name' = $vm.Name
'IP Address' = $ip
'PowerState' = $powerState
'Memory (GB)' = $memory
'Disk Capacity (GB)' = $hddSize
}
# If VM Powered off Get the vmware.log file for the VM
if ($powerState -eq "PoweredOff") {
$logPath = $vm.Extensiondata.Config.Files.LogDirectory
$dsName = $logPath.Split(']')[0].Trim('[')
$vmPath = $logPath.Split(']')[1].Trim(' ')
$ds = Get-Datastore -Name $dsName
$drvName = "LogCollect-" + (Get-Random)
New-PSDrive -Location $ds -Name $drvName -PSProvider VimDatastore -Root '\' | Out-Null
Copy-DatastoreItem -Item ($drvName + ":" + $vmPath + "vmware.log") -Destination ($dir + "\" + $vm.Name + "\") -Force:$true
#Copy-DatastoreItem -Item ($drvName + ":" + $vmPath + "vmware.log") -Destination ($Script:dir + "\" + $vm.Name + "\") -Force:$true
$lastLine = Get-Content ($dir + "\" + $vm.Name + "\" + "vmware.log") -Tail 1
#$lastLine = Get-Content ($Script:dir + "\" + $vm.Name + "\" + "vmware.log") -Tail 1
[datetime]$LastLogDate = $lastLine.Split("|")[0]
$vmProperty.Add("Date of last Log entry",$LastLogDate)
$vmProperty.Add("Can be deleted","If all criteria met")
$vmProperty.Add("Removed from Inventory",(Get-Date -Format yyyy-MMM-dd-HHmm))
}
else {
$vmProperty.Add("Date of last Log entry",$LastLogDate)
$vmProperty.Add("Can be deleted","NO")
$vmProperty.Add("Removed from Inventory","NO - Notify Decom Team - Not Powered Off")
}
Remove-PSDrive -Name $drvName -Confirm:$false | Out-Null
#$decomReport += New-Object -TypeName psobject -Property $vmProperty
$Script:decomReport += New-Object -TypeName psobject -Property $vmProperty
$i++
}
#$decomReport |
$Script:decomReport |
Sort-Object -Property 'VM Name' |
Export-Excel @Script:excelHash
#Export-Csv ($Script:directory + "\TempCSV.csv") -NoTypeInformation
}
function Remove-Decom {
# Placeholder to delete Decommissioned VM's once pre-req's are met
<#
if ($ciStatus -eq "Decommissioned") {}
#>
<#
# AND if (Has the VM been powered down for more than 30 days) {
$target = (Get-Date).AddDays(-30)
if ($LastLogDate -le $target) {
Write-Host -ForegroundColor Red "'$vm.Name' has neen powered for more than 30 days"
}
else {
Write-Host "$vm not powered down long enough, staying"
}
#>
# Delete VM From Inventory
}
Install-PowerCLI
Login-VCSA
Create-Directory
Create-File
Script:Prepare-Excel
Get-CIStatus
Get-Details
Looks like your New-PSDrive fails (sometimes), and then the subsequent Remove-PSDrive gives an error because the drive doesn't exist.
Try removing the pipe to Out-Null and perhaps add the Verbose switch on the New-PSdrive.
An alternative could be to use a try-catch
New-PSDrive ... -ErrorAction Stop
}
catch{
throw "Error in creating the PSDrive"
}
Blog: lucd.info Twitter: @LucD22 Co-author PowerCLI Reference