Skip to content

Commit

Permalink
Merge branch 'SoniaSanzV-snapshots/showSlmAndStatus/elastic#148241'
Browse files Browse the repository at this point in the history
  • Loading branch information
SoniaSanzV committed Dec 9, 2024
2 parents b3d6d91 + 54375bd commit 15d1568
Show file tree
Hide file tree
Showing 17 changed files with 179 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
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',
});
};
23 changes: 23 additions & 0 deletions x-pack/plugins/snapshot_restore/server/routes/api/policy.test.ts
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

0 comments on commit 15d1568

Please sign in to comment.