1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.codelibs.fess.app.web.admin.maintenance;
17
18 import java.io.IOException;
19 import java.net.InetAddress;
20 import java.nio.file.Files;
21 import java.nio.file.Path;
22 import java.nio.file.Paths;
23 import java.text.SimpleDateFormat;
24 import java.util.Arrays;
25 import java.util.Date;
26 import java.util.HashSet;
27 import java.util.Properties;
28 import java.util.Set;
29 import java.util.stream.Stream;
30 import java.util.zip.ZipEntry;
31 import java.util.zip.ZipOutputStream;
32
33 import org.apache.commons.text.StringEscapeUtils;
34 import org.apache.logging.log4j.LogManager;
35 import org.apache.logging.log4j.Logger;
36 import org.codelibs.core.io.CopyUtil;
37 import org.codelibs.core.lang.StringUtil;
38 import org.codelibs.curl.CurlResponse;
39 import org.codelibs.fess.Constants;
40 import org.codelibs.fess.annotation.Secured;
41 import org.codelibs.fess.app.web.base.FessAdminAction;
42 import org.codelibs.fess.helper.CoordinatorHelper;
43 import org.codelibs.fess.mylasta.direction.FessConfig.SimpleImpl;
44 import org.codelibs.fess.opensearch.client.SearchEngineClient;
45 import org.codelibs.fess.util.ComponentUtil;
46 import org.codelibs.fess.util.IpAddressUtil;
47 import org.codelibs.fess.util.SearchEngineUtil;
48 import org.lastaflute.web.Execute;
49 import org.lastaflute.web.response.ActionResponse;
50 import org.lastaflute.web.response.HtmlResponse;
51 import org.lastaflute.web.ruts.process.ActionRuntime;
52 import org.opensearch.core.action.ActionListener;
53
54 import jakarta.annotation.Resource;
55
56
57
58
59
60 public class AdminMaintenanceAction extends FessAdminAction {
61
62
63
64
65 public AdminMaintenanceAction() {
66 super();
67 }
68
69
70
71
72 public static final String ROLE = "admin-maintenance";
73
74
75
76
77 private static final Logger logger = LogManager.getLogger(AdminMaintenanceAction.class);
78
79 private static final String[] CAT_NAMES =
80 { "aliases", "allocation", "count", "fielddata", "health", "indices", "master", "nodeattrs", "nodes", "pending_tasks",
81 "plugins", "recovery", "repositories", "thread_pool", "shards", "segments", "snapshots", "templates" };
82
83
84
85
86
87
88
89
90 @Resource
91 protected SearchEngineClient searchEngineClient;
92
93
94
95
96 @Override
97 protected void setupHtmlData(final ActionRuntime runtime) {
98 super.setupHtmlData(runtime);
99 runtime.registerData("helpLink", systemHelper.getHelpLink(fessConfig.getOnlineHelpNameMaintenance()));
100 }
101
102 @Override
103 protected String getActionRole() {
104 return ROLE;
105 }
106
107
108
109
110
111
112
113
114
115
116 @Execute
117 @Secured({ ROLE, ROLE + VIEW })
118 public HtmlResponse index() {
119 saveToken();
120 return asIndexHtml();
121 }
122
123 private HtmlResponse asIndexHtml() {
124 return asHtml(path_AdminMaintenance_AdminMaintenanceJsp).useForm(ActionForm.class, op -> op.setup(f -> {
125 f.replaceAliases = Constants.ON;
126 f.resetDictionaries = null;
127 }));
128 }
129
130
131
132
133
134
135
136 @Execute
137 @Secured({ ROLE })
138 public HtmlResponse reindexOnly(final ActionForm form) {
139 validate(form, messages -> {}, this::asIndexHtml);
140 verifyToken(this::asIndexHtml);
141 if (startReindex(isCheckboxEnabled(form.replaceAliases), isCheckboxEnabled(form.resetDictionaries), form.numberOfShardsForDoc,
142 form.autoExpandReplicasForDoc)) {
143 saveInfo(messages -> messages.addSuccessStartedDataUpdate(GLOBAL));
144 }
145 return redirect(getClass());
146 }
147
148
149
150
151
152
153
154 @Execute
155 @Secured({ ROLE })
156 public HtmlResponse reloadDocIndex(final ActionForm form) {
157 validate(form, messages -> {}, this::asIndexHtml);
158 verifyToken(this::asIndexHtml);
159 final CoordinatorHelper coordinator = ComponentUtil.getCoordinatorHelper();
160 if (!coordinator.tryStartOperation("reload_doc_index")) {
161 saveError(messages -> messages.addErrorsOperationAlreadyRunning(GLOBAL,
162 coordinator.getOperationInfo("reload_doc_index").map(o -> o.hostname).orElse("unknown")));
163 return redirect(getClass());
164 }
165 try {
166 searchEngineClient.flushConfigFiles(() -> {
167 final String docIndex = fessConfig.getIndexDocumentUpdateIndex();
168 searchEngineClient.admin().indices().prepareClose(docIndex).execute(ActionListener.wrap(res -> {
169 logger.info("Closing index: {}", docIndex);
170 searchEngineClient.admin().indices().prepareOpen(docIndex).execute(ActionListener.wrap(res2 -> {
171 logger.info("Opened index: {}", docIndex);
172 coordinator.completeOperation("reload_doc_index");
173 }, e -> {
174 logger.warn("Failed to open index: {}", docIndex, e);
175 coordinator.completeOperation("reload_doc_index");
176 }));
177 }, e -> {
178 logger.warn("Failed to close index: {}", docIndex, e);
179 coordinator.completeOperation("reload_doc_index");
180 }));
181 });
182 } catch (final Exception e) {
183 coordinator.completeOperation("reload_doc_index");
184 throw e;
185 }
186 saveInfo(messages -> messages.addSuccessStartedDataUpdate(GLOBAL));
187 return redirect(getClass());
188 }
189
190
191
192
193
194
195
196 @Execute
197 @Secured({ ROLE })
198 public HtmlResponse clearCrawlerIndex(final ActionForm form) {
199 validate(form, messages -> {}, this::asIndexHtml);
200 verifyToken(this::asIndexHtml);
201 final CoordinatorHelper coordinator = ComponentUtil.getCoordinatorHelper();
202 if (!coordinator.tryStartOperation("clear_crawler_index")) {
203 saveError(messages -> messages.addErrorsOperationAlreadyRunning(GLOBAL,
204 coordinator.getOperationInfo("clear_crawler_index").map(o -> o.hostname).orElse("unknown")));
205 return redirect(getClass());
206 }
207 try {
208 searchEngineClient.admin()
209 .indices()
210 .prepareDelete(
211 fessConfig.getIndexDocumentCrawlerIndex() + ".queue",
212 fessConfig.getIndexDocumentCrawlerIndex() + ".data",
213 fessConfig.getIndexDocumentCrawlerIndex() + ".filter")
214 .execute(ActionListener.wrap(res -> {
215 logger.info("Deleted .crawler indices.");
216 coordinator.completeOperation("clear_crawler_index");
217 }, e -> {
218 logger.warn("Failed to delete .crawler.* indices.", e);
219 coordinator.completeOperation("clear_crawler_index");
220 }));
221 } catch (final Exception e) {
222 coordinator.completeOperation("clear_crawler_index");
223 throw e;
224 }
225 saveInfo(messages -> messages.addSuccessStartedDataUpdate(GLOBAL));
226 return redirect(getClass());
227 }
228
229
230
231
232
233
234
235 @Execute
236 @Secured({ ROLE, ROLE + VIEW })
237 public ActionResponse downloadLogs(final ActionForm form) {
238 validate(form, messages -> {}, this::asIndexHtml);
239 verifyTokenKeep(this::asIndexHtml);
240
241 final String diagnosticId = "log" + new SimpleDateFormat("yyyyMMddHHmm").format(ComponentUtil.getSystemHelper().getCurrentTime());
242 return asStream(diagnosticId + ".zip").contentTypeOctetStream().stream(out -> {
243 try (ZipOutputStream zos = new ZipOutputStream(out.stream())) {
244 writeLogFiles(zos, diagnosticId);
245 writeSystemProperties(zos, diagnosticId);
246 writeFessBasicConfig(zos, diagnosticId);
247 writeFessConfig(zos, diagnosticId);
248 writeFesenCat(zos, diagnosticId);
249 writeFesenJson(zos, diagnosticId);
250 }
251 });
252 }
253
254
255
256
257
258
259
260 protected void writeFesenJson(final ZipOutputStream zos, final String id) {
261 writeElastisearchJsonApi(zos, id, "cluster", "health");
262 writeElastisearchJsonApi(zos, id, "cluster", "state");
263 writeElastisearchJsonApi(zos, id, "cluster", "stats");
264 writeElastisearchJsonApi(zos, id, "cluster", "pending_tasks");
265 writeElastisearchJsonApi(zos, id, "nodes", "stats");
266 writeElastisearchJsonApi(zos, id, "nodes", "_all");
267 writeElastisearchJsonApi(zos, id, "nodes", "usage");
268 writeElastisearchJsonApi(zos, id, "remote", "info");
269 writeElastisearchJsonApi(zos, id, "tasks", "");
270 writeElastisearchJsonApi(zos, id, "nodes", "hot_threads");
271 }
272
273
274
275
276
277
278
279
280
281 protected void writeElastisearchJsonApi(final ZipOutputStream zos, final String id, final String v1, final String v2) {
282 final ZipEntry entry = new ZipEntry(id + "/es_" + v1 + "_" + v2 + ".json");
283 try {
284 zos.putNextEntry(entry);
285 try (CurlResponse response = ComponentUtil.getCurlHelper().get("/_" + v1 + "/" + v2).execute()) {
286 CopyUtil.copy(response.getContentAsStream(), zos);
287 }
288 } catch (final Exception e) {
289 logger.warn("Failed to access /_{}/{}", v1, v2, e);
290 }
291 }
292
293
294
295
296
297
298
299 protected void writeFesenCat(final ZipOutputStream zos, final String id) {
300 Arrays.stream(CAT_NAMES).forEach(name -> {
301 final ZipEntry entry = new ZipEntry(id + "/es_cat_" + name + ".txt");
302 try {
303 zos.putNextEntry(entry);
304 try (CurlResponse response = ComponentUtil.getCurlHelper().get("/_cat/" + name).param("v", "").execute()) {
305 CopyUtil.copy(response.getContentAsStream(), zos);
306 }
307 } catch (final Exception e) {
308 logger.warn("Failed to access /_cat/{}", name, e);
309 }
310 });
311 }
312
313
314
315
316
317
318
319 protected void writeFessConfig(final ZipOutputStream zos, final String id) {
320 if (fessConfig instanceof SimpleImpl) {
321 final Properties prop = new Properties();
322 ((SimpleImpl) fessConfig).keySet().stream().forEach(k -> prop.setProperty(k, fessConfig.get(k)));
323
324 final ZipEntry entry = new ZipEntry(id + "/fess_config.properties");
325 try {
326 zos.putNextEntry(entry);
327 prop.store(zos, getHostInfo());
328 } catch (final IOException e) {
329 logger.warn("Failed to access fess_config.properties.", e);
330 }
331 }
332 }
333
334
335
336
337
338
339
340 protected void writeFessBasicConfig(final ZipOutputStream zos, final String id) {
341 final String index = "fess_basic_config";
342 final ZipEntry entry = new ZipEntry(id + "/fess_basic_config.bulk");
343 try {
344 zos.putNextEntry(entry);
345 SearchEngineUtil.scroll(index, hit -> {
346 final String data = "{\"index\":{\"_index\":\"" + index + "\",\"_id\":\"" + StringEscapeUtils.escapeJson(hit.getId())
347 + "\"}}\n" + hit.getSourceAsString() + "\n";
348 try {
349 zos.write(data.getBytes(Constants.CHARSET_UTF_8));
350 } catch (final IOException e) {
351 logger.warn("Failed to access /{}/{}.", index, hit.getId(), e);
352 }
353 return true;
354 });
355 } catch (final IOException e) {
356 logger.warn("Failed to access /{}.", index, e);
357 }
358 }
359
360
361
362
363
364
365
366 protected void writeSystemProperties(final ZipOutputStream zos, final String id) {
367 final ZipEntry entry = new ZipEntry(id + "/system.properties");
368 try {
369 zos.putNextEntry(entry);
370 ComponentUtil.getSystemProperties().store(zos, getHostInfo());
371 } catch (final IOException e) {
372 logger.warn("Failed to access system.properties.", e);
373 }
374 }
375
376
377
378
379
380
381
382 protected void writeLogFiles(final ZipOutputStream zos, final String id) {
383 final String logFilePath = systemHelper.getLogFilePath();
384 if (StringUtil.isNotBlank(logFilePath)) {
385 final Path logDirPath = Paths.get(logFilePath);
386 try (Stream<Path> stream = Files.list(logDirPath)) {
387 stream.filter(entry -> isLogFilename(entry.getFileName().toString())).forEach(filePath -> {
388 final ZipEntry entry = new ZipEntry(id + "/" + filePath.getFileName().toString());
389 try {
390 zos.putNextEntry(entry);
391 final long len = Files.copy(filePath, zos);
392 if (logger.isDebugEnabled()) {
393 logger.debug("Log file: name={}, size={}", filePath.getFileName(), len);
394 }
395 } catch (final IOException e) {
396 logger.warn("Failed to access {}", filePath, e);
397 }
398 });
399 } catch (final Exception e) {
400 logger.warn("Failed to access log files.", e);
401 }
402 }
403 }
404
405
406
407
408
409
410 protected String getHostInfo() {
411 final StringBuilder buf = new StringBuilder();
412 try {
413 final InetAddress ia = InetAddress.getLocalHost();
414 final String hostname = ia.getHostName();
415 if (StringUtil.isNotBlank(hostname)) {
416 buf.append(hostname);
417 }
418 final String ip = IpAddressUtil.getUrlHost(ia);
419 if (StringUtil.isNotBlank(ip)) {
420 if (buf.length() > 0) {
421 buf.append(" : ");
422 }
423 buf.append(ip);
424 }
425 } catch (final Exception e) {
426
427 }
428 return buf.toString();
429 }
430
431
432
433
434
435
436
437 protected boolean isLogFilename(final String name) {
438 return name.endsWith(".log") || name.endsWith(".log.gz");
439 }
440
441
442
443
444
445
446
447
448 @Execute
449 @Secured({ ROLE })
450 public HtmlResponse reindexConfigIndices(final ActionForm form) {
451 validate(form, messages -> {}, this::asIndexHtml);
452 verifyToken(this::asIndexHtml);
453
454 final boolean loadBulkData = isCheckboxEnabled(form.loadBulkData);
455 final Set<String> targetPrefixes = new HashSet<>();
456 if (isCheckboxEnabled(form.rebuildConfigIndex)) {
457 targetPrefixes.add("fess_config");
458 }
459 if (isCheckboxEnabled(form.rebuildUserIndex)) {
460 targetPrefixes.add("fess_user");
461 }
462 if (isCheckboxEnabled(form.rebuildLogIndex)) {
463 targetPrefixes.add("fess_log");
464 }
465
466 if (targetPrefixes.isEmpty()) {
467 saveError(messages -> messages.addErrorsNoTargetIndexSelected(GLOBAL));
468 return redirect(getClass());
469 }
470
471 final CoordinatorHelper coordinator = ComponentUtil.getCoordinatorHelper();
472 if (!coordinator.tryStartOperation("reindex_config")) {
473 saveError(messages -> messages.addErrorsOperationAlreadyRunning(GLOBAL,
474 coordinator.getOperationInfo("reindex_config").map(o -> o.hostname).orElse("unknown")));
475 return redirect(getClass());
476 }
477
478 new Thread(() -> {
479 try {
480 if (!searchEngineClient.reindexConfigIndices(loadBulkData, targetPrefixes)) {
481 logger.warn("Failed to rebuild config indices");
482 }
483 } catch (final Exception e) {
484 logger.warn("Failed to rebuild config indices", e);
485 } finally {
486 coordinator.completeOperation("reindex_config");
487 }
488 }, "rebuild-config-indices").start();
489
490 saveInfo(messages -> messages.addSuccessStartedDataUpdate(GLOBAL));
491 return redirect(getClass());
492 }
493
494
495
496
497
498
499
500
501
502
503 protected boolean startReindex(final boolean replaceAliases, final boolean resetDictionaries, final String numberOfShards,
504 final String autoExpandReplicas) {
505 final CoordinatorHelper coordinator = ComponentUtil.getCoordinatorHelper();
506 if (!coordinator.tryStartOperation("reindex")) {
507 saveError(messages -> messages.addErrorsOperationAlreadyRunning(GLOBAL,
508 coordinator.getOperationInfo("reindex").map(o -> o.hostname).orElse("unknown")));
509 return false;
510 }
511
512 final String docIndex = "fess";
513 final String fromIndex = fessConfig.getIndexDocumentUpdateIndex();
514 final String toIndex = docIndex + "." + new SimpleDateFormat(Constants.DOCUMENT_INDEX_SUFFIX_PATTERN).format(new Date());
515 if (searchEngineClient.createIndex(docIndex, toIndex, numberOfShards, autoExpandReplicas, resetDictionaries)) {
516 try {
517 searchEngineClient.admin()
518 .cluster()
519 .prepareHealth(toIndex)
520 .setWaitForYellowStatus()
521 .execute(ActionListener.wrap(response -> {
522 try {
523 searchEngineClient.addMapping(docIndex, "doc", toIndex);
524 if (searchEngineClient.copyDocIndex(fromIndex, toIndex, replaceAliases) && replaceAliases
525 && !searchEngineClient.updateAlias(toIndex)) {
526 logger.warn("Failed to update aliases for {} and {}", fromIndex, toIndex);
527 }
528 } finally {
529 coordinator.completeOperation("reindex");
530 }
531 }, e -> {
532 coordinator.completeOperation("reindex");
533 logger.warn("Failed to reindex from {} to {}", fromIndex, toIndex, e);
534 }));
535 } catch (final Exception e) {
536 coordinator.completeOperation("reindex");
537 throw e;
538 }
539 return true;
540 }
541 coordinator.completeOperation("reindex");
542 saveError(messages -> messages.addErrorsFailedToReindex(GLOBAL, fromIndex, toIndex));
543 return false;
544 }
545
546 }