foreman_snapshot_management 1.7.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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));