Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
129 changes: 105 additions & 24 deletions wrapanapi/systems/openstack.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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.
Expand All @@ -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

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand All @@ -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",
)
Expand All @@ -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:
Expand All @@ -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)
Expand Down