@xen-orchestra/backups 0.67.2 → 0.68.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/RemoteAdapter.mjs +6 -1
- package/_getOldEntries.mjs +153 -54
- package/_incrementalVm.mjs +9 -3
- package/_otherConfig.mjs +0 -33
- package/_runners/_vmRunners/FullXapi.mjs +9 -2
- package/_runners/_vmRunners/_AbstractXapi.mjs +116 -41
- package/_runners/_writers/FullRemoteWriter.mjs +1 -0
- package/_runners/_writers/IncrementalRemoteWriter.mjs +1 -0
- package/_runners/_writers/IncrementalXapiWriter.mjs +37 -15
- package/_runners/_writers/_AbstractWriter.mjs +0 -13
- package/_runners/_writers/_MixinRemoteWriter.mjs +14 -0
- package/_runners/_writers/_listReplicatedVms.mjs +1 -3
- package/formatVmBackups.mjs +1 -1
- package/package.json +6 -6
package/RemoteAdapter.mjs
CHANGED
|
@@ -18,7 +18,7 @@ import groupBy from 'lodash/groupBy.js'
|
|
|
18
18
|
import pDefer from 'promise-toolbox/defer'
|
|
19
19
|
import pickBy from 'lodash/pickBy.js'
|
|
20
20
|
import reduce from 'lodash/reduce.js'
|
|
21
|
-
import tar from 'tar'
|
|
21
|
+
import * as tar from 'tar'
|
|
22
22
|
import zlib from 'zlib'
|
|
23
23
|
|
|
24
24
|
import { BACKUP_DIR } from './_getVmBackupDir.mjs'
|
|
@@ -613,6 +613,11 @@ export class RemoteAdapter {
|
|
|
613
613
|
// if cache is missing or broken => regenerate it and return
|
|
614
614
|
|
|
615
615
|
async _readCacheListVmBackups(vmUuid) {
|
|
616
|
+
// immutable remote can't use any caching
|
|
617
|
+
// since the cache file may be non modifiable
|
|
618
|
+
if (this._handler.isImmutable()) {
|
|
619
|
+
return this.#getCacheableDataListVmBackups(`${BACKUP_DIR}/${vmUuid}`)
|
|
620
|
+
}
|
|
616
621
|
const path = this.#getVmBackupsCache(vmUuid)
|
|
617
622
|
|
|
618
623
|
const cache = await this._readCache(path)
|
package/_getOldEntries.mjs
CHANGED
|
@@ -1,5 +1,58 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
1
3
|
import moment from 'moment-timezone'
|
|
2
4
|
import assert from 'node:assert'
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {import('moment-timezone').Moment} Moment
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {Object} LTRSettings
|
|
10
|
+
* @property {number} [firstHourOfTheDay] - First hour of the day (for daily retention)
|
|
11
|
+
* @property {number} [firstDayOfWeek] - First day of the week (for weekly retention)
|
|
12
|
+
* @property {number} [firstDayOfMonth] - First day of the month (for monthly retention)
|
|
13
|
+
* @property {number} [firstDayOfYear] - First day of the year (for yearly retention)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {Object} LTRConfig
|
|
18
|
+
* @property {number} retention - Number of time buckets to retain
|
|
19
|
+
* @property {LTRSettings} [settings] - Optional settings for the retention period
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {Object} LongTermRetention
|
|
24
|
+
* @property {LTRConfig} [daily] - Daily retention configuration
|
|
25
|
+
* @property {LTRConfig} [weekly] - Weekly retention configuration
|
|
26
|
+
* @property {LTRConfig} [monthly] - Monthly retention configuration
|
|
27
|
+
* @property {LTRConfig} [yearly] - Yearly retention configuration
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @typedef {Object} Entry
|
|
32
|
+
* @property {number | undefined} timestamp - Unix timestamp of the entry
|
|
33
|
+
* @property {string} id
|
|
34
|
+
* @property {*} [key] - Any other properties
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @typedef {Object} DateBucket
|
|
39
|
+
* @property {number} remaining - Number of remaining buckets to fill
|
|
40
|
+
* @property {string|null} lastMatchingBucket - Last bucket key that was matched
|
|
41
|
+
* @property {(date: Moment) => string} formatter - Function to format date into bucket key
|
|
42
|
+
* @property {Object.<string, Entry>} entries - Map of bucket keys to entries
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @typedef {Object} LTRDefinition
|
|
47
|
+
* @property {(options: {firstHourOfTheDay?: number, firstDayOfWeek?: number, firstDayOfMonth?: number, firstDayOfYear?: number, dateCreator: (date: Moment) => Moment}) => (date: Moment) => string} makeDateFormatter
|
|
48
|
+
* @property {string} [ancestor] - Name of the parent retention period
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Creates a function that converts dates to moment objects in a specific timezone
|
|
53
|
+
* @param {string|undefined} timezone - IANA timezone string (e.g., 'America/New_York')
|
|
54
|
+
* @returns {(date: Moment | number) => Moment} Function that creates timezoned moment objects
|
|
55
|
+
*/
|
|
3
56
|
|
|
4
57
|
function instantiateTimezonedDateCreator(timezone) {
|
|
5
58
|
return date => {
|
|
@@ -9,9 +62,12 @@ function instantiateTimezonedDateCreator(timezone) {
|
|
|
9
62
|
}
|
|
10
63
|
}
|
|
11
64
|
|
|
65
|
+
/**
|
|
66
|
+
* @type {Object.<string, LTRDefinition>}
|
|
67
|
+
*/
|
|
12
68
|
const LTR_DEFINITIONS = {
|
|
13
69
|
daily: {
|
|
14
|
-
makeDateFormatter: ({ firstHourOfTheDay = 0, dateCreator }
|
|
70
|
+
makeDateFormatter: ({ firstHourOfTheDay = 0, dateCreator }) => {
|
|
15
71
|
return date => {
|
|
16
72
|
const copy = dateCreator(date)
|
|
17
73
|
copy.hour(copy.hour() - firstHourOfTheDay)
|
|
@@ -20,11 +76,11 @@ const LTR_DEFINITIONS = {
|
|
|
20
76
|
},
|
|
21
77
|
},
|
|
22
78
|
weekly: {
|
|
23
|
-
makeDateFormatter: ({ firstDayOfWeek = 0 /* relative to timezone week start */, dateCreator }
|
|
79
|
+
makeDateFormatter: ({ firstDayOfWeek = 0 /* relative to timezone week start */, dateCreator }) => {
|
|
24
80
|
return date => {
|
|
25
81
|
const copy = dateCreator(date)
|
|
26
82
|
|
|
27
|
-
copy.date(
|
|
83
|
+
copy.date(copy.date() - firstDayOfWeek)
|
|
28
84
|
// warning, the year in term of week may different from YYYY
|
|
29
85
|
// since the computation of the first week of a year is timezone dependent
|
|
30
86
|
return copy.format('gggg-ww')
|
|
@@ -33,7 +89,7 @@ const LTR_DEFINITIONS = {
|
|
|
33
89
|
ancestor: 'daily',
|
|
34
90
|
},
|
|
35
91
|
monthly: {
|
|
36
|
-
makeDateFormatter: ({ firstDayOfMonth = 0, dateCreator }
|
|
92
|
+
makeDateFormatter: ({ firstDayOfMonth = 0, dateCreator }) => {
|
|
37
93
|
return date => {
|
|
38
94
|
const copy = dateCreator(date)
|
|
39
95
|
copy.date(copy.date() - firstDayOfMonth)
|
|
@@ -43,7 +99,7 @@ const LTR_DEFINITIONS = {
|
|
|
43
99
|
ancestor: 'weekly',
|
|
44
100
|
},
|
|
45
101
|
yearly: {
|
|
46
|
-
makeDateFormatter: ({ firstDayOfYear = 0, dateCreator }
|
|
102
|
+
makeDateFormatter: ({ firstDayOfYear = 0, dateCreator }) => {
|
|
47
103
|
return date => {
|
|
48
104
|
const copy = dateCreator(date)
|
|
49
105
|
copy.date(copy.date() - firstDayOfYear)
|
|
@@ -53,27 +109,23 @@ const LTR_DEFINITIONS = {
|
|
|
53
109
|
ancestor: 'monthly',
|
|
54
110
|
},
|
|
55
111
|
}
|
|
56
|
-
|
|
57
|
-
// returns all entries but the last retention-th
|
|
58
112
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* @
|
|
113
|
+
* Groups entries into date buckets based on long-term retention configuration
|
|
114
|
+
* @template {Entry} T
|
|
115
|
+
* @param {Array<T>} entries - Array of entries sorted in descending order by timestamp
|
|
116
|
+
* @param {LongTermRetention} longTermRetention - Configuration for retention periods
|
|
117
|
+
* @param {string | undefined} timezone - IANA timezone string
|
|
118
|
+
* @returns {Object.<string, DateBucket>} Map of retention period names to their date buckets
|
|
64
119
|
*/
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
)
|
|
71
|
-
assert.strictEqual(
|
|
72
|
-
minRetentionCount >= 0,
|
|
73
|
-
true,
|
|
74
|
-
`minRetentionCount must be a positive number, got ${JSON.stringify(minRetentionCount)}`
|
|
75
|
-
)
|
|
120
|
+
|
|
121
|
+
export function getLtrEntries(entries, longTermRetention, timezone) {
|
|
122
|
+
/**
|
|
123
|
+
* @type {Object.<string, DateBucket>}
|
|
124
|
+
*/
|
|
76
125
|
const dateBuckets = {}
|
|
126
|
+
if (!longTermRetention || Object.keys(longTermRetention).length === 0) {
|
|
127
|
+
return {}
|
|
128
|
+
}
|
|
77
129
|
const dateCreator = instantiateTimezonedDateCreator(timezone)
|
|
78
130
|
// only check buckets that have a retention set
|
|
79
131
|
for (const [duration, { retention, settings }] of Object.entries(longTermRetention)) {
|
|
@@ -81,7 +133,7 @@ export function getOldEntries(minRetentionCount, entries, { longTermRetention =
|
|
|
81
133
|
assert.notStrictEqual(
|
|
82
134
|
timezone,
|
|
83
135
|
undefined,
|
|
84
|
-
`timezone must defined for ltr, got ${JSON.stringify({
|
|
136
|
+
`timezone must defined for ltr, got ${JSON.stringify({ longTermRetention, timezone })}`
|
|
85
137
|
)
|
|
86
138
|
assert.strictEqual(
|
|
87
139
|
typeof retention,
|
|
@@ -101,45 +153,92 @@ export function getOldEntries(minRetentionCount, entries, { longTermRetention =
|
|
|
101
153
|
}
|
|
102
154
|
}
|
|
103
155
|
const nb = entries.length
|
|
104
|
-
const minDurationEntries = []
|
|
105
156
|
let previousTimestamp = -1
|
|
106
157
|
for (let i = nb - 1; i >= 0; i--) {
|
|
107
158
|
const entry = entries[i]
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
assert.
|
|
111
|
-
|
|
112
|
-
|
|
159
|
+
// force it so the compiler is sure we have a timestamp
|
|
160
|
+
if (entry.timestamp === undefined) {
|
|
161
|
+
assert.notStrictEqual(
|
|
162
|
+
entry.timestamp,
|
|
163
|
+
undefined,
|
|
164
|
+
`Can't compute long term retention if entries don't have a timestamp`
|
|
113
165
|
)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
166
|
+
continue
|
|
167
|
+
}
|
|
168
|
+
// we go through the entries from the last (most recent) to the first (oldest)
|
|
169
|
+
assert.ok(
|
|
170
|
+
previousTimestamp === -1 || entry.timestamp < previousTimestamp,
|
|
171
|
+
`entries must be sorted in desc order ${new Date(entry.timestamp)} , previous : > ${new Date(previousTimestamp)} `
|
|
172
|
+
)
|
|
173
|
+
previousTimestamp = entry.timestamp
|
|
174
|
+
const entryDate = dateCreator(entry.timestamp)
|
|
175
|
+
for (const [duration, { remaining, lastMatchingBucket, formatter }] of Object.entries(dateBuckets)) {
|
|
176
|
+
const bucket = formatter(entryDate)
|
|
177
|
+
if (lastMatchingBucket !== bucket) {
|
|
178
|
+
if (remaining === 0) {
|
|
179
|
+
continue
|
|
124
180
|
}
|
|
125
|
-
dateBuckets[duration].
|
|
181
|
+
dateBuckets[duration].lastMatchingBucket = bucket
|
|
182
|
+
dateBuckets[duration].remaining -= 1
|
|
126
183
|
}
|
|
127
|
-
|
|
128
|
-
// replicated VM entries or snapshot retention don't have a timestamp
|
|
129
|
-
// but also can't have LTR
|
|
130
|
-
assert.deepStrictEqual(
|
|
131
|
-
longTermRetention,
|
|
132
|
-
{},
|
|
133
|
-
"Can't compute long term retention if entries don't have a timestamp"
|
|
134
|
-
)
|
|
184
|
+
dateBuckets[duration].entries[bucket] = entry
|
|
135
185
|
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return dateBuckets
|
|
189
|
+
}
|
|
136
190
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
191
|
+
/**
|
|
192
|
+
* return the status of an entry
|
|
193
|
+
* @param {Array<Entry>} entries
|
|
194
|
+
* @param {Entry} entry
|
|
195
|
+
* @param {LongTermRetention} longTermRetention - Configuration for retention periods
|
|
196
|
+
* @param {string | undefined} timezone - IANA timezone string
|
|
197
|
+
* @returns {Array<{duration: string, dateBucket:string}>}
|
|
198
|
+
*/
|
|
199
|
+
export function getEntryStatus(entries, entry, longTermRetention, timezone) {
|
|
200
|
+
const dateBuckets = getLtrEntries(entries, longTermRetention, timezone)
|
|
201
|
+
const entryBuckets = []
|
|
202
|
+
for (const [duration, { entries }] of Object.entries(dateBuckets)) {
|
|
203
|
+
for (const [dateBucket, bucketEntry] of Object.entries(entries)) {
|
|
204
|
+
if (entry.id === bucketEntry.id) {
|
|
205
|
+
entryBuckets.push({ duration, dateBucket })
|
|
206
|
+
}
|
|
140
207
|
}
|
|
141
208
|
}
|
|
142
|
-
|
|
209
|
+
return entryBuckets
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* return the entries too old to be kept
|
|
214
|
+
* if multiple entries are in the same time bucket: keep only the oldest one
|
|
215
|
+
* if an entry is valid in any of the bucket OR the minRetentionCount: keep it
|
|
216
|
+
* if a bucket is completely empty: it does not count as one, thus it may extend the retention
|
|
217
|
+
* if an entry is within the most recents minRetentionCount entries : keep it
|
|
218
|
+
*
|
|
219
|
+
* @param {number} minRetentionCount - Minimum number of most recent entries to always keep
|
|
220
|
+
* @param {Array<Entry>} entries - Array of entries sorted in descending order by timestamp
|
|
221
|
+
* @param {Object} options - Additional options
|
|
222
|
+
* @param {LongTermRetention} [options.longTermRetention={}] - Configuration for retention periods
|
|
223
|
+
* @param {string | undefined} [options.timezone] - IANA timezone string
|
|
224
|
+
* @returns {Array<Entry>} Array of entries that can be removed
|
|
225
|
+
*/
|
|
226
|
+
export function getOldEntries(minRetentionCount, entries, { longTermRetention = {}, timezone } = {}) {
|
|
227
|
+
assert.strictEqual(
|
|
228
|
+
typeof minRetentionCount,
|
|
229
|
+
'number',
|
|
230
|
+
`minRetentionCount must be a number, got ${JSON.stringify(minRetentionCount)}`
|
|
231
|
+
)
|
|
232
|
+
assert.ok(
|
|
233
|
+
minRetentionCount >= 0,
|
|
234
|
+
`minRetentionCount must be a positive number, got ${JSON.stringify(minRetentionCount)}`
|
|
235
|
+
)
|
|
236
|
+
const dateBuckets = getLtrEntries(entries, longTermRetention, timezone)
|
|
237
|
+
|
|
238
|
+
const kept = new Set(entries.filter((_, index) => index >= entries.length - minRetentionCount))
|
|
239
|
+
/**
|
|
240
|
+
* @type {Set<Entry>}
|
|
241
|
+
*/
|
|
143
242
|
for (const { entries } of Object.values(dateBuckets)) {
|
|
144
243
|
for (const entry of Object.values(entries)) {
|
|
145
244
|
kept.add(entry)
|
|
@@ -147,7 +246,7 @@ export function getOldEntries(minRetentionCount, entries, { longTermRetention =
|
|
|
147
246
|
}
|
|
148
247
|
|
|
149
248
|
// ensure order is the same as the source
|
|
150
|
-
const oldEntries = entries.filter(entry => {
|
|
249
|
+
const oldEntries = entries.filter((entry, index) => {
|
|
151
250
|
return !kept.has(entry)
|
|
152
251
|
})
|
|
153
252
|
|
package/_incrementalVm.mjs
CHANGED
|
@@ -291,10 +291,16 @@ export const importIncrementalVm = defer(async function importIncrementalVm(
|
|
|
291
291
|
await xapi.VTPM_create({ VM: vmRef, contents })
|
|
292
292
|
})
|
|
293
293
|
)
|
|
294
|
-
|
|
294
|
+
const vm = await xapi.getRecord('VM', vmRef)
|
|
295
295
|
await Promise.all([
|
|
296
|
-
|
|
297
|
-
xapi.setField('VM', vmRef, 'name_label',
|
|
296
|
+
vmRecord.ha_always_run && xapi.setField('VM', vmRef, 'ha_always_run', true),
|
|
297
|
+
xapi.setField('VM', vmRef, 'name_label', vmRecord.name_label),
|
|
298
|
+
// correctly unlock the VM and reapply the target blocked operations
|
|
299
|
+
vm.update_blocked_operations({
|
|
300
|
+
start: null,
|
|
301
|
+
start_on: null,
|
|
302
|
+
...vmRecord.blocked_operations,
|
|
303
|
+
}),
|
|
298
304
|
])
|
|
299
305
|
|
|
300
306
|
return vmRef
|
package/_otherConfig.mjs
CHANGED
|
@@ -91,39 +91,6 @@ export function resetVmOtherConfig(xapi, vmRef) {
|
|
|
91
91
|
})
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
/**
|
|
95
|
-
*
|
|
96
|
-
* used to ensure compatibility with the previous snapshots that were having the config stored only into VM
|
|
97
|
-
*
|
|
98
|
-
* @param {Xapi} xapi
|
|
99
|
-
* @param {String} vmRef
|
|
100
|
-
* @returns {Promise}
|
|
101
|
-
*/
|
|
102
|
-
export async function populateVdisOtherConfig(xapi, vmRef) {
|
|
103
|
-
const otherConfig = await xapi.getField('VM', vmRef, 'other_config')
|
|
104
|
-
const {
|
|
105
|
-
[DATETIME]: datetime,
|
|
106
|
-
[DELTA_CHAIN_LENGTH]: chainLength,
|
|
107
|
-
[EXPORTED_SUCCESSFULLY]: successfully,
|
|
108
|
-
[JOB_ID]: jobId,
|
|
109
|
-
[REPLICATED_TO_SR_UUID]: replicatedTo,
|
|
110
|
-
[SCHEDULE_ID]: scheduleId,
|
|
111
|
-
[VM_UUID]: vmUuid,
|
|
112
|
-
} = otherConfig
|
|
113
|
-
|
|
114
|
-
return applyToVmAndVdis(xapi, vmRef, (type, ref) =>
|
|
115
|
-
xapi.setFieldEntries(type, ref, 'other_config', {
|
|
116
|
-
[DATETIME]: datetime,
|
|
117
|
-
[DELTA_CHAIN_LENGTH]: chainLength,
|
|
118
|
-
[EXPORTED_SUCCESSFULLY]: successfully,
|
|
119
|
-
[JOB_ID]: jobId,
|
|
120
|
-
[REPLICATED_TO_SR_UUID]: replicatedTo,
|
|
121
|
-
[SCHEDULE_ID]: scheduleId,
|
|
122
|
-
[VM_UUID]: vmUuid,
|
|
123
|
-
})
|
|
124
|
-
)
|
|
125
|
-
}
|
|
126
|
-
|
|
127
94
|
/**
|
|
128
95
|
*
|
|
129
96
|
* set the other_config key related to a backup of a VM and its associated VDIs
|
|
@@ -64,9 +64,16 @@ export const FullXapi = class FullXapiVmBackupRunner extends AbstractXapi {
|
|
|
64
64
|
const vdi = await this._xapi.getRecord('VDI', vdiRef)
|
|
65
65
|
|
|
66
66
|
// the size a of fully allocated vdi will be virtual_size exactly, it's a gross over evaluation
|
|
67
|
-
// of the real stream size in general, since a disk is
|
|
67
|
+
// of the real stream size in general, since a disk is rarely completely full
|
|
68
68
|
// vdi.physical_size seems to underevaluate a lot the real disk usage of a VDI, as of 2023-10-30
|
|
69
|
-
|
|
69
|
+
|
|
70
|
+
// xva files are tar archive with data cut in 1MB blocks
|
|
71
|
+
// each block is signed with a hash (in xxhash format)
|
|
72
|
+
// each of these blocks + hash add one tar header entry in the file
|
|
73
|
+
const XVA_BLOCK_SIZE = 1024 * 1024
|
|
74
|
+
const nbBlocks = Math.ceil(vdi.virtual_size / XVA_BLOCK_SIZE)
|
|
75
|
+
const headersSize = nbBlocks * (512 /* file header */ + 512 /* xxhash header */ + 512) /* xhxhash size */
|
|
76
|
+
maxStreamLength += XVA_BLOCK_SIZE * nbBlocks + headersSize
|
|
70
77
|
}
|
|
71
78
|
|
|
72
79
|
const sizeContainer = watchStreamSize(stream)
|
|
@@ -3,22 +3,18 @@ import groupBy from 'lodash/groupBy.js'
|
|
|
3
3
|
import { createLogger } from '@xen-orchestra/log'
|
|
4
4
|
import ignoreErrors from 'promise-toolbox/ignoreErrors'
|
|
5
5
|
import { asyncMap } from '@xen-orchestra/async-map'
|
|
6
|
+
import { asyncEach } from '@vates/async-each'
|
|
6
7
|
import { decorateMethodsWith } from '@vates/decorate-with'
|
|
7
8
|
import { defer } from 'golike-defer'
|
|
8
9
|
|
|
9
10
|
import { getOldEntries } from '../../_getOldEntries.mjs'
|
|
10
11
|
import { Task } from '../../Task.mjs'
|
|
11
12
|
import { Abstract } from './_Abstract.mjs'
|
|
12
|
-
import {
|
|
13
|
-
DATETIME,
|
|
14
|
-
JOB_ID,
|
|
15
|
-
SCHEDULE_ID,
|
|
16
|
-
populateVdisOtherConfig,
|
|
17
|
-
resetVmOtherConfig,
|
|
18
|
-
setVmOtherConfig,
|
|
19
|
-
} from '../../_otherConfig.mjs'
|
|
13
|
+
import { DATETIME, JOB_ID, SCHEDULE_ID, VM_UUID, resetVmOtherConfig, setVmOtherConfig } from '../../_otherConfig.mjs'
|
|
20
14
|
|
|
21
|
-
const { warn } = createLogger('xo:backups:AbstractXapi')
|
|
15
|
+
const { warn, info } = createLogger('xo:backups:AbstractXapi')
|
|
16
|
+
|
|
17
|
+
const TEMP_SNAPSHOT_NAME = 'xo-backup-temp-snapshot-name'
|
|
22
18
|
|
|
23
19
|
export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
24
20
|
constructor({
|
|
@@ -163,7 +159,7 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
163
159
|
|
|
164
160
|
const snapshotRef = await vm[settings.checkpointSnapshot ? '$checkpoint' : '$snapshot']({
|
|
165
161
|
ignoredVdisTag: '[NOBAK]',
|
|
166
|
-
name_label:
|
|
162
|
+
name_label: TEMP_SNAPSHOT_NAME,
|
|
167
163
|
unplugVusbs: true,
|
|
168
164
|
})
|
|
169
165
|
this.timestamp = Date.now()
|
|
@@ -173,6 +169,9 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
173
169
|
scheduleId: this.scheduleId,
|
|
174
170
|
vmUuid: vm.uuid,
|
|
175
171
|
})
|
|
172
|
+
const snapshot = await xapi.getRecord('VM', snapshotRef)
|
|
173
|
+
await snapshot.set_name_label(this._getSnapshotNameLabel(vm))
|
|
174
|
+
// reload data to ensure it is up to date with the new name label
|
|
176
175
|
this._exportedVm = await xapi.getRecord('VM', snapshotRef)
|
|
177
176
|
return this._exportedVm.uuid
|
|
178
177
|
})
|
|
@@ -182,43 +181,113 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
182
181
|
}
|
|
183
182
|
}
|
|
184
183
|
|
|
184
|
+
// handle snapshot by VDI since snapshot of cbtDestroySnapshotData jobs
|
|
185
|
+
// aren't attached to any VM
|
|
186
|
+
|
|
187
|
+
// look at all the vdi snapshots ( snapshot_of not empty )
|
|
188
|
+
// with the same vm_uuid and job_uuid
|
|
189
|
+
// for cbt_metadata list them unconditionnaly to remove older one
|
|
190
|
+
// for other: only list them if they are attached to a VM snapshot
|
|
191
|
+
// and if this vm snapshot is also part of the backup
|
|
192
|
+
// ensure they are attached to only one vm snapshot
|
|
193
|
+
// ensure any VM-snapshot harvested by this has all its disk harvested (no mix of vdi snapshot from this job and not)
|
|
194
|
+
|
|
185
195
|
async _fetchJobSnapshots() {
|
|
186
196
|
const jobId = this._jobId
|
|
187
|
-
const vmRef = this._vm.$ref
|
|
188
197
|
const xapi = this._xapi
|
|
189
198
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
199
|
+
const vdiCandidates = {}
|
|
200
|
+
|
|
201
|
+
Object.values(xapi.objects.indexes.type.VDI)
|
|
202
|
+
.filter(_ => !!_) // filter nullish
|
|
203
|
+
.filter(({ other_config, $snapshot_of }) => {
|
|
204
|
+
return $snapshot_of !== undefined && other_config[JOB_ID] === jobId && other_config[VM_UUID] === this._vm.uuid
|
|
205
|
+
})
|
|
206
|
+
.forEach(vdi => {
|
|
207
|
+
vdiCandidates[vdi.uuid] = vdi
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
// check that user snapshots are clean
|
|
195
211
|
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
if
|
|
199
|
-
|
|
212
|
+
for (const vdi of Object.values(vdiCandidates)) {
|
|
213
|
+
// cbt metadata are always considered linked to a backup job
|
|
214
|
+
// if they have the right other_config
|
|
215
|
+
if (vdi.type === 'cbt_metadata') {
|
|
216
|
+
continue
|
|
200
217
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
(
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
218
|
+
const vbds = vdi.$VBDs
|
|
219
|
+
.filter(({ $VM }) => !!$VM) // filter empty VMs
|
|
220
|
+
.filter(({ $VM }) => $VM.is_control_domain === false) // do not handle control domain
|
|
221
|
+
if (vbds.length === 0) {
|
|
222
|
+
// orphan vdi snapshot
|
|
223
|
+
info(
|
|
224
|
+
`disk snapshot ${vdi.name_label} is orphan or attached only to control domain,
|
|
225
|
+
it will be removed at the end of a successful backup run`,
|
|
226
|
+
{ vdi, attachedto: vdi.$VBDs.map(vbd => vbd?.$VM) }
|
|
227
|
+
)
|
|
228
|
+
continue
|
|
229
|
+
}
|
|
230
|
+
const userVms = vbds.map(({ $VM }) => $VM)
|
|
231
|
+
if (vbds.length > 1) {
|
|
232
|
+
warn(
|
|
233
|
+
`vdi ${vdi.name_label} (${vdi.uuid}) is linked to multipe vms : ${userVms.map(({ name_label, uuid }) => `${name_label} ${uuid}`).join(', ')}.
|
|
234
|
+
This disk snapshot will be excluded from the backup cleaning`,
|
|
235
|
+
{ vdi, userVms }
|
|
236
|
+
)
|
|
237
|
+
delete vdiCandidates[vdi.uuid]
|
|
238
|
+
continue
|
|
220
239
|
}
|
|
240
|
+
|
|
241
|
+
const vm = vbds[0].$VM
|
|
242
|
+
|
|
243
|
+
// xapi has some issue regarding vdi snapshot attached to non snapshot VM
|
|
244
|
+
// it can also be some user action forcibely linking a vdi snapshot to a vm
|
|
245
|
+
// => we exclude these from the backup processing
|
|
246
|
+
if (vm.$snapshot_of === undefined) {
|
|
247
|
+
warn(
|
|
248
|
+
`vdi ${vdi.name_label} (${vdi.uuid}) is a snapshot linked to a non snapshot vm ${vm.name_label} ${vm.uuid}.
|
|
249
|
+
This disk snapshot will be excluded from the backup cleaning`,
|
|
250
|
+
{ vdi, vm }
|
|
251
|
+
)
|
|
252
|
+
delete vdiCandidates[vdi.uuid]
|
|
253
|
+
continue
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// vdi is attached only to a snapshot that is not a backup snapshot
|
|
257
|
+
// we don't check scheduleId since we are looking for all the snapshot of this job
|
|
258
|
+
// => excludes from the list to be cleared
|
|
259
|
+
if (
|
|
260
|
+
vm.other_config[DATETIME] !== vdi.other_config[DATETIME] ||
|
|
261
|
+
vm.other_config[JOB_ID] !== vdi.other_config[JOB_ID] ||
|
|
262
|
+
vm.other_config[VM_UUID] !== vdi.other_config[VM_UUID]
|
|
263
|
+
) {
|
|
264
|
+
warn(
|
|
265
|
+
`vdi ${vdi.name_label} (${vdi.uuid}) is a snapshot linked to a snapshot vm ${vm.name_label} ${vm.uuid} out of this backup job scope.
|
|
266
|
+
This disk snapshot will be excluded from the backup cleaning`,
|
|
267
|
+
{ vdi, vm }
|
|
268
|
+
)
|
|
269
|
+
delete vdiCandidates[vdi.uuid]
|
|
270
|
+
continue
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// check if all the disks of these VM snapshot have been harvested
|
|
274
|
+
// if not => remove it from the list to ensure we won't half destroy VM later
|
|
275
|
+
vm.$VBDs
|
|
276
|
+
.filter(({ $VDI }) => !!$VDI) // filter missing keys
|
|
277
|
+
.filter(({ $VDI }) => $VDI && vdiCandidates[$VDI.uuid] === undefined)
|
|
278
|
+
.forEach(({ $VDI: outOfSnapshotsVdi, ...other }) => {
|
|
279
|
+
warn(
|
|
280
|
+
`vdi ${vdi.name_label} ${vdi.uuid} is recognized as a snapshot of the backup job,
|
|
281
|
+
linked to vm ${vm.name_label} ${vm.uuid} but vdi ${outOfSnapshotsVdi.name_label} ${outOfSnapshotsVdi.uuid}
|
|
282
|
+
is not linked to the job. This disk snapshot will be excluded from the backup cleaning`,
|
|
283
|
+
{ vdi, vm, vbds, outOfSnapshotsVdi }
|
|
284
|
+
)
|
|
285
|
+
// this will be called multiple time but it is not really an issue
|
|
286
|
+
delete vdiCandidates[vdi.uuid]
|
|
287
|
+
})
|
|
221
288
|
}
|
|
289
|
+
|
|
290
|
+
this._jobSnapshotVdis = Object.values(vdiCandidates)
|
|
222
291
|
}
|
|
223
292
|
|
|
224
293
|
async _removeUnusedSnapshots() {
|
|
@@ -268,7 +337,7 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
268
337
|
// since we won't be able to remove an attached VDI
|
|
269
338
|
assert.strictEqual(vbds.length, 1, 'VDI must be free or attached to exactly one VM')
|
|
270
339
|
const vdiVm = vbds[0].$VM
|
|
271
|
-
if (
|
|
340
|
+
if (vdiVm.$snapshot_of === undefined) {
|
|
272
341
|
// don't delete a VM (especially a control domain)
|
|
273
342
|
warn(
|
|
274
343
|
`VM ${vdiVm.uuid} (${vdiVm.name_label}) linked to VDI ${vdi.uuid} (${vdi.name_label}) should be a snapshot`
|
|
@@ -308,6 +377,11 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
308
377
|
}
|
|
309
378
|
})
|
|
310
379
|
})
|
|
380
|
+
|
|
381
|
+
// list and remove the snapshot were the jobs failed between
|
|
382
|
+
// makesnapshot and update_other_config
|
|
383
|
+
const snapshots = this._vm.$snapshots.filter(_ => !!_).filter(({ name_label }) => name_label === TEMP_SNAPSHOT_NAME)
|
|
384
|
+
await asyncEach(snapshots, snapshot => snapshot.$destroy())
|
|
311
385
|
}
|
|
312
386
|
|
|
313
387
|
async _removeSnapshotData() {
|
|
@@ -315,8 +389,9 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
|
|
|
315
389
|
// going back to a previous version of XO not supporting CBT will create a full backup
|
|
316
390
|
// this will only do something after snapshot and transfer
|
|
317
391
|
if (
|
|
392
|
+
this._exportedVm !== undefined &&
|
|
318
393
|
// don't modify the VM
|
|
319
|
-
this._exportedVm
|
|
394
|
+
this._exportedVm.$snapshot_of !== undefined &&
|
|
320
395
|
// user don't want to keep the snapshot data
|
|
321
396
|
this._settings.snapshotRetention === 0 &&
|
|
322
397
|
// preferNbd is not a guarantee that the backup used NBD, depending on the network configuration,
|
|
@@ -73,6 +73,7 @@ export class FullRemoteWriter extends MixinRemoteWriter(AbstractFullWriter) {
|
|
|
73
73
|
return { size: sizeContainer.size }
|
|
74
74
|
})
|
|
75
75
|
metadata.size = sizeContainer.size
|
|
76
|
+
metadata.tags = await this.getLongTermRetentionTags(metadata)
|
|
76
77
|
this._metadataFileName = await adapter.writeVmBackupMetadata(vm.uuid, metadata)
|
|
77
78
|
|
|
78
79
|
if (!deleteFirst) {
|
|
@@ -244,6 +244,7 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
|
|
|
244
244
|
|
|
245
245
|
return { size }
|
|
246
246
|
})
|
|
247
|
+
metadataContent.tags = await this.getLongTermRetentionTags(metadataContent)
|
|
247
248
|
metadataContent.size = size // @todo return exactly the size written by this writer
|
|
248
249
|
this._metadataFileName = await adapter.writeVmBackupMetadata(vm.uuid, metadataContent)
|
|
249
250
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { asyncMapSettled } from '@xen-orchestra/async-map'
|
|
2
2
|
import ignoreErrors from 'promise-toolbox/ignoreErrors'
|
|
3
3
|
|
|
4
|
-
import { formatFilenameDate } from '../../_filenameDate.mjs'
|
|
5
4
|
import { getOldEntries } from '../../_getOldEntries.mjs'
|
|
6
5
|
import { importIncrementalVm } from '../../_incrementalVm.mjs'
|
|
7
6
|
import { Task } from '../../Task.mjs'
|
|
@@ -9,8 +8,18 @@ import { Task } from '../../Task.mjs'
|
|
|
9
8
|
import { AbstractIncrementalWriter } from './_AbstractIncrementalWriter.mjs'
|
|
10
9
|
import { MixinXapiWriter } from './_MixinXapiWriter.mjs'
|
|
11
10
|
import { listReplicatedVms } from './_listReplicatedVms.mjs'
|
|
12
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
COPY_OF,
|
|
13
|
+
setVmOtherConfig,
|
|
14
|
+
BASE_DELTA_VDI,
|
|
15
|
+
JOB_ID,
|
|
16
|
+
SCHEDULE_ID,
|
|
17
|
+
REPLICATED_TO_SR_UUID,
|
|
18
|
+
DATETIME,
|
|
19
|
+
VM_UUID,
|
|
20
|
+
} from '../../_otherConfig.mjs'
|
|
13
21
|
import assert from 'node:assert'
|
|
22
|
+
import { formatFilenameDate } from '../../_filenameDate.mjs'
|
|
14
23
|
|
|
15
24
|
export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWriter) {
|
|
16
25
|
async checkBaseVdis(baseUuidToSrcVdi) {
|
|
@@ -94,12 +103,28 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
|
|
|
94
103
|
return asyncMapSettled(this._oldEntries, vm => vm.$destroy())
|
|
95
104
|
}
|
|
96
105
|
|
|
97
|
-
#decorateVmMetadata(backup) {
|
|
106
|
+
#decorateVmMetadata(backup, timestamp) {
|
|
98
107
|
const { _warmMigration } = this._settings
|
|
99
108
|
const sr = this._sr
|
|
100
109
|
const vm = backup.vm
|
|
110
|
+
const job = this._job
|
|
111
|
+
const scheduleId = this._scheduleId
|
|
101
112
|
|
|
113
|
+
vm.name_label = `${vm.name_label} - ${job.name} - (${formatFilenameDate(timestamp)})`
|
|
114
|
+
// update other_config data as soon as possible to ensure the next job
|
|
115
|
+
// will be able to detect any partial transfer and lean them
|
|
102
116
|
vm.other_config[COPY_OF] = vm.uuid
|
|
117
|
+
vm.other_config[JOB_ID] = job.id
|
|
118
|
+
vm.other_config[SCHEDULE_ID] = scheduleId
|
|
119
|
+
vm.other_config[REPLICATED_TO_SR_UUID] = sr.uuid
|
|
120
|
+
// set the timestamp in the past to ensure any incomplete VM will be deleted on next run
|
|
121
|
+
vm.other_config[DATETIME] = formatFilenameDate(0)
|
|
122
|
+
|
|
123
|
+
vm.blocked_operations = {
|
|
124
|
+
start: 'Start operation for this vm is blocked, clone it if you want to use it.',
|
|
125
|
+
start_on: 'Start operation for this vm is blocked, clone it if you want to use it.',
|
|
126
|
+
}
|
|
127
|
+
|
|
103
128
|
if (!_warmMigration) {
|
|
104
129
|
vm.tags.push('Continuous Replication')
|
|
105
130
|
}
|
|
@@ -118,6 +143,11 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
|
|
|
118
143
|
|
|
119
144
|
Object.values(backup.vdis).forEach(vdi => {
|
|
120
145
|
vdi.other_config[COPY_OF] = vdi.uuid
|
|
146
|
+
vdi.other_config[JOB_ID] = job.id
|
|
147
|
+
vdi.other_config[SCHEDULE_ID] = scheduleId
|
|
148
|
+
vdi.other_config[REPLICATED_TO_SR_UUID] = sr.uuid
|
|
149
|
+
vdi.other_config[VM_UUID] = vm.uuid
|
|
150
|
+
|
|
121
151
|
if (sourceVdiUuids.length > 0) {
|
|
122
152
|
const baseReplicatedTo = replicatedVdis.filter(
|
|
123
153
|
replicatedVdi => replicatedVdi.other_config[COPY_OF] === vdi.other_config[BASE_DELTA_VDI]
|
|
@@ -144,12 +174,11 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
|
|
|
144
174
|
const sr = this._sr
|
|
145
175
|
const job = this._job
|
|
146
176
|
const scheduleId = this._scheduleId
|
|
147
|
-
|
|
148
177
|
const { uuid: srUuid, $xapi: xapi } = sr
|
|
149
|
-
|
|
178
|
+
|
|
150
179
|
let targetVmRef
|
|
151
180
|
await Task.run({ name: 'transfer' }, async () => {
|
|
152
|
-
targetVmRef = await importIncrementalVm(this.#decorateVmMetadata(deltaExport), sr)
|
|
181
|
+
targetVmRef = await importIncrementalVm(this.#decorateVmMetadata(deltaExport, timestamp), sr)
|
|
153
182
|
// size is mandatory to ensure the task have the right data
|
|
154
183
|
return {
|
|
155
184
|
size: Object.values(deltaExport.disks).reduce(
|
|
@@ -166,15 +195,8 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
|
|
|
166
195
|
!_warmMigration &&
|
|
167
196
|
targetVm.ha_restart_priority !== '' &&
|
|
168
197
|
Promise.all([targetVm.set_ha_restart_priority(''), targetVm.add_tags('HA disabled')]),
|
|
169
|
-
targetVm.set_name_label(`${vm.name_label} - ${job.name} - (${formatFilenameDate(timestamp)})`),
|
|
170
|
-
asyncMap(['start', 'start_on'], op =>
|
|
171
|
-
targetVm.update_blocked_operations(
|
|
172
|
-
op,
|
|
173
|
-
'Start operation for this vm is blocked, clone it if you want to use it.'
|
|
174
|
-
)
|
|
175
|
-
),
|
|
176
198
|
setVmOtherConfig(xapi, targetVmRef, {
|
|
177
|
-
timestamp,
|
|
199
|
+
timestamp, // updated at the end to mark the transfer as complete
|
|
178
200
|
jobId: job.id,
|
|
179
201
|
scheduleId,
|
|
180
202
|
vmUuid: vm.uuid,
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import { formatFilenameDate } from '../../_filenameDate.mjs'
|
|
2
|
-
import { getVmBackupDir } from '../../_getVmBackupDir.mjs'
|
|
3
|
-
|
|
4
1
|
export class AbstractWriter {
|
|
5
2
|
constructor({ config, healthCheckSr, job, vmUuid, scheduleId, settings }) {
|
|
6
3
|
this._config = config
|
|
@@ -16,14 +13,4 @@ export class AbstractWriter {
|
|
|
16
13
|
afterBackup() {}
|
|
17
14
|
|
|
18
15
|
healthCheck(sr) {}
|
|
19
|
-
|
|
20
|
-
_isAlreadyTransferred(timestamp) {
|
|
21
|
-
const vmUuid = this._vmUuid
|
|
22
|
-
const adapter = this._adapter
|
|
23
|
-
const backupDir = getVmBackupDir(vmUuid)
|
|
24
|
-
try {
|
|
25
|
-
const actualMetadata = JSON.parse(adapter._handler.readFile(`${backupDir}/${formatFilenameDate(timestamp)}.json`))
|
|
26
|
-
return actualMetadata
|
|
27
|
-
} catch (error) {}
|
|
28
|
-
}
|
|
29
16
|
}
|
|
@@ -9,6 +9,7 @@ import { ImportVmBackup } from '../../ImportVmBackup.mjs'
|
|
|
9
9
|
import { Task } from '../../Task.mjs'
|
|
10
10
|
import * as MergeWorker from '../../merge-worker/index.mjs'
|
|
11
11
|
import ms from 'ms'
|
|
12
|
+
import { getEntryStatus } from '../../_getOldEntries.mjs'
|
|
12
13
|
|
|
13
14
|
const { info, warn } = createLogger('xo:backups:MixinBackupWriter')
|
|
14
15
|
|
|
@@ -47,6 +48,19 @@ export const MixinRemoteWriter = (BaseClass = Object) =>
|
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
async getLongTermRetentionTags(currentEntry) {
|
|
52
|
+
const settings = this._settings
|
|
53
|
+
const scheduleId = this._scheduleId
|
|
54
|
+
const vmUuid = this._vmUuid
|
|
55
|
+
const adapter = this._adapter
|
|
56
|
+
|
|
57
|
+
const entries = await adapter.listVmBackups(vmUuid, _ => _.scheduleId === scheduleId)
|
|
58
|
+
entries.push(currentEntry)
|
|
59
|
+
const buckets = getEntryStatus(entries, currentEntry, settings.longTermRetention, settings.timezone)
|
|
60
|
+
|
|
61
|
+
return buckets.map(({ duration, dateBucket }) => `${duration}=${dateBucket}`)
|
|
62
|
+
}
|
|
63
|
+
|
|
50
64
|
async beforeBackup() {
|
|
51
65
|
const { handler } = this._adapter
|
|
52
66
|
const vmBackupDir = this._vmBackupDir
|
|
@@ -20,9 +20,7 @@ export function listReplicatedVms(xapi, scheduleOrJobId, srUuid, vmUuid) {
|
|
|
20
20
|
'start' in object.blocked_operations &&
|
|
21
21
|
(oc[JOB_ID] === scheduleOrJobId || oc[SCHEDULE_ID] === scheduleOrJobId) &&
|
|
22
22
|
oc[REPLICATED_TO_SR_UUID] === srUuid &&
|
|
23
|
-
|
|
24
|
-
// 2018-03-28, JFT: to catch VMs replicated before this fix
|
|
25
|
-
oc[VM_UUID] === undefined)
|
|
23
|
+
oc[VM_UUID] === vmUuid
|
|
26
24
|
) {
|
|
27
25
|
vms[object.$id] = object
|
|
28
26
|
}
|
package/formatVmBackups.mjs
CHANGED
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
|
10
10
|
},
|
|
11
|
-
"version": "0.
|
|
11
|
+
"version": "0.68.1",
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=14.18"
|
|
14
14
|
},
|
|
@@ -26,13 +26,13 @@
|
|
|
26
26
|
"@vates/disposable": "^0.1.6",
|
|
27
27
|
"@vates/fuse-vhd": "^2.1.2",
|
|
28
28
|
"@vates/generator-toolbox": "^1.1.0",
|
|
29
|
-
"@vates/nbd-client": "^3.2.
|
|
29
|
+
"@vates/nbd-client": "^3.2.3",
|
|
30
30
|
"@vates/parse-duration": "^0.1.1",
|
|
31
31
|
"@xen-orchestra/async-map": "^0.1.2",
|
|
32
32
|
"@xen-orchestra/disk-transform": "^1.2.1",
|
|
33
33
|
"@xen-orchestra/fs": "^4.6.5",
|
|
34
34
|
"@xen-orchestra/log": "^0.7.1",
|
|
35
|
-
"@xen-orchestra/qcow2": "^1.1.
|
|
35
|
+
"@xen-orchestra/qcow2": "^1.1.2",
|
|
36
36
|
"@xen-orchestra/template": "^0.1.0",
|
|
37
37
|
"app-conf": "^3.0.0",
|
|
38
38
|
"compare-versions": "^6.0.0",
|
|
@@ -48,10 +48,10 @@
|
|
|
48
48
|
"parse-pairs": "^2.0.0",
|
|
49
49
|
"promise-toolbox": "^0.21.0",
|
|
50
50
|
"proper-lockfile": "^4.1.2",
|
|
51
|
-
"tar": "^
|
|
51
|
+
"tar": "^7.5.3",
|
|
52
52
|
"uuid": "^9.0.0",
|
|
53
53
|
"value-matcher": "^0.2.0",
|
|
54
|
-
"vhd-lib": "^4.14.
|
|
54
|
+
"vhd-lib": "^4.14.7",
|
|
55
55
|
"xen-api": "^4.7.6",
|
|
56
56
|
"yazl": "^2.5.1"
|
|
57
57
|
},
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"tmp": "^0.2.1"
|
|
63
63
|
},
|
|
64
64
|
"peerDependencies": {
|
|
65
|
-
"@xen-orchestra/xapi": "^8.6.
|
|
65
|
+
"@xen-orchestra/xapi": "^8.6.5"
|
|
66
66
|
},
|
|
67
67
|
"license": "AGPL-3.0-or-later",
|
|
68
68
|
"author": {
|