Update USB devices

- Add columns and filters
- Filter other vm devices in pci and usb devices pages when selecting devices

Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
This commit is contained in:
Francesco Torchia 2024-09-08 18:08:03 +02:00
parent 6959cee050
commit 7c04b1417a
No known key found for this signature in database
GPG Key ID: E6D011B7415D4393
7 changed files with 149 additions and 29 deletions

View File

@ -771,6 +771,14 @@ export function init($plugin, store) {
hiddenNamespaceGroupButton: true, hiddenNamespaceGroupButton: true,
listGroups: [ listGroups: [
{ {
icon: 'icon-list-grouped',
value: 'description',
field: 'groupByDevice',
hideColumn: 'description',
tooltipKey: 'resourceTable.groupBy.device'
},
{
icon: 'icon-cluster',
value: 'node', value: 'node',
field: 'groupByNode', field: 'groupByNode',
hideColumn: 'node', hideColumn: 'node',

View File

@ -55,10 +55,13 @@ export default {
const selectedDevices = []; const selectedDevices = [];
const oldFormatDevices = []; const oldFormatDevices = [];
(this.value?.domain?.devices?.hostDevices || []).forEach(({ name, deviceName }) => { const vmDevices = this.value?.domain?.devices?.hostDevices || [];
const otherDevices = this.otherDevices(vmDevices).map(({name}) => name);
vmDevices.forEach(({ name, deviceName }) => {
const checkName = (deviceName || '').split('/')?.[1]; const checkName = (deviceName || '').split('/')?.[1];
if (checkName && name.includes(checkName)) { if (checkName && name.includes(checkName) && !otherDevices.includes(name)) {
oldFormatDevices.push(name); oldFormatDevices.push(name);
} else if (this.enabledDevices.find(device => device?.metadata?.name === name)) { } else if (this.enabledDevices.find(device => device?.metadata?.name === name)) {
selectedDevices.push(name); selectedDevices.push(name);
@ -96,7 +99,12 @@ export default {
}; };
}); });
set(this.value.domain.devices, 'hostDevices', formatted); const devices = [
...this.otherDevices(this.value.domain.devices.hostDevices),
...formatted,
];
set(this.value.domain.devices, 'hostDevices', devices);
} }
}, },
@ -187,6 +195,10 @@ export default {
}, },
methods: { methods: {
otherDevices(vmDevices) {
return vmDevices.filter((device) => !this.pciDevices.find((pci) => device.name === pci.name));
},
nodeNameFromUid(uid) { nodeNameFromUid(uid) {
for (const deviceUid in this.uniqueDevices) { for (const deviceUid in this.uniqueDevices) {
const nodes = this.uniqueDevices[deviceUid].nodes; const nodes = this.uniqueDevices[deviceUid].nodes;

View File

@ -34,6 +34,36 @@ export default {
const headers = [ const headers = [
{ ...STATE }, { ...STATE },
SIMPLE_NAME, SIMPLE_NAME,
{
name: 'description',
labelKey: 'tableHeaders.description',
value: 'status.description',
sort: ['status.description']
},
{
name: 'node',
labelKey: 'tableHeaders.node',
value: 'status.nodeName',
sort: ['status.nodeName']
},
{
name: 'pciAddress',
label: 'Address',
value: 'status.pciAddress',
sort: ['status.pciAddress']
},
{
name: 'vendorID',
label: 'Vendor ID',
value: 'status.vendorID',
sort: ['status.vendorID', 'status.productID']
},
{
name: 'productID',
label: 'Product ID',
value: 'status.productID',
sort: ['status.productID', 'status.vendorID']
},
]; ];
if (!isSingleProduct) { if (!isSingleProduct) {
@ -58,12 +88,32 @@ export default {
handler(v) { handler(v) {
this.rows = v; this.rows = v;
this.filterRows = this.rows; this.filterRows = this.rows;
console.log(this.filterRows)
}, },
immediate: true, immediate: true,
}, },
}, },
methods: { methods: {
enableGroup(rows = []) {
const row = rows[0];
if (row) {
row.enablePassthroughBulk(rows);
}
},
disableGroup(rows = []) {
rows.forEach((row) => {
if (row.passthroughClaim) {
row.disablePassthrough();
}
});
},
groupIsAllEnabled(rows = []) {
return !rows.find(device => !device.passthroughClaim);
},
changeRows(filterRows) { changeRows(filterRows) {
this.$set(this, 'filterRows', filterRows); this.$set(this, 'filterRows', filterRows);
}, },
@ -94,6 +144,17 @@ export default {
:sort-generation-fn="sortGenerationFn" :sort-generation-fn="sortGenerationFn"
:rows-per-page="10" :rows-per-page="10"
> >
<template #group-by="{group}">
<div :ref="group.key" v-trim-whitespace class="group-tab">
<button v-if="groupIsAllEnabled(group.rows)" type="button" class="btn btn-sm role-secondary mr-5" @click="e=>{disableGroup(group.rows); e.target.blur()}">
{{ t('harvester.usb.disableGroup') }}
</button>
<button v-else type="button" class="btn btn-sm role-secondary mr-5" @click="e=>{enableGroup(group.rows); e.target.blur()}">
{{ t('harvester.usb.enableGroup') }}
</button>
<span v-clean-html="group.key" />
</div>
</template>
<template #cell:claimed="{row}"> <template #cell:claimed="{row}">
<span v-if="row.isEnabled">{{ row.claimedBy }}</span> <span v-if="row.isEnabled">{{ row.claimedBy }}</span>
<span v-else class="text-muted">&mdash;</span> <span v-else class="text-muted">&mdash;</span>

View File

@ -8,6 +8,9 @@ import Banner from '@components/Banner/Banner.vue';
import CompatibilityMatrix from '../CompatibilityMatrix'; import CompatibilityMatrix from '../CompatibilityMatrix';
import DeviceList from './DeviceList'; import DeviceList from './DeviceList';
import remove from 'lodash/remove';
import { get, set } from '@shell/utils/object';
export default { export default {
name: 'VirtualMachineUSBDevices', name: 'VirtualMachineUSBDevices',
components: { components: {
@ -44,6 +47,10 @@ export default {
for (const key in res) { for (const key in res) {
this[key] = res[key]; this[key] = res[key];
} }
this.selectedDevices = (this.value?.domain?.devices?.hostDevices || [])
.map(({ name }) => name)
.filter((name) => this.enabledDevices.find(device => device?.metadata?.name === name));
}, },
data() { data() {
@ -60,13 +67,34 @@ export default {
}; };
}, },
watch: {
selectedDevices(neu) {
const formatted = neu.map((selectedDevice) => {
const deviceCRD = this.enabledDevices.find(device => device.metadata.name === selectedDevice);
const deviceName = deviceCRD?.status?.resourceName;
return {
deviceName,
name: deviceCRD?.metadata.name,
};
});
const devices = [
...this.otherDevices(this.value.domain.devices.hostDevices),
...formatted,
];
set(this.value.domain.devices, 'hostDevices', devices);
}
},
computed: { computed: {
deviceOpts() { deviceOpts() {
const filteredOptions = this.enabledDevices.filter((deviceCRD) => { const filteredOptions = this.enabledDevices.filter((deviceCRD) => {
if (this.selectedDevices.length > 0) { if (this.selectedDevices.length > 0) {
const selectedDevice = this.enabledDevices.find(device => device.metadata.name === this.selectedDevices[0]); const selectedDevice = this.enabledDevices.find(device => device.metadata.name === this.selectedDevices[0]);
return !this.devicesInUse[deviceCRD?.metadata.name] && deviceCRD.status.nodeName === selectedDevice.status.nodeName; return !this.devicesInUse[deviceCRD?.metadata.name] && deviceCRD.status.nodeName === selectedDevice?.status.nodeName;
} }
return !this.devicesInUse[deviceCRD?.metadata.name]; return !this.devicesInUse[deviceCRD?.metadata.name];
@ -76,14 +104,14 @@ export default {
return { return {
value: deviceCRD?.metadata.name, value: deviceCRD?.metadata.name,
label: deviceCRD?.metadata.name, label: deviceCRD?.metadata.name,
displayLabel: deviceCRD?.status?.resourceName, displayLabel: deviceCRD?.status?.description,
}; };
}); });
}, },
enabledDevices() { enabledDevices() {
return this.devices.filter((device) => { return this.devices.filter((device) => {
return device.isEnabled; return device.status.enabled;
}) || []; }) || [];
}, },
@ -108,7 +136,7 @@ export default {
const out = {}; const out = {};
this.enabledDevices.forEach((deviceCRD) => { this.enabledDevices.forEach((deviceCRD) => {
const nodeName = deviceCRD.spec?.nodeName; const nodeName = deviceCRD.status?.nodeName;
if (!out[nodeName]) { if (!out[nodeName]) {
out[nodeName] = [deviceCRD]; out[nodeName] = [deviceCRD];
@ -127,13 +155,19 @@ export default {
remove(out, (nodeName) => { remove(out, (nodeName) => {
const device = this.enabledDevices.find(deviceCRD => deviceCRD.metadata.name === deviceUid); const device = this.enabledDevices.find(deviceCRD => deviceCRD.metadata.name === deviceUid);
return device.spec.nodeName !== nodeName; return device?.status.nodeName !== nodeName;
}); });
}); });
return out; return out;
}, },
}, },
methods: {
otherDevices(vmDevices) {
return vmDevices.filter((device) => !this.devices.find((usb) => device.name === usb.name));
}
}
}; };
</script> </script>

View File

@ -1371,9 +1371,11 @@ harvester:
label: USB Devices label: USB Devices
noPermission: Please contact system admin to add Harvester addons first noPermission: Please contact system admin to add Harvester addons first
goSetting: goSetting:
prefix: The usb addon is not enabled, click prefix: The pcidevices-controller addon is not enabled, click
middle: here middle: here
suffix: to enable it to manage your USB devices. suffix: to enable it to manage your USB devices.
enableGroup: Enable Group
disableGroup: Disable Group
available: Available USB Devices available: Available USB Devices
compatibleNodes: Compatible Nodes compatibleNodes: Compatible Nodes
impossibleSelection: 'There are no hosts with all of the selected devices.' impossibleSelection: 'There are no hosts with all of the selected devices.'

View File

@ -6,12 +6,14 @@ import Banner from '@components/Banner/Banner.vue';
import Loading from '@shell/components/Loading'; import Loading from '@shell/components/Loading';
import MessageLink from '@shell/components/MessageLink'; import MessageLink from '@shell/components/MessageLink';
import { ADD_ONS } from '../config/harvester-map'; import { ADD_ONS } from '../config/harvester-map';
import DeviceList from '../edit/kubevirt.io.virtualmachine/VirtualMachineUSBDevices/DeviceList';
export default { export default {
name: 'ListUsbDevicePage', name: 'ListUsbDevicePage',
components: { components: {
Banner, Banner,
DeviceList,
Loading, Loading,
MessageLink, MessageLink,
}, },
@ -29,7 +31,7 @@ export default {
addons: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS }), addons: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS }),
}); });
this.hasUSBAddon = hash.addons.find(addon => addon.name === ADD_ONS.USB_DEVICE_CONTROLLER)?.spec?.enabled === true; this.hasPCIAddon = hash.addons.find(addon => addon.name === ADD_ONS.PCI_DEVICE_CONTROLLER)?.spec?.enabled === true;
} catch (e) {} } catch (e) {}
} }
}, },
@ -37,7 +39,7 @@ export default {
data() { data() {
return { return {
hasAddonSchema: false, hasAddonSchema: false,
hasUSBAddon: false, hasPCIAddon: false,
schema: null, schema: null,
toUSBAddon: `${ HCI.ADD_ONS }/harvester-system/${ ADD_ONS.USB_DEVICE_CONTROLLER }?mode=edit`, toUSBAddon: `${ HCI.ADD_ONS }/harvester-system/${ ADD_ONS.USB_DEVICE_CONTROLLER }?mode=edit`,
headers: [ headers: [
@ -52,7 +54,7 @@ export default {
return !!this.schema; return !!this.schema;
}, },
rows() { devices() {
const inStore = this.$store.getters['currentProduct'].inStore; const inStore = this.$store.getters['currentProduct'].inStore;
return this.$store.getters[`${ inStore }/all`](HCI.USB_DEVICE) || []; return this.$store.getters[`${ inStore }/all`](HCI.USB_DEVICE) || [];
@ -72,7 +74,7 @@ export default {
{{ t('harvester.usb.noPermission') }} {{ t('harvester.usb.noPermission') }}
</Banner> </Banner>
</div> </div>
<div v-else-if="!hasUSBAddon"> <div v-else-if="!hasPCIAddon">
<Banner color="warning"> <Banner color="warning">
<MessageLink <MessageLink
:to="toUSBAddon" :to="toUSBAddon"
@ -82,5 +84,5 @@ export default {
/> />
</Banner> </Banner>
</div> </div>
<VGpuDeviceList v-else-if="hasSchema" :devices="rows" :schema="schema" /> <DeviceList v-else-if="hasSchema" :devices="devices" :schema="schema" />
</template> </template>

View File

@ -32,7 +32,7 @@ export default class USBDevice extends SteveModel {
out.push( out.push(
{ {
action: 'enablePassthroughBulk', action: 'enablePassthroughBulk',
enabled: !this.isEnabling, enabled: !this.passthroughClaim && !this.status.enabled,
icon: 'icon icon-fw icon-dot', icon: 'icon icon-fw icon-dot',
label: 'Enable Passthrough', label: 'Enable Passthrough',
bulkable: true, bulkable: true,
@ -40,7 +40,7 @@ export default class USBDevice extends SteveModel {
}, },
{ {
action: 'disablePassthrough', action: 'disablePassthrough',
enabled: this.isEnabling && this.claimedByMe, enabled: this.status.enabled,
icon: 'icon icon-fw icon-dot-open', icon: 'icon icon-fw icon-dot-open',
label: 'Disable Passthrough', label: 'Disable Passthrough',
bulkable: true bulkable: true
@ -69,7 +69,7 @@ export default class USBDevice extends SteveModel {
get passthroughClaim() { get passthroughClaim() {
const passthroughClaims = this.$getters['all'](HCI.USB_CLAIM) || []; const passthroughClaims = this.$getters['all'](HCI.USB_CLAIM) || [];
return !!this.status && passthroughClaims.find(req => req?.spec?.nodeName === this.status?.nodeName && req?.spec?.address === this.status?.address); return !!this.status && passthroughClaims.find(req => req?.status?.nodeName === this.status?.nodeName && req?.status?.pciAddress === this.status?.pciAddress);
} }
// this is an id for each 'type' of device - there may be multiple instances of device CRs // this is an id for each 'type' of device - there may be multiple instances of device CRs
@ -98,23 +98,12 @@ export default class USBDevice extends SteveModel {
return this.claimedBy === userName; return this.claimedBy === userName;
} }
// isEnabled controls visibility in vm create page & ability to delete claim
// isEnabling controls ability to add claim
// there will be a brief period where isEnabling === true && isEnabled === false
get isEnabled() {
return !!this.passthroughClaim?.status?.passthroughEnabled;
}
get isEnabling() {
return !!this.passthroughClaim;
}
// map status.passthroughEnabled to disabled/enabled & overwrite default dash colors // map status.passthroughEnabled to disabled/enabled & overwrite default dash colors
get claimStatusDisplay() { get claimStatusDisplay() {
if (!this.passthroughClaim) { if (!this.passthroughClaim) {
return STATUS_DISPLAY.disabled; return STATUS_DISPLAY.disabled;
} }
if (this.isEnabled) { if (this.status.enabled) {
return STATUS_DISPLAY.enabled; return STATUS_DISPLAY.enabled;
} }
@ -155,4 +144,16 @@ export default class USBDevice extends SteveModel {
}, { root: true }); }, { root: true });
} }
} }
// group device list by node
get groupByNode() {
const name = this.status?.nodeName || this.$rootGetters['i18n/t']('generic.none');
return this.$rootGetters['i18n/t']('resourceTable.groupLabel.node', { name: escapeHtml(name) });
}
// group device list by unique device (same vendorid and deviceid)
get groupByDevice() {
return this.status?.description;
}
} }