Coverage for curator/classdef.py: 95%

94 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-07-20 21:00 -0600

1"""Other Classes""" 

2import logging 

3from es_client.exceptions import ConfigurationError as ESclient_ConfigError 

4from es_client.helpers.utils import get_yaml 

5from curator import IndexList, SnapshotList 

6from curator.actions import CLASS_MAP 

7from curator.exceptions import ConfigurationError 

8from curator.config_utils import password_filter 

9from 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 

18class 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 

40class 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 

97class 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')