From 60f083413a5d4bfd498d24468d6ca2eaa23a5d9d Mon Sep 17 00:00:00 2001 From: Miguel Date: Thu, 3 Apr 2025 10:50:59 +0200 Subject: [PATCH] Agregado de ping y del campo de mascara --- ip-changer-admin.py | 31 +- ip_config.json | 2 +- ipchange - copia.py | 192 ---------- menu-ip-change.py | 878 ++++++++++++++++++++++++++++++++++++-------- ping_targets.json | 1 + 5 files changed, 749 insertions(+), 355 deletions(-) delete mode 100644 ipchange - copia.py create mode 100644 ping_targets.json diff --git a/ip-changer-admin.py b/ip-changer-admin.py index c85b3a7..d8360b2 100644 --- a/ip-changer-admin.py +++ b/ip-changer-admin.py @@ -1,7 +1,6 @@ import sys import subprocess import ctypes -import time import logging import os from datetime import datetime @@ -46,6 +45,9 @@ def run_command(logger, cmd: str) -> bool: logger.info(f"Command output: {result.stdout}") if result.stderr: logger.error(f"Command error: {result.stderr}") + + if result.returncode != 0: + logger.error(f"Command failed with return code: {result.returncode}") return result.returncode == 0 @@ -68,25 +70,38 @@ def main(): if len(sys.argv) < 3: logger.error("Insufficient arguments") - logger.info("Usage: ip_changer_admin.py ") + logger.info("Usage: ip_changer_admin.py ") logger.info("Example for static IP: ip_changer_admin.py Ethernet 192.168.1.100") logger.info("Example for DHCP: ip_changer_admin.py Ethernet dhcp") + logger.info("Example for command: ip_changer_admin.py Ethernet \"netsh interface ip set address...\"") return 1 interface = sys.argv[1] - ip_mode = sys.argv[2] + param = sys.argv[2] logger.info(f"Processing request for interface: {interface}") - logger.info(f"IP mode: {ip_mode}") + logger.info(f"Parameter: {param}") - if ip_mode.lower() == "dhcp": + # Check if the second parameter is a full command (starts with netsh) + if param.lower().startswith('netsh'): + cmd = param + elif param.lower() == "dhcp": cmd = f'netsh interface ip set address "{interface}" dhcp' else: - # Asumimos que es una IP estática - ip = ip_mode + # Assume it's an IP address (for backward compatibility) + ip = param + + # If more parameters are provided, use them for subnet mask + if len(sys.argv) > 3: + subnet_mask = sys.argv[3] + else: + subnet_mask = "255.255.255.0" + + # Extract first three octets for gateway gateway = '.'.join(ip.split('.')[:3] + ['1']) - cmd = f'netsh interface ip set address "{interface}" static {ip} 255.255.255.0 {gateway}' + cmd = f'netsh interface ip set address "{interface}" static {ip} {subnet_mask} {gateway}' + logger.info(f"Command to execute: {cmd}") success = run_command(logger, cmd) if success: diff --git a/ip_config.json b/ip_config.json index 0b2b7a5..232d008 100644 --- a/ip_config.json +++ b/ip_config.json @@ -1 +1 @@ -{"last_interface": "Ethernet", "last_ip_prefix": "10.1.33"} \ No newline at end of file +{"last_interface": "Ethernet", "last_ip_prefix": "10.1.22", "previous_config": {"name": "Ethernet", "ip_address": "10.1.22.249", "subnet_mask": "255.240.0.0", "gateway": "10.1.22.1", "dhcp_enabled": false}} \ No newline at end of file diff --git a/ipchange - copia.py b/ipchange - copia.py deleted file mode 100644 index 89ad786..0000000 --- a/ipchange - copia.py +++ /dev/null @@ -1,192 +0,0 @@ -import ctypes -import subprocess -import sys -import re -import os -import json -import time -from win10toast import ToastNotifier - -# Definir una constante para la ruta del directorio -DIR = "D:\\Proyectos\\Scripts\\IPChanger" -IP_HISTORY_FILE = os.path.join(DIR, "ip_history.json") - - -def is_admin(): - """Verifica si el script se está ejecutando con privilegios de administrador.""" - try: - return ctypes.windll.shell32.IsUserAnAdmin() - except: - return False - - -def run_as_admin(argv=None, debug=False): - """Ejecuta el script como administrador.""" - shell32 = ctypes.windll.shell32 - if argv is None and shell32.IsUserAnAdmin(): - # Ya estamos ejecutando como administrador, no es necesario hacer nada - return True - if argv is None: - argv = sys.argv - if hasattr(sys, "_MEIPASS"): - # Soporte para PyInstaller - arguments = map(str, argv[1:]) - else: - arguments = map(str, argv) - argument_line = " ".join(arguments) - executable = str(sys.executable) - if debug: - print(f"Command line: {executable} {argument_line}") - ret = shell32.ShellExecuteW(None, "runas", executable, argument_line, None, 1) - if int(ret) <= 32: - return False - return None - - -def set_static_ip(ip_address, gateway): - """Establece una dirección IP estática para el adaptador Ethernet.""" - try: - subprocess.run( - f'netsh interface ip set address "Ethernet" static {ip_address} 255.255.255.0 {gateway}', - shell=True, - ) - return f"Dirección IP establecida en {ip_address}" - except Exception as e: - return f"Error al establecer la dirección IP: {e}" - - -def set_dhcp(): - """Configura el adaptador Ethernet para obtener la dirección IP automáticamente a través de DHCP.""" - try: - subprocess.run('netsh interface ip set address "Ethernet" dhcp', shell=True) - return "Adaptador Ethernet configurado para DHCP" - except Exception as e: - return f"Error al configurar DHCP: {e}" - - -def save_current_ip(): - """Guarda la dirección IP actual del adaptador Ethernet en un archivo.""" - try: - ip_config_output = subprocess.check_output("ipconfig", shell=True).decode( - "cp850" - ) - current_ip = re.search( - "Adaptador de Ethernet Ethernet:[\s\S]*?Dirección IPv4. . . . . . . . . . . . . . : (\d+\.\d+\.\d+\.\d+)", - ip_config_output, - ) - if current_ip: - update_ip_history(current_ip.group(1)) - return f"Dirección IP actual guardada: {current_ip.group(1)}" - else: - return "No se encontró la dirección IP actual." - except Exception as e: - return f"Error al guardar la dirección IP actual: {e}" - - -def update_ip_history(current_ip): - """Actualiza el historial de las últimas 10 IPs usadas, excluyendo 'auto'.""" - if current_ip.lower() == "auto": - return - - if os.path.exists(IP_HISTORY_FILE): - with open(IP_HISTORY_FILE, "r") as file: - ip_history = json.load(file) - else: - ip_history = [] - - ip_history.insert(0, current_ip) - ip_history = ip_history[:10] - - with open("ip_history.json", "w") as file: - json.dump(ip_history, file) - - -def restore_last_ip(): - """Restaura la última dirección IP guardada para el adaptador Ethernet.""" - if os.path.exists(IP_HISTORY_FILE): - with open(IP_HISTORY_FILE, "r") as file: - ip_history = json.load(file) - if ip_history: - last_ip = ip_history[0] # Obtener la última IP del historial - gateway = last_ip.rsplit(".", 1)[0] + ".1" - return set_static_ip(last_ip, gateway) - else: - return "No hay direcciones IP guardadas en el historial." - else: - return "No se encontró el archivo de historial de IPs." - - -def get_input_parameter(): - """Solicita al usuario un parámetro por línea de comandos.""" - return input( - "Ingrese la dirección IP, 'auto' para DHCP, o 'last' para la última IP: " - ) - - -def display_ip_history(ip_history): - """Muestra el historial de las últimas 10 IPs y permite al usuario seleccionar una.""" - if ip_history: - for i, ip in enumerate(ip_history): - print(f"{i}: {ip}") - choice = input( - "Seleccione una IP del historial (0-9), o presione Enter para continuar: " - ) - if choice.isdigit() and 0 <= int(choice) < len(ip_history): - return ip_history[int(choice)] - return None - - -def main(): - if not is_admin(): - # Si no se está ejecutando como administrador, solicitar elevación - result = run_as_admin() - if result is True: - print("Ejecutando como administrador.") - elif result is False: - print("No se pudo obtener privilegios de administrador.") - return - else: - # El script se reinició con privilegios de administrador - return - - if os.path.exists(IP_HISTORY_FILE): - with open(IP_HISTORY_FILE, "r") as file: - ip_history = json.load(file) - - argument = sys.argv[1] if len(sys.argv) > 1 else None - - if not argument: - chosen_ip = display_ip_history() - if chosen_ip: - argument = chosen_ip - else: - argument = get_input_parameter() - - # Guardar la dirección IP actual solo si no es 'auto' - if argument.lower() != "auto": - message = save_current_ip() - - if argument == "auto": - message = set_dhcp() - elif argument == "last": - message = restore_last_ip() - else: - ip_parts = argument.split(".") - if len(ip_parts) == 3: - argument += ".249" - gateway = ".".join(ip_parts[:3]) + ".1" - message = set_static_ip(argument, gateway) - update_ip_history(argument) # Actualizar el historial solo con IPs estáticas - - # Aquí asumimos que 'message' contiene el mensaje que quieres mostrar - try: - toaster = ToastNotifier() - toaster.show_toast("Cambio de IP", message, duration=10) - except Exception as e: - print( - f"Se produjo un error al mostrar la notificación, pero la operación se completó con éxito: {e}" - ) - - -if __name__ == "__main__": - main() diff --git a/menu-ip-change.py b/menu-ip-change.py index e48af87..2838523 100644 --- a/menu-ip-change.py +++ b/menu-ip-change.py @@ -7,17 +7,43 @@ import re import ctypes import sys import ipaddress -from typing import List, Optional +from typing import List, Optional, Dict from dataclasses import dataclass import threading import time +import socket + +# Add libraries for ping and SNMP functionality +try: + from pythonping import ping as pythonping + + PING_LIBRARY_AVAILABLE = True +except ImportError: + PING_LIBRARY_AVAILABLE = False + print( + "PythonPing module not found. Install with 'pip install pythonping' for better ping functionality." + ) + +try: + from pysnmp.hlapi import * + + SNMP_AVAILABLE = True +except ImportError: + SNMP_AVAILABLE = False + print( + "pysnmp module not found. Install with 'pip install pysnmp' for SNMP functionality." + ) try: import wmi + WMI_AVAILABLE = True except ImportError: WMI_AVAILABLE = False - print("WMI module not found. Using alternative method for network interface detection.") + print( + "WMI module not found. Using alternative method for network interface detection." + ) + @dataclass class NetworkInterface: @@ -30,172 +56,340 @@ class NetworkInterface: active: bool = True adapter_id: str = "" + class IPChangeConfig: def __init__(self): self.config_file = "ip_config.json" self.history_file = "ip_history.json" + self.ping_targets_file = "ping_targets.json" # New file for ping targets self.load_config() + self.load_ping_targets() # Load ping targets def load_config(self): try: if os.path.exists(self.config_file): - with open(self.config_file, 'r') as f: + with open(self.config_file, "r") as f: config = json.load(f) - self.last_interface = config.get('last_interface', '') - self.last_ip_prefix = config.get('last_ip_prefix', '') + self.last_interface = config.get("last_interface", "") + self.last_ip_prefix = config.get("last_ip_prefix", "") + self.previous_config = config.get("previous_config", {}) else: - self.last_interface = '' - self.last_ip_prefix = '' + self.last_interface = "" + self.last_ip_prefix = "" + self.previous_config = {} except Exception as e: print(f"Error loading config: {e}") - self.last_interface = '' - self.last_ip_prefix = '' + self.last_interface = "" + self.last_ip_prefix = "" + self.previous_config = {} def save_config(self): try: config = { - 'last_interface': self.last_interface, - 'last_ip_prefix': self.last_ip_prefix + "last_interface": self.last_interface, + "last_ip_prefix": self.last_ip_prefix, + "previous_config": self.previous_config, } - with open(self.config_file, 'w') as f: + with open(self.config_file, "w") as f: json.dump(config, f) except Exception as e: print(f"Error saving config: {e}") + def load_ping_targets(self): + """Load saved ping targets for each IP prefix""" + try: + if os.path.exists(self.ping_targets_file): + with open(self.ping_targets_file, "r") as f: + self.ping_targets = json.load(f) + else: + self.ping_targets = {} # Dictionary to store ping targets by IP prefix + except Exception as e: + print(f"Error loading ping targets: {e}") + self.ping_targets = {} + + def save_ping_targets(self): + """Save ping targets for each IP prefix""" + try: + with open(self.ping_targets_file, "w") as f: + json.dump(self.ping_targets, f) + except Exception as e: + print(f"Error saving ping targets: {e}") + + def get_ping_target(self, ip_prefix): + """Get the saved ping target for the given IP prefix""" + if not ip_prefix: + return "" + + # Try to find exact match first + if ip_prefix in self.ping_targets: + return self.ping_targets[ip_prefix] + + # If no exact match, try to find a matching IP prefix + for prefix, target in self.ping_targets.items(): + if ip_prefix.startswith(prefix) or prefix.startswith(ip_prefix): + return target + + # Default target: IP prefix + ".1" (usually the gateway) + return f"{ip_prefix}.1" + + def set_ping_target(self, ip_prefix, target): + """Save a ping target for the given IP prefix""" + if ip_prefix and target: + self.ping_targets[ip_prefix] = target + self.save_ping_targets() + + class IPChangerApp: def __init__(self, master): self.master = master self.master.title("Network Interface IP Configuration") self.master.geometry("900x700") - + # Inicializar configuración antes que nada self.config = IPChangeConfig() - + # Inicializar estructuras de datos self.interfaces: List[NetworkInterface] = [] self.ip_history: List[str] = [] - + # Inicializar WMI si está disponible if WMI_AVAILABLE: self.wmi_client = wmi.WMI() - + # Cargar datos guardados antes de crear widgets self.load_ip_history() - + # Crear la interfaz self.create_widgets() - + + # Initialize trace variables + self.subnet_trace_id = None + self.cidr_trace_id = None + + # Set up initial traces safely + self.setup_mask_traces() + # Actualizar la lista de IPs en el combo self.update_history_display() - + # Refrescar interfaces self.refresh_interfaces() - + # Configurar pesos de la cuadrícula self.master.grid_columnconfigure(0, weight=1) self.master.grid_rowconfigure(1, weight=1) # El log expandible + def setup_mask_traces(self): + """Set up traces for subnet mask and CIDR fields safely""" + # First initialize variables + self.subnet_mask.set(self.subnet_mask.get()) # Ensure current value is valid + self.cidr_bits.set(self.cidr_bits.get()) # Ensure current value is valid + + # Add traces using a different approach - using add not trace_add + self.subnet_mask.trace("w", self.on_subnet_mask_changed) + self.cidr_bits.trace("w", self.on_cidr_bits_changed) + + self.log_message("Subnet mask traces initialized") + + def remove_mask_traces(self): + """Not needed - we'll use a different approach""" + pass # Intentionally empty + def get_ip_prefix(self, ip: str) -> str: """Extrae los primeros 3 octetos de una dirección IP""" - parts = ip.split('.') + parts = ip.split(".") if len(parts) >= 3: - return '.'.join(parts[:3]) + return ".".join(parts[:3]) return ip def create_widgets(self): # Panel superior para controles self.control_frame = ttk.Frame(self.master, padding="10") - self.control_frame.grid(row=0, column=0, sticky='new') + self.control_frame.grid(row=0, column=0, sticky="new") # Sección de interfaces - self.interfaces_frame = ttk.LabelFrame(self.control_frame, text="Network Interfaces", padding="5") - self.interfaces_frame.grid(row=0, column=0, sticky='ew', pady=(0,10)) - - ttk.Label(self.interfaces_frame, text="Select Interface:").grid(row=0, column=0, sticky='w', padx=5) + self.interfaces_frame = ttk.LabelFrame( + self.control_frame, text="Network Interfaces", padding="5" + ) + self.interfaces_frame.grid(row=0, column=0, sticky="ew", pady=(0, 10)) + + ttk.Label(self.interfaces_frame, text="Select Interface:").grid( + row=0, column=0, sticky="w", padx=5 + ) self.if_var = tk.StringVar() - self.if_combo = ttk.Combobox(self.interfaces_frame, textvariable=self.if_var, state='readonly', width=50) - self.if_combo.grid(row=0, column=1, sticky='ew', padx=5) - self.if_combo.bind('<>', self.on_interface_selected) + self.if_combo = ttk.Combobox( + self.interfaces_frame, textvariable=self.if_var, state="readonly", width=50 + ) + self.if_combo.grid(row=0, column=1, sticky="ew", padx=5) + self.if_combo.bind("<>", self.on_interface_selected) # Info de configuración actual - self.current_frame = ttk.LabelFrame(self.control_frame, text="Current Configuration", padding="5") - self.current_frame.grid(row=1, column=0, sticky='ew', pady=(0,10)) - + self.current_frame = ttk.LabelFrame( + self.control_frame, text="Current Configuration", padding="5" + ) + self.current_frame.grid(row=1, column=0, sticky="ew", pady=(0, 10)) + self.current_ip_var = tk.StringVar() self.current_mask_var = tk.StringVar() self.current_gateway_var = tk.StringVar() self.dhcp_status_var = tk.StringVar() - - ttk.Label(self.current_frame, text="Current IP:").grid(row=0, column=0, sticky='w', padx=5) - ttk.Label(self.current_frame, textvariable=self.current_ip_var).grid(row=0, column=1, sticky='w') - ttk.Label(self.current_frame, text="Subnet Mask:").grid(row=1, column=0, sticky='w', padx=5) - ttk.Label(self.current_frame, textvariable=self.current_mask_var).grid(row=1, column=1, sticky='w') - ttk.Label(self.current_frame, text="Gateway:").grid(row=2, column=0, sticky='w', padx=5) - ttk.Label(self.current_frame, textvariable=self.current_gateway_var).grid(row=2, column=1, sticky='w') - ttk.Label(self.current_frame, text="DHCP Status:").grid(row=3, column=0, sticky='w', padx=5) - ttk.Label(self.current_frame, textvariable=self.dhcp_status_var).grid(row=3, column=1, sticky='w') + + ttk.Label(self.current_frame, text="Current IP:").grid( + row=0, column=0, sticky="w", padx=5 + ) + ttk.Label(self.current_frame, textvariable=self.current_ip_var).grid( + row=0, column=1, sticky="w" + ) + ttk.Label(self.current_frame, text="Subnet Mask:").grid( + row=1, column=0, sticky="w", padx=5 + ) + ttk.Label(self.current_frame, textvariable=self.current_mask_var).grid( + row=1, column=1, sticky="w" + ) + ttk.Label(self.current_frame, text="Gateway:").grid( + row=2, column=0, sticky="w", padx=5 + ) + ttk.Label(self.current_frame, textvariable=self.current_gateway_var).grid( + row=2, column=1, sticky="w" + ) + ttk.Label(self.current_frame, text="DHCP Status:").grid( + row=3, column=0, sticky="w", padx=5 + ) + ttk.Label(self.current_frame, textvariable=self.dhcp_status_var).grid( + row=3, column=1, sticky="w" + ) # Sección de configuración IP - self.ip_frame = ttk.LabelFrame(self.control_frame, text="IP Configuration", padding="5") - self.ip_frame.grid(row=2, column=0, sticky='ew', pady=(0,10)) - - ttk.Label(self.ip_frame, text="IP Prefix (first 3 octets):").grid(row=0, column=0, sticky='w', padx=5) + self.ip_frame = ttk.LabelFrame( + self.control_frame, text="IP Configuration", padding="5" + ) + self.ip_frame.grid(row=2, column=0, sticky="ew", pady=(0, 10)) + + ttk.Label(self.ip_frame, text="IP Prefix (first 3 octets):").grid( + row=0, column=0, sticky="w", padx=5 + ) self.ip_prefix = tk.StringVar(value=self.config.last_ip_prefix) self.ip_entry = ttk.Entry(self.ip_frame, textvariable=self.ip_prefix, width=30) - self.ip_entry.grid(row=0, column=1, sticky='w', padx=5) - - ttk.Label(self.ip_frame, text="Last Octet:").grid(row=0, column=2, sticky='w', padx=5) + self.ip_entry.grid(row=0, column=1, sticky="w", padx=5) + + ttk.Label(self.ip_frame, text="Last Octet:").grid( + row=0, column=2, sticky="w", padx=5 + ) self.last_octet = tk.StringVar(value="249") - self.last_octet_entry = ttk.Entry(self.ip_frame, textvariable=self.last_octet, width=5) - self.last_octet_entry.grid(row=0, column=3, sticky='w', padx=5) + self.last_octet_entry = ttk.Entry( + self.ip_frame, textvariable=self.last_octet, width=5 + ) + self.last_octet_entry.grid(row=0, column=3, sticky="w", padx=5) + + # Add subnet mask configuration + ttk.Label(self.ip_frame, text="Subnet Mask:").grid( + row=1, column=0, sticky="w", padx=5 + ) + self.subnet_mask = tk.StringVar(value="255.255.255.0") + self.subnet_entry = ttk.Entry( + self.ip_frame, textvariable=self.subnet_mask, width=15 + ) + self.subnet_entry.grid(row=1, column=1, sticky="w", padx=5) + + ttk.Label(self.ip_frame, text="CIDR (/bits):").grid( + row=1, column=2, sticky="w", padx=5 + ) + self.cidr_bits = tk.StringVar(value="24") + self.cidr_entry = ttk.Entry( + self.ip_frame, textvariable=self.cidr_bits, width=5 + ) + self.cidr_entry.grid(row=1, column=3, sticky="w", padx=5) + + # Don't add traces here, we'll do it after widget creation is complete # Sección de historial - self.history_frame = ttk.LabelFrame(self.control_frame, text="IP History", padding="5") - self.history_frame.grid(row=3, column=0, sticky='ew', pady=(0,10)) - - ttk.Label(self.history_frame, text="Previous IPs:").grid(row=0, column=0, sticky='w', padx=5) + self.history_frame = ttk.LabelFrame( + self.control_frame, text="IP History", padding="5" + ) + self.history_frame.grid(row=3, column=0, sticky="ew", pady=(0, 10)) + + ttk.Label(self.history_frame, text="Previous IPs:").grid( + row=0, column=0, sticky="w", padx=5 + ) self.history_var = tk.StringVar() self.history_combo = ttk.Combobox( self.history_frame, textvariable=self.history_var, - state='readonly', - width=50 + state="readonly", + width=50, ) - self.history_combo.grid(row=0, column=1, sticky='ew', padx=5) - self.history_combo.bind('<>', self.on_history_selected) + self.history_combo.grid(row=0, column=1, sticky="ew", padx=5) + self.history_combo.bind("<>", self.on_history_selected) # Botones de acción self.button_frame = ttk.Frame(self.control_frame) - self.button_frame.grid(row=4, column=0, sticky='ew', pady=(0,10)) - - ttk.Button(self.button_frame, text="Set Static IP", command=self.set_static_ip).grid(row=0, column=0, padx=5) - ttk.Button(self.button_frame, text="Enable DHCP", command=self.set_dhcp).grid(row=0, column=1, padx=5) - ttk.Button(self.button_frame, text="Refresh Interfaces", command=self.refresh_interfaces).grid(row=0, column=2, padx=5) + self.button_frame.grid(row=4, column=0, sticky="ew", pady=(0, 10)) + + ttk.Button( + self.button_frame, text="Set Static IP", command=self.set_static_ip + ).grid(row=0, column=0, padx=5) + ttk.Button(self.button_frame, text="Enable DHCP", command=self.set_dhcp).grid( + row=0, column=1, padx=5 + ) + ttk.Button( + self.button_frame, text="Restore Previous", command=self.restore_previous + ).grid(row=0, column=2, padx=5) + ttk.Button( + self.button_frame, + text="Refresh Interfaces", + command=self.refresh_interfaces, + ).grid(row=0, column=3, padx=5) + + # Sección de Ping - add initial value and bind event + self.ping_frame = ttk.LabelFrame( + self.control_frame, text="Network Tools", padding="5" + ) + self.ping_frame.grid(row=5, column=0, sticky="ew", pady=(0, 10)) + + ttk.Label(self.ping_frame, text="Target IP/Host:").grid( + row=0, column=0, sticky="w", padx=5 + ) + self.ping_target = tk.StringVar() + self.ping_entry = ttk.Entry( + self.ping_frame, textvariable=self.ping_target, width=40 + ) + self.ping_entry.grid(row=0, column=1, sticky="w", padx=5) + # Bind the FocusOut event to save the ping target + self.ping_entry.bind("", self.save_current_ping_target) + ttk.Button(self.ping_frame, text="Ping", command=self.do_ping).grid( + row=0, column=2, padx=5 + ) # Log en la parte inferior self.log_frame = ttk.LabelFrame(self.master, text="Operation Log", padding="5") - self.log_frame.grid(row=1, column=0, sticky='nsew', padx=10, pady=(0,10)) - + self.log_frame.grid(row=1, column=0, sticky="nsew", padx=10, pady=(0, 10)) + self.log_text = tk.Text(self.log_frame, wrap="none", height=10) - self.log_scrollbar_y = ttk.Scrollbar(self.log_frame, orient="vertical", command=self.log_text.yview) - self.log_scrollbar_x = ttk.Scrollbar(self.log_frame, orient="horizontal", command=self.log_text.xview) - + self.log_scrollbar_y = ttk.Scrollbar( + self.log_frame, orient="vertical", command=self.log_text.yview + ) + self.log_scrollbar_x = ttk.Scrollbar( + self.log_frame, orient="horizontal", command=self.log_text.xview + ) + self.log_text.configure( yscrollcommand=self.log_scrollbar_y.set, - xscrollcommand=self.log_scrollbar_x.set + xscrollcommand=self.log_scrollbar_x.set, ) - - self.log_text.grid(row=0, column=0, sticky='nsew') - self.log_scrollbar_y.grid(row=0, column=1, sticky='ns') - self.log_scrollbar_x.grid(row=1, column=0, sticky='ew') - - ttk.Button(self.log_frame, text="Clear Log", command=self.clear_log).grid(row=2, column=0, columnspan=2, pady=5) - + + self.log_text.grid(row=0, column=0, sticky="nsew") + self.log_scrollbar_y.grid(row=0, column=1, sticky="ns") + self.log_scrollbar_x.grid(row=1, column=0, sticky="ew") + + ttk.Button(self.log_frame, text="Clear Log", command=self.clear_log).grid( + row=2, column=0, columnspan=2, pady=5 + ) + self.log_frame.grid_columnconfigure(0, weight=1) self.log_frame.grid_rowconfigure(0, weight=1) def clear_log(self): - self.log_text.delete('1.0', tk.END) + self.log_text.delete("1.0", tk.END) def log_message(self, message: str): self.log_text.insert(tk.END, f"{message}\n") @@ -205,7 +399,7 @@ class IPChangerApp: def load_ip_history(self): try: if os.path.exists(self.config.history_file): - with open(self.config.history_file, 'r') as f: + with open(self.config.history_file, "r") as f: self.ip_history = json.load(f) else: self.ip_history = [] @@ -215,7 +409,7 @@ class IPChangerApp: def save_ip_history(self): try: - with open(self.config.history_file, 'w') as f: + with open(self.config.history_file, "w") as f: json.dump(self.ip_history, f) except Exception as e: self.log_message(f"Error saving IP history: {e}") @@ -228,7 +422,7 @@ class IPChangerApp: self.update_history_display() def update_history_display(self): - self.history_combo['values'] = self.ip_history + self.history_combo["values"] = self.ip_history if self.ip_history: self.history_combo.set(self.ip_history[0]) @@ -236,13 +430,16 @@ class IPChangerApp: selected_ip = self.history_var.get() if selected_ip: # Ensure we only get the first three octets - ip_parts = selected_ip.split('.') + ip_parts = selected_ip.split(".") if len(ip_parts) >= 3: - ip_prefix = '.'.join(ip_parts[:3]) + ip_prefix = ".".join(ip_parts[:3]) self.ip_prefix.set(ip_prefix) self.config.last_ip_prefix = ip_prefix self.config.save_config() self.log_message(f"Selected IP prefix from history: {ip_prefix}") + + # Update ping target for the new IP prefix + self.update_ping_target(ip_prefix) def refresh_interfaces(self): self.interfaces.clear() @@ -251,17 +448,19 @@ class IPChangerApp: self._refresh_interfaces_wmi() else: self._refresh_interfaces_netsh() - + # Actualizar combobox - self.if_combo['values'] = [inf.name for inf in self.interfaces if inf.active] - + self.if_combo["values"] = [ + inf.name for inf in self.interfaces if inf.active + ] + # Seleccionar última interfaz usada si está disponible - if self.config.last_interface in self.if_combo['values']: + if self.config.last_interface in self.if_combo["values"]: self.if_combo.set(self.config.last_interface) self.on_interface_selected() - + self.log_message("Interfaces refreshed successfully") - + except Exception as e: self.log_message(f"Error refreshing interfaces: {str(e)}") self.show_error(f"Error refreshing interfaces: {str(e)}") @@ -270,20 +469,31 @@ class IPChangerApp: try: adapters = self.wmi_client.Win32_NetworkAdapter(PhysicalAdapter=True) configs = self.wmi_client.Win32_NetworkAdapterConfiguration() - + for adapter in adapters: - config = next((cfg for cfg in configs if cfg.InterfaceIndex == adapter.InterfaceIndex), None) - + config = next( + ( + cfg + for cfg in configs + if cfg.InterfaceIndex == adapter.InterfaceIndex + ), + None, + ) + if config and config.IPEnabled: interface = NetworkInterface( name=adapter.NetConnectionID, description=adapter.Description, ip_address=config.IPAddress[0] if config.IPAddress else "", subnet_mask=config.IPSubnet[0] if config.IPSubnet else "", - gateway=config.DefaultIPGateway[0] if config.DefaultIPGateway else "", + gateway=( + config.DefaultIPGateway[0] + if config.DefaultIPGateway + else "" + ), dhcp_enabled=config.DHCPEnabled, active=adapter.NetEnabled, - adapter_id=adapter.GUID + adapter_id=adapter.GUID, ) self.interfaces.append(interface) except Exception as e: @@ -291,35 +501,43 @@ class IPChangerApp: def _refresh_interfaces_netsh(self): try: - output = subprocess.check_output("netsh interface ipv4 show config", shell=True, text=True) + output = subprocess.check_output( + "netsh interface ipv4 show config", shell=True, text=True + ) configs = output.split("\n\n") - + for config in configs: if not config.strip(): continue - + name_match = re.search(r"Configuración de\s+\"(.+?)\"", config) if not name_match: continue - + name = name_match.group(1) - + ip_match = re.search(r"Dirección IP:\s+(\d+\.\d+\.\d+\.\d+)", config) - mask_match = re.search(r"Máscara de subred:\s+(\d+\.\d+\.\d+\.\d+)", config) - gateway_match = re.search(r"Puerta de enlace predeterminada:\s+(\d+\.\d+\.\d+\.\d+)", config) + mask_match = re.search( + r"Máscara de subred:\s+(\d+\.\d+\.\d+\.\d+)", config + ) + gateway_match = re.search( + r"Puerta de enlace predeterminada:\s+(\d+\.\d+\.\d+\.\d+)", config + ) dhcp_match = re.search(r"DHCP habilitado:\s+(\w+)", config) - + interface = NetworkInterface( name=name, description=name, ip_address=ip_match.group(1) if ip_match else "", subnet_mask=mask_match.group(1) if mask_match else "", gateway=gateway_match.group(1) if gateway_match else "", - dhcp_enabled=dhcp_match.group(1).lower() == "sí" if dhcp_match else True, - active=True + dhcp_enabled=( + dhcp_match.group(1).lower() == "sí" if dhcp_match else True + ), + active=True, ) self.interfaces.append(interface) - + except subprocess.CalledProcessError as e: raise Exception(f"Error executing netsh: {str(e)}") except Exception as e: @@ -328,37 +546,47 @@ class IPChangerApp: def on_interface_selected(self, event=None): selected = self.if_var.get() interface = next((inf for inf in self.interfaces if inf.name == selected), None) - + if interface: self.current_ip_var.set(interface.ip_address) self.current_mask_var.set(interface.subnet_mask) self.current_gateway_var.set(interface.gateway) - self.dhcp_status_var.set("Enabled" if interface.dhcp_enabled else "Disabled") - + self.dhcp_status_var.set( + "Enabled" if interface.dhcp_enabled else "Disabled" + ) + # Actualizar IP prefix si hay una IP válida if interface.ip_address: - prefix = '.'.join(interface.ip_address.split('.')[:3]) + prefix = ".".join(interface.ip_address.split(".")[:3]) self.ip_prefix.set(prefix) - + + # Update ping target for the new IP prefix + self.update_ping_target(prefix) + # Guardar interfaz seleccionada self.config.last_interface = selected self.config.save_config() - + self.log_message(f"Selected interface: {selected}") def validate_ip_input(self) -> tuple[bool, str]: try: prefix = self.ip_prefix.get().strip() last_octet = self.last_octet.get().strip() + subnet_mask = self.subnet_mask.get().strip() # Validar formato del prefijo - if not re.match(r'^\d{1,3}\.\d{1,3}\.\d{1,3}', prefix): + if not re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}", prefix): return False, "IP prefix must be in format: xxx.xxx.xxx" # Validar último octeto if not last_octet.isdigit() or not 0 <= int(last_octet) <= 255: return False, "Last octet must be between 0 and 255" + # Validar máscara de subred + if not self.is_valid_subnet_mask(subnet_mask): + return False, "Invalid subnet mask format" + # Construir y validar IP completa ip = f"{prefix}.{last_octet}" ipaddress.IPv4Address(ip) @@ -368,7 +596,9 @@ class IPChangerApp: except ValueError as e: return False, f"Invalid IP address: {str(e)}" - def execute_admin_script(self, interface: str, mode: str, debug: bool = True) -> bool: + def execute_admin_script( + self, interface: str, mode: str, debug: bool = True, subnet_mask: str = "255.255.255.0" + ) -> bool: """ Ejecuta el script administrativo con los argumentos proporcionados mode puede ser una IP o 'dhcp' @@ -377,38 +607,49 @@ class IPChangerApp: current_dir = os.path.dirname(os.path.abspath(__file__)) script_path = os.path.join(current_dir, "ip-changer-admin.py") python_exe = sys.executable - + # Verificar que el script existe if not os.path.exists(script_path): error_msg = f"Admin script not found at: {script_path}" self.log_message(error_msg) return False - - # Construir la línea de argumentos - args = f'"{script_path}" "{interface}" "{mode}"' - + + # Construir la línea de argumentos - pass both IP and subnet as a single parameter + if mode != "dhcp": + # For static IP, create the netsh command with subnet mask + gateway = f"{self.get_ip_prefix(mode)}.1" # Use the first three octets of the IP + netsh_cmd = f'netsh interface ip set address "{interface}" static {mode} {subnet_mask} {gateway}' + args = f'"{script_path}" "{interface}" "{netsh_cmd}"' + else: + # For DHCP, keep it simple + args = f'"{script_path}" "{interface}" "dhcp"' + if debug: self.log_message("Debug Information:") self.log_message(f"Current Directory: {current_dir}") self.log_message(f"Script Path: {script_path}") self.log_message(f"Python Executable: {python_exe}") self.log_message(f"Full Command: {python_exe} {args}") - + try: - self.log_message(f"Attempting to execute admin script with elevated privileges") - - ret = ctypes.windll.shell32.ShellExecuteW( - None, # hwnd - "runas", # operation - python_exe, # file - args, # parameters - current_dir, # directory - 1 # show command + self.log_message( + f"Attempting to execute admin script with elevated privileges" ) - + + ret = ctypes.windll.shell32.ShellExecuteW( + None, # hwnd + "runas", # operation + python_exe, # file + args, # parameters + current_dir, # directory + 1, # show command + ) + result_code = int(ret) if result_code > 32: - self.log_message(f"ShellExecuteW successful (return code: {result_code})") + self.log_message( + f"ShellExecuteW successful (return code: {result_code})" + ) return True else: error_codes = { @@ -424,22 +665,238 @@ class IPChangerApp: 29: "The DDE operation failed", 30: "The DDE operation is busy", 31: "The file name association is unavailable", - 32: "No application is associated with the file" + 32: "No application is associated with the file", } - self.log_message(f"Failed to execute admin script. Error code {result_code}: {error_msg}") + self.log_message( + f"Failed to execute admin script. Error code {result_code}: {error_msg}" + ) return False - + except Exception as e: self.log_message(f"Exception while executing admin script: {str(e)}") return False + def save_previous_config(self, interface_name: str): + """Guarda la configuración actual de la interfaz antes de cambiarla""" + interface = next( + (inf for inf in self.interfaces if inf.name == interface_name), None + ) + if interface: + self.config.previous_config = { + "name": interface.name, + "ip_address": interface.ip_address, + "subnet_mask": interface.subnet_mask, + "gateway": interface.gateway, + "dhcp_enabled": interface.dhcp_enabled, + } + self.config.save_config() + self.log_message(f"Previous configuration saved for {interface.name}") + + def restore_previous(self): + """Restaura la configuración IP anterior""" + prev_config = self.config.previous_config + if not prev_config: + self.show_error("No previous configuration available") + return + + interface_name = prev_config.get("name") + if not interface_name: + self.show_error("Invalid previous configuration") + return + + # Verificar que la interfaz existe + if interface_name not in [inf.name for inf in self.interfaces]: + self.show_error(f"Interface {interface_name} not found") + return + + self.log_message(f"Restoring previous configuration for {interface_name}") + + if prev_config.get("dhcp_enabled", True): + # Si la configuración anterior era DHCP + if self.execute_admin_script(interface_name, "dhcp"): + self.log_message(f"Successfully restored DHCP on {interface_name}") + time.sleep(2) + self.refresh_interfaces() + else: + self.show_error("Failed to restore DHCP configuration") + else: + # Si la configuración anterior era IP estática + ip = prev_config.get("ip_address") + if not ip: + self.show_error("Previous IP address not available") + return + + if self.execute_admin_script(interface_name, ip): + self.log_message(f"Successfully restored IP {ip} on {interface_name}") + time.sleep(2) + self.refresh_interfaces() + else: + self.show_error("Failed to restore static IP configuration") + + def do_ping(self): + """Realiza un ping a la dirección especificada usando una biblioteca de Python""" + target = self.ping_target.get().strip() + if not target: + self.show_error("Please enter a target IP or hostname") + return + + # Save the ping target when used + self.save_current_ping_target() + + self.log_message(f"Pinging {target}...") + + # Crear un hilo para el ping para no bloquear la interfaz + threading.Thread(target=self._execute_ping, args=(target,), daemon=True).start() + + def _execute_ping(self, target: str): + """Ejecuta el ping usando bibliotecas de Python en lugar del comando del sistema""" + try: + if PING_LIBRARY_AVAILABLE: + # Usar PythonPing que es multiplataforma y no requiere privilegios + self.log_message( + f"Sending 4 ICMP echo requests to {target} using PythonPing..." + ) + + # Ejecutar el ping + response = pythonping(target, count=4, timeout=1) + + # Comprobar si el ping tuvo éxito + if response.success(): + min_rtt = min(r.time_elapsed_ms for r in response) + max_rtt = max(r.time_elapsed_ms for r in response) + avg_rtt = sum(r.time_elapsed_ms for r in response) / len(response) + + # Mostrar resultados individuales + for i, reply in enumerate(response): + self.log_message( + f"Reply from {target}: time={reply.time_elapsed_ms:.2f}ms" + ) + + # Resumen + success_count = len([r for r in response if r.success]) + self.log_message(f"Ping statistics for {target}:") + self.log_message( + f" Packets: Sent = 4, Received = {success_count}, Lost = {4 - success_count}" + ) + self.log_message(f"Approximate round trip times in milliseconds:") + self.log_message( + f" Minimum = {min_rtt:.2f}ms, Maximum = {max_rtt:.2f}ms, Average = {avg_rtt:.2f}ms" + ) + else: + self.log_message(f"Could not reach host {target}") + else: + # Usar socket para hacer un ping básico (solo comprueba si el host está activo) + self.log_message( + f"PythonPing not available, checking if host {target} is reachable..." + ) + self.log_message( + "Note: This is not a true ICMP ping, just a TCP connection test" + ) + + for port in [80, 443, 22, 21]: + try: + start_time = time.time() + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(1) + result = s.connect_ex((target, port)) + elapsed_time = (time.time() - start_time) * 1000 # ms + s.close() + + if result == 0: + self.log_message( + f"Port {port} on {target} is open (TCP connect time: {elapsed_time:.2f}ms)" + ) + else: + self.log_message( + f"Port {port} on {target} is not responding" + ) + except socket.gaierror: + self.log_message(f"Could not resolve hostname {target}") + break + except socket.error as e: + self.log_message(f"Error connecting to {target}: {str(e)}") + except AttributeError as e: + # Handle specifically the attribute error + self.log_message(f"Error with PythonPing API: {str(e)}") + + # Fallback to alternative ping implementation + self.log_message("Falling back to socket-based connectivity test...") + + # Implement a simple socket-based ping + success = False + try: + # Try to connect to the host - this just checks if it's reachable + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(2) + start_time = time.time() + s.connect((target, 80)) # Try to connect to port 80 + response_time = (time.time() - start_time) * 1000 + s.close() + self.log_message( + f"Host {target} is reachable. Response time: {response_time:.2f}ms" + ) + success = True + except socket.error as se: + self.log_message(f"Could not connect to {target}: {str(se)}") + + # Try to at least resolve the hostname + try: + ip = socket.gethostbyname(target) + self.log_message( + f"Hostname {target} resolves to {ip}, but connection failed" + ) + except socket.gaierror: + self.log_message(f"Could not resolve hostname {target}") + + # Alternative implementation using subprocess directly as last resort + try: + self.log_message("Attempting to ping using system command...") + # Use subprocess to run ping directly (won't work if command not available) + use_count = "-n" if sys.platform.lower() == "win32" else "-c" + cmd = ["ping", use_count, "4", target] + process = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + stdout, stderr = process.communicate() + + if process.returncode == 0: + # Process and display output + output = stdout.decode("utf-8", errors="replace") + for line in output.splitlines(): + if line.strip(): + self.log_message(line) + else: + self.log_message( + f"System ping command failed: {stderr.decode('utf-8', errors='replace')}" + ) + except Exception as subproc_e: + self.log_message(f"System ping command error: {str(subproc_e)}") + + except Exception as e: + self.log_message(f"Error executing ping: {str(e)}") + self.log_message( + "PythonPing may need to be reinstalled or updated. Run: pip install -U pythonping" + ) + + # Try to give more information + try: + import pkg_resources + + version = pkg_resources.get_distribution("pythonping").version + self.log_message(f"Installed PythonPing version: {version}") + except Exception: + self.log_message("Could not determine PythonPing version") + def set_static_ip(self): interface = self.if_var.get() if not interface: self.show_error("Please select a network interface") return + # Guardar configuración actual antes de cambiarla + self.save_previous_config(interface) + # Validar IP is_valid, result = self.validate_ip_input() if not is_valid: @@ -447,15 +904,21 @@ class IPChangerApp: return ip = result + subnet_mask = self.subnet_mask.get() # Guardar IP actual en el historial self.add_to_history(self.ip_prefix.get()) - # Ejecutar script con privilegios - if self.execute_admin_script(interface, ip, debug=True): + # Log the actual command we'll be executing + gateway = f"{self.ip_prefix.get()}.1" + command = f'netsh interface ip set address "{interface}" static {ip} {subnet_mask} {gateway}' + self.log_message(f"Executing network command: {command}") + + # Ejecutar script con privilegios - pass subnet mask + if self.execute_admin_script(interface, ip, debug=True, subnet_mask=subnet_mask): time.sleep(2) # Esperar a que se apliquen los cambios self.refresh_interfaces() - self.log_message(f"Successfully set static IP {ip} on {interface}") + self.log_message(f"Successfully set static IP {ip} with mask {subnet_mask} on {interface}") else: self.show_error("Failed to set static IP. Check the log for details.") @@ -464,7 +927,10 @@ class IPChangerApp: if not interface: self.show_error("Please select a network interface") return - + + # Guardar configuración actual antes de cambiarla + self.save_previous_config(interface) + # Ejecutar script con privilegios if self.execute_admin_script(interface, "dhcp", debug=True): time.sleep(2) # Esperar a que se apliquen los cambios @@ -481,29 +947,29 @@ class IPChangerApp: shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - universal_newlines=True + universal_newlines=True, ) - + while True: output = process.stdout.readline() - if output == '' and process.poll() is not None: + if output == "" and process.poll() is not None: break if output: self.log_message(output.strip()) - + retcode = process.poll() - + error_output = process.stderr.read() if error_output: self.log_message(f"Error output:\n{error_output}") - + if retcode == 0: self.log_message("Command executed successfully") return True else: self.log_message(f"Command failed with return code: {retcode}") return False - + except Exception as e: self.log_message(f"Error executing command: {str(e)}") return False @@ -516,18 +982,15 @@ class IPChangerApp: return False def elevate_privileges(self): - if sys.platform == 'win32': - self.show_info("The application needs administrator privileges to change network settings. " - "Please confirm the UAC prompt.") + if sys.platform == "win32": + self.show_info( + "The application needs administrator privileges to change network settings. " + "Please confirm the UAC prompt." + ) script_path = os.path.abspath(sys.argv[0]) try: ctypes.windll.shell32.ShellExecuteW( - None, - "runas", - sys.executable, - f'"{script_path}"', - None, - 1 + None, "runas", sys.executable, f'"{script_path}"', None, 1 ) self.master.quit() except Exception as e: @@ -542,10 +1005,117 @@ class IPChangerApp: self.log_message(f"INFO: {message}") messagebox.showinfo("Information", message) + # Add new methods for subnet mask conversion + def on_subnet_mask_changed(self, *args): + """Update CIDR bits when subnet mask changes""" + # Use a static variable to prevent recursion + if hasattr(self, "_updating_mask") and self._updating_mask: + return + + try: + self._updating_mask = True + + mask = self.subnet_mask.get() + if self.is_valid_subnet_mask(mask): + # Convert mask to bits + bits = self.subnet_mask_to_cidr(mask) + self.cidr_bits.set(str(bits)) + except Exception as e: + self.log_message(f"Error updating CIDR bits: {str(e)}") + finally: + self._updating_mask = False + + def on_cidr_bits_changed(self, *args): + """Update subnet mask when CIDR bits change""" + # Use a static variable to prevent recursion + if hasattr(self, "_updating_bits") and self._updating_bits: + return + + try: + self._updating_bits = True + + bits_str = self.cidr_bits.get() + if bits_str.isdigit(): + bits = int(bits_str) + if 0 <= bits <= 32: + # Convert bits to mask + mask = self.cidr_to_subnet_mask(bits) + self.subnet_mask.set(mask) + except Exception as e: + self.log_message(f"Error updating subnet mask: {str(e)}") + finally: + self._updating_bits = False + + def is_valid_subnet_mask(self, mask: str) -> bool: + """Validate if the string is a valid subnet mask""" + try: + # Check if it has the format x.x.x.x + parts = mask.split('.') + if len(parts) != 4: + return False + + # Each part should be a number between 0-255 + for part in parts: + if not part.isdigit() or not 0 <= int(part) <= 255: + return False + + # Check if it's a valid subnet mask pattern + # Convert to binary and ensure it's a continuous sequence of 1s followed by 0s + binary = ''.join([bin(int(octet))[2:].zfill(8) for octet in parts]) + if '01' in binary: # If there's a 0 followed by 1, it's not valid + return False + + return True + except Exception: + return False + + def subnet_mask_to_cidr(self, mask: str) -> int: + """Convert subnet mask to CIDR notation bits""" + try: + # Convert each octet to binary and count the number of 1s + parts = mask.split('.') + binary = ''.join([bin(int(octet))[2:].zfill(8) for octet in parts]) + return binary.count('1') + except Exception as e: + self.log_message(f"Error converting subnet mask to CIDR: {str(e)}") + return 24 # Default to /24 + + def cidr_to_subnet_mask(self, bits: int) -> str: + """Convert CIDR bits to subnet mask in dotted decimal notation""" + try: + # Create a binary string with the specified number of 1s followed by 0s + binary = '1' * bits + '0' * (32 - bits) + + # Split the binary string into 4 octets and convert each to decimal + octets = [binary[i:i+8] for i in range(0, 32, 8)] + decimals = [str(int(octet, 2)) for octet in octets] + + return '.'.join(decimals) + except Exception as e: + self.log_message(f"Error converting CIDR to subnet mask: {str(e)}") + return "255.255.255.0" # Default to 255.255.255.0 + + def update_ping_target(self, ip_prefix): + """Update the ping target field based on the selected IP prefix""" + if ip_prefix: + target = self.config.get_ping_target(ip_prefix) + self.ping_target.set(target) + + def save_current_ping_target(self, event=None): + """Save the current ping target for the current IP prefix""" + ip_prefix = self.ip_prefix.get() + target = self.ping_target.get() + + if ip_prefix and target: + self.config.set_ping_target(ip_prefix, target) + self.log_message(f"Saved ping target {target} for IP prefix {ip_prefix}") + + def main(): root = tk.Tk() app = IPChangerApp(root) root.mainloop() + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/ping_targets.json b/ping_targets.json new file mode 100644 index 0000000..3900d64 --- /dev/null +++ b/ping_targets.json @@ -0,0 +1 @@ +{"10.1.22": "10.1.20.11"} \ No newline at end of file