Compare commits
30 Commits
xo-server-
...
vhd-lib-v0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
794c1cf89b | ||
|
|
9a5eea6e78 | ||
|
|
40568cd61f | ||
|
|
358e1441cc | ||
|
|
be930e127e | ||
|
|
3656e83df5 | ||
|
|
abbb0450f8 | ||
|
|
8e4beeb00f | ||
|
|
05d10ef985 | ||
|
|
989d27154d | ||
|
|
ec9957bd86 | ||
|
|
dc8a7c46e0 | ||
|
|
9ee2d8e0c2 | ||
|
|
6c62d6840a | ||
|
|
2a2135ac71 | ||
|
|
efaad2efb2 | ||
|
|
3b244c24d7 | ||
|
|
915052d5f6 | ||
|
|
05c6c7830d | ||
|
|
0217c51559 | ||
|
|
0c514198bb | ||
|
|
0e68834b4c | ||
|
|
ee99ef6264 | ||
|
|
bebb9bf0df | ||
|
|
4830ac9623 | ||
|
|
58b1d0fba8 | ||
|
|
cc4e69e631 | ||
|
|
e14fda6e8a | ||
|
|
ec48b77af3 | ||
|
|
c7d6a19864 |
@@ -9,6 +9,7 @@
|
||||
|
||||
[options]
|
||||
esproposal.decorators=ignore
|
||||
esproposal.optional_chaining=enable
|
||||
include_warnings=true
|
||||
module.use_strict=true
|
||||
|
||||
|
||||
21
CHANGELOG.md
21
CHANGELOG.md
@@ -4,6 +4,12 @@
|
||||
|
||||
### Enhancements
|
||||
|
||||
### Bugs
|
||||
|
||||
## **5.21.0** (2018-06-28)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Hide legacy backup creation view [#2956](https://github.com/vatesfr/xen-orchestra/issues/2956)
|
||||
- [Delta Backup NG logs] Display wether the export is a full or a delta [#2711](https://github.com/vatesfr/xen-orchestra/issues/2711)
|
||||
- Copy VDIs' UUID from SR/disks view [#3051](https://github.com/vatesfr/xen-orchestra/issues/3051)
|
||||
@@ -11,6 +17,17 @@
|
||||
- [Backup NG form] Improve feedback [#2711](https://github.com/vatesfr/xen-orchestra/issues/2711)
|
||||
- [Backup NG] Different retentions for backup and replication [#2895](https://github.com/vatesfr/xen-orchestra/issues/2895)
|
||||
- Possibility to use a fast clone when creating a VM from a snapshot [#2937](https://github.com/vatesfr/xen-orchestra/issues/2937)
|
||||
- Ability to customize cloud config templates [#2984](https://github.com/vatesfr/xen-orchestra/issues/2984)
|
||||
- Add Backup deprecation message and link to Backup NG migration blog post [#3089](https://github.com/vatesfr/xen-orchestra/issues/3089)
|
||||
- [Backup NG] Ability to cancel a running backup job [#3047](https://github.com/vatesfr/xen-orchestra/issues/3047)
|
||||
- [Backup NG form] Ability to enable/disable a schedule [#3062](https://github.com/vatesfr/xen-orchestra/issues/3062)
|
||||
- New backup/health view with non-existent backup snapshots table [#3090](https://github.com/vatesfr/xen-orchestra/issues/3090)
|
||||
- Disable cancel/destroy tasks when not allowed [#3076](https://github.com/vatesfr/xen-orchestra/issues/3076)
|
||||
- Default remote type is NFS [#3103](https://github.com/vatesfr/xen-orchestra/issues/3103) (PR [#3114](https://github.com/vatesfr/xen-orchestra/pull/3114))
|
||||
- Add legacy backups snapshots to backup/health [#3082](https://github.com/vatesfr/xen-orchestra/issues/3082) (PR [#3111](https://github.com/vatesfr/xen-orchestra/pull/3111))
|
||||
- [Backup NG logs] Add the job's name to the modal's title [#2711](https://github.com/vatesfr/xen-orchestra/issues/2711) (PR [#3115](https://github.com/vatesfr/xen-orchestra/pull/3115))
|
||||
- Adding a XCP-ng host to a XS pool now fails fast [#3061](https://github.com/vatesfr/xen-orchestra/issues/3061) (PR [#3118](https://github.com/vatesfr/xen-orchestra/pull/3118))
|
||||
- [Backup NG logs] Ability to report a failed job and copy its log to the clipboard [#3100](https://github.com/vatesfr/xen-orchestra/issues/3100) (PR [#3110](https://github.com/vatesfr/xen-orchestra/pull/3110))
|
||||
|
||||
### Bugs
|
||||
|
||||
@@ -19,6 +36,10 @@
|
||||
- Fix the retry of a single failed/interrupted VM backup [#2912](https://github.com/vatesfr/xen-orchestra/issues/2912#issuecomment-395480321)
|
||||
- New VM with Self: filter out networks that are not in the template's pool [#3011](https://github.com/vatesfr/xen-orchestra/issues/3011)
|
||||
- [Backup NG] Auto-detect when a full export is necessary.
|
||||
- Fix Load Balancer [#3075](https://github.com/vatesfr/xen-orchestra/issues/3075#event-1685469551) [#3026](https://github.com/vatesfr/xen-orchestra/issues/3026)
|
||||
- [SR stats] Don't scale XAPI iowait values [#2969](https://github.com/vatesfr/xen-orchestra/issues/2969)
|
||||
- [Backup NG] Don't list unusable SRs for CR/DR [#3050](https://github.com/vatesfr/xen-orchestra/issues/3050)
|
||||
- Fix creating VM from snapshot (PR [3117](https://github.com/vatesfr/xen-orchestra/pull/3117))
|
||||
|
||||
## **5.20.0** (2018-05-31)
|
||||
|
||||
|
||||
3
flow-typed/promise-toolbox.js
vendored
3
flow-typed/promise-toolbox.js
vendored
@@ -1,4 +1,7 @@
|
||||
declare module 'promise-toolbox' {
|
||||
declare export class CancelToken {
|
||||
static source(): { cancel: (message: any) => void, token: CancelToken };
|
||||
}
|
||||
declare export function cancelable(Function): Function
|
||||
declare export function defer<T>(): {|
|
||||
promise: Promise<T>,
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"@xen-orchestra/fs": "^0.1.0",
|
||||
"exec-promise": "^0.7.0",
|
||||
"struct-fu": "^1.2.0",
|
||||
"vhd-lib": "^0.1.2"
|
||||
"vhd-lib": "^0.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.0.0-beta.49",
|
||||
|
||||
23
packages/vhd-cli/src/_utils.js
Normal file
23
packages/vhd-cli/src/_utils.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const { createWriteStream } = require('fs')
|
||||
const { PassThrough } = require('stream')
|
||||
|
||||
const createOutputStream = path => {
|
||||
if (path !== undefined && path !== '-') {
|
||||
return createWriteStream(path)
|
||||
}
|
||||
|
||||
// introduce a through stream because stdout is not a normal stream!
|
||||
const stream = new PassThrough()
|
||||
stream.pipe(process.stdout)
|
||||
return stream
|
||||
}
|
||||
|
||||
export const writeStream = (input, path) => {
|
||||
const output = createOutputStream(path)
|
||||
|
||||
return new Promise((resolve, reject) =>
|
||||
input
|
||||
.on('error', reject)
|
||||
.pipe(output.on('error', reject).on('finish', resolve))
|
||||
)
|
||||
}
|
||||
16
packages/vhd-cli/src/commands/raw.js
Normal file
16
packages/vhd-cli/src/commands/raw.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { createContentStream } from 'vhd-lib'
|
||||
import { getHandler } from '@xen-orchestra/fs'
|
||||
import { resolve } from 'path'
|
||||
|
||||
import { writeStream } from '../_utils'
|
||||
|
||||
export default async args => {
|
||||
if (args.length < 2 || args.some(_ => _ === '-h' || _ === '--help')) {
|
||||
return `Usage: ${this.command} <input VHD> [<output raw>]`
|
||||
}
|
||||
|
||||
await writeStream(
|
||||
createContentStream(getHandler({ url: 'file:///' }), resolve(args[0])),
|
||||
args[1]
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vhd-lib",
|
||||
"version": "0.1.2",
|
||||
"version": "0.2.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Primitives for VHD file handling",
|
||||
"keywords": [],
|
||||
|
||||
31
packages/vhd-lib/src/createContentStream.js
Normal file
31
packages/vhd-lib/src/createContentStream.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import asyncIteratorToStream from 'async-iterator-to-stream'
|
||||
|
||||
import Vhd from './vhd'
|
||||
|
||||
export default asyncIteratorToStream(async function * (handler, path) {
|
||||
const fd = await handler.openFile(path, 'r')
|
||||
try {
|
||||
const vhd = new Vhd(handler, fd)
|
||||
await vhd.readHeaderAndFooter()
|
||||
await vhd.readBlockAllocationTable()
|
||||
const {
|
||||
footer: { currentSize },
|
||||
header: { blockSize },
|
||||
} = vhd
|
||||
const nFullBlocks = Math.floor(currentSize / blockSize)
|
||||
const nLeftoverBytes = currentSize % blockSize
|
||||
|
||||
const emptyBlock = Buffer.alloc(blockSize)
|
||||
for (let i = 0; i < nFullBlocks; ++i) {
|
||||
yield vhd.containsBlock(i) ? (await vhd._readBlock(i)).data : emptyBlock
|
||||
}
|
||||
if (nLeftoverBytes !== 0) {
|
||||
yield (vhd.containsBlock(nFullBlocks)
|
||||
? (await vhd._readBlock(nFullBlocks)).data
|
||||
: emptyBlock
|
||||
).slice(0, nLeftoverBytes)
|
||||
}
|
||||
} finally {
|
||||
await handler.closeFile(fd)
|
||||
}
|
||||
})
|
||||
@@ -1,5 +1,6 @@
|
||||
export { default } from './vhd'
|
||||
export { default as chainVhd } from './chain'
|
||||
export { default as createContentStream } from './createContentStream'
|
||||
export { default as createReadableRawStream } from './createReadableRawStream'
|
||||
export {
|
||||
default as createReadableSparseStream,
|
||||
|
||||
@@ -96,6 +96,7 @@ class XapiError extends BaseError {
|
||||
// slots than can be assigned later
|
||||
this.method = undefined
|
||||
this.url = undefined
|
||||
this.task = undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,7 +189,9 @@ const getTaskResult = task => {
|
||||
return Promise.reject(new Cancel('task canceled'))
|
||||
}
|
||||
if (status === 'failure') {
|
||||
return Promise.reject(wrapError(task.error_info))
|
||||
const error = wrapError(task.error_info)
|
||||
error.task = task
|
||||
return Promise.reject(error)
|
||||
}
|
||||
if (status === 'success') {
|
||||
// the result might be:
|
||||
|
||||
@@ -7,9 +7,13 @@ import { debug } from './utils'
|
||||
|
||||
export default class DensityPlan extends Plan {
|
||||
_checkRessourcesThresholds (objects, averages) {
|
||||
const { low } = this._thresholds.memoryFree
|
||||
return filter(
|
||||
objects,
|
||||
object => averages[object.id].memoryFree > this._thresholds.memoryFree.low
|
||||
object => {
|
||||
const { memory, memoryFree = memory } = averages[object.id]
|
||||
return memoryFree > low
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -145,7 +149,9 @@ export default class DensityPlan extends Plan {
|
||||
|
||||
// Test if a VM migration on a destination (of a destinations set) is possible.
|
||||
_testMigration ({ vm, destinations, hostsAverages, vmsAverages }) {
|
||||
const { _thresholds: { critical: criticalThreshold } } = this
|
||||
const {
|
||||
_thresholds: { critical: criticalThreshold },
|
||||
} = this
|
||||
|
||||
// Sort the destinations by available memory. (- -> +)
|
||||
destinations.sort(
|
||||
|
||||
@@ -56,7 +56,7 @@ export default class PerformancePlan extends Plan {
|
||||
}
|
||||
|
||||
const { averages, toOptimize } = results
|
||||
let { hosts } = results
|
||||
const { hosts } = results
|
||||
|
||||
toOptimize.sort((a, b) => {
|
||||
a = averages[a.id]
|
||||
@@ -69,12 +69,12 @@ export default class PerformancePlan extends Plan {
|
||||
const { id } = exceededHost
|
||||
|
||||
debug(`Try to optimize Host (${exceededHost.id}).`)
|
||||
hosts = filter(hosts, host => host.id !== id)
|
||||
const availableHosts = filter(hosts, host => host.id !== id)
|
||||
|
||||
// Search bests combinations for the worst host.
|
||||
await this._optimize({
|
||||
exceededHost,
|
||||
hosts,
|
||||
hosts: availableHosts,
|
||||
hostsAverages: averages,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { filter, includes, map as mapToArray } from 'lodash'
|
||||
import { filter, includes, map as mapToArray, size } from 'lodash'
|
||||
|
||||
import { EXECUTION_DELAY, debug } from './utils'
|
||||
|
||||
@@ -23,13 +23,18 @@ const numberOrDefault = (value, def) => (value >= 0 ? value : def)
|
||||
// Averages.
|
||||
// ===================================================================
|
||||
|
||||
function computeAverage (values, nPoints = values.length) {
|
||||
function computeAverage (values, nPoints) {
|
||||
if (values === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
let sum = 0
|
||||
let tot = 0
|
||||
|
||||
const { length } = values
|
||||
const start = nPoints !== undefined ? length - nPoints : 0
|
||||
|
||||
for (let i = length - nPoints; i < length; i++) {
|
||||
for (let i = start; i < length; i++) {
|
||||
const value = values[i]
|
||||
|
||||
sum += value || 0
|
||||
@@ -53,7 +58,7 @@ function computeRessourcesAverage (objects, objectsStats, nPoints) {
|
||||
cpu: computeAverage(
|
||||
mapToArray(stats.cpus, cpu => computeAverage(cpu, nPoints))
|
||||
),
|
||||
nCpus: stats.cpus.length,
|
||||
nCpus: size(stats.cpus),
|
||||
memoryFree: computeAverage(stats.memoryFree, nPoints),
|
||||
memory: computeAverage(stats.memory, nPoints),
|
||||
}
|
||||
@@ -69,9 +74,13 @@ function computeRessourcesAverageWithWeight (averages1, averages2, ratio) {
|
||||
const objectAverages = (averages[id] = {})
|
||||
|
||||
for (const averageName in averages1[id]) {
|
||||
const average1 = averages1[id][averageName]
|
||||
if (average1 === undefined) {
|
||||
continue
|
||||
}
|
||||
|
||||
objectAverages[averageName] =
|
||||
averages1[id][averageName] * ratio +
|
||||
averages2[id][averageName] * (1 - ratio)
|
||||
average1 * ratio + averages2[id][averageName] * (1 - ratio)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xo-server",
|
||||
"version": "5.20.2",
|
||||
"version": "5.20.3",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Server part of Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -111,7 +111,7 @@
|
||||
"tmp": "^0.0.33",
|
||||
"uuid": "^3.0.1",
|
||||
"value-matcher": "^0.2.0",
|
||||
"vhd-lib": "^0.1.2",
|
||||
"vhd-lib": "^0.2.0",
|
||||
"ws": "^5.0.0",
|
||||
"xen-api": "^0.16.10",
|
||||
"xml2js": "^0.4.19",
|
||||
|
||||
41
packages/xo-server/src/api/cloud-config.js
Normal file
41
packages/xo-server/src/api/cloud-config.js
Normal file
@@ -0,0 +1,41 @@
|
||||
export function getAll () {
|
||||
return this.getAllCloudConfigs()
|
||||
}
|
||||
|
||||
getAll.permission = 'admin'
|
||||
getAll.description = 'Gets all existing cloud configs templates'
|
||||
|
||||
export function create (props) {
|
||||
return this.createCloudConfig(props)
|
||||
}
|
||||
|
||||
create.permission = 'admin'
|
||||
create.description = 'Creates a new cloud config template'
|
||||
create.params = {
|
||||
name: { type: 'string' },
|
||||
template: { type: 'string' },
|
||||
}
|
||||
|
||||
export function update (props) {
|
||||
return this.updateCloudConfig(props)
|
||||
}
|
||||
|
||||
update.permission = 'admin'
|
||||
update.description = 'Modifies an existing cloud config template'
|
||||
update.params = {
|
||||
id: { type: 'string' },
|
||||
name: { type: 'string', optional: true },
|
||||
template: { type: 'string', optional: true },
|
||||
}
|
||||
|
||||
function delete_ ({ id }) {
|
||||
return this.deleteCloudConfig(id)
|
||||
}
|
||||
|
||||
delete_.permission = 'admin'
|
||||
delete_.description = 'Deletes an existing cloud config template'
|
||||
delete_.params = {
|
||||
id: { type: 'string' },
|
||||
}
|
||||
|
||||
export { delete_ as delete }
|
||||
@@ -146,8 +146,18 @@ export { uploadPatch as patch }
|
||||
|
||||
export async function mergeInto ({ source, target, force }) {
|
||||
const sourceHost = this.getObject(source.master)
|
||||
const targetHost = this.getObject(target.master)
|
||||
|
||||
if (sourceHost.productBrand !== targetHost.productBrand) {
|
||||
throw new Error(
|
||||
`a ${sourceHost.productBrand} pool cannot be merged into a ${
|
||||
targetHost.productBrand
|
||||
} pool`
|
||||
)
|
||||
}
|
||||
|
||||
const sourcePatches = sourceHost.patches
|
||||
const targetPatches = this.getObject(target.master).patches
|
||||
const targetPatches = targetHost.patches
|
||||
const counterDiff = differenceBy(sourcePatches, targetPatches, 'name')
|
||||
|
||||
if (counterDiff.length > 0) {
|
||||
|
||||
@@ -595,6 +595,7 @@ const TRANSFORMS = {
|
||||
|
||||
task (obj) {
|
||||
return {
|
||||
allowedOperations: obj.allowed_operations,
|
||||
created: toTimestamp(obj.created),
|
||||
current_operations: obj.current_operations,
|
||||
finished: toTimestamp(obj.finished),
|
||||
|
||||
@@ -98,7 +98,9 @@ const getValuesFromDepth = (obj, targetPath) => {
|
||||
const testMetric = (test, type) =>
|
||||
typeof test === 'string'
|
||||
? test === type
|
||||
: typeof test === 'function' ? test(type) : test.exec(type)
|
||||
: typeof test === 'function'
|
||||
? test(type)
|
||||
: test.exec(type)
|
||||
|
||||
const findMetric = (metrics, metricType) => {
|
||||
let testResult
|
||||
@@ -193,7 +195,6 @@ const STATS = {
|
||||
iowait: {
|
||||
test: /^iowait_(\w+)$/,
|
||||
getPath: matches => ['iowait', matches[1]],
|
||||
transformValue: value => value * 1e2,
|
||||
},
|
||||
},
|
||||
vm: {
|
||||
@@ -242,13 +243,14 @@ export default class XapiStats {
|
||||
// Execute one http request on a XenServer for get stats
|
||||
// Return stats (Json format) or throws got exception
|
||||
@limitConcurrency(3)
|
||||
_getJson (xapi, host, timestamp) {
|
||||
_getJson (xapi, host, timestamp, step) {
|
||||
return xapi
|
||||
.getResource('/rrd_updates', {
|
||||
host,
|
||||
query: {
|
||||
cf: 'AVERAGE',
|
||||
host: 'true',
|
||||
interval: step,
|
||||
json: 'true',
|
||||
start: timestamp,
|
||||
},
|
||||
@@ -316,7 +318,7 @@ export default class XapiStats {
|
||||
}
|
||||
|
||||
const timestamp = await this._getNextTimestamp(xapi, host, step)
|
||||
const json = await this._getJson(xapi, host, timestamp)
|
||||
const json = await this._getJson(xapi, host, timestamp, step)
|
||||
if (json.meta.step !== step) {
|
||||
throw new FaultyGranularity(
|
||||
`Unable to get the true granularity: ${json.meta.step}`
|
||||
|
||||
@@ -915,7 +915,7 @@ export default class BackupNg {
|
||||
xva = xva.pipe(createSizeStream())
|
||||
|
||||
const forkExport =
|
||||
nTargets === 0
|
||||
nTargets === 1
|
||||
? () => xva
|
||||
: () => {
|
||||
const fork = xva.pipe(new PassThrough())
|
||||
|
||||
71
packages/xo-server/src/xo-mixins/cloud-configs.js
Normal file
71
packages/xo-server/src/xo-mixins/cloud-configs.js
Normal file
@@ -0,0 +1,71 @@
|
||||
// @flow
|
||||
import { noSuchObject } from 'xo-common/api-errors'
|
||||
|
||||
import Collection from '../collection/redis'
|
||||
import patch from '../patch'
|
||||
|
||||
type CloudConfig = {|
|
||||
id: string,
|
||||
name: string,
|
||||
template: string,
|
||||
|}
|
||||
|
||||
class CloudConfigs extends Collection {
|
||||
get (properties) {
|
||||
return super.get(properties)
|
||||
}
|
||||
}
|
||||
|
||||
export default class {
|
||||
_app: any
|
||||
_db: {|
|
||||
add: Function,
|
||||
first: Function,
|
||||
get: Function,
|
||||
remove: Function,
|
||||
update: Function,
|
||||
|}
|
||||
|
||||
constructor (app: any) {
|
||||
this._app = app
|
||||
const db = (this._db = new CloudConfigs({
|
||||
connection: app._redis,
|
||||
prefix: 'xo:cloudConfig',
|
||||
}))
|
||||
|
||||
app.on('clean', () => db.rebuildIndexes())
|
||||
app.on('start', () =>
|
||||
app.addConfigManager(
|
||||
'cloudConfigs',
|
||||
() => db.get(),
|
||||
cloudConfigs => db.update(cloudConfigs)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
createCloudConfig (cloudConfig: $Diff<CloudConfig, {| id: string |}>) {
|
||||
return this._db.add(cloudConfig).properties
|
||||
}
|
||||
|
||||
async updateCloudConfig ({ id, name, template }: $Shape<CloudConfig>) {
|
||||
const cloudConfig = await this.getCloudConfig(id)
|
||||
patch(cloudConfig, { name, template })
|
||||
return this._db.update(cloudConfig)
|
||||
}
|
||||
|
||||
deleteCloudConfig (id: string) {
|
||||
return this._db.remove(id)
|
||||
}
|
||||
|
||||
getAllCloudConfigs (): Promise<Array<CloudConfig>> {
|
||||
return this._db.get()
|
||||
}
|
||||
|
||||
async getCloudConfig (id: string): Promise<CloudConfig> {
|
||||
const cloudConfig = await this._db.first(id)
|
||||
if (cloudConfig === null) {
|
||||
throw noSuchObject(id, 'cloud config')
|
||||
}
|
||||
return cloudConfig.properties
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,6 @@ export default class {
|
||||
Promise.all(mapToArray(remotes, remote => this._remotes.save(remote)))
|
||||
)
|
||||
|
||||
await this.initRemotes()
|
||||
await this.syncAllRemotes()
|
||||
})
|
||||
xo.on('stop', () => this.forgetAllRemotes())
|
||||
@@ -111,15 +110,4 @@ export default class {
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Should it be private?
|
||||
async initRemotes () {
|
||||
const remotes = await this.getAllRemotes()
|
||||
if (!remotes || !remotes.length) {
|
||||
await this.createRemote({
|
||||
name: 'default',
|
||||
url: 'file://var/lib/xoa-backups',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"pipette": "^0.9.3",
|
||||
"promise-toolbox": "^0.9.5",
|
||||
"tmp": "^0.0.33",
|
||||
"vhd-lib": "^0.1.2"
|
||||
"vhd-lib": "^0.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.0.0-beta.49",
|
||||
|
||||
37
packages/xo-web/src/common/cloud-config.js
Normal file
37
packages/xo-web/src/common/cloud-config.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import _ from 'intl'
|
||||
import React from 'react'
|
||||
import { map } from 'lodash'
|
||||
|
||||
import Icon from './icon'
|
||||
import Tooltip from './tooltip'
|
||||
import { alert } from './modal'
|
||||
|
||||
const AVAILABLE_TEMPLATE_VARS = {
|
||||
'{name}': 'templateNameInfo',
|
||||
'%': 'templateIndexInfo',
|
||||
}
|
||||
|
||||
const showAvailableTemplateVars = () =>
|
||||
alert(
|
||||
_('availableTemplateVarsTitle'),
|
||||
<ul>
|
||||
{map(AVAILABLE_TEMPLATE_VARS, (value, key) => (
|
||||
<li key={key}>{_.keyValue(key, _(value))}</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
|
||||
export const AvailableTemplateVars = () => (
|
||||
<Tooltip content={_('availableTemplateVarsInfo')}>
|
||||
<a
|
||||
className='text-info'
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={showAvailableTemplateVars}
|
||||
>
|
||||
<Icon icon='info' />
|
||||
</a>
|
||||
</Tooltip>
|
||||
)
|
||||
|
||||
export const DEFAULT_CLOUD_CONFIG_TEMPLATE =
|
||||
'#cloud-config\n#hostname: {name}%\n#ssh_authorized_keys:\n# - ssh-rsa <myKey>\n#packages:\n# - htop\n'
|
||||
@@ -71,6 +71,7 @@ const messages = {
|
||||
settingsAclsPage: 'ACLs',
|
||||
settingsPluginsPage: 'Plugins',
|
||||
settingsLogsPage: 'Logs',
|
||||
settingsCloudConfigsPage: 'Cloud configs',
|
||||
settingsIpsPage: 'IPs',
|
||||
settingsConfigPage: 'Config',
|
||||
aboutPage: 'About',
|
||||
@@ -84,8 +85,9 @@ const messages = {
|
||||
newImport: 'Import',
|
||||
xosan: 'XOSAN',
|
||||
backupDeprecatedMessage:
|
||||
'Backup is deprecated, use Backup NG instead to create new backups.',
|
||||
backupNgNewPage: 'New backup NG',
|
||||
'Warning: Backup is deprecated, use Backup NG instead.',
|
||||
backupMigrationLink: 'How to migrate to Backup NG',
|
||||
backupNgNewPage: 'Create a new backup with Backup NG',
|
||||
backupOverviewPage: 'Overview',
|
||||
backupNewPage: 'New',
|
||||
backupRemotesPage: 'Remotes',
|
||||
@@ -188,10 +190,17 @@ const messages = {
|
||||
sortedTableNumberOfSelectedItems: '{nSelected, number} selected',
|
||||
sortedTableSelectAllItems: 'Click here to select all items',
|
||||
|
||||
// ----- state -----
|
||||
state: 'State',
|
||||
stateDisabled: 'Disabled',
|
||||
stateEnabled: 'Enabled',
|
||||
|
||||
// ----- Forms -----
|
||||
formCancel: 'Cancel',
|
||||
formCreate: 'Create',
|
||||
formEdit: 'Edit',
|
||||
formId: 'ID',
|
||||
formName: 'Name',
|
||||
formReset: 'Reset',
|
||||
formSave: 'Save',
|
||||
add: 'Add',
|
||||
@@ -226,9 +235,10 @@ const messages = {
|
||||
selectIp: 'Select IP(s)…',
|
||||
selectIpPool: 'Select IP pool(s)…',
|
||||
selectVgpuType: 'Select VGPU type(s)…',
|
||||
fillRequiredInformations: 'Fill required informations.',
|
||||
fillOptionalInformations: 'Fill informations (optional)',
|
||||
fillRequiredInformations: 'Fill required information.',
|
||||
fillOptionalInformations: 'Fill information (optional)',
|
||||
selectTableReset: 'Reset',
|
||||
selectCloudConfigs: 'Select Cloud Config(s)…',
|
||||
|
||||
// --- Dates/Scheduler ---
|
||||
|
||||
@@ -283,12 +293,10 @@ const messages = {
|
||||
jobAction: 'Action',
|
||||
jobTag: 'Tag',
|
||||
jobScheduling: 'Scheduling',
|
||||
jobState: 'State',
|
||||
jobStateEnabled: 'Enabled',
|
||||
jobStateDisabled: 'Disabled',
|
||||
jobTimezone: 'Timezone',
|
||||
jobServerTimezone: 'Server',
|
||||
runJob: 'Run job',
|
||||
cancelJob: 'Cancel job',
|
||||
runJobConfirm: 'Are you sure you want to run {backupType} {id} ({tag})?',
|
||||
runJobVerbose: 'One shot running started. See overview for logs.',
|
||||
jobEdit: 'Edit job',
|
||||
@@ -343,6 +351,7 @@ const messages = {
|
||||
scheduleRun: 'Run schedule',
|
||||
deleteSelectedSchedules: 'Delete selected schedules',
|
||||
noScheduledJobs: 'No scheduled jobs.',
|
||||
legacySnapshotsLink: 'You can delete all your legacy backup snapshots.',
|
||||
newSchedule: 'New schedule',
|
||||
noJobs: 'No jobs found.',
|
||||
noSchedules: 'No schedules found',
|
||||
@@ -364,6 +373,7 @@ const messages = {
|
||||
migrateBackupScheduleMessage:
|
||||
'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})?',
|
||||
|
||||
// ------ New backup -----
|
||||
newBackupAdvancedSettings: 'Advanced settings',
|
||||
@@ -815,10 +825,6 @@ const messages = {
|
||||
powerStateSuspended: 'suspended',
|
||||
|
||||
// ----- VM home -----
|
||||
vmStatus: 'No Xen tools detected',
|
||||
vmName: 'No IPv4 record',
|
||||
vmDescription: 'No IP record',
|
||||
vmSettings: 'Started {ago}',
|
||||
vmCurrentStatus: 'Current status:',
|
||||
vmNotRunning: 'Not running',
|
||||
vmHaltedSince: 'Halted {ago}',
|
||||
@@ -1105,6 +1111,8 @@ const messages = {
|
||||
vmContainer: 'Resident on',
|
||||
vmSnapshotsRelatedToNonExistentBackups:
|
||||
'VM snapshots related to non-existent backups',
|
||||
snapshotOf: 'Snapshot of',
|
||||
legacySnapshots: 'Legacy backups snapshots',
|
||||
alarmMessage: 'Alarms',
|
||||
noAlarms: 'No alarms',
|
||||
alarmDate: 'Date',
|
||||
@@ -1145,7 +1153,7 @@ const messages = {
|
||||
newVmReset: 'Reset',
|
||||
newVmSelectTemplate: 'Select template',
|
||||
newVmSshKey: 'SSH key',
|
||||
newVmConfigDrive: 'Config drive',
|
||||
noConfigDrive: 'No config drive',
|
||||
newVmCustomConfig: 'Custom config',
|
||||
availableTemplateVarsInfo:
|
||||
'Click here to see the available template variables',
|
||||
@@ -1237,7 +1245,7 @@ const messages = {
|
||||
noVmImportErrorDescription: 'No description available',
|
||||
vmImportError: 'Error:',
|
||||
vmImportFileType: '{type} file:',
|
||||
vmImportConfigAlert: 'Please to check and/or modify the VM configuration.',
|
||||
vmImportConfigAlert: 'Please check and/or modify the VM configuration.',
|
||||
|
||||
// ---- Tasks ---
|
||||
noTasks: 'No pending tasks',
|
||||
@@ -1252,8 +1260,6 @@ const messages = {
|
||||
|
||||
// ---- Backup views ---
|
||||
backupSchedules: 'Schedules',
|
||||
backupSavedSchedules: 'Saved schedules',
|
||||
backupNewSchedules: 'New schedules',
|
||||
scheduleCron: 'Cron pattern',
|
||||
scheduleName: 'Name',
|
||||
scheduleTimezone: 'Timezone',
|
||||
@@ -1768,6 +1774,16 @@ const messages = {
|
||||
settingsAclsButtonTooltipSR: 'SR',
|
||||
settingsAclsButtonTooltipnetwork: 'Network',
|
||||
|
||||
// ----- Settings/Cloud configs -----
|
||||
settingsCloudConfigTemplate: 'Template',
|
||||
confirmDeleteCloudConfigsTitle:
|
||||
'Delete cloud config{nCloudConfigs, plural, one {} other {s}}',
|
||||
confirmDeleteCloudConfigsBody:
|
||||
'Are you sure you want to delete {nCloudConfigs, number} cloud config{nCloudConfigs, plural, one {} other {s}}?',
|
||||
deleteCloudConfig: 'Delete cloud config',
|
||||
editCloudConfig: 'Edit cloud config',
|
||||
deleteSelectedCloudConfigs: 'Delete selected cloud configs',
|
||||
|
||||
// ----- Config -----
|
||||
noConfigFile: 'No config file selected',
|
||||
importTip:
|
||||
|
||||
@@ -16,7 +16,11 @@ export default class BooleanInput extends Component {
|
||||
return (
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<div className='checkbox form-control'>
|
||||
<Toggle disabled={disabled} onChange={onChange} value={value} />
|
||||
<Toggle
|
||||
disabled={disabled}
|
||||
onChange={onChange}
|
||||
value={value || false}
|
||||
/>
|
||||
</div>
|
||||
</PrimitiveInputWrapper>
|
||||
)
|
||||
|
||||
@@ -94,6 +94,11 @@ const VgpuItem = connectStore(() => ({
|
||||
|
||||
const xoItemToRender = {
|
||||
// Subscription objects.
|
||||
cloudConfig: template => (
|
||||
<span>
|
||||
<Icon icon='template' /> {template.name}
|
||||
</span>
|
||||
),
|
||||
group: group => (
|
||||
<span>
|
||||
<Icon icon='group' /> {group.name}
|
||||
|
||||
51
packages/xo-web/src/common/report-bug-button.js
Normal file
51
packages/xo-web/src/common/report-bug-button.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import _ from 'intl'
|
||||
import React from 'react'
|
||||
|
||||
import ActionButton from './action-button'
|
||||
import ActionRowButton from './action-row-button'
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
export const CAN_REPORT_BUG = process.env.XOA_PLAN > 1
|
||||
|
||||
const reportBug = ({ formatMessage, message, title }) => {
|
||||
const encodedTitle = encodeURIComponent(title)
|
||||
const encodedMessage = encodeURIComponent(
|
||||
formatMessage !== undefined ? formatMessage(message) : message
|
||||
)
|
||||
|
||||
window.open(
|
||||
process.env.XOA_PLAN < 5
|
||||
? `https://xen-orchestra.com/#!/member/support?title=${encodedTitle}&message=${encodedMessage}`
|
||||
: `https://github.com/vatesfr/xen-orchestra/issues/new?title=${encodedTitle}&body=${encodedMessage}`
|
||||
)
|
||||
}
|
||||
|
||||
const ReportBugButton = ({
|
||||
formatMessage,
|
||||
message,
|
||||
rowButton,
|
||||
title,
|
||||
...props
|
||||
}) => {
|
||||
const Button = rowButton ? ActionRowButton : ActionButton
|
||||
return (
|
||||
<Button
|
||||
{...props}
|
||||
data-formatMessage={formatMessage}
|
||||
data-message={message}
|
||||
data-title={title}
|
||||
handler={reportBug}
|
||||
icon='bug'
|
||||
tooltip={_('reportBug')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
propTypes(ReportBugButton)({
|
||||
formatMessage: propTypes.func,
|
||||
message: propTypes.string.isRequired,
|
||||
rowButton: propTypes.bool,
|
||||
title: propTypes.string.isRequired,
|
||||
})
|
||||
|
||||
export default ReportBugButton
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
import { addSubscriptions, connectStore, resolveResourceSets } from './utils'
|
||||
import {
|
||||
isSrWritable,
|
||||
subscribeCloudConfigs,
|
||||
subscribeCurrentUser,
|
||||
subscribeGroups,
|
||||
subscribeIpPools,
|
||||
@@ -1038,3 +1039,18 @@ export const SelectIpPool = makeSubscriptionSelect(
|
||||
},
|
||||
{ placeholder: _('selectIpPool') }
|
||||
)
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const SelectCloudConfig = makeSubscriptionSelect(
|
||||
subscriber =>
|
||||
subscribeCloudConfigs(cloudConfigs => {
|
||||
subscriber({
|
||||
xoObjects: map(sortBy(cloudConfigs, 'name'), cloudConfig => ({
|
||||
...cloudConfig,
|
||||
type: 'cloudConfig',
|
||||
})),
|
||||
})
|
||||
}),
|
||||
{ placeholder: _('selectCloudConfigs') }
|
||||
)
|
||||
|
||||
@@ -198,7 +198,7 @@ const actionsShape = propTypes.arrayOf(
|
||||
disabled: propTypes.oneOfType([propTypes.bool, propTypes.func]),
|
||||
handler: propTypes.func.isRequired,
|
||||
icon: propTypes.string.isRequired,
|
||||
label: propTypes.node.isRequired,
|
||||
label: propTypes.oneOfType([propTypes.node, propTypes.func]).isRequired,
|
||||
level: propTypes.oneOf(['primary', 'warning', 'danger']),
|
||||
redirectOnSuccess: propTypes.oneOfType([propTypes.func, propTypes.string]),
|
||||
})
|
||||
|
||||
@@ -205,7 +205,7 @@ export const formatSizeRaw = bytes =>
|
||||
humanFormat.raw(bytes, { scale: 'binary', unit: 'B' })
|
||||
|
||||
export const formatSpeed = (bytes, milliseconds) =>
|
||||
humanFormat(bytes * 1e3 / milliseconds, { scale: 'binary', unit: 'B/s' })
|
||||
humanFormat((bytes * 1e3) / milliseconds, { scale: 'binary', unit: 'B/s' })
|
||||
|
||||
const timeScale = new humanFormat.Scale({
|
||||
ns: 1e-6,
|
||||
@@ -516,7 +516,7 @@ export const createFakeProgress = (() => {
|
||||
const startTime = Date.now() / 1e3
|
||||
return () => {
|
||||
const x = Date.now() / 1e3 - startTime
|
||||
return -Math.exp(x * Math.log(1 - S) / d) + 1
|
||||
return -Math.exp((x * Math.log(1 - S)) / d) + 1
|
||||
}
|
||||
}
|
||||
})()
|
||||
@@ -563,3 +563,10 @@ export const isLatestXosanPackInstalled = (latestXosanPack, hosts) =>
|
||||
|
||||
export const getMemoryUsedMetric = ({ memory, memoryFree = memory }) =>
|
||||
map(memory, (value, key) => value - memoryFree[key])
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const generateRandomId = () =>
|
||||
Math.random()
|
||||
.toString(36)
|
||||
.slice(2)
|
||||
|
||||
@@ -596,7 +596,11 @@ export const IopsLineChart = injectIntl(
|
||||
data: propTypes.array.isRequired,
|
||||
options: propTypes.object,
|
||||
})(({ addSumSeries, data, options = {}, intl }) => {
|
||||
const { endTimestamp, interval, stats: { iops } } = data
|
||||
const {
|
||||
endTimestamp,
|
||||
interval,
|
||||
stats: { iops },
|
||||
} = data
|
||||
|
||||
const { length } = get(iops, 'r')
|
||||
|
||||
@@ -635,7 +639,11 @@ export const IoThroughputChart = injectIntl(
|
||||
data: propTypes.array.isRequired,
|
||||
options: propTypes.object,
|
||||
})(({ addSumSeries, data, options = {}, intl }) => {
|
||||
const { endTimestamp, interval, stats: { ioThroughput } } = data
|
||||
const {
|
||||
endTimestamp,
|
||||
interval,
|
||||
stats: { ioThroughput },
|
||||
} = data
|
||||
|
||||
const { length } = get(ioThroughput, 'r') || []
|
||||
|
||||
@@ -674,7 +682,11 @@ export const LatencyChart = injectIntl(
|
||||
data: propTypes.array.isRequired,
|
||||
options: propTypes.object,
|
||||
})(({ addSumSeries, data, options = {}, intl }) => {
|
||||
const { endTimestamp, interval, stats: { latency } } = data
|
||||
const {
|
||||
endTimestamp,
|
||||
interval,
|
||||
stats: { latency },
|
||||
} = data
|
||||
|
||||
const { length } = get(latency, 'r') || []
|
||||
|
||||
@@ -713,7 +725,11 @@ export const IowaitChart = injectIntl(
|
||||
data: propTypes.array.isRequired,
|
||||
options: propTypes.object,
|
||||
})(({ addSumSeries, data, options = {}, intl }) => {
|
||||
const { endTimestamp, interval, stats: { iowait } } = data
|
||||
const {
|
||||
endTimestamp,
|
||||
interval,
|
||||
stats: { iowait },
|
||||
} = data
|
||||
|
||||
const { length } = iowait[Object.keys(iowait)[0]] || []
|
||||
|
||||
@@ -737,7 +753,7 @@ export const IowaitChart = injectIntl(
|
||||
nValues: length,
|
||||
endTimestamp,
|
||||
interval,
|
||||
valueTransform: value => `${round(value, 2)}%`,
|
||||
valueTransform: value => `${round(value, 3)}%`,
|
||||
}),
|
||||
...options,
|
||||
}}
|
||||
|
||||
@@ -1088,7 +1088,7 @@ export const migrateVm = (vm, host) =>
|
||||
_('migrateVmNoTargetHostMessage')
|
||||
)
|
||||
}
|
||||
_call('vm.migrate', { vm: vm.id, ...params })
|
||||
return _call('vm.migrate', { vm: vm.id, ...params })
|
||||
}, noop)
|
||||
|
||||
import MigrateVmsModalBody from './migrate-vms-modal' // eslint-disable-line import/first
|
||||
@@ -1679,6 +1679,15 @@ export const runJob = job => {
|
||||
return _call('job.runSequence', { idSequence: [resolveId(job)] })
|
||||
}
|
||||
|
||||
export const cancelJob = ({ id, name, runId }) =>
|
||||
confirm({
|
||||
title: _('cancelJob'),
|
||||
body: _('cancelJobConfirm', {
|
||||
id: id.slice(0, 5),
|
||||
name: <strong>{name}</strong>,
|
||||
}),
|
||||
}).then(() => _call('job.cancel', { runId }))
|
||||
|
||||
// Backup/Schedule ---------------------------------------------------------
|
||||
|
||||
export const createSchedule = (
|
||||
@@ -1843,7 +1852,7 @@ export const purgePluginConfiguration = async id => {
|
||||
subscribePlugins.forceRefresh()
|
||||
}
|
||||
|
||||
export const testPlugin = async (id, data) => _call('plugin.test', { id, data })
|
||||
export const testPlugin = (id, data) => _call('plugin.test', { id, data })
|
||||
|
||||
export const sendUsageReport = () => _call('plugin.usageReport.send')
|
||||
|
||||
@@ -2393,6 +2402,39 @@ export const setIpPool = (ipPool, { name, addresses, networks }) =>
|
||||
networks: resolveIds(networks),
|
||||
})::tap(subscribeIpPools.forceRefresh)
|
||||
|
||||
// Cloud configs --------------------------------------------------------------------
|
||||
|
||||
export const subscribeCloudConfigs = createSubscription(() =>
|
||||
_call('cloudConfig.getAll')
|
||||
)
|
||||
|
||||
export const createCloudConfig = props =>
|
||||
_call('cloudConfig.create', props)::tap(subscribeCloudConfigs.forceRefresh)
|
||||
|
||||
export const deleteCloudConfigs = ids => {
|
||||
const { length } = ids
|
||||
if (length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const vars = { nCloudConfigs: length }
|
||||
return confirm({
|
||||
title: _('confirmDeleteCloudConfigsTitle', vars),
|
||||
body: <p>{_('confirmDeleteCloudConfigsBody', vars)}</p>,
|
||||
}).then(
|
||||
() =>
|
||||
Promise.all(
|
||||
ids.map(id => _call('cloudConfig.delete', { id: resolveId(id) }))
|
||||
)::tap(subscribeCloudConfigs.forceRefresh),
|
||||
noop
|
||||
)
|
||||
}
|
||||
|
||||
export const editCloudConfig = (cloudConfig, props) =>
|
||||
_call('cloudConfig.update', { ...props, id: resolveId(cloudConfig) })::tap(
|
||||
subscribeCloudConfigs.forceRefresh
|
||||
)
|
||||
|
||||
// XO SAN ----------------------------------------------------------------------
|
||||
|
||||
export const getVolumeInfo = (xosanSr, infoType) =>
|
||||
|
||||
@@ -2,7 +2,7 @@ import addSubscriptions from 'add-subscriptions'
|
||||
import React from 'react'
|
||||
import { injectState, provideState } from '@julien-f/freactal'
|
||||
import { subscribeBackupNgJobs, subscribeSchedules } from 'xo'
|
||||
import { find, groupBy } from 'lodash'
|
||||
import { find, groupBy, keyBy } from 'lodash'
|
||||
|
||||
import New from './new'
|
||||
|
||||
@@ -18,7 +18,7 @@ export default [
|
||||
computed: {
|
||||
job: (_, { jobs, routeParams: { id } }) => find(jobs, { id }),
|
||||
schedules: (_, { schedulesByJob, routeParams: { id } }) =>
|
||||
schedulesByJob && schedulesByJob[id],
|
||||
schedulesByJob && keyBy(schedulesByJob[id], 'id'),
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
|
||||
156
packages/xo-web/src/xo-app/backup-ng/health/index.js
Normal file
156
packages/xo-web/src/xo-app/backup-ng/health/index.js
Normal file
@@ -0,0 +1,156 @@
|
||||
import _ from 'intl'
|
||||
import Component from 'base-component'
|
||||
import Icon from 'icon'
|
||||
import Link from 'link'
|
||||
import NoObjects from 'no-objects'
|
||||
import React from 'react'
|
||||
import renderXoItem from 'render-xo-item'
|
||||
import SortedTable from 'sorted-table'
|
||||
import { Card, CardHeader, CardBlock } from 'card'
|
||||
import { addSubscriptions, connectStore } from 'utils'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { FormattedRelative, FormattedTime } from 'react-intl'
|
||||
import { includes, map } from 'lodash'
|
||||
import { deleteSnapshot, deleteSnapshots, subscribeSchedules } from 'xo'
|
||||
import {
|
||||
createSelector,
|
||||
createGetObjectsOfType,
|
||||
createCollectionWrapper,
|
||||
} from 'selectors'
|
||||
|
||||
const SNAPSHOT_COLUMNS = [
|
||||
{
|
||||
name: _('snapshotDate'),
|
||||
itemRenderer: snapshot => (
|
||||
<span>
|
||||
<FormattedTime
|
||||
day='numeric'
|
||||
hour='numeric'
|
||||
minute='numeric'
|
||||
month='long'
|
||||
value={snapshot.snapshot_time * 1000}
|
||||
year='numeric'
|
||||
/>{' '}
|
||||
(<FormattedRelative value={snapshot.snapshot_time * 1000} />)
|
||||
</span>
|
||||
),
|
||||
sortCriteria: 'snapshot_time',
|
||||
sortOrder: 'desc',
|
||||
},
|
||||
{
|
||||
name: _('vmNameLabel'),
|
||||
itemRenderer: renderXoItem,
|
||||
sortCriteria: 'name_label',
|
||||
},
|
||||
{
|
||||
name: _('vmNameDescription'),
|
||||
itemRenderer: snapshot => snapshot.name_description,
|
||||
sortCriteria: 'name_description',
|
||||
},
|
||||
{
|
||||
name: _('snapshotOf'),
|
||||
itemRenderer: (snapshot, { vms }) => {
|
||||
const vm = vms[snapshot.$snapshot_of]
|
||||
return vm && <Link to={`/vms/${vm.id}`}>{renderXoItem(vm)}</Link>
|
||||
},
|
||||
sortCriteria: (snapshot, { vms }) => {
|
||||
const vm = vms[snapshot.$snapshot_of]
|
||||
return vm && vm.name_label
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const ACTIONS = [
|
||||
{
|
||||
label: _('deleteSnapshots'),
|
||||
individualLabel: _('deleteSnapshot'),
|
||||
icon: 'delete',
|
||||
level: 'danger',
|
||||
handler: deleteSnapshots,
|
||||
individualHandler: deleteSnapshot,
|
||||
},
|
||||
]
|
||||
|
||||
@addSubscriptions({
|
||||
schedules: subscribeSchedules,
|
||||
})
|
||||
@connectStore(() => {
|
||||
const getSnapshots = createGetObjectsOfType('VM-snapshot')
|
||||
|
||||
return {
|
||||
loneSnapshots: getSnapshots.filter(
|
||||
createSelector(
|
||||
createCollectionWrapper(
|
||||
(_, props) =>
|
||||
props.schedules !== undefined && map(props.schedules, 'id')
|
||||
),
|
||||
scheduleIds =>
|
||||
scheduleIds
|
||||
? _ => {
|
||||
const scheduleId = _.other['xo:backup:schedule']
|
||||
return (
|
||||
scheduleId !== undefined && !includes(scheduleIds, scheduleId)
|
||||
)
|
||||
}
|
||||
: false
|
||||
)
|
||||
),
|
||||
legacySnapshots: getSnapshots.filter([
|
||||
(() => {
|
||||
const RE = /^(?:XO_DELTA_EXPORT:|XO_DELTA_BASE_VM_SNAPSHOT_|rollingSnapshot_)/
|
||||
return (
|
||||
{ name_label } // eslint-disable-line camelcase
|
||||
) => RE.test(name_label)
|
||||
})(),
|
||||
]),
|
||||
vms: createGetObjectsOfType('VM'),
|
||||
}
|
||||
})
|
||||
export default class Health extends Component {
|
||||
render () {
|
||||
return (
|
||||
<Container>
|
||||
<Row className='lone-snapshots'>
|
||||
<Col>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon icon='vm' /> {_('vmSnapshotsRelatedToNonExistentBackups')}
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<NoObjects
|
||||
actions={ACTIONS}
|
||||
collection={this.props.loneSnapshots}
|
||||
columns={SNAPSHOT_COLUMNS}
|
||||
component={SortedTable}
|
||||
data-vms={this.props.vms}
|
||||
emptyMessage={_('noSnapshots')}
|
||||
shortcutsTarget='.lone-snapshots'
|
||||
/>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className='legacy-snapshots'>
|
||||
<Col>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon icon='vm' /> {_('legacySnapshots')}
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<NoObjects
|
||||
actions={ACTIONS}
|
||||
collection={this.props.legacySnapshots}
|
||||
columns={SNAPSHOT_COLUMNS}
|
||||
component={SortedTable}
|
||||
data-vms={this.props.vms}
|
||||
emptyMessage={_('noSnapshots')}
|
||||
shortcutsTarget='.legacy-snapshots'
|
||||
/>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import { Container, Row, Col } from 'grid'
|
||||
import { NavLink, NavTabs } from 'nav'
|
||||
import { routes } from 'utils'
|
||||
import {
|
||||
cancelJob,
|
||||
deleteBackupNgJobs,
|
||||
disableSchedule,
|
||||
enableSchedule,
|
||||
@@ -28,6 +29,7 @@ import Edit from './edit'
|
||||
import New from './new'
|
||||
import FileRestore from './file-restore'
|
||||
import Restore from './restore'
|
||||
import Health from './health'
|
||||
|
||||
const _runBackupNgJob = ({ id, name, schedule }) =>
|
||||
confirm({
|
||||
@@ -57,10 +59,10 @@ const SchedulePreviewBody = ({ item: job, userData: { schedulesByJob } }) => (
|
||||
<td>{job.settings[schedule.id].snapshotRetention}</td>
|
||||
<td>
|
||||
<StateButton
|
||||
disabledLabel={_('jobStateDisabled')}
|
||||
disabledLabel={_('stateDisabled')}
|
||||
disabledHandler={enableSchedule}
|
||||
disabledTooltip={_('logIndicationToEnable')}
|
||||
enabledLabel={_('jobStateEnabled')}
|
||||
enabledLabel={_('stateEnabled')}
|
||||
enabledHandler={disableSchedule}
|
||||
enabledTooltip={_('logIndicationToDisable')}
|
||||
handlerParam={schedule.id}
|
||||
@@ -68,15 +70,28 @@ const SchedulePreviewBody = ({ item: job, userData: { schedulesByJob } }) => (
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
data-id={job.id}
|
||||
data-name={job.name}
|
||||
data-schedule={schedule.id}
|
||||
handler={_runBackupNgJob}
|
||||
icon='run-schedule'
|
||||
size='small'
|
||||
/>
|
||||
{job.runId !== undefined ? (
|
||||
<ActionButton
|
||||
btnStyle='danger'
|
||||
handler={cancelJob}
|
||||
handlerParam={job}
|
||||
icon='cancel'
|
||||
key='cancel'
|
||||
size='small'
|
||||
tooltip={_('formCancel')}
|
||||
/>
|
||||
) : (
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
data-id={job.id}
|
||||
data-name={job.name}
|
||||
data-schedule={schedule.id}
|
||||
handler={_runBackupNgJob}
|
||||
icon='run-schedule'
|
||||
key='run'
|
||||
size='small'
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
@@ -199,6 +214,10 @@ const HEADER = (
|
||||
<Icon icon='menu-backup-file-restore' />{' '}
|
||||
{_('backupFileRestorePage')}
|
||||
</NavLink>
|
||||
<NavLink to='/backup-ng/health'>
|
||||
<Icon icon='menu-dashboard-health' />{' '}
|
||||
{_('overviewHealthDashboardPage')}
|
||||
</NavLink>
|
||||
</NavTabs>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -211,6 +230,7 @@ export default routes('overview', {
|
||||
overview: Overview,
|
||||
restore: Restore,
|
||||
'file-restore': FileRestore,
|
||||
health: Health,
|
||||
})(({ children }) => (
|
||||
<Page header={HEADER} title='backupPage' formatTitle>
|
||||
{children}
|
||||
|
||||
@@ -6,7 +6,12 @@ import renderXoItem, { renderXoItemFromId } from 'render-xo-item'
|
||||
import Select from 'form/select'
|
||||
import Tooltip from 'tooltip'
|
||||
import Upgrade from 'xoa-upgrade'
|
||||
import { addSubscriptions, resolveId, resolveIds } from 'utils'
|
||||
import {
|
||||
addSubscriptions,
|
||||
generateRandomId,
|
||||
resolveId,
|
||||
resolveIds,
|
||||
} from 'utils'
|
||||
import { Card, CardBlock, CardHeader } from 'card'
|
||||
import { constructSmartPattern, destructSmartPattern } from 'smart-backup'
|
||||
import { Container, Col, Row } from 'grid'
|
||||
@@ -14,14 +19,14 @@ import { injectState, provideState } from '@julien-f/freactal'
|
||||
import { SelectRemote, SelectSr, SelectVm } from 'select-objects'
|
||||
import { Toggle } from 'form'
|
||||
import {
|
||||
find,
|
||||
findKey,
|
||||
cloneDeep,
|
||||
flatten,
|
||||
forEach,
|
||||
includes,
|
||||
isEmpty,
|
||||
keyBy,
|
||||
map,
|
||||
mapValues,
|
||||
some,
|
||||
} from 'lodash'
|
||||
import {
|
||||
@@ -30,26 +35,19 @@ import {
|
||||
deleteSchedule,
|
||||
editBackupNgJob,
|
||||
editSchedule,
|
||||
isSrWritable,
|
||||
subscribeRemotes,
|
||||
} from 'xo'
|
||||
|
||||
import Schedules from './schedules'
|
||||
import SmartBackup from './smart-backup'
|
||||
import {
|
||||
FormFeedback,
|
||||
FormGroup,
|
||||
getRandomId,
|
||||
Input,
|
||||
Number,
|
||||
Ul,
|
||||
Li,
|
||||
} from './utils'
|
||||
import { FormFeedback, FormGroup, Input, Number, Ul, Li } from './utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const normaliseTagValues = values => resolveIds(values).map(value => [value])
|
||||
const normalizeTagValues = values => resolveIds(values).map(value => [value])
|
||||
|
||||
const normaliseCopyRentention = settings => {
|
||||
const normalizeCopyRetention = settings => {
|
||||
forEach(settings, schedule => {
|
||||
if (schedule.copyRetention === undefined) {
|
||||
schedule.copyRetention = schedule.exportRetention
|
||||
@@ -57,7 +55,7 @@ const normaliseCopyRentention = settings => {
|
||||
})
|
||||
}
|
||||
|
||||
const normaliseSettings = ({
|
||||
const normalizeSettings = ({
|
||||
settings,
|
||||
exportMode,
|
||||
copyMode,
|
||||
@@ -103,35 +101,6 @@ const destructVmsPattern = pattern =>
|
||||
vms: destructPattern(pattern),
|
||||
}
|
||||
|
||||
const getNewSettings = ({ schedules, exportMode, copyMode, snapshotMode }) => {
|
||||
const newSettings = {}
|
||||
|
||||
for (const id in schedules) {
|
||||
newSettings[id] = {
|
||||
exportRetention: exportMode ? schedules[id].exportRetention : undefined,
|
||||
copyRetention: copyMode ? schedules[id].copyRetention : undefined,
|
||||
snapshotRetention: snapshotMode
|
||||
? schedules[id].snapshotRetention
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
return newSettings
|
||||
}
|
||||
|
||||
const getNewSchedules = schedules => {
|
||||
const newSchedules = {}
|
||||
|
||||
for (const id in schedules) {
|
||||
newSchedules[id] = {
|
||||
cron: schedules[id].cron,
|
||||
timezone: schedules[id].timezone,
|
||||
}
|
||||
}
|
||||
|
||||
return newSchedules
|
||||
}
|
||||
|
||||
const REPORT_WHEN_FILTER_OPTIONS = [
|
||||
{
|
||||
label: 'reportWhenAlways',
|
||||
@@ -149,6 +118,11 @@ const REPORT_WHEN_FILTER_OPTIONS = [
|
||||
|
||||
const getOptionRenderer = ({ label }) => <span>{_(label)}</span>
|
||||
|
||||
const createDoesRetentionExist = name => {
|
||||
const predicate = setting => setting[name] > 0
|
||||
return ({ settings }) => some(settings, predicate)
|
||||
}
|
||||
|
||||
const getInitialState = () => ({
|
||||
$pool: {},
|
||||
backupMode: false,
|
||||
@@ -158,15 +132,14 @@ const getInitialState = () => ({
|
||||
deltaMode: false,
|
||||
drMode: false,
|
||||
editionMode: undefined,
|
||||
formId: getRandomId(),
|
||||
formId: generateRandomId(),
|
||||
name: '',
|
||||
newSchedules: {},
|
||||
offlineSnapshot: false,
|
||||
paramsUpdated: false,
|
||||
powerState: 'All',
|
||||
remotes: [],
|
||||
reportWhen: 'failure',
|
||||
schedules: [],
|
||||
schedules: {},
|
||||
settings: {},
|
||||
showErrors: false,
|
||||
smartMode: false,
|
||||
@@ -204,10 +177,13 @@ export default [
|
||||
name: state.name,
|
||||
mode: state.isDelta ? 'delta' : 'full',
|
||||
compression: state.compression ? 'native' : '',
|
||||
schedules: getNewSchedules(state.newSchedules),
|
||||
schedules: mapValues(
|
||||
state.schedules,
|
||||
({ id, ...schedule }) => schedule
|
||||
),
|
||||
settings: {
|
||||
...getNewSettings({
|
||||
schedules: state.newSchedules,
|
||||
...normalizeSettings({
|
||||
settings: cloneDeep(state.settings),
|
||||
exportMode: state.exportMode,
|
||||
copyMode: state.copyMode,
|
||||
snapshotMode: state.snapshotMode,
|
||||
@@ -239,75 +215,49 @@ export default [
|
||||
}
|
||||
}
|
||||
|
||||
const newSettings = {}
|
||||
if (!isEmpty(state.newSchedules)) {
|
||||
await Promise.all(
|
||||
map(state.newSchedules, async schedule => {
|
||||
const scheduleId = (await createSchedule(props.job.id, {
|
||||
cron: schedule.cron,
|
||||
timezone: schedule.timezone,
|
||||
})).id
|
||||
newSettings[scheduleId] = {
|
||||
exportRetention: schedule.exportRetention,
|
||||
copyRetention: schedule.copyRetention,
|
||||
snapshotRetention: schedule.snapshotRetention,
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
map(props.schedules, oldSchedule => {
|
||||
const scheduleId = oldSchedule.id
|
||||
const newSchedule = find(state.schedules, { id: scheduleId })
|
||||
|
||||
if (
|
||||
newSchedule !== undefined &&
|
||||
newSchedule.cron === oldSchedule.cron &&
|
||||
newSchedule.timezone === oldSchedule.timezone
|
||||
) {
|
||||
return
|
||||
}
|
||||
const id = oldSchedule.id
|
||||
const newSchedule = state.schedules[id]
|
||||
|
||||
if (newSchedule === undefined) {
|
||||
return deleteSchedule(scheduleId)
|
||||
return deleteSchedule(id)
|
||||
}
|
||||
|
||||
return editSchedule({
|
||||
id: scheduleId,
|
||||
jobId: props.job.id,
|
||||
cron: newSchedule.cron,
|
||||
timezone: newSchedule.timezone,
|
||||
})
|
||||
if (
|
||||
newSchedule.cron !== oldSchedule.cron ||
|
||||
newSchedule.timezone !== oldSchedule.timezone
|
||||
) {
|
||||
return editSchedule({
|
||||
id,
|
||||
cron: newSchedule.cron,
|
||||
timezone: newSchedule.timezone,
|
||||
})
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const oldSettings = props.job.settings
|
||||
const settings = state.settings
|
||||
if (!('' in oldSettings)) {
|
||||
oldSettings[''] = {}
|
||||
}
|
||||
for (const id in oldSettings) {
|
||||
const oldSetting = oldSettings[id]
|
||||
const newSetting = settings[id]
|
||||
const settings = cloneDeep(state.settings)
|
||||
await Promise.all(
|
||||
map(state.schedules, async schedule => {
|
||||
const tmpId = schedule.id
|
||||
if (props.schedules[tmpId] === undefined) {
|
||||
const { id } = await createSchedule(props.job.id, {
|
||||
cron: schedule.cron,
|
||||
timezone: schedule.timezone,
|
||||
})
|
||||
|
||||
if (id === '') {
|
||||
oldSetting.reportWhen = state.reportWhen
|
||||
oldSetting.concurrency = state.concurrency
|
||||
oldSetting.offlineSnapshot = state.offlineSnapshot
|
||||
} else if (!(id in settings)) {
|
||||
delete oldSettings[id]
|
||||
} else if (
|
||||
oldSetting.snapshotRetention !== newSetting.snapshotRetention ||
|
||||
oldSetting.exportRetention !== newSetting.exportRetention ||
|
||||
oldSetting.copyRetention !== newSetting.copyRetention
|
||||
) {
|
||||
newSettings[id] = {
|
||||
exportRetention: newSetting.exportRetention,
|
||||
copyRetention: newSetting.copyRetention,
|
||||
snapshotRetention: newSetting.snapshotRetention,
|
||||
settings[id] = settings[tmpId]
|
||||
delete settings[tmpId]
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
settings[''] = {
|
||||
...props.job.settings[''],
|
||||
reportWhen: state.reportWhen,
|
||||
concurrency: state.concurrency,
|
||||
offlineSnapshot: state.offlineSnapshot,
|
||||
}
|
||||
|
||||
await editBackupNgJob({
|
||||
@@ -315,11 +265,8 @@ export default [
|
||||
name: state.name,
|
||||
mode: state.isDelta ? 'delta' : 'full',
|
||||
compression: state.compression ? 'native' : '',
|
||||
settings: normaliseSettings({
|
||||
settings: {
|
||||
...oldSettings,
|
||||
...newSettings,
|
||||
},
|
||||
settings: normalizeSettings({
|
||||
settings,
|
||||
exportMode: state.exportMode,
|
||||
copyMode: state.copyMode,
|
||||
snapshotMode: state.snapshotMode,
|
||||
@@ -345,6 +292,16 @@ export default [
|
||||
...state,
|
||||
[name]: checked,
|
||||
}),
|
||||
toggleScheduleState: (_, id) => state => ({
|
||||
...state,
|
||||
schedules: {
|
||||
...state.schedules,
|
||||
[id]: {
|
||||
...state.schedules[id],
|
||||
enabled: !state.schedules[id].enabled,
|
||||
},
|
||||
},
|
||||
}),
|
||||
toggleSmartMode: (_, smartMode) => state => ({
|
||||
...state,
|
||||
smartMode,
|
||||
@@ -386,13 +343,13 @@ export default [
|
||||
const srs = job.srs !== undefined ? destructPattern(job.srs) : []
|
||||
const { concurrency, reportWhen, offlineSnapshot } =
|
||||
job.settings[''] || {}
|
||||
const settings = { ...job.settings }
|
||||
const settings = cloneDeep(job.settings)
|
||||
delete settings['']
|
||||
const drMode = job.mode === 'full' && !isEmpty(srs)
|
||||
const crMode = job.mode === 'delta' && !isEmpty(srs)
|
||||
|
||||
if (drMode || crMode) {
|
||||
normaliseCopyRentention(settings)
|
||||
normalizeCopyRetention(settings)
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -428,42 +385,24 @@ export default [
|
||||
tmpSchedule: {},
|
||||
editionMode: undefined,
|
||||
}),
|
||||
editSchedule: (_, schedule) => state => {
|
||||
const { snapshotRetention, exportRetention, copyRetention } =
|
||||
state.settings[schedule.id] || {}
|
||||
return {
|
||||
...state,
|
||||
editionMode: 'editSchedule',
|
||||
tmpSchedule: {
|
||||
exportRetention,
|
||||
copyRetention,
|
||||
snapshotRetention,
|
||||
...schedule,
|
||||
},
|
||||
}
|
||||
},
|
||||
deleteSchedule: (_, id) => async (state, props) => {
|
||||
const schedules = [...state.schedules]
|
||||
schedules.splice(findKey(state.schedules, { id }), 1)
|
||||
|
||||
return {
|
||||
...state,
|
||||
schedules,
|
||||
}
|
||||
},
|
||||
editNewSchedule: (_, schedule) => state => ({
|
||||
editSchedule: (_, schedule) => state => ({
|
||||
...state,
|
||||
editionMode: 'editNewSchedule',
|
||||
editionMode: 'editSchedule',
|
||||
tmpSchedule: {
|
||||
...schedule,
|
||||
},
|
||||
}),
|
||||
deleteNewSchedule: (_, id) => async (state, props) => {
|
||||
const newSchedules = { ...state.newSchedules }
|
||||
delete newSchedules[id]
|
||||
deleteSchedule: (_, schedule) => state => {
|
||||
const id = resolveId(schedule)
|
||||
const schedules = { ...state.schedules }
|
||||
const settings = { ...state.settings }
|
||||
|
||||
delete schedules[id]
|
||||
delete settings[id]
|
||||
return {
|
||||
...state,
|
||||
newSchedules,
|
||||
schedules,
|
||||
settings,
|
||||
}
|
||||
},
|
||||
saveSchedule: (
|
||||
@@ -471,14 +410,21 @@ export default [
|
||||
{ cron, timezone, exportRetention, copyRetention, snapshotRetention }
|
||||
) => async (state, props) => {
|
||||
if (state.editionMode === 'creation') {
|
||||
const id = generateRandomId()
|
||||
return {
|
||||
...state,
|
||||
editionMode: undefined,
|
||||
newSchedules: {
|
||||
...state.newSchedules,
|
||||
[getRandomId()]: {
|
||||
schedules: {
|
||||
...state.schedules,
|
||||
[id]: {
|
||||
id,
|
||||
cron,
|
||||
timezone,
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
...state.settings,
|
||||
[id]: {
|
||||
exportRetention,
|
||||
copyRetention,
|
||||
snapshotRetention,
|
||||
@@ -488,45 +434,27 @@ export default [
|
||||
}
|
||||
|
||||
const id = state.tmpSchedule.id
|
||||
if (state.editionMode === 'editSchedule') {
|
||||
const scheduleKey = findKey(state.schedules, { id })
|
||||
const schedules = [...state.schedules]
|
||||
schedules[scheduleKey] = {
|
||||
...schedules[scheduleKey],
|
||||
cron,
|
||||
timezone,
|
||||
}
|
||||
const schedules = { ...state.schedules }
|
||||
const settings = { ...state.settings }
|
||||
|
||||
const settings = { ...state.settings }
|
||||
settings[id] = {
|
||||
exportRetention,
|
||||
copyRetention,
|
||||
snapshotRetention,
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
editionMode: undefined,
|
||||
schedules,
|
||||
settings,
|
||||
tmpSchedule: {},
|
||||
}
|
||||
schedules[id] = {
|
||||
...schedules[id],
|
||||
cron,
|
||||
timezone,
|
||||
}
|
||||
settings[id] = {
|
||||
...settings[id],
|
||||
exportRetention,
|
||||
copyRetention,
|
||||
snapshotRetention,
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
editionMode: undefined,
|
||||
schedules,
|
||||
settings,
|
||||
tmpSchedule: {},
|
||||
newSchedules: {
|
||||
...state.newSchedules,
|
||||
[id]: {
|
||||
cron,
|
||||
timezone,
|
||||
exportRetention,
|
||||
copyRetention,
|
||||
snapshotRetention,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
setPowerState: (_, powerState) => state => ({
|
||||
@@ -597,8 +525,7 @@ export default [
|
||||
missingRemotes: state =>
|
||||
(state.backupMode || state.deltaMode) && isEmpty(state.remotes),
|
||||
missingSrs: state => (state.drMode || state.crMode) && isEmpty(state.srs),
|
||||
missingSchedules: state =>
|
||||
isEmpty(state.schedules) && isEmpty(state.newSchedules),
|
||||
missingSchedules: state => isEmpty(state.schedules),
|
||||
missingExportRetention: state =>
|
||||
state.exportMode && !state.exportRetentionExists,
|
||||
missingCopyRetention: state =>
|
||||
@@ -610,30 +537,18 @@ export default [
|
||||
(state.exportRetentionExists || state.copyRetentionExists),
|
||||
exportMode: state => state.backupMode || state.deltaMode,
|
||||
copyMode: state => state.drMode || state.crMode,
|
||||
exportRetentionExists: ({ newSchedules, settings }) =>
|
||||
some(
|
||||
{ ...newSchedules, ...settings },
|
||||
({ exportRetention }) => exportRetention > 0
|
||||
),
|
||||
copyRetentionExists: ({ newSchedules, settings }) =>
|
||||
some(
|
||||
{ ...newSchedules, ...settings },
|
||||
({ copyRetention }) => copyRetention > 0
|
||||
),
|
||||
snapshotRetentionExists: ({ newSchedules, settings }) =>
|
||||
some(
|
||||
{ ...newSchedules, ...settings },
|
||||
({ snapshotRetention }) => snapshotRetention > 0
|
||||
),
|
||||
exportRetentionExists: createDoesRetentionExist('exportRetention'),
|
||||
copyRetentionExists: createDoesRetentionExist('copyRetention'),
|
||||
snapshotRetentionExists: createDoesRetentionExist('snapshotRetention'),
|
||||
isDelta: state => state.deltaMode || state.crMode,
|
||||
isFull: state => state.backupMode || state.drMode,
|
||||
vmsSmartPattern: ({ $pool, powerState, tags }) => ({
|
||||
$pool: constructSmartPattern($pool, resolveIds),
|
||||
power_state: powerState === 'All' ? undefined : powerState,
|
||||
tags: constructSmartPattern(tags, normaliseTagValues),
|
||||
tags: constructSmartPattern(tags, normalizeTagValues),
|
||||
type: 'VM',
|
||||
}),
|
||||
srPredicate: ({ srs }) => ({ id }) => !includes(srs, id),
|
||||
srPredicate: ({ srs }) => sr => isSrWritable(sr) && !includes(srs, sr.id),
|
||||
remotePredicate: ({ remotes }) => ({ id }) => !includes(remotes, id),
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -4,10 +4,11 @@ import moment from 'moment-timezone'
|
||||
import React from 'react'
|
||||
import Scheduler, { SchedulePreview } from 'scheduling'
|
||||
import { Card, CardBlock } from 'card'
|
||||
import { generateRandomId } from 'utils'
|
||||
import { injectState, provideState } from '@julien-f/freactal'
|
||||
import { isEqual } from 'lodash'
|
||||
|
||||
import { FormFeedback, FormGroup, getRandomId, Number } from './utils'
|
||||
import { FormFeedback, FormGroup, Number } from './utils'
|
||||
|
||||
export default [
|
||||
injectState,
|
||||
@@ -27,7 +28,7 @@ export default [
|
||||
cron,
|
||||
exportRetention,
|
||||
copyRetention,
|
||||
formId: getRandomId(),
|
||||
formId: generateRandomId(),
|
||||
snapshotRetention,
|
||||
timezone,
|
||||
}),
|
||||
|
||||
@@ -2,64 +2,13 @@ import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import React from 'react'
|
||||
import SortedTable from 'sorted-table'
|
||||
import StateButton from 'state-button'
|
||||
import { Card, CardBlock, CardHeader } from 'card'
|
||||
import { injectState, provideState } from '@julien-f/freactal'
|
||||
import { isEmpty, find, findKey, size } from 'lodash'
|
||||
import { isEmpty, find, size } from 'lodash'
|
||||
|
||||
import NewSchedule from './new-schedule'
|
||||
import { FormFeedback, FormGroup } from './utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const rowTransform = (schedule, { settings }) => {
|
||||
const { exportRetention, copyRetention, snapshotRetention } =
|
||||
settings[schedule.id] || {}
|
||||
|
||||
return {
|
||||
...schedule,
|
||||
exportRetention,
|
||||
copyRetention,
|
||||
snapshotRetention,
|
||||
}
|
||||
}
|
||||
|
||||
const SAVED_SCHEDULES_INDIVIDUAL_ACTIONS = [
|
||||
{
|
||||
handler: (schedule, { editSchedule }) => editSchedule(schedule),
|
||||
label: _('scheduleEdit'),
|
||||
icon: 'edit',
|
||||
disabled: (_, { disabledEdition }) => disabledEdition,
|
||||
level: 'primary',
|
||||
},
|
||||
{
|
||||
handler: (schedule, { deleteSchedule }) => deleteSchedule(schedule.id),
|
||||
label: _('scheduleDelete'),
|
||||
disabled: (_, { disabledDeletion }) => disabledDeletion,
|
||||
icon: 'delete',
|
||||
level: 'danger',
|
||||
},
|
||||
]
|
||||
|
||||
const NEW_SCHEDULES_INDIVIDUAL_ACTIONS = [
|
||||
{
|
||||
handler: (schedule, { editNewSchedule, newSchedules }) =>
|
||||
editNewSchedule({
|
||||
id: findKey(newSchedules, schedule),
|
||||
...schedule,
|
||||
}),
|
||||
label: _('scheduleEdit'),
|
||||
disabled: (_, { disabledEdition }) => disabledEdition,
|
||||
icon: 'edit',
|
||||
level: 'primary',
|
||||
},
|
||||
{
|
||||
handler: (schedule, { deleteNewSchedule, newSchedules }) =>
|
||||
deleteNewSchedule(findKey(newSchedules, schedule)),
|
||||
label: _('scheduleDelete'),
|
||||
icon: 'delete',
|
||||
level: 'danger',
|
||||
},
|
||||
]
|
||||
import { FormFeedback } from './utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -74,14 +23,64 @@ export default [
|
||||
injectState,
|
||||
provideState({
|
||||
computed: {
|
||||
disabledDeletion: state =>
|
||||
state.schedules.length + size(state.newSchedules) <= 1,
|
||||
disabledDeletion: state => size(state.schedules) <= 1,
|
||||
disabledEdition: state =>
|
||||
state.editionMode !== undefined ||
|
||||
(!state.exportMode && !state.copyMode && !state.snapshotMode),
|
||||
error: state => find(FEEDBACK_ERRORS, error => state[error]),
|
||||
schedulesColumns: state => {
|
||||
individualActions: (
|
||||
{ disabledDeletion, disabledEdition },
|
||||
{ effects: { deleteSchedule, editSchedule } }
|
||||
) => [
|
||||
{
|
||||
disabled: disabledEdition,
|
||||
handler: editSchedule,
|
||||
icon: 'edit',
|
||||
label: _('scheduleEdit'),
|
||||
level: 'primary',
|
||||
},
|
||||
{
|
||||
disabled: disabledDeletion,
|
||||
handler: deleteSchedule,
|
||||
icon: 'delete',
|
||||
label: _('scheduleDelete'),
|
||||
level: 'danger',
|
||||
},
|
||||
],
|
||||
rowTransform: ({ settings }) => schedule => {
|
||||
const { exportRetention, copyRetention, snapshotRetention } =
|
||||
settings[schedule.id] || {}
|
||||
|
||||
return {
|
||||
...schedule,
|
||||
exportRetention,
|
||||
copyRetention,
|
||||
snapshotRetention,
|
||||
}
|
||||
},
|
||||
schedulesColumns: (state, { effects: { toggleScheduleState } }) => {
|
||||
const columns = [
|
||||
{
|
||||
itemRenderer: _ => _.name,
|
||||
sortCriteria: 'name',
|
||||
name: _('scheduleName'),
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
itemRenderer: schedule => (
|
||||
<StateButton
|
||||
disabledLabel={_('stateDisabled')}
|
||||
disabledTooltip={_('logIndicationToEnable')}
|
||||
enabledLabel={_('stateEnabled')}
|
||||
enabledTooltip={_('logIndicationToDisable')}
|
||||
handler={toggleScheduleState}
|
||||
handlerParam={schedule.id}
|
||||
state={schedule.enabled}
|
||||
/>
|
||||
),
|
||||
sortCriteria: 'enabled',
|
||||
name: _('state'),
|
||||
},
|
||||
{
|
||||
itemRenderer: _ => _.cron,
|
||||
sortCriteria: 'cron',
|
||||
@@ -119,15 +118,6 @@ export default [
|
||||
}
|
||||
return columns
|
||||
},
|
||||
savedSchedulesColumns: state => [
|
||||
{
|
||||
itemRenderer: _ => _.name,
|
||||
sortCriteria: 'name',
|
||||
name: _('scheduleName'),
|
||||
default: true,
|
||||
},
|
||||
...state.schedulesColumns,
|
||||
],
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
@@ -150,43 +140,15 @@ export default [
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
{isEmpty(state.schedules) &&
|
||||
isEmpty(state.newSchedules) && (
|
||||
<p className='text-md-center'>{_('noSchedules')}</p>
|
||||
)}
|
||||
{!isEmpty(state.schedules) && (
|
||||
<FormGroup>
|
||||
<label>
|
||||
<strong>{_('backupSavedSchedules')}</strong>
|
||||
</label>
|
||||
<SortedTable
|
||||
collection={state.schedules}
|
||||
columns={state.savedSchedulesColumns}
|
||||
data-deleteSchedule={effects.deleteSchedule}
|
||||
data-disabledDeletion={state.disabledDeletion}
|
||||
data-disabledEdition={state.disabledEdition}
|
||||
data-editSchedule={effects.editSchedule}
|
||||
data-settings={state.settings}
|
||||
individualActions={SAVED_SCHEDULES_INDIVIDUAL_ACTIONS}
|
||||
rowTransform={rowTransform}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
{!isEmpty(state.newSchedules) && (
|
||||
<FormGroup>
|
||||
<label>
|
||||
<strong>{_('backupNewSchedules')}</strong>
|
||||
</label>
|
||||
<SortedTable
|
||||
collection={state.newSchedules}
|
||||
columns={state.schedulesColumns}
|
||||
data-deleteNewSchedule={effects.deleteNewSchedule}
|
||||
data-disabledEdition={state.disabledEdition}
|
||||
data-editNewSchedule={effects.editNewSchedule}
|
||||
data-newSchedules={state.newSchedules}
|
||||
individualActions={NEW_SCHEDULES_INDIVIDUAL_ACTIONS}
|
||||
/>
|
||||
</FormGroup>
|
||||
{isEmpty(state.schedules) ? (
|
||||
<p className='text-md-center'>{_('noSchedules')}</p>
|
||||
) : (
|
||||
<SortedTable
|
||||
collection={state.schedules}
|
||||
columns={state.schedulesColumns}
|
||||
individualActions={state.individualActions}
|
||||
rowTransform={state.rowTransform}
|
||||
/>
|
||||
)}
|
||||
</CardBlock>
|
||||
</FormFeedback>
|
||||
|
||||
@@ -8,11 +8,6 @@ export const Input = props => <input {...props} className='form-control' />
|
||||
export const Ul = props => <ul {...props} className='list-group' />
|
||||
export const Li = props => <li {...props} className='list-group-item' />
|
||||
|
||||
export const getRandomId = () =>
|
||||
Math.random()
|
||||
.toString(36)
|
||||
.slice(2)
|
||||
|
||||
export const Number = [
|
||||
provideState({
|
||||
effects: {
|
||||
|
||||
@@ -13,8 +13,8 @@ import Restore from './restore'
|
||||
import FileRestore from './file-restore'
|
||||
|
||||
const DeprecatedMsg = () => (
|
||||
<div>
|
||||
<em>{_('backupDeprecatedMessage')}</em>
|
||||
<div className='alert alert-warning'>
|
||||
{_('backupDeprecatedMessage')}
|
||||
<br />
|
||||
<Link to='/backup-ng/new'>{_('backupNgNewPage')}</Link>
|
||||
</div>
|
||||
|
||||
@@ -75,13 +75,13 @@ const JOB_COLUMNS = [
|
||||
sortCriteria: ({ schedule }) => schedule.timezone,
|
||||
},
|
||||
{
|
||||
name: _('jobState'),
|
||||
name: _('state'),
|
||||
itemRenderer: ({ schedule }) => (
|
||||
<StateButton
|
||||
disabledLabel={_('jobStateDisabled')}
|
||||
disabledLabel={_('stateDisabled')}
|
||||
disabledHandler={enableSchedule}
|
||||
disabledTooltip={_('logIndicationToEnable')}
|
||||
enabledLabel={_('jobStateEnabled')}
|
||||
enabledLabel={_('stateEnabled')}
|
||||
enabledHandler={disableSchedule}
|
||||
enabledTooltip={_('logIndicationToDisable')}
|
||||
handlerParam={schedule.id}
|
||||
@@ -239,14 +239,28 @@ export default class Overview extends Component {
|
||||
<CardBlock>
|
||||
<NoObjects
|
||||
collection={schedules}
|
||||
emptyMessage={_('noScheduledJobs')}
|
||||
emptyMessage={
|
||||
<span>
|
||||
{_('noScheduledJobs')}{' '}
|
||||
<Link to='/backup-ng/health'>{_('legacySnapshotsLink')}</Link>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{() => (
|
||||
<SortedTable
|
||||
columns={JOB_COLUMNS}
|
||||
collection={this._getScheduleCollection()}
|
||||
userData={isScheduleUserMissing}
|
||||
/>
|
||||
<div>
|
||||
<div className='alert alert-warning'>
|
||||
{_('backupDeprecatedMessage')}
|
||||
<br />
|
||||
<a href='https://xen-orchestra.com/blog/migrate-backup-to-backup-ng/'>
|
||||
{_('backupMigrationLink')}
|
||||
</a>
|
||||
</div>
|
||||
<SortedTable
|
||||
columns={JOB_COLUMNS}
|
||||
collection={this._getScheduleCollection()}
|
||||
userData={isScheduleUserMissing}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</NoObjects>
|
||||
</CardBlock>
|
||||
|
||||
@@ -17,13 +17,7 @@ import { Container, Row, Col } from 'grid'
|
||||
import { Card, CardHeader, CardBlock } from 'card'
|
||||
import { FormattedRelative, FormattedTime } from 'react-intl'
|
||||
import { flatten, get, includes, isEmpty, map, mapValues } from 'lodash'
|
||||
import {
|
||||
addSubscriptions,
|
||||
connectStore,
|
||||
formatSize,
|
||||
noop,
|
||||
resolveIds,
|
||||
} from 'utils'
|
||||
import { connectStore, formatSize, noop, resolveIds } from 'utils'
|
||||
import {
|
||||
deleteMessage,
|
||||
deleteOrphanedVdis,
|
||||
@@ -32,7 +26,6 @@ import {
|
||||
deleteVdi,
|
||||
deleteVm,
|
||||
isSrWritable,
|
||||
subscribeSchedules,
|
||||
} from 'xo'
|
||||
import {
|
||||
areObjectsFetched,
|
||||
@@ -112,12 +105,12 @@ const SR_COLUMNS = [
|
||||
sr.size > 1 && (
|
||||
<Tooltip
|
||||
content={_('spaceLeftTooltip', {
|
||||
used: Math.round(sr.physical_usage / sr.size * 100),
|
||||
used: Math.round((sr.physical_usage / sr.size) * 100),
|
||||
free: formatSize(sr.size - sr.physical_usage),
|
||||
})}
|
||||
>
|
||||
<meter
|
||||
value={sr.physical_usage / sr.size * 100}
|
||||
value={(sr.physical_usage / sr.size) * 100}
|
||||
min='0'
|
||||
max='100'
|
||||
optimum='40'
|
||||
@@ -390,9 +383,6 @@ const ALARM_COLUMNS = [
|
||||
},
|
||||
]
|
||||
|
||||
@addSubscriptions({
|
||||
schedules: subscribeSchedules,
|
||||
})
|
||||
@connectStore(() => {
|
||||
const getOrphanVdiSnapshots = createGetObjectsOfType('VDI-snapshot')
|
||||
.filter([_ => !_.$snapshot_of && _.$VBDs.length === 0])
|
||||
@@ -400,15 +390,6 @@ const ALARM_COLUMNS = [
|
||||
const getOrphanVmSnapshots = createGetObjectsOfType('VM-snapshot')
|
||||
.filter([snapshot => !snapshot.$snapshot_of])
|
||||
.sort()
|
||||
const getLoneBackupSnapshots = createGetObjectsOfType('VM-snapshot').filter(
|
||||
createSelector(
|
||||
createCollectionWrapper((_, props) => map(props.schedules, 'id')),
|
||||
scheduleIds => _ => {
|
||||
const scheduleId = _.other['xo:backup:schedule']
|
||||
return scheduleId !== undefined && !includes(scheduleIds, scheduleId)
|
||||
}
|
||||
)
|
||||
)
|
||||
const getUserSrs = createGetObjectsOfType('SR').filter([isSrWritable])
|
||||
const getVdiSrs = createGetObjectsOfType('SR').pick(
|
||||
createSelector(getOrphanVdiSnapshots, snapshots => map(snapshots, '$SR'))
|
||||
@@ -424,7 +405,6 @@ const ALARM_COLUMNS = [
|
||||
vdiOrphaned: getOrphanVdiSnapshots,
|
||||
vdiSr: getVdiSrs,
|
||||
vmOrphaned: getOrphanVmSnapshots,
|
||||
vmBackupSnapshots: getLoneBackupSnapshots,
|
||||
}
|
||||
})
|
||||
export default class Health extends Component {
|
||||
@@ -510,11 +490,6 @@ export default class Health extends Component {
|
||||
this._getPoolPredicate
|
||||
)
|
||||
|
||||
_getVmBackupSnapshots = createFilter(
|
||||
() => this.props.vmBackupSnapshots,
|
||||
this._getPoolPredicate
|
||||
)
|
||||
|
||||
_getAlertMessages = createFilter(
|
||||
() => this.props.alertMessages,
|
||||
this._getPoolPredicate
|
||||
@@ -635,24 +610,6 @@ export default class Health extends Component {
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className='snapshot-vms'>
|
||||
<Col>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon icon='vm' /> {_('vmSnapshotsRelatedToNonExistentBackups')}
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<NoObjects
|
||||
collection={this._getVmBackupSnapshots()}
|
||||
columns={VM_COLUMNS}
|
||||
component={SortedTable}
|
||||
emptyMessage={_('noSnapshots')}
|
||||
shortcutsTarget='.snapshot-vms'
|
||||
/>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<Card>
|
||||
|
||||
@@ -186,7 +186,7 @@ export default class Jobs extends Component {
|
||||
'vm.clone',
|
||||
'vm.convert',
|
||||
'vm.copy',
|
||||
'vm.creatInterface',
|
||||
'vm.createInterface',
|
||||
'vm.delete',
|
||||
'vm.migrate',
|
||||
'vm.migrate',
|
||||
|
||||
@@ -76,17 +76,17 @@ const SCHEDULES_COLUMNS = [
|
||||
{
|
||||
itemRenderer: schedule => (
|
||||
<StateButton
|
||||
disabledLabel={_('jobStateDisabled')}
|
||||
disabledLabel={_('stateDisabled')}
|
||||
disabledHandler={enableSchedule}
|
||||
disabledTooltip={_('logIndicationToEnable')}
|
||||
enabledLabel={_('jobStateEnabled')}
|
||||
enabledLabel={_('stateEnabled')}
|
||||
enabledHandler={disableSchedule}
|
||||
enabledTooltip={_('logIndicationToDisable')}
|
||||
handlerParam={schedule.id}
|
||||
state={schedule.enabled}
|
||||
/>
|
||||
),
|
||||
name: _('jobState'),
|
||||
name: _('state'),
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import _, { FormattedDuration } from 'intl'
|
||||
import addSubscriptions from 'add-subscriptions'
|
||||
import Button from 'button'
|
||||
import ButtonGroup from 'button-group'
|
||||
import CopyToClipboard from 'react-copy-to-clipboard'
|
||||
import Icon from 'icon'
|
||||
import NoObjects from 'no-objects'
|
||||
import React from 'react'
|
||||
import ReportBugButton, { CAN_REPORT_BUG } from 'report-bug-button'
|
||||
import SortedTable from 'sorted-table'
|
||||
import Tooltip from 'tooltip'
|
||||
import { alert } from 'modal'
|
||||
import { Card, CardHeader, CardBlock } from 'card'
|
||||
import { keyBy } from 'lodash'
|
||||
@@ -46,11 +51,6 @@ const LOG_COLUMNS = [
|
||||
itemRenderer: log => log.jobId.slice(4, 8),
|
||||
sortCriteria: log => log.jobId,
|
||||
},
|
||||
{
|
||||
name: _('jobMode'),
|
||||
itemRenderer: log => get(() => log.data.mode),
|
||||
sortCriteria: log => get(() => log.data.mode),
|
||||
},
|
||||
{
|
||||
name: _('jobName'),
|
||||
itemRenderer: (log, { jobs }) => get(() => jobs[log.jobId].name),
|
||||
@@ -107,16 +107,37 @@ const LOG_COLUMNS = [
|
||||
},
|
||||
]
|
||||
|
||||
const showTasks = log =>
|
||||
const showTasks = (log, { jobs }) => {
|
||||
const formattedLog = JSON.stringify(log, null, 2)
|
||||
alert(
|
||||
<span>
|
||||
{_('jobModalTitle', { job: log.jobId.slice(4, 8) })}{' '}
|
||||
{get(() => jobs[log.jobId].name) || 'Job'} ({log.jobId.slice(4, 8)}){' '}
|
||||
<span style={{ fontSize: '0.5em' }} className='text-muted'>
|
||||
{log.id}
|
||||
</span>
|
||||
</span>{' '}
|
||||
{log.status !== 'success' &&
|
||||
log.status !== 'pending' && (
|
||||
<ButtonGroup>
|
||||
<Tooltip content={_('copyToClipboard')}>
|
||||
<CopyToClipboard text={formattedLog}>
|
||||
<Button size='small'>
|
||||
<Icon icon='clipboard' />
|
||||
</Button>
|
||||
</CopyToClipboard>
|
||||
</Tooltip>
|
||||
{CAN_REPORT_BUG && (
|
||||
<ReportBugButton
|
||||
message={`\`\`\`json\n${formattedLog}\n\`\`\``}
|
||||
size='small'
|
||||
title='Backup job failed'
|
||||
/>
|
||||
)}
|
||||
</ButtonGroup>
|
||||
)}
|
||||
</span>,
|
||||
<LogAlertBody id={log.id} />
|
||||
)
|
||||
}
|
||||
|
||||
const LOG_INDIVIDUAL_ACTIONS = [
|
||||
{
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import _, { FormattedDuration } from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import Copiable from 'copiable'
|
||||
import Icon from 'icon'
|
||||
import React from 'react'
|
||||
import renderXoItem, { renderXoItemFromId } from 'render-xo-item'
|
||||
@@ -148,9 +147,7 @@ export default [
|
||||
return (status === 'failure' || status === 'skipped') &&
|
||||
result !== undefined ? (
|
||||
<span className={status === 'skipped' ? 'text-info' : 'text-danger'}>
|
||||
<Copiable tagName='p' data={JSON.stringify(result, null, 2)}>
|
||||
<Icon icon='alarm' /> {result.message}
|
||||
</Copiable>
|
||||
<Icon icon='alarm' /> {result.message}
|
||||
</span>
|
||||
) : (
|
||||
<div>
|
||||
@@ -258,41 +255,32 @@ export default [
|
||||
/>
|
||||
)}
|
||||
<br />
|
||||
{operationLog.status === 'failure' ? (
|
||||
<Copiable
|
||||
tagName='p'
|
||||
data={JSON.stringify(
|
||||
operationLog.result,
|
||||
null,
|
||||
2
|
||||
)}
|
||||
>
|
||||
{_.keyValue(
|
||||
{operationLog.status === 'failure'
|
||||
? _.keyValue(
|
||||
_('taskError'),
|
||||
<span className='text-danger'>
|
||||
{operationLog.result.message}
|
||||
</span>
|
||||
)
|
||||
: operationLog.result.size > 0 && (
|
||||
<div>
|
||||
{_.keyValue(
|
||||
_('operationSize'),
|
||||
formatSize(
|
||||
operationLog.result.size
|
||||
)
|
||||
)}
|
||||
<br />
|
||||
{_.keyValue(
|
||||
_('operationSpeed'),
|
||||
formatSpeed(
|
||||
operationLog.result.size,
|
||||
operationLog.end -
|
||||
operationLog.start
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Copiable>
|
||||
) : (
|
||||
operationLog.result.size > 0 && (
|
||||
<div>
|
||||
{_.keyValue(
|
||||
_('operationSize'),
|
||||
formatSize(operationLog.result.size)
|
||||
)}
|
||||
<br />
|
||||
{_.keyValue(
|
||||
_('operationSpeed'),
|
||||
formatSpeed(
|
||||
operationLog.result.size,
|
||||
operationLog.end -
|
||||
operationLog.start
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
@@ -313,22 +301,12 @@ export default [
|
||||
)}
|
||||
<br />
|
||||
{subTaskLog.status === 'failure' &&
|
||||
subTaskLog.result !== undefined && (
|
||||
<Copiable
|
||||
tagName='p'
|
||||
data={JSON.stringify(
|
||||
subTaskLog.result,
|
||||
null,
|
||||
2
|
||||
)}
|
||||
>
|
||||
{_.keyValue(
|
||||
_('taskError'),
|
||||
<span className='text-danger'>
|
||||
{subTaskLog.result.message}
|
||||
</span>
|
||||
)}
|
||||
</Copiable>
|
||||
subTaskLog.result !== undefined &&
|
||||
_.keyValue(
|
||||
_('taskError'),
|
||||
<span className='text-danger'>
|
||||
{subTaskLog.result.message}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -362,25 +340,20 @@ export default [
|
||||
</a>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Copiable
|
||||
tagName='p'
|
||||
data={JSON.stringify(taskLog.result, null, 2)}
|
||||
>
|
||||
{_.keyValue(
|
||||
taskLog.status === 'skipped'
|
||||
? _('taskReason')
|
||||
: _('taskError'),
|
||||
<span
|
||||
className={
|
||||
taskLog.status === 'skipped'
|
||||
? 'text-info'
|
||||
: 'text-danger'
|
||||
}
|
||||
>
|
||||
{taskLog.result.message}
|
||||
</span>
|
||||
)}
|
||||
</Copiable>
|
||||
_.keyValue(
|
||||
taskLog.status === 'skipped'
|
||||
? _('taskReason')
|
||||
: _('taskError'),
|
||||
<span
|
||||
className={
|
||||
taskLog.status === 'skipped'
|
||||
? 'text-info'
|
||||
: 'text-danger'
|
||||
}
|
||||
>
|
||||
{taskLog.result.message}
|
||||
</span>
|
||||
)
|
||||
)
|
||||
) : (
|
||||
<div>
|
||||
|
||||
@@ -232,6 +232,11 @@ export default class Menu extends Component {
|
||||
icon: 'menu-backup-file-restore',
|
||||
label: 'backupFileRestorePage',
|
||||
},
|
||||
{
|
||||
to: '/backup-ng/health',
|
||||
icon: 'menu-dashboard-health',
|
||||
label: 'overviewHealthDashboardPage',
|
||||
},
|
||||
],
|
||||
},
|
||||
isAdmin && {
|
||||
@@ -285,6 +290,11 @@ export default class Menu extends Component {
|
||||
label: 'settingsLogsPage',
|
||||
},
|
||||
{ to: '/settings/ips', icon: 'ip', label: 'settingsIpsPage' },
|
||||
{
|
||||
to: '/settings/cloud-configs',
|
||||
icon: 'template',
|
||||
label: 'settingsCloudConfigsPage',
|
||||
},
|
||||
{
|
||||
to: '/settings/config',
|
||||
icon: 'menu-settings-config',
|
||||
|
||||
@@ -52,18 +52,7 @@
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.configDrive {
|
||||
display: flex;
|
||||
background-color: #eee;
|
||||
padding: 1em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.configDriveToggle {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.refreshNames, .availableTemplateVars {
|
||||
.refreshNames {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,10 @@ import store from 'store'
|
||||
import Tags from 'tags'
|
||||
import Tooltip from 'tooltip'
|
||||
import Wizard, { Section } from 'wizard'
|
||||
import { alert } from 'modal'
|
||||
import {
|
||||
AvailableTemplateVars,
|
||||
DEFAULT_CLOUD_CONFIG_TEMPLATE,
|
||||
} from 'cloud-config'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { injectIntl } from 'react-intl'
|
||||
import {
|
||||
@@ -43,12 +46,12 @@ import {
|
||||
getCloudInitConfig,
|
||||
subscribeCurrentUser,
|
||||
subscribeIpPools,
|
||||
subscribePermissions,
|
||||
subscribeResourceSets,
|
||||
XEN_DEFAULT_CPU_CAP,
|
||||
XEN_DEFAULT_CPU_WEIGHT,
|
||||
} from 'xo'
|
||||
import {
|
||||
SelectCloudConfig,
|
||||
SelectHost,
|
||||
SelectIp,
|
||||
SelectNetwork,
|
||||
@@ -90,32 +93,6 @@ import styles from './index.css'
|
||||
const NB_VMS_MIN = 2
|
||||
const NB_VMS_MAX = 100
|
||||
|
||||
const AVAILABLE_TEMPLATE_VARS = {
|
||||
'{name}': 'templateNameInfo',
|
||||
'%': 'templateIndexInfo',
|
||||
}
|
||||
|
||||
const showAvailableTemplateVars = () =>
|
||||
alert(
|
||||
_('availableTemplateVarsTitle'),
|
||||
<ul>
|
||||
{map(AVAILABLE_TEMPLATE_VARS, (value, key) => (
|
||||
<li key={key}>{_.keyValue(key, _(value))}</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
|
||||
const AvailableTemplateVarsInfo = () => (
|
||||
<Tooltip content={_('availableTemplateVarsInfo')}>
|
||||
<a
|
||||
className={classNames('text-info', styles.availableTemplateVars)}
|
||||
onClick={showAvailableTemplateVars}
|
||||
>
|
||||
<Icon icon='info' />
|
||||
</a>
|
||||
</Tooltip>
|
||||
)
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
const getObject = createGetObject((_, id) => id)
|
||||
@@ -235,7 +212,6 @@ class Vif extends BaseComponent {
|
||||
|
||||
@addSubscriptions({
|
||||
resourceSets: subscribeResourceSets,
|
||||
permissions: subscribePermissions,
|
||||
user: subscribeCurrentUser,
|
||||
})
|
||||
@connectStore(() => ({
|
||||
@@ -315,12 +291,12 @@ export default class NewVm extends BaseComponent {
|
||||
_reset = () => {
|
||||
this._replaceState({
|
||||
bootAfterCreate: true,
|
||||
configDrive: false,
|
||||
CPUs: '',
|
||||
cpuCap: '',
|
||||
cpuWeight: '',
|
||||
existingDisks: {},
|
||||
fastClone: true,
|
||||
installMethod: 'noConfigDrive',
|
||||
multipleVms: false,
|
||||
name_label: '',
|
||||
name_description: '',
|
||||
@@ -364,7 +340,7 @@ export default class NewVm extends BaseComponent {
|
||||
|
||||
let cloudConfig
|
||||
let cloudConfigs
|
||||
if (state.configDrive) {
|
||||
if (state.installMethod !== 'noConfigDrive') {
|
||||
const hostname = state.name_label
|
||||
.replace(/^\s+|\s+$/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
@@ -378,7 +354,9 @@ export default class NewVm extends BaseComponent {
|
||||
''
|
||||
)}`
|
||||
} else {
|
||||
const replacer = this._buildTemplate(state.customConfig)
|
||||
const replacer = this._buildTemplate(
|
||||
defined(state.customConfig, DEFAULT_CLOUD_CONFIG_TEMPLATE)
|
||||
)
|
||||
cloudConfig = replacer(this.state.state, 0)
|
||||
if (state.multipleVms) {
|
||||
const seqStart = state.seqStart
|
||||
@@ -521,10 +499,8 @@ export default class NewVm extends BaseComponent {
|
||||
// installation
|
||||
installMethod:
|
||||
(template.install_methods != null && template.install_methods[0]) ||
|
||||
'SSH',
|
||||
'noConfigDrive',
|
||||
sshKeys: this.props.userSshKeys && this.props.userSshKeys.length && [0],
|
||||
customConfig:
|
||||
'#cloud-config\n#hostname: {name}%\n#ssh_authorized_keys:\n# - ssh-rsa <myKey>\n#packages:\n# - htop\n',
|
||||
// interfaces
|
||||
VIFs,
|
||||
// disks
|
||||
@@ -1019,6 +995,12 @@ export default class NewVm extends BaseComponent {
|
||||
|
||||
// INSTALL SETTINGS ------------------------------------------------------------
|
||||
|
||||
_onChangeCloudConfig = cloudConfig => {
|
||||
this._setState({
|
||||
customConfig: get(() => cloudConfig.template),
|
||||
})
|
||||
}
|
||||
|
||||
_renderInstallSettings = () => {
|
||||
const { template } = this.state.state
|
||||
if (!template) {
|
||||
@@ -1026,7 +1008,6 @@ export default class NewVm extends BaseComponent {
|
||||
}
|
||||
const {
|
||||
cloudConfig,
|
||||
configDrive,
|
||||
customConfig,
|
||||
installIso,
|
||||
installMethod,
|
||||
@@ -1045,36 +1026,36 @@ export default class NewVm extends BaseComponent {
|
||||
{this._isDiskTemplate ? (
|
||||
<SectionContent key='diskTemplate' column>
|
||||
<LineItem>
|
||||
<div className={styles.configDrive}>
|
||||
<span className={styles.configDriveToggle}>
|
||||
{_('newVmConfigDrive')}
|
||||
</span>
|
||||
<label>
|
||||
<input
|
||||
checked={installMethod === 'noConfigDrive'}
|
||||
name='installMethod'
|
||||
onChange={this._linkState('installMethod')}
|
||||
type='radio'
|
||||
value='noConfigDrive'
|
||||
/>
|
||||
|
||||
<span className={styles.configDriveToggle}>
|
||||
<Toggle
|
||||
value={configDrive}
|
||||
onChange={this._linkState('configDrive')}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
{_('noConfigDrive')}
|
||||
</label>
|
||||
</LineItem>
|
||||
<br />
|
||||
<LineItem>
|
||||
<span>
|
||||
<label>
|
||||
<input
|
||||
checked={installMethod === 'SSH'}
|
||||
disabled={!configDrive}
|
||||
name='installMethod'
|
||||
onChange={this._linkState('installMethod')}
|
||||
type='radio'
|
||||
value='SSH'
|
||||
/>{' '}
|
||||
<span>{_('newVmSshKey')}</span>
|
||||
</span>
|
||||
/>
|
||||
|
||||
{_('newVmSshKey')}
|
||||
</label>
|
||||
|
||||
<span className={classNames('input-group', styles.fixedWidth)}>
|
||||
<DebounceInput
|
||||
className='form-control'
|
||||
disabled={!configDrive || installMethod !== 'SSH'}
|
||||
disabled={installMethod !== 'SSH'}
|
||||
onChange={this._linkState('newSshKey')}
|
||||
value={newSshKey}
|
||||
/>
|
||||
@@ -1088,7 +1069,7 @@ export default class NewVm extends BaseComponent {
|
||||
this.props.userSshKeys.length > 0 && (
|
||||
<span className={styles.fixedWidth}>
|
||||
<SelectSshKey
|
||||
disabled={!configDrive || installMethod !== 'SSH'}
|
||||
disabled={installMethod !== 'SSH'}
|
||||
onChange={this._onChangeSshKeys}
|
||||
multi
|
||||
value={sshKeys || []}
|
||||
@@ -1096,29 +1077,37 @@ export default class NewVm extends BaseComponent {
|
||||
</span>
|
||||
)}
|
||||
</LineItem>
|
||||
<br />
|
||||
<LineItem>
|
||||
<input
|
||||
checked={installMethod === 'customConfig'}
|
||||
disabled={!configDrive}
|
||||
name='installMethod'
|
||||
onChange={this._linkState('installMethod')}
|
||||
type='radio'
|
||||
value='customConfig'
|
||||
/>
|
||||
|
||||
<span>
|
||||
{_('newVmCustomConfig')}
|
||||
<label>
|
||||
<input
|
||||
checked={installMethod === 'customConfig'}
|
||||
name='installMethod'
|
||||
onChange={this._linkState('installMethod')}
|
||||
type='radio'
|
||||
value='customConfig'
|
||||
/>
|
||||
|
||||
<AvailableTemplateVarsInfo />
|
||||
</span>
|
||||
{_('newVmCustomConfig')}
|
||||
</label>
|
||||
|
||||
<DebounceTextarea
|
||||
className={classNames('form-control', styles.customConfig)}
|
||||
disabled={!configDrive || installMethod !== 'customConfig'}
|
||||
onChange={this._linkState('customConfig')}
|
||||
value={customConfig}
|
||||
/>
|
||||
<AvailableTemplateVars />
|
||||
|
||||
<span className={styles.inlineSelect}>
|
||||
<SelectCloudConfig
|
||||
disabled={installMethod !== 'customConfig'}
|
||||
onChange={this._onChangeCloudConfig}
|
||||
/>
|
||||
</span>
|
||||
</LineItem>
|
||||
<br />
|
||||
<DebounceTextarea
|
||||
className='form-control'
|
||||
disabled={installMethod !== 'customConfig'}
|
||||
onChange={this._linkState('customConfig')}
|
||||
rows={7}
|
||||
value={defined(customConfig, DEFAULT_CLOUD_CONFIG_TEMPLATE)}
|
||||
/>
|
||||
</SectionContent>
|
||||
) : (
|
||||
<SectionContent>
|
||||
@@ -1214,7 +1203,6 @@ export default class NewVm extends BaseComponent {
|
||||
}
|
||||
_isInstallSettingsDone = () => {
|
||||
const {
|
||||
configDrive,
|
||||
customConfig,
|
||||
installIso,
|
||||
installMethod,
|
||||
@@ -1224,7 +1212,11 @@ export default class NewVm extends BaseComponent {
|
||||
} = this.state.state
|
||||
switch (installMethod) {
|
||||
case 'customConfig':
|
||||
return customConfig || !configDrive
|
||||
return (
|
||||
customConfig === undefined ||
|
||||
customConfig.trim() !== '' ||
|
||||
installMethod === 'noConfigDrive'
|
||||
)
|
||||
case 'ISO':
|
||||
return installIso
|
||||
case 'network':
|
||||
@@ -1232,9 +1224,11 @@ export default class NewVm extends BaseComponent {
|
||||
case 'PXE':
|
||||
return true
|
||||
case 'SSH':
|
||||
return !isEmpty(sshKeys) || !configDrive
|
||||
return !isEmpty(sshKeys) || installMethod === 'noConfigDrive'
|
||||
default:
|
||||
return template && this._isDiskTemplate && !configDrive
|
||||
return (
|
||||
template && this._isDiskTemplate && installMethod === 'noConfigDrive'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1285,7 +1279,7 @@ export default class NewVm extends BaseComponent {
|
||||
|
||||
_renderDisks = () => {
|
||||
const {
|
||||
state: { configDrive, existingDisks, VDIs },
|
||||
state: { installMethod, existingDisks, VDIs },
|
||||
} = this.state
|
||||
const { pool } = this.props
|
||||
let i = 0
|
||||
@@ -1348,7 +1342,7 @@ export default class NewVm extends BaseComponent {
|
||||
<SizeInput
|
||||
className={styles.sizeInput}
|
||||
onChange={this._linkState(`existingDisks.${index}.size`)}
|
||||
readOnly={!configDrive}
|
||||
readOnly={installMethod === 'noConfigDrive'}
|
||||
value={defined(disk.size, null)}
|
||||
/>
|
||||
</Item>
|
||||
@@ -1569,7 +1563,7 @@ export default class NewVm extends BaseComponent {
|
||||
value={namePattern}
|
||||
/>
|
||||
|
||||
<AvailableTemplateVarsInfo />
|
||||
<AvailableTemplateVars />
|
||||
</Item>
|
||||
<Item label={_('newVmFirstIndex')}>
|
||||
<DebounceInput
|
||||
|
||||
@@ -617,7 +617,7 @@ class ResourceSet extends Component {
|
||||
const available = ipPoolLimits && ipPoolLimits.available
|
||||
const total = ipPoolLimits && ipPoolLimits.total
|
||||
return (
|
||||
<span className='mr-1'>
|
||||
<span className='mr-1' key={pool}>
|
||||
{renderXoItem({
|
||||
name: resolvedIpPool && resolvedIpPool.name,
|
||||
type: 'ipPool',
|
||||
@@ -743,12 +743,12 @@ export default class Self extends Component {
|
||||
? isEmpty(resourceSets)
|
||||
? _('noResourceSets')
|
||||
: map(resourceSets, resourceSet => (
|
||||
<ResourceSet
|
||||
autoExpand={location.query.resourceSet === resourceSet.id}
|
||||
key={resourceSet.id}
|
||||
resourceSet={resourceSet}
|
||||
/>
|
||||
))
|
||||
<ResourceSet
|
||||
autoExpand={location.query.resourceSet === resourceSet.id}
|
||||
key={resourceSet.id}
|
||||
resourceSet={resourceSet}
|
||||
/>
|
||||
))
|
||||
: _('loadingResourceSets')}
|
||||
</div>
|
||||
) : (
|
||||
|
||||
192
packages/xo-web/src/xo-app/settings/cloud-configs/index.js
Normal file
192
packages/xo-web/src/xo-app/settings/cloud-configs/index.js
Normal file
@@ -0,0 +1,192 @@
|
||||
import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import defined from 'xo-defined'
|
||||
import React from 'react'
|
||||
import SortedTable from 'sorted-table'
|
||||
import { addSubscriptions, generateRandomId } from 'utils'
|
||||
import {
|
||||
AvailableTemplateVars,
|
||||
DEFAULT_CLOUD_CONFIG_TEMPLATE,
|
||||
} from 'cloud-config'
|
||||
import { Container, Col } from 'grid'
|
||||
import { find } from 'lodash'
|
||||
import { injectState, provideState } from '@julien-f/freactal'
|
||||
import { Text } from 'editable'
|
||||
import { Textarea as DebounceTextarea } from 'debounce-input-decorator'
|
||||
import {
|
||||
createCloudConfig,
|
||||
deleteCloudConfigs,
|
||||
editCloudConfig,
|
||||
subscribeCloudConfigs,
|
||||
} from 'xo'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const COLUMNS = [
|
||||
{
|
||||
itemRenderer: _ => _.id.slice(4, 8),
|
||||
name: _('formId'),
|
||||
sortCriteria: _ => _.id.slice(4, 8),
|
||||
},
|
||||
{
|
||||
itemRenderer: ({ id, name }) => (
|
||||
<Text value={name} onChange={name => editCloudConfig(id, { name })} />
|
||||
),
|
||||
sortCriteria: 'name',
|
||||
name: _('formName'),
|
||||
default: true,
|
||||
},
|
||||
]
|
||||
|
||||
const ACTIONS = [
|
||||
{
|
||||
handler: deleteCloudConfigs,
|
||||
icon: 'delete',
|
||||
individualLabel: _('deleteCloudConfig'),
|
||||
label: _('deleteSelectedCloudConfigs'),
|
||||
level: 'danger',
|
||||
},
|
||||
]
|
||||
|
||||
const INDIVIDUAL_ACTIONS = [
|
||||
{
|
||||
handler: (cloudConfig, { populateForm }) => populateForm(cloudConfig),
|
||||
icon: 'edit',
|
||||
label: _('editCloudConfig'),
|
||||
level: 'primary',
|
||||
},
|
||||
]
|
||||
|
||||
const initialParams = {
|
||||
cloudConfigToEditId: undefined,
|
||||
name: '',
|
||||
template: undefined,
|
||||
}
|
||||
|
||||
export default [
|
||||
addSubscriptions({
|
||||
cloudConfigs: subscribeCloudConfigs,
|
||||
}),
|
||||
provideState({
|
||||
initialState: () => ({
|
||||
formId: generateRandomId(),
|
||||
inputNameId: generateRandomId(),
|
||||
inputTemplateId: generateRandomId(),
|
||||
...initialParams,
|
||||
}),
|
||||
effects: {
|
||||
setInputValue: (_, { target: { name, value } }) => state => ({
|
||||
...state,
|
||||
[name]: value,
|
||||
}),
|
||||
reset: () => state => ({
|
||||
...state,
|
||||
...initialParams,
|
||||
}),
|
||||
createCloudConfig: ({ reset }) => async ({
|
||||
name,
|
||||
template = DEFAULT_CLOUD_CONFIG_TEMPLATE,
|
||||
}) => {
|
||||
await createCloudConfig({ name, template })
|
||||
reset()
|
||||
},
|
||||
editCloudConfig: ({ reset }) => async (
|
||||
{ name, template, cloudConfigToEditId },
|
||||
{ cloudConfigs }
|
||||
) => {
|
||||
const oldCloudConfig = find(cloudConfigs, { id: cloudConfigToEditId })
|
||||
if (
|
||||
oldCloudConfig.name !== name ||
|
||||
oldCloudConfig.template !== template
|
||||
) {
|
||||
await editCloudConfig(cloudConfigToEditId, { name, template })
|
||||
}
|
||||
reset()
|
||||
},
|
||||
populateForm: (_, { id, name, template }) => state => ({
|
||||
...state,
|
||||
name,
|
||||
cloudConfigToEditId: id,
|
||||
template,
|
||||
}),
|
||||
},
|
||||
computed: {
|
||||
isInvalid: ({ name, template }) =>
|
||||
name.trim() === '' ||
|
||||
(template !== undefined && template.trim() === ''),
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
({ state, effects, cloudConfigs }) => (
|
||||
<Container>
|
||||
<Col mediumSize={6}>
|
||||
<form id={state.formId}>
|
||||
<div className='form-group'>
|
||||
<label htmlFor={state.inputNameId}>
|
||||
<strong>{_('formName')}</strong>{' '}
|
||||
</label>
|
||||
<input
|
||||
className='form-control'
|
||||
id={state.inputNameId}
|
||||
name='name'
|
||||
onChange={effects.setInputValue}
|
||||
type='text'
|
||||
value={state.name}
|
||||
/>
|
||||
</div>{' '}
|
||||
<div className='form-group'>
|
||||
<label htmlFor={state.inputTemplateId}>
|
||||
<strong>{_('settingsCloudConfigTemplate')}</strong>{' '}
|
||||
</label>{' '}
|
||||
<AvailableTemplateVars />
|
||||
<DebounceTextarea
|
||||
className='form-control'
|
||||
id={state.inputTemplateId}
|
||||
name='template'
|
||||
onChange={effects.setInputValue}
|
||||
rows={12}
|
||||
value={defined(state.template, DEFAULT_CLOUD_CONFIG_TEMPLATE)}
|
||||
/>
|
||||
</div>{' '}
|
||||
{state.cloudConfigToEditId !== undefined ? (
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
disabled={state.isInvalid}
|
||||
form={state.formId}
|
||||
handler={effects.editCloudConfig}
|
||||
icon='edit'
|
||||
>
|
||||
{_('formEdit')}
|
||||
</ActionButton>
|
||||
) : (
|
||||
<ActionButton
|
||||
btnStyle='success'
|
||||
disabled={state.isInvalid}
|
||||
form={state.formId}
|
||||
handler={effects.createCloudConfig}
|
||||
icon='add'
|
||||
>
|
||||
{_('formCreate')}
|
||||
</ActionButton>
|
||||
)}
|
||||
<ActionButton
|
||||
className='pull-right'
|
||||
handler={effects.reset}
|
||||
icon='cancel'
|
||||
>
|
||||
{_('formCancel')}
|
||||
</ActionButton>
|
||||
</form>
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<SortedTable
|
||||
actions={ACTIONS}
|
||||
collection={cloudConfigs}
|
||||
columns={COLUMNS}
|
||||
data-populateForm={effects.populateForm}
|
||||
individualActions={INDIVIDUAL_ACTIONS}
|
||||
/>
|
||||
</Col>
|
||||
</Container>
|
||||
),
|
||||
].reduceRight((value, decorator) => decorator(value))
|
||||
@@ -7,6 +7,7 @@ import { Container, Row, Col } from 'grid'
|
||||
import { NavLink, NavTabs } from 'nav'
|
||||
|
||||
import Acls from './acls'
|
||||
import CloudConfigs from './cloud-configs'
|
||||
import Config from './config'
|
||||
import Groups from './groups'
|
||||
import Ips from './ips'
|
||||
@@ -50,6 +51,9 @@ const HEADER = (
|
||||
<NavLink to='/settings/ips'>
|
||||
<Icon icon='ip' /> {_('settingsIpsPage')}
|
||||
</NavLink>
|
||||
<NavLink to='/settings/cloud-configs'>
|
||||
<Icon icon='template' /> {_('settingsCloudConfigsPage')}
|
||||
</NavLink>
|
||||
<NavLink to='/settings/config'>
|
||||
<Icon icon='menu-settings-config' /> {_('settingsConfigPage')}
|
||||
</NavLink>
|
||||
@@ -61,6 +65,7 @@ const HEADER = (
|
||||
|
||||
const Settings = routes('servers', {
|
||||
acls: Acls,
|
||||
'cloud-configs': CloudConfigs,
|
||||
config: Config,
|
||||
groups: Groups,
|
||||
ips: Ips,
|
||||
|
||||
@@ -8,6 +8,7 @@ import BaseComponent from 'base-component'
|
||||
import ButtonGroup from 'button-group'
|
||||
import Copiable from 'copiable'
|
||||
import NoObjects from 'no-objects'
|
||||
import ReportBugButton, { CAN_REPORT_BUG } from 'report-bug-button'
|
||||
import SortedTable from 'sorted-table'
|
||||
import styles from './index.css'
|
||||
import TabButton from 'tab-button'
|
||||
@@ -16,27 +17,12 @@ import { alert, confirm } from 'modal'
|
||||
import { createSelector } from 'selectors'
|
||||
import { subscribeApiLogs, subscribeUsers, deleteApiLog } from 'xo'
|
||||
|
||||
const CAN_REPORT_BUG = process.env.XOA_PLAN > 1
|
||||
|
||||
const reportBug = log => {
|
||||
const title = encodeURIComponent(`Error on ${log.data.method}`)
|
||||
const message = encodeURIComponent(
|
||||
`\`\`\`\n${log.data.method}\n${JSON.stringify(
|
||||
log.data.params,
|
||||
null,
|
||||
2
|
||||
)}\n${JSON.stringify(log.data.error, null, 2).replace(
|
||||
/\\n/g,
|
||||
'\n'
|
||||
)}\n\`\`\``
|
||||
)
|
||||
|
||||
window.open(
|
||||
process.env.XOA_PLAN < 5
|
||||
? `https://xen-orchestra.com/#!/member/support?title=${title}&message=${message}`
|
||||
: `https://github.com/vatesfr/xen-orchestra/issues/new?title=${title}&body=${message}`
|
||||
)
|
||||
}
|
||||
const formatMessage = data =>
|
||||
`\`\`\`\n${data.method}\n${JSON.stringify(
|
||||
data.params,
|
||||
null,
|
||||
2
|
||||
)}\n${JSON.stringify(data.error, null, 2).replace(/\\n/g, '\n')}\n\`\`\``
|
||||
|
||||
const COLUMNS = [
|
||||
{
|
||||
@@ -102,10 +88,11 @@ const COLUMNS = [
|
||||
tooltip={_('logDelete')}
|
||||
/>
|
||||
{CAN_REPORT_BUG && (
|
||||
<ActionRowButton
|
||||
handler={() => reportBug(log)}
|
||||
icon='bug'
|
||||
tooltip={_('reportBug')}
|
||||
<ReportBugButton
|
||||
message={log.data}
|
||||
formatMessage={formatMessage}
|
||||
rowButton
|
||||
title={`Error on ${log.data.method}`}
|
||||
/>
|
||||
)}
|
||||
</ButtonGroup>
|
||||
|
||||
@@ -216,26 +216,26 @@ const INDIVIDUAL_ACTIONS = [
|
||||
answer =>
|
||||
answer.success
|
||||
? alert(
|
||||
<span>
|
||||
<Icon icon='success' />{' '}
|
||||
{_('remoteTestSuccess', { name: remote.name })}
|
||||
</span>,
|
||||
_('remoteTestSuccessMessage')
|
||||
)
|
||||
<span>
|
||||
<Icon icon='success' />{' '}
|
||||
{_('remoteTestSuccess', { name: remote.name })}
|
||||
</span>,
|
||||
_('remoteTestSuccessMessage')
|
||||
)
|
||||
: alert(
|
||||
<span>
|
||||
<Icon icon='error' />{' '}
|
||||
{_('remoteTestFailure', { name: remote.name })}
|
||||
</span>,
|
||||
<p>
|
||||
<dl className='dl-horizontal'>
|
||||
<dt>{_('remoteTestError')}</dt>
|
||||
<dd>{answer.error}</dd>
|
||||
<dt>{_('remoteTestStep')}</dt>
|
||||
<dd>{answer.step}</dd>
|
||||
</dl>
|
||||
</p>
|
||||
)
|
||||
<span>
|
||||
<Icon icon='error' />{' '}
|
||||
{_('remoteTestFailure', { name: remote.name })}
|
||||
</span>,
|
||||
<p>
|
||||
<dl className='dl-horizontal'>
|
||||
<dt>{_('remoteTestError')}</dt>
|
||||
<dd>{answer.error}</dd>
|
||||
<dt>{_('remoteTestStep')}</dt>
|
||||
<dd>{answer.step}</dd>
|
||||
</dl>
|
||||
</p>
|
||||
)
|
||||
),
|
||||
icon: 'diagnosis',
|
||||
label: _('remoteTestTip'),
|
||||
@@ -272,7 +272,7 @@ export default class Remotes extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
type: 'file',
|
||||
type: 'nfs',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,11 +283,11 @@ export default class Remotes extends Component {
|
||||
some(values, ['name', this.refs.name.value])
|
||||
)
|
||||
? alert(
|
||||
<span>
|
||||
<Icon icon='error' /> {_('remoteTestName')}
|
||||
</span>,
|
||||
<p>{_('remoteTestNameFailure')}</p>
|
||||
)
|
||||
<span>
|
||||
<Icon icon='error' /> {_('remoteTestName')}
|
||||
</span>,
|
||||
<p>{_('remoteTestNameFailure')}</p>
|
||||
)
|
||||
: this._createRemote()
|
||||
|
||||
_createRemote = async () => {
|
||||
@@ -306,7 +306,7 @@ export default class Remotes extends Component {
|
||||
const url = format(urlParams)
|
||||
return createRemote(name && name.value, url).then(
|
||||
() => {
|
||||
this.setState({ type: 'file' })
|
||||
this.setState({ type: 'nfs' })
|
||||
path && (path.value = '')
|
||||
username && (username.value = '')
|
||||
password && (password.value = '')
|
||||
|
||||
@@ -10,7 +10,7 @@ import { SelectPool } from 'select-objects'
|
||||
import { connectStore, resolveIds } from 'utils'
|
||||
import { Card, CardBlock, CardHeader } from 'card'
|
||||
import { Col, Container, Row } from 'grid'
|
||||
import { flatMap, flatten, isEmpty, keys, toArray } from 'lodash'
|
||||
import { flatMap, flatten, isEmpty, keys, some, toArray } from 'lodash'
|
||||
import {
|
||||
createGetObject,
|
||||
createGetObjectsOfType,
|
||||
@@ -94,14 +94,19 @@ const COLUMNS = [
|
||||
},
|
||||
]
|
||||
|
||||
const isNotCancelable = task => !task.allowedOperations.includes('cancel')
|
||||
const isNotDestroyable = task => !task.allowedOperations.includes('destroy')
|
||||
|
||||
const INDIVIDUAL_ACTIONS = [
|
||||
{
|
||||
disabled: isNotCancelable,
|
||||
handler: cancelTask,
|
||||
icon: 'task-cancel',
|
||||
label: _('cancelTask'),
|
||||
level: 'danger',
|
||||
},
|
||||
{
|
||||
disabled: isNotDestroyable,
|
||||
handler: destroyTask,
|
||||
icon: 'task-destroy',
|
||||
label: _('destroyTask'),
|
||||
@@ -111,12 +116,14 @@ const INDIVIDUAL_ACTIONS = [
|
||||
|
||||
const GROUPED_ACTIONS = [
|
||||
{
|
||||
disabled: tasks => some(tasks, isNotCancelable),
|
||||
handler: cancelTasks,
|
||||
icon: 'task-cancel',
|
||||
label: _('cancelTasks'),
|
||||
level: 'danger',
|
||||
},
|
||||
{
|
||||
disabled: tasks => some(tasks, isNotDestroyable),
|
||||
handler: destroyTasks,
|
||||
icon: 'task-destroy',
|
||||
label: _('destroyTasks'),
|
||||
|
||||
@@ -83,7 +83,7 @@ const GROUPED_ACTIONS = [
|
||||
|
||||
const INDIVIDUAL_ACTIONS = [
|
||||
{
|
||||
handler: copyVm,
|
||||
handler: snapshot => copyVm(snapshot),
|
||||
icon: 'vm-copy',
|
||||
label: _('copySnapshot'),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user