diff --git a/pyproject.toml b/pyproject.toml index 7e614f3e..f725b02d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,8 +64,9 @@ dependencies = [ "python-heatclient", "python-ironicclient", "python-keystoneclient", - "python-neutronclient==6.12.0", - "python-novaclient==7.1.2", + "python-neutronclient==10.0.0", + "python-novaclient==18.10.0", + "openstacksdk", "python-swiftclient", "pyvcloud==23.0.4", "pyvmomi>=6.5.0.2017.5.post1", diff --git a/wrapanapi/systems/openstack.py b/wrapanapi/systems/openstack.py index 4bdc6d6d..79f81a41 100644 --- a/wrapanapi/systems/openstack.py +++ b/wrapanapi/systems/openstack.py @@ -11,6 +11,7 @@ from functools import partial from re import search +import openstack import pytz from cinderclient import exceptions as cinder_exceptions from cinderclient.v3 import client as cinderclient @@ -23,7 +24,6 @@ from novaclient import client as osclient from novaclient import exceptions as os_exceptions from novaclient.client import SessionClient -from novaclient.v2.floating_ips import FloatingIP from requests.exceptions import Timeout from swiftclient import client as swiftclient from swiftclient.exceptions import ClientException as SwiftException @@ -229,11 +229,13 @@ def assign_floating_ip(self, floating_ip_pool, safety_timer=5): if len(free_ips) > 1: # There are 2 and more ips, so we will take the first one (eldest) ip = free_ips[0] - self.logger.info("Reusing %s from pool %s", ip.ip, floating_ip_pool) + self.logger.info( + "Reusing %s from pool %s", ip.floating_ip_address, floating_ip_pool + ) else: # There is one or none, so create one. try: - ip = self._api.floating_ips.create(floating_ip_pool) + ip = self.system._create_floating_ip(floating_ip_pool) except allowed_exceptions as e: self.logger.error("Probably no more FIP slots available: %s", str(e)) free_ips = self.system.free_fips(floating_ip_pool) @@ -243,21 +245,24 @@ def assign_floating_ip(self, floating_ip_pool, safety_timer=5): ip = free_ips[0] self.logger.info( "Reused %s from pool %s because no more free spaces for new ips", - ip.ip, + ip.floating_ip_address, floating_ip_pool, ) else: # Nothing can be done raise NoMoreFloatingIPs(f"Provider {self.system.auth_url} ran out of FIPs") - self.logger.info("Created %s in pool %s", ip.ip, floating_ip_pool) - instance.add_floating_ip(ip) + self.logger.info("Created %s in pool %s", ip.floating_ip_address, floating_ip_pool) + # Use openstacksdk to associate floating IP with server + self.system.openstack_conn.compute.add_floating_ip_to_server( + instance, ip.floating_ip_address + ) # Now the grace period in which a FIP theft could happen time.sleep(safety_timer) - self.logger.info("Instance %s got a floating IP %s", self.name, ip.ip) - assert self.ip == ip.ip, "Current IP does not match reserved floating IP!" - return ip.ip + self.logger.info("Instance %s got a floating IP %s", self.name, ip.floating_ip_address) + assert self.ip == ip.floating_ip_address, "Current IP does not match reserved floating IP!" + return ip.floating_ip_address def unassign_floating_ip(self): """Disassociates the floating IP (if present) from VM. @@ -269,14 +274,20 @@ def unassign_floating_ip(self): ip_addr = self.ip if ip_addr is None: return None - floating_ips = self._api.floating_ips.findall(ip=ip_addr) + floating_ips = self.system._list_floating_ips(ip=ip_addr) if not floating_ips: return None floating_ip = floating_ips[0] self.logger.info( - "Detaching floating IP %s/%s from %s", floating_ip.id, floating_ip.ip, instance.name + "Detaching floating IP %s/%s from %s", + floating_ip.id, + floating_ip.floating_ip_address, + instance.name, + ) + # Use openstacksdk to disassociate floating IP from server + self.system.openstack_conn.compute.remove_floating_ip_from_server( + instance, floating_ip.floating_ip_address ) - instance.remove_floating_ip(floating_ip) wait_for(lambda: self.ip is None, delay=1, timeout="1m") return floating_ip @@ -629,6 +640,7 @@ def __init__(self, tenant, username, password, auth_url, **kwargs): self._sapi = None self._tenant_api = None self._stackapi = None + self._openstack_conn = None @property def _identifying_attrs(self): @@ -700,6 +712,28 @@ def napi(self): self._napi = neutronclient.Client(session=self.session) return self._napi + @property + def openstack_conn(self): + """OpenStack SDK connection for unified API access.""" + if not self._openstack_conn: + auth_kwargs = dict( + auth_url=self.auth_url, + username=self.username, + password=self.password, + project_name=self.tenant, + ) + if self.keystone_version == 3: + auth_kwargs.update( + dict( + user_domain_id=self.domain_id, + user_domain_name=self.domain_name, + project_domain_id=self.domain_id, + project_domain_name=self.domain_name, + ) + ) + self._openstack_conn = openstack.connect(**auth_kwargs, verify=False) + return self._openstack_conn + @property def tenant_api(self): if not self._tenant_api: @@ -736,6 +770,48 @@ def stackapi(self): def info(self): return f"{self.api.client.service_type} {self.api.client.version}" + def _list_floating_ips(self, **filters): + """List floating IPs using openstacksdk with optional filters.""" + openstack_filters = {} + + if "ip" in filters: + openstack_filters["floating_ip_address"] = filters["ip"] + if "fixed_ip" in filters: + if filters["fixed_ip"] is not None: + openstack_filters["fixed_ip_address"] = filters["fixed_ip"] + if "pool" in filters and filters["pool"]: + # Pool in nova corresponds to network in openstack sdk + networks = list( + self.openstack_conn.network.networks(name=filters["pool"], is_router_external=True) + ) + if networks: + openstack_filters["floating_network_id"] = networks[0].id + + floating_ips = list(self.openstack_conn.network.floating_ips(**openstack_filters)) + + # Post-process for fixed_ip=None case (unassigned floating IPs) + if "fixed_ip" in filters and filters["fixed_ip"] is None: + floating_ips = [ + fip + for fip in floating_ips + if fip.fixed_ip_address is None or fip.fixed_ip_address == "" + ] + + return floating_ips + + def _create_floating_ip(self, pool_name): + """Create floating IP using openstacksdk.""" + # Find the external network by name + networks = list( + self.openstack_conn.network.networks(name=pool_name, is_router_external=True) + ) + if not networks: + raise Exception(f"External network '{pool_name}' not found") + + network_id = networks[0].id + floating_ip = self.openstack_conn.network.create_floating_ip(floating_network_id=network_id) + return floating_ip + def _get_tenants(self): if self.keystone_version == 3: return self.tenant_api.list() @@ -1067,13 +1143,15 @@ def volume_attachments(self, volume_id): def free_fips(self, pool): """Returns list of free floating IPs sorted by ip address.""" - return sorted(self.api.floating_ips.findall(fixed_ip=None, pool=pool), key=lambda ip: ip.ip) + return sorted( + self._list_floating_ips(fixed_ip=None, pool=pool), key=lambda ip: ip.floating_ip_address + ) def delete_floating_ip(self, floating_ip): """Deletes an existing FIP. Args: - floating_ip: FloatingIP object or an IP address of the FIP. + floating_ip: OpenStack SDK FloatingIP object or an IP address of the FIP. Returns: True if it deleted a FIP, False if it did not delete it, most probably because it @@ -1082,15 +1160,18 @@ def delete_floating_ip(self, floating_ip): if floating_ip is None: # To be able to chain with unassign_floating_ip, which can return None return False - if not isinstance(floating_ip, FloatingIP): - floating_ip = self.api.floating_ips.findall(ip=floating_ip) - if not floating_ip: + if isinstance(floating_ip, str): + # If floating_ip is a string (IP address), find the floating IP object + floating_ip_list = self._list_floating_ips(ip=floating_ip) + if not floating_ip_list: return False - floating_ip = floating_ip[0] - self.logger.info("Deleting floating IP %s/%s", floating_ip.id, floating_ip.ip) - floating_ip.delete() + floating_ip = floating_ip_list[0] + self.logger.info( + "Deleting floating IP %s/%s", floating_ip.id, floating_ip.floating_ip_address + ) + self.openstack_conn.network.delete_floating_ip(floating_ip) wait_for( - lambda: len(self.api.floating_ips.findall(ip=floating_ip.ip)) == 0, + lambda: len(self._list_floating_ips(ip=floating_ip.floating_ip_address)) == 0, delay=1, timeout="1m", ) @@ -1108,7 +1189,7 @@ def get_first_floating_ip(self, pool=None): pool_name = getattr(pool, "name", pool) # obj attr, or passed thing (string) otherwise fip = None try: - fip = self.api.floating_ips.create(pool_name) + fip = self._create_floating_ip(pool_name) except os_exceptions.NotFound: self.logger.exception("Exception while creating FIP for pool: %s", pool_name) else: @@ -1120,11 +1201,11 @@ def get_first_floating_ip(self, pool=None): pool_name, ) try: - fip = next(ip for ip in self.api.floating_ips.list() if ip.instance_id is None) + fip = next(ip for ip in self._list_floating_ips() if ip.fixed_ip_address is None) except StopIteration: self.logger.error("No more Floating IPs available") return None - return fip.ip + return fip.floating_ip_address def stack_exist(self, stack_name): stack = self.stackapi.stacks.get(stack_name)