Coverage for curator/classdef.py: 95%

1"""Other Classes"""

2 import logging

3 from es_client.exceptions import ConfigurationError as ESclient_ConfigError

4 from es_client.helpers.utils import get_yaml

5 from curator import IndexList, SnapshotList

6 from curator.actions import CLASS_MAP

7 from curator.exceptions import ConfigurationError

8 from curator.config_utils import password_filter

9 from curator.helpers.testers import validate_actions

10

11# Let me tell you the story of the nearly wasted afternoon and the research that went into this

12# seemingly simple work-around. Actually, no. It's even more wasted time writing that story here.

13# Suffice to say that I couldn't use the CLASS_MAP with class objects to directly map them to class

14# instances. The Wrapper class and the ActionDef.instantiate method do all of the work for me,

15# allowing me to easily and cleanly pass *args and **kwargs to the individual action classes of

16# CLASS_MAP.

17

18 class Wrapper:

19 """Wrapper Class"""

20 def __init__(self, cls):

21 """Instantiate with passed Class (not instance or object)

22

23 :param cls: A class (not an instance of the class)

24 """

25 #: The class itself (not an instance of it), passed from ``cls``

26 self.class_object = cls

27 #: An instance of :py:attr:`class_object`

28 self.class_instance = None

29

30 def set_instance(self, *args, **kwargs):

31 """Set up :py:attr:`class_instance` from :py:attr:`class_object`"""

32 self.class_instance = self.class_object(*args, **kwargs)

33

34 def get_instance(self, *args, **kwargs):

35 """Return the instance with ``*args`` and ``**kwargs``

36 """

37 self.set_instance(*args, **kwargs)

38 return self.class_instance

39

40 class ActionsFile:

41 """Class to parse and verify entire actions file

42

43 Individual actions are :py:class:`~.curator.classdef.ActionDef` objects

44 """

45 def __init__(self, action_file):

46 self.logger = logging.getLogger(__name__)

47 #: The full, validated configuration from ``action_file``.

48 self.fullconfig = self.get_validated(action_file)

49 self.logger.debug('Action Configuration: %s', password_filter(self.fullconfig))

50 #: A dict of all actions in the provided configuration. Each original key name is preserved

51 #: and the value is now an :py:class:`~.curator.classdef.ActionDef`, rather than a dict.

52 self.actions = None

53 self.set_actions(self.fullconfig['actions'])

54

55 def get_validated(self, action_file):

56 """

57 :param action_file: The path to a valid YAML action configuration file

58 :type action_file: str

59

60 :returns: The result from passing ``action_file`` to

61 :py:func:`~.curator.helpers.testers.validate_actions`

62 """

63 try:

64 return validate_actions(get_yaml(action_file))

65 except (ESclient_ConfigError, UnboundLocalError) as err:

66 self.logger.critical('Configuration Error: %s', err)

67 raise ConfigurationError from err

68

69 def parse_actions(self, all_actions):

70 """Parse the individual actions found in ``all_actions['actions']``

71

72 :param all_actions: All actions, each its own dictionary behind a numeric key. Making the

73 keys numeric guarantees that if they are sorted, they will always be executed in order.

74

75 :type all_actions: dict

76

77 :returns:

78 :rtype: list of :py:class:`~.curator.classdef.ActionDef`

79 """

80 acts = {}

81 for idx in all_actions.keys():

82 acts[idx] = ActionDef(all_actions[idx])

83 return acts

84

85 def set_actions(self, all_actions):

86 """Set the actions via :py:meth:`~.curator.classdef.ActionsFile.parse_actions`

87

88 :param all_actions: All actions, each its own dictionary behind a numeric key. Making the

89 keys numeric guarantees that if they are sorted, they will always be executed in order.

90 :type all_actions: dict

91 :rtype: None

92 """

93 self.actions = self.parse_actions(all_actions)

94

95# In this case, I just don't care that pylint thinks I'm overdoing it with attributes

96# pylint: disable=too-many-instance-attributes

97 class ActionDef:

98 """Individual Action Definition Class

99

100 Instances of this class represent an individual action from an action file.

101 """

102 def __init__(self, action_dict):

103 #: The whole action dictionary

104 self.action_dict = action_dict

105 #: The action name

106 self.action = None

107 #: The action's class (Alias, Allocation, etc.)

108 self.action_cls = None

109 #: Only when action is alias will this be a :py:class:`~.curator.IndexList`

110 self.alias_adds = None

111 #: Only when action is alias will this be a :py:class:`~.curator.IndexList`

112 self.alias_removes = None

113 #: The list class, either :py:class:`~.curator.IndexList` or

114 #: :py:class:`~.curator.SnapshotList`. Default is :py:class:`~.curator.IndexList`

115 self.list_obj = Wrapper(IndexList)

116 #: The action ``description``

117 self.description = None

118 #: The action ``options`` :py:class:`dict`

119 self.options = {}

120 #: The action ``filters`` :py:class:`list`

121 self.filters = None

122 #: The action option ``disable_action``

123 self.disabled = None

124 #: The action option ``continue_if_exception``

125 self.cif = None

126 #: The action option ``timeout_override``

127 self.timeout_override = None

128 #: The action option ``ignore_empty_list``

129 self.iel = None

130 #: The action option ``allow_ilm_indices``

131 self.allow_ilm = None

132 self.set_root_attrs()

133 self.set_option_attrs()

134 self.log_the_options()

135 self.get_action_class()

136

137 def instantiate(self, attribute, *args, **kwargs):

138 """

139 Convert ``attribute`` from being a :py:class:`~.curator.classdef.Wrapper` of a Class to an

140 instantiated object of that Class.

141

142 This is madness or genius. You decide. This entire method plus the

143 :py:class:`~.curator.classdef.Wrapper` class came about because I couldn't cleanly

144 instantiate a class variable into a class object. It works, and that's good enough for me.

145

146 :param attribute: The `name` of an attribute that references a Wrapper class instance

147 :type attribute: str

148 """

149 try:

150 wrapper = getattr(self, attribute)

151 except AttributeError as exc:

152 raise AttributeError(f'Bad Attribute: {attribute}. Exception: {exc}') from exc

153 setattr(self, attribute, self.get_obj_instance(wrapper, *args, **kwargs))

154

155 def get_obj_instance(self, wrapper, *args, **kwargs):

156 """Get the class instance wrapper identified by ``wrapper``

157 Pass all other args and kwargs to the :py:meth:`~.curator.classdef.Wrapper.get_instance`

158 method.

159

160 :returns: An instance of the class that :py:class:`~.curator.classdef.Wrapper` is wrapping

161 """

162 if not isinstance(wrapper, Wrapper):

163 raise ConfigurationError(

164 f'{__name__} was passed wrapper which was of type {type(wrapper)}')

165 return wrapper.get_instance(*args, **kwargs)

166

167 def set_alias_extras(self):

168 """Populate the :py:attr:`alias_adds` and :py:attr:`alias_removes` attributes"""

169 self.alias_adds = Wrapper(IndexList)

170 self.alias_removes = Wrapper(IndexList)

171

172 def get_action_class(self):

173 """Get the action class from :py:const:`~.curator.actions.CLASS_MAP`

174

175 Do extra setup when action is ``alias``

176

177 Set :py:attr:`list_obj` to :py:class:`~.curator.SnapshotList` when

178 :py:attr:`~.curator.classdef.ActionDef.action` is ``delete_snapshots`` or ``restore``

179 """

180

181 self.action_cls = Wrapper(CLASS_MAP[self.action])

182 if self.action == 'alias':

183 self.set_alias_extras()

184 if self.action in ['delete_snapshots', 'restore']:

185 self.list_obj = Wrapper(SnapshotList)

186

187 def set_option_attrs(self):

188 """

189 Iteratively get the keys and values from :py:attr:`~.curator.classdef.ActionDef.options`

190 and set the attributes

191 """

192 attmap = {

193 'disable_action': 'disabled',

194 'continue_if_exception': 'cif',

195 'ignore_empty_list': 'iel',

196 'allow_ilm_indices': 'allow_ilm',

197 'timeout_override' : 'timeout_override'

198 }

199 for key in self.action_dict['options']:

200 if key in attmap:

201 setattr(self, attmap[key], self.action_dict['options'][key])

202 else:

203 self.options[key] = self.action_dict['options'][key]

204

205 def set_root_attrs(self):

206 """

207 Iteratively get the keys and values from

208 :py:attr:`~.curator.classdef.ActionDef.action_dict` and set the attributes

209 """

210 for key, value in self.action_dict.items():

211 # Gonna grab options in get_option_attrs()

212 if key == 'options':

213 continue

214 if value is not None:

215 setattr(self, key, value)

216

217 def log_the_options(self):

218 """Log options at initialization time"""

219 logger = logging.getLogger('curator.cli.ActionDef')

220 msg = (

221 f'For action {self.action}: disable_action={self.disabled}'

222 f'continue_if_exception={self.cif}, timeout_override={self.timeout_override}'

223 f'ignore_empty_list={self.iel}, allow_ilm_indices={self.allow_ilm}'

224 )

225 logger.debug(msg)

226 if self.allow_ilm:

227 logger.warning('Permitting operation on indices with an ILM policy')