Skip to content

Commit

Permalink
hotsos/core: add name aliasing support
Browse files Browse the repository at this point in the history
at the moment, scenarios are using the long import paths in order to
reference to a Python property. this feature allows assigning an alias
to a Python class or variable in order to make using plugin property
interfaces easier.

Added aliases to the plugins.
Updated existing scenarios to use aliases.
Added unit tests for the aliasing code.
Updated the docs.

Fixes #912

Signed-off-by: Mustafa Kemal Gilor <[email protected]>
  • Loading branch information
xmkg committed Aug 1, 2024
1 parent 5c66141 commit 5f9cd3b
Show file tree
Hide file tree
Showing 137 changed files with 639 additions and 257 deletions.
84 changes: 84 additions & 0 deletions doc/source/contrib/language_ref/property_ref/main_properties.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Or they can reference a Python property. This is done by prefixing the import st
vars:
foo: '@path.to.myproperty'
bar: '@property.alias'
A :ref:`factory <FactoryClasses>` reference can also be defined using the following form:

Expand Down Expand Up @@ -46,6 +47,89 @@ Variables are accessible from any property within the file in which they are def

NOTE: global properties are not yet supported.

Aliases
=======

Aliasing provides easier access to a class or property. The class or property needs to
be aliased in order to use this feature.

.. code-block:: python
class MyHelperClass:
@property
def my_awesome_property(self):
return True
Normally, if the user wants to retrieve the value of the my_awesome_property in a YAML scenario
they have to write the whole import path to the class, as follows:

.. code-block:: yaml
vars:
foo: '@hotsos.module.path.MyHelperClass.my_awesome_property'
checks:
foo_is_true:
requires:
varops: [[$foo], [eq, true]]
But, with the help of aliasing, we can make it easier to type and remember for the user:

.. code-block:: python
from hotsos.core.alias import alias
@alias("helper")
class MyHelperClass:
@property
def my_awesome_property(self):
return True
... so the previous example can be written as follows:

.. code-block:: yaml
vars:
foo: '@helper.my_awesome_property'
checks:
foo_is_true:
requires:
varops: [[$foo], [eq, true]]
Individual properties can be aliased too:

.. code-block:: python
from hotsos.core.alias import alias
class MyHelperClass:
@alias('awesomeness')
@property
def my_awesome_property(self):
return True
.. code-block:: yaml
vars:
awesome: '@awesomeness'
Aliasing works for the following constructs:

- Python property names
- Class names (e.g. Config handler class type name)

Alias Naming
------------

An alias can be anything. The only limitation is that an alias cannot start with the
name of the main package (i.e., `hotsos.`). This is to prevent mixing up aliases with
the real property paths. The alias registration will fail with `AliasForbiddenError(...)`
in such cases.

Checks
======

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,14 @@ Usage:

.. code-block:: yaml
property: <import path to python property>
property: <import path to python property or alias>
or

.. code-block:: yaml
property:
path: <import path to python property>
path: <import path to python property or alias>
ops: OPS_LIST
Cache keys:
Expand Down Expand Up @@ -269,7 +269,7 @@ Usage:
.. code-block:: yaml
config:
handler: <import path>
handler: <import path or alias>
path: <path to config file>
assertions:
- allow-unset: <bool>
Expand All @@ -295,7 +295,7 @@ Example:
checks:
checkcfg:
config:
handler: hotsos.core.plugins.openstack.OpenstackConfig
handler: openstack.config
path: etc/nova/nova.conf
assertions:
- key: debug
Expand Down
2 changes: 1 addition & 1 deletion doc/source/contrib/scenarios.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ matches and the second is a fallback:
.. code-block:: yaml
vars:
mem_current: '@hotsos.core.host_helpers.systemd.ServiceFactory.memory_current:neverfail'
mem_current: '@systemd.service.memory_current:neverfail'
checks:
is_enabled:
systemd:
Expand Down
1 change: 1 addition & 0 deletions doc/source/contrib/writing_checks_overview.rst
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,6 @@ the rest of the directory will only be run if it resolves to *True*:
requires:
or:
- property: hotsos.core.plugins.myplugin.mustbetrue
- property: alias.mustbetrue
- path: file/that/must/exist
105 changes: 105 additions & 0 deletions hotsos/core/alias.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"""Aliasing utilities."""

from hotsos.core.log import log


class AliasAlreadyInUseError(Exception):
"""Raised when an alias is already in use."""

def __init__(self, name):
self.message = f"Alias '{name}` already in use!"

def __str__(self):
return self.message


class AliasForbiddenError(Exception):
"""Raised when an alias is forbidden to use."""

def __init__(self, name):
self.message = f"Alias '{name}` is forbidden!"

def __str__(self):
return self.message


class AliasRegistry:
"""
A class that provides a registry for aliasing Python things.
"""

# A class-level dictionary to store registered aliases.
registry = {}

@staticmethod
def register(name, decoratee):
"""
Register a function, method, or property under an alias.
This method handles different types of Python objects and creates
appropriate wrappers or registrations based on the object type.
Args:
name (str): The alias under which to register the decoratee.
decoratee (callable or property): The Python object to be
registered.
Raises:
AliasAlreadyInUseError: If the alias name is already registered.
AliasForbiddenError: If the alias name starts with "hotsos."
"""
isprop = isinstance(decoratee, property)
target = decoratee.fget if isprop else decoratee

if name.startswith("hotsos."):
raise AliasForbiddenError(name)

if name in AliasRegistry.registry:
log.debug("alias registration failed -- already in use(`%s`)",
name)
raise AliasAlreadyInUseError(name)

import_path = f"{target.__module__}.{target.__qualname__}"
log.debug("registering alias `%s` --> {%s}", name, import_path)
# Register full import path.
AliasRegistry.registry[name] = import_path

@staticmethod
def resolve(the_alias, default=None):
"""
Retrieve a registered alias.
Args:
the_alias (str): The alias to retrieve.
Returns:
callable: The function or wrapper associated with the alias.
Raises:
NoSuchAliasError: No such alias in the registry.
"""

if the_alias not in AliasRegistry.registry:
log.debug(
"alias `%s` not found in the registry, "
"returning the default value",
the_alias,
)
return default

value = AliasRegistry.registry[the_alias]
log.debug("alias %s resolved to %s", the_alias, value)
return value


def alias(argument):
"""Create an alias for a property, function or a thing."""

def real_decorator(func):
"""We're not wrapping the func as we don't want
to do anything at runtime. We just want to alias
`func` to some user-defined name and call it on-demand."""
AliasRegistry.register(argument, func)
return func

return real_decorator
4 changes: 4 additions & 0 deletions hotsos/core/host_helpers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,7 @@
AAProfileFactory,
ApparmorHelper,
)
from .filestat import ( # noqa: F403,F401
FileFactory,
FileObj
)
2 changes: 2 additions & 0 deletions hotsos/core/host_helpers/apparmor.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
)
from hotsos.core.factory import FactoryBase
from hotsos.core.host_helpers.cli import CLIHelperFile
from hotsos.core.alias import alias


@dataclass
Expand Down Expand Up @@ -81,6 +82,7 @@ def profiles_unconfined(self):
return self.profiles.get('unconfined', {}).get('profiles', [])


@alias('apparmor.profile')
class AAProfileFactory(FactoryBase):
"""
Dynamically create AAProfile objects using profile name.
Expand Down
2 changes: 2 additions & 0 deletions hotsos/core/host_helpers/filestat.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from hotsos.core.config import HotSOSConfig
from hotsos.core.factory import FactoryBase
from hotsos.core.log import log
from hotsos.core.alias import alias


class FileObj():
Expand Down Expand Up @@ -38,6 +39,7 @@ def size(self):
return size


@alias('file')
class FileFactory(FactoryBase):
"""
Factory to dynamically create FileObj objects using file path as input.
Expand Down
2 changes: 2 additions & 0 deletions hotsos/core/host_helpers/packaging.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from hotsos.core.host_helpers.cli import CLIHelper
from hotsos.core.log import log
from hotsos.core.utils import sorted_dict
from hotsos.core.alias import alias


class DPKGBadVersionSyntax(Exception):
Expand Down Expand Up @@ -484,6 +485,7 @@ class AptPackage:
version: str


@alias('apt.package')
class AptFactory(FactoryBase):
"""
Factory to dynamically get package versions.
Expand Down
2 changes: 2 additions & 0 deletions hotsos/core/host_helpers/ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from hotsos.core.factory import FactoryBase
from hotsos.core.host_helpers.cli import CLIHelper
from hotsos.core.log import log
from hotsos.core.alias import alias


class SSLCertificate():
Expand Down Expand Up @@ -65,6 +66,7 @@ def certificate_expires_soon(self):
return self.certificate.days_to_expire <= self.expire_days


@alias('sslcert')
class SSLCertificatesFactory(FactoryBase):
"""
Factory to dynamically create SSLCertificate objects for given paths.
Expand Down
2 changes: 2 additions & 0 deletions hotsos/core/host_helpers/systemd.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from hotsos.core.host_helpers import CLIHelper, CLIHelperFile
from hotsos.core.host_helpers.common import ServiceManagerBase
from hotsos.core.log import log
from hotsos.core.alias import alias


class SystemdService():
Expand Down Expand Up @@ -354,6 +355,7 @@ def _service_filtered_ps(self):
return ps_filtered


@alias("systemd.service")
class ServiceFactory(FactoryBase):
"""
Factory to dynamically create SystemdService objects for given services.
Expand Down
2 changes: 2 additions & 0 deletions hotsos/core/plugins/juju/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from hotsos.core.host_helpers import PebbleHelper, SystemdHelper
from hotsos.core.plugins.juju.resources import JujuBase
from hotsos.core import plugintools
from hotsos.core.alias import alias

SVC_VALID_SUFFIX = r'[0-9a-zA-Z-_]*'
JUJU_SVC_EXPRS = [rf'mongod{SVC_VALID_SUFFIX}',
Expand All @@ -12,6 +13,7 @@
rf'(?:^|[^\s])juju-db{SVC_VALID_SUFFIX}']


@alias('juju')
class JujuChecks(plugintools.PluginPartBase, JujuBase):
""" Juju checks. """
plugin_name = 'juju'
Expand Down
3 changes: 3 additions & 0 deletions hotsos/core/plugins/juju/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from hotsos.core.config import HotSOSConfig
from hotsos.core.log import log
from hotsos.core import utils
from hotsos.core.alias import alias


class JujuMachine():
Expand Down Expand Up @@ -176,6 +177,7 @@ class JujuCharm:
version: int


@alias('juju.base')
class JujuBase():
""" Juju checks base class. """
CHARM_MANIFEST_GLOB = "agents/unit-*/state/deployer/manifests"
Expand Down Expand Up @@ -259,6 +261,7 @@ def charm_names(self):
return list(self.charms.keys())


@alias('juju.bin')
class JujuBinaryInterface(JujuBase):
""" Interface to juju binary. """
@property
Expand Down
2 changes: 2 additions & 0 deletions hotsos/core/plugins/kernel/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
from hotsos.core import host_helpers, plugintools
from hotsos.core.config import HotSOSConfig
from hotsos.core.plugins.kernel.config import KernelConfig
from hotsos.core.alias import alias


@alias('kernel')
class KernelBase():
""" Base class for kernel plugin helpers. """
@cached_property
Expand Down
Loading

0 comments on commit 5f9cd3b

Please sign in to comment.