diff --git a/Ad Menu report script b/Ad Menu report script new file mode 100644 index 0000000..3e5ec30 --- /dev/null +++ b/Ad Menu report script @@ -0,0 +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) + 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 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) 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()) + diff --git a/server status script b/server status script new file mode 100644 index 0000000..6ca4691 --- /dev/null +++ b/server status script @@ -0,0 +1,179 @@ +# 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" +# Define the network path to move logs +$networkLogPath = "\\fs\share1\logs\serverstatus" +# Define the interval to move logs (3 million seconds) +$logMoveInterval = 3000000 +# Define the interval for checking servers (60 seconds) +$checkInterval = 60 + +# 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 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") +} + +# Define function to check services and start if not running +function Check-Services { + param ( + [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", "GoogleUpdaterInternalService128.0.6537.0", "GoogleUpdaterService128.0.6537.0") + } + + $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 +function Get-CPUUsage { + param ( + [string]$server + ) + Get-WmiObject -ComputerName $server -Class Win32_Processor | Measure-Object -Property LoadPercentage -Average | Select-Object -ExpandProperty Average +} + +# 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 + [math]::round(($usedMemory / $totalMemory) * 100, 2) +} + +# Define function to check drive sizes +function Get-DriveSizes { + param ( + [string]$server + ) + 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 +function Display-ServerStatus { + param ( + [string]$server + ) + $output = "Checking server: $server`n" + + # Check services and start if not running + $servicesOutput = @() + $servicesNotRunningMessages = Check-Services -server $server + if ($servicesNotRunningMessages) { + foreach ($message in $servicesNotRunningMessages) { + $servicesOutput += $message + } + } else { + $message = "All automatic services are running" + $servicesOutput += $message + } + $output += ($servicesOutput -join "`n") + "`n" + + # Check CPU usage + $cpuUsage = Get-CPUUsage -server $server + $message = "CPU Usage: ${cpuUsage}%" + $output += $message + "`n" + + # Check memory usage + $memoryUsage = Get-MemoryUsage -server $server + $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) + $message = "Drive $($drive.DeviceID) has $freeSpacePercent% free space" + $output += $message + "`n" + } + + $output += "`n" + 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 "" +} + +# Variable to track the last log move time +$lastLogMoveTime = [datetime]::Now + +# Monitor servers continuously +while ($true) { + Clear-Host + $servers = Get-Content -Path $serverListPath + foreach ($server in $servers) { + $output = Display-ServerStatus -server $server + + # 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 + + # 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 $checkInterval +}