Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support USB Passthrough #1069

Merged
merged 20 commits into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions pkg/harvester/config/harvester.js
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,7 @@ export function init($plugin, store) {
HCI.PCI_DEVICE,
HCI.SR_IOVGPU_DEVICE,
HCI.VGPU_DEVICE,
HCI.USB_DEVICE,
HCI.ADD_ONS,
HCI.SECRET,
HCI.SETTING
Expand Down Expand Up @@ -775,6 +776,41 @@ export function init($plugin, store) {
]
});

virtualType({
labelKey: 'harvester.usb.label',
group: 'advanced',
weight: 11,
name: HCI.USB_DEVICE,
namespaced: false,
route: {
name: `${ PRODUCT_NAME }-c-cluster-resource`,
params: { resource: HCI.USB_DEVICE }
},
exact: false,
ifHaveType: HCI.USB_DEVICE,
});

configureType(HCI.USB_DEVICE, {
isCreatable: false,
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',
tooltipKey: 'resourceTable.groupBy.node'
}
]
});

configureType(HCI.ADD_ONS, {
isCreatable: false,
isRemovable: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import AsyncButton from '@shell/components/AsyncButton';
import { escapeHtml } from '@shell/utils/string';

export default {
name: 'HarvesterEnablePassthrough',
name: 'HarvesterEnablePciPassthrough',

components: {
AsyncButton,
Expand Down
118 changes: 118 additions & 0 deletions pkg/harvester/dialog/EnableUSBPassthrough.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<script>
import { HCI } from '../types';
import { mapGetters } from 'vuex';
import { Card } from '@components/Card';
import AsyncButton from '@shell/components/AsyncButton';
import { escapeHtml } from '@shell/utils/string';

export default {
name: 'HarvesterEnableUSBPassthrough',

components: {
AsyncButton,
Card,
},

props: {
resources: {
type: Array,
required: true
}
},

data() {
return {};
},

computed: { ...mapGetters({ t: 'i18n/t' }) },

methods: {
close() {
this.$emit('close');
},

async save(buttonCb) {
// isSingleProduct == this is a standalone Harvester cluster
const isSingleProduct = this.$store.getters['isSingleProduct'];
let userName = 'admin';

// if this is imported Harvester, there may be users other than 'admin
if (!isSingleProduct) {
const user = this.$store.getters['auth/v3User'];

userName = user?.username || user?.id;
}

for (let i = 0; i < this.resources.length; i++) {
const actionResource = this.resources[i];
const inStore = this.$store.getters['currentProduct'].inStore;
const pt = await this.$store.dispatch(`${ inStore }/create`, {
type: HCI.USB_CLAIM,
metadata: {
name: actionResource.metadata.name,
ownerReferences: [{
apiVersion: 'devices.harvesterhci.io/v1beta1',
kind: 'USBDevice',
name: actionResource.metadata.name,
uid: actionResource.metadata.uid,
}]
},
spec: {
pciAddress: actionResource.status.pciAddress,
nodeName: actionResource.status.nodeName,
userName
}
} );

try {
await pt.save();
buttonCb(true);
this.close();
} catch (err) {
this.$store.dispatch('growl/fromError', {
title: this.t('harvester.usb.claimError', { name: escapeHtml(actionResource.metadata.name) }),
err,
}, { root: true });
buttonCb(false);
}
}
}
}
};
</script>

<template>
<Card :show-highlight-border="false">
<h4
slot="title"
v-clean-html="t('promptRemove.title')"
class="text-default-text"
/>

<template #body>
<t k="harvester.usb.enablePassthroughWarning" :raw="true" />
</template>

<div slot="actions" class="actions">
<div class="buttons">
<button class="btn role-secondary mr-10" @click="close">
{{ t('generic.cancel') }}
</button>

<AsyncButton mode="enable" @click="save" />
</div>
</div>
</Card>
</template>

<style lang="scss" scoped>
.actions {
width: 100%;
}

.buttons {
display: flex;
justify-content: flex-end;
width: 100%;
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { mapGetters } from 'vuex';
export default {
props: {
/**
* deviceId/vendorId is unique per type of device - there may be multiple pciDevice CRD objects for a given device
* deviceId/vendorId is unique per type of device - there may be multiple CRD objects for a given device
* {
* [deviceId/vendorId]: {
* nodes: array of devicecrd.status.nodeName's for given device,
* deviceCRDs: array of all instances (pciDevice CRD) of given device
* deviceCRDs: array of all instances of given device
* }
* }
*/
Expand Down Expand Up @@ -78,9 +78,9 @@ export default {
<div class="device-col node-names">
<div class="blank-corner">
<div class="text-right">
{{ t('harvester.pci.matrixDeviceClaimName') }}
{{ t('harvester.devices.matrixDeviceClaimName') }}
</div>
<div>{{ t('harvester.pci.matrixHostName') }}</div>
<div>{{ t('harvester.devices.matrixHostName') }}</div>
</div>
<div v-for="nodeName in allNodeNames" :key="nodeName" class="node-label">
<span> {{ nodeName }}</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ export default {
label: 'Claimed By',
value: 'passthroughClaim.userName',
sort: ['passthroughClaim.userName'],

});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import { allHash } from '@shell/utils/promise';
import { HCI } from '../../../types';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import Banner from '@components/Banner/Banner.vue';
import CompatibilityMatrix from './CompatibilityMatrix';
import CompatibilityMatrix from '../CompatibilityMatrix';
import DeviceList from './DeviceList';

import remove from 'lodash/remove';
import { get, set } from '@shell/utils/object';
import { set } from '@shell/utils/object';

export default {
name: 'VirtualMachinePCIDevices',
Expand Down Expand Up @@ -53,10 +53,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);
Expand Down Expand Up @@ -94,7 +97,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);
}
},

Expand All @@ -113,9 +121,8 @@ export default {
if (vm.metadata.name === this.vm?.metadata?.name) {
return inUse;
}
const devices = get(vm, 'spec.template.spec.domain.devices.hostDevices') || [];

devices.forEach((device) => {
vm.hostDevices.forEach((device) => {
inUse[device.name] = { usedBy: [vm.metadata.name] };
});

Expand All @@ -126,19 +133,19 @@ export default {
},

devicesByNode() {
const out = {};

this.enabledDevices.forEach((deviceCRD) => {
const nodeName = deviceCRD.status?.nodeName;

if (!out[nodeName]) {
out[nodeName] = [deviceCRD];
} else {
out[nodeName].push(deviceCRD);
return this.enabledDevices?.reduce((acc, device) => {
const nodeName = device.status?.nodeName;

if (nodeName) {
if (!acc[nodeName]) {
acc[nodeName] = [];
} else {
acc[nodeName].push(device);
}
}
});

return out;
return acc;
}, {});
},

// determine which nodes contain all devices selected
Expand Down Expand Up @@ -185,6 +192,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;
Expand Down
Loading
Loading