foreman_snapshot_management 1.7.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -1
  3. data/Rakefile +7 -2
  4. data/app/controllers/api/v2/snapshots_controller.rb +22 -6
  5. data/app/models/foreman_snapshot_management/proxmox_extensions.rb +1 -1
  6. data/app/models/foreman_snapshot_management/snapshot.rb +4 -0
  7. data/app/views/api/v2/snapshots/base.json.rabl +2 -0
  8. data/app/views/foreman_snapshot_management/snapshots/_index.html.erb +12 -80
  9. data/app/views/hosts/_snapshots_tab.html.erb +8 -0
  10. data/lib/foreman_snapshot_management/engine.rb +22 -21
  11. data/lib/foreman_snapshot_management/version.rb +1 -1
  12. data/lib/tasks/foreman_snapshot_management_tasks.rake +2 -2
  13. data/locale/de/LC_MESSAGES/foreman_snapshot_management.mo +0 -0
  14. data/locale/de/foreman_snapshot_management.po +52 -19
  15. data/locale/en/LC_MESSAGES/foreman_snapshot_management.mo +0 -0
  16. data/locale/en/foreman_snapshot_management.po +44 -11
  17. data/locale/foreman_snapshot_management.pot +87 -25
  18. data/package.json +46 -0
  19. data/test/controllers/api/v2/snapshots_test.rb +232 -109
  20. data/test/controllers/foreman_snapshot_management/snapshots_controller_test.rb +1 -1
  21. data/test/factories/proxmox_factory.rb +1 -1
  22. data/webpack/components/SnapshotManagement/SnapshotManagement.js +84 -0
  23. data/webpack/components/SnapshotManagement/SnapshotManagementActions.js +212 -0
  24. data/webpack/components/SnapshotManagement/SnapshotManagementConstants.js +9 -0
  25. data/webpack/components/SnapshotManagement/SnapshotManagementReducer.js +100 -0
  26. data/webpack/components/SnapshotManagement/SnapshotManagementSelectors.js +8 -0
  27. data/webpack/components/SnapshotManagement/__tests__/SnapshotManagementActions.test.js +123 -0
  28. data/webpack/components/SnapshotManagement/__tests__/SnapshotManagementReducer.test.js +157 -0
  29. data/webpack/components/SnapshotManagement/__tests__/__snapshots__/SnapshotManagementActions.test.js.snap +314 -0
  30. data/webpack/components/SnapshotManagement/__tests__/__snapshots__/SnapshotManagementReducer.test.js.snap +214 -0
  31. data/webpack/components/SnapshotManagement/components/SnapshotForm/SnapshotForm.js +118 -0
  32. data/webpack/components/SnapshotManagement/components/SnapshotForm/SnapshotFormConstants.js +5 -0
  33. data/webpack/components/SnapshotManagement/components/SnapshotForm/__tests__/SnapshotForm.test.js +26 -0
  34. data/webpack/components/SnapshotManagement/components/SnapshotForm/__tests__/__snapshots__/SnapshotForm.test.js.snap +476 -0
  35. data/webpack/components/SnapshotManagement/components/SnapshotForm/index.js +19 -0
  36. data/webpack/components/SnapshotManagement/components/SnapshotForm/snapshotForm.scss +3 -0
  37. data/webpack/components/SnapshotManagement/components/SnapshotFormModal/SnapshotFormModal.js +37 -0
  38. data/webpack/components/SnapshotManagement/components/SnapshotFormModal/SnapshotFormModalConstants.js +1 -0
  39. data/webpack/components/SnapshotManagement/components/SnapshotFormModal/__tests__/SnapshotFormModal.test.js +19 -0
  40. data/webpack/components/SnapshotManagement/components/SnapshotFormModal/__tests__/__snapshots__/SnapshotFormModal.test.js.snap +19 -0
  41. data/webpack/components/SnapshotManagement/components/SnapshotFormModal/index.js +12 -0
  42. data/webpack/components/SnapshotManagement/components/SnapshotFormModal/useSnapshotFormModal.js +7 -0
  43. data/webpack/components/SnapshotManagement/components/SnapshotList/SnapshotList.js +314 -0
  44. data/webpack/components/SnapshotManagement/components/SnapshotList/SnapshotListHelper.js +70 -0
  45. data/webpack/components/SnapshotManagement/components/SnapshotList/__tests__/SnapshotList.test.js +88 -0
  46. data/webpack/components/SnapshotManagement/components/SnapshotList/__tests__/__snapshots__/SnapshotList.test.js.snap +1081 -0
  47. data/webpack/components/SnapshotManagement/components/SnapshotList/snapshotList.scss +13 -0
  48. data/webpack/components/SnapshotManagement/index.js +33 -0
  49. data/webpack/components/SnapshotManagement/snapshotManagement.scss +5 -0
  50. data/webpack/global_index.js +7 -0
  51. data/webpack/global_test_setup.js +11 -0
  52. data/webpack/index.js +8 -0
  53. data/webpack/reducers.js +7 -0
  54. data/webpack/test_setup.js +17 -0
  55. metadata +39 -33
@@ -19,7 +19,7 @@ module ForemanSnapshotManagement
19
19
  FactoryBot.create(:proxmox_cr)
20
20
  ComputeResource.find_by(type: 'ForemanFogProxmox::Proxmox')
21
21
  end
22
- let(:vmid) { '100' }
22
+ let(:vmid) { '1_100' }
23
23
  let(:proxmox_host) { FactoryBot.create(:host, :managed, :compute_resource => proxmox_compute_resource, :uuid => vmid) }
24
24
  let(:proxmox_snapshot) { 'snapshot1' }
25
25
  setup { ::Fog.mock! }
@@ -2,7 +2,7 @@
2
2
 
3
3
  FactoryBot.define do
4
4
  factory :proxmox_resource, :class => ComputeResource do
5
- sequence(:name) { |n| "compute_resource#{n}" }
5
+ sequence(:name) { |n| "compute_resource_proxmox#{n}" }
6
6
  organizations { [Organization.find_by(name: 'Organization 1')] }
7
7
  locations { [Location.find_by(name: 'Location 1')] }
8
8
 
@@ -0,0 +1,84 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { Button } from 'patternfly-react';
4
+
5
+ import { translate as __ } from 'foremanReact/common/I18n';
6
+
7
+ import SnapshotFormModal from './components/SnapshotFormModal';
8
+ import useSnapshotFormModal from './components/SnapshotFormModal/useSnapshotFormModal';
9
+ import SnapshotList from './components/SnapshotList/SnapshotList';
10
+ import './snapshotManagement.scss';
11
+
12
+ const SnapshotManagement = ({ canCreate, host, ...props }) => {
13
+ const children = [];
14
+ const { setModalOpen, setModalClosed } = useSnapshotFormModal();
15
+
16
+ const onCreateClick = () => {
17
+ setModalOpen();
18
+ };
19
+ const allowedHostAttr = ['id', 'name'];
20
+ const filteredHost = Object.keys(host)
21
+ .filter(key => allowedHostAttr.includes(key))
22
+ .reduce(
23
+ (obj, key) => ({
24
+ ...obj,
25
+ [key]: host[key],
26
+ }),
27
+ {}
28
+ );
29
+
30
+ if (canCreate) {
31
+ children.push(
32
+ <SnapshotFormModal
33
+ key="snapshot-form-modal"
34
+ setModalClosed={setModalClosed}
35
+ host={filteredHost}
36
+ hostId={host.id}
37
+ {...props}
38
+ />
39
+ );
40
+ children.push(
41
+ <Button
42
+ key="snapshot-create-button"
43
+ onClick={onCreateClick}
44
+ className="snapshot-create"
45
+ >
46
+ {__('Create Snapshot')}
47
+ </Button>
48
+ );
49
+ }
50
+
51
+ children.push(
52
+ <SnapshotList key="snapshot-list" host={filteredHost} {...props} />
53
+ );
54
+
55
+ return <div>{children}</div>;
56
+ };
57
+
58
+ SnapshotManagement.propTypes = {
59
+ host: PropTypes.shape({
60
+ id: PropTypes.number.isRequired,
61
+ name: PropTypes.string.isRequired,
62
+ }).isRequired,
63
+ canCreate: PropTypes.bool,
64
+ canUpdate: PropTypes.bool,
65
+ canRevert: PropTypes.bool,
66
+ canDelete: PropTypes.bool,
67
+ capabilities: PropTypes.shape({
68
+ editSnapshotName: PropTypes.bool,
69
+ limitSnapshotNameFormat: PropTypes.bool,
70
+ }),
71
+ };
72
+
73
+ SnapshotManagement.defaultProps = {
74
+ canCreate: false,
75
+ canUpdate: false,
76
+ canRevert: false,
77
+ canDelete: false,
78
+ capabilities: {
79
+ editSnapshotName: true,
80
+ limitSnapshotNameFormat: false,
81
+ },
82
+ };
83
+
84
+ export default SnapshotManagement;
@@ -0,0 +1,212 @@
1
+ import { API, actionTypeGenerator } from 'foremanReact/redux/API';
2
+ import { sprintf, translate as __ } from 'foremanReact/common/I18n';
3
+ import { addToast } from 'foremanReact/redux/actions/toasts';
4
+
5
+ import {
6
+ SNAPSHOT_LIST,
7
+ SNAPSHOT_LIST_URL,
8
+ SNAPSHOT_DELETE,
9
+ SNAPSHOT_DELETE_URL,
10
+ SNAPSHOT_UPDATE,
11
+ SNAPSHOT_UPDATE_URL,
12
+ SNAPSHOT_ROLLBACK,
13
+ SNAPSHOT_ROLLBACK_URL,
14
+ } from './SnapshotManagementConstants';
15
+
16
+ export const loadSnapshotList = hostId => async dispatch => {
17
+ const { REQUEST, SUCCESS, FAILURE } = actionTypeGenerator(SNAPSHOT_LIST);
18
+
19
+ dispatch({
20
+ type: REQUEST,
21
+ payload: { hostId },
22
+ });
23
+ try {
24
+ const { data } = await API.get(
25
+ SNAPSHOT_LIST_URL.replace(':host_id', hostId)
26
+ );
27
+ return dispatch({
28
+ type: SUCCESS,
29
+ payload: { hostId },
30
+ response: data,
31
+ });
32
+ } catch (error) {
33
+ return dispatch({
34
+ type: FAILURE,
35
+ payload: { hostId },
36
+ response: error,
37
+ });
38
+ }
39
+ };
40
+
41
+ export const snapshotDeleteAction = (host, rowData) => async dispatch => {
42
+ const { REQUEST, SUCCESS, FAILURE } = actionTypeGenerator(SNAPSHOT_DELETE);
43
+
44
+ dispatch({
45
+ type: REQUEST,
46
+ payload: {
47
+ host,
48
+ id: rowData.id,
49
+ },
50
+ });
51
+ try {
52
+ const { data } = await API.delete(
53
+ sprintf(SNAPSHOT_DELETE_URL, host.id, rowData.id)
54
+ );
55
+
56
+ dispatch(
57
+ addToast({
58
+ type: 'success',
59
+ message: sprintf(
60
+ __('Successfully removed Snapshot "%s" from host %s'),
61
+ rowData.name,
62
+ host.name
63
+ ),
64
+ key: SUCCESS,
65
+ })
66
+ );
67
+ dispatch(loadSnapshotList(host.id));
68
+ return dispatch({
69
+ type: SUCCESS,
70
+ payload: {
71
+ host,
72
+ id: rowData.id,
73
+ },
74
+ response: data,
75
+ });
76
+ } catch (error) {
77
+ dispatch(
78
+ addToast({
79
+ type: 'error',
80
+ message: sprintf(
81
+ __('Error occurred while removing Snapshot: %s'),
82
+ error
83
+ ),
84
+ key: FAILURE,
85
+ })
86
+ );
87
+ return dispatch({
88
+ type: FAILURE,
89
+ payload: {
90
+ host,
91
+ id: rowData.id,
92
+ },
93
+ response: error,
94
+ });
95
+ }
96
+ };
97
+
98
+ export const snapshotUpdateAction = (host, rowData) => async dispatch => {
99
+ const { REQUEST, SUCCESS, FAILURE } = actionTypeGenerator(SNAPSHOT_UPDATE);
100
+
101
+ dispatch({
102
+ type: REQUEST,
103
+ payload: {
104
+ host,
105
+ id: rowData.id,
106
+ snapshot: {
107
+ name: rowData.name,
108
+ description: rowData.description,
109
+ },
110
+ },
111
+ });
112
+ try {
113
+ const { data } = await API.put(
114
+ sprintf(SNAPSHOT_UPDATE_URL, host.id, rowData.id),
115
+ {
116
+ snapshot: {
117
+ name: rowData.name,
118
+ description: rowData.description,
119
+ },
120
+ }
121
+ );
122
+ dispatch(
123
+ addToast({
124
+ type: 'success',
125
+ message: sprintf(
126
+ __('Successfully updated Snapshot "%s"'),
127
+ rowData.name
128
+ ),
129
+ key: SUCCESS,
130
+ })
131
+ );
132
+ return dispatch({
133
+ type: SUCCESS,
134
+ payload: {
135
+ host,
136
+ id: rowData.id,
137
+ },
138
+ response: data,
139
+ });
140
+ } catch (error) {
141
+ dispatch(
142
+ addToast({
143
+ type: 'error',
144
+ message: sprintf(
145
+ __('Error occurred while updating Snapshot: %s'),
146
+ error
147
+ ),
148
+ key: FAILURE,
149
+ })
150
+ );
151
+ return dispatch({
152
+ type: FAILURE,
153
+ payload: {
154
+ host,
155
+ id: rowData.id,
156
+ },
157
+ response: error,
158
+ });
159
+ }
160
+ };
161
+
162
+ export const snapshotRollbackAction = (host, rowData) => async dispatch => {
163
+ const { REQUEST, SUCCESS, FAILURE } = actionTypeGenerator(SNAPSHOT_ROLLBACK);
164
+
165
+ dispatch({
166
+ type: REQUEST,
167
+ payload: {
168
+ host,
169
+ id: rowData.id,
170
+ },
171
+ });
172
+ try {
173
+ const { data } = await API.put(
174
+ sprintf(SNAPSHOT_ROLLBACK_URL, host.id, rowData.id)
175
+ );
176
+ dispatch(
177
+ addToast({
178
+ type: 'success',
179
+ message: sprintf(
180
+ __('Successfully rolled back Snapshot "%s" on host %s'),
181
+ rowData.name,
182
+ host.name
183
+ ),
184
+ key: SUCCESS,
185
+ })
186
+ );
187
+ return dispatch({
188
+ type: SUCCESS,
189
+ payload: {
190
+ host,
191
+ id: rowData.id,
192
+ },
193
+ response: data,
194
+ });
195
+ } catch (error) {
196
+ dispatch(
197
+ addToast({
198
+ type: 'error',
199
+ message: sprintf(__('Error occurred while rolling back VM: %s'), error),
200
+ key: FAILURE,
201
+ })
202
+ );
203
+ return dispatch({
204
+ type: FAILURE,
205
+ payload: {
206
+ host,
207
+ id: rowData.id,
208
+ },
209
+ response: error,
210
+ });
211
+ }
212
+ };
@@ -0,0 +1,9 @@
1
+ export const SNAPSHOT_LIST = 'FOREMAN_SNAPSHOT_MANAGEMENT_SNAPSHOT_LIST';
2
+ export const SNAPSHOT_LIST_URL = '/api/hosts/:host_id/snapshots/';
3
+ export const SNAPSHOT_DELETE = 'FOREMAN_SNAPSHOT_MANAGEMENT_SNAPSHOT_DELETE';
4
+ export const SNAPSHOT_DELETE_URL = '/api/hosts/%s/snapshots/%s/';
5
+ export const SNAPSHOT_UPDATE = 'FOREMAN_SNAPSHOT_MANAGEMENT_SNAPSHOT_UPDATE';
6
+ export const SNAPSHOT_UPDATE_URL = '/api/hosts/%s/snapshots/%s/';
7
+ export const SNAPSHOT_ROLLBACK =
8
+ 'FOREMAN_SNAPSHOT_MANAGEMENT_SNAPSHOT_ROLLBACK';
9
+ export const SNAPSHOT_ROLLBACK_URL = '/api/hosts/%s/snapshots/%s/revert';
@@ -0,0 +1,100 @@
1
+ // This is an example for a generic redux's reducer
2
+ // Reducers should be registered to foreman-core
3
+ // For a further registration demonstration, have a look in `webpack/global_index.js`
4
+
5
+ import Immutable from 'seamless-immutable';
6
+ import { cloneDeep, findIndex } from 'lodash';
7
+
8
+ import { actionTypeGenerator } from 'foremanReact/redux/API';
9
+
10
+ import {
11
+ SNAPSHOT_LIST,
12
+ SNAPSHOT_DELETE,
13
+ SNAPSHOT_UPDATE,
14
+ SNAPSHOT_ROLLBACK,
15
+ } from './SnapshotManagementConstants';
16
+
17
+ export const initialState = Immutable({
18
+ isLoading: true,
19
+ isWorking: false,
20
+ hasError: false,
21
+ snapshots: [],
22
+ });
23
+
24
+ export default (state = initialState, action) => {
25
+ const { response } = action;
26
+
27
+ const listTypes = actionTypeGenerator(SNAPSHOT_LIST);
28
+ const deleteTypes = actionTypeGenerator(SNAPSHOT_DELETE);
29
+ const updateTypes = actionTypeGenerator(SNAPSHOT_UPDATE);
30
+ const rollbackTypes = actionTypeGenerator(SNAPSHOT_ROLLBACK);
31
+
32
+ switch (action.type) {
33
+ case 'SNAPSHOT_FORM_SUBMITTED':
34
+ return state.merge({
35
+ needsReload: true,
36
+ });
37
+ case listTypes.REQUEST:
38
+ return state.merge({
39
+ snapshots: [],
40
+ isLoading: true,
41
+ hasError: false,
42
+ needsReload: false,
43
+ });
44
+ case listTypes.SUCCESS:
45
+ return state.merge({
46
+ snapshots: response.results,
47
+ isLoading: false,
48
+ needsReload: false,
49
+ });
50
+ case listTypes.FAILURE:
51
+ return state.merge({
52
+ error: response,
53
+ hasError: true,
54
+ isLoading: false,
55
+ needsReload: false,
56
+ });
57
+ case deleteTypes.REQUEST:
58
+ return state.merge({
59
+ isWorking: true,
60
+ });
61
+ case deleteTypes.SUCCESS:
62
+ case deleteTypes.FAILURE:
63
+ return state.merge({
64
+ isWorking: false,
65
+ });
66
+ case updateTypes.REQUEST:
67
+ return state.merge({
68
+ isWorking: true,
69
+ });
70
+ case updateTypes.SUCCESS: {
71
+ const snapshots = cloneDeep(state.snapshots);
72
+ const index = findIndex(snapshots, { id: response.id });
73
+
74
+ snapshots[index].name = response.name;
75
+ snapshots[index].description = response.description;
76
+
77
+ return state.merge({
78
+ isWorking: false,
79
+ snapshots,
80
+ });
81
+ }
82
+ case updateTypes.FAILURE:
83
+ return state.merge({
84
+ isWorking: false,
85
+ });
86
+ case rollbackTypes.REQUEST:
87
+ return state.merge({
88
+ snapshots: state.snapshots,
89
+ isWorking: true,
90
+ });
91
+ case rollbackTypes.SUCCESS:
92
+ case rollbackTypes.FAILURE:
93
+ return state.merge({
94
+ snapshots: state.snapshots,
95
+ isWorking: false,
96
+ });
97
+ default:
98
+ return state;
99
+ }
100
+ };
@@ -0,0 +1,8 @@
1
+ const snapshotMgmt = state => state.foremanSnapshotManagement;
2
+
3
+ export const selectSnapshots = state => snapshotMgmt(state).snapshots;
4
+ export const selectIsLoading = state => snapshotMgmt(state).isLoading;
5
+ export const selectIsWorking = state => snapshotMgmt(state).isWorking;
6
+ export const selectHasError = state => snapshotMgmt(state).hasError;
7
+ export const selectError = state => snapshotMgmt(state).error;
8
+ export const selectNeedsReload = state => snapshotMgmt(state).needsReload;
@@ -0,0 +1,123 @@
1
+ import { testActionSnapshotWithFixtures } from 'react-redux-test-utils';
2
+ import { API } from 'foremanReact/redux/API';
3
+
4
+ import {
5
+ loadSnapshotList,
6
+ snapshotDeleteAction,
7
+ snapshotRollbackAction,
8
+ snapshotUpdateAction,
9
+ } from '../SnapshotManagementActions';
10
+
11
+ jest.mock('foremanReact/redux/API/API');
12
+
13
+ const successResponse = {
14
+ data: 'some-data',
15
+ };
16
+
17
+ const doLoadSnapshotList = (hostId, serverMock) => {
18
+ API.get.mockImplementation(serverMock);
19
+
20
+ return loadSnapshotList(hostId);
21
+ };
22
+
23
+ const doDeleteSnapshot = (host, rowData, serverMock) => {
24
+ API.delete.mockImplementation(serverMock);
25
+
26
+ return snapshotDeleteAction(host, rowData);
27
+ };
28
+
29
+ const doRollbackSnapshot = (host, rowData, serverMock) => {
30
+ API.put.mockImplementation(serverMock);
31
+
32
+ return snapshotRollbackAction(host, rowData);
33
+ };
34
+
35
+ const doUpdateSnapshot = (host, rowData, serverMock) => {
36
+ API.put.mockImplementation(serverMock);
37
+
38
+ return snapshotUpdateAction(host, rowData);
39
+ };
40
+
41
+ const listFixtures = {
42
+ 'should load snapshots and success': () =>
43
+ doLoadSnapshotList(42, async () => successResponse),
44
+
45
+ 'should load snapshots and fail': () =>
46
+ doLoadSnapshotList(42, async () => {
47
+ throw new Error('some-error');
48
+ }),
49
+ };
50
+
51
+ describe('Snapshot list actions', () =>
52
+ testActionSnapshotWithFixtures(listFixtures));
53
+
54
+ const deleteFixtures = {
55
+ 'should delete snapshot and success': () =>
56
+ doDeleteSnapshot(
57
+ { id: 42, name: 'deep.thought' },
58
+ { id: 'snapshot-0815', name: 'Savegame' },
59
+ async () => successResponse
60
+ ),
61
+
62
+ 'should load snapshots and fail': () =>
63
+ doDeleteSnapshot(
64
+ { id: 42, name: 'deep.thought' },
65
+ { id: 'snapshot-0815', name: 'Savegame' },
66
+ async () => {
67
+ throw new Error('some-error');
68
+ }
69
+ ),
70
+ };
71
+
72
+ describe('Snapshot snapshot-delete actions', () =>
73
+ testActionSnapshotWithFixtures(deleteFixtures));
74
+
75
+ const rollbackFixtures = {
76
+ 'should rollback snapshot and success': () =>
77
+ doRollbackSnapshot(
78
+ { id: 42, name: 'deep.thought' },
79
+ { id: 'snapshot-0815', name: 'Savegame' },
80
+ async () => successResponse
81
+ ),
82
+
83
+ 'should load snapshots and fail': () =>
84
+ doRollbackSnapshot(
85
+ { id: 42, name: 'deep.thought' },
86
+ { id: 'snapshot-0815', name: 'Savegame' },
87
+ async () => {
88
+ throw new Error('some-error');
89
+ }
90
+ ),
91
+ };
92
+
93
+ describe('Snapshot snapshot-rollback actions', () =>
94
+ testActionSnapshotWithFixtures(rollbackFixtures));
95
+
96
+ const updateFixtures = {
97
+ 'should update snapshot and success': () =>
98
+ doUpdateSnapshot(
99
+ { id: 42, name: 'deep.thought' },
100
+ {
101
+ id: 'snapshot-0815',
102
+ name: 'Savegame',
103
+ description: 'Saw the three headed monkey!',
104
+ },
105
+ async () => successResponse
106
+ ),
107
+
108
+ 'should load snapshots and fail': () =>
109
+ doUpdateSnapshot(
110
+ { id: 42, name: 'deep.thought' },
111
+ {
112
+ id: 'snapshot-0815',
113
+ name: 'Savegame',
114
+ description: 'Saw the three headed monkey!',
115
+ },
116
+ async () => {
117
+ throw new Error('some-error');
118
+ }
119
+ ),
120
+ };
121
+
122
+ describe('Snapshot snapshot-update actions', () =>
123
+ testActionSnapshotWithFixtures(updateFixtures));