Skip to content

Commit 933b05f

Browse files
committed
feat(processor): Add OpenStack Pre-Processor
1 parent f1dc23c commit 933b05f

File tree

5 files changed

+356
-0
lines changed

5 files changed

+356
-0
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Copyright (c) 2025, Inria
2+
# Copyright (c) 2025, University of Lille
3+
# All rights reserved.
4+
#
5+
# Redistribution and use in source and binary forms, with or without
6+
# modification, are permitted provided that the following conditions are met:
7+
#
8+
# * Redistributions of source code must retain the above copyright notice, this
9+
# list of conditions and the following disclaimer.
10+
#
11+
# * Redistributions in binary form must reproduce the above copyright notice,
12+
# this list of conditions and the following disclaimer in the documentation
13+
# and/or other materials provided with the distribution.
14+
#
15+
# * Neither the name of the copyright holder nor the names of its
16+
# contributors may be used to endorse or promote products derived from
17+
# this software without specific prior written permission.
18+
#
19+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29+
30+
from powerapi.processor.pre.openstack.actor import OpenStackPreProcessorActor
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Copyright (c) 2025, Inria
2+
# Copyright (c) 2025, University of Lille
3+
# All rights reserved.
4+
#
5+
# Redistribution and use in source and binary forms, with or without
6+
# modification, are permitted provided that the following conditions are met:
7+
#
8+
# * Redistributions of source code must retain the above copyright notice, this
9+
# list of conditions and the following disclaimer.
10+
#
11+
# * Redistributions in binary form must reproduce the above copyright notice,
12+
# this list of conditions and the following disclaimer in the documentation
13+
# and/or other materials provided with the distribution.
14+
#
15+
# * Neither the name of the copyright holder nor the names of its
16+
# contributors may be used to endorse or promote products derived from
17+
# this software without specific prior written permission.
18+
#
19+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29+
30+
import re
31+
32+
LIBVIRT_INSTANCE_NAME_REGEX = re.compile(r"(instance-\d+)")
33+
34+
35+
def get_instance_name_from_libvirt_cgroup(target: str) -> str | None:
36+
"""
37+
Returns the instance name of the target.
38+
:param target: Cgroup path
39+
:return: Instance name (``instance-XXXXXXXX``)
40+
"""
41+
if "\\x" in target:
42+
# Some systems (cgroups v2 managed by systemd) escape special characters in the cgroup path.
43+
# Decoding the path is required in order to reliably extract the instance name.
44+
# For example: /sys/fs/cgroup/machine.slice/machine-qemu\\x2d3\\x2dinstance\\x2d00000003.scope/libvirt/emulator
45+
target = target.encode("utf-8").decode("unicode_escape")
46+
47+
match = LIBVIRT_INSTANCE_NAME_REGEX.match(target)
48+
if match:
49+
return match.group(1)
50+
51+
return None
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# Copyright (c) 2023, INRIA
2+
# Copyright (c) 2023, University of Lille
3+
# All rights reserved.
4+
#
5+
# Redistribution and use in source and binary forms, with or without
6+
# modification, are permitted provided that the following conditions are met:
7+
#
8+
# * Redistributions of source code must retain the above copyright notice, this
9+
# list of conditions and the following disclaimer.
10+
#
11+
# * Redistributions in binary form must reproduce the above copyright notice,
12+
# this list of conditions and the following disclaimer in the documentation
13+
# and/or other materials provided with the distribution.
14+
#
15+
# * Neither the name of the copyright holder nor the names of its
16+
# contributors may be used to endorse or promote products derived from
17+
# this software without specific prior written permission.
18+
#
19+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29+
30+
import logging
31+
32+
from powerapi.actor import Actor
33+
from powerapi.message import StartMessage, PoisonPillMessage
34+
from powerapi.processor.pre.openstack.handlers import StartMessageHandler, PoisonPillMessageHandler, HWPCReportHandler
35+
from powerapi.processor.processor_actor import ProcessorState, ProcessorActor
36+
from powerapi.report import HWPCReport
37+
from .metadata_cache_manager import OpenStackMetadataCacheManager
38+
39+
40+
class OpenStackPreProcessorState(ProcessorState):
41+
"""
42+
State of the OpenStack pre-processor actor.
43+
"""
44+
45+
def __init__(self, actor: Actor, target_actors: list, target_actors_names: list):
46+
"""
47+
:param actor: Actor instance
48+
:param target_actors: list of target actors
49+
:param target_actors_names: list of target actor names
50+
"""
51+
super().__init__(actor, target_actors, target_actors_names)
52+
53+
self.metadata_cache_manager = OpenStackMetadataCacheManager()
54+
55+
56+
class OpenStackPreProcessorActor(ProcessorActor):
57+
"""
58+
Pre-Processor Actor that adds OpenStack related metadata to reports.
59+
"""
60+
61+
def __init__(self, name: str, target_actors: list, target_actors_names: list, level_logger: int = logging.WARNING):
62+
"""
63+
:param name: Name of the actor
64+
:param target_actors: List of target actors
65+
:param target_actors_names: List of target actor names
66+
:param level_logger: Logging level of the actor
67+
"""
68+
super().__init__(name, level_logger, 5000)
69+
70+
self.state = OpenStackPreProcessorState(self, target_actors, target_actors_names)
71+
72+
def setup(self):
73+
"""
74+
Set up the OpenStack pre-processor actor.
75+
"""
76+
self.add_handler(StartMessage, StartMessageHandler(self.state))
77+
self.add_handler(PoisonPillMessage, PoisonPillMessageHandler(self.state))
78+
79+
self.add_handler(HWPCReport, HWPCReportHandler(self.state))
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# Copyright (c) 2023, INRIA
2+
# Copyright (c) 2023, University of Lille
3+
# All rights reserved.
4+
#
5+
# Redistribution and use in source and binary forms, with or without
6+
# modification, are permitted provided that the following conditions are met:
7+
#
8+
# * Redistributions of source code must retain the above copyright notice, this
9+
# list of conditions and the following disclaimer.
10+
#
11+
# * Redistributions in binary form must reproduce the above copyright notice,
12+
# this list of conditions and the following disclaimer in the documentation
13+
# and/or other materials provided with the distribution.
14+
#
15+
# * Neither the name of the copyright holder nor the names of its
16+
# contributors may be used to endorse or promote products derived from
17+
# this software without specific prior written permission.
18+
#
19+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29+
30+
from powerapi.handler import StartHandler, PoisonPillMessageHandler as PoisonPillHandler
31+
from powerapi.processor.handlers import ProcessorReportHandler
32+
from powerapi.report import HWPCReport
33+
from ._utils import get_instance_name_from_libvirt_cgroup
34+
from .metadata_cache_manager import ServerMetadata, MetadataSyncFailed
35+
36+
37+
class StartMessageHandler(StartHandler):
38+
"""
39+
Start message handler for the OpenStack processor actor.
40+
"""
41+
42+
def initialization(self):
43+
"""
44+
Initialize the OpenStack processor.
45+
"""
46+
for actor in self.state.target_actors:
47+
actor.connect_data()
48+
49+
self.state.monitor_agent.start()
50+
51+
52+
class PoisonPillMessageHandler(PoisonPillHandler):
53+
"""
54+
PoisonPill message handler for the OpenStack processor actor.
55+
"""
56+
57+
def teardown(self, soft: bool = False):
58+
"""
59+
Teardown the OpenStack processor.
60+
"""
61+
for actor in self.state.target_actors:
62+
actor.socket_interface.close()
63+
64+
65+
class HWPCReportHandler(ProcessorReportHandler):
66+
"""
67+
Generic report handler for the OpenStack processor actor.
68+
Used to add the server metadata (from the OpenStack API) to the processed report.
69+
"""
70+
71+
def try_get_server_metadata(self, sensor_name: str, instance_name: str) -> ServerMetadata | None:
72+
"""
73+
Try to get the server metadata from the cache.
74+
:param sensor_name: Name of the sensor
75+
:param instance_name: Name of the instance to fetch metadata for
76+
:return: Server metadata entry or None if not found
77+
"""
78+
server_metadata = None
79+
try:
80+
server_metadata = self.state.metadata_cache_manager.get_server_metadata(sensor_name, instance_name)
81+
if server_metadata is None:
82+
# Retry once after syncing the metadata cache.
83+
self.state.metadata_cache_manager.sync_metadata_cache_from_api()
84+
server_metadata = self.state.metadata_cache_manager.get_server_metadata(sensor_name, instance_name)
85+
except MetadataSyncFailed as exn:
86+
self.state.logger.warning(exn)
87+
88+
return server_metadata
89+
90+
def handle(self, msg: HWPCReport):
91+
"""
92+
Process an HWPCReport to add the Kubernetes metadata.
93+
:param msg: The HWPCReport to process
94+
"""
95+
instance_name = get_instance_name_from_libvirt_cgroup(msg.target)
96+
if instance_name is not None:
97+
server_metadata = self.try_get_server_metadata(msg.sensor, instance_name)
98+
if server_metadata is None:
99+
# Drop the report if the server metadata is not present in the cache.
100+
return
101+
102+
msg.target = server_metadata.server_name
103+
msg.metadata['openstack'] = vars(server_metadata)
104+
105+
self._send_report(msg)
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Copyright (c) 2025, Inria
2+
# Copyright (c) 2025, University of Lille
3+
# All rights reserved.
4+
#
5+
# Redistribution and use in source and binary forms, with or without
6+
# modification, are permitted provided that the following conditions are met:
7+
#
8+
# * Redistributions of source code must retain the above copyright notice, this
9+
# list of conditions and the following disclaimer.
10+
#
11+
# * Redistributions in binary form must reproduce the above copyright notice,
12+
# this list of conditions and the following disclaimer in the documentation
13+
# and/or other materials provided with the distribution.
14+
#
15+
# * Neither the name of the copyright holder nor the names of its
16+
# contributors may be used to endorse or promote products derived from
17+
# this software without specific prior written permission.
18+
#
19+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29+
30+
from dataclasses import dataclass
31+
32+
from openstack.connection import Connection
33+
from openstack.exceptions import SDKException
34+
35+
36+
class MetadataSyncFailed(Exception):
37+
"""
38+
Exception raised when the metadata cache sync operation fails.
39+
"""
40+
41+
42+
@dataclass(frozen=True, slots=True)
43+
class ServerMetadata:
44+
"""
45+
Represents an OpenStack server metadata cache entry.
46+
"""
47+
server_id: str
48+
server_name: str
49+
host: str
50+
metadata: dict[str, str]
51+
52+
53+
class OpenStackMetadataCacheManager:
54+
"""
55+
OpenStack metadata cache manager.
56+
Use the OpenStack API to fetch details about the servers hosted on the infrastructure.
57+
It requires credentials with sufficient permissions to access server metadata across all projects.
58+
Permission to read Nova Extended Server Attributes (OS-EXT-SRV-ATTR) is **mandatory** in order to map cgroups to servers.
59+
"""
60+
61+
def __init__(self):
62+
self._openstack_api = Connection(app_name='PowerAPI') # Configuration is taken from OS_* environment variables
63+
self._metadata_cache: dict[tuple, ServerMetadata] = {}
64+
65+
def get_server_metadata(self, host: str, instance_name: str) -> ServerMetadata | None:
66+
"""
67+
Get metadata for the server of the specified host from the cache.
68+
:param host: Name of the host (hypervisor) where the server is located
69+
:param instance_name: Name of the instance (libvirt instance name)
70+
:return: Server metadata cache entry
71+
"""
72+
return self._metadata_cache.get((host, instance_name), None)
73+
74+
def sync_metadata_cache_from_api(self) -> None:
75+
"""
76+
Sync the running servers metadata cache from the OpenStack API.
77+
"""
78+
try:
79+
for server in self._openstack_api.compute.servers(details=True, all_projects=True):
80+
cache_entry = ServerMetadata(server.id, server.name, server.host, server.metadata)
81+
self._metadata_cache[(server.host, server.instance_name)] = cache_entry
82+
except SDKException as exn:
83+
raise MetadataSyncFailed('Failed to retrieve servers metadata from OpenStack API') from exn
84+
except ValueError as exn:
85+
raise MetadataSyncFailed('Required server attribute is missing from the OpenStack API response') from exn
86+
87+
def clear_metadata_cache(self) -> None:
88+
"""
89+
Clears all server metadata entries from the cache.
90+
"""
91+
self._metadata_cache.clear()

0 commit comments

Comments
 (0)