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,
listGroups: [
{
icon: 'icon-list-grouped',
value: 'description',
field: 'groupByDevice',
hideColumn: 'description',
tooltipKey: 'resourceTable.groupBy.device'
},
{
icon: 'icon-cluster',
value: 'node',
field: 'groupByNode',
hideColumn: 'node',

View File

@ -55,10 +55,13 @@ export default {
const selectedDevices = [];
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];
if (checkName && name.includes(checkName)) {
if (checkName && name.includes(checkName) && !otherDevices.includes(name)) {
oldFormatDevices.push(name);
} else if (this.enabledDevices.find(device => device?.metadata?.name === 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: {
otherDevices(vmDevices) {
return vmDevices.filter((device) => !this.pciDevices.find((pci) => device.name === pci.name));
},
nodeNameFromUid(uid) {
for (const deviceUid in this.uniqueDevices) {
const nodes = this.uniqueDevices[deviceUid].nodes;

View File

@ -34,6 +34,36 @@ export default {
const headers = [
{ ...STATE },
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) {
@ -58,12 +88,32 @@ export default {
handler(v) {
this.rows = v;
this.filterRows = this.rows;
console.log(this.filterRows)
},
immediate: true,
},
},
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) {
this.$set(this, 'filterRows', filterRows);
},
@ -94,6 +144,17 @@ export default {
:sort-generation-fn="sortGenerationFn"
: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}">
<span v-if="row.isEnabled">{{ row.claimedBy }}</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 DeviceList from './DeviceList';
import remove from 'lodash/remove';
import { get, set } from '@shell/utils/object';
export default {
name: 'VirtualMachineUSBDevices',
components: {
@ -44,6 +47,10 @@ export default {
for (const key in res) {
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() {
@ -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: {
deviceOpts() {
const filteredOptions = this.enabledDevices.filter((deviceCRD) => {
if (this.selectedDevices.length > 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];
@ -76,14 +104,14 @@ export default {
return {
value: deviceCRD?.metadata.name,
label: deviceCRD?.metadata.name,
displayLabel: deviceCRD?.status?.resourceName,
displayLabel: deviceCRD?.status?.description,
};
});
},
enabledDevices() {
return this.devices.filter((device) => {
return device.isEnabled;
return device.status.enabled;
}) || [];
},
@ -108,7 +136,7 @@ export default {
const out = {};
this.enabledDevices.forEach((deviceCRD) => {
const nodeName = deviceCRD.spec?.nodeName;
const nodeName = deviceCRD.status?.nodeName;
if (!out[nodeName]) {
out[nodeName] = [deviceCRD];
@ -127,13 +155,19 @@ export default {
remove(out, (nodeName) => {
const device = this.enabledDevices.find(deviceCRD => deviceCRD.metadata.name === deviceUid);
return device.spec.nodeName !== nodeName;
return device?.status.nodeName !== nodeName;
});
});
return out;
},
},
methods: {
otherDevices(vmDevices) {
return vmDevices.filter((device) => !this.devices.find((usb) => device.name === usb.name));
}
}
};
</script>

View File

@ -1371,9 +1371,11 @@ harvester:
label: USB Devices
noPermission: Please contact system admin to add Harvester addons first
goSetting:
prefix: The usb addon is not enabled, click
prefix: The pcidevices-controller addon is not enabled, click
middle: here
suffix: to enable it to manage your USB devices.
enableGroup: Enable Group
disableGroup: Disable Group
available: Available USB Devices
compatibleNodes: Compatible Nodes
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 MessageLink from '@shell/components/MessageLink';
import { ADD_ONS } from '../config/harvester-map';
import DeviceList from '../edit/kubevirt.io.virtualmachine/VirtualMachineUSBDevices/DeviceList';
export default {
name: 'ListUsbDevicePage',
components: {
Banner,
DeviceList,
Loading,
MessageLink,
},
@ -29,7 +31,7 @@ export default {
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) {}
}
},
@ -37,7 +39,7 @@ export default {
data() {
return {
hasAddonSchema: false,
hasUSBAddon: false,
hasPCIAddon: false,
schema: null,
toUSBAddon: `${ HCI.ADD_ONS }/harvester-system/${ ADD_ONS.USB_DEVICE_CONTROLLER }?mode=edit`,
headers: [
@ -52,7 +54,7 @@ export default {
return !!this.schema;
},
rows() {
devices() {
const inStore = this.$store.getters['currentProduct'].inStore;
return this.$store.getters[`${ inStore }/all`](HCI.USB_DEVICE) || [];
@ -72,7 +74,7 @@ export default {
{{ t('harvester.usb.noPermission') }}
</Banner>
</div>
<div v-else-if="!hasUSBAddon">
<div v-else-if="!hasPCIAddon">
<Banner color="warning">
<MessageLink
:to="toUSBAddon"
@ -82,5 +84,5 @@ export default {
/>
</Banner>
</div>
<VGpuDeviceList v-else-if="hasSchema" :devices="rows" :schema="schema" />
<DeviceList v-else-if="hasSchema" :devices="devices" :schema="schema" />
</template>

View File

@ -32,7 +32,7 @@ export default class USBDevice extends SteveModel {
out.push(
{
action: 'enablePassthroughBulk',
enabled: !this.isEnabling,
enabled: !this.passthroughClaim && !this.status.enabled,
icon: 'icon icon-fw icon-dot',
label: 'Enable Passthrough',
bulkable: true,
@ -40,7 +40,7 @@ export default class USBDevice extends SteveModel {
},
{
action: 'disablePassthrough',
enabled: this.isEnabling && this.claimedByMe,
enabled: this.status.enabled,
icon: 'icon icon-fw icon-dot-open',
label: 'Disable Passthrough',
bulkable: true
@ -69,7 +69,7 @@ export default class USBDevice extends SteveModel {
get passthroughClaim() {
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
@ -98,23 +98,12 @@ export default class USBDevice extends SteveModel {
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
get claimStatusDisplay() {
if (!this.passthroughClaim) {
return STATUS_DISPLAY.disabled;
}
if (this.isEnabled) {
if (this.status.enabled) {
return STATUS_DISPLAY.enabled;
}
@ -155,4 +144,16 @@ export default class USBDevice extends SteveModel {
}, { 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;
}
}