os-faults/os_faults/__init__.py
Thomas Goirand 25b7d106a8 Use platformdirs rather than appdirs
The appdirs is abandonned upstream. platformdirs is API compatible.
Let's switch to it. Note this patch is already in the Debian package.

Change-Id: I021b710072a3bc8eb505ca29f30d16b4da9aa472
2024-11-27 20:12:34 +01:00

230 lines
7.0 KiB
Python

# 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.
import copy
import os
import jsonschema
import logging
import pbr.version
import platformdirs
import yaml
from os_faults.ansible import executor
from os_faults.api import error
from os_faults.api import human
from os_faults import registry
LOG = logging.getLogger(__name__)
# Set default logging handler to avoid "No handler found" warnings.
LOG.addHandler(logging.NullHandler())
def get_version():
return pbr.version.VersionInfo('os_faults').version_string()
def get_release():
return pbr.version.VersionInfo('os_faults').release_string()
APPDIRS = platformdirs.AppDirs(appname='openstack', appauthor='OpenStack')
UNIX_SITE_CONFIG_HOME = '/etc/openstack'
CONFIG_SEARCH_PATH = [
os.getcwd(),
APPDIRS.user_config_dir,
UNIX_SITE_CONFIG_HOME,
]
CONFIG_FILES = [
os.path.join(d, 'os-faults' + s)
for d in CONFIG_SEARCH_PATH
for s in ['.json', '.yaml', '.yml']
]
CONFIG_SCHEMA = {
'type': 'object',
'$schema': 'http://json-schema.org/draft-04/schema#',
'properties': {
'node_discover': {
'type': 'object',
'properties': {
'driver': {'type': 'string'},
'args': {},
},
'required': ['driver', 'args'],
'additionalProperties': False,
},
'services': {
'type': 'object',
'patternProperties': {
'.*': {
'type': 'object',
'properties': {
'driver': {'type': 'string'},
'args': {'type': 'object'},
'hosts': {
'type': 'array',
'minItems': 1,
'items': {'type': 'string'},
},
},
'required': ['driver', 'args'],
'additionalProperties': False,
}
},
'additionalProperties': False,
},
'cloud_management': {
'type': 'object',
'properties': {
'driver': {'type': 'string'},
'args': {'type': 'object'},
},
'required': ['driver'],
'additionalProperties': False,
},
'power_managements': {
'type': 'array',
'items': {
'type': 'object',
'properties': {
'driver': {'type': 'string'},
'args': {'type': 'object'},
},
'required': ['driver', 'args'],
'additionalProperties': False,
},
'minItems': 1,
},
},
'required': ['cloud_management'],
}
def get_default_config_file():
if 'OS_FAULTS_CONFIG' in os.environ:
return os.environ['OS_FAULTS_CONFIG']
for config_file in CONFIG_FILES:
if os.path.exists(config_file):
return config_file
msg = 'Config file is not found on any of paths: {}'.format(CONFIG_FILES)
raise error.OSFError(msg)
def _init_driver(params):
driver_cls = registry.get_driver(params['driver'])
args = params.get('args') or {} # driver may have no arguments
if args:
jsonschema.validate(args, driver_cls.CONFIG_SCHEMA)
return driver_cls(args)
def connect(cloud_config=None, config_filename=None):
"""Connects to the cloud
:param cloud_config: dict with cloud and power management params
:param config_filename: name of the file where to read config from
:returns: CloudManagement object
"""
if cloud_config is None:
config_filename = config_filename or get_default_config_file()
with open(config_filename) as fd:
cloud_config = yaml.safe_load(fd.read())
jsonschema.validate(cloud_config, CONFIG_SCHEMA)
cloud_management_conf = cloud_config['cloud_management']
cloud_management = _init_driver(cloud_management_conf)
services = cloud_config.get('services')
if services:
cloud_management.update_services(services)
cloud_management.validate_services()
containers = cloud_config.get('containers')
if containers:
cloud_management.update_containers(containers)
cloud_management.validate_containers()
node_discover_conf = cloud_config.get('node_discover')
if node_discover_conf:
node_discover = _init_driver(node_discover_conf)
cloud_management.set_node_discover(node_discover)
power_managements_conf = cloud_config.get('power_managements')
if power_managements_conf:
for pm_conf in power_managements_conf:
pm = _init_driver(pm_conf)
cloud_management.add_power_management(pm)
return cloud_management
def discover(cloud_config):
"""Connect to the cloud and discover nodes and services
:param cloud_config: dict with cloud and power management params
:returns: config dict with discovered nodes/services
"""
cloud_config = copy.deepcopy(cloud_config)
cloud_management = connect(cloud_config)
# discover nodes
hosts = []
for host in cloud_management.get_nodes().hosts:
hosts.append({'ip': host.ip, 'mac': host.mac, 'fqdn': host.fqdn})
LOG.info('Found node: %s' % str(host))
cloud_config['node_discover'] = {'driver': 'node_list', 'args': hosts}
# discover services
cloud_config['services'] = {}
for service_name in cloud_management.list_supported_services():
service = cloud_management.get_service(service_name)
ips = service.get_nodes().get_ips()
cloud_config['services'][service_name] = {
'driver': service.NAME,
'args': service.config
}
if ips:
cloud_config['services'][service_name]['hosts'] = ips
LOG.info('Found service "%s" on hosts: %s' % (
service_name, str(ips)))
else:
LOG.warning('Service "%s" is not found' % service_name)
return cloud_config
def human_api(cloud_management, command):
"""Executes a command written as English sentence
:param cloud_management: library instance as returned by :connect:
function
:param command: text command
"""
human.execute(cloud_management, command)
def register_ansible_modules(paths):
"""Registers ansible modules by provided paths
Allows to use custom ansible modules in NodeCollection.run_task method
:param paths: list of paths to folders with ansible modules
"""
executor.add_module_paths(paths)