diff --git a/src/platform/packages/shared/kbn-doc-links/src/get_doc_links.ts b/src/platform/packages/shared/kbn-doc-links/src/get_doc_links.ts index d82caffce0343..271d34d800471 100644 --- a/src/platform/packages/shared/kbn-doc-links/src/get_doc_links.ts +++ b/src/platform/packages/shared/kbn-doc-links/src/get_doc_links.ts @@ -793,6 +793,7 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D restoreSnapshot: `${ELASTICSEARCH_DOCS}snapshots-restore-snapshot.html`, restoreSnapshotApi: `${ELASTICSEARCH_DOCS}restore-snapshot-api.html#restore-snapshot-api-request-body`, searchableSnapshotSharedCache: `${ELASTICSEARCH_DOCS}searchable-snapshots.html#searchable-snapshots-shared-cache`, + slmStart: `${ELASTICSEARCH_DOCS}slm-api-start.html`, }, ingest: { append: `${ELASTICSEARCH_DOCS}append-processor.html`, diff --git a/x-pack/plugins/snapshot_restore/README.md b/x-pack/plugins/snapshot_restore/README.md index b6b75631b07d9..e448c7bca2c38 100644 --- a/x-pack/plugins/snapshot_restore/README.md +++ b/x-pack/plugins/snapshot_restore/README.md @@ -74,4 +74,12 @@ To run ES with plugins: 1. Run `yarn es snapshot` from the Kibana directory like normal, then exit out of process. 2. `cd .es/8.0.0` 3. `bin/elasticsearch-plugin install https://snapshots.elastic.co/downloads/elasticsearch-plugins/repository-hdfs/repository-hdfs-8.0.0-SNAPSHOT.zip` -4. Run `bin/elasticsearch` from the `.es/8.0.0` directory. Otherwise, starting ES with `yarn es snapshot` would overwrite the plugins you just installed. \ No newline at end of file +4. Run `bin/elasticsearch` from the `.es/8.0.0` directory. Otherwise, starting ES with `yarn es snapshot` would overwrite the plugins you just installed. + + +### SLM status +Snapshot lifecycle management (SLM) status is "RUNNING" by default, but it can be stoped manually (for mantenaince purpouses, for instance). When this happens, no schedule snapshots will be taken. Docs: https://www.elastic.co/guide/en/elasticsearch/reference/master/snapshot-lifecycle-management-api.html + +* To check the SLM status you can run `GET _slm/status` +* To start SLM `POST /_slm/start` +* To stop SLM `POST /_slm/stop` diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/home.test.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/home.test.ts index 222cc4e89c26e..a25dcf2a61341 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/home.test.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/home.test.ts @@ -512,6 +512,7 @@ describe('', () => { expect(row).toEqual([ '', // Checkbox snapshot.snapshot, // Snapshot + 'Complete', // The displayed message when stats is success REPOSITORY_NAME, // Repository snapshot.indices.length.toString(), // Indices snapshot.shards.total.toString(), // Shards @@ -738,7 +739,7 @@ describe('', () => { expect(find('snapshotDetail.version.value').text()).toBe(version); expect(find('snapshotDetail.uuid.value').text()).toBe(uuid); - expect(find('snapshotDetail.state.value').text()).toBe('Snapshot complete'); + expect(find('snapshotDetail.state.value').text()).toBe('Complete'); expect(find('snapshotDetail.includeGlobalState.value').text()).toEqual('Yes'); expect( find('snapshotDetail.snapshotFeatureStatesSummary.featureStatesList').text() @@ -788,10 +789,10 @@ describe('', () => { }; const mapStateToMessage = { - [SNAPSHOT_STATE.IN_PROGRESS]: 'Taking snapshot…', - [SNAPSHOT_STATE.FAILED]: 'Snapshot failed', - [SNAPSHOT_STATE.PARTIAL]: 'Partial failure ', - [SNAPSHOT_STATE.INCOMPATIBLE]: 'Incompatible version ', + [SNAPSHOT_STATE.IN_PROGRESS]: 'In progress', + [SNAPSHOT_STATE.FAILED]: 'Failed', + [SNAPSHOT_STATE.PARTIAL]: 'Partial', + [SNAPSHOT_STATE.SUCCESS]: 'Complete', }; // Call sequentially each state and verify that the message is ok diff --git a/x-pack/plugins/snapshot_restore/public/application/constants/index.ts b/x-pack/plugins/snapshot_restore/public/application/constants/index.ts index f319f1c995d0d..7ca0e3181aea0 100644 --- a/x-pack/plugins/snapshot_restore/public/application/constants/index.ts +++ b/x-pack/plugins/snapshot_restore/public/application/constants/index.ts @@ -17,7 +17,12 @@ export enum SNAPSHOT_STATE { SUCCESS = 'SUCCESS', FAILED = 'FAILED', PARTIAL = 'PARTIAL', - INCOMPATIBLE = 'INCOMPATIBLE', +} + +export enum SLM_STATE { + RUNNING = 'RUNNING', + STOPPING = 'STOPPING', + STOPPED = 'STOPPED', } const INDEX_SETTING_SUGGESTIONS: string[] = [ diff --git a/x-pack/plugins/snapshot_restore/public/application/lib/snapshot_list_params.ts b/x-pack/plugins/snapshot_restore/public/application/lib/snapshot_list_params.ts index 352eb658dd023..8c29a910c2d0c 100644 --- a/x-pack/plugins/snapshot_restore/public/application/lib/snapshot_list_params.ts +++ b/x-pack/plugins/snapshot_restore/public/application/lib/snapshot_list_params.ts @@ -15,7 +15,8 @@ export type SortField = | 'startTimeInMillis' | 'durationInMillis' | 'shards.total' - | 'shards.failed'; + | 'shards.failed' + | 'state'; export type SortDirection = Direction; diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_list.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_list.tsx index aa7a35bb2c0b2..6a74b92c4ac57 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_list.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_list.tsx @@ -8,10 +8,11 @@ import React, { Fragment, useEffect } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { RouteComponentProps } from 'react-router-dom'; -import { EuiButton, EuiCallOut, EuiSpacer, EuiPageTemplate } from '@elastic/eui'; +import { EuiButton, EuiCallOut, EuiSpacer, EuiPageTemplate, EuiLink } from '@elastic/eui'; import { reactRouterNavigate } from '@kbn/kibana-react-plugin/public'; +import { i18n } from '@kbn/i18n'; import { PageLoading, PageError, @@ -23,11 +24,15 @@ import { import { SlmPolicy } from '../../../../../common/types'; import { APP_SLM_CLUSTER_PRIVILEGES } from '../../../../../common'; -import { BASE_PATH, UIM_POLICY_LIST_LOAD } from '../../../constants'; +import { BASE_PATH, SLM_STATE, UIM_POLICY_LIST_LOAD } from '../../../constants'; import { useDecodedParams } from '../../../lib'; -import { useLoadPolicies, useLoadRetentionSettings } from '../../../services/http'; +import { + useLoadPolicies, + useLoadRetentionSettings, + useLoadSlmStatus, +} from '../../../services/http'; import { linkToAddPolicy, linkToPolicy } from '../../../services/navigation'; -import { useAppContext, useServices } from '../../../app_context'; +import { useAppContext, useCore, useServices } from '../../../app_context'; import { PolicyDetails } from './policy_details'; import { PolicyTable } from './policy_table'; @@ -52,6 +57,7 @@ export const PolicyList: React.FunctionComponent { return linkToPolicy(newPolicyName); }; @@ -157,9 +165,44 @@ export const PolicyList: React.FunctionComponent policy.schedule); const hasDuplicateSchedules = policySchedules.length > new Set(policySchedules).size; const hasRetention = Boolean(policies.find((policy: SlmPolicy) => policy.retention)); + const isSlmRunning = slmStatus?.operation_mode === SLM_STATE.RUNNING; content = (
+ {!isSlmRunning ? ( + + + } + color="warning" + iconType="warning" + > + + {i18n.translate('xpack.snapshotRestore.slmDocLink', { + defaultMessage: 'using the API.', + })} + + ), + }} + /> + + + + ) : null} + {hasDuplicateSchedules ? ( diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/components/snapshot_table.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/components/snapshot_table.tsx index 69f9d1b2f9ffb..e4907709f7e1e 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/components/snapshot_table.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/components/snapshot_table.tsx @@ -29,6 +29,7 @@ import { import { SnapshotListParams, SortDirection, SortField } from '../../../../lib'; import { DataPlaceholder, FormattedDateTime, SnapshotDeleteProvider } from '../../../../components'; import { SnapshotSearchBar } from './snapshot_search_bar'; +import { SnapshotState } from '../snapshot_details/tabs/snapshot_state'; const getLastSuccessfulManagedSnapshot = ( snapshots: SnapshotDetails[] @@ -93,6 +94,15 @@ export const SnapshotTable: React.FunctionComponent = (props: Props) => { ), }, + { + field: 'state', + name: i18n.translate('xpack.snapshotRestore.snapshotList.table.stateColumnTitle', { + defaultMessage: 'State', + }), + truncateText: false, + sortable: false, + render: (state: string) => , + }, { field: 'repository', name: i18n.translate('xpack.snapshotRestore.snapshotList.table.repositoryColumnTitle', { diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_details/tabs/snapshot_state.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_details/tabs/snapshot_state.tsx index e4629f1160f38..4b3a0215d7ec6 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_details/tabs/snapshot_state.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_details/tabs/snapshot_state.tsx @@ -5,57 +5,49 @@ * 2.0. */ -import React, { Fragment } from 'react'; +import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiIconTip, EuiLoadingSpinner } from '@elastic/eui'; +import { EuiFlexGroup, EuiHealth, EuiIcon, EuiToolTip } from '@elastic/eui'; import { SNAPSHOT_STATE } from '../../../../../constants'; import { useServices } from '../../../../../app_context'; interface Props { state: any; + displayTooltipIcon: boolean; } -export const SnapshotState: React.FC = ({ state }) => { +export const SnapshotState: React.FC = ({ state, displayTooltipIcon }) => { const { i18n } = useServices(); const stateMap: any = { [SNAPSHOT_STATE.IN_PROGRESS]: { - icon: , + color: 'primary', label: i18n.translate('xpack.snapshotRestore.snapshotState.inProgressLabel', { - defaultMessage: 'Taking snapshot…', + defaultMessage: 'In progress', }), }, [SNAPSHOT_STATE.SUCCESS]: { - icon: , + color: 'success', label: i18n.translate('xpack.snapshotRestore.snapshotState.completeLabel', { - defaultMessage: 'Snapshot complete', + defaultMessage: 'Complete', }), }, [SNAPSHOT_STATE.FAILED]: { - icon: , + color: 'danger', label: i18n.translate('xpack.snapshotRestore.snapshotState.failedLabel', { - defaultMessage: 'Snapshot failed', + defaultMessage: 'Failed', }), }, [SNAPSHOT_STATE.PARTIAL]: { - icon: , + color: 'warning', label: i18n.translate('xpack.snapshotRestore.snapshotState.partialLabel', { - defaultMessage: 'Partial failure', + defaultMessage: 'Partial', }), tip: i18n.translate('xpack.snapshotRestore.snapshotState.partialTipDescription', { defaultMessage: `Global cluster state was stored, but at least one shard wasn't stored successfully. See the 'Failed indices' tab.`, }), }, - [SNAPSHOT_STATE.INCOMPATIBLE]: { - icon: , - label: i18n.translate('xpack.snapshotRestore.snapshotState.incompatibleLabel', { - defaultMessage: 'Incompatible version', - }), - tip: i18n.translate('xpack.snapshotRestore.snapshotState.incompatibleTipDescription', { - defaultMessage: `Snapshot was created with a version of Elasticsearch incompatible with the cluster's version.`, - }), - }, }; if (!stateMap[state]) { @@ -63,26 +55,16 @@ export const SnapshotState: React.FC = ({ state }) => { return state; } - const { icon, label, tip } = stateMap[state]; + const { color, label, tip } = stateMap[state]; - const iconTip = tip && ( - - {' '} - - - ); + const iconTip = displayTooltipIcon && tip && ; return ( - - {icon} - - - {/* Escape flex layout created by EuiFlexItem. */} -
- {label} - {iconTip} -
-
-
+ + + {label} + {iconTip} + + ); }; diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_details/tabs/tab_summary.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_details/tabs/tab_summary.tsx index 00ea3fa27109b..cf1f9fd83a3d8 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_details/tabs/tab_summary.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_details/tabs/tab_summary.tsx @@ -94,7 +94,7 @@ export const TabSummary: React.FC = ({ snapshotDetails }) => { - + diff --git a/x-pack/plugins/snapshot_restore/public/application/services/http/policy_requests.ts b/x-pack/plugins/snapshot_restore/public/application/services/http/policy_requests.ts index f4e8abc34e993..945ceff724aef 100644 --- a/x-pack/plugins/snapshot_restore/public/application/services/http/policy_requests.ts +++ b/x-pack/plugins/snapshot_restore/public/application/services/http/policy_requests.ts @@ -131,3 +131,10 @@ export const executeRetention = async () => { uiMetricService.trackUiMetric(UIM_RETENTION_EXECUTE); return result; }; + +export const useLoadSlmStatus = () => { + return useRequest({ + path: `${API_BASE_PATH}policies/slm_status`, + method: 'get', + }); +}; diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/policy.test.ts b/x-pack/plugins/snapshot_restore/server/routes/api/policy.test.ts index 3d4f0f5505b30..873d270a206ca 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/policy.test.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/policy.test.ts @@ -9,6 +9,7 @@ import { addBasePath } from '../helpers'; import { registerPolicyRoutes } from './policy'; import { RouterMock, routeDependencies, RequestMock } from '../../test/helpers'; import { ResolveIndexResponseFromES } from '../../types'; +import { SlmGetStatusResponse } from '@elastic/elasticsearch/lib/api/types'; describe('[Snapshot and Restore API Routes] Policy', () => { const mockEsPolicy = { @@ -56,6 +57,7 @@ describe('[Snapshot and Restore API Routes] Policy', () => { const executeLifecycleFn = router.getMockApiFn('slm.executeLifecycle'); const deleteLifecycleFn = router.getMockApiFn('slm.deleteLifecycle'); const resolveIndicesFn = router.getMockApiFn('indices.resolveIndex'); + const getStatusFn = router.getMockApiFn('slm.getStatus'); beforeAll(() => { registerPolicyRoutes({ @@ -437,4 +439,25 @@ describe('[Snapshot and Restore API Routes] Policy', () => { await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); }); + + describe('getSlmStatusHandler', () => { + const mockRequest: RequestMock = { + method: 'get', + path: addBasePath('policies/slm_status'), + }; + + it('should return successful ES response', async () => { + const mockEsResponse: SlmGetStatusResponse = { operation_mode: 'RUNNING' }; + getStatusFn.mockResolvedValue(mockEsResponse); + + const expectedResponse = { ...mockEsResponse }; + await expect(router.runRequest(mockRequest)).resolves.toEqual({ body: expectedResponse }); + }); + + it('should throw if ES error', async () => { + getStatusFn.mockRejectedValue(new Error()); + + await expect(router.runRequest(mockRequest)).rejects.toThrowError(); + }); + }); }); diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts b/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts index 51bdf96361a24..9f948d0d2524a 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts @@ -304,4 +304,20 @@ export function registerPolicyRoutes({ return res.ok({ body: response }); }) ); + + // Get snapshot lifecycle management status + router.get( + { path: addBasePath('policies/slm_status'), validate: false }, + license.guardApiRoute(async (ctx, req, res) => { + const { client: clusterClient } = (await ctx.core).elasticsearch; + + try { + const response = await clusterClient.asCurrentUser.slm.getStatus(); + + return res.ok({ body: response }); + } catch (e) { + return handleEsError({ error: e, response: res }); + } + }) + ); } diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 9883c1b53fde6..397c73a512c10 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -44408,8 +44408,6 @@ "xpack.snapshotRestore.snapshots.breadcrumbTitle": "Snapshots", "xpack.snapshotRestore.snapshotState.completeLabel": "Snapshot terminé", "xpack.snapshotRestore.snapshotState.failedLabel": "Snapshot échoué", - "xpack.snapshotRestore.snapshotState.incompatibleLabel": "Version incompatible", - "xpack.snapshotRestore.snapshotState.incompatibleTipDescription": "Le snapshot a été créé avec une version d'Elasticsearch incompatible avec la version du cluster.", "xpack.snapshotRestore.snapshotState.inProgressLabel": "Prise de snapshot…", "xpack.snapshotRestore.snapshotState.partialLabel": "Échec partiel", "xpack.snapshotRestore.snapshotState.partialTipDescription": "L'état du cluster global a été stocké, mais au moins une partition n'a pas été stockée correctement. Consultez l'onglet \"Index échoués\".", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index e0214b2fa0834..37f24051c6001 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -44258,8 +44258,6 @@ "xpack.snapshotRestore.snapshots.breadcrumbTitle": "スナップショット", "xpack.snapshotRestore.snapshotState.completeLabel": "スナップショット完了", "xpack.snapshotRestore.snapshotState.failedLabel": "スナップショット失敗", - "xpack.snapshotRestore.snapshotState.incompatibleLabel": "互換性のないバージョン", - "xpack.snapshotRestore.snapshotState.incompatibleTipDescription": "このスナップショットはクラスターのバージョンと互換性のないバージョンの Elasticsearch で作成されました。", "xpack.snapshotRestore.snapshotState.inProgressLabel": "スナップショットを撮影中…", "xpack.snapshotRestore.snapshotState.partialLabel": "一部失敗", "xpack.snapshotRestore.snapshotState.partialTipDescription": "グローバルクラスターステータスが保存されましたが、1 つまたは複数のシャードの保存に失敗しました。「失敗したインシデント」タブをご覧ください。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 19868ab9d392e..67f0cee7591ff 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -43615,8 +43615,6 @@ "xpack.snapshotRestore.snapshots.breadcrumbTitle": "快照", "xpack.snapshotRestore.snapshotState.completeLabel": "快照完成", "xpack.snapshotRestore.snapshotState.failedLabel": "快照失败", - "xpack.snapshotRestore.snapshotState.incompatibleLabel": "不兼容版本", - "xpack.snapshotRestore.snapshotState.incompatibleTipDescription": "创建快照所用的 Elasticsearch 版本与集群的版本不兼容。", "xpack.snapshotRestore.snapshotState.inProgressLabel": "正在拍取快照……", "xpack.snapshotRestore.snapshotState.partialLabel": "部分失败", "xpack.snapshotRestore.snapshotState.partialTipDescription": "全局集群状态已存储,但至少一个分片未成功存储。请参阅'失败的索引'选项卡。", diff --git a/x-pack/test/api_integration/apis/management/snapshot_restore/lib/elasticsearch.ts b/x-pack/test/api_integration/apis/management/snapshot_restore/lib/elasticsearch.ts index b5b0bc053f3de..f1fa340f9ae2e 100644 --- a/x-pack/test/api_integration/apis/management/snapshot_restore/lib/elasticsearch.ts +++ b/x-pack/test/api_integration/apis/management/snapshot_restore/lib/elasticsearch.ts @@ -101,6 +101,10 @@ export const registerEsHelpers = (getService: FtrProviderContext['getService']) }); }; + const startSlm = () => { + return es.slm.start(); + }; + return { createRepository, createPolicy, @@ -110,5 +114,6 @@ export const registerEsHelpers = (getService: FtrProviderContext['getService']) executePolicy, createSnapshot, deleteSnapshots, + startSlm, }; }; diff --git a/x-pack/test/api_integration/apis/management/snapshot_restore/policies.ts b/x-pack/test/api_integration/apis/management/snapshot_restore/policies.ts index e0734680887d2..2e771616f9d1b 100644 --- a/x-pack/test/api_integration/apis/management/snapshot_restore/policies.ts +++ b/x-pack/test/api_integration/apis/management/snapshot_restore/policies.ts @@ -16,7 +16,7 @@ const REPO_NAME = 'test_repo'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const { createRepository, createPolicy, deletePolicy, cleanupPolicies, getPolicy } = + const { createRepository, createPolicy, deletePolicy, cleanupPolicies, getPolicy, startSlm } = registerEsHelpers(getService); describe('SLM policies', function () { @@ -229,5 +229,29 @@ export default function ({ getService }: FtrProviderContext) { }); }); }); + + describe('Show info', () => { + before(async () => { + // Make sure SLM is running + try { + await startSlm(); + } catch (err) { + // eslint-disable-next-line no-console + console.log('[Setup error] Error starting Slm'); + throw err; + } + }); + + it('should get slm status', async () => { + const { body } = await supertest + .get(`${API_BASE_PATH}/policies/slm_status`) + .set('kbn-xsrf', 'xxx') + .expect(200); + + expect(body).to.eql({ + operation_mode: 'RUNNING', + }); + }); + }); }); }