foreman_fog_proxmox 0.15.1 → 0.16.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/app/helpers/proxmox_compute_resources_helper.rb +1 -1
  3. data/app/helpers/proxmox_form_helper.rb +2 -2
  4. data/app/helpers/proxmox_vm_attrs_helper.rb +131 -0
  5. data/app/helpers/proxmox_vm_interfaces_helper.rb +3 -3
  6. data/app/helpers/proxmox_vm_volumes_helper.rb +3 -3
  7. data/app/models/concerns/fog_extensions/proxmox/node.rb +1 -1
  8. data/app/models/foreman_fog_proxmox/proxmox_compute_attributes.rb +3 -3
  9. data/app/models/foreman_fog_proxmox/proxmox_images.rb +5 -0
  10. data/app/models/foreman_fog_proxmox/proxmox_version.rb +1 -1
  11. data/app/models/foreman_fog_proxmox/proxmox_vm_commands.rb +1 -1
  12. data/app/models/foreman_fog_proxmox/proxmox_vm_new.rb +1 -1
  13. data/app/overrides/compute_resources_vms/form/add_from_profile_to_compute_attributes_form.rb +8 -0
  14. data/app/overrides/compute_resources_vms/form/add_react_component_to_host.rb +25 -0
  15. data/app/overrides/compute_resources_vms/form/update_react_component_to_host_form.rb +25 -0
  16. data/app/views/compute_resources_vms/form/proxmox/_add_react_component_to_host_form.html.erb +5 -0
  17. data/app/views/compute_resources_vms/form/proxmox/_base.html.erb +21 -21
  18. data/app/views/compute_resources_vms/form/proxmox/_update_react_component_to_host_form.html.erb +26 -0
  19. data/config/routes.rb +3 -3
  20. data/lib/foreman_fog_proxmox/version.rb +1 -1
  21. data/package.json +42 -0
  22. data/test/factories/proxmox_factory.rb +7 -7
  23. data/test/unit/foreman_fog_proxmox/proxmox_compute_attributes_test.rb +1 -1
  24. data/test/unit/foreman_fog_proxmox/proxmox_interfaces_test.rb +6 -6
  25. data/webpack/components/GeneralTabContent.js +107 -0
  26. data/webpack/components/ProxmoxComputeSelectors.js +141 -0
  27. data/webpack/components/ProxmoxContainer/MountPoint.js +91 -0
  28. data/webpack/components/ProxmoxContainer/ProxmoxContainerHardware.js +85 -0
  29. data/webpack/components/ProxmoxContainer/ProxmoxContainerNetwork.js +179 -0
  30. data/webpack/components/ProxmoxContainer/ProxmoxContainerOptions.js +104 -0
  31. data/webpack/components/ProxmoxContainer/ProxmoxContainerStorage.js +194 -0
  32. data/webpack/components/ProxmoxContainer/components/NetworkInterface.js +193 -0
  33. data/webpack/components/ProxmoxServer/ProxmoxServerHardware.js +204 -0
  34. data/webpack/components/ProxmoxServer/ProxmoxServerNetwork.js +161 -0
  35. data/webpack/components/ProxmoxServer/ProxmoxServerOptions.js +105 -0
  36. data/webpack/components/ProxmoxServer/ProxmoxServerStorage.js +272 -0
  37. data/webpack/components/ProxmoxServer/components/CDRom.js +149 -0
  38. data/webpack/components/ProxmoxServer/components/CPUFlagsModal.js +88 -0
  39. data/webpack/components/ProxmoxServer/components/HardDisk.js +143 -0
  40. data/webpack/components/ProxmoxServer/components/NetworkInterface.js +150 -0
  41. data/webpack/components/ProxmoxStoragesUtils.js +50 -0
  42. data/webpack/components/ProxmoxVmType.js +256 -0
  43. data/webpack/components/ProxmoxVmUtils.js +62 -0
  44. data/webpack/components/common/FormInputs.js +143 -0
  45. data/webpack/global_index.js +15 -0
  46. data/webpack/index.js +7 -0
  47. metadata +49 -21
@@ -0,0 +1,161 @@
1
+ import React, { useState, useEffect, useCallback } from 'react';
2
+ import { Title, PageSection, Button } from '@patternfly/react-core';
3
+ import { TimesIcon } from '@patternfly/react-icons';
4
+ import { sprintf, translate as __ } from 'foremanReact/common/I18n';
5
+ import PropTypes from 'prop-types';
6
+ import NetworkInterface from './components/NetworkInterface';
7
+
8
+ const ProxmoxServerNetwork = ({ network, bridges, paramScope }) => {
9
+ const [interfaces, setInterfaces] = useState([]);
10
+ const [nextId, setNextId] = useState(0);
11
+ const [availableIds, setAvailableIds] = useState([]);
12
+ const [usedIds, setUsedIds] = useState(new Set());
13
+ useEffect(() => {
14
+ if (network?.length > 0) {
15
+ const existingIds = new Set();
16
+ network.forEach(net => {
17
+ if (!net.value.model.value) return;
18
+ const id = parseInt(net.value.id.value.replace('net', ''), 10);
19
+ existingIds.add(id);
20
+ addInterface(null, net.value);
21
+ });
22
+ setUsedIds(existingIds);
23
+ }
24
+ }, [network]);
25
+
26
+ const getLowestAvailableId = useCallback(() => {
27
+ let id = 0;
28
+ while (usedIds.has(id)) {
29
+ id += 1;
30
+ }
31
+ return id;
32
+ }, [usedIds]);
33
+ const addInterface = useCallback(
34
+ (event, initialData = null) => {
35
+ if (event) event.preventDefault();
36
+ const netId = getLowestAvailableId();
37
+ const initData = initialData || {
38
+ id: {
39
+ name: `${paramScope}[interfaces_attributes][${nextId}][id]`,
40
+ value: `net${netId}`,
41
+ },
42
+ model: {
43
+ name: `${paramScope}[interfaces_attributes][${nextId}][model]`,
44
+ value: 'virtio',
45
+ },
46
+ bridge: {
47
+ name: `${paramScope}[interfaces_attributes][${nextId}][bridge]`,
48
+ value: bridges?.[0]?.iface || '',
49
+ },
50
+ tag: {
51
+ name: `${paramScope}[interfaces_attributes][${nextId}][tag]`,
52
+ value: '',
53
+ },
54
+ rate: {
55
+ name: `${paramScope}[interfaces_attributes][${nextId}][rate]`,
56
+ value: '',
57
+ },
58
+ queues: {
59
+ name: `${paramScope}[interfaces_attributes][${nextId}][queues]`,
60
+ value: '',
61
+ },
62
+ firewall: {
63
+ name: `${paramScope}[interfaces_attributes][${nextId}][firewall]`,
64
+ value: '0',
65
+ },
66
+ link_down: {
67
+ name: `${paramScope}[interfaces_attributes][${nextId}][link_down]`,
68
+ value: '0',
69
+ },
70
+ };
71
+ setNextId(prevId => {
72
+ if (availableIds.length > 0) {
73
+ setAvailableIds(availableIds.slice(1));
74
+ } else {
75
+ prevId += 1;
76
+ }
77
+ setUsedIds(prevIds => new Set(prevIds).add(netId));
78
+ const newId = availableIds.length > 0 ? availableIds[0] : prevId;
79
+ const newInterface = {
80
+ id: newId,
81
+ data: initData,
82
+ networks: network,
83
+ };
84
+
85
+ setInterfaces(prevInterfaces => [...prevInterfaces, newInterface]);
86
+ return prevId;
87
+ });
88
+ },
89
+ [bridges, network, paramScope, availableIds, nextId, getLowestAvailableId]
90
+ );
91
+
92
+ const removeInterface = idToRemove => {
93
+ const newInterfaces = interfaces.filter(nic => nic.id !== idToRemove);
94
+ setInterfaces(newInterfaces);
95
+ setAvailableIds([...availableIds, idToRemove].sort((a, b) => a - b));
96
+ setUsedIds(prevIds => {
97
+ const newIds = new Set(prevIds);
98
+ newIds.delete(idToRemove);
99
+ return newIds;
100
+ });
101
+ };
102
+
103
+ const updateNetworkData = (id, updatedData) => {
104
+ setInterfaces(
105
+ interfaces.map(net =>
106
+ net.id === id ? { ...net, data: updatedData } : net
107
+ )
108
+ );
109
+ };
110
+
111
+ return (
112
+ <div>
113
+ <PageSection padding={{ default: 'noPadding' }}>
114
+ <Button onClick={addInterface} variant="secondary">
115
+ {__('Add Interface')}
116
+ </Button>
117
+ {interfaces.map(nic => (
118
+ <div key={nic.id} style={{ position: 'relative' }}>
119
+ <div
120
+ style={{
121
+ marginTop: '10px',
122
+ display: 'flex',
123
+ justifyContent: 'space-between',
124
+ alignItems: 'center',
125
+ }}
126
+ >
127
+ <Title headingLevel="h4">
128
+ {sprintf(__('Nic %(nicId)s'), { nicId: nic.id })}
129
+ </Title>
130
+ <button onClick={() => removeInterface(nic.id)} type="button">
131
+ <TimesIcon />
132
+ </button>
133
+ </div>
134
+ <NetworkInterface
135
+ id={nic.id}
136
+ data={nic.data}
137
+ bridges={bridges}
138
+ networks={nic.networks}
139
+ updateNetworkData={updateNetworkData}
140
+ existingInterfaces={interfaces}
141
+ />
142
+ </div>
143
+ ))}
144
+ </PageSection>
145
+ </div>
146
+ );
147
+ };
148
+
149
+ ProxmoxServerNetwork.propTypes = {
150
+ network: PropTypes.object,
151
+ bridges: PropTypes.array,
152
+ paramScope: PropTypes.string,
153
+ };
154
+
155
+ ProxmoxServerNetwork.defaultProps = {
156
+ network: {},
157
+ bridges: [],
158
+ paramScope: '',
159
+ };
160
+
161
+ export default ProxmoxServerNetwork;
@@ -0,0 +1,105 @@
1
+ import React, { useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { translate as __ } from 'foremanReact/common/I18n';
4
+ import InputField from '../common/FormInputs';
5
+ import ProxmoxComputeSelectors from '../ProxmoxComputeSelectors';
6
+
7
+ const ProxmoxServerOptions = ({ options }) => {
8
+ const [opts, setOpts] = useState(options);
9
+
10
+ const handleChange = e => {
11
+ const { name, type, checked, value: targetValue } = e.target;
12
+ let value;
13
+ if (type === 'checkbox') {
14
+ value = checked ? '1' : '0';
15
+ } else {
16
+ value = targetValue;
17
+ }
18
+ const updatedKey = Object.keys(opts).find(key => opts[key].name === name);
19
+ setOpts(prevOpts => ({
20
+ ...prevOpts,
21
+ [updatedKey]: { ...prevOpts[updatedKey], value },
22
+ }));
23
+ };
24
+
25
+ return (
26
+ <div>
27
+ <InputField
28
+ name={opts?.boot?.name}
29
+ label={__('Boot device order')}
30
+ info={__(
31
+ 'Order your devices, e.g. order=net0;ide2;scsi0. Default empty (any)'
32
+ )}
33
+ value={opts?.boot?.value}
34
+ onChange={handleChange}
35
+ />
36
+ <InputField
37
+ name={opts?.onboot?.name}
38
+ label={__('Start at boot')}
39
+ type="checkbox"
40
+ value={opts?.onboot?.value}
41
+ checked={opts?.onboot?.value === '1'}
42
+ onChange={handleChange}
43
+ />
44
+ <InputField
45
+ name={opts?.agent?.name}
46
+ label={__('Qemu Agent')}
47
+ type="checkbox"
48
+ value={opts?.agent?.value}
49
+ checked={opts?.agent?.value === '1'}
50
+ onChange={handleChange}
51
+ />
52
+ <InputField
53
+ name={opts?.kvm?.name}
54
+ label={__('KVM')}
55
+ info={__('Enable/disable KVM hardware virtualization')}
56
+ type="checkbox"
57
+ value={opts?.kvm?.value}
58
+ checked={opts?.kvm?.value === '1'}
59
+ onChange={handleChange}
60
+ />
61
+ <InputField
62
+ name={opts?.vga?.name}
63
+ label={__('VGA')}
64
+ type="select"
65
+ value={opts?.vga?.value}
66
+ options={ProxmoxComputeSelectors.proxmoxVgasMap}
67
+ onChange={handleChange}
68
+ />
69
+ <InputField
70
+ name={opts?.scsihw?.name}
71
+ label={__('SCSI Controller')}
72
+ type="select"
73
+ value={opts?.scsihw?.value}
74
+ options={ProxmoxComputeSelectors.proxmoxScsiControllersMap}
75
+ onChange={handleChange}
76
+ />
77
+ <InputField
78
+ name={opts?.bios?.name}
79
+ label={__('BIOS')}
80
+ type="select"
81
+ options={ProxmoxComputeSelectors.proxmoxBiosMap}
82
+ value={opts?.bios?.value}
83
+ onChange={handleChange}
84
+ />
85
+ <InputField
86
+ name={opts?.ostype?.name}
87
+ label={__('OS Type')}
88
+ type="select"
89
+ options={ProxmoxComputeSelectors.proxmoxOperatingSystemsMap}
90
+ value={opts?.ostype?.value}
91
+ onChange={handleChange}
92
+ />
93
+ </div>
94
+ );
95
+ };
96
+
97
+ ProxmoxServerOptions.propTypes = {
98
+ options: PropTypes.object,
99
+ };
100
+
101
+ ProxmoxServerOptions.defaultProps = {
102
+ options: {},
103
+ };
104
+
105
+ export default ProxmoxServerOptions;
@@ -0,0 +1,272 @@
1
+ import React, { useState, useEffect, useCallback } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { Title, PageSection, Button } from '@patternfly/react-core';
4
+ import { TimesIcon } from '@patternfly/react-icons';
5
+ import { sprintf, translate as __ } from 'foremanReact/common/I18n';
6
+ import HardDisk from './components/HardDisk';
7
+ import CDRom from './components/CDRom';
8
+
9
+ const ProxmoxServerStorage = ({ storage, storages, paramScope, nodeId }) => {
10
+ const [hardDisks, setHardDisks] = useState([]);
11
+ const [nextId, setNextId] = useState(0);
12
+ const [cdRom, setCdRom] = useState(false);
13
+ const [cdRomData, setCdRomData] = useState(null);
14
+ const [nextDeviceNumbers, setNextDeviceNumbers] = useState({
15
+ ide: 0,
16
+ sata: 0,
17
+ scsi: 0,
18
+ virtio: 0,
19
+ });
20
+ const controllerRanges = {
21
+ ide: { min: 0, max: 3 },
22
+ sata: { min: 0, max: 5 },
23
+ scsi: { min: 0, max: 30 },
24
+ virtio: { min: 0, max: 15 },
25
+ };
26
+
27
+ useEffect(() => {
28
+ if (storage?.length > 0) {
29
+ const updatedCounts = { ...nextDeviceNumbers };
30
+ storage.forEach(disk => {
31
+ if (disk.name === 'hard_disk') {
32
+ const controller = disk.value.controller.value;
33
+ const device = parseInt(disk.value.device.value, 10);
34
+ if (device >= updatedCounts[controller]) {
35
+ updatedCounts[controller] = device + 1;
36
+ }
37
+ addHardDisk(null, disk.value, true);
38
+ }
39
+ if (disk.name === 'cdrom') {
40
+ addCDRom(null, disk.value, true);
41
+ }
42
+ });
43
+ setNextDeviceNumbers(updatedCounts);
44
+ }
45
+ }, [storage]);
46
+
47
+ const getNextDevice = useCallback(
48
+ (controller, type = null) => {
49
+ const isDevice2Reserved = controller === 'ide' && type !== 'cdrom';
50
+ for (
51
+ let i = controllerRanges[controller].min;
52
+ i <= controllerRanges[controller].max;
53
+ i++
54
+ ) {
55
+ if (!(isDevice2Reserved && i === 2)) {
56
+ if (
57
+ !hardDisks.some(
58
+ disk =>
59
+ disk.data.controller.value === controller &&
60
+ disk.data.device.value === i
61
+ )
62
+ ) {
63
+ return i;
64
+ }
65
+ }
66
+ }
67
+ return null;
68
+ },
69
+ [hardDisks, controllerRanges]
70
+ );
71
+
72
+ const createUniqueDevice = useCallback(
73
+ (type, selectedController = 'virtio') => {
74
+ let controller = selectedController;
75
+ if (type === 'cdrom') {
76
+ controller = 'ide';
77
+ return { controller, device: 2, id: 'ide2' };
78
+ }
79
+
80
+ const device = getNextDevice(controller, type);
81
+ if (device !== null) {
82
+ const id = `${controller}${device}`;
83
+ const newCounts = {
84
+ ...nextDeviceNumbers,
85
+ [controller]: nextDeviceNumbers[controller] + 1,
86
+ };
87
+ setNextDeviceNumbers(newCounts);
88
+ return { controller, device, id };
89
+ }
90
+ return null;
91
+ },
92
+ [getNextDevice, nextDeviceNumbers]
93
+ );
94
+
95
+ const addHardDisk = useCallback(
96
+ (event, initialData = null, isPreExisting = false) => {
97
+ if (event) event.preventDefault();
98
+ let deviceInfo = null;
99
+ if (!isPreExisting) {
100
+ const selectedController = initialData?.controller?.value || 'virtio';
101
+ deviceInfo = createUniqueDevice('hard_disk', selectedController);
102
+ if (!deviceInfo) return;
103
+ }
104
+ const { controller, device, id } = deviceInfo || {};
105
+ const initHdd = initialData || {
106
+ id: {
107
+ name: `${paramScope}[volumes_attributes][${nextId}][id]`,
108
+ value: id,
109
+ },
110
+ device: {
111
+ name: `${paramScope}[volumes_attributes][${nextId}][device]`,
112
+ value: device,
113
+ },
114
+ storageType: {
115
+ name: `${paramScope}[volumes_attributes][${nextId}][storage_type]`,
116
+ value: 'hard_disk',
117
+ },
118
+ storage: {
119
+ name: `${paramScope}[volumes_attributes][${nextId}][storage]`,
120
+ value: 'local',
121
+ },
122
+ cache: {
123
+ name: `${paramScope}[volumes_attributes][${nextId}][cache]`,
124
+ value: null,
125
+ },
126
+ size: {
127
+ name: `${paramScope}[volumes_attributes][${nextId}][size]`,
128
+ value: 8,
129
+ },
130
+ controller: {
131
+ name: `${paramScope}[volumes_attributes][${nextId}][controller]`,
132
+ value: controller,
133
+ },
134
+ };
135
+
136
+ setNextId(prevNextId => {
137
+ const newNextId = prevNextId + 1;
138
+ const newHardDisk = {
139
+ id: newNextId,
140
+ storages,
141
+ data: initHdd,
142
+ disks: storage,
143
+ };
144
+ setHardDisks(prevHardDisks => [...prevHardDisks, newHardDisk]);
145
+ return newNextId;
146
+ });
147
+ },
148
+ [nextId, paramScope, storage, storages, createUniqueDevice]
149
+ );
150
+
151
+ const removeHardDisk = idToRemove => {
152
+ const newHardDisks = hardDisks.filter(
153
+ hardDisk => hardDisk.id !== idToRemove
154
+ );
155
+ setHardDisks(newHardDisks);
156
+ };
157
+
158
+ const updateHardDiskData = (id, updatedData) => {
159
+ setHardDisks(
160
+ hardDisks.map(disk =>
161
+ disk.id === id ? { ...disk, data: updatedData } : disk
162
+ )
163
+ );
164
+ };
165
+
166
+ const addCDRom = useCallback(
167
+ (event, initialData = null, isPreExisting = false) => {
168
+ if (event) event.preventDefault();
169
+ if (!initialData && cdRom) return;
170
+
171
+ const deviceInfo = initialData
172
+ ? { controller: 'ide', device: 2, id: 'ide2' }
173
+ : createUniqueDevice('cdrom');
174
+ if (!deviceInfo) return;
175
+
176
+ const initCDRom = initialData || {
177
+ id: {
178
+ name: `${paramScope}[volumes_attributes][${nextId}][id]`,
179
+ value: deviceInfo.id,
180
+ },
181
+ volid: {
182
+ name: `${paramScope}[volumes_attributes][${nextId}][volid]`,
183
+ value: '',
184
+ },
185
+ storageType: {
186
+ name: `${paramScope}[volumes_attributes][${nextId}][storageType]`,
187
+ value: 'cdrom',
188
+ },
189
+ storage: {
190
+ name: `${paramScope}[volumes_attributes][${nextId}][storage]`,
191
+ value: 'local',
192
+ },
193
+ cdrom: {
194
+ name: `${paramScope}[volumes_attributes][${nextId}][cdrom]`,
195
+ value: '',
196
+ },
197
+ };
198
+ setCdRom(true);
199
+ setCdRomData(initCDRom);
200
+ },
201
+ [cdRom, nextId, paramScope, createUniqueDevice]
202
+ );
203
+
204
+ const removeCDRom = () => {
205
+ setCdRom(false);
206
+ };
207
+ return (
208
+ <div>
209
+ <PageSection padding={{ default: 'noPadding' }}>
210
+ <Button onClick={addCDRom} variant="secondary" isDisabled={cdRom}>
211
+ {' '}
212
+ {__('Add CD-ROM')}
213
+ </Button>
214
+ {' '}
215
+ <Button onClick={addHardDisk} variant="secondary">
216
+ {__('Add HardDisk')}
217
+ </Button>
218
+ {cdRom && cdRomData && (
219
+ <CDRom
220
+ onRemove={removeCDRom}
221
+ data={cdRomData}
222
+ storages={storages}
223
+ nodeId={nodeId}
224
+ />
225
+ )}
226
+ {hardDisks.map(hardDisk => (
227
+ <div style={{ position: 'relative' }}>
228
+ <div
229
+ style={{
230
+ marginTop: '10px',
231
+ display: 'flex',
232
+ justifyContent: 'space-between',
233
+ alignItems: 'center',
234
+ }}
235
+ >
236
+ <Title headingLevel="h4">
237
+ {sprintf(__('Hard Disk %(hddId)s'), { hddId: hardDisk.id })}
238
+ </Title>
239
+ <button onClick={() => removeHardDisk(hardDisk.id)} type="button">
240
+ <TimesIcon />
241
+ </button>
242
+ </div>
243
+ <HardDisk
244
+ id={hardDisk.id}
245
+ data={hardDisk.data}
246
+ storages={hardDisk.storages}
247
+ disks={hardDisk.disks}
248
+ updateHardDiskData={updateHardDiskData}
249
+ createUniqueDevice={createUniqueDevice}
250
+ />
251
+ </div>
252
+ ))}
253
+ </PageSection>
254
+ </div>
255
+ );
256
+ };
257
+
258
+ ProxmoxServerStorage.propTypes = {
259
+ storage: PropTypes.object,
260
+ storages: PropTypes.array,
261
+ paramScope: PropTypes.string,
262
+ nodeId: PropTypes.string,
263
+ };
264
+
265
+ ProxmoxServerStorage.defaultProps = {
266
+ storage: {},
267
+ storages: [],
268
+ paramScope: '',
269
+ nodeId: '',
270
+ };
271
+
272
+ export default ProxmoxServerStorage;
@@ -0,0 +1,149 @@
1
+ import React, { useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { Title, Divider, PageSection, Radio } from '@patternfly/react-core';
4
+ import { TimesIcon } from '@patternfly/react-icons';
5
+ import { translate as __ } from 'foremanReact/common/I18n';
6
+ import { imagesByStorage, createStoragesMap } from '../../ProxmoxStoragesUtils';
7
+ import InputField from '../../common/FormInputs';
8
+
9
+ const CDRom = ({ onRemove, data, storages, nodeId }) => {
10
+ const [cdrom, setCdrom] = useState(data);
11
+ const storagesMap = createStoragesMap(storages, 'iso', nodeId);
12
+ const imagesMap = imagesByStorage(
13
+ storages,
14
+ nodeId,
15
+ cdrom?.storage?.value,
16
+ 'iso'
17
+ );
18
+
19
+ const handleMediaChange = (_, e) => {
20
+ setCdrom({
21
+ ...cdrom,
22
+ cdrom: {
23
+ ...cdrom.cdrom,
24
+ value: e.target.value,
25
+ },
26
+ });
27
+ };
28
+
29
+ const handleChange = e => {
30
+ const { name, type, checked, value: targetValue } = e.target;
31
+ let value;
32
+ if (type === 'checkbox') {
33
+ value = checked ? '1' : '0';
34
+ } else {
35
+ value = targetValue;
36
+ }
37
+ const updatedKey = Object.keys(cdrom).find(key => cdrom[key].name === name);
38
+ const updatedData = {
39
+ ...cdrom,
40
+ [updatedKey]: { ...cdrom[updatedKey], value },
41
+ };
42
+ setCdrom(updatedData);
43
+ };
44
+
45
+ return (
46
+ <div style={{ position: 'relative' }}>
47
+ <div
48
+ style={{
49
+ display: 'flex',
50
+ justifyContent: 'space-between',
51
+ alignItems: 'center',
52
+ }}
53
+ >
54
+ <Title headingLevel="h4">{__('CD-ROM')}</Title>
55
+ <button onClick={onRemove}>
56
+ <TimesIcon />
57
+ </button>
58
+ </div>
59
+ <Divider component="li" style={{ marginBottom: '1rem' }} />
60
+ <div
61
+ style={{
62
+ display: 'flex',
63
+ justifyContent: 'space-between',
64
+ alignItems: 'center',
65
+ }}
66
+ >
67
+ <Title headingLevel="h5">{__('Media')}</Title>
68
+ </div>
69
+ <Divider component="li" style={{ marginBottom: '1rem' }} />
70
+ <div style={{ display: 'flex', gap: '1rem' }}>
71
+ <Radio
72
+ id="radio-none"
73
+ name={cdrom?.cdrom?.name}
74
+ label={__('None')}
75
+ value="none"
76
+ isChecked={cdrom?.cdrom?.value === 'none'}
77
+ onChange={handleMediaChange}
78
+ />
79
+ <Radio
80
+ id="radio-physical"
81
+ name={cdrom?.cdrom?.name}
82
+ label={__('Physical')}
83
+ value="physical"
84
+ isChecked={cdrom?.cdrom?.value === 'physical'}
85
+ onChange={handleMediaChange}
86
+ />
87
+ <Radio
88
+ id="radio-image"
89
+ name={cdrom?.cdrom?.name}
90
+ label={__('Image')}
91
+ value="image"
92
+ isChecked={cdrom?.cdrom?.value === 'image'}
93
+ onChange={handleMediaChange}
94
+ />
95
+ </div>
96
+ {cdrom?.cdrom?.value === 'image' && (
97
+ <PageSection padding={{ default: 'noPadding' }}>
98
+ <Title headingLevel="h5">{__('Image')}</Title>
99
+ <Divider component="li" style={{ marginBottom: '2rem' }} />
100
+ <InputField
101
+ label={__('Storage')}
102
+ name={cdrom?.storage?.name}
103
+ type="select"
104
+ value={
105
+ cdrom?.storage?.value ||
106
+ (storagesMap?.length > 0 ? storagesMap[0].value : '')
107
+ }
108
+ options={storagesMap}
109
+ onChange={handleChange}
110
+ />
111
+ <InputField
112
+ name={cdrom?.volid?.name}
113
+ label={__('Image ISO')}
114
+ type="select"
115
+ value={cdrom?.volid?.value}
116
+ options={imagesMap}
117
+ onChange={handleChange}
118
+ />
119
+ <input name={cdrom?.storageType?.name} type="hidden" value="cdrom" />
120
+ </PageSection>
121
+ )}
122
+ </div>
123
+ );
124
+ };
125
+
126
+ CDRom.propTypes = {
127
+ onRemove: PropTypes.func.isRequired,
128
+ data: PropTypes.shape({
129
+ cdrom: PropTypes.shape({
130
+ name: PropTypes.string.isRequired,
131
+ value: PropTypes.string.isRequired,
132
+ }).isRequired,
133
+ storage: PropTypes.shape({
134
+ name: PropTypes.string.isRequired,
135
+ value: PropTypes.string,
136
+ }).isRequired,
137
+ volid: PropTypes.shape({
138
+ name: PropTypes.string.isRequired,
139
+ value: PropTypes.string,
140
+ }).isRequired,
141
+ storageType: PropTypes.shape({
142
+ name: PropTypes.string.isRequired,
143
+ }).isRequired,
144
+ }).isRequired,
145
+ storages: PropTypes.array.isRequired,
146
+ nodeId: PropTypes.string.isRequired,
147
+ };
148
+
149
+ export default CDRom;