Compare commits

...

11 Commits

Author SHA1 Message Date
Julien Fontanet
47732f7f5a fix(xen-api): remove Promise.race 2019-04-04 11:57:43 +02:00
Enishowk
f900a5ef4f feat(xo-web/backup): add warning regarding DST (#4056)
Fixes #4042
2019-04-03 11:42:24 +02:00
badrAZ
7f1ab529ae feat(xo-server/metadata-backups): logs implementation (#4108)
See #4014
2019-04-02 15:53:12 +02:00
Julien Fontanet
49fc86e4b1 chore(xen-api): rewrite inject-event test CLI 2019-04-02 15:24:25 +02:00
Julien Fontanet
924aef84f1 chore: drop Node 4 support 2019-04-02 11:40:27 +02:00
Rajaa.BARHTAOUI
96e6e2b72a feat(xo-web/xoa): registration panel enhancements (#4104)
Fixes #4043

- Remove useless "Trial" title
- Make the "Start trial" button bigger
2019-04-02 11:39:27 +02:00
Enishowk
71997d4e65 feat(xo-web/remotes): expose mount options field for SMB (#4067)
Fixes #4063
2019-04-02 10:49:45 +02:00
Nicolas Raynaud
447f2f9506 fix(vhd-lib/createVhdStreamWithLength): handle empty VHD (#4107)
Fixes #4105
2019-04-01 16:53:02 +02:00
Julien Fontanet
79aef9024b chore(CHANGELOG): move packages after fixes 2019-03-29 16:45:51 +01:00
Julien Fontanet
fdf6f4fdf3 chore(CHANGELOG): add missing packages list 2019-03-29 16:38:59 +01:00
Julien Fontanet
4d1eaaaade feat(xo-server): 5.38.1 2019-03-29 16:38:06 +01:00
20 changed files with 465 additions and 170 deletions

View File

@@ -27,7 +27,7 @@
">2%"
],
"engines": {
"node": ">=4"
"node": ">=6"
},
"dependencies": {
"lodash": "^4.17.4",

View File

@@ -47,6 +47,15 @@
- Safely install a subset of patches on a pool [#3777](https://github.com/vatesfr/xen-orchestra/issues/3777)
- XCP-ng: no longer requires to run `yum install xcp-ng-updater` when it's already installed [#3934](https://github.com/vatesfr/xen-orchestra/issues/3934)
### Released packages
- xen-api v0.25.0
- vhd-lib v0.6.0
- @xen-orchestra/fs v0.8.0
- xo-server-usage-report v0.7.2
- xo-server v5.38.1
- xo-web v5.38.0
## **5.32.2** (2019-02-28)
### Bug fixes

View File

@@ -2,9 +2,15 @@
### Enhancements
- [Settings/remotes] Expose mount options field for SMB [#4063](https://github.com/vatesfr/xen-orchestra/issues/4063) (PR [#4067](https://github.com/vatesfr/xen-orchestra/pull/4067))
- [Backup/Schedule] Add warning regarding DST when you add a schedule [#4042](https://github.com/vatesfr/xen-orchestra/issues/4042) (PR [#4056](https://github.com/vatesfr/xen-orchestra/pull/4056))
### Bug fixes
- [Continuous Replication] Fix VHD size guess for empty files [#4105](https://github.com/vatesfr/xen-orchestra/issues/4105) (PR [#4107](https://github.com/vatesfr/xen-orchestra/pull/4107))
### Released packages
- vhd-lib v0.6.1
- xo-server v5.39.0
- xo-web v5.39.0

View File

@@ -25,7 +25,7 @@
">2%"
],
"engines": {
"node": ">=4"
"node": ">=6"
},
"dependencies": {
"lodash": "^4.17.4"

View File

@@ -25,7 +25,7 @@
">2%"
],
"engines": {
"node": ">=4"
"node": ">=6"
},
"dependencies": {},
"devDependencies": {

View File

@@ -19,9 +19,7 @@ export default bat => {
j += 4
if (j === n) {
const error = new Error('no allocated block found')
error.noBlock = true
throw error
return
}
}
lastSector = firstSector

View File

@@ -23,71 +23,110 @@ afterEach(async () => {
await pFromCallback(cb => rimraf(tempDir, cb))
})
async function convertFromRawToVhd(rawName, vhdName) {
await execa('qemu-img', ['convert', '-f', 'raw', '-Ovpc', rawName, vhdName])
}
const RAW = 'raw'
const VHD = 'vpc'
const convert = (inputFormat, inputFile, outputFormat, outputFile) =>
execa('qemu-img', [
'convert',
'-f',
inputFormat,
'-O',
outputFormat,
inputFile,
outputFile,
])
const createRandomStream = asyncIteratorToStream(function*(size) {
let requested = Math.min(size, yield)
while (size > 0) {
const buf = Buffer.allocUnsafe(requested)
for (let i = 0; i < requested; ++i) {
buf[i] = Math.floor(Math.random() * 256)
}
requested = Math.min((size -= requested), yield buf)
}
})
async function createRandomFile(name, size) {
const createRandomStream = asyncIteratorToStream(function*(size) {
while (size-- > 0) {
yield Buffer.from([Math.floor(Math.random() * 256)])
}
})
const input = await createRandomStream(size)
await pFromCallback(cb => pipeline(input, fs.createWriteStream(name), cb))
}
test('createVhdStreamWithLength can extract length', async () => {
const initialSize = 4 * 1024
const rawFileName = `${tempDir}/randomfile`
const vhdName = `${tempDir}/randomfile.vhd`
const outputVhdName = `${tempDir}/output.vhd`
await createRandomFile(rawFileName, initialSize)
await convertFromRawToVhd(rawFileName, vhdName)
const vhdSize = fs.statSync(vhdName).size
const result = await createVhdStreamWithLength(
await createReadStream(vhdName)
)
expect(result.length).toEqual(vhdSize)
const outputFileStream = await createWriteStream(outputVhdName)
await pFromCallback(cb => pipeline(result, outputFileStream, cb))
const outputSize = fs.statSync(outputVhdName).size
expect(outputSize).toEqual(vhdSize)
})
const forOwn = (object, cb) =>
Object.keys(object).forEach(key => cb(object[key], key, object))
test('createVhdStreamWithLength can skip blank after last block and before footer', async () => {
const initialSize = 4 * 1024
const rawFileName = `${tempDir}/randomfile`
const vhdName = `${tempDir}/randomfile.vhd`
const outputVhdName = `${tempDir}/output.vhd`
await createRandomFile(rawFileName, initialSize)
await convertFromRawToVhd(rawFileName, vhdName)
const vhdSize = fs.statSync(vhdName).size
// read file footer
const footer = await getStream.buffer(
createReadStream(vhdName, { start: vhdSize - FOOTER_SIZE })
describe('createVhdStreamWithLength', () => {
forOwn(
{
// qemu-img requires this length or it fill with null bytes which breaks
// the test
'can extract length': 34816,
'can handle empty file': 0,
},
(size, title) =>
it(title, async () => {
const inputRaw = `${tempDir}/input.raw`
await createRandomFile(inputRaw, size)
const inputVhd = `${tempDir}/input.vhd`
await convert(RAW, inputRaw, VHD, inputVhd)
const result = await createVhdStreamWithLength(
await createReadStream(inputVhd)
)
const { length } = result
const outputVhd = `${tempDir}/output.vhd`
await pFromCallback(
pipeline.bind(undefined, result, await createWriteStream(outputVhd))
)
// ensure the guessed length correspond to the stream length
const { size: outputSize } = await fs.stat(outputVhd)
expect(length).toEqual(outputSize)
// ensure the generated VHD is correct and contains the same data
const outputRaw = `${tempDir}/output.raw`
await convert(VHD, outputVhd, RAW, outputRaw)
await execa('cmp', [inputRaw, outputRaw])
})
)
// we'll override the footer
const endOfFile = await createWriteStream(vhdName, {
flags: 'r+',
start: vhdSize - FOOTER_SIZE,
it('can skip blank after the last block and before the footer', async () => {
const initialSize = 4 * 1024
const rawFileName = `${tempDir}/randomfile`
const vhdName = `${tempDir}/randomfile.vhd`
const outputVhdName = `${tempDir}/output.vhd`
await createRandomFile(rawFileName, initialSize)
await convert(RAW, rawFileName, VHD, vhdName)
const { size: vhdSize } = await fs.stat(vhdName)
// read file footer
const footer = await getStream.buffer(
createReadStream(vhdName, { start: vhdSize - FOOTER_SIZE })
)
// we'll override the footer
const endOfFile = await createWriteStream(vhdName, {
flags: 'r+',
start: vhdSize - FOOTER_SIZE,
})
// write a blank over the previous footer
await pFromCallback(cb => endOfFile.write(Buffer.alloc(FOOTER_SIZE), cb))
// write the footer after the new blank
await pFromCallback(cb => endOfFile.end(footer, cb))
const { size: longerSize } = await fs.stat(vhdName)
// check input file has been lengthened
expect(longerSize).toEqual(vhdSize + FOOTER_SIZE)
const result = await createVhdStreamWithLength(
await createReadStream(vhdName)
)
expect(result.length).toEqual(vhdSize)
const outputFileStream = await createWriteStream(outputVhdName)
await pFromCallback(cb => pipeline(result, outputFileStream, cb))
const { size: outputSize } = await fs.stat(outputVhdName)
// check out file has been shortened again
expect(outputSize).toEqual(vhdSize)
await execa('qemu-img', ['compare', outputVhdName, vhdName])
})
// write a blank over the previous footer
await pFromCallback(cb => endOfFile.write(Buffer.alloc(FOOTER_SIZE), cb))
// write the footer after the new blank
await pFromCallback(cb => endOfFile.end(footer, cb))
const longerSize = fs.statSync(vhdName).size
// check input file has been lengthened
expect(longerSize).toEqual(vhdSize + FOOTER_SIZE)
const result = await createVhdStreamWithLength(
await createReadStream(vhdName)
)
expect(result.length).toEqual(vhdSize)
const outputFileStream = await createWriteStream(outputVhdName)
await pFromCallback(cb => pipeline(result, outputFileStream, cb))
const outputSize = fs.statSync(outputVhdName).size
// check out file has been shortened again
expect(outputSize).toEqual(vhdSize)
await execa('qemu-img', ['compare', outputVhdName, vhdName])
})

View File

@@ -63,10 +63,14 @@ export default async function createVhdStreamWithLength(stream) {
stream.unshift(buf)
}
const firstAndLastBlocks = getFirstAndLastBlocks(table)
const footerOffset =
getFirstAndLastBlocks(table).lastSector * SECTOR_SIZE +
Math.ceil(header.blockSize / SECTOR_SIZE / 8 / SECTOR_SIZE) * SECTOR_SIZE +
header.blockSize
firstAndLastBlocks !== undefined
? firstAndLastBlocks.lastSector * SECTOR_SIZE +
Math.ceil(header.blockSize / SECTOR_SIZE / 8 / SECTOR_SIZE) *
SECTOR_SIZE +
header.blockSize
: Math.ceil(streamPosition / SECTOR_SIZE) * SECTOR_SIZE
// ignore any data after footerOffset and push footerBuffer
//

View File

@@ -253,43 +253,37 @@ export default class Vhd {
}
async _freeFirstBlockSpace(spaceNeededBytes) {
try {
const { first, firstSector, lastSector } = getFirstAndLastBlocks(
this.blockTable
const firstAndLastBlocks = getFirstAndLastBlocks(this.blockTable)
if (firstAndLastBlocks === undefined) {
return
}
const { first, firstSector, lastSector } = firstAndLastBlocks
const tableOffset = this.header.tableOffset
const { batSize } = this
const newMinSector = Math.ceil(
(tableOffset + batSize + spaceNeededBytes) / SECTOR_SIZE
)
if (
tableOffset + batSize + spaceNeededBytes >=
sectorsToBytes(firstSector)
) {
const { fullBlockSize } = this
const newFirstSector = Math.max(
lastSector + fullBlockSize / SECTOR_SIZE,
newMinSector
)
const tableOffset = this.header.tableOffset
const { batSize } = this
const newMinSector = Math.ceil(
(tableOffset + batSize + spaceNeededBytes) / SECTOR_SIZE
debug(
`freeFirstBlockSpace: move first block ${firstSector} -> ${newFirstSector}`
)
if (
tableOffset + batSize + spaceNeededBytes >=
sectorsToBytes(firstSector)
) {
const { fullBlockSize } = this
const newFirstSector = Math.max(
lastSector + fullBlockSize / SECTOR_SIZE,
newMinSector
)
debug(
`freeFirstBlockSpace: move first block ${firstSector} -> ${newFirstSector}`
)
// copy the first block at the end
const block = await this._read(
sectorsToBytes(firstSector),
fullBlockSize
)
await this._write(block, sectorsToBytes(newFirstSector))
await this._setBatEntry(first, newFirstSector)
await this.writeFooter(true)
spaceNeededBytes -= this.fullBlockSize
if (spaceNeededBytes > 0) {
return this._freeFirstBlockSpace(spaceNeededBytes)
}
}
} catch (e) {
if (!e.noBlock) {
throw e
// copy the first block at the end
const block = await this._read(sectorsToBytes(firstSector), fullBlockSize)
await this._write(block, sectorsToBytes(newFirstSector))
await this._setBatEntry(first, newFirstSector)
await this.writeFooter(true)
spaceNeededBytes -= this.fullBlockSize
if (spaceNeededBytes > 0) {
return this._freeFirstBlockSpace(spaceNeededBytes)
}
}
}

View File

@@ -0,0 +1,142 @@
function request() {
if (this._requested) {
return
}
this._requested = true
const resolve = this._resolve
if (resolve !== undefined) {
this._resolve = undefined
resolve()
}
const listeners = this._listeners
if (listeners !== undefined) {
this._listeners = undefined
for (let i = 0, n = listeners.length; i < n; ++i) {
listeners[i].call(this)
}
}
}
const INTERNAL = {}
function Source(signals) {
const request_ = (this.request = request.bind(
(this.signal = new Signal(INTERNAL))
))
if (signals === undefined) {
return
}
const n = signals.length
for (let i = 0; i < n; ++i) {
if (signals[i].requested) {
request_()
return
}
}
for (let i = 0; i < n; ++i) {
signals[i].addListener(request_)
}
}
class Subscription {
constructor(signal, listener) {
this._listener = listener
this._signal = signal
}
get closed() {
return this._signal === undefined
}
unsubscribe() {
const signal = this._signal
if (signal !== undefined) {
const listener = this._listener
this._listener = this._signal = undefined
const listeners = signal._listeners
if (listeners !== undefined) {
const i = listeners.indexOf(listener)
if (i !== -1) {
listeners.splice(i, 1)
}
}
}
}
}
const closedSubscription = new Subscription()
export default class Signal {
static source(signals) {
return new Source(signals)
}
constructor(executor) {
this._listeners = undefined
this._promise = undefined
this._requested = false
this._resolve = undefined
if (executor !== INTERNAL) {
executor(request.bind(this))
}
}
get description() {
return this._description
}
get requested() {
return this._requested
}
throwIfRequested() {
if (this._requested) {
throw new Error('this signal has been requested')
}
}
// ===========================================================================
// Promise like API
// ===========================================================================
then(listener) {
if (typeof listener !== 'function') {
return this
}
let promise = this._promise
if (promise === undefined) {
const requested = this._requested
promise = this._promise = requested
? Promise.resolve()
: new Promise(resolve => {
this._resolve = resolve
})
}
return promise.then(listener)
}
// ===========================================================================
// Observable like API (but not compatible)
// ===========================================================================
subscribe(listener) {
if (this._requested) {
listener.call(this)
return closedSubscription
}
const listeners = this._listeners
if (listeners === undefined) {
this._listeners = [listener]
} else {
listeners.push(listener)
}
return new Subscription(this, listener)
}
}

View File

@@ -25,6 +25,7 @@ import isReadOnlyCall from './_isReadOnlyCall'
import makeCallSetting from './_makeCallSetting'
import parseUrl from './_parseUrl'
import replaceSensitiveValues from './_replaceSensitiveValues'
import Signal from './_Signal'
import XapiError from './_XapiError'
// ===================================================================
@@ -92,19 +93,15 @@ export class Xapi extends EventEmitter {
this._allowUnauthorized = opts.allowUnauthorized
this._setUrl(url)
this._connected = new Promise(resolve => {
this._resolveConnected = resolve
})
this._disconnected = Promise.resolve()
this._connected = Signal.source()
this._disconnected = Signal.source()
this._sessionId = undefined
this._status = DISCONNECTED
this._debounce = opts.debounce ?? 200
this._objects = new Collection()
this._objectsByRef = { __proto__: null }
this._objectsFetched = new Promise(resolve => {
this._resolveObjectsFetched = resolve
})
this._objectsFetched = Signal.source()
this._eventWatchers = { __proto__: null }
this._taskWatchers = { __proto__: null }
this._watchedTypes = undefined
@@ -130,11 +127,11 @@ export class Xapi extends EventEmitter {
// ===========================================================================
get connected() {
return this._connected
return this._connected.signal
}
get disconnected() {
return this._disconnected
return this._disconnected.signal
}
get pool() {
@@ -161,9 +158,7 @@ export class Xapi extends EventEmitter {
assert(status === DISCONNECTED)
this._status = CONNECTING
this._disconnected = new Promise(resolve => {
this._resolveDisconnected = resolve
})
this._disconnected = Signal.source()
try {
await this._sessionOpen()
@@ -186,8 +181,7 @@ export class Xapi extends EventEmitter {
debug('%s: connected', this._humanId)
this._status = CONNECTED
this._resolveConnected()
this._resolveConnected = undefined
this._connected.request()
this.emit(CONNECTED)
} catch (error) {
ignoreErrors.call(this.disconnect())
@@ -204,9 +198,7 @@ export class Xapi extends EventEmitter {
}
if (status === CONNECTED) {
this._connected = new Promise(resolve => {
this._resolveConnected = resolve
})
this._connected = Signal.source()
} else {
assert(status === CONNECTING)
}
@@ -220,8 +212,7 @@ export class Xapi extends EventEmitter {
debug('%s: disconnected', this._humanId)
this._status = DISCONNECTED
this._resolveDisconnected()
this._resolveDisconnected = undefined
this._disconnected.request()
this.emit(DISCONNECTED)
}
@@ -672,12 +663,18 @@ export class Xapi extends EventEmitter {
}
_interruptOnDisconnect(promise) {
return Promise.race([
promise,
this._disconnected.then(() => {
throw new Error('disconnected')
}),
])
let subscription
const pWrapper = new Promise((resolve, reject) => {
subscription = this._disconnected.signal.subscribe(() => {
reject(new Error('disconnected'))
})
promise.then(resolve, reject)
})
const clean = () => {
subscription.unsubscribe()
}
pWrapper.then(clean, clean)
return pWrapper
}
async _sessionCall(method, args, timeout) {
@@ -881,10 +878,8 @@ export class Xapi extends EventEmitter {
async _watchEvents() {
// eslint-disable-next-line no-labels
mainLoop: while (true) {
if (this._resolveObjectsFetched === undefined) {
this._objectsFetched = new Promise(resolve => {
this._resolveObjectsFetched = resolve
})
if (this._objectsFetched.signal.requested) {
this._objectsFetched = Signal.source()
}
await this._connected
@@ -912,8 +907,7 @@ export class Xapi extends EventEmitter {
// initial fetch
await this._refreshCachedRecords(types)
this._resolveObjectsFetched()
this._resolveObjectsFetched = undefined
this._objectsFetched.request()
// event loop
const debounce = this._debounce
@@ -960,10 +954,8 @@ export class Xapi extends EventEmitter {
//
// It also has to manually get all objects first.
async _watchEventsLegacy() {
if (this._resolveObjectsFetched === undefined) {
this._objectsFetched = new Promise(resolve => {
this._resolveObjectsFetched = resolve
})
if (this._objectsFetched.signal.requested) {
this._objectsFetched = Signal.source()
}
await this._connected
@@ -972,8 +964,7 @@ export class Xapi extends EventEmitter {
// initial fetch
await this._refreshCachedRecords(types)
this._resolveObjectsFetched()
this._resolveObjectsFetched = undefined
this._objectsFetched.request()
await this._sessionCall('event.register', [types])

View File

@@ -4,31 +4,33 @@ import { pDelay } from 'promise-toolbox'
import { createClient } from './'
const xapi = (() => {
const [, , url, user, password] = process.argv
return createClient({
auth: { user, password },
async function main([url]) {
const xapi = createClient({
allowUnauthorized: true,
url,
watchEvents: false,
})
})()
await xapi.connect()
xapi
.connect()
// Get the pool record's ref.
.then(() => xapi.call('pool.get_all'))
// Injects lots of events.
.then(([poolRef]) => {
const loop = () =>
pDelay
.call(
xapi.call('event.inject', 'pool', poolRef),
10 // A small delay is required to avoid overloading the Xen API.
)
.then(loop)
return loop()
let loop = true
process.on('SIGINT', () => {
loop = false
})
const { pool } = xapi
// eslint-disable-next-line no-unmodified-loop-condition
while (loop) {
await pool.update_other_config(
'xo:injectEvents',
Math.random()
.toString(36)
.slice(2)
)
await pDelay(1e2)
}
await pool.update_other_config('xo:injectEvents', null)
await xapi.disconnect()
}
main(process.argv.slice(2)).catch(console.error)

View File

@@ -32,7 +32,7 @@
"dist/"
],
"engines": {
"node": ">=4"
"node": ">=6"
},
"dependencies": {
"csv-parser": "^2.1.0",

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server",
"version": "5.38.0",
"version": "5.38.1",
"license": "AGPL-3.0",
"description": "Server part of Xen-Orchestra",
"keywords": [

View File

@@ -73,6 +73,15 @@ const createSafeReaddir = (handler, methodName) => (path, options) =>
// └─ <YYYYMMDD>T<HHmmss>
// ├─ metadata.json
// └─ data
//
// Task logs emitted in a metadata backup execution:
//
// job.start
// ├─ task.start(data: { type: 'pool', id: string, pool: <Pool />, poolMaster: <Host /> })
// │ └─ task.end
// ├─ task.start(data: { type: 'xo' })
// │ └─ task.end
// └─ job.end
export default class metadataBackup {
_app: {
createJob: (
@@ -123,7 +132,13 @@ export default class metadataBackup {
})
}
async _executor({ cancelToken, job: job_, schedule }): Executor {
async _executor({
cancelToken,
job: job_,
logger,
runJobId,
schedule,
}): Executor {
if (schedule === undefined) {
throw new Error('backup job cannot run without a schedule')
}
@@ -156,6 +171,14 @@ export default class metadataBackup {
const files = []
if (job.xoMetadata && retentionXoMetadata > 0) {
const taskId = logger.notice(`Starting XO metadata backup. (${job.id})`, {
data: {
type: 'xo',
},
event: 'task.start',
parentId: runJobId,
})
const xoMetadataDir = `${DIR_XO_CONFIG_BACKUPS}/${schedule.id}`
const dir = `${xoMetadataDir}/${formattedTimestamp}`
@@ -171,7 +194,25 @@ export default class metadataBackup {
return Promise.all([
handler.outputFile(fileName, data),
handler.outputFile(metaDataFileName, metadata),
])
]).then(
result => {
logger.notice(`Backuping XO metadata is a success. (${job.id})`, {
event: 'task.end',
status: 'success',
taskId,
})
return result
},
error => {
logger.notice(`Backuping XO metadata has failed. (${job.id})`, {
event: 'task.end',
result: serializeError(error),
status: 'failure',
taskId,
})
throw error
}
)
}),
dir: xoMetadataDir,
retention: retentionXoMetadata,
@@ -181,6 +222,21 @@ export default class metadataBackup {
files.push(
...(await Promise.all(
poolIds.map(async id => {
const xapi = this._app.getXapi(id)
const poolMaster = await xapi.getRecord('host', xapi.pool.master)
const taskId = logger.notice(
`Starting metadata backup for the pool (${id}). (${job.id})`,
{
data: {
id,
pool: xapi.pool,
poolMaster,
type: 'pool',
},
event: 'task.start',
parentId: runJobId,
}
)
const poolMetadataDir = `${DIR_XO_POOL_METADATA_BACKUPS}/${
schedule.id
}/${id}`
@@ -190,12 +246,11 @@ export default class metadataBackup {
const stream = await app.getXapi(id).exportPoolMetadata(cancelToken)
const fileName = `${dir}/data`
const xapi = this._app.getXapi(id)
const metadata = JSON.stringify(
{
...commonMetadata,
pool: xapi.pool,
poolMaster: await xapi.getRecord('host', xapi.pool.master),
poolMaster,
},
null,
2
@@ -222,7 +277,33 @@ export default class metadataBackup {
})
})(),
handler.outputFile(metaDataFileName, metadata),
])
]).then(
result => {
logger.notice(
`Backuping pool metadata (${id}) is a success. (${
job.id
})`,
{
event: 'task.end',
status: 'success',
taskId,
}
)
return result
},
error => {
logger.notice(
`Backuping pool metadata (${id}) has failed. (${job.id})`,
{
event: 'task.end',
result: serializeError(error),
status: 'failure',
taskId,
}
)
throw error
}
)
}),
dir: poolMetadataDir,
retention: retentionPoolMetadata,

View File

@@ -430,6 +430,8 @@ const messages = {
'This will migrate this backup to a backup NG. This operation is not reversible. Do you want to continue?',
runBackupNgJobConfirm: 'Are you sure you want to run {name} ({id})?',
cancelJobConfirm: 'Are you sure you want to cancel {name} ({id})?',
scheduleDstWarning:
'If your country participates in DST, it is advised that you avoid scheduling jobs at the time of change. e.g. 2AM to 3AM for US.',
// ------ New backup -----
newBackupAdvancedSettings: 'Advanced settings',
@@ -547,6 +549,7 @@ const messages = {
remoteSmbPlaceHolderPassword: 'Password',
remoteSmbPlaceHolderDomain: 'Domain',
remoteSmbPlaceHolderAddressShare: '<address>\\\\<share> *',
remoteSmbPlaceHolderOptions: 'Custom mount options',
remotePlaceHolderPassword: 'password(fill to edit)',
// ------ New Storage -----
@@ -1802,7 +1805,6 @@ const messages = {
// ----- Updates View -----
updateTitle: 'Updates',
registration: 'Registration',
trial: 'Trial',
settings: 'Settings',
proxySettings: 'Proxy settings',
proxySettingsHostPlaceHolder: 'Host (myproxy.example.org)',

View File

@@ -1,4 +1,5 @@
import classNames from 'classnames'
import Icon from 'icon'
import PropTypes from 'prop-types'
import React from 'react'
import { createSchedule } from '@xen-orchestra/cron'
@@ -457,6 +458,9 @@ export default class Scheduler extends Component {
return (
<div className='card-block'>
<em>
<Icon icon='info' /> {_('scheduleDstWarning')}
</em>
<Row>
<Col largeSize={6}>
<TimePicker

View File

@@ -226,6 +226,16 @@ const COLUMNS_SMB_REMOTE = [
),
name: _('remoteShare'),
},
{
name: _('remoteOptions'),
itemRenderer: remote => (
<Text
data-remote={remote}
onChange={_editRemoteOptions}
value={remote.options || ''}
/>
),
},
COLUMN_STATE,
{
itemRenderer: (remote, { formatMessage }) => (

View File

@@ -300,6 +300,19 @@ export default decorate([
value={domain}
/>
</div>
<div className='input-group form-group'>
<span className='input-group-addon'>-o</span>
<input
className='form-control'
name='options'
onChange={effects.linkState}
placeholder={formatMessage(
messages.remoteSmbPlaceHolderOptions
)}
type='text'
value={options}
/>
</div>
</fieldset>
)}
<div className='form-group'>

View File

@@ -405,7 +405,6 @@ const Updates = decorate([
)}
{+process.env.XOA_PLAN === 1 && (
<div>
<h2>{_('trial')}</h2>
{state.isTrialAllowed && (
<div>
{state.isRegistered ? (
@@ -413,6 +412,7 @@ const Updates = decorate([
btnStyle='success'
handler={effects.startTrial}
icon='trial'
size='large'
>
{_('trialStartButton')}
</ActionButton>