Compare commits

..

7 Commits

Author SHA1 Message Date
florent
93e7bdaeb0 add proprtype 2016-02-02 20:59:08 +01:00
florent
877e471b10 working on the create vm form 2016-02-01 22:29:48 +01:00
florent
32478e470b FB proposition for react (WIP)
after a few try, I think it can work this way :

 * everything is in one store: xoApi, session, even component states. try to use boolean when possible to simplify usage in components
 * components states should rarely be used : let components be stateless, a pure function of their props
 * use connect on high level component to give them access to part of the store and the actions. A component should not have access to the full store and action.
 * use event to get data back from child components ( if they can't dispatch action themselves)
 * redux-thunk allow to dispatch multiple action in cascade (even async one) , like want to save->saving ->saved (or errored)
 * use imutable object, this will allow faster performance in the long run
2016-01-31 22:06:12 +01:00
Julien Fontanet
ccbcaa94fe Various updates. 2016-01-28 09:45:40 +01:00
Julien Fontanet
0ffa9d4225 Various updates. 2016-01-22 16:03:33 +01:00
Julien Fontanet
8e5dee79e0 Use Babel 6. 2015-12-04 11:49:19 +01:00
Julien Fontanet
9abd9d20ec Initial work on React/Redux. 2015-12-03 18:22:13 +01:00
209 changed files with 996 additions and 24712 deletions

View File

@@ -2,11 +2,11 @@
"comments": false,
"compact": true,
"plugins": [
"transform-decorators-legacy",
"transform-runtime"
],
"presets": [
"es2015",
"stage-0",
"es2015"
"react"
]
}

11
.gitignore vendored
View File

@@ -1,10 +1,9 @@
/.nyc_output/
/bower_components/
/dist/
/node_modules/*
!/node_modules/*.js
/node_modules/*.js/
jsconfig.json
.idea
npm-debug.log
npm-debug.log.*
!node_modules/*
node_modules/*/

5
.mocha.js Normal file
View File

@@ -0,0 +1,5 @@
Error.stackTraceLimit = 100
try { require('trace') } catch (_) {}
try { require('clarify') } catch (_) {}
try { require('source-map-support/register') } catch (_) {}

1
.mocha.opts Normal file
View File

@@ -0,0 +1 @@
--require ./.mocha.js

View File

@@ -1,5 +1,6 @@
/examples/
example.js
example.js.map
*.example.js
*.example.js.map

View File

@@ -1,10 +1,10 @@
language: node_js
node_js:
- 'stable'
- '4'
- '0.12'
- '0.10'
# Use containers.
# http://docs.travis-ci.com/user/workers/container-based-infrastructure/
sudo: false
before_install:
- npm i -g npm

View File

@@ -1,253 +1,5 @@
# ChangeLog
## **4.16.0** (2016-04-29)
Maintenance release
### Enhancements
- TOO\_MANY\_PENDING\_TASKS [\#861](https://github.com/vatesfr/xo-web/issues/861)
### Bug fixes
- Incorrect VM target name with continuous replication [\#904](https://github.com/vatesfr/xo-web/issues/904)
- Error while deleting users [\#901](https://github.com/vatesfr/xo-web/issues/901)
- Use an available path to the SR to create a config drive [\#882](https://github.com/vatesfr/xo-web/issues/882)
- VM autoboot don't set the right pool parameter [\#879](https://github.com/vatesfr/xo-web/issues/879)
- BUG: ACL with NFS ISO Library not working! [\#870](https://github.com/vatesfr/xo-web/issues/870)
- Broken paths in backups in SMB [\#865](https://github.com/vatesfr/xo-web/issues/865)
- Plugins page loads users/groups multiple times [\#829](https://github.com/vatesfr/xo-web/issues/829)
- "Ghost" VM remains after migration [\#769](https://github.com/vatesfr/xo-web/issues/769)
## **4.15.0** (2016-03-21)
Load balancing, SMB delta support, advanced network operations...
### Enhancements
- Add the job name inside the backup email report [\#819](https://github.com/vatesfr/xo-web/issues/819)
- Delta backup with quiesce [\#812](https://github.com/vatesfr/xo-web/issues/812)
- Hosts: No user feedback when error occurs with SR connect / disconnect [\#810](https://github.com/vatesfr/xo-web/issues/810)
- Expose components versions [\#807](https://github.com/vatesfr/xo-web/issues/807)
- Rework networks/PIFs management [\#805](https://github.com/vatesfr/xo-web/issues/805)
- Displaying all SRs and a list of available hosts for creating VM from a pool [\#790](https://github.com/vatesfr/xo-web/issues/790)
- Add "Source network" on "VM migration" screen [\#785](https://github.com/vatesfr/xo-web/issues/785)
- Migration queue [\#783](https://github.com/vatesfr/xo-web/issues/783)
- Match network names for VM migration [\#782](https://github.com/vatesfr/xo-web/issues/782)
- Disk names [\#780](https://github.com/vatesfr/xo-web/issues/780)
- Self service: should the user be able to set the CPU weight? [\#767](https://github.com/vatesfr/xo-web/issues/767)
- host & pool Citrix license status [\#763](https://github.com/vatesfr/xo-web/issues/763)
- pool view: Provide "updates" section [\#762](https://github.com/vatesfr/xo-web/issues/762)
- XOA ISO image: ambigious root disk label [\#761](https://github.com/vatesfr/xo-web/issues/761)
- Host info: provide system serial number [\#760](https://github.com/vatesfr/xo-web/issues/760)
- CIFS ISO SR Creation [\#731](https://github.com/vatesfr/xo-web/issues/731)
- MAC address not preserved on VM restore [\#707](https://github.com/vatesfr/xo-web/issues/707)
- Failing replication job should send reports [\#659](https://github.com/vatesfr/xo-web/issues/659)
- Display networks in the Pool view [\#226](https://github.com/vatesfr/xo-web/issues/226)
### Bug fixes
- Broken link to backup remote [\#821](https://github.com/vatesfr/xo-web/issues/821)
- Issue with self-signed cert for email plugin [\#817](https://github.com/vatesfr/xo-web/issues/817)
- Plugins view, reset form and errors [\#815](https://github.com/vatesfr/xo-web/issues/815)
- HVM recovery mode is broken [\#794](https://github.com/vatesfr/xo-web/issues/794)
- Disk bug when creating vm from template [\#778](https://github.com/vatesfr/xo-web/issues/778)
- Can't mount NFS shares in remote stores [\#775](https://github.com/vatesfr/xo-web/issues/775)
- VM disk name and description not passed during creation [\#774](https://github.com/vatesfr/xo-web/issues/774)
- NFS mount problem for Windows share [\#771](https://github.com/vatesfr/xo-web/issues/771)
- lodash.pluck not installed [\#757](https://github.com/vatesfr/xo-web/issues/757)
- this.\_getAuthenticationTokensForUser is not a function [\#755](https://github.com/vatesfr/xo-web/issues/755)
- CentOS 6.x 64bit template creates a VM that won't boot [\#733](https://github.com/vatesfr/xo-web/issues/733)
- Lot of xo:perf leading to XO crash [\#575](https://github.com/vatesfr/xo-web/issues/575)
- New collection checklist [\#262](https://github.com/vatesfr/xo-web/issues/262)
## **4.14.0** (2016-02-23)
Self service, custom CloudInit...
### Enhancements
- VM creation self service with quotas [\#285](https://github.com/vatesfr/xo-web/issues/285)
- Cloud config custom user data [\#706](https://github.com/vatesfr/xo-web/issues/706)
- Patches behind a proxy [\#737](https://github.com/vatesfr/xo-web/issues/737)
- Remote store status indicator [\#728](https://github.com/vatesfr/xo-web/issues/728)
- Patch list order [\#724](https://github.com/vatesfr/xo-web/issues/724)
- Enable reporting on additional backup types [\#717](https://github.com/vatesfr/xo-web/issues/717)
- Tooltip name for cancel [\#703](https://github.com/vatesfr/xo-web/issues/703)
- Portable VHD merging [\#646](https://github.com/vatesfr/xo-web/issues/646)
### Bug fixes
- Avoid merge between two delta vdi backups [\#702](https://github.com/vatesfr/xo-web/issues/702)
- Text in table is not cut anymore [\#713](https://github.com/vatesfr/xo-web/issues/713)
- Disk size edition issue with float numbers [\#719](https://github.com/vatesfr/xo-web/issues/719)
- Create vm, summary is not refreshed [\#721](https://github.com/vatesfr/xo-web/issues/721)
- Boot order problem [\#726](https://github.com/vatesfr/xo-web/issues/726)
## **4.13.0** (2016-02-05)
Backup checksum, SMB remotes...
### Enhancements
- Add SMB mount for remote [\#338](https://github.com/vatesfr/xo-web/issues/338)
- Centralize Perm in a lib [\#345](https://github.com/vatesfr/xo-web/issues/345)
- Expose interpool migration details [\#567](https://github.com/vatesfr/xo-web/issues/567)
- Add checksum for delta backup [\#617](https://github.com/vatesfr/xo-web/issues/617)
- Redirect from HTTP to HTTPS [\#626](https://github.com/vatesfr/xo-web/issues/626)
- Expose vCPU weight [\#633](https://github.com/vatesfr/xo-web/issues/633)
- Avoid metadata in delta backup [\#651](https://github.com/vatesfr/xo-web/issues/651)
- Button to clear logs [\#661](https://github.com/vatesfr/xo-web/issues/661)
- Units for RAM and disks [\#666](https://github.com/vatesfr/xo-web/issues/666)
- Remove multiple VDIs at once [\#676](https://github.com/vatesfr/xo-web/issues/676)
- Find orphaned VDI snapshots [\#679](https://github.com/vatesfr/xo-web/issues/679)
- New health view in Dashboard [\#680](https://github.com/vatesfr/xo-web/issues/680)
- Use physical usage for VDI and SR [\#682](https://github.com/vatesfr/xo-web/issues/682)
- TLS configuration [\#685](https://github.com/vatesfr/xo-web/issues/685)
- Better VM info on tree view [\#688](https://github.com/vatesfr/xo-web/issues/688)
- Absolute values in tooltips for tree view [\#690](https://github.com/vatesfr/xo-web/issues/690)
- Absolute values for host memory [\#691](https://github.com/vatesfr/xo-web/issues/691)
### Bug fixes
- Issues on host console screen [\#672](https://github.com/vatesfr/xo-web/issues/672)
- NFS remote mount fails in particular case [\#665](https://github.com/vatesfr/xo-web/issues/665)
- Unresponsive pages [\#662](https://github.com/vatesfr/xo-web/issues/662)
- Live migration fail in the same pool with local SR fails [\#655](https://github.com/vatesfr/xo-web/issues/655)
## **4.12.0** (2016-01-18)
Continuous Replication, Continuous Delta backup...
### Enhancements
- Continuous VM replication [\#582](https://github.com/vatesfr/xo-web/issues/582)
- Continuous Delta Backup [\#576](https://github.com/vatesfr/xo-web/issues/576)
- Scheduler should not run job again if previous instance is not finished [\#642](https://github.com/vatesfr/xo-web/issues/642)
- Boot VM automatically after creation [\#635](https://github.com/vatesfr/xo-web/issues/635)
- Manage existing VIFs in templates [\#630](https://github.com/vatesfr/xo-web/issues/630)
- Support templates with existing install repository [\#627](https://github.com/vatesfr/xo-web/issues/627)
- Remove running VMs [\#616](https://github.com/vatesfr/xo-web/issues/616)
- Prevent a VM to start before delta import is finished [\#613](https://github.com/vatesfr/xo-web/issues/613)
- Spawn multiple VMs at once [\#606](https://github.com/vatesfr/xo-web/issues/606)
- Fixed `suspendVM` in tree view. [\#619](https://github.com/vatesfr/xo-web/pull/619) ([pdonias](https://github.com/pdonias))
### Bug fixes
- User defined MAC address is not fetch in VM install [\#643](https://github.com/vatesfr/xo-web/issues/643)
- CoreOsCloudConfig is not shown with CoreOS [\#639](https://github.com/vatesfr/xo-web/issues/639)
- Plugin activation/deactivation in web UI seems broken [\#637](https://github.com/vatesfr/xo-web/issues/637)
- Issue when creating CloudConfig drive [\#636](https://github.com/vatesfr/xo-web/issues/636)
- CloudConfig hostname shouldn't have space [\#634](https://github.com/vatesfr/xo-web/issues/634)
- Cloned VIFs are not properly deleted on VM creation [\#632](https://github.com/vatesfr/xo-web/issues/632)
- Default PV args missing during VM creation [\#628](https://github.com/vatesfr/xo-web/issues/628)
- VM creation problems from custom templates [\#625](https://github.com/vatesfr/xo-web/issues/625)
- Emergency shutdown race condition [\#622](https://github.com/vatesfr/xo-web/issues/622)
- `vm.delete\(\)` should not delete VDIs attached to other VMs [\#621](https://github.com/vatesfr/xo-web/issues/621)
- VM creation error from template with a disk [\#581](https://github.com/vatesfr/xo-web/issues/581)
- Only delete VDI exports when VM backup is successful [\#644](https://github.com/vatesfr/xo-web/issues/644)
- Change the name of an imported VM during the import process [\#641](https://github.com/vatesfr/xo-web/issues/641)
- Creating a new VIF in view is partially broken [\#652](https://github.com/vatesfr/xo-web/issues/652)
- Grey out the "create button" during VM creation [\#654](https://github.com/vatesfr/xo-web/issues/654)
## **4.11.0** (2015-12-22)
Delta backup, CloudInit...
### Enhancements
- Visible list of SR inside a VM [\#601](https://github.com/vatesfr/xo-web/issues/601)
- VDI move [\#591](https://github.com/vatesfr/xo-web/issues/591)
- Edit pre-existing disk configuration during VM creation [\#589](https://github.com/vatesfr/xo-web/issues/589)
- Allow disk size edition [\#587](https://github.com/vatesfr/xo-web/issues/587)
- Better VDI resize support [\#585](https://github.com/vatesfr/xo-web/issues/585)
- Remove manual VM export metadata in UI [\#580](https://github.com/vatesfr/xo-web/issues/580)
- Support import VM metadata [\#579](https://github.com/vatesfr/xo-web/issues/579)
- Set a default pool SR [\#572](https://github.com/vatesfr/xo-web/issues/572)
- ISOs should be sorted by name [\#565](https://github.com/vatesfr/xo-web/issues/565)
- Button to boot a VM from a disc once [\#564](https://github.com/vatesfr/xo-web/issues/564)
- Ability to boot a PV VM from a disc [\#563](https://github.com/vatesfr/xo-web/issues/563)
- Add an option to manually run backup jobs [\#562](https://github.com/vatesfr/xo-web/issues/562)
- backups to unmounted storage [\#561](https://github.com/vatesfr/xo-web/issues/561)
- Root integer properties cannot be edited in plugins configuration form [\#550](https://github.com/vatesfr/xo-web/issues/550)
- Generic CloudConfig drive [\#549](https://github.com/vatesfr/xo-web/issues/549)
- Auto-discovery of installed xo-server plugins [\#546](https://github.com/vatesfr/xo-web/issues/546)
- Hide info on flat view [\#545](https://github.com/vatesfr/xo-web/issues/545)
- Config plugin boolean properties must have a default value \(undefined prohibited\) [\#543](https://github.com/vatesfr/xo-web/issues/543)
- Present detailed errors on plugin configuration failures [\#530](https://github.com/vatesfr/xo-web/issues/530)
- Do not reset form on failures in plugins configuration [\#529](https://github.com/vatesfr/xo-web/issues/529)
- XMPP alert plugin [\#518](https://github.com/vatesfr/xo-web/issues/518)
- Hide tag adders depending on ACLs [\#516](https://github.com/vatesfr/xo-web/issues/516)
- Choosing a framework for xo-web 5 [\#514](https://github.com/vatesfr/xo-web/issues/514)
- Prevent adding a host in an existing XAPI connection [\#466](https://github.com/vatesfr/xo-web/issues/466)
- Read only connection to Xen servers/pools [\#439](https://github.com/vatesfr/xo-web/issues/439)
- generic notification system [\#391](https://github.com/vatesfr/xo-web/issues/391)
- Data architecture review [\#384](https://github.com/vatesfr/xo-web/issues/384)
- Make filtering easier to understand/add some "default" filters [\#207](https://github.com/vatesfr/xo-web/issues/207)
- Improve performance [\#148](https://github.com/vatesfr/xo-web/issues/148)
### Bug fixes
- VM metadata export should not require a snapshot [\#615](https://github.com/vatesfr/xo-web/issues/615)
- Missing patch for all hosts is continuously refreshed [\#609](https://github.com/vatesfr/xo-web/issues/609)
- Backup import memory issue [\#608](https://github.com/vatesfr/xo-web/issues/608)
- Host list missing patch is buggy [\#604](https://github.com/vatesfr/xo-web/issues/604)
- Servers infos should not been refreshed while a field is being edited [\#595](https://github.com/vatesfr/xo-web/issues/595)
- Servers list should not been re-order while a field is being edited [\#594](https://github.com/vatesfr/xo-web/issues/594)
- Correctly display size in interface \(binary scale\) [\#592](https://github.com/vatesfr/xo-web/issues/592)
- Display failures on VM boot order modification [\#560](https://github.com/vatesfr/xo-web/issues/560)
- `vm.setBootOrder\(\)` should throw errors on failures \(non-HVM VMs\) [\#559](https://github.com/vatesfr/xo-web/issues/559)
- Hide boot order form for non-HVM VMs [\#558](https://github.com/vatesfr/xo-web/issues/558)
- Allow editing PV args even when empty \(but only for PV VMs\) [\#557](https://github.com/vatesfr/xo-web/issues/557)
- Crashes when using legacy event system [\#556](https://github.com/vatesfr/xo-web/issues/556)
- XenServer patches check error for 6.1 [\#555](https://github.com/vatesfr/xo-web/issues/555)
- activation plugin xo-server-transport-email [\#553](https://github.com/vatesfr/xo-web/issues/553)
- Server error with JSON on 32 bits Dom0 [\#552](https://github.com/vatesfr/xo-web/issues/552)
- Cloud Config drive shouldn't be created on default SR [\#548](https://github.com/vatesfr/xo-web/issues/548)
- Deep properties cannot be edited in plugins configuration form [\#521](https://github.com/vatesfr/xo-web/issues/521)
- Aborted VM export should cancel the operation [\#490](https://github.com/vatesfr/xo-web/issues/490)
- VM missing with same UUID after an inter-pool migration [\#284](https://github.com/vatesfr/xo-web/issues/284)
## **4.10.0** (2015-11-27)
Job management, email notifications, CoreOS/Docker, Quiesce snapshots...
### Enhancements
- Job management ([xo-web#487](https://github.com/vatesfr/xo-web/issues/487))
- Patch upload on all connected servers ([xo-web#168](https://github.com/vatesfr/xo-web/issues/168))
- Emergency shutdown ([xo-web#185](https://github.com/vatesfr/xo-web/issues/185))
- CoreOS/docker template install ([xo-web#246](https://github.com/vatesfr/xo-web/issues/246))
- Email for backups ([xo-web#308](https://github.com/vatesfr/xo-web/issues/308))
- Console Clipboard ([xo-web#408](https://github.com/vatesfr/xo-web/issues/408))
- Logs from CLI ([xo-web#486](https://github.com/vatesfr/xo-web/issues/486))
- Save disconnected servers ([xo-web#489](https://github.com/vatesfr/xo-web/issues/489))
- Snapshot with quiesce ([xo-web#491](https://github.com/vatesfr/xo-web/issues/491))
- Start VM in reovery mode ([xo-web#495](https://github.com/vatesfr/xo-web/issues/495))
- Username in logs ([xo-web#498](https://github.com/vatesfr/xo-web/issues/498))
- Delete associated tokens with user ([xo-web#500](https://github.com/vatesfr/xo-web/issues/500))
- Validate plugin configuration ([xo-web#503](https://github.com/vatesfr/xo-web/issues/503))
- Avoid non configured plugins to be loaded ([xo-web#504](https://github.com/vatesfr/xo-web/issues/504))
- Verbose API logs if configured ([xo-web#505](https://github.com/vatesfr/xo-web/issues/505))
- Better backup overview ([xo-web#512](https://github.com/vatesfr/xo-web/issues/512))
- VM auto power on ([xo-web#519](https://github.com/vatesfr/xo-web/issues/519))
- Title property supported in config schema ([xo-web#522](https://github.com/vatesfr/xo-web/issues/522))
- Start VM export only when necessary ([xo-web#534](https://github.com/vatesfr/xo-web/issues/534))
- Input type should be number ([xo-web#538](https://github.com/vatesfr/xo-web/issues/538))
### Bug fixes
- Numbers/int support in plugins config ([xo-web#531](https://github.com/vatesfr/xo-web/issues/531))
- Boolean support in plugins config ([xo-web#528](https://github.com/vatesfr/xo-web/issues/528))
- Keyboard unusable outside console ([xo-web#513](https://github.com/vatesfr/xo-web/issues/513))
- UsernameField for SAML ([xo-web#513](https://github.com/vatesfr/xo-web/issues/513))
- Wrong display of "no plugin found" ([xo-web#508](https://github.com/vatesfr/xo-web/issues/508))
- Bower build error ([xo-web#488](https://github.com/vatesfr/xo-web/issues/488))
- VM cloning should require SR permission ([xo-web#472](https://github.com/vatesfr/xo-web/issues/472))
- Xen tools status ([xo-web#471](https://github.com/vatesfr/xo-web/issues/471))
- Can't delete ghost user ([xo-web#464](https://github.com/vatesfr/xo-web/issues/464))
- Stats with old versions of Node ([xo-web#463](https://github.com/vatesfr/xo-web/issues/463))
## **4.9.0** (2015-11-13)
Automated DR, restore backup, VM copy

View File

@@ -1,28 +0,0 @@
<!--
Welcome to the issue section of Xen Orchestra!
Here you can:
- report an issue
- propose an enhancement
- ask a question
The template below is only a proposition for your ticket, feel free to
change it as appropriate :)
-->
### Context
- **XO version**: XO appliance / `stable` branch / `next-release` branch
If from the sources:
- **Component**: xo-web / xo-server / *unknown*
- **Node/npm version**: *just execute `npm version`*
### Expected behavior
<!-- What you expect to happen -->
### Actual behavior
<!-- What is actually happening -->

View File

@@ -33,11 +33,13 @@ $ npm run dev
If you are certain the bug is exclusively related to XO-Web, you may use the [bugtracker of this repository](https://github.com/vatesfr/xo-web/issues).
Otherwise, please consider using the [bugtracker of the general repository](https://github.com/vatesfr/xo/issues).
## Process for new release
```bash
# Switch to the stable branch.
git checkout stable
# Switch to the master branch.
git checkout master
# Fetches latest changes.
git pull --ff-only
@@ -51,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 stable to
# Fetches the last changes (the merge and version bump) from master to
# next-release.
git merge --ff-only stable
git merge --ff-only master
# Push the changes on git.
git push --follow-tags origin stable next-release
git push --follow-tags origin master next-release
# Publish this release to npm.
npm publish

View File

@@ -1,212 +0,0 @@
import angular from 'angular'
import angularChartJs from 'angular-chart.js'
import uiBootstrap from'angular-ui-bootstrap'
import uiIndeterminate from'angular-ui-indeterminate'
import uiRouter from'angular-ui-router'
import uiSelect from'angular-ui-select'
import naturalSort from 'angular-natural-sort'
import xeditable from 'angular-xeditable'
import xoDirectives from 'xo-directives'
import xoFilters from 'xo-filters'
import xoServices from 'xo-services'
import aboutState from './modules/about'
import backupState from './modules/backup'
import consoleState from './modules/console'
import dashboardState from './modules/dashboard'
import deleteVmsState from './modules/delete-vms'
import genericModalState from './modules/generic-modal'
import hostState from './modules/host'
import listState from './modules/list'
import migrateVmState from './modules/migrate-vm'
import navbarState from './modules/navbar'
import newSrState from './modules/new-sr'
import newVmState from './modules/new-vm'
import poolState from './modules/pool'
import selfState from './modules/self'
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'
// ===================================================================
export default angular.module('xoWebApp', [
uiBootstrap,
uiIndeterminate,
uiRouter,
uiSelect,
angularChartJs,
naturalSort,
xeditable,
xoDirectives,
xoFilters,
xoServices,
aboutState,
backupState,
consoleState,
dashboardState,
deleteVmsState,
genericModalState,
hostState,
listState,
migrateVmState,
navbarState,
newSrState,
newVmState,
poolState,
selfState,
settingsState,
srState,
taskScheduler,
treeState,
updater,
vmState
])
// Prevent Angular.js from mangling exception stack (interfere with
// source maps).
.factory('$exceptionHandler', () => function (exception) {
console.log(exception && exception.stack || exception)
})
.config(function (
$compileProvider,
$stateProvider,
$urlRouterProvider,
$tooltipProvider,
uiSelectConfig
) {
// Disable debug data to improve performance.
//
// In case of a bug, simply use `angular.reloadWithDebugInfo()` in
// the console.
//
// See https://docs.angularjs.org/guide/production
$compileProvider.debugInfoEnabled(false)
// Redirect to default state.
$stateProvider.state('index', {
url: '/',
controller: function ($state, xoApi) {
let isAdmin = xoApi.user && (xoApi.user.permission === 'admin')
$state.go(isAdmin ? 'tree' : 'list')
}
})
// Redirects unmatched URLs to `/`.
$urlRouterProvider.otherwise('/')
// Changes the default settings for the tooltips.
$tooltipProvider.options({
appendToBody: true,
placement: 'bottom'
})
uiSelectConfig.theme = 'bootstrap'
uiSelectConfig.resetSearchInput = true
})
.run(function (
$anchorScroll,
$cookies,
$rootScope,
$state,
editableOptions,
editableThemes,
modal,
notify,
updater,
xoApi
) {
// Milliseconds are not necessary.
const now = Math.floor(Date.now() / 1e3)
const oneWeekAgo = now - 7 * 24 * 3600
const previousDisclaimer = $cookies.get('previousDisclaimer')
if (
!previousDisclaimer ||
+previousDisclaimer < oneWeekAgo
) {
modal.alert({
title: 'Xen Orchestra from the sources',
htmlMessage: [
'You are using XO from the sources! That\'s great for a personal/non-profit usage.',
'If you are a company, it\'s better to use it with <a href="https://xen-orchestra.com/#!/xoa">XOA (turnkey appliance)</a> and our dedicated pro support!',
'This version is <strong>not bundled with any support nor updates</strong>. Use it with caution for critical tasks.'
].map(p => `<p>${p}</p>`).join('')
})
$cookies.put('previousDisclaimer', now)
}
let requestedStateName, requestedStateParams
$rootScope.$watch(() => xoApi.user, (user, previous) => {
// The user just signed in.
if (user && !previous) {
if (requestedStateName) {
$state.go(requestedStateName, requestedStateParams)
requestedStateName = requestedStateParams = null
} else {
$state.go('index')
}
}
})
$rootScope.$on('$stateChangeStart', function (event, state, stateParams, fromState) {
const { user } = xoApi
if (!user) {
event.preventDefault()
requestedStateName = state.name
requestedStateParams = stateParams
return
}
if (user.permission === 'admin') {
return
}
function forbidState () {
event.preventDefault()
notify.error({
title: 'Restricted area',
message: 'You do not have the permission to view this page'
})
if (fromState.url === '^') {
$state.go('index')
}
}
// Some pages requires the admin permission.
if (state.data && state.data.requireAdmin) {
forbidState()
return
}
const { id } = stateParams
if (id && !xoApi.canInteract(id, 'view')) {
forbidState()
return
}
})
// Work around UI Router bug (https://github.com/angular-ui/ui-router/issues/1509)
$rootScope.$on('$stateChangeSuccess', function () {
$anchorScroll()
})
editableThemes.bs3.inputClass = 'input-sm'
editableThemes.bs3.buttonsClass = 'btn-sm'
editableOptions.theme = 'bs3'
})
.name

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -1,61 +0,0 @@
//- HTML 5 Doctype
doctype html
//- The “no-js” class will be automatically removed if JavaScript is
//- available.
html.no-js(lang="en", dir="ltr")
head
meta(charset="utf-8")
//- This file is a part of Xen Orchestra Web.
//-
//- Xen Orchestra Web is free software: you can redistribute it and/or
//- modify it under the terms of the GNU Affero General Public License
//- as published by the Free Software Foundation, either version 3 of
//- the License, or (at your option) any later version.
//-
//- Xen Orchestra Web is distributed in the hope that it will be
//- useful, but WITHOUT ANY WARRANTY; without even the implied warranty
//- of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
//- Affero General Public License for more details.
//-
//- You should have received a copy of the GNU Affero General Public License
//- along with Xen Orchestra Web. If not, see
//- <http://www.gnu.org/licenses/>.
//-
//- @author Olivier Lambert <olivier.lambert@vates.fr>
//- @license http://www.gnu.org/licenses/agpl-3.0-standalone.html GNU AGPLv3
//-
//- @package Xen Orchestra Web
//- Makes sure IE is using the last engine available.
meta(http-equiv="X-UA-Compatible", content="IE=edge,chrome=1")
//- Replaces the “no-js” class by the “js” class if JavaScript is
//- available.
script.
!function(d){d.className=d.className.replace(/\\bno-js\b/,'js')}(document.documentElement)
//- (To confirm.) For smartphones and tablets: sets the page
//- width to the device width and prevents the page from being
//- zoomed in when going to landscape mode.
meta(name="viewport", content="width=device-width, initial-scale=1.0")
title Xen Orchestra
meta(name="description", content="Web interface for XenServer/XAPI Hosts")
meta(name="author", content="Vates SAS")
//- Place favicon.ico and apple-touch-icon.png in the root directory
link(rel="stylesheet", href="styles/main.css")
body(
ng-app = 'xoWebApp'
)
toaster-container
//- Navigation bar.
navbar
//- Main content (managed by the router).
.view-main(ui-view = "")
script(src="app.js")

View File

@@ -1,25 +0,0 @@
import angular from 'angular'
import uiRouter from 'angular-ui-router'
import pkg from '../../../package'
// ===================================================================
export default angular.module('xoWebApp.about', [
uiRouter
])
.config(function ($stateProvider) {
$stateProvider.state('about', {
url: '/about',
controller: 'AboutCtrl',
template: require('./view')
})
})
.controller('AboutCtrl', function ($scope, xo) {
xo.system.getServerVersion().then(version =>
$scope.serverVersion = version
)
$scope.pkg = pkg
})
// A module exports its name.
.name

View File

@@ -1,50 +0,0 @@
//- TODO: lots of stuff.
.grid-sm
.panel.panel-default
p.page-title About Xen Orchestra
p.text-center ({{pkg.name}} {{pkg.version}} - xo-server {{serverVersion}})
.grid-sm
//- Vates
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-lightbulb-o
| Vates
.panel-body
p.text-center
| We are the team behind Xen Orchestra, we are Vates! We create Open Source products and we offer commercial support for Xen and Xen Orchestra. Want to know more about us? Go to our website!
p.text-center
img(src="images/arrow.png")
br
p.text-center
a.btn.btn-success(href="https://vates.fr")
i.fa.fa-hand-o-right
| Our website
//- Open Source
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-thumbs-up
| Open Source
.panel-body
p.text-center
| This project is Open Source (AGPL), everyone is welcome aboard! You want a specific feature in XO? Report a bug? Go to our project website, read the FAQ and get involved in the project!
p.text-center
img(src="images/opensource.png")
br
p.text-center
a.btn.btn-info(href="https://xen-orchestra.com")
i.fa.fa-flask
| Project website
//- Pro support
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-truck
| Pro Support Delivered
.panel-body
p.text-center
| Our XO Appliance can be delivered with professional support: stay relaxed, we got your back! You can also have assitance for deploying or upgrade your virtualized infrastructure through our deep understanding of Xen.
p.text-center
img(src="images/support.png")
p.text-center
a.btn.btn-primary(href="https://vates.fr/services.html")
i.fa.fa-envelope
| Get services

View File

@@ -1,275 +0,0 @@
import angular from 'angular'
import filter from 'lodash.filter'
import find from 'lodash.find'
import forEach from 'lodash.foreach'
import map from 'lodash.map'
import prettyCron from 'prettycron'
import size from 'lodash.size'
import trim from 'lodash.trim'
import uiBootstrap from 'angular-ui-bootstrap'
import uiRouter from 'angular-ui-router'
import { parse } from 'xo-remote-parser'
import view from './view'
// ====================================================================
export default angular.module('backup.backup', [
uiRouter,
uiBootstrap
])
.config(function ($stateProvider) {
$stateProvider.state('backup.backup', {
url: '/backup/:id',
controller: 'BackupCtrl as ctrl',
template: view
})
})
.controller('BackupCtrl', function ($scope, $stateParams, $interval, xo, xoApi, notify, selectHighLevelFilter, filterFilter) {
const JOBKEY = 'rollingBackup'
this.ready = false
this.running = {}
this.comesForEditing = $stateParams.id
this.scheduleApi = {}
this.formData = {}
const refreshRemotes = () => {
const selectRemoteId = this.formData.remote && this.formData.remote.id
return xo.remote.getAll()
.then(remotes => {
const r = {}
forEach(remotes, remote => {
remote = parse(remote)
r[remote.id] = remote
})
this.remotes = r
if (selectRemoteId) {
this.formData.remote = this.remotes[selectRemoteId]
}
})
}
const refreshSchedules = () => {
return xo.schedule.getAll()
.then(schedules => {
const s = {}
forEach(schedules, schedule => {
this.jobs && this.jobs[schedule.job] && this.jobs[schedule.job].key === JOBKEY && (s[schedule.id] = schedule)
})
this.schedules = s
})
}
const refreshJobs = () => {
return xo.job.getAll()
.then(jobs => {
const j = {}
forEach(jobs, job => {
j[job.id] = job
})
this.jobs = j
})
}
const refresh = () => refreshRemotes().then(refreshJobs).then(refreshSchedules)
this.getReady = () => refresh().then(() => this.ready = true)
this.getReady()
const interval = $interval(refresh, 5e3)
$scope.$on('$destroy', () => $interval.cancel(interval))
const toggleState = (toggle, state) => {
const selectedVms = this.formData.selectedVms.slice()
if (toggle) {
const vms = filterFilter(selectHighLevelFilter(this.objects), {type: 'VM'})
forEach(vms, vm => {
if (vm.power_state === state) {
(selectedVms.indexOf(vm) === -1) && selectedVms.push(vm)
}
})
this.formData.selectedVms = selectedVms
} else {
const keptVms = []
for (let index in this.formData.selectedVms) {
if (this.formData.selectedVms[index].power_state !== state) {
keptVms.push(this.formData.selectedVms[index])
}
}
this.formData.selectedVms = keptVms
}
}
this.toggleAllRunning = toggle => toggleState(toggle, 'Running')
this.toggleAllHalted = toggle => toggleState(toggle, 'Halted')
this.edit = schedule => {
const vms = filterFilter(selectHighLevelFilter(this.objects), {type: 'VM'})
const job = this.jobs[schedule.job]
const selectedVms = []
forEach(job.paramsVector.items[0].values, value => {
const vm = find(vms, vm => vm.id === value.id)
vm && selectedVms.push(vm)
})
const tag = job.paramsVector.items[0].values[0].tag
const depth = job.paramsVector.items[0].values[0].depth
const _reportWhen = job.paramsVector.items[0].values[0]._reportWhen
const cronPattern = schedule.cron
const remoteId = job.paramsVector.items[0].values[0].remoteId
const onlyMetadata = job.paramsVector.items[0].values[0].onlyMetadata || false
let compress = job.paramsVector.items[0].values[0].compress
if (compress === undefined) {
compress = true // Default value
}
this.resetData()
this.formData.selectedVms = selectedVms
this.formData.tag = tag
this.formData.depth = depth
this.formData.scheduleId = schedule.id
this.formData._reportWhen = _reportWhen
this.formData.remote = this.remotes[remoteId]
this.formData.disableCompression = !compress
this.formData.onlyMetadata = onlyMetadata
this.scheduleApi.setCron(cronPattern)
}
this.save = (id, vms, remoteId, tag, depth, cron, enabled, onlyMetadata, disableCompression, _reportWhen) => {
if (!vms.length) {
notify.warning({
title: 'No Vms selected',
message: 'Choose VMs to backup'
})
return
}
const _save = (id === undefined) ? saveNew(vms, remoteId, tag, depth, cron, enabled, onlyMetadata, disableCompression, _reportWhen) : save(id, vms, remoteId, tag, depth, cron, onlyMetadata, disableCompression, _reportWhen)
return _save
.then(() => {
notify.info({
title: 'Backup',
message: 'Job schedule successfuly saved'
})
this.resetData()
})
.finally(refresh)
}
const save = (id, vms, remoteId, tag, depth, cron, onlyMetadata, disableCompression, _reportWhen) => {
const schedule = this.schedules[id]
const job = this.jobs[schedule.job]
const values = []
forEach(vms, vm => {
values.push({
id: vm.id,
remoteId,
tag,
depth,
onlyMetadata,
compress: !disableCompression,
_reportWhen
})
})
job.paramsVector.items[0].values = values
return xo.job.set(job)
.then(response => {
if (response) {
return xo.schedule.set(schedule.id, undefined, cron, undefined)
} else {
notify.error({
title: 'Update schedule',
message: 'Job updating failed'
})
throw new Error('Job updating failed')
}
})
}
const saveNew = (vms, remoteId, tag, depth, cron, enabled, onlyMetadata, disableCompression, _reportWhen) => {
const values = []
forEach(vms, vm => {
values.push({
id: vm.id,
remoteId,
tag,
depth,
onlyMetadata,
compress: !disableCompression,
_reportWhen
})
})
const job = {
type: 'call',
key: JOBKEY,
method: 'vm.rollingBackup',
paramsVector: {
type: 'crossProduct',
items: [{
type: 'set',
values
}]
}
}
return xo.job.create(job)
.then(jobId => xo.schedule.create(jobId, cron, enabled))
}
this.delete = schedule => {
let jobId = schedule.job
return xo.schedule.delete(schedule.id)
.then(() => xo.job.delete(jobId))
.finally(() => {
if (this.formData.scheduleId === schedule.id) {
this.resetData()
}
refresh()
})
}
this.run = schedule => {
this.running[schedule.id] = true
notify.info({
title: 'Run Job',
message: 'One shot running started. See overview for logs.'
})
const id = schedule.job
return xo.job.runSequence([id]).finally(() => delete this.running[schedule.id])
}
this.sanitizePath = (...paths) => (paths[0] && paths[0].charAt(0) === '/' && '/' || '') + filter(map(paths, s => s && filter(map(s.split('/'), trim)).join('/'))).join('/')
this.resetData = () => {
this.formData.allRunning = false
this.formData.allHalted = false
this.formData.selectedVms = []
this.formData.scheduleId = undefined
this.formData.tag = undefined
this.formData.path = undefined
this.formData.depth = undefined
this.formData.enabled = false
this.formData._reportWhen = undefined
this.formData.remote = undefined
this.formData.onlyMetadata = false
this.formData.disableCompression = false
this.scheduleApi && this.scheduleApi.resetData && this.scheduleApi.resetData()
}
this.size = size
this.prettyCron = prettyCron.toString.bind(prettyCron)
if (!this.comesForEditing) {
refresh()
} else {
refresh()
.then(() => {
this.edit(this.schedules[this.comesForEditing])
delete this.comesForEditing
})
}
this.resetData()
this.objects = xoApi.all
})
// A module exports its name.
.name

View File

@@ -1,154 +0,0 @@
.panel.panel-default
p.page-title
i.fa.fa-download(style="color: #e25440;")
| Backup
form#backupform(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.selectedVms, ctrl.formData.remote.id, ctrl.formData.tag, ctrl.formData.depth, ctrl.formData.cronPattern, ctrl.formData.enabled, ctrl.formData.onlyMetadata, ctrl.formData.disableCompression, ctrl.formData._reportWhen)')
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.xo-icon-vm
| VMs to backup
.panel-body.form-horizontal
.text-center(ng-if = '!ctrl.formData'): i.xo-icon-loading
.container-fluid(ng-if = 'ctrl.formData')
.alert.alert-info(ng-if = '!ctrl.formData.scheduleId') Creating New Backup
.alert.alert-warning(ng-if = 'ctrl.formData.scheduleId') Modifying Backup ID {{ ctrl.formData.scheduleId }}
.form-group
label.control-label.col-md-2(for = 'tag') Tag
.col-md-10
input#tag.form-control(form = 'backupform', ng-model = 'ctrl.formData.tag', placeholder = 'Back-up tag', required)
.form-group(ng-class = '{"has-warning": !ctrl.formData.selectedVms.length}')
label.control-label.col-md-2(for = 'vmlist') VMs
.col-md-8
ui-select(form = 'backupform', ng-model = 'ctrl.formData.selectedVms', multiple, close-on-select = 'false', required)
ui-select-match(placeholder = 'Choose VMs to backup')
i.xo-icon-working(ng-if="isVMWorking($item)")
i(class="xo-icon-{{$item.power_state | lowercase}}",ng-if="!isVMWorking($item)")
| {{$item.name_label}}
span(ng-if="$item.$container")
| ({{($item.$container | resolve).name_label}})
ui-select-choices(repeat = 'vm in ctrl.objects | selectHighLevel | filter:{type: "VM"} | filter:$select.search | orderBy:["$container", "name_label"] track by vm.id')
div
i.xo-icon-working(ng-if="isVMWorking(vm)", tooltip="{{vm.power_state}} and {{(vm.current_operations | map)[0]}}")
i(class="xo-icon-{{vm.power_state | lowercase}}",ng-if="!isVMWorking(vm)", tooltip="{{vm.power_state}}")
| {{vm.name_label}}
span(ng-if="vm.$container")
| ({{(vm.$container | resolve).name_label || ((vm.$container | resolve).master | resolve).name_label}})
.col-md-2
label(tooltip = 'select/deselect all running VMs', style = 'cursor: pointer')
input.hidden(form = 'backupform', type = 'checkbox', ng-model = 'ctrl.formData.allRunning', ng-change = 'ctrl.toggleAllRunning(ctrl.formData.allRunning)')
span.fa-stack
i.xo-icon-running.fa-stack-1x
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allRunning')
label(tooltip = 'select/deselect all halted VMs', style = 'cursor: pointer')
input.hidden(form = 'backupform', type = 'checkbox', ng-model = 'ctrl.formData.allHalted', ng-change = 'ctrl.toggleAllHalted(ctrl.formData.allHalted)')
span.fa-stack
i.xo-icon-halted.fa-stack-1x
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allHalted')
.form-group
label.control-label.col-md-2(for = 'depth') Depth
.col-md-10
input#depth.form-control(form = 'backupform', ng-model = 'ctrl.formData.depth', placeholder = 'How many backups to rollover', type = 'number', min = '1', required)
.form-group
label.control-label.col-md-2(for = 'remote') Remote
.col-md-10
select#remote.form-control(form = 'backupform', ng-options = 'remote.name group by remote.type for remote in ctrl.remotes | map | orderBy:["type","name"]', ng-model = 'ctrl.formData.remote' required)
option(value = ''): em -- Choose a file system remote point --
.form-group
.col-md-10.col-md-offset-2
a(ui-sref = 'backup.remote')
i.fa.fa-pencil
| &nbsp; Manage your remote stores
.form-group
label.control-label.col-md-2(for = 'onlyMetadata')
input#onlyMetadata(form = 'backupform', ng-model = 'ctrl.formData.onlyMetadata', type = 'checkbox')
.help-block.col-md-10 Only MetaData (no disks export)
.form-group
label.control-label.col-md-2(for = 'onlyMetadata')
input#disableCompression(form = 'backupform', ng-model = 'ctrl.formData.disableCompression', type = 'checkbox')
.help-block.col-md-10 Disable compression
.form-group(ng-if = '!ctrl.formData.scheduleId')
label.control-label.col-md-2(for = 'enabled')
input#enabled(form = 'backupform', ng-model = 'ctrl.formData.enabled', type = 'checkbox')
.help-block.col-md-10 Enable immediately after creation
.form-group
label.control-label.col-md-2(for = '_reportWhen') Report
.col-md-10
select.form-control(ng-model = 'ctrl.formData._reportWhen')
option(value = ''): em -- When to send reports --
option(value = 'never') Never
option(value = 'alway') Always
option(value = 'fail') Failure
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-clock-o
| Schedule
.panel-body.form-horizontal
.text-center(ng-if = '!ctrl.formData'): i.xo-icon-loading
xo-scheduler(data = 'ctrl.formData', api = 'ctrl.scheduleApi')
.grid-sm
.panel.panel-default
.panel-body
fieldset.center(ng-disabled = '!ctrl.ready')
button.btn.btn-lg.btn-primary(form = 'backupform', type = 'submit')
i.fa.fa-clock-o
| &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.resetData()')
| &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'): i.xo-icon-loading
.text-center(ng-if = 'ctrl.schedules && !ctrl.size(ctrl.schedules)') No scheduled jobs
table.table.table-hover(ng-if = 'ctrl.schedules && ctrl.size(ctrl.schedules)')
tr
th ID
th Tag
th.hidden-xs.hidden-sm VMs to backup
th.hidden-xs Remote
th.hidden-xs Depth
th.hidden-xs Scheduling
th.hidden-xs Only MetaData
th.hidden-xs Compression DISABLED
th Enabled now
th
tr(ng-repeat = 'schedule in ctrl.schedules | map | orderBy:"id":true track by schedule.id')
td {{ schedule.id }}
td {{ ctrl.jobs[schedule.job].paramsVector.items[0].values[0].tag }}
td.hidden-xs.hidden-sm
div(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values.length == 1')
| {{ (ctrl.jobs[schedule.job].paramsVector.items[0].values[0].id | resolve).name_label }}
div(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values.length > 1')
button.btn.btn-info(type = 'button', ng-click = 'unCollapsed = !unCollapsed')
| {{ ctrl.jobs[schedule.job].paramsVector.items[0].values.length }} VMs&nbsp;
i.fa(ng-class = '{"fa-chevron-down": !unCollapsed, "fa-chevron-up": unCollapsed}')
div(collapse = '!unCollapsed')
br
ul.list-group
li.list-group-item(ng-repeat = 'item in ctrl.jobs[schedule.job].paramsVector.items[0].values')
span(ng-if = 'item.id | resolve') {{ (item.id | resolve).name_label }}
span(ng-if = '(item.id | resolve).$container') &nbsp;({{ ((item.id | resolve).$container | resolve).name_label }})
td.hidden-xs
strong: a(ui-sref = 'backup.remote') {{ ctrl.remotes[ctrl.jobs[schedule.job].paramsVector.items[0].values[0].remoteId].name }}
td.hidden-xs {{ ctrl.jobs[schedule.job].paramsVector.items[0].values[0].depth }}
td.hidden-xs {{ ctrl.prettyCron(schedule.cron) }}
td.hidden-xs.text-center
i.fa.fa-check(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values[0].onlyMetadata')
td.hidden-xs.text-center
i.fa.fa-check(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values[0].compress === false')
td.text-center
i.fa.fa-check(ng-if = 'schedule.enabled')
td.text-right
button.btn.btn-primary(type = 'button', ng-click = 'ctrl.edit(schedule)'): i.fa.fa-pencil
| &nbsp;
button.btn.btn-warning(type = 'button', ng-click = 'ctrl.run(schedule)', ng-disabled = 'ctrl.running[schedule.id]'): i.fa.fa-play
| &nbsp;
button.btn.btn-danger(type = 'button', ng-click = 'ctrl.delete(schedule)'): i.fa.fa-trash-o

View File

@@ -1,225 +0,0 @@
import angular from 'angular'
import find from 'lodash.find'
import forEach from 'lodash.foreach'
import later from 'later'
import prettyCron from 'prettycron'
import uiBootstrap from 'angular-ui-bootstrap'
import uiRouter from 'angular-ui-router'
later.date.localTime()
import view from './view'
// ====================================================================
export default angular.module('backup.continuousReplication', [
uiRouter,
uiBootstrap
])
.config(function ($stateProvider) {
$stateProvider.state('backup.continuousReplication', {
url: '/continuous-replication/:id',
controller: 'ContinuousReplicationCtrl as ctrl',
template: view
})
})
.controller('ContinuousReplicationCtrl', function ($scope, $stateParams, $interval, xo, xoApi, notify, selectHighLevelFilter, filterFilter, bytesToSizeFilter) {
const JOBKEY = 'continuousReplication'
this.ready = false
this.running = {}
this.comesForEditing = $stateParams.id
this.scheduleApi = {}
this.formData = {}
const refreshSchedules = () => xo.schedule.getAll()
.then(schedules => {
const s = {}
forEach(schedules, schedule => {
this.jobs && this.jobs[schedule.job] && this.jobs[schedule.job].key === JOBKEY && (s[schedule.id] = schedule)
})
this.schedules = s
})
const refreshJobs = () => xo.job.getAll()
.then(jobs => {
const j = {}
forEach(jobs, job => {
j[job.id] = job
})
this.jobs = j
})
const refresh = () => refreshJobs().then(refreshSchedules)
const getReady = () => refresh().then(() => this.ready = true)
getReady()
const interval = $interval(refresh, 5e3)
$scope.$on('$destroy', () => $interval.cancel(interval))
const toggleState = (toggle, state) => {
const selectedVms = this.formData.selectedVms.slice()
if (toggle) {
const vms = filterFilter(selectHighLevelFilter(this.objects), {type: 'VM'})
forEach(vms, vm => {
if (vm.power_state === state) {
(selectedVms.indexOf(vm) === -1) && selectedVms.push(vm)
}
})
this.formData.selectedVms = selectedVms
} else {
const keptVms = []
for (let index in this.formData.selectedVms) {
if (this.formData.selectedVms[index].power_state !== state) {
keptVms.push(this.formData.selectedVms[index])
}
}
this.formData.selectedVms = keptVms
}
}
this.toggleAllRunning = toggle => toggleState(toggle, 'Running')
this.toggleAllHalted = toggle => toggleState(toggle, 'Halted')
this.edit = schedule => {
const vms = filterFilter(selectHighLevelFilter(this.objects), {type: 'VM'})
const job = this.jobs[schedule.job]
const selectedVms = []
forEach(job.paramsVector.items[0].values, value => {
const vm = find(vms, vm => vm.id === value.vm)
vm && selectedVms.push(vm)
})
const tag = job.paramsVector.items[0].values[0].tag
const selectedSr = xoApi.get(job.paramsVector.items[0].values[0].sr)
const _reportWhen = job.paramsVector.items[0].values[0]._reportWhen
const cronPattern = schedule.cron
this.resetData()
// const formData = this.formData
this.formData.selectedVms = selectedVms
this.formData.tag = tag
this.formData.selectedSr = selectedSr
this.formData.scheduleId = schedule.id
this.formData._reportWhen = _reportWhen
this.scheduleApi.setCron(cronPattern)
}
this.save = (id, vms, tag, sr, cron, enabled, _reportWhen) => {
if (!vms.length) {
notify.warning({
title: 'No Vms selected',
message: 'Choose VMs to copy'
})
return
}
const _save = (id === undefined) ? saveNew(vms, tag, sr, cron, enabled, _reportWhen) : save(id, vms, tag, sr, cron, _reportWhen)
return _save
.then(() => {
notify.info({
title: 'Continuous Replication',
message: 'Job schedule successfuly saved'
})
this.resetData()
})
.finally(refresh)
}
const save = (id, vms, tag, sr, cron, _reportWhen) => {
const schedule = this.schedules[id]
const job = this.jobs[schedule.job]
const values = []
forEach(vms, vm => {
values.push({vm: vm.id, tag, sr: sr.id, _reportWhen})
})
job.paramsVector.items[0].values = values
return xo.job.set(job)
.then(response => {
if (response) {
return xo.schedule.set(schedule.id, undefined, cron, undefined)
} else {
notify.error({
title: 'Update schedule',
message: 'Job updating failed'
})
throw new Error('Job updating failed')
}
})
}
const saveNew = (vms, tag, sr, cron, enabled, _reportWhen) => {
const values = []
forEach(vms, vm => {
values.push({vm: vm.id, tag, sr: sr.id, _reportWhen})
})
const job = {
type: 'call',
key: JOBKEY,
method: 'vm.deltaCopy',
paramsVector: {
type: 'crossProduct',
items: [{
type: 'set',
values
}]
}
}
return xo.job.create(job)
.then(jobId => xo.schedule.create(jobId, cron, enabled))
}
this.delete = schedule => {
let jobId = schedule.job
return xo.schedule.delete(schedule.id)
.then(() => xo.job.delete(jobId))
.finally(() => {
if (this.formData.scheduleId === schedule.id) {
this.resetData()
}
refresh()
})
}
this.run = schedule => {
this.running[schedule.id] = true
notify.info({
title: 'Run Job',
message: 'One shot running started. See overview for logs.'
})
const id = schedule.job
return xo.job.runSequence([id]).finally(() => delete this.running[schedule.id])
}
this.inTargetPool = vm => vm.$poolId === (this.formData.selectedSr && this.formData.selectedSr.$poolId)
this.resetData = () => {
this.formData.allRunning = false
this.formData.allHalted = false
this.formData.selectedVms = []
this.formData.scheduleId = undefined
this.formData.tag = undefined
this.formData.selectedSr = undefined
this.formData.enabled = false
this.formData._reportWhen = undefined
this.scheduleApi && this.scheduleApi.resetData && this.scheduleApi.resetData()
}
this.collectionLength = col => Object.keys(col).length
this.prettyCron = prettyCron.toString.bind(prettyCron)
if (!this.comesForEditing) {
refresh()
} else {
refresh()
.then(() => {
this.edit(this.schedules[this.comesForEditing])
delete this.comesForEditing
})
}
this.resetData()
this.objects = xoApi.all
})
// A module exports its name.
.name

View File

@@ -1,143 +0,0 @@
.panel.panel-default
p.page-title
i.fa.fa-map-signs(style="color: #e25440;")
| Continuous Replication
form#ciform(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.selectedVms, ctrl.formData.tag, ctrl.formData.selectedSr, ctrl.formData.cronPattern, ctrl.formData.enabled, ctrl.formData._reportWhen)')
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.xo-icon-vm(style='color: #e25440;')
| VMs to copy
.panel-body.form-horizontal
.text-center(ng-if = '!ctrl.formData'): i.xo-icon-loading
.container-fluid(ng-if = 'ctrl.formData')
.alert.alert-info(ng-if = '!ctrl.formData.scheduleId') Creating New Continuous Replication
.alert.alert-warning(ng-if = 'ctrl.formData.scheduleId') Modifying Continuous Replication ID {{ ctrl.formData.scheduleId }}
.form-group
label.control-label.col-md-2(for = 'tag') Tag
.col-md-10
input#tag.form-control(form = 'ciform', ng-model = 'ctrl.formData.tag', placeholder = 'VM copy tag', required)
.form-group(ng-class = '{"has-warning": !ctrl.formData.selectedVms.length}')
label.control-label.col-md-2(for = 'vmlist') VMs
.col-md-8
ui-select#vmlist(form = 'ciform', ng-model = 'ctrl.formData.selectedVms', multiple, close-on-select = 'false', required)
ui-select-match(placeholder = 'Choose VMs to copy')
span(ng-class = '{"bg-danger": ctrl.inTargetPool($item)}')
i.xo-icon-working(ng-if="isVMWorking($item)")
i(class="xo-icon-{{$item.power_state | lowercase}}",ng-if="!isVMWorking($item)")
| {{$item.name_label}}
span(ng-if="$item.$container")
| ({{($item.$container | resolve).name_label}})
ui-select-choices(repeat = 'vm in ctrl.objects | selectHighLevel | filter:{type: "VM"} | filter:$select.search | orderBy:["$container", "name_label"] track by vm.id')
div
i.xo-icon-working(ng-if="isVMWorking(vm)", tooltip="{{vm.power_state}} and {{(vm.current_operations | map)[0]}}")
i(class="xo-icon-{{vm.power_state | lowercase}}",ng-if="!isVMWorking(vm)", tooltip="{{vm.power_state}}")
| {{vm.name_label}}
span(ng-if="vm.$container")
| ({{(vm.$container | resolve).name_label || ((vm.$container | resolve).master | resolve).name_label}})
.col-md-2
label(tooltip = 'select/deselect all running VMs', style = 'cursor: pointer')
input.hidden(form = 'ciform', type = 'checkbox', ng-model = 'ctrl.formData.allRunning', ng-change = 'ctrl.toggleAllRunning(ctrl.formData.allRunning)')
span.fa-stack
i.xo-icon-running.fa-stack-1x
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allRunning')
label(tooltip = 'select/deselect all halted VMs', style = 'cursor: pointer')
input.hidden(form = 'ciform', type = 'checkbox', ng-model = 'ctrl.formData.allHalted', ng-change = 'ctrl.toggleAllHalted(ctrl.formData.allHalted)')
span.fa-stack
i.xo-icon-halted.fa-stack-1x
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allHalted')
.form-group(ng-if = '(ctrl.formData.selectedVms | filter:ctrl.inTargetPool).length')
.col-md-offset-2.col-md-10
.alert.alert-warning
i.fa.fa-exclamation-triangle
| &nbsp;At the moment, the selected VMs displayed in red are in the copy target pool.
.form-group
label.control-label.col-md-2(for = 'sr') To SR
.col-md-10
ui-select#sr(form = 'ciform', ng-model = 'ctrl.formData.selectedSr', required)
ui-select-match(placeholder = 'Choose destination SR')
i(class="xo-icon-sr")
| {{$select.selected.name_label}}
span(ng-if="$select.selected.$container")
| ({{($select.selected.$container | resolve).name_label}})
ui-select-choices(repeat = 'sr in ctrl.objects | selectHighLevel | filter:{type: "sr", content_type: "!iso"} | filter:$select.search | orderBy:["$container", "name_label"] track by sr.id')
div
i(class="xo-icon-sr")
| {{sr.name_label}} ({{sr.size - sr.physical_usage | bytesToSize }})
span(ng-if="sr.$container")
| ({{(sr.$container | resolve).name_label || ((sr.$container | resolve).master | resolve).name_label}})
.form-group(ng-if = '!ctrl.formData.scheduleId')
label.control-label.col-md-2(for = 'enabled')
input#enabled(form = 'ciform', ng-model = 'ctrl.formData.enabled', type = 'checkbox')
.help-block.col-md-8 Enable immediately after creation
.form-group
label.control-label.col-md-2(for = '_reportWhen') Report
.col-md-10
select.form-control(ng-model = 'ctrl.formData._reportWhen')
option(value = ''): em -- When to send reports --
option(value = 'never') Never
option(value = 'alway') Always
option(value = 'fail') Failure
.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 = 'ciform', 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.resetData()')
| &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'): i.xo-icon-loading
.text-center(ng-if = 'ctrl.schedules && !ctrl.collectionLength(ctrl.schedules)') No scheduled jobs
table.table.table-hover(ng-if = 'ctrl.schedules && ctrl.collectionLength(ctrl.schedules)')
tr
th ID
th Tag
th.hidden-xs.hidden-sm VMs to Copy
th.hidden-xs To SR
th.hidden-xs Scheduling
th Enabled now
th
tr(ng-repeat = 'schedule in ctrl.schedules | map | orderBy:"id":true track by schedule.id')
td {{ schedule.id }}
td {{ ctrl.jobs[schedule.job].paramsVector.items[0].values[0].tag }}
td.hidden-xs.hidden-sm
div(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values.length == 1')
| {{ (ctrl.jobs[schedule.job].paramsVector.items[0].values[0].vm | resolve).name_label }}
div(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values.length > 1')
button.btn.btn-info(type = 'button', ng-click = 'unCollapsed = !unCollapsed')
| {{ ctrl.jobs[schedule.job].paramsVector.items[0].values.length }} VMs&nbsp;
i.fa(ng-class = '{"fa-chevron-down": !unCollapsed, "fa-chevron-up": unCollapsed}')
div(collapse = '!unCollapsed')
ul.list-group
li.list-group-item(ng-repeat = 'item in ctrl.jobs[schedule.job].paramsVector.items[0].values')
span(ng-if = 'item.vm | resolve') {{ (item.vm | resolve).name_label }}
span(ng-if = '(item.vm | resolve).$container') &nbsp;({{ ((item.vm | resolve).$container | resolve).name_label }})
td.hidden-xs {{ (ctrl.jobs[schedule.job].paramsVector.items[0].values[0].sr | resolve).name_label }}
td.hidden-xs {{ ctrl.prettyCron(schedule.cron) }}
td.text-center
i.fa.fa-check(ng-if = 'schedule.enabled')
td.text-right
button.btn.btn-primary(type = 'button', ng-click = 'ctrl.edit(schedule)'): i.fa.fa-pencil
| &nbsp;
button.btn.btn-warning(type = 'button', ng-click = 'ctrl.run(schedule)', ng-disabled = 'ctrl.running[schedule.id]'): i.fa.fa-play
| &nbsp;
button.btn.btn-danger(type = 'button', ng-click = 'ctrl.delete(schedule)'): i.fa.fa-trash-o

View File

@@ -1,262 +0,0 @@
import angular from 'angular'
import filter from 'lodash.filter'
import find from 'lodash.find'
import forEach from 'lodash.foreach'
import map from 'lodash.map'
import prettyCron from 'prettycron'
import size from 'lodash.size'
import trim from 'lodash.trim'
import uiBootstrap from 'angular-ui-bootstrap'
import uiRouter from 'angular-ui-router'
import { parse } from 'xo-remote-parser'
import view from './view'
// ====================================================================
export default angular.module('backup.deltaBackup', [
uiRouter,
uiBootstrap
])
.config(function ($stateProvider) {
$stateProvider.state('backup.deltaBackup', {
url: '/delta-backup/:id',
controller: 'DeltaBackupCtrl as ctrl',
template: view
})
})
.controller('DeltaBackupCtrl', function ($scope, $stateParams, $interval, xo, xoApi, notify, selectHighLevelFilter, filterFilter) {
const JOBKEY = 'deltaBackup'
this.ready = false
this.running = {}
this.comesForEditing = $stateParams.id
this.scheduleApi = {}
this.formData = {}
const refreshRemotes = () => {
const selectRemoteId = this.formData.remote && this.formData.remote.id
return xo.remote.getAll()
.then(remotes => {
const r = {}
forEach(remotes, remote => {
remote = parse(remote)
r[remote.id] = remote
})
this.remotes = r
if (selectRemoteId) {
this.formData.remote = this.remotes[selectRemoteId]
}
})
}
const refreshSchedules = () => {
return xo.schedule.getAll()
.then(schedules => {
const s = {}
forEach(schedules, schedule => {
this.jobs && this.jobs[schedule.job] && this.jobs[schedule.job].key === JOBKEY && (s[schedule.id] = schedule)
})
this.schedules = s
})
}
const refreshJobs = () => {
return xo.job.getAll()
.then(jobs => {
const j = {}
forEach(jobs, job => {
j[job.id] = job
})
this.jobs = j
})
}
const refresh = () => refreshRemotes().then(refreshJobs).then(refreshSchedules)
this.getReady = () => refresh().then(() => this.ready = true)
this.getReady()
const interval = $interval(refresh, 5e3)
$scope.$on('$destroy', () => $interval.cancel(interval))
const toggleState = (toggle, state) => {
const selectedVms = this.formData.selectedVms.slice()
if (toggle) {
const vms = filterFilter(selectHighLevelFilter(this.objects), {type: 'VM'})
forEach(vms, vm => {
if (vm.power_state === state) {
(selectedVms.indexOf(vm) === -1) && selectedVms.push(vm)
}
})
this.formData.selectedVms = selectedVms
} else {
const keptVms = []
for (let index in this.formData.selectedVms) {
if (this.formData.selectedVms[index].power_state !== state) {
keptVms.push(this.formData.selectedVms[index])
}
}
this.formData.selectedVms = keptVms
}
}
this.toggleAllRunning = toggle => toggleState(toggle, 'Running')
this.toggleAllHalted = toggle => toggleState(toggle, 'Halted')
this.edit = schedule => {
const vms = filterFilter(selectHighLevelFilter(this.objects), {type: 'VM'})
const job = this.jobs[schedule.job]
const selectedVms = []
forEach(job.paramsVector.items[0].values, value => {
const vm = find(vms, vm => vm.id === value.vm)
vm && selectedVms.push(vm)
})
const tag = job.paramsVector.items[0].values[0].tag
const depth = job.paramsVector.items[0].values[0].depth
const _reportWhen = job.paramsVector.items[0].values[0]._reportWhen
const cronPattern = schedule.cron
const remoteId = job.paramsVector.items[0].values[0].remote
this.resetData()
this.formData.selectedVms = selectedVms
this.formData.tag = tag
this.formData.depth = depth
this.formData.scheduleId = schedule.id
this.formData._reportWhen = _reportWhen
this.formData.remote = this.remotes[remoteId]
this.scheduleApi.setCron(cronPattern)
}
this.save = (id, vms, remoteId, tag, depth, cron, enabled, _reportWhen) => {
if (!vms.length) {
notify.warning({
title: 'No Vms selected',
message: 'Choose VMs to backup'
})
return
}
const _save = (id === undefined) ? saveNew(vms, remoteId, tag, depth, cron, enabled, _reportWhen) : save(id, vms, remoteId, tag, depth, cron, _reportWhen)
return _save
.then(() => {
notify.info({
title: 'Backup',
message: 'Job schedule successfuly saved'
})
this.resetData()
})
.finally(refresh)
}
const save = (id, vms, remoteId, tag, depth, cron, _reportWhen) => {
const schedule = this.schedules[id]
const job = this.jobs[schedule.job]
const values = []
forEach(vms, vm => {
values.push({
vm: vm.id,
remote: remoteId,
tag,
depth,
_reportWhen
})
})
job.paramsVector.items[0].values = values
return xo.job.set(job)
.then(response => {
if (response) {
return xo.schedule.set(schedule.id, undefined, cron, undefined)
} else {
notify.error({
title: 'Update schedule',
message: 'Job updating failed'
})
throw new Error('Job updating failed')
}
})
}
const saveNew = (vms, remoteId, tag, depth, cron, enabled, _reportWhen) => {
const values = []
forEach(vms, vm => {
values.push({
vm: vm.id,
remote: remoteId,
tag,
depth,
_reportWhen
})
})
const job = {
type: 'call',
key: JOBKEY,
method: 'vm.rollingDeltaBackup',
paramsVector: {
type: 'crossProduct',
items: [{
type: 'set',
values
}]
}
}
return xo.job.create(job)
.then(jobId => xo.schedule.create(jobId, cron, enabled))
}
this.delete = schedule => {
let jobId = schedule.job
return xo.schedule.delete(schedule.id)
.then(() => xo.job.delete(jobId))
.finally(() => {
if (this.formData.scheduleId === schedule.id) {
this.resetData()
}
refresh()
})
}
this.run = schedule => {
this.running[schedule.id] = true
notify.info({
title: 'Run Job',
message: 'One shot running started. See overview for logs.'
})
const id = schedule.job
return xo.job.runSequence([id]).finally(() => delete this.running[schedule.id])
}
this.sanitizePath = (...paths) => (paths[0] && paths[0].charAt(0) === '/' && '/' || '') + filter(map(paths, s => s && filter(map(s.split('/'), trim)).join('/'))).join('/')
this.resetData = () => {
this.formData.allRunning = false
this.formData.allHalted = false
this.formData.selectedVms = []
this.formData.scheduleId = undefined
this.formData.tag = undefined
this.formData.path = undefined
this.formData.depth = undefined
this.formData.enabled = false
this.formData._reportWhen = undefined
this.formData.remote = undefined
this.scheduleApi && this.scheduleApi.resetData && this.scheduleApi.resetData()
}
this.size = size
this.prettyCron = prettyCron.toString.bind(prettyCron)
if (!this.comesForEditing) {
refresh()
} else {
refresh()
.then(() => {
this.edit(this.schedules[this.comesForEditing])
delete this.comesForEditing
})
}
this.resetData()
this.objects = xoApi.all
})
// A module exports its name.
.name

View File

@@ -1,140 +0,0 @@
.panel.panel-default
p.page-title
i.fa.fa-download(style="color: #e25440;")
| Delta Backup
form#backupform(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.selectedVms, ctrl.formData.remote.id, ctrl.formData.tag, ctrl.formData.depth, ctrl.formData.cronPattern, ctrl.formData.enabled, ctrl.formData._reportWhen)')
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.xo-icon-vm
| VMs to backup
.panel-body.form-horizontal
.text-center(ng-if = '!ctrl.formData'): i.xo-icon-loading
.container-fluid(ng-if = 'ctrl.formData')
.alert.alert-info(ng-if = '!ctrl.formData.scheduleId') Creating New Backup
.alert.alert-warning(ng-if = 'ctrl.formData.scheduleId') Modifying Backup ID {{ ctrl.formData.scheduleId }}
.form-group
label.control-label.col-md-2(for = 'tag') Tag
.col-md-10
input#tag.form-control(form = 'backupform', ng-model = 'ctrl.formData.tag', placeholder = 'Back-up tag', required)
.form-group(ng-class = '{"has-warning": !ctrl.formData.selectedVms.length}')
label.control-label.col-md-2(for = 'vmlist') VMs
.col-md-8
ui-select(form = 'backupform', ng-model = 'ctrl.formData.selectedVms', multiple, close-on-select = 'false', required)
ui-select-match(placeholder = 'Choose VMs to backup')
i.xo-icon-working(ng-if="isVMWorking($item)")
i(class="xo-icon-{{$item.power_state | lowercase}}",ng-if="!isVMWorking($item)")
| {{$item.name_label}}
span(ng-if="$item.$container")
| ({{($item.$container | resolve).name_label}})
ui-select-choices(repeat = 'vm in ctrl.objects | selectHighLevel | filter:{type: "VM"} | filter:$select.search | orderBy:["$container", "name_label"] track by vm.id')
div
i.xo-icon-working(ng-if="isVMWorking(vm)", tooltip="{{vm.power_state}} and {{(vm.current_operations | map)[0]}}")
i(class="xo-icon-{{vm.power_state | lowercase}}",ng-if="!isVMWorking(vm)", tooltip="{{vm.power_state}}")
| {{vm.name_label}}
span(ng-if="vm.$container")
| ({{(vm.$container | resolve).name_label || ((vm.$container | resolve).master | resolve).name_label}})
.col-md-2
label(tooltip = 'select/deselect all running VMs', style = 'cursor: pointer')
input.hidden(form = 'backupform', type = 'checkbox', ng-model = 'ctrl.formData.allRunning', ng-change = 'ctrl.toggleAllRunning(ctrl.formData.allRunning)')
span.fa-stack
i.xo-icon-running.fa-stack-1x
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allRunning')
label(tooltip = 'select/deselect all halted VMs', style = 'cursor: pointer')
input.hidden(form = 'backupform', type = 'checkbox', ng-model = 'ctrl.formData.allHalted', ng-change = 'ctrl.toggleAllHalted(ctrl.formData.allHalted)')
span.fa-stack
i.xo-icon-halted.fa-stack-1x
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allHalted')
.form-group
label.control-label.col-md-2(for = 'depth') Depth
.col-md-10
input#depth.form-control(form = 'backupform', ng-model = 'ctrl.formData.depth', placeholder = 'How many backups to rollover', type = 'number', min = '1', required)
.form-group
label.control-label.col-md-2(for = 'remote') Remote
.col-md-10
select#remote.form-control(form = 'backupform', ng-options = 'remote.name group by remote.type for remote in ctrl.remotes | map | orderBy:["type","name"]', ng-model = 'ctrl.formData.remote' required)
option(value = ''): em -- Choose a file system remote point --
.form-group
.col-md-10.col-md-offset-2
a(ui-sref = 'backup.remote')
i.fa.fa-pencil
| &nbsp; Manage your remote stores
.form-group(ng-if = '!ctrl.formData.scheduleId')
label.control-label.col-md-2(for = 'enabled')
input#enabled(form = 'backupform', ng-model = 'ctrl.formData.enabled', type = 'checkbox')
.help-block.col-md-10 Enable immediately after creation
.form-group
label.control-label.col-md-2(for = '_reportWhen') Report
.col-md-10
select.form-control(ng-model = 'ctrl.formData._reportWhen')
option(value = ''): em -- When to send reports --
option(value = 'never') Never
option(value = 'alway') Always
option(value = 'fail') Failure
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-clock-o
| Schedule
.panel-body.form-horizontal
.text-center(ng-if = '!ctrl.formData'): i.xo-icon-loading
xo-scheduler(data = 'ctrl.formData', api = 'ctrl.scheduleApi')
.grid-sm
.panel.panel-default
.panel-body
fieldset.center(ng-disabled = '!ctrl.ready')
button.btn.btn-lg.btn-primary(form = 'backupform', type = 'submit')
i.fa.fa-clock-o
| &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.resetData()')
| &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'): i.xo-icon-loading
.text-center(ng-if = 'ctrl.schedules && !ctrl.size(ctrl.schedules)') No scheduled jobs
table.table.table-hover(ng-if = 'ctrl.schedules && ctrl.size(ctrl.schedules)')
tr
th ID
th Tag
th.hidden-xs.hidden-sm VMs to backup
th.hidden-xs Remote
th.hidden-xs Depth
th.hidden-xs Scheduling
th Enabled now
th
tr(ng-repeat = 'schedule in ctrl.schedules | map | orderBy:"id":true track by schedule.id')
td {{ schedule.id }}
td {{ ctrl.jobs[schedule.job].paramsVector.items[0].values[0].tag }}
td.hidden-xs.hidden-sm
div(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values.length == 1')
| {{ (ctrl.jobs[schedule.job].paramsVector.items[0].values[0].vm | resolve).name_label }}
div(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values.length > 1')
button.btn.btn-info(type = 'button', ng-click = 'unCollapsed = !unCollapsed')
| {{ ctrl.jobs[schedule.job].paramsVector.items[0].values.length }} VMs&nbsp;
i.fa(ng-class = '{"fa-chevron-down": !unCollapsed, "fa-chevron-up": unCollapsed}')
div(collapse = '!unCollapsed')
br
ul.list-group
li.list-group-item(ng-repeat = 'item in ctrl.jobs[schedule.job].paramsVector.items[0].values')
span(ng-if = 'item.vm | resolve') {{ (item.vm | resolve).name_label }}
span(ng-if = '(item.vm | resolve).$container') &nbsp;({{ ((item.vm | resolve).$container | resolve).name_label }})
td.hidden-xs
strong: a(ui-sref = 'backup.remote') {{ ctrl.remotes[ctrl.jobs[schedule.job].paramsVector.items[0].values[0].remote].name }}
td.hidden-xs {{ ctrl.jobs[schedule.job].paramsVector.items[0].values[0].depth }}
td.hidden-xs {{ ctrl.prettyCron(schedule.cron) }}
td.text-center
i.fa.fa-check(ng-if = 'schedule.enabled')
td.text-right
button.btn.btn-primary(type = 'button', ng-click = 'ctrl.edit(schedule)'): i.fa.fa-pencil
| &nbsp;
button.btn.btn-warning(type = 'button', ng-click = 'ctrl.run(schedule)', ng-disabled = 'ctrl.running[schedule.id]'): i.fa.fa-play
| &nbsp;
button.btn.btn-danger(type = 'button', ng-click = 'ctrl.delete(schedule)'): i.fa.fa-trash-o

View File

@@ -1,227 +0,0 @@
import angular from 'angular'
import find from 'lodash.find'
import forEach from 'lodash.foreach'
import later from 'later'
import prettyCron from 'prettycron'
import uiBootstrap from 'angular-ui-bootstrap'
import uiRouter from 'angular-ui-router'
later.date.localTime()
import view from './view'
// ====================================================================
export default angular.module('backup.disasterrecovery', [
uiRouter,
uiBootstrap
])
.config(function ($stateProvider) {
$stateProvider.state('backup.disasterrecovery', {
url: '/disasterrecovery/:id',
controller: 'DisasterRecoveryCtrl as ctrl',
template: view
})
})
.controller('DisasterRecoveryCtrl', function ($scope, $stateParams, $interval, xo, xoApi, notify, selectHighLevelFilter, filterFilter) {
const JOBKEY = 'disasterRecovery'
this.ready = false
this.running = {}
this.comesForEditing = $stateParams.id
this.scheduleApi = {}
this.formData = {}
const refreshSchedules = () => xo.schedule.getAll()
.then(schedules => {
const s = {}
forEach(schedules, schedule => {
this.jobs && this.jobs[schedule.job] && this.jobs[schedule.job].key === JOBKEY && (s[schedule.id] = schedule)
})
this.schedules = s
})
const refreshJobs = () => xo.job.getAll()
.then(jobs => {
const j = {}
forEach(jobs, job => {
j[job.id] = job
})
this.jobs = j
})
const refresh = () => refreshJobs().then(refreshSchedules)
const getReady = () => refresh().then(() => this.ready = true)
getReady()
const interval = $interval(refresh, 5e3)
$scope.$on('$destroy', () => $interval.cancel(interval))
const toggleState = (toggle, state) => {
const selectedVms = this.formData.selectedVms.slice()
if (toggle) {
const vms = filterFilter(selectHighLevelFilter(this.objects), {type: 'VM'})
forEach(vms, vm => {
if (vm.power_state === state) {
(selectedVms.indexOf(vm) === -1) && selectedVms.push(vm)
}
})
this.formData.selectedVms = selectedVms
} else {
const keptVms = []
for (let index in this.formData.selectedVms) {
if (this.formData.selectedVms[index].power_state !== state) {
keptVms.push(this.formData.selectedVms[index])
}
}
this.formData.selectedVms = keptVms
}
}
this.toggleAllRunning = toggle => toggleState(toggle, 'Running')
this.toggleAllHalted = toggle => toggleState(toggle, 'Halted')
this.edit = schedule => {
const vms = filterFilter(selectHighLevelFilter(this.objects), {type: 'VM'})
const job = this.jobs[schedule.job]
const selectedVms = []
forEach(job.paramsVector.items[0].values, value => {
const vm = find(vms, vm => vm.id === value.id)
vm && selectedVms.push(vm)
})
const tag = job.paramsVector.items[0].values[0].tag
const selectedPool = xoApi.get(job.paramsVector.items[0].values[0].pool)
const depth = job.paramsVector.items[0].values[0].depth
const _reportWhen = job.paramsVector.items[0].values[0]._reportWhen
const cronPattern = schedule.cron
this.resetData()
// const formData = this.formData
this.formData.selectedVms = selectedVms
this.formData.tag = tag
this.formData.selectedPool = selectedPool
this.formData.depth = depth
this.formData.scheduleId = schedule.id
this.formData._reportWhen = _reportWhen
this.scheduleApi.setCron(cronPattern)
}
this.save = (id, vms, tag, pool, depth, cron, enabled, _reportWhen) => {
if (!vms.length) {
notify.warning({
title: 'No Vms selected',
message: 'Choose VMs to copy'
})
return
}
const _save = (id === undefined) ? saveNew(vms, tag, pool, depth, cron, enabled, _reportWhen) : save(id, vms, tag, pool, depth, cron, _reportWhen)
return _save
.then(() => {
notify.info({
title: 'Disaster Recovery',
message: 'Job schedule successfuly saved'
})
this.resetData()
})
.finally(refresh)
}
const save = (id, vms, tag, pool, depth, cron, _reportWhen) => {
const schedule = this.schedules[id]
const job = this.jobs[schedule.job]
const values = []
forEach(vms, vm => {
values.push({id: vm.id, tag, pool: pool.id, depth, _reportWhen})
})
job.paramsVector.items[0].values = values
return xo.job.set(job)
.then(response => {
if (response) {
return xo.schedule.set(schedule.id, undefined, cron, undefined)
} else {
notify.error({
title: 'Update schedule',
message: 'Job updating failed'
})
throw new Error('Job updating failed')
}
})
}
const saveNew = (vms, tag, pool, depth, cron, enabled, _reportWhen) => {
const values = []
forEach(vms, vm => {
values.push({id: vm.id, tag, pool: pool.id, depth, _reportWhen})
})
const job = {
type: 'call',
key: JOBKEY,
method: 'vm.rollingDrCopy',
paramsVector: {
type: 'crossProduct',
items: [{
type: 'set',
values
}]
}
}
return xo.job.create(job)
.then(jobId => xo.schedule.create(jobId, cron, enabled))
}
this.delete = schedule => {
let jobId = schedule.job
return xo.schedule.delete(schedule.id)
.then(() => xo.job.delete(jobId))
.finally(() => {
if (this.formData.scheduleId === schedule.id) {
this.resetData()
}
refresh()
})
}
this.run = schedule => {
this.running[schedule.id] = true
notify.info({
title: 'Run Job',
message: 'One shot running started. See overview for logs.'
})
const id = schedule.job
return xo.job.runSequence([id]).finally(() => delete this.running[schedule.id])
}
this.inTargetPool = vm => vm.$poolId === (this.formData.selectedPool && this.formData.selectedPool.id)
this.resetData = () => {
this.formData.allRunning = false
this.formData.allHalted = false
this.formData.selectedVms = []
this.formData.scheduleId = undefined
this.formData.tag = undefined
this.formData.selectedPool = undefined
this.formData.depth = undefined
this.formData.enabled = false
this.formData._reportWhen = undefined
this.scheduleApi && this.scheduleApi.resetData && this.scheduleApi.resetData()
}
this.collectionLength = col => Object.keys(col).length
this.prettyCron = prettyCron.toString.bind(prettyCron)
if (!this.comesForEditing) {
refresh()
} else {
refresh()
.then(() => {
this.edit(this.schedules[this.comesForEditing])
delete this.comesForEditing
})
}
this.resetData()
this.objects = xoApi.all
})
// A module exports its name.
.name

View File

@@ -1,153 +0,0 @@
.panel.panel-default
p.page-title
i.fa.fa-medkit(style="color: #e25440;")
| Disaster Recovery
form#drform(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.selectedVms, ctrl.formData.tag, ctrl.formData.selectedPool, ctrl.formData.depth, ctrl.formData.cronPattern, ctrl.formData.enabled, ctrl.formData._reportWhen)')
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.xo-icon-vm(style='color: #e25440;')
| VMs to copy
.panel-body.form-horizontal
.text-center(ng-if = '!ctrl.formData'): i.xo-icon-loading
.container-fluid(ng-if = 'ctrl.formData')
.alert.alert-info(ng-if = '!ctrl.formData.scheduleId') Creating New Disaster Recovery
.alert.alert-warning(ng-if = 'ctrl.formData.scheduleId') Modifying Disaster Recovery ID {{ ctrl.formData.scheduleId }}
.form-group
label.control-label.col-md-2(for = 'tag') Tag
.col-md-10
.input-group
span.input-group-addon DR_
input#tag.form-control(form = 'drform', ng-model = 'ctrl.formData.tag', placeholder = 'VM copy tag', required)
.form-group(ng-class = '{"has-warning": !ctrl.formData.selectedVms.length}')
label.control-label.col-md-2(for = 'vmlist') VMs
.col-md-8
ui-select#vmlist(form = 'drform', ng-model = 'ctrl.formData.selectedVms', multiple, close-on-select = 'false', required)
ui-select-match(placeholder = 'Choose VMs to copy')
span(ng-class = '{"bg-danger": ctrl.inTargetPool($item)}')
i.xo-icon-working(ng-if="isVMWorking($item)")
i(class="xo-icon-{{$item.power_state | lowercase}}",ng-if="!isVMWorking($item)")
| {{$item.name_label}}
span(ng-if="$item.$container")
| ({{($item.$container | resolve).name_label}})
ui-select-choices(repeat = 'vm in ctrl.objects | selectHighLevel | filter:{type: "VM"} | filter:$select.search | orderBy:["$container", "name_label"] track by vm.id')
div
i.xo-icon-working(ng-if="isVMWorking(vm)", tooltip="{{vm.power_state}} and {{(vm.current_operations | map)[0]}}")
i(class="xo-icon-{{vm.power_state | lowercase}}",ng-if="!isVMWorking(vm)", tooltip="{{vm.power_state}}")
| {{vm.name_label}}
span(ng-if="vm.$container")
| ({{(vm.$container | resolve).name_label || ((vm.$container | resolve).master | resolve).name_label}})
.col-md-2
label(tooltip = 'select/deselect all running VMs', style = 'cursor: pointer')
input.hidden(form = 'drform', type = 'checkbox', ng-model = 'ctrl.formData.allRunning', ng-change = 'ctrl.toggleAllRunning(ctrl.formData.allRunning)')
span.fa-stack
i.xo-icon-running.fa-stack-1x
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allRunning')
label(tooltip = 'select/deselect all halted VMs', style = 'cursor: pointer')
input.hidden(form = 'drform', type = 'checkbox', ng-model = 'ctrl.formData.allHalted', ng-change = 'ctrl.toggleAllHalted(ctrl.formData.allHalted)')
span.fa-stack
i.xo-icon-halted.fa-stack-1x
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allHalted')
.form-group(ng-if = '(ctrl.formData.selectedVms | filter:ctrl.inTargetPool).length')
.col-md-offset-2.col-md-10
.alert.alert-warning
i.fa.fa-exclamation-triangle
| &nbsp;At the moment, the selected VMs displayed in red are in the copy target pool.
.form-group
label.control-label.col-md-2(for = 'pool') To Pool
.col-md-10
ui-select#pool(form = 'drform', ng-model = 'ctrl.formData.selectedPool', required)
ui-select-match(placeholder = 'Choose destination pool')
i(class="xo-icon-pool")
| {{$select.selected.name_label}}
span(ng-if="$select.selected.$container")
| ({{($select.selected.$container | resolve).name_label}})
ui-select-choices(repeat = 'pool in ctrl.objects | selectHighLevel | filter:{type: "pool"} | filter:$select.search | orderBy:["$container", "name_label"] track by pool.id')
div
i(class="xo-icon-pool")
| {{pool.name_label}}
span(ng-if="pool.$container")
| ({{(pool.$container | resolve).name_label || ((pool.$container | resolve).master | resolve).name_label}})
.form-group
label.control-label.col-md-2(for = 'depth') Depth
.col-md-10
input#depth.form-control(form = 'drform', ng-model = 'ctrl.formData.depth', placeholder = 'How many VM copies to rollover', type = 'number', min = '1', required)
.form-group(ng-if = '!ctrl.formData.scheduleId')
label.control-label.col-md-2(for = 'enabled')
input#enabled(form = 'drform', ng-model = 'ctrl.formData.enabled', type = 'checkbox')
.help-block.col-md-8 Enable immediately after creation
.form-group
label.control-label.col-md-2(for = '_reportWhen') Report
.col-md-10
select.form-control(ng-model = 'ctrl.formData._reportWhen')
option(value = ''): em -- When to send reports --
option(value = 'never') Never
option(value = 'alway') Always
option(value = 'fail') Failure
.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 = 'drform', 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.resetData()')
| &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'): i.xo-icon-loading
.text-center(ng-if = 'ctrl.schedules && !ctrl.collectionLength(ctrl.schedules)') No scheduled jobs
table.table.table-hover(ng-if = 'ctrl.schedules && ctrl.collectionLength(ctrl.schedules)')
tr
th ID
th Tag
th.hidden-xs.hidden-sm VMs to Copy
th.hidden-xs To Pool
th.hidden-xs Depth
th.hidden-xs Scheduling
th Enabled now
th
tr(ng-repeat = 'schedule in ctrl.schedules | map | orderBy:"id":true track by schedule.id')
td {{ schedule.id }}
td
span.label.label-default DR_
| {{ ctrl.jobs[schedule.job].paramsVector.items[0].values[0].tag }}
td.hidden-xs.hidden-sm
div(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values.length == 1')
| {{ (ctrl.jobs[schedule.job].paramsVector.items[0].values[0].id | resolve).name_label }}
div(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values.length > 1')
button.btn.btn-info(type = 'button', ng-click = 'unCollapsed = !unCollapsed')
| {{ ctrl.jobs[schedule.job].paramsVector.items[0].values.length }} VMs&nbsp;
i.fa(ng-class = '{"fa-chevron-down": !unCollapsed, "fa-chevron-up": unCollapsed}')
div(collapse = '!unCollapsed')
ul.list-group
li.list-group-item(ng-repeat = 'item in ctrl.jobs[schedule.job].paramsVector.items[0].values')
span(ng-if = 'item.id | resolve') {{ (item.id | resolve).name_label }}
span(ng-if = '(item.id | resolve).$container') &nbsp;({{ ((item.id | resolve).$container | resolve).name_label }})
td.hidden-xs {{ (ctrl.jobs[schedule.job].paramsVector.items[0].values[0].pool | resolve).name_label }}
td.hidden-xs {{ ctrl.jobs[schedule.job].paramsVector.items[0].values[0].depth }}
td.hidden-xs {{ ctrl.prettyCron(schedule.cron) }}
td.text-center
i.fa.fa-check(ng-if = 'schedule.enabled')
td.text-right
button.btn.btn-primary(type = 'button', ng-click = 'ctrl.edit(schedule)'): i.fa.fa-pencil
| &nbsp;
button.btn.btn-warning(type = 'button', ng-click = 'ctrl.run(schedule)', ng-disabled = 'ctrl.running[schedule.id]'): i.fa.fa-play
| &nbsp;
button.btn.btn-danger(type = 'button', ng-click = 'ctrl.delete(schedule)'): i.fa.fa-trash-o

View File

@@ -1,51 +0,0 @@
import angular from 'angular'
import later from 'later'
import scheduler from 'scheduler'
import uiRouter from 'angular-ui-router'
later.date.localTime()
import backup from './backup'
import continuousReplication from './continuous-replication'
import deltaBackup from './delta-backup'
import disasterRecovery from './disaster-recovery'
import management from './management'
import mount from './remote'
import restore from './restore'
import rollingSnapshot from './rolling-snapshot'
import view from './view'
export default angular.module('backup', [
uiRouter,
backup,
continuousReplication,
deltaBackup,
disasterRecovery,
management,
mount,
restore,
rollingSnapshot,
scheduler
])
.config(function ($stateProvider) {
$stateProvider.state('backup', {
abstract: true,
data: {
requireAdmin: true
},
template: view,
url: '/backup'
})
// Redirect to default sub-state.
$stateProvider.state('backup.index', {
url: '',
controller: function ($state) {
$state.go('backup.management')
}
})
})
.name

View File

@@ -1,231 +0,0 @@
import angular from 'angular'
import filter from 'lodash.filter'
import find from 'lodash.find'
import forEach from 'lodash.foreach'
import map from 'lodash.map'
import prettyCron from 'prettycron'
import uiBootstrap from 'angular-ui-bootstrap'
import uiRouter from 'angular-ui-router'
import parse from 'xo-remote-parser'
import view from './view'
// ====================================================================
export default angular.module('backup.management', [
uiRouter,
uiBootstrap
])
.config(function ($stateProvider) {
$stateProvider.state('backup.management', {
url: '/management',
controller: 'ManagementCtrl as ctrl',
template: view
})
})
.controller('ManagementCtrl', function (
$interval,
$scope,
$state,
$stateParams,
filterFilter,
modal,
notify,
selectHighLevelFilter,
xo,
xoApi
) {
this.running = {}
const mapJobKeyToState = {
continuousReplication: 'continuousReplication',
deltaBackup: 'deltaBackup',
disasterRecovery: 'disasterrecovery',
rollingBackup: 'backup',
rollingSnapshot: 'rollingsnapshot',
__none: 'index'
}
const mapJobKeyToJobDisplay = {
continuousReplication: 'Continuous Replication',
deltaBackup: 'Delta Backup',
disasterRecovery: 'Disaster Recovery',
rollingBackup: 'Backup',
rollingSnapshot: 'Rolling Snapshot',
__none: '[unknown]'
}
this.currentLogPage = 1
this.logPageSize = 10
const refreshSchedules = () => {
xo.schedule.getAll()
.then(schedules => {
schedules = filter(schedules, schedule => this.jobs[schedule.job] && this.jobs[schedule.job].key in mapJobKeyToState)
this.schedules = this.schedules ? map(schedules, schedule => {
schedule.error = find(this.schedules, oldSchedule => schedule.id === oldSchedule.id).error
return schedule
}) : schedules
})
xo.scheduler.getScheduleTable()
.then(table => this.scheduleTable = table)
xo.remote.getAll()
.then(remotes => {
this.backUpRemotes = map(remotes, parse)
forEach(this.schedules, schedule => {
const jobRemote = this.jobs[schedule.job].paramsVector.items[0].values[0]
const key = this.jobs[schedule.job].key
// TODO: Why is the property either 'remote' or 'remoteId'?
const remoteId = jobRemote.remoteId || jobRemote.remote
const remote = find(remotes, remote => remote.id === remoteId)
schedule.error = (!remote || !remote.enabled) && key !== 'continuousReplication' && key !== 'disasterRecovery' && key !== 'rollingSnapshot'
})
})
}
const getLogs = () => {
xo.logs.get('jobs').then(logs => {
const viewLogs = {}
const logsToClear = []
forEach(logs, (log, logKey) => {
const data = log.data
const [time] = logKey.split(':')
if (data.event === 'job.start' && data.key in mapJobKeyToState) {
logsToClear.push(logKey)
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
}
logsToClear.push(logKey)
if (data.event === 'job.end') {
if (data.error) {
entry.error = data.error
}
entry.end = time
entry.duration = time - entry.start
entry.status = 'Finished'
} 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
this.logsToClear = logsToClear
})
}
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.clearLogs = () => {
modal.confirm({
title: 'Clear logs',
message: 'Are you sure you want to delete all logs ?'
})
.then(() => xo.logs.delete('jobs', this.logsToClear))
}
this.enable = id => {
this.working[id] = true
return xo.scheduler.enable(id)
.finally(() => { this.working[id] = false })
.then(refreshSchedules)
}
this.disable = id => {
this.working[id] = true
return xo.scheduler.disable(id)
.finally(() => { this.working[id] = false })
.then(refreshSchedules)
}
this.run = schedule => {
this.running[schedule.id] = true
notify.info({
title: 'Run Job',
message: 'One shot running started. See overview for logs.'
})
const id = schedule.job
return xo.job.runSequence([id]).finally(() => delete this.running[schedule.id])
}
this.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 = {}
})
// A module exports its name.
.name

View File

@@ -1,90 +0,0 @@
.panel.panel-default
p.page-title
i.fa.fa-eye(style="color: #e25440;")
| Backup 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 Job
th Tag
th.hidden-xs Scheduling
th State
tr(ng-repeat = 'schedule in ctrl.schedules | orderBy:"id":true track by schedule.id')
td {{ ctrl.displayJobKey(schedule) }}
td: a(ui-sref = 'backup.{{ctrl.resolveJobKey(schedule)}}({id: schedule.id})') {{ ctrl.resolveScheduleJobTag(schedule) }}
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.btn-danger(ui-sref = 'backup.remote' type = 'button' ng-if = 'schedule.error'): i.fa.fa-exclamation-triangle
| &nbsp;
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
| &nbsp;
button.btn.btn-warning(type = 'button', ng-click = 'ctrl.run(schedule)', ng-disabled = 'ctrl.running[schedule.id]'): i.fa.fa-play
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-file-text
| Logs
span.quick-edit(ng-if = 'ctrl.logs | isNotEmpty', tooltip = 'Remove all logs', xo-click = 'ctrl.clearLogs()')
i.fa.fa-trash-o.fa-fw
.panel-body
table.table.table-hover(ng-if = 'ctrl.logs')
thead
tr
th Job ID
th Job
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 {{ ctrl.displayLogKey(log) }}
td {{ log.start | date:'medium' }}
td {{ log.end | date:'medium' }}
td {{ log.duration | duration}}
td
span(ng-if = 'log.status === "Finished"')
span.label(ng-class = '{"label-success": (!log.error && !log.hasErrors), "label-danger": (log.error || log.hasErrors)}') {{ log.status }}
span.label(ng-if = 'log.status !== "Finished"', 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 = '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;
span(ng-if = 'call.returnedValue')
| &nbsp;
i.text-primary.fa.fa-arrow-right
| &nbsp;{{ call.returnedValue }}
span.text-danger(ng-if = 'call.error')
| &nbsp;
i.fa.fa-times
| &nbsp;{{ 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

@@ -1,58 +0,0 @@
import angular from 'angular'
import map from 'lodash.map'
import size from 'lodash.size'
import uiBootstrap from 'angular-ui-bootstrap'
import uiRouter from 'angular-ui-router'
import {format, parse} from 'xo-remote-parser'
import view from './view'
// ====================================================================
export default angular.module('backup.remote', [
uiRouter,
uiBootstrap
])
.config(function ($stateProvider) {
$stateProvider.state('backup.remote', {
url: '/remote',
controller: 'RemoteCtrl as ctrl',
template: view
})
})
.controller('RemoteCtrl', function ($scope, $state, $stateParams, $interval, xo, xoApi, notify, selectHighLevelFilter, filterFilter) {
this.ready = false
const refresh = () => {
return xo.remote.getAll()
.then(remotes => this.backUpRemotes = map(remotes, parse))
}
this.getReady = () => {
return refresh()
.then(() => this.ready = true)
}
this.getReady()
const interval = $interval(refresh, 5e3)
$scope.$on('$destroy', () => {
$interval.cancel(interval)
})
this.prepareUrl = (type, host, path, username, password, domain) => format({type, host, path, username, password, domain})
const reset = () => {
this.path = this.host = this.name = undefined
this.remoteType = 'file'
}
this.add = (name, url) => xo.remote.create(name, url).then(reset).then(refresh)
this.remove = id => xo.remote.delete(id).then(refresh)
this.enable = id => xo.remote.set(id, undefined, undefined, true).then(refresh)
this.disable = id => xo.remote.set(id, undefined, undefined, false).then(refresh)
this.size = size
reset()
})
// A module exports its name.
.name

View File

@@ -1,143 +0,0 @@
.grid-sm
.panel.panel-default
p.page-title
i.fa.fa-plug(style="color: #e25440;")
| Remotes stores for backup
.grid-sm
.panel.panel-default
.panel-body
//- {{ ctrl.backUpRemotes }} {{ ctrl.size(ctrl.backUpRemotes) }}
.text-center(ng-if = '!ctrl.size(ctrl.backUpRemotes)') No remotes
table.table.table-hover(ng-if = 'ctrl.size(ctrl.backUpRemotes)')
tbody(ng-if = '(ctrl.backUpRemotes | filter:{type:"local"}).length')
tr
th.text-info Local
th Name
th Path
th
th State
th Error
th
tr(ng-repeat = 'remote in ctrl.backUpRemotes | filter:{type:"local"} | orderBy:["name"] track by remote.id')
td
td {{ remote.name }}
td {{ remote.path }}
td
td
span(ng-if = 'remote.enabled')
span.text-success
| Accessible&nbsp;
i.fa.fa-check
//- button.btn.btn-warning.pull-right(type = 'button', ng-click = 'ctrl.disable(remote.id)'): i.fa.fa-chain-broken
span(ng-if = '!remote.enabled')
span.text-muted Unaccessible&nbsp;
button.btn.btn-primary.pull-right(type = 'button', ng-click = 'ctrl.enable(remote.id)'): i.fa.fa-link
td: span.text-muted {{ remote.error }}
td: button.btn.btn-danger.pull-right(type = 'button', ng-click = 'ctrl.remove(remote.id)'): i.fa.fa-trash
tbody(ng-if = '(ctrl.backUpRemotes | filter:{type:"nfs"}).length')
tr
th.text-info NFS
th Name
th Device
th
th State
th Error
th
tr(ng-repeat = 'remote in ctrl.backUpRemotes | filter:{type:"nfs"} | orderBy:["name"] track by remote.id')
td
td {{ remote.name }}
td {{ remote.host }}:{{ remote.share }}
td
td
span(ng-if = 'remote.enabled')
span.text-success
| Mounted&nbsp;
i.fa.fa-check
button.btn.btn-warning.pull-right(type = 'button', ng-click = 'ctrl.disable(remote.id)'): i.fa.fa-chain-broken
span(ng-if = '!remote.enabled')
span.text-muted Unmounted&nbsp;
button.btn.btn-primary.pull-right(type = 'button', ng-click = 'ctrl.enable(remote.id)'): i.fa.fa-link
td: span.text-muted {{ remote.error }}
td: button.btn.btn-danger.pull-right(type = 'button', ng-click = 'ctrl.remove(remote.id)'): i.fa.fa-trash
tbody(ng-if = '(ctrl.backUpRemotes | filter:{type:"smb"}).length')
tr
th.text-info SMB
th Name
th Share
th Auth
th State
th Error
th
tr(ng-repeat = 'remote in ctrl.backUpRemotes | filter:{type:"smb"} | orderBy:["name"] track by remote.id')
td
td {{ remote.name }}
td
strong.text-info &bsol;&bsol;
| {{ remote.host }}
strong.text-info &bsol;
| {{ remote.path }}
td {{ remote.username }}@{{remote.domain}}
td
span(ng-if = 'remote.enabled')
span.text-success
| Accessible&nbsp;
i.fa.fa-check
button.btn.btn-warning.pull-right(type = 'button', ng-click = 'ctrl.disable(remote.id)'): i.fa.fa-chain-broken
span(ng-if = '!remote.enabled')
span.text-muted Unaccessible&nbsp;
button.btn.btn-primary.pull-right(type = 'button', ng-click = 'ctrl.enable(remote.id)'): i.fa.fa-link
td: span.text-muted {{ remote.error }}
td: button.btn.btn-danger.pull-right(type = 'button', ng-click = 'ctrl.remove(remote.id)'): i.fa.fa-trash
form(ng-submit = 'ctrl.add(ctrl.name, ctrl.prepareUrl(ctrl.remoteType, ctrl.host, ctrl.path, ctrl.username, ctrl.password, ctrl.domain))')
fieldset
legend New File System Remote
.form-inline
.form-group
label.sr-only Type
select.form-control(ng-model = 'ctrl.remoteType')
option(value = 'file') Local
option(value = 'nfs') NFS
option(value = 'smb') SMB
| &nbsp;
.form-group
label.sr-only Name
input.form-control(type = 'text', ng-model = 'ctrl.name', placeholder = 'Name', required)
| &nbsp;
br
.form-inline
.form-group(ng-if = 'ctrl.remoteType === "nfs"')
label.sr-only Host
input.form-control(type = 'text', ng-model = 'ctrl.host', placeholder = 'host', required)
strong &nbsp;:&nbsp;
.input-group(ng-if = 'ctrl.remoteType !== "smb"')
span.input-group-addon /
label.sr-only Path
input.form-control(type = 'text', ng-model = 'ctrl.path', placeholder = 'path/to/backup')
.form-group(ng-if = 'ctrl.remoteType === "smb"')
.input-group
span.input-group-addon &bsol;&bsol;
label.sr-only Share
input.form-control(type = 'text', ng-model = 'ctrl.host', placeholder = 'share', required)
.input-group
span.input-group-addon &bsol;
label.sr-only Path
input.form-control(type = 'text', ng-model = 'ctrl.path', placeholder != 'path&bsol;to&bsol;backup')
br
.form-inline(ng-if = 'ctrl.remoteType === "smb"')
.form-group
label.sr-only User Name
input.form-control(type = 'text', ng-model = 'ctrl.username', placeholder = 'username', required)
| &nbsp;
.form-group
label.sr-only Password
input.form-control(type = 'password', ng-model = 'ctrl.password', placeholder = 'password', required)
| &nbsp;
.form-group
label.sr-only Domain
input.form-control(type = 'text', ng-model = 'ctrl.domain', placeholder = 'domain', required)
br
br
.form-group
button.btn.btn-primary(type = 'submit', ng-disabled = '!ctrl.ready')
| Save&nbsp;
i.fa.fa-floppy-o

View File

@@ -1,118 +0,0 @@
import angular from 'angular'
import filter from 'lodash.filter'
import find from 'lodash.find'
import forEach from 'lodash.foreach'
import size from 'lodash.size'
import uiBootstrap from 'angular-ui-bootstrap'
import uiRouter from 'angular-ui-router'
import view from './view'
// ====================================================================
export default angular.module('backup.restore', [
uiRouter,
uiBootstrap
])
.config(function ($stateProvider) {
$stateProvider.state('backup.restore', {
url: '/restore',
controller: 'RestoreCtrl as ctrl',
template: view
})
})
.controller('RestoreCtrl', function ($scope, $interval, xo, xoApi, notify, bytesToSizeFilter) {
this.loaded = {}
const srs = xoApi.getView('SRs').all
this.bytesToSize = bytesToSizeFilter
this.isEmpty = backups => backups && !(Object.keys(backups.delta) || backups.other.length)
this.size = size
const refresh = () => {
return xo.remote.getAll()
.then(remotes => {
forEach(this.backUpRemotes, remote => {
if (remote.backups) {
const freshRemote = find(remotes, {id: remote.id})
freshRemote && (freshRemote.backups = remote.backups)
}
})
this.backUpRemotes = remotes
this.writable_SRs = filter(srs, (sr) => sr.content_type !== 'iso')
})
}
refresh()
const interval = $interval(refresh, 5e3)
$scope.$on('$destroy', () => {
$interval.cancel(interval)
})
const deltaBuilder = (backups, uuid, name, tag, value) => {
let deltaBackup = backups[uuid]
? backups[uuid]
: backups[uuid] = {}
deltaBackup = deltaBackup[name]
? deltaBackup[name]
: deltaBackup[name] = {}
deltaBackup = deltaBackup[tag]
? deltaBackup[tag]
: deltaBackup[tag] = []
deltaBackup.push(value)
}
this.list = id => {
return xo.remote.list(id)
.then(files => {
const remote = find(this.backUpRemotes, {id})
if (remote) {
const backups = remote.backups = {
delta: {},
other: []
}
forEach(files, file => {
const arr = /^vm_delta_(.*)_([^\/]+)\/([^_]+)_(.*)$/.exec(file)
if (arr) {
const [ , tag, uuid, date, name ] = arr
const value = {
path: file,
date
}
deltaBuilder(backups.delta, uuid, name, tag, value)
} else {
backups.other.push(file)
}
})
}
this.loaded[remote.id] = true
})
}
const notification = {
title: 'VM import started',
message: 'Starting the VM import'
}
this.importBackup = (id, path, sr) => {
notify.info(notification)
return xo.vm.importBackup(id, path, sr)
}
this.importDeltaBackup = (id, path, sr) => {
notify.info(notification)
return xo.vm.importDeltaBackup(id, path, sr)
}
})
// A module exports its name.
.name

View File

@@ -1,68 +0,0 @@
.grid-sm
.panel.panel-default
p.page-title
i.fa.fa-upload(style="color: #e25440;")
| Backup Restore
.grid-sm
.panel.panel-default
.panel-body
.text-center(ng-if = '!ctrl.size(ctrl.backUpRemotes)') No remotes
.panel.panel-default(ng-repeat = 'remote in ctrl.backUpRemotes | orderBy:["name"] track by remote.id')
.panel-body(ng-if = '!remote.enabled || remote.error', ng-class = '{"bg-danger": remote.error, "bg-muted": !remote.error}')
a(ui-sref = 'backup.remote') {{ remote.name }}
span(ng-if = 'remote.error') &nbsp;(on error)
span(ng-if = '!remote.error') &nbsp;(disabled)
.panel-body(ng-if = 'remote.enabled')
.row
.col-sm-2
p
| {{ remote.name }}&nbsp;
button.btn.btn-default.pull-right(type = 'button', ng-click = 'ctrl.list(remote.id)'): i.fa(ng-class = '{"fa-eye": !ctrl.loaded[remote.id], "fa-refresh": ctrl.loaded[remote.id]}')
br
br
.col-sm-10
div(ng-if = 'ctrl.loaded[remote.id] && ctrl.isEmpty(remote.backups)') No backups available
div(ng-if = 'ctrl.size(remote.backups.delta)')
div(ng-repeat = '(uuid, backups) in remote.backups.delta')
.row
.col-sm-2
| {{ uuid }}
.col-sm-10
div(ng-repeat = '(name, backups) in backups')
.row
.col-sm-2
| {{ name }}
.col-sm-10
div(ng-repeat = '(tag, backups) in backups')
.row
.col-sm-2
| {{ tag }}
.col-sm-10
div(ng-repeat = 'backup in backups')
| {{ backup.date | date:'medium' }}
span.pull-right.dropdown(dropdown)
button.btn.btn-default(type = 'button', dropdown-toggle)
| Import&nbsp;
span.caret
ul.dropdown-menu(role="menu")
li(ng-repeat = 'sr in ctrl.writable_SRs | orderBy:natural("name_label") track by sr.id')
a(xo-click = "ctrl.importDeltaBackup(remote.id, backup.path, sr.id)")
i.xo-icon-host.fa-fw
| To {{sr.name_label}} ({{sr.size - sr.physical_usage | bytesToSize }})
span &nbsp;{{ (sr.$container | resolve).name_label }}
hr
hr
div(ng-if = 'ctrl.size(remote.backups.other)')
div(ng-repeat = 'backup in remote.backups.other')
| {{ backup }}
span.pull-right.dropdown(dropdown)
button.btn.btn-default(type = 'button', dropdown-toggle)
| Import&nbsp;
span.caret
ul.dropdown-menu(role="menu")
li(ng-repeat = 'sr in ctrl.writable_SRs | orderBy:natural("name_label") track by sr.id')
a(xo-click = "ctrl.importBackup(remote.id, backup, sr.id)")
i.xo-icon-host.fa-fw
| To {{sr.name_label}} ({{sr.size - sr.physical_usage | bytesToSize }})
span &nbsp;{{ (sr.$container | resolve).name_label }}
hr

View File

@@ -1,247 +0,0 @@
import angular from 'angular'
import find from 'lodash.find'
import forEach from 'lodash.foreach'
import later from 'later'
import prettyCron from 'prettycron'
import uiBootstrap from 'angular-ui-bootstrap'
import uiRouter from 'angular-ui-router'
later.date.localTime()
import view from './view'
// ====================================================================
export default angular.module('backup.rollingSnapshot', [
uiRouter,
uiBootstrap
])
.config(function ($stateProvider) {
$stateProvider.state('backup.rollingsnapshot', {
url: '/rollingsnapshot/:id',
controller: 'RollingSnapshotCtrl as ctrl',
template: view
})
})
.controller('RollingSnapshotCtrl', function ($scope, $stateParams, $interval, xo, xoApi, notify, selectHighLevelFilter, filterFilter) {
const JOBKEY = 'rollingSnapshot'
this.ready = false
this.running = {}
this.comesForEditing = $stateParams.id
this.scheduleApi = {}
this.formData = {}
const refreshSchedules = () => {
return xo.schedule.getAll()
.then(schedules => {
const s = {}
forEach(schedules, schedule => {
this.jobs && this.jobs[schedule.job] && this.jobs[schedule.job].key === JOBKEY && (s[schedule.id] = schedule)
})
this.schedules = s
})
}
const refreshJobs = () => {
return xo.job.getAll()
.then(jobs => {
const j = {}
forEach(jobs, job => j[job.id] = job)
this.jobs = j
})
}
const refresh = () => {
return refreshJobs().then(refreshSchedules)
}
this.getReady = () => refresh().then(() => this.ready = true)
this.getReady()
const interval = $interval(() => {
refresh()
}, 5e3)
$scope.$on('$destroy', () => {
$interval.cancel(interval)
})
const toggleState = (toggle, state) => {
const selectedVms = this.formData.selectedVms.slice()
if (toggle) {
const vms = filterFilter(selectHighLevelFilter(this.objects), {type: 'VM'})
forEach(vms, vm => {
if (vm.power_state === state) {
(selectedVms.indexOf(vm) === -1) && selectedVms.push(vm)
}
})
this.formData.selectedVms = selectedVms
} else {
const keptVms = []
for (let index in this.formData.selectedVms) {
if (this.formData.selectedVms[index].power_state !== state) {
keptVms.push(this.formData.selectedVms[index])
}
}
this.formData.selectedVms = keptVms
}
}
this.toggleAllRunning = toggle => toggleState(toggle, 'Running')
this.toggleAllHalted = toggle => toggleState(toggle, 'Halted')
this.edit = schedule => {
const vms = filterFilter(selectHighLevelFilter(this.objects), {type: 'VM'})
const job = this.jobs[schedule.job]
const selectedVms = []
forEach(job.paramsVector.items[0].values, value => {
const vm = find(vms, vm => vm.id === value.id)
vm && selectedVms.push(vm)
})
const tag = job.paramsVector.items[0].values[0].tag
const depth = job.paramsVector.items[0].values[0].depth
const _reportWhen = job.paramsVector.items[0].values[0]._reportWhen
const cronPattern = schedule.cron
this.resetData()
// const formData = this.formData
this.formData.selectedVms = selectedVms
this.formData.tag = tag
this.formData.depth = depth
this.formData._reportWhen = _reportWhen
this.formData.scheduleId = schedule.id
this.scheduleApi.setCron(cronPattern)
}
this.save = (id, vms, tag, depth, cron, enabled, _reportWhen) => {
if (!vms.length) {
notify.warning({
title: 'No Vms selected',
message: 'Choose VMs to snapshot'
})
return
}
const _save = (id === undefined) ? saveNew(vms, tag, depth, cron, enabled, _reportWhen) : save(id, vms, tag, depth, cron, _reportWhen)
return _save
.then(() => {
notify.info({
title: 'Rolling snapshot',
message: 'Job schedule successfuly saved'
})
this.resetData()
})
.finally(() => {
refresh()
})
}
const save = (id, vms, tag, depth, cron, _reportWhen) => {
const schedule = this.schedules[id]
const job = this.jobs[schedule.job]
const values = []
forEach(vms, vm => {
values.push({
id: vm.id,
tag,
depth,
_reportWhen
})
})
job.paramsVector.items[0].values = values
return xo.job.set(job)
.then(response => {
if (response) {
return xo.schedule.set(schedule.id, undefined, cron, undefined)
} else {
notify.error({
title: 'Update schedule',
message: 'Job updating failed'
})
throw new Error('Job updating failed')
}
})
}
const saveNew = (vms, tag, depth, cron, enabled, _reportWhen) => {
const values = []
forEach(vms, vm => {
values.push({
id: vm.id,
tag,
depth,
_reportWhen
})
})
const job = {
type: 'call',
key: JOBKEY,
method: 'vm.rollingSnapshot',
paramsVector: {
type: 'crossProduct',
items: [
{
type: 'set',
values
}
]
}
}
return xo.job.create(job)
.then(jobId => {
return xo.schedule.create(jobId, cron, enabled)
})
}
this.delete = schedule => {
let jobId = schedule.job
return xo.schedule.delete(schedule.id)
.then(() => xo.job.delete(jobId))
.finally(() => {
if (this.formData.scheduleId === schedule.id) {
this.resetData()
}
refresh()
})
}
this.run = schedule => {
this.running[schedule.id] = true
notify.info({
title: 'Run Job',
message: 'One shot running started. See overview for logs.'
})
const id = schedule.job
return xo.job.runSequence([id]).finally(() => delete this.running[schedule.id])
}
this.resetData = () => {
this.formData.allRunning = false
this.formData.allHalted = false
this.formData.selectedVms = []
this.formData.scheduleId = undefined
this.formData.tag = undefined
this.formData.depth = undefined
this.formData.enabled = false
this.formData._reportWhen = undefined
this.scheduleApi && this.scheduleApi.resetData && this.scheduleApi.resetData()
}
this.collectionLength = col => Object.keys(col).length
this.prettyCron = prettyCron.toString.bind(prettyCron)
if (!this.comesForEditing) {
refresh()
} else {
refresh()
.then(() => {
this.edit(this.schedules[this.comesForEditing])
delete this.comesForEditing
})
}
this.resetData()
this.objects = xoApi.all
})
// A module exports its name.
.name

View File

@@ -1,127 +0,0 @@
.panel.panel-default
p.page-title
i.xo-icon-snapshot(style="color: #e25440;")
| Rolling snapshots
form#snapform(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.selectedVms, ctrl.formData.tag, ctrl.formData.depth, ctrl.formData.cronPattern, ctrl.formData.enabled, ctrl.formData._reportWhen)')
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.xo-icon-vm(style='color: #e25440;')
| VMs to snapshot
.panel-body.form-horizontal
.text-center(ng-if = '!ctrl.formData'): i.xo-icon-loading
.container-fluid(ng-if = 'ctrl.formData')
.alert.alert-info(ng-if = '!ctrl.formData.scheduleId') Creating New Rolling Snapshot
.alert.alert-warning(ng-if = 'ctrl.formData.scheduleId') Modifying Rolling Snapshot ID {{ ctrl.formData.scheduleId }}
.form-group
label.control-label.col-md-2(for = 'tag') Tag
.col-md-10
input#tag.form-control(form = 'snapform', ng-model = 'ctrl.formData.tag', placeholder = 'Rolling snapshot tag', required)
.form-group(ng-class = '{"has-warning": !ctrl.formData.selectedVms.length}')
label.control-label.col-md-2(for = 'vmlist') VMs
.col-md-8
ui-select(form = 'snapform', ng-model = 'ctrl.formData.selectedVms', multiple, close-on-select = 'false', required)
ui-select-match(placeholder = 'Choose VMs to snapshot')
i.xo-icon-working(ng-if="isVMWorking($item)")
i(class="xo-icon-{{$item.power_state | lowercase}}",ng-if="!isVMWorking($item)")
| {{$item.name_label}}
span(ng-if="$item.$container")
| ({{($item.$container | resolve).name_label}})
ui-select-choices(repeat = 'vm in ctrl.objects | selectHighLevel | filter:{type: "VM"} | filter:$select.search | orderBy:["$container", "name_label"] track by vm.id')
div
i.xo-icon-working(ng-if="isVMWorking(vm)", tooltip="{{vm.power_state}} and {{(vm.current_operations | map)[0]}}")
i(class="xo-icon-{{vm.power_state | lowercase}}",ng-if="!isVMWorking(vm)", tooltip="{{vm.power_state}}")
| {{vm.name_label}}
span(ng-if="vm.$container")
| ({{(vm.$container | resolve).name_label || ((vm.$container | resolve).master | resolve).name_label}})
.col-md-2
label(tooltip = 'select/deselect all running VMs', style = 'cursor: pointer')
input.hidden(form = 'snapform', type = 'checkbox', ng-model = 'ctrl.formData.allRunning', ng-change = 'ctrl.toggleAllRunning(ctrl.formData.allRunning)')
span.fa-stack
i.xo-icon-running.fa-stack-1x
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allRunning')
label(tooltip = 'select/deselect all halted VMs', style = 'cursor: pointer')
input.hidden(form = 'snapform', type = 'checkbox', ng-model = 'ctrl.formData.allHalted', ng-change = 'ctrl.toggleAllHalted(ctrl.formData.allHalted)')
span.fa-stack
i.xo-icon-halted.fa-stack-1x
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allHalted')
.form-group
label.control-label.col-md-2(for = 'depth') Depth
.col-md-10
input#depth.form-control(form = 'snapform', ng-model = 'ctrl.formData.depth', placeholder = 'How many snapshots to rollover', type = 'number', min = '1', required)
.form-group(ng-if = '!ctrl.formData.scheduleId')
label.control-label.col-md-2(for = 'enabled')
input#enabled(form = 'snapform', ng-model = 'ctrl.formData.enabled', type = 'checkbox')
.help-block.col-md-8 Enable immediately after creation
.form-group
label.control-label.col-md-2(for = '_reportWhen') Report
.col-md-10
select.form-control(ng-model = 'ctrl.formData._reportWhen')
option(value = ''): em -- When to send reports --
option(value = 'never') Never
option(value = 'alway') Always
option(value = 'fail') Failure
.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 = 'snapform', 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.resetData()')
| &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'): i.xo-icon-loading
.text-center(ng-if = 'ctrl.schedules && !ctrl.collectionLength(ctrl.schedules)') No scheduled jobs
table.table.table-hover(ng-if = 'ctrl.schedules && ctrl.collectionLength(ctrl.schedules)')
tr
th ID
th Tag
th.hidden-xs.hidden-sm VMs to snapshot
th.hidden-xs Depth
th.hidden-xs Scheduling
th Enabled now
th
tr(ng-repeat = 'schedule in ctrl.schedules | map | orderBy:"id":true track by schedule.id')
td {{ schedule.id }}
td {{ ctrl.jobs[schedule.job].paramsVector.items[0].values[0].tag }}
td.hidden-xs.hidden-sm
div(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values.length == 1')
| {{ (ctrl.jobs[schedule.job].paramsVector.items[0].values[0].id | resolve).name_label }}
div(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values.length > 1')
button.btn.btn-info(type = 'button', ng-click = 'unCollapsed = !unCollapsed')
| {{ ctrl.jobs[schedule.job].paramsVector.items[0].values.length }} VMs&nbsp;
i.fa(ng-class = '{"fa-chevron-down": !unCollapsed, "fa-chevron-up": unCollapsed}')
div(collapse = '!unCollapsed')
br
ul.list-group
li.list-group-item(ng-repeat = 'item in ctrl.jobs[schedule.job].paramsVector.items[0].values')
span(ng-if = 'item.id | resolve') {{ (item.id | resolve).name_label }}
span(ng-if = '(item.id | resolve).$container') &nbsp;({{ ((item.id | resolve).$container | resolve).name_label }})
td.hidden-xs {{ ctrl.jobs[schedule.job].paramsVector.items[0].values[0].depth }}
td.hidden-xs {{ ctrl.prettyCron(schedule.cron) }}
td.text-center
i.fa.fa-check(ng-if = 'schedule.enabled')
td.text-right
button.btn.btn-primary(type = 'button', ng-click = 'ctrl.edit(schedule)'): i.fa.fa-pencil
| &nbsp;
button.btn.btn-warning(type = 'button', ng-click = 'ctrl.run(schedule)', ng-disabled = 'ctrl.running[schedule.id]'): i.fa.fa-play
| &nbsp;
button.btn.btn-danger(type = 'button', ng-click = 'ctrl.delete(schedule)'): i.fa.fa-trash-o

View File

@@ -1,104 +0,0 @@
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,37 +0,0 @@
.menu-grid
.side-menu
ul.nav
li
a(ui-sref = '.management', ui-sref-active = 'active')
i.fa.fa-fw.fa-eye.fa-menu
span.menu-entry Overview
li
a(ui-sref = '.rollingsnapshot')
i.xo-icon-snapshot.fa-fw.fa-menu
span.menu-entry Rolling snapshots
li
a(ui-sref = '.remote')
i.fa.fa-fw.fa-plug.fa-menu
span.menu-entry Remote stores
li
a(ui-sref = '.backup')
i.fa.fa-fw.fa-download.fa-menu
span.menu-entry Backup
li
a(ui-sref = '.deltaBackup')
i.fa.fa-fw.fa-code-fork.fa-menu
span.menu-entry Delta Backup
li
a(ui-sref = '.restore')
i.fa.fa-fw.fa-upload.fa-menu
span.menu-entry Restore
li
a(ui-sref = '.disasterrecovery')
i.fa.fa-fw.fa-medkit.fa-menu
span.menu-entry Disaster Recovery
li
a(ui-sref = '.continuousReplication')
i.fa.fa-fw.fa-map-signs.fa-menu
span.menu-entry Continuous Replication
.side-content(ui-view = '')

View File

@@ -1,119 +0,0 @@
angular = require 'angular'
forEach = require('lodash.foreach')
includes = require('lodash.includes')
Clipboard = require('clipboard')
isoDevice = require('iso-device').default
#=====================================================================
module.exports = angular.module 'xoWebApp.console', [
require 'angular-ui-router'
require('angular-no-vnc').default
isoDevice
]
.config ($stateProvider) ->
$stateProvider.state 'consoles_view',
url: '/consoles/:id'
controller: 'ConsoleCtrl'
template: require './view'
.controller 'ConsoleCtrl', ($scope, $stateParams, xoApi, xo, xoHideUnauthorizedFilter, modal) ->
{id} = $stateParams
{get} = xoApi
pool = null
host = null
do (
srsByContainer = xoApi.getIndex('srsByContainer')
poolSrs = null
hostSrs = null
) ->
updateSrs = () =>
srs = []
poolSrs and forEach(poolSrs, (sr) => srs.push(sr))
hostSrs and forEach(hostSrs, (sr) => srs.push(sr))
$scope.SRs = xoHideUnauthorizedFilter(srs)
$scope.$watchCollection(
() => pool and srsByContainer[pool.id],
(srs) =>
poolSrs = srs
updateSrs()
)
$scope.$watchCollection(
() => host and srsByContainer[host.id],
(srs) =>
hostSrs = srs
updateSrs()
)
$scope.$watch(
-> xoApi.get id
(VM) ->
$scope.consoleUrl = null
unless xoApi.user
$scope.VDIs = []
return
$scope.VM = VM
return unless (
VM? and
VM.power_state is 'Running' and
not includes(VM.current_operations, 'clean_reboot')
)
pool = get VM.$poolId
return unless pool
$scope.consoleUrl = "./api/consoles/#{id}"
host = get VM.$container # host because the VM is running.
)
$scope.startVM = xo.vm.start
$scope.stopVM = (id) ->
modal.confirm
title: 'VM shutdown'
message: 'Are you sure you want to shutdown this VM ?'
.then ->
xo.vm.stop id
$scope.rebootVM = (id) ->
modal.confirm
title: 'VM reboot'
message: 'Are you sure you want to reboot this VM ?'
.then ->
xo.vm.restart id
$scope.eject = ->
xo.vm.ejectCd id
$scope.insert = (disc_id) ->
xo.vm.insertCd id, disc_id, true
$scope.vmClipboard = ''
$scope.setClipboard = (text) ->
$scope.vmClipboard = text
$scope.$applyAsync()
$scope.shutdownHost = (id) ->
modal.confirm({
title: 'Shutdown host'
message: 'Are you sure you want to shutdown this host?'
}).then ->
xo.host.stop id
$scope.rebootHost = (id) ->
modal.confirm({
title: 'Reboot host'
message: 'Are you sure you want to reboot this host? It will be disabled then rebooted'
}).then ->
xo.host.restart id
$scope.startHost = (id) ->
xo.host.start id
clipboard = new Clipboard('.copy')
clipboard.on('error', (e) -> console.log('Clipboard', e))
# A module exports its name.
.name

View File

@@ -1,90 +0,0 @@
.container: .panel.panel-default
//- Title
p.page-title
span.fa-stack
i.fa.fa-square-o.fa-stack-2x
i.xo-icon-console.fa-stack-1x(class = 'xo-color-{{VM.power_state | lowercase}}')
| &nbsp;
a(
ng-if = 'VM.type === "VM"'
class = 'xo-color-{{VM.power_state | lowercase}}'
ui-sref = 'VMs_view({id: VM.id})'
) {{VM.name_label}}
a(
ng-if = 'VM.type === "VM-controller"'
class = 'xo-color-{{VM.power_state | lowercase}}'
ui-sref = 'hosts_view({id: VM.$container})'
) {{VM.name_label}}
.list-group
//- Toolbar
.list-group-item: .row.text-center
.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-2
.btn-group(ng-if = 'VM.type === "VM"')
button.btn.btn-default.inversed(
ng-if = "VM.power_state == ('Running' || 'Paused')"
tooltip = "Stop VM"
type = "button"
xo-click = "stopVM(VM.id)"
)
i.fa.fa-stop.fa-fw
button.btn.btn-default.inversed(
ng-if = "VM.power_state == ('Halted')"
tooltip = "Start VM"
type = "button"
xo-click = "startVM(VM.id)"
)
i.fa.fa-play.fa-fw
button.btn.btn-default.inversed(
ng-if = "VM.power_state == ('Running' || 'Paused')"
tooltip = "Reboot VM"
type = "button"
xo-click = "rebootVM(VM.id)"
)
i.fa.fa-refresh.fa-fw
.btn-group(ng-if = 'VM.type === "VM-controller"')
button.btn.btn-default.inversed(
ng-if = "VM.power_state == ('Running' || 'Paused')"
tooltip = "Shutdown Host"
type = "button"
xo-click = "shutdownHost(VM.$container)"
)
i.fa.fa-stop.fa-fw
button.btn.btn-default.inversed(
ng-if = "VM.power_state == ('Halted')"
tooltip = "Start Host"
type = "button"
xo-click = "startHost(VM.$container)"
)
i.fa.fa-play.fa-fw
button.btn.btn-default.inversed(
ng-if = "VM.power_state == ('Running' || 'Paused')"
tooltip = "Reboot Host"
type = "button"
xo-click = "rebootHost(VM.$container)"
)
i.fa.fa-refresh.fa-fw
//- Console
.list-group-item
no-vnc(
url = '{{consoleUrl}}'
remote-control = 'vncRemote',
on-clipboard-change = 'setClipboard(clipboardContent)'
)

View File

@@ -1,363 +0,0 @@
'use strict'
import angular from 'angular'
import uiRouter from 'angular-ui-router'
import uiSelect from 'angular-ui-select'
import debounce from 'lodash.debounce'
import filter from 'lodash.filter'
import foreach from 'lodash.foreach'
import xoApi from 'xo-api'
import xoCircleD3 from 'xo-circle-d3'
import xoParallelD3 from 'xo-parallel-d3'
import xoSunburstD3 from 'xo-sunburst-d3'
import view from './view'
export default angular.module('dashboard.dataviz', [
uiRouter,
uiSelect,
xoApi,
xoCircleD3,
xoParallelD3,
xoSunburstD3
])
.config(function ($stateProvider) {
$stateProvider.state('dashboard.dataviz', {
controller: 'Dataviz as ctrl',
data: {
requireAdmin: true
},
url: '/dataviz/:chart',
template: view
})
})
.filter('type', () => {
return function (objects, type) {
if (!type) {
return objects
}
return filter(objects, object => object.type === type)
}
})
.controller('Dataviz', function ($scope, $state) {
$scope.selectedChart = ''
$scope.availablecharts = {
sunburst: {
name: 'Sunburst charts',
imgs: ['images/sunburst.png', 'images/sunburst2.png'],
url: '/dataviz/sunburst'
},
circle: {
name: 'Circles charts',
imgs: ['images/circle1.png', 'images/circle2.png'],
url: '/dataviz/circle'
},
parcoords: {
name: 'VM properties',
imgs: ['images/parcoords.png'],
url: '/dataviz/parcoords'
}
}
$scope.$on('$stateChangeSuccess', function updatePage () {
$scope.selectedChart = $state.params.chart
})
})
.controller('DatavizParcoords', function DatavizParcoords (xoApi, $scope, $timeout, $interval, $state, bytesToSizeFilter) {
let hostsByPool, vmsByContainer, data
data = []
hostsByPool = xoApi.getIndex('hostsByPool')
vmsByContainer = xoApi.getIndex('vmsByContainer')
/* parallel charts */
function populateChartsData () {
foreach(xoApi.getView('pools').all, function (pool, pool_id) {
foreach(hostsByPool[pool_id], function (host, host_id) {
console.log(host_id)
foreach(vmsByContainer[host_id], function (vm, vm_id) {
let nbvdi, vdisize
nbvdi = 0
vdisize = 0
foreach(vm.$VBDs, function (vbd_id) {
let vbd
vbd = xoApi.get(vbd_id)
if (!vbd.is_cd_drive && vbd.attached) {
nbvdi++
vdisize += xoApi.get(vbd.VDI).size
}
})
data.push({
name: vm.name_label,
id: vm_id,
vcpus: vm.CPUs.number,
vifs: vm.VIFs.length,
ram: vm.memory.size / (1024 * 1024 * 1024)/* memory size in GB */,
nbvdi: nbvdi,
vdisize: vdisize / (1024 * 1024 * 1024)/* disk size in Gb */
})
})
})
})
$scope.charts = {
data: data,
labels: {
vcpus: 'vCPUs number',
ram: 'RAM quantity',
vifs: 'VIF number',
nbvdi: 'VDI number',
vdisize: 'Total space'
}
}
}
const debouncedPopulate = debounce(populateChartsData, 300, {leading: true, trailing: true})
debouncedPopulate()
$scope.$on('$destroy', xoApi.onUpdate(debouncedPopulate))
})
.controller('DatavizStorageHierarchical', function DatavizStorageHierarchical (xoApi, $scope, $timeout, $interval, $state, bytesToSizeFilter) {
$scope.charts = {
selected: {},
data: {
name: 'storage',
children: []
},
click: function (d) {
if (d.virtual) {
return
}
switch (d.type) {
case 'pool':
$state.go('pools_view', {
id: d.id
})
break
case 'host':
$state.go('hosts_view', {
id: d.id
})
break
case 'srs':
$state.go('SRs_view', {
id: d.id
})
break
}
}
}
function populateChartsData () {
function populatestorage (root, container_id) {
let srs = filter(xoApi.getIndex('srsByContainer')[container_id], (one_srs) => one_srs.SR_type !== 'iso' && one_srs.SR_type !== 'udev')
foreach(srs, function (one_srs) {
let srs_used_size = 0
const srs_storage = {
name: one_srs.name_label,
id: one_srs.id,
children: [],
size: one_srs.size,
textSize: bytesToSizeFilter(one_srs.size),
type: 'srs'
}
root.size += one_srs.size
foreach(one_srs.VDIs, function (vdi_id) {
let vdi = xoApi.get(vdi_id)
if (vdi && vdi.name_label.indexOf('.iso') === -1) {
let vdi_storage = {
name: vdi.name_label,
id: vdi_id,
size: vdi.size,
textSize: bytesToSizeFilter(vdi.size),
type: 'vdi'
}
srs_used_size += vdi.size
srs_storage.children.push(vdi_storage)
}
})
if (one_srs.size > srs_used_size) {// some unallocated space
srs_storage.children.push({
color: 'white',
name: 'Free',
id: 'free' + one_srs.id,
size: one_srs.size - srs_used_size,
textSize: bytesToSizeFilter(one_srs.size - srs_used_size),
type: 'vdi',
virtual: true
})
}
root.children.push(srs_storage)
})
root.textSize = bytesToSizeFilter(root.size)
}
let storage_children,
pools,
hostsByPool,
pool_shared_storage
storage_children = []
pools = xoApi.getView('pools')
hostsByPool = xoApi.getIndex('hostsByPool')
foreach(pools.all, function (pool, pool_id) {
let pool_storage, hosts
pool_storage = {
name: pool.name_label || 'no pool',
id: pool_id,
children: [],
size: 0,
color: pool.name_label ? null : 'white',
type: 'pool',
virtual: !pool.name_label
}
pool_shared_storage = {
name: 'Shared',
id: 'Shared' + pool_id,
children: [],
size: 0,
type: 'host',
virtual: true
}
populatestorage(pool_shared_storage, pool_id)
pool_storage.children.push(pool_shared_storage)
pool_storage.size += pool_shared_storage.size
// by hosts
hosts = hostsByPool[pool_id]
foreach(hosts, function (host, host_id) {
// there's also SR attached top
let host_storage = {
name: host.name_label,
id: host.id,
children: [],
size: 0,
type: 'host'
}
populatestorage(host_storage, host_id)
pool_storage.size += host_storage.size
pool_storage.children.push(host_storage)
})
pool_storage.textSize = bytesToSizeFilter(pool_storage.size)
storage_children.push(pool_storage)
})
$scope.charts.data.children = storage_children
}
const debouncedPopulate = debounce(populateChartsData, 300, {leading: true, trailing: true})
debouncedPopulate()
$scope.$on('$destroy', xoApi.onUpdate(debouncedPopulate))
})
.controller('DatavizRamHierarchical', function DatavizRamHierarchical (xoApi, $scope, $timeout, $state, bytesToSizeFilter) {
$scope.charts = {
selected: {},
data: {
name: 'ram',
children: []
},
click: function (d) {
if (d.virtual) {
return
}
switch (d.type) {
case 'pool':
$state.go('pools_view', {id: d.id})
break
case 'host':
$state.go('hosts_view', {id: d.id})
break
case 'vm':
$state.go('VMs_view', {id: d.id})
break
}
}
}
function populateChartsData () {
let ram_children,
pools,
vmsByContainer,
hostsByPool
ram_children = []
pools = xoApi.getView('pools')
vmsByContainer = xoApi.getIndex('vmsByContainer')
hostsByPool = xoApi.getIndex('hostsByPool')
foreach(pools.all, function (pool, pool_id) {
let pool_ram, hosts
// by hosts
pool_ram = {
name: pool.name_label || 'no pool',
id: pool_id,
children: [],
size: 0,
color: pool.name_label ? null : 'white',
type: 'pool',
virtual: !pool.name_label
}
hosts = hostsByPool[pool_id]
foreach(hosts, function (host, host_id) {
// there's also SR attached top
let vm_ram_size = 0
let host_ram = {
name: host.name_label,
id: host_id,
children: [],
size: host.memory.size,
type: 'host'
}
let VMs = vmsByContainer[host_id]
foreach(VMs, function (VM, vm_id) {
let vm_ram = {
name: VM.name_label,
id: vm_id,
size: VM.memory.size,
textSize: bytesToSizeFilter(VM.memory.size),
type: 'vm'
}
if (vm_ram.size) {
vm_ram_size += vm_ram.size
host_ram.children.push(vm_ram)
}
})
if (host_ram.size !== vm_ram_size) {
host_ram.children.push({
color: 'white',
name: 'Free',
id: 'free' + host.id,
size: host.memory.size - vm_ram_size,
textSize: bytesToSizeFilter(host.memory.size - vm_ram_size),
type: 'vm',
virtual: true
})
}
host_ram.textSize = bytesToSizeFilter(host_ram.size)
pool_ram.size += host_ram.size
pool_ram.children.push(host_ram)
})
if (pool_ram.children.length) {
pool_ram.textSize = bytesToSizeFilter(pool_ram.size)
ram_children.push(pool_ram)
}
})
$scope.charts.data.children = ram_children
}
const debouncedPopulate = debounce(populateChartsData, 300, {leading: true, trailing: true})
debouncedPopulate()
$scope.$on('$destroy', xoApi.onUpdate(debouncedPopulate))
})
// A module exports its name.
.name

View File

@@ -1,85 +0,0 @@
.grid-sm(ng-if="!selectedChart")
.panel.panel-default
p.page-title
i.fa.fa-pie-chart
| Dataviz
.panel-body.text-center
.chart-selector(
ng-repeat="(id,chart) in availablecharts"
ui-sref="dashboard.dataviz({chart:id})")
div {{chart.name }}
img.img-thumbnail(
ng-repeat="img in chart.imgs"
ng-src="{{img}}"
)
.grid-sm(ng-if="selectedChart =='sunburst'")
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-memory
| Memory usage
.panel-body.text-center(
ng-controller="DatavizRamHierarchical as ram"
style="position:relative"
)
sunburst-chart(
click="charts.click(d)"
chart-data="charts.data"
)
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-sr
| Storage
.panel-body.text-center(
ng-controller="DatavizStorageHierarchical as storage"
style="position:relative"
)
sunburst-chart(
click="charts.click(d)"
chart-data="charts.data"
)
.grid-sm(ng-if="selectedChart =='circle'")
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-memory
| Memory usage
.panel-body.text-center(
ng-controller="DatavizRamHierarchical as ram"
style="position:relative"
)
circle-chart(
click="charts.click(d)"
chart-data="charts.data"
)
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-sr
| Storage
.panel-body.text-center(
ng-controller="DatavizStorageHierarchical as storage"
style="position:relative"
)
circle-chart(
click="charts.click(d)"
chart-data="charts.data"
)
.grid-sm(ng-if="selectedChart == 'parcoords'")
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-memory
| VMs properties
.panel-body.text-center(
ng-controller="DatavizParcoords as parcoords"
)
parallel-chart(
click="charts.click(d)"
chart-labels="charts.labels"
chart-data="charts.data"
)

View File

@@ -1,89 +0,0 @@
import angular from 'angular'
import uiRouter from 'angular-ui-router'
import filter from 'lodash.filter'
import forEach from 'lodash.foreach'
import xoApi from 'xo-api'
import view from './view'
export default angular.module('dashboard.health', [
uiRouter,
xoApi
])
.config(function ($stateProvider) {
$stateProvider.state('dashboard.health', {
controller: 'Health as ctrl',
data: {
requireAdmin: true
},
url: '/health',
template: view
})
})
.controller('Health', function (xo, xoApi, $scope, modal) {
this.currentVdiPage = 1
this.currentVmPage = 1
const vms = xoApi.getView('VM-snapshot').all
const vdis = xoApi.getView('VDI-snapshot').all
const srs = xoApi.getView('SR').all
$scope.$watchCollection(() => vdis, () => {
const orphanVdiSnapshots = filter(vdis, vdi => vdi && !vdi.$snapshot_of)
this.orphanVdiSnapshots = orphanVdiSnapshots
})
$scope.$watchCollection(() => vms, () => {
const orphanVmSnapshots = filter(vms, vm => vm && !vm.$snapshot_of)
this.orphanVmSnapshots = orphanVmSnapshots
})
$scope.$watchCollection(() => srs, () => {
const warningSrs = filter(srs, sr => sr.content_type !== 'iso' && (sr.physical_usage / sr.size) >= 0.8 && (sr.physical_usage / sr.size) < 0.9)
const dangerSrs = filter(srs, sr => sr.content_type !== 'iso' && (sr.physical_usage / sr.size) >= 0.9)
this.warningSrs = warningSrs
this.dangerSrs = dangerSrs
})
this.selectedVdiForDelete = {}
this.selectedVmForDelete = {}
this.deleteVdiSnapshot = function (id) {
modal.confirm({
title: 'VDI snapshot deletion',
message: 'Are you sure you want to delete this snapshot?'
}).then(() => xo.vdi.delete(id))
}
this.deleteVmSnapshot = function (id) {
modal.confirm({
title: 'VM snapshot deletion',
message: 'Are you sure you want to delete this snapshot? (including its disks)'
}).then(() => xo.vm.delete(id, true))
}
this.deleteSelectedVdis = function () {
return modal.confirm({
title: 'VDI snapshot deletion',
message: 'Are you sure you want to delete all selected VDI snapshots? This operation is irreversible.'
}).then(() => {
forEach(this.selectedVdiForDelete, (selected, id) => console.log(id))
forEach(this.selectedVdiForDelete, (selected, id) => { selected && xo.vdi.delete(id) })
this.selectedVdiForDelete = {}
})
}
this.deleteSelectedVms = function () {
return modal.confirm({
title: 'VM snapshot deletion',
message: 'Are you sure you want to delete all selected VM snapshots? This operation is irreversible.'
}).then(() => {
forEach(this.selectedVmForDelete, (selected, id) => { selected && xo.vm.delete(id, true) })
this.selectedVmForDelete = {}
})
}
})
.name

View File

@@ -1,88 +0,0 @@
.grid-sm
.panel.panel-default
p.page-title
i.fa.fa-heartbeat
| Health
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-hdd-o
| Orphaned VDI snapshots
.panel-body
.center(ng-if = 'ctrl.orphanVdiSnapshots | isEmpty') No orphaned snapshots found
table.table.table-hover(ng-if = 'ctrl.orphanVdiSnapshots | isNotEmpty')
tr
th Name
th Description
th Tags
th Size
th SR
span.pull-right: button.btn.btn-danger(xo-click = 'ctrl.deleteSelectedVdis()', tooltip = 'Delete selected snapshots'): i.fa.fa-trash
tr(ng-repeat = 'vdi in ctrl.orphanVdiSnapshots | filter:vdiSearch | orderBy:natural("name_label") | slice:(10*(ctrl.currentVdiPage-1)):(10*ctrl.currentVdiPage) track by vdi.id')
td.oneliner {{ vdi.name_label }}
td.oneliner {{ vdi.name_description }}
td: xo-tag(object = 'vdi')
td {{ vdi.size | bytesToSize}}
td.oneliner
a(xo-sref="SRs_view({id: (vdi.$SR | resolve).id})")
| {{(vdi.$SR | resolve).name_label}} ({{((vdi.$SR | resolve).$container | resolve).name_label}})
span.pull-right
.btn-group.quick-buttons
a(xo-click="ctrl.deleteVdiSnapshot(vdi.id)"): i.fa.fa-trash-o.fa-lg(tooltip="Destroy this snapshot")
input(type = 'checkbox', ng-model = 'ctrl.selectedVdiForDelete[vdi.id]', tooltip = 'select for deletion')
.form-inline
.input-group
.input-group-addon: i.fa.fa-filter
input.form-control(type = 'text', ng-model = 'vdiSearch', placeholder = 'Enter your search here')
.center(ng-if = '(ctrl.orphanVdiSnapshots | filter:vdiSearch).length > 10 || ctrl.currentVdiPage > 1')
pagination(boundary-links="true", total-items="(ctrl.orphanVdiSnapshots | filter:vdiSearch).length", ng-model="ctrl.currentVdiPage", items-per-page="10", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-camera
| Orphaned VM snapshots
.panel-body
.center(ng-if = 'ctrl.orphanVmSnapshots | isEmpty') No orphaned snapshots found
table.table.table-hover(ng-if = 'ctrl.orphanVmSnapshots | isNotEmpty')
tr
th Name
th Description
th OS
th Container
span.pull-right: button.btn.btn-danger(xo-click = 'ctrl.deleteSelectedVms()', tooltip = 'Delete selected snapshots'): i.fa.fa-trash
tr(ng-repeat = 'vm in ctrl.orphanVmSnapshots | orderBy:natural("name_label") | slice:(10*(ctrl.currentVmPage-1)):(10*ctrl.currentVmPage) track by vm.id')
td.oneliner
i.xo-icon-working(ng-if="vm.current_operations | isNotEmpty", tooltip="{{vm.power_state}} and {{(vm.current_operations | map)[0]}}")
i(class="xo-icon-{{vm.power_state | lowercase}}",ng-if="vm.current_operations | isEmpty", tooltip="{{vm.power_state}}")
| &nbsp;&nbsp;{{ vm.name_label }}
td.oneliner {{ vm.name_description }}
td.onliner {{ vm.os_version.name }}
td.oneliner {{ (vm.$container | resolve).name_label }}
span.pull-right
.btn-group.quick-buttons
a(xo-click="ctrl.deleteVmSnapshot(vm.id)"): i.fa.fa-trash-o.fa-lg(tooltip="Destroy this snapshot")
input(type = 'checkbox', ng-model = 'ctrl.selectedVmForDelete[vm.id]', tooltip = 'select for deletion')
.form-inline
.input-group
.input-group-addon: i.fa.fa-filter
input.form-control(type = 'text', ng-model = 'vmSearch', placeholder = 'Enter your search here')
.center(ng-if = '(ctrl.orphanVmSnapshots | filter:vmSearch).length > 10 || ctrl.currentVmPage > 1')
pagination(boundary-links="true", total-items="(ctrl.orphanVmSnapshots | filter:vmSearch).length", ng-model="ctrl.currentVmPage", items-per-page="10", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-database
| SR Warnings
.panel-body
.center(ng-if = '(ctrl.warningSrs | isEmpty) && (ctrl.dangerSrs | isEmpty)') No warnings found
table.table.table-hover(ng-if = '(ctrl.warningSrs | isNotEmpty) || (ctrl.dangerSrs | isNotEmpty)')
tr
th SR
th Physical usage
tr(ng-repeat = 'sr in ctrl.dangerSrs')
td: a(xo-sref="SRs_view({id: sr.id})") {{ sr.name_label }} ({{ (sr.$container | resolve).name_label }})
td: span.label.label-danger {{ [sr.physical_usage, sr.size] | percentage }}
tr(ng-repeat = 'sr in ctrl.warningSrs')
td: a(xo-sref="SRs_view({id: sr.id})") {{ sr.name_label }} ({{ (sr.$container | resolve).name_label }})
td: span.label.label-warning {{ [sr.physical_usage, sr.size] | percentage }}

View File

@@ -1,43 +0,0 @@
import angular from 'angular'
import uiRouter from 'angular-ui-router'
import dataviz from './dataviz'
import filter from 'lodash.filter'
import health from './health'
import stats from './stats'
import overview from './overview'
import view from './view'
export default angular.module('dashboard', [
uiRouter,
dataviz,
health,
stats,
overview
])
.config(function ($stateProvider) {
$stateProvider.state('dashboard', {
abstract: true,
data: {
requireAdmin: true
},
template: view,
url: '/dashboard'
})
// Redirect to default sub-state.
$stateProvider.state('dashboard.index', {
url: '',
controller: function ($state) {
$state.go('dashboard.overview')
}
})
})
.filter('underStat', () => {
let isUnderStat = object => object.type === 'host' || object.type === 'VM'
return objects => filter(objects, isUnderStat)
})
.name

View File

@@ -1,177 +0,0 @@
'use strict'
import angular from 'angular'
import uiRouter from 'angular-ui-router'
import uiSelect from 'angular-ui-select'
import clone from 'lodash.clonedeep'
import debounce from 'lodash.debounce'
import foreach from 'lodash.foreach'
import xoApi from 'xo-api'
import xoServices from 'xo-services'
import view from './view'
export default angular.module('dashboard.overview', [
uiRouter,
uiSelect,
xoApi,
xoServices
])
.config(function ($stateProvider) {
$stateProvider.state('dashboard.overview', {
controller: 'Overview as ctrl',
data: {
requireAdmin: true
},
url: '/overview',
template: view
})
})
.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: {
nb: 0
},
hosts: {
nb: 0
},
vms: {
nb: 0,
running: 0,
halted: 0,
action: 0
},
ram: [0, 0],
cpu: [0, 0],
srs: []
})
$scope.installAllPatches = function () {
modal.confirm({
title: 'Install all the missing patches',
message: 'Are you sure you want to install all the missing patches? This could take a while...'
}).then(() =>
foreach($scope.pools.all, function (pool, pool_id) {
let pool_hosts = $scope.hostsByPool[pool_id]
foreach(pool_hosts, function (host, host_id) {
console.log('Installing all missing patches on host ', host_id)
xo.host.installAllPatches(host_id)
})
})
)
}
$scope.installHostPatches = function (hostId) {
modal.confirm({
title: 'Update host (' + $scope.nbUpdates[hostId] + ' patch(es))',
message: 'Are you sure you want to install all the missing patches on this host? This could take a while...'
}).then(() => {
console.log('Installing all missing patches on host ', hostId)
xo.host.installAllPatches(hostId)
})
}
const nbUpdates = $scope.nbUpdates = {}
function populateChartsData () {
let pools,
vmsByContainer,
hostsByPool,
nb_hosts,
nb_pools,
vms,
srsByContainer,
srs
nb_pools = 0
nb_hosts = 0
vms = {
nb: 0,
states: [0, 0, 0, 0]
}
const runningStateToIndex = {
Running: 0,
Halted: 1,
Suspended: 2,
Action: 3
}
nb_pools = 0
srs = []
// update vdi, set them to the right host
$scope.pools = pools = xoApi.getView('pools')
srsByContainer = xoApi.getIndex('srsByContainer')
vmsByContainer = xoApi.getIndex('vmsByContainer')
$scope.hostsByPool = hostsByPool = xoApi.getIndex('hostsByPool')
foreach(pools.all, function (pool, pool_id) {
let pool_hosts = hostsByPool[pool_id]
foreach(pool_hosts, function (host, host_id) {
if (host_id in nbUpdates) {
return
}
xo.host.listMissingPatches(host_id)
.then(result => {
nbUpdates[host_id] = result.length
}
)
})
})
foreach(pools.all, function (pool, pool_id) {
nb_pools++
let pool_srs = srsByContainer[pool_id]
foreach(pool_srs, (one_srs) => {
if (one_srs.SR_type !== 'iso' && one_srs.SR_type !== 'udev') {
one_srs = clone(one_srs)
one_srs.ratio = one_srs.size ? one_srs.physical_usage / one_srs.size : 0
one_srs.pool_label = pool.name_label
srs.push(one_srs)
}
})
let VMs = vmsByContainer[pool_id]
foreach(VMs, function (VM) {
// non running VM
vms.states[runningStateToIndex[VM['power_state']]]++
vms.nb++
})
let hosts = hostsByPool[pool_id]
foreach(hosts, function (host, host_id) {
let hosts_srs = srsByContainer[host_id]
foreach(hosts_srs, (one_srs) => {
if (one_srs.SR_type !== 'iso' && one_srs.SR_type !== 'udev') {
one_srs = clone(one_srs)
one_srs.ratio = one_srs.size ? one_srs.physical_usage / one_srs.size : 0
one_srs.host_label = host.name_label
one_srs.pool_label = pool.name_label
srs.push(one_srs)
}
})
nb_hosts++
let VMs = vmsByContainer[host_id]
foreach(VMs, function (VM) {
vms.states[runningStateToIndex[VM['power_state']]]++
vms.nb++
})
})
})
$scope.hosts.nb = nb_hosts
$scope.vms = vms
$scope.pools.nb = nb_pools
$scope.srs = srs
$scope.ram = [xoApi.stats.$memory.usage, xoApi.stats.$memory.size - xoApi.stats.$memory.usage]
$scope.cpu = [[xoApi.stats.$vCPUs], [xoApi.stats.$CPUs]]
}
const debouncedPopulate = debounce(populateChartsData, 300, {leading: true, trailing: true})
debouncedPopulate()
$scope.$on('$destroy', xoApi.onUpdate(debouncedPopulate))
})
.name

View File

@@ -1,139 +0,0 @@
.grid-sm
.panel.panel-default
p.page-title
i.fa.fa-dashboard
| Dashboard
.grid-sm
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-cloud
| Pools
.panel-body.text-center
p.big-stat {{pools.nb}}
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-server
| Hosts
.panel-body.text-center
p.big-stat {{hosts.nb}}
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-desktop
| VMs
.panel-body.text-center
p.big-stat {{vms.nb}}
.grid-sm
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-memory
| Global RAM usage
.panel-body.text-center
canvas(
id="doughnut"
class="chart chart-doughnut"
data="ram"
labels="['Used', 'Free']"
options='{responsive: false,tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>"}'
)
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-dashboard
| vCPUs/CPUs
.panel-body.text-center
canvas(
id="bar"
class="chart chart-bar"
data="cpu"
labels="['']"
series="['vCPUs','CPUs']"
options="{scaleShowGridLines: false, barDatasetSpacing : 10, showScale: false, responsive: false}"
)
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-question-circle
| VMs power state
.panel-body.text-center
canvas(
id="pie"
class="chart chart-pie"
data="vms.states"
labels="['Running', 'Halted', 'Suspended', 'Action']"
colours="['#5cb85c', '#d9534f', '#5bc0de', '#f0ad4e']"
options="{responsive: false}"
)
.grid-sm
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-database
| Storage
.panel-body
table.table.table-hover
tr
th Name
th pool
th host
th Format
th Size
th Physical/Allocated usage
//- TODO: display PBD status for each SR of this host (connected or not)
//- Shared SR
tr(xo-sref="SRs_view({id: SR.id})", ng-repeat="SR in srs | map | orderBy:'-ratio' track by SR.id")
td.oneliner
| {{SR.name_label}}
td.oneliner
| {{SR.pool_label}}
td.oneliner
| {{SR.host_label}}
td {{SR.SR_type}}
td {{SR.size | bytesToSize}}
td
.progress-condensed
.progress-bar(
role="progressbar",
aria-valuemin="0",
aria-valuenow="{{SR.physical_usage}}",
aria-valuemax="{{SR.size}}",
style="width: {{[SR.physical_usage, SR.size] | percentage}}",
tooltip="Physical usage: {{[SR.physical_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 | map | 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

@@ -1,363 +0,0 @@
import angular from 'angular'
import Bluebird from 'bluebird'
import uiRouter from 'angular-ui-router'
import filter from 'lodash.filter'
import find from 'lodash.find'
import forEach from 'lodash.foreach'
import sortBy from 'lodash.sortby'
import xoApi from 'xo-api'
import xoHorizon from'xo-horizon'
import xoServices from 'xo-services'
import xoWeekHeatmap from'xo-week-heatmap'
import view from './view'
export default angular.module('dashboard.stats', [
uiRouter,
xoApi,
xoHorizon,
xoServices,
xoWeekHeatmap
])
.config(function ($stateProvider) {
$stateProvider.state('dashboard.stats', {
controller: 'stats as bigController',
data: {
requireAdmin: true
},
url: '/stats',
template: view
})
})
.filter('type', () => {
return function (objects, type) {
if (!type) {
return objects
}
return filter(objects, object => object.type === type)
}
})
.controller('stats', function () {})
.controller('statsHeatmap', function (xoApi, xo, xoAggregate, notify, bytesToSizeFilter) {
this.charts = {
heatmap: null
}
this.objects = xoApi.all
this.prepareTypeFilter = function (selection) {
const object = selection[0]
this.typeFilter = object && object.type || undefined
}
this.selectAll = function (type) {
this.selected = filter(this.objects, object =>
(object.type === type && object.power_state === 'Running'))
this.typeFilter = type
}
this.prepareMetrics = function (objects) {
this.chosen = objects && objects.slice()
this.metrics = undefined
this.selectedMetric = undefined
if (this.chosen && this.chosen.length) {
this.loadingMetrics = true
const statPromises = []
forEach(this.chosen, object => {
const apiType = (object.type === 'host' && 'host') || (object.type === 'VM' && 'vm') || undefined
if (!apiType) {
notify.error({
title: 'Unhandled object ' + (objects.name_label || ''),
message: 'There is no stats available for this type of objects'
})
object._ignored = true
} else {
delete object._ignored
statPromises.push(
xo[apiType].refreshStats(object.id, 'hours') // hours granularity (7 * 24 hours)
.then(result => {
if (result.stats === undefined) {
object._ignored = true
throw new Error('No stats')
}
return {object, result}
})
.catch(error => {
error.object = object
object._ignored = true
throw error
})
)
}
})
Bluebird.settle(statPromises)
.then(stats => {
const averageMetrics = {}
let averageObjectLayers = {}
let averageCPULayers = 0
forEach(stats, statePromiseInspection => { // One object...
if (statePromiseInspection.isRejected()) {
notify.warning({
title: 'Error fetching stats',
message: 'Metrics do not include ' + statePromiseInspection.reason().object.name_label
})
} else if (statePromiseInspection.isFulfilled()) {
const {object, result} = statePromiseInspection.value()
// Make date array
result.stats.date = []
let timestamp = result.endTimestamp
for (let i = result.stats.memory.length - 1; i >= 0; i--) {
result.stats.date.unshift(timestamp)
timestamp -= 3600
}
const averageCPU = averageMetrics['All CPUs'] && averageMetrics['All CPUs'].values || []
forEach(result.stats.cpus, (values, metricKey) => { // Every CPU metric of this object
metricKey = 'CPU ' + metricKey
averageObjectLayers[metricKey] !== undefined || (averageObjectLayers[metricKey] = 0)
averageObjectLayers[metricKey]++
averageCPULayers++
const mapValues = averageMetrics[metricKey] && averageMetrics[metricKey].values || [] // already fed or not
forEach(values, (value, key) => {
if (mapValues[key] === undefined) { // first value
mapValues.push({
value: +value,
date: +result.stats.date[key] * 1000
})
} else { // average with previous
mapValues[key].value = ((mapValues[key].value || 0) * (averageObjectLayers[metricKey] - 1) + (+value)) / averageObjectLayers[metricKey]
}
if (averageCPU[key] === undefined) { // first overall value
averageCPU.push({
value: +value,
date: +result.stats.date[key] * 1000
})
} else { // average with previous overall value
averageCPU[key].value = (averageCPU[key].value * (averageCPULayers - 1) + value) / averageCPULayers
}
})
averageMetrics[metricKey] = {
key: metricKey,
values: mapValues
}
})
averageMetrics['All CPUs'] = {
key: 'All CPUs',
values: averageCPU
}
forEach(result.stats.vifs, (vif, vifType) => {
const rw = (vifType === 'rx') ? 'out' : 'in'
forEach(vif, (values, metricKey) => {
metricKey = 'Network ' + metricKey + ' ' + rw
averageObjectLayers[metricKey] !== undefined || (averageObjectLayers[metricKey] = 0)
averageObjectLayers[metricKey]++
const mapValues = averageMetrics[metricKey] && averageMetrics[metricKey].values || [] // already fed or not
forEach(values, (value, key) => {
if (mapValues[key] === undefined) { // first value
mapValues.push({
value: +value,
date: +result.stats.date[key] * 1000
})
} else { // average with previous
mapValues[key].value = ((mapValues[key].value || 0) * (averageObjectLayers[metricKey] - 1) + (+value)) / averageObjectLayers[metricKey]
}
})
averageMetrics[metricKey] = {
key: metricKey,
values: mapValues,
filter: bytesToSizeFilter
}
})
})
forEach(result.stats.pifs, (pif, pifType) => {
const rw = (pifType === 'rx') ? 'out' : 'in'
forEach(pif, (values, metricKey) => {
metricKey = 'NIC ' + metricKey + ' ' + rw
averageObjectLayers[metricKey] !== undefined || (averageObjectLayers[metricKey] = 0)
averageObjectLayers[metricKey]++
const mapValues = averageMetrics[metricKey] && averageMetrics[metricKey].values || [] // already fed or not
forEach(values, (value, key) => {
if (mapValues[key] === undefined) { // first value
mapValues.push({
value: +value,
date: +result.stats.date[key] * 1000
})
} else { // average with previous
mapValues[key].value = ((mapValues[key].value || 0) * (averageObjectLayers[metricKey] - 1) + (+value)) / averageObjectLayers[metricKey]
}
})
averageMetrics[metricKey] = {
key: metricKey,
values: mapValues,
filter: bytesToSizeFilter
}
})
})
forEach(result.stats.xvds, (xvd, xvdType) => {
const rw = (xvdType === 'r') ? 'read' : 'write'
forEach(xvd, (values, metricKey) => {
metricKey = 'Disk ' + metricKey + ' ' + rw
averageObjectLayers[metricKey] !== undefined || (averageObjectLayers[metricKey] = 0)
averageObjectLayers[metricKey]++
const mapValues = averageMetrics[metricKey] && averageMetrics[metricKey].values || [] // already fed or not
forEach(values, (value, key) => {
if (mapValues[key] === undefined) { // first value
mapValues.push({
value: +value,
date: +result.stats.date[key] * 1000
})
} else { // average with previous
mapValues[key].value = ((mapValues[key].value || 0) * (averageObjectLayers[metricKey] - 1) + (+value)) / averageObjectLayers[metricKey]
}
})
averageMetrics[metricKey] = {
key: metricKey,
values: mapValues,
filter: bytesToSizeFilter
}
})
})
if (result.stats.load) {
const metricKey = 'Load average'
averageObjectLayers[metricKey] !== undefined || (averageObjectLayers[metricKey] = 0)
averageObjectLayers[metricKey]++
const mapValues = averageMetrics[metricKey] && averageMetrics[metricKey].values || [] // already fed or not
forEach(result.stats.load, (value, key) => {
if (mapValues[key] === undefined) { // first value
mapValues.push({
value: +value,
date: +result.stats.date[key] * 1000
})
} else { // average with previous
mapValues[key].value = ((mapValues[key].value || 0) * (averageObjectLayers[metricKey] - 1) + (+value)) / averageObjectLayers[metricKey]
}
})
averageMetrics[metricKey] = {
key: metricKey,
values: mapValues
}
}
if (result.stats.memoryUsed) {
const metricKey = 'RAM Used'
averageObjectLayers[metricKey] !== undefined || (averageObjectLayers[metricKey] = 0)
averageObjectLayers[metricKey]++
const mapValues = averageMetrics[metricKey] && averageMetrics[metricKey].values || [] // already fed or not
forEach(result.stats.memoryUsed, (value, key) => {
if (mapValues[key] === undefined) { // first value
mapValues.push({
value: +value * (object.type === 'host' ? 1024 : 1),
date: +result.stats.date[key] * 1000
})
} else { // average with previous
mapValues[key].value = ((mapValues[key].value || 0) * (averageObjectLayers[metricKey] - 1) + (+value)) / averageObjectLayers[metricKey]
}
})
averageMetrics[metricKey] = {
key: metricKey,
values: mapValues,
filter: bytesToSizeFilter
}
}
}
})
this.metrics = sortBy(averageMetrics, (_, key) => key)
this.loadingMetrics = false
})
}
}
})
.controller('statsHorizons', function ($scope, xoApi, xoAggregate, xo, $timeout) {
let ctrl, stats
ctrl = this
ctrl.synchronizescale = true
ctrl.objects = xoApi.all
ctrl.chosen = []
this.prepareTypeFilter = function (selection) {
const object = selection[0]
ctrl.typeFilter = object && object.type || undefined
}
this.selectAll = function (type) {
ctrl.selected = filter(ctrl.objects, object =>
(object.type === type && object.power_state === 'Running'))
ctrl.typeFilter = type
}
this.prepareMetrics = function (objects) {
ctrl.chosen = objects
ctrl.selectedMetric = null
ctrl.loadingMetrics = true
xoAggregate
.refreshStats(ctrl.chosen, 'hours')
.then(function (result) {
stats = result
ctrl.metrics = stats.keys
ctrl.stats = {}
// $timeout(refreshStats, 1000)
ctrl.loadingMetrics = false
})
.catch(function (e) {
console.log(' ERROR ', e)
})
}
this.toggleSynchronizeScale = function () {
ctrl.synchronizescale = !ctrl.synchronizescale
if (ctrl.selectedMetric) {
ctrl.prepareStat()
}
}
this.prepareStat = function () {
let min, max
max = 0
min = 0
ctrl.stats = {}
// compute a global extent => the chart will have the same scale
if (ctrl.synchronizescale) {
forEach(stats.details, function (stat, object_id) {
forEach(stat[ctrl.selectedMetric], function (val) {
if (!isNaN(val.value)) {
max = Math.max(val.value || 0, max)
}
})
})
ctrl.extents = [min, max]
} else {
ctrl.extents = null
}
forEach(stats.details, function (stat, object_id) {
const label = find(ctrl.chosen, {id: object_id})
ctrl.stats[label.name_label] = stat[ctrl.selectedMetric]
})
}
})
.name

View File

@@ -1,155 +0,0 @@
.grid-sm
.panel.panel-default
p.page-title
i.fa.fa-bar-chart
| Stats
.grid-sm
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-fire
| Weekly Heatmap
.panel-body(ng-controller='statsHeatmap as heatmap')
| {{heatmap.toto}}
form
.grid-sm
.grid-cell.grid--gutters
.container-fluid
.form-group
ui-select.form-control(ng-model = 'heatmap.selected', ng-change = 'heatmap.prepareTypeFilter(heatmap.selected)', multiple, close-on-select = 'false')
ui-select-match(placeholder = 'Choose an object')
i(class = 'xo-icon-{{ $item.type | lowercase }}')
| {{ $item.name_label }}
ui-select-choices(repeat = 'object in heatmap.objects | underStat | type:heatmap.typeFilter | filter:{ power_state: "Running" } | filter:$select.search | map | orderBy:["type", "name_label"] track by object.id')
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 }})
//- br
.btn-group(role = 'group')
button.btn.btn-default(ng-click = 'heatmap.selected = []', tooltip = 'Clear selection')
i.fa.fa-times
button.btn.btn-default(ng-click = 'heatmap.selectAll("VM")', tooltip = 'Choose all VMs')
i.xo-icon-vm
button.btn.btn-default(ng-click = 'heatmap.selectAll("host")', tooltip = 'Choose all hosts')
i.xo-icon-host
button.btn.btn-success(ng-click = 'heatmap.prepareMetrics(heatmap.selected)', tooltip = 'Load metrics')
i.fa.fa-check
| &nbsp;Select
.grid-cell.grid--gutters
.container-fluid
span(ng-if = 'heatmap.loadingMetrics')
| Loading metrics ...&nbsp;
i.fa.fa-circle-o-notch.fa-spin
.form-group(ng-if = 'heatmap.metrics')
ui-select(ng-model = 'heatmap.selectedMetric')
ui-select-match(placeholder = 'Choose a metric') {{ $select.selected.key }}
ui-select-choices(repeat = 'metric in heatmap.metrics | filter:$select.search | map | orderBy:["key"]') {{ metric.key }}
br
p.text-center(ng-if = 'heatmap.chosen.length')
span(ng-repeat = 'object in heatmap.chosen', ng-class = '{"text-danger": object._ignored}')
i(class = 'xo-icon-{{ object.type | lowercase }}')
| &nbsp;
span(ng-if = '!object._ignored') {{ object.name_label }}
del(ng-if = 'object._ignored') {{ object.name_label }}
| &nbsp;&ensp;
weekheatmap(ng-if = 'heatmap.selectedMetric', chart-data='heatmap.selectedMetric')
.grid-sm
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-fire
| Weekly Charts
.panel-body(ng-controller="statsHorizons as horizons")
form
.grid-sm
.grid-cell.grid--gutters
.container-fluid
.form-group
ui-select.form-control(
ng-model = 'horizons.selected',
ng-change = 'horizons.prepareTypeFilter(horizons.selected)',
multiple,
close-on-select = 'false'
)
ui-select-match(placeholder = 'Choose an object')
i(class = 'xo-icon-{{ $item.type | lowercase }}')
| {{ $item.name_label }}
ui-select-choices(repeat = 'object in horizons.objects | underStat | type:horizons.typeFilter | filter:{ power_state: "Running" } | filter:$select.search | map | orderBy:["type", "name_label"] track by object.id')
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 }})
//- br
.btn-group(role = 'group')
button.btn.btn-default(ng-click = 'horizons.selected = []', tooltip = 'Clear selection')
i.fa.fa-times
button.btn.btn-default(ng-click = 'horizons.selectAll("VM")', tooltip = 'Choose all VMs')
i.xo-icon-vm
button.btn.btn-default(ng-click = 'horizons.selectAll("host")', tooltip = 'Choose all hosts')
i.xo-icon-host
button.btn.btn-success(ng-click = 'horizons.prepareMetrics(horizons.selected)', tooltip = 'Load metrics')
i.fa.fa-check
| &nbsp;Select
.grid-cell.grid--gutters
.container-fluid
span(ng-if = 'horizons.loadingMetrics')
| Loading metrics ...&nbsp;
i.fa.fa-circle-o-notch.fa-spin
.form-group(ng-if = 'horizons.metrics && !horizons.loadingMetrics')
ui-select(ng-model = 'horizons.selectedMetric',ng-change='horizons.prepareStat()')
ui-select-match(placeholder = 'Choose a metric') {{ $select.selected }}
ui-select-choices(repeat = 'metric in horizons.metrics | filter:$select.search | map | orderBy:["key"]') {{ metric }}
br
button.btn.btn-primary.pull-right(
tooltip="Desynchronize Scale",
ng-click="horizons.toggleSynchronizeScale()"
ng-if='horizons.synchronizescale && horizons.selectedMetric'
)
i.fa.fa-balance-scale
button.btn.btn-default.pull-right(
tooltip="Synchronize Scale",
ng-click="horizons.toggleSynchronizeScale()"
ng-if='!horizons.synchronizescale && horizons.selectedMetric'
)
i.fa.fa-balance-scale
br
p.text-center(ng-if = 'horizons.chosen.length')
span(ng-repeat = 'object in horizons.chosen', ng-class = '{"text-danger": object._ignored}')
i(class = 'xo-icon-{{ object.type | lowercase }}')
| &nbsp;
span(ng-if = '!object._ignored') {{ object.name_label }}
del(ng-if = 'object._ignored') {{ object.name_label }}
| &nbsp;&ensp;
div(
ng-repeat='(label,stat) in horizons.stats'
ng-if='!horizons.loadingMetrics'
style='position:relative'
)
horizon(
ng-if='$first'
chart-data='stat'
show-axis='true'
axis-orientation='top'
selected='horizons.selectedDate'
extent='horizons.extents'
label='{{label}}'
)
horizon(
ng-if='$middle'
chart-data='stat'
selected='horizons.selectedDate'
extent='horizons.extents'
label='{{label}}'
)
horizon(
ng-if='$last && !$first'
chart-data='stat'
show-axis='true'
axis-orientation='bottom'
selected='horizons.selectedDate'
extent='horizons.extents'
label='{{label}}'
)

View File

@@ -1,20 +0,0 @@
.menu-grid
.side-menu
ul.nav
li
a(ui-sref = '.overview', ui-sref-active = 'active')
i.fa.fa-fw.fa-dashboard.fa-menu
span.menu-entry Overview
li
a(ui-sref = '.dataviz({chart:""})')
i.fa.fa-fw.fa-pie-chart.fa-menu
span.menu-entry Dataviz
li
a(ui-sref = '.stats')
i.fa.fa-fw.fa-bar-chart.fa-menu
span.menu-entry Stats
li
a(ui-sref = '.health')
i.fa.fa-fw.fa-heartbeat.fa-menu
span.menu-entry Health
.side-content(ui-view = '')

View File

@@ -1,63 +0,0 @@
// TODO: should be integrated xo.deleteVms()
import angular from 'angular'
import forEach from 'lodash.foreach'
import uiBootstrap from 'angular-ui-bootstrap'
import xoServices from 'xo-services'
import view from './view'
// ===================================================================
export default angular.module('xoWebApp.deleteVms', [
uiBootstrap,
xoServices
])
.controller('DeleteVmsCtrl', function (
$scope,
$modalInstance,
xoApi,
VMsIds
) {
$scope.$watchCollection(() => xoApi.get(VMsIds), function (VMs) {
$scope.VMs = VMs
})
// Do disks have to be deleted for a given VM.
let disks = $scope.disks = {}
forEach(VMsIds, id => {
disks[id] = true
})
$scope.delete = function () {
let value = []
forEach(VMsIds, id => {
value.push([id, disks[id]])
})
$modalInstance.close(value)
}
})
.service('deleteVmsModal', function ($modal, xo) {
return function (ids) {
return $modal.open({
controller: 'DeleteVmsCtrl',
template: view,
resolve: {
VMsIds: () => ids
}
}).result.then(function (toDelete) {
let promises = []
forEach(toDelete, ([id, deleteDisks]) => {
promises.push(xo.vm.delete(id, deleteDisks))
})
return promises
})
}
})
// A module exports its name.
.name

View File

@@ -1,27 +0,0 @@
form(ng-submit="delete()")
.modal-header
h3 VMs deletion
.modal-body
p
| You are going to delete the following VMs, this is a
strong dangerous action
| !
table.table
tr
th.col-sm-3 Name
th.col-sm-6 Description
th.col-sm-3 Delete disks?
tbody
tr(ng-repeat="VM in VMs | orderBy:natural('name_label') track by VM.id")
td {{VM.name_label}}
td {{VM.name_description}}
td
input(type="checkbox", ng-model="disks[VM.id]")
p
i.fa.fa-exclamation-triangle
| &nbsp;All snapshots will be deleted too
.modal-footer
button.btn.btn-primary(type="submit")
| Delete
button.btn.btn-warning(type="button", ng-click="$dismiss()")
| Cancel

View File

@@ -1,52 +0,0 @@
import angular from 'angular'
import uiBootstrap from 'angular-ui-bootstrap'
import template from './view'
// ===================================================================
export default angular.module('xoWebApp.genericModal', [
uiBootstrap
])
.controller('GenericModalCtrl', function ($modalInstance, $sce, options) {
const {
htmlMessage,
message,
noButtonLabel = undefined,
title,
yesButtonLabel = 'Ok'
} = options
this.title = title
this.message = message
this.htmlMessage = htmlMessage && $sce.trustAsHtml(htmlMessage)
this.yesButtonLabel = yesButtonLabel
this.noButtonLabel = noButtonLabel
})
.service('modal', function ($modal) {
return {
alert: ({ title, htmlMessage, message }) => $modal.open({
controller: 'GenericModalCtrl as $ctrl',
template,
resolve: {
options: () => ({ title, htmlMessage, message })
}
}).result,
confirm: ({ title, htmlMessage, message }) => $modal.open({
controller: 'GenericModalCtrl as $ctrl',
template,
resolve: {
options: () => ({
title,
htmlMessage,
message,
noButtonLabel: 'Cancel'
})
}
}).result
}
})
// A module exports its name.
.name

View File

@@ -1,11 +0,0 @@
.modal-header
h3
i.fa.fa-exclamation-triangle.text-danger
| {{$ctrl.title}}
.modal-body(ng-if = "$ctrl.htmlMessage", ng-bind-html = "$ctrl.htmlMessage")
.modal-body(ng-if = "!$ctrl.htmlMessage") {{$ctrl.message}}
.modal-footer
button.btn.btn-primary(type="button", ng-click="$close()")
| {{$ctrl.yesButtonLabel}}
button.btn.btn-warning(ng-if="$ctrl.noButtonLabel", type="button", ng-click="$dismiss()")
| {{$ctrl.noButtonLabel}}

View File

@@ -1,466 +0,0 @@
angular = require 'angular'
forEach = require 'lodash.foreach'
intersection = require 'lodash.intersection'
map = require 'lodash.map'
omit = require 'lodash.omit'
sum = require 'lodash.sum'
throttle = require 'lodash.throttle'
find = require 'lodash.find'
filter = require 'lodash.filter'
#=====================================================================
module.exports = angular.module 'xoWebApp.host', [
require 'angular-ui-router'
require('ng-file-upload')
require('tag').default
]
.config ($stateProvider) ->
$stateProvider.state 'hosts_view',
url: '/hosts/:id'
controller: 'HostCtrl'
template: require './view'
.controller 'HostCtrl', (
$scope, $stateParams, $http
$timeout
$window
dateFilter
Upload
xoApi, xo, modal, notify, bytesToSizeFilter
) ->
do (
hostId = $stateParams.id
controllers = xoApi.getIndex('vmControllersByContainer')
poolPatches = xoApi.getIndex('poolPatchesByPool')
srs = xoApi.getIndex('srsByContainer')
tasks = xoApi.getIndex('runningTasksByHost')
vms = xoApi.getIndex('vmsByContainer')
) ->
Object.defineProperties($scope, {
controller: {
get: () => controllers[hostId]
},
poolPatches: {
get: () => $scope.host && poolPatches[$scope.host.$poolId]
},
sharedSrs: {
get: () => $scope.host && srs[$scope.host.$poolId]
},
srs: {
get: () => srs[hostId]
},
tasks: {
get: () => tasks[hostId]
},
vms: {
get: () => vms[hostId]
}
})
$window.bytesToSize = bytesToSizeFilter # FIXME dirty workaround to custom a Chart.js tooltip template
host = null
$scope.currentPatchPage = 1
$scope.currentLogPage = 1
$scope.currentPCIPage = 1
$scope.currentGPUPage = 1
$scope.currentLicensePage = 1
$scope.refreshStatControl = refreshStatControl = {
baseStatInterval: 5000
baseTimeOut: 10000
period: null
running: false
attempt: 0
start: () ->
return if this.running
this.stop()
this.running = true
this._reset()
$scope.$on('$destroy', () => this.stop())
return this._trig(Date.now())
_trig: (t1) ->
if this.running
timeoutSecurity = $timeout(
() => this.stop(),
this.baseTimeOut
)
return $scope.refreshStats($scope.host.id)
.then () => this._reset()
.catch (err) =>
if !this.running || this.attempt >= 2 || $scope.host.power_state isnt 'Running' || $scope.isVMWorking($scope.host)
return this.stop()
else
this.attempt++
.finally () =>
$timeout.cancel(timeoutSecurity)
if this.running
t2 = Date.now()
return this.period = $timeout(
() => this._trig(t2),
Math.max(this.baseStatInterval - (t2 - t1), 0)
)
_reset: () ->
this.attempt = 0
stop: () ->
if this.period
$timeout.cancel(this.period)
this.running = false
return
}
$scope.$watch(
-> xoApi.get $stateParams.id
(host) ->
$scope.host = host
return unless host?
$scope.hostParams = Object.getOwnPropertyNames(host.license_params)
pool = $scope.pool = xoApi.get host.$poolId
SRsToPBDs = $scope.SRsToPBDs = Object.create null
for PBD in host.$PBDs
PBD = xoApi.get PBD
# If this PBD is unknown, just skips it.
continue unless PBD
SRsToPBDs[PBD.SR] = PBD
$scope.listMissingPatches($scope.host.id)
if host.power_state is 'Running'
refreshStatControl.start()
else
refreshStatControl.stop()
)
$scope.$watch('vms', (vms) =>
$scope.vCPUs = sum(map(vms, (vm) => +vm.CPUs.number))
)
$scope.cancelTask = (id) ->
modal.confirm({
title: 'Cancel task'
message: 'Are you sure you want to cancel this task?'
}).then ->
xo.task.cancel id
$scope.destroyTask = (id) ->
modal.confirm({
title: 'Destroy task'
message: 'Are you sure you want to destroy this task?'
}).then ->
xo.task.destroy id
$scope.disconnectPBD = xo.pbd.disconnect
$scope.removePBD = xo.pbd.delete
$scope.new_sr = xo.pool.new_sr
$scope.pool_addHost = (id) ->
xo.host.attach id
$scope.pools = xoApi.getView('pools')
$scope.hostsByPool = xoApi.getIndex('hostsByPool')
$scope.pool_moveHost = (target) ->
modal.confirm({
title: 'Move host to another pool'
message: 'Are you sure you want to move this host?'
}).then ->
xo.pool.mergeInto({ source: $scope.pool.id, target: target.id })
$scope.pool_removeHost = (id) ->
modal.confirm({
title: 'Remove host from pool'
message: 'Are you sure you want to detach this host from its pool? It will be automatically rebooted AND LOCAL STORAGE WILL BE ERASED.'
}).then ->
xo.host.detach id
$scope.rebootHost = (id) ->
modal.confirm({
title: 'Reboot host'
message: 'Are you sure you want to reboot this host? It will be disabled then rebooted'
}).then ->
xo.host.restart id
$scope.enableHost = (id) ->
xo.host.enable id
notify.info {
title: 'Host action'
message: 'Host is enabled'
}
$scope.disableHost = (id) ->
modal.confirm({
title: 'Disable host'
message: 'Are you sure you want to disable this host? In disabled state, no new VMs can be started and currently active VMs on the host continue to execute.'
}).then ->
xo.host.disable id
.then ->
notify.info {
title: 'Host action'
message: 'Host is disabled'
}
$scope.restartToolStack = (id) ->
modal.confirm({
title: 'Restart XAPI'
message: 'Are you sure you want to restart the XAPI toolstack?'
}).then ->
xo.host.restartToolStack id
$scope.shutdownHost = (id) ->
modal.confirm({
title: 'Shutdown host'
message: 'Are you sure you want to shutdown this 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
$data = {
id: host.id
}
if name_label isnt host.name_label
$data.name_label = name_label
if name_description isnt host.name_description
$data.name_description = name_description
if enabled isnt host.enabled
if host.enabled
$scope.disableHost($data.id)
else
$scope.enableHost($data.id)
# enabled is not set via the "set" method, so we remove it before send it
delete $data.enabled
xoApi.call 'host.set', $data
$scope.deleteAllLog = ->
modal.confirm({
title: 'Log deletion'
message: 'Are you sure you want to delete all the logs?'
}).then ->
forEach $scope.host.messages, (log) ->
console.log "Remove log #{log.id}"
xo.log.delete log.id
return
$scope.deleteLog = (id) ->
console.log "Remove log #{id}"
xo.log.delete id
$scope.connectPBD = (id) ->
console.log "Connect PBD #{id}"
xoApi.call 'pbd.connect', {id: id}
$scope.disconnectPBD = (id) ->
console.log "Disconnect PBD #{id}"
xoApi.call 'pbd.disconnect', {id: id}
$scope.removePBD = (id) ->
console.log "Remove PBD #{id}"
xoApi.call 'pbd.delete', {id: id}
$scope.connectPIF = (id) ->
console.log "Connect PIF #{id}"
xoApi.call 'pif.connect', {id: id}
$scope.disconnectPIF = (id) ->
console.log "Disconnect PIF #{id}"
xoApi.call 'pif.disconnect', {id: id}
$scope.removePIF = (id) ->
console.log "Remove PIF #{id}"
xoApi.call 'pif.delete', {id: id}
$scope.importVm = ($files, id) ->
file = $files[0]
notify.info {
title: 'VM import started'
message: "Starting the VM import"
}
xo.vm.import id
.then ({ $sendTo: url }) ->
return Upload.http {
method: 'POST'
url
data: file
}
.then (result) ->
throw result.status if result.status isnt 200
notify.info
title: 'VM import'
message: 'Success'
$scope.createNetwork = (name, description, pif, mtu, vlan) ->
$scope.createNetworkWaiting = true # disables form fields
notify.info {
title: 'Network creation...'
message: 'Creating the network'
}
params = {
pool: $scope.host.$pool
name,
}
if mtu then params.mtu = mtu
if pif then params.pif = pif
if vlan then params.vlan = vlan
if description then params.description = description
xoApi.call 'network.create', params
.then ->
$scope.creatingNetwork = false
$scope.createNetworkWaiting = false
$scope.addIp = (pif, ip, netmask, dns, gateway, ipMethod) ->
notify.info {
title: 'IP configuration...'
message: 'Configuring new IP mode'
}
xoApi.call('pif.reconfigureIp', {
id: pif.id,
mode: ipMethod,
ip,
netmask,
dns,
gateway
})
$scope.physicalPifs = () ->
physicalPifs = []
forEach $scope.host.$PIFs, (pif) ->
pif = xoApi.get(pif)
if pif.physical
physicalPifs.push pif.id
return physicalPifs
$scope.isPoolPatch = (patch) ->
return false if $scope.poolPatches is undefined
return $scope.poolPatches.hasOwnProperty(patch.uuid)
$scope.isPoolPatchApplied = (patch) ->
return true if patch.applied
hostPatch = intersection(patch.$host_patches, $scope.host.patches)
return false if not hostPatch.length
hostPatch = xoApi.get(hostPatch[0])
return hostPatch.applied
$scope.listMissingPatches = (id) ->
return xo.host.listMissingPatches id
.then (result) ->
$scope.updates = omit(result,map($scope.poolPatches,'id'))
$scope.installPatch = (id, patchUid) ->
console.log("Install patch "+patchUid+" on "+id)
notify.info {
title: 'Patch host'
message: "Patching the host, please wait..."
}
xo.host.installPatch id, patchUid
$scope.installAllPatches = (id) ->
modal.confirm({
title: 'Install all the missing patches'
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 patches on host ' + id)
xo.host.installAllPatches id
$scope.refreshStats = (id) ->
return xo.host.refreshStats id
.then (result) ->
result.stats.cpuSeries = []
if result.stats.cpus.length >= 12
nValues = result.stats.cpus[0].length
nCpus = result.stats.cpus.length
cpuAVG = (0 for [1..nValues])
forEach result.stats.cpus, (cpu) ->
forEach cpu, (stat, index) ->
cpuAVG[index] += stat
return
return
forEach cpuAVG, (cpu, index) ->
cpuAVG[index] /= nCpus
return
result.stats.cpus = [cpuAVG]
result.stats.cpuSeries.push 'CPU AVG'
else
forEach result.stats.cpus, (v,k) ->
result.stats.cpuSeries.push 'CPU ' + k
return
result.stats.pifSeries = []
pifsArray = []
forEach result.stats.pifs.rx, (v,k) ->
return unless v
result.stats.pifSeries.push '#' + k + ' in'
result.stats.pifSeries.push '#' + k + ' out'
pifsArray.push (v || [])
pifsArray.push (result.stats.pifs.tx[k] || [])
return
result.stats.pifs = pifsArray
forEach result.stats.memoryUsed, (v, k) ->
result.stats.memoryUsed[k] = v*1024
forEach result.stats.memory, (v, k) ->
result.stats.memory[k] = v*1024
result.stats.date = []
timestamp = result.endTimestamp
for i in [result.stats.memory.length-1..0] by -1
result.stats.date.unshift new Date(timestamp*1000).toLocaleTimeString()
timestamp -= 5
$scope.stats = result.stats
$scope.statView = {
cpuOnly: false,
ramOnly: false,
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

@@ -1,619 +0,0 @@
.grid-sm
.panel.panel-default
p.page-title
i.xo-icon-host(class="xo-color-{{host.power_state | lowercase}}")
| {{host.name_label}}
small(ng-if="pool.name_label")
| (
a(ui-sref="pools_view({id: pool.id})") {{pool.name_label}}
| )
p.center {{host.bios_strings["system-manufacturer"]}} {{host.bios_strings["system-product-name"]}}
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-cogs
| General
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 Edit", ng-click="hostSettings.$cancel()")
i.fa.fa-undo.fa-fw
.panel-body
form(editable-form="", name="hostSettings", onbeforesave="saveHost($data)")
dl.dl-horizontal
dt Name
dd
span(editable-text="host.name_label", e-name="name_label", e-form="hostSettings")
| {{host.name_label}}
dt Description
dd
span(editable-text="host.name_description", e-name="name_description", e-form="hostSettings")
| {{host.name_description}}
dt Enabled
dd
span(editable-select="host.enabled", e-ng-options="ap.v as ap.t for ap in [{v: true, t:'Yes'}, {v: false, t:'No'}]", e-name="enabled", e-form="hostSettings")
| {{host.enabled ? 'Yes' : 'No'}}
dt Tags
dd
xo-tag(ng-if = 'host', object = 'host')
dt CPUs
dd {{host.CPUs["cpu_count"]}}x {{host.CPUs["modelname"]}}
dt Hostname
dd
| {{host.hostname}}
dt UUID
dd {{host.UUID}}
dt iQN
dd {{host.iSCSI_name}}
dt(ng-if="refreshStatControl.running && stats") vCPUs/CPUs:
dd(ng-if="refreshStatControl.running && stats") {{vCPUs}}/{{host.CPUs['cpu_count']}}
dt(ng-if="refreshStatControl.running && stats") Running VMs:
dd(ng-if="refreshStatControl.running && stats") {{vms | count}}
dt(ng-if="refreshStatControl.running && stats") RAM (used/free):
dd(ng-if="refreshStatControl.running && stats") {{host.memory.usage | bytesToSize}}/{{host.memory.size | bytesToSize}}
.btn-form(ng-show="hostSettings.$visible")
p.center
button.btn.btn-default(type="button", ng-disabled="hostSettings.$waiting", ng-click="hostSettings.$cancel()")
i.fa.fa-times
| Cancel
| &nbsp;
button.btn.btn-primary(type="submit", ng-disabled="hostSettings.$waiting")
i.fa.fa-save
| Save
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-stats
| Stats
.panel-body(ng-if="refreshStatControl.running && stats")
div(ng-if="statView.cpuOnly", ng-click="statView.cpuOnly = false")
p.stat-name
i.xo-icon-cpu
| &nbsp; CPU usage
canvas.chart.chart-line.chart-stat-full(
id="bigCpu"
data="stats.cpus"
labels="stats.date"
series="stats.cpuSeries"
colours="['#0000ff', '#9999ff', '#000099', '#5555ff', '#000055']"
legend="true"
options='{responsive: true, maintainAspectRatio: false, tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= Math.round(10*value)/10 %>", multiTooltipTemplate:"<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%= Math.round(10*value)/10 %>", pointDot: false, showScale: false, animation: false, datasetStrokeWidth: 0.8, scaleOverride: true, scaleSteps: 100, scaleStartValue: 0, scaleStepWidth: 1, pointHitDetectionRadius: 0}'
)
div(ng-if="statView.ramOnly", ng-click="statView.ramOnly = false")
p.stat-name
i.xo-icon-memory
//- i.fa.fa-bar-chart
//- i.fa.fa-tasks
//- i.fa.fa-server
| &nbsp; RAM usage
canvas.chart.chart-line.chart-stat-full(
id="bigRam"
data="[stats.memoryUsed,stats.memory]"
labels="stats.date"
series="['Used RAM', 'Total RAM']"
colours="['#ff0000', '#ffbbbb']"
legend="true"
options='{responsive: true, maintainAspectRatio: false, tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>", multiTooltipTemplate:"<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%= bytesToSize(value) %>", datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false, pointHitDetectionRadius: 0}'
)
div(ng-if="statView.netOnly", ng-click="statView.netOnly = false")
p.stat-name
i.xo-icon-network
| &nbsp; Network I/O
canvas.chart.chart-line.chart-stat-full(
id="bigNet"
data="stats.pifs"
labels="stats.date"
series="stats.pifSeries"
colours="['#dddd00', '#dddd77', '#777700', '#dddd55', '#555500', '#ffdd00']"
legend="true"
options='{responsive: true, maintainAspectRatio: false, tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>", multiTooltipTemplate:"<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%= bytesToSize(value) %>", datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false, pointHitDetectionRadius: 0}'
)
div(ng-if="statView.loadOnly", ng-click="statView.loadOnly = false")
p.stat-name
i.fa.fa-cogs
| &nbsp; Load Average
canvas.chart.chart-line.chart-stat-full(
id="bigLoad"
data="[stats.load]"
labels="stats.date"
series="['Load']"
colours="['#960094']"
legend="true"
options='{responsive: true, maintainAspectRatio: false, multiTooltipTemplate:"<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%= bytesToSize(value) %>", datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false, pointHitDetectionRadius: 0}'
)
div(ng-if="!statView.netOnly && !statView.loadOnly && !statView.cpuOnly && !statView.ramOnly")
.row
.col-md-6(ng-click="statView.cpuOnly=true")
p.stat-name
i.xo-icon-cpu
| &nbsp; CPU usage
canvas.chart.chart-line.chart-stat-preview(
id="smallCpu"
data="stats.cpus"
labels="stats.date"
series="stats.cpuSeries"
colours="['#0000ff', '#9999ff', '#000099', '#5555ff', '#000055']"
options='{responsive: true, maintainAspectRatio: false, showTooltips: false, pointDot: false, showScale: false, animation: false, datasetStrokeWidth: 0.8, scaleOverride: true, scaleSteps: 100, scaleStartValue: 0, scaleStepWidth: 1}'
)
.col-md-6(ng-click="statView.ramOnly=true")
p.stat-name
i.xo-icon-memory
//- i.fa.fa-bar-chart
//- i.fa.fa-tasks
//- i.fa.fa-server
| &nbsp; RAM usage
canvas.chart.chart-line.chart-stat-preview(
id="smallRam"
data="[stats.memoryUsed,stats.memory]"
labels="stats.date"
series="['Used RAM', 'Total RAM']"
colours="['#ff0000', '#ffbbbb']"
options="{responsive: true, maintainAspectRatio: false, showTooltips: false, datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false}"
)
.row
.col-md-6(ng-click="statView.netOnly=true")
p.stat-name
i.xo-icon-network
| &nbsp; Network I/O
canvas.chart.chart-line.chart-stat-preview(
id="smallNet"
data="stats.pifs"
labels="stats.date"
series="stats.pifSeries"
colours="['#dddd00', '#dddd77', '#777700', '#dddd55', '#555500', '#ffdd00']"
options="{responsive: true, maintainAspectRatio: false, showTooltips: false, datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false}"
)
.col-md-6(ng-click="statView.loadOnly=true")
p.stat-name
i.fa.fa-cogs
| &nbsp; Load Average
canvas.chart.chart-line.chart-stat-preview(
id="smallDisk"
data="[stats.load]"
labels="stats.date"
series="['Load']"
colours="['#960094']"
options="{responsive: true, maintainAspectRatio: false, showTooltips: false, datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false}"
)
.panel-body(ng-if="!refreshStatControl.running || !stats")
.row
.col-sm-4
p.stat-name CPU usage:
p.center.mid-stat {{vCPUs}}/{{host.CPUs['cpu_count']}}
.col-sm-4
p.stat-name RAM used:
p.center.mid-stat {{host.memory.usage | bytesToSize}}
.col-sm-4
p.stat-name Running VMs:
p.center.mid-stat {{vms | count}}
p.center(ng-if="refreshStatControl.running")
i.xo-icon-loading
| &nbsp; Fetching stats...
//- Action panel
.grid-sm(ng-if = 'canOperate()')
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-flash
| Actions
.panel-body.text-center
.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})", 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})", 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)", 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)", 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)", 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)", ng-if = 'canAdmin()')
i.fa.fa-check-circle.fa-2x.fa-fw
.grid-cell.btn-group
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)", 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"
type="button"
style="width: 90%"
)
i.fa.fa-cloud-download.fa-2x.fa-fw
span.caret
ul.dropdown-menu.left(role="menu")
li(ng-repeat="p in pools.all | map | orderBy:natural('name_label') track by p.id" ng-if="p!=pool")
a(xo-click="pool_moveHost(p)")
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)", 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"
style="width: 90%"
ngf-select = 'importVm($files, host.id)'
)
i.fa.fa-upload.fa-2x.fa-fw
.grid-cell.btn-group(style="margin-bottom: 0.5em")
button.btn(
tooltip="Host console"
tooltip-placement="top"
type="button"
style="width: 90%"
xo-sref="consoles_view({id: controller.id})"
)
i.xo-icon-console.fa-2x.fa-fw
//- TODO: Memory panel
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-memory
| Memory
.panel-body.text-center
.progress
.progress-bar-host(role="progressbar", aria-valuemin="0", aria-valuenow="{{controller.memory.size}}", aria-valuemax="{{host.memory.size}}", style="width: {{[controller.memory.size, host.memory.size] | percentage}}", tooltip="{{host.name_label}}: {{[controller.memory.size, host.memory.size] | percentage}}")
small {{host.name_label}}
.progress-bar.progress-bar-vm(ng-repeat="VM in vms | map | orderBy:natural('name_label') track by VM.id", role="progressbar", aria-valuemin="0", aria-valuenow="{{VM.memory.size}}", aria-valuemax="{{host.memory.size}}", style="width: {{[VM.memory.size, host.memory.size] | percentage}}", xo-sref="VMs_view({id: VM.id})", tooltip="{{VM.name_label}}: {{[VM.memory.size, host.memory.size] | percentage}}")
small {{VM.name_label}}
ul.list-inline.text-center
li Total: {{host.memory.size | bytesToSize}}
li Currently used: {{host.memory.usage | bytesToSize}}
li Available: {{host.memory.size-host.memory.usage | bytesToSize}}
//- SR panel
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-sr
| Storage
.panel-body
table.table.table-hover
tr
th Name
th Format
th Size
th Physical usage
th Type
th Status
//- TODO: display PBD status for each SR of this host (connected or not)
//- Shared SR
tr(xo-sref="SRs_view({id: SR.id})", ng-repeat="SR in sharedSrs | map | orderBy:natural('name_label') track by SR.id")
td.oneliner
| {{SR.name_label}}
td {{SR.SR_type}}
td {{SR.size | bytesToSize}}
td
.progress-condensed
.progress-bar(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.physical_usage}}", aria-valuemax="{{SR.size}}", style="width: {{[SR.physical_usage, SR.size] | percentage}}", tooltip="Physical usage: {{[SR.physical_usage, SR.size] | percentage}}")
td
span.label.label-primary Shared
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)", 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)", ng-if = 'canAdmin()')
i.fa.fa-link.fa-lg
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
tr(xo-sref="SRs_view({id: SR.id})", ng-repeat="SR in srs | map | orderBy:natural('name_label') track by SR.id")
td
| {{SR.name_label}}
td {{SR.SR_type}}
td {{SR.size | bytesToSize}}
td
.progress-condensed
.progress-bar(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.physical_usage}}", aria-valuemax="{{SR.size}}", style="width: {{[SR.physical_usage, SR.size] | percentage}}", tooltip="Physical usage: {{[SR.physical_usage, SR.size] | percentage}}")
td
span.label.label-info Local
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)", 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)", ng-if = 'canAdmin()')
i.fa.fa-link.fa-lg
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
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-network
| PIFs
.panel-body
table.table.table-hover
th.col-md-1 Device
th.col-md-1 Network
th.col-md-1 VLAN
th.col-md-2 Address
th.col-md-2 MAC
th.col-md-1 MTU
th.col-md-2 Link status
tbody(ng-repeat="PIF in host.$PIFs | resolve | orderBy:natural('device') track by PIF.id")
tr
td
| {{PIF.device}}&nbsp;
span.label.label-primary(ng-if="PIF.management") XAPI
| &nbsp;
span.label.label-primary(ng-if="PIF.physical") Phys.
td {{(PIF.$network | resolve).name_label}}
td
span(ng-if="PIF.vlan > -1")
| {{PIF.vlan}}
span(ng-if="PIF.vlan == -1")
| -
td {{PIF.ip}} ({{PIF.mode}})
span.quick-buttons(ng-click="configuringIp = !configuringIp" tooltip="Configure IP")
i.fa.fa-edit.fa-lg
td {{PIF.mac}}
td {{PIF.mtu}}
td
span.label.label-default(ng-if="!PIF.attached") Disconnected
span.label.label-success(ng-if="PIF.attached") Connected
span.pull-right.btn-group.quick-buttons(ng-if="canAdmin()")
i.fa.fa-unlink.fa-lg.text-danger(ng-if="PIF.disallowUnplug" tooltip="Disconnection not allowed")
i.fa.fa-unlink.fa-lg.text-danger(ng-if="!PIF.disallowUnplug && PIF.management" tooltip="Management PIF")
| &nbsp;&nbsp;
i.fa.fa-trash.fa-lg.text-danger(ng-if="PIF.disallowUnplug" tooltip="Disconnection not allowed")
i.fa.fa-trash.fa-lg.text-danger(ng-if="!PIF.disallowUnplug && PIF.management" tooltip="Management PIF")
a(tooltip="Disconnect this interface" xo-click="disconnectPIF(PIF.id)", ng-if = 'PIF.attached && !PIF.disallowUnplug && !PIF.management')
i.fa.fa-unlink.fa-lg
a(tooltip="Connect this interface" xo-click="connectPIF(PIF.id)", ng-if = '!PIF.attached')
i.fa.fa-link.fa-lg
a(tooltip="Remove this interface", xo-click="removePIF(PIF.id)", ng-if = '!PIF.physical && !PIF.disallowUnplug && !PIF.management')
i.fa.fa-trash-o.fa-lg
tr(ng-if="configuringIp")
td(colspan="7")
form.form-inline#configureIpForm(name = 'configureIpForm', ng-submit = 'addIp(PIF, newIp, newNetmask, newDns, newGateway, ipMethod); $parent.configuringIp=false', ng-init='ipMethod="Static"')
label
.form-group
input(type="radio" name="ipMethod" ng-model="ipMethod" value="Static" checked)
.form-group
| &nbsp;
span(for = 'newIp') IP:&nbsp;&nbsp;
input#newIp.form-control(type = 'text', ng-model = 'newIp', placeholder = '{{PIF.ip}}', required, ng-disabled="ipMethod !== 'Static'")
| &nbsp;&nbsp;
span(for = 'newNetmask') Netmask:&nbsp;&nbsp;
input#newNetmask.form-control(type = 'text', ng-model = 'newNetmask', placeholder = '{{PIF.netmask}}', required, ng-disabled="ipMethod !== 'Static'")
| &nbsp;&nbsp;
span(for = 'newDns') DNS:&nbsp;&nbsp;
input#newDns.form-control(type = 'text', ng-model = 'newDns', placeholder = '{{PIF.dns}}', ng-disabled="ipMethod !== 'Static'")
| &nbsp;&nbsp;
span(for = 'newGateway') Gateway:&nbsp;&nbsp;
input#newGateway.form-control(type = 'text', ng-model = 'newGateway', placeholder = '{{PIF.gateway}}', ng-disabled="ipMethod !== 'Static'")
| &nbsp;&nbsp;
br
label
.form-group
input(type="radio" name="ipMethod" ng-model="ipMethod" value="DHCP")
.form-group
| &nbsp;Use DHCP
br
label
.form-group
input(type="radio" name="ipMethod" ng-model="ipMethod" value="None")
.form-group
| &nbsp;Remove IP
br
button.btn.btn-primary(type = 'submit') OK
.text-right
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-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 physicalPifs()')
option(value = '') None
| &nbsp;
.form-group
label.control-label(for = 'newNetworkName') Name&nbsp;
input#newNetworkName.form-control(type = 'text', ng-model = 'newNetworkName', required)
| &nbsp;
.form-group
label.control-label(for = 'newNetworkDescription') Description&nbsp;
input#newNetworkDescription.form-control(type = 'text', ng-model = 'newNetworkDescription', placeholder= 'Network created with Xen Orchestra')
| &nbsp;
.form-group
label.control-label(for = 'newNetworkVlan') VLAN&nbsp;
input#newNetworkVlan.form-control(type = 'text', ng-model = 'newNetworkVlan', placeholder = 'Defaut: no VLAN')
| &nbsp;
.form-group
label(for = 'newNetworkMTU') MTU&nbsp;
input#newNetworkMTU.form-control(type = 'text', ng-model = 'newNetworkMTU', placeholder = 'Default: 1500')
| &nbsp;
.form-group
button.btn.btn-primary(type = 'submit')
i.fa.fa-plus-square
| Create
span(ng-if = 'createNetworkWaiting')
| &nbsp;
i.xo-icon-loading-sm
br
//- CPU and Logs panels
.grid-sm
//- Task panel
.panel.panel-default
.panel-heading.panel-title(ng-if="tasks | isNotEmpty")
i.fa.fa-spinner.fa-pulse
| Pending tasks
.panel-heading.panel-title(ng-if="tasks | isEmpty")
i.fa.fa-spinner
| Pending tasks
.panel-body
p.center(ng-if="tasks | isEmpty") No recent tasks
table.table.table-hover(ng-if="tasks | isNotEmpty")
th Date
th Progress
th Name
//- TODO: working reverse order, from recent to oldest
tr(ng-repeat="task in tasks | map | orderBy:'created':true track by task.id")
td.oneliner {{task.created * 1e3 | date:'medium'}}
td
.progress-condensed
.progress-bar.progress-bar-success.progress-bar-striped.active.progress-bar-black(role="progressbar", aria-valuemin="0", aria-valuenow="{{task.progress*100}}", aria-valuemax="100", style="width: {{task.progress*100}}%", tooltip="Progress: {{task.progress*100 | number:1}}%")
| {{task.progress*100 | number:1}}%
td.oneliner
| {{task.name_label}}
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)")
i.fa.fa-trash-o.fa-lg(tooltip="Destroy this task")
//- Logs panel
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-comments
| Logs
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
table.table.table-hover(ng-if="host.messages | isNotEmpty")
th Date
th Name
tr(ng-repeat="message in host.messages | map | orderBy:'-time' | slice:(5*(currentLogPage-1)):(5*currentLogPage) track by message.id")
td {{message.time*1e3 | date:"medium"}}
td
| {{message.name}}
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')
pagination(boundary-links="true", total-items="host.messages | count", ng-model="$parent.currentLogPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
.grid-sm
//- Patches panel
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-file-code-o
| 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", ng-if = 'canAdmin()')
i.fa.fa-download
.panel-body
table.table.table-hover(ng-if="poolPatches || updates")
th.col-sm-2 Name
th.col-sm-5 Description
th.col-sm-3 Applied/Released date
th.col-sm-1 Size
th.col-sm-1 Status
tr(
ng-repeat="patch in updates"
ng-if="!isPoolPatch(patch)"
)
td.oneliner {{patch.name}}
td.oneliner
a(href="{{patch.documentationUrl}}", target="_blank") {{patch.description}}
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", ng-if = 'canAdmin()')
span.label.label-danger Missing
span.label.label-danger(ng-if = '!canAdmin()') Missing
tr(ng-repeat="patch in poolPatches | map | orderBy:'-name'| slice:(5*(currentPatchPage-1)):(5*currentPatchPage)")
td.oneliner {{patch.name}}
td.oneliner {{patch.description}}
//- TODO: use a proper function for patch date, like poolPatchToHostPatch
td.oneliner {{((patch.$host_patches[0]) | resolve).time*1e3 | date:"medium"}}
td {{patch.size | bytesToSize}}
td
span(ng-if="isPoolPatchApplied(patch)")
span.label.label-success 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=">>")
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-plug
| PCI Devices
.panel-body
p.center(ng-if="!host.$PCIs") No PCI devices available
table.table.table-hover(ng-if="host.$PCIs")
th PCI Info
th Device Name
tr(ng-repeat="pci in host.$PCIs | resolve | orderBy:'pci_id' | slice:(5*(currentPCIPage-1)):(5*currentPCIPage) track by pci.id")
td.oneliner {{pci.pci_id}} ({{pci.class_name}})
td.oneliner {{pci.device_name}}
.center(ng-if = '(host.$PCIs | resolve).length > 5')
pagination(boundary-links="true", total-items="(host.$PCIs | resolve).length", ng-model="$parent.currentPCIPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-desktop
| GPUs
.panel-body
p.center(ng-if="host.$PGPUs.length === 0") No GPUs available
table.table.table-hover(ng-if="host.$PGPUs.length !== 0")
th Device
tr(ng-repeat="pgpu in host.$PGPUs | resolve | orderBy:'device' | slice:(5*(currentGPUPage-1)):(5*currentGPUPage) track by pgpu.id")
td.oneliner {{pgpu.device}}
.center(ng-if = '(host.$PGPUs | resolve).length > 5')
pagination(boundary-links="true", total-items="(host.$PGPUs | resolve).length", ng-model="$parent.currentGPUPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-asterisk
| Misc
.panel-body(style="overflow:hidden")
.row(ng-repeat="(key, value) in host.bios_strings")
label.control-label.col-sm-4
| {{key}}:
.col-sm-8 {{value}}
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-book
| License
.panel-body
.row
label.control-label.col-sm-3
| Server:
.col-sm-9 {{host.license_server.address}}:{{host.license_server.port}}
br
.row(ng-repeat="key in hostParams | slice:(10*(currentLicensePage-1)):(10*currentLicensePage) track by key")
label.control-label.col-sm-7
| {{key}}:
.col-sm-5 {{host.license_params[key]}}
.center
pagination(boundary-links="true", total-items="hostParams.length", ng-model="currentLicensePage", items-per-page="10", max-size="10", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")

View File

@@ -1,126 +0,0 @@
import angular from 'angular'
import uiRouter from 'angular-ui-router'
import xoTag from 'tag'
import includes from 'lodash.includes'
import xoApi from 'xo-api'
import view from './view'
// ===================================================================
export default angular.module('xoWebApp.list', [
uiRouter,
xoApi,
xoTag
])
.config(function ($stateProvider) {
$stateProvider.state('list', {
url: '/list',
controller: 'ListCtrl as list',
template: view
})
})
.controller('ListCtrl', function (xo, xoApi, $state, $scope, $rootScope) {
const user = xoApi.user
$scope.createButton = user.permission !== 'admin'
if (user.permission !== 'admin') {
$scope.createButton = false
xo.resourceSet.getAll()
.then(sets => {
$scope.resourceSets = sets
$scope.createButton = sets.length > 0
})
}
this.hosts = xoApi.getView('host')
this.pools = xoApi.getView('pool')
this.SRs = xoApi.getView('SR')
this.VMs = xoApi.getView('VM')
this.hostsByPool = xoApi.getIndex('hostsByPool')
this.runningHostsByPool = xoApi.getIndex('runningHostsByPool')
this.vmsByContainer = xoApi.getIndex('vmsByContainer')
$scope.canView = function (id) {
return xoApi.canInteract(id, 'view')
}
$scope.shouldAppear = (obj) => {
// States
const powerState = obj.power_state
// If there is a search option on the power state (running or halted),
// then objects that do not have a power_state (eg: SRs) are not displayed
if (($scope.states['running'] !== 2 || $scope.states['halted'] !== 2) && !powerState) return false
if (powerState) {
if ($scope.states[powerState.toLowerCase()] === 0) return false
if (($scope.states['running'] === 1 || $scope.states['halted'] === 1) &&
$scope.states[powerState.toLowerCase()] !== 1) return false
}
if ($scope.states['disconnected'] !== 2 && !obj.$PBDs) return false
let disconnected = false
if (obj.$PBDs) {
for (const id of obj.$PBDs) {
const pbd = xoApi.get(id)
disconnected |= !pbd.attached
}
if ($scope.states['disconnected'] === 0 && disconnected) return false
if ($scope.states['disconnected'] === 1 && !disconnected) return false
}
// Types
if ($scope.types[obj.type.toLowerCase()] === 0) return false
if ($scope.types[obj.type.toLowerCase()] === 2 && includes($scope.types, 1)) return false
return true
}
const _initOptions = () => {
$scope.types = {
'host': 2,
'pool': 2,
'sr': 2,
'vm': 2
}
$scope.states = {
'running': 2,
'halted': 2,
'disconnected': 2
}
}
_initOptions()
$scope.parsedListFilter = $scope.listFilter
$rootScope.searchParse = () => {
let keyWords = []
const words = $scope.listFilter ? $scope.listFilter.split(' ') : ['']
_initOptions()
for (const word of words) {
let isOption = word.charAt(0) === '*'
const isNegation = word.charAt(0) === '!'
// as long as there is a '!', it is an option. ie !vm <=> !*vm
isOption = isOption || isNegation
let option = (isNegation ? word.substring(1, word.length) : word).toLowerCase()
option = option.charAt(0) === '*' ? option.substring(1, option.length) : option
if (!isOption) {
if (option !== '') keyWords.push(option)
} else {
if ($scope.types.hasOwnProperty(option)) {
$scope.types[option] = isNegation ? 0 : 1
} else if ($scope.states.hasOwnProperty(option)) {
$scope.states[option] = isNegation ? 0 : 1
}
}
}
$scope.parsedListFilter = keyWords.join(' ')
}
$scope.onClick = (type) => {
$rootScope.options[type.toLowerCase()] = !$rootScope.options[type.toLowerCase()]
$rootScope.updateListFilter(type.toLowerCase())
}
})
// A module exports its name.
.name

View File

@@ -1,205 +0,0 @@
.sub-bar
.grid(style="margin-left:1em")
.btn-group.dropdown.col-sm-1(dropdown)
a.btn.navbar-btn.dropdown-toggle.filter(dropdown-toggle)
| Types
i.fa.fa-caret-down
ul.dropdown-menu.inverse(role="menu" style="color:white")
li(
ng-repeat = "type in ['VM', 'SR', 'Host', 'Pool']"
ng-click='onClick(type)'
)
| &nbsp
label(ng-click)
i.fa.fa-square-o(ng-if='!options[type.toLowerCase()]')
i.fa.fa-check-square-o(ng-if='options[type.toLowerCase()]')
| {{type}}
.btn-group.dropdown.col-sm-1(dropdown)
a.btn.navbar-btn.dropdown-toggle.filter(dropdown-toggle)
| States
i.fa.fa-caret-down
ul.dropdown-menu.inverse(role="menu" style="color:white")
li(
ng-repeat = "state in ['Running', 'Halted', 'Disconnected']"
ng-click='onClick(state)'
)
| &nbsp
label(ng-click)
i.fa.fa-square-o(ng-if='!options[state.toLowerCase()]')
i.fa.fa-check-square-o(ng-if='options[state.toLowerCase()]')
| {{state}}
.btn-group.col-sm-1.col-sm-offset-9(ng-if='createButton')
a.btn.navbar-btn.filter(xo-sref='VMs_new()' tooltip = 'Create VM')
i.fa.fa-desktop.text-success &nbsp
i.fa.fa-plus.text-success
//- TODO: print a message when no entries.
//- FIXME: Ugly trick to force the results to be under the sub bar.
div(style="margin-top: 50px; visibility: hidden; height: 0") .
//- If it's a (named) pool.
.grid.flat-object(
ng-repeat="pool in list.pools.all | xoHideUnauthorized | filter:parsedListFilter | orderBy:natural('name_label') track by pool.id"
ng-if="pool.name_label && shouldAppear(pool)"
xo-sref="pools_view({id: pool.id})"
)
//- Icon.
.grid-cell.flat-cell.flat-cell-type
i.xo-icon-pool
//- Properties & tags.
.grid-cell
//- Properties.
.grid-sm
.grid-cell.flat-cell.flat-cell-name
| {{pool.name_label}}
.grid-cell.flat-cell.flat-cell-description
i {{pool.name_description}}
.grid-cell.flat-cell(ng-init="default_SR = (pool.default_SR | resolve)")
div(ng-if="default_SR")
| Default SR:
a(ui-sref="SRs_view({id: default_SR.id})") {{default_SR.name_label}}
div(ng-if="!default_SR")
em No default SR.
.grid-cell.flat-cell(ng-init="master = (pool.master | resolve)")
div(ng-if="master")
| Master:
a(ui-sref="hosts_view({id: master.id})") {{master.name_label}}
div(ng-if="!master")
em Unknown master.
.grid-cell.flat-cell
div(ng-if="pool.HA_enabled")
| HA enabled
div(ng-if="!pool.HA_enabled")
| HA disabled
.grid-cell.flat-cell
| {{list.runningHostsByPool[pool.id] | count}}/{{list.hostsByPool[pool.id] | count}} hosts
//- /Properties.
//- Tags.
.grid
.grid-cell
.grid-cell.flat-cell-tag
i.fa.fa-tag &nbsp;
xo-tag(object = 'pool')
//- /Tags.
//- /Properties & tags.
//- /Pool.
//- If it's a host.
.grid.flat-object(
ng-repeat="host in list.hosts.all | xoHideUnauthorized | filter:parsedListFilter | orderBy:natural('name_label') track by host.id"
ng-if="shouldAppear(host)"
xo-sref="hosts_view({id: host.id})"
)
//- Icon.
.grid-cell.flat-cell.flat-cell-type
i.xo-icon-host(class="xo-color-{{host.power_state | lowercase}}")
//- Properties & tags.
.grid-cell
//- Properties.
.grid-sm
.grid-cell.flat-cell.flat-cell-name
| {{host.name_label}}
.grid-cell.flat-cell.flat-cell-description
i {{host.name_description}}
.grid-cell.flat-cell
| Address: {{host.address}}
//- .grid-cell.flat-cell
//- | {{host.$vCPUs}} vCPUs used on {{host.CPUs["cpu_count"]}} cores
.grid-cell.flat-cell
.progress-condensed
.progress-bar(role="progressbar", aria-valuemin="0", aria-valuenow="{{100*host.memory.usage/host.memory.size}}", aria-valuemax="100", style="width: {{[host.memory.usage, host.memory.size] | percentage}}", tooltip="RAM: {{[host.memory.usage, host.memory.size] | percentage}} allocated")
| {{[host.memory.usage, host.memory.size] | percentage}}
.grid-cell.flat-cell
| {{list.vmsByContainer[host.id] | count}} VMs running
//- /Properties.
//- Tags.
.grid
.grid-cell
.grid-cell.flat-cell-tag
i.fa.fa-tag &nbsp;
xo-tag(object = 'host')
//- /Tags.
//- /Properties & tags.
//- /Host.
//- If it's a VM.
.grid.flat-object(
ng-repeat="VM in list.VMs.all | xoHideUnauthorized | filter:parsedListFilter | orderBy:natural('name_label') track by VM.id"
ng-if="shouldAppear(VM)"
xo-sref="VMs_view({id: VM.id})"
)
//- Icon.
.grid-cell.flat-cell.flat-cell-type
i.xo-icon-vm(class="xo-color-{{VM.power_state | lowercase}}")
//- Properties & tags.
.grid-cell
//- Properties.
.grid-sm
.grid-cell.flat-cell.flat-cell-name
| {{VM.name_label}}
.grid-cell.flat-cell.flat-cell-description
i {{VM.name_description}}
.grid-cell.flat-cell
| Address: {{VM.addresses["0/ip"]}}
.grid-cell.flat-cell
| {{VM.CPUs.number}} vCPUs
.grid-cell.flat-cell
| {{VM.memory.size | bytesToSize}} RAM
.grid-cell.flat-cell(ng-init="container = (VM.$container | resolve)", ng-if="canView((VM.$container | resolve).id)")
div(ng-if="'pool' === container.type")
| Resident on:
a(ui-sref="pools_view({id: container.id})") {{container.name_label}}
div(ng-if="'host' === container.type", ng-init="pool = (container.$poolId | resolve)")
| Resident on:
a(ui-sref="hosts_view({id: container.id})") {{container.name_label}}
small(ng-if="pool.name_label && canView(pool.id)")
| (
a(ui-sref="pools_view({id: pool.id})") {{pool.name_label}}
| )
//- /Properties.
//- Tags.
.grid
.grid-cell
.grid-cell.flat-cell-tag
i.fa.fa-tag &nbsp;
xo-tag(object = 'VM')
//- /Tags.
//- /Properties & tags.
//- /VM.
//- If it's a SR.
.grid.flat-object(
ng-repeat="SR in list.SRs.all | xoHideUnauthorized | filter:parsedListFilter | orderBy:natural('name_label') track by SR.id"
ng-if="shouldAppear(SR)"
xo-sref="SRs_view({id: SR.id})"
)
//- Icon.
.grid-cell.flat-cell.flat-cell-type
i.xo-icon-sr
//- Properties & tags.
.grid-cell
//- Properties.
.grid-sm
.grid-cell.flat-cell.flat-cell-name
| {{SR.name_label}}
.grid-cell.flat-cell.flat-cell-description
i {{SR.name_description}}
.grid-cell.flat-cell
span(ng-if="SR.content_type !== 'disk'") Usage: {{[SR.physical_usage, SR.size] | percentage}} ({{SR.physical_usage | bytesToSize}}/{{SR.size | bytesToSize}})
.grid-cell.flat-cell
| Type: {{SR.SR_type}}
.grid-cell.flat-cell(ng-init="container = (SR.$container | resolve)")
div(ng-if="'pool' === container.type")
strong
| Shared on:
a(ui-sref="pools_view({id: container.id})") {{container.name_label}}
div(ng-if="'host' === container.type")
| Connected to:
a(ui-sref="hosts_view({id: container.id})") {{container.name_label}}
//- /Properties.
//- Tags.
.grid
.grid-cell
.grid-cell.flat-cell-tag
i.fa.fa-tag &nbsp;
xo-tag(object = 'SR')
//- /Tags.
//- /Properties & tags.
//- /SR.

View File

@@ -1,82 +0,0 @@
import angular from 'angular'
import forEach from 'lodash.foreach'
import find from 'lodash.find'
import uiBootstrap from 'angular-ui-bootstrap'
import xoServices from 'xo-services'
import view from './view'
// ===================================================================
export default angular.module('xoWebApp.migrateVm', [
uiBootstrap,
xoServices
])
.controller('MigrateVmCtrl', function (
$scope,
$modalInstance,
xoApi,
VDIs,
srsOnTargetPool,
srsOnTargetHost,
VIFs,
networks,
defaults,
intraPoolMigration
) {
$scope.VDIs = VDIs
$scope.SRs = srsOnTargetPool.concat(srsOnTargetHost)
$scope.VIFs = VIFs
$scope.networks = networks
$scope.intraPoolMigration = intraPoolMigration
$scope.selected = {}
$scope.selected.migrationNetwork = defaults.network
$scope.selected.vdi = {}
forEach($scope.VDIs, (vdi) => {
$scope.selected.vdi[vdi.id] = defaults.sr
})
if (!intraPoolMigration) {
$scope.selected.vif = {}
forEach($scope.VIFs, (vif) => {
const network = find($scope.networks, (network) => network.name_label === xoApi.get(vif.$network).name_label)
$scope.selected.vif[vif.id] = network
// Try to find a target network with the same name
? network.id
// Otherwise the default network
: defaults.network
})
}
$scope.migrate = function () {
$modalInstance.close($scope.selected)
}
})
.service('migrateVmModal', function ($modal, xo, xoApi) {
return function (state, id, hostId, VDIs, srsOnTargetPool, srsOnTargetHost, VIFs, networks, defaults, intraPoolMigration) {
return $modal.open({
controller: 'MigrateVmCtrl',
template: view,
resolve: {
VDIs: () => VDIs,
srsOnTargetPool: () => srsOnTargetPool,
srsOnTargetHost: () => srsOnTargetHost,
VIFs: () => VIFs,
networks: () => networks,
defaults: () => defaults,
intraPoolMigration: () => intraPoolMigration
}
}).result.then(function (selected) {
let isAdmin = xoApi.user && (xoApi.user.permission === 'admin')
state.go(isAdmin ? 'tree' : 'list')
return xo.vm.migrate(id, hostId, selected.vdi, intraPoolMigration ? undefined : selected.vif, selected.migrationNetwork)
})
}
})
// A module exports its name.
.name

View File

@@ -1,50 +0,0 @@
form(ng-submit="migrate()")
.modal-header
h3 VM migration
.modal-body
.form-inline
label Choose a network to migrate the VM:&nbsp
select.form-control(
ng-options="network.id as network.name_label for network in networks"
ng-model="selected.migrationNetwork"
)
p &nbsp
p
strong For each VDI, choose an SR:
table.table
tr
th.col-sm-5 Name
th.col-sm-7 SRs
tbody
tr(ng-repeat="vdi in VDIs")
td {{ vdi.name_label }}
td
table.table
tbody
tr
select.form-control(
ng-options="sr.id as sr.name_label for sr in SRs"
ng-model="selected.vdi[vdi.id]"
)
p(ng-if="!intraPoolMigration")
strong For each VIF, choose a target network:
table.table(ng-if="!intraPoolMigration")
tr
th.col-sm-5 VIF (network)
th.col-sm-7 Target Network
tbody
tr(ng-repeat="vif in (VIFs | orderBy:'device')")
td {{vif.MAC}} ({{(vif.$network | resolve).name_label}})
td
table.table
tbody
tr
select.form-control(
ng-options="network.id as network.name_label for network in networks"
ng-model="selected.vif[vif.id]"
)
.modal-footer
button.btn.btn-primary(type="submit")
| Migrate
button.btn.btn-warning(type="button", ng-click="$dismiss()")
| Cancel

View File

@@ -1,125 +0,0 @@
import angular from 'angular'
import uiRouter from 'angular-ui-router'
import updater from '../updater'
import xoServices from 'xo-services'
import includes from 'lodash.includes'
import view from './view'
// ===================================================================
export default angular.module('xoWebApp.navbar', [
uiRouter,
updater,
xoServices
])
.controller('NavbarCtrl', function ($state, xoApi, xo, $scope, updater, $rootScope) {
this.updater = updater
// TODO: It would make sense to inject xoApi in the scope.
Object.defineProperties(this, {
status: {
get: () => xoApi.status
},
user: {
get: () => xoApi.user
}
})
this.logIn = xoApi.logIn
this.logOut = function () {
xoApi.logOut()
}
// When a searched is entered, we must switch to the list view if
// necessary. When the text field is empty again, we must swith
// to tree view
let timeout
$scope.ensureListView = function (listFilter) {
clearTimeout(timeout)
timeout = window.setTimeout(function () {
$state.go('list').then(() =>
$rootScope.searchParse(),
$scope.updateOptions()
)
}, 400)
}
const _isOption = function (word, option) {
if (word === '*' + option || word === '!' + option || word === '!*' + option) {
return true
}
return false
}
const _removeOption = function (option) {
if (!$scope.$root.listFilter) {
return
}
const words = $scope.$root.listFilter.split(' ')
$scope.$root.listFilter = ''
for (const word of words) {
if (!_isOption(word, option) && word !== '') {
$scope.$root.listFilter += word + ' '
}
}
}
const _addOption = function (option) {
if (!$scope.$root.listFilter) {
$scope.$root.listFilter = '*' + option + ' '
return
}
const words = $scope.$root.listFilter.split(' ')
if (!includes(words, '*' + option) && !includes(words, '!' + option) && !includes(words, '!*' + option)) {
if ($scope.$root.listFilter.charAt($scope.$root.listFilter.length - 1) !== ' ') {
$scope.$root.listFilter += ' '
}
$scope.$root.listFilter += '*' + option + ' '
}
}
$rootScope.options = {
'vm': false,
'sr': false,
'host': false,
'pool': false,
'running': false,
'halted': false,
'disconnected': false
}
// Checkboxes --> Text
// Update text field after a checkbox has been clicked
$rootScope.updateListFilter = function (option) {
if ($rootScope.options[option]) {
_addOption(option)
} else {
_removeOption(option)
}
$scope.ensureListView($scope.$root.listFilter)
}
// Text --> Checkboxes
// Update checkboxes after the text field has been changed
$scope.updateOptions = function () {
const words = $scope.$root.listFilter ? $scope.$root.listFilter.split(' ') : ['']
for (const opt in $rootScope.options) {
$rootScope.options[opt] = false
for (let word of words) {
if (_isOption(word, opt)) {
$rootScope.options[opt] = true
}
}
}
}
this.tasks = xoApi.getView('runningTasks')
})
.directive('navbar', function () {
return {
restrict: 'E',
controller: 'NavbarCtrl as navbar',
template: view,
scope: {}
}
})
// A module exports its name.
.name

View File

@@ -1,145 +0,0 @@
nav.navbar.navbar-inverse.navbar-fixed-top(role = 'navigation')
//- Brand and toggle get grouped for better mobile display
.navbar-header
//- Button used to (un)collapse on mobile display.
button.navbar-toggle(type="button", ng-init="collapsed = true", ng-click="collapsed = !collapsed")
span.sr-only Toggle navigation
span.icon-bar
span.icon-bar
span.icon-bar
//- Brand name.
a.navbar-brand(ui-sref = 'index')
img.navbar-logo(src="images/logo.png")
| Xen Orchestra
//- All navbar items are collapsed on mobile display.
.collapse.navbar-collapse(ng-class="!collapsed && 'in'")
//- Search form of the navbar.
form.navbar-form.navbar-left(role="search", style="width: 250px")
//- Forced width due to issue with `input`s (https://github.com/twbs/bootstrap/issues/9950.
.input-group
input.form-control.inverse(
type = 'text'
placeholder = ''
ng-model = '$root.listFilter'
ng-change = 'ensureListView($root.listFilter)'
)
span.input-group-btn
button.btn.btn-search(
type = 'button'
ng-click = 'ensureListView($root.listFilter)'
)
i.fa.fa-search
//- /Search form.
ul.nav.navbar-nav
li
a(href="https://xen-orchestra.com/#/pricing?pk_campaign=xoa_source", target="_blank", tooltip="Source version without Pro support. Use in production at your own risk.")
i.xo-icon-info.text-danger
span.hidden-sm No Pro Support!
//- Right items of the navbar.
ul.nav.navbar-nav.navbar-right
li.navbar-text(ng-if="'disconnected' === navbar.status")
i.xo-icon-error
| Disconnected from XO-Server
li.navbar-text(ng-if="'connecting' === navbar.status")
i.fa.fa-refresh.fa-spin
| Connecting to XO-Server
//- Running tasks
li.disabled(ng-if="!navbar.tasks.size", tooltip="No running tasks")
a.dropdown-toggle.inverse
i.xo-icon-task
li.dropdown(dropdown, ng-if="navbar.tasks.size")
a.dropdown-toggle.inverse(dropdown-toggle)
i.xo-icon-task
ul.dropdown-menu.inverse
li.task-menu(
ng-repeat="task in navbar.tasks.all | map | orderBy:natural('name_label') track by task.id"
)
a(
ui-sref="hosts_view({id: task.$host})"
tooltip = "{{task.name_label}}"
)
//- i.fa.fa-spinner.fa-fw
//- | {{task.name_label}}
.progress-condensed
.progress-bar.progress-bar-success.progress-bar-striped.active.progress-bar-black(
role = "progressbar"
aria-valuemin = "0"
aria-valuenow = "{{task.progress*100}}"
aria-valuemax = "100"
style = "width: {{task.progress*100}}%"
)
| {{task.progress*100 | number:1}}%
//- Main menu.
li.dropdown(dropdown)
a.dropdown-toggle.inverse(dropdown-toggle)
i.fa.fa-th
ul.dropdown-menu.inverse
li(
ui-sref-active = 'active'
ng-class = '{ disabled: navbar.user.permission !== "admin" }'
)
a(ui-sref = 'tree')
i.fa.fa-indent
| Tree view
li(ui-sref-active="active")
a(ui-sref="list")
i.fa.fa-align-justify
| Flat view
li(
ui-sref-active="active"
ng-class = '{ disabled: navbar.user.permission !== "admin" }'
)
a(ui-sref="dashboard.index")
i.fa.fa-dashboard
| Dashboard
li.divider
li(ng-class = '{ disabled: navbar.user.permission !== "admin" }')
a(ui-sref = 'self.index')
i.fa.fa-cloud
| Self Service
li.divider
li(ng-class = '{ disabled: navbar.user.permission !== "admin" }')
a(ui-sref = 'backup.index')
i.fa.fa-archive
| Backup
li(ng-class = '{ disabled: navbar.user.permission !== "admin" }')
a(ui-sref = 'taskscheduler.index')
i.fa.fa-cogs
| Job Manager
li.divider
li(
ui-sref-active = 'active'
ng-class = '{ disabled: navbar.user.permission !== "admin" }'
)
a(ui-sref="settings.index")
i.fa.fa-cog
| Settings
li.divider
li(ui-sref-active="active")
a(ui-sref="about")
i.fa.fa-info-circle(style="color:#5bc0de")
| About
//- /Main menu.
li
a(ui-sref="settings.update")
i.fa.fa-question-circle.text-warning(ng-if = '!navbar.updater.state', tooltip = 'No update information available')
i.fa.fa-question-circle.text-info(ng-if = 'navbar.updater.state == "connected"', tooltip = 'Update information may be available')
i.fa.fa-check.text-success(ng-if = 'navbar.updater.state == "upToDate"', tooltip = 'Your XOA is up-to-date')
i.fa.fa-bell.text-primary(ng-if = 'navbar.updater.state == "upgradeNeeded"', tooltip = 'You need to update your XOA (new version is available)')
i.fa.fa-bell-slash.text-warning(ng-if = 'navbar.updater.state == "registerNeeded"', tooltip = 'Your XOA is not registered for updates')
i.fa.fa-exclamation-triangle.text-danger(ng-if = 'navbar.updater.state == "error"', tooltip = 'Can\'t fetch update information')
li
a(ng-if = '!navbar.user.provider', ui-sref="{{navbar.user.provider ? 'settings.users' : 'settings.user'}}", tooltip="{{navbar.user.email}}")
i.fa.fa-user
span.hidden-sm {{navbar.user.email}}
li
a(ng-click = 'navbar.logOut()')
i.fa.fa-sign-out
| &nbsp;
| &nbsp;
//- /Right items.
//- /Navbar items.
//- /Navbar.

View File

@@ -1,445 +0,0 @@
import angular from 'angular'
import Bluebird from 'bluebird'
import forEach from 'lodash.foreach'
import uiRouter from 'angular-ui-router'
import view from './view'
import _indexOf from 'lodash.indexof'
// ===================================================================
export default angular.module('xoWebApp.newSr', [
uiRouter
])
.config(function ($stateProvider) {
$stateProvider.state('SRs_new', {
url: '/srs/new/:container',
controller: 'NewSrCtrl as newSr',
template: view
})
})
.controller('NewSrCtrl', function ($scope, $state, $stateParams, xo, xoApi, notify, modal, bytesToSizeFilter) {
this.reset = function (data = {}) {
this.data = {}
delete this.lockCreation
this.lock = !(
(data.srType === 'Local') &&
(data.srPath && data.srPath.path) ||
data.srType === 'SMB'
)
}
this.resetLists = function () {
delete this.data.nfsList
delete this.data.scsiList
delete this.lockCreation
this.lock = true
this.resetErrors()
}
this.resetErrors = function () {
delete this.data.error
}
/*
* Loads NFS paths and iScsi iqn`s
*/
this.populateSettings = function (type, server, auth, user, password) {
this.reset()
this.loading = true
server = this._parseAddress(server)
if (type === 'NFS' || type === 'NFS_ISO') {
xoApi.call('sr.probeNfs', {
host: this.container.id,
server: server.host
})
.then(response => this.data.paths = response)
.catch(error => notify.warning({
title: 'NFS Detection',
message: error.message
}))
.finally(() => this.loading = false)
} else if (type === 'iSCSI') {
let params = {
host: this.container.id
}
if (auth) {
params.chapUser = user
params.chapPassword = password
}
params.target = server.host
if (server.port) {
params.port = server.port
}
xoApi.call('sr.probeIscsiIqns', params)
.then(response => {
if (response.length > 0) {
this.data.iqns = response
} else {
notify.warning({
title: 'iSCSI Detection',
message: 'No IQNs found'
})
}
})
.catch(error => notify.warning({
title: 'iSCSI Detection',
message: error.message
}))
.finally(() => this.loading = false)
} else {
this.loading = false
}
}
/*
* Loads iScsi LUNs
*/
this.populateIScsiIds = function (iqn, auth, user, password) {
delete this.data.iScsiIds
this.loading = true
let params = {
host: this.container.id,
target: iqn.ip,
targetIqn: iqn.iqn
}
if (auth) {
params.chapUser = user
params.chapPassword = password
}
xoApi.call('sr.probeIscsiLuns', params)
.then(response => {
forEach(response, item => {
item.display = 'LUN ' + item.id + ': ' +
item.serial + ' ' + bytesToSizeFilter(item.size) +
' (' + item.vendor + ')'
})
this.data.iScsiIds = response
})
.catch(error => notify.warning({
title: 'LUNs Detection',
message: error.message
}))
.finally(() => this.loading = false)
}
this._parseAddress = function (address) {
let index = address.indexOf(':')
let port = false
let host = address
if (index > -1) {
port = address.substring(index + 1)
host = address.substring(0, index)
}
return {
host,
port
}
}
this._prepareNfsParams = function (data) {
let server = this._parseAddress(data.srServer)
let params = {
host: this.container.id,
nameLabel: data.srName,
nameDescription: data.srDesc,
server: server.host,
serverPath: data.srPath.path
}
return params
}
this._prepareScsiParams = function (data) {
let params = {
host: this.container.id,
nameLabel: data.srName,
nameDescription: data.srDesc,
target: data.srIqn.ip,
targetIqn: data.srIqn.iqn,
scsiId: data.srIScsiId.scsiId
}
let server = this._parseAddress(data.srServer)
if (server.port) {
params.port = server.port
}
if (data.srAuth) {
params.chapUser = data.srChapUser
params.chapPassword = data.srChapPassword
}
return params
}
this.createSR = function (data) {
this.lock = true
this.creating = true
let operationToPromise
switch (data.srType) {
case 'NFS':
let nfsParams = this._prepareNfsParams(data)
operationToPromise = this._checkNfsExistence(nfsParams)
.then(() => xoApi.call('sr.createNfs', nfsParams))
break
case 'iSCSI':
let scsiParams = this._prepareScsiParams(data)
operationToPromise = this._checkScsiExistence(scsiParams)
.then(() => xoApi.call('sr.createIscsi', scsiParams))
break
case 'lvm':
let device = data.srDevice.device
operationToPromise = xoApi.call('sr.createLvm', {
host: this.container.id,
nameLabel: data.srName,
nameDescription: data.srDesc,
device
})
break
case 'Local':
operationToPromise = xoApi.call('sr.createIso', {
host: this.container.id,
nameLabel: data.srName,
nameDescription: data.srDesc,
type: 'local',
path: data.srPath.path
})
break
case 'NFS_ISO':
let server = this._parseAddress(data.srServer || '')
const path = (
data.srType === 'NFS_ISO'
? server.host + ':'
: ''
) + data.srPath.path
operationToPromise = xoApi.call('sr.createIso', {
host: this.container.id,
nameLabel: data.srName,
nameDescription: data.srDesc,
type: 'nfs',
path
})
break
case 'SMB':
operationToPromise = xoApi.call('sr.createIso', {
host: this.container.id,
nameLabel: data.srName,
nameDescription: data.srDesc,
type: 'smb',
path: data.srServer,
user: data.user,
password: data.password
})
break
default:
operationToPromise = Bluebird.reject({message: 'Unhanled SR Type'})
break
}
operationToPromise
.then(id => {
$state.go('SRs_view', {id})
})
.catch(error => {
notify.error({
title: 'Storage Creation Error',
message: error.message
})
})
.finally(() => {
this.lock = false
this.creating = false
})
}
this._checkScsiExistence = function (params) {
this.resetLists()
return xoApi.call('sr.probeIscsiExists', params)
.then(response => {
if (response.length > 0) {
this.data.scsiList = response
return modal.confirm({
title: 'Previous LUN Usage',
message: 'This LUN has been previously used as a Storage by a XenServer host. All data will be lost if you choose to continue the SR creation. Are you sure?'
})
}
return true
})
}
this._checkNfsExistence = function (params) {
this.resetLists()
return xoApi.call('sr.probeNfsExists', params)
.then(response => {
if (response.length > 0) {
this.data.nfsList = response
return modal.confirm({
title: 'Previous Path Usage',
message: 'This path has been previously used as a Storage by a XenServer host. All data will be lost if you choose to continue the SR creation. Are you sure?'
})
}
return true
})
}
const hostsByPool = xoApi.getIndex('hostsByPool')
const srsByContainer = xoApi.getIndex('srsByContainer')
this._gatherConnectedUuids = function () {
const srIds = []
// Shared SRs.
forEach(srsByContainer[this.container.$poolId], sr => {
srIds.push(sr.id)
})
// Local SRs.
forEach(hostsByPool[this.container.$poolId], host => {
forEach(srsByContainer[host.id], sr => {
srIds.push(sr.id)
})
})
return srIds
}
this._processSRList = function (list) {
let inUse = false
let SRs = this._gatherConnectedUuids()
forEach(list, item => {
inUse = (item.used = _indexOf(SRs, item.uuid) > -1) || inUse
})
this.lockCreation = inUse
return list
}
this.loadScsiList = function (data) {
this.resetLists()
this.loading = true
let params = this._prepareScsiParams(data)
xoApi.call('sr.probeIscsiExists', params)
.then(response => {
if (response.length > 0) {
this.data.scsiList = this._processSRList(response)
}
this.lock = !Boolean(data.srIScsiId)
})
.catch(error => {
notify.error({
title: 'iSCSI Error',
message: error.message
})
})
.finally(() => this.loading = false)
}
this.loadNfsList = function (data) {
this.resetLists()
let server = this._parseAddress(data.srServer)
xoApi.call('sr.probeNfsExists', {
host: this.container.id,
server: server.host,
serverPath: data.srPath.path
})
.then(response => {
if (response.length > 0) {
this.data.nfsList = this._processSRList(response)
}
this.lock = !Boolean(data.srPath.path)
})
.catch(error => {
notify.error({
title: 'NFS error',
message: error.message
})
})
}
this.reattachNfs = function (uuid, {name, nameError}, {desc, descError}, iso) {
this._reattach(uuid, 'nfs', {name, nameError}, {desc, descError}, iso)
}
this.reattachIScsi = function (uuid, {name, nameError}, {desc, descError}) {
this._reattach(uuid, 'iscsi', {name, nameError}, {desc, descError})
}
this._reattach = function (uuid, type, {name, nameError}, {desc, descError}, iso = false) {
this.resetErrors()
let method = 'sr.reattach' + (iso ? 'Iso' : '')
if (nameError || descError) {
this.data.error = {
name: nameError,
desc: descError
}
notify.warning({
title: 'Missing parameters',
message: 'Complete the General section information, please'
})
} else {
this.lock = true
this.attaching = true
xoApi.call(method, {
host: this.container.id,
uuid,
nameLabel: name,
nameDescription: desc,
type
})
.then(id => {
$state.go('SRs_view', {id})
})
.catch(error => notify.error({
title: 'reattach',
message: error.message
}))
.finally(() => {
this.lock = false
this.attaching = false
})
}
}
this.reset()
$scope.$watch(() => xoApi.get($stateParams.container), container => {
this.container = container
})
})
// A module exports its name.
.name

View File

@@ -1,201 +0,0 @@
.grid
.panel.panel-default
p.page-title
i.xo-icon-sr
| Add SR on&nbsp;
a(ng-if="'pool' === newSr.container.type", ui-sref="pools_view({id: newSr.container.id})")
| {{newSr.container.name_label}}
a(ng-if="'host' === newSr.container.type", ui-sref="hosts_view({id: newSr.container.id})")
| {{newSr.container.name_label}}
form.form-horizontal(name = 'srForm' ng-submit="newSr.createSR(formData)")
.grid
//- Choose SR type panel
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-info-circle
| General
.panel-body
.form-group
label.col-sm-3.control-label Type
.col-sm-9
select.form-control(ng-change = 'newSr.reset(formData)', ng-model = 'formData.srType', name = 'srType', ng-required = 'true')
option(value="") -- Choose a type of SR --
optgroup(label="VDI SR")
option(value="NFS") NFS
option(value="iSCSI") iSCSI
option(value="lvm") Local LVM
optgroup(label="ISO SR")
option(value="Local") Local
option(value="NFS_ISO") NFS ISO
option(value="SMB") SMB
.form-group(ng-class = '{"has-error": newSr.data.error.name}')
label.col-sm-3.control-label Name
.col-sm-9
input.form-control(type="text", placeholder="", name = 'srName', ng-model = 'formData.srName', ng-required = 'true')
.form-group(ng-class = '{"has-error": newSr.data.error.desc}')
label.col-sm-3.control-label Description
.col-sm-9
input.form-control(type="text", placeholder="SR Created by Xen Orchestra", name = 'srDesc', ng-model = 'formData.srDesc', ng-required = 'true')
//- Choose SR details
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-cogs
| Settings
.panel-body
.form-group(ng-if = 'formData.srType === "NFS" || formData.srType === "iSCSI" || formData.srType === "NFS_ISO"')
label.col-sm-3.control-label
| Server
span(ng-if = 'formData.srType === "iSCSI"')
| &nbsp;(auth&nbsp;
input(type = 'checkbox', ng-model = 'formData.srAuth')
| &nbsp;)
.col-sm-9
.input-group
input.form-control(type="text", placeholder='address{{ formData.srType === "iSCSI" ? "[:port]" : "" }}', name = 'srServer', ng-model = 'formData.srServer', required)
span.input-group-btn
button.btn.btn-default(type = 'button', ng-click = 'newSr.populateSettings(formData.srType, formData.srServer, formData.srAuth, formData.srChapUser, formData.srChapPassword)')
i.fa.fa-search
//- For SMB
.form-group(ng-if='formData.srType === "SMB"')
label.col-sm-3.control-label
| Server
.col-sm-9
input.form-control(type="text", name='srServer', ng-model='formData.srServer', placeholder='\\\\\\\\<server>\\\\<path>' required)
.form-group(ng-if='formData.srType === "SMB"')
label.col-sm-3.control-label
| User
.col-sm-9
input.form-control(type="text", name='user', ng-model='formData.user', required)
.form-group(ng-if='formData.srType === "SMB"')
label.col-sm-3.control-label
| Password
.col-sm-9
input.form-control(type="password", name='password', ng-model='formData.password', required)
//- For Local LVM
.form-group(ng-if = 'formData.srType === "lvm"')
label.col-sm-3.control-label Device
.col-sm-9
input.form-control(
ng-if = 'formData.srType === "lvm"'
type = 'text'
name = 'srDevice'
ng-model = 'formData.srDevice.device'
placeholder = 'Device, e.g /dev/sda...'
ng-change = 'newSr.lock = !formData.srDevice.device'
required
)
.form-group(ng-if = 'newSr.data.paths || formData.srType === "Local"')
label.col-sm-3.control-label Path
.col-sm-9
//- For NFS
select.form-control(
ng-if = 'newSr.data.paths'
name = 'srPath'
ng-change = 'newSr.loadNfsList(formData)'
ng-model = 'formData.srPath'
ng-options = 'item.path for item in newSr.data.paths', required)
option(value = '', disabled) -- Choose path --
//- For Local
input.form-control(
ng-if = 'formData.srType === "Local"'
type = 'text'
name = 'srPath'
ng-model = 'formData.srPath.path'
ng-change = 'newSr.lock = !formData.srPath.path'
required
)
//- For iScsi
.form-group(ng-if = 'formData.srType === "iSCSI"')
.col-sm-9.col-sm-offset-3.form-inline(ng-if = 'formData.srAuth')
label.sr-only(for = 'chapUser') User
input#chapUser.form-control(type = 'text', ng-model = 'formData.srChapUser', placeholder = 'user', ng-required = 'formData.srAuth')
| &ensp;
label.sr-only(for = 'chapUser') Password
input#chapPassword.form-control(type = 'password', ng-model = 'formData.srChapPassword', placeholder = 'password', ng-required = 'formData.srAuth')
.form-group(ng-if = 'newSr.data.iqns')
label.col-sm-3.control-label IQN
.col-sm-9
select.form-control(ng-change = 'newSr.populateIScsiIds(formData.srIqn, formData.srAuth, formData.srChapUser, formData.srChapPassword)', name = 'srIqn', ng-model = 'formData.srIqn', ng-options = '(item.iqn + " (" + item.ip + ")") for item in newSr.data.iqns', required)
option(value = '', disabled) -- Choose IQN --
.form-group(ng-if = 'newSr.data.iScsiIds')
label.col-sm-3.control-label LUN
.col-sm-9
select.form-control(name = 'srIScsiId', ng-change = 'newSr.loadScsiList(formData)', ng-model = 'formData.srIScsiId', ng-options = 'item.display for item in newSr.data.iScsiIds', required)
option(value = '', disabled) -- Choose LUN --
.form-group.text-center(ng-if = 'newSr.loading')
i.xo-icon-loading
.grid(ng-if = 'newSr.data.nfsList && newSr.data.nfsList.length > 0')
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-eye
| NFS storage use
.panel-body
table.table.table-condensed
tr
th.text-center Storage UUID
th
tr(ng-repeat = 'nfsSr in newSr.data.nfsList')
td.text-center {{ nfsSr.uuid }}
td.text-center(ng_if = '!nfsSr.used')
button.btn.btn-sm.btn-primary(type = 'button', ng-class = '{disabled: newSr.lock}', ng-click = 'newSr.reattachNfs(nfsSr.uuid, {name: formData.srName, nameError: srForm.srName.$error.required}, {desc: formData.srDesc, descError: srForm.srDesc.$error.required}, "NFS_ISO" === formData.srType)') Reattach
td.text-center(ng_if = 'nfsSr.used', ng-class = '{disabled: newSr.lock}')
button.btn.btn-sm.btn-danger(ui-sref = 'SRs_view({id: nfsSr.uuid})', ng-class = '{disabled: newSr.lock}')
i.fa.fa-eye
| In use
p.text-center(ng-if = 'newSr.attaching')
i.xo-icon-loading
.grid(ng-if = 'newSr.data.scsiList && newSr.data.scsiList.length > 0')
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-eye
| iSCSI storage use
.panel-body
table.table.table-condensed
tr
th.text-center Storage UUID
th
tr(ng-repeat = 'scsiSr in newSr.data.scsiList')
td.text-center {{ scsiSr.uuid }}
td.text-center(ng_if = '!scsiSr.used')
button.btn.btn-sm.btn-primary(type = 'button', ng-class = '{disabled: newSr.lock}', ng-click = 'newSr.reattachIScsi(scsiSr.uuid, {name: formData.srName, nameError: srForm.srName.$error.required}, {desc: formData.srDesc, descError: srForm.srDesc.$error.required})') Reattach
td.text-center(ng_if = 'scsiSr.used')
button.btn.btn-sm.btn-danger(ui-sref = 'SRs_view({id: scsiSr.uuid})', ng-class = '{disabled: newSr.lock}')
i.fa.fa-eye
| In use
p.text-center(ng-if = 'newSr.attaching')
i.xo-icon-loading
//- Summary
.grid
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-flag-checkered
| Summary
.panel-body
.grid
.grid-cell
p.stat-name
| Name:
p.center.big {{formData.srName}}
.grid-cell
p.stat-name
| Type:
p.center.big {{formData.srType}}
.grid-cell
div(ng-if = 'formData.srType === "iSCSI"')
p.stat-name Size
p.center.big {{formData.srIScsiId.size | bytesToSize}}
div(ng-if = 'formData.srType === "NFS"')
p.stat-name Path
p.center.big {{formData.srPath.path}}
p.center
button.btn.btn-lg.btn-primary(type="submit", ng-disabled = 'newSr.lock || newSr.lockCreation')
i.fa.fa-play
| &nbsp;Create SR&nbsp;
i.xo-icon-loading-sm(ng-if = 'newSr.creating')

View File

@@ -1,588 +0,0 @@
angular = require 'angular'
cloneDeep = require 'lodash.clonedeep'
filter = require 'lodash.filter'
forEach = require 'lodash.foreach'
trim = require 'lodash.trim'
includes = require 'lodash.includes'
forEach = require 'lodash.foreach'
#=====================================================================
module.exports = angular.module 'xoWebApp.newVm', [
require 'angular-ui-router'
]
.config ($stateProvider) ->
$stateProvider.state 'VMs_new',
url: '/vms/new/:container'
controller: 'NewVmsCtrl as ctrl'
template: require './view'
.controller 'NewVmsCtrl', (
$scope, $stateParams, $state
xoApi, xo
bytesToSizeFilter, sizeToBytesFilter
notify
) ->
$scope.min = Math.min
user = xoApi.user
$scope.isAdmin = user.permission == 'admin'
userGroups = user.groups
if !$scope.isAdmin
$scope.resourceSets = []
$scope.userResourceSets = []
$scope.resourceSet = ''
xo.resourceSet.getAll()
.then (sets) ->
$scope.resourceSets = sets
$scope.resourceSet = $scope.resourceSets[0]
$scope.updateResourceSet($scope.resourceSet)
$scope.updateResourceSet = (resourceSet) ->
$scope.resourceSet = resourceSet
$scope.template = ''
$scope.templates = []
$scope.writable_SRs = []
$scope.ISO_SRs = []
srs = []
$scope.resourceSetNetworks = []
$scope.pools = []
forEach $scope.resourceSet.objects, (id) ->
obj = xoApi.get id
if obj.type is 'VM-template'
$scope.templates.push(obj)
else if obj.type is 'SR'
srs.push(obj)
else if obj.type is 'network'
$scope.resourceSetNetworks.push(obj)
$scope.writable_SRs = filter(srs, (sr) => sr.content_type isnt 'iso')
$scope.ISO_SRs = filter(srs, (sr) => sr.content_type is 'iso')
$scope.multipleVmsActive = false
$scope.vmsNames = ['VM1', 'VM2']
$scope.numberOfVms = 1
$scope.newNumberOfVms = 2
$scope.checkNumberOfVms = ->
if $scope.newNumberOfVms && Number.isInteger($scope.newNumberOfVms)
$scope.newNumberOfVms = $scope.numberOfVms = Math.min(100,Math.max(2,$scope.newNumberOfVms))
else
$scope.newNumberOfVms = $scope.numberOfVms = 2
$scope.refreshNames = ->
$scope.defaultName = 'VM'
$scope.defaultName = $scope.name_label if $scope.name_label
forEach($scope.vmsNames, (name, index) ->
$scope.vmsNames[index] = $scope.defaultName + (index+1)
)
$scope.toggleBootAfterCreate = ->
$scope.bootAfterCreate = false if $scope.multipleVmsActive
$scope.configDriveActive = false
existingDisks = {}
$scope.saveChange = (position, propertyName, value) ->
if not existingDisks[position]?
existingDisks[position] = {}
existingDisks[position][propertyName] = value
$scope.updateVdiSize = (position) ->
$scope.saveChange(position, 'size', bytesToSizeFilter(sizeToBytesFilter($scope.existingDiskSizeValues[position] + ' ' + $scope.existingDiskSizeUnits[position])))
$scope.updateTotalDiskBytes()
$scope.initExistingValues = (template) ->
$scope.name_label = template.name_label
sizes = {}
$scope.templateVBDs = []
$scope.existingDiskSizeValues = {}
$scope.existingDiskSizeUnits = {}
forEach xoApi.get(template.$VBDs), (VBD) ->
if VBD.is_cd_drive or not VBD.VDI? or not (VDI = xoApi.get(VBD.VDI))?
return
$scope.templateVBDs.push(VBD)
sizes[VBD.position] = bytesToSizeFilter VDI.size
$scope.existingDiskSizeValues[VBD.position] = +sizes[VBD.position].split(' ')[0]
$scope.existingDiskSizeUnits[VBD.position] = sizes[VBD.position].split(' ')[1]
$scope.VIFs.length = 0
if template.VIFs.length
forEach xoApi.get(template.VIFs), (VIF) ->
network = xoApi.get(VIF.$network)
$scope.addVIF(network)
return
else $scope.addVIF()
$scope.memory = template.memory.size
{get} = xoApi
removeItems = do ->
splice = Array::splice.call.bind Array::splice
(array, index, n) -> splice array, index, n ? 1
merge = do ->
push = Array::push.apply.bind Array::push
(args...) ->
result = []
for arg in args
push result, arg if arg?
result
pool = default_SR = null
host = null
poolHosts = null
hostsSrs = null
do (
networks = xoApi.getIndex('networksByPool')
srsByContainer = xoApi.getIndex('srsByContainer')
vmTemplatesByContainer = xoApi.getIndex('vmTemplatesByContainer')
poolSrs = null
hostSrs = null
poolTemplates = null
hostTemplates = null
) ->
Object.defineProperties($scope, {
networks: {
get: () => pool && networks[pool.id]
}
})
$scope.updateSrs = () =>
srs = []
$scope.selectedLocalSrs = {}
Object.defineProperty($scope.selectedLocalSrs, "size", {
value: 0,
writable: true,
enumerable: false
})
$scope.forcedHost = undefined
poolSrs and forEach(poolSrs, (sr) => srs.push(sr))
hostSrs and forEach(hostSrs, (sr) => srs.push(sr))
poolHosts and forEach(poolHosts, (host) =>
forEach(hostsSrs[host.id], (sr) ->
srs.push(sr))
)
if pool or $scope.resourceSet
selectedSrs = []
forEach($scope.templateVBDs, (vbd) ->
selectedSrs.push(xoApi.get(vbd.VDI).$SR)
)
forEach($scope.VDIs, (vdi) ->
selectedSrs.push(vdi.SR)
)
if $scope.resourceSet
forEach(selectedSrs, (sr) ->
sr = xoApi.get sr
container = xoApi.get sr.$container
if container.type is 'host'
if not $scope.selectedLocalSrs[sr.$container]
$scope.selectedLocalSrs[sr.$container] = []
$scope.selectedLocalSrs.size++
$scope.forcedHost = sr.$container
if not includes($scope.selectedLocalSrs[sr.$container], sr.id)
$scope.selectedLocalSrs[sr.$container].push(sr.id)
)
else
forEach(poolHosts, (host) ->
forEach(hostsSrs[host.id], (sr) ->
if includes(selectedSrs, sr.id)
if not $scope.selectedLocalSrs[host.id]
$scope.selectedLocalSrs[host.id] = []
$scope.selectedLocalSrs.size++
$scope.forcedHost = host.id
if not includes($scope.selectedLocalSrs[host.id], sr.id)
$scope.selectedLocalSrs[host.id].push(sr.id)
)
)
if not $scope.resourceSet
$scope.writable_SRs = filter(srs, (sr) => sr.content_type isnt 'iso')
$scope.ISO_SRs = filter(srs, (sr) => sr.content_type is 'iso')
updateTemplates = () =>
templates = []
poolTemplates and forEach(poolTemplates, (template) => templates.push(template))
hostTemplates and forEach(hostTemplates, (template) => templates.push(template))
$scope.templates = templates
$scope.$watchCollection(
() => pool and srsByContainer[pool.id],
(srs) =>
poolSrs = srs
$scope.updateSrs()
)
$scope.$watchCollection(
() => host and srsByContainer[host.id],
(srs) =>
hostSrs = srs
$scope.updateSrs()
)
$scope.$watchCollection(
() => pool and vmTemplatesByContainer[pool.id],
(templates) =>
poolTemplates = templates
updateTemplates()
)
$scope.$watchCollection(
() => host and vmTemplatesByContainer[host.id],
(templates) =>
hostTemplates = templates
updateTemplates()
)
$scope.$watch(
-> get $stateParams.container
(container) ->
$scope.container = container
# If the container was not found, no need to continue.
return unless container?
if container.type is 'host'
host = container
pool = (get container.$poolId) ? {}
poolHosts = []
hostsSrs = {}
else
host = {}
pool = container
objects = filter(xoApi.all, (obj) -> obj.type is 'host' or obj.type is 'SR')
poolHosts = filter(objects, (obj) -> obj.type is 'host' and obj.$poolId is pool.id)
hostsSrs = {}
forEach(poolHosts, (host) ->
hostsSrs[host.id] = filter(objects, (obj) -> obj.type is 'SR' and obj.$container is host.id)
)
default_SR = get pool.default_SR
default_SR = if default_SR then default_SR.id else ''
)
$scope.availableMethods = {}
$scope.CPUs = ''
$scope.pv_args = ''
$scope.installation_cdrom = ''
$scope.installation_method = ''
$scope.installation_network = ''
$scope.memory = null
$scope.memoryValue = null
$scope.units = ['MiB', 'GiB', 'TiB']
$scope.memoryUnit = $scope.units[1]
$scope.name_description = 'Created by XO'
$scope.name_label = ''
$scope.template = ''
$scope.totalDiskBytes = 0
$scope.firstSR = ''
$scope.VDIs = []
$scope.VIFs = []
$scope.isDiskTemplate = false
$scope.cloudConfigSshKey = ''
$scope.cloudConfigCustom = '#cloud-config\n#hostname: myhostname\n#ssh_authorized_keys:\n# - ssh-rsa <myKey>\n#packages:\n# - htop\n'
$scope.cloudConfigLoading = false
$scope.cloudConfigError = false
$scope.bootAfterCreate = true
$scope.updateMemory = ->
if $scope.memoryValue
$scope.memory = sizeToBytesFilter $scope.memoryValue + ' ' + $scope.memoryUnit
else
$scope.memory = $scope.template.memory.size
$scope.updateMemoryUnit = (memoryUnit) ->
$scope.memoryUnit = memoryUnit
$scope.updateMemory()
$scope.updateTotalDiskBytes = ->
$scope.totalDiskBytes = 0
forEach $scope.existingDiskSizeValues, (value, key) ->
$scope.totalDiskBytes += sizeToBytesFilter value + ' ' + $scope.existingDiskSizeUnits[key]
forEach $scope.VDIs, (VDI) ->
$scope.totalDiskBytes += (sizeToBytesFilter VDI.sizeValue + ' ' + VDI.sizeUnit) || 0
$scope.addVIF = do ->
id = 0
(network = '') ->
$scope.VIFs.push {
id: id++
network
}
$scope.removeVIF = (index) -> removeItems $scope.VIFs, index
$scope.moveVDI = (index, direction) ->
{VDIs} = $scope
newIndex = index + direction
[VDIs[index], VDIs[newIndex]] = [VDIs[newIndex], VDIs[index]]
$scope.removeVDI = (index) ->
removeItems $scope.VDIs, index
$scope.updateTotalDiskBytes()
VDI_id = 0
$scope.addVDI = ->
$scope.VDIs.push {
id: VDI_id++
bootable: false
name_label: $scope.name_label + '_disk' + (VDI_id - 1)
name_description: 'Created by XO'
size: ''
sizeValue: ''
sizeUnit: $scope.units[1]
SR: default_SR || $scope.writable_SRs[0] && $scope.writable_SRs[0].id
type: 'system'
}
$scope.updateSrs()
$scope.$watch('name_label', (newName, oldName) ->
forEach $scope.VDIs, (vdi, index) ->
if vdi.name_label is oldName + '_disk' + index
vdi.name_label = newName + '_disk' + index
)
# When the selected template changes, updates other variables.
$scope.$watch 'template', (template) ->
return unless template
# After each template change, initialize coreOsCloudConfig to empty
$scope.coreOsCloudConfig = ''
# Fetch the PV args
$scope.pv_args = template.PV_args
{install_methods} = template.template_info
availableMethods = $scope.availableMethods = Object.create null
for method in install_methods
availableMethods[method] = true
if install_methods.length is 1 # FIXME: does not work with network.
$scope.installation_method = install_methods[0]
else
delete $scope.installation_method
delete $scope.installation_method
delete $scope.installation_network
# if the template already have a configured install repository
installRepository = template.template_info.install_repository
if installRepository
if installRepository is 'cdrom'
$scope.installation_method = 'cdrom'
else
$scope.installation_network = template.template_info.install_repository
$scope.installation_method = 'network'
VDIs = $scope.VDIs = cloneDeep template.template_info.disks
forEach VDIs, (vdi, index) ->
vdi.name_label = $scope.name_label + '_disk' + index
vdi.name_description = 'Created by XO'
# if the template has no config disk
# nor it's Other install media (specific case)
if VDIs.length is 0 and template.name_label isnt 'Other install media'
$scope.isDiskTemplate = true
else $scope.isDiskTemplate = false
for VDI in VDIs
VDI.id = VDI_id++
VDI.SR or= default_SR || $scope.writable_SRs[0] && $scope.writable_SRs[0].id
VDI.size = bytesToSizeFilter VDI.size
VDI.sizeValue = if VDI.size then +VDI.size.split(' ')[0] else null
VDI.sizeUnit = VDI.size.split(' ')[1]
# if the template is labeled CoreOS
# we'll use config drive setup
if template.name_label == 'CoreOS'
return xo.vm.getCloudInitConfig template.id
.then (result) ->
$scope.coreOsCloudConfig = result
$scope.updateTotalDiskBytes()
$scope.updateSrs()
$scope.uploadCloudConfig = (file) ->
$scope.cloudConfigError = false
return unless file
reader = new FileReader()
reader.onerror = () ->
$scope.cloudConfigError = true
reader.onload = (event) ->
$scope.cloudConfigCustom = event.target.result
reader.onloadend = (event) ->
$scope.cloudConfigLoading = false
if file.size > 2e6
reader.onerror()
return
$scope.cloudConfigLoading = true
reader.readAsText(file)
$scope.createVMs = ->
if !$scope.multipleVmsActive
$scope.createVM($scope.name_label)
return
forEach($scope.vmsNames, (name) ->
$scope.createVM(name)
)
# Send the client on the tree view
$state.go 'index'
xenDefaultWeight = 256
$scope.weightMap = {
'Quarter (1/4)': xenDefaultWeight / 4,
'Half (1/2)': xenDefaultWeight / 2,
'Normal': xenDefaultWeight,
'Double (x2)': xenDefaultWeight * 2
}
$scope.createVM = (name_label) ->
{
resourceSet
CPUs
cpuWeight
pv_args
installation_cdrom
installation_method
installation_network
memoryValue
memoryUnit
name_description
template
VDIs
VIFs
} = $scope
forEach VDIs, (vdi) ->
vdi.size = bytesToSizeFilter(sizeToBytesFilter(vdi.sizeValue + ' ' + vdi.sizeUnit))
# Does not edit the displayed data directly.
VDIs = cloneDeep VDIs
for VDI, index in VDIs
# store the first VDI's SR for later use (e.g: coreOsCloudConfig)
if VDI.id == 0
$scope.firstSR = VDI.SR or default_SR
# Removes the dummy identifier used for AngularJS.
delete VDI.id
# Adds the device number based on the index.
VDI.device = "#{index}"
# Default VDI name and description
VDI.name_label = VDI.name_label || name_label + '_disk' + index
VDI.name_description = VDI.name_description || 'Created by XO'
# TODO: handles invalid values.
forEach existingDisks, (disk, index) ->
if disk.name_label is ''
delete disk.name_label
if disk.name_description is ''
delete disk.name_description
# Does not edit the displayed data directly.
VIFs = cloneDeep VIFs
for VIF in VIFs
# Removes the dummy identifier used for AngularJS.
delete VIF.id
# xo-server expects a network id, not the whole object
VIF.network = VIF.network.id
# Removes the mac address if empty.
if 'mac' of VIF
VIF.mac = trim(VIF.mac)
delete VIF.mac unless VIF.mac
if installation_method is 'cdrom'
installation = {
method: 'cdrom'
repository: installation_cdrom
}
else if installation_network
matches = /^(http|ftp|nfs)/i.exec installation_network
throw new Error 'invalid network URL' unless matches
installation = {
method: matches[1].toLowerCase()
repository: installation_network
}
else if installation_method is 'pxe'
installation = {
method: 'network'
repository: 'pxe'
}
else
installation = undefined
data = {
resourceSet: resourceSet && resourceSet.id
installation
pv_args
name_label
template: template.id
VDIs
VIFs
existingDisks
}
# TODO:
# - disable the form during creation
# - indicate the progress of the operation
notify.info {
title: 'VM creation'
message: 'VM creation started'
}
$scope.creatingVM = true
id = null
xoApi.call('vm.create', data)
.then (id_) ->
id = id_
# If nothing to sets, just stops.
return unless CPUs or name_description or memoryValue
data = {
id
}
data.CPUs = +CPUs if CPUs
if cpuWeight
data.cpuWeight = cpuWeight
if name_description
data.name_description = name_description
if pv_args
data.pv_args = pv_args
if memoryValue
# FIXME: handles invalid entries.
data.memory = memoryValue + ' ' + memoryUnit
return xo.vm.set(data)
.then () ->
# If a CloudConfig drive needs to be created
if $scope.coreOsCloudConfig
# Use the CoreOS specific Cloud Config creation
return xo.vm.createCloudInitConfigDrive(id, $scope.firstSR, $scope.coreOsCloudConfig, true).then ->
return xo.docker.register(id)
if $scope.configDriveActive
# User creation is less universal...
# $scope.cloudContent = '#cloud-config\nhostname: ' + name_label + '\nusers:\n - name: olivier\n sudo: ALL=(ALL) NOPASSWD:ALL\n groups: sudo\n shell: /bin/bash\n ssh_authorized_keys:\n - ' + $scope.cloudConfigSshKey + '\n'
# So keep it basic for now: hostname and ssh key
hostname = name_label
# Remove leading and trailing spaces.
.replace(/^\s+|\s+$/g, '')
# Replace spaces with '-'.
.replace(/\s+/g, '-')
if $scope.configDriveMethod == 'standard'
$scope.cloudContent = '#cloud-config\nhostname: ' + hostname + '\nssh_authorized_keys:\n - ' + $scope.cloudConfigSshKey + '\n'
else
$scope.cloudContent = $scope.cloudConfigCustom
# The first SR for a template with an existing disk
$scope.firstSR = (get (get template.$VBDs[0]).VDI).$SR
# Use the generic CloudConfig creation
return xo.vm.createCloudInitConfigDrive(id, $scope.firstSR, $scope.cloudContent).then ->
# Boot directly on disk
return xo.vm.setBootOrder({vm: id, order: 'c'})
.then () ->
if $scope.bootAfterCreate
xo.vm.start id
if !$scope.multipleVmsActive
if resourceSet
# FIXME When using self service, ACL permissions are not updated fast enough to access VM view right after creation
$state.go 'index'
else
# Send the client on the VM view
$state.go 'VMs_view', { id }
.catch (error) ->
notify.error {
title: 'VM creation'
message: 'The creation failed'
}
$scope.creatingVM = false
console.log error
# A module exports its name.
.name

View File

@@ -1,496 +0,0 @@
.grid
.panel.panel-default
.col-sm-4
p.page-title.col-sm-4
i.xo-icon-vm
| Create VM
span(ng-if='isAdmin') on
a(ng-if="'pool' === container.type", ui-sref="pools_view({id: container.id})")
| {{container.name_label}}
a(ng-if="'host' === container.type", ui-sref="hosts_view({id: container.id})")
| {{container.name_label}}
form.col-sm-4.form-horizontal(ng-if="resourceSet")
.form-group(style="margin-top:4px;margin-bottom:4px;")
label.col-sm-5.control-label Resource set:
.col-sm-7(ng-if='resourceSets.length > 1')
select.form-control(
style="max-width:20em;"
ng-model="$parent.resourceSet"
ng-options="resourceSet.name for resourceSet in resourceSets | orderBy:natural('name') track by resourceSet.id"
ng-change="updateResourceSet(resourceSet)"
required=""
)
.col-sm-7.form-control-static(ng-if='resourceSets.length === 1')
| {{ resourceSet.name }}
//- Add server panel
form.form-horizontal(ng-submit="createVMs()")
.grid
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-info-circle
| VM info
.panel-body
.form-group
label.col-sm-3.control-label Template
.col-sm-9
select.form-control(ng-model="template", ng-options="template.name_label for template in templates | map | orderBy:natural('name_label') track by template.id", required="", ng-change = 'initExistingValues(template)')
.form-group
label.col-sm-3.control-label Name
.col-sm-9
input.form-control(type="text", placeholder="Name of your new VM", ng-required="!multipleVmsActive", ng-model="name_label")
.form-group
label.col-sm-3.control-label Description
.col-sm-9
input.form-control(type="text", placeholder="Optional description of you new VM", ng-model="name_description")
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-dashboard
| Performances
.panel-body
.form-group
label.col-sm-3.control-label vCPUs
.col-sm-9
input.form-control(type="text", placeholder="{{template.CPUs.number}}", ng-model="CPUs")
.form-group
label.col-sm-3.control-label CPU Weight
.col-sm-9
select.form-control(ng-model = "cpuWeight", ng-options='value as key for (key, value) in weightMap track by value' ng-disabled='resourceSet')
option(value = '') default
.form-group
label.col-sm-3.control-label RAM
.col-sm-9
.input-group
input.form-control(type='number' min="0" step="0.01" placeholder="{{ template.memory.size | bytesConvert:memoryUnit:'iB' }}" ng-model="memoryValue" ng-change="updateMemory()")
span.input-group-btn.dropdown(dropdown)
button.btn.btn-default.dropdown-toggle(type = 'button' dropdown-toggle)
| {{ memoryUnit }}&nbsp;
span.caret
ul.dropdown-menu(role = 'menu' style='min-width:0')
li(ng-repeat="memoryUnit in units")
a(ng-click="updateMemoryUnit(memoryUnit)") {{ memoryUnit }}
.grid
//- Cloud Config Panel, only for templates with existing disks
.panel.panel-default(ng-if="isDiskTemplate")
.panel-heading.panel-title
i.fa.fa-cloud
| Config Drive
span.pull-right
label(style = 'cursor: pointer;')
input.hidden(type = 'checkbox', ng-model = '$parent.configDriveActive', ng-click = '$parent.configDriveMethod = "standard"')
i.fa(ng-class = '{"fa-toggle-on": $parent.configDriveActive, "text-success": $parent.configDriveActive, "fa-toggle-off": !$parent.configDriveActive}', style = 'font-size: 1.5em;')
.panel-body
fieldset(ng-disabled = '!$parent.configDriveActive')
.form-group
label.col-sm-3.control-label SSH Key
.col-sm-9
.input-group
span.input-group-addon
input(
type = 'radio'
name = 'configDriveMethod'
ng-model = '$parent.configDriveMethod'
value = 'standard'
)
input.form-control(
type="text"
placeholder="ssh-rsa AAAA.... you@machine"
ng-model="$parent.cloudConfigSshKey"
ng-disabled = '$parent.configDriveMethod !== "standard"'
name="cloudConfigSshKey"
required
)
.form-group
label.col-sm-3.control-label
a(href='http://cloudinit.readthedocs.org/en/latest/topics/examples.html', target='_blank') Custom config
.col-sm-9
.input-group
span.input-group-addon
input(
type = 'radio'
name = 'configDriveMethod'
ng-model = '$parent.configDriveMethod'
value = 'custom'
)
textarea.form-control(
rows='4'
style="resize: none;"
ng-model="$parent.cloudConfigCustom"
ng-disabled = '$parent.configDriveMethod !== "custom"'
name="cloudConfigCustom"
required
)
br
button.btn.btn-default(
type = 'button'
ng-disabled = '$parent.configDriveMethod !== "custom"'
ngf-select = '$parent.uploadCloudConfig($files[0]); fileName = $files[0].name'
) Select file
span(style='max-width: 1em' ng-init='fileName = "None"')
| Selected file : {{ fileName }}
i.fa.fa-spinner.fa-spin(ng-show = 'cloudConfigLoading')
i.fa.fa-exclamation-triangle.text-danger(ng-show = 'cloudConfigError' tooltip = 'Error while loading file')
//- Install panel, only if an installation method is needed
.panel.panel-default(ng-if="!isDiskTemplate")
.panel-heading.panel-title
i.fa.fa-download
| Install settings
.panel-body
.form-group(ng-show="availableMethods.cdrom")
label.col-sm-3.control-label ISO/DVD
.col-sm-9
.input-group
span.input-group-addon
input(
type = 'radio'
name = 'installation_method'
ng-model = '$parent.installation_method'
value = 'cdrom'
)
select.form-control.disabled(
ng-disabled="'cdrom' !== installation_method"
ng-model="$parent.installation_cdrom"
)
option(value = '') Please select
optgroup(ng-repeat="SR in ISO_SRs | orderBy:natural('name_label') track by SR.id", ng-if="SR.VDIs.length", label="{{SR.name_label}}")
option(ng-repeat="VDI in SR.VDIs | resolve | orderBy:natural('name_label') track by VDI.id", ng-value="VDI.id")
| {{VDI.name_label}}
.form-group(
ng-show = '(availableMethods.http || availableMethods.ftp || availableMethods.nfs)'
)
label.col-sm-3.control-label Network
.col-sm-9
.input-group
span.input-group-addon
input(
type = 'radio'
name = 'installation_method'
ng-model = '$parent.installation_method'
value = 'network'
)
input.form-control(type="text", ng-disabled="'network' !== installation_method", placeholder="e.g: http://ftp.debian.org/debian", ng-model="$parent.installation_network")
.form-group(ng-show = 'template.virtualizationMode === "hvm"')
label.col-sm-3.control-label PXE
.col-sm-9
input(
type = 'radio'
name = 'installation_method'
ng-model = '$parent.installation_method'
value = 'pxe'
)
.form-group(ng-show='template.virtualizationMode === "pv"')
label.col-sm-3.control-label PV Args
.col-sm-9
input.form-control(type="text", placeholder="{{template.PV_args}}", ng-model="$parent.pv_args")
//- Interface panel
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-network
| Interfaces
.panel-body
table.table.table-hover
tr
th MAC
th Network
th.col-md-1 &#160;
//- Buttons
tr(ng-repeat="VIF in VIFs track by VIF.id")
td
input.form-control(type="text", ng-model="VIF.mac", ng-pattern="/^\s*[0-9a-f]{2}(:[0-9a-f]{2}){5}\s*$/i", placeholder="Auto-generated if empty")
td
select.form-control(
ng-options = 'network as (network.name_label + " (" + (network.$pool | resolve).name_label + ")") for network in (networks || resourceSetNetworks) | map | orderBy:natural("name_label") track by network.id'
ng-model = 'VIF.network'
required
)
option(value = '') Please select
td
.pull-right
button.btn.btn-default(type="button", ng-click="removeVIF($index)", title="Remove this interface")
i.fa.fa-times
.btn-form
p.center
.btn-form
p.center
button.btn.btn-success(type="button", ng-click="addVIF()")
i.fa.fa-plus
| Add interface
//- end of misc and interface panel
//- Cloud config panel
.grid(ng-if = 'coreOsCloudConfig')
.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.coreOsCloudConfig', name='coreOsCloudConfig')
| {{coreOsCloudConfig}}
//- Multiple VMs panel
.grid
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-clone
| Multiple VMs
span.pull-right
label(style = 'cursor: pointer;')
input.hidden(type = 'checkbox', ng-model = 'multipleVmsActive', ng-click='refreshNames(); checkNumberOfVms()', ng-change="toggleBootAfterCreate()")
i.fa(ng-class = '{"fa-toggle-on": multipleVmsActive, "text-success": multipleVmsActive, "fa-toggle-off": !multipleVmsActive}', style = 'font-size: 1.5em;')
.panel-body(ng-if="multipleVmsActive")
.form-group
label.col-md-offset-4.col-sm-2.control-label
i.fa.fa-refresh(ng-click = "$parent.refreshNames()" tooltip="Set VMs to default names")
| Number of VMs
.col-sm-2
.input-group(style="width:10em")
input.form-control(type="number" ng-model="$parent.newNumberOfVms")
span.input-group-btn
button.btn.btn-default(type="button" ng-click="checkNumberOfVms()")
i.fa.fa-arrow-right
.col-sm-6(ng-repeat="offset in [0, 1]")
.form-group(
ng-repeat = "n in [].constructor($parent.numberOfVms).slice(0, ($parent.numberOfVms+1)/2) track by $index"
ng-if = "2*$index+offset < $parent.numberOfVms"
)
label.col-sm-2.control-label VM \#{{ 2*$index+1+offset }}
.col-sm-10
input.form-control(
type = "text"
required
placeholder = "Name of new VM \#{{ (2*$index+1+offset) }}"
ng-model = "$parent.vmsNames[2*$index+offset]"
ng-init = "$parent.vmsNames[2*$index+offset] = $parent.defaultName + (2*$index+1+offset)"
)
//- Disk panel
.grid
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-disk
| Disks
.panel-body
table.table.table-hover
tr
th.col-md-2 SR
th.col-md-1 Bootable?
th.col-md-2 Size
th.col-md-2 Name
th.col-md-3 Description
th.col-md-2 &#160;
//- Buttons
tr(ng-repeat="VBD in (templateVBDs | orderBy:'position') track by VBD.id", ng-if="isDiskTemplate")
td
select.form-control(ng-model="(VBD.VDI | resolve).$SR", ng-options="SR.id as (SR.name_label + ' on ' + (SR.$container | resolve).name_label + ' (' + (SR.size - SR.physical_usage | bytesToSize) + ' free)') for SR in (writable_SRs | orderBy:natural('name_label'))", ng-change = 'saveChange(VBD.position, "$SR", (VBD.VDI | resolve).$SR); updateSrs()', required)
option(value = '') Please select
td.text-center
i.fa.fa-check(ng-if = 'VBD.bootable')
td(style = "overflow: visible")
.input-group
input.form-control(
type='number'
min="0"
step="0.01"
placeholder="Size of this virtual disk"
ng-model="existingDiskSizeValues[VBD.position]"
ng-readonly='!configDriveActive'
ng-change = 'updateVdiSize(VBD.position)'
required
)
span.input-group-btn.dropdown(dropdown)
button.btn.btn-default.dropdown-toggle(type = 'button' dropdown-toggle ng-disabled='!configDriveActive')
| {{ existingDiskSizeUnits[VBD.position] }}&nbsp
span.caret
ul.dropdown-menu(role="menu" style="min-width:0")
li(ng-repeat="unit in $parent.units")
a(ng-click="existingDiskSizeUnits[VBD.position] = unit; updateVdiSize(VBD.position)") {{ unit }}
td
input.form-control(type="text", placeholder="Name of this virtual disk", ng-model="(VBD.VDI | resolve).name_label", ng-change = 'saveChange(VBD.position, "name_label", (VBD.VDI | resolve).name_label)')
td
input.form-control(type="text", placeholder="Description of this virtual disk", ng-model="(VBD.VDI | resolve).name_description", ng-change = 'saveChange(VBD.position, "name_description", (VBD.VDI | resolve).name_description)')
td
tr(ng-repeat="VDI in VDIs track by VDI.id")
td
select.form-control(ng-model="VDI.SR", ng-options="SR.id as (SR.name_label + ' on ' + (SR.$container | resolve).name_label + ' (' + (SR.size - SR.physical_usage | bytesToSize) + ' free)') for SR in (writable_SRs | orderBy:natural('name_label'))" ng-change="updateSrs()")
td.text-center
input(type="checkbox", ng-model="VDI.bootable")
td(style = "overflow: visible")
.input-group
input.form-control(
type='number'
min="0"
step="0.01"
placeholder="Size of this virtual disk"
ng-model="VDI.sizeValue"
ng-change = 'updateTotalDiskBytes()'
required
)
span.input-group-btn.dropdown(dropdown)
button.btn.btn-default.dropdown-toggle(type = 'button' dropdown-toggle)
| {{ VDI.sizeUnit }}&nbsp
span.caret
ul.dropdown-menu(role="menu" style="min-width:0")
li(ng-repeat="unit in units")
a(ng-click="VDI.sizeUnit = unit; updateTotalDiskBytes()") {{ unit }}
td
input.form-control(type="text", placeholder="Name of this virtual disk", ng-model="VDI.name_label")
td
input.form-control(type="text", placeholder="Description of this virtual disk", ng-model="VDI.name_description")
td
.btn-group
button.btn.btn-default(type="button", ng-click="moveVDI($index, -1)", ng-disabled="$first", title="Move this disk up")
i.fa.fa-chevron-up
button.btn.btn-default(type="button", ng-click="moveVDI($index, 1)", ng-disabled="$last", title="Move this disk down")
i.fa.fa-chevron-down
.pull-right
button.btn.btn-default(type="button", ng-click="removeVDI($index)", title="Remove this disk")
i.fa.fa-times
.btn-form
p.center
.btn-form
p.center
button.btn.btn-success(type="button", ng-click="addVDI()")
i.fa.fa-plus
| Add disk
//- Confirmation panel
.grid
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-flag-checkered
| Summary
.panel-body
.grid
.grid-cell
p.center.big
span(ng-if="!multipleVmsActive") {{name_label}}
span(ng-if="multipleVmsActive") {{numberOfVms}} new VMs
| &nbsp;
span.small(ng-if="template.name_label") ({{template.name_label}})
.grid
.grid-cell
//- p.stat-name vCPUs
p.center.big(tooltip="vCPUs")
| {{CPUs || template.CPUs.number || 0}}x&nbsp;
i.xo-icon-cpu
.grid-cell
//- p.stat-name RAM
p.center.big(tooltip="RAM")
span(ng-if="memoryValue") {{memory | bytesToSize}}
span(ng-if="!memoryValue") {{template.memory.size | bytesToSize}}
| &nbsp;
i.xo-icon-memory
.grid-cell
//- p.stat-name Disks
p.center.big(tooltip="Disks")
| {{(VDIs.length) || (templateVBDs.length) || 0}}x&nbsp;
i.xo-icon-disk
.grid-cell
//- p.stat-name Interfaces
p.center.big(tooltip="Network interfaces")
| {{(VIFs.length) || (template.VIFs.length) || 0}}x&nbsp;
i.xo-icon-network
.grid(ng-if="template && resourceSet")
.grid-cell
.center-block(ng-if="resourceSet.limits.cpus" style="width:60%")
.progress
.progress-bar(
role="progressbar",
aria-valuemin="0",
aria-valuenow="{{resourceSet.limits.cpus.total - resourceSet.limits.cpus.available}}",
aria-valuemax="{{resourceSet.limits.cpus.total}}",
style="width: {{[resourceSet.limits.cpus.total - resourceSet.limits.cpus.available, resourceSet.limits.cpus.total] | percentage}}",
tooltip="{{resourceSet.limits.cpus.total - resourceSet.limits.cpus.available}} vCPUs already in use"
)
.progress-bar(
ng-class = '{"progress-bar-success": numberOfVms * (CPUs || template.CPUs.number || 0) <= resourceSet.limits.cpus.available, "progress-bar-danger": numberOfVms * (CPUs || template.CPUs.number || 0) > resourceSet.limits.cpus.available}'
role="progressbar",
aria-valuemin="0",
aria-valuenow="{{numberOfVms * (CPUs || template.CPUs.number || 0)}}",
aria-valuemax="{{resourceSet.limits.cpus.total}}",
style="width: {{[min(numberOfVms * (CPUs || template.CPUs.number || 0), resourceSet.limits.cpus.available), resourceSet.limits.cpus.total] | percentage}}",
tooltip="{{numberOfVms * (CPUs || template.CPUs.number || 0)}} vCPUs / {{resourceSet.limits.cpus.available}} remaining"
)
.grid-cell
.center-block(ng-if="resourceSet.limits.memory" style="width:60%")
.progress
.progress-bar(
role="progressbar",
aria-valuemin="0",
aria-valuenow="{{resourceSet.limits.memory.total - resourceSet.limits.memory.available}}",
aria-valuemax="{{resourceSet.limits.memory.total}}",
style="width: {{[resourceSet.limits.memory.total - resourceSet.limits.memory.available, resourceSet.limits.memory.total] | percentage}}",
tooltip="{{resourceSet.limits.memory.total - resourceSet.limits.memory.available | bytesToSize}} already in use"
)
.progress-bar(
ng-class = '{"progress-bar-success": numberOfVms * memory <= resourceSet.limits.memory.available, "progress-bar-danger": numberOfVms * memory > resourceSet.limits.memory.available}'
role="progressbar",
aria-valuemin="0",
aria-valuenow="{{numberOfVms * memory}}",
aria-valuemax="{{resourceSet.limits.memory.total}}",
style="width: {{[min(numberOfVms * memory, resourceSet.limits.memory.available), resourceSet.limits.memory.total] | percentage}}",
tooltip="{{numberOfVms * memory | bytesToSize}} / {{resourceSet.limits.memory.available | bytesToSize}} remaining"
)
.grid-cell
.center-block(ng-if="resourceSet.limits.disk" style="width:60%")
.progress
.progress-bar(
role="progressbar",
aria-valuemin="0",
aria-valuenow="{{resourceSet.limits.disk.total - resourceSet.limits.disk.available}}",
aria-valuemax="{{resourceSet.limits.disk.total}}",
style="width: {{[resourceSet.limits.disk.total - resourceSet.limits.disk.available, resourceSet.limits.disk.total] | percentage}}",
tooltip="{{resourceSet.limits.disk.total - resourceSet.limits.disk.available | bytesToSize}} already in use"
)
.progress-bar(
ng-class = '{"progress-bar-success": numberOfVms * totalDiskBytes <= resourceSet.limits.disk.available, "progress-bar-danger": numberOfVms * totalDiskBytes > resourceSet.limits.disk.available}'
role="progressbar",
aria-valuemin="0",
aria-valuenow="{{numberOfVms * totalDiskBytes}}",
aria-valuemax="{{resourceSet.limits.disk.total}}",
style="width: {{[min(numberOfVms * totalDiskBytes, resourceSet.limits.disk.available), resourceSet.limits.disk.total] | percentage}}",
tooltip="{{numberOfVms * totalDiskBytes | bytesToSize}} / {{resourceSet.limits.disk.available | bytesToSize}} remaining"
)
.grid-cell
p.center(ng-if="isDiskTemplate")
| Cloud configuration is&nbsp;
strong.text-success(ng-if = 'configDriveActive') enabled.
strong.text-danger(ng-if = '!configDriveActive') disabled.
p.center(ng-if="selectedLocalSrs.size === 1")
label
| The VM will be created on {{(forcedHost | resolve).name_label}} since
span(ng-repeat="sr in selectedLocalSrs[forcedHost]") {{(sr | resolve).name_label}}
span(ng-if="$index < selectedLocalSrs[forcedHost].length - 2") ,
span(ng-if="$index === selectedLocalSrs[forcedHost].length - 2") and
span(ng-if="selectedLocalSrs[forcedHost].length > 1") are
span(ng-if="selectedLocalSrs[forcedHost].length === 1") is
| on {{(forcedHost | resolve).name_label}}
p.text-danger(ng-if="selectedLocalSrs.size > 1")
label.control-label Incompatible disks:
ul(ng-if="selectedLocalSrs.size > 1")
li.text-danger(ng-repeat="(host, srs) in selectedLocalSrs")
span(ng-repeat="sr in srs") {{(sr | resolve).name_label}}
span(ng-if="$index < srs.length - 2") ,
span(ng-if="$index === srs.length - 2") and
span(ng-if="srs.length > 1") are
span(ng-if="srs.length === 1") is
| on {{(host | resolve).name_label}}
p.center
label
input(type='checkbox', ng-model = 'bootAfterCreate')
span(ng-if='!multipleVmsActive') Boot VM after creation
span(ng-if='multipleVmsActive') Boot {{numberOfVms}} VMs after creation
p.center
button.btn.btn-lg.btn-primary(
type="submit"
ng-disabled = [
'creatingVM',
'resourceSet.limits.cpus && (CPUs || template.CPUs.number || 0) > resourceSet.limits.cpus.available',
'resourceSet.limits.memory && memory > resourceSet.limits.memory.available',
'resourceSet.limits.disk && totalDiskBytes > resourceSet.limits.disk.available',
'selectedLocalSrs.size > 1'
].join(' || ')
)
i.fa.fa-play(ng-if = '!creatingVM')
i.fa.fa-circle-o-notch.fa-spin(ng-if = 'creatingVM')
| Create VM

View File

@@ -1,259 +0,0 @@
import angular from 'angular'
import forEach from 'lodash.foreach'
import uiRouter from 'angular-ui-router'
import xoTag from 'tag'
import view from './view'
// ===================================================================
export default angular.module('xoWebApp.pool', [
uiRouter,
xoTag
])
.config(function ($stateProvider) {
$stateProvider.state('pools_view', {
url: '/pools/:id',
controller: 'PoolCtrl',
template: view
})
})
.controller('PoolCtrl', function ($scope, $stateParams, xoApi, xo, modal, notify) {
{
const {id} = $stateParams
const hostsByPool = xoApi.getIndex('hostsByPool')
const runningHostsByPool = xoApi.getIndex('runningHostsByPool')
const srsByContainer = xoApi.getIndex('srsByContainer')
const networksByPool = xoApi.getIndex('networksByPool')
Object.defineProperties($scope, {
pool: {
get: () => xoApi.get(id)
},
hosts: {
get: () => hostsByPool[id]
},
runningHosts: {
get: () => runningHostsByPool[id]
},
srs: {
get: () => srsByContainer[id]
},
networks: {
get: () => networksByPool[id]
}
})
}
$scope.$watch(() => $scope.pool && $scope.hosts, result => {
if (result) {
$scope.listMissingPatches()
xo.pool.getLicenseState($scope.pool.id).then(result => {
$scope.license = result
})
}
})
$scope.currentLogPage = 1
$scope.savePool = function ($data) {
let {pool} = $scope
let {name_label, name_description} = $data
$data = {
id: pool.id
}
if (name_label !== pool.name_label) {
$data.name_label = name_label
}
if (name_description !== pool.name_description) {
$data.name_description = name_description
}
xoApi.call('pool.set', $data)
}
$scope.deleteAllLog = function () {
return modal.confirm({
title: 'Log deletion',
message: 'Are you sure you want to delete all the logs?'
}).then(function () {
// TODO: return all promises.
forEach($scope.pool.messages, function (message) {
xo.log.delete(message.id)
console.log('Remove log', message.id)
})
})
}
$scope.setDefaultSr = function (id) {
let {pool} = $scope
return modal.confirm({
title: 'Set default SR',
message: 'Are you sure you want to set this SR as default?'
}).then(function () {
return xo.pool.setDefaultSr(pool.id, id)
})
}
$scope.deleteLog = function (id) {
console.log('Remove log', id)
return xo.log.delete(id)
}
$scope.nbUpdates = {}
$scope.totalUpdates = 0
$scope.listMissingPatches = () => {
forEach($scope.hosts, function (host, host_id) {
xo.host.listMissingPatches(host_id)
.then(result => {
$scope.nbUpdates[host_id] = result.length
$scope.totalUpdates += result.length
}
)
})
}
$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.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)
})
}
$scope.canAdmin = function (id = undefined) {
if (id === undefined) {
id = $scope.pool && $scope.pool.id
}
return id && xoApi.canInteract(id, 'administrate') || false
}
$scope.connectPIF = function (id) {
console.log(`Connect PIF ${id}`)
xoApi.call('pif.connect', {id: id})
}
$scope.disconnectPIF = function (id) {
console.log(`Disconnect PIF ${id}`)
xoApi.call('pif.disconnect', {id: id})
}
$scope.removePIF = function (id) {
console.log(`Remove PIF ${id}`)
xoApi.call('pif.delete', {id: id})
}
$scope.deleteNetwork = function (id) {
return modal.confirm({
title: 'Network deletion',
message: 'Are you sure you want to delete this network?'
}).then(function () {
console.log(`Delete network ${id}`)
notify.info({
title: 'Network deletion...',
message: 'Deleting the network'
})
xoApi.call('network.delete', {id: id})
})
}
$scope.disallowDelete = function (network) {
let disallow = false
forEach(network.PIFs, pif => {
const PIF = xoApi.get(pif)
if (PIF.disallowUnplug || PIF.management) {
disallow = true
return false
}
})
return disallow
}
$scope.createNetwork = function (name, description, pif, mtu, vlan) {
$scope.createNetworkWaiting = true
notify.info({
title: 'Network creation...',
message: 'Creating the network'
})
const params = {
pool: $scope.pool.id,
name: name
}
if (mtu) {
params.mtu = mtu
}
if (pif) {
params.pif = pif
}
if (vlan) {
params.vlan = vlan
}
if (description) {
params.description = description
}
return xoApi.call('network.create', params).then(function () {
$scope.creatingNetwork = false
$scope.createNetworkWaiting = false
})
}
$scope.physicalPifs = () => {
const physicalPifs = []
const host = xoApi.get($scope.pool.master)
forEach(host.$PIFs, pif => {
pif = xoApi.get(pif)
if (pif.physical) {
physicalPifs.push(pif.id)
}
})
return physicalPifs
}
// $scope.patchPool = ($files, id) ->
// file = $files[0]
// xo.pool.patch id
// .then ({ $sendTo: url }) ->
// return Upload.http {
// method: 'POST'
// url
// data: file
// }
// .progress throttle(
// (event) ->
// percentage = (100 * event.loaded / event.total)|0
// notify.info
// title: 'Upload patch'
// message: "#{percentage}%"
// 6e3
// )
// .then (result) ->
// throw result.status if result.status isnt 200
// notify.info
// title: 'Upload patch'
// message: 'Success'
})
// A module exports its name.
.name

View File

@@ -1,291 +0,0 @@
//- TODO: lots of stuff.
.grid-sm
.panel.panel-default
p.page-title
i.xo-icon-pool
| {{pool.name_label}}
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-cogs
| General
span.quick-edit(tooltip="Edit General settings", ng-click="poolSettings.$show()")
i.fa.fa-edit.fa-fw
.panel-body
form(editable-form="", name="poolSettings", onbeforesave="savePool($data)")
dl.dl-horizontal
dt Name
dd
span(editable-text="pool.name_label", e-name="name_label", e-form="poolSettings")
| {{pool.name_label}}
dt Description
dd
span(editable-text="pool.name_description", e-name="name_description", e-form="poolSettings")
| {{pool.name_description}}
dt Master
dd(ng-repeat="master in [pool.master] | resolve")
a(ui-sref="hosts_view({id: master.id})")
| {{master.name_label}}
dt Tags
dd
xo-tag(ng-if = 'pool', object = 'pool')
dt(ng-if="pool.default_SR") Default SR
dd(ng-if="pool.default_SR", ng-init="default_SR = (pool.default_SR | resolve)")
a(ui-sref="SRs_view({id: default_SR.id})") {{default_SR.name_label}}
dt HA
dd
| {{pool.HA_enabled}}
dt UUID
dd {{pool.UUID}}
.btn-form(ng-show="poolSettings.$visible")
p.center
button.btn.btn-default(type="button", ng-disabled="poolSettings.$waiting", ng-click="poolSettings.$cancel()")
i.fa.fa-times
| Cancel
| &nbsp;
button.btn.btn-primary(type="submit", ng-disabled="poolSettings.$waiting")
i.fa.fa-save
| Save
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-stats
| Stats
.row
.col-xs-6
p.stat-name Hosts:
p.center.big-stat {{hosts | count}}
.col-xs-6
p.stat-name Running:
p.center.big-stat {{runningHosts | count}}
//- Action panel
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-flash
| Actions
.panel-body
.grid-cell.text-center
.grid
.grid-cell.btn-group
button.btn(tooltip="Add SR", tooltip-placement="top", type="button", style="width: 90%", disabled)
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: pool.id})")
i.xo-icon-vm.fa-2x.fa-fw
.grid-cell.btn-group
button.btn(tooltip="Patch the pool", tooltip-placement="top", type="button", style="width: 90%", ngf-select = "patchPool($files, pool.id)")
i.fa.fa-file-code-o.fa-2x.fa-fw
.grid-cell.btn-group
button.btn(tooltip="Add Host", tooltip-placement="top", type="button", style="width: 90%")
i.xo-icon-host.fa-2x.fa-fw
.grid-cell.btn-group
button.btn(tooltip="Disconnect", tooltip-placement="top", type="button", style="width: 90%; margin-bottom: 0.5em")
i.fa.fa-unlink.fa-2x.fa-fw
//- Hosts panel
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-host
| Hosts
.panel-body
table.table.table-hover.table-condensed
th Name
th.col-md-4 Description
th.col-md-6 Memory
tr(xo-sref="hosts_view({id: host.id})", ng-repeat="host in hosts | map | orderBy:natural('name_label') track by host.id")
td.oneliner {{host.name_label}}
td.oneliner {{host.name_description}}
td
.progress-condensed
.progress-bar(
role="progressbar",
aria-valuemin="0",
aria-valuenow="{{host.memory.usage}}",
aria-valuemax="{{host.memory.size}}",
style="width: {{[host.memory.usage, host.memory.size] | percentage}}",
tooltip="RAM: {{host.memory.usage | bytesToSize}}/{{host.memory.size | bytesToSize}} ({{[host.memory.usage, host.memory.size] | percentage}})"
)
//- Shared SR panel
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-sr
| Shared SR
.panel-body
table.table.table-hover
th Name
th Description
th Type
th Size
th.col-md-4 Physical/Allocated usage
th.col-md-1 Action
tr(
ng-repeat="SR in srs | map | orderBy:natural('name_label') track by SR.id"
xo-sref="SRs_view({id: SR.id})"
)
td.oneliner
| {{SR.name_label}}&nbsp;
span.label.label-primary(ng-if="SR.id === pool.default_SR") Default SR
td.oneliner {{SR.name_description}}
td {{SR.SR_type}}
td {{SR.size | bytesToSize}}
td
.progress-condensed
.progress-bar(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.physical_usage}}", aria-valuemax="{{SR.size}}", style="width: {{[SR.physical_usage, SR.size] | percentage}}", tooltip="Physical usage: {{[SR.physical_usage, SR.size] | percentage}}")
td
span.pull-right.btn-group.quick-buttons
a(ng-if="SR.id !== pool.default_SR", xo-click="setDefaultSr(SR.id)")
i.fa.fa-hdd-o.fa-lg(tooltip="Set as default SR")
//- Networks/Interfaces panel
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-network
| Networks
.panel-body
table.table.table-hover
th.col-md-2 Name
th.col-md-2 Description
th.col-md-7 PIFs
th.col-md-1
tr(ng-repeat="network in networks track by network.id" ng-init="showPIFs = false")
td {{network.name_label}}
td {{network.name_description}}
td
a(ng-if="network.PIFs.length && !showPIFs" ng-click="$parent.showPIFs=true") show PIFs ({{network.PIFs.length}})
a(ng-if="network.PIFs.length && showPIFs" ng-click="$parent.showPIFs=false") hide PIFs
table.table.table-sm.table-hover(ng-if="network.PIFs.length && showPIFs")
th.col-md-2 Device
th.col-md-2 Host
th.col-md-1 VLAN
th.col-md-2 Address
th.col-md-2 MAC
th.col-md-2 Link status
tr(ng-repeat="PIF in network.PIFs | resolve | orderBy:natural('($host | resolve).name_label')")
td
| {{PIF.device}}
span.label.label-primary(ng-if="PIF.management") XAPI
| &nbsp;
span.label.label-primary(ng-if="PIF.physical") Phys.
td {{(PIF.$host | resolve).name_label}}
td
span(ng-if="PIF.vlan > -1") {{PIF.vlan}}
span(ng-if="PIF.vlan == -1") -
td {{PIF.ip}} ({{PIF.mode}})
td {{PIF.mac}}
td
span.label.label-default(ng-if="!PIF.attached") Disconnected
span.label.label-success(ng-if="PIF.attached") Connected
span.pull-right.btn-group.quick-buttons(ng-if="canAdmin()")
i.fa.fa-unlink.fa-lg.text-danger(ng-if="PIF.disallowUnplug" tooltip="Disconnection not allowed")
i.fa.fa-unlink.fa-lg.text-danger(ng-if="!PIF.disallowUnplug && PIF.management" tooltip="Management PIF")
a(tooltip="Disconnect this interface" xo-click="disconnectPIF(PIF.id)", ng-if = 'PIF.attached && !PIF.disallowUnplug && !PIF.management')
i.fa.fa-unlink.fa-lg
a(tooltip="Connect this interface" xo-click="connectPIF(PIF.id)", ng-if = '!PIF.attached')
i.fa.fa-link.fa-lg
span(ng-if="!network.PIFs.length") No PIF attached to this network
td
span.pull-right.btn-group.quick-buttons
i.fa.fa-trash-o.text-danger.fa-lg(ng-if="disallowDelete(network)" tooltip="Some PIFs cannot be disconnected")
i.fa.fa-trash-o.text-danger.fa-lg(ng-if="network.name_label === 'Host internal management network'" tooltip="Management network")
a(tooltip="Remove network" xo-click="$parent.deleteNetwork(network.id)", ng-if = 'canAdmin() && network.name_label !== "Host internal management network" && !disallowDelete(network)')
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", 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-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 physicalPifs()')
option(value = '') None
| &nbsp;
.form-group
label.control-label(for = 'newNetworkName') Name&nbsp;
input#newNetworkName.form-control(type = 'text', ng-model = 'newNetworkName', required)
| &nbsp;
.form-group
label.control-label(for = 'newNetworkDescription') Description&nbsp;
input#newNetworkDescription.form-control(type = 'text', ng-model = 'newNetworkDescription', placeholder= 'Network created with Xen Orchestra')
| &nbsp;
.form-group
label.control-label(for = 'newNetworkVlan') VLAN&nbsp;
input#newNetworkVlan.form-control(type = 'text', ng-model = 'newNetworkVlan', placeholder = 'Defaut: no VLAN')
| &nbsp;
.form-group
label(for = 'newNetworkMTU') MTU&nbsp;
input#newNetworkMTU.form-control(type = 'text', ng-model = 'newNetworkMTU', placeholder = 'Default: 1500')
| &nbsp;
.form-group
button.btn.btn-primary(type = 'submit')
i.fa.fa-plus-square
| Create
span(ng-if = 'createNetworkWaiting')
| &nbsp;
i.xo-icon-loading-sm
.grid-sm
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-refresh
| Updates
span.quick-edit(
ng-if="totalUpdates"
tooltip="Update all"
ng-click="installAllPatches()"
)
i.fa.fa-download.fa-fw
.panel-body
p.center(ng-if="!totalUpdates") Everything up to date
table.table.table-hover(ng-if="totalUpdates")
tr
th Host
th Description
th Missing patches
th Install
tr( ng-repeat="host in hosts" ng-if="nbUpdates[host.id]")
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
//- Logs panel
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-comments
| Logs
span.quick-edit(ng-if="pool.messages | isNotEmpty", tooltip="Remove all logs", xo-click="deleteAllLog()")
i.fa.fa-trash-o.fa-fw
.panel-body
p.center(ng-if="pool.messages | isEmpty") No recent logs
table.table.table-hover(ng-if="pool.messages | isNotEmpty")
th Date
th Name
tr(ng-repeat="message in pool.messages | map | orderBy:'-time' | slice:(5*(currentLogPage-1)):(5*currentLogPage) track by message.id")
td {{message.time*1e3 | date:"medium"}}
td
| {{message.name}}
span.pull-right.btn-group.quick-buttons
a(xo-click="deleteLog(message.id)")
i.fa.fa-trash-o.fa-lg(tooltip="Remove this log entry")
.center(ng-if = '(pool.messages | count) > 5 || currentLogPage > 1')
pagination(boundary-links="true", total-items="pool.messages | count", ng-model="$parent.currentLogPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-book
| License
.panel-body
.row(ng-repeat="(key, value) in license")
label.control-label.col-sm-3
| {{key}}:
.col-sm-9 {{value}}

View File

@@ -1,250 +0,0 @@
import angular from 'angular'
import uiBootstrap from 'angular-ui-bootstrap'
import uiRouter from 'angular-ui-router'
import assign from 'lodash.assign'
import differenceBy from 'lodash.differenceby'
import filter from 'lodash.filter'
import find from 'lodash.find'
import forEach from 'lodash.foreach'
import includes from 'lodash.includes'
import intersection from 'lodash.intersection'
import map from 'lodash.map'
import view from './view'
// ====================================================================
export default angular.module('self.admin', [
uiRouter,
uiBootstrap
])
.config(function ($stateProvider) {
$stateProvider.state('self.admin', {
url: '/admin',
resolve: {
users (xo) {
return xo.user.getAll()
},
groups (xo) {
return xo.group.getAll()
}
},
controller: 'AdminCtrl as ctrl',
template: view
})
})
.controller('AdminCtrl', function (xo, xoApi, $scope, users, groups, sizeToBytesFilter, bytesToSizeFilter) {
users.push(...groups)
this.sizeUnits = ['MiB', 'GiB', 'TiB']
let validHosts
this.resourceSets = {}
const loadSets = () => {
xo.resourceSet.getAll()
.then(sets => this.resourceSets = sets)
}
const reset = () => {
this.srs = []
this.networks = []
this.templates = []
this.eligibleHosts = []
validHosts = []
delete this.editing
delete this.selectedNetworks
delete this.selectedSrs
delete this.selectedTemplates
delete this.selectedPools
delete this.selectedSubjects
delete this.name
delete this.cpuMax
delete this.memoryMax
delete this.diskMax
this.memoryUnit = this.sizeUnits[1]
this.diskUnit = this.sizeUnits[1]
}
this.reset = reset
reset()
loadSets()
this.pools = xoApi.getView('pool').all
const hosts = xoApi.getView('host').all
const srs = xoApi.getView('SR').all
const networks = xoApi.getView('network').all
const vmTemplatesByContainer = xoApi.getIndex('vmTemplatesByContainer')
this.subjects = users
const collectById = function (array) {
const collection = {}
forEach(array, item => collection[item.id] = item)
return collection
}
this.listSubjects = collectById(users)
// When a pool selection happens
const filterSrs = () => filter(srs, sr => {
let found = false
forEach(this.selectedPools, pool => !(found = sr.$poolId === pool.id))
return found
})
const gatherTemplates = () => {
const vmTemplates = {}
forEach(this.selectedPools, pool => {
assign(vmTemplates, vmTemplatesByContainer[pool.id])
})
return vmTemplates
}
$scope.$watchCollection(() => this.selectedPools, () => {
validHosts = filter(hosts, host => {
let found = false
forEach(this.selectedPools, pool => !(found = host.$poolId === pool.id))
return found
})
this.srs = filterSrs()
this.selectedSrs = intersection(this.selectedSrs, this.srs)
this.vmTemplates = gatherTemplates()
// TODO : Why isn't this working fine? (`intersection` uses SameValueZero as comparison: http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero)
// this.selectedTemplates = intersection(this.selectedTemplates, this.vmTemplates)
this.selectedTemplates = filter(this.selectedTemplates, (template) => this.vmTemplates.hasOwnProperty(template.id))
this.networks = filterNetworks()
this.selectedNetworks = intersection(this.selectedNetworks, this.networks)
this.eligibleHosts = resolveHosts()
})
const filterNetworks = () => {
const selectableHosts = filter(validHosts, host => {
let keptBySr
forEach(this.selectedSrs, sr => !(keptBySr = intersection(sr.$PBDs, host.$PBDs).length > 0))
return keptBySr
})
return filter(networks, network => {
let kept = false
forEach(selectableHosts, host => !(kept = intersection(network.PIFs, host.PIFs).length > 0))
return kept
})
}
// When a SR selection happens
const constraintNetworks = () => {
this.networks = filterNetworks()
this.selectedNetworks = intersection(this.selectedNetworks, this.networks)
resolveHosts()
}
const resolveHosts = () => {
const keptHosts = filter(validHosts, host => {
let keptBySr = false
forEach(this.selectedSrs, sr => !(keptBySr = intersection(sr.$PBDs, host.$PBDs).length > 0))
let keptByNetwork
forEach(this.selectedNetworks, network => !(keptByNetwork = intersection(network.PIFs, host.PIFs).length > 0))
return keptBySr && keptByNetwork
})
this.eligibleHosts = keptHosts
this.excludedHosts = differenceBy(map(hosts), keptHosts, item => item && item.id)
}
$scope.$watchCollection(() => this.selectedSrs, constraintNetworks)
$scope.$watchCollection(() => this.selectedNetworks, resolveHosts)
this.save = function (name, subjects, pools, templates, srs, networks, cpuMax, memoryMax, memoryUnit, diskMax, diskUnit, id) {
return save(name, subjects, pools, templates, srs, networks, cpuMax, memoryMax, memoryUnit, diskMax, diskUnit, id)
.then(reset)
.then(loadSets)
}
this.create = function (name, subjects, pools, templates, srs, networks, cpuMax, memoryMax, memoryUnit, diskMax, diskUnit) {
return xo.resourceSet.create(name)
.then(set => {
save(name, subjects, pools, templates, srs, networks, cpuMax, memoryMax, memoryUnit, diskMax, diskUnit, set.id)
})
.then(reset)
.then(loadSets)
}
const save = function (name, subjects, pools, templates, srs, networks, cpuMax, memoryMax, memoryUnit, diskMax, diskUnit, id) {
const limits = {}
if (cpuMax) {
limits.cpus = cpuMax
}
if (memoryMax) {
limits.memory = sizeToBytesFilter(`${memoryMax} ${memoryUnit}`)
}
if (diskMax) {
limits.disk = sizeToBytesFilter(`${diskMax} ${diskUnit}`)
}
const getIds = arr => map(arr, item => item.id)
subjects = getIds(subjects)
pools = getIds(pools)
templates = getIds(templates)
srs = getIds(srs)
networks = getIds(networks)
const objects = Array.of(...templates, ...srs, ...networks)
return xo.resourceSet.set(id, name, subjects, objects, limits)
}
this.edit = id => {
window.scroll(0, 0)
const set = find(this.resourceSets, rs => rs.id === id)
if (set) {
this.editing = id
this.name = set.name
const getObjects = arr => map(arr, id => xoApi.get(id))
const objects = getObjects(set.objects)
const selectedPools = {}
forEach(objects, object => {
const poolId = object.poolId || object.$poolId
if (poolId) { selectedPools[poolId] = true }
})
this.selectedPools = getObjects(Object.keys(selectedPools))
this.selectedSrs = filter(objects, object => object.type === 'SR')
this.selectedNetworks = filter(objects, object => object.type === 'network')
this.selectedTemplates = filter(objects, object => object.type === 'VM-template')
this.selectedSubjects = filter(users, user => includes(set.subjects, user.id))
this.cpuMax = set.limits.cpus && set.limits.cpus.total
if (set.limits.memory) {
const memory = bytesToSizeFilter(set.limits.memory.total).split(' ')
this.memoryMax = +memory[0]
this.memoryUnit = memory[1]
} else {
delete this.memoryMax
this.memoryUnit = this.sizeUnits[1]
}
if (set.limits.disk) {
const disk = bytesToSizeFilter(set.limits.disk.total).split(' ')
this.diskMax = +disk[0]
this.diskUnit = disk[1]
} else {
delete this.diskMax
this.diskUnit = this.sizeUnits[1]
}
}
}
this.delete = id => {
xo.resourceSet.delete(id).then(() => {
if (id === this.editing) {
reset()
}
loadSets()
})
}
})
// A module exports its name.
.name

View File

@@ -1,180 +0,0 @@
.panel.panel-default
p.page-title
i.fa.fa-wrench(style="color: #e25440;")
| Administration
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-pencil-square-o
| Creation and edition
.panel-body
.alert.alert-info(ng-if = 'ctrl.editing') Editing an existing set
form.form-horizontal(ng-submit = 'ctrl[ctrl.editing ? "save" : "create"](ctrl.name, ctrl.selectedSubjects, ctrl.selectedPools, ctrl.selectedTemplates, ctrl.selectedSrs, ctrl.selectedNetworks, ctrl.cpuMax, ctrl.memoryMax, ctrl.memoryUnit, ctrl.diskMax, ctrl.diskUnit, ctrl.editing)')
.form-group
.col-sm-4
input.form-control(type = 'text', ng-model = 'ctrl.name', placeholder = 'resource set name', required)
.col-sm-4
ui-select(ng-model = 'ctrl.selectedSubjects', multiple, close-on-select = 'false', required)
ui-select-match(placeholder = 'choose user(s) and/or group(s)')
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 = 'subject in ctrl.subjects | filter:{ permission: "!admin" } | filter:$select.search')
div(ng-if = 'subject.email')
i.xo-icon-user.fa-fw
| {{subject.email}}
div(ng-if = 'subject.name')
i.xo-icon-group.fa-fw
| {{subject.name}}
.col-sm-4
ui-select(ng-model = 'ctrl.selectedPools', multiple, close-on-select = 'false', required)
ui-select-match(placeholder = 'choose pool(s)')
i(class = 'xo-icon-{{$item.type | lowercase}}')
| {{$item.name_label}}
ui-select-choices(repeat = 'pool in ctrl.pools | map | filter:$select.search | orderBy:["type", "name_label"]')
div
i(class = 'xo-icon-{{pool.type | lowercase}}')
| {{pool.name_label}}
fieldset(ng-disabled = 'ctrl.selectedPools | isEmpty')
.form-group
.col-sm-4
ui-select(ng-model = 'ctrl.selectedTemplates', multiple, close-on-select = 'false', required)
ui-select-match(placeholder = 'choose VM templates')
i(class = 'xo-icon-{{$item.type | lowercase}}')
| {{$item.name_label}}
ui-select-choices(repeat = 'template in ctrl.vmTemplates | map | filter:$select.search | orderBy:["type", "name_label"]')
div
i(class = 'xo-icon-{{template.type | lowercase}}')
| {{template.name_label}}
.col-sm-4
ui-select(ng-model = 'ctrl.selectedSrs', multiple, close-on-select = 'false', required)
ui-select-match(placeholder = 'choose storages')
i(class = 'xo-icon-{{$item.type | lowercase}}')
| {{$item.name_label}}
span(ng-if="$item.$container")
| ({{($item.$container | resolve).name_label}})
ui-select-choices(repeat = 'sr in ctrl.srs | map | filter:$select.search | orderBy:["type", "name_label"]')
div
i(class = 'xo-icon-{{sr.type | lowercase}}')
| {{sr.name_label}}
span(ng-if="sr.$container")
| ({{(sr.$container | resolve).name_label}})
.col-sm-4
fieldset(ng-disabled = 'ctrl.selectedSrs | isEmpty')
ui-select(ng-model = 'ctrl.selectedNetworks', multiple, close-on-select = 'false', required)
ui-select-match(placeholder = 'choose networks')
i(class = 'xo-icon-{{$item.type | lowercase}}')
| {{$item.name_label}}
span(ng-if="$item.$poolId")
| ({{($item.$poolId | resolve).name_label}})
ui-select-choices(repeat = 'network in ctrl.networks | map | filter:$select.search | orderBy:["type", "name_label"]')
div
i(class = 'xo-icon-{{network.type | lowercase}}')
| {{network.name_label}}
span(ng-if="network.$poolId")
| ({{(network.$poolId | resolve).name_label}})
.form-group
.col-sm-4
input.form-control(type = 'number' min = '0' placeholder = 'Maximum CPUs' ng-model = 'ctrl.cpuMax')
.col-sm-4
.input-group
input.form-control(type = 'number' min = '0' placeholder = 'Maximum RAM' ng-model = 'ctrl.memoryMax')
span.input-group-btn.dropdown(dropdown)
button.btn.btn-default.dropdown-toggle(type = 'button' dropdown-toggle)
| {{ ctrl.memoryUnit }}&nbsp;
span.caret
ul.dropdown-menu(role = 'menu' style='min-width:0')
li(ng-repeat = 'unit in ctrl.sizeUnits')
a(ng-click = 'ctrl.memoryUnit = unit') {{ unit }}
.col-sm-4
.input-group
input.form-control(type = 'number' min = '0' placeholder = 'Max. disk Space' ng-model = 'ctrl.diskMax')
span.input-group-btn.dropdown(dropdown)
button.btn.btn-default.dropdown-toggle(type = 'button' dropdown-toggle)
| {{ ctrl.diskUnit }}&nbsp;
span.caret
ul.dropdown-menu(role = 'menu' style='min-width:0')
li(ng-repeat = 'unit in ctrl.sizeUnits')
a(ng-click = 'ctrl.diskUnit = unit') {{ unit }}
.row
.col-sm-8
h4 Available hosts
p.text-muted VMs created from this resource set shall run on the following hosts
ul.list-group
li.list-group-item(ng-if = 'ctrl.eligibleHosts | isEmpty'): em.text-muted No hosts available
li.list-group-item(ng-if = 'ctrl.eligibleHosts | isNotEmpty', ng-repeat = 'host in ctrl.eligibleHosts')
| {{ host.name_label }}
span(ng-if = '(host.$poolId | resolve)') &nbsp;({{ (host.$poolId | resolve).name_label }})
.col-sm-4
h4 Excluded hosts
ul.list-group
li.list-group-item(ng-repeat = 'host in ctrl.excludedHosts')
s
| {{ host.name_label }}
span(ng-if = '(host.$poolId | resolve)') &nbsp;({{ (host.$poolId | resolve).name_label }})
.form-group
.col-sm-10
button.btn.btn-lg.btn-primary(type = 'submit', ng-disabled = '(ctrl.selectedSrs | isEmpty) || (ctrl.selectedNetworks | isEmpty) || (ctrl.selectedTemplates | isEmpty) || (ctrl.selectedSubjects | isEmpty)')
span(ng-if='!ctrl.editing') Create
span(ng-if='ctrl.editing') Edit
| &nbsp;
button.btn.btn-lg.btn-default(type = 'button', ng-click = 'ctrl.reset()') Reset
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-list-alt
| Resource sets
.panel-body
div(ng-repeat = 's in ctrl.resourceSets | orderBy:"name"')
.row
.col-sm-9
h4 {{ s.name }}
.col-sm-3
button.btn.btn-primary(type = 'button', ng-click = 'ctrl.edit(s.id)'): i.fa.fa-pencil
| &nbsp;
button.btn.btn-danger(type = 'button', ng-click = 'ctrl.delete(s.id)'): i.fa.fa-trash
.row
.col-sm-9
ul.list-group
li.list-group-item
span(ng-repeat = 'subject in s.subjects')
span(ng-if = 'ctrl.listSubjects[subject].email')
i.fa.fa-user
| &nbsp;{{ ctrl.listSubjects[subject].email }}&nbsp;
span(ng-if = 'ctrl.listSubjects[subject].name')
i.fa.fa-users
| &nbsp;{{ ctrl.listSubjects[subject].name }}&nbsp;
li.list-group-item
span(ng-repeat = 'template in s.objects')
span(ng-if = '(template | resolve).type == "VM-template"')
i.xo-icon-vm
| &nbsp;{{ (template | resolve).name_label }}&nbsp;
li.list-group-item
span(ng-repeat = 'sr in s.objects')
span(ng-if = '(sr | resolve).type == "SR"')
i.xo-icon-sr
| &nbsp;{{ (sr | resolve).name_label }} ({{ ((sr | resolve).$container | resolve).name_label }})&nbsp;
li.list-group-item
span(ng-repeat = 'network in s.objects')
span(ng-if = '(network | resolve).type == "network"')
i.xo-icon-network
| &nbsp;{{ (network | resolve).name_label }} ({{ ((network | resolve).$poolId | resolve).name_label }})&nbsp;
li.list-group-item(ng-if="s.limits && (s.limits.cpus || s.limits.memory || s.limits.disk)")
span(ng-if="s.limits.cpus && s.limits.cpus.total")
i.xo-icon-cpu
| &nbsp;Max. vCPUs: {{ s.limits.cpus.total }} ({{ s.limits.cpus.available }} remaining)
br
span(ng-if="s.limits.memory && s.limits.memory.total")
i.xo-icon-memory
| &nbsp;Max. RAM: {{ s.limits.memory.total | bytesToSize }} ({{ s.limits.memory.available | bytesToSize }} remaining)
br
span(ng-if="s.limits.disk && s.limits.disk.total")
i.xo-icon-disk
| &nbsp;Max. disk space: {{ s.limits.disk.total | bytesToSize }} ({{ s.limits.disk.available | bytesToSize }} remaining)
.col-sm-3
//- ul.list-group
li.list-group-item max. CPUS: {{ s.cpuMax }}
li.list-group-item max. RAM: {{ s.memoryMax }}
li.list-group-item max. Disk space: {{ s.diskMax }}
hr

View File

@@ -1,100 +0,0 @@
import angular from 'angular'
import uiBootstrap from 'angular-ui-bootstrap'
import uiRouter from 'angular-ui-router'
import slice from 'lodash.slice'
import forEach from 'lodash.foreach'
import find from 'lodash.find'
import view from './view'
// ====================================================================
export default angular.module('self.dashboard', [
uiRouter,
uiBootstrap
])
.config(function ($stateProvider) {
$stateProvider.state('self.dashboard', {
url: '/dashboard',
resolve: {
users (xo) {
return xo.user.getAll()
},
groups (xo) {
return xo.group.getAll()
}
},
controller: 'DashboardCtrl as ctrl',
template: view
})
})
.controller('DashboardCtrl', function (xo, xoApi, $scope, $window, users, groups, bytesToSizeFilter) {
this.resourceSetsPerPage = 5
$window.bytesToSize = bytesToSizeFilter // FIXME dirty workaround to custom a Chart.js tooltip template
this.get = xoApi.get
this.pageIndex = 0
this.numberOfPages = 0
this.resourceSetsToShow = []
const loadSets = () => {
xo.resourceSet.getAll()
.then(sets => {
this.resourceSets = sets
this.resourceSet = this.resourceSets[0]
this.numberOfPages = Math.ceil(sets.length / this.resourceSetsPerPage)
this.updateResourceSetsToShow()
})
}
loadSets()
this.updateResourceSetsToShow = () => {
this.resourceSetsToShow = slice(this.resourceSets, this.resourceSetsPerPage * this.pageIndex, this.resourceSetsPerPage * (this.pageIndex + 1))
}
const getList = (ids, list) => {
const collection = []
forEach(ids, id => {
const item = find(list, item => item.id === id)
if (item) {
collection.push(item)
}
})
return collection
}
this.getUsers = (ids) => getList(ids, users)
this.getGroups = (ids) => getList(ids, groups)
this.getObjectsByType = (arr) => {
const objects = {}
forEach(arr, id => {
const obj = this.get(id)
if (!objects[obj.type]) {
objects[obj.type] = []
}
objects[obj.type].push(obj)
})
return objects
}
$scope.$watch('ctrl.resourceSet', (resourceSet) => {
if (!resourceSet) {
return
}
this.cpusStats = [0, 0]
this.memoryStats = [0, 0]
this.diskStats = [0, 0]
if (resourceSet.limits.cpus) {
this.cpusStats = [resourceSet.limits.cpus.total - resourceSet.limits.cpus.available, resourceSet.limits.cpus.available]
}
if (resourceSet.limits.memory) {
this.memoryStats = [resourceSet.limits.memory.total - resourceSet.limits.memory.available, resourceSet.limits.memory.available]
}
if (resourceSet.limits.disk) {
this.diskStats = [resourceSet.limits.disk.total - resourceSet.limits.disk.available, resourceSet.limits.disk.available]
}
})
})
// A module exports its name.
.name

View File

@@ -1,123 +0,0 @@
.panel.panel-default.alert.alert-danger.text-center(ng-if="!ctrl.resourceSets.length")
| No resource set found.
a(ui-sref = 'self.admin') Create one here.
.grid(ng-if="ctrl.resourceSets.length")
.panel.panel-default
.col-sm-4(ng-if="ctrl.resourceSets.length <= ctrl.resourceSetsPerPage")
button.btn.btn-default.col-sm-1.col-sm-offset-3(
ng-if="ctrl.resourceSets.length > ctrl.resourceSetsPerPage"
style="margin-top:4px;margin-bottom:4px;"
ng-disabled="ctrl.pageIndex === 0"
ng-click="ctrl.pageIndex = ctrl.pageIndex - 1; ctrl.updateResourceSetsToShow()"
)
i.fa.fa-chevron-left
p.page-title.col-sm-4
i.fa.xo-icon-cpu(style="color: #e25440;")
| Dashboard
span(ng-if="ctrl.resourceSets.length > ctrl.resourceSetsPerPage" style="font-size: 0.8em") ({{ ctrl.pageIndex + 1 }}/{{ ctrl.numberOfPages }})
button.btn.btn-default.col-sm-1(
ng-if="ctrl.resourceSets.length > ctrl.resourceSetsPerPage"
style="margin-top:4px;margin-bottom:4px;"
ng-disabled="ctrl.pageIndex + 1 === ctrl.numberOfPages"
ng-click="ctrl.pageIndex = ctrl.pageIndex + 1; ctrl.updateResourceSetsToShow()"
)
i.fa.fa-chevron-right
.well.panel(
ng-repeat="resourceSet in ctrl.resourceSetsToShow"
ng-init="users = ctrl.getUsers(resourceSet.subjects); groups = ctrl.getGroups(resourceSet.subjects); objects = ctrl.getObjectsByType(resourceSet.objects); showDetails = false"
)
.panel.panel-default
.grid
.col-sm-4
p.page-title.col-sm-4 {{ resourceSet.name }}
.col-sm-4(style="padding-right:4px")
button.btn.btn-default.pull-right(ng-click="showDetails = !showDetails" style="margin-top:4px; margin-bottom:4px;")
i.fa(ng-class="{'fa-chevron-up': showDetails, 'fa-chevron-down': !showDetails}")
.panel.panel-default(ng-if="showDetails")
ul.list-group
li.list-group-item(ng-if="users.length")
.grid
label.control-label.col-sm-2
i.xo-icon-user
| Users:
.col-sm-10
span(ng-repeat="user in users track by user.id") {{ user.email }}
span(ng-if="!$last") ,
li.list-group-item(ng-if="groups.length")
.grid
label.control-label.col-sm-2
i.xo-icon-group
| Groups:
.col-sm-10
span(ng-repeat="group in groups track by group.id") {{ group.name }}
span(ng-if="!$last") ,
li.list-group-item(ng-if="objects['VM-template'].length")
.grid
label.control-label.col-sm-2
i.xo-icon-vm
| Templates:
.col-sm-10
span(ng-repeat="template in objects['VM-template'] track by template.id") {{ template.name_label }}
span(ng-if="!$last") ,
li.list-group-item(ng-if="objects.SR.length")
.grid
label.control-label.col-sm-2
i.xo-icon-sr
| SRs:
.col-sm-10
span(ng-repeat="sr in objects.SR track by sr.id") {{ sr.name_label }} ({{ ctrl.get(sr.$container).name_label }})
span(ng-if="!$last") ,
li.list-group-item(ng-if="objects.network.length")
.grid
label.control-label.col-sm-2
i.xo-icon-network
| Networks:
.col-sm-10
span(ng-repeat="network in objects.network track by network.id") {{ network.name_label }} ({{ ctrl.get(network.$pool).name_label }})
span(ng-if="!$last") ,
.grid-sm
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-cpu
| vCPUs
span(ng-if="resourceSet.limits.cpus" style="font-variant: normal; font-weight: normal") ({{ resourceSet.limits.cpus.total }})
.panel-body.text-center(style="height:185px")
canvas(
ng-if="resourceSet.limits.cpus"
class="chart chart-doughnut"
data="[resourceSet.limits.cpus.total - resourceSet.limits.cpus.available, resourceSet.limits.cpus.available]"
labels="['Used', 'Available']"
options='{responsive: false,tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= value %> vCPUs"}'
)
p.big-stat(ng-if="!resourceSet.limits.cpus || !resourceSet.limits.cpus.total") &infin;
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-memory
| Memory
span(ng-if="resourceSet.limits.memory" style="font-variant: normal; font-weight: normal") ({{ resourceSet.limits.memory.total | bytesToSize }})
.panel-body.text-center(style="height:185px")
canvas(
ng-if="resourceSet.limits.memory"
class="chart chart-doughnut"
data="[resourceSet.limits.memory.total - resourceSet.limits.memory.available, resourceSet.limits.memory.available]"
labels="['Used', 'Available']"
options='{responsive: false,tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>"}'
)
p.big-stat(ng-if="!resourceSet.limits.memory || !resourceSet.limits.memory.total") &infin;
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-sr
| Storage
span(ng-if="resourceSet.limits.disk" style="font-variant: normal; font-weight: normal") ({{ resourceSet.limits.disk.total | bytesToSize }})
.panel-body.text-center(style="height:185px")
canvas(
ng-if="resourceSet.limits.disk"
class="chart chart-doughnut"
data="[resourceSet.limits.disk.total - resourceSet.limits.disk.available, resourceSet.limits.disk.available]"
labels="['Used', 'Available']"
options='{responsive: false,tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>"}'
)
p.big-stat(ng-if="!resourceSet.limits.disk || !resourceSet.limits.disk.total") &infin;

View File

@@ -1,34 +0,0 @@
import angular from 'angular'
import uiRouter from 'angular-ui-router'
import admin from './admin'
import dashboard from './dashboard'
import view from './view'
export default angular.module('self', [
uiRouter,
admin,
dashboard
])
.config(function ($stateProvider) {
$stateProvider.state('self', {
abstract: true,
data: {
requireAdmin: true
},
template: view,
url: '/self'
})
// Redirect to default sub-state.
$stateProvider.state('self.index', {
url: '',
controller: function ($state) {
$state.go('self.dashboard')
}
})
})
.name

View File

@@ -1,12 +0,0 @@
.menu-grid
.side-menu
ul.nav
li
a(ui-sref = '.dashboard', ui-sref-active = 'active')
i.fa.fa-fw.xo-icon-cpu.fa-menu
span.menu-entry Dashboard
li
a(ui-sref = '.admin')
i.fa.fa-fw.fa-wrench.fa-menu
span.menu-entry Administration
.side-content(ui-view = '')

View File

@@ -1,131 +0,0 @@
import angular from 'angular'
import uiBootstrap from 'angular-ui-bootstrap'
import uiRouter from 'angular-ui-router'
import uiSelect from 'angular-ui-select'
import Bluebird from 'bluebird'
import filter from 'lodash.filter'
import forEach from 'lodash.foreach'
import xoApi from 'xo-api'
import xoServices from 'xo-services'
import view from './view'
const HIGH_LEVEL_OBJECTS = {
pool: true,
host: true,
VM: true,
SR: true,
network: true
}
export default angular.module('settings.acls', [
uiBootstrap,
uiRouter,
uiSelect,
xoApi,
xoServices
])
.config(function ($stateProvider) {
$stateProvider.state('settings.acls', {
controller: 'SettingsAcls as ctrl',
url: '/acls',
resolve: {
users (xo) {
return xo.user.getAll()
},
groups (xo) {
return xo.group.getAll()
},
roles (xo) {
return xo.role.getAll()
}
},
template: view
})
})
.controller('SettingsAcls', function ($scope, users, groups, roles, xoApi, xo, selectHighLevelFilter, filterFilter) {
const refreshAcls = () => {
xo.acl.get().then(acls => {
forEach(acls, acl => acl.newRole = acl.action)
this.acls = acls
})
}
refreshAcls()
this.types = Object.keys(HIGH_LEVEL_OBJECTS)
this.selectedTypes = {}
this.users = users
this.roles = roles
this.groups = groups
{
let usersById = this.usersById = Object.create(null)
for (let user of users) {
usersById[user.id] = user
}
let groupsById = this.groupsById = Object.create(null)
for (let group of groups) {
groupsById[group.id] = group
}
let rolesById = this.rolesById = Object.create(null)
for (let role of roles) {
rolesById[role.id] = role
}
}
this.entities = this.users.concat(this.groups)
this.objects = xoApi.all
this.getUser = (id) => {
for (let user of this.users) {
if (user.id === id) {
return user
}
}
}
this.addAcl = () => {
const promises = []
forEach(this.selectedObjects, object => promises.push(xo.acl.add(this.subject.id, object.id, this.role.id)))
this.subject = this.selectedObjects = this.role = null
Bluebird.all(promises).then(refreshAcls)
}
this.removeAcl = (subject, object, role) => {
xo.acl.remove(subject, object, role).then(refreshAcls)
}
this.editAcl = (subject, object, role, newRole) => {
console.log(subject, object, role, newRole)
xo.acl.remove(subject, object, role)
.then(xo.acl.add(subject, object, newRole))
.then(refreshAcls)
}
this.toggleType = (toggle, type) => {
const selectedObjects = this.selectedObjects && this.selectedObjects.slice() || []
if (toggle) {
const objects = filterFilter(selectHighLevelFilter(this.objects), {type})
forEach(objects, object => { selectedObjects.indexOf(object) === -1 && selectedObjects.push(object) })
this.selectedObjects = selectedObjects
} else {
const keptObjects = []
for (let index in this.selectedObjects) {
const object = this.selectedObjects[index]
if (object.type !== type) {
keptObjects.push(object)
}
}
this.selectedObjects = keptObjects
}
}
})
.filter('selectHighLevel', () => {
let isHighLevel = (object) => HIGH_LEVEL_OBJECTS[object.type]
return (objects) => filter(objects, isHighLevel)
})
.name

View File

@@ -1,88 +0,0 @@
.grid-sm
.panel.panel-default
p.page-title
i.fa.fa-key(style="color: #e25440;")
| ACLs
.grid-lg
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-plus-circle
| Create
.panel-body
form(ng-submit = 'ctrl.addAcl()')
.form-group
ui-select(ng-model = 'ctrl.subject')
ui-select-match(placeholder = 'Choose a user or group')
div
span(ng-if = '$select.selected.email')
i.xo-icon-user.fa-fw
| {{$select.selected.email}}
span(ng-if = '$select.selected.name')
i.xo-icon-group.fa-fw
| {{$select.selected.name}}
ui-select-choices(repeat = 'entity in ctrl.entities | 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}}
.form-group
ui-select(ng-model = 'ctrl.selectedObjects', multiple, close-on-select = 'false', required)
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 = 'ctrl.selectedTypes[type]', ng-change = 'ctrl.toggleType(ctrl.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 = 'ctrl.selectedTypes[type]')
.form-group
ui-select(ng-model = 'ctrl.role')
ui-select-match(placeholder = 'Choose a role')
div
i(class = 'xo-icon-{{$select.selected.type | lowercase}}')
| {{$select.selected.name}}
ui-select-choices(repeat = 'role in ctrl.roles | filter:$select.search | orderBy:"name"')
div
i(class = 'xo-icon-{{role.type | lowercase}}')
| {{role.name}}
.text-center
button.btn.btn-success
i.fa.fa-plus
| Create
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-street-view
| Manage
.panel-body
table.table.table-hover
tr
th User
th Object
th Role
th
tr(ng-repeat = 'acl in ctrl.acls | orderBy:["subject", "object"] track by acl.id')
td {{ ctrl.usersById[acl.subject].email || ctrl.groupsById[acl.subject].name }}
td {{(acl.object | resolve).name_label}}
td
select.form-control(ng-options = 'role.id as role.name for role in ctrl.roles | orderBy:"name"', ng-model = 'acl.newRole', ng-change = 'ctrl.editAcl(acl.subject, acl.object, acl.action, acl.newRole)')
td
button.btn.btn-danger(ng-click = 'ctrl.removeAcl(acl.subject, acl.object, acl.action)')
i.fa.fa-trash

View File

@@ -1,160 +0,0 @@
import angular from 'angular'
import filter from 'lodash.filter'
import find from 'lodash.find'
import uiRouter from 'angular-ui-router'
import uiSelect from 'angular-ui-select'
import uiEvent from 'angular-ui-event'
import xoApi from 'xo-api'
import xoServices from 'xo-services'
import view from './view'
export default angular.module('settings.group', [
uiRouter,
uiSelect,
uiEvent,
xoApi,
xoServices
])
.config(function ($stateProvider) {
$stateProvider.state('settings.group', {
controller: 'SettingsGroup as ctrl',
url: '/group/:groupId',
resolve: {
acls (xo) {
return xo.acl.get()
},
groups (xo) {
return xo.group.getAll()
},
roles (xo) {
return xo.role.getAll()
},
users (xo) {
return xo.user.getAll()
}
},
template: view
})
})
.controller('SettingsGroup', function ($scope, $state, $stateParams, $interval, acls, groups, roles, users, xoApi, xo) {
this.acls = acls
this.roles = roles
this.users = users
this.userEmails = Object.create(null)
this.users.forEach(user => {
this.userEmails[user.id] = user.email
})
{
let rolesById = Object.create(null)
for (let role of roles) {
rolesById[role.id] = role
}
this.rolesById = rolesById
}
this.objects = xoApi.all
this.removals = Object.create(null)
const findGroup = groups => {
this.group = filter(groups, gr => gr.id === $stateParams.groupId).pop()
if (!this.group) {
$state.go('settings.groups')
}
}
findGroup(groups)
const refreshUsers = () => {
xo.user.getAll().then(users => {
this.users = users
this.userEmails = Object.create(null)
this.users.forEach(user => {
this.userEmails[user.id] = user.email
})
})
}
const refreshGroups = () => {
if (!this.isModified()) {
xo.group.getAll().then(groups => findGroup(groups))
}
}
const refreshAcls = () => {
xo.acl.get().then(acls => {
this.acls = acls
})
}
const interval = $interval(() => {
refreshUsers()
refreshGroups()
}, 5e3)
$scope.$on('$destroy', () => {
$interval.cancel(interval)
})
this.addUserToGroup = (group, user) => {
if (user !== null) {
group.users.push(user.id)
this.addedUser = null
this.modified = true
}
}
this.saveGroup = (group) => {
const users = []
group.users.forEach(user => {
let remove = this.removals && this.removals[user]
if (!remove) {
users.push(user)
}
})
this.removals = Object.create(null)
xo.group.setUsers(group.id, users)
.then(() => {
group.users = users
this.modified = false
})
}
this.cancelEdition = () => {
this.modified = false
this.removals = Object.create(null)
refreshGroups()
}
this.isModified = () => this.modified || Object.keys(this.removals).length
this.matchesGroup = acl => {
return acl.subject === this.group.id
}
this.removeAcl = (object, role) => {
xo.acl.remove(this.group.id, object, role).then(refreshAcls)
}
})
.filter('notInGroup', function () {
return function (users, group) {
const filtered = []
users.forEach(user => {
if (!group.users || group.users.indexOf(user.id) === -1) {
filtered.push(user)
}
})
return filtered
}
})
.filter('canAccess', () => {
return (objects, group, acls) => {
const accessed = []
const groupAcls = filter(acls, acl => acl.subject === group.id)
groupAcls.forEach(acl => {
const found = find(objects, object => object.id === acl.object)
found && accessed.push(found)
})
return accessed
}
})
.name

View File

@@ -1,69 +0,0 @@
.grid-sm
.panel.panel-default
p.page-title
i.xo-icon-group(style="color: #e25440;")
| {{ ctrl.group.name }}&nbsp;
a.btn.btn-default(ui-sref = 'settings.groups')
i.fa.fa-level-up
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-street-view
| Members&nbsp;
span(ng-if = 'ctrl.isModified()') (*)
.panel-body
ul.list-group(ng-if = '!ctrl.group.users.length')
li.list-group-item.disabled: em (empty)
ul.list-group(ng-if = 'ctrl.group.users.length')
li.list-group-item(ng-repeat = 'user in ctrl.group.users')
span(ng-if = '!ctrl.removals[user]') {{ ctrl.userEmails[user] }}&nbsp;
del(ng-if = 'ctrl.removals[user]') {{ ctrl.userEmails[user] }}&nbsp;
span.pull-right
label
input.hidden(type = 'checkbox', ng-model = 'ctrl.removals[user]')
| &nbsp;
i.fa.fa-trash-o(tooltip="Remove user from group", style = 'cursor: pointer')
p
ui-select(ng-if = '(ctrl.users | notInGroup:ctrl.group).length', ng-model = 'ctrl.addedUser', on-select = 'ctrl.addUserToGroup(ctrl.group, ctrl.addedUser)')
ui-select-match(
placeholder = 'Choose a user to add'
) {{$select.selected.email}}
ui-select-choices(
repeat = 'addedUser in ctrl.users | notInGroup:ctrl.group | filter:$select.search'
) {{addedUser.email}}
em.text-muted(ng-if = '!(ctrl.users | notInGroup:ctrl.group).length') No available users to add
button.btn.btn-primary(ng-if = 'ctrl.isModified()', type="button", ng-click = 'ctrl.saveGroup(ctrl.group)')
i.fa.fa-save
| Save
| &nbsp;
button.btn.btn-default(ng-if = 'ctrl.isModified()', type="button", ng-click = 'ctrl.cancelEdition()')
i.fa.fa-times
| Cancel
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-key
| ACLs&nbsp;
.panel-body
table.table.table-hover
tr
th Object
th Role
th
tr(ng-repeat = 'acl in ctrl.acls | filter:ctrl.matchesGroup track by acl.id')
td {{(acl.object | resolve).name_label}}
td {{ ctrl.rolesById[acl.action].name }}
td
button.btn.btn-danger(ng-click = 'ctrl.removeAcl(acl.object, acl.action)')
i.fa.fa-trash
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-eye
| Accessible objects&nbsp;
.panel-body
p(ng-repeat = 'object in ctrl.objects | selectHighLevel | canAccess:ctrl.group:ctrl.acls | orderBy:["type", "name_label"]')
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}})

View File

@@ -1,189 +0,0 @@
import angular from 'angular'
import uiRouter from 'angular-ui-router'
import uiSelect from 'angular-ui-select'
import uiEvent from 'angular-ui-event'
import xoApi from 'xo-api'
import xoServices from 'xo-services'
import view from './view'
import modal from './modal'
export default angular.module('settings.groups', [
uiRouter,
uiSelect,
uiEvent,
xoApi,
xoServices
])
.config(function ($stateProvider) {
$stateProvider.state('settings.groups', {
controller: 'SettingsGroups as ctrl',
url: '/groups',
resolve: {
users (xo) {
return xo.user.getAll()
},
groups (xo) {
return xo.group.getAll()
}
},
template: view
})
})
.controller('SettingsGroups', function ($scope, $interval, users, groups, xoApi, xo, $modal) {
this.uiCollapse = Object.create(null)
this.addedUsers = []
this.users = users
this.userEmails = Object.create(null)
this.users.forEach(user => {
this.userEmails[user.id] = user.email
})
this.groups = groups
const selectedGroups = this.selectedGroups = {}
this.newGroups = []
const refreshUsers = () => {
xo.user.getAll().then(users => {
this.users = users
this.userEmails = Object.create(null)
this.users.forEach(user => {
this.userEmails[user.id] = user.email
})
})
}
const refreshGroups = () => {
if (!this._editingGroup && !this.modified) {
return xo.group.getAll().then(groups => this.groups = groups)
} else {
return this.groups
}
}
const interval = $interval(() => {
refreshUsers()
refreshGroups()
}, 5e3)
$scope.$on('$destroy', () => {
$interval.cancel(interval)
})
this.addGroup = () => {
this.newGroups.push({
// Fake (unique) id needed by Angular.JS
id: Math.random()
})
}
if (!this.groups.length) {
this.addGroup()
}
this.deleteGroup = id => {
const modalInstance = $modal.open({
template: modal,
backdrop: false
})
return modalInstance.result
.then(() => {
return xo.group.delete(id)
.then(() => {
return refreshGroups()
})
.then(groups => {
if (!groups.length) {
this.addGroup()
}
})
})
.catch(() => {})
}
this.saveGroups = () => {
const newGroups = this.newGroups
const groups = this.groups
const updateGroups = []
for (let i = 0, len = groups.length; i < len; i++) {
const group = groups[i]
const {id} = group
if (selectedGroups[id]) {
delete selectedGroups[id]
xo.group.delete(id)
} else {
xo.group.set(group)
updateGroups.push(group)
}
}
for (let i = 0, len = newGroups.length; i < len; i++) {
const group = newGroups[i]
const {name} = group
if (!name) {
continue
}
xo.group.create({name})
.then(function (id) {
group.id = id
group.users = []
})
updateGroups.push(group)
}
this.groups = updateGroups
this.newGroups.length = 0
this.modified = false
if (!this.groups.length) {
this.addGroup()
}
}
this.addUserToGroup = (group, index) => {
group.users.push(this.addedUsers[index].id)
delete this.addedUsers[index]
}
this.flagUserRemoval = (group, index, remove) => {
group.removals || (group.removals = {})
group.removals[group.users[index]] = remove
}
this.saveGroup = (group) => {
const users = []
group.users.forEach(user => {
let remove = group.removals && group.removals[user]
if (!remove) {
users.push(user)
}
})
group.removals && delete group.removals
xo.group.setUsers(group.id, users)
.then(() => {
group.users = users
this.uiCollapse[group.id] = false
})
}
this.editingGroup = (editing = undefined) => editing !== undefined && (this._editingGroup = editing) || this._editingGroup
this.cancelModifications = () => {
this.newGroups.length = 0
this.editingGroup(false)
this.modified = false
refreshGroups()
}
})
.filter('notInGroup', function () {
return function (users, group) {
const filtered = []
users.forEach(user => {
if (!group.users || group.users.indexOf(user.id) === -1) {
filtered.push(user)
}
})
return filtered
}
})
.name

View File

@@ -1,12 +0,0 @@
.modal-header
button.close(
type = 'button',
ng-click = '$dismiss()'
)
span(aria-hidden = 'true') &times;
h4.modal-title Confirm group suppression
.modal-body
p Are you sure you want to delete this group ? It's user list and associated ACLs will be lost after that.
button.btn.btn-default(type = 'button', ng-click = '$close()') Ok
| &ensp;
button.btn.btn-default(type = 'button', ng-click = '$dismiss()') Cancel

View File

@@ -1,49 +0,0 @@
.grid-sm
.panel.panel-default
p.page-title
i.xo-icon-group(style="color: #e25440;")
| Groups
.grid-sm
.panel.panel-default
form(ng-submit="ctrl.saveGroups()", autocomplete="off").panel-body
table.table.table-hover
tr
th.col-md-5 Name
th.col-md-5 Information
th.col-md-2
tr(ng-repeat="group in ctrl.groups | orderBy:natural('id') track by group.id")
td
input.form-control(type="text", ng-model="group.name", ui-event = '{focus: "ctrl.editingGroup(true)", blur: "ctrl.editingGroup(false)"}', ng-change = 'ctrl.modified = true')
td
span(ng-if = '!group.users.length'): em (empty)
span(ng-if = 'group.users.length')
strong {{ group.users.length }} members:&nbsp;
span(ng-repeat = 'user in group.users | limitTo:4')
| {{ ctrl.userEmails[user] }}{{ $last ? (group.users.length > 4 ? ',...' : '') : ', ' }}
| &nbsp;
td
a.btn.btn-primary(ui-sref = 'settings.group({groupId: group.id})')
| Edit&nbsp;
i.fa.fa-pencil
| &nbsp;
button.btn.btn-danger(type = 'button', ng-click = 'ctrl.deleteGroup(group.id)')
i.fa.fa-trash
tr(ng-repeat="group in ctrl.newGroups")
td
input.form-control(type = "text", ng-model = "group.name", placeholder = "New group name", ng-change = 'ctrl.modified = true')
td
button.btn.btn-btn-default(type = 'button', ng-click = 'ctrl.newGroups.splice($index, 1)')
i.fa.fa-times
td &#160;
p
button.btn.btn-success(type="button", ng-click="ctrl.addGroup()")
i.fa.fa-plus
| &nbsp;
span(ng-if = 'ctrl.modified')
button.btn.btn-primary(type="submit")
i.fa.fa-save
| Save
| &nbsp;
button.btn.btn-default(type="button", ng-click = "ctrl.cancelModifications()")
i.fa.fa-times
| Cancel

View File

@@ -1,45 +0,0 @@
import angular from 'angular'
import uiRouter from 'angular-ui-router'
import acls from './acls'
import group from './group'
import groups from './groups'
import plugins from './plugins'
import servers from './servers'
import update from './update'
import user from './user'
import users from './users'
import view from './view'
export default angular.module('settings', [
uiRouter,
acls,
group,
groups,
plugins,
servers,
update,
user,
users
])
.config(function ($stateProvider) {
$stateProvider.state('settings', {
abstract: true,
data: {
requireAdmin: true
},
template: view,
url: '/settings'
})
// Redirect to default sub-state.
$stateProvider.state('settings.index', {
url: '',
controller: function ($state) {
$state.go('settings.servers')
}
})
})
.name

View File

@@ -1,213 +0,0 @@
import angular from 'angular'
import find from 'lodash.find'
import forEach from 'lodash.foreach'
import includes from 'lodash.includes'
import trim from 'lodash.trim'
import uiRouter from 'angular-ui-router'
import remove from 'lodash.remove'
import xoApi from 'xo-api'
import xoServices from 'xo-services'
import view from './view'
function loadDefaults (schema, configuration) {
if (!schema || !configuration) {
return
}
forEach(schema.properties, (item, key) => {
if (item.type === 'boolean' && !(key in configuration)) { // String default values are used as placeholders in view
configuration[key] = Boolean(item && item.default)
}
})
}
function setOptionalProperties (configurationSchema) {
if (!configurationSchema) {
return
}
forEach(configurationSchema.properties, (property, key) => {
let { required } = configurationSchema
if (!required) {
required = configurationSchema.required = []
}
property.optional = !includes(required, key)
const { type, items } = property
if (type === 'object') {
setOptionalProperties(property)
} else if (type === 'array' && items && items.type === 'object') {
setOptionalProperties(items)
}
})
}
function cleanUpConfiguration (schema, configuration, dump = {}) {
if (!schema || !configuration) {
return
}
function sanitizeItem (item) {
if (typeof item === 'string') {
item = trim(item)
}
return item
}
function keepItem (item) {
return !(item == null || item === '' || (Array.isArray(item) && item.length === 0))
}
forEach(configuration, (item, key) => {
item = sanitizeItem(item)
configuration[key] = item
dump[key] = item
if (!keepItem(item) || !schema.properties || !(key in schema.properties)) {
delete dump[key]
} else if (schema.properties && schema.properties[key]) {
const type = schema.properties[key].type
if (type === 'integer' || type === 'number') {
dump[key] = +dump[key]
} else if (type === 'object') {
dump[key] = {}
cleanUpConfiguration(schema.properties[key], item, dump[key])
}
}
})
}
export default angular.module('settings.plugins', [
uiRouter,
xoApi,
xoServices
])
.config(function ($stateProvider) {
$stateProvider.state('settings.plugins', {
controller: 'SettingsPlugins as ctrl',
url: '/plugins',
data: {
requireAdmin: true
},
resolve: {
},
template: view
})
})
.controller('SettingsPlugins', function (xo, notify, modal) {
this.disabled = {}
const preparePluginForView = plugin => {
const { configurationSchema } = plugin
plugin._loaded = plugin.loaded
plugin._autoload = plugin.autoload
if (!plugin.configuration) {
plugin.configuration = {}
}
setOptionalProperties(configurationSchema)
loadDefaults(configurationSchema, plugin.configuration)
}
const refreshPlugin = id => {
return xo.plugin.get()
.then(plugins => {
const plugin = find(plugins, plugin => plugin.id === id)
if (plugin) {
preparePluginForView(plugin)
remove(this.plugins, plugin => plugin.id === id)
this.plugins.push(plugin)
}
})
}
const refreshPlugins = () => xo.plugin.get().then(plugins => {
forEach(plugins, preparePluginForView)
this.plugins = plugins
})
refreshPlugins()
const _execPluginMethod = (id, method, ...args) => {
this.disabled[id] = true
return xo.plugin[method](...args)
.finally(() => {
this.disabled[id] = false
})
}
this.configure = (plugin) => {
const newConfiguration = {}
plugin.errors = []
cleanUpConfiguration(plugin.configurationSchema, plugin.configuration, newConfiguration)
_execPluginMethod(plugin.id, 'configure', plugin.id, newConfiguration)
.then(() => {
notify.info({
title: 'Plugin configuration',
message: 'Successfully saved'
})
refreshPlugin(plugin.id)
})
.catch(err => {
forEach(err.data, data => {
const fieldPath = data.field.split('.').slice(1)
const fieldPathTitles = []
let groupObject = plugin.configurationSchema
forEach(fieldPath, groupName => {
groupObject = groupObject.properties[groupName]
fieldPathTitles.push(groupObject.title || groupName)
})
plugin.errors.push(`${fieldPathTitles.join(' > ')} ${data.message}`)
})
})
}
this.purgeConfiguration = (plugin) => {
modal.confirm({
title: 'Purge configuration',
message: 'Are you sure you want to purge this configuration ?'
}).then(() => {
_execPluginMethod(plugin.id, 'purgeConfiguration', plugin.id).then(() => {
refreshPlugin(plugin.id).then(() =>
notify.info({
title: 'Purge configuration',
message: 'This plugin config is now purged.'
})
)
})
})
}
this.toggleAutoload = (plugin) => {
let method
if (!plugin._autoload && plugin.autoload) {
method = 'disableAutoload'
} else if (plugin._autoload && !plugin.autoload) {
method = 'enableAutoload'
}
if (method) {
_execPluginMethod(plugin.id, method, plugin.id)
}
}
this.toggleLoad = (plugin) => {
let method
if (!plugin._loaded && plugin.loaded && plugin.unloadable !== false) {
method = 'unload'
} else if (plugin._loaded && !plugin.loaded) {
method = 'load'
}
if (method) {
_execPluginMethod(plugin.id, method, plugin.id)
refreshPlugin(plugin.id)
}
}
})
.name

View File

@@ -1,52 +0,0 @@
.grid-sm
.panel.panel-default
p.page-title
i.xo-icon-plugin(style="color: #e25440;")
| Plugins
.grid-sm
.panel.panel-default
.panel-body
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
.checkbox.small
label
i.fa.fa-2x(ng-class = '{"fa-toggle-on": plugin.loaded, "fa-toggle-off": !plugin.loaded, "text-success": plugin.loaded}')
span(ng-if = 'plugin.loaded && plugin.unloadable === false')
| &nbsp;
i.fa.fa-2x.fa-lock(tooltip = 'This plugin cannot be unloaded without a server restart')
input.hidden(type = 'checkbox', ng-model = 'plugin._loaded', ng-change = 'ctrl.toggleLoad(plugin)', ng-disabled = 'plugin.unloadable === false && plugin.loaded || ctrl.disabled[plugin.id]')
| &nbsp;&nbsp;
span.text-info {{ plugin.name }}
span(style="font-size:0.7em") (v{{ plugin.version }})
.checkbox.small
label
| Auto-load at server start&nbsp;
input(type = 'checkbox', ng-model = 'plugin._autoload', ng-change = 'ctrl.toggleAutoload(plugin)', ng-disabled = 'ctrl.disabled[plugin.id]')
.form-group.pull-right.small
button.btn.btn-default(type = 'button', ng-click = 'isExpanded = !isExpanded'): i.fa(ng-class = '{"fa-plus": !isExpanded, "fa-minus": isExpanded}')
hr
div(collapse = '!isExpanded')
p(ng-if = '!plugin.configurationSchema') This plugin has no specific configuration
form.form-horizontal(form = 'pluginform' ng-if = 'plugin.configurationSchema', ng-submit = 'ctrl.configure(plugin)')
fieldset(ng-disabled = 'ctrl.disabled[plugin.id]')
object-input(
form = '"pluginform"',
property = 'plugin.configurationSchema',
model = 'plugin.configuration',
group = ''
)
.form-group
.col-md-offset-2.col-md-10.text-danger(ng-repeat = "err in plugin.errors")
| {{ err }}
.form-group
.col-md-offset-2.col-md-10
.btn-toolbar
.btn-group
button.btn.btn-primary(type = 'submit')
| Save configuration&nbsp;
i.fa.fa-floppy-o
.btn-group
button.btn.btn-danger(type = 'button' ng-click = 'ctrl.purgeConfiguration(plugin)')
| Purge configuration&nbsp;
i.fa.fa-trash-o

View File

@@ -1,148 +0,0 @@
import angular from 'angular'
import forEach from 'lodash.foreach'
import includes from 'lodash.includes'
import uiRouter from 'angular-ui-router'
import uiSelect from 'angular-ui-select'
import xoApi from 'xo-api'
import xoServices from 'xo-services'
import view from './view'
export default angular.module('settings.servers', [
uiRouter,
uiSelect,
xoApi,
xoServices
])
.config(function ($stateProvider) {
$stateProvider.state('settings.servers', {
controller: 'SettingsServers as ctrl',
url: '/servers',
resolve: {
servers (xo) {
return xo.server.getAll()
}
},
template: view
})
})
.controller('SettingsServers', function ($scope, $rootScope, $interval, $filter, servers, xoApi, xo, notify) {
const orderBy = $filter('orderBy')
this.servers = orderBy(servers, $rootScope.natural('host'))
$scope.readOnly = {}
forEach(this.servers, (server) => {
$scope.readOnly[server.id] = Boolean(server.readOnly)
})
const selected = this.selectedServers = {}
const newServers = this.newServers = []
const refreshServers = () => {
xo.server.getAll().then(servers => {
this.servers = orderBy(servers, $rootScope.natural('host'))
})
}
const refreshServersIfUnfocused = () => {
if (!$scope.isFocused) {
refreshServers()
}
}
const interval = $interval(refreshServersIfUnfocused, 10e3)
$scope.$on('$destroy', () => {
$interval.cancel(interval)
})
this.connectServer = (id) => {
notify.info({
title: 'Server connect',
message: 'Connecting the server...'
})
xo.server.connect(id).catch(error => {
notify.error({
title: 'Server connection error',
message: error.message
})
})
}
this.disconnectServer = (id) => {
notify.info({
title: 'Server disconnect',
message: 'Disconnecting the server...'
})
xo.server.disconnect(id)
}
this.addServer = () => {
newServers.push({
// Fake (unique) id needed by Angular.JS
id: Math.random(),
status: 'connecting'
})
}
this.addServer()
this.saveServers = () => {
const addresses = []
forEach(xoApi.getView('host').all, host => addresses.push(host.address))
const newServers = this.newServers
const servers = this.servers
const updateServers = []
for (let i = 0, len = servers.length; i < len; i++) {
const server = servers[i]
const {id} = server
if (selected[id]) {
delete selected[id]
xo.server.remove(id)
} else {
if (!server.password) {
delete server.password
}
server.readOnly = $scope.readOnly[id]
xo.server.set(server)
delete server.password
updateServers.push(server)
}
}
for (let i = 0, len = newServers.length; i < len; i++) {
const server = newServers[i]
const {host, username, password, readOnly} = server
if (!host) {
continue
}
if (includes(addresses, host)) {
notify.warning({
title: 'Server already connected',
message: `You are already connected to ${host}`
})
continue
}
xo.server.add({
host,
username,
password,
readOnly,
autoConnect: false
}).then(function (id) {
server.id = id
$scope.readOnly[id] = Boolean(readOnly)
xo.server.connect(id).catch(error => {
notify.error({
title: 'Server connection error',
message: error.message
})
})
})
delete server.password
updateServers.push(server)
}
this.servers = updateServers
this.newServers.length = 0
this.addServer()
}
})
.name

View File

@@ -1,98 +0,0 @@
.grid-sm
.panel.panel-default
p.page-title
i.fa.fa-cloud(style="color: #e25440;")
| Servers
.grid-sm
.panel.panel-default
form(ng-submit="ctrl.saveServers()", autocomplete="off").panel-body
table.table.table-hover
tr
th.col-md-5 Host
th.col-md-2 User
th.col-md-2 Password
th.col-md-1.text.center Actions
th.col-md-1.text.center Read only
th.col-md-1.text-center
i.fa.fa-trash-o.fa-lg(tooltip="Forget server")
tr(ng-repeat="server in ctrl.servers track by server.id")
td
.input-group
span.input-group-addon.hidden-xs(ng-if="server.status === 'connected'")
i.xo-icon-success.fa-lg(tooltip="Connected")
span.input-group-addon.hidden-xs(ng-if="server.status === 'disconnected'")
i.xo-icon-failure.fa-lg(tooltip="Disconnected")
span.input-group-addon.hidden-xs(ng-if="server.status === 'connecting'")
i.fa.fa-cog.fa-lg.fa-spin(tooltip="Connecting...")
input.form-control(
type="text",
ng-model="server.host",
ng-focus="$parent.isFocused = true",
ng-blur="$parent.isFocused = false"
)
td
input.form-control(
type="text",
ng-model="server.username",
ng-focus="$parent.isFocused = true",
ng-blur="$parent.isFocused = false"
)
td
input.form-control(
type="password",
ng-model="server.password",
placeholder="Fill to change the password",
ng-focus="$parent.isFocused = true",
ng-blur="$parent.isFocused = false"
)
td.text-center
button.btn.btn-default(
ng-if="server.status === 'disconnected'",
type="button",
ng-click="ctrl.connectServer(server.id)",
tooltip="Reconnect this server"
)
i.fa.fa-link
button.btn.btn-danger(
ng-if="server.status === 'connected'",
type="button",
ng-click="ctrl.disconnectServer(server.id)"
tooltip="Disconnect this server"
)
i.fa.fa-unlink
td.text-center
input(type="checkbox", ng-model="readOnly[server.id]")
td.text-center
input(type="checkbox", ng-model="ctrl.selectedServers[server.id]")
tr(ng-repeat="server in ctrl.newServers")
td
input.form-control(
type = "text"
ng-model = "server.host"
placeholder = "address[:port]"
)
td
input.form-control(
type = "text"
ng-model = "server.username"
ng-required = "server.host"
placeholder = "user"
)
td
input.form-control(
type="password"
ng-model="server.password"
ng-required = "server.host"
placeholder="password"
)
td &#160;
td.text-center
input( type="checkbox", ng-model="server.readOnly")
td &#160;
p.text-center
button.btn.btn-primary(type="submit")
i.fa.fa-save
| Save
| &nbsp;
button.btn.btn-success(type="button", ng-click="ctrl.addServer()")
i.fa.fa-plus

View File

@@ -1,99 +0,0 @@
import angular from 'angular'
import uiRouter from 'angular-ui-router'
import _assign from 'lodash.assign'
import ansiUp from 'ansi_up'
import updater from '../../updater'
import xoApi from 'xo-api'
import xoServices from 'xo-services'
import {AuthenticationFailed} from '../../updater'
import view from './view'
export default angular.module('settings.update', [
uiRouter,
updater,
xoApi,
xoServices
])
.config(function ($stateProvider) {
$stateProvider.state('settings.update', {
controller: 'SettingsUpdate as ctrl',
url: '/update',
onExit: updater => {
updater.removeAllListeners('end')
},
template: view
})
})
.filter('ansitohtml', function ($sce) {
return function (input) {
return $sce.trustAsHtml(ansiUp.ansi_to_html(input))
}
})
.controller('SettingsUpdate', function (xoApi, xo, updater, notify) {
this.updater = updater
this.updater.isRegistered()
.then(() => this.updater.on('end', () => this.updater.isRegistered()))
.catch(err => console.error(err))
this.updater.getConfiguration()
.then(configuration => this.configuration = _assign({}, configuration))
.then(() => this.withAuth = Boolean(this.configuration.proxyUser))
.catch(error => notify.error({
title: 'XOA Updater',
message: error.message
}))
this.registerXoa = (email, password, renewRegister) => {
this.regPwd = ''
this.updater.register(email, password, renewRegister)
.tap(() => this.renewRegister = false)
.then(() => this.updater.update())
.catch(AuthenticationFailed, () => {})
.catch(err => console.error(err))
}
this.update = () => {
this.updater.update()
.catch(error => notify.error({
title: 'XOA Updater',
message: error.message
}))
}
this.upgrade = () => {
this.updater.upgrade()
.catch(error => notify.error({
title: 'XOA Updater',
message: error.message
}))
}
this.configure = (host, port, username, password) => {
const config = {}
if (!this.withAuth) {
username = null
password = null
}
config.proxyHost = host && host.trim() || null
config.proxyPort = port && port.trim() || null
config.proxyUser = username || null
config.proxyPassword = password || null
return this.updater.configure(config)
.then(configuration => this.configuration = _assign({}, configuration))
.then(() => this.withAuth = Boolean(this.configuration.proxyUser))
.catch(error => notify.error({
title: 'XOA Updater',
message: error.message
}))
.finally(() => this.update())
}
this.valid = trial => {
return trial && trial.end && Date.now() < trial.end
}
})
.name

View File

@@ -1,128 +0,0 @@
.grid-sm
.panel.panel-default
p.page-title
i.fa.fa-refresh(style="color: #e25440;")
| Update
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-globe
| Status
.panel-body
p(ng-if = '!ctrl.updater.state')
a.btn.btn-warning: i.fa.fa-question-circle(ng-if = '!ctrl.updater.state', tooltip = 'No update information available')
| &nbsp;No update information available&nbsp;
a.btn.btn-default(ng-class = '{disabled: ctrl.updater.isConnected}', ng-click = 'ctrl.update()')
i.fa.fa-refresh(ng-class = '{"fa-spin": ctrl.updater.isConnected}')
.form-group(ng-if = 'ctrl.updater.state && ctrl.updater.state === "registerNeeded"')
a.btn.btn-warning(ng-if = 'ctrl.updater.state === "registerNeeded"'): i.fa.fa-bell-slash(tooltip = 'Your XOA is not registered for updates')
| &nbsp;Registration needed&nbsp;
button.btn.btn-default(ng-if = 'ctrl.updater.registerState === "registered"', ng-click = 'ctrl.updater.update()', ng-class = '{disabled: ctrl.updater.updating || ctrl.updater.upgrading}'): i.fa.fa-refresh(ng-class = '{"fa-spin": ctrl.updater.updating || ctrl.updater.upgrading}')
.form-group(ng-if = 'ctrl.updater.state && ctrl.updater.state !== "registerNeeded"')
a.btn.btn-info(ng-if = 'ctrl.updater.state === "connected"'): i.fa.fa-question-circle(tooltip = 'Update information may be available')
a.btn.btn-success(ng-if = 'ctrl.updater.state === "upToDate"'): i.fa.fa-check(tooltip = 'Your XOA is up-to-date')
a.btn.btn-primary(ng-if = 'ctrl.updater.state === "upgradeNeeded"'): i.fa.fa-bell(tooltip = 'You need to update your XOA (new version is available)')
a.btn.btn-danger(ng-if = 'ctrl.updater.state === "error"'): i.fa.fa-exclamation-triangle(tooltip = 'Can\'t fetch update information')
| &nbsp;
button#update.btn.btn-info(type = 'button', ng-click = 'ctrl.update()', ng-class = '{disabled: ctrl.updater.updating || ctrl.updater.upgrading}')
| Check for updates&nbsp;
i.fa.fa-refresh(ng-class = '{"fa-spin": ctrl.updater.updating}')
| &nbsp;
button#upgrade.btn.btn-primary(ng-if = 'ctrl.updater.state === "upgradeNeeded"', type = 'button', ng-click = 'ctrl.upgrade()', ng-class = '{disabled: ctrl.updater.updating || ctrl.updater.upgrading}')
| Upgrade&nbsp;
i.fa.fa-cog(ng-class = '{"fa-spin": ctrl.updater.upgrading}')
div
p(ng-repeat = 'entry in ctrl.updater._log')
span(ng-class = '{"text-danger": entry.level === "error", "text-muted": entry.level === "info", "text-warning": entry.level === "warning", "text-success": entry.level === "success"}') {{ entry.date }}
| &nbsp;:&nbsp;
span(style = 'word-wrap: break-word;', ng-bind-html = 'entry.message | ansitohtml')
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-pencil
| Registration
.panel-body.text-center
.text-warning(ng-if = '!ctrl.updater.state || ctrl.updater.registerState === "unknown"')
| No registration information available.
br
span.big-stat
i.fa.fa-exclamation-triangle.text-warning
div(ng-if = 'ctrl.updater.state && ctrl.updater.registerState === "error"')
.text-danger Can't fetch registration information.
br
span.big-stat
i.fa.fa-exclamation-triangle.text-danger
br
.text-danger {{ ctrl.updater.registerError }}
br
button.btn.btn-default(type = 'button', ng-click = 'ctrl.updater.isRegistered()')
i.fa.fa-refresh
| Refresh
form(ng-if = 'ctrl.updater.state && (ctrl.renewRegister || ctrl.updater.registerState === "unregistered")', ng-submit = 'ctrl.registerXoa(ctrl.regEmail, ctrl.regPwd, ctrl.renewRegister)')
p.form-static-control(ng-if = '!ctrl.renewRegister') XOA is not registered.
p.form-static-control(ng-if = 'ctrl.renewRegister')
| Forget previous registration ?&nbsp;
button.btn.btn-default(type = 'button', ng-click = 'ctrl.renewRegister = false') Cancel
p.small Your xen-orchestra.com email and password
.form-group
.input-group
span.input-group-addon: i.fa.fa-envelope-o.fa-fw
label.sr-only(for = 'regEmail') Email
input#regEmail.form-control(type = 'email', placeholder = 'Email', ng-model = 'ctrl.regEmail', required)
.form-group
.input-group
span.input-group-addon: i.fa.fa-key.fa-fw
label.sr-only(for = 'regPwd') Email
input#regPwd.form-control(type = 'password', placeholder = 'Password', ng-model = 'ctrl.regPwd', required)
.form-group
button.btn.btn-primary(type = 'submit')
i.fa.fa-check
| Register
p.form-static-control.text-danger {{ ctrl.updater.registerError }}
p(ng-if = 'ctrl.updater.state && ctrl.updater.registerState === "registered" && !ctrl.renewRegister')
| Your Xen Orchestra appliance is registered to
span.text-success {{ ctrl.updater.token.registrationEmail }}
| .
br
br
i.fa.fa-check-circle.fa-3x.text-success
br
br
button.btn.btn-default(type = 'button', ng-click = 'ctrl.renewRegister = true') Register to someone else ?
.grid-sm(ng-if = 'ctrl.updater.state && ctrl.configuration')
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-cogs
| Settings
.panel-body
form(ng-submit = 'ctrl.configure(ctrl.configuration.proxyHost, ctrl.configuration.proxyPort, ctrl.configuration.proxyUser, ctrl.configuration.proxyPassword)')
h4
i.fa.fa-globe
| Proxy settings
p
| If you need a proxy to access the Internet&ensp;
label
input(type = 'checkbox', ng-model = 'ctrl.withAuth')
| with authentication
fieldset.form-inline
.form-group
//- label.control-label Host:&nbsp;
input.form-control(type = 'text', ng-model = 'ctrl.configuration.proxyHost', placeholder = 'Host (myproxy.example.org)')
| &nbsp;
.form-group
//- label.control-label Port:&nbsp;
input.form-control(type = 'text', ng-model = 'ctrl.configuration.proxyPort', placeholder = 'Port (3128 ?...)')
br
div(ng-hide = '!ctrl.withAuth')
fieldset.form-inline(ng-disabled = '!ctrl.withAuth')
.form-group
input.form-control(type = 'text', ng-model = 'ctrl.configuration.proxyUser', placeholder = 'User name', required)
| &nbsp;
.form-group
input.form-control(type = 'password', ng-model = 'ctrl.configuration.proxyPassword', placeholder = 'Password')
br
fieldset
.form-group
button.btn.btn-primary(type = 'submit')
i.fa.fa-floppy-o
| Save

View File

@@ -1,48 +0,0 @@
import angular from 'angular'
import uiRouter from 'angular-ui-router'
import xoApi from 'xo-api'
import xoServices from 'xo-services'
import view from './view'
export default angular.module('settings.user', [
uiRouter,
xoApi,
xoServices
])
.config(function ($stateProvider) {
$stateProvider.state('settings.user', {
controller: 'SettingsUser as ctrl',
url: '/user',
data: {
requireAdmin: false
},
resolve: {
},
template: view
})
})
.controller('SettingsUser', function (xo, notify) {
this.changePassword = function (oldPassword, newPassword) {
this.working = true
xo.user.changePassword(oldPassword, newPassword)
.then(() => {
this.oldPassword = ''
this.newPassword = ''
this.confirmPassword = ''
notify.info({
title: 'Change password',
message: 'Password has been successfully change'
})
})
.catch(error => notify.error({
title: 'Change password',
message: error.message
}))
.finally(() => this.working = false)
}
})
.name

View File

@@ -1,21 +0,0 @@
.grid-sm
.panel.panel-default
p.page-title
i.xo-icon-user(style="color: #e25440;")
| Profile
.grid-sm
.panel.panel-default
.panel-body
.row
.col-sm-6
form(ng-submit = 'ctrl.changePassword(ctrl.oldPassword, ctrl.newPassword)')
fieldset(ng-disabled = 'ctrl.working')
legend Change password
.form-group
input.form-control(type = 'password', ng-model = 'ctrl.oldPassword', placeholder = 'Current password', required)
.form-group
input.form-control(type = 'password', ng-model = 'ctrl.newPassword', placeholder = 'New password', required)
.form-group(ng-class = '{"has-error": ctrl.confirmPassword && ctrl.newPassword && (ctrl.confirmPassword !== ctrl.newPassword)}')
input.form-control(type = 'password', ng-model = 'ctrl.confirmPassword', placeholder = 'Confirm password', required)
.form-group
button.btn.btn-primary(type = 'submit', ng-disabled = '!ctrl.oldPassword || !ctrl.newPassword || ctrl.newPassword !== ctrl.confirmPassword') Save password

View File

@@ -1,132 +0,0 @@
import angular from 'angular'
import passwordGenerator from 'password-generator'
import uiRouter from 'angular-ui-router'
import uiSelect from 'angular-ui-select'
import uiEvent from 'angular-ui-event'
import xoApi from 'xo-api'
import xoServices from 'xo-services'
import view from './view'
export default angular.module('settings.users', [
uiRouter,
uiSelect,
uiEvent,
xoApi,
xoServices
])
.config(function ($stateProvider) {
$stateProvider.state('settings.users', {
controller: 'SettingsUsers as ctrl',
url: '/users',
resolve: {
users (xo) {
return xo.user.getAll()
}
},
template: view
})
})
.controller('SettingsUsers', function ($scope, $interval, users, xoApi, xo) {
this.users = users
this.permissions = [
{
label: 'User',
value: 'none'
},
{
label: 'Admin',
value: 'admin'
}
]
const selected = this.selectedUsers = {}
this.newUsers = []
const refreshUsers = () => {
if (!this._editingUser) {
xo.user.getAll().then(users => {
this.users = users
this.userEmails = Object.create(null)
this.users.forEach(user => {
this.userEmails[user.id] = user.email
})
})
}
}
const interval = $interval(() => {
refreshUsers()
}, 5e3)
$scope.$on('$destroy', () => {
$interval.cancel(interval)
})
this.addUser = () => {
this.newUsers.push({
// Fake (unique) id needed by Angular.JS
id: Math.random(),
permission: 'none'
})
}
this.addUser()
this.saveUsers = () => {
const newUsers = this.newUsers
const users = this.users
const updateUsers = []
for (let i = 0, len = users.length; i < len; i++) {
const user = users[i]
const {id} = user
if (selected[id]) {
delete selected[id]
xo.user.delete(id)
} else {
if (!user.password) {
delete user.password
}
xo.user.set(user)
delete user.password
updateUsers.push(user)
}
}
for (let i = 0, len = newUsers.length; i < len; i++) {
const user = newUsers[i]
const {email, permission, password} = user
if (!email) {
continue
}
xo.user.create({
email,
permission,
password
}).then(function (id) {
user.id = id
})
delete user.password
updateUsers.push(user)
}
this.users = updateUsers
this.newUsers.length = 0
this.userEmails = Object.create(null)
this.users.forEach(user => {
this.userEmails[user.id] = user.email
})
this.addUser()
}
this.editingUser = editing => {
this._editingUser = editing
}
this.generatePassword = (user) => {
// Generate password of 8 letters/numbers/underscore
user.password = passwordGenerator(8, false)
}
})
.name

View File

@@ -1,111 +0,0 @@
.grid-sm
.panel.panel-default
p.page-title
i.xo-icon-user(style="color: #e25440;")
| Users
.grid-sm
.panel.panel-default
.panel-body
form(ng-submit="ctrl.saveUsers()", autocomplete="off")
table.table.table-hover
tr
th.col-md-4 Email
th.col-md-4 Permissions
th.col-md-3 Password
th.col-md-1.text-center
i.fa.fa-trash-o.fa-lg(tooltip="Remove user")
tr(ng-repeat="user in ctrl.users | orderBy:natural('id') track by user.id")
td
input.form-control(
type="text"
ng-model="user.email"
ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}'
)
td
select.form-control(
ng-options="p.value as p.label for p in ctrl.permissions"
ng-model="user.permission"
ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}'
)
td
div.input-group
span.input-group-btn
button.btn.btn-default(
type = "button"
tooltip = "Generate random password"
ng-click = "ctrl.generatePassword(user); showPassword = true"
)
i.fa.fa-key
input.form-control(
type = "{{ showPassword ? 'text' : 'password' }}"
ng-model = "user.password"
placeholder = "Fill to change the password"
ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}'
)
span.input-group-btn
button.btn.btn-default(
type = "button"
tooltip = "Reveal password"
ng-show = "user.password.length > 0"
ng-mousedown = "showPassword = true"
ng-mouseup = "showPassword = false"
ng-mouseleave = "showPassword = false"
)
i.fa.fa-eye(ng-if = "showPassword")
i.fa.fa-eye-slash(ng-if = "!showPassword")
td.text-center
input(
type="checkbox"
ng-model="ctrl.selectedUsers[user.id]"
ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}'
)
tr(ng-repeat="user in ctrl.newUsers")
td
input.form-control(
type = "text"
ng-model = "user.email"
placeholder = "email"
ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}'
)
td
select.form-control(
ng-options = "p.value as p.label for p in ctrl.permissions"
ng-model = "user.permission"
ng-required = "user.email"
ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}'
)
td
div.input-group
span.input-group-btn
button.btn.btn-default(
type = "button"
tooltip = "Generate random password"
ng-click = "ctrl.generatePassword(user); showPassword = true"
)
i.fa.fa-key
input.form-control(
type = "{{ showPassword ? 'text' : 'password' }}"
ng-model = "user.password"
ng-required = "user.email"
placeholder = "password"
ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}'
)
span.input-group-btn
button.btn.btn-default(
type = "button"
tooltip = "Reveal password"
ng-show = "user.password.length > 0"
ng-mousedown = "showPassword = true"
ng-mouseup = "showPassword = false"
ng-mouseleave = "showPassword = false"
)
i.fa.fa-eye(ng-if = "showPassword")
i.fa.fa-eye-slash(ng-if = "!showPassword")
td &#160;
p.text-center
button.btn.btn-primary(type="submit")
i.fa.fa-save
| Save
| &nbsp;
button.btn.btn-success(type="button", ng-click="ctrl.addUser()")
i.fa.fa-plus

View File

@@ -1,28 +0,0 @@
.menu-grid
.side-menu
ul.nav
li
a(ui-sref = '.servers', ui-sref-active = 'active')
i.fa.fa-fw.fa-cloud.fa-menu
span.menu-entry Servers
li
a(ui-sref = '.users')
i.xo-icon-user.fa-fw.fa-menu
span.menu-entry Users
li
a(ui-sref = '.groups')
i.xo-icon-group.fa-fw.fa-menu
span.menu-entry Groups
li
a(ui-sref = '.acls')
i.fa.fa-fw.fa-key.fa-menu
span.menu-entry ACLs
li
a(ui-sref = '.plugins')
i.xo-icon-plugin.fa-fw.fa-menu
span.menu-entry Plugins
li
a(ui-sref = '.update')
i.fa.fa-fw.fa-refresh.fa-menu
span.menu-entry Update
.side-content(ui-view = '')

View File

@@ -1,263 +0,0 @@
import angular from 'angular'
import escapeRegExp from 'lodash.escaperegexp'
import filter from 'lodash.filter'
import forEach from 'lodash.foreach'
import isEmpty from 'lodash.isempty'
import trim from 'lodash.trim'
import uiRouter from 'angular-ui-router'
import Bluebird from 'bluebird'
import xoTag from 'tag'
import view from './view'
// ===================================================================
export default angular.module('xoWebApp.sr', [
uiRouter,
xoTag
])
.config(function ($stateProvider) {
$stateProvider.state('SRs_view', {
url: '/srs/:id',
controller: 'SrCtrl',
template: view
})
})
.filter('vdiFilter', (xoApi, filterFilter) => {
return (input, search) => {
search && (search = trim(search).toLowerCase())
return filter(input, vdi => {
let vbd, vm
let vmName = vdi.$VBDs && vdi.$VBDs[0] && (vbd = xoApi.get(vdi.$VBDs[0])) && (vm = xoApi.get(vbd.VM)) && vm.name_label
vmName && (vmName = vmName.toLowerCase())
return !search || (vmName && (vmName.search(escapeRegExp(search)) !== -1) || filterFilter([vdi], search).length)
})
}
})
.controller('SrCtrl', function ($scope, $stateParams, $state, $q, notify, xoApi, xo, modal, $window, bytesToSizeFilter, sizeToBytesFilter) {
$window.bytesToSize = bytesToSizeFilter // FIXME dirty workaround to custom a Chart.js tooltip template
$scope.units = ['MiB', 'GiB', 'TiB']
$scope.currentLogPage = 1
$scope.currentVDIPage = 1
let {get} = xoApi
$scope.$watch(() => xoApi.get($stateParams.id), function (SR) {
const VDIs = []
if (SR) {
forEach(SR.VDIs, vdi => {
vdi = xoApi.get(vdi)
if (vdi) {
const size = bytesToSizeFilter(vdi.size)
VDIs.push({...vdi, size, sizeValue: size.split(' ')[0], sizeUnit: size.split(' ')[1]})
}
})
}
$scope.SR = SR
$scope.VDIs = VDIs
})
$scope.selectedForDelete = {}
$scope.deleteSelectedVdis = function () {
return modal.confirm({
title: 'VDI deletion',
message: 'Are you sure you want to delete all selected VDIs? This operation is irreversible.'
}).then(function () {
forEach($scope.selectedForDelete, (selected, id) => selected && xo.vdi.delete(id))
$scope.selectedForDelete = {}
})
}
$scope.saveSR = function ($data) {
let {SR} = $scope
let {name_label, name_description} = $data
$data = {
id: SR.id
}
if (name_label !== SR.name_label) {
$data.name_label = name_label
}
if (name_description !== SR.name_description) {
$data.name_description = name_description
}
return xoApi.call('sr.set', $data)
}
$scope.deleteVDI = function (id) {
console.log('Delete VDI', id)
return modal.confirm({
title: 'VDI deletion',
message: 'Are you sure you want to delete this VDI? This operation is irreversible.'
}).then(function () {
return xo.vdi.delete(id)
})
}
$scope.disconnectVBD = function (id) {
console.log('Disconnect VBD', id)
return modal.confirm({
title: 'VDI disconnection',
message: 'Are you sure you want to disconnect this VDI?'
}).then(function () {
return xoApi.call('vbd.disconnect', {id: id})
})
}
$scope.connectPBD = function (id) {
console.log('Connect PBD', id)
return xo.pbd.connect(id)
}
$scope.disconnectPBD = function (id) {
console.log('Disconnect PBD', id)
return xo.pbd.disconnect(id)
}
$scope.reconnectAllHosts = function () {
// TODO: return a Bluebird.all(promises).
for (let id of $scope.SR.$PBDs) {
let pbd = xoApi.get(id)
xoApi.call('pbd.connect', {id: pbd.id})
}
}
$scope.disconnectAllHosts = function () {
return modal.confirm({
title: 'Disconnect hosts',
message: 'Are you sure you want to disconnect all hosts to this SR?'
}).then(function () {
for (let id of $scope.SR.$PBDs) {
let pbd = xoApi.get(id)
xoApi.call('pbd.disconnect', {id: pbd.id})
console.log(pbd.id)
}
})
}
$scope.rescanSr = function (id) {
console.log('Rescan SR', id)
return xoApi.call('sr.scan', {id: id})
}
$scope.removeSR = function (id) {
console.log('Remove SR', id)
return modal.confirm({
title: 'SR deletion',
message: 'Are you sure you want to delete this SR? This operation is irreversible.'
}).then(function () {
return Bluebird.map($scope.SR.$PBDs, pbdId => {
let pbd = xoApi.get(pbdId)
return xoApi.call('pbd.disconnect', { id: pbd.id })
})
}).then(function () {
return xoApi.call('sr.destroy', {id: id})
}).then(function () {
$state.go('index')
notify.info({
title: 'SR remove',
message: 'SR is removed'
})
})
}
$scope.forgetSR = function (id) {
console.log('Forget SR', id)
return modal.confirm({
title: 'SR forget',
message: 'Are you sure you want to forget this SR? No VDI on this SR will be removed.'
}).then(function () {
return Bluebird.map($scope.SR.$PBDs, pbdId => {
let pbd = xoApi.get(pbdId)
return xoApi.call('pbd.disconnect', { id: pbd.id })
})
}).then(function () {
return xoApi.call('sr.forget', {id: id})
}).then(function () {
$state.go('index')
notify.info({
title: 'SR forget',
message: 'SR is forgotten'
})
})
}
$scope.saveDisks = function (data) {
// Group data by disk.
let disks = {}
let sizeChanges = false
forEach(data, function (value, key) {
let i = key.indexOf('/')
let id = key.slice(0, i)
let prop = key.slice(i + 1)
;(disks[id] || (disks[id] = {}))[prop] = value
})
forEach(disks, function (attributes, id) {
let disk = get(id)
attributes.size = bytesToSizeFilter(sizeToBytesFilter(attributes.sizeValue + ' ' + attributes.sizeUnit))
if (attributes.size !== bytesToSizeFilter(disk.size)) { // /!\ attributes are provided by a modified copy of disk
sizeChanges = true
return false
}
})
let promises = []
const preCheck = sizeChanges ? modal.confirm({
title: 'Disk resizing',
message: 'Growing the size of a disk is not reversible'
}) : $q.resolve()
return preCheck
.then(() => {
forEach(disks, function (attributes, id) {
let disk = get(id)
// Resize disks
attributes.size = bytesToSizeFilter(sizeToBytesFilter(attributes.sizeValue + ' ' + attributes.sizeUnit))
if (attributes.size !== bytesToSizeFilter(disk.size)) { // /!\ attributes are provided by a modified copy of disk
promises.push(xo.disk.resize(id, attributes.size))
}
delete attributes.size
// Keep only changed attributes.
forEach(attributes, function (value, name) {
if (value === disk[name]) {
delete attributes[name]
}
})
if (!isEmpty(attributes)) {
// Inject id.
attributes.id = id
// Ask the server to update the object.
promises.push(xoApi.call('vdi.set', attributes))
}
})
return $q.all(promises)
})
}
})
// A module exports its name.
.name

Some files were not shown because too many files have changed in this diff Show More