From f7df1c410936b1f136e81fc32b8b83b4d152d4b5 Mon Sep 17 00:00:00 2001 From: Florent BEAUCHAMP Date: Fri, 27 Sep 2024 10:30:33 +0200 Subject: [PATCH 1/4] feat(xo-server/api): add a schedule.runSequence method (#7985) --- packages/xo-server/src/api/schedule.mjs | 24 +++++++++++++++++++ .../xo-server/src/xo-mixins/jobs/index.mjs | 4 ++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/packages/xo-server/src/api/schedule.mjs b/packages/xo-server/src/api/schedule.mjs index 2d73c246b9b..9ceaf0dac7e 100644 --- a/packages/xo-server/src/api/schedule.mjs +++ b/packages/xo-server/src/api/schedule.mjs @@ -1,5 +1,7 @@ // FIXME so far, no acls for schedules +import { Task } from '@xen-orchestra/mixins/Tasks.mjs' + export async function getAll() { return /* await */ this.getAllSchedules() } @@ -64,3 +66,25 @@ delete_.params = { } export { delete_ as delete } + +export async function runSequence({ schedules }) { + const t = this.tasks.create({ type: 'xo:schedule:sequence', name: 'Schedule sequence' }) + await t.run(async () => { + const nb = schedules.length + const signal = Task.abortSignal + for (let i = 0; i < nb; i++) { + signal.throwIfAborted() + Task.set('progress', Math.round((i * 100) / nb)) + const idSchedule = schedules[i] + // we can't auto resolve array parameters, we have to resolve them by hand + const schedule = await this.getSchedule(idSchedule) + const job = await this.getJob(schedule.jobId) + await this.runJob(job, schedule) + } + }) +} +runSequence.permission = 'admin' +runSequence.description = 'Run a sequence of schedules, one after the other' +runSequence.params = { + schedules: { type: 'array', items: { type: 'string' } }, +} diff --git a/packages/xo-server/src/xo-mixins/jobs/index.mjs b/packages/xo-server/src/xo-mixins/jobs/index.mjs index 1409fcca952..79935800355 100644 --- a/packages/xo-server/src/xo-mixins/jobs/index.mjs +++ b/packages/xo-server/src/xo-mixins/jobs/index.mjs @@ -165,7 +165,7 @@ export default class Jobs { } @decorateWith(defer) - async _runJob($defer, job, schedule, data_) { + async runJob($defer, job, schedule, data_) { const logger = this._logger const { id, type } = job @@ -316,7 +316,7 @@ export default class Jobs { const jobs = await Promise.all(idSequence.map(id => this.getJob(id))) for (const job of jobs) { - await this._runJob(job, schedule, data) + await this.runJob(job, schedule, data) } } } From d520e53f9e7ad0d45ee45ebfd13d402a970deede Mon Sep 17 00:00:00 2001 From: Pierre Donias Date: Fri, 27 Sep 2024 12:13:55 +0200 Subject: [PATCH 2/4] feat(xo-web/backup): backup job sequences (#8014) See #7985 --- CHANGELOG.unreleased.md | 4 +- packages/xo-web/src/common/intl/messages.js | 5 + packages/xo-web/src/common/render-xo-item.js | 82 ++++++-- packages/xo-web/src/common/select-objects.js | 1 + packages/xo-web/src/icons.scss | 4 + packages/xo-web/src/xo-app/backup/edit.js | 33 ++- packages/xo-web/src/xo-app/backup/index.js | 11 +- .../xo-web/src/xo-app/backup/new/index.js | 1 + .../src/xo-app/backup/new/sequence/index.js | 191 ++++++++++++++++++ .../xo-web/src/xo-app/backup/sequences.js | 123 +++++++++++ packages/xo-web/src/xo-app/backup/utils.js | 6 +- packages/xo-web/src/xo-app/menu/index.js | 5 + 12 files changed, 441 insertions(+), 25 deletions(-) create mode 100644 packages/xo-web/src/xo-app/backup/new/sequence/index.js create mode 100644 packages/xo-web/src/xo-app/backup/sequences.js diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index 498050711e2..efed7e325a6 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -19,7 +19,7 @@ - `rest get --output $file` now displays progress information during download - `rest post` and `rest put` now accept `--input $file` to upload a file and display progress information - [Backup] Detect invalid VDI exports that are incorrectly reported as successful by XAPI -- [Backup/HealthCheck] Improve error messages on health check timeout (PR [#8016](https://github.com/vatesfr/xen-orchestra/pull/8016)) +- [Backup] Backup job sequences: configure lists of backup jobs to run in order one after the other (PRs [#7985](https://github.com/vatesfr/xen-orchestra/pull/7985), [#8014](https://github.com/vatesfr/xen-orchestra/pull/8014)) ### Bug fixes @@ -48,6 +48,6 @@ - xen-api minor - xo-cli minor - xo-server minor -- xo-web patch +- xo-web minor diff --git a/packages/xo-web/src/common/intl/messages.js b/packages/xo-web/src/common/intl/messages.js index ec2412c27c9..5a574050983 100644 --- a/packages/xo-web/src/common/intl/messages.js +++ b/packages/xo-web/src/common/intl/messages.js @@ -152,6 +152,8 @@ const messages = { xcpNg: 'XCP-ng', noFileSelected: 'No file selected', nRetriesVmBackupFailures: 'Number of retries if VM backup fails', + sequence: 'Sequence', + sequences: 'Sequences', // ----- Modals ----- alertOk: 'OK', @@ -531,7 +533,9 @@ const messages = { scheduleAdd: 'Add a schedule', scheduleDelete: 'Delete', scheduleRun: 'Run schedule', + scheduleSequence: 'Schedule sequence', unnamedSchedule: 'Unnamed schedule', + unnamedJob: 'Unnamed Job', deleteSelectedSchedules: 'Delete selected schedules', noScheduledJobs: 'No scheduled jobs.', legacySnapshotsLink: 'You can delete all your legacy backup snapshots.', @@ -1670,6 +1674,7 @@ const messages = { missingVm: 'Missing VM', missingVmInJob: 'This VM does not belong to this job', missingSchedule: 'Missing schedule', + unknownSchedule: 'Unknown schedule', noDetachedBackups: 'No backups', noDuplicatedMacAddresses: 'No duplicated MAC addresses', reason: 'Reason', diff --git a/packages/xo-web/src/common/render-xo-item.js b/packages/xo-web/src/common/render-xo-item.js index a059d894704..226dfa02d20 100644 --- a/packages/xo-web/src/common/render-xo-item.js +++ b/packages/xo-web/src/common/render-xo-item.js @@ -4,6 +4,7 @@ import CopyToClipboard from 'react-copy-to-clipboard' import PropTypes from 'prop-types' import React from 'react' import { get } from '@xen-orchestra/defined' +import { injectState, provideState } from 'reaclette' import find from 'lodash/find.js' import isEmpty from 'lodash/isEmpty.js' @@ -13,7 +14,16 @@ import Link from './link' import Tooltip from './tooltip' import { addSubscriptions, connectStore, formatSize, NumericDate, ShortDate } from './utils' import { createGetObject, createSelector } from './selectors' -import { isSrWritable, subscribeBackupNgJobs, subscribeProxies, subscribeRemotes, subscribeUsers } from './xo' +import { + isSrWritable, + subscribeBackupNgJobs, + subscribeMetadataBackupJobs, + subscribeProxies, + subscribeRemotes, + subscribeSchedules, + subscribeUsers, + subscribeMirrorBackupJobs, +} from './xo' // =================================================================== @@ -493,6 +503,62 @@ BackupJob.defaultProps = { // =================================================================== +export const Schedule = decorate([ + addSubscriptions(({ id }) => ({ + schedule: cb => subscribeSchedules(schedules => cb(schedules.find(schedule => schedule.id === id))), + backupJobs: subscribeBackupNgJobs, + metadataBackupJobs: subscribeMetadataBackupJobs, + mirrorBackupJobs: subscribeMirrorBackupJobs, + })), + provideState({ + initialState: () => ({}), + computed: { + job: (_, { backupJobs = [], metadataBackupJobs = [], mirrorBackupJobs = [], schedule }) => + schedule && [...backupJobs, ...metadataBackupJobs, ...mirrorBackupJobs].find(job => job.id === schedule.jobId), + }, + }), + injectState, + ({ id, schedule, showJob, showState, state: { job } }) => { + if (schedule === undefined) { + return unknowItem(id, 'schedule') + } + + const isEnabled = schedule.enabled + const scheduleName = schedule.name.trim() + const jobName = job?.name.trim() + + return ( + + {showState && ( + + {isEnabled ? _('stateEnabled') : _('stateDisabled')} + + )} + {scheduleName === '' ? {_('unnamedSchedule')} : scheduleName} + {showJob && ( + + {' '} + ({job ? jobName === '' ? {_('unnamedJob')} : jobName : unknowItem(schedule.jobId, 'job')}) + + )} + + ) + }, +]) + +Schedule.propTypes = { + id: PropTypes.string.isRequired, + showState: PropTypes.bool, + showJob: PropTypes.bool, +} + +Schedule.defaultProps = { + showState: true, + showJob: true, +} + +// =================================================================== + export const Vgpu = connectStore(() => ({ vgpuType: createGetObject((_, props) => props.vgpu.vgpuType), }))(({ vgpu, vgpuType }) => ( @@ -684,18 +750,8 @@ const xoItemToRender = { PCI: props => , - schedule: schedule => { - const isEnabled = schedule.enabled - const scheduleName = schedule.name.trim() - return ( - - - {isEnabled ? _('stateEnabled') : _('stateDisabled')} - - {scheduleName === '' ? {_('unnamedSchedule')} : scheduleName} - - ) - }, + schedule: props => , + job: job => {job.name}, } diff --git a/packages/xo-web/src/common/select-objects.js b/packages/xo-web/src/common/select-objects.js index d6476f3381c..d102019982d 100644 --- a/packages/xo-web/src/common/select-objects.js +++ b/packages/xo-web/src/common/select-objects.js @@ -257,6 +257,7 @@ class GenericSelect extends React.Component { : undefined, memoryFree: option.xoItem.type === 'host' || undefined, showNetwork: true, + ...(this.props.optionProps ?? {}), })} ) diff --git a/packages/xo-web/src/icons.scss b/packages/xo-web/src/icons.scss index 8fa4ac0cd7b..e7a32f9c6d1 100644 --- a/packages/xo-web/src/icons.scss +++ b/packages/xo-web/src/icons.scss @@ -961,6 +961,10 @@ @extend .fa; @extend .fa-eye; } + &-sequence { + @extend .fa; + @extend .fa-list-ol; + } &-new { @extend .fa; @extend .fa-plus; diff --git a/packages/xo-web/src/xo-app/backup/edit.js b/packages/xo-web/src/xo-app/backup/edit.js index c1aea4de924..0071df8c04d 100644 --- a/packages/xo-web/src/xo-app/backup/edit.js +++ b/packages/xo-web/src/xo-app/backup/edit.js @@ -6,15 +6,23 @@ import Icon from 'icon' import React from 'react' import { injectState, provideState } from 'reaclette' import { find, groupBy, keyBy } from 'lodash' -import { subscribeBackupNgJobs, subscribeMetadataBackupJobs, subscribeMirrorBackupJobs, subscribeSchedules } from 'xo' +import { + subscribeBackupNgJobs, + subscribeJobs, + subscribeMetadataBackupJobs, + subscribeMirrorBackupJobs, + subscribeSchedules, +} from 'xo' import Metadata from './new/metadata' import New from './new' import NewMirrorBackup from './new/mirror' +import NewSequence from './new/sequence' export default decorate([ addSubscriptions({ - jobs: subscribeBackupNgJobs, + jobs: subscribeJobs, + backupJobs: subscribeBackupNgJobs, metadataJobs: subscribeMetadataBackupJobs, mirrorBackupJobs: subscribeMirrorBackupJobs, schedulesByJob: cb => @@ -24,11 +32,20 @@ export default decorate([ }), provideState({ computed: { - job: (_, { jobs, metadataJobs, mirrorBackupJobs, routeParams: { id } }) => - defined(find(jobs, { id }), find(metadataJobs, { id }), find(mirrorBackupJobs, { id })), + job: (_, { backupJobs, jobs, metadataJobs, mirrorBackupJobs, routeParams: { id } }) => + defined( + find(jobs, { id }), + find(backupJobs, { id }), + find(metadataJobs, { id }), + find(mirrorBackupJobs, { id }) + ), schedules: (_, { schedulesByJob, routeParams: { id } }) => schedulesByJob && keyBy(schedulesByJob[id], 'id'), loading: (_, props) => - props.jobs === undefined || props.metadataJobs === undefined || props.schedulesByJob === undefined, + props.jobs === undefined || + props.backupJobs === undefined || + props.metadataJobs === undefined || + props.mirrorBackupJobs === undefined || + props.schedulesByJob === undefined, }, }), injectState, @@ -43,7 +60,11 @@ export default decorate([ ) : job.type === 'mirrorBackup' ? ( - ) : ( + ) : job.type === 'metadataBackup' ? ( + ) : job.type === 'call' && job.method === 'schedule.runSequence' ? ( + + ) : ( + 'Unknown job type' ), ]) diff --git a/packages/xo-web/src/xo-app/backup/index.js b/packages/xo-web/src/xo-app/backup/index.js index 4027d46e8bd..ae18192f965 100644 --- a/packages/xo-web/src/xo-app/backup/index.js +++ b/packages/xo-web/src/xo-app/backup/index.js @@ -15,9 +15,10 @@ import { subscribeBackupNgJobs, subscribeSchedules } from 'xo' import Edit from './edit' import FileRestore from './file-restore' import Health from './health' -import NewVmBackup, { NewMetadataBackup, NewMirrorBackup } from './new' +import NewVmBackup, { NewMetadataBackup, NewMirrorBackup, NewSequence } from './new' import Overview from './overview' import Restore, { RestoreMetadata } from './restore' +import Sequences from './sequences' import Page from '../page' @@ -55,6 +56,9 @@ const HEADER = ( {_('backupOverviewPage')} + + {_('sequences')} + {_('backupNewPage')} @@ -89,6 +93,9 @@ const ChooseBackupType = () => (

{_('backupMetadata')} + {' '} + + {_('sequence')}

@@ -104,7 +111,9 @@ export default routes('overview', { 'new/vms': NewVmBackup, 'new/mirror': NewMirrorBackup, 'new/metadata': NewMetadataBackup, + 'new/sequence': NewSequence, overview: Overview, + sequences: Sequences, restore: Restore, 'restore/metadata': RestoreMetadata, 'file-restore': FileRestore, diff --git a/packages/xo-web/src/xo-app/backup/new/index.js b/packages/xo-web/src/xo-app/backup/new/index.js index 4c52eab4c0d..e34c0c7c7b6 100644 --- a/packages/xo-web/src/xo-app/backup/new/index.js +++ b/packages/xo-web/src/xo-app/backup/new/index.js @@ -48,6 +48,7 @@ import { canDeltaBackup, constructPattern, destructPattern, FormFeedback, FormGr export NewMetadataBackup from './metadata' export NewMirrorBackup from './mirror' +export NewSequence from './sequence' // =================================================================== diff --git a/packages/xo-web/src/xo-app/backup/new/sequence/index.js b/packages/xo-web/src/xo-app/backup/new/sequence/index.js new file mode 100644 index 00000000000..3fe0c6427b7 --- /dev/null +++ b/packages/xo-web/src/xo-app/backup/new/sequence/index.js @@ -0,0 +1,191 @@ +import _ from 'intl' +import ActionButton from 'action-button' +import decorate from 'apply-decorators' +import Icon from 'icon' +import moment from 'moment-timezone' +import React from 'react' +import Scheduler from 'scheduling' +import Upgrade from 'xoa-upgrade' +import { Card, CardBlock, CardHeader } from 'card' +import { Container, Col, Row } from 'grid' +import { createJob, editJob, createSchedule, editSchedule } from 'xo' +import { generateId } from 'reaclette-utils' +import { injectState, provideState } from 'reaclette' +import { SelectSchedule } from 'select-objects' + +import { Input } from '../../utils' + +const NewSequence = decorate([ + provideState({ + initialState: props => ({ + name: props.job?.name ?? '', + sequenceSchedule: + props.schedule !== undefined + ? { cronPattern: props.schedule.cron, timezone: props.schedule.timezone } + : { + cronPattern: '0 0 * * *', + timezone: moment.tz.guess(), + }, + schedules: props.job?.paramsVector?.items?.[0]?.values?.[0]?.schedules?.map(scheduleId => ({ + id: scheduleId, + key: generateId(), + })) ?? [{ key: generateId() }, { key: generateId() }], + }), + effects: { + addSchedule: () => state => ({ schedules: [...state.schedules, { key: generateId() }] }), + removeSchedule: (_, scheduleKey) => state => ({ + schedules: state.schedules.filter(schedule => schedule.key !== scheduleKey), + }), + selectSchedule: (_, scheduleKey, schedule) => state => ({ + schedules: state.schedules.map(s => (s.key !== scheduleKey ? s : { key: s.key, ...schedule })), + }), + onChangeName: (_, event) => ({ name: event.target.value }), + onChangeSequenceSchedule: (_, sequenceSchedule) => ({ sequenceSchedule }), + save: () => async (state, props) => { + const jobFormData = { + name: state.name, + paramsVector: { + type: 'crossProduct', + items: [ + { + type: 'set', + values: [{ schedules: state.schedules.map(schedule => schedule.id) }], + }, + ], + }, + } + + const scheduleFormData = { + cron: state.sequenceSchedule.cronPattern, + timezone: state.sequenceSchedule.timezone, + } + + if (props.job !== undefined) { + await editJob({ + id: props.job.id, + ...jobFormData, + }) + + await editSchedule({ + id: props.schedule.id, + jobId: props.job.id, + enabled: true, + ...scheduleFormData, + }) + } else { + const jobId = await createJob({ + type: 'call', + key: 'genericTask', + method: 'schedule.runSequence', + ...jobFormData, + }) + + await createSchedule(jobId, { + enabled: true, + ...scheduleFormData, + }) + } + }, + }, + }), + injectState, + ({ job, state, effects }) => ( +
+ + + + + + {_('sequence')} + + + + + + + + + +
    + {state.schedules.map(schedule => ( +
  1. +
    + effects.selectSchedule(schedule.key, s)} + value={schedule} + required + optionProps={{ showState: false }} + /> + +
    +
  2. + ))} +
+ + {_('add')} + + +
+
+
+ +
+ + + + + {_('scheduleSequence')} + + + + + + + + + + + + {_('formSave')} + + + + +
+
+ ), +]) + +export default props => ( + + + +) diff --git a/packages/xo-web/src/xo-app/backup/sequences.js b/packages/xo-web/src/xo-app/backup/sequences.js new file mode 100644 index 00000000000..5f00c5d0fa6 --- /dev/null +++ b/packages/xo-web/src/xo-app/backup/sequences.js @@ -0,0 +1,123 @@ +import _ from 'intl' +import ActionButton from 'action-button' +import addSubscriptions from 'add-subscriptions' +import Copiable from 'copiable' +import decorate from 'apply-decorators' +import filter from 'lodash/filter.js' +import groupBy from 'lodash/groupBy.js' +import Icon from 'icon' +import React from 'react' +import SortedTable from 'sorted-table' +import { Card, CardHeader, CardBlock } from 'card' +import { deleteJobs, subscribeJobs, subscribeSchedules, runJob } from 'xo' +import { noop } from 'utils' +import { Schedule } from 'render-xo-item' + +const COLUMNS = [ + { + itemRenderer: ({ id }) => ( + + {id.slice(4, 8)} + + ), + name: _('jobId'), + }, + { + valuePath: 'name', + name: _('jobName'), + default: true, + }, + { + name: _('sequence'), + itemRenderer: sequenceJob => { + const scheduleIds = sequenceJob.paramsVector?.items?.[0]?.values?.[0]?.schedules + if (scheduleIds === undefined) { + return null + } + + return ( +
    + {scheduleIds.map((scheduleId, i) => ( +
  1. + +
  2. + ))} +
+ ) + }, + }, + { + name: _('schedule'), + itemRenderer: (sequenceJob, { schedulesByJob }) => { + const schedules = schedulesByJob?.[sequenceJob.id] + if (schedules === undefined || schedules.length === 0) { + return null + } + + return schedules[0].cron + }, + }, +] + +const ACTIONS = [ + { + handler: jobs => deleteJobs(jobs), + label: _('delete'), + icon: 'delete', + level: 'danger', + }, +] + +const INDIVIDUAL_ACTIONS = [ + { + handler: job => runJob(job), + label: _('scheduleRun'), + icon: 'run-schedule', + level: 'primary', + }, + { + handler: (job, { router }) => router.push(`/backup/${job.id}/edit`), + label: _('formEdit'), + icon: 'edit', + level: 'primary', + }, +] + +const Sequences = decorate([ + addSubscriptions({ + sequenceJobs: cb => subscribeJobs(jobs => cb(filter(jobs, { type: 'call', method: 'schedule.runSequence' }))), + schedulesByJob: cb => subscribeSchedules(schedules => cb(groupBy(schedules, 'jobId'))), + }), + ({ sequenceJobs, schedulesByJob, router }) => ( +
+
+ + + {_('sequences')} + + {_('new')} + + + + + + +
+
+ ), +]) + +export default Sequences diff --git a/packages/xo-web/src/xo-app/backup/utils.js b/packages/xo-web/src/xo-app/backup/utils.js index 8a8c66d1953..559c312a274 100644 --- a/packages/xo-web/src/xo-app/backup/utils.js +++ b/packages/xo-web/src/xo-app/backup/utils.js @@ -4,10 +4,10 @@ import PropTypes from 'prop-types' import React from 'react' import { resolveId, resolveIds } from 'utils' -export const FormGroup = props =>
-export const Input = props => +export const FormGroup = props =>
+export const Input = props => export const Ul = props =>
    -export const Li = props =>
  • +export const Li = props =>
  • export const destructPattern = pattern => pattern && (pattern.id.__or || [pattern.id]) export const constructPattern = values => diff --git a/packages/xo-web/src/xo-app/menu/index.js b/packages/xo-web/src/xo-app/menu/index.js index b56c7a4f8e0..3277b3cfa13 100644 --- a/packages/xo-web/src/xo-app/menu/index.js +++ b/packages/xo-web/src/xo-app/menu/index.js @@ -310,6 +310,11 @@ export default class Menu extends Component { icon: 'menu-backup-overview', label: 'backupOverviewPage', }, + { + to: '/backup/sequences', + icon: 'menu-backup-sequence', + label: 'sequences', + }, { to: '/backup/new', icon: 'menu-backup-new', From eb39e149723b2fb0e19c22931348485bead654ab Mon Sep 17 00:00:00 2001 From: Julien Fontanet Date: Thu, 26 Sep 2024 09:25:21 +0200 Subject: [PATCH 3/4] fix(xo-server/rest-api): set apiContext This makes the REST API more closely resemblethe JSON-RPC API and addresses several issues such as the broken _Rolling Pool Update_ pool action. Fixes https://xcp-ng.org/forum/post/82867 --- CHANGELOG.unreleased.md | 2 ++ packages/xo-server/src/xo-mixins/api.mjs | 20 +++++++++++++++---- packages/xo-server/src/xo-mixins/rest-api.mjs | 5 ++--- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index efed7e325a6..1f96a0d7d84 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -25,6 +25,8 @@ > Users must be able to say: “I had this issue, happy to know it's fixed” +- [REST API] Fix broken _Rolling Pool Update_ pool action [Forum#82867](https://xcp-ng.org/forum/post/82867) + ### Packages to release > When modifying a package, add it here with its release type. diff --git a/packages/xo-server/src/xo-mixins/api.mjs b/packages/xo-server/src/xo-mixins/api.mjs index 901c68d6400..b49d55a09df 100644 --- a/packages/xo-server/src/xo-mixins/api.mjs +++ b/packages/xo-server/src/xo-mixins/api.mjs @@ -316,18 +316,30 @@ export default class Api { throw new MethodNotFound(name) } - const apiContext = { __proto__: null, connection } - + let user const userId = connection.get('user_id', undefined) if (userId !== undefined) { - const user = await this._app.getUser(userId) + user = await this._app.getUser(userId) + } + + return this.runWithApiContext(user, () => { + this.apiContext.connection = connection + + return this.#callApiMethod(name, method, params) + }) + } + + async runWithApiContext(user, fn) { + const apiContext = { __proto__: null } + + if (user !== undefined) { apiContext.user = user apiContext.permission = user.permission } else { apiContext.permission = 'none' } - return this.#apiContext.run(apiContext, () => this.#callApiMethod(name, method, params)) + return this.#apiContext.run(apiContext, fn) } async #callApiMethod(name, method, params) { diff --git a/packages/xo-server/src/xo-mixins/rest-api.mjs b/packages/xo-server/src/xo-mixins/rest-api.mjs index 0d51c5b3824..6e43f569378 100644 --- a/packages/xo-server/src/xo-mixins/rest-api.mjs +++ b/packages/xo-server/src/xo-mixins/rest-api.mjs @@ -461,8 +461,7 @@ export default class RestApi { app.authenticateUser({ token: cookies.authenticationToken ?? cookies.token }, { ip }).then( ({ user }) => { if (user.permission === 'admin') { - req.user = user - return next() + return app.runWithApiContext(user, next) } res.sendStatus(401) @@ -658,7 +657,7 @@ export default class RestApi { params.affinityHost = affinity params.installRepository = install?.repository - const vm = await $xapi.createVm(template, params, undefined, req.user.id) + const vm = await $xapi.createVm(template, params, undefined, app.apiContext.user.id) $defer.onFailure.call($xapi, 'VM_destroy', vm.$ref) if (boot) { From cdfe0b037416113af06882466f5ac8bd5eba8ed0 Mon Sep 17 00:00:00 2001 From: MlssFrncJrg <119158464+MelissaFrncJrg@users.noreply.github.com> Date: Fri, 27 Sep 2024 12:37:25 +0200 Subject: [PATCH 4/4] feat(xo-server,xo-web): display bond mode of a network (#8010) --- CHANGELOG.unreleased.md | 1 + packages/xo-server/src/xapi-object-to-xo.mjs | 10 ++++++++++ packages/xo-web/src/common/intl/messages.js | 1 + .../xo-web/src/common/sorted-table/pifs-column.js | 11 ++++++++--- 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index 1f96a0d7d84..1ab005e4bf6 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -20,6 +20,7 @@ - `rest post` and `rest put` now accept `--input $file` to upload a file and display progress information - [Backup] Detect invalid VDI exports that are incorrectly reported as successful by XAPI - [Backup] Backup job sequences: configure lists of backup jobs to run in order one after the other (PRs [#7985](https://github.com/vatesfr/xen-orchestra/pull/7985), [#8014](https://github.com/vatesfr/xen-orchestra/pull/8014)) +- [Pool/Network] Display the bond mode of a network [#7802](https://github.com/vatesfr/xen-orchestra/issues/7802) (PR [#8010](https://github.com/vatesfr/xen-orchestra/pull/8010)) ### Bug fixes diff --git a/packages/xo-server/src/xapi-object-to-xo.mjs b/packages/xo-server/src/xapi-object-to-xo.mjs index 39440601a53..9f3272d1b7f 100644 --- a/packages/xo-server/src/xapi-object-to-xo.mjs +++ b/packages/xo-server/src/xapi-object-to-xo.mjs @@ -957,6 +957,16 @@ const TRANSFORMS = { VUSBs: link(obj, 'VUSBs'), } }, + + // ----------------------------------------------------------------- + + bond(obj) { + return { + type: 'bond', + master: link(obj, 'master'), + mode: obj.mode, + } + }, } // =================================================================== diff --git a/packages/xo-web/src/common/intl/messages.js b/packages/xo-web/src/common/intl/messages.js index 5a574050983..c785e2b6f37 100644 --- a/packages/xo-web/src/common/intl/messages.js +++ b/packages/xo-web/src/common/intl/messages.js @@ -973,6 +973,7 @@ const messages = { noHost: 'No hosts', memoryLeftTooltip: '{used}% used ({free} free)', // ----- Pool network tab ----- + bondMode: 'Bond Mode', pif: 'PIF', poolNetworkAutomatic: 'Automatic', poolNetworkNameLabel: 'Name', diff --git a/packages/xo-web/src/common/sorted-table/pifs-column.js b/packages/xo-web/src/common/sorted-table/pifs-column.js index ab9420b0983..7c6662b3bf7 100644 --- a/packages/xo-web/src/common/sorted-table/pifs-column.js +++ b/packages/xo-web/src/common/sorted-table/pifs-column.js @@ -8,7 +8,7 @@ import map from 'lodash/map' import React, { Component } from 'react' import Tooltip from 'tooltip' import { connectStore } from 'utils' -import { createGetObject, createSelector } from 'selectors' +import { createGetObject, createGetObjectsOfType, createSelector } from 'selectors' import { connectPif, disconnectPif } from 'xo' @connectStore(() => { @@ -19,11 +19,14 @@ import { connectPif, disconnectPif } from 'xo' pif => pif?.attached && !pif?.isBondMaster && (pif?.management || pif?.disallowUnplug) ) - return { host, pif, disableUnplug } + const bonds = createGetObjectsOfType('bond') + const bond = createSelector(pif, bonds, (pif, bonds) => Object.values(bonds).find(bond => bond.master === pif.id)) + + return { host, pif, disableUnplug, bond } }) class PifItem extends Component { render() { - const { pif, host, disableUnplug } = this.props + const { pif, host, disableUnplug, bond } = this.props return ( @@ -31,6 +34,7 @@ class PifItem extends Component { {host?.name_label ?? _('unknown')} {pif?.ip ?? _('unknown')} {pif?.mac ?? _('unknown')} + {bond?.mode ?? '-'} {pif?.carrier === undefined ? ( {_('unknown')} @@ -78,6 +82,7 @@ export default class PifsColumn extends BaseComponent { {_('homeTypeHost')} {_('pifAddressLabel')} {_('pifMacLabel')} + {_('bondMode')} {_('pifStatusLabel')}