Skip to content

Commit

Permalink
Snapshots: show slm and status (elastic#199622)
Browse files Browse the repository at this point in the history
Closes [elastic#148241](elastic#148241)

## Summary

This PR introduces a few changes in the snapshot and restore plugin.

#### Snapshots tab
* The state column has been aded to the table
* In the detail flyout, it has been changed the way in which the status
is displayed (icon and text).
* No new test needed since this info was already there. The related
tests have been updated.

<table>
  <tr>
    <td style="padding-right: 10px;">


<img alt="Screenshot 2024-11-27 at 15 17 03"
src="https://github.com/user-attachments/assets/92b5c5cf-d90b-454c-9cf7-2eb101562c5f">


  </td>
  <td>
<img alt="Screenshot 2024-11-27 at 15 54 32"
src="https://github.com/user-attachments/assets/62395bad-4c70-44bb-84b3-c8a30ae9278a">

  

  </td>
  </tr>
</table>


#### Policies tab
* The copy of the callout for warning that two or more policies have the
same schedule has been changed. For testing that, you need to have two
or more policies that have the same time por execution. No test added
for this, is only a copy change
[[code](https://github.com/elastic/kibana/pull/199622/files#diff-e8e12f0dfdc97e4e064f8a07965312c8c91ca66578bdcaf5ee807e879cebcb6eR207)]
<img width="1249" alt="Screenshot 2024-11-27 at 15 17 25"
src="https://github.com/user-attachments/assets/5417910f-573a-4c22-a5e2-44ec2ce256b3">


* A new callout has been added to warn that the SLM status is different
from “running”. SLM status is "running" by default, but it could happen
that the user stop it for any reason and then does not restart it. This
causes policies not to run when they are scheduled. To known the SLM
status a new api call has been introduced (`GET _slm/status`).
* I've created a new doc link to
https://www.elastic.co/guide/en/elasticsearch/reference/current/slm-api-start.html
    * I've added new tests for the new api call.
  
<img width="1246" alt="Screenshot 2024-11-27 at 15 59 56"
src="https://github.com/user-attachments/assets/cd5ae491-16a4-4a68-a223-f44ce398ac2f">


        

## Testing
You will need to have at least one policy to test this. For that, you
can run Elastic using the following:
```
yarn es snapshot --license=trial -E path.repo=/tmp/es-backups
```

From the console, you can add a repository (you can also do it from the
UI):
```
PUT /_snapshot/my_backup
{
  "type": "fs",
  "settings": {
    "location": "/tmp/es-backups",
    "chunk_size": "10mb"
  }
}
````

And for creating a policy you can run this:
```
PUT _slm/policy/nightly-snapshots
{
  "schedule": "0 30 1 * * ?",       
  "name": "<nightly-snap-{now/d}>", 
  "repository": "my_backup1",
  "retention": {                    
    "expire_after": "30d",
    "min_count": 5,
    "max_count": 50
  }
}
```

SLM status should be started by default (unless you have stopped it).
For starting it you can use `POST /_slm/start` and for stop it `POST
/_slm/stop`

## Demo



https://github.com/user-attachments/assets/b83cd3ba-4821-4295-87f2-ecf427ec46e0

---------

Co-authored-by: shainaraskas <[email protected]>
  • Loading branch information
2 people authored and CAWilson94 committed Dec 9, 2024
1 parent 9579ee3 commit 7557e24
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 7557e24

Please sign in to comment.