Compare commits

...

53 Commits

Author SHA1 Message Date
Julien Fontanet
a921cb2d0d 4.10.0 2015-11-27 14:35:50 +01:00
Olivier Lambert
f3aaa363d8 Merge pull request #541 from vatesfr/marsaudf-UI-fix
Minor UI fix
2015-11-27 13:39:20 +01:00
Fabrice Marsaud
45a79e1920 Minor UI fix 2015-11-27 13:35:27 +01:00
Olivier Lambert
6fd9b2a453 Merge pull request #493 from vatesfr/marsaudf-task-manager
Generic task manager
2015-11-27 12:03:01 +01:00
Olivier Lambert
01d8e89a71 add changelog 2015-11-27 11:58:00 +01:00
Fabrice Marsaud
c89fa63910 Minor UI fix 2015-11-27 11:25:23 +01:00
Fabrice Marsaud
9fc5c49dbf UI enhancements 2015-11-27 11:25:23 +01:00
Fabrice Marsaud
7dfc269df9 Enhanced UI inputs for XO object management 2015-11-27 11:25:18 +01:00
Fabrice Marsaud
76d0b397db Instant one shot for generic jobs 2015-11-27 11:24:25 +01:00
Fabrice Marsaud
5413f887af Bare generic job creation and scheduling 2015-11-27 11:24:13 +01:00
Julien Fontanet
b3d0c61f0e Merge pull request #540 from vatesfr/abhamonr-plugin-input-type-number
Plugin 'number' property use input number in config form (fix #538)
2015-11-27 10:49:32 +01:00
wescoeur
4ce0441d68 Plugin 'number' property use input number in config form 2015-11-27 10:42:45 +01:00
Julien Fontanet
72be34e18d Move clipboard to dev deps. 2015-11-27 10:04:43 +01:00
Julien Fontanet
d2961b7650 Merge pull request #537 from vatesfr/abhamonr-plugins-supports-numbers
Plugin config supports integer properties (fix #531).
2015-11-27 09:54:46 +01:00
wescoeur
fdca1bbf72 Plugin config supports integer properties. 2015-11-27 09:43:33 +01:00
Julien Fontanet
ab7a2f9dee Merge pull request #536 from vatesfr/abhamonr-plugin-boolean-checkbox
Plugin boolean properties use checkboxes (fix #528).
2015-11-27 09:42:06 +01:00
wescoeur
7b72857a3b Plugin boolean properties use checkboxes 2015-11-26 22:44:24 +01:00
Olivier Lambert
4787146658 Merge pull request #533 from vatesfr/marsaudf-backup-ui
better backup log display
2015-11-26 16:30:18 +01:00
Fabrice Marsaud
430f9356c3 Minor button fix 2015-11-26 16:27:40 +01:00
Fabrice Marsaud
70a3b3518f Better schedule state UI in overview 2015-11-26 16:25:00 +01:00
Fabrice Marsaud
c0944c17e0 better backup log display 2015-11-26 16:07:47 +01:00
Julien Fontanet
da1b2a91e7 Merge pull request #526 from vatesfr/pierre-console-keyboard-unfocus
Console has keyboard and mouse focus only when mouse is hovering
2015-11-26 15:59:48 +01:00
Pierre
aa27492713 Console catches keyboard and mouse inputs only when mouse is hovering.
Also, when the mouse enters the VM screen, the current active element is unfocused.
2015-11-26 15:44:01 +01:00
Julien Fontanet
afe589dec3 Merge pull request #527 from vatesfr/abhamonr-plugin-title-property
Support title property in plugin configuration schema
2015-11-26 15:25:25 +01:00
wescoeur
978d140c8f Support title property in plugin configuration schema 2015-11-26 14:31:32 +01:00
Olivier Lambert
2ce213b62c Merge pull request #525 from vatesfr/pierre-clipboard-management-through-console
Clipboard management through console
2015-11-26 11:35:57 +01:00
Pierre
7748266078 Clipboard support in console.
- From VM to client :
	1) Copy text in VM
	2) The text field (above the console) updates automatically with the VM's clipboard content
	3) Click on the 'Copy' button to get the text in the local clipboard
- From client to VM :
	1) Write text in the text field
	2) The VM's clipboard updates automatically with the new content
	3) Paste text anywhere in the VM
2015-11-26 11:23:40 +01:00
Olivier Lambert
83783d07a1 hide action panel for host or VM if only viewer 2015-11-25 14:38:47 +01:00
Olivier Lambert
49a1f2c7c5 Merge pull request #517 from vatesfr/marsaudf-disable-host-buttons#474
Disable host buttons relying on ACLs
2015-11-25 14:31:49 +01:00
Olivier Lambert
ddfc0151fc Merge pull request #515 from vatesfr/marsaudf-backup-display#512
Tag display for backup schedules in overview #512
2015-11-25 14:01:33 +01:00
Fabrice Marsaud
81c508e13c Host view OK 2015-11-25 10:25:10 +01:00
Fabrice Marsaud
7195cfc3cf First step 2015-11-24 17:31:26 +01:00
Fabrice Marsaud
93fe5e2cf7 PR feedback 2015-11-24 17:31:00 +01:00
Fabrice Marsaud
a2bf795d12 Tag display for backup schedules in overview 2015-11-24 17:31:00 +01:00
Julien Fontanet
c8d78f39e0 Upgrade npm to latest on Travis. 2015-11-24 17:17:58 +01:00
Fabrice Marsaud
d9ab8a1c8b Fix #508 2015-11-23 15:24:23 +01:00
Olivier Lambert
5125ad4889 Merge pull request #506 from vatesfr/pierre-emergency-host-shutdown
Emergency button in host view is now calling the server function
2015-11-20 17:32:17 +01:00
Olivier Lambert
951e85b04b Merge pull request #507 from vatesfr/olivierlambert-cloudconfig
CoreOS cloud config management during VM creation
2015-11-20 17:32:10 +01:00
Olivier Lambert
711d922695 CoreOS cloud config during VM creation 2015-11-20 17:10:10 +01:00
Pierre
3692ffcde7 Rename function : emergencyHostShutdown -> emergencyShutdownHost 2015-11-20 10:19:06 +01:00
Pierre
b049420c59 Emergency button in host view is now calling the server function (suspends all the VMs running on the host and then shuts the host down) 2015-11-19 17:07:10 +01:00
Olivier Lambert
241103c369 Merge pull request #501 from vatesfr/pierre-install-patches-on-all-pools
Created panel in dashboard
2015-11-19 14:49:59 +01:00
Pierre
2128367113 Update panel in dashboard. 2015-11-19 12:29:48 +01:00
Julien Fontanet
f555c8190d Revert "nvm (on Travis) does not use stable correctly."
This reverts commit f85dc3b7e7.
2015-11-18 17:32:54 +01:00
Pierre
d5df633def Removed some useless CSS 2015-11-18 17:13:54 +01:00
Olivier Lambert
fe7dc859e3 Merge pull request #499 from vatesfr/pierre-suspend-all-vms-and-shutdown-host
emergency shutdown feature in host view (suspend all VMs then shutdown)
2015-11-18 17:04:04 +01:00
Pierre
569c5046c6 Added an emergency button in Action panel (host view) : suspends all the VMs and shuts the host down. 2015-11-18 16:56:56 +01:00
Julien Fontanet
e0210ae2d8 Stable is the new stable branch. 2015-11-18 16:30:30 +01:00
Julien Fontanet
f85dc3b7e7 nvm (on Travis) does not use stable correctly. 2015-11-18 16:10:07 +01:00
Olivier Lambert
92d4363120 tree view improvements and fix 2015-11-17 15:18:57 +01:00
Olivier Lambert
6c69220de2 add start in recovery mode for HVM guests and support new API call setBootOrder() instead of bootOrder() 2015-11-17 14:59:26 +01:00
Julien Fontanet
3a1229b072 Only test on stable as there is just linting for now. 2015-11-16 16:59:55 +01:00
Olivier Lambert
45538c9f62 add quiesce display in VM view 2015-11-16 13:28:51 +01:00
51 changed files with 2419 additions and 429 deletions

View File

@@ -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

View File

@@ -1,5 +1,45 @@
# ChangeLog
## **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

View File

@@ -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

View File

@@ -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,

View File

@@ -1,11 +1,6 @@
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()
@@ -18,7 +13,6 @@ import restore from './restore'
import rollingSnapshot from './rolling-snapshot'
import view from './view'
import scheduler from './scheduler'
export default angular.module('backup', [
uiRouter,
@@ -28,7 +22,8 @@ export default angular.module('backup', [
management,
mount,
restore,
rollingSnapshot
rollingSnapshot,
scheduler
])
.config(function ($stateProvider) {
$stateProvider.state('backup', {
@@ -49,302 +44,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

View File

@@ -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'
@@ -21,15 +22,17 @@ export default angular.module('backup.management', [
})
.controller('ManagementCtrl', function ($scope, $state, $stateParams, $interval, xo, xoApi, notify, selectHighLevelFilter, filterFilter) {
const mapJobKeyToState = {
'rollingSnapshot': 'rollingsnapshot',
'rollingBackup': 'backup',
'disasterRecovery': 'disasterrecovery'
rollingSnapshot: 'rollingsnapshot',
rollingBackup: 'backup',
disasterRecovery: 'disasterrecovery',
__none: 'index'
}
const mapJobKeyToJobDisplay = {
'rollingSnapshot': 'Rolling Snapshot',
'rollingBackup': 'Backup',
'disasterRecovery': 'Disaster Recovery'
rollingSnapshot: 'Rolling Snapshot',
rollingBackup: 'Backup',
disasterRecovery: 'Disaster Recovery',
__none: '[unknown]'
}
this.currentLogPage = 1
@@ -37,7 +40,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 +48,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 +63,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 +107,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 +161,10 @@ 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.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 = {}

View File

@@ -16,25 +16,22 @@
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&nbsp;
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
.grid-sm
.panel.panel-default
.panel-heading.panel-title
@@ -53,8 +50,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 }}&nbsp;
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}')
| &nbsp;{{ log.jobId }}
td {{ ctrl.displayLogKey(log) }}
td {{ log.start | date:'medium' }}
td {{ log.end | date:'medium' }}
@@ -68,6 +65,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 }}:&#32;
span(ng-repeat = '(key, param) in call.params')
strong {{ key }}:
| &nbsp;{{ param }}&nbsp;

View File

@@ -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

View File

@@ -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
| &nbsp;
| 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)'
)

View File

@@ -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,32 @@ 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)
})
}
function populateChartsData () {
let pools,
vmsByContainer,
@@ -74,11 +100,22 @@ 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')
$scope.nbUpdates = {}
foreach(pools.all, function (pool, pool_id) {
let pool_hosts = hostsByPool[pool_id]
foreach(pool_hosts, function (host, host_id) {
xo.host.listMissingPatches(host_id)
.then(result => {
$scope.nbUpdates[host_id] = result.length
}
)
})
})
foreach(pools.all, function (pool, pool_id) {
nb_pools++

View File

@@ -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

View File

@@ -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

View File

@@ -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
| &nbsp; 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}}&nbsp;
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&nbsp;
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=">>")

View File

@@ -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'

View File

@@ -114,6 +114,7 @@ module.exports = angular.module 'xoWebApp.newVm', [
$scope.name_description = ''
$scope.name_label = ''
$scope.template = ''
$scope.firstSR = ''
$scope.VDIs = []
$scope.VIFs = []
$scope.isDiskTemplate = false
@@ -150,6 +151,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 cloudConfig to empty
$scope.cloudConfig = ''
{install_methods} = template.template_info
availableMethods = $scope.availableMethods = Object.create null
@@ -173,6 +176,15 @@ module.exports = angular.module 'xoWebApp.newVm', [
VDI.id = VDI_id++
VDI.size = bytesToSizeFilter VDI.size
VDI.SR or= default_SR
# store the first SR for later use (e.g: CloudConfig)
if VDI.id == 0
$scope.firstSR = VDI.SR or= default_SR
# 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.cloudConfig = result
$scope.createVM = ->
{
@@ -270,6 +282,9 @@ module.exports = angular.module 'xoWebApp.newVm', [
# FIXME: handles invalid entries.
data.memory = memory
if $scope.cloudConfig
xo.vm.createCloudInitConfigDrive(id, $scope.firstSR, $scope.cloudConfig).then ->
xo.docker.register id
xoApi.call('vm.set', data).then -> id
.then (id) ->
$state.go 'VMs_view', { id }

View File

@@ -154,6 +154,18 @@ 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")
.panel.panel-default

View File

@@ -56,8 +56,14 @@ function cleanUpConfiguration (schema, configuration) {
configuration[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)
} else if (schema.properties && schema.properties[key]) {
const type = schema.properties[key].type
if (type === 'integer' || type === 'number') {
configuration[key] = +configuration[key]
} else if (type === 'object') {
cleanUpConfiguration(schema.properties[key], item)
}
}
})
}
@@ -172,7 +178,7 @@ export default angular.module('settings.plugins', [
}
})
.directive('objectInput', () => {
.directive('confObjectInput', () => {
return {
restrict: 'E',
template: objectInputView,
@@ -181,12 +187,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 = {

View File

@@ -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}}&nbsp;:&nbsp;
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')

View File

@@ -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 }}&nbsp;
@@ -30,15 +30,15 @@
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)')
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

View 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

View 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()') &nbsp;*
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

View 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()') &nbsp;*
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
input(form = '{{ ctrl.form }}', type = 'checkbox', ng-model = 'ctrl.model')

View 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()') &nbsp;*
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') &nbsp;({{ ($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}}')
| &nbsp;{{ object.name_label }}
span(ng-if = 'object.$container') &nbsp;({{ (object.$container | resolve).name_label || ((object.$container | resolve).master | resolve).name_label }})

View 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

View 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()') &nbsp;*
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()')

View 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()') &nbsp;*
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()')

View 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()') &nbsp;*
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')
| &nbsp;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]')

View 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()') &nbsp;*
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') &nbsp;({{ ($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}}')
| &nbsp;{{ object.name_label }}
span(ng-if = 'object.$container') &nbsp;({{ (object.$container | resolve).name_label || ((object.$container | resolve).master | resolve).name_label }})

View 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()') &nbsp;*
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') &nbsp;({{ ($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}}')
| &nbsp;{{ object.name_label }}
span(ng-if = 'object.$container') &nbsp;({{ (object.$container | resolve).name_label || ((object.$container | resolve).master | resolve).name_label }})

View 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()') &nbsp;*
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()')

View 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
| &nbsp;
i.fa.fa-arrow-right
| &nbsp;
i.fa.fa-database
| &nbsp;Save&nbsp;
| &nbsp;
button.btn.btn-lg.btn-default(type = 'button', ng-click = 'ctrl.resetForm()')
| &nbsp;Reset&nbsp;
.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 }}&ensp;
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
| &nbsp;
button.btn.btn-danger(type = 'button', ng-click = 'ctrl.delete(job.id)'): i.fa.fa-trash

View 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()') &nbsp;*
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)')
| &nbsp;{{ $item.name_label }}
span(ng-if = '$item.$container') &nbsp;({{ ($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 }}')
| &nbsp;{{ vm.name_label }}
span(ng-if = 'vm.$container') &nbsp;({{ (vm.$container | resolve).name_label || ((vm.$container | resolve).master | resolve).name_label }})

View 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()') &nbsp;*
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}}

View 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()') &nbsp;*
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]')

View 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()') &nbsp;*
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}}

View File

@@ -0,0 +1,160 @@
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.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.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

View File

@@ -0,0 +1,83 @@
.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
.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}')
| &nbsp;{{ 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') &nbsp;{{ 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 }}:&#32;
span(ng-repeat = '(key, param) in call.params')
| {{ key }}:
strong &nbsp;{{ param }}&#32;
span(ng-if = 'call.returnedValue')
| &#32;
i.text-primary.fa.fa-arrow-right
| &#32;{{ call.returnedValue }}
span.text-danger(ng-if = 'call.error')
| &#32;
i.fa.fa-times
| &#32;{{ 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 = '>>')

View File

@@ -0,0 +1,103 @@
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
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.prettyCron = prettyCron.toString.bind(prettyCron)
})
// A module exports its name.
.name

View File

@@ -0,0 +1,75 @@
.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
| &nbsp;
i.fa.fa-arrow-right
| &nbsp;
i.fa.fa-database
| &nbsp;Save&nbsp;
| &nbsp;
button.btn.btn-lg.btn-default(type = 'button', ng-click = 'ctrl.reset()')
| &nbsp;Reset&nbsp;
.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 }}&ensp;
span.text-muted.hidden-xs ({{schedule.id}})&ensp;
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
| &nbsp;
button.btn.btn-danger(type = 'button', ng-click = 'ctrl.delete(schedule.id)'): i.fa.fa-trash

View 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 = '')

View File

@@ -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

View File

@@ -234,7 +234,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 xoApi.call 'vm.setBootOrder', {vm: id, order: paramString}
.finally () ->
$scope.savingBootOrder = false
$scope.bootReordering = false
@@ -302,6 +302,19 @@ module.exports = angular.module 'xoWebApp.vm', [
message: 'Start VM'
}
$scope.recoveryStartVM = (id) ->
oldBootParams = vm.boot.order
xoApi.call('vm.setBootOrder', {vm: id, order: 'dn'}).then(
-> xo.vm.start id
).then(
->
notify.info {
title: 'VM starting...'
message: 'Start VM'
}
return xoApi.call 'vm.setBootOrder', {vm: id, order: oldBootParams}
)
$scope.stopVM = (id) ->
modal.confirm
title: 'VM shutdown'

View File

@@ -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') && VM.virtualizationMode == 'hvm'")
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
@@ -738,13 +741,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

View File

@@ -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
}

View File

@@ -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.

314
app/node_modules/scheduler/index.js generated vendored Normal file
View 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
View File

@@ -0,0 +1,9 @@
{
"private": true,
"browserify": {
"transform": [
"babelify",
"browserify-plain-jade"
]
}
}

104
app/node_modules/scheduler/view.jade generated vendored Normal file
View 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
| &nbsp;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
| &nbsp;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
| &nbsp;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
| &nbsp;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:&nbsp;
| {{ ctrl.prettyCron(ctrl.data.cronPattern) }}
.form-inline.container-fluid
.form-group
label Preview:&nbsp;
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 ...

View File

@@ -1,5 +1,6 @@
import angular from 'angular'
import forEach from 'lodash.foreach'
import includes from 'lodash.includes'
import isEmpty from 'lodash.isempty'
import map from 'lodash.map'
import slice from 'lodash.slice'
@@ -105,6 +106,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() || ''))

25
app/node_modules/xo/index.js generated vendored
View File

@@ -129,12 +129,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 +231,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 })
})
},
@@ -281,6 +290,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) => ({ vm, sr, config })
}),
// TODO: create/set/pause
connectPci: action('Connect PCI device', 'vm.attachPci', {
argsMapper: (vm, pciId) => ({vm, pciId})
@@ -319,20 +334,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
}
}),

View File

@@ -1,6 +1,6 @@
{
"name": "xo-web",
"version": "4.9.0",
"version": "4.10.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",
@@ -62,6 +63,7 @@
"lodash.isempty": "^3.0.3",
"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",