feat(lite): add possibility to edit a VIF (#10505)

This commit is contained in:
Alina D
2026-10-29 16:23:38 +01:00
committed by GitHub
parent 79e88245da
commit 909fa94161
28 changed files with 657 additions and 94 deletions

View File

@@ -19,6 +19,7 @@
- [Lite] Display dates and times as `YYYY-MM-DD HH:mm:ss` (PR [#10547](https://github.com/vatesfr/xen-orchestra/pull/10547))
- [Pool/Hosts] Add a side panel displaying the selected host's details, and a search bar to filter the hosts table (PR [#10580](https://github.com/vatesfr/xen-orchestra/pull/10580))
- [VM/Host] Add console clipboard management (PR [#10625](https://github.com/vatesfr/xen-orchestra/pull/10625))
- [VM/Network] Add the possibility to edit a VIF (PR [#10505](https://github.com/vatesfr/xen-orchestra/pull/10505))
## **0.26.0** (2026-10-01)

View File

@@ -25,6 +25,14 @@ export function createVifOperations(xenApi: XenApi) {
qos_algorithm_type?: string
}
const setOtherConfig = (vifRefs: VifRefs, key: string, value: string) =>
Promise.all(
toArray(vifRefs).map(async vifRef => {
await xenApi.call('VIF.remove_from_other_config', [vifRef, key])
await xenApi.call('VIF.add_to_other_config', [vifRef, key, value])
})
)
return {
create: async (vifs: VifCreateParams[]) => {
const results: VifRefs = []
@@ -78,5 +86,34 @@ export function createVifOperations(xenApi: XenApi) {
plug: (vifRefs: VifRefs) => Promise.all(toArray(vifRefs).map(vifRef => xenApi.call('VIF.plug', [vifRef]))),
unplug: (vifRefs: VifRefs) => Promise.all(toArray(vifRefs).map(vifRef => xenApi.call('VIF.unplug', [vifRef]))),
unplugForce: (vifRefs: VifRefs) =>
Promise.all(toArray(vifRefs).map(vifRef => xenApi.call('VIF.unplug_force', [vifRef]))),
move: (vifRefs: VifRefs, networkRef: NetworkRef) =>
Promise.all(toArray(vifRefs).map(vifRef => xenApi.call('VIF.move', [vifRef, networkRef]))),
setIpv4Allowed: (vifRefs: VifRefs, ips: string[]) =>
Promise.all(toArray(vifRefs).map(vifRef => xenApi.call('VIF.set_ipv4_allowed', [vifRef, ips]))),
setIpv6Allowed: (vifRefs: VifRefs, ips: string[]) =>
Promise.all(toArray(vifRefs).map(vifRef => xenApi.call('VIF.set_ipv6_allowed', [vifRef, ips]))),
setLockingMode: (vifRefs: VifRefs, lockingMode: VIF_LOCKING_MODE) =>
Promise.all(toArray(vifRefs).map(vifRef => xenApi.call('VIF.set_locking_mode', [vifRef, lockingMode]))),
setRateLimit: (vifRefs: VifRefs, kbps: number | null) =>
Promise.all(
toArray(vifRefs).map(async vifRef => {
await xenApi.call('VIF.set_qos_algorithm_type', [vifRef, kbps === null ? '' : 'ratelimit'])
await xenApi.call('VIF.remove_from_qos_algorithm_params', [vifRef, 'kbps'])
if (kbps !== null) {
await xenApi.call('VIF.add_to_qos_algorithm_params', [vifRef, 'kbps', String(kbps)])
}
})
),
setTxChecksumming: (vifRefs: VifRefs, enabled: boolean) => setOtherConfig(vifRefs, 'ethtool-tx', String(enabled)),
}
}

View File

@@ -25,6 +25,7 @@
<script lang="ts" setup>
import type { XenApiNetwork, XenApiVif, XenApiVm } from '@/libs/xen-api/xen-api.types.ts'
import { useVifConnection } from '@/modules/vif/composables/use-vif-connection.composable.ts'
import { useVifEdit } from '@/modules/vif/composables/use-vif-edit.composable.ts'
import { useNetworkStore } from '@/stores/xen-api/network.store.ts'
import { useVifStore } from '@/stores/xen-api/vif.store.ts'
import { useVmGuestMetricsStore } from '@/stores/xen-api/vm-guest-metrics.store.ts'
@@ -59,6 +60,8 @@ const { getByOpaqueRef: getGuestMetricsByOpaqueRef } = useVmGuestMetricsStore().
const { t } = useI18n()
const { goToVifEdit } = useVifEdit()
const selectedVifId = useRouteQuery('id')
const getNetworkName = (networkRef: XenApiNetwork['$ref']) => getNetworkByOpaqueRef(networkRef)?.name_label ?? ''
@@ -147,7 +150,10 @@ const { HeadCells, BodyCells } = useVifColumns({
actions: r =>
r({
onClick: () => (selectedVifId.value = vif.uuid),
actions: [connectionAction.value],
actions: [
{ label: t('action:edit'), icon: 'action:edit', onClick: () => goToVifEdit(vif) } satisfies ActionItem,
connectionAction.value,
],
}),
}
},

View File

@@ -0,0 +1,20 @@
<template>
<UiButton size="medium" variant="tertiary" accent="brand" left-icon="action:edit" @click="goToVifEdit(vif)">
{{ t('action:edit') }}
</UiButton>
</template>
<script lang="ts" setup>
import type { XenApiVif } from '@/libs/xen-api/xen-api.types.ts'
import { useVifEdit } from '@/modules/vif/composables/use-vif-edit.composable.ts'
import UiButton from '@core/components/ui/button/UiButton.vue'
import { useI18n } from 'vue-i18n'
const { vif } = defineProps<{
vif: XenApiVif
}>()
const { t } = useI18n()
const { goToVifEdit } = useVifEdit()
</script>

View File

@@ -1,10 +1,10 @@
<template>
<div class="new-vif-buttons-section">
<div class="vif-form-buttons-section">
<UiLink :to="cancelTo" size="medium">
{{ t('cancel') }}
</UiLink>
<UiButton type="submit" size="medium" accent="brand" variant="primary">
{{ t('action:create-vif') }}
{{ submitLabel }}
</UiButton>
</div>
</template>
@@ -17,13 +17,14 @@ import { type RouteLocationRaw } from 'vue-router'
defineProps<{
cancelTo: RouteLocationRaw
submitLabel: string
}>()
const { t } = useI18n()
</script>
<style lang="postcss" scoped>
.new-vif-buttons-section {
.vif-form-buttons-section {
display: flex;
justify-content: center;
align-items: center;

View File

@@ -0,0 +1,106 @@
<template>
<VtsForm class="edit-vif-form" @submit="onSubmit()">
<div class="row">
<VifNetworkSelect v-bind="networkSelectBindings">
<template #option="{ option }">
<VtsOption :option>
<span class="network-select-option">
<VtsIcon :name="option.properties.icon" size="medium" />
{{ option.properties.label }}
</span>
</VtsOption>
</template>
</VifNetworkSelect>
<VifMacInput v-bind="macInputBindings" />
</div>
<div class="row">
<VifRateLimitInput v-bind="rateLimitInputBindings" />
<VifAllowedIpsTextarea v-bind="allowedIpsTextareaBindings" />
</div>
<div class="tx-checksumming">
<VifTxChecksummingCheckbox v-bind="txChecksummingCheckboxBindings" />
</div>
<VifFormButtonsSection :cancel-to :submit-label="t('action:save')" />
</VtsForm>
</template>
<script setup lang="ts">
import type { XenApiVif } from '@/libs/xen-api/xen-api.types.ts'
import VifAllowedIpsTextarea from '@/modules/vif/components/form/inputs/VifAllowedIpsTextarea.vue'
import VifMacInput from '@/modules/vif/components/form/inputs/VifMacInput.vue'
import VifNetworkSelect from '@/modules/vif/components/form/inputs/VifNetworkSelect.vue'
import VifRateLimitInput from '@/modules/vif/components/form/inputs/VifRateLimitInput.vue'
import VifTxChecksummingCheckbox from '@/modules/vif/components/form/inputs/VifTxChecksummingCheckbox.vue'
import VifFormButtonsSection from '@/modules/vif/components/form/VifFormButtonsSection.vue'
import { useEditVifForm } from '@/modules/vif/form/edit/use-edit-vif-form.ts'
import type { EditVifPayload } from '@/modules/vif/jobs/vif-edit.job.ts'
import VtsForm from '@core/components/form/VtsForm.vue'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import VtsOption from '@core/components/select/VtsOption.vue'
import { useI18n } from 'vue-i18n'
import type { RouteLocationRaw } from 'vue-router'
const { vif } = defineProps<{
vif: XenApiVif
cancelTo: RouteLocationRaw
}>()
const emit = defineEmits<{
save: [data: EditVifPayload]
}>()
const { t } = useI18n()
const {
networkSelectBindings,
macInputBindings,
rateLimitInputBindings,
allowedIpsTextareaBindings,
txChecksummingCheckboxBindings,
validateAndBuildPayload,
} = useEditVifForm(vif)
async function onSubmit() {
const payload = await validateAndBuildPayload()
if (payload !== undefined) {
emit('save', payload)
}
}
</script>
<style lang="postcss" scoped>
.edit-vif-form {
.row {
display: flex;
align-items: start;
flex-direction: column;
gap: 2.4rem;
& > * {
width: 100%;
min-width: 0;
}
.network-select-option {
display: flex;
align-items: center;
gap: 0.8rem;
}
@media (--medium-or-large) {
flex-direction: row;
gap: 8rem;
max-width: 88rem;
}
&:not(:first-child) {
margin-block-start: 2.4rem;
}
}
.tx-checksumming {
margin-block-start: 2.4rem;
}
}
</style>

View File

@@ -0,0 +1,26 @@
<template>
<VtsInputWrapper :label="t('mac-address')" :message="messages" wrap-message>
<UiInput v-model.trim="model" accent="brand" :required :placeholder="required ? undefined : t('auto-generated')" />
</VtsInputWrapper>
</template>
<script lang="ts" setup>
import VtsInputWrapper, { type InputWrapperMessage } from '@core/components/input-wrapper/VtsInputWrapper.vue'
import UiInput from '@core/components/ui/input/UiInput.vue'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const { info, error } = defineProps<{
error?: InputWrapperMessage
info?: string
required?: boolean
}>()
const model = defineModel<string>({ required: true })
const { t } = useI18n()
const messages = computed<InputWrapperMessage>(
() => [info, error].filter(message => message !== undefined) as InputWrapperMessage
)
</script>

View File

@@ -20,23 +20,24 @@
<div class="tx-checksumming">
<VifTxChecksummingCheckbox v-bind="txChecksummingCheckboxBindings" />
</div>
<NewVifButtonsSection :cancel-to />
<VifFormButtonsSection :cancel-to :submit-label="t('action:create-vif')" />
</VtsForm>
</template>
<script setup lang="ts">
import type { XenApiVm } from '@/libs/xen-api/xen-api.types.ts'
import VifAllowedIpsTextarea from '@/modules/vif/components/form/new/inputs/VifAllowedIpsTextarea.vue'
import VifMacInput from '@/modules/vif/components/form/new/inputs/VifMacInput.vue'
import VifNetworkSelect from '@/modules/vif/components/form/new/inputs/VifNetworkSelect.vue'
import VifRateLimitInput from '@/modules/vif/components/form/new/inputs/VifRateLimitInput.vue'
import VifTxChecksummingCheckbox from '@/modules/vif/components/form/new/inputs/VifTxChecksummingCheckbox.vue'
import NewVifButtonsSection from '@/modules/vif/components/form/new/NewVifButtonsSection.vue'
import VifAllowedIpsTextarea from '@/modules/vif/components/form/inputs/VifAllowedIpsTextarea.vue'
import VifMacInput from '@/modules/vif/components/form/inputs/VifMacInput.vue'
import VifNetworkSelect from '@/modules/vif/components/form/inputs/VifNetworkSelect.vue'
import VifRateLimitInput from '@/modules/vif/components/form/inputs/VifRateLimitInput.vue'
import VifTxChecksummingCheckbox from '@/modules/vif/components/form/inputs/VifTxChecksummingCheckbox.vue'
import VifFormButtonsSection from '@/modules/vif/components/form/VifFormButtonsSection.vue'
import { useNewVifForm } from '@/modules/vif/form/new/use-new-vif-form.ts'
import type { NewVifPayload } from '@/modules/vif/jobs/vif-create.job.ts'
import VtsForm from '@core/components/form/VtsForm.vue'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import VtsOption from '@core/components/select/VtsOption.vue'
import { useI18n } from 'vue-i18n'
import type { RouteLocationRaw } from 'vue-router'
const { vmRef } = defineProps<{
@@ -48,6 +49,8 @@ const emit = defineEmits<{
create: [data: NewVifPayload]
}>()
const { t } = useI18n()
const {
networkSelectBindings,
macInputBindings,

View File

@@ -1,19 +0,0 @@
<template>
<VtsInputWrapper :label="t('mac-address')" :message="error">
<UiInput v-model.trim="model" accent="brand" :placeholder="t('auto-generated')" />
</VtsInputWrapper>
</template>
<script lang="ts" setup>
import VtsInputWrapper, { type InputWrapperMessage } from '@core/components/input-wrapper/VtsInputWrapper.vue'
import UiInput from '@core/components/ui/input/UiInput.vue'
import { useI18n } from 'vue-i18n'
defineProps<{
error?: InputWrapperMessage
}>()
const model = defineModel<string>({ required: true })
const { t } = useI18n()
</script>

View File

@@ -1,6 +1,7 @@
<template>
<VtsSidePanel :has-selection="!!vif" class="vm-vifs-side-panel" @close="emit('close')">
<template v-if="vif" #actions>
<VifEditButton :vif />
<VifConnectionToggleButton v-if="vm" :vif :vm />
</template>
<template v-if="vif" #default>
@@ -51,6 +52,15 @@
<VtsCopyButton :value="String(vif.MTU)" />
</template>
</VtsCardRowKeyValue>
<!-- RATE LIMIT -->
<VtsCardRowKeyValue>
<template #key>
{{ t('rate-limit') }}
</template>
<template #value>
{{ rateLimit !== undefined ? `${rateLimit} ${t('bytes:kbs')}` : '' }}
</template>
</VtsCardRowKeyValue>
<!-- LOCKING MODE -->
<VtsCardRowKeyValue>
<template #key>
@@ -60,7 +70,15 @@
{{ vif.locking_mode }}
</template>
</VtsCardRowKeyValue>
<!-- TODO Need to add TX Checksumming -->
<!-- TX CHECK SUMMING -->
<VtsCardRowKeyValue>
<template #key>
{{ t('check-summing') }}
</template>
<template #value>
{{ isTxChecksummingEnabled(vif) }}
</template>
</VtsCardRowKeyValue>
</div>
</UiPanelCard>
<!-- VIF NETWORK INFORMATION -->
@@ -109,6 +127,8 @@
<script setup lang="ts">
import type { XenApiVif } from '@/libs/xen-api/xen-api.types.ts'
import VifConnectionToggleButton from '@/modules/vif/components/actions/connection/VifConnectionToggleButton.vue'
import VifEditButton from '@/modules/vif/components/actions/edit/VifEditButton.vue'
import { isTxChecksummingEnabled } from '@/modules/vif/utils/vif-tx-checksumming.util.ts'
import { useNetworkStore } from '@/stores/xen-api/network.store.ts'
import { useVmGuestMetricsStore } from '@/stores/xen-api/vm-guest-metrics.store.ts'
import { useVmStore } from '@/stores/xen-api/vm.store.ts'
@@ -155,6 +175,14 @@ const ipAddresses = computed(() => {
const network = computed(() => (vif !== undefined ? getNetworkByOpaqueRef(vif.network) : undefined))
const status = computed(() => (vif?.currently_attached ? 'connected' : 'disconnected'))
const rateLimit = computed(() => {
if (vif?.qos_algorithm_type !== 'ratelimit') {
return undefined
}
return vif.qos_algorithm_params.kbps
})
</script>
<style scoped lang="postcss">

View File

@@ -0,0 +1,12 @@
import type { XenApiVif } from '@/libs/xen-api/xen-api.types.ts'
import { useRouter } from 'vue-router'
export function useVifEdit() {
const router = useRouter()
function goToVifEdit(vif: XenApiVif) {
return router.push({ name: '/vif/[uuid]/edit', params: { uuid: vif.uuid } })
}
return { goToVifEdit }
}

View File

@@ -0,0 +1,21 @@
import type { XenApiVif } from '@/libs/xen-api/xen-api.types.ts'
import { useVifForm } from '@/modules/vif/form/use-vif-form-base.ts'
import { isTxChecksummingEnabled } from '@/modules/vif/utils/vif-tx-checksumming.util.ts'
import { useI18n } from 'vue-i18n'
export function useEditVifForm(vif: XenApiVif) {
const { t } = useI18n()
const { kbps } = vif.qos_algorithm_params
return useVifForm(
{
network: vif.network,
mac: vif.MAC,
rateLimit: vif.qos_algorithm_type === 'ratelimit' && kbps !== undefined ? Number(kbps) : undefined,
allowedIps: [...vif.ipv4_allowed, ...vif.ipv6_allowed].join('; '),
txChecksumming: isTxChecksummingEnabled(vif),
},
{ isMacRequired: true, macInfo: t('mac-address-change-cut-connection') }
)
}

View File

@@ -1,79 +1,38 @@
import type { XenApiNetwork, XenApiVm } from '@/libs/xen-api/xen-api.types.ts'
import { useNetworkUtils } from '@/modules/network/composables/network-utils.composable.ts'
import {
type BaseVifFormData,
buildBaseVifPayload,
useVifFormBaseValidation,
} from '@/modules/vif/form/use-vif-form-base.ts'
import type { XenApiVm } from '@/libs/xen-api/xen-api.types.ts'
import { useVifForm } from '@/modules/vif/form/use-vif-form-base.ts'
import type { NewVifPayload } from '@/modules/vif/jobs/vif-create.job.ts'
import { useNetworkStore } from '@/stores/xen-api/network.store.ts'
import { objectIcon } from '@core/icons'
import { mergeValidationConfigs, required } from '@core/packages/form-validation'
import { useValidatedForm } from '@core/packages/validated-form'
import { toComputed } from '@core/utils/to-computed.util.ts'
import { type MaybeRefOrGetter, reactive } from 'vue'
export type NewVifFormData = BaseVifFormData & {
network: XenApiNetwork['$ref'] | undefined
}
import type { MaybeRefOrGetter } from 'vue'
export function useNewVifForm(rawVmRef: MaybeRefOrGetter<XenApiVm['$ref']>) {
const vmRef = toComputed(rawVmRef)
const { records: networks } = useNetworkStore().subscribe()
const { getNetworkStatus } = useNetworkUtils()
const formData = reactive<NewVifFormData>({
network: undefined,
mac: '',
rateLimit: undefined,
allowedIps: '',
txChecksumming: true,
})
const { useField, useFormSelect, useSelect, validate } = useValidatedForm(
formData,
mergeValidationConfigs<BaseVifFormData, NewVifFormData>(useVifFormBaseValidation(), {
errors: {
onSubmit: () => ({
network: { required },
}),
},
})
const { validateAndBuildPayload: validateAndBuildVifPayload, ...bindings } = useVifForm(
{
network: undefined,
mac: '',
rateLimit: undefined,
allowedIps: '',
txChecksumming: true,
},
{ isMacRequired: false }
)
const { id: networkSelectId } = useFormSelect('network', networks, {
searchable: true,
required: true,
option: {
id: '$ref',
label: 'name_label',
value: '$ref',
properties: network => ({ icon: objectIcon('network', getNetworkStatus(network)) }),
},
})
async function validateAndBuildPayload(): Promise<NewVifPayload | undefined> {
const isValid = await validate()
const payload = await validateAndBuildVifPayload()
if (!isValid || formData.network === undefined) {
if (payload === undefined) {
return undefined
}
return {
vmRef: vmRef.value,
network: formData.network,
...buildBaseVifPayload(formData),
...payload,
}
}
return {
networkSelectBindings: useSelect(networkSelectId),
macInputBindings: useField('mac'),
rateLimitInputBindings: useField('rateLimit'),
allowedIpsTextareaBindings: useField('allowedIps'),
txChecksummingCheckboxBindings: useField('txChecksumming'),
...bindings,
validateAndBuildPayload,
}
}

View File

@@ -1,24 +1,41 @@
import type { BaseVifPayload } from '@/modules/vif/jobs/vif-create.job.ts'
import type { XenApiNetwork } from '@/libs/xen-api/xen-api.types.ts'
import { useNetworkUtils } from '@/modules/network/composables/network-utils.composable.ts'
import type { BaseVifPayload, VifPayload } from '@/modules/vif/jobs/vif-create.job.ts'
import { useNetworkStore } from '@/stores/xen-api/network.store.ts'
import { objectIcon } from '@core/icons'
import {
type FormValidationConfig,
ipAddresses,
macAddress,
mergeValidationConfigs,
minValue,
or,
required,
withMessage,
} from '@core/packages/form-validation'
import { useValidatedForm } from '@core/packages/validated-form'
import { type IpAddress, isIpv6, parseIpList } from '@core/utils/ip-address.utils.ts'
import { VIF_LOCKING_MODE } from '@vates/types'
import { reactive } from 'vue'
import { useI18n } from 'vue-i18n'
export type BaseVifFormData = {
type BaseVifFormData = {
mac: string
rateLimit: number | undefined
allowedIps: string
txChecksumming: boolean
}
export function useVifFormBaseValidation(): FormValidationConfig<BaseVifFormData> {
type VifFormData = BaseVifFormData & {
network: XenApiNetwork['$ref'] | undefined
}
type VifFormOptions = {
isMacRequired: boolean
macInfo?: string
}
function useVifFormBaseValidation(): FormValidationConfig<BaseVifFormData> {
const { t } = useI18n()
return {
@@ -38,7 +55,7 @@ export function useVifFormBaseValidation(): FormValidationConfig<BaseVifFormData
}
}
export function buildBaseVifPayload(formData: BaseVifFormData): BaseVifPayload {
function buildBaseVifPayload(formData: BaseVifFormData): BaseVifPayload {
const allowedIps = parseIpList(formData.allowedIps) as IpAddress[]
const ipv4 = allowedIps.filter(ip => !isIpv6(ip))
const ipv6 = allowedIps.filter(ip => isIpv6(ip))
@@ -56,3 +73,59 @@ export function buildBaseVifPayload(formData: BaseVifFormData): BaseVifPayload {
other_config: { 'ethtool-tx': String(formData.txChecksumming) },
}
}
export function useVifForm(initialData: VifFormData, { isMacRequired, macInfo }: VifFormOptions) {
const { records: networks } = useNetworkStore().subscribe()
const { getNetworkStatus } = useNetworkUtils()
const formData = reactive<VifFormData>(initialData)
const { useField, useFormSelect, useSelect, validate } = useValidatedForm(
formData,
mergeValidationConfigs<BaseVifFormData, VifFormData>(useVifFormBaseValidation(), {
errors: {
onSubmit: () => {
if (isMacRequired) {
return { network: { required }, mac: { required } }
}
return { network: { required } }
},
},
})
)
const { id: networkSelectId } = useFormSelect('network', networks, {
searchable: true,
required: true,
option: {
id: '$ref',
label: 'name_label',
value: '$ref',
properties: network => ({ icon: objectIcon('network', getNetworkStatus(network)) }),
},
})
async function validateAndBuildPayload(): Promise<VifPayload | undefined> {
const isValid = await validate()
if (!isValid || formData.network === undefined) {
return undefined
}
return {
network: formData.network,
...buildBaseVifPayload(formData),
}
}
return {
networkSelectBindings: useSelect(networkSelectId),
macInputBindings: useField('mac', () => ({ required: isMacRequired, info: macInfo })),
rateLimitInputBindings: useField('rateLimit'),
allowedIpsTextareaBindings: useField('allowedIps'),
txChecksummingCheckboxBindings: useField('txChecksumming'),
validateAndBuildPayload,
}
}

View File

@@ -5,3 +5,8 @@ export const vifsArg = defineJobArg({
identify: (vif: XenApiVif) => vif.$ref,
toArray: true,
})
export const vifArg = defineJobArg({
identify: (vif: XenApiVif) => vif?.$ref,
toArray: false,
})

View File

@@ -16,11 +16,14 @@ export type BaseVifPayload = {
other_config: Record<string, string>
}
export type NewVifPayload = BaseVifPayload & {
vmRef: XenApiVm['$ref']
export type VifPayload = BaseVifPayload & {
network: XenApiNetwork['$ref']
}
export type NewVifPayload = VifPayload & {
vmRef: XenApiVm['$ref']
}
export const useVifCreateJob = defineJob('vif.create', [newVifPayloadArg], () => {
const xapi = useXenApiStore().getXapi()
const { t } = useI18n()

View File

@@ -0,0 +1,7 @@
import type { EditVifPayload } from '@/modules/vif/jobs/vif-edit.job.ts'
import { defineJobArg } from '@core/packages/job'
export const editVifPayloadArg = defineJobArg<EditVifPayload>({
identify: false,
toArray: false,
})

View File

@@ -0,0 +1,130 @@
import type { XenApiVif } from '@/libs/xen-api/xen-api.types.ts'
import { vifArg } from '@/modules/vif/jobs/vif-args.ts'
import type { VifPayload } from '@/modules/vif/jobs/vif-create.job.ts'
import { editVifPayloadArg } from '@/modules/vif/jobs/vif-edit-args.ts'
import { isTxChecksummingEnabled } from '@/modules/vif/utils/vif-tx-checksumming.util.ts'
import { useXenApiStore } from '@/stores/xen-api.store.ts'
import { defineJob, JobError, JobRunningError } from '@core/packages/job'
import { VIF_LOCKING_MODE } from '@vates/types'
import { xor } from 'lodash-es'
import { useI18n } from 'vue-i18n'
export type EditVifPayload = VifPayload
export const useVifEditJob = defineJob('vif.edit', [vifArg, editVifPayloadArg], () => {
const xapi = useXenApiStore().getXapi()
const { t } = useI18n()
function getLockingMode(vif: XenApiVif, payload: EditVifPayload): VIF_LOCKING_MODE {
if (payload.locking_mode !== undefined) {
return payload.locking_mode
}
const isNetworkChanged = payload.network !== vif.network
if (isNetworkChanged) {
return VIF_LOCKING_MODE.NETWORK_DEFAULT
}
const hadAllowedIps = vif.ipv4_allowed.length > 0 || vif.ipv6_allowed.length > 0
const areAllowedIpsRemoved = hadAllowedIps && vif.locking_mode === VIF_LOCKING_MODE.LOCKED
if (areAllowedIpsRemoved) {
return VIF_LOCKING_MODE.NETWORK_DEFAULT
}
return vif.locking_mode
}
async function recreate(vif: XenApiVif, payload: EditVifPayload) {
if (vif.currently_attached) {
await xapi.vif.unplugForce(vif.$ref)
}
await xapi.vif.delete(vif.$ref)
const [newVifRef] = await xapi.vif.create([
{
...payload,
vmRef: vif.VM,
device: vif.device,
MTU: vif.MTU,
locking_mode: getLockingMode(vif, payload),
other_config: { ...vif.other_config, ...payload.other_config },
},
])
if (vif.currently_attached) {
await xapi.vif.plug(newVifRef)
}
return newVifRef
}
const isSameIpList = (a: string[], b: string[]) => xor(a, b).length === 0
async function update(vif: XenApiVif, payload: EditVifPayload) {
if (payload.network !== vif.network) {
try {
await xapi.vif.move(vif.$ref, payload.network)
} catch (error) {
console.warn(`VIF.move failed for ${vif.uuid}, recreating it instead:`, error)
return recreate(vif, payload)
}
}
const ipv4 = payload.ipv4_allowed ?? []
const ipv6 = payload.ipv6_allowed ?? []
if (!isSameIpList(ipv4, vif.ipv4_allowed)) {
await xapi.vif.setIpv4Allowed(vif.$ref, ipv4)
}
if (!isSameIpList(ipv6, vif.ipv6_allowed)) {
await xapi.vif.setIpv6Allowed(vif.$ref, ipv6)
}
const lockingMode = getLockingMode(vif, payload)
if (lockingMode !== vif.locking_mode) {
await xapi.vif.setLockingMode(vif.$ref, lockingMode)
}
const kbps = payload.qos_algorithm_params?.kbps
const currentKbps = vif.qos_algorithm_type === 'ratelimit' ? vif.qos_algorithm_params.kbps : undefined
if (kbps !== currentKbps) {
await xapi.vif.setRateLimit(vif.$ref, kbps === undefined ? null : Number(kbps))
}
const txChecksumming = payload.other_config['ethtool-tx'] === 'true'
if (txChecksumming !== isTxChecksummingEnabled(vif)) {
await xapi.vif.setTxChecksumming(vif.$ref, txChecksumming)
}
return vif.$ref
}
return {
run(vif, payload): Promise<XenApiVif['$ref']> {
const isMacChanged = payload.MAC !== undefined && payload.MAC.toLowerCase() !== vif.MAC.toLowerCase()
if (isMacChanged) {
return recreate(vif, payload)
}
return update(vif, payload)
},
validate: (isRunning, vif, payload) => {
if (isRunning) {
throw new JobRunningError(t('job:edit:in-progress'))
}
if (vif === undefined || payload === undefined) {
throw new JobError(t('job:arg:missing-payload'))
}
},
}
})

View File

@@ -0,0 +1,7 @@
import type { XenApiVif } from '@/libs/xen-api/xen-api.types.ts'
export function isTxChecksummingEnabled(vif: XenApiVif) {
const value = vif.other_config['ethtool-tx']
return value !== 'false' && value !== 'off'
}

View File

@@ -0,0 +1,112 @@
<template>
<UiHeadBar icon="fa:edit">
{{ t('edit-vif') }}
</UiHeadBar>
<div class="card-container">
<VtsOperationPendingCard v-if="isRunning" :title="t('editing-vif')" />
<VtsOperationErrorCard
v-else-if="error"
:title="t('unable-to-edit-vif')"
:error
:error-message="t('edit-vif:error-message')"
>
<template #actions>
<UiButton variant="secondary" accent="brand" size="medium" @click="handleGoBack()">
{{ t('action:go-back') }}
</UiButton>
</template>
</VtsOperationErrorCard>
<VtsStateHero v-else-if="!areVifsReady" format="page" type="busy" size="large" />
<VtsStateHero v-else-if="!vif" format="page" type="not-found" size="large">
{{ t('object-not-found', { id: vifUuid }) }}
</VtsStateHero>
<UiCard v-if="vif" v-show="canDisplayForm">
<UiTitle>{{ t('configuration') }}</UiTitle>
<EditVifForm :vif :cancel-to="cancelRoute" @save="editVif" />
</UiCard>
</div>
</template>
<script setup lang="ts">
import type { XenApiVif } from '@/libs/xen-api/xen-api.types.ts'
import EditVifForm from '@/modules/vif/components/form/edit/EditVifForm.vue'
import { type EditVifPayload, useVifEditJob } from '@/modules/vif/jobs/vif-edit.job.ts'
import { useVifStore } from '@/stores/xen-api/vif.store.ts'
import { useVmStore } from '@/stores/xen-api/vm.store.ts'
import VtsOperationErrorCard from '@core/components/operation-error-card/VtsOperationErrorCard.vue'
import VtsOperationPendingCard from '@core/components/operation-pending-card/VtsOperationPendingCard.vue'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import UiButton from '@core/components/ui/button/UiButton.vue'
import UiCard from '@core/components/ui/card/UiCard.vue'
import UiHeadBar from '@core/components/ui/head-bar/UiHeadBar.vue'
import UiTitle from '@core/components/ui/title/UiTitle.vue'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { type RouteLocationRaw, useRoute, useRouter } from 'vue-router'
const { t } = useI18n()
const router = useRouter()
const route = useRoute<'/vif/[uuid]/edit'>()
const { isReady: areVifsReady, getByUuid: getVifByUuid } = useVifStore().subscribe()
const { getByOpaqueRef: getVmByOpaqueRef } = useVmStore().subscribe()
const vifUuid = computed(() => route.params.uuid as XenApiVif['uuid'])
const vif = computed(() => getVifByUuid(vifUuid.value))
const vm = computed(() => (vif.value === undefined ? undefined : getVmByOpaqueRef(vif.value.VM)))
const formPayload = ref<EditVifPayload>()
const error = ref<Error | undefined>()
const editedVif = ref<XenApiVif>()
const { canRun, run: edit, isRunning } = useVifEditJob(editedVif, formPayload)
const canDisplayForm = computed(() => !isRunning.value && error.value === undefined)
const cancelRoute = computed<RouteLocationRaw>(() => {
if (!vm.value) {
return { name: '/' }
}
return { name: '/vm/[uuid]/network', params: { uuid: vm.value.uuid } }
})
async function editVif(payload: EditVifPayload) {
editedVif.value = vif.value
formPayload.value = payload
if (!canRun.value) {
return
}
const redirectRoute = cancelRoute.value
try {
await edit()
await router.push(redirectRoute)
} catch (_error) {
error.value = _error as Error
}
}
function handleGoBack() {
error.value = undefined
}
</script>
<style lang="postcss" scoped>
.card-container {
padding: 0.8rem;
}
</style>

View File

@@ -1024,6 +1024,13 @@ declare module 'vue-router/auto-routes' {
Record<never, never>,
| never
>,
'/vif/[uuid]/edit': RouteRecordInfo<
'/vif/[uuid]/edit',
'/vif/:uuid/edit',
{ uuid: ParamValue<true> },
{ uuid: ParamValue<false> },
| never
>,
'/vif/new': RouteRecordInfo<
'/vif/new',
'/vif/new',
@@ -1984,6 +1991,12 @@ declare module 'vue-router/auto-routes' {
views:
| never
}
'src/pages/vif/[uuid]/edit.vue': {
routes:
| '/vif/[uuid]/edit'
views:
| never
}
'src/pages/vif/new.vue': {
routes:
| '/vif/new'

View File

@@ -422,6 +422,9 @@
"duplicating-vm": "Duplicating VM",
"duplication-method": "Duplication method",
"duration": "Duration",
"edit-vif": "Edit VIF",
"edit-vif:error-message": "An error occurred while editing the VIF. Please review your configuration settings and try again.",
"editing-vif": "Editing VIF",
"email": "Email",
"empty": "Empty",
"enabled": "Enabled",
@@ -803,6 +806,7 @@
"logs": "Logs",
"mac-address": "MAC address",
"mac-addresses": "MAC addresses",
"mac-address-change-cut-connection": "Changing the MAC address will temporarily cut the connection",
"manage-citrix-pv-drivers-via-windows-update": "Manage citrix PV drivers via Windows Update",
"manage-vm-lifecycle-in-xo-5": "Manage VM lifecycle in XO-5",
"management": "Management",
@@ -1409,6 +1413,7 @@
"unable-to-create-new-vif": "Unable to create new VIF",
"unable-to-delete-network": "Unable to delete network",
"unable-to-duplicate-vm": "Unable to duplicate VM",
"unable-to-edit-vif": "Unable to edit VIF",
"unable-to-migrate-vdi": "Unable to migrate VDI on SR",
"unauthorized": "Unauthorized",
"unknown": "Unknown",

View File

@@ -422,6 +422,9 @@
"duplicating-vm": "Duplication de la VM",
"duplication-method": "Méthode de duplication",
"duration": "Durée",
"edit-vif": "Modifier le VIF",
"edit-vif:error-message": "Une erreur s'est produite lors de la modification du VIF. Veuillez vérifier vos paramètres et réessayer.",
"editing-vif": "Modification du VIF",
"email": "Email",
"empty": "Vide",
"enabled": "Activé",
@@ -803,6 +806,7 @@
"logs": "Logs",
"mac-address": "Adresse MAC",
"mac-addresses": "Adresses MAC",
"mac-address-change-cut-connection": "Modifier l'adresse MAC coupera temporairement la connexion",
"manage-citrix-pv-drivers-via-windows-update": "Gérer les pilotes Citrix PV via Windows Update",
"manage-vm-lifecycle-in-xo-5": "Gérer le cycle de vie de la VM dans XO-5",
"management": "Gestion",
@@ -1409,6 +1413,7 @@
"unable-to-create-new-vif": "Impossible de créer un nouveau VIF",
"unable-to-delete-network": "Impossible de supprimer le réseau",
"unable-to-duplicate-vm": "Impossible de dupliquer la VM",
"unable-to-edit-vif": "Impossible de modifier le VIF",
"unable-to-migrate-vdi": "Impossible de migrer le VDI sur le SR",
"unauthorized": "Non autorisé",
"unknown": "Inconnu",

View File

@@ -31,4 +31,6 @@
<!--packages-start-->
- @xen-orchestra/web-core minor
<!--packages-end-->