curator/cli.py (216 lines of code) (raw):
"""Main CLI for Curator"""
import sys
import logging
import click
from es_client.defaults import OPTION_DEFAULTS
from es_client.helpers.config import (
cli_opts,
context_settings,
generate_configdict,
get_client,
get_config,
options_from_dict,
)
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.defaults.settings import (
CLICK_DRYRUN,
VERSION_MAX,
VERSION_MIN,
default_config_file,
footer,
snapshot_actions,
)
from curator.exceptions import NoIndices, NoSnapshots
from curator.helpers.testers import ilm_policy_check
from curator._version import __version__
ONOFF = {'on': '', 'off': 'no-'}
click_opt_wrap = option_wrapper()
# pylint: disable=R0913, R0914, W0613, W0622, W0718
def ilm_action_skip(client, action_def):
"""
Skip rollover action if ``allow_ilm_indices`` is ``false``. For all other
non-snapshot actions, add the ``ilm`` filtertype to the
:py:attr:`~.curator.ActionDef.filters` list.
:param action_def: An action object
:type action_def: :py:class:`~.curator.classdef.ActionDef`
:returns: ``True`` if ``action_def.action`` is ``rollover`` and the alias
identified by ``action_def.options['name']`` is associated with an ILM
policy. This hacky work-around is because the Rollover action does not
use :py:class:`~.curator.IndexList`
:rtype: bool
"""
logger = logging.getLogger(__name__)
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'],
)
return True
elif action_def.filters:
action_def.filters.append({'filtertype': 'ilm'})
else:
action_def.filters = [{'filtertype': 'ilm'}]
return False
def exception_handler(action_def, err):
"""Do the grunt work with the exception
:param action_def: An action object
:param err: The exception
:type action_def: :py:class:`~.curator.classdef.ActionDef`
:type err: :py:exc:`Exception`
"""
logger = logging.getLogger(__name__)
if isinstance(err, (NoIndices, NoSnapshots)):
if action_def.iel:
logger.info(
'Skipping action "%s" due to empty list: %s',
action_def.action,
type(err),
)
else:
logger.error(
'Unable to complete action "%s". No actionable items in list: %s',
action_def.action,
type(err),
)
sys.exit(1)
else:
logger.error(
'Failed to complete action: %s. %s: %s', action_def.action, type(err), err
)
if action_def.cif:
logger.info(
'Continuing execution with next action because "continue_if_exception" '
'is set to True for action %s',
action_def.action,
)
else:
sys.exit(1)
def process_action(client, action_def, dry_run=False):
"""
Do the ``action`` in ``action_def.action``, using the associated options and
any ``kwargs``.
:param client: A client connection object
:param action_def: The ``action`` object
:type client: :py:class:`~.elasticsearch.Elasticsearch`
:type action_def: :py:class:`~.curator.classdef.ActionDef`
:rtype: None
"""
logger = logging.getLogger(__name__)
logger.debug('Configuration dictionary: %s', action_def.action_dict)
mykwargs = {}
logger.debug('INITIAL Action kwargs: %s', mykwargs)
# Add some settings to mykwargs...
if action_def.action == 'delete_indices':
mykwargs['master_timeout'] = 30
# Update the defaults with whatever came with opts, minus any Nones
mykwargs.update(prune_nones(action_def.options))
# Pop out the search_pattern option, if present.
ptrn = mykwargs.pop('search_pattern', '*')
hidn = mykwargs.pop('include_hidden', False)
logger.debug('Action kwargs: %s', mykwargs)
logger.debug('Post search_pattern & include_hidden Action kwargs: %s', mykwargs)
# Set up the action
logger.debug('Running "%s"', action_def.action.upper())
if action_def.action == 'alias':
# Special behavior for this action, as it has 2 index lists
action_def.instantiate('action_cls', **mykwargs)
action_def.instantiate(
'alias_adds', client, search_pattern=ptrn, include_hidden=hidn
)
action_def.instantiate(
'alias_removes', client, search_pattern=ptrn, include_hidden=hidn
)
if 'remove' in action_def.action_dict:
logger.debug('Removing indices from alias "%s"', action_def.options['name'])
action_def.alias_removes.iterate_filters(action_def.action_dict['remove'])
action_def.action_cls.remove(
action_def.alias_removes,
warn_if_no_indices=action_def.options['warn_if_no_indices'],
)
if 'add' in action_def.action_dict:
logger.debug('Adding indices to alias "%s"', action_def.options['name'])
action_def.alias_adds.iterate_filters(action_def.action_dict['add'])
action_def.action_cls.add(
action_def.alias_adds,
warn_if_no_indices=action_def.options['warn_if_no_indices'],
)
elif action_def.action in ['cluster_routing', 'create_index', 'rollover']:
action_def.instantiate('action_cls', client, **mykwargs)
else:
if action_def.action in ['delete_snapshots', 'restore']:
mykwargs.pop('repository') # We don't need to send this value to the action
action_def.instantiate(
'list_obj', client, repository=action_def.options['repository']
)
else:
action_def.instantiate(
'list_obj', client, search_pattern=ptrn, include_hidden=hidn
)
action_def.list_obj.iterate_filters({'filters': action_def.filters})
logger.debug(f'Pre Instantiation Action kwargs: {mykwargs}')
action_def.instantiate('action_cls', action_def.list_obj, **mykwargs)
# Do the action
if dry_run:
action_def.action_cls.do_dry_run()
else:
logger.debug('Doing the action here.')
action_def.action_cls.do_action()
def run(ctx: click.Context) -> None:
"""
:param ctx: The Click command context
:type ctx: :py:class:`Context <click.Context>`
Called by :py:func:`cli` to execute what was collected at the command-line
"""
logger = logging.getLogger(__name__)
logger.debug('action_file: %s', ctx.params['action_file'])
all_actions = ActionsFile(ctx.params['action_file'])
for idx in sorted(list(all_actions.actions.keys())):
action_def = all_actions.actions[idx]
# Skip to next action if 'disabled'
if action_def.disabled:
logger.info(
'Action ID: %s: "%s" not performed because "disable_action" '
'is set to True',
idx,
action_def.action,
)
continue
logger.info('Preparing Action ID: %s, "%s"', idx, action_def.action)
# Override the timeout, if specified, otherwise use the default.
if action_def.timeout_override:
ctx.obj['configdict']['elasticsearch']['client'][
'request_timeout'
] = action_def.timeout_override
# Create a client object for each action...
logger.info('Creating client object and testing connection')
try:
client = get_client(
configdict=ctx.obj['configdict'],
version_max=VERSION_MAX,
version_min=VERSION_MIN,
)
except ClientException as exc:
# No matter where logging is set to go, make sure we dump these messages to
# the CLI
click.echo('Unable to establish client connection to Elasticsearch!')
click.echo(f'Exception: {exc}')
sys.exit(1)
except Exception as other:
logger.debug('Fatal exception encountered: %s', other)
# Filter ILM indices unless expressly permitted
if ilm_action_skip(client, action_def):
continue
#
# Process the action
#
msg = (
f'Trying Action ID: {idx}, "{action_def.action}": {action_def.description}'
)
try:
logger.info(msg)
process_action(client, action_def, dry_run=ctx.params['dry_run'])
except Exception as err:
exception_handler(action_def, err)
logger.info('Action ID: %s, "%s" completed.', idx, action_def.action)
logger.info('All actions completed.')
@click.command(
context_settings=context_settings(),
epilog=footer(__version__, tail='command-line.html'),
)
@options_from_dict(OPTION_DEFAULTS)
@click_opt_wrap(*cli_opts('dry-run', settings=CLICK_DRYRUN))
@click.argument('action_file', type=click.Path(exists=True), nargs=1)
@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,
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,
loglevel,
logfile,
logformat,
blacklist,
dry_run,
action_file,
):
"""
Curator for Elasticsearch indices
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
"""
ctx.obj = {}
ctx.obj['default_config'] = default_config_file()
get_config(ctx)
configure_logging(ctx)
generate_configdict(ctx)
run(ctx)