1
1
from __future__ import unicode_literals , absolute_import
2
+ from datetime import datetime
2
3
4
+ from elasticsearch_dsl import connections
3
5
from django .conf import settings
4
6
from django .core .management .base import BaseCommand , CommandError
5
7
from six .moves import input
9
11
class Command (BaseCommand ):
10
12
help = 'Manage elasticsearch index.'
11
13
14
+ def __init__ (self , * args , ** kwargs ):
15
+ super (Command , self ).__init__ (* args , ** kwargs )
16
+ self .es_conn = connections .get_connection ()
17
+
12
18
def add_arguments (self , parser ):
13
19
parser .add_argument (
14
20
'--models' ,
@@ -63,6 +69,21 @@ def add_arguments(self, parser):
63
69
dest = 'parallel' ,
64
70
help = 'Run populate/rebuild update single threaded'
65
71
)
72
+ parser .add_argument (
73
+ '--use-alias' ,
74
+ action = 'store_true' ,
75
+ dest = 'use_alias' ,
76
+ help = 'Use alias with indices'
77
+ )
78
+ parser .add_argument (
79
+ '--use-alias-keep-index' ,
80
+ action = 'store_true' ,
81
+ dest = 'use_alias_keep_index' ,
82
+ help = """
83
+ Do not delete replaced indices when used with '--rebuild' and
84
+ '--use-alias' args
85
+ """
86
+ )
66
87
parser .set_defaults (parallel = getattr (settings , 'ELASTICSEARCH_DSL_PARALLEL' , False ))
67
88
parser .add_argument (
68
89
'--refresh' ,
@@ -107,10 +128,18 @@ def _get_models(self, args):
107
128
108
129
return set (models )
109
130
110
- def _create (self , models , options ):
131
+ def _create (self , models , aliases , options ):
111
132
for index in registry .get_indices (models ):
112
- self .stdout .write ("Creating index '{}'" .format (index ._name ))
113
- index .create ()
133
+ alias_exists = index ._name in aliases
134
+ if not alias_exists :
135
+ self .stdout .write ("Creating index '{}'" .format (index ._name ))
136
+ index .create ()
137
+ elif options ['action' ] == 'create' :
138
+ self .stdout .write (
139
+ "'{}' already exists as an alias. Run '--delete' with"
140
+ " '--use-alias' arg to delete indices pointed at the "
141
+ "alias to make index name available." .format (index ._name )
142
+ )
114
143
115
144
def _populate (self , models , options ):
116
145
parallel = options ['parallel' ]
@@ -123,29 +152,138 @@ def _populate(self, models, options):
123
152
qs = doc ().get_indexing_queryset ()
124
153
doc ().update (qs , parallel = parallel , refresh = options ['refresh' ])
125
154
126
- def _delete (self , models , options ):
155
+ def _get_alias_indices (self , alias ):
156
+ alias_indices = self .es_conn .indices .get_alias (name = alias )
157
+ return list (alias_indices .keys ())
158
+
159
+ def _delete_alias_indices (self , alias ):
160
+ alias_indices = self ._get_alias_indices (alias )
161
+ alias_delete_actions = [
162
+ {"remove_index" : {"index" : index }} for index in alias_indices
163
+ ]
164
+ self .es_conn .indices .update_aliases ({"actions" : alias_delete_actions })
165
+ for index in alias_indices :
166
+ self .stdout .write ("Deleted index '{}'" .format (index ))
167
+
168
+ def _delete (self , models , aliases , options ):
127
169
index_names = [index ._name for index in registry .get_indices (models )]
128
170
129
171
if not options ['force' ]:
130
172
response = input (
131
173
"Are you sure you want to delete "
132
- "the '{}' indexes ? [y/N]: " .format (", " .join (index_names )))
174
+ "the '{}' indices ? [y/N]: " .format (", " .join (index_names )))
133
175
if response .lower () != 'y' :
134
176
self .stdout .write ('Aborted' )
135
177
return False
136
178
137
- for index in registry .get_indices (models ):
138
- self .stdout .write ("Deleting index '{}'" .format (index ._name ))
139
- index .delete (ignore = 404 )
179
+ if options ['use_alias' ]:
180
+ for index in index_names :
181
+ alias_exists = index in aliases
182
+ if alias_exists :
183
+ self ._delete_alias_indices (index )
184
+ elif self .es_conn .indices .exists (index = index ):
185
+ self .stdout .write (
186
+ "'{}' refers to an index, not an alias. Run "
187
+ "'--delete' without '--use-alias' arg to delete "
188
+ "index." .format (index )
189
+ )
190
+ return False
191
+ else :
192
+ for index in registry .get_indices (models ):
193
+ alias_exists = index ._name in aliases
194
+ if not alias_exists :
195
+ self .stdout .write ("Deleting index '{}'" .format (index ._name ))
196
+ index .delete (ignore = 404 )
197
+ elif options ['action' ] == 'rebuild' :
198
+ self ._delete_alias_indices (index ._name )
199
+ elif options ['action' ] == 'delete' :
200
+ self .stdout .write (
201
+ "'{}' refers to an alias, not an index. Run "
202
+ "'--delete' with '--use-alias' arg to delete indices "
203
+ "pointed at the alias." .format (index ._name )
204
+ )
205
+ return False
206
+
140
207
return True
141
208
142
- def _rebuild (self , models , options ):
143
- if not self ._delete (models , options ):
209
+ def _update_alias (self , alias , new_index , alias_exists , options ):
210
+ alias_actions = [{"add" : {"alias" : alias , "index" : new_index }}]
211
+
212
+ delete_existing_index = False
213
+ if not alias_exists and self .es_conn .indices .exists (index = alias ):
214
+ # Elasticsearch will return an error if an index already
215
+ # exists with the desired alias name. Therefore, we need to
216
+ # delete that index.
217
+ delete_existing_index = True
218
+ alias_actions .append ({"remove_index" : {"index" : alias }})
219
+
220
+ old_indices = []
221
+ alias_delete_actions = []
222
+ if alias_exists :
223
+ # Elasticsearch will return an error if we search for
224
+ # indices by alias but the alias doesn't exist. Therefore,
225
+ # we want to be sure the alias exists.
226
+ old_indices = self ._get_alias_indices (alias )
227
+ alias_actions .append (
228
+ {"remove" : {"alias" : alias , "indices" : old_indices }}
229
+ )
230
+ alias_delete_actions = [
231
+ {"remove_index" : {"index" : index }} for index in old_indices
232
+ ]
233
+
234
+ self .es_conn .indices .update_aliases ({"actions" : alias_actions })
235
+ if delete_existing_index :
236
+ self .stdout .write ("Deleted index '{}'" .format (alias ))
237
+
238
+ self .stdout .write (
239
+ "Added alias '{}' to index '{}'" .format (alias , new_index )
240
+ )
241
+
242
+ if old_indices :
243
+ for index in old_indices :
244
+ self .stdout .write (
245
+ "Removed alias '{}' from index '{}'" .format (alias , index )
246
+ )
247
+
248
+ if alias_delete_actions and not options ['use_alias_keep_index' ]:
249
+ self .es_conn .indices .update_aliases (
250
+ {"actions" : alias_delete_actions }
251
+ )
252
+ for index in old_indices :
253
+ self .stdout .write ("Deleted index '{}'" .format (index ))
254
+
255
+ def _rebuild (self , models , aliases , options ):
256
+ if (not options ['use_alias' ]
257
+ and not self ._delete (models , aliases , options )):
144
258
return
145
259
146
- self ._create (models , options )
260
+ if options ['use_alias' ]:
261
+ alias_index_pairs = []
262
+ index_suffix = "-" + datetime .now ().strftime ("%Y%m%d%H%M%S%f" )
263
+ for index in registry .get_indices (models ):
264
+ # The alias takes the original index name value. The
265
+ # index name sent to Elasticsearch will be the alias
266
+ # plus the suffix from above. In addition, the index
267
+ # name needs to be limited to 255 characters, of which
268
+ # 21 will always be taken by the suffix, leaving 234
269
+ # characters from the original index name value.
270
+ new_index = index ._name [:234 ] + index_suffix
271
+ alias_index_pairs .append (
272
+ {'alias' : index ._name , 'index' : new_index }
273
+ )
274
+ index ._name = new_index
275
+
276
+ self ._create (models , aliases , options )
147
277
self ._populate (models , options )
148
278
279
+ if options ['use_alias' ]:
280
+ for alias_index_pair in alias_index_pairs :
281
+ alias = alias_index_pair ['alias' ]
282
+ alias_exists = alias in aliases
283
+ self ._update_alias (
284
+ alias , alias_index_pair ['index' ], alias_exists , options
285
+ )
286
+
149
287
def handle (self , * args , ** options ):
150
288
if not options ['action' ]:
151
289
raise CommandError (
@@ -156,14 +294,21 @@ def handle(self, *args, **options):
156
294
action = options ['action' ]
157
295
models = self ._get_models (options ['models' ])
158
296
297
+ # We need to know if and which aliases exist to mitigate naming
298
+ # conflicts with indices, therefore this is needed regardless
299
+ # of using the '--use-alias' arg.
300
+ aliases = []
301
+ for index in self .es_conn .indices .get_alias ().values ():
302
+ aliases += index ['aliases' ].keys ()
303
+
159
304
if action == 'create' :
160
- self ._create (models , options )
305
+ self ._create (models , aliases , options )
161
306
elif action == 'populate' :
162
307
self ._populate (models , options )
163
308
elif action == 'delete' :
164
- self ._delete (models , options )
309
+ self ._delete (models , aliases , options )
165
310
elif action == 'rebuild' :
166
- self ._rebuild (models , options )
311
+ self ._rebuild (models , aliases , options )
167
312
else :
168
313
raise CommandError (
169
314
"Invalid action. Must be one of"
0 commit comments