From 42df785b0e35076d2a69f054b831cc6ec7ef1076 Mon Sep 17 00:00:00 2001 From: Kevin Flowers <110041895+flowcompro@users.noreply.github.com> Date: Sun, 23 Jun 2024 01:31:37 -0400 Subject: [PATCH 1/9] Create Sho shunned devices --- Sho shunned devices | 49 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 Sho shunned devices diff --git a/Sho shunned devices b/Sho shunned devices new file mode 100644 index 0000000..a5c3898 --- /dev/null +++ b/Sho shunned devices @@ -0,0 +1,49 @@ +import paramiko +import getpass + +def get_asa_shunned_devices(hostname, username, password): + try: + # Create an SSH client + client = paramiko.SSHClient() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + # Connect to the ASA + print(f"Connecting to {hostname}...") + client.connect(hostname, username=username, password=password) + print("Connected.") + + # Open a shell session + ssh_shell = client.invoke_shell() + + # Send the 'enable' command and provide the enable password if required + ssh_shell.send("enable\n") + ssh_shell.send(password + "\n") + + # Send the 'show shun' command + ssh_shell.send("show shun\n") + + # Allow some time for the command to execute and capture the output + ssh_shell.settimeout(2) + output = "" + while True: + try: + data = ssh_shell.recv(1024).decode('utf-8') + output += data + except paramiko.ssh_exception.SSHException: + break + + # Print the output + print(output) + + except Exception as e: + print(f"An error occurred: {e}") + finally: + client.close() + +if __name__ == "__main__": + # Prompt for credentials + hostname = input("Enter the ASA hostname or IP address: ") + username = input("Enter your username: ") + password = getpass.getpass("Enter your password: ") + + get_asa_shunned_devices(hostname, username, password) From 460eb5222543e82c77636fafb17541da3bf3e3f2 Mon Sep 17 00:00:00 2001 From: Kevin Flowers <110041895+flowcompro@users.noreply.github.com> Date: Sun, 23 Jun 2024 12:17:20 -0400 Subject: [PATCH 2/9] Create Ad Menu report script This menu presents the following choices 1: Count Users in AD 2: Count Computer Objects in AD 3: Count Groups in AD 4: Accounts Created in Last X Days 5: Computers Created in Last X Days 6: Groups Created in Last X Days 7: Inactive Accounts in Last 30 Days 8: Inactive Accounts in Last X Days 9: Computers Not Logged In in Last X Days 10: Complete Report of Users, Computers, and Groups 11: Deleted Objects in Last 5 Days 0: Exit Enter your choice: --- Ad Menu report script | 1 + 1 file changed, 1 insertion(+) create mode 100644 Ad Menu report script diff --git a/Ad Menu report script b/Ad Menu report script new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/Ad Menu report script @@ -0,0 +1 @@ + From 4f9dec8393793c79c7afeec0508af441924f3f3a Mon Sep 17 00:00:00 2001 From: Kevin Flowers <110041895+flowcompro@users.noreply.github.com> Date: Sun, 23 Jun 2024 12:17:57 -0400 Subject: [PATCH 3/9] Update Ad Menu report script --- Ad Menu report script | 133 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) diff --git a/Ad Menu report script b/Ad Menu report script index 8b13789..3e5ec30 100644 --- a/Ad Menu report script +++ b/Ad Menu report script @@ -1 +1,134 @@ +# Import the Active Directory module +Import-Module ActiveDirectory + +function Show-Menu { + Clear-Host + Write-Host "1: Count Users in AD" + Write-Host "2: Count Computer Objects in AD" + Write-Host "3: Count Groups in AD" + Write-Host "4: Accounts Created in Last X Days" + Write-Host "5: Computers Created in Last X Days" + Write-Host "6: Groups Created in Last X Days" + Write-Host "7: Inactive Accounts in Last 30 Days" + Write-Host "8: Inactive Accounts in Last X Days" + Write-Host "9: Computers Not Logged In in Last X Days" + Write-Host "10: Complete Report of Users, Computers, and Groups" + Write-Host "11: Deleted Objects in Last 5 Days" + Write-Host "0: Exit" +} + +function Get-UserCount { + (Get-ADUser -Filter *).Count +} + +function Get-ComputerCount { + (Get-ADComputer -Filter *).Count +} + +function Get-GroupCount { + (Get-ADGroup -Filter *).Count +} + +function Get-AccountsCreatedInLastXDays { + param ( + [int]$days + ) + $date = (Get-Date).AddDays(-$days) + Get-ADUser -Filter {whenCreated -ge $date} | Select-Object Name, whenCreated +} + +function Get-ComputersCreatedInLastXDays { + param ( + [int]$days + ) + $date = (Get-Date).AddDays(-$days) + Get-ADComputer -Filter {whenCreated -ge $date} | Select-Object Name, whenCreated +} + +function Get-GroupsCreatedInLastXDays { + param ( + [int]$days + ) + $date = (Get-Date).AddDays(-$days) + Get-ADGroup -Filter {whenCreated -ge $date} | Select-Object Name, whenCreated +} + +function Get-InactiveAccountsLast30Days { + $date = (Get-Date).AddDays(-30) + Get-ADUser -Filter {lastLogonTimestamp -le $date} | Select-Object Name, lastLogonTimestamp +} + +function Get-InactiveAccountsInLastXDays { + param ( + [int]$days + ) + $date = (Get-Date).AddDays(-$days) + Get-ADUser -Filter {lastLogonTimestamp -le $date} | Select-Object Name, lastLogonTimestamp +} + +function Get-ComputersNotLoggedInInLastXDays { + param ( + [int]$days + ) + $date = (Get-Date).AddDays(-$days) + Get-ADComputer -Filter {lastLogonTimestamp -le $date} | Select-Object Name, lastLogonTimestamp +} + +function Get-CompleteReport { + $userCount = Get-UserCount + $computerCount = Get-ComputerCount + $groupCount = Get-GroupCount + Write-Host "Users: $userCount" + Write-Host "Computers: $computerCount" + Write-Host "Groups: $groupCount" +} + +function Get-DeletedObjectsLast5Days { + $date = (Get-Date).AddDays(-5) + Get-ADObject -Filter {isDeleted -eq $true -and whenChanged -ge $date} -IncludeDeletedObjects | Select-Object Name, whenChanged +} + +do { + Show-Menu + $choice = Read-Host "Enter your choice" + switch ($choice) { + 1 { Write-Host "User count in AD: $(Get-UserCount)" } + 2 { Write-Host "Computer count in AD: $(Get-ComputerCount)" } + 3 { Write-Host "Group count in AD: $(Get-GroupCount)" } + 4 { + $days = Read-Host "Enter the number of days" + Get-AccountsCreatedInLastXDays -days $days | Format-Table -AutoSize + } + 5 { + $days = Read-Host "Enter the number of days" + Get-ComputersCreatedInLastXDays -days $days | Format-Table -AutoSize + } + 6 { + $days = Read-Host "Enter the number of days" + Get-GroupsCreatedInLastXDays -days $days | Format-Table -AutoSize + } + 7 { + Write-Host "Inactive accounts in last 30 days:" + Get-InactiveAccountsLast30Days | Format-Table -AutoSize + } + 8 { + $days = Read-Host "Enter the number of days" + Write-Host "Inactive accounts in last $days days:" + Get-InactiveAccountsInLastXDays -days $days | Format-Table -AutoSize + } + 9 { + $days = Read-Host "Enter the number of days" + Write-Host "Computers not logged in last $days days:" + Get-ComputersNotLoggedInInLastXDays -days $days | Format-Table -AutoSize + } + 10 { Get-CompleteReport } + 11 { + Write-Host "Deleted objects in the last 5 days:" + Get-DeletedObjectsLast5Days | Format-Table -AutoSize + } + 0 { Write-Host "Exiting..."; break } + default { Write-Host "Invalid choice, please select a valid option." } + } + Pause +} while ($choice -ne 0) From c09e5e81bc51a70a6d1483a3cbf51098325e4fcb Mon Sep 17 00:00:00 2001 From: Kevin Flowers <110041895+flowcompro@users.noreply.github.com> Date: Mon, 24 Jun 2024 13:40:55 -0400 Subject: [PATCH 4/9] Create server status script This script will read a list of servers from a file then check for non running services, cpu usage memory and harddrive space send results to a log file and keep the last 12 logs and run every two ours and show information on the screen --- server status script | 156 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 server status script diff --git a/server status script b/server status script new file mode 100644 index 0000000..7d30321 --- /dev/null +++ b/server status script @@ -0,0 +1,156 @@ +# Define the path to the file containing the list of servers +$serverListPath = "C:\Users\god\Downloads\Scripts\server status\serverlist.txt" +# Define the path to the directory where logs will be stored +$logDirectory = "C:\Users\god\Downloads\Scripts\server status\logs" + +# Create the log directory if it doesn't exist +if (-not (Test-Path -Path $logDirectory)) { + New-Item -ItemType Directory -Path $logDirectory +} + +# Function to clean up old log files +function Cleanup-OldLogs { + $logFiles = Get-ChildItem -Path $logDirectory -Filter "serverstats_*.txt" | Sort-Object LastWriteTime -Descending + if ($logFiles.Count -gt 12) { + $filesToDelete = $logFiles | Select-Object -Skip 12 + foreach ($file in $filesToDelete) { + Remove-Item -Path $file.FullName + } + } +} + +# Function to get the current timestamp +function Get-Timestamp { + return (Get-Date).ToString("yyyyMMdd_HHmmss") +} + +# Define function to check services +function Check-Services { + param ( + [string]$server + ) + $serviceStatus = Get-WmiObject -ComputerName $server -Class Win32_Service | Where-Object { $_.StartMode -eq "Auto" -and $_.DelayedAutoStart -ne $true -and $_.State -ne "Running" } + $serviceStatus +} + +# Define function to check CPU usage +function Get-CPUUsage { + param ( + [string]$server + ) + $cpuLoad = Get-WmiObject -ComputerName $server -Class Win32_Processor | Measure-Object -Property LoadPercentage -Average | Select-Object -ExpandProperty Average + $cpuLoad +} + +# Define function to check memory usage +function Get-MemoryUsage { + param ( + [string]$server + ) + $memoryStatus = Get-WmiObject -ComputerName $server -Class Win32_OperatingSystem + $totalMemory = $memoryStatus.TotalVisibleMemorySize + $freeMemory = $memoryStatus.FreePhysicalMemory + $usedMemory = $totalMemory - $freeMemory + $memoryUsagePercent = [math]::round(($usedMemory / $totalMemory) * 100, 2) + $memoryUsagePercent +} + +# Define function to check drive sizes +function Get-DriveSizes { + param ( + [string]$server + ) + $driveSizes = Get-WmiObject -ComputerName $server -Class Win32_LogicalDisk -Filter "DriveType=3 AND DeviceID!='D:'" | Select-Object DeviceID, Size, FreeSpace + $driveSizes +} + +# Function to display output in red +function Write-OutputRed { + param ( + [string]$message + ) + Write-Host $message -ForegroundColor Red + return $message +} + +# Function to display output for a server +function Display-ServerStatus { + param ( + [string]$server + ) + $output = "Checking server: $server" + "`n" + Write-Host "Checking server: $server" -ForegroundColor Yellow + + # Check services + $servicesOutput = @() + $servicesNotRunning = Check-Services -server $server + if ($servicesNotRunning) { + foreach ($service in $servicesNotRunning) { + $message = "Service '$($service.Name)' is set to Automatic but not running" + $servicesOutput += $message + Write-OutputRed $message + } + } else { + $message = "All automatic services are running" + $servicesOutput += $message + Write-Host $message -ForegroundColor Green + } + $output += $servicesOutput -join "`n" + "`n" + + # Check CPU usage + $cpuUsage = Get-CPUUsage -server $server + if ($cpuUsage -gt 90) { + $message = Write-OutputRed "CPU Usage: ${cpuUsage}%" + } else { + $message = "CPU Usage: ${cpuUsage}%" + Write-Host $message + } + $output += $message + "`n" + + # Check memory usage + $memoryUsage = Get-MemoryUsage -server $server + if ($memoryUsage -gt 90) { + $message = Write-OutputRed "Memory Usage: ${memoryUsage}%" + } else { + $message = "Memory Usage: ${memoryUsage}%" + Write-Host $message + } + $output += $message + "`n" + + # Check drive sizes + $driveSizes = Get-DriveSizes -server $server + foreach ($drive in $driveSizes) { + $freeSpacePercent = [math]::round(($drive.FreeSpace / $drive.Size) * 100, 2) + if ($freeSpacePercent -lt 10) { + $message = "Drive $($drive.DeviceID) has less than 10% free space ($freeSpacePercent%)" + $message = Write-OutputRed $message + } else { + $message = "Drive $($drive.DeviceID) has $freeSpacePercent% free space" + Write-Host $message + } + $output += $message + "`n" + } + + $output += "`n" + return $output +} + +# Monitor servers continuously +while ($true) { + Clear-Host + $servers = Get-Content -Path $serverListPath + foreach ($server in $servers) { + $output = Display-ServerStatus -server $server + Write-Host $output + + # Write the log content to the file + $timestamp = Get-Timestamp + $logFilePath = "$logDirectory\serverstats_${server}_$timestamp.txt" + Set-Content -Path $logFilePath -Value $output + } + + # Clean up old log files + Cleanup-OldLogs + + Start-Sleep -Seconds 7200 # Wait for 2 hours +} From 1664d72e508aa4eb7ed071505110e08c09f8f752 Mon Sep 17 00:00:00 2001 From: Kevin Flowers <110041895+flowcompro@users.noreply.github.com> Date: Mon, 24 Jun 2024 21:10:13 -0400 Subject: [PATCH 5/9] Update server status script --- server status script | 98 +++++++++++++++++++++----------------------- 1 file changed, 47 insertions(+), 51 deletions(-) diff --git a/server status script b/server status script index 7d30321..f9b8a37 100644 --- a/server status script +++ b/server status script @@ -11,8 +11,8 @@ if (-not (Test-Path -Path $logDirectory)) { # Function to clean up old log files function Cleanup-OldLogs { $logFiles = Get-ChildItem -Path $logDirectory -Filter "serverstats_*.txt" | Sort-Object LastWriteTime -Descending - if ($logFiles.Count -gt 12) { - $filesToDelete = $logFiles | Select-Object -Skip 12 + if ($logFiles.Count -gt 300) { + $filesToDelete = $logFiles | Select-Object -Skip 300 foreach ($file in $filesToDelete) { Remove-Item -Path $file.FullName } @@ -24,13 +24,24 @@ function Get-Timestamp { return (Get-Date).ToString("yyyyMMdd_HHmmss") } -# Define function to check services +# Define function to check services and start if not running function Check-Services { param ( [string]$server ) - $serviceStatus = Get-WmiObject -ComputerName $server -Class Win32_Service | Where-Object { $_.StartMode -eq "Auto" -and $_.DelayedAutoStart -ne $true -and $_.State -ne "Running" } - $serviceStatus + $services = Get-WmiObject -ComputerName $server -Class Win32_Service | + Where-Object { $_.StartMode -eq "Auto" -and $_.DelayedAutoStart -ne $true -and $_.State -ne "Running" -and $_.Name -notin @("RemoteRegistry", "TrustedInstaller") } + + $serviceMessages = @() + + foreach ($service in $services) { + $serviceName = $service.Name + $serviceController = Get-Service -ComputerName $server -Name $serviceName + Start-Service -InputObject $serviceController + $serviceMessages += "Service '$serviceName' was not running and has been started" + } + + return $serviceMessages } # Define function to check CPU usage @@ -38,8 +49,7 @@ function Get-CPUUsage { param ( [string]$server ) - $cpuLoad = Get-WmiObject -ComputerName $server -Class Win32_Processor | Measure-Object -Property LoadPercentage -Average | Select-Object -ExpandProperty Average - $cpuLoad + Get-WmiObject -ComputerName $server -Class Win32_Processor | Measure-Object -Property LoadPercentage -Average | Select-Object -ExpandProperty Average } # Define function to check memory usage @@ -51,8 +61,7 @@ function Get-MemoryUsage { $totalMemory = $memoryStatus.TotalVisibleMemorySize $freeMemory = $memoryStatus.FreePhysicalMemory $usedMemory = $totalMemory - $freeMemory - $memoryUsagePercent = [math]::round(($usedMemory / $totalMemory) * 100, 2) - $memoryUsagePercent + [math]::round(($usedMemory / $totalMemory) * 100, 2) } # Define function to check drive sizes @@ -60,17 +69,7 @@ function Get-DriveSizes { param ( [string]$server ) - $driveSizes = Get-WmiObject -ComputerName $server -Class Win32_LogicalDisk -Filter "DriveType=3 AND DeviceID!='D:'" | Select-Object DeviceID, Size, FreeSpace - $driveSizes -} - -# Function to display output in red -function Write-OutputRed { - param ( - [string]$message - ) - Write-Host $message -ForegroundColor Red - return $message + Get-WmiObject -ComputerName $server -Class Win32_LogicalDisk -Filter "DriveType=3 AND DeviceID!='D:'" | Select-Object DeviceID, Size, FreeSpace } # Function to display output for a server @@ -78,56 +77,36 @@ function Display-ServerStatus { param ( [string]$server ) - $output = "Checking server: $server" + "`n" - Write-Host "Checking server: $server" -ForegroundColor Yellow + $output = "Checking server: $server`n" - # Check services + # Check services and start if not running $servicesOutput = @() - $servicesNotRunning = Check-Services -server $server - if ($servicesNotRunning) { - foreach ($service in $servicesNotRunning) { - $message = "Service '$($service.Name)' is set to Automatic but not running" + $servicesNotRunningMessages = Check-Services -server $server + if ($servicesNotRunningMessages) { + foreach ($message in $servicesNotRunningMessages) { $servicesOutput += $message - Write-OutputRed $message } } else { $message = "All automatic services are running" $servicesOutput += $message - Write-Host $message -ForegroundColor Green } - $output += $servicesOutput -join "`n" + "`n" + $output += ($servicesOutput -join "`n") + "`n" # Check CPU usage $cpuUsage = Get-CPUUsage -server $server - if ($cpuUsage -gt 90) { - $message = Write-OutputRed "CPU Usage: ${cpuUsage}%" - } else { - $message = "CPU Usage: ${cpuUsage}%" - Write-Host $message - } + $message = "CPU Usage: ${cpuUsage}%" $output += $message + "`n" # Check memory usage $memoryUsage = Get-MemoryUsage -server $server - if ($memoryUsage -gt 90) { - $message = Write-OutputRed "Memory Usage: ${memoryUsage}%" - } else { - $message = "Memory Usage: ${memoryUsage}%" - Write-Host $message - } + $message = "Memory Usage: ${memoryUsage}%" $output += $message + "`n" # Check drive sizes $driveSizes = Get-DriveSizes -server $server foreach ($drive in $driveSizes) { $freeSpacePercent = [math]::round(($drive.FreeSpace / $drive.Size) * 100, 2) - if ($freeSpacePercent -lt 10) { - $message = "Drive $($drive.DeviceID) has less than 10% free space ($freeSpacePercent%)" - $message = Write-OutputRed $message - } else { - $message = "Drive $($drive.DeviceID) has $freeSpacePercent% free space" - Write-Host $message - } + $message = "Drive $($drive.DeviceID) has $freeSpacePercent% free space" $output += $message + "`n" } @@ -135,22 +114,39 @@ function Display-ServerStatus { return $output } +# Function to display the countdown timer +function Display-Countdown { + param ( + [int]$seconds + ) + for ($i = $seconds; $i -gt 0; $i--) { + Write-Host -NoNewline "`rTime left before next check: $i seconds" + Start-Sleep -Seconds 1 + } + Write-Host "" +} + # Monitor servers continuously while ($true) { Clear-Host $servers = Get-Content -Path $serverListPath foreach ($server in $servers) { $output = Display-ServerStatus -server $server - Write-Host $output # Write the log content to the file $timestamp = Get-Timestamp $logFilePath = "$logDirectory\serverstats_${server}_$timestamp.txt" Set-Content -Path $logFilePath -Value $output + + # Display the output + Write-Host "" + Write-Host $output + Write-Host "" } # Clean up old log files Cleanup-OldLogs - Start-Sleep -Seconds 7200 # Wait for 2 hours + # Display the countdown timer + Display-Countdown -seconds 7200 } From 9147166bcc681c2ce84e3a6d764071547544c060 Mon Sep 17 00:00:00 2001 From: Kevin Flowers <110041895+flowcompro@users.noreply.github.com> Date: Tue, 25 Jun 2024 09:05:39 -0400 Subject: [PATCH 6/9] Update server status script --- server status script | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/server status script b/server status script index f9b8a37..b3be703 100644 --- a/server status script +++ b/server status script @@ -2,6 +2,12 @@ $serverListPath = "C:\Users\god\Downloads\Scripts\server status\serverlist.txt" # Define the path to the directory where logs will be stored $logDirectory = "C:\Users\god\Downloads\Scripts\server status\logs" +# Define the network path to move logs +$networkLogPath = "\\fs\shares\Logs\serverstatus" +# Define the interval to move logs (3 million seconds) +$logMoveInterval = 120 +# Define the interval for checking servers (2 hours) +$checkInterval = 60 # Create the log directory if it doesn't exist if (-not (Test-Path -Path $logDirectory)) { @@ -11,14 +17,22 @@ if (-not (Test-Path -Path $logDirectory)) { # Function to clean up old log files function Cleanup-OldLogs { $logFiles = Get-ChildItem -Path $logDirectory -Filter "serverstats_*.txt" | Sort-Object LastWriteTime -Descending - if ($logFiles.Count -gt 300) { - $filesToDelete = $logFiles | Select-Object -Skip 300 + if ($logFiles.Count -gt 3000000) { + $filesToDelete = $logFiles | Select-Object -Skip 3000000 foreach ($file in $filesToDelete) { Remove-Item -Path $file.FullName } } } +# Function to move log files to network path +function Move-LogFiles { + $logFiles = Get-ChildItem -Path $logDirectory -Filter "serverstats_*.txt" + foreach ($file in $logFiles) { + Move-Item -Path $file.FullName -Destination $networkLogPath + } +} + # Function to get the current timestamp function Get-Timestamp { return (Get-Date).ToString("yyyyMMdd_HHmmss") @@ -126,6 +140,9 @@ function Display-Countdown { Write-Host "" } +# Variable to track the last log move time +$lastLogMoveTime = [datetime]::Now + # Monitor servers continuously while ($true) { Clear-Host @@ -147,6 +164,13 @@ while ($true) { # Clean up old log files Cleanup-OldLogs + # Check if it's time to move the log files + $currentTime = [datetime]::Now + if (($currentTime - $lastLogMoveTime).TotalSeconds -ge $logMoveInterval) { + Move-LogFiles + $lastLogMoveTime = $currentTime + } + # Display the countdown timer - Display-Countdown -seconds 7200 + Display-Countdown -seconds $checkInterval } From 515a6d6192fa2d2dbb0ec9942f71198760b24d52 Mon Sep 17 00:00:00 2001 From: Kevin Flowers <110041895+flowcompro@users.noreply.github.com> Date: Tue, 25 Jun 2024 17:58:21 -0400 Subject: [PATCH 7/9] Update server status script --- server status script | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/server status script b/server status script index b3be703..6ca4691 100644 --- a/server status script +++ b/server status script @@ -3,10 +3,10 @@ $serverListPath = "C:\Users\god\Downloads\Scripts\server status\serverlist.txt" # Define the path to the directory where logs will be stored $logDirectory = "C:\Users\god\Downloads\Scripts\server status\logs" # Define the network path to move logs -$networkLogPath = "\\fs\shares\Logs\serverstatus" +$networkLogPath = "\\fs\share1\logs\serverstatus" # Define the interval to move logs (3 million seconds) -$logMoveInterval = 120 -# Define the interval for checking servers (2 hours) +$logMoveInterval = 3000000 +# Define the interval for checking servers (60 seconds) $checkInterval = 60 # Create the log directory if it doesn't exist @@ -44,7 +44,10 @@ function Check-Services { [string]$server ) $services = Get-WmiObject -ComputerName $server -Class Win32_Service | - Where-Object { $_.StartMode -eq "Auto" -and $_.DelayedAutoStart -ne $true -and $_.State -ne "Running" -and $_.Name -notin @("RemoteRegistry", "TrustedInstaller") } + Where-Object { + $_.StartMode -eq "Auto" -and $_.DelayedAutoStart -ne $true -and $_.State -ne "Running" -and + $_.Name -notin @("RemoteRegistry", "TrustedInstaller", "GoogleUpdaterInternalService128.0.6537.0", "GoogleUpdaterService128.0.6537.0") + } $serviceMessages = @() From 5b1b14303e79eff450010cee7fcbdbb9adcddda1 Mon Sep 17 00:00:00 2001 From: Kevin Flowers <110041895+flowcompro@users.noreply.github.com> Date: Sun, 4 May 2025 20:23:24 -0400 Subject: [PATCH 8/9] Add files via upload --- DHCP_backup_complete_jetDB_and_textV2.ps1 | 69 +++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 DHCP_backup_complete_jetDB_and_textV2.ps1 diff --git a/DHCP_backup_complete_jetDB_and_textV2.ps1 b/DHCP_backup_complete_jetDB_and_textV2.ps1 new file mode 100644 index 0000000..b0631e1 --- /dev/null +++ b/DHCP_backup_complete_jetDB_and_textV2.ps1 @@ -0,0 +1,69 @@ + # === GLOBAL CONFIGURATION === +$DateStamp = Get-Date -Format "yyyyMMdd_HHmmss" +$DateFolder = "DHCP Backup Dated $($DateStamp.Substring(0,8))" + +# TEXT EXPORT DESTINATION +$TextRoot = "\\fs\backups\DHCP\DHCP_TextBackups" +$TextFolder = Join-Path -Path $TextRoot -ChildPath "DHCP_text_backup $DateStamp" +$TextLogFile = Join-Path -Path $TextRoot -ChildPath "dhcp_backup_status.log" + +# JET DB DESTINATION +$JetRoot = "\\fs\backups\DHCP\DHCP_JetBackups" +$JetFolder = Join-Path -Path $JetRoot -ChildPath "DHCP Jet Backup $DateStamp" +$JetLogFile = Join-Path -Path $JetRoot -ChildPath "dhcp_jet_backup.log" +$JetSource = "$env:SystemRoot\System32\dhcp\backup" + +# === GENERAL LOG FUNCTION === +function Write-Log { + param ( + [string]$Message, + [string]$LogPath + ) + $Timestamped = "$(Get-Date -Format "s") - $Message" + Write-Output $Timestamped + try { + Add-Content -Path $LogPath -Value $Timestamped + } catch { + Write-Warning "Could not write to log file: $_" + } +} + +# === TEXT EXPORT BACKUP === +try { + New-Item -Path $TextFolder -ItemType Directory -Force | Out-Null + $ExportFile = Join-Path -Path $TextFolder -ChildPath "dhcp_backup_$DateStamp.txt" + netsh dhcp server export "$ExportFile" all + Write-Log "SUCCESS: DHCP text export saved to $ExportFile" $TextLogFile +} catch { + Write-Log "ERROR during text export: $($_.Exception.Message)" $TextLogFile +} + +# === JET DB BACKUP === +try { + Write-Log "Starting DHCP Jet database backup..." $JetLogFile + + # Ensure destination exists + if (!(Test-Path $JetFolder)) { + New-Item -Path $JetFolder -ItemType Directory -Force | Out-Null + } + + # Stop DHCP Server for clean backup + Write-Log "Stopping DHCP Server service..." $JetLogFile + Stop-Service dhcpserver -Force -ErrorAction Stop + + # Copy the Jet database backup folder + Write-Log "Copying backup from $JetSource to $JetFolder..." $JetLogFile + Copy-Item -Path "$JetSource\*" -Destination $JetFolder -Recurse -Force -ErrorAction Stop + + # Start DHCP Server again + Write-Log "Starting DHCP Server service..." $JetLogFile + Start-Service dhcpserver -ErrorAction Stop + + Write-Log "SUCCESS: Jet database backup completed to $JetFolder" $JetLogFile +} catch { + Write-Log "ERROR: Jet DB backup failed - $($_.Exception.Message)" $JetLogFile +} + +# === FINAL STATUS LOG === +Write-Log "COMPLETED: DHCP backup finished for $DateStamp" $TextLogFile +Exit 0 From 4b488112430d28596034224a8b725347a27e1a1a Mon Sep 17 00:00:00 2001 From: Kevin Flowers <110041895+flowcompro@users.noreply.github.com> Date: Sun, 1 Feb 2026 03:03:44 -0500 Subject: [PATCH 9/9] Create addvlanv14.py --- addvlanv14.py | 830 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 830 insertions(+) create mode 100644 addvlanv14.py diff --git a/addvlanv14.py b/addvlanv14.py new file mode 100644 index 0000000..e3fe11b --- /dev/null +++ b/addvlanv14.py @@ -0,0 +1,830 @@ + #!/usr/bin/env python3 +""" +Cisco IOS and IOS XE VLAN Trunk Manager +Version: v14 +Author: Kevin Flowers +================================================================================ +COMMANDS TO RUN +================================================================================ + +ADD VLANs to trunks and create VLANs globally if missing +python addvlansv14.py --switches switches.txt --vlans vlans.csv --verbose + +REMOVE VLANs from trunks and delete from VLAN database +This removes VLANs from trunks, deletes any SVI if present, then deletes VLANs +globally, with verification output. +python addvlansv14.py --switches switches.txt --vlans vlans.csv --remove --delete-svi --delete-vlans-global --verbose --verify + +================================================================================ + +Key fix in v14 +SVI detection is now strict and reliable. +Older versions used "show running-config interface Vlan" and searched for the string "interface Vlan". +On some switches that do not support SVIs, that command returns an error that can still contain those words. +That created false blockers and prevented "no vlan " from running. + +v14 uses: +show running-config | section ^interface Vlan$ + +Only if the output contains a real config stanza starting with: +interface Vlan +will it be treated as an SVI that blocks VLAN deletion. + +INSTRUCTIONS +================================================================================ + +Overview +This script connects to each switch in your inventory file, discovers trunk +ports, and then either adds or removes VLANs based on the VLAN file you provide. + +In ADD mode it creates missing VLANs in the VLAN database and then adds only the +missing VLANs to each trunk allowed list. + +In REMOVE mode it removes only the target VLANs from trunk allowed lists and can +optionally delete the VLAN from the VLAN database so it disappears from show vlan. + +================================================================================ +FILES YOU PROVIDE +================================================================================ + +1) Switch inventory file + +TXT format +Each line is a switch IP or hostname. +Blank lines and lines starting with # are ignored. + +Example switches.txt +10.192.25.14 +10.192.25.10 +10.192.25.12 + +CSV format +Must include a header with a column named ip or host. + +Example switches.csv +ip +10.192.25.14 +10.192.25.10 + +2) VLAN file + +TXT format +Each line is a VLAN ID. +Blank lines and lines starting with # are ignored. + +Example vlans.txt +300 +999 + +CSV format +Must include a header with a column named vlan_id or vlan or id. +Optional name column can be name or vlan_name. + +Example vlans.csv +vlan_id,name +300,test +999,NATIVE_PARKING + +================================================================================ +CREDENTIALS AND ENABLE MODE +================================================================================ + +The script will prompt for: +Username +Password +Enable secret + +Enable secret is requested only once and then cached and reused for all switches. + +If a switch starts at > prompt, the script will enter enable mode. +If a switch starts at # prompt, the script will continue without prompting. + +================================================================================ +WHAT THE SCRIPT DOES IN ADD MODE +================================================================================ + +Step 1 Connect to the switch and enter enable mode +Step 2 Disable terminal paging so command output does not pause +Step 3 Discover trunk ports using show interfaces trunk +Step 4 Read current VLAN database using show vlan brief +Step 5 Create only VLANs missing from the VLAN database +Step 6 For each trunk port, check the current allowed VLAN state +Step 7 Add only VLANs that are missing from that trunk port allowed list +Step 8 Save logs and move to the next switch + +================================================================================ +WHAT THE SCRIPT DOES IN REMOVE MODE +================================================================================ + +Step 1 Connect to the switch and enter enable mode +Step 2 Disable terminal paging so command output does not pause +Step 3 Discover trunk ports using show interfaces trunk +Step 4 For each trunk port, remove only the target VLANs if present +Step 5 Optional delete SVI interface Vlan if it exists +Step 6 Optional delete VLAN from VLAN database using no vlan +Step 7 Verify results and move to the next switch + +================================================================================ +IMPORTANT BEHAVIOR TO UNDERSTAND +================================================================================ + +Removing VLANs from trunk allowed lists does NOT remove the VLAN from the VLAN +database. + +To fully remove a VLAN so it disappears from show vlan, you must run: +no vlan + +VLAN deletion may be blocked for safety if: +An SVI interface Vlan exists +An access port references the VLAN +The VLAN still appears in trunk output + +The script detects these conditions and safely skips deletion if needed. + +================================================================================ +SCRIPT SWITCHES AND WHAT THEY MEAN +================================================================================ + +--switches +Required. +Path to the switch inventory file. + +--vlans +Required. +Path to the VLAN definitions file. + +--verbose +Optional. +Prints per trunk port details including add_candidates or remove_candidates. + +--verify +Optional. +Prints post change verification per VLAN: +exists_globally VLAN still present in show vlan +svi_exists interface Vlan still exists +trunk_has_vlan VLAN still appears in trunk output + +--remove +Optional. +Enables REMOVE mode. +Without this switch, the script runs in ADD mode. + +--delete-svi +Optional. +REMOVE mode only. +Deletes interface Vlan if present. + +--delete-vlans-global +Optional. +REMOVE mode only. +Deletes VLANs from the VLAN database using no vlan . + +--dry-run +Optional. +No configuration changes are made. +The script still connects and shows what it would do. + +--out +Optional. +Output directory for logs and backups. +Default is output_vlan_push. + +================================================================================ +OUTPUT FILES +================================================================================ + +Pre change backups are saved per switch under: +\backups\\\ + +Session logs are saved under: +\session__.log + +================================================================================ +""" + +from __future__ import annotations + +import argparse +import csv +import datetime as dt +import os +import re +from dataclasses import dataclass +from getpass import getpass +from typing import List, Optional, Set, Tuple, Callable + +from netmiko import ConnectHandler, NetmikoTimeoutException, NetmikoAuthenticationException + + +@dataclass(frozen=True) +class VlanDef: + vlan_id: int + name: Optional[str] = None + + +def now_stamp() -> str: + return dt.datetime.now().strftime("%Y%m%d_%H%M%S") + + +def ensure_dir(path: str) -> None: + os.makedirs(path, exist_ok=True) + + +def is_probably_csv(path: str) -> bool: + return path.lower().endswith(".csv") + + +def safe_filename(text: str) -> str: + return re.sub(r"[^A-Za-z0-9_.]+", "_", text).strip("_") + + +def read_switch_ips(path: str) -> List[str]: + ips: List[str] = [] + seen = set() + + with open(path, "r", encoding="utf-8-sig", newline="") as f: + sample = f.read(2048) + f.seek(0) + + if is_probably_csv(path) or ("," in sample and "ip" in sample.lower()): + reader = csv.DictReader(f) + if not reader.fieldnames: + raise ValueError("Switch CSV has no header row") + fields = [h.strip().lower() for h in reader.fieldnames] + key = "ip" if "ip" in fields else ("host" if "host" in fields else None) + if not key: + raise ValueError("Switch CSV must include a column named ip or host") + for row in reader: + raw = (row.get(key) or "").strip() + if raw and raw not in seen: + ips.append(raw) + seen.add(raw) + else: + for line in f: + line = line.strip() + if not line or line.startswith("#"): + continue + if line not in seen: + ips.append(line) + seen.add(line) + + return ips + + +def read_vlans(path: str) -> List[VlanDef]: + vlans: List[VlanDef] = [] + seen = set() + + with open(path, "r", encoding="utf-8-sig", newline="") as f: + sample = f.read(2048) + f.seek(0) + + if is_probably_csv(path) or ("," in sample and "vlan" in sample.lower()): + reader = csv.DictReader(f) + if not reader.fieldnames: + raise ValueError("VLAN CSV has no header row") + + fields = [h.strip().lower() for h in reader.fieldnames] + id_field = ( + "vlan_id" if "vlan_id" in fields + else "vlan" if "vlan" in fields + else "id" if "id" in fields + else None + ) + name_field = "name" if "name" in fields else ("vlan_name" if "vlan_name" in fields else None) + + if not id_field: + raise ValueError("VLAN CSV must include vlan_id or vlan or id column") + + for row in reader: + raw = (row.get(id_field) or "").strip() + if not raw: + continue + try: + vid = int(raw) + except ValueError: + continue + vname = (row.get(name_field) or "").strip() if name_field else "" + if vid not in seen: + vlans.append(VlanDef(vid, vname if vname else None)) + seen.add(vid) + else: + for line in f: + line = line.strip() + if not line or line.startswith("#"): + continue + if line.isdigit(): + vid = int(line) + if vid not in seen: + vlans.append(VlanDef(vid)) + seen.add(vid) + + return vlans + + +def parse_existing_vlans(show_vlan_brief: str) -> Set[int]: + existing: Set[int] = set() + for line in show_vlan_brief.splitlines(): + line = line.strip() + if not line or line.lower().startswith("vlan"): + continue + m = re.match(r"^(\d+)\s+", line) + if m: + existing.add(int(m.group(1))) + return existing + + +def build_vlan_create_cmds(vlans_to_create: List[VlanDef]) -> List[str]: + cmds: List[str] = [] + for v in vlans_to_create: + cmds.append(f"vlan {v.vlan_id}") + if v.name: + safe_name = re.sub(r"[^A-Za-z0-9 _]", "", v.name).strip() + if safe_name: + cmds.append(f"name {safe_name}") + cmds.append("exit") + return cmds + + +def expand_vlan_spec_to_set_or_all(spec: str) -> Optional[Set[int]]: + raw = (spec or "").strip().lower() + if not raw: + return set() + if raw == "all": + return None + if raw in {"1-4094", "1-4095", "1-4096"}: + return None + + vlans: Set[int] = set() + for part in raw.split(","): + part = part.strip() + if not part: + continue + if "-" in part: + a, b = part.split("-", 1) + a = a.strip() + b = b.strip() + if a.isdigit() and b.isdigit(): + start = int(a) + end = int(b) + if start <= end: + for v in range(start, end + 1): + vlans.add(v) + elif part.isdigit(): + vlans.add(int(part)) + return vlans + + +def chunk_vlan_list(vlans: List[int], max_len: int = 120) -> List[str]: + chunks: List[str] = [] + current: List[str] = [] + current_len = 0 + + for vid in vlans: + s = str(vid) + add_len = len(s) + (1 if current else 0) + if current and (current_len + add_len) > max_len: + chunks.append(",".join(current)) + current = [s] + current_len = len(s) + else: + current_len = current_len + add_len if current else len(s) + current.append(s) + + if current: + chunks.append(",".join(current)) + + return chunks + + +def parse_trunk_ports_from_show_interfaces_trunk(output: str) -> List[str]: + trunk_ports: Set[str] = set() + in_table = False + + for line in output.splitlines(): + raw = line.rstrip() + if not raw: + continue + low = raw.lower() + + if low.startswith("port") and "mode" in low: + in_table = True + continue + + if not in_table: + continue + + parts = raw.split() + if not parts: + continue + + if parts[0].lower() == "port": + continue + + port = parts[0] + if re.match(r"^(gi|te|fo|hu|tw|po|fa)\S+$", port.lower()): + trunk_ports.add(port) + + return sorted(trunk_ports, key=lambda x: (len(x), x)) + + +def get_allowed_vlans_for_interface(conn: ConnectHandler, iface: str) -> Optional[Set[int]]: + out = conn.send_command(f"show interfaces {iface} switchport", use_textfsm=False) + for line in out.splitlines(): + low = line.lower().strip() + if low.startswith("trunking vlans allowed:"): + spec = line.split(":", 1)[1].strip() + return expand_vlan_spec_to_set_or_all(spec) + if low.startswith("trunking vlans enabled:"): + spec = line.split(":", 1)[1].strip() + return expand_vlan_spec_to_set_or_all(spec) + return set() + + +def connect_ios(ip: str, username: str, password: str, session_log: str, secret: Optional[str]) -> ConnectHandler: + device = { + "device_type": "cisco_ios", + "host": ip, + "username": username, + "password": password, + "secret": secret or "", + "session_log": session_log, + "fast_cli": False, + "global_delay_factor": 1, + "keepalive": 30, + "conn_timeout": 20, + "banner_timeout": 25, + "auth_timeout": 25, + } + return ConnectHandler(**device) + + +def ensure_enable_mode(conn: ConnectHandler, ip: str, shared_secret: Optional[str]) -> Optional[str]: + prompt = conn.find_prompt().strip() + + if prompt.endswith("#"): + return shared_secret + + if prompt.endswith(">"): + if not shared_secret: + shared_secret = getpass("Enable secret: ") + conn.secret = shared_secret + conn.enable() + prompt2 = conn.find_prompt().strip() + if prompt2.endswith("#"): + return shared_secret + raise ValueError(f"{ip} enable failed. Prompt after enable attempt was: {prompt2}") + + raise ValueError(f"{ip} unexpected prompt: {prompt}") + + +def prep_terminal(conn: ConnectHandler) -> None: + conn.send_command_timing("terminal length 0") + conn.send_command_timing("terminal width 0") + + +def write_text_file(path: str, content: str) -> None: + with open(path, "w", encoding="utf-8") as f: + f.write(content) + + +def gather_prechange_backups( + conn: ConnectHandler, + out_dir: str, + ip: str, + run_id: str, + vlan_ids: List[int], + verbose: bool, +) -> str: + base = os.path.join(out_dir, "backups", safe_filename(ip), run_id) + ensure_dir(base) + + commands: List[Tuple[str, str, bool]] = [ + ("show_version.txt", "show version", True), + ("show_vlan_brief.txt", "show vlan brief", False), + ("show_interfaces_trunk.txt", "show interfaces trunk", False), + ("show_spanning_tree_summary.txt", "show spanning-tree summary", False), + ("show_run_vlan_section.txt", "show running-config | section ^vlan", True), + ("show_run_interface_vlan_lines.txt", "show running-config | include interface Vlan", True), + ("show_run_access_vlan_refs.txt", "show running-config | include switchport access vlan", True), + ("show_run_trunk_allowed_refs.txt", "show running-config | include trunk allowed vlan", True), + ] + + for filename, cmd, heavy in commands: + try: + out = conn.send_command_timing(cmd) if heavy else conn.send_command(cmd, use_textfsm=False) + except Exception as e: + out = f"ERROR running command: {cmd}\n{e}\n" + write_text_file(os.path.join(base, filename), out) + + for vid in vlan_ids: + try: + out1 = conn.send_command_timing(f"show running-config | include vlan {vid}") + except Exception as e: + out1 = f"ERROR running command: show running-config | include vlan {vid}\n{e}\n" + write_text_file(os.path.join(base, f"show_run_include_vlan_{vid}.txt"), out1) + + try: + out2 = conn.send_command_timing(f"show running-config | section ^interface Vlan{vid}$") + except Exception as e: + out2 = f"ERROR running command: show running-config | section ^interface Vlan{vid}$\n{e}\n" + write_text_file(os.path.join(base, f"show_run_section_interface_Vlan{vid}.txt"), out2) + + if verbose: + print(f" Pre change backups saved to: {base}") + + return base + + +def svi_exists(conn: ConnectHandler, vlan_id: int) -> bool: + out = conn.send_command_timing(f"show running-config | section ^interface Vlan{vlan_id}$") + return bool(re.search(rf"^interface Vlan{vlan_id}\b", out, flags=re.M)) + + +def remove_svi_best_effort(conn: ConnectHandler, vlan_id: int, verbose: bool) -> bool: + if not svi_exists(conn, vlan_id): + return False + + attempts = [ + [f"no interface Vlan{vlan_id}"], + [f"default interface Vlan{vlan_id}"], + ] + + for cmdset in attempts: + out = conn.send_config_set(cmdset, cmd_verify=False) + bad = any(x in out.lower() for x in ["invalid input", "incomplete command", "ambiguous command"]) + if verbose and bad: + print(" Device rejected SVI removal attempt, output follows") + print(out.strip()) + if not svi_exists(conn, vlan_id): + return True + + return False + + +def vlan_delete_blockers(conn: ConnectHandler, vlan_id: int) -> List[str]: + blockers: List[str] = [] + + if svi_exists(conn, vlan_id): + blockers.append(f"SVI exists interface Vlan{vlan_id}") + + run_access = conn.send_command_timing(f"show running-config | include switchport access vlan {vlan_id}") + if run_access.strip(): + blockers.append(f"Access ports reference vlan {vlan_id}") + + trunk_check = conn.send_command("show interfaces trunk | include " + str(vlan_id), use_textfsm=False) + if trunk_check.strip(): + blockers.append("Still appears in show interfaces trunk output") + + return blockers + + +def verify_vlan_exists(conn: ConnectHandler, vlan_id: int) -> bool: + vlan_out = conn.send_command("show vlan brief", use_textfsm=False) + return vlan_id in parse_existing_vlans(vlan_out) + + +def build_trunk_cmds_for_mode( + conn: ConnectHandler, + trunk_ports: List[str], + vlan_ids: List[int], + mode: str, + verbose: bool, +) -> List[str]: + cmds: List[str] = [] + + for iface in trunk_ports: + allowed = get_allowed_vlans_for_interface(conn, iface) + + if mode == "remove": + if allowed is None: + candidates = vlan_ids[:] + allowed_desc = "ALL" + else: + candidates = [v for v in vlan_ids if v in allowed] + allowed_desc = "EXPLICIT" + + if verbose: + print(f" {iface} allowed={allowed_desc} remove_candidates={candidates}") + + if not candidates: + continue + + cmds.append(f"interface {iface}") + for chunk in chunk_vlan_list(candidates): + cmds.append(f"switchport trunk allowed vlan remove {chunk}") + cmds.append("exit") + + else: + if allowed is None: + candidates = [] + allowed_desc = "ALL" + else: + candidates = [v for v in vlan_ids if v not in allowed] + allowed_desc = "EXPLICIT" + + if verbose: + print(f" {iface} allowed={allowed_desc} add_candidates={candidates}") + + if not candidates: + continue + + cmds.append(f"interface {iface}") + for chunk in chunk_vlan_list(candidates): + cmds.append(f"switchport trunk allowed vlan add {chunk}") + cmds.append("exit") + + if verbose and not cmds: + print(" No trunk changes required") + + return cmds + + +def apply_changes_with_retry_and_secret_cache( + ip: str, + username: str, + password: str, + secret_in: Optional[str], + session_log: str, + apply_fn: Callable[[ConnectHandler, Optional[str]], Optional[str]], +) -> Optional[str]: + secret = secret_in + + try: + conn = connect_ios(ip, username, password, session_log, secret) + secret = ensure_enable_mode(conn, ip, secret) + prep_terminal(conn) + secret = apply_fn(conn, secret) + conn.disconnect() + return secret + except Exception as e: + msg = str(e).lower() + try: + conn.disconnect() + except Exception: + pass + if "socket is closed" not in msg: + raise + + conn2 = connect_ios(ip, username, password, session_log, secret) + secret = ensure_enable_mode(conn2, ip, secret) + prep_terminal(conn2) + secret = apply_fn(conn2, secret) + conn2.disconnect() + return secret + + +def run_switch( + conn: ConnectHandler, + vlan_defs: List[VlanDef], + mode: str, + dry_run: bool, + verbose: bool, + delete_vlans_global: bool, + delete_svi: bool, + verify: bool, +) -> None: + vlan_ids = sorted({v.vlan_id for v in vlan_defs}) + + existing_vlan_ids = parse_existing_vlans(conn.send_command("show vlan brief", use_textfsm=False)) + trunk_out = conn.send_command("show interfaces trunk", use_textfsm=False) + trunk_ports = parse_trunk_ports_from_show_interfaces_trunk(trunk_out) + + if verbose: + print(f"Trunks found: {len(trunk_ports)}") + + if mode == "add": + missing_defs = [v for v in vlan_defs if v.vlan_id not in existing_vlan_ids] + if missing_defs and verbose: + print(f"Global VLANs to create: {[v.vlan_id for v in missing_defs]}") + if missing_defs and not dry_run: + conn.send_config_set(build_vlan_create_cmds(missing_defs), cmd_verify=False) + + trunk_cmds = build_trunk_cmds_for_mode(conn, trunk_ports, vlan_ids, mode, verbose) + + if dry_run: + return + + if trunk_cmds: + conn.send_config_set(trunk_cmds, cmd_verify=False) + + if mode == "remove" and delete_svi: + for vid in vlan_ids: + if svi_exists(conn, vid): + if verbose: + print(f" Deleting SVI for VLAN {vid}") + removed = remove_svi_best_effort(conn, vid, verbose) + if verbose and removed: + print(f" SVI Vlan{vid} removed") + + if mode == "remove" and delete_vlans_global: + for vid in vlan_ids: + if vid not in existing_vlan_ids: + if verbose: + print(f" VLAN {vid} not present in vlan database, skipping no vlan") + continue + + blockers = vlan_delete_blockers(conn, vid) + if blockers: + print(f" Skipping no vlan {vid} due to blockers: {blockers}") + continue + + if verbose: + print(f" Deleting VLAN from database: no vlan {vid}") + out = conn.send_config_set([f"no vlan {vid}"], cmd_verify=False) + + if verbose and any(x in out.lower() for x in ["invalid input", "incomplete command", "ambiguous command"]): + print(" Device rejected no vlan command, output follows") + print(out.strip()) + + if verify: + for vid in vlan_ids: + exists_after = verify_vlan_exists(conn, vid) + trunk_after = conn.send_command("show interfaces trunk | include " + str(vid), use_textfsm=False).strip() + svi_after = svi_exists(conn, vid) + print(f" VERIFY vlan={vid} exists_globally={exists_after} svi_exists={svi_after} trunk_has_vlan_text={bool(trunk_after)}") + + +def main() -> int: + parser = argparse.ArgumentParser(description="Add or remove VLANs on trunk ports across Cisco IOS and IOS XE switches") + parser.add_argument("--switches", required=True, help="Switch inventory file txt or csv") + parser.add_argument("--vlans", required=True, help="VLAN definitions file txt or csv") + parser.add_argument("--out", default="output_vlan_push", help="Output directory for session logs and backups") + parser.add_argument("--dry-run", action="store_true", help="Do not change devices, only report actions") + parser.add_argument("--remove", action="store_true", help="Remove mode") + parser.add_argument("--delete-vlans-global", action="store_true", help="Remove mode only, deletes VLANs from vlan database using no vlan") + parser.add_argument("--delete-svi", action="store_true", help="Remove mode only, attempts to delete interface Vlan when it actually exists") + parser.add_argument("--verbose", action="store_true", help="Print detailed per interface actions") + parser.add_argument("--verify", action="store_true", help="Verify VLAN state after changes") + args = parser.parse_args() + + ensure_dir(args.out) + run_id = now_stamp() + + switch_ips = read_switch_ips(args.switches) + vlan_defs = read_vlans(args.vlans) + + if not switch_ips: + print("No switches found in the switches file") + return 2 + if not vlan_defs: + print("No VLANs found in the VLANs file") + return 2 + + if (args.delete_vlans_global or args.delete_svi) and not args.remove: + print("ERROR delete-vlans-global and delete-svi can only be used with remove mode") + return 2 + + mode = "remove" if args.remove else "add" + print(f"Mode: {mode.upper()}") + print(f"Switches loaded: {len(switch_ips)}") + print(f"VLANs loaded: {len(vlan_defs)}") + + username = input("Username: ").strip() + password = getpass("Password: ") + + cached_enable_secret: Optional[str] = None + vlan_ids = sorted({v.vlan_id for v in vlan_defs}) + + for ip in switch_ips: + session_log = os.path.join(args.out, f"session_{safe_filename(ip)}_{run_id}.log") + + try: + def apply_fn(conn: ConnectHandler, secret: Optional[str]) -> Optional[str]: + gather_prechange_backups(conn, args.out, ip, run_id, vlan_ids, args.verbose) + run_switch( + conn=conn, + vlan_defs=vlan_defs, + mode=mode, + dry_run=args.dry_run, + verbose=args.verbose, + delete_vlans_global=args.delete_vlans_global, + delete_svi=args.delete_svi, + verify=args.verify, + ) + return secret + + cached_enable_secret = apply_changes_with_retry_and_secret_cache( + ip=ip, + username=username, + password=password, + secret_in=cached_enable_secret, + session_log=session_log, + apply_fn=apply_fn, + ) + + print(f"{ip}: OK") + + except NetmikoAuthenticationException as e: + print(f"{ip}: AUTH_FAIL {e}") + except NetmikoTimeoutException as e: + print(f"{ip}: TIMEOUT {e}") + except Exception as e: + print(f"{ip}: ERROR {e}") + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) +