Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[8.x] Snapshots: show slm and status (#199622) #203372

Merged
merged 1 commit into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/kbn-doc-links/src/get_doc_links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,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`,
Expand Down
10 changes: 9 additions & 1 deletion x-pack/plugins/snapshot_restore/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
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`
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,7 @@ describe('<SnapshotRestoreHome />', () => {
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
Expand Down Expand Up @@ -738,7 +739,7 @@ describe('<SnapshotRestoreHome />', () => {

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()
Expand Down Expand Up @@ -788,10 +789,10 @@ describe('<SnapshotRestoreHome />', () => {
};

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ export type SortField =
| 'startTimeInMillis'
| 'durationInMillis'
| 'shards.total'
| 'shards.failed';
| 'shards.failed'
| 'state';

export type SortDirection = Direction;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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';
Expand All @@ -52,6 +57,7 @@ export const PolicyList: React.FunctionComponent<RouteComponentProps<MatchParams

const { uiMetricService } = useServices();
const { core } = useAppContext();
const { docLinks } = useCore();

// Load retention cluster settings
const {
Expand All @@ -61,6 +67,8 @@ export const PolicyList: React.FunctionComponent<RouteComponentProps<MatchParams
resendRequest: reloadRetentionSettings,
} = useLoadRetentionSettings();

const { data: slmStatus } = useLoadSlmStatus();

const openPolicyDetailsUrl = (newPolicyName: SlmPolicy['name']): string => {
return linkToPolicy(newPolicyName);
};
Expand Down Expand Up @@ -157,9 +165,44 @@ export const PolicyList: React.FunctionComponent<RouteComponentProps<MatchParams
const policySchedules = policies.map((policy: SlmPolicy) => 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 = (
<section data-test-subj="policyList">
{!isSlmRunning ? (
<Fragment>
<EuiCallOut
title={
<FormattedMessage
id="xpack.snapshotRestore.slmWarningTitle"
defaultMessage="Snapshot lifecycle management (SLM) is not running"
/>
}
color="warning"
iconType="warning"
>
<FormattedMessage
id="xpack.snapshotRestore.slmWarningDescription"
defaultMessage="Policies are not being executed. You must restart SLM {slmDocLink}"
values={{
slmDocLink: (
<EuiLink
href={docLinks.links.snapshotRestore.slmStart}
external={true}
target="_blank"
>
{i18n.translate('xpack.snapshotRestore.slmDocLink', {
defaultMessage: 'using the API.',
})}
</EuiLink>
),
}}
/>
</EuiCallOut>
<EuiSpacer />
</Fragment>
) : null}

{hasDuplicateSchedules ? (
<Fragment>
<EuiCallOut
Expand All @@ -174,7 +217,7 @@ export const PolicyList: React.FunctionComponent<RouteComponentProps<MatchParams
>
<FormattedMessage
id="xpack.snapshotRestore.policyScheduleWarningDescription"
defaultMessage="Only one snapshot can be taken at a time. To avoid snapshot failures, edit or delete the policies."
defaultMessage="Only one snapshot can be taken at a time. To avoid snapshot failures, edit the policies to run on different schedules, or delete redundant policies."
/>
</EuiCallOut>
<EuiSpacer />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
Expand Down Expand Up @@ -93,6 +94,15 @@ export const SnapshotTable: React.FunctionComponent<Props> = (props: Props) => {
</EuiLink>
),
},
{
field: 'state',
name: i18n.translate('xpack.snapshotRestore.snapshotList.table.stateColumnTitle', {
defaultMessage: 'State',
}),
truncateText: false,
sortable: false,
render: (state: string) => <SnapshotState state={state} displayTooltipIcon={false} />,
},
{
field: 'repository',
name: i18n.translate('xpack.snapshotRestore.snapshotList.table.repositoryColumnTitle', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,84 +5,66 @@
* 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<Props> = ({ state }) => {
export const SnapshotState: React.FC<Props> = ({ state, displayTooltipIcon }) => {
const { i18n } = useServices();

const stateMap: any = {
[SNAPSHOT_STATE.IN_PROGRESS]: {
icon: <EuiLoadingSpinner size="m" />,
color: 'primary',
label: i18n.translate('xpack.snapshotRestore.snapshotState.inProgressLabel', {
defaultMessage: 'Taking snapshot…',
defaultMessage: 'In progress',
}),
},
[SNAPSHOT_STATE.SUCCESS]: {
icon: <EuiIcon color="success" type="check" />,
color: 'success',
label: i18n.translate('xpack.snapshotRestore.snapshotState.completeLabel', {
defaultMessage: 'Snapshot complete',
defaultMessage: 'Complete',
}),
},
[SNAPSHOT_STATE.FAILED]: {
icon: <EuiIcon color="danger" type="cross" />,
color: 'danger',
label: i18n.translate('xpack.snapshotRestore.snapshotState.failedLabel', {
defaultMessage: 'Snapshot failed',
defaultMessage: 'Failed',
}),
},
[SNAPSHOT_STATE.PARTIAL]: {
icon: <EuiIcon color="warning" type="warning" />,
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: <EuiIcon color="warning" type="warning" />,
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]) {
// Help debug unexpected state.
return state;
}

const { icon, label, tip } = stateMap[state];
const { color, label, tip } = stateMap[state];

const iconTip = tip && (
<Fragment>
{' '}
<EuiIconTip content={tip} />
</Fragment>
);
const iconTip = displayTooltipIcon && tip && <EuiIcon type="questionInCircle" />;

return (
<EuiFlexGroup gutterSize="xs" alignItems="center" responsive={false}>
<EuiFlexItem grow={false}>{icon}</EuiFlexItem>

<EuiFlexItem grow={false}>
{/* Escape flex layout created by EuiFlexItem. */}
<div>
{label}
{iconTip}
</div>
</EuiFlexItem>
</EuiFlexGroup>
<EuiToolTip position="top" content={tip}>
<EuiFlexGroup gutterSize="xs" alignItems="center" responsive={false}>
<EuiHealth color={color}>{label}</EuiHealth>
{iconTip}
</EuiFlexGroup>
</EuiToolTip>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export const TabSummary: React.FC<Props> = ({ snapshotDetails }) => {
</EuiDescriptionListTitle>

<EuiDescriptionListDescription className="eui-textBreakWord" data-test-subj="value">
<SnapshotState state={state} />
<SnapshotState state={state} displayTooltipIcon={true} />
</EuiDescriptionListDescription>
</EuiFlexItem>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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();
});
});
});
16 changes: 16 additions & 0 deletions x-pack/plugins/snapshot_restore/server/routes/api/policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}
})
);
}
Loading
Loading