Compare commits
149 Commits
v5.x.all.i
...
v4.11.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f6ee34592 | ||
|
|
3ae58a323e | ||
|
|
26b958c270 | ||
|
|
12a4af5900 | ||
|
|
69479d538c | ||
|
|
829397dd5a | ||
|
|
2bc89026db | ||
|
|
ebbc44d181 | ||
|
|
2228a1e36b | ||
|
|
a8cbf3e8ff | ||
|
|
fa32e3d734 | ||
|
|
0d17148ff0 | ||
|
|
aa38411cf7 | ||
|
|
4913c8699d | ||
|
|
1035a11487 | ||
|
|
15c2efe706 | ||
|
|
d7fd71bb62 | ||
|
|
b11ee993fa | ||
|
|
614aa7873c | ||
|
|
1adf31fe15 | ||
|
|
824ffd7b5b | ||
|
|
c31c6fdebb | ||
|
|
83f3276429 | ||
|
|
d21f68ce54 | ||
|
|
18b1e1b133 | ||
|
|
0edaa40052 | ||
|
|
627077c8f3 | ||
|
|
a897b1798d | ||
|
|
50e39993bf | ||
|
|
5e397dd01e | ||
|
|
f57ff5d5e0 | ||
|
|
5c3e40917c | ||
|
|
90a2dc4581 | ||
|
|
b64243fdd6 | ||
|
|
42db87d305 | ||
|
|
e7ab1b589a | ||
|
|
e9979c9887 | ||
|
|
3bb9bb56f0 | ||
|
|
5a99474c55 | ||
|
|
182ee6c25f | ||
|
|
4d3f0a06db | ||
|
|
0e182c519b | ||
|
|
b1ee30ce7d | ||
|
|
93ba764e23 | ||
|
|
433e17bb81 | ||
|
|
61c09083ad | ||
|
|
018377e724 | ||
|
|
b76f9513ba | ||
|
|
40ebb7ba75 | ||
|
|
a9e52e8954 | ||
|
|
3c8876cac7 | ||
|
|
b7e005f9c7 | ||
|
|
e6fe0a19fa | ||
|
|
fba11b6a44 | ||
|
|
c270e7f5dd | ||
|
|
9ee00d345e | ||
|
|
0379fbc4eb | ||
|
|
9748a3ae91 | ||
|
|
1881944748 | ||
|
|
3721fa194c | ||
|
|
8c3fcad20b | ||
|
|
decf373d0b | ||
|
|
ff1d50f993 | ||
|
|
ef34204b59 | ||
|
|
270b636d80 | ||
|
|
ac01da2ae9 | ||
|
|
0136310c54 | ||
|
|
ecf4cf852e | ||
|
|
c66384adfb | ||
|
|
98bdda629d | ||
|
|
a8286f9cba | ||
|
|
fa3db4fcf6 | ||
|
|
ddac0cfee1 | ||
|
|
9368673459 | ||
|
|
43dc999ab5 | ||
|
|
3b7333e866 | ||
|
|
bc0ddbaf16 | ||
|
|
45f0ae7e1c | ||
|
|
a521c4ae01 | ||
|
|
5b8238adeb | ||
|
|
ec330474fa | ||
|
|
ece28904a8 | ||
|
|
4f1c495afb | ||
|
|
5fdd27b7e6 | ||
|
|
91f449af9a | ||
|
|
efc0a0dfe3 | ||
|
|
fee47baa66 | ||
|
|
0ad7bfc7e7 | ||
|
|
bd64143ae1 | ||
|
|
ec982ba9a3 | ||
|
|
6280f6ff98 | ||
|
|
35d20390a9 | ||
|
|
c487c5042f | ||
|
|
aaf7927aa2 | ||
|
|
3c677f3d21 | ||
|
|
94eb76b3a6 | ||
|
|
a921cb2d0d | ||
|
|
f3aaa363d8 | ||
|
|
45a79e1920 | ||
|
|
6fd9b2a453 | ||
|
|
01d8e89a71 | ||
|
|
c89fa63910 | ||
|
|
9fc5c49dbf | ||
|
|
7dfc269df9 | ||
|
|
76d0b397db | ||
|
|
5413f887af | ||
|
|
b3d0c61f0e | ||
|
|
4ce0441d68 | ||
|
|
72be34e18d | ||
|
|
d2961b7650 | ||
|
|
fdca1bbf72 | ||
|
|
ab7a2f9dee | ||
|
|
7b72857a3b | ||
|
|
4787146658 | ||
|
|
430f9356c3 | ||
|
|
70a3b3518f | ||
|
|
c0944c17e0 | ||
|
|
da1b2a91e7 | ||
|
|
aa27492713 | ||
|
|
afe589dec3 | ||
|
|
978d140c8f | ||
|
|
2ce213b62c | ||
|
|
7748266078 | ||
|
|
83783d07a1 | ||
|
|
49a1f2c7c5 | ||
|
|
ddfc0151fc | ||
|
|
81c508e13c | ||
|
|
7195cfc3cf | ||
|
|
93fe5e2cf7 | ||
|
|
a2bf795d12 | ||
|
|
c8d78f39e0 | ||
|
|
d9ab8a1c8b | ||
|
|
5125ad4889 | ||
|
|
951e85b04b | ||
|
|
711d922695 | ||
|
|
3692ffcde7 | ||
|
|
b049420c59 | ||
|
|
241103c369 | ||
|
|
2128367113 | ||
|
|
f555c8190d | ||
|
|
d5df633def | ||
|
|
fe7dc859e3 | ||
|
|
569c5046c6 | ||
|
|
e0210ae2d8 | ||
|
|
f85dc3b7e7 | ||
|
|
92d4363120 | ||
|
|
6c69220de2 | ||
|
|
3a1229b072 | ||
|
|
45538c9f62 |
@@ -1,8 +1,10 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- 'stable'
|
||||
- '0.12'
|
||||
|
||||
# Use containers.
|
||||
# http://docs.travis-ci.com/user/workers/container-based-infrastructure/
|
||||
sudo: false
|
||||
|
||||
before_install:
|
||||
- npm i -g npm
|
||||
|
||||
98
CHANGELOG.md
98
CHANGELOG.md
@@ -1,5 +1,103 @@
|
||||
# ChangeLog
|
||||
|
||||
## **4.11.0** (2015-12-22)
|
||||
|
||||
Delta backup, CloudInit...
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Visible list of SR inside a VM [\#601](https://github.com/vatesfr/xo-web/issues/601)
|
||||
- VDI move [\#591](https://github.com/vatesfr/xo-web/issues/591)
|
||||
- Edit pre-existing disk configuration during VM creation [\#589](https://github.com/vatesfr/xo-web/issues/589)
|
||||
- Allow disk size edition [\#587](https://github.com/vatesfr/xo-web/issues/587)
|
||||
- Better VDI resize support [\#585](https://github.com/vatesfr/xo-web/issues/585)
|
||||
- Remove manual VM export metadata in UI [\#580](https://github.com/vatesfr/xo-web/issues/580)
|
||||
- Support import VM metadata [\#579](https://github.com/vatesfr/xo-web/issues/579)
|
||||
- Set a default pool SR [\#572](https://github.com/vatesfr/xo-web/issues/572)
|
||||
- ISOs should be sorted by name [\#565](https://github.com/vatesfr/xo-web/issues/565)
|
||||
- Button to boot a VM from a disc once [\#564](https://github.com/vatesfr/xo-web/issues/564)
|
||||
- Ability to boot a PV VM from a disc [\#563](https://github.com/vatesfr/xo-web/issues/563)
|
||||
- Add an option to manually run backup jobs [\#562](https://github.com/vatesfr/xo-web/issues/562)
|
||||
- backups to unmounted storage [\#561](https://github.com/vatesfr/xo-web/issues/561)
|
||||
- Root integer properties cannot be edited in plugins configuration form [\#550](https://github.com/vatesfr/xo-web/issues/550)
|
||||
- Generic CloudConfig drive [\#549](https://github.com/vatesfr/xo-web/issues/549)
|
||||
- Auto-discovery of installed xo-server plugins [\#546](https://github.com/vatesfr/xo-web/issues/546)
|
||||
- Hide info on flat view [\#545](https://github.com/vatesfr/xo-web/issues/545)
|
||||
- Config plugin boolean properties must have a default value \(undefined prohibited\) [\#543](https://github.com/vatesfr/xo-web/issues/543)
|
||||
- Present detailed errors on plugin configuration failures [\#530](https://github.com/vatesfr/xo-web/issues/530)
|
||||
- Do not reset form on failures in plugins configuration [\#529](https://github.com/vatesfr/xo-web/issues/529)
|
||||
- XMPP alert plugin [\#518](https://github.com/vatesfr/xo-web/issues/518)
|
||||
- Hide tag adders depending on ACLs [\#516](https://github.com/vatesfr/xo-web/issues/516)
|
||||
- Choosing a framework for xo-web 5 [\#514](https://github.com/vatesfr/xo-web/issues/514)
|
||||
- Prevent adding a host in an existing XAPI connection [\#466](https://github.com/vatesfr/xo-web/issues/466)
|
||||
- Read only connection to Xen servers/pools [\#439](https://github.com/vatesfr/xo-web/issues/439)
|
||||
- generic notification system [\#391](https://github.com/vatesfr/xo-web/issues/391)
|
||||
- Data architecture review [\#384](https://github.com/vatesfr/xo-web/issues/384)
|
||||
- Make filtering easier to understand/add some "default" filters [\#207](https://github.com/vatesfr/xo-web/issues/207)
|
||||
- Improve performance [\#148](https://github.com/vatesfr/xo-web/issues/148)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- VM metadata export should not require a snapshot [\#615](https://github.com/vatesfr/xo-web/issues/615)
|
||||
- Missing patch for all hosts is continuously refreshed [\#609](https://github.com/vatesfr/xo-web/issues/609)
|
||||
- Backup import memory issue [\#608](https://github.com/vatesfr/xo-web/issues/608)
|
||||
- Host list missing patch is buggy [\#604](https://github.com/vatesfr/xo-web/issues/604)
|
||||
- Servers infos should not been refreshed while a field is being edited [\#595](https://github.com/vatesfr/xo-web/issues/595)
|
||||
- Servers list should not been re-order while a field is being edited [\#594](https://github.com/vatesfr/xo-web/issues/594)
|
||||
- Correctly display size in interface \(binary scale\) [\#592](https://github.com/vatesfr/xo-web/issues/592)
|
||||
- Display failures on VM boot order modification [\#560](https://github.com/vatesfr/xo-web/issues/560)
|
||||
- `vm.setBootOrder\(\)` should throw errors on failures \(non-HVM VMs\) [\#559](https://github.com/vatesfr/xo-web/issues/559)
|
||||
- Hide boot order form for non-HVM VMs [\#558](https://github.com/vatesfr/xo-web/issues/558)
|
||||
- Allow editing PV args even when empty \(but only for PV VMs\) [\#557](https://github.com/vatesfr/xo-web/issues/557)
|
||||
- Crashes when using legacy event system [\#556](https://github.com/vatesfr/xo-web/issues/556)
|
||||
- XenServer patches check error for 6.1 [\#555](https://github.com/vatesfr/xo-web/issues/555)
|
||||
- activation plugin xo-server-transport-email [\#553](https://github.com/vatesfr/xo-web/issues/553)
|
||||
- Server error with JSON on 32 bits Dom0 [\#552](https://github.com/vatesfr/xo-web/issues/552)
|
||||
- Cloud Config drive shouldn't be created on default SR [\#548](https://github.com/vatesfr/xo-web/issues/548)
|
||||
- Deep properties cannot be edited in plugins configuration form [\#521](https://github.com/vatesfr/xo-web/issues/521)
|
||||
- Aborted VM export should cancel the operation [\#490](https://github.com/vatesfr/xo-web/issues/490)
|
||||
- VM missing with same UUID after an inter-pool migration [\#284](https://github.com/vatesfr/xo-web/issues/284)
|
||||
|
||||
## **4.10.0** (2015-11-27)
|
||||
|
||||
Job management, email notifications, CoreOS/Docker, Quiesce snapshots...
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Job management ([xo-web#487](https://github.com/vatesfr/xo-web/issues/487))
|
||||
- Patch upload on all connected servers ([xo-web#168](https://github.com/vatesfr/xo-web/issues/168))
|
||||
- Emergency shutdown ([xo-web#185](https://github.com/vatesfr/xo-web/issues/185))
|
||||
- CoreOS/docker template install ([xo-web#246](https://github.com/vatesfr/xo-web/issues/246))
|
||||
- Email for backups ([xo-web#308](https://github.com/vatesfr/xo-web/issues/308))
|
||||
- Console Clipboard ([xo-web#408](https://github.com/vatesfr/xo-web/issues/408))
|
||||
- Logs from CLI ([xo-web#486](https://github.com/vatesfr/xo-web/issues/486))
|
||||
- Save disconnected servers ([xo-web#489](https://github.com/vatesfr/xo-web/issues/489))
|
||||
- Snapshot with quiesce ([xo-web#491](https://github.com/vatesfr/xo-web/issues/491))
|
||||
- Start VM in reovery mode ([xo-web#495](https://github.com/vatesfr/xo-web/issues/495))
|
||||
- Username in logs ([xo-web#498](https://github.com/vatesfr/xo-web/issues/498))
|
||||
- Delete associated tokens with user ([xo-web#500](https://github.com/vatesfr/xo-web/issues/500))
|
||||
- Validate plugin configuration ([xo-web#503](https://github.com/vatesfr/xo-web/issues/503))
|
||||
- Avoid non configured plugins to be loaded ([xo-web#504](https://github.com/vatesfr/xo-web/issues/504))
|
||||
- Verbose API logs if configured ([xo-web#505](https://github.com/vatesfr/xo-web/issues/505))
|
||||
- Better backup overview ([xo-web#512](https://github.com/vatesfr/xo-web/issues/512))
|
||||
- VM auto power on ([xo-web#519](https://github.com/vatesfr/xo-web/issues/519))
|
||||
- Title property supported in config schema ([xo-web#522](https://github.com/vatesfr/xo-web/issues/522))
|
||||
- Start VM export only when necessary ([xo-web#534](https://github.com/vatesfr/xo-web/issues/534))
|
||||
- Input type should be number ([xo-web#538](https://github.com/vatesfr/xo-web/issues/538))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Numbers/int support in plugins config ([xo-web#531](https://github.com/vatesfr/xo-web/issues/531))
|
||||
- Boolean support in plugins config ([xo-web#528](https://github.com/vatesfr/xo-web/issues/528))
|
||||
- Keyboard unusable outside console ([xo-web#513](https://github.com/vatesfr/xo-web/issues/513))
|
||||
- UsernameField for SAML ([xo-web#513](https://github.com/vatesfr/xo-web/issues/513))
|
||||
- Wrong display of "no plugin found" ([xo-web#508](https://github.com/vatesfr/xo-web/issues/508))
|
||||
- Bower build error ([xo-web#488](https://github.com/vatesfr/xo-web/issues/488))
|
||||
- VM cloning should require SR permission ([xo-web#472](https://github.com/vatesfr/xo-web/issues/472))
|
||||
- Xen tools status ([xo-web#471](https://github.com/vatesfr/xo-web/issues/471))
|
||||
- Can't delete ghost user ([xo-web#464](https://github.com/vatesfr/xo-web/issues/464))
|
||||
- Stats with old versions of Node ([xo-web#463](https://github.com/vatesfr/xo-web/issues/463))
|
||||
|
||||
## **4.9.0** (2015-11-13)
|
||||
|
||||
Automated DR, restore backup, VM copy
|
||||
|
||||
10
README.md
10
README.md
@@ -38,8 +38,8 @@ Otherwise, please consider using the [bugtracker of the general repository](http
|
||||
## Process for new release
|
||||
|
||||
```bash
|
||||
# Switch to the master branch.
|
||||
git checkout master
|
||||
# Switch to the stable branch.
|
||||
git checkout stable
|
||||
|
||||
# Fetches latest changes.
|
||||
git pull --ff-only
|
||||
@@ -53,12 +53,12 @@ npm version minor
|
||||
# Go back to the next-release branch.
|
||||
git checkout next-release
|
||||
|
||||
# Fetches the last changes (the merge and version bump) from master to
|
||||
# Fetches the last changes (the merge and version bump) from stable to
|
||||
# next-release.
|
||||
git merge --ff-only master
|
||||
git merge --ff-only stable
|
||||
|
||||
# Push the changes on git.
|
||||
git push --follow-tags origin master next-release
|
||||
git push --follow-tags origin stable next-release
|
||||
|
||||
# Publish this release to npm.
|
||||
npm publish
|
||||
|
||||
@@ -28,6 +28,7 @@ import newVmState from './modules/new-vm'
|
||||
import poolState from './modules/pool'
|
||||
import settingsState from './modules/settings'
|
||||
import srState from './modules/sr'
|
||||
import taskScheduler from './modules/task-scheduler'
|
||||
import treeState from './modules/tree'
|
||||
import updater from './modules/updater'
|
||||
import vmState from './modules/vm'
|
||||
@@ -63,6 +64,7 @@ export default angular.module('xoWebApp', [
|
||||
poolState,
|
||||
settingsState,
|
||||
srState,
|
||||
taskScheduler,
|
||||
treeState,
|
||||
updater,
|
||||
vmState,
|
||||
|
||||
@@ -29,6 +29,7 @@ export default angular.module('backup.backup', [
|
||||
|
||||
this.ready = false
|
||||
|
||||
this.running = {}
|
||||
this.comesForEditing = $stateParams.id
|
||||
this.scheduleApi = {}
|
||||
this.formData = {}
|
||||
@@ -220,6 +221,16 @@ export default angular.module('backup.backup', [
|
||||
})
|
||||
}
|
||||
|
||||
this.run = schedule => {
|
||||
this.running[schedule.id] = true
|
||||
notify.info({
|
||||
title: 'Run Job',
|
||||
message: 'One shot running started. See overview for logs.'
|
||||
})
|
||||
const id = schedule.job
|
||||
return xo.job.runSequence([id]).finally(() => delete this.running[schedule.id])
|
||||
}
|
||||
|
||||
this.sanitizePath = (...paths) => (paths[0] && paths[0].charAt(0) === '/' && '/' || '') + filter(map(paths, s => s && filter(map(s.split('/'), trim)).join('/'))).join('/')
|
||||
|
||||
this.resetData = () => {
|
||||
|
||||
@@ -108,7 +108,7 @@ form#backupform(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.s
|
||||
th.hidden-xs Remote
|
||||
th.hidden-xs Depth
|
||||
th.hidden-xs Scheduling
|
||||
th.hidden-xs Only MetaData.hidden-xs
|
||||
th.hidden-xs Only MetaData
|
||||
th.hidden-xs Compression DISABLED
|
||||
th Enabled now
|
||||
th
|
||||
@@ -141,4 +141,6 @@ form#backupform(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.s
|
||||
td.text-right
|
||||
button.btn.btn-primary(type = 'button', ng-click = 'ctrl.edit(schedule)'): i.fa.fa-pencil
|
||||
|
|
||||
button.btn.btn-warning(type = 'button', ng-click = 'ctrl.run(schedule)', ng-disabled = 'ctrl.running[schedule.id]'): i.fa.fa-play
|
||||
|
|
||||
button.btn.btn-danger(type = 'button', ng-click = 'ctrl.delete(schedule)'): i.fa.fa-trash-o
|
||||
|
||||
255
app/modules/backup/delta-backup/index.js
Normal file
255
app/modules/backup/delta-backup/index.js
Normal file
@@ -0,0 +1,255 @@
|
||||
import angular from 'angular'
|
||||
import filter from 'lodash.filter'
|
||||
import find from 'lodash.find'
|
||||
import forEach from 'lodash.foreach'
|
||||
import map from 'lodash.map'
|
||||
import prettyCron from 'prettycron'
|
||||
import size from 'lodash.size'
|
||||
import trim from 'lodash.trim'
|
||||
import uiBootstrap from 'angular-ui-bootstrap'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
|
||||
import view from './view'
|
||||
|
||||
// ====================================================================
|
||||
|
||||
export default angular.module('backup.deltaBackup', [
|
||||
uiRouter,
|
||||
uiBootstrap
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('backup.deltaBackup', {
|
||||
url: '/delta-backup/:id',
|
||||
controller: 'DeltaBackupCtrl as ctrl',
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('DeltaBackupCtrl', function ($scope, $stateParams, $interval, xo, xoApi, notify, selectHighLevelFilter, filterFilter) {
|
||||
const JOBKEY = 'deltaBackup'
|
||||
|
||||
this.ready = false
|
||||
|
||||
this.running = {}
|
||||
this.comesForEditing = $stateParams.id
|
||||
this.scheduleApi = {}
|
||||
this.formData = {}
|
||||
|
||||
const refreshRemotes = () => {
|
||||
const selectRemoteId = this.formData.remote && this.formData.remote.id
|
||||
return xo.remote.getAll()
|
||||
.then(remotes => {
|
||||
const r = {}
|
||||
forEach(remotes, remote => {
|
||||
r[remote.id] = remote
|
||||
})
|
||||
this.remotes = r
|
||||
if (selectRemoteId) {
|
||||
this.formData.remote = this.remotes[selectRemoteId]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const refreshSchedules = () => {
|
||||
return xo.schedule.getAll()
|
||||
.then(schedules => {
|
||||
const s = {}
|
||||
forEach(schedules, schedule => {
|
||||
this.jobs && this.jobs[schedule.job] && this.jobs[schedule.job].key === JOBKEY && (s[schedule.id] = schedule)
|
||||
})
|
||||
this.schedules = s
|
||||
})
|
||||
}
|
||||
|
||||
const refreshJobs = () => {
|
||||
return xo.job.getAll()
|
||||
.then(jobs => {
|
||||
const j = {}
|
||||
forEach(jobs, job => {
|
||||
j[job.id] = job
|
||||
})
|
||||
this.jobs = j
|
||||
})
|
||||
}
|
||||
|
||||
const refresh = () => refreshRemotes().then(refreshJobs).then(refreshSchedules)
|
||||
|
||||
this.getReady = () => refresh().then(() => this.ready = true)
|
||||
this.getReady()
|
||||
|
||||
const interval = $interval(refresh, 5e3)
|
||||
$scope.$on('$destroy', () => $interval.cancel(interval))
|
||||
|
||||
const toggleState = (toggle, state) => {
|
||||
const selectedVms = this.formData.selectedVms.slice()
|
||||
if (toggle) {
|
||||
const vms = filterFilter(selectHighLevelFilter(this.objects), {type: 'VM'})
|
||||
forEach(vms, vm => {
|
||||
if (vm.power_state === state) {
|
||||
(selectedVms.indexOf(vm) === -1) && selectedVms.push(vm)
|
||||
}
|
||||
})
|
||||
this.formData.selectedVms = selectedVms
|
||||
} else {
|
||||
const keptVms = []
|
||||
for (let index in this.formData.selectedVms) {
|
||||
if (this.formData.selectedVms[index].power_state !== state) {
|
||||
keptVms.push(this.formData.selectedVms[index])
|
||||
}
|
||||
}
|
||||
this.formData.selectedVms = keptVms
|
||||
}
|
||||
}
|
||||
|
||||
this.toggleAllRunning = toggle => toggleState(toggle, 'Running')
|
||||
this.toggleAllHalted = toggle => toggleState(toggle, 'Halted')
|
||||
|
||||
this.edit = schedule => {
|
||||
const vms = filterFilter(selectHighLevelFilter(this.objects), {type: 'VM'})
|
||||
const job = this.jobs[schedule.job]
|
||||
const selectedVms = []
|
||||
forEach(job.paramsVector.items[0].values, value => {
|
||||
const vm = find(vms, vm => vm.id === value.vm)
|
||||
vm && selectedVms.push(vm)
|
||||
})
|
||||
const tag = job.paramsVector.items[0].values[0].tag
|
||||
const depth = job.paramsVector.items[0].values[0].depth
|
||||
const cronPattern = schedule.cron
|
||||
const remoteId = job.paramsVector.items[0].values[0].remote
|
||||
|
||||
this.resetData()
|
||||
this.formData.selectedVms = selectedVms
|
||||
this.formData.tag = tag
|
||||
this.formData.depth = depth
|
||||
this.formData.scheduleId = schedule.id
|
||||
this.formData.remote = this.remotes[remoteId]
|
||||
this.scheduleApi.setCron(cronPattern)
|
||||
}
|
||||
|
||||
this.save = (id, vms, remoteId, tag, depth, cron, enabled) => {
|
||||
if (!vms.length) {
|
||||
notify.warning({
|
||||
title: 'No Vms selected',
|
||||
message: 'Choose VMs to backup'
|
||||
})
|
||||
return
|
||||
}
|
||||
const _save = (id === undefined) ? saveNew(vms, remoteId, tag, depth, cron, enabled) : save(id, vms, remoteId, tag, depth, cron)
|
||||
return _save
|
||||
.then(() => {
|
||||
notify.info({
|
||||
title: 'Backup',
|
||||
message: 'Job schedule successfuly saved'
|
||||
})
|
||||
this.resetData()
|
||||
})
|
||||
.finally(refresh)
|
||||
}
|
||||
|
||||
const save = (id, vms, remoteId, tag, depth, cron) => {
|
||||
const schedule = this.schedules[id]
|
||||
const job = this.jobs[schedule.job]
|
||||
const values = []
|
||||
forEach(vms, vm => {
|
||||
values.push({
|
||||
vm: vm.id,
|
||||
remote: remoteId,
|
||||
tag,
|
||||
depth
|
||||
})
|
||||
})
|
||||
job.paramsVector.items[0].values = values
|
||||
return xo.job.set(job)
|
||||
.then(response => {
|
||||
if (response) {
|
||||
return xo.schedule.set(schedule.id, undefined, cron, undefined)
|
||||
} else {
|
||||
notify.error({
|
||||
title: 'Update schedule',
|
||||
message: 'Job updating failed'
|
||||
})
|
||||
throw new Error('Job updating failed')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const saveNew = (vms, remoteId, tag, depth, cron, enabled) => {
|
||||
const values = []
|
||||
forEach(vms, vm => {
|
||||
values.push({
|
||||
vm: vm.id,
|
||||
remote: remoteId,
|
||||
tag,
|
||||
depth
|
||||
})
|
||||
})
|
||||
const job = {
|
||||
type: 'call',
|
||||
key: JOBKEY,
|
||||
method: 'vm.rollingDeltaBackup',
|
||||
paramsVector: {
|
||||
type: 'crossProduct',
|
||||
items: [{
|
||||
type: 'set',
|
||||
values
|
||||
}]
|
||||
}
|
||||
}
|
||||
return xo.job.create(job)
|
||||
.then(jobId => xo.schedule.create(jobId, cron, enabled))
|
||||
}
|
||||
|
||||
this.delete = schedule => {
|
||||
let jobId = schedule.job
|
||||
return xo.schedule.delete(schedule.id)
|
||||
.then(() => xo.job.delete(jobId))
|
||||
.finally(() => {
|
||||
if (this.formData.scheduleId === schedule.id) {
|
||||
this.resetData()
|
||||
}
|
||||
refresh()
|
||||
})
|
||||
}
|
||||
|
||||
this.run = schedule => {
|
||||
this.running[schedule.id] = true
|
||||
notify.info({
|
||||
title: 'Run Job',
|
||||
message: 'One shot running started. See overview for logs.'
|
||||
})
|
||||
const id = schedule.job
|
||||
return xo.job.runSequence([id]).finally(() => delete this.running[schedule.id])
|
||||
}
|
||||
|
||||
this.sanitizePath = (...paths) => (paths[0] && paths[0].charAt(0) === '/' && '/' || '') + filter(map(paths, s => s && filter(map(s.split('/'), trim)).join('/'))).join('/')
|
||||
|
||||
this.resetData = () => {
|
||||
this.formData.allRunning = false
|
||||
this.formData.allHalted = false
|
||||
this.formData.selectedVms = []
|
||||
this.formData.scheduleId = undefined
|
||||
this.formData.tag = undefined
|
||||
this.formData.path = undefined
|
||||
this.formData.depth = undefined
|
||||
this.formData.enabled = false
|
||||
this.formData.remote = undefined
|
||||
this.scheduleApi && this.scheduleApi.resetData && this.scheduleApi.resetData()
|
||||
}
|
||||
|
||||
this.size = size
|
||||
this.prettyCron = prettyCron.toString.bind(prettyCron)
|
||||
|
||||
if (!this.comesForEditing) {
|
||||
refresh()
|
||||
} else {
|
||||
refresh()
|
||||
.then(() => {
|
||||
this.edit(this.schedules[this.comesForEditing])
|
||||
delete this.comesForEditing
|
||||
})
|
||||
}
|
||||
this.resetData()
|
||||
this.objects = xoApi.all
|
||||
})
|
||||
|
||||
// A module exports its name.
|
||||
.name
|
||||
132
app/modules/backup/delta-backup/view.jade
Normal file
132
app/modules/backup/delta-backup/view.jade
Normal file
@@ -0,0 +1,132 @@
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.fa.fa-download(style="color: #e25440;")
|
||||
| Delta Backup
|
||||
form#backupform(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.selectedVms, ctrl.formData.remote.id, ctrl.formData.tag, ctrl.formData.depth, ctrl.formData.cronPattern, ctrl.formData.enabled)')
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.xo-icon-vm
|
||||
| VMs to backup
|
||||
.panel-body.form-horizontal
|
||||
.text-center(ng-if = '!ctrl.formData'): i.xo-icon-loading
|
||||
.container-fluid(ng-if = 'ctrl.formData')
|
||||
.alert.alert-info(ng-if = '!ctrl.formData.scheduleId') Creating New Backup
|
||||
.alert.alert-warning(ng-if = 'ctrl.formData.scheduleId') Modifying Backup ID {{ ctrl.formData.scheduleId }}
|
||||
.form-group
|
||||
label.control-label.col-md-2(for = 'tag') Tag
|
||||
.col-md-10
|
||||
input#tag.form-control(form = 'backupform', ng-model = 'ctrl.formData.tag', placeholder = 'Back-up tag', required)
|
||||
.form-group(ng-class = '{"has-warning": !ctrl.formData.selectedVms.length}')
|
||||
label.control-label.col-md-2(for = 'vmlist') VMs
|
||||
.col-md-8
|
||||
ui-select(form = 'backupform', ng-model = 'ctrl.formData.selectedVms', multiple, close-on-select = 'false', required)
|
||||
ui-select-match(placeholder = 'Choose VMs to backup')
|
||||
i.xo-icon-working(ng-if="isVMWorking($item)")
|
||||
i(class="xo-icon-{{$item.power_state | lowercase}}",ng-if="!isVMWorking($item)")
|
||||
| {{$item.name_label}}
|
||||
span(ng-if="$item.$container")
|
||||
| ({{($item.$container | resolve).name_label}})
|
||||
ui-select-choices(repeat = 'vm in ctrl.objects | selectHighLevel | filter:{type: "VM"} | filter:$select.search | orderBy:["$container", "name_label"] track by vm.id')
|
||||
div
|
||||
i.xo-icon-working(ng-if="isVMWorking(vm)", tooltip="{{vm.power_state}} and {{(vm.current_operations | map)[0]}}")
|
||||
i(class="xo-icon-{{vm.power_state | lowercase}}",ng-if="!isVMWorking(vm)", tooltip="{{vm.power_state}}")
|
||||
| {{vm.name_label}}
|
||||
span(ng-if="vm.$container")
|
||||
| ({{(vm.$container | resolve).name_label || ((vm.$container | resolve).master | resolve).name_label}})
|
||||
.col-md-2
|
||||
label(tooltip = 'select/deselect all running VMs', style = 'cursor: pointer')
|
||||
input.hidden(form = 'backupform', type = 'checkbox', ng-model = 'ctrl.formData.allRunning', ng-change = 'ctrl.toggleAllRunning(ctrl.formData.allRunning)')
|
||||
span.fa-stack
|
||||
i.xo-icon-running.fa-stack-1x
|
||||
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allRunning')
|
||||
label(tooltip = 'select/deselect all halted VMs', style = 'cursor: pointer')
|
||||
input.hidden(form = 'backupform', type = 'checkbox', ng-model = 'ctrl.formData.allHalted', ng-change = 'ctrl.toggleAllHalted(ctrl.formData.allHalted)')
|
||||
span.fa-stack
|
||||
i.xo-icon-halted.fa-stack-1x
|
||||
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allHalted')
|
||||
.form-group
|
||||
label.control-label.col-md-2(for = 'depth') Depth
|
||||
.col-md-10
|
||||
input#depth.form-control(form = 'backupform', ng-model = 'ctrl.formData.depth', placeholder = 'How many backups to rollover', type = 'number', min = '1', required)
|
||||
.form-group
|
||||
label.control-label.col-md-2(for = 'remote') Remote
|
||||
.col-md-10
|
||||
select#remote.form-control(form = 'backupform', ng-options = 'remote.name group by remote.type for remote in ctrl.remotes', ng-model = 'ctrl.formData.remote' required)
|
||||
option(value = ''): em -- Choose a file system remote point --
|
||||
.form-group
|
||||
.col-md-10.col-md-offset-2
|
||||
a(ui-sref = 'backup.remote')
|
||||
i.fa.fa-pencil
|
||||
| Manage your remote stores
|
||||
.form-group(ng-if = '!ctrl.formData.scheduleId')
|
||||
label.control-label.col-md-2(for = 'enabled')
|
||||
input#enabled(form = 'backupform', ng-model = 'ctrl.formData.enabled', type = 'checkbox')
|
||||
.help-block.col-md-10 Enable immediatly after creation
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-clock-o
|
||||
| Schedule
|
||||
.panel-body.form-horizontal
|
||||
.text-center(ng-if = '!ctrl.formData'): i.xo-icon-loading
|
||||
xo-scheduler(data = 'ctrl.formData', api = 'ctrl.scheduleApi')
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-body
|
||||
fieldset.center(ng-disabled = '!ctrl.ready')
|
||||
button.btn.btn-lg.btn-primary(form = 'backupform', type = 'submit')
|
||||
i.fa.fa-clock-o
|
||||
|
|
||||
i.fa.fa-arrow-right
|
||||
|
|
||||
i.fa.fa-database
|
||||
| Save
|
||||
|
|
||||
button.btn.btn-lg.btn-default(type = 'button', ng-click = 'ctrl.resetData()')
|
||||
| Reset
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-list-ul
|
||||
| Schedules
|
||||
.panel-body
|
||||
.text-center(ng-if = '!ctrl.schedules'): i.xo-icon-loading
|
||||
.text-center(ng-if = 'ctrl.schedules && !ctrl.size(ctrl.schedules)') No scheduled jobs
|
||||
table.table.table-hover(ng-if = 'ctrl.schedules && ctrl.size(ctrl.schedules)')
|
||||
tr
|
||||
th ID
|
||||
th Tag
|
||||
th.hidden-xs.hidden-sm VMs to backup
|
||||
th.hidden-xs Remote
|
||||
th.hidden-xs Depth
|
||||
th.hidden-xs Scheduling
|
||||
th Enabled now
|
||||
th
|
||||
tr(ng-repeat = 'schedule in ctrl.schedules | orderBy:"id":true track by schedule.id')
|
||||
td {{ schedule.id }}
|
||||
td {{ ctrl.jobs[schedule.job].paramsVector.items[0].values[0].tag }}
|
||||
td.hidden-xs.hidden-sm
|
||||
div(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values.length == 1')
|
||||
| {{ (ctrl.jobs[schedule.job].paramsVector.items[0].values[0].id | resolve).name_label }}
|
||||
div(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values.length > 1')
|
||||
button.btn.btn-info(type = 'button', ng-click = 'unCollapsed = !unCollapsed')
|
||||
| {{ ctrl.jobs[schedule.job].paramsVector.items[0].values.length }} VMs
|
||||
i.fa(ng-class = '{"fa-chevron-down": !unCollapsed, "fa-chevron-up": unCollapsed}')
|
||||
div(collapse = '!unCollapsed')
|
||||
br
|
||||
ul.list-group
|
||||
li.list-group-item(ng-repeat = 'item in ctrl.jobs[schedule.job].paramsVector.items[0].values')
|
||||
span(ng-if = 'item.vm | resolve') {{ (item.vm | resolve).name_label }}
|
||||
span(ng-if = '(item.vm | resolve).$container') ({{ ((item.vm | resolve).$container | resolve).name_label }})
|
||||
td.hidden-xs
|
||||
strong: a(ui-sref = 'scheduler.remote') {{ ctrl.remotes[ctrl.jobs[schedule.job].paramsVector.items[0].values[0].remoteId].name }}
|
||||
td.hidden-xs {{ ctrl.jobs[schedule.job].paramsVector.items[0].values[0].depth }}
|
||||
td.hidden-xs {{ ctrl.prettyCron(schedule.cron) }}
|
||||
td.text-center
|
||||
i.fa.fa-check(ng-if = 'schedule.enabled')
|
||||
td.text-right
|
||||
button.btn.btn-primary(type = 'button', ng-click = 'ctrl.edit(schedule)'): i.fa.fa-pencil
|
||||
|
|
||||
button.btn.btn-warning(type = 'button', ng-click = 'ctrl.run(schedule)', ng-disabled = 'ctrl.running[schedule.id]'): i.fa.fa-play
|
||||
|
|
||||
button.btn.btn-danger(type = 'button', ng-click = 'ctrl.delete(schedule)'): i.fa.fa-trash-o
|
||||
@@ -28,6 +28,7 @@ export default angular.module('backup.disasterrecovery', [
|
||||
|
||||
this.ready = false
|
||||
|
||||
this.running = {}
|
||||
this.comesForEditing = $stateParams.id
|
||||
this.scheduleApi = {}
|
||||
this.formData = {}
|
||||
@@ -179,6 +180,16 @@ export default angular.module('backup.disasterrecovery', [
|
||||
})
|
||||
}
|
||||
|
||||
this.run = schedule => {
|
||||
this.running[schedule.id] = true
|
||||
notify.info({
|
||||
title: 'Run Job',
|
||||
message: 'One shot running started. See overview for logs.'
|
||||
})
|
||||
const id = schedule.job
|
||||
return xo.job.runSequence([id]).finally(() => delete this.running[schedule.id])
|
||||
}
|
||||
|
||||
this.inTargetPool = vm => vm.$poolId === (this.formData.selectedPool && this.formData.selectedPool.id)
|
||||
|
||||
this.resetData = () => {
|
||||
|
||||
@@ -140,4 +140,6 @@ form#drform(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.selec
|
||||
td.text-right
|
||||
button.btn.btn-primary(type = 'button', ng-click = 'ctrl.edit(schedule)'): i.fa.fa-pencil
|
||||
|
|
||||
button.btn.btn-warning(type = 'button', ng-click = 'ctrl.run(schedule)', ng-disabled = 'ctrl.running[schedule.id]'): i.fa.fa-play
|
||||
|
|
||||
button.btn.btn-danger(type = 'button', ng-click = 'ctrl.delete(schedule)'): i.fa.fa-trash-o
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import angular from 'angular'
|
||||
import assign from 'lodash.assign'
|
||||
import forEach from 'lodash.foreach'
|
||||
import indexOf from 'lodash.indexof'
|
||||
import later from 'later'
|
||||
import moment from 'moment'
|
||||
import prettyCron from 'prettycron'
|
||||
import remove from 'lodash.remove'
|
||||
import scheduler from 'scheduler'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
|
||||
later.date.localTime()
|
||||
|
||||
import backup from './backup'
|
||||
import deltaBackup from './delta-backup'
|
||||
import disasterRecovery from './disaster-recovery'
|
||||
import management from './management'
|
||||
import mount from './remote'
|
||||
@@ -18,17 +14,18 @@ import restore from './restore'
|
||||
import rollingSnapshot from './rolling-snapshot'
|
||||
|
||||
import view from './view'
|
||||
import scheduler from './scheduler'
|
||||
|
||||
export default angular.module('backup', [
|
||||
uiRouter,
|
||||
|
||||
backup,
|
||||
deltaBackup,
|
||||
disasterRecovery,
|
||||
management,
|
||||
mount,
|
||||
restore,
|
||||
rollingSnapshot
|
||||
rollingSnapshot,
|
||||
scheduler
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('backup', {
|
||||
@@ -49,302 +46,4 @@ export default angular.module('backup', [
|
||||
})
|
||||
})
|
||||
|
||||
.directive('xoScheduler', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: scheduler,
|
||||
controller: 'XoScheduler as ctrl',
|
||||
bindToController: true,
|
||||
scope: {
|
||||
data: '=',
|
||||
api: '='
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
.controller('XoScheduler', function () {
|
||||
this.init = () => {
|
||||
let i, j
|
||||
|
||||
const minutes = []
|
||||
for (i = 0; i < 6; i++) {
|
||||
minutes[i] = []
|
||||
for (j = 0; j < 10; j++) {
|
||||
minutes[i].push(10 * i + j)
|
||||
}
|
||||
}
|
||||
this.minutes = minutes
|
||||
|
||||
const hours = []
|
||||
for (i = 0; i < 3; i++) {
|
||||
hours[i] = []
|
||||
for (j = 0; j < 8; j++) {
|
||||
hours[i].push(8 * i + j)
|
||||
}
|
||||
}
|
||||
this.hours = hours
|
||||
|
||||
const days = []
|
||||
for (i = 0; i < 4; i++) {
|
||||
days[i] = []
|
||||
for (j = 1; j < 8; j++) {
|
||||
days[i].push(7 * i + j)
|
||||
}
|
||||
}
|
||||
days.push([29, 30, 31])
|
||||
this.days = days
|
||||
|
||||
this.months = [
|
||||
[
|
||||
{v: 1, l: 'Jan'},
|
||||
{v: 2, l: 'Feb'},
|
||||
{v: 3, l: 'Mar'},
|
||||
{v: 4, l: 'Apr'},
|
||||
{v: 5, l: 'May'},
|
||||
{v: 6, l: 'Jun'}
|
||||
],
|
||||
[
|
||||
{v: 7, l: 'Jul'},
|
||||
{v: 8, l: 'Aug'},
|
||||
{v: 9, l: 'Sep'},
|
||||
{v: 10, l: 'Oct'},
|
||||
{v: 11, l: 'Nov'},
|
||||
{v: 12, l: 'Dec'}
|
||||
]
|
||||
]
|
||||
|
||||
this.dayWeeks = [
|
||||
{v: 0, l: 'Sun'},
|
||||
{v: 1, l: 'Mon'},
|
||||
{v: 2, l: 'Tue'},
|
||||
{v: 3, l: 'Wed'},
|
||||
{v: 4, l: 'Thu'},
|
||||
{v: 5, l: 'Fri'},
|
||||
{v: 6, l: 'Sat'}
|
||||
]
|
||||
this.resetData()
|
||||
}
|
||||
|
||||
this.selectMinute = function (minute) {
|
||||
if (this.isSelectedMinute(minute)) {
|
||||
remove(this.data.minSelect, v => String(v) === String(minute))
|
||||
} else {
|
||||
this.data.minSelect.push(minute)
|
||||
}
|
||||
}
|
||||
|
||||
this.isSelectedMinute = function (minute) {
|
||||
return indexOf(this.data.minSelect, minute) > -1 || indexOf(this.data.minSelect, String(minute)) > -1
|
||||
}
|
||||
|
||||
this.selectHour = function (hour) {
|
||||
if (this.isSelectedHour(hour)) {
|
||||
remove(this.data.hourSelect, v => String(v) === String(hour))
|
||||
} else {
|
||||
this.data.hourSelect.push(hour)
|
||||
}
|
||||
}
|
||||
|
||||
this.isSelectedHour = function (hour) {
|
||||
return indexOf(this.data.hourSelect, hour) > -1 || indexOf(this.data.hourSelect, String(hour)) > -1
|
||||
}
|
||||
|
||||
this.selectDay = function (day) {
|
||||
if (this.isSelectedDay(day)) {
|
||||
remove(this.data.daySelect, v => String(v) === String(day))
|
||||
} else {
|
||||
this.data.daySelect.push(day)
|
||||
}
|
||||
}
|
||||
|
||||
this.isSelectedDay = function (day) {
|
||||
return indexOf(this.data.daySelect, day) > -1 || indexOf(this.data.daySelect, String(day)) > -1
|
||||
}
|
||||
|
||||
this.selectMonth = function (month) {
|
||||
if (this.isSelectedMonth(month)) {
|
||||
remove(this.data.monthSelect, v => String(v) === String(month))
|
||||
} else {
|
||||
this.data.monthSelect.push(month)
|
||||
}
|
||||
}
|
||||
|
||||
this.isSelectedMonth = function (month) {
|
||||
return indexOf(this.data.monthSelect, month) > -1 || indexOf(this.data.monthSelect, String(month)) > -1
|
||||
}
|
||||
|
||||
this.selectDayWeek = function (dayWeek) {
|
||||
if (this.isSelectedDayWeek(dayWeek)) {
|
||||
remove(this.data.dayWeekSelect, v => String(v) === String(dayWeek))
|
||||
} else {
|
||||
this.data.dayWeekSelect.push(dayWeek)
|
||||
}
|
||||
}
|
||||
|
||||
this.isSelectedDayWeek = function (dayWeek) {
|
||||
return indexOf(this.data.dayWeekSelect, dayWeek) > -1 || indexOf(this.data.dayWeekSelect, String(dayWeek)) > -1
|
||||
}
|
||||
|
||||
this.noMinutePlan = function (set = false) {
|
||||
if (!set) {
|
||||
// The last part (after &&) of this expression is reliable because we maintain the minSelect array with lodash.remove
|
||||
return this.data.min === 'select' && this.data.minSelect.length === 1 && String(this.data.minSelect[0]) === '0'
|
||||
} else {
|
||||
this.data.minSelect = [0]
|
||||
this.data.min = 'select'
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
this.noHourPlan = function (set = false) {
|
||||
if (!set) {
|
||||
// The last part (after &&) of this expression is reliable because we maintain the hourSelect array with lodash.remove
|
||||
return this.data.hour === 'select' && this.data.hourSelect.length === 1 && String(this.data.hourSelect[0]) === '0'
|
||||
} else {
|
||||
this.data.hourSelect = [0]
|
||||
this.data.hour = 'select'
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
this.resetData = () => {
|
||||
this.data.minRange = 5
|
||||
this.data.hourRange = 2
|
||||
this.data.minSelect = [0]
|
||||
this.data.hourSelect = []
|
||||
this.data.daySelect = []
|
||||
this.data.monthSelect = []
|
||||
this.data.dayWeekSelect = []
|
||||
this.data.min = 'select'
|
||||
this.data.hour = 'all'
|
||||
this.data.day = 'all'
|
||||
this.data.month = 'all'
|
||||
this.data.dayWeek = 'all'
|
||||
this.data.cronPattern = '* * * * *'
|
||||
this.data.summary = []
|
||||
this.data.previewLimit = 0
|
||||
|
||||
this.update()
|
||||
}
|
||||
|
||||
this.update = () => {
|
||||
const d = this.data
|
||||
const i = (d.min === 'all' && '*') ||
|
||||
(d.min === 'range' && ('*/' + d.minRange)) ||
|
||||
(d.min === 'select' && d.minSelect.join(',')) ||
|
||||
'*'
|
||||
const h = (d.hour === 'all' && '*') ||
|
||||
(d.hour === 'range' && ('*/' + d.hourRange)) ||
|
||||
(d.hour === 'select' && d.hourSelect.join(',')) ||
|
||||
'*'
|
||||
const dm = (d.day === 'all' && '*') ||
|
||||
(d.day === 'select' && d.daySelect.join(',')) ||
|
||||
'*'
|
||||
const m = (d.month === 'all' && '*') ||
|
||||
(d.month === 'select' && d.monthSelect.join(',')) ||
|
||||
'*'
|
||||
const dw = (d.dayWeek === 'all' && '*') ||
|
||||
(d.dayWeek === 'select' && d.dayWeekSelect.join(',')) ||
|
||||
'*'
|
||||
this.data.cronPattern = i + ' ' + h + ' ' + dm + ' ' + m + ' ' + dw
|
||||
|
||||
const tabState = {
|
||||
min: {
|
||||
all: d.min === 'all',
|
||||
range: d.min === 'range',
|
||||
select: d.min === 'select'
|
||||
},
|
||||
hour: {
|
||||
all: d.hour === 'all',
|
||||
range: d.hour === 'range',
|
||||
select: d.hour === 'select'
|
||||
},
|
||||
day: {
|
||||
all: d.day === 'all',
|
||||
range: d.day === 'range',
|
||||
select: d.day === 'select'
|
||||
},
|
||||
month: {
|
||||
all: d.month === 'all',
|
||||
select: d.month === 'select'
|
||||
},
|
||||
dayWeek: {
|
||||
all: d.dayWeek === 'all',
|
||||
select: d.dayWeek === 'select'
|
||||
}
|
||||
}
|
||||
this.tabs = tabState
|
||||
this.summarize()
|
||||
}
|
||||
|
||||
this.summarize = () => {
|
||||
const schedule = later.parse.cron(this.data.cronPattern)
|
||||
const occurences = later.schedule(schedule).next(25)
|
||||
this.data.summary = []
|
||||
forEach(occurences, occurence => {
|
||||
this.data.summary.push(moment(occurence).format('LLLL'))
|
||||
})
|
||||
}
|
||||
|
||||
const cronToData = (data, cron) => {
|
||||
const d = Object.create(null)
|
||||
const cronItems = cron.split(' ')
|
||||
|
||||
if (cronItems[0] === '*') {
|
||||
d.min = 'all'
|
||||
} else if (cronItems[0].indexOf('/') !== -1) {
|
||||
d.min = 'range'
|
||||
const [, range] = cronItems[0].split('/')
|
||||
d.minRange = range
|
||||
} else {
|
||||
d.min = 'select'
|
||||
d.minSelect = cronItems[0].split(',')
|
||||
}
|
||||
|
||||
if (cronItems[1] === '*') {
|
||||
d.hour = 'all'
|
||||
} else if (cronItems[1].indexOf('/') !== -1) {
|
||||
d.hour = 'range'
|
||||
const [, range] = cronItems[1].split('/')
|
||||
d.hourRange = range
|
||||
} else {
|
||||
d.hour = 'select'
|
||||
d.hourSelect = cronItems[1].split(',')
|
||||
}
|
||||
|
||||
if (cronItems[2] === '*') {
|
||||
d.day = 'all'
|
||||
} else {
|
||||
d.day = 'select'
|
||||
d.daySelect = cronItems[2].split(',')
|
||||
}
|
||||
|
||||
if (cronItems[3] === '*') {
|
||||
d.month = 'all'
|
||||
} else {
|
||||
d.month = 'select'
|
||||
d.monthSelect = cronItems[3].split(',')
|
||||
}
|
||||
|
||||
if (cronItems[4] === '*') {
|
||||
d.dayWeek = 'all'
|
||||
} else {
|
||||
d.dayWeek = 'select'
|
||||
d.dayWeekSelect = cronItems[4].split(',')
|
||||
}
|
||||
|
||||
assign(data, d)
|
||||
}
|
||||
|
||||
this.prettyCron = prettyCron.toString.bind(prettyCron)
|
||||
|
||||
this.api.setCron = cron => {
|
||||
cronToData(this.data, cron)
|
||||
this.update()
|
||||
}
|
||||
this.api.resetData = this.resetData.bind(this)
|
||||
|
||||
this.init()
|
||||
})
|
||||
|
||||
.name
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import angular from 'angular'
|
||||
import filter from 'lodash.filter'
|
||||
import forEach from 'lodash.foreach'
|
||||
import prettyCron from 'prettycron'
|
||||
import uiBootstrap from 'angular-ui-bootstrap'
|
||||
@@ -20,16 +21,21 @@ export default angular.module('backup.management', [
|
||||
})
|
||||
})
|
||||
.controller('ManagementCtrl', function ($scope, $state, $stateParams, $interval, xo, xoApi, notify, selectHighLevelFilter, filterFilter) {
|
||||
this.running = {}
|
||||
const mapJobKeyToState = {
|
||||
'rollingSnapshot': 'rollingsnapshot',
|
||||
'rollingBackup': 'backup',
|
||||
'disasterRecovery': 'disasterrecovery'
|
||||
deltaBackup: 'deltaBackup',
|
||||
disasterRecovery: 'disasterrecovery',
|
||||
rollingBackup: 'backup',
|
||||
rollingSnapshot: 'rollingsnapshot',
|
||||
__none: 'index'
|
||||
}
|
||||
|
||||
const mapJobKeyToJobDisplay = {
|
||||
'rollingSnapshot': 'Rolling Snapshot',
|
||||
'rollingBackup': 'Backup',
|
||||
'disasterRecovery': 'Disaster Recovery'
|
||||
deltaBackup: 'Delta Backup',
|
||||
disasterRecovery: 'Disaster Recovery',
|
||||
rollingBackup: 'Backup',
|
||||
rollingSnapshot: 'Rolling Snapshot',
|
||||
__none: '[unknown]'
|
||||
}
|
||||
|
||||
this.currentLogPage = 1
|
||||
@@ -37,7 +43,7 @@ export default angular.module('backup.management', [
|
||||
|
||||
const refreshSchedules = () => {
|
||||
xo.schedule.getAll()
|
||||
.then(schedules => this.schedules = schedules)
|
||||
.then(schedules => this.schedules = filter(schedules, schedule => this.jobs[schedule.job] && this.jobs[schedule.job].key in mapJobKeyToState))
|
||||
xo.scheduler.getScheduleTable()
|
||||
.then(table => this.scheduleTable = table)
|
||||
}
|
||||
@@ -45,12 +51,10 @@ export default angular.module('backup.management', [
|
||||
const getLogs = () => {
|
||||
xo.logs.get('jobs').then(logs => {
|
||||
const viewLogs = {}
|
||||
|
||||
forEach(logs, (log, logKey) => {
|
||||
const data = log.data
|
||||
const [time] = logKey.split(':')
|
||||
|
||||
if (data.event === 'job.start') {
|
||||
if (data.event === 'job.start' && data.key in mapJobKeyToState) {
|
||||
viewLogs[logKey] = {
|
||||
logKey,
|
||||
jobId: data.jobId,
|
||||
@@ -62,31 +66,33 @@ export default angular.module('backup.management', [
|
||||
}
|
||||
} else {
|
||||
const runJobId = data.runJobId
|
||||
const entry = viewLogs[runJobId]
|
||||
if (!entry) {
|
||||
return
|
||||
}
|
||||
|
||||
if (data.event === 'job.end') {
|
||||
const entry = viewLogs[runJobId]
|
||||
|
||||
if (data.error) {
|
||||
entry.error = data.error
|
||||
}
|
||||
|
||||
entry.end = time
|
||||
entry.duration = time - entry.start
|
||||
entry.status = 'Terminated'
|
||||
} else if (data.event === 'jobCall.start') {
|
||||
viewLogs[runJobId].calls[logKey] = {
|
||||
entry.calls[logKey] = {
|
||||
callKey: logKey,
|
||||
params: resolveParams(data.params),
|
||||
method: data.method,
|
||||
time
|
||||
}
|
||||
} else if (data.event === 'jobCall.end') {
|
||||
const call = viewLogs[runJobId].calls[data.runCallId]
|
||||
const call = entry.calls[data.runCallId]
|
||||
|
||||
if (data.error) {
|
||||
call.error = data.error
|
||||
viewLogs[runJobId].hasErrors = true
|
||||
entry.hasErrors = true
|
||||
} else {
|
||||
call.returnedValue = data.returnedValue
|
||||
call.returnedValue = resolveReturn(data.returnedValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,17 +110,24 @@ export default angular.module('backup.management', [
|
||||
|
||||
const resolveParams = params => {
|
||||
for (let key in params) {
|
||||
if (key === 'id') {
|
||||
const xoObject = xoApi.get(params[key])
|
||||
if (xoObject) {
|
||||
params[xoObject.type] = xoObject.name_label
|
||||
delete params[key]
|
||||
}
|
||||
const xoObject = xoApi.get(params[key])
|
||||
if (xoObject) {
|
||||
const newKey = xoObject.type || key
|
||||
params[newKey] = xoObject.name_label || xoObject.name || params[key]
|
||||
newKey !== key && delete params[key]
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
const resolveReturn = returnValue => {
|
||||
const xoObject = xoApi.get(returnValue)
|
||||
let xoName = xoObject && (xoObject.name_label || xoObject.name)
|
||||
xoName && (xoName += xoObject.type && ` (${xoObject.type})` || '')
|
||||
returnValue = xoName || returnValue
|
||||
return returnValue
|
||||
}
|
||||
|
||||
this.prettyCron = prettyCron.toString.bind(prettyCron)
|
||||
|
||||
const refreshJobs = () => {
|
||||
@@ -151,9 +164,19 @@ export default angular.module('backup.management', [
|
||||
.finally(() => { this.working[id] = false })
|
||||
.then(refreshSchedules)
|
||||
}
|
||||
this.resolveJobKey = schedule => mapJobKeyToState[this.jobs[schedule.job].key]
|
||||
this.displayJobKey = schedule => mapJobKeyToJobDisplay[this.jobs[schedule.job].key]
|
||||
this.run = schedule => {
|
||||
this.running[schedule.id] = true
|
||||
notify.info({
|
||||
title: 'Run Job',
|
||||
message: 'One shot running started. See overview for logs.'
|
||||
})
|
||||
const id = schedule.job
|
||||
return xo.job.runSequence([id]).finally(() => delete this.running[schedule.id])
|
||||
}
|
||||
this.resolveJobKey = schedule => mapJobKeyToState[this.jobs[schedule.job] && this.jobs[schedule.job].key || '__none']
|
||||
this.displayJobKey = schedule => mapJobKeyToJobDisplay[this.jobs[schedule.job] && this.jobs[schedule.job].key || '__none']
|
||||
this.displayLogKey = log => mapJobKeyToJobDisplay[log.key]
|
||||
this.resolveScheduleJobTag = schedule => this.jobs[schedule.job] && this.jobs[schedule.job].paramsVector && this.jobs[schedule.job].paramsVector.items[0].values[0].tag || schedule.id
|
||||
|
||||
this.collectionLength = col => Object.keys(col).length
|
||||
this.working = {}
|
||||
|
||||
@@ -16,25 +16,23 @@
|
||||
td.text-center No scheduled jobs
|
||||
table.table.table-hover(ng-if = 'ctrl.schedules && ctrl.collectionLength(ctrl.schedules)')
|
||||
tr
|
||||
th ID
|
||||
th Job
|
||||
th Scheduling
|
||||
th Tag
|
||||
th.hidden-xs Scheduling
|
||||
th State
|
||||
th
|
||||
tr(ng-repeat = 'schedule in ctrl.schedules | orderBy:"id":true track by schedule.id')
|
||||
td: a(ui-sref = 'backup.{{ctrl.resolveJobKey(schedule)}}({id: schedule.id})') {{ schedule.id }}
|
||||
td {{ ctrl.displayJobKey(schedule) }}
|
||||
td {{ ctrl.prettyCron(schedule.cron) }}
|
||||
td: a(ui-sref = 'backup.{{ctrl.resolveJobKey(schedule)}}({id: schedule.id})') {{ ctrl.resolveScheduleJobTag(schedule) }}
|
||||
td.hidden-xs {{ ctrl.prettyCron(schedule.cron) }}
|
||||
td
|
||||
span.text-success(ng-if = 'ctrl.scheduleTable[schedule.id] === true')
|
||||
| enabled
|
||||
i.fa.fa-cogs
|
||||
span.text-muted(ng-if = 'ctrl.scheduleTable[schedule.id] === false') disabled
|
||||
span.text-warning(ng-if = 'ctrl.scheduleTable[schedule.id] === undefined') ?
|
||||
td.text-right
|
||||
fieldset(ng-disabled = 'ctrl.working[schedule.id]')
|
||||
button.btn.btn-success(ng-if = 'ctrl.scheduleTable[schedule.id] === false', type = 'button', ng-click = 'ctrl.enable(schedule.id)') Enable
|
||||
button.btn.btn-warning(ng-if = 'ctrl.scheduleTable[schedule.id] === true', type = 'button', ng-click = 'ctrl.disable(schedule.id)') Disable
|
||||
span.label.label-success.hidden-xs(ng-if = 'ctrl.scheduleTable[schedule.id] === true') enabled
|
||||
span.label.label-default.hidden-xs(ng-if = 'ctrl.scheduleTable[schedule.id] === false') disabled
|
||||
span.label.label-warning.hidden-xs(ng-if = 'ctrl.scheduleTable[schedule.id] === undefined') unknown
|
||||
fieldset.pull-right(ng-disabled = 'ctrl.working[schedule.id]')
|
||||
button.btn(ng-if = 'ctrl.scheduleTable[schedule.id] === false', type = 'button', ng-click = 'ctrl.enable(schedule.id)'): i.fa.fa-toggle-off
|
||||
button.btn.btn-success(ng-if = 'ctrl.scheduleTable[schedule.id] === true', type = 'button', ng-click = 'ctrl.disable(schedule.id)'): i.fa.fa-toggle-on
|
||||
|
|
||||
button.btn.btn-warning(type = 'button', ng-click = 'ctrl.run(schedule)', ng-disabled = 'ctrl.running[schedule.id]'): i.fa.fa-play
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
@@ -53,8 +51,8 @@
|
||||
tbody(ng-repeat = 'log in ctrl.logs | map | filter:ctrl.logSearch | orderBy:"-time" | slice:(ctrl.logPageSize * (ctrl.currentLogPage - 1)):(ctrl.logPageSize * ctrl.currentLogPage) track by log.logKey')
|
||||
tr
|
||||
td
|
||||
| {{ log.jobId }}
|
||||
button.btn.btn-sm(type = 'button', tooltip = 'See calls', ng-click = 'seeCalls = !seeCalls', ng-class = '{"btn-default": !log.hasErrors, "btn-danger": log.hasErrors}'): i.fa(ng-class = '{"fa-caret-down": !seeCalls, "fa-caret-up": seeCalls}')
|
||||
| {{ log.jobId }}
|
||||
td {{ ctrl.displayLogKey(log) }}
|
||||
td {{ log.start | date:'medium' }}
|
||||
td {{ log.end | date:'medium' }}
|
||||
@@ -68,6 +66,7 @@
|
||||
td(colspan = '6')
|
||||
ul.list-group
|
||||
li.list-group-item(ng-repeat = 'call in log.calls | map | orderBy:"-time" track by call.callKey')
|
||||
strong.text-info {{ call.method }}: 
|
||||
span(ng-repeat = '(key, param) in call.params')
|
||||
strong {{ key }}:
|
||||
| {{ param }}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import angular from 'angular'
|
||||
import filter from 'lodash.filter'
|
||||
import find from 'lodash.find'
|
||||
import forEach from 'lodash.foreach'
|
||||
import size from 'lodash.size'
|
||||
@@ -20,19 +21,26 @@ export default angular.module('backup.restore', [
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('RestoreCtrl', function ($scope, $interval, xo, xoApi, notify, $upload) {
|
||||
.controller('RestoreCtrl', function ($scope, $interval, xo, xoApi, notify, $upload, bytesToSizeFilter) {
|
||||
this.loaded = {}
|
||||
this.hosts = xoApi.getView('hosts')
|
||||
|
||||
const srs = xoApi.getView('SRs').all
|
||||
|
||||
this.bytesToSize = bytesToSizeFilter
|
||||
this.isEmpty = backups => backups && !(Object.keys(backups.delta) || backups.other.length)
|
||||
this.size = size
|
||||
|
||||
const refresh = () => {
|
||||
return xo.remote.getAll()
|
||||
.then(remotes => {
|
||||
forEach(this.backUpRemotes, remote => {
|
||||
if (remote.files) {
|
||||
if (remote.backups) {
|
||||
const freshRemote = find(remotes, {id: remote.id})
|
||||
freshRemote && (freshRemote.files = remote.files)
|
||||
freshRemote && (freshRemote.backups = remote.backups)
|
||||
}
|
||||
})
|
||||
this.backUpRemotes = remotes
|
||||
this.writable_SRs = filter(srs, (sr) => sr.content_type !== 'iso')
|
||||
})
|
||||
}
|
||||
|
||||
@@ -43,24 +51,67 @@ export default angular.module('backup.restore', [
|
||||
$interval.cancel(interval)
|
||||
})
|
||||
|
||||
const deltaBuilder = (backups, uuid, name, tag, value) => {
|
||||
let deltaBackup = backups[uuid]
|
||||
? backups[uuid]
|
||||
: backups[uuid] = {}
|
||||
|
||||
deltaBackup = deltaBackup[name]
|
||||
? deltaBackup[name]
|
||||
: deltaBackup[name] = {}
|
||||
|
||||
deltaBackup = deltaBackup[tag]
|
||||
? deltaBackup[tag]
|
||||
: deltaBackup[tag] = []
|
||||
|
||||
deltaBackup.push(value)
|
||||
}
|
||||
|
||||
this.list = id => {
|
||||
return xo.remote.list(id)
|
||||
.then(files => {
|
||||
const remote = find(this.backUpRemotes, {id})
|
||||
remote && (remote.files = files)
|
||||
|
||||
if (remote) {
|
||||
const backups = remote.backups = {
|
||||
delta: {},
|
||||
other: []
|
||||
}
|
||||
|
||||
forEach(files, file => {
|
||||
const arr = /^vm_delta_(.*)_([^\/]+)\/([^_]+)_(.*)\.xva$/.exec(file)
|
||||
|
||||
if (arr) {
|
||||
const [ path, tag, uuid, date, name ] = arr
|
||||
const value = {
|
||||
path: /^(vm_delta_.*)\.xva$/.exec(path)[1],
|
||||
date
|
||||
}
|
||||
deltaBuilder(backups.delta, uuid, name, tag, value)
|
||||
} else {
|
||||
backups.other.push(file)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
this.loaded[remote.id] = true
|
||||
})
|
||||
}
|
||||
|
||||
this.importVm = (id, file, host) => {
|
||||
notify.info({
|
||||
title: 'VM import started',
|
||||
message: 'Starting the VM import'
|
||||
})
|
||||
return xo.remote.importVm(id, file, host)
|
||||
const notification = {
|
||||
title: 'VM import started',
|
||||
message: 'Starting the VM import'
|
||||
}
|
||||
|
||||
this.size = size
|
||||
this.importBackup = (id, path, sr) => {
|
||||
notify.info(notification)
|
||||
return xo.vm.importBackup(id, path, sr)
|
||||
}
|
||||
|
||||
this.importDeltaBackup = (id, path, sr) => {
|
||||
notify.info(notification)
|
||||
return xo.vm.importDeltaBackup(id, path, sr)
|
||||
}
|
||||
})
|
||||
|
||||
// A module exports its name.
|
||||
|
||||
@@ -14,24 +14,55 @@
|
||||
span(ng-if = '!remote.error') (disabled)
|
||||
.panel-body(ng-if = 'remote.enabled')
|
||||
.row
|
||||
.col-sm-3
|
||||
.col-sm-2
|
||||
p
|
||||
| {{ remote.name }}
|
||||
button.btn.btn-default.pull-right(type = 'button', ng-click = 'ctrl.list(remote.id)'): i.fa(ng-class = '{"fa-eye": !ctrl.loaded[remote.id], "fa-refresh": ctrl.loaded[remote.id]}')
|
||||
br
|
||||
br
|
||||
.col-sm-9
|
||||
div(ng-if = 'ctrl.loaded[remote.id] && !ctrl.size(remote.files)') No backups available
|
||||
div(ng-if = 'ctrl.size(remote.files)')
|
||||
div(ng-repeat = 'file in remote.files')
|
||||
| {{ file }}
|
||||
.col-sm-10
|
||||
div(ng-if = 'ctrl.loaded[remote.id] && ctrl.isEmpty(remote.backups)') No backups available
|
||||
div(ng-if = 'ctrl.size(remote.backups.delta)')
|
||||
div(ng-repeat = '(uuid, backups) in remote.backups.delta')
|
||||
.row
|
||||
.col-sm-2
|
||||
| {{ uuid }}
|
||||
.col-sm-10
|
||||
div(ng-repeat = '(name, backups) in backups')
|
||||
.row
|
||||
.col-sm-2
|
||||
| {{ name }}
|
||||
.col-sm-10
|
||||
div(ng-repeat = '(tag, backups) in backups')
|
||||
.row
|
||||
.col-sm-2
|
||||
| {{ tag }}
|
||||
.col-sm-10
|
||||
div(ng-repeat = 'backup in backups')
|
||||
| {{ backup.date | date:'medium' }}
|
||||
span.pull-right.dropdown(dropdown)
|
||||
button.btn.btn-default(type = 'button', dropdown-toggle)
|
||||
| Import
|
||||
span.caret
|
||||
ul.dropdown-menu(role="menu")
|
||||
li(ng-repeat = 'sr in ctrl.writable_SRs | orderBy:natural("name_label") track by sr.id')
|
||||
a(xo-click = "ctrl.importDeltaBackup(remote.id, backup.path, sr.id)")
|
||||
i.xo-icon-host.fa-fw
|
||||
| To {{sr.name_label}} ({{sr.size - sr.usage | bytesToSize }})
|
||||
span {{ (sr.$container | resolve).name_label }}
|
||||
hr
|
||||
hr
|
||||
div(ng-if = 'ctrl.size(remote.backups.other)')
|
||||
div(ng-repeat = 'backup in remote.backups.other')
|
||||
| {{ backup }}
|
||||
span.pull-right.dropdown(dropdown)
|
||||
button.btn.btn-default(type = 'button', dropdown-toggle)
|
||||
| Import
|
||||
span.caret
|
||||
ul.dropdown-menu(role="menu")
|
||||
li(ng-repeat = 'h in ctrl.hosts.all | orderBy:natural("name_label") track by h.id')
|
||||
a(xo-click = "ctrl.importVm(remote.id, file, h.id)")
|
||||
li(ng-repeat = 'sr in ctrl.writable_SRs | orderBy:natural("name_label") track by sr.id')
|
||||
a(xo-click = "ctrl.importBackup(remote.id, backup, sr.id)")
|
||||
i.xo-icon-host.fa-fw
|
||||
| To {{h.name_label}}
|
||||
| To {{sr.name_label}} ({{sr.size - sr.usage | bytesToSize }})
|
||||
span {{ (sr.$container | resolve).name_label }}
|
||||
hr
|
||||
|
||||
@@ -28,6 +28,7 @@ export default angular.module('backup.rollingSnapshot', [
|
||||
|
||||
this.ready = false
|
||||
|
||||
this.running = {}
|
||||
this.comesForEditing = $stateParams.id
|
||||
this.scheduleApi = {}
|
||||
this.formData = {}
|
||||
@@ -200,6 +201,16 @@ export default angular.module('backup.rollingSnapshot', [
|
||||
})
|
||||
}
|
||||
|
||||
this.run = schedule => {
|
||||
this.running[schedule.id] = true
|
||||
notify.info({
|
||||
title: 'Run Job',
|
||||
message: 'One shot running started. See overview for logs.'
|
||||
})
|
||||
const id = schedule.job
|
||||
return xo.job.runSequence([id]).finally(() => delete this.running[schedule.id])
|
||||
}
|
||||
|
||||
this.resetData = () => {
|
||||
this.formData.allRunning = false
|
||||
this.formData.allHalted = false
|
||||
|
||||
@@ -114,4 +114,6 @@ form#snapform(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.sel
|
||||
td.text-right
|
||||
button.btn.btn-primary(type = 'button', ng-click = 'ctrl.edit(schedule)'): i.fa.fa-pencil
|
||||
|
|
||||
button.btn.btn-warning(type = 'button', ng-click = 'ctrl.run(schedule)', ng-disabled = 'ctrl.running[schedule.id]'): i.fa.fa-play
|
||||
|
|
||||
button.btn.btn-danger(type = 'button', ng-click = 'ctrl.delete(schedule)'): i.fa.fa-trash-o
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
a(ui-sref = '.backup')
|
||||
i.fa.fa-fw.fa-download.fa-menu
|
||||
span.menu-entry Backup
|
||||
li
|
||||
a(ui-sref = '.deltaBackup')
|
||||
i.fa.fa-fw.fa-code-fork.fa-menu
|
||||
span.menu-entry Delta Backup
|
||||
li
|
||||
a(ui-sref = '.restore')
|
||||
i.fa.fa-fw.fa-upload.fa-menu
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
angular = require 'angular'
|
||||
forEach = require('lodash.foreach')
|
||||
includes = require('lodash.includes')
|
||||
Clipboard = require('clipboard')
|
||||
|
||||
isoDevice = require('iso-device')
|
||||
|
||||
@@ -89,5 +90,13 @@ module.exports = angular.module 'xoWebApp.console', [
|
||||
$scope.insert = (disc_id) ->
|
||||
xo.vm.insertCd id, disc_id, true
|
||||
|
||||
$scope.vmClipboard = ''
|
||||
$scope.setClipboard = (text) ->
|
||||
$scope.vmClipboard = text
|
||||
$scope.$applyAsync()
|
||||
|
||||
clipboard = new Clipboard('.copy')
|
||||
clipboard.on('error', (e) -> console.log('Clipboard', e))
|
||||
|
||||
# A module exports its name.
|
||||
.name
|
||||
|
||||
@@ -15,15 +15,22 @@
|
||||
|
||||
//- Toolbar
|
||||
.list-group-item: .row.text-center
|
||||
.col-sm-6: iso-device(ng-if = 'VM && SRs', vm = 'VM', srs = 'SRs')
|
||||
.col-sm-3: button.btn.btn-default(
|
||||
.col-sm-4: iso-device(ng-if = 'VM && SRs', vm = 'VM', srs = 'SRs')
|
||||
.col-sm-2: button.btn.btn-default(
|
||||
ng-click = 'vncRemote.sendCtrlAltDel()'
|
||||
)
|
||||
i.fa.fa-keyboard-o
|
||||
|
|
||||
| Ctrl+Alt+Del
|
||||
.col-sm-4
|
||||
.input-group
|
||||
input#vm-clipboard.form-control(ng-model='vmClipboard' ng-change='vncRemote.pasteToClipboard(vmClipboard)')
|
||||
span.input-group-btn
|
||||
button.btn.btn-default.copy(data-clipboard-target='#vm-clipboard' tooltip="Copy text into local clipboard")
|
||||
i.fa.fa-clipboard
|
||||
| Copy
|
||||
//- Action panel
|
||||
.col-sm-3
|
||||
.col-sm-2
|
||||
.btn-group
|
||||
button.btn.btn-default.inversed(
|
||||
ng-if = "VM.power_state == ('Running' || 'Paused')"
|
||||
@@ -50,5 +57,6 @@
|
||||
.list-group-item
|
||||
no-vnc(
|
||||
url = '{{consoleUrl}}'
|
||||
remote-control = 'vncRemote'
|
||||
remote-control = 'vncRemote',
|
||||
on-clipboard-change = 'setClipboard(clipboardContent)'
|
||||
)
|
||||
|
||||
@@ -115,9 +115,7 @@ export default angular.module('dashboard.dataviz', [
|
||||
|
||||
const debouncedPopulate = debounce(populateChartsData, 300, {leading: true, trailing: true})
|
||||
debouncedPopulate()
|
||||
xoApi.onUpdate(function () {
|
||||
debouncedPopulate()
|
||||
})
|
||||
$scope.$on('$destroy', xoApi.onUpdate(debouncedPopulate))
|
||||
})
|
||||
.controller('DatavizStorageHierarchical', function DatavizStorageHierarchical (xoApi, $scope, $timeout, $interval, $state, bytesToSizeFilter) {
|
||||
$scope.charts = {
|
||||
@@ -255,9 +253,7 @@ export default angular.module('dashboard.dataviz', [
|
||||
const debouncedPopulate = debounce(populateChartsData, 300, {leading: true, trailing: true})
|
||||
|
||||
debouncedPopulate()
|
||||
xoApi.onUpdate(function () {
|
||||
debouncedPopulate()
|
||||
})
|
||||
$scope.$on('$destroy', xoApi.onUpdate(debouncedPopulate))
|
||||
})
|
||||
.controller('DatavizRamHierarchical', function DatavizRamHierarchical (xoApi, $scope, $timeout, $state, bytesToSizeFilter) {
|
||||
$scope.charts = {
|
||||
@@ -361,9 +357,7 @@ export default angular.module('dashboard.dataviz', [
|
||||
const debouncedPopulate = debounce(populateChartsData, 300, {leading: true, trailing: true})
|
||||
|
||||
debouncedPopulate()
|
||||
xoApi.onUpdate(function () {
|
||||
debouncedPopulate()
|
||||
})
|
||||
$scope.$on('$destroy', xoApi.onUpdate(debouncedPopulate))
|
||||
})
|
||||
// A module exports its name.
|
||||
.name
|
||||
|
||||
@@ -28,7 +28,7 @@ export default angular.module('dashboard.overview', [
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('Overview', function ($scope, $window, xoApi, xo, $timeout, bytesToSizeFilter) {
|
||||
.controller('Overview', function ($scope, $window, xoApi, xo, $timeout, bytesToSizeFilter, modal) {
|
||||
$window.bytesToSize = bytesToSizeFilter // FIXME dirty workaround to custom a Chart.js tooltip template
|
||||
angular.extend($scope, {
|
||||
pools: {
|
||||
@@ -47,6 +47,33 @@ export default angular.module('dashboard.overview', [
|
||||
cpu: [0, 0],
|
||||
srs: []
|
||||
})
|
||||
|
||||
$scope.installAllPatches = function () {
|
||||
modal.confirm({
|
||||
title: 'Install all the missing patches',
|
||||
message: 'Are you sure you want to install all the missing patches? This could take a while...'
|
||||
}).then(() =>
|
||||
foreach($scope.pools.all, function (pool, pool_id) {
|
||||
let pool_hosts = $scope.hostsByPool[pool_id]
|
||||
foreach(pool_hosts, function (host, host_id) {
|
||||
console.log('Installing all missing patches on host ', host_id)
|
||||
xo.host.installAllPatches(host_id)
|
||||
})
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
$scope.installHostPatches = function (hostId) {
|
||||
modal.confirm({
|
||||
title: 'Update host (' + $scope.nbUpdates[hostId] + ' patch(es))',
|
||||
message: 'Are you sure you want to install all the missing patches on this host? This could take a while...'
|
||||
}).then(() => {
|
||||
console.log('Installing all missing patches on host ', hostId)
|
||||
xo.host.installAllPatches(hostId)
|
||||
})
|
||||
}
|
||||
|
||||
const nbUpdates = $scope.nbUpdates = {}
|
||||
function populateChartsData () {
|
||||
let pools,
|
||||
vmsByContainer,
|
||||
@@ -74,11 +101,25 @@ export default angular.module('dashboard.overview', [
|
||||
srs = []
|
||||
|
||||
// update vdi, set them to the right host
|
||||
pools = xoApi.getView('pools')
|
||||
$scope.pools = pools = xoApi.getView('pools')
|
||||
|
||||
srsByContainer = xoApi.getIndex('srsByContainer')
|
||||
vmsByContainer = xoApi.getIndex('vmsByContainer')
|
||||
hostsByPool = xoApi.getIndex('hostsByPool')
|
||||
$scope.hostsByPool = hostsByPool = xoApi.getIndex('hostsByPool')
|
||||
foreach(pools.all, function (pool, pool_id) {
|
||||
let pool_hosts = hostsByPool[pool_id]
|
||||
foreach(pool_hosts, function (host, host_id) {
|
||||
if (host_id in nbUpdates) {
|
||||
return
|
||||
}
|
||||
|
||||
xo.host.listMissingPatches(host_id)
|
||||
.then(result => {
|
||||
nbUpdates[host_id] = result.length
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
foreach(pools.all, function (pool, pool_id) {
|
||||
nb_pools++
|
||||
@@ -129,9 +170,8 @@ export default angular.module('dashboard.overview', [
|
||||
const debouncedPopulate = debounce(populateChartsData, 300, {leading: true, trailing: true})
|
||||
|
||||
debouncedPopulate()
|
||||
xoApi.onUpdate(function () {
|
||||
debouncedPopulate()
|
||||
})
|
||||
|
||||
$scope.$on('$destroy', xoApi.onUpdate(debouncedPopulate))
|
||||
})
|
||||
|
||||
.name
|
||||
|
||||
@@ -100,4 +100,34 @@
|
||||
.progress-condensed
|
||||
.progress-bar(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.usage}}", aria-valuemax="{{SR.size}}", style="width: {{[SR.physical_usage, SR.size] | percentage}}", tooltip="Physical usage: {{[SR.physical_usage, SR.size] | percentage}}")
|
||||
.progress-bar.progress-bar-info(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.physical_usage}}", aria-valuemax="{{SR.size}}", style="width: {{[(SR.usage-SR.physical_usage), SR.size] | percentage}}", tooltip="Allocated: {{[(SR.usage), SR.size] | percentage}}")
|
||||
|
||||
.grid-sm
|
||||
.grid-cell
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-refresh
|
||||
| Updates
|
||||
span.quick-edit(
|
||||
tooltip="Update all"
|
||||
ng-click="installAllPatches()"
|
||||
)
|
||||
i.fa.fa-download.fa-fw
|
||||
.panel-body
|
||||
table.table.table-hover
|
||||
tr
|
||||
th Pool
|
||||
th Host
|
||||
th Description
|
||||
th Missing patches
|
||||
th Install
|
||||
tbody(ng-repeat="pool in pools.all | orderBy:'name_label'")
|
||||
tr( ng-repeat="host in hostsByPool[pool.id]" ng-if="nbUpdates[host.id]")
|
||||
td.oneliner
|
||||
| {{ pool.name_label }}
|
||||
td.oneliner
|
||||
| {{ host.name_label }}
|
||||
td.oneliner
|
||||
| {{ host.name_description }}
|
||||
td {{ nbUpdates[host.id] }}
|
||||
td
|
||||
button.btn.btn-success(ng-click="installHostPatches(host.id)" tooltip="Install {{ nbUpdates[host.id] }} patch(es)")
|
||||
| Update host
|
||||
|
||||
@@ -6,6 +6,8 @@ omit = require 'lodash.omit'
|
||||
sum = require 'lodash.sum'
|
||||
throttle = require 'lodash.throttle'
|
||||
find = require 'lodash.find'
|
||||
filter = require 'lodash.filter'
|
||||
pluck = require 'lodash.pluck'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
@@ -213,6 +215,14 @@ module.exports = angular.module 'xoWebApp.host', [
|
||||
}).then ->
|
||||
xo.host.stop id
|
||||
|
||||
|
||||
$scope.emergencyShutdownHost = (hostId) ->
|
||||
modal.confirm({
|
||||
title: 'Shutdown host'
|
||||
message: 'Are you sure you want to suspend all the VMs on this host and shut the host down?'
|
||||
}).then ->
|
||||
xo.host.emergencyShutdownHost hostId
|
||||
|
||||
$scope.saveHost = ($data) ->
|
||||
{host} = $scope
|
||||
{name_label, name_description, enabled} = $data
|
||||
@@ -410,5 +420,23 @@ module.exports = angular.module 'xoWebApp.host', [
|
||||
netOnly: false,
|
||||
loadOnly: false
|
||||
}
|
||||
|
||||
$scope.canAdmin = (id = undefined) ->
|
||||
if id == undefined
|
||||
id = $scope.host && $scope.host.id
|
||||
|
||||
return id && xoApi.canInteract(id, 'administrate') || false
|
||||
|
||||
$scope.canOperate = (id = undefined) ->
|
||||
if id == undefined
|
||||
id = $scope.host && $scope.host.id
|
||||
|
||||
return id && xoApi.canInteract(id, 'operate') || false
|
||||
|
||||
$scope.canView = (id = undefined) ->
|
||||
if id == undefined
|
||||
id = $scope.host && $scope.host.id
|
||||
|
||||
return id && xoApi.canInteract(id, 'view') || false
|
||||
# A module exports its name.
|
||||
.name
|
||||
|
||||
@@ -13,8 +13,10 @@
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-cogs
|
||||
| General
|
||||
span.quick-edit(tooltip="Edit General settings", ng-click="hostSettings.$show()")
|
||||
span.quick-edit(tooltip="Edit General settings", ng-click="hostSettings.$show()", ng-if = '!hostSettings.$visible && canAdmin()')
|
||||
i.fa.fa-edit.fa-fw
|
||||
span.quick-edit(ng-if="hostSettings.$visible", tooltip="Cancel Edition", ng-click="hostSettings.$cancel()")
|
||||
i.fa.fa-undo.fa-fw
|
||||
.panel-body
|
||||
form(editable-form="", name="hostSettings", onbeforesave="saveHost($data)")
|
||||
dl.dl-horizontal
|
||||
@@ -186,7 +188,7 @@
|
||||
i.xo-icon-loading
|
||||
| Fetching stats...
|
||||
//- Action panel
|
||||
.grid-sm
|
||||
.grid-sm(ng-if = 'canOperate()')
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-flash
|
||||
@@ -195,35 +197,39 @@
|
||||
.grid-sm.grid--gutters
|
||||
.grid.grid-cell
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Add SR", tooltip-placement="top", type="button", style="width: 90%", xo-sref="SRs_new({container: host.id})")
|
||||
button.btn(tooltip="Add SR", tooltip-placement="top", type="button", style="width: 90%", xo-sref="SRs_new({container: host.id})", ng-if = 'canAdmin()')
|
||||
i.xo-icon-sr.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Add VM", tooltip-placement="top", type="button", style="width: 90%", xo-sref="VMs_new({container: host.id})")
|
||||
button.btn(tooltip="Add VM", tooltip-placement="top", type="button", style="width: 90%", xo-sref="VMs_new({container: host.id})", ng-if = 'canAdmin()')
|
||||
i.xo-icon-vm.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Reboot host", tooltip-placement="top", type="button", style="width: 90%", xo-click="rebootHost(host.id)")
|
||||
button.btn(tooltip="Reboot host", tooltip-placement="top", type="button", style="width: 90%", xo-click="rebootHost(host.id)", ng-if = 'canOperate()')
|
||||
i.fa.fa-refresh.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Shutdown host", tooltip-placement="top", type="button", style="width: 90%", xo-click="shutdownHost(host.id)")
|
||||
button.btn(tooltip="Shutdown host", tooltip-placement="top", type="button", style="width: 90%", xo-click="shutdownHost(host.id)", ng-if = 'canOperate()')
|
||||
i.fa.fa-power-off.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Suspend all VMs and shutdown host", tooltip-placement="top", type="button", style="width: 90%", xo-click="emergencyShutdownHost(host.id)", ng-if = 'canOperate()')
|
||||
i.fa.fa-exclamation-triangle.fa-2x.fa-fw
|
||||
.grid.grid-cell
|
||||
.grid-cell.btn-group(ng-if="host.enabled")
|
||||
button.btn(tooltip="Disable host", tooltip-placement="top", type="button", style="width: 90%", xo-click="disableHost(host.id)")
|
||||
button.btn(tooltip="Disable host", tooltip-placement="top", type="button", style="width: 90%", xo-click="disableHost(host.id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-times-circle.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(ng-if="!host.enabled")
|
||||
button.btn(tooltip="Enable host", tooltip-placement="top", type="button", style="width: 90%", xo-click="enableHost(host.id)")
|
||||
button.btn(tooltip="Enable host", tooltip-placement="top", type="button", style="width: 90%", xo-click="enableHost(host.id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-check-circle.fa-2x.fa-fw
|
||||
.grid.grid-cell
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Restart toolstack", tooltip-placement="top", type="button", style="width: 90%", xo-click="restartToolStack(host.id)")
|
||||
button.btn(tooltip="Restart toolstack", tooltip-placement="top", type="button", style="width: 90%", xo-click="restartToolStack(host.id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-retweet.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(ng-if="pool.name_label && (hostsByPool[pool.id] | count)>1")
|
||||
button.btn(tooltip="Remove from pool", tooltip-placement="top", style="width: 90%", type="button", xo-click="pool_removeHost(host.id)")
|
||||
button.btn(tooltip="Remove from pool", tooltip-placement="top", style="width: 90%", type="button", xo-click="pool_removeHost(host.id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-cloud-upload.fa-2x.fa-fw
|
||||
.grid-cell.btn-group.dropdown(
|
||||
ng-if="pool.name_label && (hostsByPool[pool.id] | count)==1"
|
||||
dropdown
|
||||
)
|
||||
button.btn.dropdown-toggle(
|
||||
ng-if = 'canAdmin()'
|
||||
dropdown-toggle
|
||||
tooltip="Move host to another pool"
|
||||
tooltip-placement="top"
|
||||
@@ -238,10 +244,11 @@
|
||||
i.xo-icon-host.fa-fw
|
||||
| To {{p.name_label}}
|
||||
.grid-cell.btn-group(ng-if="!pool.name_label")
|
||||
button.btn(tooltip="Add to pool", tooltip-placement="top", style="width: 90%", type="button", xo-click="pool_addHost(host.id)")
|
||||
button.btn(tooltip="Add to pool", tooltip-placement="top", style="width: 90%", type="button", xo-click="pool_addHost(host.id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-cloud-download.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(style="margin-bottom: 0.5em")
|
||||
button.btn(
|
||||
ng-if = 'canAdmin()'
|
||||
tooltip="Import VM"
|
||||
tooltip-placement="top"
|
||||
type="button"
|
||||
@@ -305,14 +312,14 @@
|
||||
td(ng-if="SRsToPBDs[SR.id].attached")
|
||||
span.label.label-success Connected
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(tooltip="Disconnect this SR", xo-click="disconnectPBD(SRsToPBDs[SR.id].id)")
|
||||
a(tooltip="Disconnect this SR", xo-click="disconnectPBD(SRsToPBDs[SR.id].id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-unlink.fa-lg
|
||||
td(ng-if="!SRsToPBDs[SR.id].attached")
|
||||
span.label.label-default Disconnected
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(tooltip="Reconnect this SR", xo-click="connectPBD(SRsToPBDs[SR.id].id)")
|
||||
a(tooltip="Reconnect this SR", xo-click="connectPBD(SRsToPBDs[SR.id].id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-link.fa-lg
|
||||
a(tooltip="Forget this SR", xo-click="removePBD(SRsToPBDs[SR.id].id)")
|
||||
a(tooltip="Forget this SR", xo-click="removePBD(SRsToPBDs[SR.id].id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-ban.fa-lg
|
||||
//- Local SR
|
||||
//- TODO: migrate to SRs and not PBDs when implemented in xo-server spec
|
||||
@@ -330,14 +337,14 @@
|
||||
td(ng-if="SRsToPBDs[SR.id].attached")
|
||||
span.label.label-success Connected
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(tooltip="Disconnect this SR", xo-click="disconnectPBD(SRsToPBDs[SR.id].id)")
|
||||
a(tooltip="Disconnect this SR", xo-click="disconnectPBD(SRsToPBDs[SR.id].id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-unlink.fa-lg
|
||||
td(ng-if="!SRsToPBDs[SR.id].attached")
|
||||
span.label.label-default Disconnected
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(tooltip="Reconnect this SR", xo-click="connectPBD(SRsToPBDs[SR.id].id)")
|
||||
a(tooltip="Reconnect this SR", xo-click="connectPBD(SRsToPBDs[SR.id].id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-link.fa-lg
|
||||
a(tooltip="Forget this SR", xo-click="removePBD(SRsToPBDs[SR.id].id)")
|
||||
a(tooltip="Forget this SR", xo-click="removePBD(SRsToPBDs[SR.id].id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-ban.fa-lg
|
||||
//- Networks/Interfaces panel
|
||||
.grid-sm
|
||||
@@ -355,7 +362,7 @@
|
||||
th.col-md-1 Link status
|
||||
tr(ng-repeat="PIF in host.$PIFs | resolve | orderBy:natural('name_label') track by PIF.id")
|
||||
td
|
||||
| {{PIF.device}}
|
||||
| {{PIF.device}}
|
||||
span.label.label-primary(ng-if="PIF.management") XAPI
|
||||
td
|
||||
span(ng-if="PIF.vlan > -1")
|
||||
@@ -368,23 +375,23 @@
|
||||
td(ng-if="PIF.attached")
|
||||
span.label.label-success Connected
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(tooltip="Disconnect this interface", xo-click="disconnectPIF(PIF.id)")
|
||||
a(tooltip="Disconnect this interface", xo-click="disconnectPIF(PIF.id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-unlink.fa-lg
|
||||
td(ng-if="!PIF.attached")
|
||||
span.label.label-default Disconnected
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(tooltip="Connect this interface", xo-click="connectPIF(PIF.id)")
|
||||
a(tooltip="Connect this interface", xo-click="connectPIF(PIF.id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-link.fa-lg
|
||||
a(tooltip="Remove this interface", xo-click="removePIF(PIF.id)")
|
||||
a(tooltip="Remove this interface", xo-click="removePIF(PIF.id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-trash-o.fa-lg
|
||||
.text-right
|
||||
button.btn(type="button", ng-class = '{"btn-success": creatingNetwork, "btn-primary": !creatingNetwork}', ng-click="creatingNetwork = !creatingNetwork")
|
||||
button.btn(type="button", ng-class = '{"btn-success": creatingNetwork, "btn-primary": !creatingNetwork}', ng-click="creatingNetwork = !creatingNetwork", ng-hide = '!canAdmin()', ng-disabled = '!canAdmin()')
|
||||
i.fa.fa-plus(ng-if = '!creatingNetwork')
|
||||
i.fa.fa-minus(ng-if = 'creatingNetwork')
|
||||
| Create Network
|
||||
br
|
||||
form.form-inline.text-right#createNetworkForm(ng-if = 'creatingNetwork', name = 'createNetworkForm', ng-submit = 'createNetwork(newNetworkName, newNetworkDescription, newNetworkPIF, newNetworkMTU, newNetworkVlan)')
|
||||
fieldset(ng-attr-disabled = '{{ createNetworkWaiting ? true : undefined }}')
|
||||
fieldset(ng-disabled = 'createNetworkWaiting || !canAdmin()')
|
||||
.form-group
|
||||
label(for = 'newNetworkPIF') Interface
|
||||
select.form-control(ng-model = 'newNetworkPIF', ng-change = 'updateMTU(newNetworkPIF)', ng-options='(PIF | resolve).device for PIF in host.$PIFs')
|
||||
@@ -439,7 +446,7 @@
|
||||
| {{task.progress*100 | number:1}}%
|
||||
td.oneliner
|
||||
| {{task.name_label}}
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
span.pull-right.btn-group.quick-buttons(ng-if = 'canAdmin()')
|
||||
a(xo-click="cancelTask(task.id)")
|
||||
i.fa.fa-times.fa-lg(tooltip="Cancel this task")
|
||||
a(xo-click="destroyTask(task.id)")
|
||||
@@ -451,7 +458,7 @@
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-comments
|
||||
| Logs
|
||||
span.quick-edit(ng-if="host.messages | isNotEmpty", tooltip="Remove all logs", ng-click="deleteAllLog()")
|
||||
span.quick-edit(ng-if="(host.messages | isNotEmpty) && canAdmin()", tooltip="Remove all logs", ng-click="deleteAllLog()")
|
||||
i.fa.fa-trash-o.fa-fw
|
||||
.panel-body
|
||||
p.center(ng-if="host.messages | isEmpty") No recent logs
|
||||
@@ -462,7 +469,7 @@
|
||||
td {{message.time*1e3 | date:"medium"}}
|
||||
td
|
||||
| {{message.name}}
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
span.pull-right.btn-group.quick-buttons(ng-if = 'canAdmin()')
|
||||
a(xo-click="deleteLog(message.id)")
|
||||
i.fa.fa-trash-o.fa-lg(tooltip="Remove this log entry")
|
||||
.center(ng-if = '(host.messages | count) > 5 || currentLogPage > 1')
|
||||
@@ -475,7 +482,7 @@
|
||||
| Patches
|
||||
span.quick-edit(ng-click="listMissingPatches(host.id)", tooltip="Check for updates")
|
||||
i.fa.fa-question-circle
|
||||
span.quick-edit(ng-click="installAllPatches(host.id)", tooltip="Install all the missing patches", style="margin-right:5px")
|
||||
span.quick-edit(ng-click="installAllPatches(host.id)", tooltip="Install all the missing patches", style="margin-right:5px", ng-if = 'canAdmin()')
|
||||
i.fa.fa-download
|
||||
.panel-body
|
||||
table.table.table-hover(ng-if="poolPatches || updates")
|
||||
@@ -494,8 +501,9 @@
|
||||
td.oneliner {{patch.date | date:"medium"}}
|
||||
td -
|
||||
td
|
||||
span(ng-click="installPatch(host.id, patch.uuid)", tooltip="Click to install the patch on this host")
|
||||
span(ng-click="installPatch(host.id, patch.uuid)", tooltip="Click to install the patch on this host", ng-if = 'canAdmin()')
|
||||
span.label.label-danger Missing
|
||||
span.label.label-danger(ng-if = '!canAdmin()') Missing
|
||||
tr(ng-repeat="patch in poolPatches | map | slice:(5*(currentPatchPage-1)):(5*currentPatchPage)")
|
||||
td.oneliner {{patch.name}}
|
||||
td.oneliner {{patch.description}}
|
||||
@@ -505,8 +513,10 @@
|
||||
td
|
||||
span(ng-if="isPoolPatchApplied(patch)")
|
||||
span.label.label-success Applied
|
||||
span(ng-click="installPatch(host.id, patch.uuid)", ng-if="!isPoolPatchApplied(patch)", tooltip="Click to apply the patch on this host")
|
||||
span.label.label-warning Not applied
|
||||
span(ng-if="!isPoolPatchApplied(patch)")
|
||||
span(ng-click="installPatch(host.id, patch.uuid)", tooltip="Click to apply the patch on this host", ng-if = 'canAdmin()')
|
||||
span.label.label-warning Not applied
|
||||
span.label.label-warning(ng-if = '!canAdmin()') Not applied
|
||||
.center(ng-if = '(poolPatches | count) > 5 || currentPatchPage > 1')
|
||||
pagination(boundary-links="true", total-items="poolPatches | count", ng-model="$parent.currentPatchPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import angular from 'angular'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
import xoTag from 'tag'
|
||||
import includes from 'lodash.includes'
|
||||
|
||||
import xoApi from 'xo-api'
|
||||
|
||||
@@ -20,7 +21,7 @@ export default angular.module('xoWebApp.list', [
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('ListCtrl', function (xoApi) {
|
||||
.controller('ListCtrl', function (xoApi, $scope, $rootScope) {
|
||||
this.hosts = xoApi.getView('host')
|
||||
this.pools = xoApi.getView('pool')
|
||||
this.SRs = xoApi.getView('SR')
|
||||
@@ -29,7 +30,84 @@ export default angular.module('xoWebApp.list', [
|
||||
this.hostsByPool = xoApi.getIndex('hostsByPool')
|
||||
this.runningHostsByPool = xoApi.getIndex('runningHostsByPool')
|
||||
this.vmsByContainer = xoApi.getIndex('vmsByContainer')
|
||||
})
|
||||
$scope.canView = function (id) {
|
||||
return xoApi.canInteract(id, 'view')
|
||||
}
|
||||
|
||||
$scope.shouldAppear = (obj) => {
|
||||
// States
|
||||
const powerState = obj.power_state
|
||||
// If there is a search option on the power state (running or halted),
|
||||
// then objects that do not have a power_state (eg: SRs) are not displayed
|
||||
if (($scope.states['running'] !== 2 || $scope.states['halted'] !== 2) && !powerState) return false
|
||||
if (powerState) {
|
||||
if ($scope.states[powerState.toLowerCase()] === 0) return false
|
||||
if (($scope.states['running'] === 1 || $scope.states['halted'] === 1) &&
|
||||
$scope.states[powerState.toLowerCase()] !== 1) return false
|
||||
}
|
||||
|
||||
if ($scope.states['disconnected'] !== 2 && !obj.$PBDs) return false
|
||||
let disconnected = false
|
||||
if (obj.$PBDs) {
|
||||
for (const id of obj.$PBDs) {
|
||||
const pbd = xoApi.get(id)
|
||||
disconnected |= !pbd.attached
|
||||
}
|
||||
if ($scope.states['disconnected'] === 0 && disconnected) return false
|
||||
if ($scope.states['disconnected'] === 1 && !disconnected) return false
|
||||
}
|
||||
|
||||
// Types
|
||||
if ($scope.types[obj.type.toLowerCase()] === 0) return false
|
||||
if ($scope.types[obj.type.toLowerCase()] === 2 && includes($scope.types, 1)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const _initOptions = () => {
|
||||
$scope.types = {
|
||||
'host': 2,
|
||||
'pool': 2,
|
||||
'sr': 2,
|
||||
'vm': 2
|
||||
}
|
||||
$scope.states = {
|
||||
'running': 2,
|
||||
'halted': 2,
|
||||
'disconnected': 2
|
||||
}
|
||||
}
|
||||
_initOptions()
|
||||
|
||||
$scope.parsedListFilter = $scope.listFilter
|
||||
$rootScope.searchParse = () => {
|
||||
let keyWords = []
|
||||
const words = $scope.listFilter ? $scope.listFilter.split(' ') : ['']
|
||||
_initOptions()
|
||||
for (const word of words) {
|
||||
let isOption = word.charAt(0) === '*'
|
||||
const isNegation = word.charAt(0) === '!'
|
||||
// as long as there is a '!', it is an option. ie !vm <=> !*vm
|
||||
isOption = isOption || isNegation
|
||||
let option = (isNegation ? word.substring(1, word.length) : word).toLowerCase()
|
||||
option = option.charAt(0) === '*' ? option.substring(1, option.length) : option
|
||||
if (!isOption) {
|
||||
if (option !== '') keyWords.push(option)
|
||||
} else {
|
||||
if ($scope.types.hasOwnProperty(option)) {
|
||||
$scope.types[option] = isNegation ? 0 : 1
|
||||
} else if ($scope.states.hasOwnProperty(option)) {
|
||||
$scope.states[option] = isNegation ? 0 : 1
|
||||
}
|
||||
}
|
||||
}
|
||||
$scope.parsedListFilter = keyWords.join(' ')
|
||||
}
|
||||
|
||||
$scope.onClick = (type) => {
|
||||
$rootScope.options[type.toLowerCase()] = !$rootScope.options[type.toLowerCase()]
|
||||
$rootScope.updateListFilter(type.toLowerCase())
|
||||
}
|
||||
})
|
||||
// A module exports its name.
|
||||
.name
|
||||
|
||||
@@ -1,7 +1,44 @@
|
||||
.sub-bar
|
||||
.grid(style="margin-left:1em")
|
||||
.btn-group.dropdown(dropdown)
|
||||
a.btn.navbar-btn.dropdown-toggle.filter(dropdown-toggle)
|
||||
| Types
|
||||
i.fa.fa-caret-down
|
||||
ul.dropdown-menu.inverse(role="menu" style="color:white")
|
||||
li(
|
||||
ng-repeat = "type in ['VM', 'SR', 'Host', 'Pool']"
|
||||
ng-click='onClick(type)'
|
||||
)
|
||||
|  
|
||||
label(ng-click)
|
||||
i.fa.fa-square-o(ng-if='!options[type.toLowerCase()]')
|
||||
i.fa.fa-check-square-o(ng-if='options[type.toLowerCase()]')
|
||||
| {{type}}
|
||||
.btn-group.dropdown(dropdown)
|
||||
a.btn.navbar-btn.dropdown-toggle.filter(dropdown-toggle)
|
||||
| States
|
||||
i.fa.fa-caret-down
|
||||
ul.dropdown-menu.inverse(role="menu" style="color:white")
|
||||
li(
|
||||
ng-repeat = "state in ['Running', 'Halted', 'Disconnected']"
|
||||
ng-click='onClick(state)'
|
||||
)
|
||||
|  
|
||||
label(ng-click)
|
||||
i.fa.fa-square-o(ng-if='!options[state.toLowerCase()]')
|
||||
i.fa.fa-check-square-o(ng-if='options[state.toLowerCase()]')
|
||||
| {{state}}
|
||||
//- TODO: print a message when no entries.
|
||||
|
||||
//- FIXME: Ugly trick to force the results to be under the sub bar.
|
||||
div(style="margin-top: 50px; visibility: hidden; height: 0") .
|
||||
|
||||
//- If it's a (named) pool.
|
||||
.grid.flat-object(ng-repeat="pool in list.pools.all | xoHideUnauthorized | filter:listFilter | orderBy:natural('name_label') track by pool.id", ng-if="pool.name_label", xo-sref="pools_view({id: pool.id})")
|
||||
.grid.flat-object(
|
||||
ng-repeat="pool in list.pools.all | xoHideUnauthorized | filter:parsedListFilter | orderBy:natural('name_label') track by pool.id"
|
||||
ng-if="pool.name_label && shouldAppear(pool)"
|
||||
xo-sref="pools_view({id: pool.id})"
|
||||
)
|
||||
//- Icon.
|
||||
.grid-cell.flat-cell.flat-cell-type
|
||||
i.xo-icon-pool
|
||||
@@ -43,7 +80,11 @@
|
||||
//- /Properties & tags.
|
||||
//- /Pool.
|
||||
//- If it's a host.
|
||||
.grid.flat-object(ng-repeat="host in list.hosts.all | xoHideUnauthorized | filter:listFilter | orderBy:natural('name_label') track by host.id", xo-sref="hosts_view({id: host.id})")
|
||||
.grid.flat-object(
|
||||
ng-repeat="host in list.hosts.all | xoHideUnauthorized | filter:parsedListFilter | orderBy:natural('name_label') track by host.id"
|
||||
ng-if="shouldAppear(host)"
|
||||
xo-sref="hosts_view({id: host.id})"
|
||||
)
|
||||
//- Icon.
|
||||
.grid-cell.flat-cell.flat-cell-type
|
||||
i.xo-icon-host(class="xo-color-{{host.power_state | lowercase}}")
|
||||
@@ -76,7 +117,11 @@
|
||||
//- /Properties & tags.
|
||||
//- /Host.
|
||||
//- If it's a VM.
|
||||
.grid.flat-object(ng-repeat="VM in list.VMs.all | xoHideUnauthorized | filter:listFilter | orderBy:natural('name_label') track by VM.id", xo-sref="VMs_view({id: VM.id})")
|
||||
.grid.flat-object(
|
||||
ng-repeat="VM in list.VMs.all | xoHideUnauthorized | filter:parsedListFilter | orderBy:natural('name_label') track by VM.id"
|
||||
ng-if="shouldAppear(VM)"
|
||||
xo-sref="VMs_view({id: VM.id})"
|
||||
)
|
||||
//- Icon.
|
||||
.grid-cell.flat-cell.flat-cell-type
|
||||
i.xo-icon-vm(class="xo-color-{{VM.power_state | lowercase}}")
|
||||
@@ -94,14 +139,14 @@
|
||||
| {{VM.CPUs.number}} vCPUs
|
||||
.grid-cell.flat-cell
|
||||
| {{VM.memory.size | bytesToSize}} RAM
|
||||
.grid-cell.flat-cell(ng-init="container = (VM.$container | resolve)")
|
||||
.grid-cell.flat-cell(ng-init="container = (VM.$container | resolve)", ng-if="canView((VM.$container | resolve).id)")
|
||||
div(ng-if="'pool' === container.type")
|
||||
| Resident on:
|
||||
a(ui-sref="pools_view({id: container.id})") {{container.name_label}}
|
||||
div(ng-if="'host' === container.type", ng-init="pool = (container.$poolId | resolve)")
|
||||
| Resident on:
|
||||
a(ui-sref="hosts_view({id: container.id})") {{container.name_label}}
|
||||
small(ng-if="pool.name_label")
|
||||
small(ng-if="pool.name_label && canView(pool.id)")
|
||||
| (
|
||||
a(ui-sref="pools_view({id: pool.id})") {{pool.name_label}}
|
||||
| )
|
||||
@@ -116,7 +161,11 @@
|
||||
//- /Properties & tags.
|
||||
//- /VM.
|
||||
//- If it's a SR.
|
||||
.grid.flat-object(ng-repeat="SR in list.SRs.all | xoHideUnauthorized | filter:listFilter | orderBy:natural('name_label') track by SR.id", xo-sref="SRs_view({id: SR.id})")
|
||||
.grid.flat-object(
|
||||
ng-repeat="SR in list.SRs.all | xoHideUnauthorized | filter:parsedListFilter | orderBy:natural('name_label') track by SR.id"
|
||||
ng-if="shouldAppear(SR)"
|
||||
xo-sref="SRs_view({id: SR.id})"
|
||||
)
|
||||
//- Icon.
|
||||
.grid-cell.flat-cell.flat-cell-type
|
||||
i.xo-icon-sr
|
||||
|
||||
@@ -3,6 +3,7 @@ import uiRouter from 'angular-ui-router'
|
||||
|
||||
import updater from '../updater'
|
||||
import xoServices from 'xo-services'
|
||||
import includes from 'lodash.includes'
|
||||
|
||||
import view from './view'
|
||||
|
||||
@@ -14,7 +15,7 @@ export default angular.module('xoWebApp.navbar', [
|
||||
updater,
|
||||
xoServices
|
||||
])
|
||||
.controller('NavbarCtrl', function ($state, xoApi, xo, $scope, updater) {
|
||||
.controller('NavbarCtrl', function ($state, xoApi, xo, $scope, updater, $rootScope) {
|
||||
this.updater = updater
|
||||
// TODO: It would make sense to inject xoApi in the scope.
|
||||
Object.defineProperties(this, {
|
||||
@@ -31,9 +32,82 @@ export default angular.module('xoWebApp.navbar', [
|
||||
}
|
||||
|
||||
// When a searched is entered, we must switch to the list view if
|
||||
// necessary.
|
||||
this.ensureListView = function () {
|
||||
$state.go('list')
|
||||
// necessary. When the text field is empty again, we must swith
|
||||
// to tree view
|
||||
let timeout
|
||||
$scope.ensureListView = function (listFilter) {
|
||||
clearTimeout(timeout)
|
||||
timeout = window.setTimeout(function () {
|
||||
$state.go('list').then(() =>
|
||||
$rootScope.searchParse(),
|
||||
$scope.updateOptions()
|
||||
)
|
||||
}, 400)
|
||||
}
|
||||
|
||||
const _isOption = function (word, option) {
|
||||
if (word === '*' + option || word === '!' + option || word === '!*' + option) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
const _removeOption = function (option) {
|
||||
if (!$scope.$root.listFilter) {
|
||||
return
|
||||
}
|
||||
const words = $scope.$root.listFilter.split(' ')
|
||||
$scope.$root.listFilter = ''
|
||||
for (const word of words) {
|
||||
if (!_isOption(word, option) && word !== '') {
|
||||
$scope.$root.listFilter += word + ' '
|
||||
}
|
||||
}
|
||||
}
|
||||
const _addOption = function (option) {
|
||||
if (!$scope.$root.listFilter) {
|
||||
$scope.$root.listFilter = '*' + option + ' '
|
||||
return
|
||||
}
|
||||
const words = $scope.$root.listFilter.split(' ')
|
||||
if (!includes(words, '*' + option) && !includes(words, '!' + option) && !includes(words, '!*' + option)) {
|
||||
if ($scope.$root.listFilter.charAt($scope.$root.listFilter.length - 1) !== ' ') {
|
||||
$scope.$root.listFilter += ' '
|
||||
}
|
||||
$scope.$root.listFilter += '*' + option + ' '
|
||||
}
|
||||
}
|
||||
|
||||
$rootScope.options = {
|
||||
'vm': false,
|
||||
'sr': false,
|
||||
'host': false,
|
||||
'pool': false,
|
||||
'running': false,
|
||||
'halted': false,
|
||||
'disconnected': false
|
||||
}
|
||||
// Checkboxes --> Text
|
||||
// Update text field after a checkbox has been clicked
|
||||
$rootScope.updateListFilter = function (option) {
|
||||
if ($rootScope.options[option]) {
|
||||
_addOption(option)
|
||||
} else {
|
||||
_removeOption(option)
|
||||
}
|
||||
$scope.ensureListView($scope.$root.listFilter)
|
||||
}
|
||||
// Text --> Checkboxes
|
||||
// Update checkboxes after the text field has been changed
|
||||
$scope.updateOptions = function () {
|
||||
const words = $scope.$root.listFilter ? $scope.$root.listFilter.split(' ') : ['']
|
||||
for (const opt in $rootScope.options) {
|
||||
$rootScope.options[opt] = false
|
||||
for (let word of words) {
|
||||
if (_isOption(word, opt)) {
|
||||
$rootScope.options[opt] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.tasks = xoApi.getView('runningTasks')
|
||||
|
||||
@@ -21,12 +21,12 @@ nav.navbar.navbar-inverse.navbar-fixed-top(role = 'navigation')
|
||||
type = 'text'
|
||||
placeholder = ''
|
||||
ng-model = '$root.listFilter'
|
||||
ng-change = 'navbar.ensureListView()'
|
||||
ng-change = 'ensureListView($root.listFilter)'
|
||||
)
|
||||
span.input-group-btn
|
||||
button.btn.btn-search(
|
||||
type = 'button'
|
||||
ng-click = 'navbar.ensureListView()'
|
||||
ng-click = 'ensureListView($root.listFilter)'
|
||||
)
|
||||
i.fa.fa-search
|
||||
//- /Search form.
|
||||
@@ -92,15 +92,15 @@ nav.navbar.navbar-inverse.navbar-fixed-top(role = 'navigation')
|
||||
a(ui-sref="dashboard.index")
|
||||
i.fa.fa-dashboard
|
||||
| Dashboard
|
||||
//- li.disabled(ui-sref-active="active")
|
||||
//- a(ui-sref="graph")
|
||||
//- i.fa.fa-sitemap
|
||||
//- | Graphs view
|
||||
li.divider
|
||||
li(ng-class = '{ disabled: navbar.user.permission !== "admin" }')
|
||||
a(ui-sref = 'backup.index')
|
||||
i.fa.fa-archive
|
||||
| Backup
|
||||
li
|
||||
a(ui-sref = 'taskscheduler.index')
|
||||
i.fa.fa-cogs
|
||||
| Job Manager
|
||||
li.divider
|
||||
li(
|
||||
ui-sref-active = 'active'
|
||||
|
||||
@@ -11,16 +11,28 @@ module.exports = angular.module 'xoWebApp.newVm', [
|
||||
.config ($stateProvider) ->
|
||||
$stateProvider.state 'VMs_new',
|
||||
url: '/vms/new/:container'
|
||||
controller: 'NewVmsCtrl'
|
||||
controller: 'NewVmsCtrl as ctrl'
|
||||
template: require './view'
|
||||
.controller 'NewVmsCtrl', (
|
||||
$scope, $stateParams, $state
|
||||
xoApi, xo
|
||||
bytesToSizeFilter, sizeToBytesFilter
|
||||
bytesToSizeFilter
|
||||
notify
|
||||
) ->
|
||||
{get} = xoApi
|
||||
$scope.configDriveActive = false
|
||||
existingDisks = {}
|
||||
$scope.saveChange = (position, propertyName, value) ->
|
||||
if not existingDisks[position]?
|
||||
existingDisks[position] = {}
|
||||
existingDisks[position][propertyName] = value
|
||||
$scope.initExistingDiskSizes = (template) ->
|
||||
sizes = {}
|
||||
forEach xoApi.get(template.$VBDs), (VBD) ->
|
||||
sizes[VBD.position] = bytesToSizeFilter xoApi.get(VBD.VDI).size
|
||||
$scope.existingDiskSizes = sizes
|
||||
|
||||
|
||||
{get} = xoApi
|
||||
removeItems = do ->
|
||||
splice = Array::splice.call.bind Array::splice
|
||||
(array, index, n) -> splice array, index, n ? 1
|
||||
@@ -103,7 +115,6 @@ module.exports = angular.module 'xoWebApp.newVm', [
|
||||
default_SR = get pool.default_SR
|
||||
default_SR = if default_SR then default_SR.id else ''
|
||||
)
|
||||
|
||||
$scope.availableMethods = {}
|
||||
$scope.CPUs = ''
|
||||
$scope.pv_args = ''
|
||||
@@ -114,9 +125,11 @@ module.exports = angular.module 'xoWebApp.newVm', [
|
||||
$scope.name_description = ''
|
||||
$scope.name_label = ''
|
||||
$scope.template = ''
|
||||
$scope.firstSR = ''
|
||||
$scope.VDIs = []
|
||||
$scope.VIFs = []
|
||||
$scope.isDiskTemplate = false
|
||||
$scope.cloudConfigSshKey = ''
|
||||
|
||||
$scope.addVIF = do ->
|
||||
id = 0
|
||||
@@ -150,6 +163,8 @@ module.exports = angular.module 'xoWebApp.newVm', [
|
||||
# When the selected template changes, updates other variables.
|
||||
$scope.$watch 'template', (template) ->
|
||||
return unless template
|
||||
# After each template change, initialize coreOsCloudConfig to empty
|
||||
$scope.coreOsCloudConfig = ''
|
||||
|
||||
{install_methods} = template.template_info
|
||||
availableMethods = $scope.availableMethods = Object.create null
|
||||
@@ -171,8 +186,14 @@ module.exports = angular.module 'xoWebApp.newVm', [
|
||||
else $scope.isDiskTemplate = false
|
||||
for VDI in VDIs
|
||||
VDI.id = VDI_id++
|
||||
VDI.size = bytesToSizeFilter VDI.size
|
||||
VDI.SR or= default_SR
|
||||
VDI.size = bytesToSizeFilter VDI.size
|
||||
# if the template is labeled CoreOS
|
||||
# we'll use config drive setup
|
||||
if template.name_label == 'CoreOS'
|
||||
return xo.vm.getCloudInitConfig template.id
|
||||
.then (result) ->
|
||||
$scope.coreOsCloudConfig = result
|
||||
|
||||
$scope.createVM = ->
|
||||
{
|
||||
@@ -191,14 +212,16 @@ module.exports = angular.module 'xoWebApp.newVm', [
|
||||
# Does not edit the displayed data directly.
|
||||
VDIs = cloneDeep VDIs
|
||||
for VDI, index in VDIs
|
||||
# store the first VDI's SR for later use (e.g: coreOsCloudConfig)
|
||||
if VDI.id == 0
|
||||
$scope.firstSR = VDI.SR or default_SR
|
||||
|
||||
# Removes the dummy identifier used for AngularJS.
|
||||
delete VDI.id
|
||||
|
||||
# Adds the device number based on the index.
|
||||
VDI.device = "#{index}"
|
||||
|
||||
# Transforms the size from human readable format to bytes.
|
||||
VDI.size = sizeToBytesFilter VDI.size
|
||||
# TODO: handles invalid values.
|
||||
|
||||
# Does not edit the displayed data directly.
|
||||
@@ -240,6 +263,7 @@ module.exports = angular.module 'xoWebApp.newVm', [
|
||||
template: template.id
|
||||
VDIs
|
||||
VIFs
|
||||
existingDisks
|
||||
}
|
||||
|
||||
# TODO:
|
||||
@@ -249,7 +273,6 @@ module.exports = angular.module 'xoWebApp.newVm', [
|
||||
title: 'VM creation'
|
||||
message: 'VM creation started'
|
||||
}
|
||||
|
||||
xoApi.call('vm.create', data).then (id) ->
|
||||
# If nothing to sets, just stops.
|
||||
return id unless CPUs or name_description or memory
|
||||
@@ -266,12 +289,27 @@ module.exports = angular.module 'xoWebApp.newVm', [
|
||||
data.pv_args = pv_args
|
||||
|
||||
if memory
|
||||
memory = sizeToBytesFilter memory
|
||||
# FIXME: handles invalid entries.
|
||||
data.memory = memory
|
||||
|
||||
xoApi.call('vm.set', data).then -> id
|
||||
return xoApi.call('vm.set', data).then -> id
|
||||
.then (id) ->
|
||||
# If a CloudConfig drive needs to be created
|
||||
if $scope.coreOsCloudConfig
|
||||
# Use the CoreOS specific Cloud Config creation
|
||||
xo.vm.createCloudInitConfigDrive(id, $scope.firstSR, $scope.coreOsCloudConfig, true).then ->
|
||||
xo.docker.register id
|
||||
if $scope.configDriveActive
|
||||
# User creation is less universal...
|
||||
# $scope.cloudContent = '#cloud-config\nhostname: ' + name_label + '\nusers:\n - name: olivier\n sudo: ALL=(ALL) NOPASSWD:ALL\n groups: sudo\n shell: /bin/bash\n ssh_authorized_keys:\n - ' + $scope.cloudConfigSshKey + '\n'
|
||||
# So keep it basic for now: hostname and ssh key
|
||||
$scope.cloudContent = '#cloud-config\nhostname: ' + name_label + '\nssh_authorized_keys:\n - ' + $scope.cloudConfigSshKey + '\n'
|
||||
# The first SR for a template with an existing disk
|
||||
$scope.firstSR = (get (get template.$VBDs[0]).VDI).$SR
|
||||
# Use the generic CloudConfig creation
|
||||
xo.vm.createCloudInitConfigDrive(id, $scope.firstSR, $scope.cloudContent)
|
||||
|
||||
# Send the client on the VM view
|
||||
$state.go 'VMs_view', { id }
|
||||
.catch (error) ->
|
||||
notify.error {
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.xo-icon-vm
|
||||
| Create VM on
|
||||
| Create VM on
|
||||
a(ng-if="'pool' === container.type", ui-sref="pools_view({id: container.id})")
|
||||
| {{container.name_label}}
|
||||
| {{container.name_label}}
|
||||
a(ng-if="'host' === container.type", ui-sref="hosts_view({id: container.id})")
|
||||
| {{container.name_label}}
|
||||
| {{container.name_label}}
|
||||
//- Add server panel
|
||||
form.form-horizontal(ng-submit="createVM()")
|
||||
.grid
|
||||
@@ -18,7 +18,7 @@ form.form-horizontal(ng-submit="createVM()")
|
||||
.form-group
|
||||
label.col-sm-3.control-label Template
|
||||
.col-sm-9
|
||||
select.form-control(ng-model="template", ng-options="template.name_label for template in templates | orderBy:natural('name_label') track by template.id", required="")
|
||||
select.form-control(ng-model="template", ng-options="template.name_label for template in templates | orderBy:natural('name_label') track by template.id", required="", ng-change = 'initExistingDiskSizes(template)')
|
||||
.form-group
|
||||
label.col-sm-3.control-label Name
|
||||
.col-sm-9
|
||||
@@ -47,12 +47,24 @@ form.form-horizontal(ng-submit="createVM()")
|
||||
| Template info
|
||||
.panel-body
|
||||
p.center This template will create automatically a VM with:
|
||||
.col-md-6
|
||||
ul(ng-repeat="VIF in template.VIFs | resolve | orderBy:natural('device') track by VIF.id")
|
||||
li Interface \#{{VIF.device}} (MTU {{VIF.MTU}}) on {{(VIF.$network | resolve).name_label}}
|
||||
.col-md-6
|
||||
ul(ng-repeat = 'VBD in (template.$VBDs | resolve) track by VBD.id')
|
||||
li Disk {{(VBD.VDI | resolve).name_label}} ({{(VBD.VDI | resolve).size | bytesToSize}}) on {{((VBD.VDI | resolve).$SR | resolve).name_label}}
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-cloud
|
||||
| Config Drive
|
||||
span.pull-right
|
||||
label(style = 'cursor: pointer;')
|
||||
input.hidden(type = 'checkbox', ng-model = '$parent.configDriveActive')
|
||||
i.fa(ng-class = '{"fa-toggle-on": $parent.configDriveActive, "text-success": $parent.configDriveActive, "fa-toggle-off": !$parent.configDriveActive}', style = 'font-size: 1.5em;')
|
||||
.panel-body
|
||||
fieldset(ng-disabled = '!$parent.configDriveActive')
|
||||
.form-group
|
||||
label.col-sm-3.control-label SSH Key
|
||||
.col-sm-9
|
||||
input.form-control(type="text", placeholder="ssh-rsa AAAA.... you@machine", ng-model="$parent.cloudConfigSshKey", name="cloudConfigSshKey", required)
|
||||
.grid(ng-if="!isDiskTemplate")
|
||||
//- Install panel
|
||||
.panel.panel-default
|
||||
@@ -154,8 +166,20 @@ form.form-horizontal(ng-submit="createVM()")
|
||||
i.fa.fa-plus
|
||||
| Add interface
|
||||
//- end of misc and interface panel
|
||||
//- Cloud config panel
|
||||
.grid(ng-if = 'cloudConfig')
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-cloud
|
||||
| Cloud config
|
||||
.pull-right.small
|
||||
button.btn.btn-default(type = 'button', ng-click = 'isExpanded = !isExpanded'): i.fa(ng-class = '{"fa-plus": !isExpanded, "fa-minus": isExpanded}')
|
||||
.panel-body
|
||||
textarea.form-control(rows="20", collapse= '!isExpanded', ng-model='$parent.cloudConfig', name='cloudConfig')
|
||||
| {{cloudConfig}}
|
||||
|
||||
//- Disk panel
|
||||
.grid(ng-if="!isDiskTemplate")
|
||||
.grid
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-disk
|
||||
@@ -170,13 +194,25 @@ form.form-horizontal(ng-submit="createVM()")
|
||||
th.col-md-4 Description
|
||||
th.col-md-1  
|
||||
//- Buttons
|
||||
tr(ng-repeat="VBD in (template.$VBDs | resolve) track by VBD.id", ng-if="isDiskTemplate")
|
||||
td
|
||||
select.form-control(ng-model="(VBD.VDI | resolve).$SR", ng-options="SR.id as (SR.name_label + ' (' + (SR.size - SR.usage | bytesToSize) + ' free)') for SR in (writable_SRs | orderBy:natural('name_label'))", ng-change = 'saveChange(VBD.position, "$SR", (VBD.VDI | resolve).$SR)')
|
||||
td.text-center
|
||||
i.fa.fa-check(ng-if = 'VBD.bootable')
|
||||
td
|
||||
input.form-control(type="text", ng-model="existingDiskSizes[VBD.position]", ng-readonly = '!configDriveActive', ng-change = 'saveChange(VBD.position, "size", (existingDiskSizes[VBD.position]))', placeholder = "Size e.g 128 MiB, 8 GiB, 2 TiB...")
|
||||
td
|
||||
input.form-control(type="text", placeholder="Name of this virtual disk", ng-model="(VBD.VDI | resolve).name_label", ng-change = 'saveChange(VBD.position, "name_label", (VBD.VDI | resolve).name_label)')
|
||||
td
|
||||
input.form-control(type="text", placeholder="Description of this virtual disk", ng-model="(VBD.VDI | resolve).name_description", ng-change = 'saveChange(VBD.position, "name_description", (VBD.VDI | resolve).name_description)')
|
||||
td
|
||||
tr(ng-repeat="VDI in VDIs track by VDI.id")
|
||||
td
|
||||
select.form-control(ng-model="VDI.SR", ng-options="SR.id as (SR.name_label + ' (' + (SR.size - SR.usage | bytesToSize) + ' free)') for SR in (writable_SRs | orderBy:natural('name_label'))")
|
||||
td.text-center
|
||||
input(type="checkbox", ng-model="VDI.bootable")
|
||||
td
|
||||
input.form-control(type="text", ng-model="VDI.size", required="")
|
||||
input.form-control(type="text", ng-model="VDI.size", required="", placeholder = "Size e.g 128 MiB, 8 GiB, 2 TiB...")
|
||||
td
|
||||
input.form-control(type="text", placeholder="Name of this virtual disk", ng-model="VDI.name_label")
|
||||
td
|
||||
@@ -230,6 +266,10 @@ form.form-horizontal(ng-submit="createVM()")
|
||||
p.center.big(tooltip="Network interfaces")
|
||||
| {{(VIFs.length) || (template.VIFs.length) || 0}}x
|
||||
i.xo-icon-network
|
||||
p.center(ng-if="isDiskTemplate")
|
||||
| Cloud configuration is
|
||||
strong.text-success(ng-if = 'configDriveActive') enabled.
|
||||
strong.text-danger(ng-if = '!configDriveActive') disabled.
|
||||
p.center
|
||||
button.btn.btn-lg.btn-primary(type="submit")
|
||||
i.fa.fa-play
|
||||
|
||||
@@ -75,6 +75,16 @@ export default angular.module('xoWebApp.pool', [
|
||||
})
|
||||
}
|
||||
|
||||
$scope.setDefaultSr = function (id) {
|
||||
let {pool} = $scope
|
||||
return modal.confirm({
|
||||
title: 'Set default SR',
|
||||
message: 'Are you sure you want to set this SR as default?'
|
||||
}).then(function () {
|
||||
return xo.pool.setDefaultSr(pool.id, id)
|
||||
})
|
||||
}
|
||||
|
||||
$scope.deleteLog = function (id) {
|
||||
console.log('Remove log', id)
|
||||
return xo.log.delete(id)
|
||||
|
||||
@@ -114,11 +114,14 @@
|
||||
th Type
|
||||
th Size
|
||||
th.col-md-4 Physical/Allocated usage
|
||||
th.col-md-1 Action
|
||||
tr(
|
||||
ng-repeat="SR in srs | map | orderBy:natural('name_label') track by SR.id"
|
||||
xo-sref="SRs_view({id: SR.id})"
|
||||
)
|
||||
td.oneliner {{SR.name_label}}
|
||||
td.oneliner
|
||||
| {{SR.name_label}}
|
||||
span.label.label-primary(ng-if="SR.id === pool.default_SR") Default SR
|
||||
td.oneliner {{SR.name_description}}
|
||||
td {{SR.SR_type}}
|
||||
td {{SR.size | bytesToSize}}
|
||||
@@ -126,6 +129,10 @@
|
||||
.progress-condensed
|
||||
.progress-bar(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.usage}}", aria-valuemax="{{SR.size}}", style="width: {{[SR.physical_usage, SR.size] | percentage}}", tooltip="Physical usage: {{[SR.physical_usage, SR.size] | percentage}}")
|
||||
.progress-bar.progress-bar-info(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.physical_usage}}", aria-valuemax="{{SR.size}}", style="width: {{[(SR.usage-SR.physical_usage), SR.size] | percentage}}", tooltip="Allocated: {{[(SR.usage), SR.size] | percentage}}")
|
||||
td
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(ng-if="SR.id !== pool.default_SR", xo-click="setDefaultSr(SR.id)")
|
||||
i.fa.fa-hdd-o.fa-lg(tooltip="Set as default SR")
|
||||
|
||||
//- Logs panel
|
||||
.grid-sm
|
||||
|
||||
@@ -26,12 +26,12 @@ function loadDefaults (schema, configuration) {
|
||||
}
|
||||
forEach(schema.properties, (item, key) => {
|
||||
if (item.type === 'boolean' && !(key in configuration)) { // String default values are used as placeholders in view
|
||||
configuration[key] = item && item.default
|
||||
configuration[key] = Boolean(item && item.default)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function cleanUpConfiguration (schema, configuration) {
|
||||
function cleanUpConfiguration (schema, configuration, dump = {}) {
|
||||
if (!schema || !configuration) {
|
||||
return
|
||||
}
|
||||
@@ -44,20 +44,25 @@ function cleanUpConfiguration (schema, configuration) {
|
||||
}
|
||||
|
||||
function keepItem (item) {
|
||||
if (item === undefined || item === null || item === '' || (Array.isArray(item) && item.length === 0) || item.__use === false) {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
return !(item == null || item === '' || (Array.isArray(item) && item.length === 0))
|
||||
}
|
||||
|
||||
forEach(configuration, (item, key) => {
|
||||
item = sanitizeItem(item)
|
||||
configuration[key] = item
|
||||
dump[key] = item
|
||||
|
||||
if (!keepItem(item) || !schema.properties || !(key in schema.properties)) {
|
||||
delete configuration[key]
|
||||
} else if (schema.properties && schema.properties[key] && schema.properties[key].type === 'object') {
|
||||
cleanUpConfiguration(schema.properties[key], item)
|
||||
delete dump[key]
|
||||
} else if (schema.properties && schema.properties[key]) {
|
||||
const type = schema.properties[key].type
|
||||
|
||||
if (type === 'integer' || type === 'number') {
|
||||
dump[key] = +dump[key]
|
||||
} else if (type === 'object') {
|
||||
dump[key] = {}
|
||||
cleanUpConfiguration(schema.properties[key], item, dump[key])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -80,7 +85,7 @@ export default angular.module('settings.plugins', [
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('SettingsPlugins', function (xo, notify) {
|
||||
.controller('SettingsPlugins', function (xo, notify, modal) {
|
||||
this.disabled = {}
|
||||
|
||||
const refreshPlugins = () => xo.plugin.get().then(plugins => {
|
||||
@@ -100,21 +105,51 @@ export default angular.module('settings.plugins', [
|
||||
this.disabled[id] = true
|
||||
return xo.plugin[method](...args)
|
||||
.finally(() => {
|
||||
return refreshPlugins()
|
||||
.then(() => this.disabled[id] = false)
|
||||
this.disabled[id] = false
|
||||
})
|
||||
}
|
||||
|
||||
this.isRequired = isRequired
|
||||
this.isPassword = isPassword
|
||||
|
||||
this.configure = (plugin) => {
|
||||
cleanUpConfiguration(plugin.configurationSchema, plugin.configuration)
|
||||
_execPluginMethod(plugin.id, 'configure', plugin.id, plugin.configuration)
|
||||
const newConfiguration = {}
|
||||
plugin.errors = []
|
||||
|
||||
cleanUpConfiguration(plugin.configurationSchema, plugin.configuration, newConfiguration)
|
||||
_execPluginMethod(plugin.id, 'configure', plugin.id, newConfiguration)
|
||||
.then(() => notify.info({
|
||||
title: 'Plugin configuration',
|
||||
message: 'Successfully saved'
|
||||
}))
|
||||
.catch(err => {
|
||||
forEach(err.data, data => {
|
||||
const fieldPath = data.field.split('.').slice(1)
|
||||
const fieldPathTitles = []
|
||||
let groupObject = plugin.configurationSchema
|
||||
forEach(fieldPath, groupName => {
|
||||
groupObject = groupObject.properties[groupName]
|
||||
fieldPathTitles.push(groupObject.title || groupName)
|
||||
})
|
||||
plugin.errors.push(`${fieldPathTitles.join(' > ')} ${data.message}`)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
this.purgeConfiguration = (plugin) => {
|
||||
modal.confirm({
|
||||
title: 'Purge configuration',
|
||||
message: 'Are you sure you want to purge this configuration ?'
|
||||
}).then(() => {
|
||||
_execPluginMethod(plugin.id, 'purgeConfiguration', plugin.id).then(() => {
|
||||
notify.info({
|
||||
title: 'Purge configuration',
|
||||
message: 'This plugin config is now purged.'
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
this.toggleAutoload = (plugin) => {
|
||||
let method
|
||||
if (!plugin._autoload && plugin.autoload) {
|
||||
@@ -172,7 +207,7 @@ export default angular.module('settings.plugins', [
|
||||
}
|
||||
})
|
||||
|
||||
.directive('objectInput', () => {
|
||||
.directive('confObjectInput', () => {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: objectInputView,
|
||||
@@ -181,12 +216,12 @@ export default angular.module('settings.plugins', [
|
||||
schema: '=',
|
||||
required: '='
|
||||
},
|
||||
controller: 'ObjectInput as ctrl',
|
||||
controller: 'ConfObjectInput as ctrl',
|
||||
bindToController: true
|
||||
}
|
||||
})
|
||||
|
||||
.controller('ObjectInput', function ($scope, xo, xoApi) {
|
||||
.controller('ConfObjectInput', function ($scope, xo, xoApi) {
|
||||
const prepareModel = () => {
|
||||
if (this.model === undefined || this.model === null) {
|
||||
this.model = {
|
||||
|
||||
@@ -5,10 +5,25 @@
|
||||
fieldset(ng-disabled = '!ctrl.required && !ctrl.model.__use', ng-hide = '!ctrl.required && !ctrl.model.__use')
|
||||
ul(style = 'padding-left: 0;')
|
||||
li.list-group-item(ng-repeat = '(key, value) in ctrl.schema.properties track by key')
|
||||
.input-group
|
||||
.input-group(ng-if = 'value.type != "boolean"')
|
||||
span.input-group-addon
|
||||
| {{key}}
|
||||
| {{value.title || key}}
|
||||
span.text-warning(ng-if = 'ctrl.isRequired(key, ctrl.schema)') *
|
||||
input.form-control.input-sm(ng-if = '!ctrl.isPassword(key)', type = 'text', ng-model = 'ctrl.model[key]', ng-required = 'ctrl.isRequired(key, ctrl.schema)')
|
||||
input.form-control.input-sm(ng-if = 'ctrl.isPassword(key)', type = 'password', ng-model = 'ctrl.model[key]', ng-required = 'ctrl.isRequired(key, ctrl.schema)')
|
||||
input.form-control.input-sm(
|
||||
ng-if = 'value.type != "number" && value.type != "integer"',
|
||||
type = '{{ctrl.isPassword(key) ? "password" : "text"}}',
|
||||
ng-model = 'ctrl.model[key]',
|
||||
ng-required = 'ctrl.isRequired(key, ctrl.schema)'
|
||||
)
|
||||
input.form-control.input-sm(
|
||||
ng-if = 'value.type == "number" || value.type == "integer"',
|
||||
type = 'number',
|
||||
ng-model = 'ctrl.model[key]',
|
||||
ng-required = 'ctrl.isRequired(key, ctrl.schema)'
|
||||
)
|
||||
.form-inline(ng-if = 'value.type == "boolean"')
|
||||
.checkbox.small('style="color: #31708F;"') {{value.title || key}} :
|
||||
label('style="color: #A7AFB0;"')
|
||||
i.fa.fa-2x(ng-class = '{"fa-toggle-on": ctrl.model[key], "fa-toggle-off": !ctrl.model[key]}')
|
||||
input.hidden(type = 'checkbox', ng-model = 'ctrl.model[key]')
|
||||
.help-block(ng-bind-html = 'ctrl.schema.properties[key].description | md2html')
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-body
|
||||
p.text-center(ng-if = '!ctrl.plugins || !crtl.plugins.length') No plugins found
|
||||
p.text-center(ng-if = '!ctrl.plugins || !ctrl.plugins.length') No plugins found
|
||||
div(ng-repeat = 'plugin in ctrl.plugins | orderBy:"name" track by plugin.id')
|
||||
h3.form-inline.clearfix
|
||||
span.text-info {{ plugin.name }}
|
||||
@@ -30,19 +30,28 @@
|
||||
form.form-horizontal(ng-if = 'plugin.configurationSchema', ng-submit = 'ctrl.configure(plugin)')
|
||||
fieldset(ng-disabled = 'ctrl.disabled[plugin.id]')
|
||||
.form-group(ng-repeat = '(key, prop) in plugin.configurationSchema.properties')
|
||||
label.col-md-2.control-label
|
||||
| {{key}}
|
||||
label.col-md-2.control-label
|
||||
| {{prop.title || key}}
|
||||
span.text-warning(ng-if = 'ctrl.isRequired(key, plugin.configurationSchema)') *
|
||||
.col-md-5
|
||||
input.form-control(ng-if = 'prop.type === "string" && !ctrl.isPassword(key)', type = 'text', ng-model = 'plugin.configuration[key]', ng-required = 'ctrl.isRequired(key, plugin.configurationSchema)', placeholder = '{{ plugin.configurationSchema.properties[key].default }}')
|
||||
input.form-control(ng-if = 'prop.type === "string" && ctrl.isPassword(key)', type = 'password', ng-model = 'plugin.configuration[key]', ng-required = 'ctrl.isRequired(key, plugin.configurationSchema)')
|
||||
input.form-control(ng-if = 'prop.type === "integer" || prop.type === "number"', type = 'number', ng-model = 'plugin.configuration[key]', ng-required = 'ctrl.isRequired(key, plugin.configurationSchema)', placeholder = '{{ plugin.configurationSchema.properties[key].default }}')
|
||||
input.form-control(ng-if = 'prop.type === "string"', type = '{{ ctrl.isPassword(key) ? "password" : "text" }}', ng-model = 'plugin.configuration[key]', ng-required = 'ctrl.isRequired(key, plugin.configurationSchema)')
|
||||
multi-string-input(ng-if = 'prop.type === "array" && prop.items.type === "string"', model = 'plugin.configuration[key]')
|
||||
.checkbox(ng-if = 'prop.type === "boolean"'): label: input(type = 'checkbox', ng-model = 'plugin.configuration[key]')
|
||||
object-input(ng-if = 'prop.type === "object"', model = 'plugin.configuration[key]', schema = 'prop', required = 'ctrl.isRequired(key, plugin.configurationSchema)')
|
||||
conf-object-input(ng-if = 'prop.type === "object"', model = 'plugin.configuration[key]', schema = 'prop', required = 'ctrl.isRequired(key, plugin.configurationSchema)')
|
||||
.col-md-5
|
||||
span.help-block(ng-bind-html = 'prop.description | md2html')
|
||||
.form-group
|
||||
.col-md-offset-2.col-md-10.text-danger(ng-repeat = "err in plugin.errors")
|
||||
| {{ err }}
|
||||
.form-group
|
||||
.col-md-offset-2.col-md-10
|
||||
button.btn.btn-primary(type = 'submit')
|
||||
| Save configuration
|
||||
i.fa.fa-floppy-o
|
||||
.btn-toolbar
|
||||
.btn-group
|
||||
button.btn.btn-primary(type = 'submit')
|
||||
| Save configuration
|
||||
i.fa.fa-floppy-o
|
||||
.btn-group
|
||||
button.btn.btn-danger(type = 'button' ng-click = 'ctrl.purgeConfiguration(plugin)')
|
||||
| Purge configuration
|
||||
i.fa.fa-trash-o
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import angular from 'angular'
|
||||
import forEach from 'lodash.foreach'
|
||||
import includes from 'lodash.includes'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
import uiSelect from 'angular-ui-select'
|
||||
|
||||
@@ -26,18 +28,28 @@ export default angular.module('settings.servers', [
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('SettingsServers', function ($scope, $interval, servers, xoApi, xo, notify) {
|
||||
this.servers = servers
|
||||
.controller('SettingsServers', function ($scope, $rootScope, $interval, $filter, servers, xoApi, xo, notify) {
|
||||
const orderBy = $filter('orderBy')
|
||||
this.servers = orderBy(servers, $rootScope.natural('host'))
|
||||
$scope.readOnly = {}
|
||||
forEach(this.servers, (server) => {
|
||||
$scope.readOnly[server.id] = Boolean(server.readOnly)
|
||||
})
|
||||
const selected = this.selectedServers = {}
|
||||
const newServers = this.newServers = []
|
||||
|
||||
const refreshServers = () => {
|
||||
xo.server.getAll().then(servers => {
|
||||
this.servers = servers
|
||||
this.servers = orderBy(servers, $rootScope.natural('host'))
|
||||
})
|
||||
}
|
||||
const refreshServersIfUnfocused = () => {
|
||||
if (!$scope.isFocused) {
|
||||
refreshServers()
|
||||
}
|
||||
}
|
||||
|
||||
const interval = $interval(refreshServers, 10e3)
|
||||
const interval = $interval(refreshServersIfUnfocused, 10e3)
|
||||
$scope.$on('$destroy', () => {
|
||||
$interval.cancel(interval)
|
||||
})
|
||||
@@ -73,6 +85,9 @@ export default angular.module('settings.servers', [
|
||||
|
||||
this.addServer()
|
||||
this.saveServers = () => {
|
||||
const addresses = []
|
||||
forEach(xoApi.getView('host').all, host => addresses.push(host.address))
|
||||
|
||||
const newServers = this.newServers
|
||||
const servers = this.servers
|
||||
const updateServers = []
|
||||
@@ -87,6 +102,7 @@ export default angular.module('settings.servers', [
|
||||
if (!server.password) {
|
||||
delete server.password
|
||||
}
|
||||
server.readOnly = $scope.readOnly[id]
|
||||
xo.server.set(server)
|
||||
delete server.password
|
||||
updateServers.push(server)
|
||||
@@ -94,17 +110,26 @@ export default angular.module('settings.servers', [
|
||||
}
|
||||
for (let i = 0, len = newServers.length; i < len; i++) {
|
||||
const server = newServers[i]
|
||||
const {host, username, password} = server
|
||||
const {host, username, password, readOnly} = server
|
||||
if (!host) {
|
||||
continue
|
||||
}
|
||||
if (includes(addresses, host)) {
|
||||
notify.warning({
|
||||
title: 'Server already connected',
|
||||
message: `You are already connected to ${host}`
|
||||
})
|
||||
continue
|
||||
}
|
||||
xo.server.add({
|
||||
host,
|
||||
username,
|
||||
password,
|
||||
readOnly,
|
||||
autoConnect: false
|
||||
}).then(function (id) {
|
||||
server.id = id
|
||||
$scope.readOnly[id] = Boolean(readOnly)
|
||||
xo.server.connect(id).catch(error => {
|
||||
notify.error({
|
||||
title: 'Server connection error',
|
||||
|
||||
@@ -10,11 +10,12 @@
|
||||
tr
|
||||
th.col-md-5 Host
|
||||
th.col-md-2 User
|
||||
th.col-md-3 Password
|
||||
th.col-md-2 Password
|
||||
th.col-md-1.text.center Actions
|
||||
th.col-md-1.text.center Read only
|
||||
th.col-md-1.text-center
|
||||
i.fa.fa-trash-o.fa-lg(tooltip="Forget server")
|
||||
tr(ng-repeat="server in ctrl.servers | orderBy:natural('host') track by server.id")
|
||||
tr(ng-repeat="server in ctrl.servers track by server.id")
|
||||
td
|
||||
.input-group
|
||||
span.input-group-addon.hidden-xs(ng-if="server.status === 'connected'")
|
||||
@@ -23,11 +24,27 @@
|
||||
i.xo-icon-failure.fa-lg(tooltip="Disconnected")
|
||||
span.input-group-addon.hidden-xs(ng-if="server.status === 'connecting'")
|
||||
i.fa.fa-cog.fa-lg.fa-spin(tooltip="Connecting...")
|
||||
input.form-control(type="text", ng-model="server.host")
|
||||
input.form-control(
|
||||
type="text",
|
||||
ng-model="server.host",
|
||||
ng-focus="$parent.isFocused = true",
|
||||
ng-blur="$parent.isFocused = false"
|
||||
)
|
||||
td
|
||||
input.form-control(type="text", ng-model="server.username")
|
||||
input.form-control(
|
||||
type="text",
|
||||
ng-model="server.username",
|
||||
ng-focus="$parent.isFocused = true",
|
||||
ng-blur="$parent.isFocused = false"
|
||||
)
|
||||
td
|
||||
input.form-control(type="password", ng-model="server.password", placeholder="Fill to change the password")
|
||||
input.form-control(
|
||||
type="password",
|
||||
ng-model="server.password",
|
||||
placeholder="Fill to change the password",
|
||||
ng-focus="$parent.isFocused = true",
|
||||
ng-blur="$parent.isFocused = false"
|
||||
)
|
||||
td.text-center
|
||||
button.btn.btn-default(
|
||||
ng-if="server.status === 'disconnected'",
|
||||
@@ -43,6 +60,8 @@
|
||||
tooltip="Disconnect this server"
|
||||
)
|
||||
i.fa.fa-unlink
|
||||
td.text-center
|
||||
input(type="checkbox", ng-model="readOnly[server.id]")
|
||||
td.text-center
|
||||
input(type="checkbox", ng-model="ctrl.selectedServers[server.id]")
|
||||
tr(ng-repeat="server in ctrl.newServers")
|
||||
@@ -67,6 +86,8 @@
|
||||
placeholder="password"
|
||||
)
|
||||
td  
|
||||
td.text-center
|
||||
input( type="checkbox", ng-model="server.readOnly")
|
||||
td  
|
||||
p.text-center
|
||||
button.btn.btn-primary(type="submit")
|
||||
|
||||
@@ -44,7 +44,15 @@ export default angular.module('xoWebApp.sr', [
|
||||
|
||||
let {get} = xoApi
|
||||
$scope.$watch(() => xoApi.get($stateParams.id), function (SR) {
|
||||
const VDIs = []
|
||||
if (SR) {
|
||||
forEach(SR.VDIs, vdi => {
|
||||
vdi = xoApi.get(vdi)
|
||||
vdi && VDIs.push({...vdi, size: bytesToSizeFilter(vdi.size)})
|
||||
})
|
||||
}
|
||||
$scope.SR = SR
|
||||
$scope.VDIs = VDIs
|
||||
})
|
||||
|
||||
$scope.saveSR = function ($data) {
|
||||
@@ -176,6 +184,7 @@ export default angular.module('xoWebApp.sr', [
|
||||
$scope.saveDisks = function (data) {
|
||||
// Group data by disk.
|
||||
let disks = {}
|
||||
let sizeChanges = false
|
||||
forEach(data, function (value, key) {
|
||||
let i = key.indexOf('/')
|
||||
|
||||
@@ -185,27 +194,50 @@ export default angular.module('xoWebApp.sr', [
|
||||
;(disks[id] || (disks[id] = {}))[prop] = value
|
||||
})
|
||||
|
||||
let promises = []
|
||||
forEach(disks, function (attributes, id) {
|
||||
// Keep only changed attributes.
|
||||
let disk = get(id)
|
||||
|
||||
forEach(attributes, function (value, name) {
|
||||
if (value === disk[name]) {
|
||||
delete attributes[name]
|
||||
}
|
||||
})
|
||||
|
||||
if (!isEmpty(attributes)) {
|
||||
// Inject id.
|
||||
attributes.id = id
|
||||
|
||||
// Ask the server to update the object.
|
||||
promises.push(xoApi.call('vdi.set', attributes))
|
||||
if (attributes.size !== bytesToSizeFilter(disk.size)) { // /!\ attributes are provided by a modified copy of disk
|
||||
sizeChanges = true
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
return $q.all(promises)
|
||||
let promises = []
|
||||
|
||||
const preCheck = sizeChanges ? modal.confirm({
|
||||
title: 'Disk resizing',
|
||||
message: 'Growing the size of a disk is not reversible'
|
||||
}) : $q.resolve()
|
||||
|
||||
return preCheck
|
||||
.then(() => {
|
||||
forEach(disks, function (attributes, id) {
|
||||
let disk = get(id)
|
||||
|
||||
// Resize disks
|
||||
if (attributes.size !== bytesToSizeFilter(disk.size)) { // /!\ attributes are provided by a modified copy of disk
|
||||
promises.push(xo.disk.resize(id, attributes.size))
|
||||
}
|
||||
delete attributes.size
|
||||
|
||||
// Keep only changed attributes.
|
||||
forEach(attributes, function (value, name) {
|
||||
if (value === disk[name]) {
|
||||
delete attributes[name]
|
||||
}
|
||||
})
|
||||
|
||||
if (!isEmpty(attributes)) {
|
||||
// Inject id.
|
||||
attributes.id = id
|
||||
|
||||
// Ask the server to update the object.
|
||||
promises.push(xoApi.call('vdi.set', attributes))
|
||||
}
|
||||
})
|
||||
|
||||
return $q.all(promises)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -8,8 +8,10 @@
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-cogs
|
||||
| General
|
||||
span.quick-edit(tooltip="Edit General settings", ng-click="srSettings.$show()")
|
||||
span.quick-edit(tooltip="Edit General settings", ng-click="srSettings.$show()", ng-if = '!srSettings.$visible')
|
||||
i.fa.fa-edit.fa-fw
|
||||
span.quick-edit(tooltip="Cancel Edition", ng-click="srSettings.$cancel()", ng-if = 'srSettings.$visible')
|
||||
i.fa.fa-undo.fa-fw
|
||||
.panel-body
|
||||
form(editable-form="", name="srSettings", onbeforesave="saveSR($data)")
|
||||
dl.dl-horizontal
|
||||
@@ -109,8 +111,18 @@
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-disk
|
||||
| Virtual disks
|
||||
span.quick-edit(tooltip="Edit disks", ng-click="disksForm.$show()")
|
||||
span.quick-edit(
|
||||
ng-if="!disksForm.$visible"
|
||||
tooltip="Edit disks"
|
||||
ng-click="disksForm.$show()"
|
||||
)
|
||||
i.fa.fa-edit.fa-fw
|
||||
span.quick-edit(
|
||||
ng-if="disksForm.$visible"
|
||||
tooltip="Cancel Edition"
|
||||
ng-click="disksForm.$cancel()"
|
||||
)
|
||||
i.fa.fa-undo.fa-fw
|
||||
span.quick-edit(tooltip="Rescan", ng-click="rescanSr(SR.id)")
|
||||
i.fa.fa-refresh.fa-fw
|
||||
.panel-body
|
||||
@@ -121,7 +133,7 @@
|
||||
th Tags
|
||||
th Size
|
||||
th Virtual Machine:
|
||||
tr(ng-repeat="VDI in SR.VDIs | resolve | vdiFilter:vdiSearch | orderBy:natural('name_label') | slice:(10*(currentVDIPage-1)):(10*currentVDIPage)")
|
||||
tr(ng-repeat="VDI in VDIs | vdiFilter:vdiSearch | orderBy:natural('name_label') | slice:(10*(currentVDIPage-1)):(10*currentVDIPage)")
|
||||
td.oneliner
|
||||
span(
|
||||
editable-text="VDI.name_label"
|
||||
@@ -138,9 +150,11 @@
|
||||
td
|
||||
xo-tag(object = 'VDI')
|
||||
td
|
||||
//- FIXME: should be editable, but the server needs first
|
||||
//- to accept a human readable string.
|
||||
| {{VDI.size | bytesToSize}}
|
||||
span(
|
||||
editable-text="VDI.size"
|
||||
e-name = '{{VDI.id}}/size'
|
||||
)
|
||||
| {{VDI.size}}
|
||||
td.oneliner {{((VDI.$VBDs[0] | resolve).VM | resolve).name_label}}
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(ng-if="(VDI.$VBDs[0] | resolve).attached", xo-click="disconnectVBD(VDI.$VBDs[0])")
|
||||
@@ -152,8 +166,8 @@
|
||||
.input-group
|
||||
.input-group-addon: i.fa.fa-filter
|
||||
input.form-control(type = 'text', ng-model = 'vdiSearch', placeholder = 'Enter your search here')
|
||||
.center(ng-if = '(SR.VDIs | resolve | vdiFilter:vdiSearch).length > 10 || currentVDIPage > 1')
|
||||
pagination(boundary-links="true", total-items="(SR.VDIs | resolve | vdiFilter:vdiSearch).length", ng-model="$parent.currentVDIPage", items-per-page="10", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
|
||||
.center(ng-if = '(VDIs | vdiFilter:vdiSearch).length > 10 || currentVDIPage > 1')
|
||||
pagination(boundary-links="true", total-items="(VDIs | vdiFilter:vdiSearch).length", ng-model="$parent.currentVDIPage", items-per-page="10", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
|
||||
.btn-form(ng-show="disksForm.$visible")
|
||||
p.center
|
||||
button.btn.btn-default(
|
||||
@@ -189,7 +203,7 @@
|
||||
span.label.label-success Connected
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(xo-click="disconnectPBD(PBD.id)")
|
||||
i.fa.fa-unlink.fa-lg(tooltip="Disconnect to this host")
|
||||
i.fa.fa-unlink.fa-lg(tooltip="Disconnect from this host")
|
||||
td(ng-if="!PBD.attached")
|
||||
span.label.label-default Disconnected
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
|
||||
41
app/modules/task-scheduler/index.js
Normal file
41
app/modules/task-scheduler/index.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import angular from 'angular'
|
||||
import later from 'later'
|
||||
import scheduler from 'scheduler'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
|
||||
later.date.localTime()
|
||||
|
||||
import job from './job'
|
||||
import overview from './overview'
|
||||
import schedule from './schedule'
|
||||
|
||||
import view from './view'
|
||||
|
||||
export default angular.module('taskScheduler', [
|
||||
uiRouter,
|
||||
scheduler,
|
||||
|
||||
job,
|
||||
overview,
|
||||
schedule
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('taskscheduler', {
|
||||
abstract: true,
|
||||
data: {
|
||||
requireAdmin: true
|
||||
},
|
||||
template: view,
|
||||
url: '/taskscheduler'
|
||||
})
|
||||
|
||||
// Redirect to default sub-state.
|
||||
$stateProvider.state('taskscheduler.index', {
|
||||
url: '',
|
||||
controller: function ($state) {
|
||||
$state.go('taskscheduler.overview')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
.name
|
||||
19
app/modules/task-scheduler/job/array-input-view.jade
Normal file
19
app/modules/task-scheduler/job/array-input-view.jade
Normal file
@@ -0,0 +1,19 @@
|
||||
.form-group(ng-if = 'ctrl.active()')
|
||||
label.col-md-2.control-label(ng-if = 'ctrl.key')
|
||||
| {{ ctrl.key }}
|
||||
span.text-warning(ng-if = 'ctrl.isRequired()') *
|
||||
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
|
||||
ul(style = 'padding-left: 0;')
|
||||
li.list-group-item.clearfix(ng-repeat = 'item in ctrl.model track by $index')
|
||||
| {{item}}
|
||||
a.pull-right(ng-click = 'ctrl.remove($index)'): i.fa.fa-times
|
||||
form(ng-submit = 'ctrl.add(ctrl.newItem); ctrl.newItem = ""')
|
||||
.input-group
|
||||
input.form-control.input-sm(ng-if = 'ctrl.getType(ctrl.property.items) === "string"', type = 'text', ng-model = 'ctrl.newItem', ng-required = '!param.optional')
|
||||
input.form-control.input-sm(ng-if = 'ctrl.getType(ctrl.property.items) === "integer"', type = 'number', step = '1', ng-model = 'ctrl.newItem', ng-required = '!param.optional')
|
||||
input.form-control.input-sm(ng-if = 'ctrl.getType(ctrl.property.items) === "number"', type = 'number', step = 'any', ng-model = 'ctrl.newItem', ng-required = '!param.optional')
|
||||
span.input-group-addon(ng-if = 'ctrl.getType(ctrl.property.items) === "boolean"')
|
||||
input(type = 'checkbox', ng-model = 'ctrl.newItem')
|
||||
input.form-control.input-sm(ng-if = 'ctrl.getType(ctrl.property.items) === "boolean"', disabled)
|
||||
span.input-group-btn
|
||||
button.btn.btn-primary.btn-sm(type = 'submit') Add
|
||||
6
app/modules/task-scheduler/job/boolean-input-view.jade
Normal file
6
app/modules/task-scheduler/job/boolean-input-view.jade
Normal file
@@ -0,0 +1,6 @@
|
||||
.form-group(ng-if = 'ctrl.active()')
|
||||
label.col-md-2.control-label(ng-if = 'ctrl.key')
|
||||
| {{ ctrl.key }}
|
||||
span.text-warning(ng-if = 'ctrl.isRequired()') *
|
||||
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
|
||||
input(form = '{{ ctrl.form }}', type = 'checkbox', ng-model = 'ctrl.model')
|
||||
15
app/modules/task-scheduler/job/host-input-view.jade
Normal file
15
app/modules/task-scheduler/job/host-input-view.jade
Normal file
@@ -0,0 +1,15 @@
|
||||
.form-group(ng-if = 'ctrl.active()')
|
||||
label.col-md-2.control-label(ng-if = 'ctrl.key')
|
||||
| Hosts
|
||||
span.text-warning(ng-if = 'ctrl.isRequired()') *
|
||||
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
|
||||
ui-select(form = 'ctrl.form', ng-model = 'ctrl.intraModel', multiple, close-on-select = 'false', ng-required = 'ctrl.isRequired()', on-remove = 'ctrl.exportRemove($item)', on-select = 'ctrl.exportSelect($item)')
|
||||
ui-select-match(placeholder = 'Choose hosts')
|
||||
i(class = 'xo-icon-{{$item.type | lowercase}}')
|
||||
| {{$item.name_label}}
|
||||
span(ng-if = '$item.$container') ({{ ($item.$container | resolve).name_label }})
|
||||
ui-select-choices(repeat = 'object in ctrl.objects | selectHighLevel | filter:{type: "host"} | filter:$select.search | orderBy:["$container", "name_label"] track by object.id')
|
||||
div
|
||||
i(class = 'xo-icon-{{object.type | lowercase}}')
|
||||
| {{ object.name_label }}
|
||||
span(ng-if = 'object.$container') ({{ (object.$container | resolve).name_label || ((object.$container | resolve).master | resolve).name_label }})
|
||||
827
app/modules/task-scheduler/job/index.js
Normal file
827
app/modules/task-scheduler/job/index.js
Normal file
@@ -0,0 +1,827 @@
|
||||
import angular from 'angular'
|
||||
import assign from 'lodash.assign'
|
||||
import cloneDeep from 'lodash.clonedeep'
|
||||
import find from 'lodash.find'
|
||||
import forEach from 'lodash.foreach'
|
||||
import includes from 'lodash.includes'
|
||||
import map from 'lodash.map'
|
||||
import mapValues from 'lodash.mapvalues'
|
||||
import remove from 'lodash.remove'
|
||||
import trim from 'lodash.trim'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
import uiBootstrap from 'angular-ui-bootstrap'
|
||||
import Bluebird from 'bluebird'
|
||||
Bluebird.longStackTraces()
|
||||
|
||||
import arrayInputView from './array-input-view'
|
||||
import booleanInputView from './boolean-input-view'
|
||||
import hostInputView from './host-input-view'
|
||||
import integerInputView from './integer-input-view'
|
||||
import numberInputView from './number-input-view'
|
||||
import objectInputView from './object-input-view'
|
||||
import poolInputView from './pool-input-view'
|
||||
import srInputView from './sr-input-view'
|
||||
import stringInputView from './string-input-view'
|
||||
import view from './view'
|
||||
import vmInputView from './vm-input-view'
|
||||
import xoEntityInputView from './xo-entity-input-view'
|
||||
import xoObjectInputView from './xo-object-input-view'
|
||||
import xoRoleInputView from './xo-role-input-view'
|
||||
|
||||
// ====================================================================
|
||||
|
||||
const JOB_KEY = 'genericTask'
|
||||
|
||||
const jobCompliantMethods = [
|
||||
'acl.add',
|
||||
'acl.remove',
|
||||
'host.detach',
|
||||
'host.disable',
|
||||
'host.enable',
|
||||
'host.installAllPatches',
|
||||
'host.restart',
|
||||
'host.restartAgent',
|
||||
'host.set',
|
||||
'host.start',
|
||||
'host.stop',
|
||||
'job.runSequence',
|
||||
'vm.attachDisk',
|
||||
'vm.backup',
|
||||
'vm.clone',
|
||||
'vm.convert',
|
||||
'vm.copy',
|
||||
'vm.creatInterface',
|
||||
'vm.delete',
|
||||
'vm.migrate',
|
||||
'vm.migrate_pool',
|
||||
'vm.restart',
|
||||
'vm.resume',
|
||||
'vm.revert',
|
||||
'vm.rollingBackup',
|
||||
'vm.rollingDrCopy',
|
||||
'vm.rollingSnapshot',
|
||||
'vm.set',
|
||||
'vm.setBootOrder',
|
||||
'vm.snapshot',
|
||||
'vm.start',
|
||||
'vm.stop',
|
||||
'vm.suspend'
|
||||
]
|
||||
|
||||
const getType = function (param) {
|
||||
if (!param) {
|
||||
return
|
||||
}
|
||||
if (Array.isArray(param.type)) {
|
||||
if (includes(param.type, 'integer')) {
|
||||
return 'integer'
|
||||
} else if (includes(param.type, 'number')) {
|
||||
return 'number'
|
||||
} else {
|
||||
return 'string'
|
||||
}
|
||||
}
|
||||
return param.type
|
||||
}
|
||||
|
||||
const isRequired = function (param) {
|
||||
if (!param) {
|
||||
return
|
||||
}
|
||||
return (!param.optional && !(includes(['boolean', 'array'], getType(param))))
|
||||
}
|
||||
/**
|
||||
* Takes care of unfilled not-required data and unwanted white-spaces
|
||||
*/
|
||||
const cleanUpData = function (data) {
|
||||
if (!data) {
|
||||
return
|
||||
}
|
||||
|
||||
function sanitizeItem (item) {
|
||||
if (typeof item === 'string') {
|
||||
item = trim(item)
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
function keepItem (item) {
|
||||
if ((item === undefined) || (item === null) || (item === '') || (Array.isArray(item) && item.length === 0) || item.__use === false) {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
forEach(data, (item, key) => {
|
||||
item = sanitizeItem(item)
|
||||
data[key] = item
|
||||
if (!keepItem(item)) {
|
||||
delete data[key]
|
||||
} else if (typeof item === 'object') {
|
||||
cleanUpData(item)
|
||||
}
|
||||
})
|
||||
|
||||
delete data.__use
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries extracting XO Object targeted property
|
||||
*/
|
||||
const reduceXoObject = function (value, propertyName = 'id') {
|
||||
return value && value[propertyName] || value
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapts all data "arrayed" by UI-multiple-selectors to job's cross-product trick
|
||||
*/
|
||||
const dataToParamVectorItems = function (params, data) {
|
||||
const items = []
|
||||
forEach(params, (param, name) => {
|
||||
if (Array.isArray(data[name]) && getType(param) !== 'array') {
|
||||
const values = []
|
||||
if (data[name].length === 1) { // One value, no need to engage cross-product
|
||||
data[name] = data[name].pop()
|
||||
} else {
|
||||
forEach(data[name], value => {
|
||||
values.push({[name]: reduceXoObject(value, name)})
|
||||
})
|
||||
if (values.length) { // No values at all
|
||||
items.push({
|
||||
type: 'set',
|
||||
values
|
||||
})
|
||||
}
|
||||
delete data[name]
|
||||
}
|
||||
}
|
||||
})
|
||||
if (Object.keys(data).length) {
|
||||
items.push({
|
||||
type: 'set',
|
||||
values: [mapValues(data, reduceXoObject)]
|
||||
})
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
const actionGroup = {
|
||||
group: undefined,
|
||||
get: function () {
|
||||
return this.group
|
||||
},
|
||||
set: function (group) {
|
||||
this.group = group
|
||||
}
|
||||
}
|
||||
|
||||
const _initXoObjectInput = function () {
|
||||
if (this.model === undefined) {
|
||||
this.model = []
|
||||
}
|
||||
if (!Array.isArray(this.model)) {
|
||||
this.model = [this.model]
|
||||
}
|
||||
this.intraModel = map(this.model, value => find(this.objects, object => object.id === value) || value)
|
||||
}
|
||||
|
||||
const _exportRemove = function (removedItem) {
|
||||
remove(this.model, item => item === reduceXoObject(removedItem))
|
||||
}
|
||||
|
||||
const _exportSelect = function (addedItem) {
|
||||
const addOn = reduceXoObject(addedItem)
|
||||
if (!find(this.model, item => item === addOn)) {
|
||||
this.model.push(addOn)
|
||||
}
|
||||
}
|
||||
|
||||
export default angular.module('xoWebApp.taskscheduler.job', [
|
||||
uiRouter,
|
||||
uiBootstrap
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('taskscheduler.job', {
|
||||
url: '/job/:id',
|
||||
controller: 'JobCtrl as ctrl',
|
||||
template: view
|
||||
})
|
||||
})
|
||||
|
||||
.controller('JobCtrl', function ($scope, xo, xoApi, notify, $stateParams) {
|
||||
this.scheduleApi = {}
|
||||
this.formData = {}
|
||||
this.running = {}
|
||||
this.ready = false
|
||||
|
||||
let comesForEditing = $stateParams.id
|
||||
|
||||
this.resetData = () => {
|
||||
this.formData = {}
|
||||
}
|
||||
this.resetForm = () => {
|
||||
this.resetData()
|
||||
this.editedJobId = undefined
|
||||
this.jobName = undefined
|
||||
this.selectedAction = undefined
|
||||
}
|
||||
this.resetForm()
|
||||
|
||||
$scope.$watch(() => this.selectedAction, newAction => actionGroup.set(newAction && newAction.group))
|
||||
|
||||
const loadActions = () => xoApi.call('system.getMethodsInfo')
|
||||
.then(response => {
|
||||
const actions = []
|
||||
|
||||
for (let method in response) {
|
||||
if (includes(jobCompliantMethods, method)) {
|
||||
let [group, command] = method.split('.')
|
||||
response[method].properties = response[method].params
|
||||
response[method].type = 'object'
|
||||
delete response[method].params
|
||||
actions.push({
|
||||
method,
|
||||
group,
|
||||
command,
|
||||
info: response[method]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.actions = actions
|
||||
this.ready = true
|
||||
})
|
||||
|
||||
const loadJobs = () => xo.job.getAll().then(jobs => {
|
||||
const j = {}
|
||||
forEach(jobs, job => {
|
||||
if (job.key === JOB_KEY) {
|
||||
j[job.id] = job
|
||||
}
|
||||
})
|
||||
this.jobs = j
|
||||
})
|
||||
|
||||
const refresh = () => loadJobs()
|
||||
const getReady = () => loadActions().then(refresh).then(() => this.ready = true)
|
||||
getReady().then(() => {
|
||||
if (comesForEditing) {
|
||||
this.edit(comesForEditing)
|
||||
comesForEditing = undefined
|
||||
}
|
||||
})
|
||||
|
||||
const saveNew = (name, action, data) => {
|
||||
const job = {
|
||||
type: 'call',
|
||||
name,
|
||||
key: JOB_KEY,
|
||||
method: action.method,
|
||||
paramsVector: {
|
||||
type: 'crossProduct',
|
||||
items: dataToParamVectorItems(action.info.properties, data)
|
||||
}
|
||||
}
|
||||
return xo.job.create(job)
|
||||
}
|
||||
|
||||
const save = (id, name, action, data) => {
|
||||
const job = this.jobs[id]
|
||||
job.name = name
|
||||
job.method = action.method
|
||||
job.paramsVector = {
|
||||
type: 'crossProduct',
|
||||
items: dataToParamVectorItems(action.info.properties, data)
|
||||
}
|
||||
return xo.job.set(job)
|
||||
}
|
||||
|
||||
this.save = (id, name, action, data) => {
|
||||
const dataClone = cleanUpData(cloneDeep(data))
|
||||
const saved = (id !== undefined) ? save(id, name, action, dataClone) : saveNew(name, action, dataClone)
|
||||
return saved
|
||||
.then(() => this.resetForm())
|
||||
.finally(refresh)
|
||||
}
|
||||
|
||||
this.edit = id => {
|
||||
this.resetForm()
|
||||
try {
|
||||
const job = this.jobs[id]
|
||||
if (job) {
|
||||
this.editedJobId = id
|
||||
this.jobName = job.name
|
||||
this.selectedAction = find(this.actions, action => action.method === job.method)
|
||||
const data = {}
|
||||
const paramsVector = job.paramsVector
|
||||
if (paramsVector) {
|
||||
if (paramsVector.type !== 'crossProduct') {
|
||||
throw new Error(`Unknown parameter-vector type ${paramsVector.type}`)
|
||||
}
|
||||
forEach(paramsVector.items, item => {
|
||||
if (item.type !== 'set') {
|
||||
throw new Error(`Unknown parameter-vector item type ${item.type}`)
|
||||
}
|
||||
if (item.values.length === 1) {
|
||||
assign(data, item.values[0])
|
||||
} else {
|
||||
forEach(item.values, valueItem => {
|
||||
forEach(valueItem, (value, key) => {
|
||||
if (data[key] === undefined) {
|
||||
data[key] = []
|
||||
}
|
||||
data[key].push(value)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
this.formData = data
|
||||
}
|
||||
} catch (error) {
|
||||
this.resetForm()
|
||||
notify.error({
|
||||
title: 'Unhandled Job',
|
||||
message: error.message
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.delete = id => xo.job.delete(id).then(refresh).then(() => {
|
||||
if (id === this.editedJobId) {
|
||||
this.resetForm()
|
||||
}
|
||||
})
|
||||
|
||||
this.run = id => {
|
||||
this.running[id] = true
|
||||
notify.info({
|
||||
title: 'Run Job',
|
||||
message: 'One shot running started. See overview for logs.'
|
||||
})
|
||||
return xo.job.runSequence([id]).finally(() => delete this.running[id])
|
||||
}
|
||||
})
|
||||
|
||||
.directive('stringInput', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
model: '=',
|
||||
form: '=',
|
||||
key: '=',
|
||||
property: '='
|
||||
},
|
||||
bindToController: true,
|
||||
controller: function () {
|
||||
this.isRequired = () => isRequired(this.property)
|
||||
this.active = () => getType(this.property) === 'string' && !includes(['id', 'host', 'host_id', 'target_host_id', 'sr', 'target_sr_id', 'vm', 'pool', 'subject', 'object', 'action'], this.key)
|
||||
},
|
||||
controllerAs: 'ctrl',
|
||||
template: stringInputView
|
||||
}
|
||||
})
|
||||
|
||||
.directive('booleanInput', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
model: '=',
|
||||
form: '=',
|
||||
key: '=',
|
||||
property: '='
|
||||
},
|
||||
bindToController: true,
|
||||
controller: function () {
|
||||
this.isRequired = () => isRequired(this.property)
|
||||
this.active = () => getType(this.property) === 'boolean'
|
||||
},
|
||||
controllerAs: 'ctrl',
|
||||
template: booleanInputView
|
||||
}
|
||||
})
|
||||
|
||||
.directive('integerInput', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
model: '=',
|
||||
form: '=',
|
||||
key: '=',
|
||||
property: '='
|
||||
},
|
||||
bindToController: true,
|
||||
controller: function () {
|
||||
this.isRequired = () => isRequired(this.property)
|
||||
this.active = () => getType(this.property) === 'integer'
|
||||
},
|
||||
controllerAs: 'ctrl',
|
||||
template: integerInputView
|
||||
}
|
||||
})
|
||||
|
||||
.directive('numberInput', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
model: '=',
|
||||
form: '=',
|
||||
key: '=',
|
||||
property: '='
|
||||
},
|
||||
bindToController: true,
|
||||
controller: function () {
|
||||
this.isRequired = () => isRequired(this.property)
|
||||
this.active = () => getType(this.property) === 'number'
|
||||
},
|
||||
controllerAs: 'ctrl',
|
||||
template: numberInputView
|
||||
}
|
||||
})
|
||||
|
||||
.directive('arrayInput', function ($compile) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
model: '=',
|
||||
form: '=',
|
||||
key: '=',
|
||||
property: '='
|
||||
},
|
||||
controller: 'ArrayInput as ctrl',
|
||||
bindToController: true,
|
||||
link: function (scope, element, attrs) {
|
||||
const updateElement = () => {
|
||||
if (scope.ctrl.property.items) {
|
||||
element.append(arrayInputView)
|
||||
}
|
||||
$compile(element.contents())(scope)
|
||||
}
|
||||
|
||||
updateElement()
|
||||
}
|
||||
}
|
||||
})
|
||||
.controller('ArrayInput', function ($scope) {
|
||||
this.isRequired = () => false
|
||||
this.getType = getType
|
||||
this.active = () => getType(this.property) === 'array'
|
||||
this.add = value => {
|
||||
const type = getType(this.property.items)
|
||||
switch (type) {
|
||||
case 'boolean':
|
||||
value = Boolean(value)
|
||||
break
|
||||
case 'string':
|
||||
value = trim(value)
|
||||
break
|
||||
}
|
||||
this.model.push(value)
|
||||
}
|
||||
this.remove = index => this.model.splice(index, 1)
|
||||
const init = () => {
|
||||
if (this.model === undefined || this.model === null) {
|
||||
this.model = []
|
||||
}
|
||||
}
|
||||
|
||||
if (this.active()) {
|
||||
init()
|
||||
if (!Array.isArray(this.model)) {
|
||||
throw new Error('arrayInput directive model must be an array')
|
||||
}
|
||||
$scope.$watch(() => this.model, init)
|
||||
}
|
||||
})
|
||||
|
||||
.directive('objectInput', function ($compile) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
model: '=',
|
||||
form: '=',
|
||||
key: '=',
|
||||
property: '='
|
||||
},
|
||||
controller: 'ObjectInput as ctrl',
|
||||
bindToController: true,
|
||||
link: function (scope, element, attrs) {
|
||||
const updateElement = () => {
|
||||
if (scope.ctrl.property.properties) {
|
||||
element.append(objectInputView)
|
||||
}
|
||||
$compile(element.contents())(scope)
|
||||
}
|
||||
|
||||
updateElement()
|
||||
}
|
||||
}
|
||||
})
|
||||
.controller('ObjectInput', function ($scope) {
|
||||
this.isRequired = () => isRequired(this.property)
|
||||
this.active = () => getType(this.property) === 'object' && (this.key !== 'object' || actionGroup.get() !== 'acl')
|
||||
const init = () => {
|
||||
if (this.model === undefined || this.model === null) {
|
||||
this.model = {
|
||||
__use: this.isRequired()
|
||||
}
|
||||
}
|
||||
if (typeof this.model !== 'object' || Array.isArray(this.model)) {
|
||||
throw new Error('objectInput directive model must be a plain object')
|
||||
}
|
||||
const use = this.model.__use
|
||||
delete this.model.__use
|
||||
this.model.__use = Object.keys(this.model).length > 0 || use
|
||||
forEach(this.property.properties, (property, key) => {
|
||||
if (getType(property) === 'boolean') {
|
||||
this.model[key] = Boolean(this.model[key])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (this.active()) {
|
||||
init()
|
||||
$scope.$watch(() => this.model, (newVal, oldVal) => {
|
||||
if (newVal !== oldVal) {
|
||||
init()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
.directive('vmInput', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
form: '=',
|
||||
key: '=',
|
||||
property: '=',
|
||||
model: '='
|
||||
},
|
||||
controller: 'VmInput as ctrl',
|
||||
bindToController: true,
|
||||
template: vmInputView
|
||||
}
|
||||
})
|
||||
.controller('VmInput', function ($scope, xoApi) {
|
||||
this.objects = xoApi.all
|
||||
this.isRequired = () => isRequired(this.property)
|
||||
this.active = () => getType(this.property) === 'string' && (this.key === 'vm' || (actionGroup.get() === 'vm' && this.key === 'id'))
|
||||
|
||||
this.init = _initXoObjectInput
|
||||
this.exportRemove = _exportRemove
|
||||
this.exportSelect = _exportSelect
|
||||
|
||||
if (this.active()) {
|
||||
this.init()
|
||||
$scope.$watch(() => this.model, (newVal, oldVal) => {
|
||||
if (newVal !== oldVal) {
|
||||
this.init()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
.directive('hostInput', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
form: '=',
|
||||
key: '=',
|
||||
property: '=',
|
||||
model: '='
|
||||
},
|
||||
controller: 'HostInput as ctrl',
|
||||
bindToController: true,
|
||||
template: hostInputView
|
||||
}
|
||||
})
|
||||
.controller('HostInput', function ($scope, xoApi) {
|
||||
this.objects = xoApi.all
|
||||
this.isRequired = () => isRequired(this.property)
|
||||
this.active = () => getType(this.property) === 'string' && (includes(['host', 'host_id', 'target_host_id'], this.key) || (actionGroup.get() === 'host' && this.key === 'id'))
|
||||
|
||||
this.init = _initXoObjectInput
|
||||
this.exportRemove = _exportRemove
|
||||
this.exportSelect = _exportSelect
|
||||
|
||||
if (this.active()) {
|
||||
this.init()
|
||||
$scope.$watch(() => this.model, (newVal, oldVal) => {
|
||||
if (newVal !== oldVal) {
|
||||
this.init()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
.directive('srInput', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
form: '=',
|
||||
key: '=',
|
||||
property: '=',
|
||||
model: '='
|
||||
},
|
||||
controller: 'SrInput as ctrl',
|
||||
bindToController: true,
|
||||
template: srInputView
|
||||
}
|
||||
})
|
||||
.controller('SrInput', function ($scope, xoApi) {
|
||||
this.objects = xoApi.all
|
||||
this.isRequired = () => isRequired(this.property)
|
||||
this.active = () => getType(this.property) === 'string' && includes(['sr', 'sr_id', 'target_sr_id'], this.key)
|
||||
|
||||
this.init = _initXoObjectInput
|
||||
this.exportRemove = _exportRemove
|
||||
this.exportSelect = _exportSelect
|
||||
|
||||
if (this.active()) {
|
||||
this.init()
|
||||
$scope.$watch(() => this.model, (newVal, oldVal) => {
|
||||
if (newVal !== oldVal) {
|
||||
this.init()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
.directive('poolInput', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
form: '=',
|
||||
key: '=',
|
||||
property: '=',
|
||||
model: '='
|
||||
},
|
||||
controller: 'PoolInput as ctrl',
|
||||
bindToController: true,
|
||||
template: poolInputView
|
||||
}
|
||||
})
|
||||
.controller('PoolInput', function ($scope, xoApi) {
|
||||
this.objects = xoApi.all
|
||||
this.isRequired = () => isRequired(this.property)
|
||||
this.active = () => getType(this.property) === 'string' && includes(['pool', 'pool_id', 'target_pool_id'], this.key)
|
||||
|
||||
this.init = _initXoObjectInput
|
||||
this.exportRemove = _exportRemove
|
||||
this.exportSelect = _exportSelect
|
||||
|
||||
if (this.active()) {
|
||||
this.init()
|
||||
$scope.$watch(() => this.model, (newVal, oldVal) => {
|
||||
if (newVal !== oldVal) {
|
||||
this.init()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
.directive('xoEntityInput', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
form: '=',
|
||||
key: '=',
|
||||
property: '=',
|
||||
model: '='
|
||||
},
|
||||
controller: 'XoEntityInput as ctrl',
|
||||
bindToController: true,
|
||||
template: xoEntityInputView
|
||||
}
|
||||
})
|
||||
.controller('XoEntityInput', function ($scope, xo) {
|
||||
this.ready = false
|
||||
this.isRequired = () => isRequired(this.property)
|
||||
this.active = () => this.ready && getType(this.property) === 'string' && this.key === 'subject' && actionGroup.get() === 'acl'
|
||||
|
||||
this.init = _initXoObjectInput
|
||||
this.exportRemove = _exportRemove
|
||||
this.exportSelect = _exportSelect
|
||||
|
||||
Bluebird.props({
|
||||
users: xo.user.getAll(),
|
||||
groups: xo.group.getAll()
|
||||
})
|
||||
.then(p => {
|
||||
this.objects = p.users.concat(p.groups)
|
||||
this.ready = true
|
||||
if (this.active()) {
|
||||
this.init()
|
||||
$scope.$watch(() => this.model, (newVal, oldVal) => {
|
||||
if (newVal !== oldVal) {
|
||||
this.init()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
.directive('xoRoleInput', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
form: '=',
|
||||
key: '=',
|
||||
property: '=',
|
||||
model: '='
|
||||
},
|
||||
controller: 'XoRoleInput as ctrl',
|
||||
bindToController: true,
|
||||
template: xoRoleInputView
|
||||
}
|
||||
})
|
||||
.controller('XoRoleInput', function ($scope, xo) {
|
||||
this.ready = false
|
||||
this.isRequired = () => isRequired(this.property)
|
||||
this.active = () => this.ready && getType(this.property) === 'string' && this.key === 'action' && actionGroup.get() === 'acl'
|
||||
|
||||
this.init = _initXoObjectInput
|
||||
this.exportRemove = _exportRemove
|
||||
this.exportSelect = _exportSelect
|
||||
|
||||
xo.role.getAll()
|
||||
.then(roles => {
|
||||
this.objects = roles
|
||||
this.ready = true
|
||||
if (this.active()) {
|
||||
this.init()
|
||||
$scope.$watch(() => this.model, (newVal, oldVal) => {
|
||||
if (newVal !== oldVal) {
|
||||
this.init()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
.directive('xoObjectInput', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
form: '=',
|
||||
key: '=',
|
||||
property: '=',
|
||||
model: '='
|
||||
},
|
||||
controller: 'XoObjectInput as ctrl',
|
||||
bindToController: true,
|
||||
template: xoObjectInputView
|
||||
}
|
||||
})
|
||||
.controller('XoObjectInput', function ($scope, xoApi, filterFilter, selectHighLevelFilter) {
|
||||
const HIGH_LEVEL_OBJECTS = {
|
||||
pool: true,
|
||||
host: true,
|
||||
VM: true,
|
||||
SR: true,
|
||||
network: true
|
||||
}
|
||||
this.types = Object.keys(HIGH_LEVEL_OBJECTS)
|
||||
this.objects = xoApi.all
|
||||
|
||||
this.isRequired = () => isRequired(this.property)
|
||||
this.active = () => getType(this.property) === 'string' && this.key === 'object' && actionGroup.get() === 'acl'
|
||||
this.toggleType = (toggle, type) => {
|
||||
const selectedObjects = this.intraModel && this.intraModel.slice() || []
|
||||
if (toggle) {
|
||||
const objects = filterFilter(selectHighLevelFilter(this.objects), {type})
|
||||
forEach(objects, object => { selectedObjects.indexOf(object) === -1 && selectedObjects.push(object) })
|
||||
this.intraModel = selectedObjects
|
||||
} else {
|
||||
const keptObjects = []
|
||||
for (let index in selectedObjects) {
|
||||
const object = selectedObjects[index]
|
||||
if (object.type !== type) {
|
||||
keptObjects.push(object)
|
||||
}
|
||||
}
|
||||
this.intraModel = keptObjects
|
||||
}
|
||||
this.model.length = 0
|
||||
forEach(this.intraModel, item => this.model.push(reduceXoObject(item)))
|
||||
}
|
||||
|
||||
this.init = _initXoObjectInput
|
||||
this.exportRemove = _exportRemove
|
||||
this.exportSelect = _exportSelect
|
||||
|
||||
if (this.active()) {
|
||||
this.init()
|
||||
$scope.$watch(() => this.model, (newVal, oldVal) => {
|
||||
if (newVal !== oldVal) {
|
||||
this.init()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
// A module exports its name.
|
||||
.name
|
||||
6
app/modules/task-scheduler/job/integer-input-view.jade
Normal file
6
app/modules/task-scheduler/job/integer-input-view.jade
Normal file
@@ -0,0 +1,6 @@
|
||||
.form-group(ng-if = 'ctrl.active()')
|
||||
label.col-md-2.control-label(ng-if = 'ctrl.key')
|
||||
| {{ ctrl.key }}
|
||||
span.text-warning(ng-if = 'ctrl.isRequired()') *
|
||||
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
|
||||
input.form-control(form = '{{ ctrl.form }}', type = 'number', step = '1', ng-model = 'ctrl.model', ng-required = 'ctrl.isRequired()')
|
||||
6
app/modules/task-scheduler/job/number-input-view.jade
Normal file
6
app/modules/task-scheduler/job/number-input-view.jade
Normal file
@@ -0,0 +1,6 @@
|
||||
.form-group(ng-if = 'ctrl.active()')
|
||||
label.col-md-2.control-label(ng-if = 'ctrl.key')
|
||||
| {{ ctrl.key }}
|
||||
span.text-warning(ng-if = 'ctrl.isRequired()') *
|
||||
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
|
||||
input.form-control(form = '{{ ctrl.form }}', type = 'number', step = 'any', ng-model = 'ctrl.model', ng-required = 'ctrl.isRequired()')
|
||||
30
app/modules/task-scheduler/job/object-input-view.jade
Normal file
30
app/modules/task-scheduler/job/object-input-view.jade
Normal file
@@ -0,0 +1,30 @@
|
||||
.form-group(ng-if = 'ctrl.active()')
|
||||
label.col-md-2.control-label(ng-if = 'ctrl.key')
|
||||
| {{ ctrl.key }}
|
||||
span.text-warning(ng-if = 'ctrl.isRequired()') *
|
||||
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
|
||||
.checkbox(ng-if = '!ctrl.isRequired()')
|
||||
label
|
||||
input(type = 'checkbox', ng-model = 'ctrl.model.__use')
|
||||
| Fill informations (optional)
|
||||
hr
|
||||
.help-block(ng-if = 'ctrl.isRequired()')
|
||||
span.text-warning Fill required informations
|
||||
hr
|
||||
fieldset.form-horizontal(ng-disabled = '!ctrl.isRequired() && !ctrl.model.__use', ng-hide = '!ctrl.isRequired() && !ctrl.model.__use')
|
||||
.form-group(ng-if = '(ctrl.property.properties | count) < 1')
|
||||
p.help-block No parameters
|
||||
div(ng-repeat = '(key, property) in ctrl.property.properties')
|
||||
array-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
|
||||
boolean-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
|
||||
host-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
|
||||
integer-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
|
||||
number-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
|
||||
object-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
|
||||
pool-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
|
||||
sr-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
|
||||
string-input(form = 'ctrl.form', model = 'ctrl.model[key]', key = 'key', property = 'property')
|
||||
vm-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
|
||||
xo-entity-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
|
||||
xo-object-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
|
||||
xo-role-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
|
||||
15
app/modules/task-scheduler/job/pool-input-view.jade
Normal file
15
app/modules/task-scheduler/job/pool-input-view.jade
Normal file
@@ -0,0 +1,15 @@
|
||||
.form-group(ng-if = 'ctrl.active()')
|
||||
label.col-md-2.control-label(ng-if = 'ctrl.key')
|
||||
| Pools
|
||||
span.text-warning(ng-if = 'ctrl.isRequired()') *
|
||||
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
|
||||
ui-select(form = 'ctrl.form', ng-model = 'ctrl.intraModel', multiple, close-on-select = 'false', ng-required = 'ctrl.isRequired()', on-remove = 'ctrl.exportRemove($item)', on-select = 'ctrl.exportSelect($item)')
|
||||
ui-select-match(placeholder = 'Choose pools')
|
||||
i(class = 'xo-icon-{{$item.type | lowercase}}')
|
||||
| {{$item.name_label}}
|
||||
span(ng-if = '$item.$container') ({{ ($item.$container | resolve).name_label }})
|
||||
ui-select-choices(repeat = 'object in ctrl.objects | selectHighLevel | filter:{type: "pool"} | filter:$select.search | orderBy:["$container", "name_label"] track by object.id')
|
||||
div
|
||||
i(class = 'xo-icon-{{object.type | lowercase}}')
|
||||
| {{ object.name_label }}
|
||||
span(ng-if = 'object.$container') ({{ (object.$container | resolve).name_label || ((object.$container | resolve).master | resolve).name_label }})
|
||||
15
app/modules/task-scheduler/job/sr-input-view.jade
Normal file
15
app/modules/task-scheduler/job/sr-input-view.jade
Normal file
@@ -0,0 +1,15 @@
|
||||
.form-group(ng-if = 'ctrl.active()')
|
||||
label.col-md-2.control-label(ng-if = 'ctrl.key')
|
||||
| SRs
|
||||
span.text-warning(ng-if = 'ctrl.isRequired()') *
|
||||
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
|
||||
ui-select(form = 'ctrl.form', ng-model = 'ctrl.intraModel', multiple, close-on-select = 'false', ng-required = 'ctrl.isRequired()', on-remove = 'ctrl.exportRemove($item)', on-select = 'ctrl.exportSelect($item)')
|
||||
ui-select-match(placeholder = 'Choose Storage Repositories')
|
||||
i(class = 'xo-icon-{{$item.type | lowercase}}')
|
||||
| {{$item.name_label}}
|
||||
span(ng-if = '$item.$container') ({{ ($item.$container | resolve).name_label }})
|
||||
ui-select-choices(repeat = 'object in ctrl.objects | selectHighLevel | filter:{type: "sr"} | filter:$select.search | orderBy:["$container", "name_label"] track by object.id')
|
||||
div
|
||||
i(class = 'xo-icon-{{object.type | lowercase}}')
|
||||
| {{ object.name_label }}
|
||||
span(ng-if = 'object.$container') ({{ (object.$container | resolve).name_label || ((object.$container | resolve).master | resolve).name_label }})
|
||||
6
app/modules/task-scheduler/job/string-input-view.jade
Normal file
6
app/modules/task-scheduler/job/string-input-view.jade
Normal file
@@ -0,0 +1,6 @@
|
||||
.form-group(ng-if = 'ctrl.active()')
|
||||
label.col-md-2.control-label(ng-if = 'ctrl.key')
|
||||
| {{ ctrl.key }}
|
||||
span.text-warning(ng-if = 'ctrl.isRequired()') *
|
||||
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
|
||||
input.form-control(form = '{{ ctrl.form }}', type = 'text', ng-model = 'ctrl.model', ng-required = 'ctrl.isRequired()')
|
||||
68
app/modules/task-scheduler/job/view.jade
Normal file
68
app/modules/task-scheduler/job/view.jade
Normal file
@@ -0,0 +1,68 @@
|
||||
.grid
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.fa.fa-cogs
|
||||
| Jobs
|
||||
form#jobform(ng-submit = 'ctrl.save(ctrl.editedJobId, ctrl.jobName, ctrl.selectedAction, ctrl.formData)')
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-wrench
|
||||
| {{ ctrl.editedJobId ? "Edit" : "Create" }}
|
||||
.panel-body
|
||||
.alert.alert-warning(ng-if = 'ctrl.editedJobId') Editing Job ID: {{ ctrl.editedJobId }}
|
||||
fieldset.form-horizontal(ng-disabled = '!ctrl.ready')
|
||||
.form-group
|
||||
label.col-sm-2.control-label Job Name
|
||||
.col-sm-10
|
||||
input.form-control(form = 'jobform', type = 'text', ng-model = 'ctrl.jobName', required, placeholder = 'An explicit name for your job')
|
||||
.form-group
|
||||
label.col-sm-2.control-label {{ ctrl.selectedAction ? (ctrl.selectedAction.group + ".") : "Action" }}
|
||||
.col-sm-10
|
||||
select.form-control(form = 'jobform', ng-model = 'ctrl.selectedAction', ng-options = 'action.command group by action.group for action in ctrl.actions', ng-change = 'ctrl.resetData()', required)
|
||||
option(value = '') -- Choose an action --
|
||||
p.help-block(ng-if = 'ctrl.selectedAction.info.description') {{ ctrl.selectedAction.info.description }}
|
||||
.form-group(ng-if = 'ctrl.selectedAction.info.permission')
|
||||
label.col-sm-2.control-label Permission
|
||||
.col-sm-10: p.form-control-static {{ ctrl.selectedAction.info.permission }}
|
||||
fieldset.form-horizontal(ng-if = 'ctrl.selectedAction', ng-disabled = '!ctrl.ready')
|
||||
legend Parameters
|
||||
object-input(form = '"jobform"', property = 'ctrl.selectedAction.info', model = 'ctrl.formData')
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-body
|
||||
fieldset.center(ng-disabled = '!ctrl.ready')
|
||||
button.btn.btn-lg.btn-primary(form = 'jobform', type = 'submit')
|
||||
i.fa.fa-clock-o
|
||||
|
|
||||
i.fa.fa-arrow-right
|
||||
|
|
||||
i.fa.fa-database
|
||||
| Save
|
||||
|
|
||||
button.btn.btn-lg.btn-default(type = 'button', ng-click = 'ctrl.resetForm()')
|
||||
| Reset
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-list-ul
|
||||
| Jobs
|
||||
.panel-body
|
||||
.text-center(ng-if = '!(ctrl.jobs | count)') No jobs found
|
||||
table.table(ng-if = 'ctrl.jobs | count')
|
||||
tr
|
||||
th Name
|
||||
th Action
|
||||
th
|
||||
tr(ng-repeat = 'job in ctrl.jobs | map | orderBy:"name" track by job.id')
|
||||
td
|
||||
| {{ job.name }} 
|
||||
span.text-muted.hidden-xs ({{ job.id }})
|
||||
td {{ job.method }}
|
||||
td
|
||||
span.pull-left
|
||||
button.btn.btn-warning(type = 'button', ng-click = 'ctrl.run(job.id)', ng-disabled = 'ctrl.running[job.id]'): i.fa.fa-play
|
||||
span.pull-right
|
||||
button.btn.btn-primary(type = 'button', ng-click = 'ctrl.edit(job.id)'): i.fa.fa-pencil
|
||||
|
|
||||
button.btn.btn-danger(type = 'button', ng-click = 'ctrl.delete(job.id)'): i.fa.fa-trash
|
||||
17
app/modules/task-scheduler/job/vm-input-view.jade
Normal file
17
app/modules/task-scheduler/job/vm-input-view.jade
Normal file
@@ -0,0 +1,17 @@
|
||||
.form-group(ng-if = 'ctrl.active()')
|
||||
label.col-md-2.control-label(ng-if = 'ctrl.key')
|
||||
| VMs
|
||||
span.text-warning(ng-if = 'ctrl.isRequired()') *
|
||||
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
|
||||
ui-select(form = 'ctrl.form', ng-model = 'ctrl.intraModel', multiple, close-on-select = 'false', ng-required = 'ctrl.isRequired()', on-remove = 'ctrl.exportRemove($item)', on-select = 'ctrl.exportSelect($item)')
|
||||
ui-select-match(placeholder = 'Choose VMs')
|
||||
i.xo-icon-working(ng-if = 'isVMWorking($item)')
|
||||
i(ng-class = '"xo-icon-" + ($item.power_state | lowercase)', ng-if = '!isVMWorking($item)')
|
||||
| {{ $item.name_label }}
|
||||
span(ng-if = '$item.$container') ({{ ($item.$container | resolve).name_label }})
|
||||
ui-select-choices(repeat = 'vm in ctrl.objects | selectHighLevel | filter:{type: "vm"} | filter:$select.search | orderBy:["$container", "name_label"] track by vm.id')
|
||||
div
|
||||
i.xo-icon-working(ng-if = 'isVMWorking(vm)', tooltip = '{{ vm.power_state }} and {{ (vm.current_operations | map)[0] }}')
|
||||
i(ng-class = '"xo-icon-" + (vm.power_state | lowercase)', ng-if = '!isVMWorking(vm)', tooltip = '{{ vm.power_state }}')
|
||||
| {{ vm.name_label }}
|
||||
span(ng-if = 'vm.$container') ({{ (vm.$container | resolve).name_label || ((vm.$container | resolve).master | resolve).name_label }})
|
||||
21
app/modules/task-scheduler/job/xo-entity-input-view.jade
Normal file
21
app/modules/task-scheduler/job/xo-entity-input-view.jade
Normal file
@@ -0,0 +1,21 @@
|
||||
.form-group(ng-if = 'ctrl.active()')
|
||||
label.col-md-2.control-label(ng-if = 'ctrl.key')
|
||||
| Users / Groups
|
||||
span.text-warning(ng-if = 'ctrl.isRequired()') *
|
||||
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
|
||||
ui-select(ng-model = 'ctrl.intraModel', multiple, close-on-select = 'false', ng-required = 'ctrl.isRequired()', on-remove = 'ctrl.exportRemove($item)', on-select = 'ctrl.exportSelect($item)')
|
||||
ui-select-match(placeholder = 'Choose users or groups')
|
||||
span(ng-if = '$item.email')
|
||||
i.xo-icon-user.fa-fw
|
||||
| {{$item.email}}
|
||||
span(ng-if = '$item.name')
|
||||
i.xo-icon-group.fa-fw
|
||||
| {{$item.name}}
|
||||
ui-select-choices(repeat = 'entity in ctrl.objects | filter:{ permission: "!admin" } | filter:$select.search')
|
||||
div
|
||||
span(ng-if = 'entity.email')
|
||||
i.xo-icon-user.fa-fw
|
||||
| {{entity.email}}
|
||||
span(ng-if = 'entity.name')
|
||||
i.xo-icon-group.fa-fw
|
||||
| {{entity.name}}
|
||||
28
app/modules/task-scheduler/job/xo-object-input-view.jade
Normal file
28
app/modules/task-scheduler/job/xo-object-input-view.jade
Normal file
@@ -0,0 +1,28 @@
|
||||
.form-group(ng-if = 'ctrl.active()')
|
||||
label.col-md-2.control-label(ng-if = 'ctrl.key')
|
||||
| Objects
|
||||
span.text-warning(ng-if = 'ctrl.isRequired()') *
|
||||
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
|
||||
ui-select(ng-model = 'ctrl.intraModel', multiple, close-on-select = 'false', ng-required = 'ctrl.isRequired()', on-remove = 'ctrl.exportRemove($item)', on-select = 'ctrl.exportSelect($item)')
|
||||
ui-select-match(placeholder = 'Choose an object')
|
||||
i(class = 'xo-icon-{{$item.type | lowercase}}')
|
||||
| {{$item.name_label}}
|
||||
span(ng-if="($item.type === 'SR' || $item.type === 'VM') && $item.$container")
|
||||
| ({{($item.$container | resolve).name_label}})
|
||||
span(ng-if="$item.type === 'network'")
|
||||
| ({{($item.$poolId | resolve).name_label}})
|
||||
ui-select-choices(repeat = 'object in ctrl.objects | selectHighLevel | filter:$select.search | orderBy:["type", "name_label"]')
|
||||
div
|
||||
i(class = 'xo-icon-{{object.type | lowercase}}')
|
||||
| {{object.name_label}}
|
||||
span(ng-if="(object.type === 'SR' || object.type === 'VM') && object.$container")
|
||||
| ({{(object.$container | resolve).name_label}})
|
||||
span(ng-if="object.type === 'network'")
|
||||
| ({{(object.$poolId | resolve).name_label}})
|
||||
.text-center
|
||||
span(ng-repeat = 'type in ctrl.types')
|
||||
label(tooltip = 'select/deselect all {{type}}s', style = 'cursor: pointer')
|
||||
input.hidden(type = 'checkbox', ng-model = 'selectedTypes[type]', ng-change = 'ctrl.toggleType(selectedTypes[type], type)')
|
||||
span.fa-stack
|
||||
i(class = 'xo-icon-{{type | lowercase}}').fa-stack-1x
|
||||
i.fa.fa-square-o.fa-stack-2x.text-info(ng-if = 'selectedTypes[type]')
|
||||
13
app/modules/task-scheduler/job/xo-role-input-view.jade
Normal file
13
app/modules/task-scheduler/job/xo-role-input-view.jade
Normal file
@@ -0,0 +1,13 @@
|
||||
.form-group(ng-if = 'ctrl.active()')
|
||||
label.col-md-2.control-label(ng-if = 'ctrl.key')
|
||||
| Roles
|
||||
span.text-warning(ng-if = 'ctrl.isRequired()') *
|
||||
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
|
||||
ui-select(ng-model = 'ctrl.intraModel', multiple, close-on-select = 'false', ng-required = 'ctrl.isRequired()', on-remove = 'ctrl.exportRemove($item)', on-select = 'ctrl.exportSelect($item)')
|
||||
ui-select-match(placeholder = 'Choose a role')
|
||||
i(class = 'xo-icon-{{$item.type | lowercase}}')
|
||||
| {{$item.name}}
|
||||
ui-select-choices(repeat = 'role in ctrl.objects | filter:$select.search | orderBy:"name"')
|
||||
div
|
||||
i(class = 'xo-icon-{{role.type | lowercase}}')
|
||||
| {{role.name}}
|
||||
171
app/modules/task-scheduler/overview/index.js
Normal file
171
app/modules/task-scheduler/overview/index.js
Normal file
@@ -0,0 +1,171 @@
|
||||
import angular from 'angular'
|
||||
import filter from 'lodash.filter'
|
||||
import forEach from 'lodash.foreach'
|
||||
import prettyCron from 'prettycron'
|
||||
import uiBootstrap from 'angular-ui-bootstrap'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
|
||||
import view from './view'
|
||||
|
||||
// ====================================================================
|
||||
|
||||
const JOB_KEY = 'genericTask'
|
||||
|
||||
export default angular.module('taskscheduler.overview', [
|
||||
uiRouter,
|
||||
uiBootstrap
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('taskscheduler.overview', {
|
||||
url: '/overview',
|
||||
controller: 'OverviewCtrl as ctrl',
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('OverviewCtrl', function ($scope, $state, $stateParams, $interval, xo, xoApi, notify, selectHighLevelFilter, filterFilter) {
|
||||
this.running = {}
|
||||
|
||||
this.currentLogPage = 1
|
||||
this.logPageSize = 10
|
||||
|
||||
const refreshSchedules = () => {
|
||||
xo.schedule.getAll()
|
||||
.then(schedules => this.schedules = filter(schedules, schedule => this.jobs[schedule.job] && this.jobs[schedule.job].key === JOB_KEY))
|
||||
xo.scheduler.getScheduleTable()
|
||||
.then(table => this.scheduleTable = table)
|
||||
}
|
||||
|
||||
const getLogs = () => {
|
||||
xo.logs.get('jobs').then(logs => {
|
||||
const viewLogs = {}
|
||||
forEach(logs, (log, logKey) => {
|
||||
const data = log.data
|
||||
const [time] = logKey.split(':')
|
||||
if (data.event === 'job.start' && data.key === JOB_KEY) {
|
||||
viewLogs[logKey] = {
|
||||
logKey,
|
||||
jobId: data.jobId,
|
||||
key: data.key,
|
||||
userId: data.userId,
|
||||
start: time,
|
||||
calls: {},
|
||||
time
|
||||
}
|
||||
} else {
|
||||
const runJobId = data.runJobId
|
||||
const entry = viewLogs[runJobId]
|
||||
if (!entry) {
|
||||
return
|
||||
}
|
||||
|
||||
if (data.event === 'job.end') {
|
||||
if (data.error) {
|
||||
entry.error = data.error
|
||||
}
|
||||
entry.end = time
|
||||
entry.duration = time - entry.start
|
||||
entry.status = 'Terminated'
|
||||
} else if (data.event === 'jobCall.start') {
|
||||
entry.calls[logKey] = {
|
||||
callKey: logKey,
|
||||
params: resolveParams(data.params),
|
||||
method: data.method,
|
||||
time
|
||||
}
|
||||
} else if (data.event === 'jobCall.end') {
|
||||
const call = entry.calls[data.runCallId]
|
||||
|
||||
if (data.error) {
|
||||
call.error = data.error
|
||||
entry.hasErrors = true
|
||||
} else {
|
||||
call.returnedValue = resolveReturn(data.returnedValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
forEach(viewLogs, log => {
|
||||
if (log.end === undefined) {
|
||||
log.status = 'In progress'
|
||||
}
|
||||
})
|
||||
|
||||
this.logs = viewLogs
|
||||
})
|
||||
}
|
||||
|
||||
const resolveParams = params => {
|
||||
for (let key in params) {
|
||||
const xoObject = xoApi.get(params[key])
|
||||
if (xoObject) {
|
||||
const newKey = xoObject.type || key
|
||||
params[newKey] = xoObject.name_label || xoObject.name || params[key]
|
||||
newKey !== key && delete params[key]
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
const resolveReturn = returnValue => {
|
||||
const xoObject = xoApi.get(returnValue)
|
||||
let xoName = xoObject && (xoObject.name_label || xoObject.name)
|
||||
xoName && (xoName += xoObject.type && ` (${xoObject.type})` || '')
|
||||
returnValue = xoName || returnValue
|
||||
return returnValue
|
||||
}
|
||||
|
||||
this.prettyCron = prettyCron.toString.bind(prettyCron)
|
||||
|
||||
const refreshJobs = () => {
|
||||
return xo.job.getAll()
|
||||
.then(jobs => {
|
||||
const j = {}
|
||||
forEach(jobs, job => j[job.id] = job)
|
||||
this.jobs = j
|
||||
})
|
||||
}
|
||||
|
||||
const refresh = () => {
|
||||
refreshJobs().then(refreshSchedules)
|
||||
getLogs()
|
||||
}
|
||||
|
||||
refresh()
|
||||
const interval = $interval(() => {
|
||||
refresh()
|
||||
}, 5e3)
|
||||
$scope.$on('$destroy', () => {
|
||||
$interval.cancel(interval)
|
||||
})
|
||||
|
||||
this.enable = id => {
|
||||
this.working[id] = true
|
||||
return xo.scheduler.enable(id)
|
||||
.finally(() => { this.working[id] = false })
|
||||
.then(refreshSchedules)
|
||||
}
|
||||
this.disable = id => {
|
||||
this.working[id] = true
|
||||
return xo.scheduler.disable(id)
|
||||
.finally(() => { this.working[id] = false })
|
||||
.then(refreshSchedules)
|
||||
}
|
||||
this.run = schedule => {
|
||||
this.running[schedule.id] = true
|
||||
notify.info({
|
||||
title: 'Run Job',
|
||||
message: 'One shot running started. See overview for logs.'
|
||||
})
|
||||
const id = schedule.job
|
||||
return xo.job.runSequence([id]).finally(() => delete this.running[schedule.id])
|
||||
}
|
||||
|
||||
this.displayScheduleJobName = schedule => this.jobs[schedule.job] && this.jobs[schedule.job].name
|
||||
|
||||
this.collectionLength = col => Object.keys(col).length
|
||||
this.working = {}
|
||||
})
|
||||
|
||||
// A module exports its name.
|
||||
.name
|
||||
85
app/modules/task-scheduler/overview/view.jade
Normal file
85
app/modules/task-scheduler/overview/view.jade
Normal file
@@ -0,0 +1,85 @@
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.fa.fa-eye
|
||||
| Job Scheduling Overview
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-clock-o
|
||||
| Schedules
|
||||
.panel-body
|
||||
//- The 2 tables below are here for a "full-width" effect of the content vs the menu (cf sheduler/view.jade)
|
||||
table.table(ng-if = '!ctrl.schedules')
|
||||
tr
|
||||
td.text-center: i.xo-icon-loading
|
||||
table.table(ng-if = 'ctrl.schedules && !ctrl.collectionLength(ctrl.schedules)')
|
||||
tr
|
||||
td.text-center No scheduled jobs
|
||||
table.table.table-hover(ng-if = 'ctrl.schedules && ctrl.collectionLength(ctrl.schedules)')
|
||||
tr
|
||||
th Name
|
||||
th Job
|
||||
th.hidden-xs Scheduling
|
||||
th State
|
||||
tr(ng-repeat = 'schedule in ctrl.schedules | orderBy:"id":true track by schedule.id')
|
||||
td: a(ui-sref = 'taskscheduler.schedule({id: schedule.id})') {{ schedule.name || schedule.id }}
|
||||
td
|
||||
a(ui-sref = 'taskscheduler.job({id: schedule.job})') {{ ctrl.displayScheduleJobName(schedule) || schedule.job }}
|
||||
td.hidden-xs {{ ctrl.prettyCron(schedule.cron) }}
|
||||
td
|
||||
span.label.label-success.hidden-xs(ng-if = 'ctrl.scheduleTable[schedule.id] === true') enabled
|
||||
span.label.label-default.hidden-xs(ng-if = 'ctrl.scheduleTable[schedule.id] === false') disabled
|
||||
span.label.label-warning.hidden-xs(ng-if = 'ctrl.scheduleTable[schedule.id] === undefined') unknown
|
||||
fieldset.pull-right(ng-disabled = 'ctrl.working[schedule.id]')
|
||||
button.btn(ng-if = 'ctrl.scheduleTable[schedule.id] === false', type = 'button', ng-click = 'ctrl.enable(schedule.id)'): i.fa.fa-toggle-off
|
||||
button.btn.btn-success(ng-if = 'ctrl.scheduleTable[schedule.id] === true', type = 'button', ng-click = 'ctrl.disable(schedule.id)'): i.fa.fa-toggle-on
|
||||
|
|
||||
button.btn.btn-warning(type = 'button', ng-click = 'ctrl.run(schedule)', ng-disabled = 'ctrl.running[schedule.id]'): i.fa.fa-play
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-file-text
|
||||
| Logs
|
||||
.panel-body
|
||||
table.table.table-hover(ng-if = 'ctrl.logs')
|
||||
thead
|
||||
tr
|
||||
th Job ID
|
||||
th Start
|
||||
th End
|
||||
th Duration
|
||||
th Status
|
||||
tbody(ng-repeat = 'log in ctrl.logs | map | filter:ctrl.logSearch | orderBy:"-time" | slice:(ctrl.logPageSize * (ctrl.currentLogPage - 1)):(ctrl.logPageSize * ctrl.currentLogPage) track by log.logKey')
|
||||
tr
|
||||
td
|
||||
button.btn.btn-sm(type = 'button', tooltip = 'See calls', ng-click = 'seeCalls = !seeCalls', ng-class = '{"btn-default": !log.hasErrors, "btn-danger": log.hasErrors}'): i.fa(ng-class = '{"fa-caret-down": !seeCalls, "fa-caret-up": seeCalls}')
|
||||
| {{ log.jobId }}
|
||||
td {{ log.start | date:'medium' }}
|
||||
td {{ log.end | date:'medium' }}
|
||||
td {{ log.duration | duration}}
|
||||
td
|
||||
span(ng-if = 'log.status === "Terminated"')
|
||||
span.label(ng-class = '{"label-success": (!log.error && !log.hasErrors), "label-danger": (log.error || log.hasErrors)}') {{ log.status }}
|
||||
span.label(ng-if = 'log.status !== "Terminated"', ng-class = '{"label-warning": log.status === "In progress", "label-default": !log.status}') {{ log.status || "unknown" }}
|
||||
p.text-danger(ng-if = 'log.error') {{ log.error }}
|
||||
tr.bg-info(collapse = '!seeCalls')
|
||||
td(colspan = '5')
|
||||
ul.list-group
|
||||
li.list-group-item(ng-repeat = 'call in log.calls | map | orderBy:"-time" track by call.callKey')
|
||||
strong.text-info {{ call.method }}: 
|
||||
span(ng-repeat = '(key, param) in call.params')
|
||||
| {{ key }}:
|
||||
strong {{ param }} 
|
||||
span(ng-if = 'call.returnedValue')
|
||||
|  
|
||||
i.text-primary.fa.fa-arrow-right
|
||||
|  {{ call.returnedValue }}
|
||||
span.text-danger(ng-if = 'call.error')
|
||||
|  
|
||||
i.fa.fa-times
|
||||
|  {{ call.error }}
|
||||
.form-inline
|
||||
.input-group
|
||||
.input-group-addon: i.fa.fa-search
|
||||
input.form-control(type = 'text', ng-model = 'ctrl.logSearch', placeholder = 'Search logs...')
|
||||
.center(ng-if = '(ctrl.logs | map | filter:ctrl.logSearch | count) > ctrl.logPageSize || currentLogPage > 1')
|
||||
pagination.pagination-sm(boundary-links = 'true', total-items = 'ctrl.logs | map | filter:ctrl.logSearch | count', ng-model = 'ctrl.currentLogPage', items-per-page = 'ctrl.logPageSize', max-size = '10', previous-text = '<', next-text = '>', first-text = '<<', last-text = '>>')
|
||||
115
app/modules/task-scheduler/schedule/index.js
Normal file
115
app/modules/task-scheduler/schedule/index.js
Normal file
@@ -0,0 +1,115 @@
|
||||
import angular from 'angular'
|
||||
import Bluebird from 'bluebird'
|
||||
import find from 'lodash.find'
|
||||
import forEach from 'lodash.foreach'
|
||||
import prettyCron from 'prettycron'
|
||||
import uiBootstrap from 'angular-ui-bootstrap'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
Bluebird.longStackTraces()
|
||||
|
||||
import view from './view'
|
||||
|
||||
// ====================================================================
|
||||
|
||||
const JOB_KEY = 'genericTask'
|
||||
|
||||
export default angular.module('xoWebApp.taskscheduler.schedule', [
|
||||
uiRouter,
|
||||
uiBootstrap
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('taskscheduler.schedule', {
|
||||
url: '/schedule/:id',
|
||||
controller: 'ScheduleCtrl as ctrl',
|
||||
template: view
|
||||
})
|
||||
})
|
||||
|
||||
.controller('ScheduleCtrl', function (xo, xoApi, notify, $stateParams) {
|
||||
this.scheduleApi = {}
|
||||
this.formData = {}
|
||||
this.ready = false
|
||||
this.running = {}
|
||||
let comesForEditing = $stateParams.id
|
||||
|
||||
this.reset = () => {
|
||||
this.formData.editedScheduleId = undefined
|
||||
this.formData.scheduleName = undefined
|
||||
this.formData.selectedJob = undefined
|
||||
this.formData.enabled = false
|
||||
this.scheduleApi && this.scheduleApi.resetData && this.scheduleApi.resetData()
|
||||
}
|
||||
|
||||
this.reset()
|
||||
|
||||
const refreshJobs = () => xo.job.getAll().then(jobs => {
|
||||
const j = {}
|
||||
forEach(jobs, job => {
|
||||
if (job.key === JOB_KEY) {
|
||||
j[job.id] = job
|
||||
}
|
||||
})
|
||||
this.jobs = j
|
||||
})
|
||||
|
||||
const refreshSchedules = () => xo.schedule.getAll().then(schedules => {
|
||||
const s = {}
|
||||
forEach(schedules, schedule => {
|
||||
if (this.jobs && this.jobs[schedule.job] && (this.jobs[schedule.job].key === JOB_KEY)) {
|
||||
s[schedule.id] = schedule
|
||||
}
|
||||
})
|
||||
this.schedules = s
|
||||
})
|
||||
|
||||
const refresh = () => refreshJobs().then(refreshSchedules)
|
||||
const getReady = () => refresh().then(() => this.ready = true)
|
||||
getReady().then(() => {
|
||||
if (comesForEditing) {
|
||||
this.edit(comesForEditing)
|
||||
comesForEditing = undefined
|
||||
}
|
||||
})
|
||||
|
||||
const saveNew = (name, job, cron, enabled) => xo.schedule.create(job.id, cron, enabled, name)
|
||||
const save = (id, name, job, cron) => xo.schedule.set(id, job.id, cron, undefined, name)
|
||||
|
||||
this.save = (id, name, job, cron, enabled) => {
|
||||
const saved = (id !== undefined) ? save(id, name, job, cron) : saveNew(name, job, cron, enabled)
|
||||
return saved
|
||||
.then(() => this.reset())
|
||||
.finally(refresh)
|
||||
}
|
||||
|
||||
this.edit = id => {
|
||||
this.reset()
|
||||
const schedule = this.schedules[id]
|
||||
if (schedule) {
|
||||
this.formData.editedScheduleId = schedule.id
|
||||
this.formData.scheduleName = schedule.name
|
||||
this.formData.selectedJob = find(this.jobs, job => job.id = schedule.job)
|
||||
this.scheduleApi.setCron(schedule.cron)
|
||||
}
|
||||
}
|
||||
|
||||
this.delete = (id) => xo.schedule.delete(id).then(refresh).then(() => {
|
||||
if (id === this.formData.editedScheduleId) {
|
||||
this.reset()
|
||||
}
|
||||
})
|
||||
|
||||
this.run = schedule => {
|
||||
this.running[schedule.id] = true
|
||||
notify.info({
|
||||
title: 'Run Job',
|
||||
message: 'One shot running started. See overview for logs.'
|
||||
})
|
||||
const id = schedule.job
|
||||
return xo.job.runSequence([id]).finally(() => delete this.running[schedule.id])
|
||||
}
|
||||
|
||||
this.prettyCron = prettyCron.toString.bind(prettyCron)
|
||||
})
|
||||
|
||||
// A module exports its name.
|
||||
.name
|
||||
77
app/modules/task-scheduler/schedule/view.jade
Normal file
77
app/modules/task-scheduler/schedule/view.jade
Normal file
@@ -0,0 +1,77 @@
|
||||
.grid
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.fa.fa-clock-o
|
||||
| Job scheduler
|
||||
form#scheduleform(ng-submit = 'ctrl.save(ctrl.formData.editedScheduleId, ctrl.formData.scheduleName, ctrl.formData.selectedJob, ctrl.formData.cronPattern, ctrl.formData.enabled)')
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-cogs
|
||||
| Job to schedule
|
||||
.panel-body
|
||||
.alert.alert-warning(ng-if = 'ctrl.formData.editedScheduleId') Editing Schedule ID: {{ ctrl.formData.editedScheduleId }}
|
||||
fieldset.form-horizontal(ng-disabled = '!ctrl.ready')
|
||||
.form-group
|
||||
label.col-sm-2.control-label Schedule Name
|
||||
.col-sm-10
|
||||
input.form-control(form = 'scheduleform', type = 'text', ng-model = 'ctrl.formData.scheduleName', required, placeholder = 'An explicit name for your schedule')
|
||||
.form-group
|
||||
label.col-sm-2.control-label Job
|
||||
.col-sm-10
|
||||
select.form-control(form = 'scheduleform', ng-model = 'ctrl.formData.selectedJob', ng-options = '(job.name + " (" + job.id + ")") for job in (ctrl.jobs | map | orderBy:"name")', required)
|
||||
option(value = '') -- Choose a job --
|
||||
p.help-block(ng-if = 'ctrl.formData.selectedJob') {{ ctrl.selectedJob }}
|
||||
.form-group(ng-if = '!ctrl.formData.editedScheduleId')
|
||||
label.control-label.col-md-2(for = 'enabled')
|
||||
input#enabled(form = 'scheduleform', ng-model = 'ctrl.formData.enabled', type = 'checkbox')
|
||||
.help-block.col-md-10 Enable immediatly after creation
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-clock-o
|
||||
| Schedule
|
||||
.panel-body.form-horizontal
|
||||
.text-center(ng-if = '!ctrl.formData'): i.xo-icon-loading
|
||||
xo-scheduler(data = 'ctrl.formData', api = 'ctrl.scheduleApi')
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-body
|
||||
fieldset.center(ng-disabled = '!ctrl.ready')
|
||||
button.btn.btn-lg.btn-primary(form = 'scheduleform', type = 'submit')
|
||||
i.fa.fa-clock-o
|
||||
|
|
||||
i.fa.fa-arrow-right
|
||||
|
|
||||
i.fa.fa-database
|
||||
| Save
|
||||
|
|
||||
button.btn.btn-lg.btn-default(type = 'button', ng-click = 'ctrl.reset()')
|
||||
| Reset
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-list-ul
|
||||
| Schedules
|
||||
.panel-body
|
||||
.text-center(ng-if = '!(ctrl.schedules | count)') No schedules found
|
||||
table.table(ng-if = 'ctrl.schedules | count')
|
||||
tr
|
||||
th Name
|
||||
th Job
|
||||
th.hidden-xs Schedule
|
||||
th
|
||||
tr(ng-repeat = 'schedule in ctrl.schedules | map | orderBy:"name" track by schedule.id')
|
||||
td
|
||||
| {{ schedule.name }} 
|
||||
span.text-muted.hidden-xs ({{schedule.id}}) 
|
||||
br.visible-xs-block
|
||||
span.label.label-success(ng-if = 'schedule.enabled') enabled
|
||||
td {{ ctrl.jobs[schedule.job].name }} ({{ ctrl.jobs[schedule.job].method }})
|
||||
td.hidden-xs {{ ctrl.prettyCron(schedule.cron) }}
|
||||
td
|
||||
span.pull-right
|
||||
button.btn.btn-primary(type = 'button', ng-click = 'ctrl.edit(schedule.id)'): i.fa.fa-pencil
|
||||
|
|
||||
button.btn.btn-warning(type = 'button', ng-click = 'ctrl.run(schedule)', ng-disabled = 'ctrl.running[schedule.id]'): i.fa.fa-play
|
||||
|
|
||||
button.btn.btn-danger(type = 'button', ng-click = 'ctrl.delete(schedule.id)'): i.fa.fa-trash
|
||||
17
app/modules/task-scheduler/view.jade
Normal file
17
app/modules/task-scheduler/view.jade
Normal file
@@ -0,0 +1,17 @@
|
||||
.menu-grid
|
||||
.side-menu
|
||||
ul.nav
|
||||
li
|
||||
a(ui-sref = '.overview', ui-sref-active = 'active')
|
||||
i.fa.fa-fw.fa-eye.fa-menu
|
||||
span.menu-entry Overview
|
||||
li
|
||||
a(ui-sref = '.job')
|
||||
i.fa.fa-fw.fa-cogs.fa-menu
|
||||
span.menu-entry Jobs
|
||||
li
|
||||
a(ui-sref = '.schedule')
|
||||
i.fa.fa-fw.fa-clock-o.fa-menu
|
||||
span.menu-entry Scheduler
|
||||
|
||||
.side-content(ui-view = '')
|
||||
@@ -310,16 +310,16 @@ div(style="margin-top: 57px; visibility: hidden; height: 0") .
|
||||
i.xo-icon-working(ng-if="isVMWorking(VM)", tooltip="{{VM.power_state}} and {{(VM.current_operations | map)[0]}}")
|
||||
i(class="xo-icon-{{VM.power_state | lowercase}}",ng-if="!isVMWorking(VM)", tooltip="{{VM.power_state}}")
|
||||
//- VM name.
|
||||
td.vm-name.col-xs-8.col-sm-2.col-md-2
|
||||
td.vm-name.col-xs-8.col-sm-3.col-md-3
|
||||
p.vm {{VM.name_label}}
|
||||
//- Quick actions.
|
||||
td.vm-quick-buttons.col-md-2.hidden-xs
|
||||
td.vm-quick-buttons.col-md-1.col-sm-1.hidden-xs
|
||||
.quick-buttons
|
||||
a(tooltip="Shutdown VM", xo-click="confirmAction('stopVM', VM.id)")
|
||||
a(ng-if="VM.power_state == ('Running' || 'Paused')", tooltip="Shutdown VM", xo-click="confirmAction('stopVM', VM.id)")
|
||||
i.fa.fa-stop
|
||||
a(tooltip="Start VM", xo-click="startVM(VM.id)")
|
||||
a(ng-if="VM.power_state == ('Halted')", tooltip="Start VM", xo-click="startVM(VM.id)")
|
||||
i.fa.fa-play
|
||||
a(tooltip="Reboot VM", xo-click="confirmAction('rebootVM', VM.id)")
|
||||
a(ng-if="VM.power_state == ('Running' || 'Paused')", tooltip="Reboot VM", xo-click="confirmAction('rebootVM', VM.id)")
|
||||
i.fa.fa-refresh
|
||||
a(tooltip="VM Console", xo-sref="consoles_view({id: VM.id})")
|
||||
i.xo-icon-console
|
||||
@@ -364,20 +364,20 @@ div(style="margin-top: 57px; visibility: hidden; height: 0") .
|
||||
i.xo-icon-working(ng-if="isVMWorking(VM)", tooltip="{{VM.power_state}} and {{(VM.current_operations | map)[0]}}")
|
||||
i(class="xo-icon-{{VM.power_state | lowercase}}",ng-if="!isVMWorking(VM)", tooltip="{{VM.power_state}}")
|
||||
//- VM name.
|
||||
td.vm-name.col-xs-8.col-sm-2.col-md-2
|
||||
td.vm-name.col-xs-8.col-sm-3.col-md-3
|
||||
p.vm {{VM.name_label}}
|
||||
//- Quick actions.
|
||||
td.vm-quick-buttons.col-md-2.hidden-xs
|
||||
td.vm-quick-buttons.col-md-1.hidden-xs
|
||||
.quick-buttons
|
||||
a(tooltip="Shutdown VM", xo-click="stopVM(VM.id)")
|
||||
a(ng-if="VM.power_state == ('Running' || 'Paused')", tooltip="Shutdown VM", xo-click="stopVM(VM.id)")
|
||||
i.fa.fa-stop
|
||||
a(ng-if="VM.power_state == 'Suspended'", tooltip="Resume VM", xo-click="resumeVM(VM.id)")
|
||||
i.fa.fa-play
|
||||
a(ng-if="VM.power_state != 'Suspended'", tooltip="Start VM", xo-click="startVM(VM.id)")
|
||||
i.fa.fa-play
|
||||
a(tooltip="Reboot VM", xo-click="rebootVM(VM.id)")
|
||||
a(ng-if="VM.power_state == ('Running' || 'Paused')", tooltip="Reboot VM", xo-click="rebootVM(VM.id)")
|
||||
i.fa.fa-refresh
|
||||
a(tooltip="VM Console")
|
||||
a(tooltip="VM Console", xo-sref="consoles_view({id: VM.id})")
|
||||
i.xo-icon-console
|
||||
//- Description.
|
||||
td.vm-description.col-md-4.hidden-xs
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
angular = require 'angular'
|
||||
assign = require 'lodash.assign'
|
||||
filter = require 'lodash.filter'
|
||||
forEach = require 'lodash.foreach'
|
||||
isEmpty = require 'lodash.isempty'
|
||||
@@ -20,7 +21,7 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
.controller 'VmCtrl', (
|
||||
$scope, $state, $stateParams, $location, $q
|
||||
xoApi, xo
|
||||
sizeToBytesFilter, bytesToSizeFilter, xoHideUnauthorizedFilter
|
||||
bytesToSizeFilter, xoHideUnauthorizedFilter
|
||||
modal
|
||||
$window
|
||||
$timeout
|
||||
@@ -36,6 +37,7 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
do (
|
||||
networksByPool = xoApi.getIndex('networksByPool')
|
||||
srsByContainer = xoApi.getIndex('srsByContainer')
|
||||
hostsByPool = xoApi.getIndex('hostsByPool')
|
||||
poolSrs = null
|
||||
hostSrs = null
|
||||
) ->
|
||||
@@ -48,6 +50,10 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
srs = []
|
||||
poolSrs and forEach(poolSrs, (sr) => srs.push(sr))
|
||||
hostSrs and forEach(hostSrs, (sr) => srs.push(sr))
|
||||
if (($scope.VM?.power_state is 'Halted') || ($scope.VM?.power_state is 'Suspended')) && pool.id
|
||||
forEach hostsByPool[pool.id], (host) ->
|
||||
forEach srsByContainer[host.id], (sr) -> srs.push(sr)
|
||||
|
||||
srs = xoHideUnauthorizedFilter(srs)
|
||||
$scope.writable_SRs = filter(srs, (sr) => sr.content_type isnt 'iso')
|
||||
$scope.SRs = srs
|
||||
@@ -161,9 +167,11 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
continue unless oVbd
|
||||
oVdi = get oVbd.VDI
|
||||
continue unless oVdi
|
||||
VDIs.push oVdi if oVdi and not oVbd.is_cd_drive
|
||||
if not oVbd.is_cd_drive
|
||||
oVdi = assign({}, oVdi, {size: bytesToSizeFilter(oVdi.size), position: oVbd.position})
|
||||
VDIs.push oVdi
|
||||
|
||||
$scope.VDIs = sortBy(VDIs, (value) -> (get resolveVBD(value))?.position);
|
||||
$scope.VDIs = sortBy(VDIs, 'position');
|
||||
|
||||
descriptor = (obj) ->
|
||||
if !obj
|
||||
@@ -234,7 +242,7 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
$scope.savingBootOrder = true
|
||||
paramString = ''
|
||||
forEach(bootParams, (boot) -> boot.v && paramString += boot.e)
|
||||
return xoApi.call 'vm.bootOrder', {vm: id, order: paramString}
|
||||
return xo.vm.setBootOrder {vm: id, order: paramString}
|
||||
.finally () ->
|
||||
$scope.savingBootOrder = false
|
||||
$scope.bootReordering = false
|
||||
@@ -302,6 +310,13 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
message: 'Start VM'
|
||||
}
|
||||
|
||||
$scope.recoveryStartVM = (id) ->
|
||||
xo.vm.recoveryStart id
|
||||
notify.info {
|
||||
title: 'VM starting...'
|
||||
message: 'Start VM in recovery mode'
|
||||
}
|
||||
|
||||
$scope.stopVM = (id) ->
|
||||
modal.confirm
|
||||
title: 'VM shutdown'
|
||||
@@ -405,9 +420,9 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
$data = {
|
||||
id: VM.id
|
||||
}
|
||||
if memory isnt $scope.memorySize and (memory = sizeToBytesFilter memory)
|
||||
if memory isnt $scope.memorySize
|
||||
$data.memory = memory
|
||||
$scope.memorySize = bytesToSizeFilter memory
|
||||
$scope.memorySize = memory
|
||||
if CPUs isnt VM.CPUs.number
|
||||
$data.CPUs = +CPUs
|
||||
if name_label isnt VM.name_label
|
||||
@@ -450,6 +465,7 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
$scope.saveDisks = (data) ->
|
||||
# Group data by disk.
|
||||
disks = {}
|
||||
sizeChanges = false
|
||||
forEach data, (value, key) ->
|
||||
i = key.indexOf '/'
|
||||
(disks[key.slice 0, i] ?= {})[key.slice i + 1] = value
|
||||
@@ -457,62 +473,73 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
|
||||
promises = []
|
||||
|
||||
# Handle SR change.
|
||||
forEach disks, (attributes, id) ->
|
||||
disk = get id
|
||||
if attributes.$SR isnt disk.$SR
|
||||
promises.push (migrateDisk id, attributes.$SR)
|
||||
if attributes.size isnt bytesToSizeFilter(disk.size) # /!\ attributes are provided by a modified copy of disk
|
||||
sizeChanges = true
|
||||
return false
|
||||
|
||||
return
|
||||
preCheck = if sizeChanges then modal.confirm({title: 'Disk resizing', message: 'Growing the size of a disk is not reversible'}) else $q.resolve()
|
||||
|
||||
return preCheck
|
||||
.then ->
|
||||
# Handle SR change.
|
||||
forEach disks, (attributes, id) ->
|
||||
disk = get id
|
||||
if attributes.$SR isnt disk.$SR
|
||||
promises.push(migrateDisk(id, attributes.$SR))
|
||||
|
||||
if attributes.size isnt bytesToSizeFilter(disk.size) # /!\ attributes are provided by a modified copy of disk
|
||||
promises.push(xo.disk.resize(id, attributes.size))
|
||||
delete attributes.size
|
||||
|
||||
# Keep only changed attributes.
|
||||
forEach attributes, (value, name) ->
|
||||
delete attributes[name] if value is disk[name]
|
||||
return
|
||||
|
||||
unless isEmpty attributes
|
||||
# Inject id.
|
||||
attributes.id = id
|
||||
|
||||
# Ask the server to update the object.
|
||||
promises.push(xoApi.call('vdi.set', attributes))
|
||||
|
||||
forEach disks, (attributes, id) ->
|
||||
# Keep only changed attributes.
|
||||
disk = get id
|
||||
forEach attributes, (value, name) ->
|
||||
delete attributes[name] if value is disk[name]
|
||||
return
|
||||
|
||||
unless isEmpty attributes
|
||||
# Inject id.
|
||||
attributes.id = id
|
||||
# Handle Position changes
|
||||
vbds = xoApi.get($scope.VM.$VBDs)
|
||||
notFreePositions = Object.create(null)
|
||||
forEach vbds, (vbd) ->
|
||||
if vbd.is_cd_drive
|
||||
notFreePositions[vbd.position] = null
|
||||
|
||||
# Ask the server to update the object.
|
||||
promises.push xoApi.call 'vdi.set', attributes
|
||||
return
|
||||
position = 0
|
||||
forEach $scope.VDIs, (vdi) ->
|
||||
oVbd = get(resolveVBD(vdi))
|
||||
unless oVbd
|
||||
return
|
||||
|
||||
# Handle Position changes
|
||||
vbds = xoApi.get($scope.VM.$VBDs)
|
||||
notFreePositions = Object.create(null)
|
||||
forEach vbds, (vbd) ->
|
||||
if vbd.is_cd_drive
|
||||
notFreePositions[vbd.position] = null
|
||||
while position of notFreePositions
|
||||
++position
|
||||
|
||||
position = 0
|
||||
forEach $scope.VDIs, (vdi) ->
|
||||
oVbd = get(resolveVBD(vdi))
|
||||
unless oVbd
|
||||
return
|
||||
if +oVbd.position isnt position
|
||||
promises.push(
|
||||
xoApi.call('vbd.set', {
|
||||
id: oVbd.id,
|
||||
position: String(position)
|
||||
})
|
||||
)
|
||||
|
||||
while position of notFreePositions
|
||||
++position
|
||||
|
||||
if +oVbd.position isnt position
|
||||
promises.push(
|
||||
xoApi.call('vbd.set', {
|
||||
id: oVbd.id,
|
||||
position: String(position)
|
||||
})
|
||||
)
|
||||
|
||||
++position
|
||||
|
||||
return $q.all promises
|
||||
.catch (err) ->
|
||||
console.log(err);
|
||||
notify.error {
|
||||
title: 'saveDisks'
|
||||
message: err
|
||||
}
|
||||
return $q.all promises
|
||||
.catch (err) ->
|
||||
console.log(err);
|
||||
notify.error {
|
||||
title: 'saveDisks'
|
||||
message: err
|
||||
}
|
||||
|
||||
$scope.deleteDisk = (id) ->
|
||||
modal.confirm({
|
||||
|
||||
@@ -81,10 +81,10 @@
|
||||
| {{memorySize}}
|
||||
dt UUID
|
||||
dd {{VM.UUID}}
|
||||
dt(ng-if="VM.PV_args") PV Args
|
||||
dd(ng-if="VM.PV_args")
|
||||
dt(ng-if= "VM.virtualizationMode !== 'hvm'") PV Args
|
||||
dd(ng-if= "VM.virtualizationMode !== 'hvm'")
|
||||
span(editable-text="VM.PV_args", e-name="PV_args", e-form="vmSettings")
|
||||
| {{VM.PV_args}}
|
||||
| {{VM.PV_args}}
|
||||
dt(ng-if="refreshStatControl.running && stats") Xen tools
|
||||
dd(ng-if="refreshStatControl.running && stats")
|
||||
span.text-success(ng-if="VM.xenTools === 'up to date'") Installed
|
||||
@@ -241,7 +241,7 @@
|
||||
br
|
||||
p.center(ng-if="refreshStatControl.running")
|
||||
i.xo-icon-loading
|
||||
| Fetching stats...
|
||||
| Fetching stats...
|
||||
.grid
|
||||
.grid-cell(ng-if="VM.os_version.distro")
|
||||
p.stat-name OS:
|
||||
@@ -255,7 +255,7 @@
|
||||
span(ng-if="VM.PV_drivers && !VM.PV_drivers_up_to_date") Outdated
|
||||
|
||||
//- Action panel
|
||||
.grid-sm
|
||||
.grid-sm(ng-if = 'canOperate()')
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-flash
|
||||
@@ -272,6 +272,9 @@
|
||||
.grid-cell.btn-group(ng-if="VM.power_state == ('Halted')")
|
||||
button.btn(tooltip="Start VM", tooltip-placement="top", type="button", style="width: 90%", xo-click="startVM(VM.id)", ng-if = 'canOperate()')
|
||||
i.fa.fa-play.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(ng-if="VM.power_state == ('Halted')")
|
||||
button.btn(tooltip="Start VM in recovery mode", tooltip-placement="top", type="button", style="width: 90%", xo-click="recoveryStartVM(VM.id)", ng-if = 'canOperate()')
|
||||
i.fa.fa-forward.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(ng-if="VM.power_state == ('Running' || 'Paused')")
|
||||
button.btn(tooltip="Reboot VM", tooltip-placement="top", type="button", style="width: 90%", xo-click="rebootVM(VM.id)", ng-if = 'canOperate()')
|
||||
i.fa.fa-refresh.fa-2x.fa-fw
|
||||
@@ -344,24 +347,8 @@
|
||||
button.btn(tooltip="Create a snapshot", tooltip-placement="top", style="width: 90%", type="button", xo-click="snapshotVM(VM.id,VM.name_label)", ng-if = 'canAdmin()')
|
||||
i.xo-icon-snapshot.fa-2x.fa-fw
|
||||
.grid-cell.btn-group.dropdown(dropdown, ng-if="canAdmin()")
|
||||
button.btn.dropdown-toggle(
|
||||
dropdown-toggle
|
||||
tooltip="Export the VM"
|
||||
tooltip-placement="top"
|
||||
style="width: 90%"
|
||||
type="button"
|
||||
)
|
||||
button.btn(tooltip="Export the VM", tooltip-placement="top", style="width: 90%", type="button", xo-click="exportVM(VM.id)", ng-if="canAdmin()")
|
||||
i.fa.fa-download.fa-2x.fa-fw
|
||||
span.caret
|
||||
ul.dropdown-menu.left(role="menu")
|
||||
li(ng-if = 'canAdmin()')
|
||||
a(xo-click="exportVM(VM.id)")
|
||||
i.fa.fa-download.fa-fw
|
||||
| Full export
|
||||
li(ng-if = 'canAdmin()')
|
||||
a(xo-click="exportOnlyMetadataVM(VM.id)")
|
||||
i.fa.fa-database.fa-fw
|
||||
| Only Metadata
|
||||
.grid-cell.btn-group.dropdown(dropdown, ng-if="canAdmin()")
|
||||
button.btn.dropdown-toggle(
|
||||
dropdown-toggle
|
||||
@@ -476,19 +463,21 @@
|
||||
td
|
||||
xo-tag(object = 'VDI')
|
||||
td
|
||||
//- FIXME: should be editable, but the server needs first
|
||||
//- to accept a human readable string.
|
||||
| {{VDI.size | bytesToSize}}
|
||||
span(
|
||||
editable-text="VDI.size"
|
||||
e-name = '{{VDI.id}}/size'
|
||||
)
|
||||
| {{VDI.size}}
|
||||
td.oneliner
|
||||
span(
|
||||
ng-if = 'canView((VDI.$SR | resolve).id)'
|
||||
editable-select="(VDI.$SR | resolve).id"
|
||||
e-ng-options="SR.id as (SR.name_label + ' (' + (SR.size - SR.usage | bytesToSize) + ' free)') for SR in writable_SRs"
|
||||
e-ng-options="SR.id as (SR.name_label + ' (' + (SR.size - SR.usage | bytesToSize) + ' free) ' + (SR.$container | resolve).name_label) for SR in writable_SRs"
|
||||
e-name = '{{VDI.id}}/$SR'
|
||||
)
|
||||
//- Are SR editable? will trigger moving VDI to the new SR
|
||||
a(xo-sref="SRs_view({id: (VDI.$SR | resolve).id})")
|
||||
| {{(VDI.$SR | resolve).name_label}}
|
||||
| {{(VDI.$SR | resolve).name_label}} ({{((VDI.$SR | resolve).$container | resolve).name_label}})
|
||||
td(ng-if="isConnected(VDI)")
|
||||
span.label.label-success Connected
|
||||
span.pull-right.btn-group.quick-buttons(ng-if="canAdmin()")
|
||||
@@ -565,7 +554,7 @@
|
||||
i.fa.fa-minus(ng-if = 'creatingVdi')
|
||||
| New Disk
|
||||
|
|
||||
button.btn(type="button", ng-class = '{"btn-success": bootReordering, "btn-primary": !bootReordering}', ng-disabled="disksForm.$waiting", ng-click="bootReordering = !bootReordering;adding = false;creatingVdi = false", ng-hide = '!canAdmin()')
|
||||
button.btn(type="button", ng-class = '{"btn-success": bootReordering, "btn-primary": !bootReordering}', ng-disabled="disksForm.$waiting", ng-click="bootReordering = !bootReordering;adding = false;creatingVdi = false", ng-hide = '!canAdmin()', ng-show = "VM.virtualizationMode === 'hvm'")
|
||||
i.fa.fa-plus(ng-if = '!bootReordering')
|
||||
i.fa.fa-minus(ng-if = 'bootReordering')
|
||||
| Boot order
|
||||
@@ -606,11 +595,11 @@
|
||||
|
|
||||
.form-group
|
||||
//- label(for = 'newDiskSize') Size
|
||||
input#newDiskSize.form-control(type = 'text', ng-model = 'newDiskSize', required, placeholder = 'Size e.g 128MB, 8GB, 2TB...')
|
||||
input#newDiskSize.form-control(type = 'text', ng-model = 'newDiskSize', required, placeholder = 'Size e.g 128 MiB, 8 GiB, 2 TiB...')
|
||||
|
|
||||
.form-group
|
||||
//- label(for = 'newDiskSR') SR
|
||||
select.form-control(ng-model = 'newDiskSR', required, ng-options="SR.id as (SR.name_label + ' (' + (SR.size - SR.usage | bytesToSize) + ' free)') for SR in writable_SRs")
|
||||
select.form-control(ng-model = 'newDiskSR', required, ng-options="SR.id as (SR.name_label + ' (' + (SR.size - SR.usage | bytesToSize) + ' free) ' + (SR.$container | resolve).name_label) for SR in writable_SRs")
|
||||
option(value = '', disabled) Choose your SR
|
||||
|
|
||||
br
|
||||
@@ -738,13 +727,15 @@
|
||||
.panel-body
|
||||
p.center(ng-if="!VM.snapshots.length") No snapshots
|
||||
table.table.table-hover(ng-if="VM.snapshots.length")
|
||||
th Date
|
||||
th Name
|
||||
th.col-md-4 Date
|
||||
th.col-md-8 Name
|
||||
tr(ng-repeat="snapshot in VM.snapshots | resolve | orderBy:'-snapshot_time' | slice:(5*(currentSnapPage-1)):(5*currentSnapPage) track by snapshot.id")
|
||||
td.oneliner {{snapshot.snapshot_time*1e3 | date:"medium"}}
|
||||
td.oneliner
|
||||
span(editable-text="snapshot.name_label", e-name="name_label", e-form="vmSnap", onbeforesave="saveSnapshot(snapshot.id, $data)")
|
||||
| {{snapshot.name_label}}
|
||||
span(ng-if="snapshot.tags | includes:'quiesce'")
|
||||
i.fa.fa-info-circle(tooltip = "Quiesced snapshot")
|
||||
| {{snapshot.name_label}}
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(tooltip="Export this snapshot", type="button", xo-click="exportVM(snapshot.id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-upload.fa-lg
|
||||
|
||||
33
app/node_modules/angular-no-vnc/index.js
generated
vendored
33
app/node_modules/angular-no-vnc/index.js
generated
vendored
@@ -77,7 +77,10 @@ export default angular.module('no-vnc', [])
|
||||
rfb = new RFB({
|
||||
encrypt: isSecure,
|
||||
target: canvas,
|
||||
wsProtocols: ['chat']
|
||||
wsProtocols: ['chat'],
|
||||
onClipboard (rfb, text) {
|
||||
setClipboard(text)
|
||||
}
|
||||
})
|
||||
|
||||
rfb._onUpdateState = (rfb, state) => {
|
||||
@@ -101,6 +104,11 @@ export default angular.module('no-vnc', [])
|
||||
}
|
||||
|
||||
this.remoteControl = {
|
||||
pasteToClipboard (text) {
|
||||
if (rfb) {
|
||||
rfb.clipboardPasteFrom(text)
|
||||
}
|
||||
},
|
||||
sendCtrlAltDel () {
|
||||
if (rfb) {
|
||||
rfb.sendCtrlAltDel()
|
||||
@@ -108,8 +116,28 @@ export default angular.module('no-vnc', [])
|
||||
}
|
||||
}
|
||||
|
||||
const setClipboard = (text) => {
|
||||
this.onClipboardChange({ clipboardContent: text })
|
||||
}
|
||||
|
||||
let canvas = $element.find('canvas')[0]
|
||||
|
||||
$scope.unfocus = () => {
|
||||
if (rfb) {
|
||||
rfb.get_keyboard().ungrab()
|
||||
rfb.get_mouse().ungrab()
|
||||
}
|
||||
}
|
||||
$scope.focus = () => {
|
||||
if (rfb) {
|
||||
if (document.activeElement) {
|
||||
document.activeElement.blur()
|
||||
}
|
||||
rfb.get_keyboard().grab()
|
||||
rfb.get_mouse().grab()
|
||||
}
|
||||
}
|
||||
|
||||
$attrs.$observe('url', (url) => {
|
||||
reset(url)
|
||||
})
|
||||
@@ -122,7 +150,8 @@ export default angular.module('no-vnc', [])
|
||||
controller: 'NoVncCtrl as noVnc',
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
remoteControl: '='
|
||||
remoteControl: '=',
|
||||
onClipboardChange: '&'
|
||||
},
|
||||
template: view
|
||||
}
|
||||
|
||||
2
app/node_modules/angular-no-vnc/view.jade
generated
vendored
2
app/node_modules/angular-no-vnc/view.jade
generated
vendored
@@ -1,5 +1,7 @@
|
||||
canvas.center-block(
|
||||
height = "{{noVnc.height}}"
|
||||
width = "{{noVnc.width}}"
|
||||
ng-mouseenter = 'focus()'
|
||||
ng-mouseleave = 'unfocus()'
|
||||
)
|
||||
| Sorry, your browser does not support the canvas element.
|
||||
|
||||
2
app/node_modules/iso-device/view.jade
generated
vendored
2
app/node_modules/iso-device/view.jade
generated
vendored
@@ -4,7 +4,7 @@
|
||||
select.form-control(
|
||||
ng-model = 'isoDevice.isos.mounted'
|
||||
ng-change = 'isoDevice.insert(isoDevice.vm, isoDevice.isos.mounted)'
|
||||
ng-options = 'iso.iso.id as iso.label group by iso.sr for iso in isoDevice.isos.opts'
|
||||
ng-options = 'iso.iso.id as iso.label group by iso.sr for iso in isoDevice.isos.opts | orderBy:"label"'
|
||||
unfocus-on-change
|
||||
)
|
||||
option(value = '', disabled) -- CD Drive (empty) --
|
||||
|
||||
314
app/node_modules/scheduler/index.js
generated
vendored
Normal file
314
app/node_modules/scheduler/index.js
generated
vendored
Normal file
@@ -0,0 +1,314 @@
|
||||
import angular from 'angular'
|
||||
import assign from 'lodash.assign'
|
||||
import forEach from 'lodash.foreach'
|
||||
import indexOf from 'lodash.indexof'
|
||||
import later from 'later'
|
||||
import moment from 'moment'
|
||||
import prettyCron from 'prettycron'
|
||||
import remove from 'lodash.remove'
|
||||
|
||||
later.date.localTime()
|
||||
|
||||
import view from './view'
|
||||
|
||||
export default angular.module('xoWebApp.scheduler', [])
|
||||
|
||||
.directive('xoScheduler', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: view,
|
||||
controller: 'XoScheduler as ctrl',
|
||||
bindToController: true,
|
||||
scope: {
|
||||
data: '=',
|
||||
api: '='
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
.controller('XoScheduler', function () {
|
||||
this.init = () => {
|
||||
let i, j
|
||||
|
||||
const minutes = []
|
||||
for (i = 0; i < 6; i++) {
|
||||
minutes[i] = []
|
||||
for (j = 0; j < 10; j++) {
|
||||
minutes[i].push(10 * i + j)
|
||||
}
|
||||
}
|
||||
this.minutes = minutes
|
||||
|
||||
const hours = []
|
||||
for (i = 0; i < 3; i++) {
|
||||
hours[i] = []
|
||||
for (j = 0; j < 8; j++) {
|
||||
hours[i].push(8 * i + j)
|
||||
}
|
||||
}
|
||||
this.hours = hours
|
||||
|
||||
const days = []
|
||||
for (i = 0; i < 4; i++) {
|
||||
days[i] = []
|
||||
for (j = 1; j < 8; j++) {
|
||||
days[i].push(7 * i + j)
|
||||
}
|
||||
}
|
||||
days.push([29, 30, 31])
|
||||
this.days = days
|
||||
|
||||
this.months = [
|
||||
[
|
||||
{v: 1, l: 'Jan'},
|
||||
{v: 2, l: 'Feb'},
|
||||
{v: 3, l: 'Mar'},
|
||||
{v: 4, l: 'Apr'},
|
||||
{v: 5, l: 'May'},
|
||||
{v: 6, l: 'Jun'}
|
||||
],
|
||||
[
|
||||
{v: 7, l: 'Jul'},
|
||||
{v: 8, l: 'Aug'},
|
||||
{v: 9, l: 'Sep'},
|
||||
{v: 10, l: 'Oct'},
|
||||
{v: 11, l: 'Nov'},
|
||||
{v: 12, l: 'Dec'}
|
||||
]
|
||||
]
|
||||
|
||||
this.dayWeeks = [
|
||||
{v: 0, l: 'Sun'},
|
||||
{v: 1, l: 'Mon'},
|
||||
{v: 2, l: 'Tue'},
|
||||
{v: 3, l: 'Wed'},
|
||||
{v: 4, l: 'Thu'},
|
||||
{v: 5, l: 'Fri'},
|
||||
{v: 6, l: 'Sat'}
|
||||
]
|
||||
this.resetData()
|
||||
}
|
||||
|
||||
this.selectMinute = function (minute) {
|
||||
if (this.isSelectedMinute(minute)) {
|
||||
remove(this.data.minSelect, v => String(v) === String(minute))
|
||||
} else {
|
||||
this.data.minSelect.push(minute)
|
||||
}
|
||||
}
|
||||
|
||||
this.isSelectedMinute = function (minute) {
|
||||
return indexOf(this.data.minSelect, minute) > -1 || indexOf(this.data.minSelect, String(minute)) > -1
|
||||
}
|
||||
|
||||
this.selectHour = function (hour) {
|
||||
if (this.isSelectedHour(hour)) {
|
||||
remove(this.data.hourSelect, v => String(v) === String(hour))
|
||||
} else {
|
||||
this.data.hourSelect.push(hour)
|
||||
}
|
||||
}
|
||||
|
||||
this.isSelectedHour = function (hour) {
|
||||
return indexOf(this.data.hourSelect, hour) > -1 || indexOf(this.data.hourSelect, String(hour)) > -1
|
||||
}
|
||||
|
||||
this.selectDay = function (day) {
|
||||
if (this.isSelectedDay(day)) {
|
||||
remove(this.data.daySelect, v => String(v) === String(day))
|
||||
} else {
|
||||
this.data.daySelect.push(day)
|
||||
}
|
||||
}
|
||||
|
||||
this.isSelectedDay = function (day) {
|
||||
return indexOf(this.data.daySelect, day) > -1 || indexOf(this.data.daySelect, String(day)) > -1
|
||||
}
|
||||
|
||||
this.selectMonth = function (month) {
|
||||
if (this.isSelectedMonth(month)) {
|
||||
remove(this.data.monthSelect, v => String(v) === String(month))
|
||||
} else {
|
||||
this.data.monthSelect.push(month)
|
||||
}
|
||||
}
|
||||
|
||||
this.isSelectedMonth = function (month) {
|
||||
return indexOf(this.data.monthSelect, month) > -1 || indexOf(this.data.monthSelect, String(month)) > -1
|
||||
}
|
||||
|
||||
this.selectDayWeek = function (dayWeek) {
|
||||
if (this.isSelectedDayWeek(dayWeek)) {
|
||||
remove(this.data.dayWeekSelect, v => String(v) === String(dayWeek))
|
||||
} else {
|
||||
this.data.dayWeekSelect.push(dayWeek)
|
||||
}
|
||||
}
|
||||
|
||||
this.isSelectedDayWeek = function (dayWeek) {
|
||||
return indexOf(this.data.dayWeekSelect, dayWeek) > -1 || indexOf(this.data.dayWeekSelect, String(dayWeek)) > -1
|
||||
}
|
||||
|
||||
this.noMinutePlan = function (set = false) {
|
||||
if (!set) {
|
||||
// The last part (after &&) of this expression is reliable because we maintain the minSelect array with lodash.remove
|
||||
return this.data.min === 'select' && this.data.minSelect.length === 1 && String(this.data.minSelect[0]) === '0'
|
||||
} else {
|
||||
this.data.minSelect = [0]
|
||||
this.data.min = 'select'
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
this.noHourPlan = function (set = false) {
|
||||
if (!set) {
|
||||
// The last part (after &&) of this expression is reliable because we maintain the hourSelect array with lodash.remove
|
||||
return this.data.hour === 'select' && this.data.hourSelect.length === 1 && String(this.data.hourSelect[0]) === '0'
|
||||
} else {
|
||||
this.data.hourSelect = [0]
|
||||
this.data.hour = 'select'
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
this.resetData = () => {
|
||||
this.data.minRange = 5
|
||||
this.data.hourRange = 2
|
||||
this.data.minSelect = [0]
|
||||
this.data.hourSelect = []
|
||||
this.data.daySelect = []
|
||||
this.data.monthSelect = []
|
||||
this.data.dayWeekSelect = []
|
||||
this.data.min = 'select'
|
||||
this.data.hour = 'all'
|
||||
this.data.day = 'all'
|
||||
this.data.month = 'all'
|
||||
this.data.dayWeek = 'all'
|
||||
this.data.cronPattern = '* * * * *'
|
||||
this.data.summary = []
|
||||
this.data.previewLimit = 0
|
||||
|
||||
this.update()
|
||||
}
|
||||
|
||||
this.update = () => {
|
||||
const d = this.data
|
||||
const i = (d.min === 'all' && '*') ||
|
||||
(d.min === 'range' && ('*/' + d.minRange)) ||
|
||||
(d.min === 'select' && d.minSelect.join(',')) ||
|
||||
'*'
|
||||
const h = (d.hour === 'all' && '*') ||
|
||||
(d.hour === 'range' && ('*/' + d.hourRange)) ||
|
||||
(d.hour === 'select' && d.hourSelect.join(',')) ||
|
||||
'*'
|
||||
const dm = (d.day === 'all' && '*') ||
|
||||
(d.day === 'select' && d.daySelect.join(',')) ||
|
||||
'*'
|
||||
const m = (d.month === 'all' && '*') ||
|
||||
(d.month === 'select' && d.monthSelect.join(',')) ||
|
||||
'*'
|
||||
const dw = (d.dayWeek === 'all' && '*') ||
|
||||
(d.dayWeek === 'select' && d.dayWeekSelect.join(',')) ||
|
||||
'*'
|
||||
this.data.cronPattern = i + ' ' + h + ' ' + dm + ' ' + m + ' ' + dw
|
||||
|
||||
const tabState = {
|
||||
min: {
|
||||
all: d.min === 'all',
|
||||
range: d.min === 'range',
|
||||
select: d.min === 'select'
|
||||
},
|
||||
hour: {
|
||||
all: d.hour === 'all',
|
||||
range: d.hour === 'range',
|
||||
select: d.hour === 'select'
|
||||
},
|
||||
day: {
|
||||
all: d.day === 'all',
|
||||
range: d.day === 'range',
|
||||
select: d.day === 'select'
|
||||
},
|
||||
month: {
|
||||
all: d.month === 'all',
|
||||
select: d.month === 'select'
|
||||
},
|
||||
dayWeek: {
|
||||
all: d.dayWeek === 'all',
|
||||
select: d.dayWeek === 'select'
|
||||
}
|
||||
}
|
||||
this.tabs = tabState
|
||||
this.summarize()
|
||||
}
|
||||
|
||||
this.summarize = () => {
|
||||
const schedule = later.parse.cron(this.data.cronPattern)
|
||||
const occurences = later.schedule(schedule).next(25)
|
||||
this.data.summary = []
|
||||
forEach(occurences, occurence => {
|
||||
this.data.summary.push(moment(occurence).format('LLLL'))
|
||||
})
|
||||
}
|
||||
|
||||
const cronToData = (data, cron) => {
|
||||
const d = Object.create(null)
|
||||
const cronItems = cron.split(' ')
|
||||
|
||||
if (cronItems[0] === '*') {
|
||||
d.min = 'all'
|
||||
} else if (cronItems[0].indexOf('/') !== -1) {
|
||||
d.min = 'range'
|
||||
const [, range] = cronItems[0].split('/')
|
||||
d.minRange = range
|
||||
} else {
|
||||
d.min = 'select'
|
||||
d.minSelect = cronItems[0].split(',')
|
||||
}
|
||||
|
||||
if (cronItems[1] === '*') {
|
||||
d.hour = 'all'
|
||||
} else if (cronItems[1].indexOf('/') !== -1) {
|
||||
d.hour = 'range'
|
||||
const [, range] = cronItems[1].split('/')
|
||||
d.hourRange = range
|
||||
} else {
|
||||
d.hour = 'select'
|
||||
d.hourSelect = cronItems[1].split(',')
|
||||
}
|
||||
|
||||
if (cronItems[2] === '*') {
|
||||
d.day = 'all'
|
||||
} else {
|
||||
d.day = 'select'
|
||||
d.daySelect = cronItems[2].split(',')
|
||||
}
|
||||
|
||||
if (cronItems[3] === '*') {
|
||||
d.month = 'all'
|
||||
} else {
|
||||
d.month = 'select'
|
||||
d.monthSelect = cronItems[3].split(',')
|
||||
}
|
||||
|
||||
if (cronItems[4] === '*') {
|
||||
d.dayWeek = 'all'
|
||||
} else {
|
||||
d.dayWeek = 'select'
|
||||
d.dayWeekSelect = cronItems[4].split(',')
|
||||
}
|
||||
|
||||
assign(data, d)
|
||||
}
|
||||
|
||||
this.prettyCron = prettyCron.toString.bind(prettyCron)
|
||||
|
||||
this.api.setCron = cron => {
|
||||
cronToData(this.data, cron)
|
||||
this.update()
|
||||
}
|
||||
this.api.resetData = this.resetData.bind(this)
|
||||
|
||||
this.init()
|
||||
})
|
||||
|
||||
.name
|
||||
9
app/node_modules/scheduler/package.json
generated
vendored
Normal file
9
app/node_modules/scheduler/package.json
generated
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"private": true,
|
||||
"browserify": {
|
||||
"transform": [
|
||||
"babelify",
|
||||
"browserify-plain-jade"
|
||||
]
|
||||
}
|
||||
}
|
||||
104
app/node_modules/scheduler/view.jade
generated
vendored
Normal file
104
app/node_modules/scheduler/view.jade
generated
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
accordion(ng-if = 'ctrl.data', close-others= 'false', ng-click = 'ctrl.update()')
|
||||
accordion-group
|
||||
accordion-heading Month
|
||||
tabset
|
||||
tab(select = 'ctrl.data.month = "all"', active = 'ctrl.tabs.month.all')
|
||||
tab-heading every month
|
||||
tab(select = 'ctrl.data.month = "select"', active = 'ctrl.tabs.month.select')
|
||||
tab-heading each selected month
|
||||
br
|
||||
table.table.table-bordered
|
||||
tr(ng-repeat = 'line in ctrl.months')
|
||||
td(ng-click = 'ctrl.selectMonth(month.v)', ng-class = '{"bg-success": ctrl.isSelectedMonth(month.v)}',ng-repeat = 'month in line') {{ month.l }}
|
||||
accordion-group
|
||||
accordion-heading Day of the month
|
||||
tabset
|
||||
tab(select = 'ctrl.data.day = "all"', active = 'ctrl.tabs.day.all')
|
||||
tab-heading every day
|
||||
tab(select = 'ctrl.data.day = "select"', active = 'ctrl.tabs.day.select')
|
||||
tab-heading each selected day
|
||||
br
|
||||
p.text-warning
|
||||
i.fa.fa-warning
|
||||
| This selection can restrict or be restricted by "Day of week" selections below. Use the summary preview to ensure your choice.
|
||||
br
|
||||
table.table.table-bordered
|
||||
tr(ng-repeat = 'line in ctrl.days')
|
||||
td(ng-click = 'ctrl.selectDay(day)', ng-class = '{"bg-success": ctrl.isSelectedDay(day)}',ng-repeat = 'day in line') {{ day }}
|
||||
accordion-group
|
||||
accordion-heading Day of week
|
||||
tabset
|
||||
tab(select = 'ctrl.data.dayWeek = "all"', active = 'ctrl.tabs.dayWeek.all')
|
||||
tab-heading every day of week
|
||||
tab(select = 'ctrl.data.dayWeek = "select"', active = 'ctrl.tabs.dayWeek.select')
|
||||
tab-heading each selected day of week
|
||||
br
|
||||
p.text-warning
|
||||
i.fa.fa-warning
|
||||
| This selection can restrict or be restricted by "Day of the month" selections up ahead. Use the summary preview to ensure your choice.
|
||||
br
|
||||
table.table.table-bordered
|
||||
tr
|
||||
td(ng-click = 'ctrl.selectDayWeek(dayWeek.v)', ng-class = '{"bg-success": ctrl.isSelectedDayWeek(dayWeek.v)}',ng-repeat = 'dayWeek in ctrl.dayWeeks') {{ dayWeek.l }}
|
||||
accordion-group
|
||||
accordion-heading Hour
|
||||
button.btn.btn-primary(ng-if = '!ctrl.noHourPlan()', type = 'button', ng-click = 'ctrl.noHourPlan(true)') Plan nothing on a hourly grain
|
||||
button.btn.btn-primary.disabled(ng-if = 'ctrl.noHourPlan()', type = 'button')
|
||||
i.fa.fa-info-circle
|
||||
| Nothing planned on a hourly grain
|
||||
br
|
||||
br
|
||||
tabset
|
||||
tab(select = 'ctrl.data.hour = "all"', active = 'ctrl.tabs.hour.all')
|
||||
tab-heading every hour
|
||||
tab(select = 'ctrl.data.hour = "range"', active = 'ctrl.tabs.hour.range')
|
||||
tab-heading every N hour
|
||||
br
|
||||
.form-group
|
||||
label.col-sm-2.control-label {{ ctrl.data.hourRange }}
|
||||
.col-sm-10
|
||||
input.form-control(type = 'range', min = '2', max = '23', step = '1', ng-model = 'ctrl.data.hourRange', ng-change = 'ctrl.update()')
|
||||
tab(select = 'ctrl.data.hour = "select"', active = 'ctrl.tabs.hour.select')
|
||||
tab-heading each selected hour
|
||||
br
|
||||
table.table.table-bordered
|
||||
tr(ng-repeat = 'line in ctrl.hours')
|
||||
td(ng-click = 'ctrl.selectHour(hour)', ng-class = '{"bg-success": ctrl.isSelectedHour(hour)}',ng-repeat = 'hour in line') {{ hour }}
|
||||
accordion-group
|
||||
accordion-heading Minute
|
||||
button.btn.btn-primary(ng-if = '!ctrl.noMinutePlan()', type = 'button', ng-click = 'ctrl.noMinutePlan(true)') Plan nothing on a minute grain
|
||||
button.btn.btn-primary.disabled(ng-if = 'ctrl.noMinutePlan()', type = 'button')
|
||||
i.fa.fa-info-circle
|
||||
| Nothing planned on a minute grain
|
||||
br
|
||||
br
|
||||
tabset
|
||||
tab(select = 'ctrl.data.min = "all"', active = 'ctrl.tabs.min.all')
|
||||
tab-heading every minute
|
||||
tab(select = 'ctrl.data.min = "range"', active = 'ctrl.tabs.min.range')
|
||||
tab-heading every N minutes
|
||||
br
|
||||
.form-group
|
||||
label.col-sm-2.control-label {{ ctrl.data.minRange }}
|
||||
.col-sm-10
|
||||
input.form-control(type = 'range', min = '2', max = '59', step = '1', ng-model = 'ctrl.data.minRange', ng-change = 'ctrl.update()')
|
||||
tab(select = 'ctrl.data.min = "select"', active = 'ctrl.tabs.min.select')
|
||||
tab-heading each selected minute
|
||||
br
|
||||
table.table.table-bordered
|
||||
tr(ng-repeat = 'line in ctrl.minutes')
|
||||
td(ng-click = 'ctrl.selectMinute(min)', ng-class = '{"bg-success": ctrl.isSelectedMinute(min)}',ng-repeat = 'min in line') {{ min }}
|
||||
input.form-control.hidden(type ='text', readonly, ng-model = 'ctrl.data.cronPattern')
|
||||
.text-center(ng-if = '!ctrl.data'): i.xo-icon-loading
|
||||
div(ng-if = 'ctrl.data')
|
||||
p
|
||||
strong Scheduled to run:
|
||||
| {{ ctrl.prettyCron(ctrl.data.cronPattern) }}
|
||||
.form-inline.container-fluid
|
||||
.form-group
|
||||
label Preview:
|
||||
input.form-control(type = 'range', min = '0', max = '{{ ctrl.data.summary.length - 3 }}', step = '1', ng-model = 'ctrl.data.previewLimit')
|
||||
br
|
||||
ul
|
||||
li(ng-repeat = 'occurence in ctrl.data.summary | limitTo: +ctrl.data.previewLimit+3') {{ occurence }}
|
||||
li ...
|
||||
4
app/node_modules/tag/index.js
generated
vendored
4
app/node_modules/tag/index.js
generated
vendored
@@ -33,6 +33,10 @@ export default angular.module('xoWebApp.tag', [])
|
||||
this.object = get(this.object)
|
||||
}
|
||||
|
||||
$scope.canAdmin = (id) => {
|
||||
return id && xoApi.canInteract(id, 'administrate') || false
|
||||
}
|
||||
|
||||
this.add = (tag) => {
|
||||
tag = trim(tag)
|
||||
if (tag === '') {
|
||||
|
||||
2
app/node_modules/tag/view.jade
generated
vendored
2
app/node_modules/tag/view.jade
generated
vendored
@@ -6,7 +6,7 @@
|
||||
i.fa.fa-times(ng-click = 'ctrl.remove(tag)')
|
||||
|  {{tag}}
|
||||
|  
|
||||
i.fa.add-button(ng-class = '{"fa-plus": !edit, "fa-minus": edit, edit: edit}', ng-click = 'edit = !edit; newTag = ""')
|
||||
i.fa.add-button(ng-class = '{"fa-plus": !edit, "fa-minus": edit, edit: edit}', ng-click = 'edit = !edit; newTag = ""', ng-hide = '!canAdmin(ctrl.object.id)')
|
||||
|  
|
||||
form.form-inline(ng-attr-id = 'xo-tag-{{ctrl.object.id}}',ng-submit = 'ctrl.add(newTag); newTag = ""; edit = false')
|
||||
.input-group(ng-hide = '!edit')
|
||||
|
||||
51
app/node_modules/xo-filters/index.js
generated
vendored
51
app/node_modules/xo-filters/index.js
generated
vendored
@@ -1,6 +1,8 @@
|
||||
import angular from 'angular'
|
||||
import forEach from 'lodash.foreach'
|
||||
import includes from 'lodash.includes'
|
||||
import isEmpty from 'lodash.isempty'
|
||||
import isNumber from 'lodash.isnumber'
|
||||
import map from 'lodash.map'
|
||||
import slice from 'lodash.slice'
|
||||
import xoApi from 'xo-api'
|
||||
@@ -17,7 +19,11 @@ export default angular.module('xoWebApp.filters', [
|
||||
.filter('bytesToSize', () => {
|
||||
const powers = ['', 'K', 'M', 'G', 'T', 'P']
|
||||
|
||||
return function bytesToSize (bytes, unit = 'B', base = 1024) {
|
||||
return function bytesToSize (bytes, unit = 'iB', base = 1024) {
|
||||
if (!isNumber(bytes)) {
|
||||
return bytes
|
||||
}
|
||||
|
||||
let i = 0
|
||||
while (bytes >= base) {
|
||||
bytes /= base
|
||||
@@ -31,47 +37,7 @@ export default angular.module('xoWebApp.filters', [
|
||||
// Maximum 1 decimals.
|
||||
bytes = ((bytes * 10) | 0) / 10
|
||||
|
||||
return `${bytes}${powers[i]}${unit}`
|
||||
}
|
||||
})
|
||||
|
||||
.filter('sizeToBytes', () => {
|
||||
/* eslint no-multi-spaces: 0 */
|
||||
|
||||
const RE = new RegExp('^' +
|
||||
'(\\d+(?:\\.\\d+)?)' + // digits ('.' digits)?
|
||||
'\\s*' + // Optional spaces between the digits and the unit.
|
||||
'([kmgtp])?' + // Optional unit modifier K/M/G/T/P.
|
||||
'b?' + // Optional unit (“b”), not meaningful.
|
||||
'$', 'i')
|
||||
|
||||
const factors = {
|
||||
k: 1024,
|
||||
m: 1048576,
|
||||
g: 1073741824,
|
||||
t: 1099511627776,
|
||||
p: 1125899906842624
|
||||
}
|
||||
|
||||
return function sizeToBytes (size) {
|
||||
const matches = RE.exec(size)
|
||||
|
||||
// If the input is invalid, just returns null.
|
||||
if (!matches) {
|
||||
return null
|
||||
}
|
||||
|
||||
let value = +matches[1]
|
||||
|
||||
const modifier = matches[2]
|
||||
if (modifier) {
|
||||
const factor = factors[modifier.toLowerCase()]
|
||||
if (factor) {
|
||||
value *= factor
|
||||
}
|
||||
}
|
||||
|
||||
return Math.round(value)
|
||||
return `${bytes} ${powers[i]}${unit}`
|
||||
}
|
||||
})
|
||||
|
||||
@@ -105,6 +71,7 @@ export default angular.module('xoWebApp.filters', [
|
||||
|
||||
.filter('isEmpty', () => isEmpty)
|
||||
.filter('isNotEmpty', () => (collection) => !isEmpty(collection))
|
||||
.filter('includes', () => includes)
|
||||
|
||||
.filter('duration', () => (n, unit = 'ms') => (n > 0 && moment.duration(n, unit).humanize() || ''))
|
||||
|
||||
|
||||
47
app/node_modules/xo/index.js
generated
vendored
47
app/node_modules/xo/index.js
generated
vendored
@@ -104,6 +104,9 @@ export default angular.module('xo', [
|
||||
disk: {
|
||||
create: action('Create disk', 'disk.create', {
|
||||
argsMapper: (name, size, sr) => ({name, size, sr})
|
||||
}),
|
||||
resize: action('Resize disk', 'disk.resize', {
|
||||
argsMapper: (id, size) => ({id, size})
|
||||
})
|
||||
},
|
||||
|
||||
@@ -113,7 +116,10 @@ export default angular.module('xo', [
|
||||
patch: action('Upload patch', 'pool.patch', {
|
||||
argsMapper: (pool) => ({pool})
|
||||
}),
|
||||
mergeInto: action('Merge pools', 'pool.mergeInto')
|
||||
mergeInto: action('Merge pools', 'pool.mergeInto'),
|
||||
setDefaultSr: action('Set the default SR', 'pool.setDefaultSr', {
|
||||
argsMapper: (pool, sr) => ({pool, sr})
|
||||
})
|
||||
},
|
||||
|
||||
host: {
|
||||
@@ -129,12 +135,15 @@ export default angular.module('xo', [
|
||||
listMissingPatches: action('Check available patches', 'host.listMissingPatches', {
|
||||
argsMapper: (host) => ({host})
|
||||
}),
|
||||
installPatch: action('Install a patch from a patch id', 'host.installPatch', {
|
||||
installPatch: action('Install a patch from a patch id', 'host.installPatch', {
|
||||
argsMapper: (host, patch) => ({host, patch})
|
||||
}),
|
||||
installAllPatches: action('Install all the missing patches on a host', 'host.installAllPatches', {
|
||||
argsMapper: (host) => ({host})
|
||||
}),
|
||||
emergencyShutdownHost: action('Suspend all VMs running on host and shutdown host', 'host.emergencyShutdownHost', {
|
||||
argsMapper: (host) => ({host})
|
||||
}),
|
||||
refreshStats: action('Get Stats', 'host.stats', {
|
||||
notification: false,
|
||||
argsMapper: (host, granularity) => ({host, granularity})
|
||||
@@ -228,6 +237,12 @@ export default angular.module('xo', [
|
||||
}),
|
||||
restart: action('Restart a Docker Container', 'docker.restart', {
|
||||
argsMapper: (vm, container) => ({vm, container})
|
||||
}),
|
||||
register: action('Register the VM for the Docker plugin', 'docker.register', {
|
||||
argsMapper: (vm) => ({ vm })
|
||||
}),
|
||||
deregister: action('Deregister the VM for the Docker plugin', 'docker.deregister', {
|
||||
argsMapper: (vm) => ({ vm })
|
||||
})
|
||||
},
|
||||
|
||||
@@ -262,16 +277,24 @@ export default angular.module('xo', [
|
||||
import: action('Import VM', 'vm.import', {
|
||||
argsMapper: (host) => ({ host })
|
||||
}),
|
||||
importBackup: action('Imports a VM from a remote point', 'vm.importBackup', {
|
||||
argsMapper: (remote, file, sr) => ({ remote, file, sr })
|
||||
}),
|
||||
importDeltaBackup: action('Imports a delta backup from a remote point', 'vm.importDeltaBackup', {
|
||||
argsMapper: (remote, filePath, sr) => ({ remote, filePath, sr })
|
||||
}),
|
||||
migrate: action('Migrate VM', 'vm.migrate', {
|
||||
argsMapper: (id, host_id) => ({ id, host_id })
|
||||
}),
|
||||
restart: action('Restart VM', 'vm.restart', {
|
||||
argsMapper: (id, force = false) => ({ id, force })
|
||||
}),
|
||||
recoveryStart: action('Start VM in recovery mode', 'vm.recoveryStart'),
|
||||
start: action('Start VM', 'vm.start'),
|
||||
stop: action('Stop VM', 'vm.stop', {
|
||||
argsMapper: (id, force = false) => ({ id, force })
|
||||
}),
|
||||
setBootOrder: action('Set the boot order', 'vm.setBootOrder'),
|
||||
revert: action('Revert snapshot', 'vm.revert'),
|
||||
suspend: action('Suspend VM', 'vm.suspend'),
|
||||
resume: action('Resume VM', 'vm.resume', {
|
||||
@@ -281,6 +304,12 @@ export default angular.module('xo', [
|
||||
notification: false,
|
||||
argsMapper: (id, granularity) => ({id, granularity})
|
||||
}),
|
||||
getCloudInitConfig: action('Get Cloud Init Template', 'vm.getCloudInitConfig', {
|
||||
argsMapper: (template) => ({ template })
|
||||
}),
|
||||
createCloudInitConfigDrive: action('Create Cloud Config Drive', 'vm.createCloudInitConfigDrive', {
|
||||
argsMapper: (vm, sr, config, coreos) => ({ vm, sr, config, coreos })
|
||||
}),
|
||||
// TODO: create/set/pause
|
||||
connectPci: action('Connect PCI device', 'vm.attachPci', {
|
||||
argsMapper: (vm, pciId) => ({vm, pciId})
|
||||
@@ -319,20 +348,24 @@ export default angular.module('xo', [
|
||||
}),
|
||||
delete: action('Delete a job', 'job.delete', {
|
||||
argsMapper: (id) => ({id})
|
||||
}),
|
||||
runSequence: action('Run a sequence of jobs', 'job.runSequence', {
|
||||
argsMapper: (idSequence) => ({idSequence})
|
||||
})
|
||||
},
|
||||
|
||||
schedule: {
|
||||
getAll: action('Get all schedules', 'schedule.getAll'),
|
||||
create: action('Create a schedule', 'schedule.create', {
|
||||
argsMapper: (jobId, cron, enabled) => ({jobId, cron, enabled})
|
||||
argsMapper: (jobId, cron, enabled, name) => ({jobId, cron, enabled, name})
|
||||
}),
|
||||
set: action('Modify a schedule', 'schedule.set', {
|
||||
argsMapper: (id, jobId = undefined, cron = undefined, enabled = undefined) => {
|
||||
argsMapper: (id, jobId = undefined, cron = undefined, enabled = undefined, name = undefined) => {
|
||||
const args = {id}
|
||||
jobId !== undefined && (args.jobId = jobId)
|
||||
cron !== undefined && (args.cron = cron)
|
||||
enabled !== undefined && (args.enabled = enabled)
|
||||
name !== undefined && (args.name = name)
|
||||
return args
|
||||
}
|
||||
}),
|
||||
@@ -370,9 +403,6 @@ export default angular.module('xo', [
|
||||
}),
|
||||
list: action('List files found at the remote point', 'remote.list', {
|
||||
argsMapper: id => ({id})
|
||||
}),
|
||||
importVm: action('Imports a VM form a remote point', 'remote.importVm', {
|
||||
argsMapper: (id, file, host) => ({id, file, host})
|
||||
})
|
||||
},
|
||||
|
||||
@@ -401,6 +431,9 @@ export default angular.module('xo', [
|
||||
}),
|
||||
unload: action('Unloads a plugin', 'plugin.unload', {
|
||||
argsMapper: (id) => ({id})
|
||||
}),
|
||||
purgeConfiguration: action('Purge a plugin configuration', 'plugin.purgeConfiguration', {
|
||||
argsMapper: (id) => ({id})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -445,6 +445,18 @@ html, body, .view-main {
|
||||
background-color: #2e3133;
|
||||
}
|
||||
|
||||
// transparent background for dropdown buttons
|
||||
.filter {
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
border: 0;
|
||||
color: #9d9d9d;
|
||||
box-shadow: none !important;
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0) !important;
|
||||
color: white !important;
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Main view
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xo-web",
|
||||
"version": "4.9.0",
|
||||
"version": "4.11.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Web interface client for Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -27,6 +27,7 @@
|
||||
"browserify": "^12.0.1",
|
||||
"browserify-plain-jade": "^0.2.2",
|
||||
"bundle-collapser": "^1.1.4",
|
||||
"clipboard": "^1.5.5",
|
||||
"coffeeify": "^1.0.0",
|
||||
"d3": "^3.5.5",
|
||||
"event-stream": "^3.3.0",
|
||||
@@ -60,8 +61,10 @@
|
||||
"lodash.indexof": "^3.0.2",
|
||||
"lodash.intersection": "^3.1.0",
|
||||
"lodash.isempty": "^3.0.3",
|
||||
"lodash.isnumber": "^3.0.1",
|
||||
"lodash.keys": "^3.1.2",
|
||||
"lodash.map": "^3.1.2",
|
||||
"lodash.mapvalues": "^3.0.1",
|
||||
"lodash.omit": "^3.1.0",
|
||||
"lodash.pluck": "^3.1.0",
|
||||
"lodash.pull": "^3.0.1",
|
||||
|
||||
Reference in New Issue
Block a user