Last active
January 16, 2026 03:19
-
-
Save robkooper/2a16b4a3923e036a3921c1a0198b1c16 to your computer and use it in GitHub Desktop.
openstack rebind
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| OpenStack Floating IP Rebinding Script | |
| This script removes and re-adds a floating IP to a VM while | |
| preserving the MAC address of the interface. | |
| Usage: | |
| python rebind_floating_ip.py --vm <vm_name> --floating-ip <ip> --network <network_name> [--dry-run] | |
| """ | |
| import argparse | |
| import sys | |
| import time | |
| from typing import Optional | |
| try: | |
| import openstack | |
| from openstack.exceptions import OpenStackCloudException | |
| except ImportError: | |
| print("Error: openstack SDK not installed. Install with: pip install openstacksdk") | |
| sys.exit(1) | |
| class FloatingIPManager: | |
| def __init__(self, dry_run: bool = False): | |
| self.dry_run = dry_run | |
| self.conn = None | |
| def connect(self): | |
| """Initialize OpenStack connection""" | |
| try: | |
| self.conn = openstack.connect() | |
| if self.dry_run: | |
| print("[DRY-RUN] Connected to OpenStack cloud") | |
| else: | |
| print("Connected to OpenStack cloud") | |
| except Exception as e: | |
| print(f"Error connecting to OpenStack: {e}") | |
| sys.exit(1) | |
| def get_server(self, vm_name: str): | |
| """Get server by name""" | |
| print(f"Looking for VM: {vm_name}") | |
| server = self.conn.compute.find_server(vm_name) | |
| if not server: | |
| print(f"Error: VM '{vm_name}' not found") | |
| sys.exit(1) | |
| print(f"Found VM: {server.name} (ID: {server.id})") | |
| return server | |
| def get_floating_ip(self, ip_address: str): | |
| """Get floating IP object""" | |
| print(f"Looking for floating IP: {ip_address}") | |
| floating_ip = None | |
| for fip in self.conn.network.ips(): | |
| if fip.floating_ip_address == ip_address: | |
| floating_ip = fip | |
| break | |
| if not floating_ip: | |
| print(f"Error: Floating IP '{ip_address}' not found") | |
| sys.exit(1) | |
| print(f"Found floating IP: {floating_ip.floating_ip_address} (ID: {floating_ip.id})") | |
| return floating_ip | |
| def get_network(self, network_name: str): | |
| """Get network by name""" | |
| print(f"Looking for network: {network_name}") | |
| network = self.conn.network.find_network(network_name) | |
| if not network: | |
| print(f"Error: Network '{network_name}' not found") | |
| sys.exit(1) | |
| print(f"Found network: {network.name} (ID: {network.id})") | |
| return network | |
| def find_port_for_floating_ip(self, server, floating_ip): | |
| """Find the port currently associated with the floating IP""" | |
| if floating_ip.port_id: | |
| port = self.conn.network.get_port(floating_ip.port_id) | |
| print(f"Floating IP is currently attached to port: {port.id}") | |
| print(f"Port MAC address: {port.mac_address}") | |
| if port.security_group_ids: | |
| print(f"Port security groups: {', '.join(port.security_group_ids)}") | |
| else: | |
| print("Port has no security groups") | |
| return port | |
| else: | |
| print("Floating IP is not currently attached to any port") | |
| return None | |
| def find_server_ports(self, server, network_id: str): | |
| """Find all ports for a server on a specific network""" | |
| ports = [] | |
| for port in self.conn.network.ports(device_id=server.id): | |
| if port.network_id == network_id: | |
| ports.append(port) | |
| return ports | |
| def auto_detect_floating_ip(self, server): | |
| """Auto-detect floating IP attached to the server""" | |
| print("\n=== Auto-detecting Floating IP ===") | |
| floating_ips = [] | |
| # Get all floating IPs and check if they're attached to this server | |
| for fip in self.conn.network.ips(): | |
| if fip.port_id: | |
| port = self.conn.network.get_port(fip.port_id) | |
| if port.device_id == server.id: | |
| floating_ips.append(fip) | |
| if len(floating_ips) == 0: | |
| print("Error: No floating IPs found attached to this VM") | |
| sys.exit(1) | |
| elif len(floating_ips) == 1: | |
| fip = floating_ips[0] | |
| print(f"Found floating IP: {fip.floating_ip_address}") | |
| return fip.floating_ip_address | |
| else: | |
| print(f"Found {len(floating_ips)} floating IPs attached to this VM:") | |
| for i, fip in enumerate(floating_ips, 1): | |
| print(f" {i}. {fip.floating_ip_address}") | |
| print("\nError: Multiple floating IPs found. Please specify which one with --floating-ip") | |
| sys.exit(1) | |
| def auto_detect_network(self, server, floating_ip_address: str): | |
| """Auto-detect network from the floating IP's port""" | |
| print("\n=== Auto-detecting Network ===") | |
| # Get the floating IP to find its port | |
| floating_ip = None | |
| for fip in self.conn.network.ips(): | |
| if fip.floating_ip_address == floating_ip_address: | |
| floating_ip = fip | |
| break | |
| if not floating_ip or not floating_ip.port_id: | |
| print("Error: Could not find port for floating IP") | |
| sys.exit(1) | |
| # Get the port to find its network | |
| port = self.conn.network.get_port(floating_ip.port_id) | |
| network = self.conn.network.get_network(port.network_id) | |
| print(f"Found network: {network.name}") | |
| return network.name | |
| def remove_floating_ip(self, floating_ip): | |
| """Step 1: Remove floating IP""" | |
| print("\n=== Step 1: Remove Floating IP ===") | |
| if floating_ip.port_id: | |
| if self.dry_run: | |
| print(f"[DRY-RUN] Would disassociate floating IP {floating_ip.floating_ip_address} from port {floating_ip.port_id}") | |
| else: | |
| print(f"Disassociating floating IP {floating_ip.floating_ip_address}...") | |
| self.conn.network.update_ip(floating_ip, port_id=None) | |
| print("Floating IP disassociated") | |
| time.sleep(2) | |
| else: | |
| print("Floating IP is already disassociated") | |
| def remove_interface(self, server, port): | |
| """Step 2: Remove interface from VM""" | |
| print("\n=== Step 2: Remove Interface from VM ===") | |
| if self.dry_run: | |
| print(f"[DRY-RUN] Would detach port {port.id} (MAC: {port.mac_address}) from server {server.name}") | |
| print(f"[DRY-RUN] Would delete port {port.id}") | |
| else: | |
| print(f"Detaching port {port.id} from server {server.name}...") | |
| self.conn.compute.delete_server_interface(port.id, server=server.id) | |
| print("Interface detached") | |
| time.sleep(3) | |
| # Delete the old port to free up the MAC address | |
| print(f"Deleting old port {port.id} to free MAC address...") | |
| self.conn.network.delete_port(port.id) | |
| print("Old port deleted") | |
| # Wait for port deletion to complete | |
| print("Waiting for port deletion to complete...") | |
| max_wait = 30 | |
| waited = 0 | |
| while waited < max_wait: | |
| try: | |
| self.conn.network.get_port(port.id) | |
| time.sleep(2) | |
| waited += 2 | |
| except Exception: | |
| # Port is gone | |
| break | |
| if waited >= max_wait: | |
| print("Warning: Port deletion taking longer than expected, but continuing...") | |
| else: | |
| print(f"Port deletion confirmed (waited {waited}s)") | |
| time.sleep(2) # Extra safety margin | |
| def add_interface(self, server, network, mac_address: str, security_group_ids: list): | |
| """Step 3: Add interface to VM with same MAC address and security groups""" | |
| print("\n=== Step 3: Add Interface to VM ===") | |
| if self.dry_run: | |
| print(f"[DRY-RUN] Would create new port on network {network.name} with MAC {mac_address}") | |
| if security_group_ids: | |
| print(f"[DRY-RUN] Would apply security groups: {', '.join(security_group_ids)}") | |
| print(f"[DRY-RUN] Would attach port to server {server.name}") | |
| # Return a dummy port for dry-run | |
| class DummyPort: | |
| def __init__(self, mac, sg_ids): | |
| self.id = "dry-run-port-id" | |
| self.mac_address = mac | |
| self.security_group_ids = sg_ids | |
| return DummyPort(mac_address, security_group_ids) | |
| else: | |
| # Create port with specific MAC address and security groups | |
| print(f"Creating new port on network {network.name} with MAC {mac_address}...") | |
| port_args = { | |
| 'network_id': network.id, | |
| 'mac_address': mac_address | |
| } | |
| if security_group_ids: | |
| port_args['security_group_ids'] = security_group_ids | |
| print(f"Applying security groups: {', '.join(security_group_ids)}") | |
| port = self.conn.network.create_port(**port_args) | |
| print(f"Created port: {port.id} (MAC: {port.mac_address})") | |
| # Attach port to server | |
| print(f"Attaching port to server {server.name}...") | |
| self.conn.compute.create_server_interface(server=server.id, port_id=port.id) | |
| print("Interface attached") | |
| time.sleep(3) | |
| return port | |
| def add_floating_ip(self, floating_ip, port): | |
| """Step 4: Add floating IP back""" | |
| print("\n=== Step 4: Add Floating IP Back ===") | |
| if self.dry_run: | |
| print(f"[DRY-RUN] Would associate floating IP {floating_ip.floating_ip_address} to port {port.id}") | |
| else: | |
| print(f"Associating floating IP {floating_ip.floating_ip_address} to port {port.id}...") | |
| self.conn.network.update_ip(floating_ip, port_id=port.id) | |
| print("Floating IP associated") | |
| time.sleep(2) | |
| def reboot_vm(self, server): | |
| """Step 5: Reboot VM""" | |
| print("\n=== Step 5: Reboot VM ===") | |
| if self.dry_run: | |
| print(f"[DRY-RUN] Would reboot server {server.name}") | |
| else: | |
| print(f"Rebooting server {server.name}...") | |
| self.conn.compute.reboot_server(server.id, reboot_type='SOFT') | |
| print("Reboot initiated") | |
| def rebind_floating_ip(self, vm_name: str, floating_ip_address: str, network_name: str): | |
| """Main workflow to rebind floating IP""" | |
| print("=" * 60) | |
| if self.dry_run: | |
| print("DRY-RUN MODE - No changes will be made") | |
| else: | |
| print("LIVE MODE - Changes will be applied") | |
| print("=" * 60) | |
| # Get resources | |
| server = self.get_server(vm_name) | |
| floating_ip = self.get_floating_ip(floating_ip_address) | |
| network = self.get_network(network_name) | |
| # Find current port and save MAC address | |
| current_port = self.find_port_for_floating_ip(server, floating_ip) | |
| if not current_port: | |
| print("Error: Cannot determine MAC address - floating IP not currently attached") | |
| sys.exit(1) | |
| mac_address = current_port.mac_address | |
| print(f"\nMAC address to preserve: {mac_address}") | |
| # Save security groups | |
| security_group_ids = current_port.security_group_ids if current_port.security_group_ids else [] | |
| if security_group_ids: | |
| print(f"Security groups to preserve: {', '.join(security_group_ids)}") | |
| else: | |
| print("No security groups to preserve") | |
| # Execute workflow | |
| self.remove_floating_ip(floating_ip) | |
| self.remove_interface(server, current_port) | |
| new_port = self.add_interface(server, network, mac_address, security_group_ids) | |
| self.add_floating_ip(floating_ip, new_port) | |
| self.reboot_vm(server) | |
| print("\n" + "=" * 60) | |
| if self.dry_run: | |
| print("DRY-RUN COMPLETE - No changes were made") | |
| else: | |
| print("OPERATION COMPLETE") | |
| print("=" * 60) | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description='Rebind OpenStack floating IP while preserving MAC address', | |
| formatter_class=argparse.RawDescriptionHelpFormatter, | |
| epilog=""" | |
| Examples: | |
| # Auto-detect floating IP and network from VM (simplest) | |
| python rebind_floating_ip.py --vm my-server --dry-run | |
| # Specify specific floating IP and network | |
| python rebind_floating_ip.py --vm my-server --floating-ip 203.0.113.10 --network private-net --dry-run | |
| # Live mode (makes actual changes) | |
| python rebind_floating_ip.py --vm my-server | |
| Note: Ensure your OpenStack credentials are configured via environment variables or clouds.yaml | |
| """ | |
| ) | |
| parser.add_argument('--vm', required=True, help='VM/Server name') | |
| parser.add_argument('--floating-ip', help='Floating IP address (auto-detected if not specified)') | |
| parser.add_argument('--network', help='Network name for the interface (auto-detected if not specified)') | |
| parser.add_argument('--dry-run', action='store_true', help='Show what would be done without making changes') | |
| args = parser.parse_args() | |
| # Create manager and execute | |
| manager = FloatingIPManager(dry_run=args.dry_run) | |
| manager.connect() | |
| # Auto-detect floating IP and network if not provided | |
| floating_ip_address = args.floating_ip | |
| network_name = args.network | |
| if not floating_ip_address or not network_name: | |
| server = manager.get_server(args.vm) | |
| if not floating_ip_address: | |
| floating_ip_address = manager.auto_detect_floating_ip(server) | |
| if not network_name: | |
| network_name = manager.auto_detect_network(server, floating_ip_address) | |
| manager.rebind_floating_ip(args.vm, floating_ip_address, network_name) | |
| if __name__ == '__main__': | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment