diff --git a/Dockerfile b/Dockerfile index 2b98323e..98a7c18c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1 -ARG PYVER=3.11.4 -ARG ALPTAG=3.17 +ARG PYVER=3.11.7 +ARG ALPTAG=3.18 FROM python:${PYVER}-alpine${ALPTAG} as builder # Add the community repo for access to patchelf binary package diff --git a/LICENSE b/LICENSE index b8b69f54..5fe4c8eb 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2011–2023 Elasticsearch and contributors. +Copyright 2011–2024 Elasticsearch and contributors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/curator/__init__.py b/curator/__init__.py index 75c96a2b..14d5291e 100644 --- a/curator/__init__.py +++ b/curator/__init__.py @@ -4,7 +4,6 @@ from curator.exceptions import * from curator.defaults import * from curator.validators import * -from curator.logtools import * from curator.indexlist import IndexList from curator.snapshotlist import SnapshotList from curator.actions import * diff --git a/curator/_version.py b/curator/_version.py index b493cce1..557a82fa 100644 --- a/curator/_version.py +++ b/curator/_version.py @@ -1,2 +1,2 @@ """Curator Version""" -__version__ = '8.0.8' +__version__ = '8.0.9' diff --git a/curator/classdef.py b/curator/classdef.py index 6d4e6164..be91de75 100644 --- a/curator/classdef.py +++ b/curator/classdef.py @@ -1,11 +1,11 @@ """Other Classes""" import logging -from es_client.exceptions import ConfigurationError as ESclient_ConfigError +from es_client.exceptions import FailedValidation +from es_client.helpers.schemacheck import password_filter from es_client.helpers.utils import get_yaml from curator import IndexList, SnapshotList from curator.actions import CLASS_MAP from curator.exceptions import ConfigurationError -from curator.config_utils import password_filter from curator.helpers.testers import validate_actions # Let me tell you the story of the nearly wasted afternoon and the research that went into this @@ -62,7 +62,7 @@ def get_validated(self, action_file): """ try: return validate_actions(get_yaml(action_file)) - except (ESclient_ConfigError, UnboundLocalError) as err: + except (FailedValidation, UnboundLocalError) as err: self.logger.critical('Configuration Error: %s', err) raise ConfigurationError from err diff --git a/curator/cli.py b/curator/cli.py index 60ee9485..95ab05dd 100644 --- a/curator/cli.py +++ b/curator/cli.py @@ -1,87 +1,20 @@ """Main CLI for Curator""" import sys import logging -import pathlib import click -from es_client.builder import ClientArgs, OtherArgs -from es_client.helpers.utils import get_yaml, check_config, prune_nones, verify_url_schema +from es_client.defaults import LOGGING_SETTINGS +from es_client.helpers.config import cli_opts, context_settings, get_args, get_client, get_config +from es_client.helpers.logging import configure_logging +from es_client.helpers.utils import option_wrapper, prune_nones from curator.exceptions import ClientException from curator.classdef import ActionsFile -from curator.config_utils import check_logging_config, set_logging -from curator.defaults import settings +from curator.defaults.settings import CLICK_DRYRUN, default_config_file, footer, snapshot_actions from curator.exceptions import NoIndices, NoSnapshots -from curator.helpers.getters import get_client from curator.helpers.testers import ilm_policy_check -from curator.cli_singletons.utils import get_width from curator._version import __version__ -def configfile_callback(ctx, param, value): - """Callback to validate whether the provided config file exists and is writeable - - :param ctx: The click context - :param param: The click parameter object - :param value: The value of the parameter - - :type ctx: Click context - :type param: Click object - :type value: Any - - :returns: Config file path or None - :rtype: str - """ - logger = logging.getLogger(__name__) - logger.debug('Click ctx = %s', ctx) - logger.debug('Click param = %s', param) - logger.debug('Click value = %s', value) - path = pathlib.Path(value) - if path.is_file(): - return value - logger.warning('Config file not found: %s', value) - return None - -def override_logging(config, loglevel, logfile, logformat): - """Get logging config and override from command-line options - - :param config: The configuration from file - :param loglevel: The log level - :param logfile: The log file to write - :param logformat: Which log format to use - - :type config: dict - :type loglevel: str - :type logfile: str - :type logformat: str - - :returns: Log configuration ready for validation - :rtype: dict - """ - # Check for log settings from config file - init_logcfg = check_logging_config(config) - - # Override anything with options from the command-line - if loglevel: - init_logcfg['loglevel'] = loglevel - if logfile: - init_logcfg['logfile'] = logfile - if logformat: - init_logcfg['logformat'] = logformat - return init_logcfg - -def cli_hostslist(hosts): - """ - :param hosts: One or more hosts. - :type hosts: str or list - - :returns: A list of hosts that came in from the command-line, or ``None`` - :rtype: list or ``None`` - """ - hostslist = [] - if hosts: - for host in list(hosts): - hostslist.append(verify_url_schema(host)) - else: - hostslist = None - return hostslist +ONOFF = {'on': '', 'off': 'no-'} +click_opt_wrap = option_wrapper() def ilm_action_skip(client, action_def): """ @@ -97,7 +30,7 @@ def ilm_action_skip(client, action_def): :rtype: bool """ logger = logging.getLogger(__name__) - if not action_def.allow_ilm and action_def.action not in settings.snapshot_actions(): + if not action_def.allow_ilm and action_def.action not in snapshot_actions(): if action_def.action == 'rollover': if ilm_policy_check(client, action_def.options['name']): logger.info('Alias %s is associated with ILM policy.', action_def.options['name']) @@ -261,35 +194,35 @@ def run(client_args, other_args, action_file, dry_run=False): logger.info('Action ID: %s, "%s" completed.', idx, action_def.action) logger.info('All actions completed.') -# pylint: disable=unused-argument, redefined-builtin -@click.command(context_settings=get_width()) -@click.option('--config', help='Path to configuration file.', type=str, default=settings.config_file(), callback=configfile_callback) -@click.option('--hosts', help='Elasticsearch URL to connect to', multiple=True) -@click.option('--cloud_id', help='Shorthand to connect to Elastic Cloud instance') -@click.option('--api_token', help='The base64 encoded API Key token', type=str) -@click.option('--id', help='API Key "id" value', type=str) -@click.option('--api_key', help='API Key "api_key" value', type=str) -@click.option('--username', help='Username used to create "basic_auth" tuple') -@click.option('--password', help='Password used to create "basic_auth" tuple') -@click.option('--bearer_auth', type=str) -@click.option('--opaque_id', type=str) -@click.option('--request_timeout', help='Request timeout in seconds', type=float) -@click.option('--http_compress', help='Enable HTTP compression', is_flag=True, default=None) -@click.option('--verify_certs', help='Verify SSL/TLS certificate(s)', is_flag=True, default=None) -@click.option('--ca_certs', help='Path to CA certificate file or directory') -@click.option('--client_cert', help='Path to client certificate file') -@click.option('--client_key', help='Path to client certificate key') -@click.option('--ssl_assert_hostname', help='Hostname or IP address to verify on the node\'s certificate.', type=str) -@click.option('--ssl_assert_fingerprint', help='SHA-256 fingerprint of the node\'s certificate. If this value is given then root-of-trust verification isn\'t done and only the node\'s certificate fingerprint is verified.', type=str) -@click.option('--ssl_version', help='Minimum acceptable TLS/SSL version', type=str) -@click.option('--master-only', help='Only run if the single host provided is the elected master', is_flag=True, default=None) -@click.option('--skip_version_test', help='Do not check the host version', is_flag=True, default=None) -@click.option('--dry-run', is_flag=True, help='Do not perform any changes.') -@click.option('--loglevel', help='Log level', type=click.Choice(['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'])) -@click.option('--logfile', help='log file') -@click.option('--logformat', help='Log output format', type=click.Choice(['default', 'logstash', 'json', 'ecs'])) +# pylint: disable=unused-argument, redefined-builtin, too-many-arguments, too-many-locals, line-too-long +@click.command(context_settings=context_settings(), epilog=footer(__version__, tail='command-line.html')) +@click_opt_wrap(*cli_opts('config')) +@click_opt_wrap(*cli_opts('hosts')) +@click_opt_wrap(*cli_opts('cloud_id')) +@click_opt_wrap(*cli_opts('api_token')) +@click_opt_wrap(*cli_opts('id')) +@click_opt_wrap(*cli_opts('api_key')) +@click_opt_wrap(*cli_opts('username')) +@click_opt_wrap(*cli_opts('password')) +@click_opt_wrap(*cli_opts('bearer_auth')) +@click_opt_wrap(*cli_opts('opaque_id')) +@click_opt_wrap(*cli_opts('request_timeout')) +@click_opt_wrap(*cli_opts('http_compress', onoff=ONOFF)) +@click_opt_wrap(*cli_opts('verify_certs', onoff=ONOFF)) +@click_opt_wrap(*cli_opts('ca_certs')) +@click_opt_wrap(*cli_opts('client_cert')) +@click_opt_wrap(*cli_opts('client_key')) +@click_opt_wrap(*cli_opts('ssl_assert_hostname')) +@click_opt_wrap(*cli_opts('ssl_assert_fingerprint')) +@click_opt_wrap(*cli_opts('ssl_version')) +@click_opt_wrap(*cli_opts('master-only', onoff=ONOFF)) +@click_opt_wrap(*cli_opts('skip_version_test', onoff=ONOFF)) +@click_opt_wrap(*cli_opts('dry-run', settings=CLICK_DRYRUN)) +@click_opt_wrap(*cli_opts('loglevel', settings=LOGGING_SETTINGS)) +@click_opt_wrap(*cli_opts('logfile', settings=LOGGING_SETTINGS)) +@click_opt_wrap(*cli_opts('logformat', settings=LOGGING_SETTINGS)) @click.argument('action_file', type=click.Path(exists=True), nargs=1) -@click.version_option(version=__version__) +@click.version_option(__version__, '-v', '--version', prog_name="curator") @click.pass_context def cli( ctx, config, hosts, cloud_id, api_token, id, api_key, username, password, bearer_auth, @@ -298,75 +231,21 @@ def cli( dry_run, loglevel, logfile, logformat, action_file ): """ - Curator for Elasticsearch indices. - - See http://elastic.co/guide/en/elasticsearch/client/curator/current - """ - client_args = ClientArgs() - other_args = OtherArgs() - if config: - from_yaml = get_yaml(config) - else: - # Use empty defaults. - from_yaml = {'elasticsearch': {'client': {}, 'other_settings': {}}, 'logging': {}} - raw_config = check_config(from_yaml) - client_args.update_settings(raw_config['client']) - other_args.update_settings(raw_config['other_settings']) - - set_logging(check_logging_config( - {'logging': override_logging(from_yaml, loglevel, logfile, logformat)})) - - hostslist = cli_hostslist(hosts) + Curator for Elasticsearch indices - cli_client = prune_nones({ - 'hosts': hostslist, - 'cloud_id': cloud_id, - 'bearer_auth': bearer_auth, - 'opaque_id': opaque_id, - 'request_timeout': request_timeout, - 'http_compress': http_compress, - 'verify_certs': verify_certs, - 'ca_certs': ca_certs, - 'client_cert': client_cert, - 'client_key': client_key, - 'ssl_assert_hostname': ssl_assert_hostname, - 'ssl_assert_fingerprint': ssl_assert_fingerprint, - 'ssl_version': ssl_version - }) + The default $HOME/.curator/curator.yml configuration file (--config) + can be used but is not needed. + + Command-line settings will always override YAML configuration settings. - cli_other = prune_nones({ - 'master_only': master_only, - 'skip_version_test': skip_version_test, - 'username': username, - 'password': password, - 'api_key': { - 'id': id, - 'api_key': api_key, - 'token': api_token, - } - }) - # Remove `api_key` root key if `id` and `api_key` and `token` are all None - if id is None and api_key is None and api_token is None: - del cli_other['api_key'] + Some less-frequently used client configuration options are now hidden. To see the full list, + run: - # If hosts are in the config file, but cloud_id is specified at the command-line, - # we need to remove the hosts parameter as cloud_id and hosts are mutually exclusive - if cloud_id: - click.echo('cloud_id provided at CLI, superseding any other configured hosts') - client_args.hosts = None - cli_client.pop('hosts', None) - - # Likewise, if hosts are provided at the command-line, but cloud_id was in the config file, - # we need to remove the cloud_id parameter from the config file-based dictionary before merging - if hosts: - click.echo('hosts specified manually, superseding any other cloud_id or hosts') - client_args.hosts = None - client_args.cloud_id = None - cli_client.pop('cloud_id', None) - - # Update the objects if we have settings after pruning None values - if cli_client: - client_args.update_settings(cli_client) - if cli_other: - other_args.update_settings(cli_other) + curator_cli -h + """ + ctx.obj = {} + ctx.obj['dry_run'] = dry_run + cfg = get_config(ctx.params, default_config_file()) + configure_logging(cfg, ctx.params) + client_args, other_args = get_args(ctx.params, cfg) run(client_args, other_args, action_file, dry_run) diff --git a/curator/cli_singletons/alias.py b/curator/cli_singletons/alias.py index 3c414385..f6e77046 100644 --- a/curator/cli_singletons/alias.py +++ b/curator/cli_singletons/alias.py @@ -1,10 +1,11 @@ """Alias Singleton""" import logging import click +from es_client.helpers.config import context_settings from curator.cli_singletons.object_class import CLIAction -from curator.cli_singletons.utils import get_width, json_to_dict, validate_filter_json +from curator.cli_singletons.utils import json_to_dict, validate_filter_json -@click.command(context_settings=get_width()) +@click.command(context_settings=context_settings()) @click.option('--name', type=str, help='Alias name', required=True) @click.option( '--add', @@ -48,12 +49,6 @@ def alias(ctx, name, add, remove, warn_if_no_indices, extra_settings, allow_ilm_ logger.debug('manual_options %s', manual_options) # ctx.info_name is the name of the function or name specified in @click.command decorator ignore_empty_list = warn_if_no_indices - logger.debug('ctx.info_name %s', ctx.info_name) - logger.debug('ignore_empty_list %s', ignore_empty_list) - logger.debug('add %s', add) - logger.debug('remove %s', remove) - logger.debug('warn_if_no_indices %s', warn_if_no_indices) - logger.debug("ctx.obj['dry_run'] %s", ctx.obj['dry_run']) action = CLIAction( ctx.info_name, ctx.obj['config'], @@ -62,5 +57,4 @@ def alias(ctx, name, add, remove, warn_if_no_indices, extra_settings, allow_ilm_ ignore_empty_list, add=add, remove=remove, warn_if_no_indices=warn_if_no_indices, # alias specific kwargs ) - logger.debug('We did not get here, did we?') action.do_singleton_action(dry_run=ctx.obj['dry_run']) diff --git a/curator/cli_singletons/allocation.py b/curator/cli_singletons/allocation.py index 85a57624..1ede28bc 100644 --- a/curator/cli_singletons/allocation.py +++ b/curator/cli_singletons/allocation.py @@ -1,9 +1,10 @@ """Allocation Singleton""" import click +from es_client.helpers.config import context_settings from curator.cli_singletons.object_class import CLIAction -from curator.cli_singletons.utils import get_width, validate_filter_json +from curator.cli_singletons.utils import validate_filter_json -@click.command(context_settings=get_width()) +@click.command(context_settings=context_settings()) @click.option('--key', type=str, required=True, help='Node identification tag') @click.option('--value', type=str, default=None, help='Value associated with --key') @click.option('--allocation_type', type=click.Choice(['require', 'include', 'exclude'])) diff --git a/curator/cli_singletons/close.py b/curator/cli_singletons/close.py index fef72bad..8039b50c 100644 --- a/curator/cli_singletons/close.py +++ b/curator/cli_singletons/close.py @@ -1,9 +1,10 @@ """Close Singleton""" import click +from es_client.helpers.config import context_settings from curator.cli_singletons.object_class import CLIAction -from curator.cli_singletons.utils import get_width, validate_filter_json +from curator.cli_singletons.utils import validate_filter_json -@click.command(context_settings=get_width()) +@click.command(context_settings=context_settings()) @click.option('--delete_aliases', is_flag=True, help='Delete all aliases from indices to be closed') @click.option('--skip_flush', is_flag=True, help='Skip flush phase for indices to be closed') @click.option( diff --git a/curator/cli_singletons/delete.py b/curator/cli_singletons/delete.py index 7780ae57..57deedd9 100644 --- a/curator/cli_singletons/delete.py +++ b/curator/cli_singletons/delete.py @@ -1,10 +1,11 @@ """Delete Index and Delete Snapshot Singletons""" import click +from es_client.helpers.config import context_settings from curator.cli_singletons.object_class import CLIAction -from curator.cli_singletons.utils import get_width, validate_filter_json +from curator.cli_singletons.utils import validate_filter_json #### Indices #### -@click.command(context_settings=get_width()) +@click.command(context_settings=context_settings()) @click.option( '--ignore_empty_list', is_flag=True, @@ -38,7 +39,7 @@ def delete_indices(ctx, ignore_empty_list, allow_ilm_indices, filter_list): action.do_singleton_action(dry_run=ctx.obj['dry_run']) #### Snapshots #### -@click.command(context_settings=get_width()) +@click.command(context_settings=context_settings()) @click.option('--repository', type=str, required=True, help='Snapshot repository name') @click.option('--retry_count', type=int, help='Number of times to retry (max 3)') @click.option('--retry_interval', type=int, help='Time in seconds between retries') diff --git a/curator/cli_singletons/forcemerge.py b/curator/cli_singletons/forcemerge.py index a7ea7d39..95ff1211 100644 --- a/curator/cli_singletons/forcemerge.py +++ b/curator/cli_singletons/forcemerge.py @@ -1,9 +1,10 @@ """ForceMerge Singleton""" import click +from es_client.helpers.config import context_settings from curator.cli_singletons.object_class import CLIAction -from curator.cli_singletons.utils import get_width, validate_filter_json +from curator.cli_singletons.utils import validate_filter_json -@click.command(context_settings=get_width()) +@click.command(context_settings=context_settings()) @click.option( '--max_num_segments', type=int, diff --git a/curator/cli_singletons/object_class.py b/curator/cli_singletons/object_class.py index 6ac3cad7..622fd7a2 100644 --- a/curator/cli_singletons/object_class.py +++ b/curator/cli_singletons/object_class.py @@ -3,6 +3,8 @@ import sys from voluptuous import Schema from es_client.builder import Builder +from es_client.exceptions import FailedValidation +from es_client.helpers.schemacheck import SchemaCheck from es_client.helpers.utils import prune_nones from curator import IndexList, SnapshotList from curator.actions import ( @@ -12,7 +14,7 @@ from curator.defaults.settings import snapshot_actions from curator.exceptions import ConfigurationError, NoIndices, NoSnapshots from curator.helpers.testers import validate_filters -from curator.validators import SchemaCheck, options +from curator.validators import options from curator.validators.filter_functions import validfilters CLASS_MAP = { @@ -101,9 +103,8 @@ def __init__(self, action, client_args, option_dict, filter_list, ignore_empty_l else: self.check_filters(filter_list) - builder = Builder(configdict=client_args) - try: + builder = Builder(configdict=client_args) builder.connect() # pylint: disable=broad-except except Exception as exc: @@ -139,7 +140,7 @@ def check_options(self, option_dict): # Remove this after the schema check, as the action class won't need it as an arg if self.action in ['delete_snapshots', 'restore']: del self.options['repository'] - except ConfigurationError as exc: + except FailedValidation as exc: self.logger.critical('Unable to parse options: %s', exc) sys.exit(1) @@ -154,7 +155,7 @@ def check_filters(self, filter_dict, loc='singleton', key='filters'): f'{self.action} singleton action "{key}"' ).result() self.filters = validate_filters(self.action, _) - except ConfigurationError as exc: + except FailedValidation as exc: self.logger.critical('Unable to parse filters: %s', exc) sys.exit(1) diff --git a/curator/cli_singletons/open_indices.py b/curator/cli_singletons/open_indices.py index ac19e82a..4ba94750 100644 --- a/curator/cli_singletons/open_indices.py +++ b/curator/cli_singletons/open_indices.py @@ -1,9 +1,10 @@ """Open (closed) Index Singleton""" import click +from es_client.helpers.config import context_settings from curator.cli_singletons.object_class import CLIAction -from curator.cli_singletons.utils import get_width, validate_filter_json +from curator.cli_singletons.utils import validate_filter_json -@click.command(name='open', context_settings=get_width()) +@click.command(name='open', context_settings=context_settings()) @click.option( '--ignore_empty_list', is_flag=True, diff --git a/curator/cli_singletons/replicas.py b/curator/cli_singletons/replicas.py index 835ee04f..9ae792d2 100644 --- a/curator/cli_singletons/replicas.py +++ b/curator/cli_singletons/replicas.py @@ -1,9 +1,10 @@ """Change Replica Count Singleton""" import click +from es_client.helpers.config import context_settings from curator.cli_singletons.object_class import CLIAction -from curator.cli_singletons.utils import get_width, validate_filter_json +from curator.cli_singletons.utils import validate_filter_json -@click.command(context_settings=get_width()) +@click.command(context_settings=context_settings()) @click.option('--count', type=int, required=True, help='Number of replicas (max 10)') @click.option( '--wait_for_completion/--no-wait_for_completion', diff --git a/curator/cli_singletons/restore.py b/curator/cli_singletons/restore.py index db4c8c21..8ff4921b 100644 --- a/curator/cli_singletons/restore.py +++ b/curator/cli_singletons/restore.py @@ -1,10 +1,11 @@ """Snapshot Restore Singleton""" import click +from es_client.helpers.config import context_settings from curator.cli_singletons.object_class import CLIAction -from curator.cli_singletons.utils import get_width, json_to_dict, validate_filter_json +from curator.cli_singletons.utils import json_to_dict, validate_filter_json # pylint: disable=line-too-long -@click.command(context_settings=get_width()) +@click.command(context_settings=context_settings()) @click.option('--repository', type=str, required=True, help='Snapshot repository') @click.option('--name', type=str, help='Snapshot name', required=False, default=None) @click.option('--index', multiple=True, help='Index name to restore. (Can invoke repeatedly for multiple indices)') diff --git a/curator/cli_singletons/rollover.py b/curator/cli_singletons/rollover.py index c587a43c..dd1cc94e 100644 --- a/curator/cli_singletons/rollover.py +++ b/curator/cli_singletons/rollover.py @@ -1,11 +1,12 @@ """Index Rollover Singleton""" import click +from es_client.helpers.config import context_settings from es_client.helpers.utils import prune_nones from curator.cli_singletons.object_class import CLIAction -from curator.cli_singletons.utils import get_width, json_to_dict +from curator.cli_singletons.utils import json_to_dict # pylint: disable=line-too-long -@click.command(context_settings=get_width()) +@click.command(context_settings=context_settings()) @click.option('--name', type=str, help='Alias name', required=True) @click.option('--max_age', type=str, help='max_age condition value (see documentation)') @click.option('--max_docs', type=str, help='max_docs condition value (see documentation)') diff --git a/curator/cli_singletons/show.py b/curator/cli_singletons/show.py index 15da9493..51b5e72a 100644 --- a/curator/cli_singletons/show.py +++ b/curator/cli_singletons/show.py @@ -1,14 +1,17 @@ """Show Index/Snapshot Singletons""" from datetime import datetime import click +from es_client.helpers.config import context_settings from curator.cli_singletons.object_class import CLIAction -from curator.cli_singletons.utils import get_width, validate_filter_json +from curator.cli_singletons.utils import validate_filter_json from curator.helpers.getters import byte_size +from curator.defaults.settings import footer +from curator._version import __version__ #### Indices #### # pylint: disable=line-too-long -@click.command(context_settings=get_width()) +@click.command(context_settings=context_settings(), epilog=footer(__version__, tail='singleton-cli.html#_show_indicessnapshots')) @click.option('--verbose', help='Show verbose output.', is_flag=True, show_default=True) @click.option('--header', help='Print header if --verbose', is_flag=True, show_default=True) @click.option('--epoch', help='Print time as epoch if --verbose', is_flag=True, show_default=True) @@ -80,7 +83,7 @@ def show_indices(ctx, verbose, header, epoch, ignore_empty_list, allow_ilm_indic #### Snapshots #### # pylint: disable=line-too-long -@click.command(context_settings=get_width()) +@click.command(context_settings=context_settings(), epilog=footer(__version__, tail='singleton-cli.html#_show_indicessnapshots')) @click.option('--repository', type=str, required=True, help='Snapshot repository name') @click.option('--ignore_empty_list', is_flag=True, help='Do not raise exception if there are no actionable snapshots') @click.option('--filter_list', callback=validate_filter_json, default='{"filtertype":"none"}', help='JSON string representing an array of filters.') diff --git a/curator/cli_singletons/shrink.py b/curator/cli_singletons/shrink.py index 409cd21e..0b209800 100644 --- a/curator/cli_singletons/shrink.py +++ b/curator/cli_singletons/shrink.py @@ -1,10 +1,11 @@ """Shrink Index Singleton""" import click +from es_client.helpers.config import context_settings from curator.cli_singletons.object_class import CLIAction -from curator.cli_singletons.utils import get_width, json_to_dict, validate_filter_json +from curator.cli_singletons.utils import json_to_dict, validate_filter_json # pylint: disable=line-too-long -@click.command(context_settings=get_width()) +@click.command(context_settings=context_settings()) @click.option('--shrink_node', default='DETERMINISTIC', type=str, help='Named node, or DETERMINISTIC', show_default=True) @click.option('--node_filters', help='JSON version of node_filters (see documentation)', callback=json_to_dict) @click.option('--number_of_shards', default=1, type=int, help='Shrink to this many shards per index') diff --git a/curator/cli_singletons/snapshot.py b/curator/cli_singletons/snapshot.py index 414e0306..7ed26b1a 100644 --- a/curator/cli_singletons/snapshot.py +++ b/curator/cli_singletons/snapshot.py @@ -1,10 +1,11 @@ """Snapshot Singleton""" import click +from es_client.helpers.config import context_settings from curator.cli_singletons.object_class import CLIAction -from curator.cli_singletons.utils import get_width, validate_filter_json +from curator.cli_singletons.utils import validate_filter_json # pylint: disable=line-too-long -@click.command(context_settings=get_width()) +@click.command(context_settings=context_settings()) @click.option('--repository', type=str, required=True, help='Snapshot repository') @click.option('--name', type=str, help='Snapshot name', show_default=True, default='curator-%Y%m%d%H%M%S') @click.option('--ignore_unavailable', is_flag=True, show_default=True, help='Ignore unavailable shards/indices.') diff --git a/curator/cli_singletons/utils.py b/curator/cli_singletons/utils.py index 8a894475..76a13aae 100644 --- a/curator/cli_singletons/utils.py +++ b/curator/cli_singletons/utils.py @@ -1,13 +1,8 @@ """Singleton Utils Module""" import json -from shutil import get_terminal_size from click import BadParameter from es_client.helpers.utils import ensure_list -def get_width(): - """Determine terminal width""" - return dict(max_content_width=get_terminal_size()[0]) - # Click functions require ctx and param to be passed positionally even if not used # pylint: disable=unused-argument def json_to_dict(ctx, param, value): diff --git a/curator/config_utils.py b/curator/config_utils.py deleted file mode 100644 index 9a65f1fc..00000000 --- a/curator/config_utils.py +++ /dev/null @@ -1,82 +0,0 @@ -"""Configuration utilty functions""" -import logging -from copy import deepcopy -import click -from es_client.helpers.utils import prune_nones, ensure_list -from curator.defaults.logging_defaults import config_logging -from curator.validators.schemacheck import SchemaCheck -from curator.logtools import LogInfo, Blacklist - -def check_logging_config(config): - """ - Ensure that the top-level key ``logging`` is in ``config`` before passing it to - :py:class:`~.curator.validators.schemacheck.SchemaCheck` for value validation. - - :param config: Logging configuration data - - :type config: dict - - :returns: :py:class:`~.curator.validators.schemacheck.SchemaCheck` validated logging - configuration. - """ - - if not isinstance(config, dict): - click.echo( - f'Must supply logging information as a dictionary. ' - f'You supplied: "{config}" which is "{type(config)}"' - f'Using default logging values.' - ) - log_settings = {} - elif not 'logging' in config: - click.echo('No "logging" setting in supplied configuration. Using defaults.') - log_settings = {} - else: - if config['logging']: - log_settings = prune_nones(config['logging']) - else: - log_settings = {} - return SchemaCheck( - log_settings, config_logging(), 'Logging Configuration', 'logging').result() - -def set_logging(log_opts): - """Configure global logging options - - :param log_opts: Logging configuration data - - :type log_opts: dict - - :rtype: None - """ - # Set up logging - loginfo = LogInfo(log_opts) - logging.root.addHandler(loginfo.handler) - logging.root.setLevel(loginfo.numeric_log_level) - _ = logging.getLogger('curator.cli') - # Set up NullHandler() to handle nested elasticsearch8.trace Logger - # instance in elasticsearch python client - logging.getLogger('elasticsearch8.trace').addHandler(logging.NullHandler()) - if log_opts['blacklist']: - for bl_entry in ensure_list(log_opts['blacklist']): - for handler in logging.root.handlers: - handler.addFilter(Blacklist(bl_entry)) - -def password_filter(data): - """ - Recursively look through all nested structures of ``data`` for the key ``'password'`` and redact - the value. - - :param data: Configuration data - - :type data: dict - - :returns: A :py:class:`~.copy.deepcopy` of ``data`` with the value obscured by ``REDACTED`` - if the key is ``'password'``. - """ - def iterdict(mydict): - for key, value in mydict.items(): - if isinstance(value, dict): - iterdict(value) - elif key == "password": - mydict.update({"password": "REDACTED"}) - return mydict - return iterdict(deepcopy(data)) diff --git a/curator/curator_cli.py b/curator/curator_cli.py deleted file mode 100755 index b7c7a8fd..00000000 --- a/curator/curator_cli.py +++ /dev/null @@ -1,9 +0,0 @@ -"""CLI Wrapper used by run_curator.py""" -from curator.singletons import cli - -def main(): - """Main function called by run_curator.py""" - # This is because click uses decorators, and pylint doesn't catch that - # pylint: disable=E1120 - # pylint: disable=E1123 - cli(obj={}) diff --git a/curator/defaults/logging_defaults.py b/curator/defaults/logging_defaults.py deleted file mode 100644 index 5a2815aa..00000000 --- a/curator/defaults/logging_defaults.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Define valid schemas for client configuration validation""" -from six import string_types -from voluptuous import All, Any, Coerce, Optional, Schema - -# Configuration file: logging -def config_logging(): - """ - Logging schema with defaults: - - .. code-block:: yaml - - logging: - loglevel: INFO - logfile: None - logformat: default - blacklist: ['elastic_transport', 'urllib3'] - - :returns: A valid :py:class:`~.voluptuous.schema_builder.Schema` of all acceptable values with - the default values set. - :rtype: :py:class:`~.voluptuous.schema_builder.Schema` - """ - return Schema( - { - Optional('loglevel', default='INFO'): - Any(None, 'NOTSET', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL', - All(Coerce(int), Any(0, 10, 20, 30, 40, 50)) - ), - Optional('logfile', default=None): Any(None, *string_types), - Optional('logformat', default='default'): - Any(None, All(Any(*string_types), Any('default', 'json', 'logstash', 'ecs'))), - Optional('blacklist', default=['elastic_transport', 'urllib3']): Any(None, list), - } - ) diff --git a/curator/defaults/settings.py b/curator/defaults/settings.py index 451af5ca..722428d6 100644 --- a/curator/defaults/settings.py +++ b/curator/defaults/settings.py @@ -2,26 +2,45 @@ from os import path from six import string_types from voluptuous import Any, Boolean, Coerce, Optional +from curator.exceptions import CuratorException -# Elasticsearch versions supported -def version_max(): - """ - :returns: The maximum Elasticsearch version Curator supports: ``(8, 99, 99)`` - """ - return (8, 99, 99) -def version_min(): +CURATOR_DOCS = 'https://www.elastic.co/guide/en/elasticsearch/client/curator' +CLICK_DRYRUN = { + 'dry-run': {'help': 'Do not perform any changes.', 'is_flag': True}, +} + +# Click specifics + +def footer(version, tail='index.html'): """ - :returns: The minimum Elasticsearch version Curator supports: ``(8, 99, 99)`` + Generate a footer linking to Curator docs based on Curator version + + :param version: The Curator version + + :type version: str + + :returns: An epilog/footer suitable for Click """ - return (8, 0, 0) + if not isinstance(version, str): + raise CuratorException('Parameter version is not a string: {type(version)}') + majmin = '' + try: + ver = version.split('.') + majmin = f'{ver[0]}.{ver[1]}' + except Exception as exc: + msg = f'Could not determine Curator version from provided value: {version}' + raise CuratorException(msg) from exc + return f'Learn more at {CURATOR_DOCS}/{majmin}/{tail}' # Default Config file location -def config_file(): +def default_config_file(): """ :returns: The default configuration file location: ``path.join(path.expanduser('~'), '.curator', 'curator.yml')`` """ - return path.join(path.expanduser('~'), '.curator', 'curator.yml') + default = path.join(path.expanduser('~'), '.curator', 'curator.yml') + if path.isfile(default): + return default # Default filter patterns (regular expressions) def regex_map(): diff --git a/curator/helpers/getters.py b/curator/helpers/getters.py index 2bcfb39f..7bbad6e7 100644 --- a/curator/helpers/getters.py +++ b/curator/helpers/getters.py @@ -1,11 +1,8 @@ """Utility functions that get things""" -# :pylint disable= import logging import re from elasticsearch8 import exceptions as es8exc -from es_client.defaults import VERSION_MAX, VERSION_MIN -from es_client.builder import Builder -from curator.exceptions import ClientException, CuratorException, FailedExecution, MissingArgument +from curator.exceptions import CuratorException, FailedExecution, MissingArgument def byte_size(num, suffix='B'): """ @@ -45,46 +42,6 @@ def get_alias_actions(oldidx, newidx, aliases): actions.append({'add': {'index': newidx, 'alias': alias}}) return actions -def get_client( - configdict=None, configfile=None, autoconnect=False, version_min=VERSION_MIN, - version_max=VERSION_MAX): - """Get an Elasticsearch Client using :py:class:`es_client.Builder` - - Build a client out of settings from `configfile` or `configdict` - If neither `configfile` nor `configdict` is provided, empty defaults will be used. - If both are provided, `configdict` will be used, and `configfile` ignored. - - :param configdict: A configuration dictionary - :param configfile: A configuration file - :param autoconnect: Connect to client automatically - :param verion_min: Minimum acceptable version of Elasticsearch (major, minor, patch) - :param verion_max: Maximum acceptable version of Elasticsearch (major, minor, patch) - - :type configdict: dict - :type configfile: str - :type autoconnect: bool - :type version_min: tuple - :type version_max: tuple - - :returns: A client connection object - :rtype: :py:class:`~.elasticsearch.Elasticsearch` - """ - logger = logging.getLogger(__name__) - logger.info('Creating client object and testing connection') - - builder = Builder( - configdict=configdict, configfile=configfile, autoconnect=autoconnect, - version_min=version_min, version_max=version_max - ) - - try: - builder.connect() - except Exception as exc: - logger.critical('Exception encountered: %s', exc) - raise ClientException from exc - - return builder.client - def get_data_tiers(client): """ Get all valid data tiers from the node roles of each node in the cluster by polling each node diff --git a/curator/helpers/testers.py b/curator/helpers/testers.py index 67242259..9147ea36 100644 --- a/curator/helpers/testers.py +++ b/curator/helpers/testers.py @@ -3,12 +3,13 @@ from voluptuous import Schema from elasticsearch8 import Elasticsearch from elasticsearch8.exceptions import NotFoundError +from es_client.helpers.schemacheck import SchemaCheck from es_client.helpers.utils import prune_nones from curator.helpers.getters import get_repository, get_write_index from curator.exceptions import ( ConfigurationError, MissingArgument, RepositoryException, SearchableSnapshotException) from curator.defaults.settings import index_filtertypes, snapshot_actions, snapshot_filtertypes -from curator.validators import SchemaCheck, actions, options +from curator.validators import actions, options from curator.validators.filter_functions import validfilters from curator.helpers.utils import report_failure diff --git a/curator/indexlist.py b/curator/indexlist.py index 11ece065..352d647f 100644 --- a/curator/indexlist.py +++ b/curator/indexlist.py @@ -3,6 +3,7 @@ import itertools import logging from elasticsearch8.exceptions import NotFoundError, TransportError +from es_client.helpers.schemacheck import SchemaCheck from es_client.helpers.utils import ensure_list from curator.defaults import settings from curator.exceptions import ActionError, ConfigurationError, MissingArgument, NoIndices @@ -13,7 +14,6 @@ from curator.helpers.getters import byte_size, get_indices from curator.helpers.testers import verify_client_object from curator.helpers.utils import chunk_index_list, report_failure, to_csv -from curator.validators import SchemaCheck from curator.validators.filter_functions import filterstructure class IndexList: @@ -312,7 +312,9 @@ def get_index_settings(self): def get_index_state(self): """ For each index in self.indices, populate ``index_info`` with: + state (open or closed) + from the cat API """ self.loggit.debug('Getting index state -- BEGIN') diff --git a/curator/logtools.py b/curator/logtools.py deleted file mode 100644 index c3558b17..00000000 --- a/curator/logtools.py +++ /dev/null @@ -1,151 +0,0 @@ -"""Logging tools""" -import sys -import json -import logging -import time -from pathlib import Path -import ecs_logging -from curator.exceptions import LoggingException - -def de_dot(dot_string, msg): - """ - Turn message and dotted string into a nested dictionary. Used by :py:class:`LogstashFormatter` - - :param dot_string: The dotted string - :param msg: The message - - :type dot_string: str - :type msg: str - """ - arr = dot_string.split('.') - arr.append(msg) - retval = None - for idx in range(len(arr), 1, -1): - if not retval: - try: - retval = {arr[idx-2]: arr[idx-1]} - except Exception as err: - raise LoggingException(err) from err - else: - try: - new_d = {arr[idx-2]: retval} - retval = new_d - except Exception as err: - raise LoggingException(err) from err - return retval - -def deepmerge(source, destination): - """ - Recursively merge deeply nested dictionary structures, ``source`` into ``destination`` - - :param source: Source dictionary - :param destination: Destination dictionary - - :type source: dict - :type destination: dict - - :returns: destination - :rtype: dict - """ - for key, value in source.items(): - if isinstance(value, dict): - node = destination.setdefault(key, {}) - deepmerge(value, node) - else: - destination[key] = value - return destination - -def is_docker(): - """Check if we're running in a docker container""" - cgroup = Path('/proc/self/cgroup') - return Path('/.dockerenv').is_file() or cgroup.is_file() and 'docker' in cgroup.read_text() - -class LogstashFormatter(logging.Formatter): - """Logstash formatting (JSON)""" - # The LogRecord attributes we want to carry over to the Logstash message, - # mapped to the corresponding output key. - WANTED_ATTRS = { - 'levelname': 'loglevel', - 'funcName': 'function', - 'lineno': 'linenum', - 'message': 'message', - 'name': 'name' - } - - def format(self, record): - """ - :param record: The incoming log message - - :rtype: :py:meth:`json.dumps` - """ - self.converter = time.gmtime - timestamp = '%s.%03dZ' % ( - self.formatTime(record, datefmt='%Y-%m-%dT%H:%M:%S'), record.msecs) - result = {'@timestamp': timestamp} - available = record.__dict__ - # This is cleverness because 'message' is NOT a member key of ``record.__dict__`` - # the ``getMessage()`` method is effectively ``msg % args`` (actual keys) - # By manually adding 'message' to ``available``, it simplifies the code - available['message'] = record.getMessage() - for attribute in set(self.WANTED_ATTRS).intersection(available): - result = deepmerge( - de_dot(self.WANTED_ATTRS[attribute], getattr(record, attribute)), result - ) - # The following is mostly for the ecs format. You can't have 2x 'message' keys in - # WANTED_ATTRS, so we set the value to 'log.original' in ecs, and this code block - # guarantees it still appears as 'message' too. - if 'message' not in result.items(): - result['message'] = available['message'] - return json.dumps(result, sort_keys=True) - -class Whitelist(logging.Filter): - """How to whitelist logs""" - # pylint: disable=super-init-not-called - def __init__(self, *whitelist): - self.whitelist = [logging.Filter(name) for name in whitelist] - - def filter(self, record): - return any(f.filter(record) for f in self.whitelist) - -class Blacklist(Whitelist): - """Blacklist monkey-patch of Whitelist""" - def filter(self, record): - return not Whitelist.filter(self, record) - -class LogInfo: - """Logging Class""" - def __init__(self, cfg): - """Class Setup - - :param cfg: The logging configuration - :type: cfg: dict - """ - cfg['loglevel'] = 'INFO' if not 'loglevel' in cfg else cfg['loglevel'] - cfg['logfile'] = None if not 'logfile' in cfg else cfg['logfile'] - cfg['logformat'] = 'default' if not 'logformat' in cfg else cfg['logformat'] - #: Attribute. The numeric equivalent of ``cfg['loglevel']`` - self.numeric_log_level = getattr(logging, cfg['loglevel'].upper(), None) - #: Attribute. The logging format string to use. - self.format_string = '%(asctime)s %(levelname)-9s %(message)s' - - if not isinstance(self.numeric_log_level, int): - raise ValueError(f"Invalid log level: {cfg['loglevel']}") - - #: Attribute. Which logging handler to use - if is_docker(): - self.handler = logging.FileHandler('/proc/1/fd/1') - else: - self.handler = logging.StreamHandler(stream=sys.stdout) - if cfg['logfile']: - self.handler = logging.FileHandler(cfg['logfile']) - - if self.numeric_log_level == 10: # DEBUG - self.format_string = ( - '%(asctime)s %(levelname)-9s %(name)22s %(funcName)22s:%(lineno)-4d %(message)s') - - if cfg['logformat'] == 'json' or cfg['logformat'] == 'logstash': - self.handler.setFormatter(LogstashFormatter()) - elif cfg['logformat'] == 'ecs': - self.handler.setFormatter(ecs_logging.StdlibFormatter()) - else: - self.handler.setFormatter(logging.Formatter(self.format_string)) diff --git a/curator/repomgrcli.py b/curator/repomgrcli.py index 1b854f4c..bc320b44 100644 --- a/curator/repomgrcli.py +++ b/curator/repomgrcli.py @@ -4,13 +4,17 @@ import pprint import click from elasticsearch8 import ApiError, NotFoundError -from es_client.builder import ClientArgs, OtherArgs, Builder -from es_client.helpers.utils import check_config, get_yaml, prune_nones, verify_url_schema -from curator.defaults import settings -from curator.config_utils import check_logging_config, set_logging +from es_client.defaults import LOGGING_SETTINGS, SHOW_OPTION +from es_client.builder import Builder +from es_client.helpers.config import cli_opts, context_settings, get_config, get_args +from es_client.helpers.logging import configure_logging +from es_client.helpers.utils import option_wrapper, prune_nones +from curator.defaults.settings import CLICK_DRYRUN, default_config_file, footer from curator.helpers.getters import get_repository from curator._version import __version__ -from curator.cli_singletons.utils import get_width + +ONOFF = {'on': '', 'off': 'no-'} +click_opt_wrap = option_wrapper() # pylint: disable=unused-argument def delete_callback(ctx, param, value): @@ -39,7 +43,9 @@ def show_repos(client): :type client: :py:class:`~.elasticsearch.Elasticsearch` :rtype: None """ + logger = logging.getLogger(__name__) for repository in sorted(get_repository(client, '*').keys()): + logger.debug('REPOSITORY = %s', repository) click.echo(f'{repository}') sys.exit(0) @@ -295,138 +301,118 @@ def source( create_repo(ctx, repo_name=name, repo_type='source', repo_settings=source_settings, verify=verify) -# pylint: disable=unused-argument, redefined-builtin -@click.group(context_settings=get_width()) -@click.option('--config', help='Path to configuration file.', type=click.Path(exists=True), default=settings.config_file()) -@click.option('--hosts', help='Elasticsearch URL to connect to', multiple=True) -@click.option('--cloud_id', help='Shorthand to connect to Elastic Cloud instance') -@click.option('--id', help='API Key "id" value', type=str) -@click.option('--api_key', help='API Key "api_key" value', type=str) -@click.option('--username', help='Username used to create "basic_auth" tuple') -@click.option('--password', help='Password used to create "basic_auth" tuple') -@click.option('--bearer_auth', type=str) -@click.option('--opaque_id', type=str) -@click.option('--request_timeout', help='Request timeout in seconds', type=float) -@click.option('--http_compress', help='Enable HTTP compression', is_flag=True, default=None) -@click.option('--verify_certs', help='Verify SSL/TLS certificate(s)', is_flag=True, default=None) -@click.option('--ca_certs', help='Path to CA certificate file or directory') -@click.option('--client_cert', help='Path to client certificate file') -@click.option('--client_key', help='Path to client certificate key') -@click.option('--ssl_assert_hostname', help='Hostname or IP address to verify on the node\'s certificate.', type=str) -@click.option('--ssl_assert_fingerprint', help='SHA-256 fingerprint of the node\'s certificate. If this value is given then root-of-trust verification isn\'t done and only the node\'s certificate fingerprint is verified.', type=str) -@click.option('--ssl_version', help='Minimum acceptable TLS/SSL version', type=str) -@click.option('--master-only', help='Only run if the single host provided is the elected master', is_flag=True, default=None) -@click.option('--skip_version_test', help='Do not check the host version', is_flag=True, default=None) -@click.option('--dry-run', is_flag=True, help='Do not perform any changes. NON-FUNCTIONAL PLACEHOLDER! DO NOT USE!') -@click.option('--loglevel', help='Log level', type=click.Choice(['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'])) -@click.option('--logfile', help='log file') -@click.option('--logformat', help='Log output format', type=click.Choice(['default', 'logstash', 'json', 'ecs'])) -@click.version_option(version=__version__) +# pylint: disable=unused-argument, redefined-builtin, too-many-arguments, too-many-locals +@click.group('repo_mgr_cli', context_settings=context_settings(), epilog=footer(__version__)) +@click_opt_wrap(*cli_opts('config')) +@click_opt_wrap(*cli_opts('hosts')) +@click_opt_wrap(*cli_opts('cloud_id')) +@click_opt_wrap(*cli_opts('api_token')) +@click_opt_wrap(*cli_opts('id')) +@click_opt_wrap(*cli_opts('api_key')) +@click_opt_wrap(*cli_opts('username')) +@click_opt_wrap(*cli_opts('password')) +@click_opt_wrap(*cli_opts('bearer_auth')) +@click_opt_wrap(*cli_opts('opaque_id')) +@click_opt_wrap(*cli_opts('request_timeout')) +@click_opt_wrap(*cli_opts('http_compress', onoff=ONOFF)) +@click_opt_wrap(*cli_opts('verify_certs', onoff=ONOFF)) +@click_opt_wrap(*cli_opts('ca_certs')) +@click_opt_wrap(*cli_opts('client_cert')) +@click_opt_wrap(*cli_opts('client_key')) +@click_opt_wrap(*cli_opts('ssl_assert_hostname')) +@click_opt_wrap(*cli_opts('ssl_assert_fingerprint')) +@click_opt_wrap(*cli_opts('ssl_version')) +@click_opt_wrap(*cli_opts('master-only', onoff=ONOFF)) +@click_opt_wrap(*cli_opts('skip_version_test', onoff=ONOFF)) +@click_opt_wrap(*cli_opts('dry-run', settings=CLICK_DRYRUN)) +@click_opt_wrap(*cli_opts('loglevel', settings=LOGGING_SETTINGS)) +@click_opt_wrap(*cli_opts('logfile', settings=LOGGING_SETTINGS)) +@click_opt_wrap(*cli_opts('logformat', settings=LOGGING_SETTINGS)) +@click.version_option(__version__, '-v', '--version', prog_name="es_repo_mgr") @click.pass_context def repo_mgr_cli( - ctx, config, hosts, cloud_id, id, api_key, username, password, bearer_auth, + ctx, config, hosts, cloud_id, api_token, id, api_key, username, password, bearer_auth, opaque_id, request_timeout, http_compress, verify_certs, ca_certs, client_cert, client_key, ssl_assert_hostname, ssl_assert_fingerprint, ssl_version, master_only, skip_version_test, dry_run, loglevel, logfile, logformat ): - """Repository manager for Elasticsearch Curator.""" - # Ensure a passable ctx object - ctx.ensure_object(dict) - - # Extract client args - client_args = ClientArgs() - other_args = OtherArgs() - if config: - from_yaml = get_yaml(config) - raw_config = check_config(from_yaml) - client_args.update_settings(raw_config['client']) - other_args.update_settings(raw_config['other_settings']) - - # Check for log settings from config file - init_logcfg = check_logging_config(from_yaml) - - # Override anything with options from the command-line - if loglevel: - init_logcfg['loglevel'] = loglevel - if logfile: - init_logcfg['logfile'] = logfile - if logformat: - init_logcfg['logformat'] = logformat - - # Now enable logging with the merged settings - set_logging(check_logging_config({'logging': init_logcfg})) - logger = logging.getLogger(__name__) - logger.debug('Logging options validated.') - - hostslist = [] - if hosts: - for host in list(hosts): - hostslist.append(verify_url_schema(host)) - else: - hostslist = None - - cli_client = prune_nones({ - 'hosts': hostslist, - 'cloud_id': cloud_id, - 'bearer_auth': bearer_auth, - 'opaque_id': opaque_id, - 'request_timeout': request_timeout, - 'http_compress': http_compress, - 'verify_certs': verify_certs, - 'ca_certs': ca_certs, - 'client_cert': client_cert, - 'client_key': client_key, - 'ssl_assert_hostname': ssl_assert_hostname, - 'ssl_assert_fingerprint': ssl_assert_fingerprint, - 'ssl_version': ssl_version - }) - - cli_other = prune_nones({ - 'master_only': master_only, - 'skip_version_test': skip_version_test, - 'username': username, - 'password': password, - 'api_key': { - 'id': id, - 'api_key': api_key - } - }) - - # Remove `api_key` root key if `id` and `api_key` are both None - if id is None and api_key is None: - del cli_other['api_key'] - - # If hosts are in the config file, but cloud_id is specified at the command-line, - # we need to remove the hosts parameter as cloud_id and hosts are mutually exclusive - if cloud_id: - click.echo('cloud_id provided at CLI, superseding any other configured hosts') - client_args.hosts = None - cli_client.pop('hosts', None) - - # Likewise, if hosts are provided at the command-line, but cloud_id was in the config file, - # we need to remove the cloud_id parameter from the config file-based dictionary before merging - if hosts: - click.echo('hosts specified manually, superseding any other cloud_id or hosts') - client_args.hosts = None - client_args.cloud_id = None - cli_client.pop('cloud_id', None) + """ + Repository manager for Elasticsearch Curator + + The default $HOME/.curator/curator.yml configuration file (--config) + can be used but is not needed. + + Command-line settings will always override YAML configuration settings. - # Update the objects if we have settings after pruning None values - if cli_client: - client_args.update_settings(cli_client) - if cli_other: - other_args.update_settings(cli_other) + Some less-frequently used client configuration options are now hidden. To see the full list, + run: - # Build a "final_config" that reflects CLI args overriding anything from a config_file - final_config = { + es_repo_mgr show-all-options + """ + ctx.obj = {} + # Ensure a passable ctx object + ctx.ensure_object(dict) + ctx.obj['dry_run'] = dry_run + cfg = get_config(ctx.params, default_config_file()) + configure_logging(cfg, ctx.params) + logger = logging.getLogger('curator.repomgrcli') + client_args, other_args = get_args(ctx.params, cfg) + ctx.obj['esconfig'] = { 'elasticsearch': { 'client': prune_nones(client_args.asdict()), 'other_settings': prune_nones(other_args.asdict()) } } - ctx.obj['esconfig'] = final_config - ctx.obj['dry_run'] = dry_run - logger.debug('YOU HAVE REACHED THIS PHASE') + logger.debug('Exiting initial command function...') + + +@repo_mgr_cli.command( + 'show-all-options', + context_settings=context_settings(), + short_help='Show all configuration options', + epilog=footer(__version__)) +@click_opt_wrap(*cli_opts('config')) +@click_opt_wrap(*cli_opts('hosts')) +@click_opt_wrap(*cli_opts('cloud_id')) +@click_opt_wrap(*cli_opts('api_token')) +@click_opt_wrap(*cli_opts('id')) +@click_opt_wrap(*cli_opts('api_key')) +@click_opt_wrap(*cli_opts('username')) +@click_opt_wrap(*cli_opts('password')) +@click_opt_wrap(*cli_opts('bearer_auth', override=SHOW_OPTION)) +@click_opt_wrap(*cli_opts('opaque_id', override=SHOW_OPTION)) +@click_opt_wrap(*cli_opts('request_timeout')) +@click_opt_wrap(*cli_opts('http_compress', onoff=ONOFF, override=SHOW_OPTION)) +@click_opt_wrap(*cli_opts('verify_certs', onoff=ONOFF)) +@click_opt_wrap(*cli_opts('ca_certs')) +@click_opt_wrap(*cli_opts('client_cert')) +@click_opt_wrap(*cli_opts('client_key')) +@click_opt_wrap(*cli_opts('ssl_assert_hostname', override=SHOW_OPTION)) +@click_opt_wrap(*cli_opts('ssl_assert_fingerprint', override=SHOW_OPTION)) +@click_opt_wrap(*cli_opts('ssl_version', override=SHOW_OPTION)) +@click_opt_wrap(*cli_opts('master-only', onoff=ONOFF, override=SHOW_OPTION)) +@click_opt_wrap(*cli_opts('skip_version_test', onoff=ONOFF, override=SHOW_OPTION)) +@click_opt_wrap(*cli_opts('dry-run', settings=CLICK_DRYRUN)) +@click_opt_wrap(*cli_opts('loglevel', settings=LOGGING_SETTINGS)) +@click_opt_wrap(*cli_opts('logfile', settings=LOGGING_SETTINGS)) +@click_opt_wrap(*cli_opts('logformat', settings=LOGGING_SETTINGS)) +@click.version_option(__version__, '-v', '--version', prog_name="es_repo_mgr") +@click.pass_context +def show_all_options( + ctx, config, hosts, cloud_id, api_token, id, api_key, username, password, bearer_auth, + opaque_id, request_timeout, http_compress, verify_certs, ca_certs, client_cert, client_key, + ssl_assert_hostname, ssl_assert_fingerprint, ssl_version, master_only, skip_version_test, + dry_run, loglevel, logfile, logformat +): + """ + ALL CLIENT OPTIONS + + The following is the full list of settings available for configuring a connection using + command-line options. + """ + ctx = click.get_current_context() + click.echo(ctx.get_help()) + ctx.exit() @repo_mgr_cli.group('create') @click.pass_context diff --git a/curator/singletons.py b/curator/singletons.py index f04876f5..4828deae 100644 --- a/curator/singletons.py +++ b/curator/singletons.py @@ -1,156 +1,91 @@ """CLI module for curator_cli""" import click -from es_client.builder import ClientArgs, OtherArgs -from es_client.helpers.utils import check_config, get_yaml, prune_nones, verify_url_schema -from curator.defaults import settings -from curator.config_utils import check_logging_config, set_logging +from es_client.defaults import LOGGING_SETTINGS, SHOW_OPTION +from es_client.helpers.config import cli_opts, context_settings, get_config, get_args +from es_client.helpers.logging import configure_logging +from es_client.helpers.utils import option_wrapper, prune_nones +from curator.defaults.settings import CLICK_DRYRUN, default_config_file, footer from curator._version import __version__ from curator.cli_singletons import ( alias, allocation, close, delete_indices, delete_snapshots, forcemerge, open_indices, replicas, restore, rollover, snapshot, shrink ) from curator.cli_singletons.show import show_indices, show_snapshots -from curator.cli_singletons.utils import get_width -# pylint: disable=unused-argument, redefined-builtin -@click.group(context_settings=get_width()) -@click.option('--config', help='Path to configuration file.', type=click.Path(exists=True), default=settings.config_file()) -@click.option('--hosts', help='Elasticsearch URL to connect to', multiple=True) -@click.option('--cloud_id', help='Shorthand to connect to Elastic Cloud instance') -@click.option('--api_token', help='The base64 encoded API Key token', type=str) -@click.option('--id', help='API Key "id" value', type=str) -@click.option('--api_key', help='API Key "api_key" value', type=str) -@click.option('--username', help='Username used to create "basic_auth" tuple') -@click.option('--password', help='Password used to create "basic_auth" tuple') -@click.option('--bearer_auth', type=str) -@click.option('--opaque_id', type=str) -@click.option('--request_timeout', help='Request timeout in seconds', type=float) -@click.option('--http_compress', help='Enable HTTP compression', is_flag=True, default=None) -@click.option('--verify_certs', help='Verify SSL/TLS certificate(s)', is_flag=True, default=None) -@click.option('--ca_certs', help='Path to CA certificate file or directory') -@click.option('--client_cert', help='Path to client certificate file') -@click.option('--client_key', help='Path to client certificate key') -@click.option('--ssl_assert_hostname', help='Hostname or IP address to verify on the node\'s certificate.', type=str) -@click.option('--ssl_assert_fingerprint', help='SHA-256 fingerprint of the node\'s certificate. If this value is given then root-of-trust verification isn\'t done and only the node\'s certificate fingerprint is verified.', type=str) -@click.option('--ssl_version', help='Minimum acceptable TLS/SSL version', type=str) -@click.option('--master-only', help='Only run if the single host provided is the elected master', is_flag=True, default=None) -@click.option('--skip_version_test', help='Do not check the host version', is_flag=True, default=None) -@click.option('--dry-run', is_flag=True, help='Do not perform any changes.') -@click.option('--loglevel', help='Log level', type=click.Choice(['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'])) -@click.option('--logfile', help='log file') -@click.option('--logformat', help='Log output format', type=click.Choice(['default', 'logstash', 'json', 'ecs'])) -@click.version_option(version=__version__) +ONOFF = {'on': '', 'off': 'no-'} +click_opt_wrap = option_wrapper() + +# pylint: disable=unused-argument, redefined-builtin, too-many-arguments, too-many-locals +@click.group( + context_settings=context_settings(), epilog=footer(__version__, tail='singleton-cli.html')) +@click_opt_wrap(*cli_opts('config')) +@click_opt_wrap(*cli_opts('hosts')) +@click_opt_wrap(*cli_opts('cloud_id')) +@click_opt_wrap(*cli_opts('api_token')) +@click_opt_wrap(*cli_opts('id')) +@click_opt_wrap(*cli_opts('api_key')) +@click_opt_wrap(*cli_opts('username')) +@click_opt_wrap(*cli_opts('password')) +@click_opt_wrap(*cli_opts('bearer_auth', override=SHOW_OPTION)) +@click_opt_wrap(*cli_opts('opaque_id', override=SHOW_OPTION)) +@click_opt_wrap(*cli_opts('request_timeout')) +@click_opt_wrap(*cli_opts('http_compress', onoff=ONOFF, override=SHOW_OPTION)) +@click_opt_wrap(*cli_opts('verify_certs', onoff=ONOFF)) +@click_opt_wrap(*cli_opts('ca_certs')) +@click_opt_wrap(*cli_opts('client_cert')) +@click_opt_wrap(*cli_opts('client_key')) +@click_opt_wrap(*cli_opts('ssl_assert_hostname', override=SHOW_OPTION)) +@click_opt_wrap(*cli_opts('ssl_assert_fingerprint', override=SHOW_OPTION)) +@click_opt_wrap(*cli_opts('ssl_version', override=SHOW_OPTION)) +@click_opt_wrap(*cli_opts('master-only', onoff=ONOFF, override=SHOW_OPTION)) +@click_opt_wrap(*cli_opts('skip_version_test', onoff=ONOFF, override=SHOW_OPTION)) +@click_opt_wrap(*cli_opts('dry-run', settings=CLICK_DRYRUN)) +@click_opt_wrap(*cli_opts('loglevel', settings=LOGGING_SETTINGS)) +@click_opt_wrap(*cli_opts('logfile', settings=LOGGING_SETTINGS)) +@click_opt_wrap(*cli_opts('logformat', settings=LOGGING_SETTINGS)) +@click.version_option(__version__, '-v', '--version', prog_name='curator_cli') @click.pass_context -def cli( +def curator_cli( ctx, config, hosts, cloud_id, api_token, id, api_key, username, password, bearer_auth, opaque_id, request_timeout, http_compress, verify_certs, ca_certs, client_cert, client_key, ssl_assert_hostname, ssl_assert_fingerprint, ssl_version, master_only, skip_version_test, dry_run, loglevel, logfile, logformat ): - """CLI input""" - client_args = ClientArgs() - other_args = OtherArgs() - if config: - from_yaml = get_yaml(config) - raw_config = check_config(from_yaml) - client_args.update_settings(raw_config['client']) - other_args.update_settings(raw_config['other_settings']) - - # Check for log settings from config file - init_logcfg = check_logging_config(from_yaml) - - # Override anything with options from the command-line - if loglevel: - init_logcfg['loglevel'] = loglevel - if logfile: - init_logcfg['logfile'] = logfile - if logformat: - init_logcfg['logformat'] = logformat - - # Now enable logging with the merged settings - set_logging(check_logging_config({'logging': init_logcfg})) - - hostslist = [] - if hosts: - for host in list(hosts): - hostslist.append(verify_url_schema(host)) - else: - hostslist = None - - cli_client = prune_nones({ - 'hosts': hostslist, - 'cloud_id': cloud_id, - 'bearer_auth': bearer_auth, - 'opaque_id': opaque_id, - 'request_timeout': request_timeout, - 'http_compress': http_compress, - 'verify_certs': verify_certs, - 'ca_certs': ca_certs, - 'client_cert': client_cert, - 'client_key': client_key, - 'ssl_assert_hostname': ssl_assert_hostname, - 'ssl_assert_fingerprint': ssl_assert_fingerprint, - 'ssl_version': ssl_version - }) - - cli_other = prune_nones({ - 'master_only': master_only, - 'skip_version_test': skip_version_test, - 'username': username, - 'password': password, - 'api_key': { - 'id': id, - 'api_key': api_key, - 'token': api_token, - } - }) - # Remove `api_key` root key if `id` and `api_key` and `token` are all None - if id is None and api_key is None and api_token is None: - del cli_other['api_key'] - - # If hosts are in the config file, but cloud_id is specified at the command-line, - # we need to remove the hosts parameter as cloud_id and hosts are mutually exclusive - if cloud_id: - click.echo('cloud_id provided at CLI, superseding any other configured hosts') - client_args.hosts = None - cli_client.pop('hosts', None) - - # Likewise, if hosts are provided at the command-line, but cloud_id was in the config file, - # we need to remove the cloud_id parameter from the config file-based dictionary before merging - if hosts: - click.echo('hosts specified manually, superseding any other cloud_id or hosts') - client_args.hosts = None - client_args.cloud_id = None - cli_client.pop('cloud_id', None) - - # Update the objects if we have settings after pruning None values - if cli_client: - client_args.update_settings(cli_client) - if cli_other: - other_args.update_settings(cli_other) - - # Build a "final_config" that reflects CLI args overriding anything from a config_file + """ + Curator CLI (Singleton Tool) + + Run a single action from the command-line. + + The default $HOME/.curator/curator.yml configuration file (--config) + can be used but is not needed. + + Command-line settings will always override YAML configuration settings. + """ + ctx.obj = {} + ctx.obj['dry_run'] = dry_run + cfg = get_config(ctx.params, default_config_file()) + configure_logging(cfg, ctx.params) + client_args, other_args = get_args(ctx.params, cfg) final_config = { 'elasticsearch': { 'client': prune_nones(client_args.asdict()), 'other_settings': prune_nones(other_args.asdict()) } } - ctx.obj['config'] = final_config - ctx.obj['dry_run'] = dry_run + # Add the subcommands -cli.add_command(alias) -cli.add_command(allocation) -cli.add_command(close) -cli.add_command(delete_indices) -cli.add_command(delete_snapshots) -cli.add_command(forcemerge) -cli.add_command(open_indices) -cli.add_command(replicas) -cli.add_command(snapshot) -cli.add_command(restore) -cli.add_command(rollover) -cli.add_command(shrink) -cli.add_command(show_indices) -cli.add_command(show_snapshots) +curator_cli.add_command(alias) +curator_cli.add_command(allocation) +curator_cli.add_command(close) +curator_cli.add_command(delete_indices) +curator_cli.add_command(delete_snapshots) +curator_cli.add_command(forcemerge) +curator_cli.add_command(open_indices) +curator_cli.add_command(replicas) +curator_cli.add_command(snapshot) +curator_cli.add_command(restore) +curator_cli.add_command(rollover) +curator_cli.add_command(shrink) +curator_cli.add_command(show_indices) +curator_cli.add_command(show_snapshots) diff --git a/curator/snapshotlist.py b/curator/snapshotlist.py index 8d3deb81..ed1e95cd 100644 --- a/curator/snapshotlist.py +++ b/curator/snapshotlist.py @@ -1,6 +1,7 @@ """SnapshotList""" import re import logging +from es_client.helpers.schemacheck import SchemaCheck from curator.exceptions import ConfigurationError, FailedExecution, MissingArgument, NoSnapshots from curator.helpers.date_ops import ( absolute_date_range, date_range, fix_epoch, get_date_regex, get_point_of_reference, @@ -10,7 +11,6 @@ from curator.helpers.testers import repository_exists, verify_client_object from curator.helpers.utils import report_failure from curator.defaults import settings -from curator.validators import SchemaCheck from curator.validators.filter_functions import filterstructure class SnapshotList: diff --git a/curator/validators/__init__.py b/curator/validators/__init__.py index a83bc25c..e69de29b 100644 --- a/curator/validators/__init__.py +++ b/curator/validators/__init__.py @@ -1 +0,0 @@ -from curator.validators.schemacheck import SchemaCheck diff --git a/curator/validators/actions.py b/curator/validators/actions.py index 8f1364bb..07d1a4d1 100644 --- a/curator/validators/actions.py +++ b/curator/validators/actions.py @@ -1,8 +1,8 @@ """Validate root ``actions`` and individual ``action`` Schemas""" from voluptuous import Any, In, Schema, Optional, Required from six import string_types +from es_client.helpers.schemacheck import SchemaCheck from curator.defaults import settings -from curator.validators import SchemaCheck def root(): """ diff --git a/curator/validators/filter_functions.py b/curator/validators/filter_functions.py index 31c1d69c..6e1f3b36 100644 --- a/curator/validators/filter_functions.py +++ b/curator/validators/filter_functions.py @@ -1,10 +1,10 @@ """Functions validating the ``filter`` Schema of an ``action``""" import logging from voluptuous import Any, In, Required, Schema +from es_client.helpers.schemacheck import SchemaCheck from es_client.helpers.utils import prune_nones from curator.defaults import settings, filtertypes from curator.exceptions import ConfigurationError -from curator.validators import SchemaCheck logger = logging.getLogger(__name__) diff --git a/curator/validators/logconfig.py b/curator/validators/logconfig.py deleted file mode 100644 index 4972da7c..00000000 --- a/curator/validators/logconfig.py +++ /dev/null @@ -1,12 +0,0 @@ -"""Logging Schema definition""" -from voluptuous import Optional, Schema -from curator.defaults import logging_defaults - -def logging(): - """ - Pulls value from :py:func:`~.curator.defaults.logging_defaults.config_logging` - - :returns: ``{Optional('logging'): logging_defaults.config_logging()}`` - :rtype: :py:class:`~.voluptuous.schema_builder.Schema` - """ - return Schema({Optional('logging'): logging_defaults.config_logging()}) diff --git a/curator/validators/schemacheck.py b/curator/validators/schemacheck.py deleted file mode 100644 index 05dcc478..00000000 --- a/curator/validators/schemacheck.py +++ /dev/null @@ -1,83 +0,0 @@ -"""Schema checker""" -import logging -import re -from curator.exceptions import ConfigurationError - -class SchemaCheck(object): - def __init__(self, config, schema, test_what, location): - """ - Validate ``config`` with the provided :py:class:`~.voluptuous.schema_builder.Schema` from - ``schema``. ``test_what`` and ``location`` are for reporting the results, in case of - failure. If validation is successful, :py:meth:`result` returns ``config`` a valid - :py:class:`~.voluptuous.schema_builder.Schema`. - - :param config: A configuration dictionary. - :type config: dict - :param schema: A voluptuous schema definition - :type schema: :py:class:`~.voluptuous.schema_builder.Schema` - :param test_what: which configuration block is being validated - :type test_what: str - :param location: A string to report which configuration sub-block is being tested. - :type location: str - """ - self.loggit = logging.getLogger('curator.validators.SchemaCheck') - # Set the Schema for validation... - self.loggit.debug('Schema: %s', schema) - self.loggit.debug('"%s" config: %s', test_what, config) - #: Object attribute that gets the value of param ``config`` - self.config = config - #: Object attribute that gets the value of param ``schema`` - self.schema = schema - #: Object attribute that gets the value of param ``test_what`` - self.test_what = test_what - #: Object attribute that gets the value of param ``location`` - self.location = location - #: Object attribute that is only populated in :py:meth:`__parse_error` - self.badvalue = None - #: Object attribute that is only populated in :py:meth:`result` if an Exception is raised - self.error = None - - def __parse_error(self): - """ - Report the error, and try to report the bad key or value as well. - """ - def get_badvalue(data_string, data): - elements = re.sub(r'[\'\]]', '', data_string).split('[') - elements.pop(0) # Get rid of data as the first element - value = None - for k in elements: - try: - key = int(k) - except ValueError: - key = k - if value is None: - value = data[key] - # if this fails, it's caught below - return value - try: - self.badvalue = get_badvalue(str(self.error).split()[-1], self.config) - except Exception: - self.badvalue = '(could not determine)' - - def result(self): - """ - Evaluate :py:attr:`config` using :py:attr:`schema`. Try to parse and log the error if - validation fails, then raise a :py:exc:`~.curator.exceptions.ConfigurationError` - - :returns: A validated :py:class:`~.voluptuous.schema_builder.Schema` based on - :py:attr:`config` - """ - try: - return self.schema(self.config) - except Exception as err: - try: - # pylint: disable=E1101 - self.error = err.errors[0] - except Exception: - self.error = f'{err}' - self.__parse_error() - self.loggit.error('Schema error: %s', self.error) - raise ConfigurationError( - f'Configuration: {self.test_what}: Location: {self.location}: Bad Value: ' - f'"{self.badvalue}", {self.error}. Check configuration file.' - ) from err diff --git a/docs/Changelog.rst b/docs/Changelog.rst index 40b7328a..547cf0c6 100644 --- a/docs/Changelog.rst +++ b/docs/Changelog.rst @@ -3,6 +3,33 @@ Changelog ========= +8.0.9 (31 January 2024) +----------------------- + +**Announcements** + +Curator is improving command-line options using new defaults and helpers from +from the ``es_client`` module. This will make things appear a bit cleaner at +the command-line as well as normalize command-line structure between projects +using ``es_client``. No more reimplementing the same code in 5 different +projects! + +**Changes** + + * Fix Docker logging per #1694. It should detect whether that path exists and + that the process has write permissions before blindly attempting to use it. + * If ``--config`` is not specified Curator will now assume you either mean to + use CLI options exclusively or look for a config in the default location. + Curator will not halt on the absence of ``--config`` any more, per #1698 + * Increment Dockerfile settings to ``python:3.11.7-alpine3.18`` + * Some command-line options are hidden by default now but remain usable. The + help output explains how to see the full list, if needed. + * Dependency bumps + * As ``es_client`` covers all of the same upstream dependencies that were + necessary in previous releases, all local dependencies have been erased + in favor of that one. For this release, that is ``es_client==8.12.3`` + + 8.0.8 (21 July 2023) -------------------- diff --git a/docs/asciidoc/command-line.asciidoc b/docs/asciidoc/command-line.asciidoc index 45b36669..195afb91 100644 --- a/docs/asciidoc/command-line.asciidoc +++ b/docs/asciidoc/command-line.asciidoc @@ -53,40 +53,40 @@ The help output looks like this: $ curator --help Usage: curator [OPTIONS] ACTION_FILE - Curator for Elasticsearch indices. + Curator for Elasticsearch indices - See http://elastic.co/guide/en/elasticsearch/client/curator/current + The default $HOME/.curator/curator.yml configuration file (--config) can be used but is not needed. + + Command-line settings will always override YAML configuration settings. + + Some less-frequently used client configuration options are now hidden. To see the full list, run: + + curator_cli -h Options: --config PATH Path to configuration file. - --hosts TEXT Elasticsearch URL to connect to - --cloud_id TEXT Shorthand to connect to Elastic Cloud instance + --hosts TEXT Elasticsearch URL to connect to. + --cloud_id TEXT Elastic Cloud instance id + --api_token TEXT The base64 encoded API Key token --id TEXT API Key "id" value --api_key TEXT API Key "api_key" value - --username TEXT Username used to create "basic_auth" tuple - --password TEXT Password used to create "basic_auth" tuple - --bearer_auth TEXT - --opaque_id TEXT + --username TEXT Elasticsearch username + --password TEXT Elasticsearch password --request_timeout FLOAT Request timeout in seconds - --http_compress Enable HTTP compression - --verify_certs Verify SSL/TLS certificate(s) + --verify_certs / --no-verify_certs + Verify SSL/TLS certificate(s) [default: verify_certs] --ca_certs TEXT Path to CA certificate file or directory --client_cert TEXT Path to client certificate file - --client_key TEXT Path to client certificate key - --ssl_assert_hostname TEXT Hostname or IP address to verify on the node's certificate. - --ssl_assert_fingerprint TEXT SHA-256 fingerprint of the node's certificate. If this value is given then root-of-trust - verification isn't done and only the node's certificate fingerprint is verified. - --ssl_version TEXT Minimum acceptable TLS/SSL version - --master-only Only run if the single host provided is the elected master - --skip_version_test Do not check the host version + --client_key TEXT Path to client key file --dry-run Do not perform any changes. --loglevel [DEBUG|INFO|WARNING|ERROR|CRITICAL] Log level - --logfile TEXT log file - --logformat [default|logstash|json|ecs] - Log output format - --version Show the version and exit. - --help Show this message and exit. + --logfile TEXT Log file + --logformat [default|ecs] Log output format + -v, --version Show the version and exit. + -h, --help Show this message and exit. + + Learn more at https://www.elastic.co/guide/en/elasticsearch/client/curator/8.0/command-line.html ------- You can use <> in your configuration files. @@ -95,20 +95,23 @@ You can use <> in your configuration files. Running Curator from the command-line using Docker requires only a few additional steps. -Docker-based Curator requires you to map a volume for your configuration files. Neglecting -to volume map your configuration directory to `/.curator` and attempting to pass options at the -command-line will not work. +Should you desire to use them, Docker-based Curator requires you to map a volume for your +configuration and/or log files. Attempting to read a YAML configuration file if you have +neglected to volume map your configuration directory to `/.curator` will not work. It looks like this: [source,sh] ------- -docker run --rm --name myimagename \ - -v /PATH/TO/MY/CONFIGS:/.curator \ - untergeek/curator:mytag \ +docker run [-t] --rm --name myimagename \ + -v /PATH/TO/MY/CONFIGS:/.curator \ + untergeek/curator:mytag \ --config /.curator/config.yml /.curator/actionfile.yml ------- +NOTE: While testing, adding the `-t` flag will allocate a pseudo-tty, allowing you to see terminal + output that would otherwise be hidden. + Both of the files `config.yml` and `actionfile.yml` should already exist in the path `/PATH/TO/MY/CONFIGS` before run time. @@ -136,37 +139,48 @@ IMPORTANT: While both the configuration file and the command-line arguments can $ curator_cli --help Usage: curator_cli [OPTIONS] COMMAND [ARGS]... + Curator CLI (Singleton Tool) + + Run a single action from the command-line. + + The default $HOME/.curator/curator.yml configuration file (--config) can be used but is not needed. + + Command-line settings will always override YAML configuration settings. + Options: --config PATH Path to configuration file. - --hosts TEXT Elasticsearch URL to connect to - --cloud_id TEXT Shorthand to connect to Elastic Cloud instance + --hosts TEXT Elasticsearch URL to connect to. + --cloud_id TEXT Elastic Cloud instance id --api_token TEXT The base64 encoded API Key token --id TEXT API Key "id" value --api_key TEXT API Key "api_key" value - --username TEXT Username used to create "basic_auth" tuple - --password TEXT Password used to create "basic_auth" tuple - --bearer_auth TEXT - --opaque_id TEXT + --username TEXT Elasticsearch username + --password TEXT Elasticsearch password + --bearer_auth TEXT Bearer authentication token + --opaque_id TEXT X-Opaque-Id HTTP header value --request_timeout FLOAT Request timeout in seconds - --http_compress Enable HTTP compression - --verify_certs Verify SSL/TLS certificate(s) + --http_compress / --no-http_compress + Enable HTTP compression [default: no-http_compress] + --verify_certs / --no-verify_certs + Verify SSL/TLS certificate(s) [default: verify_certs] --ca_certs TEXT Path to CA certificate file or directory --client_cert TEXT Path to client certificate file - --client_key TEXT Path to client certificate key + --client_key TEXT Path to client key file --ssl_assert_hostname TEXT Hostname or IP address to verify on the node's certificate. --ssl_assert_fingerprint TEXT SHA-256 fingerprint of the node's certificate. If this value is given then root-of-trust verification isn't done and only the node's certificate fingerprint is verified. --ssl_version TEXT Minimum acceptable TLS/SSL version - --master-only Only run if the single host provided is the elected master - --skip_version_test Do not check the host version + --master-only / --no-master-only + Only run if the single host provided is the elected master [default: no-master-only] + --skip_version_test / --no-skip_version_test + Elasticsearch version compatibility check [default: no-skip_version_test] --dry-run Do not perform any changes. --loglevel [DEBUG|INFO|WARNING|ERROR|CRITICAL] Log level - --logfile TEXT log file - --logformat [default|logstash|json|ecs] - Log output format - --version Show the version and exit. - --help Show this message and exit. + --logfile TEXT Log file + --logformat [default|ecs] Log output format + -v, --version Show the version and exit. + -h, --help Show this message and exit. Commands: alias Add/Remove Indices to/from Alias @@ -183,11 +197,41 @@ Commands: show-snapshots Show Snapshots shrink Shrink Indices to --number_of_shards snapshot Snapshot Indices + + Learn more at https://www.elastic.co/guide/en/elasticsearch/client/curator/8.0/singleton-cli.html --------- The option flags for the given commands match those used for the same <>. The only difference is how filtering is handled. +=== Running Curator from Docker + +Running `curator_cli` from the command-line using Docker requires only a few additional steps. + +Should you desire to use them, Docker-based `curator_cli` requires you to map a volume for your +configuration and/or log files. Attempting to read a YAML configuration file if you have +neglected to volume map your configuration directory to `/.curator` will not work. + +It looks like this: + +[source,sh] +------- +docker run [-t] --rm --name myimagename \ + --entrypoint /curator/curator_cli \ + -v /PATH/TO/MY/CONFIGS:/.curator \ + untergeek/curator:mytag \ + --config /.curator/config.yml [OPTIONS] COMMAND [ARGS]... +------- + +NOTE: While testing, adding the `-t` flag will allocate a pseudo-tty, allowing you to see terminal + output that would otherwise be hidden. + +The `config.yml` file should already exist in the path `/PATH/TO/MY/CONFIGS` before run time. + +The `--rm` in the command means that the container (not the image) will be deleted after +completing execution. You definitely want this as there is no reason to keep creating +containers for each run. The eventual cleanup from this would be unpleasant. + === Command-line filtering Recent improvements in Curator include schema and setting validation. With @@ -246,6 +290,8 @@ Options: --filter_list TEXT JSON string representing an array of filters. [required] --help Show this message and exit. + + Learn more at https://www.elastic.co/guide/en/elasticsearch/client/curator/8.0/singleton-cli.html#_show_indicessnapshots ----------- [source,sh] @@ -260,6 +306,8 @@ Options: --filter_list TEXT JSON string representing an array of filters. [required] --help Show this message and exit. + + Learn more at https://www.elastic.co/guide/en/elasticsearch/client/curator/8.0/singleton-cli.html#_show_indicessnapshots ----------- The `show-snapshots` command will only show snapshots matching the provided diff --git a/docs/asciidoc/index.asciidoc b/docs/asciidoc/index.asciidoc index 79f0d3d0..75913364 100644 --- a/docs/asciidoc/index.asciidoc +++ b/docs/asciidoc/index.asciidoc @@ -1,11 +1,11 @@ -:curator_version: 8.0.8 +:curator_version: 8.0.9 :curator_major: 8 :curator_doc_tree: 8.0 -:es_py_version: 8.8.2 -:es_doc_tree: 8.8 -:stack_doc_tree: 8.8 -:pybuild_ver: 3.11.4 -:copyright_years: 2011-2023 +:es_py_version: 8.12.0 +:es_doc_tree: 8.12 +:stack_doc_tree: 8.12 +:pybuild_ver: 3.11.7 +:copyright_years: 2011-2024 :ref: http://www.elastic.co/guide/en/elasticsearch/reference/{es_doc_tree} :esref: http://www.elastic.co/guide/en/elasticsearch/reference/{stack_doc_tree} :fbref: https://www.elastic.co/guide/en/beats/filebeat/{stack_doc_tree} diff --git a/docs/conf.py b/docs/conf.py index bfbc6692..cd744fa6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -72,8 +72,8 @@ intersphinx_mapping = { 'python': ('https://docs.python.org/3.11', None), - 'es_client': ('https://es-client.readthedocs.io/en/v8.8.2', None), - 'elasticsearch8': ('https://elasticsearch-py.readthedocs.io/en/v8.8.2', None), + 'es_client': ('https://es-client.readthedocs.io/en/v8.12.3', None), + 'elasticsearch8': ('https://elasticsearch-py.readthedocs.io/en/v8.12.0', None), 'voluptuous': ('http://alecthomas.github.io/voluptuous/docs/_build/html', None), 'click': ('https://click.palletsprojects.com/en/8.1.x', None), } diff --git a/docs/defaults.rst b/docs/defaults.rst index 58f859bb..90a92138 100644 --- a/docs/defaults.rst +++ b/docs/defaults.rst @@ -19,13 +19,6 @@ Filter Types .. automodule:: curator.defaults.filtertypes :members: -.. _defaults_logging: - -Logging Defaults -================ - -.. automodule:: curator.defaults.logging_defaults - :members: .. _defaults_options: diff --git a/docs/helpers.rst b/docs/helpers.rst index 5f517003..6c2acde0 100644 --- a/docs/helpers.rst +++ b/docs/helpers.rst @@ -41,6 +41,7 @@ Date Ops .. autofunction:: parse_datemath + .. _helpers_getters: Getters @@ -50,10 +51,12 @@ Getters .. autofunction:: byte_size -.. autofunction:: get_client +.. autofunction:: get_alias_actions .. autofunction:: get_data_tiers +.. autofunction:: get_frozen_prefix + .. autofunction:: get_indices .. autofunction:: get_repository @@ -62,6 +65,8 @@ Getters .. autofunction:: get_snapshot_data +.. autofunction:: get_tier_preference + .. autofunction:: get_write_index .. autofunction:: index_size diff --git a/docs/other_modules.rst b/docs/other_modules.rst index 8e95cc7f..9aa57118 100644 --- a/docs/other_modules.rst +++ b/docs/other_modules.rst @@ -10,10 +10,6 @@ Other Modules .. autofunction:: process_action -.. autofunction:: override_logging - -.. autofunction:: cli_hostslist - .. autofunction:: ilm_action_skip .. autofunction:: exception_handler @@ -81,34 +77,6 @@ Other Modules :type logformat: str :type action_file: str -``curator.config_utils`` -======================== - -.. py:module:: curator.config_utils - -.. autofunction:: check_logging_config - -.. autofunction:: set_logging - -.. autofunction:: password_filter - -``curator.logtools`` -==================== - -.. py:module:: curator.logtools - -.. autofunction:: de_dot - -.. autofunction:: deepmerge - -.. autoclass:: LogInfo - -``curator.logtools.LogstashFormatter`` --------------------------------------- - -This inherits from :py:class:`logging.Formatter`, so some of what you see documented is inherited. - -.. autoclass:: LogstashFormatter ``curator.repomgrcli`` ====================== diff --git a/docs/usage.rst b/docs/usage.rst index 7cc8c080..4ff35b22 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -46,17 +46,18 @@ Logging ======= Elasticsearch Curator uses the standard `logging library`_ from Python. It inherits the -``elastic_transport`` logger from ``elasticsearch-py``. Clients use the ``elastic_transport`` -logger to log standard activity, depending on the log level. +``ecs-logging`` formatting module from ``es_client``, which inherits the ``elastic_transport`` +logger from ``elasticsearch8``. Clients use the ``elastic_transport`` logger to log standard +activity, depending on the log level. -It is recommended to use :py:class:`~.curator.config_utils.set_logging` to enable logging, as this -has been provided for you. +It is recommended to use :py:class:`~.es_client.helpers.logging.set_logging` to enable +logging, as this has been provided for you. This is quite simple: .. code-block:: python - from curator.config_utils import set_logging + from es_client.helpers.logging import set_logging import logging LOG = { @@ -86,9 +87,9 @@ and ``CRITICAL``. The setting ``logfile`` must be ``None`` or a path to a writeable file. If ``None``, it will log to ``STDOUT``. -Available settings for ``logformat`` are: ``default``, ``json``, ``logstash``, and ``ecs``. The -options ``json`` and ``logstash`` are synonymous. The ``ecs`` option uses -`the Python ECS Log Formatter`_ and is great if you plan on ingesting your logs into Elasticsearch. +Available settings for ``logformat`` are: ``default``, ``json``, and ``ecs``. The ``ecs`` option +uses `the Python ECS Log Formatter`_ and is great if you plan on ingesting your logs into +Elasticsearch. Blacklisting logs by way of the ``blacklist`` setting should remain configured with the defaults (``['elastic_transport', 'urllib3']``), unless you are troubleshooting a connection issue. The diff --git a/docs/validators.rst b/docs/validators.rst index a5b9735b..f78c5877 100644 --- a/docs/validators.rst +++ b/docs/validators.rst @@ -3,12 +3,6 @@ Validators ########## -SchemaCheck -=========== - -.. autoclass:: curator.validators.SchemaCheck - :members: - Actions ======= @@ -26,10 +20,3 @@ Filter Functions .. automodule:: curator.validators.filter_functions :members: - -Log Config -========== - -.. automodule:: curator.validators.logconfig - :members: - diff --git a/pyproject.toml b/pyproject.toml index 4e8b4dff..28311821 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,14 +28,7 @@ keywords = [ 'index-expiry' ] dependencies = [ - "elasticsearch8==8.8.2", - "es_client==8.8.2.post1", - "ecs-logging==2.0.2", - "click==8.1.4", - "pyyaml==6.0.1", - "voluptuous>=0.13.1", - "certifi>=2023.5.7", - "six>=1.16.0", + "es_client==8.12.3" ] [project.optional-dependencies] @@ -49,7 +42,7 @@ doc = ["sphinx", "sphinx_rtd_theme"] [project.scripts] curator = "curator.cli:cli" -curator_cli = "curator.curator_cli:main" +curator_cli = "curator.singletons:curator_cli" es_repo_mgr = "curator.repomgrcli:repo_mgr_cli" [project.urls] @@ -93,7 +86,7 @@ dependencies = [ [tool.hatch.envs.test.scripts] step0 = "$(docker_test/scripts/destroy.sh 2&>1 /dev/null)" -step1 = "step0 ; echo 'Starting test environment in Docker...' ; $(AUTO_EXPORT=y docker_test/scripts/create.sh 8.6.1 2&>1 /dev/null)" +step1 = "step0 ; echo 'Starting test environment in Docker...' ; $(AUTO_EXPORT=y docker_test/scripts/create.sh 8.12.1 2&>1 /dev/null)" step2 = "step1 ; source docker_test/curatortestenv; echo 'Running tests:'" step3 = "step2 ; pytest ; EXITCODE=$?" step4 = "step3 ; echo 'Tests complete! Destroying Docker test environment...' " @@ -103,7 +96,7 @@ run = "run-coverage --no-cov" [[tool.hatch.envs.test.matrix]] python = ["3.9", "3.10", "3.11"] -version = ["8.0.5"] +version = ["8.0.9"] [tool.pytest.ini_options] pythonpath = [".", "curator"] diff --git a/run_singleton.py b/run_singleton.py index 56927315..d8e99de3 100755 --- a/run_singleton.py +++ b/run_singleton.py @@ -19,11 +19,11 @@ """ import sys import click -from curator.singletons import cli +from curator.singletons import curator_cli if __name__ == '__main__': try: - cli(obj={}) + curator_cli() except RuntimeError as err: click.echo(f'{err}') sys.exit(1) diff --git a/tests/integration/test_cli.py b/tests/integration/test_cli.py index 95e2df08..e124af82 100644 --- a/tests/integration/test_cli.py +++ b/tests/integration/test_cli.py @@ -24,7 +24,7 @@ def test_cli_client_config(self): def test_cli_unreachable_cloud_id(self): self.create_indices(10) self.write_config(self.args['actionfile'], testvars.disabled_proto.format('close', 'delete_indices')) - self.invoke_runner_alt(hosts='http://127.0.0.2:9200', cloud_id='abc:def', username='user', password='pass') + self.invoke_runner_alt(cloud_id='abc:def', username='user', password='pass') assert 1 == self.result.exit_code def test_no_config(self): # This test checks whether localhost:9200 is provided if no hosts or @@ -36,7 +36,7 @@ def test_no_config(self): if HOST == 'http://127.0.0.1:9200': localtest = True self.create_indices(10) - self.write_config(self.args['configfile'], ' \n') # Empty file. + self.write_config(self.args['configfile'], '---\n') # Empty YAML file. self.write_config(self.args['actionfile'], testvars.disabled_proto.format('close', 'delete_indices')) self.invoke_runner() if localtest: diff --git a/tests/integration/test_es_repo_mgr.py b/tests/integration/test_es_repo_mgr.py index 652d3a12..4acb36f9 100644 --- a/tests/integration/test_es_repo_mgr.py +++ b/tests/integration/test_es_repo_mgr.py @@ -146,7 +146,8 @@ def test_delete_repository_notfound(self): class TestCLIShowRepositories(CuratorTestCase): def test_show_repository(self): self.create_repository() - self.write_config(self.args['configfile'], testvars.client_conf_logfile.format(HOST, os.devnull)) + self.write_config( + self.args['configfile'], testvars.client_conf_logfile.format(HOST, os.devnull)) test = clicktest.CliRunner() result = test.invoke( repo_mgr_cli, @@ -155,4 +156,5 @@ def test_show_repository(self): 'show' ] ) - assert self.args['repository'] == result.output.rstrip() + # The splitlines()[-1] allows me to only capture the last line and ignore other output + assert self.args['repository'] == result.output.splitlines()[-1] diff --git a/tests/unit/test_class_index_list.py b/tests/unit/test_class_index_list.py index 9ef41f03..c2624b72 100644 --- a/tests/unit/test_class_index_list.py +++ b/tests/unit/test_class_index_list.py @@ -4,6 +4,7 @@ from copy import deepcopy from mock import Mock import yaml +from es_client.exceptions import FailedValidation from curator.exceptions import ActionError, ConfigurationError, FailedExecution, MissingArgument, NoIndices from curator.helpers.date_ops import fix_epoch from curator import IndexList @@ -551,12 +552,12 @@ def test_no_filtertype(self): self.builder(key='4') config = {'filters': [{'no_filtertype':'fail'}]} self.assertRaises( - ConfigurationError, self.ilo.iterate_filters, config) + FailedValidation, self.ilo.iterate_filters, config) def test_invalid_filtertype(self): self.builder(key='4') config = {'filters': [{'filtertype':12345.6789}]} self.assertRaises( - ConfigurationError, self.ilo.iterate_filters, config) + FailedValidation, self.ilo.iterate_filters, config) def test_pattern_filtertype(self): self.builder(key='4') config = yaml.load(testvars.pattern_ft, Loader=yaml.FullLoader)['actions'][1] @@ -614,7 +615,7 @@ def test_none_filtertype(self): def test_unknown_filtertype_raises(self): self.builder() config = yaml.load(testvars.invalid_ft, Loader=yaml.FullLoader)['actions'][1] - self.assertRaises(ConfigurationError, self.ilo.iterate_filters, config) + self.assertRaises(FailedValidation, self.ilo.iterate_filters, config) def test_ilm_filtertype_exclude(self): self.builder() # If we don't deepcopy, then it munges the settings for future references. diff --git a/tests/unit/test_class_snapshot_list.py b/tests/unit/test_class_snapshot_list.py index e4d07cb2..79758aed 100644 --- a/tests/unit/test_class_snapshot_list.py +++ b/tests/unit/test_class_snapshot_list.py @@ -2,6 +2,7 @@ from unittest import TestCase from mock import Mock import yaml +from es_client.exceptions import FailedValidation from curator import SnapshotList from curator.exceptions import ConfigurationError, FailedExecution, MissingArgument, NoSnapshots # Get test variables and constants from a single source @@ -307,7 +308,7 @@ def test_no_filtertype(self): slo = SnapshotList(client, repository=testvars.repo_name) config = {'filters': [{'no_filtertype':'fail'}]} self.assertRaises( - ConfigurationError, slo.iterate_filters, config) + FailedValidation, slo.iterate_filters, config) def test_invalid_filtertype_class(self): client = Mock() client.snapshot.get.return_value = testvars.snapshots @@ -315,7 +316,7 @@ def test_invalid_filtertype_class(self): slo = SnapshotList(client, repository=testvars.repo_name) config = {'filters': [{'filtertype':12345.6789}]} self.assertRaises( - ConfigurationError, slo.iterate_filters, config) + FailedValidation, slo.iterate_filters, config) def test_invalid_filtertype(self): client = Mock() client.snapshot.get.return_value = testvars.snapshots @@ -323,7 +324,7 @@ def test_invalid_filtertype(self): slo = SnapshotList(client, repository=testvars.repo_name) config = yaml.load(testvars.invalid_ft, Loader=yaml.FullLoader)['actions'][1] self.assertRaises( - ConfigurationError, + FailedValidation, slo.iterate_filters, config ) def test_age_filtertype(self): diff --git a/tests/unit/test_cli_methods.py b/tests/unit/test_cli_methods.py deleted file mode 100644 index 70a466b1..00000000 --- a/tests/unit/test_cli_methods.py +++ /dev/null @@ -1,39 +0,0 @@ -"""test_cli_methods""" -import logging -from unittest import TestCase -from curator.logtools import LogInfo -# from . import CLITestCase -from . import testvars - -class TestCLI_A(TestCase): - def test_loginfo_defaults(self): - loginfo = LogInfo({}) - self.assertEqual(20, loginfo.numeric_log_level) - self.assertEqual(testvars.default_format, loginfo.format_string) - def test_loginfo_debug(self): - loginfo = LogInfo({"loglevel": "DEBUG"}) - self.assertEqual(10, loginfo.numeric_log_level) - self.assertEqual(testvars.debug_format, loginfo.format_string) - def test_loginfo_bad_level_raises(self): - self.assertRaises(ValueError, LogInfo, {"loglevel": "NOTALOGLEVEL"}) - def test_loginfo_logstash_formatter(self): - loginfo = LogInfo({"logformat": "logstash"}) - logging.root.addHandler(loginfo.handler) - logging.root.setLevel(loginfo.numeric_log_level) - logger = logging.getLogger('testing') - logger.info('testing') - self.assertEqual(20, loginfo.numeric_log_level) - -# class TestCLI_B(CLITestCase): -# def test_read_file_pass(self): -# cfg = curator.get_yaml(self.args['yamlfile']) -# self.assertEqual('http://127.0.0.1:9200', cfg['client']['hosts']) -# def test_read_file_corrupt_fail(self): -# with self.assertRaises(SystemExit) as get: -# curator.get_yaml(self.args['invalid_yaml']) -# self.assertEqual(get.exception.code, 1) -# def test_read_file_missing_fail(self): -# self.assertRaises( -# curator.FailedExecution, -# curator.read_file, self.args['no_file_here'] -# ) diff --git a/tests/unit/test_settings.py b/tests/unit/test_settings.py new file mode 100644 index 00000000..5b16c7f0 --- /dev/null +++ b/tests/unit/test_settings.py @@ -0,0 +1,24 @@ +"""Unit testing for helpers.creators functions""" +from unittest import TestCase +import pytest +from curator.defaults.settings import CURATOR_DOCS, footer +from curator.exceptions import CuratorException +from curator._version import __version__ + +class TestFooter(TestCase): + """Test defaults.settings.footer functionality.""" + mytail = 'tail.html' + ver = __version__.split('.') + majmin = f'{ver[0]}.{ver[1]}' + def test_basic_functionality(self): + """Should return a URL with the major/minor version in the path""" + expected = f'Learn more at {CURATOR_DOCS}/{self.majmin}/{self.mytail}' + assert expected == footer(__version__, tail=self.mytail) + def test_raises_with_nonstring(self): + """Should raise an exception if a non-string value is passed as version""" + with pytest.raises(CuratorException): + footer(1234) + def test_raises_with_unsplittable_string(self): + """Should raise an exception if a non-period delimited string value is passed as version""" + with pytest.raises(CuratorException): + footer('invalid') diff --git a/tests/unit/test_validators.py b/tests/unit/test_validators.py index 5de45077..57f71378 100644 --- a/tests/unit/test_validators.py +++ b/tests/unit/test_validators.py @@ -1,8 +1,9 @@ """Test filter and schema validators""" from unittest import TestCase from voluptuous import Schema +from es_client.exceptions import FailedValidation +from es_client.helpers.schemacheck import SchemaCheck from curator.exceptions import ConfigurationError -from curator.validators import SchemaCheck from curator.validators.filter_functions import validfilters, singlefilter @@ -167,7 +168,7 @@ def test_space_name_age_no_ts(self): 'filters', 'testing' ) - self.assertRaises(ConfigurationError, schema.result) + self.assertRaises(FailedValidation, schema.result) def test_space_field_stats_age(self): action = 'delete_indices' config = [ @@ -198,7 +199,7 @@ def test_space_field_stats_age_no_field(self): 'filters', 'testing' ) - self.assertRaises(ConfigurationError, schema.result) + self.assertRaises(FailedValidation, schema.result) def test_space_creation_date_age(self): action = 'delete_indices' config = [