Merge "Replace neutronclient with openstacksdk"

This commit is contained in:
Zuul
2026-02-04 09:51:00 +00:00
committed by Gerrit Code Review
9 changed files with 350 additions and 59 deletions

View File

@@ -22,13 +22,14 @@ LOG = log.getLogger(__name__)
class FirewallPollster(base.BaseServicesPollster):
"""Pollster to capture firewalls status samples."""
"""Pollster to capture firewall group status samples (FWaaS v2)."""
FIELDS = ['admin_state_up',
'description',
'name',
'status',
'firewall_policy_id',
'ingress_firewall_policy_id',
'egress_firewall_policy_id',
]
@property
@@ -54,7 +55,7 @@ class FirewallPollster(base.BaseServicesPollster):
unit='firewall',
volume=status,
user_id=None,
project_id=fw['tenant_id'],
project_id=fw.get('project_id') or fw.get('tenant_id'),
resource_id=fw['id'],
resource_metadata=self.extract_metadata(fw)
)
@@ -87,7 +88,7 @@ class FirewallPolicyPollster(base.BaseServicesPollster):
unit='firewall_policy',
volume=1,
user_id=None,
project_id=fw['tenant_id'],
project_id=fw.get('project_id') or fw.get('tenant_id'),
resource_id=fw['id'],
resource_metadata=self.extract_metadata(fw)
)

View File

@@ -24,12 +24,15 @@ LOG = log.getLogger(__name__)
class VPNServicesPollster(base.BaseServicesPollster):
"""Pollster to capture VPN status samples."""
FIELDS = ['admin_state_up',
FIELDS = ['is_admin_state_up',
'description',
'name',
'status',
'subnet_id',
'router_id'
'router_id',
'flavor_id',
'external_v4_ip',
'external_v6_ip',
]
@property
@@ -55,7 +58,7 @@ class VPNServicesPollster(base.BaseServicesPollster):
unit='vpnservice',
volume=status,
user_id=None,
project_id=vpn['tenant_id'],
project_id=vpn.get('project_id') or vpn.get('tenant_id'),
resource_id=vpn['id'],
resource_metadata=self.extract_metadata(vpn)
)
@@ -69,16 +72,16 @@ class IPSecConnectionsPollster(base.BaseServicesPollster):
'peer_address',
'peer_id',
'peer_cidrs',
'peer_ep_group_id',
'psk',
'initiator',
'ikepolicy_id',
'dpd',
'ipsecpolicy_id',
'vpnservice_id',
'mtu',
'admin_state_up',
'is_admin_state_up',
'status',
'tenant_id'
'project_id'
]
@property
@@ -97,7 +100,7 @@ class IPSecConnectionsPollster(base.BaseServicesPollster):
unit='ipsec_site_connection',
volume=1,
user_id=None,
project_id=conn['tenant_id'],
project_id=conn.get('project_id') or conn.get('tenant_id'),
resource_id=conn['id'],
resource_metadata=self.extract_metadata(conn)
)

View File

@@ -14,8 +14,8 @@
import functools
from neutronclient.common import exceptions
from neutronclient.v2_0 import client as clientv20
from openstack import connection
from openstack import exceptions as os_exc
from oslo_config import cfg
from oslo_log import log
@@ -31,12 +31,13 @@ LOG = log.getLogger(__name__)
def logged(func):
"""Decorator to log exceptions from openstacksdk calls."""
@functools.wraps(func)
def with_logging(*args, **kwargs):
try:
return func(*args, **kwargs)
except exceptions.NeutronClientException as e:
except os_exc.HttpException as e:
if e.status_code == 404:
LOG.warning("The resource could not be found.")
else:
@@ -50,9 +51,13 @@ def logged(func):
class Client:
"""A client which gets information via python-neutronclient."""
"""A client which gets information via openstacksdk."""
def __init__(self, conf):
"""Initialize the Neutron client using openstacksdk.
:param conf: Oslo config object with service credentials.
"""
creds = conf.service_credentials
params = {
'session': keystone_client.get_session(conf),
@@ -60,29 +65,32 @@ class Client:
'region_name': creds.region_name,
'service_type': conf.service_types.neutron,
}
self.client = clientv20.Client(**params)
self.conn = connection.Connection(conf=conf, **params)
@logged
def vpn_get_all(self):
resp = self.client.list_vpnservices()
return resp.get('vpnservices')
"""Get all VPN services."""
return [vpn.to_dict() for vpn in self.conn.network.vpn_services()]
@logged
def ipsec_site_connections_get_all(self):
resp = self.client.list_ipsec_site_connections()
return resp.get('ipsec_site_connections')
"""Get all IPSec site connections."""
return [conn.to_dict()
for conn in self.conn.network.vpn_ipsec_site_connections()]
@logged
def firewall_get_all(self):
resp = self.client.list_firewalls()
return resp.get('firewalls')
"""Get all firewall groups (FWaaS v2)."""
return [fw.to_dict()
for fw in self.conn.network.firewall_groups()]
@logged
def fw_policy_get_all(self):
resp = self.client.list_firewall_policies()
return resp.get('firewall_policies')
"""Get all firewall policies."""
return [policy.to_dict()
for policy in self.conn.network.firewall_policies()]
@logged
def fip_get_all(self):
fips = self.client.list_floatingips()['floatingips']
return fips
"""Get all floating IPs."""
return [fip.to_dict() for fip in self.conn.network.ips()]

View File

@@ -41,8 +41,8 @@ def prepare_service(argv=None, config_files=None, conf=None):
keystone_client.register_keystoneauth_opts(conf)
log.register_options(conf)
log_levels = (conf.default_log_levels +
['futurist=INFO', 'neutronclient=INFO',
'keystoneclient=INFO'])
['futurist=INFO', 'keystoneclient=INFO',
'openstack=INFO'])
log.set_defaults(default_log_levels=log_levels)
conf(argv[1:], project='ceilometer', validate_default_values=True,

View File

@@ -0,0 +1,87 @@
# Copyright (C) 2026 Red Hat
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from openstack.network.v2 import firewall_group as sdk_firewall_group
from openstack.network.v2 import firewall_policy as sdk_firewall_policy
from openstack.network.v2 import floating_ip as sdk_floating_ip
from openstack.network.v2 import vpn_ipsec_site_connection as sdk_ipsec_conn
from openstack.network.v2 import vpn_service as sdk_vpn_service
class FakeSDKNetworkClient:
def ips(self):
FLOATING_IP_0 = sdk_floating_ip.FloatingIP(
connection=None,
id='fip-123',
floating_ip_address='192.168.1.100',
fixed_ip_address='10.0.0.5',
status='ACTIVE',
project_id='project-abc',
router_id='router-456'
)
return iter([FLOATING_IP_0])
def firewall_groups(self):
FIREWALL_GROUP_0 = sdk_firewall_group.FirewallGroup(
connection=None,
id='fw-123',
name='my-firewall',
status='ACTIVE',
project_id='project-abc',
ingress_firewall_policy_id='policy-1',
egress_firewall_policy_id='policy-2'
)
return iter([FIREWALL_GROUP_0])
def firewall_policies(self):
FIREWALL_POLICY_0 = sdk_firewall_policy.FirewallPolicy(
connection=None,
id='policy-123',
name='my-policy',
project_id='project-abc',
firewall_rules=['rule-1', 'rule-2']
)
return iter([FIREWALL_POLICY_0])
def vpn_ipsec_site_connections(self):
VPN_IPSEC_CONN_0 = sdk_ipsec_conn.VpnIPSecSiteConnection(
connection=None,
id='ipsec-123',
name='my-ipsec',
status='ACTIVE',
project_id='project-abc'
)
return iter([VPN_IPSEC_CONN_0])
def vpn_services(self):
VPN_SERVICE_0 = sdk_vpn_service.VpnService(
connection=None,
id='vpn-123',
name='my-vpn',
status='ACTIVE',
project_id='project-abc'
)
return iter([VPN_SERVICE_0])
class FakeConnection:
"""Fake connection object for testing."""
def __init__(self):
"""Initialize with a mock network attribute."""
self.network = FakeSDKNetworkClient()

View File

@@ -55,43 +55,58 @@ class TestFirewallPollster(_BaseTestFWPollster):
'description': '',
'admin_state_up': True,
'id': 'fdde3d818-fdcb-fg4b-de7f-6750dc8a9d7a',
'firewall_policy_id': 'bbe3d818-bdcb-4e4b-b47f-5650dc8a9d7a',
'tenant_id': 'a4eb9f4938bb418bbc4f8eb31802fefa'},
'ingress_firewall_policy_id':
'bbe3d818-bdcb-4e4b-b47f-5650dc8a9d7a',
'egress_firewall_policy_id':
'cce3d818-bdcb-4e4b-b47f-5650dc8a9d7b',
'project_id': 'a4eb9f4938bb418bbc4f8eb31802fefa'},
{'status': 'INACTIVE',
'name': 'myfw2',
'description': '',
'admin_state_up': True,
'id': 'e0d707dc-6194-4471-8286-0635bf65a055',
'firewall_policy_id': 'e0d707dc-6194-4471-8286-0635bf65a055',
'tenant_id': 'a4eb9f4938bb418bbc4f8eb31802fefa'},
'ingress_firewall_policy_id':
'e0d707dc-6194-4471-8286-0635bf65a055',
'egress_firewall_policy_id': None,
'project_id': 'a4eb9f4938bb418bbc4f8eb31802fefa'},
{'status': 'PENDING_CREATE',
'name': 'myfw3',
'description': '',
'admin_state_up': True,
'id': 'e538d353-31e9-4581-a511-0a487ff71d0d',
'firewall_policy_id': 'bbe3d818-bdcb-4e4b-b47f-5650dc8a9d7a',
'tenant_id': 'a4eb9f4938bb418bbc4f8eb31802fefa'},
'ingress_firewall_policy_id':
'bbe3d818-bdcb-4e4b-b47f-5650dc8a9d7a',
'egress_firewall_policy_id':
'dde3d818-bdcb-4e4b-b47f-5650dc8a9d7c',
'project_id': 'a4eb9f4938bb418bbc4f8eb31802fefa'},
{'status': 'ERROR',
'name': 'myfw4',
'description': '',
'admin_state_up': True,
'id': '06f698c4-dc63-43c4-a2d9-7b978e80f09a',
'firewall_policy_id': 'bef98f97-789f-418e-82ad-3e5d69618916',
'tenant_id': 'a4eb9f4938bb418bbc4f8eb31802fefa'},
'ingress_firewall_policy_id':
'bef98f97-789f-418e-82ad-3e5d69618916',
'egress_firewall_policy_id': None,
'project_id': 'a4eb9f4938bb418bbc4f8eb31802fefa'},
{'status': 'UNKNOWN',
'name': 'myfw5',
'description': '',
'admin_state_up': True,
'id': 'c65a1bec-ab59-44ce-b784-1c725f427998',
'firewall_policy_id': 'd45b975e-738f-42c3-a4b3-760d3a58ab51',
'tenant_id': 'a4eb9f4938bb418bbc4f8eb31802fefa'},
'ingress_firewall_policy_id':
'd45b975e-738f-42c3-a4b3-760d3a58ab51',
'egress_firewall_policy_id':
'e45b975e-738f-42c3-a4b3-760d3a58ab52',
'project_id': 'a4eb9f4938bb418bbc4f8eb31802fefa'},
{'status': None,
'name': 'myfw6',
'description': '',
'admin_state_up': True,
'id': 'ab5d19ff-32a8-49e5-aa2b-d008157359d9',
'firewall_policy_id': '79b9c933-2a7c-4f93-bbf9-d165f0326581',
'tenant_id': 'a4eb9f4938bb418bbc4f8eb31802fefa'},
'ingress_firewall_policy_id':
'79b9c933-2a7c-4f93-bbf9-d165f0326581',
'egress_firewall_policy_id': None,
'project_id': 'a4eb9f4938bb418bbc4f8eb31802fefa'},
]
def test_fw_get_samples(self):
@@ -150,7 +165,7 @@ class TestIPSecConnectionsPollster(_BaseTestFWPollster):
return [{'name': 'my_fw_policy',
'description': 'fw_policy',
'admin_state_up': True,
'tenant_id': 'abe3d818-fdcb-fg4b-de7f-6650dc8a9d7a',
'project_id': 'abe3d818-fdcb-fg4b-de7f-6650dc8a9d7a',
'firewall_rules': [{'enabled': True,
'action': 'allow',
'ip_version': 4,

View File

@@ -53,51 +53,75 @@ class TestVPNServicesPollster(_BaseTestVPNPollster):
return [{'status': 'ACTIVE',
'name': 'myvpn1',
'description': '',
'admin_state_up': True,
'is_admin_state_up': True,
'id': 'fdde3d818-fdcb-fg4b-de7f-6750dc8a9d7a',
'subnet_id': 'bbe3d818-bdcb-4e4b-b47f-5650dc8a9d7a',
'project_id': 'a4eb9f4938bb418bbc4f8eb31802fefa',
'tenant_id': 'a4eb9f4938bb418bbc4f8eb31802fefa',
'router_id': 'ade3d818-fdcb-fg4b-de7f-6750dc8a9d7a'},
'router_id': 'ade3d818-fdcb-fg4b-de7f-6750dc8a9d7a',
'flavor_id': None,
'external_v4_ip': '192.168.1.232',
'external_v6_ip': '2001:db8::158'},
{'status': 'INACTIVE',
'name': 'myvpn2',
'description': '',
'admin_state_up': True,
'is_admin_state_up': True,
'id': 'cdde3d818-fdcb-fg4b-de7f-6750dc8a9d7a',
'subnet_id': 'bbe3d818-bdcb-4e4b-b47f-5650dc8a9d7a',
'project_id': 'a4eb9f4938bb418bbc4f8eb31802fefa',
'tenant_id': 'a4eb9f4938bb418bbc4f8eb31802fefa',
'router_id': 'ade3d818-fdcb-fg4b-de7f-6750dc8a9d7a'},
'router_id': 'ade3d818-fdcb-fg4b-de7f-6750dc8a9d7a',
'flavor_id': None,
'external_v4_ip': '192.168.1.233',
'external_v6_ip': '2001:db8::159'},
{'status': 'PENDING_CREATE',
'name': 'myvpn3',
'description': '',
'id': 'bdde3d818-fdcb-fg4b-de7f-6750dc8a9d7a',
'admin_state_up': True,
'is_admin_state_up': True,
'subnet_id': 'bbe3d818-bdcb-4e4b-b47f-5650dc8a9d7a',
'project_id': 'a4eb9f4938bb418bbc4f8eb31802fefa',
'tenant_id': 'a4eb9f4938bb418bbc4f8eb31802fefa',
'router_id': 'ade3d818-fdcb-fg4b-de7f-6750dc8a9d7a'},
'router_id': 'ade3d818-fdcb-fg4b-de7f-6750dc8a9d7a',
'flavor_id': 'flavor-123',
'external_v4_ip': '192.168.1.234',
'external_v6_ip': '2001:db8::160'},
{'status': 'error',
'name': 'myvpn4',
'description': '',
'id': 'edde3d818-fdcb-fg4b-de7f-6750dc8a9d7a',
'admin_state_up': False,
'is_admin_state_up': False,
'subnet_id': 'bbe3d818-bdcb-4e4b-b47f-5650dc8a9d7a',
'project_id': 'a4eb9f4938bb418bbc4f8eb31802fefa',
'tenant_id': 'a4eb9f4938bb418bbc4f8eb31802fefa',
'router_id': 'ade3d818-fdcb-fg4b-de7f-6750dc8a9d7a'},
'router_id': 'ade3d818-fdcb-fg4b-de7f-6750dc8a9d7a',
'flavor_id': None,
'external_v4_ip': '192.168.1.235',
'external_v6_ip': '2001:db8::161'},
{'status': 'UNKNOWN',
'name': 'myvpn5',
'description': '',
'id': '34e6383a-b1ab-4602-b26a-a1ae7b759212',
'admin_state_up': False,
'is_admin_state_up': False,
'subnet_id': '8c20bbbf-1409-4bc4-b652-3aeda66746c1',
'project_id': 'a4eb9f4938bb418bbc4f8eb31802fefa',
'tenant_id': 'a4eb9f4938bb418bbc4f8eb31802fefa',
'router_id': '0e5c9333-2ef5-4c90-9cca-5cc898515da4'},
'router_id': '0e5c9333-2ef5-4c90-9cca-5cc898515da4',
'flavor_id': None,
'external_v4_ip': '192.168.1.236',
'external_v6_ip': '2001:db8::162'},
{'status': None,
'name': 'myvpn6',
'description': '',
'id': '6e94ff61-8dea-4154-98f1-4020e4b2cecd',
'admin_state_up': False,
'is_admin_state_up': False,
'subnet_id': '5e2a20c3-547a-43e4-90c5-26d32ea42d10',
'project_id': 'a4eb9f4938bb418bbc4f8eb31802fefa',
'tenant_id': 'a4eb9f4938bb418bbc4f8eb31802fefa',
'router_id': '5b14df87-60c1-4fc7-8ad5-7811b2199c7f'},
'router_id': '5b14df87-60c1-4fc7-8ad5-7811b2199c7f',
'flavor_id': None,
'external_v4_ip': '192.168.1.237',
'external_v6_ip': '2001:db8::163'},
]
def test_vpn_get_samples(self):
@@ -162,15 +186,13 @@ class TestIPSecConnectionsPollster(_BaseTestVPNPollster):
'mtu': 1500,
'psk': 'abcd',
'initiator': 'bi-directional',
'dpd': {
'action': 'hold',
'interval': 30,
'timeout': 120},
'ikepolicy_id': 'ade3d818-fdcb-fg4b-de7f-4550dc8a9d7a',
'ipsecpolicy_id': 'fce3d818-fdcb-fg4b-de7f-7850dc8a9d7a',
'vpnservice_id': 'dce3d818-fdcb-fg4b-de7f-5650dc8a9d7a',
'admin_state_up': True,
'peer_ep_group_id': '5cd73c38-1292-461e-9ff4-705b1c13a31c',
'is_admin_state_up': True,
'status': 'ACTIVE',
'project_id': 'abe3d818-fdcb-fg4b-de7f-6650dc8a9d7a',
'tenant_id': 'abe3d818-fdcb-fg4b-de7f-6650dc8a9d7a',
'id': 'fdfbcec-fdcb-fg4b-de7f-6650dc8a9d7a'}
]

View File

@@ -0,0 +1,156 @@
# Copyright (C) 2026 Red Hat
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from unittest import mock
from openstack import exceptions as os_exc
from oslotest import base
from ceilometer import neutron_client
from ceilometer import service
from ceilometer.tests.unit import fakes
class TestNeutronClient(base.BaseTestCase):
"""Tests for the Neutron client using openstacksdk."""
def setUp(self):
super().setUp()
self.CONF = service.prepare_service([], [])
self.mock_connection = mock.patch(
'openstack.connection.Connection',
autospec=True, return_value=fakes.FakeConnection())
self.mock_conn_class = self.mock_connection.start()
self.addCleanup(self.mock_connection.stop)
# Create the client (uses mocked Connection)
self.nc = neutron_client.Client(self.CONF)
def test_fip_get_all(self):
"""Test getting all floating IPs returns list of dicts."""
result = self.nc.fip_get_all()
self.assertEqual(1, len(result))
self.assertIsInstance(result[0], dict)
self.assertEqual('fip-123', result[0]['id'])
self.assertEqual('192.168.1.100', result[0]['floating_ip_address'])
def test_fip_get_all_empty(self):
"""Test getting floating IPs when none exist."""
self.nc.conn.network.ips = mock.Mock(return_value=iter([]))
result = self.nc.fip_get_all()
self.assertEqual([], result)
def test_vpn_get_all(self):
"""Test getting all VPN services returns list of dicts."""
result = self.nc.vpn_get_all()
self.assertEqual(1, len(result))
self.assertIsInstance(result[0], dict)
self.assertEqual('vpn-123', result[0]['id'])
self.assertEqual('my-vpn', result[0]['name'])
def test_vpn_get_all_empty(self):
"""Test getting VPN services when none exist."""
self.nc.conn.network.vpn_services = mock.Mock(
return_value=iter([]))
result = self.nc.vpn_get_all()
self.assertEqual([], result)
def test_ipsec_site_connections_get_all(self):
"""Test getting all IPSec site connections returns list of dicts."""
result = self.nc.ipsec_site_connections_get_all()
self.assertEqual(1, len(result))
self.assertIsInstance(result[0], dict)
self.assertEqual('ipsec-123', result[0]['id'])
def test_ipsec_site_connections_get_all_empty(self):
"""Test getting IPSec connections when none exist."""
self.nc.conn.network.vpn_ipsec_site_connections =\
mock.Mock(return_value=iter([]))
result = self.nc.ipsec_site_connections_get_all()
self.assertEqual([], result)
def test_firewall_get_all(self):
"""Test getting all firewall groups returns list of dicts."""
result = self.nc.firewall_get_all()
self.assertEqual(1, len(result))
self.assertIsInstance(result[0], dict)
self.assertEqual('fw-123', result[0]['id'])
self.assertEqual('my-firewall', result[0]['name'])
def test_firewall_get_all_empty(self):
"""Test getting firewall groups when none exist."""
self.nc.conn.network.firewall_groups = mock.Mock(
return_value=iter([]))
result = self.nc.firewall_get_all()
self.assertEqual([], result)
def test_fw_policy_get_all(self):
"""Test getting all firewall policies returns list of dicts."""
result = self.nc.fw_policy_get_all()
self.assertEqual(1, len(result))
self.assertIsInstance(result[0], dict)
self.assertEqual('policy-123', result[0]['id'])
self.assertEqual('my-policy', result[0]['name'])
def test_fw_policy_get_all_empty(self):
"""Test getting firewall policies when none exist."""
self.nc.conn.network.firewall_policies = mock.Mock(
return_value=iter([]))
result = self.nc.fw_policy_get_all()
self.assertEqual([], result)
def test_fip_get_all_404_returns_empty(self):
"""Test that 404 errors return empty list."""
self.nc.conn.network.ips = mock.Mock(
side_effect=os_exc.HttpException(http_status=404))
result = self.nc.fip_get_all()
self.assertEqual([], result)
def test_fip_get_all_other_http_error_returns_empty(self):
"""Test that other HTTP errors return empty list."""
self.nc.conn.network.ips = mock.Mock(side_effect=os_exc.HttpException(
http_status=500))
result = self.nc.fip_get_all()
self.assertEqual([], result)
def test_fip_get_all_non_http_exception_raises(self):
"""Test that non-HTTP exceptions are re-raised."""
self.nc.conn.network.ips = mock.Mock(side_effect=RuntimeError(
"Connection failed"))
self.assertRaises(RuntimeError, self.nc.fip_get_all)

View File

@@ -22,7 +22,6 @@ oslo.utils>=4.7.0 # Apache-2.0
oslo.privsep>=1.32.0 # Apache-2.0
python-keystoneclient>=3.18.0 # Apache-2.0
keystoneauth1>=3.18.0 # Apache-2.0
python-neutronclient>=6.7.0 # Apache-2.0
python-novaclient>=9.1.0 # Apache-2.0
python-swiftclient>=3.2.0 # Apache-2.0
python-cinderclient>=3.3.0 # Apache-2.0