Coverage for curator/cli_singletons/object_class.py: 81%

118 statements  

« prev     ^ index     » next       coverage.py v7.3.0, created at 2023-08-16 15:27 -0600

1"""Object builder""" 

2import logging 

3import sys 

4from voluptuous import Schema 

5from curator import IndexList, SnapshotList 

6from curator.actions import ( 

7 Alias, Allocation, Close, ClusterRouting, CreateIndex, DeleteIndices, DeleteSnapshots, 

8 ForceMerge, IndexSettings, Open, Reindex, Replicas, Restore, Rollover, Shrink, 

9 Snapshot 

10) 

11from curator.defaults.settings import snapshot_actions 

12from curator.exceptions import ConfigurationError, NoIndices, NoSnapshots 

13from curator.validators import SchemaCheck, filters, options 

14from curator.utils import get_client, prune_nones, validate_filters 

15 

16 

17 

18CLASS_MAP = { 

19 'alias' : Alias, 

20 'allocation' : Allocation, 

21 'close' : Close, 

22 'cluster_routing' : ClusterRouting, 

23 'create_index' : CreateIndex, 

24 'delete_indices' : DeleteIndices, 

25 'delete_snapshots' : DeleteSnapshots, 

26 'forcemerge' : ForceMerge, 

27 'index_settings' : IndexSettings, 

28 'open' : Open, 

29 'reindex' : Reindex, 

30 'replicas' : Replicas, 

31 'restore' : Restore, 

32 'rollover': Rollover, 

33 'shrink': Shrink, 

34 'snapshot' : Snapshot, 

35} 

36 

37EXCLUDED_OPTIONS = [ 

38 'ignore_empty_list', 'timeout_override', 

39 'continue_if_exception', 'disable_action' 

40] 

41 

42class cli_action(): 

43 """ 

44 Unified class for all CLI singleton actions 

45 """ 

46 def __init__(self, action, client_args, option_dict, filter_list, ignore_empty_list, **kwargs): 

47 self.logger = logging.getLogger('curator.cli_singletons.cli_action.' + action) 

48 self.action = action 

49 self.repository = kwargs['repository'] if 'repository' in kwargs else None 

50 if action[:5] != 'show_': # Ignore CLASS_MAP for show_indices/show_snapshots 

51 try: 

52 self.action_class = CLASS_MAP[action] 

53 except KeyError: 

54 self.logger.critical('Action must be one of {0}'.format(list(CLASS_MAP.keys()))) 

55 self.check_options(option_dict) 

56 else: 

57 self.options = option_dict 

58 # Extract allow_ilm_indices so it can be handled separately. 

59 if 'allow_ilm_indices' in self.options: 

60 self.allow_ilm = self.options.pop('allow_ilm_indices') 

61 else: 

62 self.allow_ilm = False 

63 if action == 'alias': 

64 self.alias = { 

65 'name': option_dict['name'], 

66 'extra_settings': option_dict['extra_settings'], 

67 'wini': kwargs['warn_if_no_indices'] if 'warn_if_no_indices' in kwargs else False 

68 } 

69 for k in ['add', 'remove']: 

70 if k in kwargs: 

71 self.alias[k] = {} 

72 self.check_filters(kwargs[k], loc='alias singleton', key=k) 

73 self.alias[k]['filters'] = self.filters 

74 if self.allow_ilm: 

75 self.alias[k]['filters'].append({'filtertype':'ilm'}) 

76 elif action in ['cluster_routing', 'create_index', 'rollover']: 

77 self.action_kwargs = {} 

78 # No filters for these actions 

79 if action == 'rollover': 

80 # Ugh. Look how I screwed up here with args instead of kwargs, 

81 # like EVERY OTHER ACTION seems to have... grr 

82 # todo: fix this in Curator 6, as it's an API-level change. 

83 self.action_args = (option_dict['name'], option_dict['conditions']) 

84 for k in ['new_index', 'extra_settings', 'wait_for_active_shards']: 

85 self.action_kwargs[k] = kwargs[k] if k in kwargs else None 

86 else: 

87 self.check_filters(filter_list) 

88 self.client = get_client(**client_args) 

89 self.ignore = ignore_empty_list 

90 

91 def prune_excluded(self, option_dict): 

92 """Prune excluded options""" 

93 for k in list(option_dict.keys()): 

94 if k in EXCLUDED_OPTIONS: 

95 del option_dict[k] 

96 return option_dict 

97 

98 def check_options(self, option_dict): 

99 """Validate provided options""" 

100 try: 

101 self.logger.debug('Validating provided options: {0}'.format(option_dict)) 

102 # Kludgy work-around to needing 'repository' in options for these actions 

103 # but only to pass the schema check. It's removed again below. 

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

105 option_dict['repository'] = self.repository 

106 _ = SchemaCheck( 

107 prune_nones(option_dict), 

108 options.get_schema(self.action), 

109 'options', 

110 '{0} singleton action "options"'.format(self.action) 

111 ).result() 

112 self.options = self.prune_excluded(_) 

113 # Remove this after the schema check, as the action class won't need it as an arg 

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

115 del self.options['repository'] 

116 except ConfigurationError as err: 

117 self.logger.critical('Unable to parse options: {0}'.format(err)) 

118 sys.exit(1) 

119 

120 def check_filters(self, filter_dict, loc='singleton', key='filters'): 

121 """Validate provided filters""" 

122 try: 

123 self.logger.debug('Validating provided filters: {0}'.format(filter_dict)) 

124 _ = SchemaCheck( 

125 filter_dict, 

126 Schema(filters.Filters(self.action, location=loc)), 

127 key, 

128 '{0} singleton action "{1}"'.format(self.action, key) 

129 ).result() 

130 self.filters = validate_filters(self.action, _) 

131 except ConfigurationError as err: 

132 self.logger.critical('Unable to parse filters: {0}'.format(err)) 

133 sys.exit(1) 

134 

135 def do_filters(self): 

136 """Actually run the filters""" 

137 self.logger.debug('Running filters and testing for empty list object') 

138 if self.allow_ilm: 

139 self.filters.append({'filtertype':'ilm', 'exclude':True}) 

140 try: 

141 self.list_object.iterate_filters({'filters':self.filters}) 

142 self.list_object.empty_list_check() 

143 except (NoIndices, NoSnapshots) as err: 

144 otype = 'index' if isinstance(err, NoIndices) else 'snapshot' 

145 if self.ignore: 

146 self.logger.info('Singleton action not performed: empty {0} list'.format(otype)) 

147 sys.exit(0) 

148 else: 

149 self.logger.error('Singleton action failed due to empty {0} list'.format(otype)) 

150 sys.exit(1) 

151 

152 def get_list_object(self): 

153 """Get either a SnapshotList or IndexList object""" 

154 if self.action in snapshot_actions() or self.action == 'show_snapshots': 

155 self.list_object = SnapshotList(self.client, repository=self.repository) 

156 else: 

157 self.list_object = IndexList(self.client) 

158 

159 def get_alias_obj(self): 

160 """Get the Alias object""" 

161 action_obj = Alias(name=self.alias['name'], extra_settings=self.alias['extra_settings']) 

162 for k in ['remove', 'add']: 

163 if k in self.alias: 

164 self.logger.debug( 

165 '{0}ing matching indices {1} alias "{2}"'.format( 

166 'Add' if k == 'add' else 'Remov', # 0 = "Add" or "Remov" 

167 'to' if k == 'add' else 'from', # 1 = "to" or "from" 

168 self.alias['name'] # 2 = the alias name 

169 ) 

170 ) 

171 self.alias[k]['ilo'] = IndexList(self.client) 

172 self.alias[k]['ilo'].iterate_filters({'filters':self.alias[k]['filters']}) 

173 fltr = getattr(action_obj, k) 

174 fltr(self.alias[k]['ilo'], warn_if_no_indices=self.alias['wini']) 

175 return action_obj 

176 

177 def do_singleton_action(self, dry_run=False): 

178 """Execute the (ostensibly) completely ready to run action""" 

179 self.logger.debug('Doing the singleton "{0}" action here.'.format(self.action)) 

180 try: 

181 if self.action == 'alias': 

182 action_obj = self.get_alias_obj() 

183 elif self.action in ['cluster_routing', 'create_index', 'rollover']: 

184 action_obj = self.action_class(self.client, *self.action_args, **self.action_kwargs) 

185 else: 

186 self.get_list_object() 

187 self.do_filters() 

188 self.logger.debug('OPTIONS = {0}'.format(self.options)) 

189 action_obj = self.action_class(self.list_object, **self.options) 

190 try: 

191 if dry_run: 

192 action_obj.do_dry_run() 

193 else: 

194 action_obj.do_action() 

195 except Exception as err: 

196 raise err # pass it on? 

197 except Exception as err: 

198 self.logger.critical( 

199 'Failed to complete action: {0}. {1}: {2}'.format(self.action, type(err), err)) 

200 sys.exit(1) 

201 self.logger.info('"{0}" action completed.'.format(self.action))