Compare commits
241 Commits
v5.x.all.i
...
xo-web/v4.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
155debc864 | ||
|
|
a5975ac38b | ||
|
|
204f1cfd6b | ||
|
|
2d22e043a0 | ||
|
|
c26cacaf4e | ||
|
|
f0048544e2 | ||
|
|
cf227dbfa2 | ||
|
|
a5f8bdbe61 | ||
|
|
e442553c6f | ||
|
|
7134acfcd6 | ||
|
|
82439f444e | ||
|
|
1a17908488 | ||
|
|
9af30e99f8 | ||
|
|
6f942c3417 | ||
|
|
57083c90cd | ||
|
|
e28bcdd978 | ||
|
|
0b4a5ab2eb | ||
|
|
034704a330 | ||
|
|
5c60eaf6ab | ||
|
|
f5709eac2c | ||
|
|
5a5e714aca | ||
|
|
747d48e4d9 | ||
|
|
07a0200f30 | ||
|
|
1c5313f2d9 | ||
|
|
05e08719fb | ||
|
|
ca0e616f88 | ||
|
|
a8d20caba4 | ||
|
|
0d4bbb0a48 | ||
|
|
b9cc219530 | ||
|
|
e204ab5871 | ||
|
|
16d0c05b4b | ||
|
|
6f8329d191 | ||
|
|
d751463b26 | ||
|
|
d3b66eff59 | ||
|
|
4257d0332a | ||
|
|
b80442c061 | ||
|
|
3a0f6820ad | ||
|
|
1bc92f5363 | ||
|
|
818ddcf01e | ||
|
|
618ba361c7 | ||
|
|
599160a325 | ||
|
|
35fba6f4ed | ||
|
|
a14aad75fd | ||
|
|
3513e85b0b | ||
|
|
66c0390fc7 | ||
|
|
a6549ccb08 | ||
|
|
15d2878014 | ||
|
|
d271be8723 | ||
|
|
6f9d2d99dd | ||
|
|
5d62664ee3 | ||
|
|
7124d9f2f8 | ||
|
|
0459744771 | ||
|
|
417544b781 | ||
|
|
f9028cb366 | ||
|
|
9a264719a9 | ||
|
|
96c213dcc4 | ||
|
|
dec1a8e204 | ||
|
|
a17fd697e2 | ||
|
|
a6ab66e799 | ||
|
|
17095ec3c6 | ||
|
|
82687147b8 | ||
|
|
ba76422c1f | ||
|
|
083b3c4ece | ||
|
|
5ecfdf38a8 | ||
|
|
dd1acf3c2a | ||
|
|
76e9c2d196 | ||
|
|
15f046959d | ||
|
|
bf3ba04624 | ||
|
|
d997894d9a | ||
|
|
c1059db6e5 | ||
|
|
8ad29a2836 | ||
|
|
93a454b835 | ||
|
|
da899386ec | ||
|
|
05d22903ea | ||
|
|
33945520f1 | ||
|
|
40284809cf | ||
|
|
efc18aaaec | ||
|
|
348441b046 | ||
|
|
66601b2e7c | ||
|
|
724c5e4b73 | ||
|
|
7eff29bc65 | ||
|
|
ca002003c2 | ||
|
|
f0675f1f3c | ||
|
|
976186c525 | ||
|
|
89d5777e52 | ||
|
|
8dbb69809c | ||
|
|
7348bd5d15 | ||
|
|
9a46a466f7 | ||
|
|
fafc5c8553 | ||
|
|
4ffdfaa506 | ||
|
|
e3989840ee | ||
|
|
b3e6f531a1 | ||
|
|
4f6ee34592 | ||
|
|
3ae58a323e | ||
|
|
26b958c270 | ||
|
|
12a4af5900 | ||
|
|
69479d538c | ||
|
|
829397dd5a | ||
|
|
2bc89026db | ||
|
|
ebbc44d181 | ||
|
|
2228a1e36b | ||
|
|
a8cbf3e8ff | ||
|
|
fa32e3d734 | ||
|
|
0d17148ff0 | ||
|
|
aa38411cf7 | ||
|
|
4913c8699d | ||
|
|
1035a11487 | ||
|
|
15c2efe706 | ||
|
|
d7fd71bb62 | ||
|
|
b11ee993fa | ||
|
|
614aa7873c | ||
|
|
1adf31fe15 | ||
|
|
824ffd7b5b | ||
|
|
c31c6fdebb | ||
|
|
83f3276429 | ||
|
|
d21f68ce54 | ||
|
|
18b1e1b133 | ||
|
|
0edaa40052 | ||
|
|
627077c8f3 | ||
|
|
a897b1798d | ||
|
|
50e39993bf | ||
|
|
5e397dd01e | ||
|
|
f57ff5d5e0 | ||
|
|
5c3e40917c | ||
|
|
90a2dc4581 | ||
|
|
b64243fdd6 | ||
|
|
42db87d305 | ||
|
|
e7ab1b589a | ||
|
|
e9979c9887 | ||
|
|
3bb9bb56f0 | ||
|
|
5a99474c55 | ||
|
|
182ee6c25f | ||
|
|
4d3f0a06db | ||
|
|
0e182c519b | ||
|
|
b1ee30ce7d | ||
|
|
93ba764e23 | ||
|
|
433e17bb81 | ||
|
|
61c09083ad | ||
|
|
018377e724 | ||
|
|
b76f9513ba | ||
|
|
40ebb7ba75 | ||
|
|
a9e52e8954 | ||
|
|
3c8876cac7 | ||
|
|
b7e005f9c7 | ||
|
|
e6fe0a19fa | ||
|
|
fba11b6a44 | ||
|
|
c270e7f5dd | ||
|
|
9ee00d345e | ||
|
|
0379fbc4eb | ||
|
|
9748a3ae91 | ||
|
|
1881944748 | ||
|
|
3721fa194c | ||
|
|
8c3fcad20b | ||
|
|
decf373d0b | ||
|
|
ff1d50f993 | ||
|
|
ef34204b59 | ||
|
|
270b636d80 | ||
|
|
ac01da2ae9 | ||
|
|
0136310c54 | ||
|
|
ecf4cf852e | ||
|
|
c66384adfb | ||
|
|
98bdda629d | ||
|
|
a8286f9cba | ||
|
|
fa3db4fcf6 | ||
|
|
ddac0cfee1 | ||
|
|
9368673459 | ||
|
|
43dc999ab5 | ||
|
|
3b7333e866 | ||
|
|
bc0ddbaf16 | ||
|
|
45f0ae7e1c | ||
|
|
a521c4ae01 | ||
|
|
5b8238adeb | ||
|
|
ec330474fa | ||
|
|
ece28904a8 | ||
|
|
4f1c495afb | ||
|
|
5fdd27b7e6 | ||
|
|
91f449af9a | ||
|
|
efc0a0dfe3 | ||
|
|
fee47baa66 | ||
|
|
0ad7bfc7e7 | ||
|
|
bd64143ae1 | ||
|
|
ec982ba9a3 | ||
|
|
6280f6ff98 | ||
|
|
35d20390a9 | ||
|
|
c487c5042f | ||
|
|
aaf7927aa2 | ||
|
|
3c677f3d21 | ||
|
|
94eb76b3a6 | ||
|
|
a921cb2d0d | ||
|
|
f3aaa363d8 | ||
|
|
45a79e1920 | ||
|
|
6fd9b2a453 | ||
|
|
01d8e89a71 | ||
|
|
c89fa63910 | ||
|
|
9fc5c49dbf | ||
|
|
7dfc269df9 | ||
|
|
76d0b397db | ||
|
|
5413f887af | ||
|
|
b3d0c61f0e | ||
|
|
4ce0441d68 | ||
|
|
72be34e18d | ||
|
|
d2961b7650 | ||
|
|
fdca1bbf72 | ||
|
|
ab7a2f9dee | ||
|
|
7b72857a3b | ||
|
|
4787146658 | ||
|
|
430f9356c3 | ||
|
|
70a3b3518f | ||
|
|
c0944c17e0 | ||
|
|
da1b2a91e7 | ||
|
|
aa27492713 | ||
|
|
afe589dec3 | ||
|
|
978d140c8f | ||
|
|
2ce213b62c | ||
|
|
7748266078 | ||
|
|
83783d07a1 | ||
|
|
49a1f2c7c5 | ||
|
|
ddfc0151fc | ||
|
|
81c508e13c | ||
|
|
7195cfc3cf | ||
|
|
93fe5e2cf7 | ||
|
|
a2bf795d12 | ||
|
|
c8d78f39e0 | ||
|
|
d9ab8a1c8b | ||
|
|
5125ad4889 | ||
|
|
951e85b04b | ||
|
|
711d922695 | ||
|
|
3692ffcde7 | ||
|
|
b049420c59 | ||
|
|
241103c369 | ||
|
|
2128367113 | ||
|
|
f555c8190d | ||
|
|
d5df633def | ||
|
|
fe7dc859e3 | ||
|
|
569c5046c6 | ||
|
|
e0210ae2d8 | ||
|
|
f85dc3b7e7 | ||
|
|
92d4363120 | ||
|
|
6c69220de2 | ||
|
|
3a1229b072 | ||
|
|
45538c9f62 |
@@ -1,8 +1,10 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- 'stable'
|
||||
- '0.12'
|
||||
|
||||
# Use containers.
|
||||
# http://docs.travis-ci.com/user/workers/container-based-infrastructure/
|
||||
sudo: false
|
||||
|
||||
before_install:
|
||||
- npm i -g npm
|
||||
|
||||
133
CHANGELOG.md
133
CHANGELOG.md
@@ -1,5 +1,138 @@
|
||||
# ChangeLog
|
||||
|
||||
## **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
|
||||
|
||||
10
README.md
10
README.md
@@ -38,8 +38,8 @@ Otherwise, please consider using the [bugtracker of the general repository](http
|
||||
## Process for new release
|
||||
|
||||
```bash
|
||||
# Switch to the master branch.
|
||||
git checkout master
|
||||
# Switch to the stable branch.
|
||||
git checkout stable
|
||||
|
||||
# Fetches latest changes.
|
||||
git pull --ff-only
|
||||
@@ -53,12 +53,12 @@ npm version minor
|
||||
# Go back to the next-release branch.
|
||||
git checkout next-release
|
||||
|
||||
# Fetches the last changes (the merge and version bump) from master to
|
||||
# Fetches the last changes (the merge and version bump) from stable to
|
||||
# next-release.
|
||||
git merge --ff-only master
|
||||
git merge --ff-only stable
|
||||
|
||||
# Push the changes on git.
|
||||
git push --follow-tags origin master next-release
|
||||
git push --follow-tags origin stable next-release
|
||||
|
||||
# Publish this release to npm.
|
||||
npm publish
|
||||
|
||||
@@ -22,12 +22,14 @@ 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 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'
|
||||
@@ -57,12 +59,14 @@ export default angular.module('xoWebApp', [
|
||||
genericModalState,
|
||||
hostState,
|
||||
listState,
|
||||
migrateVmState,
|
||||
navbarState,
|
||||
newSrState,
|
||||
newVmState,
|
||||
poolState,
|
||||
settingsState,
|
||||
srState,
|
||||
taskScheduler,
|
||||
treeState,
|
||||
updater,
|
||||
vmState,
|
||||
@@ -72,7 +76,7 @@ export default angular.module('xoWebApp', [
|
||||
// Prevent Angular.js from mangling exception stack (interfere with
|
||||
// source maps).
|
||||
.factory('$exceptionHandler', () => function (exception) {
|
||||
throw exception
|
||||
console.log(exception && exception.stack || exception)
|
||||
})
|
||||
|
||||
.config(function (
|
||||
|
||||
@@ -29,6 +29,7 @@ export default angular.module('backup.backup', [
|
||||
|
||||
this.ready = false
|
||||
|
||||
this.running = {}
|
||||
this.comesForEditing = $stateParams.id
|
||||
this.scheduleApi = {}
|
||||
this.formData = {}
|
||||
@@ -220,6 +221,16 @@ export default angular.module('backup.backup', [
|
||||
})
|
||||
}
|
||||
|
||||
this.run = schedule => {
|
||||
this.running[schedule.id] = true
|
||||
notify.info({
|
||||
title: 'Run Job',
|
||||
message: 'One shot running started. See overview for logs.'
|
||||
})
|
||||
const id = schedule.job
|
||||
return xo.job.runSequence([id]).finally(() => delete this.running[schedule.id])
|
||||
}
|
||||
|
||||
this.sanitizePath = (...paths) => (paths[0] && paths[0].charAt(0) === '/' && '/' || '') + filter(map(paths, s => s && filter(map(s.split('/'), trim)).join('/'))).join('/')
|
||||
|
||||
this.resetData = () => {
|
||||
|
||||
@@ -108,7 +108,7 @@ form#backupform(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.s
|
||||
th.hidden-xs Remote
|
||||
th.hidden-xs Depth
|
||||
th.hidden-xs Scheduling
|
||||
th.hidden-xs Only MetaData.hidden-xs
|
||||
th.hidden-xs Only MetaData
|
||||
th.hidden-xs Compression DISABLED
|
||||
th Enabled now
|
||||
th
|
||||
@@ -141,4 +141,6 @@ form#backupform(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.s
|
||||
td.text-right
|
||||
button.btn.btn-primary(type = 'button', ng-click = 'ctrl.edit(schedule)'): i.fa.fa-pencil
|
||||
|
|
||||
button.btn.btn-warning(type = 'button', ng-click = 'ctrl.run(schedule)', ng-disabled = 'ctrl.running[schedule.id]'): i.fa.fa-play
|
||||
|
|
||||
button.btn.btn-danger(type = 'button', ng-click = 'ctrl.delete(schedule)'): i.fa.fa-trash-o
|
||||
|
||||
222
app/modules/backup/continuous-replication/index.js
Normal file
222
app/modules/backup/continuous-replication/index.js
Normal file
@@ -0,0 +1,222 @@
|
||||
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 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.scheduleApi.setCron(cronPattern)
|
||||
}
|
||||
|
||||
this.save = (id, vms, tag, sr, cron, enabled) => {
|
||||
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) : save(id, vms, tag, sr, cron)
|
||||
return _save
|
||||
.then(() => {
|
||||
notify.info({
|
||||
title: 'Continuous Replication',
|
||||
message: 'Job schedule successfuly saved'
|
||||
})
|
||||
this.resetData()
|
||||
})
|
||||
.finally(refresh)
|
||||
}
|
||||
|
||||
const save = (id, vms, tag, sr, cron) => {
|
||||
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})
|
||||
})
|
||||
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) => {
|
||||
const values = []
|
||||
forEach(vms, vm => {
|
||||
values.push({vm: vm.id, tag, sr: sr.id})
|
||||
})
|
||||
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.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
|
||||
135
app/modules/backup/continuous-replication/view.jade
Normal file
135
app/modules/backup/continuous-replication/view.jade
Normal file
@@ -0,0 +1,135 @@
|
||||
.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)')
|
||||
.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
|
||||
| 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
|
||||
.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
|
||||
|
|
||||
i.fa.fa-arrow-right
|
||||
|
|
||||
i.fa.fa-database
|
||||
| Save
|
||||
|
|
||||
button.btn.btn-lg.btn-default(type = 'button', ng-click = 'ctrl.resetData()')
|
||||
| Reset
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-list-ul
|
||||
| Schedules
|
||||
.panel-body
|
||||
.text-center(ng-if = '!ctrl.schedules'): i.xo-icon-loading
|
||||
.text-center(ng-if = 'ctrl.schedules && !ctrl.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 | 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
|
||||
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') ({{ ((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
|
||||
|
|
||||
button.btn.btn-warning(type = 'button', ng-click = 'ctrl.run(schedule)', ng-disabled = 'ctrl.running[schedule.id]'): i.fa.fa-play
|
||||
|
|
||||
button.btn.btn-danger(type = 'button', ng-click = 'ctrl.delete(schedule)'): i.fa.fa-trash-o
|
||||
259
app/modules/backup/delta-backup/index.js
Normal file
259
app/modules/backup/delta-backup/index.js
Normal file
@@ -0,0 +1,259 @@
|
||||
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)
|
||||
if (remote.type !== 'smb') {
|
||||
r[remote.id] = remote
|
||||
}
|
||||
})
|
||||
this.remotes = r
|
||||
if (selectRemoteId) {
|
||||
this.formData.remote = this.remotes[selectRemoteId]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const refreshSchedules = () => {
|
||||
return xo.schedule.getAll()
|
||||
.then(schedules => {
|
||||
const s = {}
|
||||
forEach(schedules, schedule => {
|
||||
this.jobs && this.jobs[schedule.job] && this.jobs[schedule.job].key === JOBKEY && (s[schedule.id] = schedule)
|
||||
})
|
||||
this.schedules = s
|
||||
})
|
||||
}
|
||||
|
||||
const refreshJobs = () => {
|
||||
return xo.job.getAll()
|
||||
.then(jobs => {
|
||||
const j = {}
|
||||
forEach(jobs, job => {
|
||||
j[job.id] = job
|
||||
})
|
||||
this.jobs = j
|
||||
})
|
||||
}
|
||||
|
||||
const refresh = () => refreshRemotes().then(refreshJobs).then(refreshSchedules)
|
||||
|
||||
this.getReady = () => refresh().then(() => this.ready = true)
|
||||
this.getReady()
|
||||
|
||||
const interval = $interval(refresh, 5e3)
|
||||
$scope.$on('$destroy', () => $interval.cancel(interval))
|
||||
|
||||
const toggleState = (toggle, state) => {
|
||||
const selectedVms = this.formData.selectedVms.slice()
|
||||
if (toggle) {
|
||||
const vms = filterFilter(selectHighLevelFilter(this.objects), {type: 'VM'})
|
||||
forEach(vms, vm => {
|
||||
if (vm.power_state === state) {
|
||||
(selectedVms.indexOf(vm) === -1) && selectedVms.push(vm)
|
||||
}
|
||||
})
|
||||
this.formData.selectedVms = selectedVms
|
||||
} else {
|
||||
const keptVms = []
|
||||
for (let index in this.formData.selectedVms) {
|
||||
if (this.formData.selectedVms[index].power_state !== state) {
|
||||
keptVms.push(this.formData.selectedVms[index])
|
||||
}
|
||||
}
|
||||
this.formData.selectedVms = keptVms
|
||||
}
|
||||
}
|
||||
|
||||
this.toggleAllRunning = toggle => toggleState(toggle, 'Running')
|
||||
this.toggleAllHalted = toggle => toggleState(toggle, 'Halted')
|
||||
|
||||
this.edit = schedule => {
|
||||
const vms = filterFilter(selectHighLevelFilter(this.objects), {type: 'VM'})
|
||||
const job = this.jobs[schedule.job]
|
||||
const selectedVms = []
|
||||
forEach(job.paramsVector.items[0].values, value => {
|
||||
const vm = find(vms, vm => vm.id === value.vm)
|
||||
vm && selectedVms.push(vm)
|
||||
})
|
||||
const tag = job.paramsVector.items[0].values[0].tag
|
||||
const depth = job.paramsVector.items[0].values[0].depth
|
||||
const cronPattern = schedule.cron
|
||||
const remoteId = job.paramsVector.items[0].values[0].remote
|
||||
|
||||
this.resetData()
|
||||
this.formData.selectedVms = selectedVms
|
||||
this.formData.tag = tag
|
||||
this.formData.depth = depth
|
||||
this.formData.scheduleId = schedule.id
|
||||
this.formData.remote = this.remotes[remoteId]
|
||||
this.scheduleApi.setCron(cronPattern)
|
||||
}
|
||||
|
||||
this.save = (id, vms, remoteId, tag, depth, cron, enabled) => {
|
||||
if (!vms.length) {
|
||||
notify.warning({
|
||||
title: 'No Vms selected',
|
||||
message: 'Choose VMs to backup'
|
||||
})
|
||||
return
|
||||
}
|
||||
const _save = (id === undefined) ? saveNew(vms, remoteId, tag, depth, cron, enabled) : save(id, vms, remoteId, tag, depth, cron)
|
||||
return _save
|
||||
.then(() => {
|
||||
notify.info({
|
||||
title: 'Backup',
|
||||
message: 'Job schedule successfuly saved'
|
||||
})
|
||||
this.resetData()
|
||||
})
|
||||
.finally(refresh)
|
||||
}
|
||||
|
||||
const save = (id, vms, remoteId, tag, depth, cron) => {
|
||||
const schedule = this.schedules[id]
|
||||
const job = this.jobs[schedule.job]
|
||||
const values = []
|
||||
forEach(vms, vm => {
|
||||
values.push({
|
||||
vm: vm.id,
|
||||
remote: remoteId,
|
||||
tag,
|
||||
depth
|
||||
})
|
||||
})
|
||||
job.paramsVector.items[0].values = values
|
||||
return xo.job.set(job)
|
||||
.then(response => {
|
||||
if (response) {
|
||||
return xo.schedule.set(schedule.id, undefined, cron, undefined)
|
||||
} else {
|
||||
notify.error({
|
||||
title: 'Update schedule',
|
||||
message: 'Job updating failed'
|
||||
})
|
||||
throw new Error('Job updating failed')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const saveNew = (vms, remoteId, tag, depth, cron, enabled) => {
|
||||
const values = []
|
||||
forEach(vms, vm => {
|
||||
values.push({
|
||||
vm: vm.id,
|
||||
remote: remoteId,
|
||||
tag,
|
||||
depth
|
||||
})
|
||||
})
|
||||
const job = {
|
||||
type: 'call',
|
||||
key: JOBKEY,
|
||||
method: 'vm.rollingDeltaBackup',
|
||||
paramsVector: {
|
||||
type: 'crossProduct',
|
||||
items: [{
|
||||
type: 'set',
|
||||
values
|
||||
}]
|
||||
}
|
||||
}
|
||||
return xo.job.create(job)
|
||||
.then(jobId => xo.schedule.create(jobId, cron, enabled))
|
||||
}
|
||||
|
||||
this.delete = schedule => {
|
||||
let jobId = schedule.job
|
||||
return xo.schedule.delete(schedule.id)
|
||||
.then(() => xo.job.delete(jobId))
|
||||
.finally(() => {
|
||||
if (this.formData.scheduleId === schedule.id) {
|
||||
this.resetData()
|
||||
}
|
||||
refresh()
|
||||
})
|
||||
}
|
||||
|
||||
this.run = schedule => {
|
||||
this.running[schedule.id] = true
|
||||
notify.info({
|
||||
title: 'Run Job',
|
||||
message: 'One shot running started. See overview for logs.'
|
||||
})
|
||||
const id = schedule.job
|
||||
return xo.job.runSequence([id]).finally(() => delete this.running[schedule.id])
|
||||
}
|
||||
|
||||
this.sanitizePath = (...paths) => (paths[0] && paths[0].charAt(0) === '/' && '/' || '') + filter(map(paths, s => s && filter(map(s.split('/'), trim)).join('/'))).join('/')
|
||||
|
||||
this.resetData = () => {
|
||||
this.formData.allRunning = false
|
||||
this.formData.allHalted = false
|
||||
this.formData.selectedVms = []
|
||||
this.formData.scheduleId = undefined
|
||||
this.formData.tag = undefined
|
||||
this.formData.path = undefined
|
||||
this.formData.depth = undefined
|
||||
this.formData.enabled = false
|
||||
this.formData.remote = undefined
|
||||
this.scheduleApi && this.scheduleApi.resetData && this.scheduleApi.resetData()
|
||||
}
|
||||
|
||||
this.size = size
|
||||
this.prettyCron = prettyCron.toString.bind(prettyCron)
|
||||
|
||||
if (!this.comesForEditing) {
|
||||
refresh()
|
||||
} else {
|
||||
refresh()
|
||||
.then(() => {
|
||||
this.edit(this.schedules[this.comesForEditing])
|
||||
delete this.comesForEditing
|
||||
})
|
||||
}
|
||||
this.resetData()
|
||||
this.objects = xoApi.all
|
||||
})
|
||||
|
||||
// A module exports its name.
|
||||
.name
|
||||
132
app/modules/backup/delta-backup/view.jade
Normal file
132
app/modules/backup/delta-backup/view.jade
Normal file
@@ -0,0 +1,132 @@
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.fa.fa-download(style="color: #e25440;")
|
||||
| Delta Backup
|
||||
form#backupform(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.selectedVms, ctrl.formData.remote.id, ctrl.formData.tag, ctrl.formData.depth, ctrl.formData.cronPattern, ctrl.formData.enabled)')
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.xo-icon-vm
|
||||
| VMs to backup
|
||||
.panel-body.form-horizontal
|
||||
.text-center(ng-if = '!ctrl.formData'): i.xo-icon-loading
|
||||
.container-fluid(ng-if = 'ctrl.formData')
|
||||
.alert.alert-info(ng-if = '!ctrl.formData.scheduleId') Creating New Backup
|
||||
.alert.alert-warning(ng-if = 'ctrl.formData.scheduleId') Modifying Backup ID {{ ctrl.formData.scheduleId }}
|
||||
.form-group
|
||||
label.control-label.col-md-2(for = 'tag') Tag
|
||||
.col-md-10
|
||||
input#tag.form-control(form = 'backupform', ng-model = 'ctrl.formData.tag', placeholder = 'Back-up tag', required)
|
||||
.form-group(ng-class = '{"has-warning": !ctrl.formData.selectedVms.length}')
|
||||
label.control-label.col-md-2(for = 'vmlist') VMs
|
||||
.col-md-8
|
||||
ui-select(form = 'backupform', ng-model = 'ctrl.formData.selectedVms', multiple, close-on-select = 'false', required)
|
||||
ui-select-match(placeholder = 'Choose VMs to backup')
|
||||
i.xo-icon-working(ng-if="isVMWorking($item)")
|
||||
i(class="xo-icon-{{$item.power_state | lowercase}}",ng-if="!isVMWorking($item)")
|
||||
| {{$item.name_label}}
|
||||
span(ng-if="$item.$container")
|
||||
| ({{($item.$container | resolve).name_label}})
|
||||
ui-select-choices(repeat = 'vm in ctrl.objects | selectHighLevel | filter:{type: "VM"} | filter:$select.search | orderBy:["$container", "name_label"] track by vm.id')
|
||||
div
|
||||
i.xo-icon-working(ng-if="isVMWorking(vm)", tooltip="{{vm.power_state}} and {{(vm.current_operations | map)[0]}}")
|
||||
i(class="xo-icon-{{vm.power_state | lowercase}}",ng-if="!isVMWorking(vm)", tooltip="{{vm.power_state}}")
|
||||
| {{vm.name_label}}
|
||||
span(ng-if="vm.$container")
|
||||
| ({{(vm.$container | resolve).name_label || ((vm.$container | resolve).master | resolve).name_label}})
|
||||
.col-md-2
|
||||
label(tooltip = 'select/deselect all running VMs', style = 'cursor: pointer')
|
||||
input.hidden(form = 'backupform', type = 'checkbox', ng-model = 'ctrl.formData.allRunning', ng-change = 'ctrl.toggleAllRunning(ctrl.formData.allRunning)')
|
||||
span.fa-stack
|
||||
i.xo-icon-running.fa-stack-1x
|
||||
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allRunning')
|
||||
label(tooltip = 'select/deselect all halted VMs', style = 'cursor: pointer')
|
||||
input.hidden(form = 'backupform', type = 'checkbox', ng-model = 'ctrl.formData.allHalted', ng-change = 'ctrl.toggleAllHalted(ctrl.formData.allHalted)')
|
||||
span.fa-stack
|
||||
i.xo-icon-halted.fa-stack-1x
|
||||
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allHalted')
|
||||
.form-group
|
||||
label.control-label.col-md-2(for = 'depth') Depth
|
||||
.col-md-10
|
||||
input#depth.form-control(form = 'backupform', ng-model = 'ctrl.formData.depth', placeholder = 'How many backups to rollover', type = 'number', min = '1', required)
|
||||
.form-group
|
||||
label.control-label.col-md-2(for = 'remote') Remote
|
||||
.col-md-10
|
||||
select#remote.form-control(form = 'backupform', ng-options = 'remote.name group by remote.type for remote in ctrl.remotes', ng-model = 'ctrl.formData.remote' required)
|
||||
option(value = ''): em -- Choose a file system remote point --
|
||||
.form-group
|
||||
.col-md-10.col-md-offset-2
|
||||
a(ui-sref = 'backup.remote')
|
||||
i.fa.fa-pencil
|
||||
| Manage your remote stores
|
||||
.form-group(ng-if = '!ctrl.formData.scheduleId')
|
||||
label.control-label.col-md-2(for = 'enabled')
|
||||
input#enabled(form = 'backupform', ng-model = 'ctrl.formData.enabled', type = 'checkbox')
|
||||
.help-block.col-md-10 Enable immediately after creation
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-clock-o
|
||||
| Schedule
|
||||
.panel-body.form-horizontal
|
||||
.text-center(ng-if = '!ctrl.formData'): i.xo-icon-loading
|
||||
xo-scheduler(data = 'ctrl.formData', api = 'ctrl.scheduleApi')
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-body
|
||||
fieldset.center(ng-disabled = '!ctrl.ready')
|
||||
button.btn.btn-lg.btn-primary(form = 'backupform', type = 'submit')
|
||||
i.fa.fa-clock-o
|
||||
|
|
||||
i.fa.fa-arrow-right
|
||||
|
|
||||
i.fa.fa-database
|
||||
| Save
|
||||
|
|
||||
button.btn.btn-lg.btn-default(type = 'button', ng-click = 'ctrl.resetData()')
|
||||
| Reset
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-list-ul
|
||||
| Schedules
|
||||
.panel-body
|
||||
.text-center(ng-if = '!ctrl.schedules'): i.xo-icon-loading
|
||||
.text-center(ng-if = 'ctrl.schedules && !ctrl.size(ctrl.schedules)') No scheduled jobs
|
||||
table.table.table-hover(ng-if = 'ctrl.schedules && ctrl.size(ctrl.schedules)')
|
||||
tr
|
||||
th ID
|
||||
th Tag
|
||||
th.hidden-xs.hidden-sm VMs to backup
|
||||
th.hidden-xs Remote
|
||||
th.hidden-xs Depth
|
||||
th.hidden-xs Scheduling
|
||||
th Enabled now
|
||||
th
|
||||
tr(ng-repeat = 'schedule in ctrl.schedules | orderBy:"id":true track by schedule.id')
|
||||
td {{ schedule.id }}
|
||||
td {{ ctrl.jobs[schedule.job].paramsVector.items[0].values[0].tag }}
|
||||
td.hidden-xs.hidden-sm
|
||||
div(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values.length == 1')
|
||||
| {{ (ctrl.jobs[schedule.job].paramsVector.items[0].values[0].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
|
||||
i.fa(ng-class = '{"fa-chevron-down": !unCollapsed, "fa-chevron-up": unCollapsed}')
|
||||
div(collapse = '!unCollapsed')
|
||||
br
|
||||
ul.list-group
|
||||
li.list-group-item(ng-repeat = 'item in ctrl.jobs[schedule.job].paramsVector.items[0].values')
|
||||
span(ng-if = 'item.vm | resolve') {{ (item.vm | resolve).name_label }}
|
||||
span(ng-if = '(item.vm | resolve).$container') ({{ ((item.vm | resolve).$container | resolve).name_label }})
|
||||
td.hidden-xs
|
||||
strong: a(ui-sref = 'scheduler.remote') {{ ctrl.remotes[ctrl.jobs[schedule.job].paramsVector.items[0].values[0].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
|
||||
|
|
||||
button.btn.btn-warning(type = 'button', ng-click = 'ctrl.run(schedule)', ng-disabled = 'ctrl.running[schedule.id]'): i.fa.fa-play
|
||||
|
|
||||
button.btn.btn-danger(type = 'button', ng-click = 'ctrl.delete(schedule)'): i.fa.fa-trash-o
|
||||
@@ -28,6 +28,7 @@ export default angular.module('backup.disasterrecovery', [
|
||||
|
||||
this.ready = false
|
||||
|
||||
this.running = {}
|
||||
this.comesForEditing = $stateParams.id
|
||||
this.scheduleApi = {}
|
||||
this.formData = {}
|
||||
@@ -179,6 +180,16 @@ export default angular.module('backup.disasterrecovery', [
|
||||
})
|
||||
}
|
||||
|
||||
this.run = schedule => {
|
||||
this.running[schedule.id] = true
|
||||
notify.info({
|
||||
title: 'Run Job',
|
||||
message: 'One shot running started. See overview for logs.'
|
||||
})
|
||||
const id = schedule.job
|
||||
return xo.job.runSequence([id]).finally(() => delete this.running[schedule.id])
|
||||
}
|
||||
|
||||
this.inTargetPool = vm => vm.$poolId === (this.formData.selectedPool && this.formData.selectedPool.id)
|
||||
|
||||
this.resetData = () => {
|
||||
|
||||
@@ -75,7 +75,7 @@ form#drform(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.selec
|
||||
.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 immediatly after creation
|
||||
.help-block.col-md-8 Enable immediately after creation
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-clock-o
|
||||
@@ -140,4 +140,6 @@ form#drform(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.selec
|
||||
td.text-right
|
||||
button.btn.btn-primary(type = 'button', ng-click = 'ctrl.edit(schedule)'): i.fa.fa-pencil
|
||||
|
|
||||
button.btn.btn-warning(type = 'button', ng-click = 'ctrl.run(schedule)', ng-disabled = 'ctrl.running[schedule.id]'): i.fa.fa-play
|
||||
|
|
||||
button.btn.btn-danger(type = 'button', ng-click = 'ctrl.delete(schedule)'): i.fa.fa-trash-o
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import angular from 'angular'
|
||||
import assign from 'lodash.assign'
|
||||
import forEach from 'lodash.foreach'
|
||||
import indexOf from 'lodash.indexof'
|
||||
import later from 'later'
|
||||
import moment from 'moment'
|
||||
import prettyCron from 'prettycron'
|
||||
import remove from 'lodash.remove'
|
||||
import scheduler from 'scheduler'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
|
||||
later.date.localTime()
|
||||
|
||||
import backup from './backup'
|
||||
import continuousReplication from './continuous-replication'
|
||||
import deltaBackup from './delta-backup'
|
||||
import disasterRecovery from './disaster-recovery'
|
||||
import management from './management'
|
||||
import mount from './remote'
|
||||
@@ -18,17 +15,19 @@ import restore from './restore'
|
||||
import rollingSnapshot from './rolling-snapshot'
|
||||
|
||||
import view from './view'
|
||||
import scheduler from './scheduler'
|
||||
|
||||
export default angular.module('backup', [
|
||||
uiRouter,
|
||||
|
||||
backup,
|
||||
continuousReplication,
|
||||
deltaBackup,
|
||||
disasterRecovery,
|
||||
management,
|
||||
mount,
|
||||
restore,
|
||||
rollingSnapshot
|
||||
rollingSnapshot,
|
||||
scheduler
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('backup', {
|
||||
@@ -49,302 +48,4 @@ export default angular.module('backup', [
|
||||
})
|
||||
})
|
||||
|
||||
.directive('xoScheduler', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: scheduler,
|
||||
controller: 'XoScheduler as ctrl',
|
||||
bindToController: true,
|
||||
scope: {
|
||||
data: '=',
|
||||
api: '='
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
.controller('XoScheduler', function () {
|
||||
this.init = () => {
|
||||
let i, j
|
||||
|
||||
const minutes = []
|
||||
for (i = 0; i < 6; i++) {
|
||||
minutes[i] = []
|
||||
for (j = 0; j < 10; j++) {
|
||||
minutes[i].push(10 * i + j)
|
||||
}
|
||||
}
|
||||
this.minutes = minutes
|
||||
|
||||
const hours = []
|
||||
for (i = 0; i < 3; i++) {
|
||||
hours[i] = []
|
||||
for (j = 0; j < 8; j++) {
|
||||
hours[i].push(8 * i + j)
|
||||
}
|
||||
}
|
||||
this.hours = hours
|
||||
|
||||
const days = []
|
||||
for (i = 0; i < 4; i++) {
|
||||
days[i] = []
|
||||
for (j = 1; j < 8; j++) {
|
||||
days[i].push(7 * i + j)
|
||||
}
|
||||
}
|
||||
days.push([29, 30, 31])
|
||||
this.days = days
|
||||
|
||||
this.months = [
|
||||
[
|
||||
{v: 1, l: 'Jan'},
|
||||
{v: 2, l: 'Feb'},
|
||||
{v: 3, l: 'Mar'},
|
||||
{v: 4, l: 'Apr'},
|
||||
{v: 5, l: 'May'},
|
||||
{v: 6, l: 'Jun'}
|
||||
],
|
||||
[
|
||||
{v: 7, l: 'Jul'},
|
||||
{v: 8, l: 'Aug'},
|
||||
{v: 9, l: 'Sep'},
|
||||
{v: 10, l: 'Oct'},
|
||||
{v: 11, l: 'Nov'},
|
||||
{v: 12, l: 'Dec'}
|
||||
]
|
||||
]
|
||||
|
||||
this.dayWeeks = [
|
||||
{v: 0, l: 'Sun'},
|
||||
{v: 1, l: 'Mon'},
|
||||
{v: 2, l: 'Tue'},
|
||||
{v: 3, l: 'Wed'},
|
||||
{v: 4, l: 'Thu'},
|
||||
{v: 5, l: 'Fri'},
|
||||
{v: 6, l: 'Sat'}
|
||||
]
|
||||
this.resetData()
|
||||
}
|
||||
|
||||
this.selectMinute = function (minute) {
|
||||
if (this.isSelectedMinute(minute)) {
|
||||
remove(this.data.minSelect, v => String(v) === String(minute))
|
||||
} else {
|
||||
this.data.minSelect.push(minute)
|
||||
}
|
||||
}
|
||||
|
||||
this.isSelectedMinute = function (minute) {
|
||||
return indexOf(this.data.minSelect, minute) > -1 || indexOf(this.data.minSelect, String(minute)) > -1
|
||||
}
|
||||
|
||||
this.selectHour = function (hour) {
|
||||
if (this.isSelectedHour(hour)) {
|
||||
remove(this.data.hourSelect, v => String(v) === String(hour))
|
||||
} else {
|
||||
this.data.hourSelect.push(hour)
|
||||
}
|
||||
}
|
||||
|
||||
this.isSelectedHour = function (hour) {
|
||||
return indexOf(this.data.hourSelect, hour) > -1 || indexOf(this.data.hourSelect, String(hour)) > -1
|
||||
}
|
||||
|
||||
this.selectDay = function (day) {
|
||||
if (this.isSelectedDay(day)) {
|
||||
remove(this.data.daySelect, v => String(v) === String(day))
|
||||
} else {
|
||||
this.data.daySelect.push(day)
|
||||
}
|
||||
}
|
||||
|
||||
this.isSelectedDay = function (day) {
|
||||
return indexOf(this.data.daySelect, day) > -1 || indexOf(this.data.daySelect, String(day)) > -1
|
||||
}
|
||||
|
||||
this.selectMonth = function (month) {
|
||||
if (this.isSelectedMonth(month)) {
|
||||
remove(this.data.monthSelect, v => String(v) === String(month))
|
||||
} else {
|
||||
this.data.monthSelect.push(month)
|
||||
}
|
||||
}
|
||||
|
||||
this.isSelectedMonth = function (month) {
|
||||
return indexOf(this.data.monthSelect, month) > -1 || indexOf(this.data.monthSelect, String(month)) > -1
|
||||
}
|
||||
|
||||
this.selectDayWeek = function (dayWeek) {
|
||||
if (this.isSelectedDayWeek(dayWeek)) {
|
||||
remove(this.data.dayWeekSelect, v => String(v) === String(dayWeek))
|
||||
} else {
|
||||
this.data.dayWeekSelect.push(dayWeek)
|
||||
}
|
||||
}
|
||||
|
||||
this.isSelectedDayWeek = function (dayWeek) {
|
||||
return indexOf(this.data.dayWeekSelect, dayWeek) > -1 || indexOf(this.data.dayWeekSelect, String(dayWeek)) > -1
|
||||
}
|
||||
|
||||
this.noMinutePlan = function (set = false) {
|
||||
if (!set) {
|
||||
// The last part (after &&) of this expression is reliable because we maintain the minSelect array with lodash.remove
|
||||
return this.data.min === 'select' && this.data.minSelect.length === 1 && String(this.data.minSelect[0]) === '0'
|
||||
} else {
|
||||
this.data.minSelect = [0]
|
||||
this.data.min = 'select'
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
this.noHourPlan = function (set = false) {
|
||||
if (!set) {
|
||||
// The last part (after &&) of this expression is reliable because we maintain the hourSelect array with lodash.remove
|
||||
return this.data.hour === 'select' && this.data.hourSelect.length === 1 && String(this.data.hourSelect[0]) === '0'
|
||||
} else {
|
||||
this.data.hourSelect = [0]
|
||||
this.data.hour = 'select'
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
this.resetData = () => {
|
||||
this.data.minRange = 5
|
||||
this.data.hourRange = 2
|
||||
this.data.minSelect = [0]
|
||||
this.data.hourSelect = []
|
||||
this.data.daySelect = []
|
||||
this.data.monthSelect = []
|
||||
this.data.dayWeekSelect = []
|
||||
this.data.min = 'select'
|
||||
this.data.hour = 'all'
|
||||
this.data.day = 'all'
|
||||
this.data.month = 'all'
|
||||
this.data.dayWeek = 'all'
|
||||
this.data.cronPattern = '* * * * *'
|
||||
this.data.summary = []
|
||||
this.data.previewLimit = 0
|
||||
|
||||
this.update()
|
||||
}
|
||||
|
||||
this.update = () => {
|
||||
const d = this.data
|
||||
const i = (d.min === 'all' && '*') ||
|
||||
(d.min === 'range' && ('*/' + d.minRange)) ||
|
||||
(d.min === 'select' && d.minSelect.join(',')) ||
|
||||
'*'
|
||||
const h = (d.hour === 'all' && '*') ||
|
||||
(d.hour === 'range' && ('*/' + d.hourRange)) ||
|
||||
(d.hour === 'select' && d.hourSelect.join(',')) ||
|
||||
'*'
|
||||
const dm = (d.day === 'all' && '*') ||
|
||||
(d.day === 'select' && d.daySelect.join(',')) ||
|
||||
'*'
|
||||
const m = (d.month === 'all' && '*') ||
|
||||
(d.month === 'select' && d.monthSelect.join(',')) ||
|
||||
'*'
|
||||
const dw = (d.dayWeek === 'all' && '*') ||
|
||||
(d.dayWeek === 'select' && d.dayWeekSelect.join(',')) ||
|
||||
'*'
|
||||
this.data.cronPattern = i + ' ' + h + ' ' + dm + ' ' + m + ' ' + dw
|
||||
|
||||
const tabState = {
|
||||
min: {
|
||||
all: d.min === 'all',
|
||||
range: d.min === 'range',
|
||||
select: d.min === 'select'
|
||||
},
|
||||
hour: {
|
||||
all: d.hour === 'all',
|
||||
range: d.hour === 'range',
|
||||
select: d.hour === 'select'
|
||||
},
|
||||
day: {
|
||||
all: d.day === 'all',
|
||||
range: d.day === 'range',
|
||||
select: d.day === 'select'
|
||||
},
|
||||
month: {
|
||||
all: d.month === 'all',
|
||||
select: d.month === 'select'
|
||||
},
|
||||
dayWeek: {
|
||||
all: d.dayWeek === 'all',
|
||||
select: d.dayWeek === 'select'
|
||||
}
|
||||
}
|
||||
this.tabs = tabState
|
||||
this.summarize()
|
||||
}
|
||||
|
||||
this.summarize = () => {
|
||||
const schedule = later.parse.cron(this.data.cronPattern)
|
||||
const occurences = later.schedule(schedule).next(25)
|
||||
this.data.summary = []
|
||||
forEach(occurences, occurence => {
|
||||
this.data.summary.push(moment(occurence).format('LLLL'))
|
||||
})
|
||||
}
|
||||
|
||||
const cronToData = (data, cron) => {
|
||||
const d = Object.create(null)
|
||||
const cronItems = cron.split(' ')
|
||||
|
||||
if (cronItems[0] === '*') {
|
||||
d.min = 'all'
|
||||
} else if (cronItems[0].indexOf('/') !== -1) {
|
||||
d.min = 'range'
|
||||
const [, range] = cronItems[0].split('/')
|
||||
d.minRange = range
|
||||
} else {
|
||||
d.min = 'select'
|
||||
d.minSelect = cronItems[0].split(',')
|
||||
}
|
||||
|
||||
if (cronItems[1] === '*') {
|
||||
d.hour = 'all'
|
||||
} else if (cronItems[1].indexOf('/') !== -1) {
|
||||
d.hour = 'range'
|
||||
const [, range] = cronItems[1].split('/')
|
||||
d.hourRange = range
|
||||
} else {
|
||||
d.hour = 'select'
|
||||
d.hourSelect = cronItems[1].split(',')
|
||||
}
|
||||
|
||||
if (cronItems[2] === '*') {
|
||||
d.day = 'all'
|
||||
} else {
|
||||
d.day = 'select'
|
||||
d.daySelect = cronItems[2].split(',')
|
||||
}
|
||||
|
||||
if (cronItems[3] === '*') {
|
||||
d.month = 'all'
|
||||
} else {
|
||||
d.month = 'select'
|
||||
d.monthSelect = cronItems[3].split(',')
|
||||
}
|
||||
|
||||
if (cronItems[4] === '*') {
|
||||
d.dayWeek = 'all'
|
||||
} else {
|
||||
d.dayWeek = 'select'
|
||||
d.dayWeekSelect = cronItems[4].split(',')
|
||||
}
|
||||
|
||||
assign(data, d)
|
||||
}
|
||||
|
||||
this.prettyCron = prettyCron.toString.bind(prettyCron)
|
||||
|
||||
this.api.setCron = cron => {
|
||||
cronToData(this.data, cron)
|
||||
this.update()
|
||||
}
|
||||
this.api.resetData = this.resetData.bind(this)
|
||||
|
||||
this.init()
|
||||
})
|
||||
|
||||
.name
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import angular from 'angular'
|
||||
import filter from 'lodash.filter'
|
||||
import forEach from 'lodash.foreach'
|
||||
import prettyCron from 'prettycron'
|
||||
import uiBootstrap from 'angular-ui-bootstrap'
|
||||
@@ -19,17 +20,35 @@ export default angular.module('backup.management', [
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('ManagementCtrl', function ($scope, $state, $stateParams, $interval, xo, xoApi, notify, selectHighLevelFilter, filterFilter) {
|
||||
.controller('ManagementCtrl', function (
|
||||
$interval,
|
||||
$scope,
|
||||
$state,
|
||||
$stateParams,
|
||||
filterFilter,
|
||||
modal,
|
||||
notify,
|
||||
selectHighLevelFilter,
|
||||
xo,
|
||||
xoApi
|
||||
) {
|
||||
this.running = {}
|
||||
const mapJobKeyToState = {
|
||||
'rollingSnapshot': 'rollingsnapshot',
|
||||
'rollingBackup': 'backup',
|
||||
'disasterRecovery': 'disasterrecovery'
|
||||
continuousReplication: 'continuousReplication',
|
||||
deltaBackup: 'deltaBackup',
|
||||
disasterRecovery: 'disasterrecovery',
|
||||
rollingBackup: 'backup',
|
||||
rollingSnapshot: 'rollingsnapshot',
|
||||
__none: 'index'
|
||||
}
|
||||
|
||||
const mapJobKeyToJobDisplay = {
|
||||
'rollingSnapshot': 'Rolling Snapshot',
|
||||
'rollingBackup': 'Backup',
|
||||
'disasterRecovery': 'Disaster Recovery'
|
||||
continuousReplication: 'Continuous Replication',
|
||||
deltaBackup: 'Delta Backup',
|
||||
disasterRecovery: 'Disaster Recovery',
|
||||
rollingBackup: 'Backup',
|
||||
rollingSnapshot: 'Rolling Snapshot',
|
||||
__none: '[unknown]'
|
||||
}
|
||||
|
||||
this.currentLogPage = 1
|
||||
@@ -37,7 +56,7 @@ export default angular.module('backup.management', [
|
||||
|
||||
const refreshSchedules = () => {
|
||||
xo.schedule.getAll()
|
||||
.then(schedules => this.schedules = schedules)
|
||||
.then(schedules => this.schedules = filter(schedules, schedule => this.jobs[schedule.job] && this.jobs[schedule.job].key in mapJobKeyToState))
|
||||
xo.scheduler.getScheduleTable()
|
||||
.then(table => this.scheduleTable = table)
|
||||
}
|
||||
@@ -45,12 +64,12 @@ export default angular.module('backup.management', [
|
||||
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') {
|
||||
if (data.event === 'job.start' && data.key in mapJobKeyToState) {
|
||||
logsToClear.push(logKey)
|
||||
viewLogs[logKey] = {
|
||||
logKey,
|
||||
jobId: data.jobId,
|
||||
@@ -62,31 +81,33 @@ export default angular.module('backup.management', [
|
||||
}
|
||||
} else {
|
||||
const runJobId = data.runJobId
|
||||
|
||||
const entry = viewLogs[runJobId]
|
||||
if (!entry) {
|
||||
return
|
||||
}
|
||||
logsToClear.push(logKey)
|
||||
if (data.event === 'job.end') {
|
||||
const entry = viewLogs[runJobId]
|
||||
|
||||
if (data.error) {
|
||||
entry.error = data.error
|
||||
}
|
||||
|
||||
entry.end = time
|
||||
entry.duration = time - entry.start
|
||||
entry.status = 'Terminated'
|
||||
entry.status = 'Finished'
|
||||
} else if (data.event === 'jobCall.start') {
|
||||
viewLogs[runJobId].calls[logKey] = {
|
||||
entry.calls[logKey] = {
|
||||
callKey: logKey,
|
||||
params: resolveParams(data.params),
|
||||
method: data.method,
|
||||
time
|
||||
}
|
||||
} else if (data.event === 'jobCall.end') {
|
||||
const call = viewLogs[runJobId].calls[data.runCallId]
|
||||
const call = entry.calls[data.runCallId]
|
||||
|
||||
if (data.error) {
|
||||
call.error = data.error
|
||||
viewLogs[runJobId].hasErrors = true
|
||||
entry.hasErrors = true
|
||||
} else {
|
||||
call.returnedValue = data.returnedValue
|
||||
call.returnedValue = resolveReturn(data.returnedValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,22 +120,30 @@ export default angular.module('backup.management', [
|
||||
})
|
||||
|
||||
this.logs = viewLogs
|
||||
this.logsToClear = logsToClear
|
||||
})
|
||||
}
|
||||
|
||||
const resolveParams = params => {
|
||||
for (let key in params) {
|
||||
if (key === 'id') {
|
||||
const xoObject = xoApi.get(params[key])
|
||||
if (xoObject) {
|
||||
params[xoObject.type] = xoObject.name_label
|
||||
delete params[key]
|
||||
}
|
||||
const xoObject = xoApi.get(params[key])
|
||||
if (xoObject) {
|
||||
const newKey = xoObject.type || key
|
||||
params[newKey] = xoObject.name_label || xoObject.name || params[key]
|
||||
newKey !== key && delete params[key]
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
const resolveReturn = returnValue => {
|
||||
const xoObject = xoApi.get(returnValue)
|
||||
let xoName = xoObject && (xoObject.name_label || xoObject.name)
|
||||
xoName && (xoName += xoObject.type && ` (${xoObject.type})` || '')
|
||||
returnValue = xoName || returnValue
|
||||
return returnValue
|
||||
}
|
||||
|
||||
this.prettyCron = prettyCron.toString.bind(prettyCron)
|
||||
|
||||
const refreshJobs = () => {
|
||||
@@ -139,6 +168,14 @@ export default angular.module('backup.management', [
|
||||
$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)
|
||||
@@ -151,9 +188,19 @@ export default angular.module('backup.management', [
|
||||
.finally(() => { this.working[id] = false })
|
||||
.then(refreshSchedules)
|
||||
}
|
||||
this.resolveJobKey = schedule => mapJobKeyToState[this.jobs[schedule.job].key]
|
||||
this.displayJobKey = schedule => mapJobKeyToJobDisplay[this.jobs[schedule.job].key]
|
||||
this.run = schedule => {
|
||||
this.running[schedule.id] = true
|
||||
notify.info({
|
||||
title: 'Run Job',
|
||||
message: 'One shot running started. See overview for logs.'
|
||||
})
|
||||
const id = schedule.job
|
||||
return xo.job.runSequence([id]).finally(() => delete this.running[schedule.id])
|
||||
}
|
||||
this.resolveJobKey = schedule => mapJobKeyToState[this.jobs[schedule.job] && this.jobs[schedule.job].key || '__none']
|
||||
this.displayJobKey = schedule => mapJobKeyToJobDisplay[this.jobs[schedule.job] && this.jobs[schedule.job].key || '__none']
|
||||
this.displayLogKey = log => mapJobKeyToJobDisplay[log.key]
|
||||
this.resolveScheduleJobTag = schedule => this.jobs[schedule.job] && this.jobs[schedule.job].paramsVector && this.jobs[schedule.job].paramsVector.items[0].values[0].tag || schedule.id
|
||||
|
||||
this.collectionLength = col => Object.keys(col).length
|
||||
this.working = {}
|
||||
|
||||
@@ -16,30 +16,30 @@
|
||||
td.text-center No scheduled jobs
|
||||
table.table.table-hover(ng-if = 'ctrl.schedules && ctrl.collectionLength(ctrl.schedules)')
|
||||
tr
|
||||
th ID
|
||||
th Job
|
||||
th Scheduling
|
||||
th Tag
|
||||
th.hidden-xs Scheduling
|
||||
th State
|
||||
th
|
||||
tr(ng-repeat = 'schedule in ctrl.schedules | orderBy:"id":true track by schedule.id')
|
||||
td: a(ui-sref = 'backup.{{ctrl.resolveJobKey(schedule)}}({id: schedule.id})') {{ schedule.id }}
|
||||
td {{ ctrl.displayJobKey(schedule) }}
|
||||
td {{ ctrl.prettyCron(schedule.cron) }}
|
||||
td: a(ui-sref = 'backup.{{ctrl.resolveJobKey(schedule)}}({id: schedule.id})') {{ ctrl.resolveScheduleJobTag(schedule) }}
|
||||
td.hidden-xs {{ ctrl.prettyCron(schedule.cron) }}
|
||||
td
|
||||
span.text-success(ng-if = 'ctrl.scheduleTable[schedule.id] === true')
|
||||
| enabled
|
||||
i.fa.fa-cogs
|
||||
span.text-muted(ng-if = 'ctrl.scheduleTable[schedule.id] === false') disabled
|
||||
span.text-warning(ng-if = 'ctrl.scheduleTable[schedule.id] === undefined') ?
|
||||
td.text-right
|
||||
fieldset(ng-disabled = 'ctrl.working[schedule.id]')
|
||||
button.btn.btn-success(ng-if = 'ctrl.scheduleTable[schedule.id] === false', type = 'button', ng-click = 'ctrl.enable(schedule.id)') Enable
|
||||
button.btn.btn-warning(ng-if = 'ctrl.scheduleTable[schedule.id] === true', type = 'button', ng-click = 'ctrl.disable(schedule.id)') Disable
|
||||
span.label.label-success.hidden-xs(ng-if = 'ctrl.scheduleTable[schedule.id] === true') enabled
|
||||
span.label.label-default.hidden-xs(ng-if = 'ctrl.scheduleTable[schedule.id] === false') disabled
|
||||
span.label.label-warning.hidden-xs(ng-if = 'ctrl.scheduleTable[schedule.id] === undefined') unknown
|
||||
fieldset.pull-right(ng-disabled = 'ctrl.working[schedule.id]')
|
||||
button.btn(ng-if = 'ctrl.scheduleTable[schedule.id] === false', type = 'button', ng-click = 'ctrl.enable(schedule.id)'): i.fa.fa-toggle-off
|
||||
button.btn.btn-success(ng-if = 'ctrl.scheduleTable[schedule.id] === true', type = 'button', ng-click = 'ctrl.disable(schedule.id)'): i.fa.fa-toggle-on
|
||||
|
|
||||
button.btn.btn-warning(type = 'button', ng-click = 'ctrl.run(schedule)', ng-disabled = 'ctrl.running[schedule.id]'): i.fa.fa-play
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
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
|
||||
@@ -53,21 +53,22 @@
|
||||
tbody(ng-repeat = 'log in ctrl.logs | map | filter:ctrl.logSearch | orderBy:"-time" | slice:(ctrl.logPageSize * (ctrl.currentLogPage - 1)):(ctrl.logPageSize * ctrl.currentLogPage) track by log.logKey')
|
||||
tr
|
||||
td
|
||||
| {{ log.jobId }}
|
||||
button.btn.btn-sm(type = 'button', tooltip = 'See calls', ng-click = 'seeCalls = !seeCalls', ng-class = '{"btn-default": !log.hasErrors, "btn-danger": log.hasErrors}'): i.fa(ng-class = '{"fa-caret-down": !seeCalls, "fa-caret-up": seeCalls}')
|
||||
| {{ log.jobId }}
|
||||
td {{ ctrl.displayLogKey(log) }}
|
||||
td {{ log.start | date:'medium' }}
|
||||
td {{ log.end | date:'medium' }}
|
||||
td {{ log.duration | duration}}
|
||||
td
|
||||
span(ng-if = 'log.status === "Terminated"')
|
||||
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 !== "Terminated"', ng-class = '{"label-warning": log.status === "In progress", "label-default": !log.status}') {{ log.status || "unknown" }}
|
||||
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') {{ 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 }}: 
|
||||
span(ng-repeat = '(key, param) in call.params')
|
||||
strong {{ key }}:
|
||||
| {{ param }}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import angular from 'angular'
|
||||
import filter from 'lodash.filter'
|
||||
import map from 'lodash.map'
|
||||
import trim from 'lodash.trim'
|
||||
import size from 'lodash.size'
|
||||
import uiBootstrap from 'angular-ui-bootstrap'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
import size from 'lodash.size'
|
||||
import {format, parse} from 'xo-remote-parser'
|
||||
|
||||
import view from './view'
|
||||
|
||||
@@ -26,7 +25,7 @@ export default angular.module('backup.remote', [
|
||||
|
||||
const refresh = () => {
|
||||
return xo.remote.getAll()
|
||||
.then(remotes => this.backUpRemotes = remotes)
|
||||
.then(remotes => this.backUpRemotes = map(remotes, parse))
|
||||
}
|
||||
|
||||
this.getReady = () => {
|
||||
@@ -40,15 +39,7 @@ export default angular.module('backup.remote', [
|
||||
$interval.cancel(interval)
|
||||
})
|
||||
|
||||
this.sanitizePath = (...paths) => filter(map(paths, s => s && filter(map(s.split('/'), trim)).join('/'))).join('/')
|
||||
this.prepareUrl = (type, host, path) => {
|
||||
let url = type + ':/'
|
||||
if (type === 'nfs') {
|
||||
url += '/' + host + ':'
|
||||
}
|
||||
url += '/' + this.sanitizePath(path)
|
||||
return url
|
||||
}
|
||||
this.prepareUrl = (type, host, path, username, password, domain) => format({type, host, path, username, password, domain})
|
||||
|
||||
const reset = () => {
|
||||
this.path = this.host = this.name = undefined
|
||||
@@ -56,7 +47,7 @@ export default angular.module('backup.remote', [
|
||||
}
|
||||
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 => { console.log('GO !!!'); xo.remote.set(id, undefined, undefined, true).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
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
th.text-info Local
|
||||
th Name
|
||||
th Path
|
||||
th
|
||||
th State
|
||||
th Error
|
||||
th
|
||||
@@ -21,6 +22,7 @@
|
||||
td
|
||||
td {{ remote.name }}
|
||||
td {{ remote.path }}
|
||||
td
|
||||
td
|
||||
span(ng-if = 'remote.enabled')
|
||||
span.text-success
|
||||
@@ -37,6 +39,7 @@
|
||||
th.text-info NFS
|
||||
th Name
|
||||
th Device
|
||||
th
|
||||
th State
|
||||
th Error
|
||||
th
|
||||
@@ -44,6 +47,7 @@
|
||||
td
|
||||
td {{ remote.name }}
|
||||
td {{ remote.host }}:{{ remote.share }}
|
||||
td
|
||||
td
|
||||
span(ng-if = 'remote.enabled')
|
||||
span.text-success
|
||||
@@ -55,7 +59,36 @@
|
||||
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))')
|
||||
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 \\
|
||||
| {{ remote.host }}
|
||||
strong.text-info \
|
||||
| {{ remote.path }}
|
||||
td {{ remote.username }}@{{remote.domain}}
|
||||
td
|
||||
span(ng-if = 'remote.enabled')
|
||||
span.text-success
|
||||
| Accessible
|
||||
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
|
||||
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
|
||||
@@ -64,21 +97,47 @@
|
||||
select.form-control(ng-model = 'ctrl.remoteType')
|
||||
option(value = 'file') Local
|
||||
option(value = 'nfs') NFS
|
||||
option(value = 'smb') SMB
|
||||
|
|
||||
.form-group
|
||||
label.sr-only Name
|
||||
input.form-control(type = 'text', ng-model = 'ctrl.name', placeholder = 'Name', required)
|
||||
|
|
||||
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 :
|
||||
.input-group
|
||||
.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', required)
|
||||
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 \\
|
||||
label.sr-only Share
|
||||
input.form-control(type = 'text', ng-model = 'ctrl.host', placeholder = 'share', required)
|
||||
.input-group
|
||||
span.input-group-addon \
|
||||
label.sr-only Path
|
||||
input.form-control(type = 'text', ng-model = 'ctrl.path', placeholder != 'path\to\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)
|
||||
|
|
||||
.form-group
|
||||
button.btn.btn-primary.pull-right(type = 'submit', ng-disabled = '!ctrl.ready')
|
||||
| Save
|
||||
i.fa.fa-floppy-o
|
||||
label.sr-only Password
|
||||
input.form-control(type = 'password', ng-model = 'ctrl.password', placeholder = 'password', required)
|
||||
|
|
||||
.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
|
||||
i.fa.fa-floppy-o
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import angular from 'angular'
|
||||
import filter from 'lodash.filter'
|
||||
import find from 'lodash.find'
|
||||
import forEach from 'lodash.foreach'
|
||||
import size from 'lodash.size'
|
||||
@@ -20,19 +21,26 @@ export default angular.module('backup.restore', [
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('RestoreCtrl', function ($scope, $interval, xo, xoApi, notify, $upload) {
|
||||
.controller('RestoreCtrl', function ($scope, $interval, xo, xoApi, notify, $upload, bytesToSizeFilter) {
|
||||
this.loaded = {}
|
||||
this.hosts = xoApi.getView('hosts')
|
||||
|
||||
const srs = xoApi.getView('SRs').all
|
||||
|
||||
this.bytesToSize = bytesToSizeFilter
|
||||
this.isEmpty = backups => backups && !(Object.keys(backups.delta) || backups.other.length)
|
||||
this.size = size
|
||||
|
||||
const refresh = () => {
|
||||
return xo.remote.getAll()
|
||||
.then(remotes => {
|
||||
forEach(this.backUpRemotes, remote => {
|
||||
if (remote.files) {
|
||||
if (remote.backups) {
|
||||
const freshRemote = find(remotes, {id: remote.id})
|
||||
freshRemote && (freshRemote.files = remote.files)
|
||||
freshRemote && (freshRemote.backups = remote.backups)
|
||||
}
|
||||
})
|
||||
this.backUpRemotes = remotes
|
||||
this.writable_SRs = filter(srs, (sr) => sr.content_type !== 'iso')
|
||||
})
|
||||
}
|
||||
|
||||
@@ -43,24 +51,67 @@ export default angular.module('backup.restore', [
|
||||
$interval.cancel(interval)
|
||||
})
|
||||
|
||||
const deltaBuilder = (backups, uuid, name, tag, value) => {
|
||||
let deltaBackup = backups[uuid]
|
||||
? backups[uuid]
|
||||
: backups[uuid] = {}
|
||||
|
||||
deltaBackup = deltaBackup[name]
|
||||
? deltaBackup[name]
|
||||
: deltaBackup[name] = {}
|
||||
|
||||
deltaBackup = deltaBackup[tag]
|
||||
? deltaBackup[tag]
|
||||
: deltaBackup[tag] = []
|
||||
|
||||
deltaBackup.push(value)
|
||||
}
|
||||
|
||||
this.list = id => {
|
||||
return xo.remote.list(id)
|
||||
.then(files => {
|
||||
const remote = find(this.backUpRemotes, {id})
|
||||
remote && (remote.files = files)
|
||||
|
||||
if (remote) {
|
||||
const backups = remote.backups = {
|
||||
delta: {},
|
||||
other: []
|
||||
}
|
||||
|
||||
forEach(files, file => {
|
||||
const arr = /^vm_delta_(.*)_([^\/]+)\/([^_]+)_(.*)$/.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
|
||||
})
|
||||
}
|
||||
|
||||
this.importVm = (id, file, host) => {
|
||||
notify.info({
|
||||
title: 'VM import started',
|
||||
message: 'Starting the VM import'
|
||||
})
|
||||
return xo.remote.importVm(id, file, host)
|
||||
const notification = {
|
||||
title: 'VM import started',
|
||||
message: 'Starting the VM import'
|
||||
}
|
||||
|
||||
this.size = size
|
||||
this.importBackup = (id, path, sr) => {
|
||||
notify.info(notification)
|
||||
return xo.vm.importBackup(id, path, sr)
|
||||
}
|
||||
|
||||
this.importDeltaBackup = (id, path, sr) => {
|
||||
notify.info(notification)
|
||||
return xo.vm.importDeltaBackup(id, path, sr)
|
||||
}
|
||||
})
|
||||
|
||||
// A module exports its name.
|
||||
|
||||
@@ -14,24 +14,55 @@
|
||||
span(ng-if = '!remote.error') (disabled)
|
||||
.panel-body(ng-if = 'remote.enabled')
|
||||
.row
|
||||
.col-sm-3
|
||||
.col-sm-2
|
||||
p
|
||||
| {{ remote.name }}
|
||||
button.btn.btn-default.pull-right(type = 'button', ng-click = 'ctrl.list(remote.id)'): i.fa(ng-class = '{"fa-eye": !ctrl.loaded[remote.id], "fa-refresh": ctrl.loaded[remote.id]}')
|
||||
br
|
||||
br
|
||||
.col-sm-9
|
||||
div(ng-if = 'ctrl.loaded[remote.id] && !ctrl.size(remote.files)') No backups available
|
||||
div(ng-if = 'ctrl.size(remote.files)')
|
||||
div(ng-repeat = 'file in remote.files')
|
||||
| {{ file }}
|
||||
.col-sm-10
|
||||
div(ng-if = 'ctrl.loaded[remote.id] && ctrl.isEmpty(remote.backups)') No backups available
|
||||
div(ng-if = 'ctrl.size(remote.backups.delta)')
|
||||
div(ng-repeat = '(uuid, backups) in remote.backups.delta')
|
||||
.row
|
||||
.col-sm-2
|
||||
| {{ uuid }}
|
||||
.col-sm-10
|
||||
div(ng-repeat = '(name, backups) in backups')
|
||||
.row
|
||||
.col-sm-2
|
||||
| {{ name }}
|
||||
.col-sm-10
|
||||
div(ng-repeat = '(tag, backups) in backups')
|
||||
.row
|
||||
.col-sm-2
|
||||
| {{ tag }}
|
||||
.col-sm-10
|
||||
div(ng-repeat = 'backup in backups')
|
||||
| {{ backup.date | date:'medium' }}
|
||||
span.pull-right.dropdown(dropdown)
|
||||
button.btn.btn-default(type = 'button', dropdown-toggle)
|
||||
| Import
|
||||
span.caret
|
||||
ul.dropdown-menu(role="menu")
|
||||
li(ng-repeat = 'sr in ctrl.writable_SRs | orderBy:natural("name_label") track by sr.id')
|
||||
a(xo-click = "ctrl.importDeltaBackup(remote.id, backup.path, sr.id)")
|
||||
i.xo-icon-host.fa-fw
|
||||
| To {{sr.name_label}} ({{sr.size - sr.physical_usage | bytesToSize }})
|
||||
span {{ (sr.$container | resolve).name_label }}
|
||||
hr
|
||||
hr
|
||||
div(ng-if = 'ctrl.size(remote.backups.other)')
|
||||
div(ng-repeat = 'backup in remote.backups.other')
|
||||
| {{ backup }}
|
||||
span.pull-right.dropdown(dropdown)
|
||||
button.btn.btn-default(type = 'button', dropdown-toggle)
|
||||
| Import
|
||||
span.caret
|
||||
ul.dropdown-menu(role="menu")
|
||||
li(ng-repeat = 'h in ctrl.hosts.all | orderBy:natural("name_label") track by h.id')
|
||||
a(xo-click = "ctrl.importVm(remote.id, file, h.id)")
|
||||
li(ng-repeat = 'sr in ctrl.writable_SRs | orderBy:natural("name_label") track by sr.id')
|
||||
a(xo-click = "ctrl.importBackup(remote.id, backup, sr.id)")
|
||||
i.xo-icon-host.fa-fw
|
||||
| To {{h.name_label}}
|
||||
| To {{sr.name_label}} ({{sr.size - sr.physical_usage | bytesToSize }})
|
||||
span {{ (sr.$container | resolve).name_label }}
|
||||
hr
|
||||
|
||||
@@ -28,6 +28,7 @@ export default angular.module('backup.rollingSnapshot', [
|
||||
|
||||
this.ready = false
|
||||
|
||||
this.running = {}
|
||||
this.comesForEditing = $stateParams.id
|
||||
this.scheduleApi = {}
|
||||
this.formData = {}
|
||||
@@ -200,6 +201,16 @@ export default angular.module('backup.rollingSnapshot', [
|
||||
})
|
||||
}
|
||||
|
||||
this.run = schedule => {
|
||||
this.running[schedule.id] = true
|
||||
notify.info({
|
||||
title: 'Run Job',
|
||||
message: 'One shot running started. See overview for logs.'
|
||||
})
|
||||
const id = schedule.job
|
||||
return xo.job.runSequence([id]).finally(() => delete this.running[schedule.id])
|
||||
}
|
||||
|
||||
this.resetData = () => {
|
||||
this.formData.allRunning = false
|
||||
this.formData.allHalted = false
|
||||
|
||||
@@ -52,7 +52,7 @@ form#snapform(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.sel
|
||||
.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 immediatly after creation
|
||||
.help-block.col-md-8 Enable immediately after creation
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-clock-o
|
||||
@@ -114,4 +114,6 @@ form#snapform(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.sel
|
||||
td.text-right
|
||||
button.btn.btn-primary(type = 'button', ng-click = 'ctrl.edit(schedule)'): i.fa.fa-pencil
|
||||
|
|
||||
button.btn.btn-warning(type = 'button', ng-click = 'ctrl.run(schedule)', ng-disabled = 'ctrl.running[schedule.id]'): i.fa.fa-play
|
||||
|
|
||||
button.btn.btn-danger(type = 'button', ng-click = 'ctrl.delete(schedule)'): i.fa.fa-trash-o
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
a(ui-sref = '.backup')
|
||||
i.fa.fa-fw.fa-download.fa-menu
|
||||
span.menu-entry Backup
|
||||
li
|
||||
a(ui-sref = '.deltaBackup')
|
||||
i.fa.fa-fw.fa-code-fork.fa-menu
|
||||
span.menu-entry Delta Backup
|
||||
li
|
||||
a(ui-sref = '.restore')
|
||||
i.fa.fa-fw.fa-upload.fa-menu
|
||||
@@ -25,5 +29,9 @@
|
||||
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 = '')
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
angular = require 'angular'
|
||||
forEach = require('lodash.foreach')
|
||||
includes = require('lodash.includes')
|
||||
Clipboard = require('clipboard')
|
||||
|
||||
isoDevice = require('iso-device')
|
||||
|
||||
@@ -89,5 +90,30 @@ module.exports = angular.module 'xoWebApp.console', [
|
||||
$scope.insert = (disc_id) ->
|
||||
xo.vm.insertCd id, disc_id, true
|
||||
|
||||
$scope.vmClipboard = ''
|
||||
$scope.setClipboard = (text) ->
|
||||
$scope.vmClipboard = text
|
||||
$scope.$applyAsync()
|
||||
|
||||
$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
|
||||
|
||||
@@ -7,24 +7,37 @@
|
||||
i.xo-icon-console.fa-stack-1x(class = 'xo-color-{{VM.power_state | lowercase}}')
|
||||
|
|
||||
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-6: iso-device(ng-if = 'VM && SRs', vm = 'VM', srs = 'SRs')
|
||||
.col-sm-3: button.btn.btn-default(
|
||||
.col-sm-4: iso-device(ng-if = 'VM && SRs', vm = 'VM', srs = 'SRs')
|
||||
.col-sm-2: button.btn.btn-default(
|
||||
ng-click = 'vncRemote.sendCtrlAltDel()'
|
||||
)
|
||||
i.fa.fa-keyboard-o
|
||||
|
|
||||
| Ctrl+Alt+Del
|
||||
.col-sm-4
|
||||
.input-group
|
||||
input#vm-clipboard.form-control(ng-model='vmClipboard' ng-change='vncRemote.pasteToClipboard(vmClipboard)')
|
||||
span.input-group-btn
|
||||
button.btn.btn-default.copy(data-clipboard-target='#vm-clipboard' tooltip="Copy text into local clipboard")
|
||||
i.fa.fa-clipboard
|
||||
| Copy
|
||||
//- Action panel
|
||||
.col-sm-3
|
||||
.btn-group
|
||||
.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"
|
||||
@@ -46,9 +59,32 @@
|
||||
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'
|
||||
remote-control = 'vncRemote',
|
||||
on-clipboard-change = 'setClipboard(clipboardContent)'
|
||||
)
|
||||
|
||||
@@ -115,9 +115,7 @@ export default angular.module('dashboard.dataviz', [
|
||||
|
||||
const debouncedPopulate = debounce(populateChartsData, 300, {leading: true, trailing: true})
|
||||
debouncedPopulate()
|
||||
xoApi.onUpdate(function () {
|
||||
debouncedPopulate()
|
||||
})
|
||||
$scope.$on('$destroy', xoApi.onUpdate(debouncedPopulate))
|
||||
})
|
||||
.controller('DatavizStorageHierarchical', function DatavizStorageHierarchical (xoApi, $scope, $timeout, $interval, $state, bytesToSizeFilter) {
|
||||
$scope.charts = {
|
||||
@@ -255,9 +253,7 @@ export default angular.module('dashboard.dataviz', [
|
||||
const debouncedPopulate = debounce(populateChartsData, 300, {leading: true, trailing: true})
|
||||
|
||||
debouncedPopulate()
|
||||
xoApi.onUpdate(function () {
|
||||
debouncedPopulate()
|
||||
})
|
||||
$scope.$on('$destroy', xoApi.onUpdate(debouncedPopulate))
|
||||
})
|
||||
.controller('DatavizRamHierarchical', function DatavizRamHierarchical (xoApi, $scope, $timeout, $state, bytesToSizeFilter) {
|
||||
$scope.charts = {
|
||||
@@ -361,9 +357,7 @@ export default angular.module('dashboard.dataviz', [
|
||||
const debouncedPopulate = debounce(populateChartsData, 300, {leading: true, trailing: true})
|
||||
|
||||
debouncedPopulate()
|
||||
xoApi.onUpdate(function () {
|
||||
debouncedPopulate()
|
||||
})
|
||||
$scope.$on('$destroy', xoApi.onUpdate(debouncedPopulate))
|
||||
})
|
||||
// A module exports its name.
|
||||
.name
|
||||
|
||||
@@ -1,29 +1,19 @@
|
||||
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.health', [
|
||||
uiRouter,
|
||||
xoApi,
|
||||
xoHorizon,
|
||||
xoServices,
|
||||
xoWeekHeatmap
|
||||
xoApi
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('dashboard.health', {
|
||||
controller: 'Health as bigController',
|
||||
controller: 'Health as ctrl',
|
||||
data: {
|
||||
requireAdmin: true
|
||||
},
|
||||
@@ -32,332 +22,68 @@ export default angular.module('dashboard.health', [
|
||||
})
|
||||
})
|
||||
|
||||
.filter('type', () => {
|
||||
return function (objects, type) {
|
||||
if (!type) {
|
||||
return objects
|
||||
}
|
||||
return filter(objects, object => object.type === type)
|
||||
}
|
||||
})
|
||||
.controller('Health', function () {})
|
||||
.controller('HealthHeatmap', function (xoApi, xo, xoAggregate, notify, bytesToSizeFilter) {
|
||||
this.charts = {
|
||||
heatmap: null
|
||||
}
|
||||
this.objects = xoApi.all
|
||||
.controller('Health', function (xo, xoApi, $scope, modal) {
|
||||
this.currentVdiPage = 1
|
||||
this.currentVmPage = 1
|
||||
|
||||
this.prepareTypeFilter = function (selection) {
|
||||
const object = selection[0]
|
||||
this.typeFilter = object && object.type || undefined
|
||||
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.selectAll = function (type) {
|
||||
this.selected = filter(this.objects, object =>
|
||||
(object.type === type && object.power_state === 'Running'))
|
||||
this.typeFilter = type
|
||||
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.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('HealthHorizons', 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.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.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]
|
||||
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
|
||||
|
||||
@@ -4,152 +4,85 @@
|
||||
i.fa.fa-heartbeat
|
||||
| Health
|
||||
.grid-sm
|
||||
.grid-cell
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-fire
|
||||
| Weekly Heatmap
|
||||
.panel-body(ng-controller='HealthHeatmap 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 | 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
|
||||
| Select
|
||||
.grid-cell.grid--gutters
|
||||
.container-fluid
|
||||
span(ng-if = 'heatmap.loadingMetrics')
|
||||
| Loading metrics ...
|
||||
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 | 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 }}')
|
||||
|
|
||||
span(ng-if = '!object._ignored') {{ object.name_label }}
|
||||
del(ng-if = 'object._ignored') {{ object.name_label }}
|
||||
|  
|
||||
weekheatmap(ng-if = 'heatmap.selectedMetric', chart-data='heatmap.selectedMetric')
|
||||
.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
|
||||
.grid-cell
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-fire
|
||||
| Weekly Charts
|
||||
.panel-body(ng-controller="HealthHorizons 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 | 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
|
||||
| Select
|
||||
.grid-cell.grid--gutters
|
||||
.container-fluid
|
||||
span(ng-if = 'horizons.loadingMetrics')
|
||||
| Loading metrics ...
|
||||
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 | 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 }}')
|
||||
|
|
||||
span(ng-if = '!object._ignored') {{ object.name_label }}
|
||||
del(ng-if = 'object._ignored') {{ object.name_label }}
|
||||
|  
|
||||
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}}'
|
||||
)
|
||||
.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}}")
|
||||
| {{ 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.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 }}
|
||||
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 }}
|
||||
|
||||
@@ -4,6 +4,7 @@ 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'
|
||||
@@ -12,6 +13,7 @@ export default angular.module('dashboard', [
|
||||
uiRouter,
|
||||
dataviz,
|
||||
health,
|
||||
stats,
|
||||
overview
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
|
||||
@@ -28,7 +28,7 @@ export default angular.module('dashboard.overview', [
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('Overview', function ($scope, $window, xoApi, xo, $timeout, bytesToSizeFilter) {
|
||||
.controller('Overview', function ($scope, $window, xoApi, xo, $timeout, bytesToSizeFilter, modal) {
|
||||
$window.bytesToSize = bytesToSizeFilter // FIXME dirty workaround to custom a Chart.js tooltip template
|
||||
angular.extend($scope, {
|
||||
pools: {
|
||||
@@ -47,6 +47,33 @@ export default angular.module('dashboard.overview', [
|
||||
cpu: [0, 0],
|
||||
srs: []
|
||||
})
|
||||
|
||||
$scope.installAllPatches = function () {
|
||||
modal.confirm({
|
||||
title: 'Install all the missing patches',
|
||||
message: 'Are you sure you want to install all the missing patches? This could take a while...'
|
||||
}).then(() =>
|
||||
foreach($scope.pools.all, function (pool, pool_id) {
|
||||
let pool_hosts = $scope.hostsByPool[pool_id]
|
||||
foreach(pool_hosts, function (host, host_id) {
|
||||
console.log('Installing all missing patches on host ', host_id)
|
||||
xo.host.installAllPatches(host_id)
|
||||
})
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
$scope.installHostPatches = function (hostId) {
|
||||
modal.confirm({
|
||||
title: 'Update host (' + $scope.nbUpdates[hostId] + ' patch(es))',
|
||||
message: 'Are you sure you want to install all the missing patches on this host? This could take a while...'
|
||||
}).then(() => {
|
||||
console.log('Installing all missing patches on host ', hostId)
|
||||
xo.host.installAllPatches(hostId)
|
||||
})
|
||||
}
|
||||
|
||||
const nbUpdates = $scope.nbUpdates = {}
|
||||
function populateChartsData () {
|
||||
let pools,
|
||||
vmsByContainer,
|
||||
@@ -74,11 +101,25 @@ export default angular.module('dashboard.overview', [
|
||||
srs = []
|
||||
|
||||
// update vdi, set them to the right host
|
||||
pools = xoApi.getView('pools')
|
||||
$scope.pools = pools = xoApi.getView('pools')
|
||||
|
||||
srsByContainer = xoApi.getIndex('srsByContainer')
|
||||
vmsByContainer = xoApi.getIndex('vmsByContainer')
|
||||
hostsByPool = xoApi.getIndex('hostsByPool')
|
||||
$scope.hostsByPool = hostsByPool = xoApi.getIndex('hostsByPool')
|
||||
foreach(pools.all, function (pool, pool_id) {
|
||||
let pool_hosts = hostsByPool[pool_id]
|
||||
foreach(pool_hosts, function (host, host_id) {
|
||||
if (host_id in nbUpdates) {
|
||||
return
|
||||
}
|
||||
|
||||
xo.host.listMissingPatches(host_id)
|
||||
.then(result => {
|
||||
nbUpdates[host_id] = result.length
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
foreach(pools.all, function (pool, pool_id) {
|
||||
nb_pools++
|
||||
@@ -129,9 +170,8 @@ export default angular.module('dashboard.overview', [
|
||||
const debouncedPopulate = debounce(populateChartsData, 300, {leading: true, trailing: true})
|
||||
|
||||
debouncedPopulate()
|
||||
xoApi.onUpdate(function () {
|
||||
debouncedPopulate()
|
||||
})
|
||||
|
||||
$scope.$on('$destroy', xoApi.onUpdate(debouncedPopulate))
|
||||
})
|
||||
|
||||
.name
|
||||
|
||||
@@ -98,6 +98,42 @@
|
||||
td {{SR.size | bytesToSize}}
|
||||
td
|
||||
.progress-condensed
|
||||
.progress-bar(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.usage}}", aria-valuemax="{{SR.size}}", style="width: {{[SR.physical_usage, SR.size] | percentage}}", tooltip="Physical usage: {{[SR.physical_usage, SR.size] | percentage}}")
|
||||
.progress-bar.progress-bar-info(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.physical_usage}}", aria-valuemax="{{SR.size}}", style="width: {{[(SR.usage-SR.physical_usage), SR.size] | percentage}}", tooltip="Allocated: {{[(SR.usage), SR.size] | percentage}}")
|
||||
|
||||
.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 | 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
|
||||
|
||||
363
app/modules/dashboard/stats/index.js
Normal file
363
app/modules/dashboard/stats/index.js
Normal file
@@ -0,0 +1,363 @@
|
||||
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
|
||||
155
app/modules/dashboard/stats/view.jade
Normal file
155
app/modules/dashboard/stats/view.jade
Normal file
@@ -0,0 +1,155 @@
|
||||
.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 | 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
|
||||
| Select
|
||||
.grid-cell.grid--gutters
|
||||
.container-fluid
|
||||
span(ng-if = 'heatmap.loadingMetrics')
|
||||
| Loading metrics ...
|
||||
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 | 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 }}')
|
||||
|
|
||||
span(ng-if = '!object._ignored') {{ object.name_label }}
|
||||
del(ng-if = 'object._ignored') {{ object.name_label }}
|
||||
|  
|
||||
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 | 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
|
||||
| Select
|
||||
.grid-cell.grid--gutters
|
||||
.container-fluid
|
||||
span(ng-if = 'horizons.loadingMetrics')
|
||||
| Loading metrics ...
|
||||
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 | 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 }}')
|
||||
|
|
||||
span(ng-if = '!object._ignored') {{ object.name_label }}
|
||||
del(ng-if = 'object._ignored') {{ object.name_label }}
|
||||
|  
|
||||
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}}'
|
||||
)
|
||||
@@ -9,6 +9,10 @@
|
||||
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
|
||||
|
||||
@@ -6,6 +6,8 @@ omit = require 'lodash.omit'
|
||||
sum = require 'lodash.sum'
|
||||
throttle = require 'lodash.throttle'
|
||||
find = require 'lodash.find'
|
||||
filter = require 'lodash.filter'
|
||||
pluck = require 'lodash.pluck'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
@@ -213,6 +215,14 @@ module.exports = angular.module 'xoWebApp.host', [
|
||||
}).then ->
|
||||
xo.host.stop id
|
||||
|
||||
|
||||
$scope.emergencyShutdownHost = (hostId) ->
|
||||
modal.confirm({
|
||||
title: 'Shutdown host'
|
||||
message: 'Are you sure you want to suspend all the VMs on this host and shut the host down?'
|
||||
}).then ->
|
||||
xo.host.emergencyShutdownHost hostId
|
||||
|
||||
$scope.saveHost = ($data) ->
|
||||
{host} = $scope
|
||||
{name_label, name_description, enabled} = $data
|
||||
@@ -410,5 +420,23 @@ module.exports = angular.module 'xoWebApp.host', [
|
||||
netOnly: false,
|
||||
loadOnly: false
|
||||
}
|
||||
|
||||
$scope.canAdmin = (id = undefined) ->
|
||||
if id == undefined
|
||||
id = $scope.host && $scope.host.id
|
||||
|
||||
return id && xoApi.canInteract(id, 'administrate') || false
|
||||
|
||||
$scope.canOperate = (id = undefined) ->
|
||||
if id == undefined
|
||||
id = $scope.host && $scope.host.id
|
||||
|
||||
return id && xoApi.canInteract(id, 'operate') || false
|
||||
|
||||
$scope.canView = (id = undefined) ->
|
||||
if id == undefined
|
||||
id = $scope.host && $scope.host.id
|
||||
|
||||
return id && xoApi.canInteract(id, 'view') || false
|
||||
# A module exports its name.
|
||||
.name
|
||||
|
||||
@@ -13,8 +13,10 @@
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-cogs
|
||||
| General
|
||||
span.quick-edit(tooltip="Edit General settings", ng-click="hostSettings.$show()")
|
||||
span.quick-edit(tooltip="Edit General settings", ng-click="hostSettings.$show()", ng-if = '!hostSettings.$visible && canAdmin()')
|
||||
i.fa.fa-edit.fa-fw
|
||||
span.quick-edit(ng-if="hostSettings.$visible", tooltip="Cancel Edition", ng-click="hostSettings.$cancel()")
|
||||
i.fa.fa-undo.fa-fw
|
||||
.panel-body
|
||||
form(editable-form="", name="hostSettings", onbeforesave="saveHost($data)")
|
||||
dl.dl-horizontal
|
||||
@@ -186,7 +188,7 @@
|
||||
i.xo-icon-loading
|
||||
| Fetching stats...
|
||||
//- Action panel
|
||||
.grid-sm
|
||||
.grid-sm(ng-if = 'canOperate()')
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-flash
|
||||
@@ -195,35 +197,39 @@
|
||||
.grid-sm.grid--gutters
|
||||
.grid.grid-cell
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Add SR", tooltip-placement="top", type="button", style="width: 90%", xo-sref="SRs_new({container: host.id})")
|
||||
button.btn(tooltip="Add SR", tooltip-placement="top", type="button", style="width: 90%", xo-sref="SRs_new({container: host.id})", ng-if = 'canAdmin()')
|
||||
i.xo-icon-sr.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Add VM", tooltip-placement="top", type="button", style="width: 90%", xo-sref="VMs_new({container: host.id})")
|
||||
button.btn(tooltip="Add VM", tooltip-placement="top", type="button", style="width: 90%", xo-sref="VMs_new({container: host.id})", ng-if = 'canAdmin()')
|
||||
i.xo-icon-vm.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Reboot host", tooltip-placement="top", type="button", style="width: 90%", xo-click="rebootHost(host.id)")
|
||||
button.btn(tooltip="Reboot host", tooltip-placement="top", type="button", style="width: 90%", xo-click="rebootHost(host.id)", ng-if = 'canOperate()')
|
||||
i.fa.fa-refresh.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Shutdown host", tooltip-placement="top", type="button", style="width: 90%", xo-click="shutdownHost(host.id)")
|
||||
button.btn(tooltip="Shutdown host", tooltip-placement="top", type="button", style="width: 90%", xo-click="shutdownHost(host.id)", ng-if = 'canOperate()')
|
||||
i.fa.fa-power-off.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Suspend all VMs and shutdown host", tooltip-placement="top", type="button", style="width: 90%", xo-click="emergencyShutdownHost(host.id)", ng-if = 'canOperate()')
|
||||
i.fa.fa-exclamation-triangle.fa-2x.fa-fw
|
||||
.grid.grid-cell
|
||||
.grid-cell.btn-group(ng-if="host.enabled")
|
||||
button.btn(tooltip="Disable host", tooltip-placement="top", type="button", style="width: 90%", xo-click="disableHost(host.id)")
|
||||
button.btn(tooltip="Disable host", tooltip-placement="top", type="button", style="width: 90%", xo-click="disableHost(host.id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-times-circle.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(ng-if="!host.enabled")
|
||||
button.btn(tooltip="Enable host", tooltip-placement="top", type="button", style="width: 90%", xo-click="enableHost(host.id)")
|
||||
button.btn(tooltip="Enable host", tooltip-placement="top", type="button", style="width: 90%", xo-click="enableHost(host.id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-check-circle.fa-2x.fa-fw
|
||||
.grid.grid-cell
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Restart toolstack", tooltip-placement="top", type="button", style="width: 90%", xo-click="restartToolStack(host.id)")
|
||||
button.btn(tooltip="Restart toolstack", tooltip-placement="top", type="button", style="width: 90%", xo-click="restartToolStack(host.id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-retweet.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(ng-if="pool.name_label && (hostsByPool[pool.id] | count)>1")
|
||||
button.btn(tooltip="Remove from pool", tooltip-placement="top", style="width: 90%", type="button", xo-click="pool_removeHost(host.id)")
|
||||
button.btn(tooltip="Remove from pool", tooltip-placement="top", style="width: 90%", type="button", xo-click="pool_removeHost(host.id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-cloud-upload.fa-2x.fa-fw
|
||||
.grid-cell.btn-group.dropdown(
|
||||
ng-if="pool.name_label && (hostsByPool[pool.id] | count)==1"
|
||||
dropdown
|
||||
)
|
||||
button.btn.dropdown-toggle(
|
||||
ng-if = 'canAdmin()'
|
||||
dropdown-toggle
|
||||
tooltip="Move host to another pool"
|
||||
tooltip-placement="top"
|
||||
@@ -238,10 +244,11 @@
|
||||
i.xo-icon-host.fa-fw
|
||||
| To {{p.name_label}}
|
||||
.grid-cell.btn-group(ng-if="!pool.name_label")
|
||||
button.btn(tooltip="Add to pool", tooltip-placement="top", style="width: 90%", type="button", xo-click="pool_addHost(host.id)")
|
||||
button.btn(tooltip="Add to pool", tooltip-placement="top", style="width: 90%", type="button", xo-click="pool_addHost(host.id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-cloud-download.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(style="margin-bottom: 0.5em")
|
||||
button.btn(
|
||||
ng-if = 'canAdmin()'
|
||||
tooltip="Import VM"
|
||||
tooltip-placement="top"
|
||||
type="button"
|
||||
@@ -286,7 +293,7 @@
|
||||
th Name
|
||||
th Format
|
||||
th Size
|
||||
th Physical/Allocated usage
|
||||
th Physical usage
|
||||
th Type
|
||||
th Status
|
||||
//- TODO: display PBD status for each SR of this host (connected or not)
|
||||
@@ -298,21 +305,20 @@
|
||||
td {{SR.size | bytesToSize}}
|
||||
td
|
||||
.progress-condensed
|
||||
.progress-bar(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.usage}}", aria-valuemax="{{SR.size}}", style="width: {{[SR.physical_usage, SR.size] | percentage}}", tooltip="Physical usage: {{[SR.physical_usage, SR.size] | percentage}}")
|
||||
.progress-bar.progress-bar-info(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.physical_usage}}", aria-valuemax="{{SR.size}}", style="width: {{[(SR.usage-SR.physical_usage), SR.size] | percentage}}", tooltip="Allocated: {{[(SR.usage), SR.size] | percentage}}")
|
||||
.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)")
|
||||
a(tooltip="Disconnect this SR", xo-click="disconnectPBD(SRsToPBDs[SR.id].id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-unlink.fa-lg
|
||||
td(ng-if="!SRsToPBDs[SR.id].attached")
|
||||
span.label.label-default Disconnected
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(tooltip="Reconnect this SR", xo-click="connectPBD(SRsToPBDs[SR.id].id)")
|
||||
a(tooltip="Reconnect this SR", xo-click="connectPBD(SRsToPBDs[SR.id].id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-link.fa-lg
|
||||
a(tooltip="Forget this SR", xo-click="removePBD(SRsToPBDs[SR.id].id)")
|
||||
a(tooltip="Forget this SR", xo-click="removePBD(SRsToPBDs[SR.id].id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-ban.fa-lg
|
||||
//- Local SR
|
||||
//- TODO: migrate to SRs and not PBDs when implemented in xo-server spec
|
||||
@@ -323,21 +329,20 @@
|
||||
td {{SR.size | bytesToSize}}
|
||||
td
|
||||
.progress-condensed
|
||||
.progress-bar(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.usage}}", aria-valuemax="{{SR.size}}", style="width: {{[SR.physical_usage, SR.size] | percentage}}", tooltip="Physical usage: {{[SR.physical_usage, SR.size] | percentage}}")
|
||||
.progress-bar.progress-bar-info(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.physical_usage}}", aria-valuemax="{{SR.size}}", style="width: {{[(SR.usage-SR.physical_usage), SR.size] | percentage}}", tooltip="Allocated: {{[(SR.usage), SR.size] | percentage}}")
|
||||
.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)")
|
||||
a(tooltip="Disconnect this SR", xo-click="disconnectPBD(SRsToPBDs[SR.id].id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-unlink.fa-lg
|
||||
td(ng-if="!SRsToPBDs[SR.id].attached")
|
||||
span.label.label-default Disconnected
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(tooltip="Reconnect this SR", xo-click="connectPBD(SRsToPBDs[SR.id].id)")
|
||||
a(tooltip="Reconnect this SR", xo-click="connectPBD(SRsToPBDs[SR.id].id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-link.fa-lg
|
||||
a(tooltip="Forget this SR", xo-click="removePBD(SRsToPBDs[SR.id].id)")
|
||||
a(tooltip="Forget this SR", xo-click="removePBD(SRsToPBDs[SR.id].id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-ban.fa-lg
|
||||
//- Networks/Interfaces panel
|
||||
.grid-sm
|
||||
@@ -355,7 +360,7 @@
|
||||
th.col-md-1 Link status
|
||||
tr(ng-repeat="PIF in host.$PIFs | resolve | orderBy:natural('name_label') track by PIF.id")
|
||||
td
|
||||
| {{PIF.device}}
|
||||
| {{PIF.device}}
|
||||
span.label.label-primary(ng-if="PIF.management") XAPI
|
||||
td
|
||||
span(ng-if="PIF.vlan > -1")
|
||||
@@ -368,23 +373,23 @@
|
||||
td(ng-if="PIF.attached")
|
||||
span.label.label-success Connected
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(tooltip="Disconnect this interface", xo-click="disconnectPIF(PIF.id)")
|
||||
a(tooltip="Disconnect this interface", xo-click="disconnectPIF(PIF.id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-unlink.fa-lg
|
||||
td(ng-if="!PIF.attached")
|
||||
span.label.label-default Disconnected
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(tooltip="Connect this interface", xo-click="connectPIF(PIF.id)")
|
||||
a(tooltip="Connect this interface", xo-click="connectPIF(PIF.id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-link.fa-lg
|
||||
a(tooltip="Remove this interface", xo-click="removePIF(PIF.id)")
|
||||
a(tooltip="Remove this interface", xo-click="removePIF(PIF.id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-trash-o.fa-lg
|
||||
.text-right
|
||||
button.btn(type="button", ng-class = '{"btn-success": creatingNetwork, "btn-primary": !creatingNetwork}', ng-click="creatingNetwork = !creatingNetwork")
|
||||
button.btn(type="button", ng-class = '{"btn-success": creatingNetwork, "btn-primary": !creatingNetwork}', ng-click="creatingNetwork = !creatingNetwork", ng-hide = '!canAdmin()', ng-disabled = '!canAdmin()')
|
||||
i.fa.fa-plus(ng-if = '!creatingNetwork')
|
||||
i.fa.fa-minus(ng-if = 'creatingNetwork')
|
||||
| Create Network
|
||||
br
|
||||
form.form-inline.text-right#createNetworkForm(ng-if = 'creatingNetwork', name = 'createNetworkForm', ng-submit = 'createNetwork(newNetworkName, newNetworkDescription, newNetworkPIF, newNetworkMTU, newNetworkVlan)')
|
||||
fieldset(ng-attr-disabled = '{{ createNetworkWaiting ? true : undefined }}')
|
||||
fieldset(ng-disabled = 'createNetworkWaiting || !canAdmin()')
|
||||
.form-group
|
||||
label(for = 'newNetworkPIF') Interface
|
||||
select.form-control(ng-model = 'newNetworkPIF', ng-change = 'updateMTU(newNetworkPIF)', ng-options='(PIF | resolve).device for PIF in host.$PIFs')
|
||||
@@ -439,7 +444,7 @@
|
||||
| {{task.progress*100 | number:1}}%
|
||||
td.oneliner
|
||||
| {{task.name_label}}
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
span.pull-right.btn-group.quick-buttons(ng-if = 'canAdmin()')
|
||||
a(xo-click="cancelTask(task.id)")
|
||||
i.fa.fa-times.fa-lg(tooltip="Cancel this task")
|
||||
a(xo-click="destroyTask(task.id)")
|
||||
@@ -451,7 +456,7 @@
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-comments
|
||||
| Logs
|
||||
span.quick-edit(ng-if="host.messages | isNotEmpty", tooltip="Remove all logs", ng-click="deleteAllLog()")
|
||||
span.quick-edit(ng-if="(host.messages | isNotEmpty) && canAdmin()", tooltip="Remove all logs", ng-click="deleteAllLog()")
|
||||
i.fa.fa-trash-o.fa-fw
|
||||
.panel-body
|
||||
p.center(ng-if="host.messages | isEmpty") No recent logs
|
||||
@@ -462,7 +467,7 @@
|
||||
td {{message.time*1e3 | date:"medium"}}
|
||||
td
|
||||
| {{message.name}}
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
span.pull-right.btn-group.quick-buttons(ng-if = 'canAdmin()')
|
||||
a(xo-click="deleteLog(message.id)")
|
||||
i.fa.fa-trash-o.fa-lg(tooltip="Remove this log entry")
|
||||
.center(ng-if = '(host.messages | count) > 5 || currentLogPage > 1')
|
||||
@@ -475,7 +480,7 @@
|
||||
| Patches
|
||||
span.quick-edit(ng-click="listMissingPatches(host.id)", tooltip="Check for updates")
|
||||
i.fa.fa-question-circle
|
||||
span.quick-edit(ng-click="installAllPatches(host.id)", tooltip="Install all the missing patches", style="margin-right:5px")
|
||||
span.quick-edit(ng-click="installAllPatches(host.id)", tooltip="Install all the missing patches", style="margin-right:5px", ng-if = 'canAdmin()')
|
||||
i.fa.fa-download
|
||||
.panel-body
|
||||
table.table.table-hover(ng-if="poolPatches || updates")
|
||||
@@ -494,8 +499,9 @@
|
||||
td.oneliner {{patch.date | date:"medium"}}
|
||||
td -
|
||||
td
|
||||
span(ng-click="installPatch(host.id, patch.uuid)", tooltip="Click to install the patch on this host")
|
||||
span(ng-click="installPatch(host.id, patch.uuid)", tooltip="Click to install the patch on this host", ng-if = 'canAdmin()')
|
||||
span.label.label-danger Missing
|
||||
span.label.label-danger(ng-if = '!canAdmin()') Missing
|
||||
tr(ng-repeat="patch in poolPatches | map | slice:(5*(currentPatchPage-1)):(5*currentPatchPage)")
|
||||
td.oneliner {{patch.name}}
|
||||
td.oneliner {{patch.description}}
|
||||
@@ -505,8 +511,10 @@
|
||||
td
|
||||
span(ng-if="isPoolPatchApplied(patch)")
|
||||
span.label.label-success Applied
|
||||
span(ng-click="installPatch(host.id, patch.uuid)", ng-if="!isPoolPatchApplied(patch)", tooltip="Click to apply the patch on this host")
|
||||
span.label.label-warning Not applied
|
||||
span(ng-if="!isPoolPatchApplied(patch)")
|
||||
span(ng-click="installPatch(host.id, patch.uuid)", tooltip="Click to apply the patch on this host", ng-if = 'canAdmin()')
|
||||
span.label.label-warning Not applied
|
||||
span.label.label-warning(ng-if = '!canAdmin()') Not applied
|
||||
.center(ng-if = '(poolPatches | count) > 5 || currentPatchPage > 1')
|
||||
pagination(boundary-links="true", total-items="poolPatches | count", ng-model="$parent.currentPatchPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import angular from 'angular'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
import xoTag from 'tag'
|
||||
import includes from 'lodash.includes'
|
||||
|
||||
import xoApi from 'xo-api'
|
||||
|
||||
@@ -20,7 +21,7 @@ export default angular.module('xoWebApp.list', [
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('ListCtrl', function (xoApi) {
|
||||
.controller('ListCtrl', function (xoApi, $scope, $rootScope) {
|
||||
this.hosts = xoApi.getView('host')
|
||||
this.pools = xoApi.getView('pool')
|
||||
this.SRs = xoApi.getView('SR')
|
||||
@@ -29,7 +30,84 @@ export default angular.module('xoWebApp.list', [
|
||||
this.hostsByPool = xoApi.getIndex('hostsByPool')
|
||||
this.runningHostsByPool = xoApi.getIndex('runningHostsByPool')
|
||||
this.vmsByContainer = xoApi.getIndex('vmsByContainer')
|
||||
})
|
||||
$scope.canView = function (id) {
|
||||
return xoApi.canInteract(id, 'view')
|
||||
}
|
||||
|
||||
$scope.shouldAppear = (obj) => {
|
||||
// States
|
||||
const powerState = obj.power_state
|
||||
// If there is a search option on the power state (running or halted),
|
||||
// then objects that do not have a power_state (eg: SRs) are not displayed
|
||||
if (($scope.states['running'] !== 2 || $scope.states['halted'] !== 2) && !powerState) return false
|
||||
if (powerState) {
|
||||
if ($scope.states[powerState.toLowerCase()] === 0) return false
|
||||
if (($scope.states['running'] === 1 || $scope.states['halted'] === 1) &&
|
||||
$scope.states[powerState.toLowerCase()] !== 1) return false
|
||||
}
|
||||
|
||||
if ($scope.states['disconnected'] !== 2 && !obj.$PBDs) return false
|
||||
let disconnected = false
|
||||
if (obj.$PBDs) {
|
||||
for (const id of obj.$PBDs) {
|
||||
const pbd = xoApi.get(id)
|
||||
disconnected |= !pbd.attached
|
||||
}
|
||||
if ($scope.states['disconnected'] === 0 && disconnected) return false
|
||||
if ($scope.states['disconnected'] === 1 && !disconnected) return false
|
||||
}
|
||||
|
||||
// Types
|
||||
if ($scope.types[obj.type.toLowerCase()] === 0) return false
|
||||
if ($scope.types[obj.type.toLowerCase()] === 2 && includes($scope.types, 1)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const _initOptions = () => {
|
||||
$scope.types = {
|
||||
'host': 2,
|
||||
'pool': 2,
|
||||
'sr': 2,
|
||||
'vm': 2
|
||||
}
|
||||
$scope.states = {
|
||||
'running': 2,
|
||||
'halted': 2,
|
||||
'disconnected': 2
|
||||
}
|
||||
}
|
||||
_initOptions()
|
||||
|
||||
$scope.parsedListFilter = $scope.listFilter
|
||||
$rootScope.searchParse = () => {
|
||||
let keyWords = []
|
||||
const words = $scope.listFilter ? $scope.listFilter.split(' ') : ['']
|
||||
_initOptions()
|
||||
for (const word of words) {
|
||||
let isOption = word.charAt(0) === '*'
|
||||
const isNegation = word.charAt(0) === '!'
|
||||
// as long as there is a '!', it is an option. ie !vm <=> !*vm
|
||||
isOption = isOption || isNegation
|
||||
let option = (isNegation ? word.substring(1, word.length) : word).toLowerCase()
|
||||
option = option.charAt(0) === '*' ? option.substring(1, option.length) : option
|
||||
if (!isOption) {
|
||||
if (option !== '') keyWords.push(option)
|
||||
} else {
|
||||
if ($scope.types.hasOwnProperty(option)) {
|
||||
$scope.types[option] = isNegation ? 0 : 1
|
||||
} else if ($scope.states.hasOwnProperty(option)) {
|
||||
$scope.states[option] = isNegation ? 0 : 1
|
||||
}
|
||||
}
|
||||
}
|
||||
$scope.parsedListFilter = keyWords.join(' ')
|
||||
}
|
||||
|
||||
$scope.onClick = (type) => {
|
||||
$rootScope.options[type.toLowerCase()] = !$rootScope.options[type.toLowerCase()]
|
||||
$rootScope.updateListFilter(type.toLowerCase())
|
||||
}
|
||||
})
|
||||
// A module exports its name.
|
||||
.name
|
||||
|
||||
@@ -1,7 +1,44 @@
|
||||
.sub-bar
|
||||
.grid(style="margin-left:1em")
|
||||
.btn-group.dropdown(dropdown)
|
||||
a.btn.navbar-btn.dropdown-toggle.filter(dropdown-toggle)
|
||||
| Types
|
||||
i.fa.fa-caret-down
|
||||
ul.dropdown-menu.inverse(role="menu" style="color:white")
|
||||
li(
|
||||
ng-repeat = "type in ['VM', 'SR', 'Host', 'Pool']"
|
||||
ng-click='onClick(type)'
|
||||
)
|
||||
|  
|
||||
label(ng-click)
|
||||
i.fa.fa-square-o(ng-if='!options[type.toLowerCase()]')
|
||||
i.fa.fa-check-square-o(ng-if='options[type.toLowerCase()]')
|
||||
| {{type}}
|
||||
.btn-group.dropdown(dropdown)
|
||||
a.btn.navbar-btn.dropdown-toggle.filter(dropdown-toggle)
|
||||
| States
|
||||
i.fa.fa-caret-down
|
||||
ul.dropdown-menu.inverse(role="menu" style="color:white")
|
||||
li(
|
||||
ng-repeat = "state in ['Running', 'Halted', 'Disconnected']"
|
||||
ng-click='onClick(state)'
|
||||
)
|
||||
|  
|
||||
label(ng-click)
|
||||
i.fa.fa-square-o(ng-if='!options[state.toLowerCase()]')
|
||||
i.fa.fa-check-square-o(ng-if='options[state.toLowerCase()]')
|
||||
| {{state}}
|
||||
//- TODO: print a message when no entries.
|
||||
|
||||
//- FIXME: Ugly trick to force the results to be under the sub bar.
|
||||
div(style="margin-top: 50px; visibility: hidden; height: 0") .
|
||||
|
||||
//- If it's a (named) pool.
|
||||
.grid.flat-object(ng-repeat="pool in list.pools.all | xoHideUnauthorized | filter:listFilter | orderBy:natural('name_label') track by pool.id", ng-if="pool.name_label", xo-sref="pools_view({id: pool.id})")
|
||||
.grid.flat-object(
|
||||
ng-repeat="pool in list.pools.all | xoHideUnauthorized | filter:parsedListFilter | orderBy:natural('name_label') track by pool.id"
|
||||
ng-if="pool.name_label && shouldAppear(pool)"
|
||||
xo-sref="pools_view({id: pool.id})"
|
||||
)
|
||||
//- Icon.
|
||||
.grid-cell.flat-cell.flat-cell-type
|
||||
i.xo-icon-pool
|
||||
@@ -43,7 +80,11 @@
|
||||
//- /Properties & tags.
|
||||
//- /Pool.
|
||||
//- If it's a host.
|
||||
.grid.flat-object(ng-repeat="host in list.hosts.all | xoHideUnauthorized | filter:listFilter | orderBy:natural('name_label') track by host.id", xo-sref="hosts_view({id: host.id})")
|
||||
.grid.flat-object(
|
||||
ng-repeat="host in list.hosts.all | xoHideUnauthorized | filter:parsedListFilter | orderBy:natural('name_label') track by host.id"
|
||||
ng-if="shouldAppear(host)"
|
||||
xo-sref="hosts_view({id: host.id})"
|
||||
)
|
||||
//- Icon.
|
||||
.grid-cell.flat-cell.flat-cell-type
|
||||
i.xo-icon-host(class="xo-color-{{host.power_state | lowercase}}")
|
||||
@@ -76,7 +117,11 @@
|
||||
//- /Properties & tags.
|
||||
//- /Host.
|
||||
//- If it's a VM.
|
||||
.grid.flat-object(ng-repeat="VM in list.VMs.all | xoHideUnauthorized | filter:listFilter | orderBy:natural('name_label') track by VM.id", xo-sref="VMs_view({id: VM.id})")
|
||||
.grid.flat-object(
|
||||
ng-repeat="VM in list.VMs.all | xoHideUnauthorized | filter:parsedListFilter | orderBy:natural('name_label') track by VM.id"
|
||||
ng-if="shouldAppear(VM)"
|
||||
xo-sref="VMs_view({id: VM.id})"
|
||||
)
|
||||
//- Icon.
|
||||
.grid-cell.flat-cell.flat-cell-type
|
||||
i.xo-icon-vm(class="xo-color-{{VM.power_state | lowercase}}")
|
||||
@@ -94,14 +139,14 @@
|
||||
| {{VM.CPUs.number}} vCPUs
|
||||
.grid-cell.flat-cell
|
||||
| {{VM.memory.size | bytesToSize}} RAM
|
||||
.grid-cell.flat-cell(ng-init="container = (VM.$container | resolve)")
|
||||
.grid-cell.flat-cell(ng-init="container = (VM.$container | resolve)", ng-if="canView((VM.$container | resolve).id)")
|
||||
div(ng-if="'pool' === container.type")
|
||||
| Resident on:
|
||||
a(ui-sref="pools_view({id: container.id})") {{container.name_label}}
|
||||
div(ng-if="'host' === container.type", ng-init="pool = (container.$poolId | resolve)")
|
||||
| Resident on:
|
||||
a(ui-sref="hosts_view({id: container.id})") {{container.name_label}}
|
||||
small(ng-if="pool.name_label")
|
||||
small(ng-if="pool.name_label && canView(pool.id)")
|
||||
| (
|
||||
a(ui-sref="pools_view({id: pool.id})") {{pool.name_label}}
|
||||
| )
|
||||
@@ -116,7 +161,11 @@
|
||||
//- /Properties & tags.
|
||||
//- /VM.
|
||||
//- If it's a SR.
|
||||
.grid.flat-object(ng-repeat="SR in list.SRs.all | xoHideUnauthorized | filter:listFilter | orderBy:natural('name_label') track by SR.id", xo-sref="SRs_view({id: SR.id})")
|
||||
.grid.flat-object(
|
||||
ng-repeat="SR in list.SRs.all | xoHideUnauthorized | filter:parsedListFilter | orderBy:natural('name_label') track by SR.id"
|
||||
ng-if="shouldAppear(SR)"
|
||||
xo-sref="SRs_view({id: SR.id})"
|
||||
)
|
||||
//- Icon.
|
||||
.grid-cell.flat-cell.flat-cell-type
|
||||
i.xo-icon-sr
|
||||
@@ -129,7 +178,7 @@
|
||||
.grid-cell.flat-cell.flat-cell-description
|
||||
i {{SR.name_description}}
|
||||
.grid-cell.flat-cell
|
||||
| Usage: {{[SR.usage, SR.size] | percentage}} ({{SR.usage | bytesToSize}}/{{SR.size | bytesToSize}})
|
||||
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)")
|
||||
|
||||
75
app/modules/migrate-vm/index.js
Normal file
75
app/modules/migrate-vm/index.js
Normal file
@@ -0,0 +1,75 @@
|
||||
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.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) => {
|
||||
$scope.selected.vif[vif.id] = 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
|
||||
50
app/modules/migrate-vm/view.jade
Normal file
50
app/modules/migrate-vm/view.jade
Normal file
@@ -0,0 +1,50 @@
|
||||
form(ng-submit="migrate()")
|
||||
.modal-header
|
||||
h3 VM migration
|
||||
.modal-body
|
||||
.form-inline
|
||||
label Choose a network to migrate the VM: 
|
||||
select.form-control(
|
||||
ng-options="network.id as network.name_label for network in networks"
|
||||
ng-model="selected.migrationNetwork"
|
||||
)
|
||||
p  
|
||||
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 network:
|
||||
table.table(ng-if="!intraPoolMigration")
|
||||
tr
|
||||
th.col-sm-5 Device
|
||||
th.col-sm-7 Networks
|
||||
tbody
|
||||
tr(ng-repeat="vif in VIFs")
|
||||
td VIF \#{{ vif.device }} ({{vif.MAC}})
|
||||
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
|
||||
@@ -3,6 +3,7 @@ import uiRouter from 'angular-ui-router'
|
||||
|
||||
import updater from '../updater'
|
||||
import xoServices from 'xo-services'
|
||||
import includes from 'lodash.includes'
|
||||
|
||||
import view from './view'
|
||||
|
||||
@@ -14,7 +15,7 @@ export default angular.module('xoWebApp.navbar', [
|
||||
updater,
|
||||
xoServices
|
||||
])
|
||||
.controller('NavbarCtrl', function ($state, xoApi, xo, $scope, updater) {
|
||||
.controller('NavbarCtrl', function ($state, xoApi, xo, $scope, updater, $rootScope) {
|
||||
this.updater = updater
|
||||
// TODO: It would make sense to inject xoApi in the scope.
|
||||
Object.defineProperties(this, {
|
||||
@@ -31,9 +32,82 @@ export default angular.module('xoWebApp.navbar', [
|
||||
}
|
||||
|
||||
// When a searched is entered, we must switch to the list view if
|
||||
// necessary.
|
||||
this.ensureListView = function () {
|
||||
$state.go('list')
|
||||
// necessary. When the text field is empty again, we must swith
|
||||
// to tree view
|
||||
let timeout
|
||||
$scope.ensureListView = function (listFilter) {
|
||||
clearTimeout(timeout)
|
||||
timeout = window.setTimeout(function () {
|
||||
$state.go('list').then(() =>
|
||||
$rootScope.searchParse(),
|
||||
$scope.updateOptions()
|
||||
)
|
||||
}, 400)
|
||||
}
|
||||
|
||||
const _isOption = function (word, option) {
|
||||
if (word === '*' + option || word === '!' + option || word === '!*' + option) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
const _removeOption = function (option) {
|
||||
if (!$scope.$root.listFilter) {
|
||||
return
|
||||
}
|
||||
const words = $scope.$root.listFilter.split(' ')
|
||||
$scope.$root.listFilter = ''
|
||||
for (const word of words) {
|
||||
if (!_isOption(word, option) && word !== '') {
|
||||
$scope.$root.listFilter += word + ' '
|
||||
}
|
||||
}
|
||||
}
|
||||
const _addOption = function (option) {
|
||||
if (!$scope.$root.listFilter) {
|
||||
$scope.$root.listFilter = '*' + option + ' '
|
||||
return
|
||||
}
|
||||
const words = $scope.$root.listFilter.split(' ')
|
||||
if (!includes(words, '*' + option) && !includes(words, '!' + option) && !includes(words, '!*' + option)) {
|
||||
if ($scope.$root.listFilter.charAt($scope.$root.listFilter.length - 1) !== ' ') {
|
||||
$scope.$root.listFilter += ' '
|
||||
}
|
||||
$scope.$root.listFilter += '*' + option + ' '
|
||||
}
|
||||
}
|
||||
|
||||
$rootScope.options = {
|
||||
'vm': false,
|
||||
'sr': false,
|
||||
'host': false,
|
||||
'pool': false,
|
||||
'running': false,
|
||||
'halted': false,
|
||||
'disconnected': false
|
||||
}
|
||||
// Checkboxes --> Text
|
||||
// Update text field after a checkbox has been clicked
|
||||
$rootScope.updateListFilter = function (option) {
|
||||
if ($rootScope.options[option]) {
|
||||
_addOption(option)
|
||||
} else {
|
||||
_removeOption(option)
|
||||
}
|
||||
$scope.ensureListView($scope.$root.listFilter)
|
||||
}
|
||||
// Text --> Checkboxes
|
||||
// Update checkboxes after the text field has been changed
|
||||
$scope.updateOptions = function () {
|
||||
const words = $scope.$root.listFilter ? $scope.$root.listFilter.split(' ') : ['']
|
||||
for (const opt in $rootScope.options) {
|
||||
$rootScope.options[opt] = false
|
||||
for (let word of words) {
|
||||
if (_isOption(word, opt)) {
|
||||
$rootScope.options[opt] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.tasks = xoApi.getView('runningTasks')
|
||||
|
||||
@@ -21,12 +21,12 @@ nav.navbar.navbar-inverse.navbar-fixed-top(role = 'navigation')
|
||||
type = 'text'
|
||||
placeholder = ''
|
||||
ng-model = '$root.listFilter'
|
||||
ng-change = 'navbar.ensureListView()'
|
||||
ng-change = 'ensureListView($root.listFilter)'
|
||||
)
|
||||
span.input-group-btn
|
||||
button.btn.btn-search(
|
||||
type = 'button'
|
||||
ng-click = 'navbar.ensureListView()'
|
||||
ng-click = 'ensureListView($root.listFilter)'
|
||||
)
|
||||
i.fa.fa-search
|
||||
//- /Search form.
|
||||
@@ -92,15 +92,15 @@ nav.navbar.navbar-inverse.navbar-fixed-top(role = 'navigation')
|
||||
a(ui-sref="dashboard.index")
|
||||
i.fa.fa-dashboard
|
||||
| Dashboard
|
||||
//- li.disabled(ui-sref-active="active")
|
||||
//- a(ui-sref="graph")
|
||||
//- i.fa.fa-sitemap
|
||||
//- | Graphs view
|
||||
li.divider
|
||||
li(ng-class = '{ disabled: navbar.user.permission !== "admin" }')
|
||||
a(ui-sref = 'backup.index')
|
||||
i.fa.fa-archive
|
||||
| Backup
|
||||
li
|
||||
a(ui-sref = 'taskscheduler.index')
|
||||
i.fa.fa-cogs
|
||||
| Job Manager
|
||||
li.divider
|
||||
li(
|
||||
ui-sref-active = 'active'
|
||||
|
||||
@@ -2,6 +2,7 @@ angular = require 'angular'
|
||||
cloneDeep = require 'lodash.clonedeep'
|
||||
filter = require 'lodash.filter'
|
||||
forEach = require 'lodash.foreach'
|
||||
trim = require 'lodash.trim'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
@@ -11,7 +12,7 @@ module.exports = angular.module 'xoWebApp.newVm', [
|
||||
.config ($stateProvider) ->
|
||||
$stateProvider.state 'VMs_new',
|
||||
url: '/vms/new/:container'
|
||||
controller: 'NewVmsCtrl'
|
||||
controller: 'NewVmsCtrl as ctrl'
|
||||
template: require './view'
|
||||
.controller 'NewVmsCtrl', (
|
||||
$scope, $stateParams, $state
|
||||
@@ -19,8 +20,53 @@ module.exports = angular.module 'xoWebApp.newVm', [
|
||||
bytesToSizeFilter, sizeToBytesFilter
|
||||
notify
|
||||
) ->
|
||||
{get} = xoApi
|
||||
$scope.multipleVmsActive = false
|
||||
$scope.vmsNames = ['VM1', 'VM2']
|
||||
$scope.numberOfVms = 2
|
||||
$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.initExistingValues = (template) ->
|
||||
$scope.name_label = template.name_label
|
||||
sizes = {}
|
||||
$scope.existingDiskSizeValues = {}
|
||||
$scope.existingDiskSizeUnits = {}
|
||||
forEach xoApi.get(template.$VBDs), (VBD) ->
|
||||
sizes[VBD.position] = bytesToSizeFilter xoApi.get(VBD.VDI).size
|
||||
$scope.existingDiskSizeValues[VBD.position] = parseInt(sizes[VBD.position].split(' ')[0], 10)
|
||||
$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()
|
||||
|
||||
{get} = xoApi
|
||||
removeItems = do ->
|
||||
splice = Array::splice.call.bind Array::splice
|
||||
(array, index, n) -> splice array, index, n ? 1
|
||||
@@ -103,29 +149,35 @@ module.exports = angular.module 'xoWebApp.newVm', [
|
||||
default_SR = get pool.default_SR
|
||||
default_SR = if default_SR then default_SR.id else ''
|
||||
)
|
||||
|
||||
$scope.availableMethods = {}
|
||||
$scope.CPUs = ''
|
||||
$scope.pv_args = ''
|
||||
$scope.installation_cdrom = ''
|
||||
$scope.installation_method = ''
|
||||
$scope.installation_network = ''
|
||||
$scope.memory = ''
|
||||
$scope.name_description = ''
|
||||
$scope.memoryValue = null
|
||||
$scope.units = ['MiB', 'GiB', 'TiB']
|
||||
$scope.memoryUnit = $scope.units[0]
|
||||
$scope.name_description = 'Created by XO'
|
||||
$scope.name_label = ''
|
||||
$scope.template = ''
|
||||
$scope.firstSR = ''
|
||||
$scope.VDIs = []
|
||||
$scope.VIFs = []
|
||||
$scope.isDiskTemplate = false
|
||||
$scope.cloudConfigSshKey = ''
|
||||
$scope.bootAfterCreate = true
|
||||
|
||||
$scope.updateMemoryUnit = (memoryUnit) ->
|
||||
$scope.memoryUnit = memoryUnit
|
||||
|
||||
$scope.addVIF = do ->
|
||||
id = 0
|
||||
->
|
||||
(network = '') ->
|
||||
$scope.VIFs.push {
|
||||
id: id++
|
||||
network: ''
|
||||
network
|
||||
}
|
||||
$scope.addVIF()
|
||||
|
||||
$scope.removeVIF = (index) -> removeItems $scope.VIFs, index
|
||||
|
||||
@@ -143,6 +195,8 @@ module.exports = angular.module 'xoWebApp.newVm', [
|
||||
id: VDI_id++
|
||||
bootable: false
|
||||
size: ''
|
||||
sizeValue: null
|
||||
sizeUnit: $scope.units[0]
|
||||
SR: default_SR
|
||||
type: 'system'
|
||||
}
|
||||
@@ -150,7 +204,11 @@ module.exports = angular.module 'xoWebApp.newVm', [
|
||||
# When the selected template changes, updates other variables.
|
||||
$scope.$watch 'template', (template) ->
|
||||
return unless template
|
||||
# After each template change, initialize coreOsCloudConfig to empty
|
||||
$scope.coreOsCloudConfig = ''
|
||||
|
||||
# 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
|
||||
@@ -160,45 +218,84 @@ module.exports = angular.module 'xoWebApp.newVm', [
|
||||
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
|
||||
# if the template has no config disk
|
||||
# nor it's Other install media (specific case)
|
||||
# then do NOT display disk and network panel
|
||||
if VDIs.length is 0 and template.name_label isnt 'Other install media'
|
||||
$scope.isDiskTemplate = true
|
||||
$scope.VIFs.length = 0
|
||||
else $scope.isDiskTemplate = false
|
||||
for VDI in VDIs
|
||||
VDI.id = VDI_id++
|
||||
VDI.size = bytesToSizeFilter VDI.size
|
||||
VDI.SR or= default_SR
|
||||
VDI.size = bytesToSizeFilter VDI.size
|
||||
VDI.sizeValue = if VDI.size then parseInt(VDI.size.split(' ')[0], 10) 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.createVM = ->
|
||||
$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 'tree'
|
||||
|
||||
xenDefaultWeight = 256
|
||||
$scope.weightMap = {
|
||||
'Quarter (1/4)': xenDefaultWeight / 4,
|
||||
'Half (1/2)': xenDefaultWeight / 2,
|
||||
'Normal': xenDefaultWeight,
|
||||
'Double (x2)': xenDefaultWeight * 2
|
||||
}
|
||||
|
||||
$scope.createVM = (name_label) ->
|
||||
{
|
||||
CPUs
|
||||
cpuWeight
|
||||
pv_args
|
||||
installation_cdrom
|
||||
installation_method
|
||||
installation_network
|
||||
memory
|
||||
memoryValue
|
||||
memoryUnit
|
||||
name_description
|
||||
name_label
|
||||
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}"
|
||||
|
||||
# Transforms the size from human readable format to bytes.
|
||||
VDI.size = sizeToBytesFilter VDI.size
|
||||
# TODO: handles invalid values.
|
||||
|
||||
# Does not edit the displayed data directly.
|
||||
@@ -207,10 +304,13 @@ module.exports = angular.module 'xoWebApp.newVm', [
|
||||
# Removes the dummy identifier used for AngularJS.
|
||||
delete VIF.id
|
||||
|
||||
# Removes the MAC address if empty.
|
||||
if 'MAC' of VIF
|
||||
VIF.MAC = VIF.MAC.trim()
|
||||
delete VIF.MAC unless VIF.MAC
|
||||
# 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'
|
||||
@@ -232,7 +332,6 @@ module.exports = angular.module 'xoWebApp.newVm', [
|
||||
}
|
||||
else
|
||||
installation = undefined
|
||||
|
||||
data = {
|
||||
installation
|
||||
pv_args
|
||||
@@ -240,8 +339,8 @@ module.exports = angular.module 'xoWebApp.newVm', [
|
||||
template: template.id
|
||||
VDIs
|
||||
VIFs
|
||||
existingDisks
|
||||
}
|
||||
|
||||
# TODO:
|
||||
# - disable the form during creation
|
||||
# - indicate the progress of the operation
|
||||
@@ -249,35 +348,67 @@ module.exports = angular.module 'xoWebApp.newVm', [
|
||||
title: 'VM creation'
|
||||
message: 'VM creation started'
|
||||
}
|
||||
$scope.creatingVM = true
|
||||
id = null
|
||||
xoApi.call('vm.create', data)
|
||||
.then (id_) ->
|
||||
id = id_
|
||||
|
||||
xoApi.call('vm.create', data).then (id) ->
|
||||
# If nothing to sets, just stops.
|
||||
return id unless CPUs or name_description or memory
|
||||
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 memory
|
||||
memory = sizeToBytesFilter memory
|
||||
if memoryValue
|
||||
# FIXME: handles invalid entries.
|
||||
data.memory = memory
|
||||
|
||||
xoApi.call('vm.set', data).then -> id
|
||||
.then (id) ->
|
||||
$state.go 'VMs_view', { id }
|
||||
data.memoryValue = memoryValue + ' ' + memoryUnit
|
||||
return xoApi.call('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, '-')
|
||||
$scope.cloudContent = '#cloud-config\nhostname: ' + hostname + '\nssh_authorized_keys:\n - ' + $scope.cloudConfigSshKey + '\n'
|
||||
# The first SR for a template with an existing disk
|
||||
$scope.firstSR = (get (get template.$VBDs[0]).VDI).$SR
|
||||
# Use the generic CloudConfig creation
|
||||
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
|
||||
# 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
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.xo-icon-vm
|
||||
| Create VM on
|
||||
| Create VM on
|
||||
a(ng-if="'pool' === container.type", ui-sref="pools_view({id: container.id})")
|
||||
| {{container.name_label}}
|
||||
| {{container.name_label}}
|
||||
a(ng-if="'host' === container.type", ui-sref="hosts_view({id: container.id})")
|
||||
| {{container.name_label}}
|
||||
| {{container.name_label}}
|
||||
//- Add server panel
|
||||
form.form-horizontal(ng-submit="createVM()")
|
||||
form.form-horizontal(ng-submit="createVMs()")
|
||||
.grid
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
@@ -18,11 +18,11 @@ form.form-horizontal(ng-submit="createVM()")
|
||||
.form-group
|
||||
label.col-sm-3.control-label Template
|
||||
.col-sm-9
|
||||
select.form-control(ng-model="template", ng-options="template.name_label for template in templates | orderBy:natural('name_label') track by template.id", required="")
|
||||
select.form-control(ng-model="template", ng-options="template.name_label for template in templates | orderBy:natural('name_label') track by template.id", required="", ng-change = '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", required="", ng-model="name_label")
|
||||
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
|
||||
@@ -36,26 +36,42 @@ form.form-horizontal(ng-submit="createVM()")
|
||||
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')
|
||||
option(value = '') default
|
||||
.form-group
|
||||
label.col-sm-3.control-label RAM
|
||||
.col-sm-9
|
||||
input.form-control(type="text", placeholder="{{template.memory.size | bytesToSize}}", ng-model="memory")
|
||||
.grid(ng-if="isDiskTemplate")
|
||||
.panel.panel-default
|
||||
.input-group
|
||||
input.form-control(type='number' min="0" placeholder="{{ template.memory.size | bytesConvert:memoryUnit:'iB' }}" ng-model="memoryValue")
|
||||
.input-group-btn
|
||||
span.pull-right.dropdown(dropdown)
|
||||
button.btn.btn-default.dropdown-toggle(type='button' dropdown-toggle)
|
||||
| {{ memoryUnit }}
|
||||
span.caret
|
||||
ul.dropdown-menu(role="menu")
|
||||
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-info-circle
|
||||
| Template info
|
||||
i.fa.fa-cloud
|
||||
| Config Drive
|
||||
span.pull-right
|
||||
label(style = 'cursor: pointer;')
|
||||
input.hidden(type = 'checkbox', ng-model = '$parent.configDriveActive')
|
||||
i.fa(ng-class = '{"fa-toggle-on": $parent.configDriveActive, "text-success": $parent.configDriveActive, "fa-toggle-off": !$parent.configDriveActive}', style = 'font-size: 1.5em;')
|
||||
.panel-body
|
||||
p.center This template will create automatically a VM with:
|
||||
.col-md-6
|
||||
ul(ng-repeat="VIF in template.VIFs | resolve | orderBy:natural('device') track by VIF.id")
|
||||
li Interface \#{{VIF.device}} (MTU {{VIF.MTU}}) on {{(VIF.$network | resolve).name_label}}
|
||||
.col-md-6
|
||||
ul(ng-repeat = 'VBD in (template.$VBDs | resolve) track by VBD.id')
|
||||
li Disk {{(VBD.VDI | resolve).name_label}} ({{(VBD.VDI | resolve).size | bytesToSize}}) on {{((VBD.VDI | resolve).$SR | resolve).name_label}}
|
||||
.grid(ng-if="!isDiskTemplate")
|
||||
//- Install panel
|
||||
.panel.panel-default
|
||||
fieldset(ng-disabled = '!$parent.configDriveActive')
|
||||
.form-group
|
||||
label.col-sm-3.control-label SSH Key
|
||||
.col-sm-9
|
||||
input.form-control(type="text", placeholder="ssh-rsa AAAA.... you@machine", ng-model="$parent.cloudConfigSshKey", name="cloudConfigSshKey", required)
|
||||
//- 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
|
||||
@@ -70,19 +86,17 @@ form.form-horizontal(ng-submit="createVM()")
|
||||
name = 'installation_method'
|
||||
ng-model = '$parent.installation_method'
|
||||
value = 'cdrom'
|
||||
required
|
||||
)
|
||||
select.form-control.disabled(
|
||||
ng-disabled="'cdrom' !== installation_method"
|
||||
ng-model="$parent.installation_cdrom"
|
||||
required
|
||||
)
|
||||
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'
|
||||
ng-show = '(availableMethods.http || availableMethods.ftp || availableMethods.nfs)'
|
||||
)
|
||||
label.col-sm-3.control-label Network
|
||||
.col-sm-9
|
||||
@@ -93,7 +107,6 @@ form.form-horizontal(ng-submit="createVM()")
|
||||
name = 'installation_method'
|
||||
ng-model = '$parent.installation_method'
|
||||
value = 'network'
|
||||
required
|
||||
)
|
||||
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"')
|
||||
@@ -104,22 +117,12 @@ form.form-horizontal(ng-submit="createVM()")
|
||||
name = 'installation_method'
|
||||
ng-model = '$parent.installation_method'
|
||||
value = 'pxe'
|
||||
required
|
||||
)
|
||||
.form-group(ng-show="template.PV_args")
|
||||
.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")
|
||||
|
||||
//- <div class="form-group"> FIXME
|
||||
//- <label class="col-sm-3 control-label">Home server</label>
|
||||
//- <div class="col-sm-9">
|
||||
//- <select class="form-control">
|
||||
//- <option>Default (auto)</option>
|
||||
//- </select>
|
||||
//- </div>
|
||||
//- </div>
|
||||
|
||||
//- Interface panel
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
@@ -134,10 +137,10 @@ form.form-horizontal(ng-submit="createVM()")
|
||||
//- 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="00:00:00:00:00")
|
||||
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.id as network.name_label for network in networks | orderBy:natural("name_label") track by network.id'
|
||||
ng-options = 'network as network.name_label for network in networks | orderBy:natural("name_label") track by network.id'
|
||||
ng-model = 'VIF.network'
|
||||
required
|
||||
)
|
||||
@@ -154,8 +157,56 @@ form.form-horizontal(ng-submit="createVM()")
|
||||
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()', 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(ng-if="!isDiskTemplate")
|
||||
.grid
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-disk
|
||||
@@ -170,13 +221,50 @@ form.form-horizontal(ng-submit="createVM()")
|
||||
th.col-md-4 Description
|
||||
th.col-md-1  
|
||||
//- Buttons
|
||||
tr(ng-repeat="VBD in (template.$VBDs | resolve) track by VBD.id", ng-if="isDiskTemplate")
|
||||
td
|
||||
select.form-control(ng-model="(VBD.VDI | resolve).$SR", ng-options="SR.id as (SR.name_label + ' (' + (SR.size - SR.physical_usage | bytesToSize) + ' free)') for SR in (writable_SRs | orderBy:natural('name_label'))", ng-change = 'saveChange(VBD.position, "$SR", (VBD.VDI | resolve).$SR)')
|
||||
td.text-center
|
||||
i.fa.fa-check(ng-if = 'VBD.bootable')
|
||||
td
|
||||
.input-group
|
||||
input.form-control(
|
||||
type='number'
|
||||
min="0"
|
||||
placeholder="Size of this virtual disk"
|
||||
ng-model="existingDiskSizeValues[VBD.position]"
|
||||
ng-readonly='!configDriveActive'
|
||||
ng-change = 'updateVdiSize(VBD.position)'
|
||||
)
|
||||
.input-group-btn
|
||||
span.pull-right.dropdown(dropdown)
|
||||
button.btn.btn-default.dropdown-toggle(type='button' dropdown-toggle ng-disabled='!configDriveActive')
|
||||
| {{ existingDiskSizeUnits[VBD.position] }}
|
||||
span.caret
|
||||
ul.dropdown-menu(role="menu")
|
||||
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 + ' (' + (SR.size - SR.usage | bytesToSize) + ' free)') for SR in (writable_SRs | orderBy:natural('name_label'))")
|
||||
select.form-control(ng-model="VDI.SR", ng-options="SR.id as (SR.name_label + ' (' + (SR.size - SR.physical_usage | bytesToSize) + ' free)') for SR in (writable_SRs | orderBy:natural('name_label'))")
|
||||
td.text-center
|
||||
input(type="checkbox", ng-model="VDI.bootable")
|
||||
td
|
||||
input.form-control(type="text", ng-model="VDI.size", required="")
|
||||
.input-group
|
||||
input.form-control(type='number' min="0" placeholder="Size of this virtual disk" ng-model="VDI.sizeValue")
|
||||
.input-group-btn
|
||||
span.pull-right.dropdown(dropdown)
|
||||
button.btn.btn-default.dropdown-toggle(type='button' dropdown-toggle)
|
||||
| {{ VDI.sizeUnit }}
|
||||
span.caret
|
||||
ul.dropdown-menu(role="menu")
|
||||
li(ng-repeat="unit in units")
|
||||
a(ng-click="VDI.sizeUnit = unit") {{ unit }}
|
||||
td
|
||||
input.form-control(type="text", placeholder="Name of this virtual disk", ng-model="VDI.name_label")
|
||||
td
|
||||
@@ -206,7 +294,9 @@ form.form-horizontal(ng-submit="createVM()")
|
||||
.panel-body
|
||||
.grid
|
||||
.grid-cell
|
||||
p.center.big {{name_label}}
|
||||
p.center.big
|
||||
span(ng-if="!multipleVmsActive") {{name_label}}
|
||||
span(ng-if="multipleVmsActive") {{numberOfVms}} new VMs
|
||||
|
|
||||
span.small(ng-if="template.name_label") ({{template.name_label}})
|
||||
.grid
|
||||
@@ -218,7 +308,9 @@ form.form-horizontal(ng-submit="createVM()")
|
||||
.grid-cell
|
||||
//- p.stat-name RAM
|
||||
p.center.big(tooltip="RAM")
|
||||
| {{(memory) || (template.memory.size | bytesToSize)}}
|
||||
span(ng-if="memory") {{memory}} {{memoryUnit}}
|
||||
span(ng-if="!memory") {{template.memory.size | bytesToSize}}
|
||||
|
|
||||
i.xo-icon-memory
|
||||
.grid-cell
|
||||
//- p.stat-name Disks
|
||||
@@ -230,7 +322,17 @@ form.form-horizontal(ng-submit="createVM()")
|
||||
p.center.big(tooltip="Network interfaces")
|
||||
| {{(VIFs.length) || (template.VIFs.length) || 0}}x
|
||||
i.xo-icon-network
|
||||
p.center(ng-if="isDiskTemplate")
|
||||
| Cloud configuration is
|
||||
strong.text-success(ng-if = 'configDriveActive') enabled.
|
||||
strong.text-danger(ng-if = '!configDriveActive') disabled.
|
||||
p.center
|
||||
button.btn.btn-lg.btn-primary(type="submit")
|
||||
i.fa.fa-play
|
||||
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')
|
||||
i.fa.fa-play(ng-if = '!creatingVM')
|
||||
i.fa.fa-circle-o-notch.fa-spin(ng-if = 'creatingVM')
|
||||
| Create VM
|
||||
|
||||
@@ -75,6 +75,16 @@ export default angular.module('xoWebApp.pool', [
|
||||
})
|
||||
}
|
||||
|
||||
$scope.setDefaultSr = function (id) {
|
||||
let {pool} = $scope
|
||||
return modal.confirm({
|
||||
title: 'Set default SR',
|
||||
message: 'Are you sure you want to set this SR as default?'
|
||||
}).then(function () {
|
||||
return xo.pool.setDefaultSr(pool.id, id)
|
||||
})
|
||||
}
|
||||
|
||||
$scope.deleteLog = function (id) {
|
||||
console.log('Remove log', id)
|
||||
return xo.log.delete(id)
|
||||
|
||||
@@ -99,7 +99,14 @@
|
||||
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}}")
|
||||
.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
|
||||
@@ -114,18 +121,24 @@
|
||||
th Type
|
||||
th Size
|
||||
th.col-md-4 Physical/Allocated usage
|
||||
th.col-md-1 Action
|
||||
tr(
|
||||
ng-repeat="SR in srs | map | orderBy:natural('name_label') track by SR.id"
|
||||
xo-sref="SRs_view({id: SR.id})"
|
||||
)
|
||||
td.oneliner {{SR.name_label}}
|
||||
td.oneliner
|
||||
| {{SR.name_label}}
|
||||
span.label.label-primary(ng-if="SR.id === pool.default_SR") Default SR
|
||||
td.oneliner {{SR.name_description}}
|
||||
td {{SR.SR_type}}
|
||||
td {{SR.size | bytesToSize}}
|
||||
td
|
||||
.progress-condensed
|
||||
.progress-bar(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.usage}}", aria-valuemax="{{SR.size}}", style="width: {{[SR.physical_usage, SR.size] | percentage}}", tooltip="Physical usage: {{[SR.physical_usage, SR.size] | percentage}}")
|
||||
.progress-bar.progress-bar-info(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.physical_usage}}", aria-valuemax="{{SR.size}}", style="width: {{[(SR.usage-SR.physical_usage), SR.size] | percentage}}", tooltip="Allocated: {{[(SR.usage), SR.size] | percentage}}")
|
||||
.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")
|
||||
|
||||
//- Logs panel
|
||||
.grid-sm
|
||||
|
||||
@@ -4,6 +4,7 @@ import forEach from 'lodash.foreach'
|
||||
import marked from 'marked'
|
||||
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'
|
||||
@@ -26,12 +27,12 @@ function loadDefaults (schema, configuration) {
|
||||
}
|
||||
forEach(schema.properties, (item, key) => {
|
||||
if (item.type === 'boolean' && !(key in configuration)) { // String default values are used as placeholders in view
|
||||
configuration[key] = item && item.default
|
||||
configuration[key] = Boolean(item && item.default)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function cleanUpConfiguration (schema, configuration) {
|
||||
function cleanUpConfiguration (schema, configuration, dump = {}) {
|
||||
if (!schema || !configuration) {
|
||||
return
|
||||
}
|
||||
@@ -44,20 +45,25 @@ function cleanUpConfiguration (schema, configuration) {
|
||||
}
|
||||
|
||||
function keepItem (item) {
|
||||
if (item === undefined || item === null || item === '' || (Array.isArray(item) && item.length === 0) || item.__use === false) {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
return !(item == null || item === '' || (Array.isArray(item) && item.length === 0))
|
||||
}
|
||||
|
||||
forEach(configuration, (item, key) => {
|
||||
item = sanitizeItem(item)
|
||||
configuration[key] = item
|
||||
dump[key] = item
|
||||
|
||||
if (!keepItem(item) || !schema.properties || !(key in schema.properties)) {
|
||||
delete configuration[key]
|
||||
} else if (schema.properties && schema.properties[key] && schema.properties[key].type === 'object') {
|
||||
cleanUpConfiguration(schema.properties[key], item)
|
||||
delete dump[key]
|
||||
} else if (schema.properties && schema.properties[key]) {
|
||||
const type = schema.properties[key].type
|
||||
|
||||
if (type === 'integer' || type === 'number') {
|
||||
dump[key] = +dump[key]
|
||||
} else if (type === 'object') {
|
||||
dump[key] = {}
|
||||
cleanUpConfiguration(schema.properties[key], item, dump[key])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -80,18 +86,32 @@ export default angular.module('settings.plugins', [
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('SettingsPlugins', function (xo, notify) {
|
||||
.controller('SettingsPlugins', function (xo, notify, modal) {
|
||||
this.disabled = {}
|
||||
|
||||
const refreshPlugins = () => xo.plugin.get().then(plugins => {
|
||||
forEach(plugins, plugin => {
|
||||
plugin._loaded = plugin.loaded
|
||||
plugin._autoload = plugin.autoload
|
||||
if (!plugin.configuration) {
|
||||
plugin.configuration = {}
|
||||
const preparePluginForView = plugin => {
|
||||
plugin._loaded = plugin.loaded
|
||||
plugin._autoload = plugin.autoload
|
||||
if (!plugin.configuration) {
|
||||
plugin.configuration = {}
|
||||
}
|
||||
loadDefaults(plugin.configurationSchema, plugin.configuration)
|
||||
}
|
||||
|
||||
const refreshPlugin = id => {
|
||||
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)
|
||||
}
|
||||
loadDefaults(plugin.configurationSchema, plugin.configuration)
|
||||
})
|
||||
}
|
||||
|
||||
const refreshPlugins = () => xo.plugin.get().then(plugins => {
|
||||
forEach(plugins, preparePluginForView)
|
||||
this.plugins = plugins
|
||||
})
|
||||
refreshPlugins()
|
||||
@@ -100,21 +120,52 @@ export default angular.module('settings.plugins', [
|
||||
this.disabled[id] = true
|
||||
return xo.plugin[method](...args)
|
||||
.finally(() => {
|
||||
return refreshPlugins()
|
||||
.then(() => this.disabled[id] = false)
|
||||
refreshPlugin(id)
|
||||
this.disabled[id] = false
|
||||
})
|
||||
}
|
||||
|
||||
this.isRequired = isRequired
|
||||
this.isPassword = isPassword
|
||||
|
||||
this.configure = (plugin) => {
|
||||
cleanUpConfiguration(plugin.configurationSchema, plugin.configuration)
|
||||
_execPluginMethod(plugin.id, 'configure', plugin.id, plugin.configuration)
|
||||
const newConfiguration = {}
|
||||
plugin.errors = []
|
||||
|
||||
cleanUpConfiguration(plugin.configurationSchema, plugin.configuration, newConfiguration)
|
||||
_execPluginMethod(plugin.id, 'configure', plugin.id, newConfiguration)
|
||||
.then(() => notify.info({
|
||||
title: 'Plugin configuration',
|
||||
message: 'Successfully saved'
|
||||
}))
|
||||
.catch(err => {
|
||||
forEach(err.data, data => {
|
||||
const fieldPath = data.field.split('.').slice(1)
|
||||
const fieldPathTitles = []
|
||||
let groupObject = plugin.configurationSchema
|
||||
forEach(fieldPath, groupName => {
|
||||
groupObject = groupObject.properties[groupName]
|
||||
fieldPathTitles.push(groupObject.title || groupName)
|
||||
})
|
||||
plugin.errors.push(`${fieldPathTitles.join(' > ')} ${data.message}`)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
this.purgeConfiguration = (plugin) => {
|
||||
modal.confirm({
|
||||
title: 'Purge configuration',
|
||||
message: 'Are you sure you want to purge this configuration ?'
|
||||
}).then(() => {
|
||||
_execPluginMethod(plugin.id, 'purgeConfiguration', plugin.id).then(() => {
|
||||
notify.info({
|
||||
title: 'Purge configuration',
|
||||
message: 'This plugin config is now purged.'
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
this.toggleAutoload = (plugin) => {
|
||||
let method
|
||||
if (!plugin._autoload && plugin.autoload) {
|
||||
@@ -152,12 +203,16 @@ export default angular.module('settings.plugins', [
|
||||
})
|
||||
|
||||
.controller('MultiString', function ($scope, xo, xoApi) {
|
||||
if (this.model === undefined || this.model === null) {
|
||||
this.model = []
|
||||
}
|
||||
if (!Array.isArray(this.model)) {
|
||||
throw new Error('multiString directive model must be an array')
|
||||
const checkModel = () => {
|
||||
if (this.model === undefined || this.model === null) {
|
||||
this.model = []
|
||||
}
|
||||
if (!Array.isArray(this.model)) {
|
||||
throw new Error('multiString directive model must be an array')
|
||||
}
|
||||
}
|
||||
checkModel()
|
||||
$scope.$watch(() => this.model, checkModel)
|
||||
|
||||
this.add = (string) => {
|
||||
string = trim(string)
|
||||
@@ -172,7 +227,7 @@ export default angular.module('settings.plugins', [
|
||||
}
|
||||
})
|
||||
|
||||
.directive('objectInput', () => {
|
||||
.directive('confObjectInput', () => {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: objectInputView,
|
||||
@@ -181,12 +236,12 @@ export default angular.module('settings.plugins', [
|
||||
schema: '=',
|
||||
required: '='
|
||||
},
|
||||
controller: 'ObjectInput as ctrl',
|
||||
controller: 'ConfObjectInput as ctrl',
|
||||
bindToController: true
|
||||
}
|
||||
})
|
||||
|
||||
.controller('ObjectInput', function ($scope, xo, xoApi) {
|
||||
.controller('ConfObjectInput', function ($scope, xo, xoApi) {
|
||||
const prepareModel = () => {
|
||||
if (this.model === undefined || this.model === null) {
|
||||
this.model = {
|
||||
|
||||
@@ -5,10 +5,25 @@
|
||||
fieldset(ng-disabled = '!ctrl.required && !ctrl.model.__use', ng-hide = '!ctrl.required && !ctrl.model.__use')
|
||||
ul(style = 'padding-left: 0;')
|
||||
li.list-group-item(ng-repeat = '(key, value) in ctrl.schema.properties track by key')
|
||||
.input-group
|
||||
.input-group(ng-if = 'value.type != "boolean"')
|
||||
span.input-group-addon
|
||||
| {{key}}
|
||||
| {{value.title || key}}
|
||||
span.text-warning(ng-if = 'ctrl.isRequired(key, ctrl.schema)') *
|
||||
input.form-control.input-sm(ng-if = '!ctrl.isPassword(key)', type = 'text', ng-model = 'ctrl.model[key]', ng-required = 'ctrl.isRequired(key, ctrl.schema)')
|
||||
input.form-control.input-sm(ng-if = 'ctrl.isPassword(key)', type = 'password', ng-model = 'ctrl.model[key]', ng-required = 'ctrl.isRequired(key, ctrl.schema)')
|
||||
input.form-control.input-sm(
|
||||
ng-if = 'value.type != "number" && value.type != "integer"',
|
||||
type = '{{ctrl.isPassword(key) ? "password" : "text"}}',
|
||||
ng-model = 'ctrl.model[key]',
|
||||
ng-required = 'ctrl.isRequired(key, ctrl.schema)'
|
||||
)
|
||||
input.form-control.input-sm(
|
||||
ng-if = 'value.type == "number" || value.type == "integer"',
|
||||
type = 'number',
|
||||
ng-model = 'ctrl.model[key]',
|
||||
ng-required = 'ctrl.isRequired(key, ctrl.schema)'
|
||||
)
|
||||
.form-inline(ng-if = 'value.type == "boolean"')
|
||||
.checkbox.small('style="color: #31708F;"') {{value.title || key}} :
|
||||
label('style="color: #A7AFB0;"')
|
||||
i.fa.fa-2x(ng-class = '{"fa-toggle-on": ctrl.model[key], "fa-toggle-off": !ctrl.model[key]}')
|
||||
input.hidden(type = 'checkbox', ng-model = 'ctrl.model[key]')
|
||||
.help-block(ng-bind-html = 'ctrl.schema.properties[key].description | md2html')
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-body
|
||||
p.text-center(ng-if = '!ctrl.plugins || !crtl.plugins.length') No plugins found
|
||||
p.text-center(ng-if = '!ctrl.plugins || !ctrl.plugins.length') No plugins found
|
||||
div(ng-repeat = 'plugin in ctrl.plugins | orderBy:"name" track by plugin.id')
|
||||
h3.form-inline.clearfix
|
||||
span.text-info {{ plugin.name }}
|
||||
@@ -30,19 +30,28 @@
|
||||
form.form-horizontal(ng-if = 'plugin.configurationSchema', ng-submit = 'ctrl.configure(plugin)')
|
||||
fieldset(ng-disabled = 'ctrl.disabled[plugin.id]')
|
||||
.form-group(ng-repeat = '(key, prop) in plugin.configurationSchema.properties')
|
||||
label.col-md-2.control-label
|
||||
| {{key}}
|
||||
label.col-md-2.control-label
|
||||
| {{prop.title || key}}
|
||||
span.text-warning(ng-if = 'ctrl.isRequired(key, plugin.configurationSchema)') *
|
||||
.col-md-5
|
||||
input.form-control(ng-if = 'prop.type === "string" && !ctrl.isPassword(key)', type = 'text', ng-model = 'plugin.configuration[key]', ng-required = 'ctrl.isRequired(key, plugin.configurationSchema)', placeholder = '{{ plugin.configurationSchema.properties[key].default }}')
|
||||
input.form-control(ng-if = 'prop.type === "string" && ctrl.isPassword(key)', type = 'password', ng-model = 'plugin.configuration[key]', ng-required = 'ctrl.isRequired(key, plugin.configurationSchema)')
|
||||
input.form-control(ng-if = 'prop.type === "integer" || prop.type === "number"', type = 'number', ng-model = 'plugin.configuration[key]', ng-required = 'ctrl.isRequired(key, plugin.configurationSchema)', placeholder = '{{ plugin.configurationSchema.properties[key].default }}')
|
||||
input.form-control(ng-if = 'prop.type === "string"', type = '{{ ctrl.isPassword(key) ? "password" : "text" }}', ng-model = 'plugin.configuration[key]', ng-required = 'ctrl.isRequired(key, plugin.configurationSchema)')
|
||||
multi-string-input(ng-if = 'prop.type === "array" && prop.items.type === "string"', model = 'plugin.configuration[key]')
|
||||
.checkbox(ng-if = 'prop.type === "boolean"'): label: input(type = 'checkbox', ng-model = 'plugin.configuration[key]')
|
||||
object-input(ng-if = 'prop.type === "object"', model = 'plugin.configuration[key]', schema = 'prop', required = 'ctrl.isRequired(key, plugin.configurationSchema)')
|
||||
conf-object-input(ng-if = 'prop.type === "object"', model = 'plugin.configuration[key]', schema = 'prop', required = 'ctrl.isRequired(key, plugin.configurationSchema)')
|
||||
.col-md-5
|
||||
span.help-block(ng-bind-html = 'prop.description | md2html')
|
||||
.form-group
|
||||
.col-md-offset-2.col-md-10.text-danger(ng-repeat = "err in plugin.errors")
|
||||
| {{ err }}
|
||||
.form-group
|
||||
.col-md-offset-2.col-md-10
|
||||
button.btn.btn-primary(type = 'submit')
|
||||
| Save configuration
|
||||
i.fa.fa-floppy-o
|
||||
.btn-toolbar
|
||||
.btn-group
|
||||
button.btn.btn-primary(type = 'submit')
|
||||
| Save configuration
|
||||
i.fa.fa-floppy-o
|
||||
.btn-group
|
||||
button.btn.btn-danger(type = 'button' ng-click = 'ctrl.purgeConfiguration(plugin)')
|
||||
| Purge configuration
|
||||
i.fa.fa-trash-o
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import angular from 'angular'
|
||||
import forEach from 'lodash.foreach'
|
||||
import includes from 'lodash.includes'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
import uiSelect from 'angular-ui-select'
|
||||
|
||||
@@ -26,18 +28,28 @@ export default angular.module('settings.servers', [
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('SettingsServers', function ($scope, $interval, servers, xoApi, xo, notify) {
|
||||
this.servers = servers
|
||||
.controller('SettingsServers', function ($scope, $rootScope, $interval, $filter, servers, xoApi, xo, notify) {
|
||||
const orderBy = $filter('orderBy')
|
||||
this.servers = orderBy(servers, $rootScope.natural('host'))
|
||||
$scope.readOnly = {}
|
||||
forEach(this.servers, (server) => {
|
||||
$scope.readOnly[server.id] = Boolean(server.readOnly)
|
||||
})
|
||||
const selected = this.selectedServers = {}
|
||||
const newServers = this.newServers = []
|
||||
|
||||
const refreshServers = () => {
|
||||
xo.server.getAll().then(servers => {
|
||||
this.servers = servers
|
||||
this.servers = orderBy(servers, $rootScope.natural('host'))
|
||||
})
|
||||
}
|
||||
const refreshServersIfUnfocused = () => {
|
||||
if (!$scope.isFocused) {
|
||||
refreshServers()
|
||||
}
|
||||
}
|
||||
|
||||
const interval = $interval(refreshServers, 10e3)
|
||||
const interval = $interval(refreshServersIfUnfocused, 10e3)
|
||||
$scope.$on('$destroy', () => {
|
||||
$interval.cancel(interval)
|
||||
})
|
||||
@@ -73,6 +85,9 @@ export default angular.module('settings.servers', [
|
||||
|
||||
this.addServer()
|
||||
this.saveServers = () => {
|
||||
const addresses = []
|
||||
forEach(xoApi.getView('host').all, host => addresses.push(host.address))
|
||||
|
||||
const newServers = this.newServers
|
||||
const servers = this.servers
|
||||
const updateServers = []
|
||||
@@ -87,6 +102,7 @@ export default angular.module('settings.servers', [
|
||||
if (!server.password) {
|
||||
delete server.password
|
||||
}
|
||||
server.readOnly = $scope.readOnly[id]
|
||||
xo.server.set(server)
|
||||
delete server.password
|
||||
updateServers.push(server)
|
||||
@@ -94,17 +110,26 @@ export default angular.module('settings.servers', [
|
||||
}
|
||||
for (let i = 0, len = newServers.length; i < len; i++) {
|
||||
const server = newServers[i]
|
||||
const {host, username, password} = server
|
||||
const {host, username, password, readOnly} = server
|
||||
if (!host) {
|
||||
continue
|
||||
}
|
||||
if (includes(addresses, host)) {
|
||||
notify.warning({
|
||||
title: 'Server already connected',
|
||||
message: `You are already connected to ${host}`
|
||||
})
|
||||
continue
|
||||
}
|
||||
xo.server.add({
|
||||
host,
|
||||
username,
|
||||
password,
|
||||
readOnly,
|
||||
autoConnect: false
|
||||
}).then(function (id) {
|
||||
server.id = id
|
||||
$scope.readOnly[id] = Boolean(readOnly)
|
||||
xo.server.connect(id).catch(error => {
|
||||
notify.error({
|
||||
title: 'Server connection error',
|
||||
|
||||
@@ -10,11 +10,12 @@
|
||||
tr
|
||||
th.col-md-5 Host
|
||||
th.col-md-2 User
|
||||
th.col-md-3 Password
|
||||
th.col-md-2 Password
|
||||
th.col-md-1.text.center Actions
|
||||
th.col-md-1.text.center Read only
|
||||
th.col-md-1.text-center
|
||||
i.fa.fa-trash-o.fa-lg(tooltip="Forget server")
|
||||
tr(ng-repeat="server in ctrl.servers | orderBy:natural('host') track by server.id")
|
||||
tr(ng-repeat="server in ctrl.servers track by server.id")
|
||||
td
|
||||
.input-group
|
||||
span.input-group-addon.hidden-xs(ng-if="server.status === 'connected'")
|
||||
@@ -23,11 +24,27 @@
|
||||
i.xo-icon-failure.fa-lg(tooltip="Disconnected")
|
||||
span.input-group-addon.hidden-xs(ng-if="server.status === 'connecting'")
|
||||
i.fa.fa-cog.fa-lg.fa-spin(tooltip="Connecting...")
|
||||
input.form-control(type="text", ng-model="server.host")
|
||||
input.form-control(
|
||||
type="text",
|
||||
ng-model="server.host",
|
||||
ng-focus="$parent.isFocused = true",
|
||||
ng-blur="$parent.isFocused = false"
|
||||
)
|
||||
td
|
||||
input.form-control(type="text", ng-model="server.username")
|
||||
input.form-control(
|
||||
type="text",
|
||||
ng-model="server.username",
|
||||
ng-focus="$parent.isFocused = true",
|
||||
ng-blur="$parent.isFocused = false"
|
||||
)
|
||||
td
|
||||
input.form-control(type="password", ng-model="server.password", placeholder="Fill to change the password")
|
||||
input.form-control(
|
||||
type="password",
|
||||
ng-model="server.password",
|
||||
placeholder="Fill to change the password",
|
||||
ng-focus="$parent.isFocused = true",
|
||||
ng-blur="$parent.isFocused = false"
|
||||
)
|
||||
td.text-center
|
||||
button.btn.btn-default(
|
||||
ng-if="server.status === 'disconnected'",
|
||||
@@ -43,6 +60,8 @@
|
||||
tooltip="Disconnect this server"
|
||||
)
|
||||
i.fa.fa-unlink
|
||||
td.text-center
|
||||
input(type="checkbox", ng-model="readOnly[server.id]")
|
||||
td.text-center
|
||||
input(type="checkbox", ng-model="ctrl.selectedServers[server.id]")
|
||||
tr(ng-repeat="server in ctrl.newServers")
|
||||
@@ -67,6 +86,8 @@
|
||||
placeholder="password"
|
||||
)
|
||||
td  
|
||||
td.text-center
|
||||
input( type="checkbox", ng-model="server.readOnly")
|
||||
td  
|
||||
p.text-center
|
||||
button.btn.btn-primary(type="submit")
|
||||
|
||||
@@ -36,17 +36,41 @@ export default angular.module('xoWebApp.sr', [
|
||||
})
|
||||
}
|
||||
})
|
||||
.controller('SrCtrl', function ($scope, $stateParams, $state, $q, notify, xoApi, xo, modal, $window, bytesToSizeFilter) {
|
||||
.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
|
||||
@@ -176,6 +200,7 @@ export default angular.module('xoWebApp.sr', [
|
||||
$scope.saveDisks = function (data) {
|
||||
// Group data by disk.
|
||||
let disks = {}
|
||||
let sizeChanges = false
|
||||
forEach(data, function (value, key) {
|
||||
let i = key.indexOf('/')
|
||||
|
||||
@@ -185,27 +210,52 @@ export default angular.module('xoWebApp.sr', [
|
||||
;(disks[id] || (disks[id] = {}))[prop] = value
|
||||
})
|
||||
|
||||
let promises = []
|
||||
forEach(disks, function (attributes, id) {
|
||||
// Keep only changed attributes.
|
||||
let disk = get(id)
|
||||
|
||||
forEach(attributes, function (value, name) {
|
||||
if (value === disk[name]) {
|
||||
delete attributes[name]
|
||||
}
|
||||
})
|
||||
|
||||
if (!isEmpty(attributes)) {
|
||||
// Inject id.
|
||||
attributes.id = id
|
||||
|
||||
// Ask the server to update the object.
|
||||
promises.push(xoApi.call('vdi.set', attributes))
|
||||
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
|
||||
}
|
||||
})
|
||||
|
||||
return $q.all(promises)
|
||||
let promises = []
|
||||
|
||||
const preCheck = sizeChanges ? modal.confirm({
|
||||
title: 'Disk resizing',
|
||||
message: 'Growing the size of a disk is not reversible'
|
||||
}) : $q.resolve()
|
||||
|
||||
return preCheck
|
||||
.then(() => {
|
||||
forEach(disks, function (attributes, id) {
|
||||
let disk = get(id)
|
||||
|
||||
// Resize disks
|
||||
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)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -8,8 +8,10 @@
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-cogs
|
||||
| General
|
||||
span.quick-edit(tooltip="Edit General settings", ng-click="srSettings.$show()")
|
||||
span.quick-edit(tooltip="Edit General settings", ng-click="srSettings.$show()", ng-if = '!srSettings.$visible')
|
||||
i.fa.fa-edit.fa-fw
|
||||
span.quick-edit(tooltip="Cancel Edition", ng-click="srSettings.$cancel()", ng-if = 'srSettings.$visible')
|
||||
i.fa.fa-undo.fa-fw
|
||||
.panel-body
|
||||
form(editable-form="", name="srSettings", onbeforesave="saveSR($data)")
|
||||
dl.dl-horizontal
|
||||
@@ -52,19 +54,12 @@
|
||||
| Stats
|
||||
.panel-body
|
||||
.row
|
||||
.col-sm-6.col-lg-4
|
||||
p.stat-name Physical Alloc:
|
||||
.col-sm-6
|
||||
p.stat-name Physical usage:
|
||||
canvas.stat-simple(id="doughnut", class="chart chart-doughnut", data="[(SR.physical_usage), (SR.size - SR.physical_usage)]", labels="['Used', 'Free']", options='{tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>"}')
|
||||
.col-sm-6.col-lg-4
|
||||
p.stat-name Virtual Alloc:
|
||||
canvas.stat-simple(id="doughnut", class="chart chart-doughnut", data="[(SR.usage), (SR.size - SR.usage)]", labels="['Used', 'Free']", options='{tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>"}')
|
||||
.col-sm-4.visible-lg
|
||||
.col-sm-6
|
||||
p.stat-name VDIs:
|
||||
p.center.big-stat {{SR.VDIs.length}}
|
||||
.row.hidden-lg
|
||||
.col-sm-12
|
||||
br
|
||||
p.stat-name {{SR.VDIs.length}} VDIs
|
||||
//- Action panel
|
||||
.grid
|
||||
.panel.panel-default
|
||||
@@ -96,39 +91,63 @@
|
||||
| VDI Map
|
||||
.panel-body
|
||||
.progress
|
||||
.progress-bar.progress-bar-vm(ng-if="((VDI.size/SR.size)*100) > 0.5", ng-repeat="VDI in SR.VDIs | resolve | orderBy:natural('name_label') track by VDI.id", role="progressbar", aria-valuemin="0", aria-valuenow="{{VDI.size}}", aria-valuemax="{{SR.size}}", style="width: {{[VDI.size, SR.size] | percentage}}", tooltip="{{VDI.name_label}} ({{[VDI.size, SR.size] | percentage}})")
|
||||
.progress-bar.progress-bar-vm(
|
||||
ng-if="((VDI.usage/SR.size)*100) > 0.5",
|
||||
ng-repeat="VDI in SR.VDIs | resolve | orderBy:natural('name_label') track by VDI.id",
|
||||
role="progressbar",
|
||||
aria-valuemin="0",
|
||||
aria-valuenow="{{VDI.usage}}",
|
||||
aria-valuemax="{{SR.size}}",
|
||||
style="width: {{[VDI.usage, SR.size] | percentage}}",
|
||||
tooltip="{{VDI.name_label}} ({{VDI.usage | bytesToSize}}) {{[VDI.usage, SR.size] | percentage}}"
|
||||
)
|
||||
//- display the name only if it fits in its progress bar
|
||||
span(ng-if="VDI.name_label.length < ((VDI.size/SR.size)*100)") {{VDI.name_label}}
|
||||
span(ng-if="VDI.name_label.length < ((VDI.usage/SR.size)*100)") {{VDI.name_label}}
|
||||
ul.list-inline.text-center
|
||||
li Total: {{SR.size | bytesToSize}}
|
||||
li Currently used: {{SR.usage | bytesToSize}}
|
||||
li Available: {{SR.size-SR.usage | bytesToSize}}
|
||||
li Currently used: {{SR.physical_usage | bytesToSize}}
|
||||
li Available: {{SR.size-SR.physical_usage | bytesToSize}}
|
||||
//- TODO: VDIs.
|
||||
.grid
|
||||
form(name = "disksForm" editable-form = '' onbeforesave = 'saveDisks($data)').panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-disk
|
||||
| Virtual disks
|
||||
span.quick-edit(tooltip="Edit disks", ng-click="disksForm.$show()")
|
||||
span.quick-edit(
|
||||
ng-if="!disksForm.$visible"
|
||||
tooltip="Edit disks"
|
||||
ng-click="disksForm.$show()"
|
||||
)
|
||||
i.fa.fa-edit.fa-fw
|
||||
span.quick-edit(
|
||||
ng-if="disksForm.$visible"
|
||||
tooltip="Cancel Edition"
|
||||
ng-click="disksForm.$cancel()"
|
||||
)
|
||||
i.fa.fa-undo.fa-fw
|
||||
span.quick-edit(tooltip="Rescan", ng-click="rescanSr(SR.id)")
|
||||
i.fa.fa-refresh.fa-fw
|
||||
.panel-body
|
||||
table.table.table-hover
|
||||
tr
|
||||
th Name
|
||||
th Description
|
||||
th Tags
|
||||
th Size
|
||||
th Virtual Machine:
|
||||
tr(ng-repeat="VDI in SR.VDIs | resolve | vdiFilter:vdiSearch | orderBy:natural('name_label') | slice:(10*(currentVDIPage-1)):(10*currentVDIPage)")
|
||||
th.col-sm-2 Name
|
||||
th.col-sm-2 Description
|
||||
th.col-sm-1 Tags
|
||||
th.col-sm-1 Size
|
||||
th.col-sm-1(ng-show="disksForm.$visible")
|
||||
th.col-sm-2
|
||||
| Virtual Machine:
|
||||
span.pull-right: button.btn.btn-danger(type = 'button', xo-click = 'deleteSelectedVdis()', tooltip = 'Delete selected disks'): i.fa.fa-trash
|
||||
tr(ng-repeat="VDI in VDIs | vdiFilter:vdiSearch | orderBy:natural('name_label') | slice:(10*(currentVDIPage-1)):(10*currentVDIPage)")
|
||||
td.oneliner
|
||||
span(
|
||||
editable-text="VDI.name_label"
|
||||
e-name = '{{VDI.id}}/name_label'
|
||||
)
|
||||
| {{VDI.name_label}}
|
||||
span.label.label-info(ng-if="VDI.$snapshot_of") snapshot
|
||||
span(ng-if="VDI.type === 'VDI-snapshot'")
|
||||
span.label.label-info(ng-if="VDI.$snapshot_of") snapshot
|
||||
span.label.label-warning(ng-if="!VDI.$snapshot_of") orphaned snapshot
|
||||
td.oneliner
|
||||
span(
|
||||
editable-text="VDI.name_description"
|
||||
@@ -138,22 +157,32 @@
|
||||
td
|
||||
xo-tag(object = 'VDI')
|
||||
td
|
||||
//- FIXME: should be editable, but the server needs first
|
||||
//- to accept a human readable string.
|
||||
| {{VDI.size | bytesToSize}}
|
||||
span(
|
||||
editable-text="VDI.sizeValue"
|
||||
e-name = '{{VDI.id}}/sizeValue'
|
||||
)
|
||||
| {{VDI.sizeValue}} {{VDI.sizeUnit}}
|
||||
td(ng-show="disksForm.$visible")
|
||||
span(
|
||||
editable-select="VDI.sizeUnit"
|
||||
e-ng-options="unit for unit in units"
|
||||
e-name="{{VDI.id}}/sizeUnit"
|
||||
)
|
||||
td.oneliner {{((VDI.$VBDs[0] | resolve).VM | resolve).name_label}}
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(ng-if="(VDI.$VBDs[0] | resolve).attached", xo-click="disconnectVBD(VDI.$VBDs[0])")
|
||||
i.fa.fa-unlink.fa-lg(tooltip="Disconnect this disk")
|
||||
a(ng-if="!(VDI.$VBDs[0] | resolve).attached", xo-click="deleteVDI(VDI.id)")
|
||||
i.fa.fa-trash-o.fa-lg(tooltip="Destroy this disk")
|
||||
span.pull-right
|
||||
.btn-group.quick-buttons
|
||||
a(ng-if="(VDI.$VBDs[0] | resolve).attached", xo-click="disconnectVBD(VDI.$VBDs[0])")
|
||||
i.fa.fa-unlink.fa-lg(tooltip="Disconnect this disk")
|
||||
a(ng-if="!(VDI.$VBDs[0] | resolve).attached", xo-click="deleteVDI(VDI.id)")
|
||||
i.fa.fa-trash-o.fa-lg(tooltip="Destroy this disk")
|
||||
input(ng-if = '!(VDI.$VBDs[0] | resolve).attached', type = 'checkbox', ng-model = 'selectedForDelete[VDI.id]', tooltip = 'select for deletion')
|
||||
//- TODO: Ability to create new VDIs.
|
||||
.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 = '(SR.VDIs | resolve | vdiFilter:vdiSearch).length > 10 || currentVDIPage > 1')
|
||||
pagination(boundary-links="true", total-items="(SR.VDIs | resolve | vdiFilter:vdiSearch).length", ng-model="$parent.currentVDIPage", items-per-page="10", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
|
||||
.center(ng-if = '(VDIs | vdiFilter:vdiSearch).length > 10 || currentVDIPage > 1')
|
||||
pagination(boundary-links="true", total-items="(VDIs | vdiFilter:vdiSearch).length", ng-model="$parent.currentVDIPage", items-per-page="10", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
|
||||
.btn-form(ng-show="disksForm.$visible")
|
||||
p.center
|
||||
button.btn.btn-default(
|
||||
@@ -189,7 +218,7 @@
|
||||
span.label.label-success Connected
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(xo-click="disconnectPBD(PBD.id)")
|
||||
i.fa.fa-unlink.fa-lg(tooltip="Disconnect to this host")
|
||||
i.fa.fa-unlink.fa-lg(tooltip="Disconnect from this host")
|
||||
td(ng-if="!PBD.attached")
|
||||
span.label.label-default Disconnected
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
|
||||
41
app/modules/task-scheduler/index.js
Normal file
41
app/modules/task-scheduler/index.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import angular from 'angular'
|
||||
import later from 'later'
|
||||
import scheduler from 'scheduler'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
|
||||
later.date.localTime()
|
||||
|
||||
import job from './job'
|
||||
import overview from './overview'
|
||||
import schedule from './schedule'
|
||||
|
||||
import view from './view'
|
||||
|
||||
export default angular.module('taskScheduler', [
|
||||
uiRouter,
|
||||
scheduler,
|
||||
|
||||
job,
|
||||
overview,
|
||||
schedule
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('taskscheduler', {
|
||||
abstract: true,
|
||||
data: {
|
||||
requireAdmin: true
|
||||
},
|
||||
template: view,
|
||||
url: '/taskscheduler'
|
||||
})
|
||||
|
||||
// Redirect to default sub-state.
|
||||
$stateProvider.state('taskscheduler.index', {
|
||||
url: '',
|
||||
controller: function ($state) {
|
||||
$state.go('taskscheduler.overview')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
.name
|
||||
19
app/modules/task-scheduler/job/array-input-view.jade
Normal file
19
app/modules/task-scheduler/job/array-input-view.jade
Normal file
@@ -0,0 +1,19 @@
|
||||
.form-group(ng-if = 'ctrl.active()')
|
||||
label.col-md-2.control-label(ng-if = 'ctrl.key')
|
||||
| {{ ctrl.key }}
|
||||
span.text-warning(ng-if = 'ctrl.isRequired()') *
|
||||
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
|
||||
ul(style = 'padding-left: 0;')
|
||||
li.list-group-item.clearfix(ng-repeat = 'item in ctrl.model track by $index')
|
||||
| {{item}}
|
||||
a.pull-right(ng-click = 'ctrl.remove($index)'): i.fa.fa-times
|
||||
form(ng-submit = 'ctrl.add(ctrl.newItem); ctrl.newItem = ""')
|
||||
.input-group
|
||||
input.form-control.input-sm(ng-if = 'ctrl.getType(ctrl.property.items) === "string"', type = 'text', ng-model = 'ctrl.newItem', ng-required = '!param.optional')
|
||||
input.form-control.input-sm(ng-if = 'ctrl.getType(ctrl.property.items) === "integer"', type = 'number', step = '1', ng-model = 'ctrl.newItem', ng-required = '!param.optional')
|
||||
input.form-control.input-sm(ng-if = 'ctrl.getType(ctrl.property.items) === "number"', type = 'number', step = 'any', ng-model = 'ctrl.newItem', ng-required = '!param.optional')
|
||||
span.input-group-addon(ng-if = 'ctrl.getType(ctrl.property.items) === "boolean"')
|
||||
input(type = 'checkbox', ng-model = 'ctrl.newItem')
|
||||
input.form-control.input-sm(ng-if = 'ctrl.getType(ctrl.property.items) === "boolean"', disabled)
|
||||
span.input-group-btn
|
||||
button.btn.btn-primary.btn-sm(type = 'submit') Add
|
||||
6
app/modules/task-scheduler/job/boolean-input-view.jade
Normal file
6
app/modules/task-scheduler/job/boolean-input-view.jade
Normal file
@@ -0,0 +1,6 @@
|
||||
.form-group(ng-if = 'ctrl.active()')
|
||||
label.col-md-2.control-label(ng-if = 'ctrl.key')
|
||||
| {{ ctrl.key }}
|
||||
span.text-warning(ng-if = 'ctrl.isRequired()') *
|
||||
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
|
||||
input(form = '{{ ctrl.form }}', type = 'checkbox', ng-model = 'ctrl.model')
|
||||
15
app/modules/task-scheduler/job/host-input-view.jade
Normal file
15
app/modules/task-scheduler/job/host-input-view.jade
Normal file
@@ -0,0 +1,15 @@
|
||||
.form-group(ng-if = 'ctrl.active()')
|
||||
label.col-md-2.control-label(ng-if = 'ctrl.key')
|
||||
| Hosts
|
||||
span.text-warning(ng-if = 'ctrl.isRequired()') *
|
||||
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
|
||||
ui-select(form = 'ctrl.form', ng-model = 'ctrl.intraModel', multiple, close-on-select = 'false', ng-required = 'ctrl.isRequired()', on-remove = 'ctrl.exportRemove($item)', on-select = 'ctrl.exportSelect($item)')
|
||||
ui-select-match(placeholder = 'Choose hosts')
|
||||
i(class = 'xo-icon-{{$item.type | lowercase}}')
|
||||
| {{$item.name_label}}
|
||||
span(ng-if = '$item.$container') ({{ ($item.$container | resolve).name_label }})
|
||||
ui-select-choices(repeat = 'object in ctrl.objects | selectHighLevel | filter:{type: "host"} | filter:$select.search | orderBy:["$container", "name_label"] track by object.id')
|
||||
div
|
||||
i(class = 'xo-icon-{{object.type | lowercase}}')
|
||||
| {{ object.name_label }}
|
||||
span(ng-if = 'object.$container') ({{ (object.$container | resolve).name_label || ((object.$container | resolve).master | resolve).name_label }})
|
||||
827
app/modules/task-scheduler/job/index.js
Normal file
827
app/modules/task-scheduler/job/index.js
Normal file
@@ -0,0 +1,827 @@
|
||||
import angular from 'angular'
|
||||
import assign from 'lodash.assign'
|
||||
import cloneDeep from 'lodash.clonedeep'
|
||||
import find from 'lodash.find'
|
||||
import forEach from 'lodash.foreach'
|
||||
import includes from 'lodash.includes'
|
||||
import map from 'lodash.map'
|
||||
import mapValues from 'lodash.mapvalues'
|
||||
import remove from 'lodash.remove'
|
||||
import trim from 'lodash.trim'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
import uiBootstrap from 'angular-ui-bootstrap'
|
||||
import Bluebird from 'bluebird'
|
||||
Bluebird.longStackTraces()
|
||||
|
||||
import arrayInputView from './array-input-view'
|
||||
import booleanInputView from './boolean-input-view'
|
||||
import hostInputView from './host-input-view'
|
||||
import integerInputView from './integer-input-view'
|
||||
import numberInputView from './number-input-view'
|
||||
import objectInputView from './object-input-view'
|
||||
import poolInputView from './pool-input-view'
|
||||
import srInputView from './sr-input-view'
|
||||
import stringInputView from './string-input-view'
|
||||
import view from './view'
|
||||
import vmInputView from './vm-input-view'
|
||||
import xoEntityInputView from './xo-entity-input-view'
|
||||
import xoObjectInputView from './xo-object-input-view'
|
||||
import xoRoleInputView from './xo-role-input-view'
|
||||
|
||||
// ====================================================================
|
||||
|
||||
const JOB_KEY = 'genericTask'
|
||||
|
||||
const jobCompliantMethods = [
|
||||
'acl.add',
|
||||
'acl.remove',
|
||||
'host.detach',
|
||||
'host.disable',
|
||||
'host.enable',
|
||||
'host.installAllPatches',
|
||||
'host.restart',
|
||||
'host.restartAgent',
|
||||
'host.set',
|
||||
'host.start',
|
||||
'host.stop',
|
||||
'job.runSequence',
|
||||
'vm.attachDisk',
|
||||
'vm.backup',
|
||||
'vm.clone',
|
||||
'vm.convert',
|
||||
'vm.copy',
|
||||
'vm.creatInterface',
|
||||
'vm.delete',
|
||||
'vm.migrate',
|
||||
'vm.migrate',
|
||||
'vm.restart',
|
||||
'vm.resume',
|
||||
'vm.revert',
|
||||
'vm.rollingBackup',
|
||||
'vm.rollingDrCopy',
|
||||
'vm.rollingSnapshot',
|
||||
'vm.set',
|
||||
'vm.setBootOrder',
|
||||
'vm.snapshot',
|
||||
'vm.start',
|
||||
'vm.stop',
|
||||
'vm.suspend'
|
||||
]
|
||||
|
||||
const getType = function (param) {
|
||||
if (!param) {
|
||||
return
|
||||
}
|
||||
if (Array.isArray(param.type)) {
|
||||
if (includes(param.type, 'integer')) {
|
||||
return 'integer'
|
||||
} else if (includes(param.type, 'number')) {
|
||||
return 'number'
|
||||
} else {
|
||||
return 'string'
|
||||
}
|
||||
}
|
||||
return param.type
|
||||
}
|
||||
|
||||
const isRequired = function (param) {
|
||||
if (!param) {
|
||||
return
|
||||
}
|
||||
return (!param.optional && !(includes(['boolean', 'array'], getType(param))))
|
||||
}
|
||||
/**
|
||||
* Takes care of unfilled not-required data and unwanted white-spaces
|
||||
*/
|
||||
const cleanUpData = function (data) {
|
||||
if (!data) {
|
||||
return
|
||||
}
|
||||
|
||||
function sanitizeItem (item) {
|
||||
if (typeof item === 'string') {
|
||||
item = trim(item)
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
function keepItem (item) {
|
||||
if ((item === undefined) || (item === null) || (item === '') || (Array.isArray(item) && item.length === 0) || item.__use === false) {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
forEach(data, (item, key) => {
|
||||
item = sanitizeItem(item)
|
||||
data[key] = item
|
||||
if (!keepItem(item)) {
|
||||
delete data[key]
|
||||
} else if (typeof item === 'object') {
|
||||
cleanUpData(item)
|
||||
}
|
||||
})
|
||||
|
||||
delete data.__use
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries extracting XO Object targeted property
|
||||
*/
|
||||
const reduceXoObject = function (value, propertyName = 'id') {
|
||||
return value && value[propertyName] || value
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapts all data "arrayed" by UI-multiple-selectors to job's cross-product trick
|
||||
*/
|
||||
const dataToParamVectorItems = function (params, data) {
|
||||
const items = []
|
||||
forEach(params, (param, name) => {
|
||||
if (Array.isArray(data[name]) && getType(param) !== 'array') {
|
||||
const values = []
|
||||
if (data[name].length === 1) { // One value, no need to engage cross-product
|
||||
data[name] = data[name].pop()
|
||||
} else {
|
||||
forEach(data[name], value => {
|
||||
values.push({[name]: reduceXoObject(value, name)})
|
||||
})
|
||||
if (values.length) { // No values at all
|
||||
items.push({
|
||||
type: 'set',
|
||||
values
|
||||
})
|
||||
}
|
||||
delete data[name]
|
||||
}
|
||||
}
|
||||
})
|
||||
if (Object.keys(data).length) {
|
||||
items.push({
|
||||
type: 'set',
|
||||
values: [mapValues(data, reduceXoObject)]
|
||||
})
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
const actionGroup = {
|
||||
group: undefined,
|
||||
get: function () {
|
||||
return this.group
|
||||
},
|
||||
set: function (group) {
|
||||
this.group = group
|
||||
}
|
||||
}
|
||||
|
||||
const _initXoObjectInput = function () {
|
||||
if (this.model === undefined) {
|
||||
this.model = []
|
||||
}
|
||||
if (!Array.isArray(this.model)) {
|
||||
this.model = [this.model]
|
||||
}
|
||||
this.intraModel = map(this.model, value => find(this.objects, object => object.id === value) || value)
|
||||
}
|
||||
|
||||
const _exportRemove = function (removedItem) {
|
||||
remove(this.model, item => item === reduceXoObject(removedItem))
|
||||
}
|
||||
|
||||
const _exportSelect = function (addedItem) {
|
||||
const addOn = reduceXoObject(addedItem)
|
||||
if (!find(this.model, item => item === addOn)) {
|
||||
this.model.push(addOn)
|
||||
}
|
||||
}
|
||||
|
||||
export default angular.module('xoWebApp.taskscheduler.job', [
|
||||
uiRouter,
|
||||
uiBootstrap
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('taskscheduler.job', {
|
||||
url: '/job/:id',
|
||||
controller: 'JobCtrl as ctrl',
|
||||
template: view
|
||||
})
|
||||
})
|
||||
|
||||
.controller('JobCtrl', function ($scope, xo, xoApi, notify, $stateParams) {
|
||||
this.scheduleApi = {}
|
||||
this.formData = {}
|
||||
this.running = {}
|
||||
this.ready = false
|
||||
|
||||
let comesForEditing = $stateParams.id
|
||||
|
||||
this.resetData = () => {
|
||||
this.formData = {}
|
||||
}
|
||||
this.resetForm = () => {
|
||||
this.resetData()
|
||||
this.editedJobId = undefined
|
||||
this.jobName = undefined
|
||||
this.selectedAction = undefined
|
||||
}
|
||||
this.resetForm()
|
||||
|
||||
$scope.$watch(() => this.selectedAction, newAction => actionGroup.set(newAction && newAction.group))
|
||||
|
||||
const loadActions = () => xoApi.call('system.getMethodsInfo')
|
||||
.then(response => {
|
||||
const actions = []
|
||||
|
||||
for (let method in response) {
|
||||
if (includes(jobCompliantMethods, method)) {
|
||||
let [group, command] = method.split('.')
|
||||
response[method].properties = response[method].params
|
||||
response[method].type = 'object'
|
||||
delete response[method].params
|
||||
actions.push({
|
||||
method,
|
||||
group,
|
||||
command,
|
||||
info: response[method]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.actions = actions
|
||||
this.ready = true
|
||||
})
|
||||
|
||||
const loadJobs = () => xo.job.getAll().then(jobs => {
|
||||
const j = {}
|
||||
forEach(jobs, job => {
|
||||
if (job.key === JOB_KEY) {
|
||||
j[job.id] = job
|
||||
}
|
||||
})
|
||||
this.jobs = j
|
||||
})
|
||||
|
||||
const refresh = () => loadJobs()
|
||||
const getReady = () => loadActions().then(refresh).then(() => this.ready = true)
|
||||
getReady().then(() => {
|
||||
if (comesForEditing) {
|
||||
this.edit(comesForEditing)
|
||||
comesForEditing = undefined
|
||||
}
|
||||
})
|
||||
|
||||
const saveNew = (name, action, data) => {
|
||||
const job = {
|
||||
type: 'call',
|
||||
name,
|
||||
key: JOB_KEY,
|
||||
method: action.method,
|
||||
paramsVector: {
|
||||
type: 'crossProduct',
|
||||
items: dataToParamVectorItems(action.info.properties, data)
|
||||
}
|
||||
}
|
||||
return xo.job.create(job)
|
||||
}
|
||||
|
||||
const save = (id, name, action, data) => {
|
||||
const job = this.jobs[id]
|
||||
job.name = name
|
||||
job.method = action.method
|
||||
job.paramsVector = {
|
||||
type: 'crossProduct',
|
||||
items: dataToParamVectorItems(action.info.properties, data)
|
||||
}
|
||||
return xo.job.set(job)
|
||||
}
|
||||
|
||||
this.save = (id, name, action, data) => {
|
||||
const dataClone = cleanUpData(cloneDeep(data))
|
||||
const saved = (id !== undefined) ? save(id, name, action, dataClone) : saveNew(name, action, dataClone)
|
||||
return saved
|
||||
.then(() => this.resetForm())
|
||||
.finally(refresh)
|
||||
}
|
||||
|
||||
this.edit = id => {
|
||||
this.resetForm()
|
||||
try {
|
||||
const job = this.jobs[id]
|
||||
if (job) {
|
||||
this.editedJobId = id
|
||||
this.jobName = job.name
|
||||
this.selectedAction = find(this.actions, action => action.method === job.method)
|
||||
const data = {}
|
||||
const paramsVector = job.paramsVector
|
||||
if (paramsVector) {
|
||||
if (paramsVector.type !== 'crossProduct') {
|
||||
throw new Error(`Unknown parameter-vector type ${paramsVector.type}`)
|
||||
}
|
||||
forEach(paramsVector.items, item => {
|
||||
if (item.type !== 'set') {
|
||||
throw new Error(`Unknown parameter-vector item type ${item.type}`)
|
||||
}
|
||||
if (item.values.length === 1) {
|
||||
assign(data, item.values[0])
|
||||
} else {
|
||||
forEach(item.values, valueItem => {
|
||||
forEach(valueItem, (value, key) => {
|
||||
if (data[key] === undefined) {
|
||||
data[key] = []
|
||||
}
|
||||
data[key].push(value)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
this.formData = data
|
||||
}
|
||||
} catch (error) {
|
||||
this.resetForm()
|
||||
notify.error({
|
||||
title: 'Unhandled Job',
|
||||
message: error.message
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.delete = id => xo.job.delete(id).then(refresh).then(() => {
|
||||
if (id === this.editedJobId) {
|
||||
this.resetForm()
|
||||
}
|
||||
})
|
||||
|
||||
this.run = id => {
|
||||
this.running[id] = true
|
||||
notify.info({
|
||||
title: 'Run Job',
|
||||
message: 'One shot running started. See overview for logs.'
|
||||
})
|
||||
return xo.job.runSequence([id]).finally(() => delete this.running[id])
|
||||
}
|
||||
})
|
||||
|
||||
.directive('stringInput', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
model: '=',
|
||||
form: '=',
|
||||
key: '=',
|
||||
property: '='
|
||||
},
|
||||
bindToController: true,
|
||||
controller: function () {
|
||||
this.isRequired = () => isRequired(this.property)
|
||||
this.active = () => getType(this.property) === 'string' && !includes(['id', 'host', 'host_id', 'target_host_id', 'sr', 'target_sr_id', 'vm', 'pool', 'subject', 'object', 'action'], this.key)
|
||||
},
|
||||
controllerAs: 'ctrl',
|
||||
template: stringInputView
|
||||
}
|
||||
})
|
||||
|
||||
.directive('booleanInput', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
model: '=',
|
||||
form: '=',
|
||||
key: '=',
|
||||
property: '='
|
||||
},
|
||||
bindToController: true,
|
||||
controller: function () {
|
||||
this.isRequired = () => isRequired(this.property)
|
||||
this.active = () => getType(this.property) === 'boolean'
|
||||
},
|
||||
controllerAs: 'ctrl',
|
||||
template: booleanInputView
|
||||
}
|
||||
})
|
||||
|
||||
.directive('integerInput', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
model: '=',
|
||||
form: '=',
|
||||
key: '=',
|
||||
property: '='
|
||||
},
|
||||
bindToController: true,
|
||||
controller: function () {
|
||||
this.isRequired = () => isRequired(this.property)
|
||||
this.active = () => getType(this.property) === 'integer'
|
||||
},
|
||||
controllerAs: 'ctrl',
|
||||
template: integerInputView
|
||||
}
|
||||
})
|
||||
|
||||
.directive('numberInput', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
model: '=',
|
||||
form: '=',
|
||||
key: '=',
|
||||
property: '='
|
||||
},
|
||||
bindToController: true,
|
||||
controller: function () {
|
||||
this.isRequired = () => isRequired(this.property)
|
||||
this.active = () => getType(this.property) === 'number'
|
||||
},
|
||||
controllerAs: 'ctrl',
|
||||
template: numberInputView
|
||||
}
|
||||
})
|
||||
|
||||
.directive('arrayInput', function ($compile) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
model: '=',
|
||||
form: '=',
|
||||
key: '=',
|
||||
property: '='
|
||||
},
|
||||
controller: 'ArrayInput as ctrl',
|
||||
bindToController: true,
|
||||
link: function (scope, element, attrs) {
|
||||
const updateElement = () => {
|
||||
if (scope.ctrl.property.items) {
|
||||
element.append(arrayInputView)
|
||||
}
|
||||
$compile(element.contents())(scope)
|
||||
}
|
||||
|
||||
updateElement()
|
||||
}
|
||||
}
|
||||
})
|
||||
.controller('ArrayInput', function ($scope) {
|
||||
this.isRequired = () => false
|
||||
this.getType = getType
|
||||
this.active = () => getType(this.property) === 'array'
|
||||
this.add = value => {
|
||||
const type = getType(this.property.items)
|
||||
switch (type) {
|
||||
case 'boolean':
|
||||
value = Boolean(value)
|
||||
break
|
||||
case 'string':
|
||||
value = trim(value)
|
||||
break
|
||||
}
|
||||
this.model.push(value)
|
||||
}
|
||||
this.remove = index => this.model.splice(index, 1)
|
||||
const init = () => {
|
||||
if (this.model === undefined || this.model === null) {
|
||||
this.model = []
|
||||
}
|
||||
}
|
||||
|
||||
if (this.active()) {
|
||||
init()
|
||||
if (!Array.isArray(this.model)) {
|
||||
throw new Error('arrayInput directive model must be an array')
|
||||
}
|
||||
$scope.$watch(() => this.model, init)
|
||||
}
|
||||
})
|
||||
|
||||
.directive('objectInput', function ($compile) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
model: '=',
|
||||
form: '=',
|
||||
key: '=',
|
||||
property: '='
|
||||
},
|
||||
controller: 'ObjectInput as ctrl',
|
||||
bindToController: true,
|
||||
link: function (scope, element, attrs) {
|
||||
const updateElement = () => {
|
||||
if (scope.ctrl.property.properties) {
|
||||
element.append(objectInputView)
|
||||
}
|
||||
$compile(element.contents())(scope)
|
||||
}
|
||||
|
||||
updateElement()
|
||||
}
|
||||
}
|
||||
})
|
||||
.controller('ObjectInput', function ($scope) {
|
||||
this.isRequired = () => isRequired(this.property)
|
||||
this.active = () => getType(this.property) === 'object' && (this.key !== 'object' || actionGroup.get() !== 'acl')
|
||||
const init = () => {
|
||||
if (this.model === undefined || this.model === null) {
|
||||
this.model = {
|
||||
__use: this.isRequired()
|
||||
}
|
||||
}
|
||||
if (typeof this.model !== 'object' || Array.isArray(this.model)) {
|
||||
throw new Error('objectInput directive model must be a plain object')
|
||||
}
|
||||
const use = this.model.__use
|
||||
delete this.model.__use
|
||||
this.model.__use = Object.keys(this.model).length > 0 || use
|
||||
forEach(this.property.properties, (property, key) => {
|
||||
if (getType(property) === 'boolean') {
|
||||
this.model[key] = Boolean(this.model[key])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (this.active()) {
|
||||
init()
|
||||
$scope.$watch(() => this.model, (newVal, oldVal) => {
|
||||
if (newVal !== oldVal) {
|
||||
init()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
.directive('vmInput', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
form: '=',
|
||||
key: '=',
|
||||
property: '=',
|
||||
model: '='
|
||||
},
|
||||
controller: 'VmInput as ctrl',
|
||||
bindToController: true,
|
||||
template: vmInputView
|
||||
}
|
||||
})
|
||||
.controller('VmInput', function ($scope, xoApi) {
|
||||
this.objects = xoApi.all
|
||||
this.isRequired = () => isRequired(this.property)
|
||||
this.active = () => getType(this.property) === 'string' && (this.key === 'vm' || (actionGroup.get() === 'vm' && this.key === 'id'))
|
||||
|
||||
this.init = _initXoObjectInput
|
||||
this.exportRemove = _exportRemove
|
||||
this.exportSelect = _exportSelect
|
||||
|
||||
if (this.active()) {
|
||||
this.init()
|
||||
$scope.$watch(() => this.model, (newVal, oldVal) => {
|
||||
if (newVal !== oldVal) {
|
||||
this.init()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
.directive('hostInput', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
form: '=',
|
||||
key: '=',
|
||||
property: '=',
|
||||
model: '='
|
||||
},
|
||||
controller: 'HostInput as ctrl',
|
||||
bindToController: true,
|
||||
template: hostInputView
|
||||
}
|
||||
})
|
||||
.controller('HostInput', function ($scope, xoApi) {
|
||||
this.objects = xoApi.all
|
||||
this.isRequired = () => isRequired(this.property)
|
||||
this.active = () => getType(this.property) === 'string' && (includes(['host', 'host_id', 'target_host_id'], this.key) || (actionGroup.get() === 'host' && this.key === 'id'))
|
||||
|
||||
this.init = _initXoObjectInput
|
||||
this.exportRemove = _exportRemove
|
||||
this.exportSelect = _exportSelect
|
||||
|
||||
if (this.active()) {
|
||||
this.init()
|
||||
$scope.$watch(() => this.model, (newVal, oldVal) => {
|
||||
if (newVal !== oldVal) {
|
||||
this.init()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
.directive('srInput', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
form: '=',
|
||||
key: '=',
|
||||
property: '=',
|
||||
model: '='
|
||||
},
|
||||
controller: 'SrInput as ctrl',
|
||||
bindToController: true,
|
||||
template: srInputView
|
||||
}
|
||||
})
|
||||
.controller('SrInput', function ($scope, xoApi) {
|
||||
this.objects = xoApi.all
|
||||
this.isRequired = () => isRequired(this.property)
|
||||
this.active = () => getType(this.property) === 'string' && includes(['sr', 'sr_id', 'target_sr_id'], this.key)
|
||||
|
||||
this.init = _initXoObjectInput
|
||||
this.exportRemove = _exportRemove
|
||||
this.exportSelect = _exportSelect
|
||||
|
||||
if (this.active()) {
|
||||
this.init()
|
||||
$scope.$watch(() => this.model, (newVal, oldVal) => {
|
||||
if (newVal !== oldVal) {
|
||||
this.init()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
.directive('poolInput', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
form: '=',
|
||||
key: '=',
|
||||
property: '=',
|
||||
model: '='
|
||||
},
|
||||
controller: 'PoolInput as ctrl',
|
||||
bindToController: true,
|
||||
template: poolInputView
|
||||
}
|
||||
})
|
||||
.controller('PoolInput', function ($scope, xoApi) {
|
||||
this.objects = xoApi.all
|
||||
this.isRequired = () => isRequired(this.property)
|
||||
this.active = () => getType(this.property) === 'string' && includes(['pool', 'pool_id', 'target_pool_id'], this.key)
|
||||
|
||||
this.init = _initXoObjectInput
|
||||
this.exportRemove = _exportRemove
|
||||
this.exportSelect = _exportSelect
|
||||
|
||||
if (this.active()) {
|
||||
this.init()
|
||||
$scope.$watch(() => this.model, (newVal, oldVal) => {
|
||||
if (newVal !== oldVal) {
|
||||
this.init()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
.directive('xoEntityInput', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
form: '=',
|
||||
key: '=',
|
||||
property: '=',
|
||||
model: '='
|
||||
},
|
||||
controller: 'XoEntityInput as ctrl',
|
||||
bindToController: true,
|
||||
template: xoEntityInputView
|
||||
}
|
||||
})
|
||||
.controller('XoEntityInput', function ($scope, xo) {
|
||||
this.ready = false
|
||||
this.isRequired = () => isRequired(this.property)
|
||||
this.active = () => this.ready && getType(this.property) === 'string' && this.key === 'subject' && actionGroup.get() === 'acl'
|
||||
|
||||
this.init = _initXoObjectInput
|
||||
this.exportRemove = _exportRemove
|
||||
this.exportSelect = _exportSelect
|
||||
|
||||
Bluebird.props({
|
||||
users: xo.user.getAll(),
|
||||
groups: xo.group.getAll()
|
||||
})
|
||||
.then(p => {
|
||||
this.objects = p.users.concat(p.groups)
|
||||
this.ready = true
|
||||
if (this.active()) {
|
||||
this.init()
|
||||
$scope.$watch(() => this.model, (newVal, oldVal) => {
|
||||
if (newVal !== oldVal) {
|
||||
this.init()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
.directive('xoRoleInput', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
form: '=',
|
||||
key: '=',
|
||||
property: '=',
|
||||
model: '='
|
||||
},
|
||||
controller: 'XoRoleInput as ctrl',
|
||||
bindToController: true,
|
||||
template: xoRoleInputView
|
||||
}
|
||||
})
|
||||
.controller('XoRoleInput', function ($scope, xo) {
|
||||
this.ready = false
|
||||
this.isRequired = () => isRequired(this.property)
|
||||
this.active = () => this.ready && getType(this.property) === 'string' && this.key === 'action' && actionGroup.get() === 'acl'
|
||||
|
||||
this.init = _initXoObjectInput
|
||||
this.exportRemove = _exportRemove
|
||||
this.exportSelect = _exportSelect
|
||||
|
||||
xo.role.getAll()
|
||||
.then(roles => {
|
||||
this.objects = roles
|
||||
this.ready = true
|
||||
if (this.active()) {
|
||||
this.init()
|
||||
$scope.$watch(() => this.model, (newVal, oldVal) => {
|
||||
if (newVal !== oldVal) {
|
||||
this.init()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
.directive('xoObjectInput', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
form: '=',
|
||||
key: '=',
|
||||
property: '=',
|
||||
model: '='
|
||||
},
|
||||
controller: 'XoObjectInput as ctrl',
|
||||
bindToController: true,
|
||||
template: xoObjectInputView
|
||||
}
|
||||
})
|
||||
.controller('XoObjectInput', function ($scope, xoApi, filterFilter, selectHighLevelFilter) {
|
||||
const HIGH_LEVEL_OBJECTS = {
|
||||
pool: true,
|
||||
host: true,
|
||||
VM: true,
|
||||
SR: true,
|
||||
network: true
|
||||
}
|
||||
this.types = Object.keys(HIGH_LEVEL_OBJECTS)
|
||||
this.objects = xoApi.all
|
||||
|
||||
this.isRequired = () => isRequired(this.property)
|
||||
this.active = () => getType(this.property) === 'string' && this.key === 'object' && actionGroup.get() === 'acl'
|
||||
this.toggleType = (toggle, type) => {
|
||||
const selectedObjects = this.intraModel && this.intraModel.slice() || []
|
||||
if (toggle) {
|
||||
const objects = filterFilter(selectHighLevelFilter(this.objects), {type})
|
||||
forEach(objects, object => { selectedObjects.indexOf(object) === -1 && selectedObjects.push(object) })
|
||||
this.intraModel = selectedObjects
|
||||
} else {
|
||||
const keptObjects = []
|
||||
for (let index in selectedObjects) {
|
||||
const object = selectedObjects[index]
|
||||
if (object.type !== type) {
|
||||
keptObjects.push(object)
|
||||
}
|
||||
}
|
||||
this.intraModel = keptObjects
|
||||
}
|
||||
this.model.length = 0
|
||||
forEach(this.intraModel, item => this.model.push(reduceXoObject(item)))
|
||||
}
|
||||
|
||||
this.init = _initXoObjectInput
|
||||
this.exportRemove = _exportRemove
|
||||
this.exportSelect = _exportSelect
|
||||
|
||||
if (this.active()) {
|
||||
this.init()
|
||||
$scope.$watch(() => this.model, (newVal, oldVal) => {
|
||||
if (newVal !== oldVal) {
|
||||
this.init()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
// A module exports its name.
|
||||
.name
|
||||
6
app/modules/task-scheduler/job/integer-input-view.jade
Normal file
6
app/modules/task-scheduler/job/integer-input-view.jade
Normal file
@@ -0,0 +1,6 @@
|
||||
.form-group(ng-if = 'ctrl.active()')
|
||||
label.col-md-2.control-label(ng-if = 'ctrl.key')
|
||||
| {{ ctrl.key }}
|
||||
span.text-warning(ng-if = 'ctrl.isRequired()') *
|
||||
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
|
||||
input.form-control(form = '{{ ctrl.form }}', type = 'number', step = '1', ng-model = 'ctrl.model', ng-required = 'ctrl.isRequired()')
|
||||
6
app/modules/task-scheduler/job/number-input-view.jade
Normal file
6
app/modules/task-scheduler/job/number-input-view.jade
Normal file
@@ -0,0 +1,6 @@
|
||||
.form-group(ng-if = 'ctrl.active()')
|
||||
label.col-md-2.control-label(ng-if = 'ctrl.key')
|
||||
| {{ ctrl.key }}
|
||||
span.text-warning(ng-if = 'ctrl.isRequired()') *
|
||||
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
|
||||
input.form-control(form = '{{ ctrl.form }}', type = 'number', step = 'any', ng-model = 'ctrl.model', ng-required = 'ctrl.isRequired()')
|
||||
30
app/modules/task-scheduler/job/object-input-view.jade
Normal file
30
app/modules/task-scheduler/job/object-input-view.jade
Normal file
@@ -0,0 +1,30 @@
|
||||
.form-group(ng-if = 'ctrl.active()')
|
||||
label.col-md-2.control-label(ng-if = 'ctrl.key')
|
||||
| {{ ctrl.key }}
|
||||
span.text-warning(ng-if = 'ctrl.isRequired()') *
|
||||
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
|
||||
.checkbox(ng-if = '!ctrl.isRequired()')
|
||||
label
|
||||
input(type = 'checkbox', ng-model = 'ctrl.model.__use')
|
||||
| Fill informations (optional)
|
||||
hr
|
||||
.help-block(ng-if = 'ctrl.isRequired()')
|
||||
span.text-warning Fill required informations
|
||||
hr
|
||||
fieldset.form-horizontal(ng-disabled = '!ctrl.isRequired() && !ctrl.model.__use', ng-hide = '!ctrl.isRequired() && !ctrl.model.__use')
|
||||
.form-group(ng-if = '(ctrl.property.properties | count) < 1')
|
||||
p.help-block No parameters
|
||||
div(ng-repeat = '(key, property) in ctrl.property.properties')
|
||||
array-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
|
||||
boolean-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
|
||||
host-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
|
||||
integer-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
|
||||
number-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
|
||||
object-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
|
||||
pool-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
|
||||
sr-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
|
||||
string-input(form = 'ctrl.form', model = 'ctrl.model[key]', key = 'key', property = 'property')
|
||||
vm-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
|
||||
xo-entity-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
|
||||
xo-object-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
|
||||
xo-role-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
|
||||
15
app/modules/task-scheduler/job/pool-input-view.jade
Normal file
15
app/modules/task-scheduler/job/pool-input-view.jade
Normal file
@@ -0,0 +1,15 @@
|
||||
.form-group(ng-if = 'ctrl.active()')
|
||||
label.col-md-2.control-label(ng-if = 'ctrl.key')
|
||||
| Pools
|
||||
span.text-warning(ng-if = 'ctrl.isRequired()') *
|
||||
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
|
||||
ui-select(form = 'ctrl.form', ng-model = 'ctrl.intraModel', multiple, close-on-select = 'false', ng-required = 'ctrl.isRequired()', on-remove = 'ctrl.exportRemove($item)', on-select = 'ctrl.exportSelect($item)')
|
||||
ui-select-match(placeholder = 'Choose pools')
|
||||
i(class = 'xo-icon-{{$item.type | lowercase}}')
|
||||
| {{$item.name_label}}
|
||||
span(ng-if = '$item.$container') ({{ ($item.$container | resolve).name_label }})
|
||||
ui-select-choices(repeat = 'object in ctrl.objects | selectHighLevel | filter:{type: "pool"} | filter:$select.search | orderBy:["$container", "name_label"] track by object.id')
|
||||
div
|
||||
i(class = 'xo-icon-{{object.type | lowercase}}')
|
||||
| {{ object.name_label }}
|
||||
span(ng-if = 'object.$container') ({{ (object.$container | resolve).name_label || ((object.$container | resolve).master | resolve).name_label }})
|
||||
15
app/modules/task-scheduler/job/sr-input-view.jade
Normal file
15
app/modules/task-scheduler/job/sr-input-view.jade
Normal file
@@ -0,0 +1,15 @@
|
||||
.form-group(ng-if = 'ctrl.active()')
|
||||
label.col-md-2.control-label(ng-if = 'ctrl.key')
|
||||
| SRs
|
||||
span.text-warning(ng-if = 'ctrl.isRequired()') *
|
||||
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
|
||||
ui-select(form = 'ctrl.form', ng-model = 'ctrl.intraModel', multiple, close-on-select = 'false', ng-required = 'ctrl.isRequired()', on-remove = 'ctrl.exportRemove($item)', on-select = 'ctrl.exportSelect($item)')
|
||||
ui-select-match(placeholder = 'Choose Storage Repositories')
|
||||
i(class = 'xo-icon-{{$item.type | lowercase}}')
|
||||
| {{$item.name_label}}
|
||||
span(ng-if = '$item.$container') ({{ ($item.$container | resolve).name_label }})
|
||||
ui-select-choices(repeat = 'object in ctrl.objects | selectHighLevel | filter:{type: "sr"} | filter:$select.search | orderBy:["$container", "name_label"] track by object.id')
|
||||
div
|
||||
i(class = 'xo-icon-{{object.type | lowercase}}')
|
||||
| {{ object.name_label }}
|
||||
span(ng-if = 'object.$container') ({{ (object.$container | resolve).name_label || ((object.$container | resolve).master | resolve).name_label }})
|
||||
6
app/modules/task-scheduler/job/string-input-view.jade
Normal file
6
app/modules/task-scheduler/job/string-input-view.jade
Normal file
@@ -0,0 +1,6 @@
|
||||
.form-group(ng-if = 'ctrl.active()')
|
||||
label.col-md-2.control-label(ng-if = 'ctrl.key')
|
||||
| {{ ctrl.key }}
|
||||
span.text-warning(ng-if = 'ctrl.isRequired()') *
|
||||
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
|
||||
input.form-control(form = '{{ ctrl.form }}', type = 'text', ng-model = 'ctrl.model', ng-required = 'ctrl.isRequired()')
|
||||
68
app/modules/task-scheduler/job/view.jade
Normal file
68
app/modules/task-scheduler/job/view.jade
Normal file
@@ -0,0 +1,68 @@
|
||||
.grid
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.fa.fa-cogs
|
||||
| Jobs
|
||||
form#jobform(ng-submit = 'ctrl.save(ctrl.editedJobId, ctrl.jobName, ctrl.selectedAction, ctrl.formData)')
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-wrench
|
||||
| {{ ctrl.editedJobId ? "Edit" : "Create" }}
|
||||
.panel-body
|
||||
.alert.alert-warning(ng-if = 'ctrl.editedJobId') Editing Job ID: {{ ctrl.editedJobId }}
|
||||
fieldset.form-horizontal(ng-disabled = '!ctrl.ready')
|
||||
.form-group
|
||||
label.col-sm-2.control-label Job Name
|
||||
.col-sm-10
|
||||
input.form-control(form = 'jobform', type = 'text', ng-model = 'ctrl.jobName', required, placeholder = 'An explicit name for your job')
|
||||
.form-group
|
||||
label.col-sm-2.control-label {{ ctrl.selectedAction ? (ctrl.selectedAction.group + ".") : "Action" }}
|
||||
.col-sm-10
|
||||
select.form-control(form = 'jobform', ng-model = 'ctrl.selectedAction', ng-options = 'action.command group by action.group for action in ctrl.actions', ng-change = 'ctrl.resetData()', required)
|
||||
option(value = '') -- Choose an action --
|
||||
p.help-block(ng-if = 'ctrl.selectedAction.info.description') {{ ctrl.selectedAction.info.description }}
|
||||
.form-group(ng-if = 'ctrl.selectedAction.info.permission')
|
||||
label.col-sm-2.control-label Permission
|
||||
.col-sm-10: p.form-control-static {{ ctrl.selectedAction.info.permission }}
|
||||
fieldset.form-horizontal(ng-if = 'ctrl.selectedAction', ng-disabled = '!ctrl.ready')
|
||||
legend Parameters
|
||||
object-input(form = '"jobform"', property = 'ctrl.selectedAction.info', model = 'ctrl.formData')
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-body
|
||||
fieldset.center(ng-disabled = '!ctrl.ready')
|
||||
button.btn.btn-lg.btn-primary(form = 'jobform', type = 'submit')
|
||||
i.fa.fa-clock-o
|
||||
|
|
||||
i.fa.fa-arrow-right
|
||||
|
|
||||
i.fa.fa-database
|
||||
| Save
|
||||
|
|
||||
button.btn.btn-lg.btn-default(type = 'button', ng-click = 'ctrl.resetForm()')
|
||||
| Reset
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-list-ul
|
||||
| Jobs
|
||||
.panel-body
|
||||
.text-center(ng-if = '!(ctrl.jobs | count)') No jobs found
|
||||
table.table(ng-if = 'ctrl.jobs | count')
|
||||
tr
|
||||
th Name
|
||||
th Action
|
||||
th
|
||||
tr(ng-repeat = 'job in ctrl.jobs | map | orderBy:"name" track by job.id')
|
||||
td
|
||||
| {{ job.name }} 
|
||||
span.text-muted.hidden-xs ({{ job.id }})
|
||||
td {{ job.method }}
|
||||
td
|
||||
span.pull-left
|
||||
button.btn.btn-warning(type = 'button', ng-click = 'ctrl.run(job.id)', ng-disabled = 'ctrl.running[job.id]'): i.fa.fa-play
|
||||
span.pull-right
|
||||
button.btn.btn-primary(type = 'button', ng-click = 'ctrl.edit(job.id)'): i.fa.fa-pencil
|
||||
|
|
||||
button.btn.btn-danger(type = 'button', ng-click = 'ctrl.delete(job.id)'): i.fa.fa-trash
|
||||
17
app/modules/task-scheduler/job/vm-input-view.jade
Normal file
17
app/modules/task-scheduler/job/vm-input-view.jade
Normal file
@@ -0,0 +1,17 @@
|
||||
.form-group(ng-if = 'ctrl.active()')
|
||||
label.col-md-2.control-label(ng-if = 'ctrl.key')
|
||||
| VMs
|
||||
span.text-warning(ng-if = 'ctrl.isRequired()') *
|
||||
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
|
||||
ui-select(form = 'ctrl.form', ng-model = 'ctrl.intraModel', multiple, close-on-select = 'false', ng-required = 'ctrl.isRequired()', on-remove = 'ctrl.exportRemove($item)', on-select = 'ctrl.exportSelect($item)')
|
||||
ui-select-match(placeholder = 'Choose VMs')
|
||||
i.xo-icon-working(ng-if = 'isVMWorking($item)')
|
||||
i(ng-class = '"xo-icon-" + ($item.power_state | lowercase)', ng-if = '!isVMWorking($item)')
|
||||
| {{ $item.name_label }}
|
||||
span(ng-if = '$item.$container') ({{ ($item.$container | resolve).name_label }})
|
||||
ui-select-choices(repeat = 'vm in ctrl.objects | selectHighLevel | filter:{type: "vm"} | filter:$select.search | orderBy:["$container", "name_label"] track by vm.id')
|
||||
div
|
||||
i.xo-icon-working(ng-if = 'isVMWorking(vm)', tooltip = '{{ vm.power_state }} and {{ (vm.current_operations | map)[0] }}')
|
||||
i(ng-class = '"xo-icon-" + (vm.power_state | lowercase)', ng-if = '!isVMWorking(vm)', tooltip = '{{ vm.power_state }}')
|
||||
| {{ vm.name_label }}
|
||||
span(ng-if = 'vm.$container') ({{ (vm.$container | resolve).name_label || ((vm.$container | resolve).master | resolve).name_label }})
|
||||
21
app/modules/task-scheduler/job/xo-entity-input-view.jade
Normal file
21
app/modules/task-scheduler/job/xo-entity-input-view.jade
Normal file
@@ -0,0 +1,21 @@
|
||||
.form-group(ng-if = 'ctrl.active()')
|
||||
label.col-md-2.control-label(ng-if = 'ctrl.key')
|
||||
| Users / Groups
|
||||
span.text-warning(ng-if = 'ctrl.isRequired()') *
|
||||
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
|
||||
ui-select(ng-model = 'ctrl.intraModel', multiple, close-on-select = 'false', ng-required = 'ctrl.isRequired()', on-remove = 'ctrl.exportRemove($item)', on-select = 'ctrl.exportSelect($item)')
|
||||
ui-select-match(placeholder = 'Choose users or groups')
|
||||
span(ng-if = '$item.email')
|
||||
i.xo-icon-user.fa-fw
|
||||
| {{$item.email}}
|
||||
span(ng-if = '$item.name')
|
||||
i.xo-icon-group.fa-fw
|
||||
| {{$item.name}}
|
||||
ui-select-choices(repeat = 'entity in ctrl.objects | filter:{ permission: "!admin" } | filter:$select.search')
|
||||
div
|
||||
span(ng-if = 'entity.email')
|
||||
i.xo-icon-user.fa-fw
|
||||
| {{entity.email}}
|
||||
span(ng-if = 'entity.name')
|
||||
i.xo-icon-group.fa-fw
|
||||
| {{entity.name}}
|
||||
28
app/modules/task-scheduler/job/xo-object-input-view.jade
Normal file
28
app/modules/task-scheduler/job/xo-object-input-view.jade
Normal file
@@ -0,0 +1,28 @@
|
||||
.form-group(ng-if = 'ctrl.active()')
|
||||
label.col-md-2.control-label(ng-if = 'ctrl.key')
|
||||
| Objects
|
||||
span.text-warning(ng-if = 'ctrl.isRequired()') *
|
||||
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
|
||||
ui-select(ng-model = 'ctrl.intraModel', multiple, close-on-select = 'false', ng-required = 'ctrl.isRequired()', on-remove = 'ctrl.exportRemove($item)', on-select = 'ctrl.exportSelect($item)')
|
||||
ui-select-match(placeholder = 'Choose an object')
|
||||
i(class = 'xo-icon-{{$item.type | lowercase}}')
|
||||
| {{$item.name_label}}
|
||||
span(ng-if="($item.type === 'SR' || $item.type === 'VM') && $item.$container")
|
||||
| ({{($item.$container | resolve).name_label}})
|
||||
span(ng-if="$item.type === 'network'")
|
||||
| ({{($item.$poolId | resolve).name_label}})
|
||||
ui-select-choices(repeat = 'object in ctrl.objects | selectHighLevel | filter:$select.search | orderBy:["type", "name_label"]')
|
||||
div
|
||||
i(class = 'xo-icon-{{object.type | lowercase}}')
|
||||
| {{object.name_label}}
|
||||
span(ng-if="(object.type === 'SR' || object.type === 'VM') && object.$container")
|
||||
| ({{(object.$container | resolve).name_label}})
|
||||
span(ng-if="object.type === 'network'")
|
||||
| ({{(object.$poolId | resolve).name_label}})
|
||||
.text-center
|
||||
span(ng-repeat = 'type in ctrl.types')
|
||||
label(tooltip = 'select/deselect all {{type}}s', style = 'cursor: pointer')
|
||||
input.hidden(type = 'checkbox', ng-model = 'selectedTypes[type]', ng-change = 'ctrl.toggleType(selectedTypes[type], type)')
|
||||
span.fa-stack
|
||||
i(class = 'xo-icon-{{type | lowercase}}').fa-stack-1x
|
||||
i.fa.fa-square-o.fa-stack-2x.text-info(ng-if = 'selectedTypes[type]')
|
||||
13
app/modules/task-scheduler/job/xo-role-input-view.jade
Normal file
13
app/modules/task-scheduler/job/xo-role-input-view.jade
Normal file
@@ -0,0 +1,13 @@
|
||||
.form-group(ng-if = 'ctrl.active()')
|
||||
label.col-md-2.control-label(ng-if = 'ctrl.key')
|
||||
| Roles
|
||||
span.text-warning(ng-if = 'ctrl.isRequired()') *
|
||||
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
|
||||
ui-select(ng-model = 'ctrl.intraModel', multiple, close-on-select = 'false', ng-required = 'ctrl.isRequired()', on-remove = 'ctrl.exportRemove($item)', on-select = 'ctrl.exportSelect($item)')
|
||||
ui-select-match(placeholder = 'Choose a role')
|
||||
i(class = 'xo-icon-{{$item.type | lowercase}}')
|
||||
| {{$item.name}}
|
||||
ui-select-choices(repeat = 'role in ctrl.objects | filter:$select.search | orderBy:"name"')
|
||||
div
|
||||
i(class = 'xo-icon-{{role.type | lowercase}}')
|
||||
| {{role.name}}
|
||||
193
app/modules/task-scheduler/overview/index.js
Normal file
193
app/modules/task-scheduler/overview/index.js
Normal file
@@ -0,0 +1,193 @@
|
||||
import angular from 'angular'
|
||||
import filter from 'lodash.filter'
|
||||
import forEach from 'lodash.foreach'
|
||||
import prettyCron from 'prettycron'
|
||||
import uiBootstrap from 'angular-ui-bootstrap'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
|
||||
import view from './view'
|
||||
|
||||
// ====================================================================
|
||||
|
||||
const JOB_KEY = 'genericTask'
|
||||
|
||||
export default angular.module('taskscheduler.overview', [
|
||||
uiRouter,
|
||||
uiBootstrap
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('taskscheduler.overview', {
|
||||
url: '/overview',
|
||||
controller: 'OverviewCtrl as ctrl',
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('OverviewCtrl', function (
|
||||
$interval,
|
||||
$scope,
|
||||
$state,
|
||||
$stateParams,
|
||||
filterFilter,
|
||||
modal,
|
||||
notify,
|
||||
selectHighLevelFilter,
|
||||
xo,
|
||||
xoApi
|
||||
) {
|
||||
this.running = {}
|
||||
|
||||
this.currentLogPage = 1
|
||||
this.logPageSize = 10
|
||||
|
||||
const refreshSchedules = () => {
|
||||
xo.schedule.getAll()
|
||||
.then(schedules => this.schedules = filter(schedules, schedule => this.jobs[schedule.job] && this.jobs[schedule.job].key === JOB_KEY))
|
||||
xo.scheduler.getScheduleTable()
|
||||
.then(table => this.scheduleTable = table)
|
||||
}
|
||||
|
||||
const getLogs = () => {
|
||||
xo.logs.get('jobs').then(logs => {
|
||||
const viewLogs = {}
|
||||
const logsToClear = []
|
||||
forEach(logs, (log, logKey) => {
|
||||
const data = log.data
|
||||
const [time] = logKey.split(':')
|
||||
if (data.event === 'job.start' && data.key === JOB_KEY) {
|
||||
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.displayScheduleJobName = schedule => this.jobs[schedule.job] && this.jobs[schedule.job].name
|
||||
|
||||
this.collectionLength = col => Object.keys(col).length
|
||||
this.working = {}
|
||||
})
|
||||
|
||||
// A module exports its name.
|
||||
.name
|
||||
87
app/modules/task-scheduler/overview/view.jade
Normal file
87
app/modules/task-scheduler/overview/view.jade
Normal file
@@ -0,0 +1,87 @@
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.fa.fa-eye
|
||||
| Job Scheduling Overview
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-clock-o
|
||||
| Schedules
|
||||
.panel-body
|
||||
//- The 2 tables below are here for a "full-width" effect of the content vs the menu (cf sheduler/view.jade)
|
||||
table.table(ng-if = '!ctrl.schedules')
|
||||
tr
|
||||
td.text-center: i.xo-icon-loading
|
||||
table.table(ng-if = 'ctrl.schedules && !ctrl.collectionLength(ctrl.schedules)')
|
||||
tr
|
||||
td.text-center No scheduled jobs
|
||||
table.table.table-hover(ng-if = 'ctrl.schedules && ctrl.collectionLength(ctrl.schedules)')
|
||||
tr
|
||||
th Name
|
||||
th Job
|
||||
th.hidden-xs Scheduling
|
||||
th State
|
||||
tr(ng-repeat = 'schedule in ctrl.schedules | orderBy:"id":true track by schedule.id')
|
||||
td: a(ui-sref = 'taskscheduler.schedule({id: schedule.id})') {{ schedule.name || schedule.id }}
|
||||
td
|
||||
a(ui-sref = 'taskscheduler.job({id: schedule.job})') {{ ctrl.displayScheduleJobName(schedule) || schedule.job }}
|
||||
td.hidden-xs {{ ctrl.prettyCron(schedule.cron) }}
|
||||
td
|
||||
span.label.label-success.hidden-xs(ng-if = 'ctrl.scheduleTable[schedule.id] === true') enabled
|
||||
span.label.label-default.hidden-xs(ng-if = 'ctrl.scheduleTable[schedule.id] === false') disabled
|
||||
span.label.label-warning.hidden-xs(ng-if = 'ctrl.scheduleTable[schedule.id] === undefined') unknown
|
||||
fieldset.pull-right(ng-disabled = 'ctrl.working[schedule.id]')
|
||||
button.btn(ng-if = 'ctrl.scheduleTable[schedule.id] === false', type = 'button', ng-click = 'ctrl.enable(schedule.id)'): i.fa.fa-toggle-off
|
||||
button.btn.btn-success(ng-if = 'ctrl.scheduleTable[schedule.id] === true', type = 'button', ng-click = 'ctrl.disable(schedule.id)'): i.fa.fa-toggle-on
|
||||
|
|
||||
button.btn.btn-warning(type = 'button', ng-click = 'ctrl.run(schedule)', ng-disabled = 'ctrl.running[schedule.id]'): i.fa.fa-play
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-file-text
|
||||
| Logs
|
||||
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 Start
|
||||
th End
|
||||
th Duration
|
||||
th Status
|
||||
tbody(ng-repeat = 'log in ctrl.logs | map | filter:ctrl.logSearch | orderBy:"-time" | slice:(ctrl.logPageSize * (ctrl.currentLogPage - 1)):(ctrl.logPageSize * ctrl.currentLogPage) track by log.logKey')
|
||||
tr
|
||||
td
|
||||
button.btn.btn-sm(type = 'button', tooltip = 'See calls', ng-click = 'seeCalls = !seeCalls', ng-class = '{"btn-default": !log.hasErrors, "btn-danger": log.hasErrors}'): i.fa(ng-class = '{"fa-caret-down": !seeCalls, "fa-caret-up": seeCalls}')
|
||||
| {{ log.jobId }}
|
||||
td {{ log.start | date:'medium' }}
|
||||
td {{ log.end | date:'medium' }}
|
||||
td {{ log.duration | duration}}
|
||||
td
|
||||
span(ng-if = 'log.status === "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') {{ log.error }}
|
||||
tr.bg-info(collapse = '!seeCalls')
|
||||
td(colspan = '5')
|
||||
ul.list-group
|
||||
li.list-group-item(ng-repeat = 'call in log.calls | map | orderBy:"-time" track by call.callKey')
|
||||
strong.text-info {{ call.method }}: 
|
||||
span(ng-repeat = '(key, param) in call.params')
|
||||
| {{ key }}:
|
||||
strong {{ param }} 
|
||||
span(ng-if = 'call.returnedValue')
|
||||
|  
|
||||
i.text-primary.fa.fa-arrow-right
|
||||
|  {{ call.returnedValue }}
|
||||
span.text-danger(ng-if = 'call.error')
|
||||
|  
|
||||
i.fa.fa-times
|
||||
|  {{ call.error }}
|
||||
.form-inline
|
||||
.input-group
|
||||
.input-group-addon: i.fa.fa-search
|
||||
input.form-control(type = 'text', ng-model = 'ctrl.logSearch', placeholder = 'Search logs...')
|
||||
.center(ng-if = '(ctrl.logs | map | filter:ctrl.logSearch | count) > ctrl.logPageSize || currentLogPage > 1')
|
||||
pagination.pagination-sm(boundary-links = 'true', total-items = 'ctrl.logs | map | filter:ctrl.logSearch | count', ng-model = 'ctrl.currentLogPage', items-per-page = 'ctrl.logPageSize', max-size = '10', previous-text = '<', next-text = '>', first-text = '<<', last-text = '>>')
|
||||
115
app/modules/task-scheduler/schedule/index.js
Normal file
115
app/modules/task-scheduler/schedule/index.js
Normal file
@@ -0,0 +1,115 @@
|
||||
import angular from 'angular'
|
||||
import Bluebird from 'bluebird'
|
||||
import find from 'lodash.find'
|
||||
import forEach from 'lodash.foreach'
|
||||
import prettyCron from 'prettycron'
|
||||
import uiBootstrap from 'angular-ui-bootstrap'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
Bluebird.longStackTraces()
|
||||
|
||||
import view from './view'
|
||||
|
||||
// ====================================================================
|
||||
|
||||
const JOB_KEY = 'genericTask'
|
||||
|
||||
export default angular.module('xoWebApp.taskscheduler.schedule', [
|
||||
uiRouter,
|
||||
uiBootstrap
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('taskscheduler.schedule', {
|
||||
url: '/schedule/:id',
|
||||
controller: 'ScheduleCtrl as ctrl',
|
||||
template: view
|
||||
})
|
||||
})
|
||||
|
||||
.controller('ScheduleCtrl', function (xo, xoApi, notify, $stateParams) {
|
||||
this.scheduleApi = {}
|
||||
this.formData = {}
|
||||
this.ready = false
|
||||
this.running = {}
|
||||
let comesForEditing = $stateParams.id
|
||||
|
||||
this.reset = () => {
|
||||
this.formData.editedScheduleId = undefined
|
||||
this.formData.scheduleName = undefined
|
||||
this.formData.selectedJob = undefined
|
||||
this.formData.enabled = false
|
||||
this.scheduleApi && this.scheduleApi.resetData && this.scheduleApi.resetData()
|
||||
}
|
||||
|
||||
this.reset()
|
||||
|
||||
const refreshJobs = () => xo.job.getAll().then(jobs => {
|
||||
const j = {}
|
||||
forEach(jobs, job => {
|
||||
if (job.key === JOB_KEY) {
|
||||
j[job.id] = job
|
||||
}
|
||||
})
|
||||
this.jobs = j
|
||||
})
|
||||
|
||||
const refreshSchedules = () => xo.schedule.getAll().then(schedules => {
|
||||
const s = {}
|
||||
forEach(schedules, schedule => {
|
||||
if (this.jobs && this.jobs[schedule.job] && (this.jobs[schedule.job].key === JOB_KEY)) {
|
||||
s[schedule.id] = schedule
|
||||
}
|
||||
})
|
||||
this.schedules = s
|
||||
})
|
||||
|
||||
const refresh = () => refreshJobs().then(refreshSchedules)
|
||||
const getReady = () => refresh().then(() => this.ready = true)
|
||||
getReady().then(() => {
|
||||
if (comesForEditing) {
|
||||
this.edit(comesForEditing)
|
||||
comesForEditing = undefined
|
||||
}
|
||||
})
|
||||
|
||||
const saveNew = (name, job, cron, enabled) => xo.schedule.create(job.id, cron, enabled, name)
|
||||
const save = (id, name, job, cron) => xo.schedule.set(id, job.id, cron, undefined, name)
|
||||
|
||||
this.save = (id, name, job, cron, enabled) => {
|
||||
const saved = (id !== undefined) ? save(id, name, job, cron) : saveNew(name, job, cron, enabled)
|
||||
return saved
|
||||
.then(() => this.reset())
|
||||
.finally(refresh)
|
||||
}
|
||||
|
||||
this.edit = id => {
|
||||
this.reset()
|
||||
const schedule = this.schedules[id]
|
||||
if (schedule) {
|
||||
this.formData.editedScheduleId = schedule.id
|
||||
this.formData.scheduleName = schedule.name
|
||||
this.formData.selectedJob = find(this.jobs, job => job.id = schedule.job)
|
||||
this.scheduleApi.setCron(schedule.cron)
|
||||
}
|
||||
}
|
||||
|
||||
this.delete = (id) => xo.schedule.delete(id).then(refresh).then(() => {
|
||||
if (id === this.formData.editedScheduleId) {
|
||||
this.reset()
|
||||
}
|
||||
})
|
||||
|
||||
this.run = schedule => {
|
||||
this.running[schedule.id] = true
|
||||
notify.info({
|
||||
title: 'Run Job',
|
||||
message: 'One shot running started. See overview for logs.'
|
||||
})
|
||||
const id = schedule.job
|
||||
return xo.job.runSequence([id]).finally(() => delete this.running[schedule.id])
|
||||
}
|
||||
|
||||
this.prettyCron = prettyCron.toString.bind(prettyCron)
|
||||
})
|
||||
|
||||
// A module exports its name.
|
||||
.name
|
||||
77
app/modules/task-scheduler/schedule/view.jade
Normal file
77
app/modules/task-scheduler/schedule/view.jade
Normal file
@@ -0,0 +1,77 @@
|
||||
.grid
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.fa.fa-clock-o
|
||||
| Job scheduler
|
||||
form#scheduleform(ng-submit = 'ctrl.save(ctrl.formData.editedScheduleId, ctrl.formData.scheduleName, ctrl.formData.selectedJob, ctrl.formData.cronPattern, ctrl.formData.enabled)')
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-cogs
|
||||
| Job to schedule
|
||||
.panel-body
|
||||
.alert.alert-warning(ng-if = 'ctrl.formData.editedScheduleId') Editing Schedule ID: {{ ctrl.formData.editedScheduleId }}
|
||||
fieldset.form-horizontal(ng-disabled = '!ctrl.ready')
|
||||
.form-group
|
||||
label.col-sm-2.control-label Schedule Name
|
||||
.col-sm-10
|
||||
input.form-control(form = 'scheduleform', type = 'text', ng-model = 'ctrl.formData.scheduleName', required, placeholder = 'An explicit name for your schedule')
|
||||
.form-group
|
||||
label.col-sm-2.control-label Job
|
||||
.col-sm-10
|
||||
select.form-control(form = 'scheduleform', ng-model = 'ctrl.formData.selectedJob', ng-options = '(job.name + " (" + job.id + ")") for job in (ctrl.jobs | map | orderBy:"name")', required)
|
||||
option(value = '') -- Choose a job --
|
||||
p.help-block(ng-if = 'ctrl.formData.selectedJob') {{ ctrl.selectedJob }}
|
||||
.form-group(ng-if = '!ctrl.formData.editedScheduleId')
|
||||
label.control-label.col-md-2(for = 'enabled')
|
||||
input#enabled(form = 'scheduleform', ng-model = 'ctrl.formData.enabled', type = 'checkbox')
|
||||
.help-block.col-md-10 Enable immediatly after creation
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-clock-o
|
||||
| Schedule
|
||||
.panel-body.form-horizontal
|
||||
.text-center(ng-if = '!ctrl.formData'): i.xo-icon-loading
|
||||
xo-scheduler(data = 'ctrl.formData', api = 'ctrl.scheduleApi')
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-body
|
||||
fieldset.center(ng-disabled = '!ctrl.ready')
|
||||
button.btn.btn-lg.btn-primary(form = 'scheduleform', type = 'submit')
|
||||
i.fa.fa-clock-o
|
||||
|
|
||||
i.fa.fa-arrow-right
|
||||
|
|
||||
i.fa.fa-database
|
||||
| Save
|
||||
|
|
||||
button.btn.btn-lg.btn-default(type = 'button', ng-click = 'ctrl.reset()')
|
||||
| Reset
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-list-ul
|
||||
| Schedules
|
||||
.panel-body
|
||||
.text-center(ng-if = '!(ctrl.schedules | count)') No schedules found
|
||||
table.table(ng-if = 'ctrl.schedules | count')
|
||||
tr
|
||||
th Name
|
||||
th Job
|
||||
th.hidden-xs Schedule
|
||||
th
|
||||
tr(ng-repeat = 'schedule in ctrl.schedules | map | orderBy:"name" track by schedule.id')
|
||||
td
|
||||
| {{ schedule.name }} 
|
||||
span.text-muted.hidden-xs ({{schedule.id}}) 
|
||||
br.visible-xs-block
|
||||
span.label.label-success(ng-if = 'schedule.enabled') enabled
|
||||
td {{ ctrl.jobs[schedule.job].name }} ({{ ctrl.jobs[schedule.job].method }})
|
||||
td.hidden-xs {{ ctrl.prettyCron(schedule.cron) }}
|
||||
td
|
||||
span.pull-right
|
||||
button.btn.btn-primary(type = 'button', ng-click = 'ctrl.edit(schedule.id)'): i.fa.fa-pencil
|
||||
|
|
||||
button.btn.btn-warning(type = 'button', ng-click = 'ctrl.run(schedule)', ng-disabled = 'ctrl.running[schedule.id]'): i.fa.fa-play
|
||||
|
|
||||
button.btn.btn-danger(type = 'button', ng-click = 'ctrl.delete(schedule.id)'): i.fa.fa-trash
|
||||
17
app/modules/task-scheduler/view.jade
Normal file
17
app/modules/task-scheduler/view.jade
Normal file
@@ -0,0 +1,17 @@
|
||||
.menu-grid
|
||||
.side-menu
|
||||
ul.nav
|
||||
li
|
||||
a(ui-sref = '.overview', ui-sref-active = 'active')
|
||||
i.fa.fa-fw.fa-eye.fa-menu
|
||||
span.menu-entry Overview
|
||||
li
|
||||
a(ui-sref = '.job')
|
||||
i.fa.fa-fw.fa-cogs.fa-menu
|
||||
span.menu-entry Jobs
|
||||
li
|
||||
a(ui-sref = '.schedule')
|
||||
i.fa.fa-fw.fa-clock-o.fa-menu
|
||||
span.menu-entry Scheduler
|
||||
|
||||
.side-content(ui-view = '')
|
||||
@@ -164,7 +164,7 @@ module.exports = angular.module 'xoWebApp.tree', [
|
||||
$scope.force_stopVM = (id) -> xo.vm.stop id, true
|
||||
$scope.rebootVM = xo.vm.restart
|
||||
$scope.force_rebootVM = (id) -> xo.vm.restart id, true
|
||||
$scope.suspendVM = (id) -> xo.vm.suspend id, true
|
||||
$scope.suspendVM = (id) -> xo.vm.suspend id
|
||||
$scope.resumeVM = (id) -> xo.vm.resume id, true
|
||||
$scope.migrateVM = (id, hostId) -> xo.vm.migrate id, hostId
|
||||
|
||||
|
||||
@@ -205,8 +205,8 @@ div(style="margin-top: 57px; visibility: hidden; height: 0") .
|
||||
i.xo-icon-sr
|
||||
| {{SR.name_label}}
|
||||
td.col-md-6.right.no-border
|
||||
.progress.progress-small(tooltip="Disk: {{[SR.usage, SR.size] | percentage}} allocated")
|
||||
.progress-bar(role="progressbar", aria-valuenow="{{100*SR.usage/SR.size}}", aria-valuemin="0", aria-valuemax="100", style="width: {{[SR.usage, SR.size] | percentage}}")
|
||||
.progress.progress-small(tooltip="Disk: {{SR.physical_usage | bytesToSize}}/{{SR.size | bytesToSize}} ({{[SR.physical_usage, SR.size] | percentage}})")
|
||||
.progress-bar(role="progressbar", aria-valuenow="{{100*SR.physical_usage/SR.size}}", aria-valuemin="0", aria-valuemax="100", style="width: {{[SR.physical_usage, SR.size] | percentage}}")
|
||||
//- Contains all the hosts of this pool.
|
||||
.grid-cell.grid--gutters.hosts-vms-cells
|
||||
//- Contains a host and all its children (VMs).
|
||||
@@ -276,7 +276,7 @@ div(style="margin-top: 57px; visibility: hidden; height: 0") .
|
||||
//- Memory
|
||||
li(ng-if="host.power_state === 'Running' && host.enabled")
|
||||
i.xo-icon-memory.i-progress
|
||||
.progress.progress-small(tooltip="RAM: {{[host.memory.usage, host.memory.size] | percentage}} allocated")
|
||||
.progress.progress-small(tooltip="RAM: {{host.memory.usage | bytesToSize}}/{{host.memory.size | bytesToSize}} ({{[host.memory.usage, host.memory.size] | percentage}})")
|
||||
.progress-bar(role="progressbar", aria-valuenow="{{100*host.memory.usage/host.memory.size}}", aria-valuemin="0", aria-valuemax="100", style="width: {{[host.memory.usage, host.memory.size] | percentage}}")
|
||||
//- Host address
|
||||
li.text-muted.substats
|
||||
@@ -310,16 +310,16 @@ div(style="margin-top: 57px; visibility: hidden; height: 0") .
|
||||
i.xo-icon-working(ng-if="isVMWorking(VM)", tooltip="{{VM.power_state}} and {{(VM.current_operations | map)[0]}}")
|
||||
i(class="xo-icon-{{VM.power_state | lowercase}}",ng-if="!isVMWorking(VM)", tooltip="{{VM.power_state}}")
|
||||
//- VM name.
|
||||
td.vm-name.col-xs-8.col-sm-2.col-md-2
|
||||
td.vm-name.col-xs-8.col-sm-3.col-md-3
|
||||
p.vm {{VM.name_label}}
|
||||
//- Quick actions.
|
||||
td.vm-quick-buttons.col-md-2.hidden-xs
|
||||
td.vm-quick-buttons.col-md-1.col-sm-1.hidden-xs
|
||||
.quick-buttons
|
||||
a(tooltip="Shutdown VM", xo-click="confirmAction('stopVM', VM.id)")
|
||||
a(ng-if="VM.power_state == ('Running' || 'Paused')", tooltip="Shutdown VM", xo-click="confirmAction('stopVM', VM.id)")
|
||||
i.fa.fa-stop
|
||||
a(tooltip="Start VM", xo-click="startVM(VM.id)")
|
||||
a(ng-if="VM.power_state == ('Halted')", tooltip="Start VM", xo-click="startVM(VM.id)")
|
||||
i.fa.fa-play
|
||||
a(tooltip="Reboot VM", xo-click="confirmAction('rebootVM', VM.id)")
|
||||
a(ng-if="VM.power_state == ('Running' || 'Paused')", tooltip="Reboot VM", xo-click="confirmAction('rebootVM', VM.id)")
|
||||
i.fa.fa-refresh
|
||||
a(tooltip="VM Console", xo-sref="consoles_view({id: VM.id})")
|
||||
i.xo-icon-console
|
||||
@@ -333,7 +333,11 @@ div(style="margin-top: 57px; visibility: hidden; height: 0") .
|
||||
//- Memory
|
||||
td.vm-memory-stat.col-md-2.hidden-xs
|
||||
.cpu
|
||||
| {{VM.memory.size | bytesToSize}}
|
||||
span.text-muted
|
||||
span(tooltip = "{{VM.CPUs.number}} vCPUs ({{VM.CPUs.max}} max)")
|
||||
| {{VM.CPUs.number}}x
|
||||
i.xo-icon-cpu
|
||||
| {{VM.memory.size | bytesToSize}}
|
||||
i.xo-icon-docker.fa-fw(ng-if="VM.docker", tooltip="Docker enabled")
|
||||
i.fa.fa-fw(ng-if="VM.xenTools === 'up to date' && !VM.docker")
|
||||
i.xo-icon-warning.fa-fw(ng-if="VM.xenTools === 'out of date'", tooltip="Xen tools outdated")
|
||||
@@ -364,20 +368,20 @@ div(style="margin-top: 57px; visibility: hidden; height: 0") .
|
||||
i.xo-icon-working(ng-if="isVMWorking(VM)", tooltip="{{VM.power_state}} and {{(VM.current_operations | map)[0]}}")
|
||||
i(class="xo-icon-{{VM.power_state | lowercase}}",ng-if="!isVMWorking(VM)", tooltip="{{VM.power_state}}")
|
||||
//- VM name.
|
||||
td.vm-name.col-xs-8.col-sm-2.col-md-2
|
||||
td.vm-name.col-xs-8.col-sm-3.col-md-3
|
||||
p.vm {{VM.name_label}}
|
||||
//- Quick actions.
|
||||
td.vm-quick-buttons.col-md-2.hidden-xs
|
||||
td.vm-quick-buttons.col-md-1.hidden-xs
|
||||
.quick-buttons
|
||||
a(tooltip="Shutdown VM", xo-click="stopVM(VM.id)")
|
||||
a(ng-if="VM.power_state == ('Running' || 'Paused')", tooltip="Shutdown VM", xo-click="stopVM(VM.id)")
|
||||
i.fa.fa-stop
|
||||
a(ng-if="VM.power_state == 'Suspended'", tooltip="Resume VM", xo-click="resumeVM(VM.id)")
|
||||
i.fa.fa-play
|
||||
a(ng-if="VM.power_state != 'Suspended'", tooltip="Start VM", xo-click="startVM(VM.id)")
|
||||
i.fa.fa-play
|
||||
a(tooltip="Reboot VM", xo-click="rebootVM(VM.id)")
|
||||
a(ng-if="VM.power_state == ('Running' || 'Paused')", tooltip="Reboot VM", xo-click="rebootVM(VM.id)")
|
||||
i.fa.fa-refresh
|
||||
a(tooltip="VM Console")
|
||||
a(tooltip="VM Console", xo-sref="consoles_view({id: VM.id})")
|
||||
i.xo-icon-console
|
||||
//- Description.
|
||||
td.vm-description.col-md-4.hidden-xs
|
||||
@@ -389,7 +393,11 @@ div(style="margin-top: 57px; visibility: hidden; height: 0") .
|
||||
//- Memory
|
||||
td.vm-memory-stat.col-md-2.hidden-xs
|
||||
.cpu
|
||||
| {{VM.memory.size | bytesToSize}}
|
||||
span.text-muted
|
||||
span(tooltip = "{{VM.CPUs.number}} vCPUs ({{VM.CPUs.max}} max)")
|
||||
| {{VM.CPUs.number}}x
|
||||
i.xo-icon-cpu
|
||||
| {{VM.memory.size | bytesToSize}}
|
||||
i.xo-icon-docker.fa-fw(ng-if="VM.docker", tooltip="Docker enabled")
|
||||
i.fa.fa-fw(ng-if="VM.xenTools === 'up to date' && !VM.docker")
|
||||
i.xo-icon-warning.fa-fw(ng-if="VM.xenTools === 'out of date'", tooltip="Xen tools outdated")
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
angular = require 'angular'
|
||||
assign = require 'lodash.assign'
|
||||
filter = require 'lodash.filter'
|
||||
find = require 'lodash.find'
|
||||
forEach = require 'lodash.foreach'
|
||||
includes = require 'lodash.includes'
|
||||
isEmpty = require 'lodash.isempty'
|
||||
sortBy = require 'lodash.sortby'
|
||||
|
||||
@@ -20,8 +23,9 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
.controller 'VmCtrl', (
|
||||
$scope, $state, $stateParams, $location, $q
|
||||
xoApi, xo
|
||||
sizeToBytesFilter, bytesToSizeFilter, xoHideUnauthorizedFilter
|
||||
bytesToSizeFilter, sizeToBytesFilter, xoHideUnauthorizedFilter, bytesConvertFilter
|
||||
modal
|
||||
migrateVmModal
|
||||
$window
|
||||
$timeout
|
||||
dateFilter
|
||||
@@ -30,12 +34,25 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
$window.bytesToSize = bytesToSizeFilter # FIXME dirty workaround to custom a Chart.js tooltip template
|
||||
{get} = xoApi
|
||||
|
||||
checkMainObject = ->
|
||||
if !$scope.VM
|
||||
$state.go('index')
|
||||
return false
|
||||
else
|
||||
return true
|
||||
|
||||
pool = null
|
||||
host = null
|
||||
vm = null
|
||||
$scope.srsByContainer = xoApi.getIndex('srsByContainer')
|
||||
$scope.networksByPool = xoApi.getIndex('networksByPool')
|
||||
$scope.pools = xoApi.getView('pools')
|
||||
$scope.PIFs = xoApi.getView('PIFs')
|
||||
$scope.VIFs = xoApi.getView('VIFs')
|
||||
do (
|
||||
networksByPool = xoApi.getIndex('networksByPool')
|
||||
srsByContainer = xoApi.getIndex('srsByContainer')
|
||||
hostsByPool = xoApi.getIndex('hostsByPool')
|
||||
poolSrs = null
|
||||
hostSrs = null
|
||||
) ->
|
||||
@@ -48,6 +65,10 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
srs = []
|
||||
poolSrs and forEach(poolSrs, (sr) => srs.push(sr))
|
||||
hostSrs and forEach(hostSrs, (sr) => srs.push(sr))
|
||||
if (($scope.VM?.power_state is 'Halted') || ($scope.VM?.power_state is 'Suspended')) && pool.id
|
||||
forEach hostsByPool[pool.id], (host) ->
|
||||
forEach srsByContainer[host.id], (sr) -> srs.push(sr)
|
||||
|
||||
srs = xoHideUnauthorizedFilter(srs)
|
||||
$scope.writable_SRs = filter(srs, (sr) => sr.content_type isnt 'iso')
|
||||
$scope.SRs = srs
|
||||
@@ -98,7 +119,8 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
() => this.stop(),
|
||||
this.baseTimeOut
|
||||
)
|
||||
return $scope.refreshStats($scope.VM.id)
|
||||
promise = if $scope.VM?.id then $scope.refreshStats($scope.VM.id) else $q.reject()
|
||||
return promise
|
||||
.then () => this._reset()
|
||||
.catch (err) =>
|
||||
if !this.running || this.attempt >= 2 || $scope.VM.power_state isnt 'Running' || $scope.isVMWorking($scope.VM)
|
||||
@@ -131,8 +153,14 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
$scope.VM = vm = VM
|
||||
return unless VM?
|
||||
|
||||
$scope.cpuWeight = VM.cpuWeight || 0
|
||||
|
||||
# For the edition of this VM.
|
||||
$scope.memorySize = bytesToSizeFilter VM.memory.size
|
||||
$scope.bytes = VM.memory.size
|
||||
memory = bytesToSizeFilter($scope.bytes).split(' ')
|
||||
$scope.memoryValue = memory[0]
|
||||
$scope.memoryUnit = memory[1]
|
||||
|
||||
$scope.bootParams = parseBootParams($scope.VM.boot.order)
|
||||
|
||||
$scope.prepareVDIs()
|
||||
@@ -161,9 +189,19 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
continue unless oVbd
|
||||
oVdi = get oVbd.VDI
|
||||
continue unless oVdi
|
||||
VDIs.push oVdi if oVdi and not oVbd.is_cd_drive
|
||||
|
||||
$scope.VDIs = sortBy(VDIs, (value) -> (get resolveVBD(value))?.position);
|
||||
if not oVbd.is_cd_drive
|
||||
size = bytesToSizeFilter(oVdi.size)
|
||||
oVdi = assign({}, oVdi, {
|
||||
size,
|
||||
sizeValue: size.split(' ')[0],
|
||||
sizeUnit: size.split(' ')[1],
|
||||
position: oVbd.position
|
||||
})
|
||||
oVdi.xoBootable = $scope.isBootable oVdi
|
||||
VDIs.push oVdi
|
||||
|
||||
$scope.VDIs = sortBy(VDIs, 'position');
|
||||
|
||||
descriptor = (obj) ->
|
||||
if !obj
|
||||
@@ -234,7 +272,7 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
$scope.savingBootOrder = true
|
||||
paramString = ''
|
||||
forEach(bootParams, (boot) -> boot.v && paramString += boot.e)
|
||||
return xoApi.call 'vm.bootOrder', {vm: id, order: paramString}
|
||||
return xo.vm.setBootOrder {vm: id, order: paramString}
|
||||
.finally () ->
|
||||
$scope.savingBootOrder = false
|
||||
$scope.bootReordering = false
|
||||
@@ -302,6 +340,13 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
message: 'Start VM'
|
||||
}
|
||||
|
||||
$scope.recoveryStartVM = (id) ->
|
||||
xo.vm.recoveryStart id
|
||||
notify.info {
|
||||
title: 'VM starting...'
|
||||
message: 'Start VM in recovery mode'
|
||||
}
|
||||
|
||||
$scope.stopVM = (id) ->
|
||||
modal.confirm
|
||||
title: 'VM shutdown'
|
||||
@@ -365,11 +410,57 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
}
|
||||
|
||||
$scope.migrateVM = (id, hostId) ->
|
||||
modal.confirm
|
||||
title: 'VM migrate'
|
||||
message: 'Are you sure you want to migrate this VM?'
|
||||
.then ->
|
||||
xo.vm.migrate id, hostId
|
||||
targetHost = $scope.hosts.all[hostId]
|
||||
targetPoolId = $scope.hosts.all[hostId].$poolId
|
||||
targetPool = $scope.pools.all[targetPoolId]
|
||||
{VDIs} = $scope
|
||||
|
||||
vmSrsOnTargetPool = true
|
||||
forEach(VDIs, (vdi) ->
|
||||
vmSrsOnTargetPool = vmSrsOnTargetPool && $scope.srsByContainer[targetPoolId].hasOwnProperty(vdi.$SR)
|
||||
)
|
||||
|
||||
if vmSrsOnTargetPool
|
||||
modal.confirm
|
||||
title: 'VM migrate'
|
||||
message: 'Are you sure you want to migrate this VM?'
|
||||
.then ->
|
||||
xo.vm.migrate id, hostId
|
||||
return
|
||||
|
||||
defaults = {}
|
||||
VIFs = []
|
||||
networks = []
|
||||
srsOnTargetPool = []
|
||||
srsOnTargetHost = []
|
||||
|
||||
# Possible SRs for each VDI
|
||||
forEach($scope.srsByContainer[targetPoolId], (sr) ->
|
||||
srsOnTargetPool.push(sr) if sr.content_type != 'iso'
|
||||
)
|
||||
forEach($scope.srsByContainer[targetHost.id], (sr) ->
|
||||
srsOnTargetHost.push(sr) if sr.content_type != 'iso'
|
||||
)
|
||||
defaults.sr = targetPool.default_SR
|
||||
|
||||
|
||||
# Possible networks for each VIF
|
||||
forEach($scope.VM.VIFs, (vifId) ->
|
||||
VIFs.push($scope.VIFs.all[vifId])
|
||||
)
|
||||
|
||||
poolNetworks = $scope.networksByPool[targetPoolId]
|
||||
forEach(targetHost.PIFs, (pifId) ->
|
||||
networkId = $scope.PIFs.all[pifId].$network
|
||||
networks.push(poolNetworks[networkId])
|
||||
)
|
||||
defaultPIF = find($scope.PIFs.all, (pif) -> pif.management && includes(targetHost.PIFs, pif.id))
|
||||
defaults.network = defaultPIF.$network
|
||||
|
||||
{pool} = $scope
|
||||
intraPoolMigration = (pool.id == targetPoolId)
|
||||
|
||||
migrateVmModal($state, id, hostId, $scope.VDIs, srsOnTargetPool, srsOnTargetHost, VIFs, networks, defaults, intraPoolMigration)
|
||||
|
||||
$scope.destroyVM = (id) ->
|
||||
modal.confirm
|
||||
@@ -398,18 +489,32 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
|
||||
xoApi.call 'vm.set', result
|
||||
|
||||
$scope.xenDefaultWeight = xenDefaultWeight = 256
|
||||
$scope.weightMap = {0: 'Default'}
|
||||
$scope.weightMap[xenDefaultWeight / 4] = 'Quarter (1/4)'
|
||||
$scope.weightMap[xenDefaultWeight / 2] = 'Half (1/2)'
|
||||
$scope.weightMap[xenDefaultWeight] = 'Normal'
|
||||
$scope.weightMap[xenDefaultWeight * 2] = 'Double (x2)'
|
||||
|
||||
$scope.units = ['MiB', 'GiB', 'TiB']
|
||||
|
||||
$scope.saveVM = ($data) ->
|
||||
{VM} = $scope
|
||||
{CPUs, memory, name_label, name_description, high_availability, auto_poweron, PV_args} = $data
|
||||
{CPUs, cpuWeight, memoryValue, memoryUnit, name_label, name_description, high_availability, auto_poweron, PV_args} = $data
|
||||
|
||||
cpuWeight = cpuWeight || 0 # 0 will let XenServer use it's default value
|
||||
|
||||
newBytes = sizeToBytesFilter(memoryValue + ' ' + memoryUnit)
|
||||
|
||||
$data = {
|
||||
id: VM.id
|
||||
}
|
||||
if memory isnt $scope.memorySize and (memory = sizeToBytesFilter memory)
|
||||
$data.memory = memory
|
||||
$scope.memorySize = bytesToSizeFilter memory
|
||||
if $scope.bytes isnt newBytes
|
||||
$data.memory = bytesToSizeFilter(newBytes)
|
||||
if CPUs isnt VM.CPUs.number
|
||||
$data.CPUs = +CPUs
|
||||
if cpuWeight isnt (VM.cpuWeight || 0)
|
||||
$data.cpuWeight = +cpuWeight
|
||||
if name_label isnt VM.name_label
|
||||
$data.name_label = name_label
|
||||
if name_description isnt VM.name_description
|
||||
@@ -436,83 +541,115 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
return
|
||||
|
||||
migrateDisk = (id, sr_id) ->
|
||||
return modal.confirm({
|
||||
notify.info {
|
||||
title: 'Disk migration'
|
||||
message: 'Are you sure you want to migrate (move) this disk to another SR?'
|
||||
}).then ->
|
||||
notify.info {
|
||||
title: 'Disk migration'
|
||||
message: 'Disk migration started'
|
||||
}
|
||||
xo.vdi.migrate id, sr_id
|
||||
return
|
||||
message: 'Disk migration started'
|
||||
}
|
||||
xo.vdi.migrate id, sr_id
|
||||
return
|
||||
|
||||
$scope.saveDisks = (data) ->
|
||||
$scope.saveDisks = (data, vdis) ->
|
||||
# Group data by disk.
|
||||
disks = {}
|
||||
sizeChanges = false
|
||||
srChanges = false
|
||||
forEach data, (value, key) ->
|
||||
i = key.indexOf '/'
|
||||
(disks[key.slice 0, i] ?= {})[key.slice i + 1] = value
|
||||
return
|
||||
|
||||
# Setting correctly formatted disk size properties
|
||||
forEach disks, (disk) ->
|
||||
disk.size = bytesToSizeFilter(sizeToBytesFilter(disk.sizeValue + ' ' + disk.sizeUnit))
|
||||
disk.sizeValue = disk.size.split(' ')[0]
|
||||
disk.sizeUnit = disk.size.split(' ')[1]
|
||||
|
||||
promises = []
|
||||
|
||||
# Handle SR change.
|
||||
# Set bootable status
|
||||
forEach vdis, (vdi) ->
|
||||
bootable = vdi.xoBootable
|
||||
if $scope.isBootable(vdi) != bootable
|
||||
id = (get resolveVBD(vdi)).id
|
||||
promises.push (xo.vbd.setBootable id, bootable)
|
||||
return
|
||||
|
||||
# Disk resize
|
||||
forEach disks, (attributes, id) ->
|
||||
disk = get id
|
||||
if attributes.$SR isnt disk.$SR
|
||||
promises.push (migrateDisk id, attributes.$SR)
|
||||
srChanges = true
|
||||
if attributes.size isnt bytesToSizeFilter(disk.size) # /!\ attributes are provided by a modified copy of disk
|
||||
sizeChanges = true
|
||||
return false
|
||||
|
||||
return
|
||||
message = ''
|
||||
if sizeChanges
|
||||
message += 'Growing the size of a disk is not reversible. '
|
||||
if srChanges
|
||||
message += 'You are about to migrate (move) some disk(s) to another SR. '
|
||||
message += 'Are you sure you want to perform those changes?'
|
||||
preCheck = if sizeChanges or srChanges then modal.confirm({title: 'Disk modifications', message: message}) else $q.resolve()
|
||||
|
||||
return preCheck
|
||||
.then ->
|
||||
# Handle SR change.
|
||||
forEach disks, (attributes, id) ->
|
||||
disk = get id
|
||||
if attributes.$SR isnt disk.$SR
|
||||
promises.push(migrateDisk(id, attributes.$SR))
|
||||
|
||||
if attributes.size isnt bytesToSizeFilter(disk.size) # /!\ attributes are provided by a modified copy of disk
|
||||
promises.push(xo.disk.resize(id, attributes.size))
|
||||
delete attributes.size
|
||||
|
||||
# Keep only changed attributes.
|
||||
forEach attributes, (value, name) ->
|
||||
delete attributes[name] if value is disk[name]
|
||||
return
|
||||
|
||||
unless isEmpty attributes
|
||||
# Inject id.
|
||||
attributes.id = id
|
||||
|
||||
# Ask the server to update the object.
|
||||
promises.push(xoApi.call('vdi.set', attributes))
|
||||
|
||||
forEach disks, (attributes, id) ->
|
||||
# Keep only changed attributes.
|
||||
disk = get id
|
||||
forEach attributes, (value, name) ->
|
||||
delete attributes[name] if value is disk[name]
|
||||
return
|
||||
|
||||
unless isEmpty attributes
|
||||
# Inject id.
|
||||
attributes.id = id
|
||||
# Handle Position changes
|
||||
vbds = xoApi.get($scope.VM.$VBDs)
|
||||
notFreePositions = Object.create(null)
|
||||
forEach vbds, (vbd) ->
|
||||
if vbd.is_cd_drive
|
||||
notFreePositions[vbd.position] = null
|
||||
|
||||
# Ask the server to update the object.
|
||||
promises.push xoApi.call 'vdi.set', attributes
|
||||
return
|
||||
position = 0
|
||||
forEach $scope.VDIs, (vdi) ->
|
||||
oVbd = get(resolveVBD(vdi))
|
||||
unless oVbd
|
||||
return
|
||||
|
||||
# Handle Position changes
|
||||
vbds = xoApi.get($scope.VM.$VBDs)
|
||||
notFreePositions = Object.create(null)
|
||||
forEach vbds, (vbd) ->
|
||||
if vbd.is_cd_drive
|
||||
notFreePositions[vbd.position] = null
|
||||
while position of notFreePositions
|
||||
++position
|
||||
|
||||
position = 0
|
||||
forEach $scope.VDIs, (vdi) ->
|
||||
oVbd = get(resolveVBD(vdi))
|
||||
unless oVbd
|
||||
return
|
||||
if +oVbd.position isnt position
|
||||
promises.push(
|
||||
xoApi.call('vbd.set', {
|
||||
id: oVbd.id,
|
||||
position: String(position)
|
||||
})
|
||||
)
|
||||
|
||||
while position of notFreePositions
|
||||
++position
|
||||
|
||||
if +oVbd.position isnt position
|
||||
promises.push(
|
||||
xoApi.call('vbd.set', {
|
||||
id: oVbd.id,
|
||||
position: String(position)
|
||||
})
|
||||
)
|
||||
|
||||
++position
|
||||
|
||||
return $q.all promises
|
||||
.catch (err) ->
|
||||
console.log(err);
|
||||
notify.error {
|
||||
title: 'saveDisks'
|
||||
message: err
|
||||
}
|
||||
return $q.all promises
|
||||
.catch (err) ->
|
||||
console.log(err);
|
||||
notify.error {
|
||||
title: 'saveDisks'
|
||||
message: err
|
||||
}
|
||||
|
||||
$scope.deleteDisk = (id) ->
|
||||
modal.confirm({
|
||||
@@ -530,7 +667,7 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
if not vdi?
|
||||
return
|
||||
for vbd in vdi.$VBDs
|
||||
rVbd = vbd if (get vbd).VM is $scope.VM.id
|
||||
rVbd = vbd if (get vbd)?.VM is $scope.VM?.id
|
||||
return rVbd || null
|
||||
|
||||
$scope.disconnectVBD = (vdi) ->
|
||||
@@ -712,6 +849,7 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
xo.docker.unpause VM, container
|
||||
|
||||
$scope.addVdi = (vdi, readonly, bootable) ->
|
||||
return unless checkMainObject()
|
||||
|
||||
$scope.addWaiting = true # disables form fields
|
||||
position = $scope.maxPos + 1
|
||||
@@ -731,6 +869,7 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
$scope.addWaiting = false
|
||||
|
||||
$scope.isConnected = isConnected = (vdi) -> (get resolveVBD(vdi))?.attached
|
||||
$scope.isBootable = isBootable = (vdi) -> (get resolveVBD(vdi))?.bootable
|
||||
|
||||
$scope.isFreeForWriting = isFreeForWriting = (vdi) ->
|
||||
free = true
|
||||
@@ -740,6 +879,7 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
return free
|
||||
|
||||
$scope.createVdi = (name, size, sr, bootable, readonly) ->
|
||||
return unless checkMainObject
|
||||
|
||||
$scope.createVdiWaiting = true # disables form fields
|
||||
position = $scope.maxPos + 1
|
||||
@@ -770,9 +910,10 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
$scope.createVdiWaiting = false
|
||||
|
||||
$scope.updateMTU = (network) ->
|
||||
$scope.newInterfaceMTU = network.MTU
|
||||
$scope.newInterfaceMTU = network && network.MTU
|
||||
|
||||
$scope.createInterface = (network, mtu, automac, mac) ->
|
||||
return unless checkMainObject()
|
||||
|
||||
$scope.createVifWaiting = true # disables form fields
|
||||
|
||||
@@ -806,19 +947,19 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
|
||||
$scope.canAdmin = (id = undefined) ->
|
||||
if id == undefined
|
||||
id = $scope.VM && $scope.VM.id
|
||||
id = $scope.VM?.id
|
||||
|
||||
return id && xoApi.canInteract(id, 'administrate') || false
|
||||
|
||||
$scope.canOperate = (id = undefined) ->
|
||||
if id == undefined
|
||||
id = $scope.VM && $scope.VM.id
|
||||
id = $scope.VM?.id
|
||||
|
||||
return id && xoApi.canInteract(id, 'operate') || false
|
||||
|
||||
$scope.canView = (id = undefined) ->
|
||||
if id == undefined
|
||||
id = $scope.VM && $scope.VM.id
|
||||
id = $scope.VM?.id
|
||||
|
||||
return id && xoApi.canInteract(id, 'view') || false
|
||||
|
||||
|
||||
@@ -71,20 +71,35 @@
|
||||
e-form="vmSettings"
|
||||
)
|
||||
| {{VM.CPUs.number}}
|
||||
dt CPU Weight
|
||||
dd
|
||||
span(
|
||||
editable-select="cpuWeight"
|
||||
e-ng-options="key as value for (key, value) in weightMap"
|
||||
e-name="cpuWeight"
|
||||
e-form="vmSettings"
|
||||
)
|
||||
| {{ weightMap[VM.cpuWeight || 0] }}
|
||||
dt RAM
|
||||
dd
|
||||
span(
|
||||
editable-text="memorySize"
|
||||
e-name="memory"
|
||||
editable-text="memoryValue"
|
||||
e-name="memoryValue"
|
||||
e-form="vmSettings"
|
||||
)
|
||||
| {{memoryValue}} {{memoryUnit}}
|
||||
span(
|
||||
editable-select="memoryUnit"
|
||||
e-ng-options="unit for unit in units"
|
||||
e-name="memoryUnit"
|
||||
e-form="vmSettings"
|
||||
)
|
||||
| {{memorySize}}
|
||||
dt UUID
|
||||
dd {{VM.UUID}}
|
||||
dt(ng-if="VM.PV_args") PV Args
|
||||
dd(ng-if="VM.PV_args")
|
||||
dt(ng-if= "VM.virtualizationMode !== 'hvm'") PV Args
|
||||
dd(ng-if= "VM.virtualizationMode !== 'hvm'")
|
||||
span(editable-text="VM.PV_args", e-name="PV_args", e-form="vmSettings")
|
||||
| {{VM.PV_args}}
|
||||
| {{VM.PV_args}}
|
||||
dt(ng-if="refreshStatControl.running && stats") Xen tools
|
||||
dd(ng-if="refreshStatControl.running && stats")
|
||||
span.text-success(ng-if="VM.xenTools === 'up to date'") Installed
|
||||
@@ -241,7 +256,7 @@
|
||||
br
|
||||
p.center(ng-if="refreshStatControl.running")
|
||||
i.xo-icon-loading
|
||||
| Fetching stats...
|
||||
| Fetching stats...
|
||||
.grid
|
||||
.grid-cell(ng-if="VM.os_version.distro")
|
||||
p.stat-name OS:
|
||||
@@ -255,129 +270,114 @@
|
||||
span(ng-if="VM.PV_drivers && !VM.PV_drivers_up_to_date") Outdated
|
||||
|
||||
//- Action panel
|
||||
.grid-sm
|
||||
.grid-sm(ng-if = 'canOperate()')
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-flash
|
||||
| Actions
|
||||
.panel-body.text-center
|
||||
.grid-sm.grid--gutters
|
||||
.grid.grid-cell
|
||||
.grid-cell.btn-group(ng-if="VM.power_state == ('Running' || 'Paused')")
|
||||
button.btn(tooltip="Stop VM", tooltip-placement="top", type="button", style="width: 90%", xo-click="stopVM(VM.id)", ng-if = 'canOperate()')
|
||||
i.fa.fa-stop.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(ng-if="VM.power_state == ('Running')")
|
||||
button.btn(tooltip="Suspend VM", tooltip-placement="top", type="button", style="width: 90%", xo-click="suspendVM(VM.id)", ng-if = 'canOperate()')
|
||||
i.fa.fa-pause.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(ng-if="VM.power_state == ('Halted')")
|
||||
button.btn(tooltip="Start VM", tooltip-placement="top", type="button", style="width: 90%", xo-click="startVM(VM.id)", ng-if = 'canOperate()')
|
||||
i.fa.fa-play.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(ng-if="VM.power_state == ('Running' || 'Paused')")
|
||||
button.btn(tooltip="Reboot VM", tooltip-placement="top", type="button", style="width: 90%", xo-click="rebootVM(VM.id)", ng-if = 'canOperate()')
|
||||
i.fa.fa-refresh.fa-2x.fa-fw
|
||||
.grid-cell.btn-group.dropdown(
|
||||
ng-if="VM.power_state == ('Running' || 'Paused')"
|
||||
dropdown
|
||||
.grid-cell.btn-group(ng-if="VM.power_state == ('Running' || 'Paused')")
|
||||
button.btn(tooltip="Stop VM", tooltip-placement="top", type="button", style="width: 90%", xo-click="stopVM(VM.id)", ng-if = 'canOperate()')
|
||||
i.fa.fa-stop.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(ng-if="VM.power_state == ('Running')")
|
||||
button.btn(tooltip="Suspend VM", tooltip-placement="top", type="button", style="width: 90%", xo-click="suspendVM(VM.id)", ng-if = 'canOperate()')
|
||||
i.fa.fa-pause.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(ng-if="VM.power_state == ('Halted')")
|
||||
button.btn(tooltip="Start VM", tooltip-placement="top", type="button", style="width: 90%", xo-click="startVM(VM.id)", ng-if = 'canOperate()')
|
||||
i.fa.fa-play.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(ng-if="VM.power_state == ('Halted')")
|
||||
button.btn(tooltip="Start VM in recovery mode", tooltip-placement="top", type="button", style="width: 90%", xo-click="recoveryStartVM(VM.id)", ng-if = 'canOperate()')
|
||||
i.fa.fa-forward.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(ng-if="VM.power_state == ('Running' || 'Paused')")
|
||||
button.btn(tooltip="Reboot VM", tooltip-placement="top", type="button", style="width: 90%", xo-click="rebootVM(VM.id)", ng-if = 'canOperate()')
|
||||
i.fa.fa-refresh.fa-2x.fa-fw
|
||||
.grid-cell.btn-group.dropdown(
|
||||
ng-if="VM.power_state == ('Running' || 'Paused')"
|
||||
dropdown
|
||||
)
|
||||
button.btn.disabled(
|
||||
ng-if="canAdmin() && (hosts.all | count)==1"
|
||||
tooltip = "No other host available"
|
||||
style="width: 90%"
|
||||
)
|
||||
button.btn.disabled(
|
||||
ng-if="canAdmin() && (hosts.all | count)==1"
|
||||
tooltip = "No other host available"
|
||||
style="width: 90%"
|
||||
)
|
||||
i.fa.fa-share.fa-2x.fa-fw
|
||||
span.caret
|
||||
button.btn.dropdown-toggle(
|
||||
ng-if = "canAdmin() && (hosts.all | count)>1"
|
||||
dropdown-toggle
|
||||
tooltip="Migrate VM"
|
||||
tooltip-placement="top"
|
||||
type="button"
|
||||
style="width: 90%"
|
||||
)
|
||||
i.fa.fa-share.fa-2x.fa-fw
|
||||
span.caret
|
||||
ul.dropdown-menu.left(role="menu", ng-if = 'canAdmin()')
|
||||
li(ng-repeat="h in hosts.all | orderBy:natural('name_label') track by h.id" ng-if="h!=host")
|
||||
a(xo-click="migrateVM(VM.id, h.id)")
|
||||
i.xo-icon-host.fa-fw
|
||||
| To {{h.name_label}}
|
||||
.grid-cell.btn-group(ng-if="VM.power_state == ('Running' || 'Paused')")
|
||||
button.btn(tooltip="Force Reboot", tooltip-placement="top", type="button", style="width: 90%", xo-click="force_rebootVM(VM.id)", ng-if = 'canOperate()')
|
||||
i.fa.fa-flash.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(ng-if="VM.power_state == ('Halted')")
|
||||
button.btn(tooltip="Delete VM", tooltip-placement="top", type="button", style="width: 90%", xo-click="destroyVM(VM.id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-trash-o.fa-2x.fa-fw
|
||||
.grid-cell.btn-group.dropdown(
|
||||
ng-if="VM.power_state == ('Halted')"
|
||||
dropdown
|
||||
i.fa.fa-share.fa-2x.fa-fw
|
||||
span.caret
|
||||
button.btn.dropdown-toggle(
|
||||
ng-if = "canAdmin() && (hosts.all | count)>1"
|
||||
dropdown-toggle
|
||||
tooltip="Migrate VM"
|
||||
tooltip-placement="top"
|
||||
type="button"
|
||||
style="width: 90%"
|
||||
)
|
||||
button.btn.dropdown-toggle(
|
||||
ng-if = 'canAdmin()'
|
||||
dropdown-toggle
|
||||
tooltip="Create a clone"
|
||||
tooltip-placement="top"
|
||||
style="width: 90%"
|
||||
type="button"
|
||||
)
|
||||
i.fa.fa-files-o.fa-2x.fa-fw
|
||||
span.caret
|
||||
ul.dropdown-menu.left(role="menu")
|
||||
li(ng-if = 'canAdmin()')
|
||||
a(xo-click="cloneVM(VM.id,VM.name_label,false)")
|
||||
i.fa.fa-code-fork.fa-fw
|
||||
| Fast clone
|
||||
li(ng-if = 'canAdmin()')
|
||||
a(xo-click="cloneVM(VM.id,VM.name_label,true)")
|
||||
i.xo-icon-disk.fa-fw
|
||||
| Full disk copy
|
||||
.grid-cell.btn-group(ng-if="VM.power_state == ('Halted')")
|
||||
button.btn(tooltip="Convert to template", tooltip-placement="top", type="button", style="width: 90%", xo-click="convertVM(VM.id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-thumb-tack.fa-2x.fa-fw
|
||||
.grid.grid-cell
|
||||
.grid-cell.btn-group(ng-if="VM.power_state == ('Running' || 'Paused')")
|
||||
button.btn(tooltip="Force Shutdown", tooltip-placement="top", type="button", style="width: 90%", xo-click="force_stopVM(VM.id)", ng-if = 'canOperate()')
|
||||
i.fa.fa-power-off.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(ng-if="VM.power_state == ('Suspended')")
|
||||
button.btn(tooltip="Resume VM", tooltip-placement="top", type="button", style="width: 90%", xo-click="resumeVM(VM.id)", ng-if = 'canOperate()')
|
||||
i.fa.fa-play.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Create a snapshot", tooltip-placement="top", style="width: 90%", type="button", xo-click="snapshotVM(VM.id,VM.name_label)", ng-if = 'canAdmin()')
|
||||
i.xo-icon-snapshot.fa-2x.fa-fw
|
||||
.grid-cell.btn-group.dropdown(dropdown, ng-if="canAdmin()")
|
||||
button.btn.dropdown-toggle(
|
||||
dropdown-toggle
|
||||
tooltip="Export the VM"
|
||||
tooltip-placement="top"
|
||||
style="width: 90%"
|
||||
type="button"
|
||||
)
|
||||
i.fa.fa-download.fa-2x.fa-fw
|
||||
span.caret
|
||||
ul.dropdown-menu.left(role="menu")
|
||||
li(ng-if = 'canAdmin()')
|
||||
a(xo-click="exportVM(VM.id)")
|
||||
i.fa.fa-download.fa-fw
|
||||
| Full export
|
||||
li(ng-if = 'canAdmin()')
|
||||
a(xo-click="exportOnlyMetadataVM(VM.id)")
|
||||
i.fa.fa-database.fa-fw
|
||||
| Only Metadata
|
||||
.grid-cell.btn-group.dropdown(dropdown, ng-if="canAdmin()")
|
||||
button.btn.dropdown-toggle(
|
||||
dropdown-toggle
|
||||
tooltip="Copy the VM"
|
||||
tooltip-placement="top"
|
||||
style="width: 90%"
|
||||
type="button"
|
||||
)
|
||||
i.fa.fa-clone.fa-2x.fa-fw
|
||||
span.caret
|
||||
ul.dropdown-menu.left(role="menu")
|
||||
li(ng-repeat = 'SR in objects | selectHighLevel | filter:{type: "SR", content_type: "!iso"} | orderBy:"($container | resolve).name_label"')
|
||||
a(xo-click = 'copyVM(VM.id, SR.id)') {{ SR.name_label}} ({{(SR.$container | resolve).name_label}})
|
||||
.grid-cell.btn-group(style="margin-bottom: 0.5em")
|
||||
button.btn(tooltip="VM Console", tooltip-placement="top", type="button", style="width: 90%", xo-sref="consoles_view({id: VM.id})")
|
||||
i.xo-icon-console.fa-2x.fa-fw
|
||||
i.fa.fa-share.fa-2x.fa-fw
|
||||
span.caret
|
||||
ul.dropdown-menu.left(role="menu", ng-if = 'canAdmin()')
|
||||
li(ng-repeat="h in hosts.all | orderBy:natural('name_label') track by h.id" ng-if="h!=host")
|
||||
a(xo-click="migrateVM(VM.id, h.id)")
|
||||
i.xo-icon-host.fa-fw
|
||||
| To {{h.name_label}}
|
||||
.grid-cell.btn-group(ng-if="VM.power_state == ('Running' || 'Paused')")
|
||||
button.btn(tooltip="Force Reboot", tooltip-placement="top", type="button", style="width: 90%", xo-click="force_rebootVM(VM.id)", ng-if = 'canOperate()')
|
||||
i.fa.fa-flash.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Delete VM", tooltip-placement="top", type="button", style="width: 90%", xo-click="destroyVM(VM.id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-trash-o.fa-2x.fa-fw
|
||||
.grid-cell.btn-group.dropdown(
|
||||
ng-if="VM.power_state == ('Halted')"
|
||||
dropdown
|
||||
)
|
||||
button.btn.dropdown-toggle(
|
||||
ng-if = 'canAdmin()'
|
||||
dropdown-toggle
|
||||
tooltip="Create a clone"
|
||||
tooltip-placement="top"
|
||||
style="width: 90%"
|
||||
type="button"
|
||||
)
|
||||
i.fa.fa-files-o.fa-2x.fa-fw
|
||||
span.caret
|
||||
ul.dropdown-menu.left(role="menu")
|
||||
li(ng-if = 'canAdmin()')
|
||||
a(xo-click="cloneVM(VM.id,VM.name_label,false)")
|
||||
i.fa.fa-code-fork.fa-fw
|
||||
| Fast clone
|
||||
li(ng-if = 'canAdmin()')
|
||||
a(xo-click="cloneVM(VM.id,VM.name_label,true)")
|
||||
i.xo-icon-disk.fa-fw
|
||||
| Full disk copy
|
||||
.grid-cell.btn-group(ng-if="VM.power_state == ('Halted')")
|
||||
button.btn(tooltip="Convert to template", tooltip-placement="top", type="button", style="width: 90%", xo-click="convertVM(VM.id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-thumb-tack.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(ng-if="VM.power_state == ('Running' || 'Paused')")
|
||||
button.btn(tooltip="Force Shutdown", tooltip-placement="top", type="button", style="width: 90%", xo-click="force_stopVM(VM.id)", ng-if = 'canOperate()')
|
||||
i.fa.fa-power-off.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(ng-if="VM.power_state == ('Suspended')")
|
||||
button.btn(tooltip="Resume VM", tooltip-placement="top", type="button", style="width: 90%", xo-click="resumeVM(VM.id)", ng-if = 'canOperate()')
|
||||
i.fa.fa-play.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Create a snapshot", tooltip-placement="top", style="width: 90%", type="button", xo-click="snapshotVM(VM.id,VM.name_label)", ng-if = 'canAdmin()')
|
||||
i.xo-icon-snapshot.fa-2x.fa-fw
|
||||
.grid-cell.btn-group.dropdown(dropdown, ng-if="canAdmin()")
|
||||
button.btn(tooltip="Export the VM", tooltip-placement="top", style="width: 90%", type="button", xo-click="exportVM(VM.id)", ng-if="canAdmin()")
|
||||
i.fa.fa-download.fa-2x.fa-fw
|
||||
.grid-cell.btn-group.dropdown(dropdown, ng-if="canAdmin()")
|
||||
button.btn.dropdown-toggle(
|
||||
dropdown-toggle
|
||||
tooltip="Copy the VM"
|
||||
tooltip-placement="top"
|
||||
style="width: 90%"
|
||||
type="button"
|
||||
)
|
||||
i.fa.fa-clone.fa-2x.fa-fw
|
||||
span.caret
|
||||
ul.dropdown-menu.left(role="menu")
|
||||
li(ng-repeat = 'SR in objects | selectHighLevel | filter:{type: "SR", content_type: "!iso"} | orderBy:"($container | resolve).name_label"')
|
||||
a(xo-click = 'copyVM(VM.id, SR.id)') {{ SR.name_label}} ({{(SR.$container | resolve).name_label}})
|
||||
.grid-cell.btn-group(style="margin-bottom: 0.5em")
|
||||
button.btn(tooltip="VM Console", tooltip-placement="top", type="button", style="width: 90%", xo-sref="consoles_view({id: VM.id})")
|
||||
i.xo-icon-console.fa-2x.fa-fw
|
||||
//- Docker Panel (if Docker VM)
|
||||
.grid-sm(ng-if="VM.docker")
|
||||
.panel.panel-default
|
||||
@@ -449,16 +449,18 @@
|
||||
)
|
||||
i.fa.fa-undo.fa-fw
|
||||
.panel-body
|
||||
form(name = "disksForm", editable-form = '', onbeforesave = 'saveDisks($data)')
|
||||
form(name = "disksForm", editable-form = '', onbeforesave = 'saveDisks($data, VDIs)')
|
||||
table.table.table-hover
|
||||
tr
|
||||
th Name
|
||||
th Description
|
||||
th Tags
|
||||
th Size
|
||||
th SR
|
||||
th Status
|
||||
th(ng-show="disksForm.$visible")
|
||||
th.col-md-2 Name
|
||||
th.col-md-2 Description
|
||||
th.col-md-2 Tags
|
||||
th.col-md-1 Size
|
||||
th.col-md-1(ng-show="disksForm.$visible")
|
||||
th.col-md-2 SR
|
||||
th.col-md-1 Bootable
|
||||
th.col-md-2 Status
|
||||
th.col-md-2(ng-show="disksForm.$visible")
|
||||
//- FIXME: ng-init seems to disrupt the implicit $watch.
|
||||
tr(ng-repeat = 'VDI in VDIs track by VDI.id')
|
||||
td.oneliner
|
||||
@@ -476,19 +478,41 @@
|
||||
td
|
||||
xo-tag(object = 'VDI')
|
||||
td
|
||||
//- FIXME: should be editable, but the server needs first
|
||||
//- to accept a human readable string.
|
||||
| {{VDI.size | bytesToSize}}
|
||||
span(
|
||||
editable-text="VDI.sizeValue"
|
||||
e-name = '{{VDI.id}}/sizeValue'
|
||||
)
|
||||
| {{VDI.sizeValue}} {{VDI.sizeUnit}}
|
||||
td(ng-show="disksForm.$visible")
|
||||
span(
|
||||
editable-select="VDI.sizeUnit"
|
||||
e-ng-options="unit for unit in units"
|
||||
e-name="{{VDI.id}}/sizeUnit"
|
||||
)
|
||||
td.oneliner
|
||||
span(
|
||||
ng-if = 'canView((VDI.$SR | resolve).id)'
|
||||
editable-select="(VDI.$SR | resolve).id"
|
||||
e-ng-options="SR.id as (SR.name_label + ' (' + (SR.size - SR.usage | bytesToSize) + ' free)') for SR in writable_SRs"
|
||||
e-ng-options="SR.id as (SR.name_label + ' (' + (SR.size - SR.physical_usage | bytesToSize) + ' free) ' + (SR.$container | resolve).name_label) for SR in writable_SRs"
|
||||
e-name = '{{VDI.id}}/$SR'
|
||||
)
|
||||
//- Are SR editable? will trigger moving VDI to the new SR
|
||||
a(xo-sref="SRs_view({id: (VDI.$SR | resolve).id})")
|
||||
| {{(VDI.$SR | resolve).name_label}}
|
||||
| {{(VDI.$SR | resolve).name_label}} ({{((VDI.$SR | resolve).$container | resolve).name_label}})
|
||||
td
|
||||
span.label.label-success(
|
||||
ng-if="isBootable(VDI)"
|
||||
ng-show="!disksForm.$visible"
|
||||
) Bootable
|
||||
span.label.label-default(
|
||||
ng-if="!isBootable(VDI)"
|
||||
ng-show="!disksForm.$visible"
|
||||
) No Bootable
|
||||
input(
|
||||
ng-show="disksForm.$visible"
|
||||
type="checkbox"
|
||||
ng-model="VDI.xoBootable"
|
||||
)
|
||||
td(ng-if="isConnected(VDI)")
|
||||
span.label.label-success Connected
|
||||
span.pull-right.btn-group.quick-buttons(ng-if="canAdmin()")
|
||||
@@ -565,7 +589,7 @@
|
||||
i.fa.fa-minus(ng-if = 'creatingVdi')
|
||||
| New Disk
|
||||
|
|
||||
button.btn(type="button", ng-class = '{"btn-success": bootReordering, "btn-primary": !bootReordering}', ng-disabled="disksForm.$waiting", ng-click="bootReordering = !bootReordering;adding = false;creatingVdi = false", ng-hide = '!canAdmin()')
|
||||
button.btn(type="button", ng-class = '{"btn-success": bootReordering, "btn-primary": !bootReordering}', ng-disabled="disksForm.$waiting", ng-click="bootReordering = !bootReordering;adding = false;creatingVdi = false", ng-hide = '!canAdmin()', ng-show = "VM.virtualizationMode === 'hvm'")
|
||||
i.fa.fa-plus(ng-if = '!bootReordering')
|
||||
i.fa.fa-minus(ng-if = 'bootReordering')
|
||||
| Boot order
|
||||
@@ -606,11 +630,11 @@
|
||||
|
|
||||
.form-group
|
||||
//- label(for = 'newDiskSize') Size
|
||||
input#newDiskSize.form-control(type = 'text', ng-model = 'newDiskSize', required, placeholder = 'Size e.g 128MB, 8GB, 2TB...')
|
||||
input#newDiskSize.form-control(type = 'text', ng-model = 'newDiskSize', required, placeholder = 'Size e.g 128 MiB, 8 GiB, 2 TiB...')
|
||||
|
|
||||
.form-group
|
||||
//- label(for = 'newDiskSR') SR
|
||||
select.form-control(ng-model = 'newDiskSR', required, ng-options="SR.id as (SR.name_label + ' (' + (SR.size - SR.usage | bytesToSize) + ' free)') for SR in writable_SRs")
|
||||
select.form-control(ng-model = 'newDiskSR', required, ng-options="SR.id as (SR.name_label + ' (' + (SR.size - SR.physical_usage | bytesToSize) + ' free) ' + (SR.$container | resolve).name_label) for SR in writable_SRs")
|
||||
option(value = '', disabled) Choose your SR
|
||||
|
|
||||
br
|
||||
@@ -738,13 +762,15 @@
|
||||
.panel-body
|
||||
p.center(ng-if="!VM.snapshots.length") No snapshots
|
||||
table.table.table-hover(ng-if="VM.snapshots.length")
|
||||
th Date
|
||||
th Name
|
||||
th.col-md-4 Date
|
||||
th.col-md-8 Name
|
||||
tr(ng-repeat="snapshot in VM.snapshots | resolve | orderBy:'-snapshot_time' | slice:(5*(currentSnapPage-1)):(5*currentSnapPage) track by snapshot.id")
|
||||
td.oneliner {{snapshot.snapshot_time*1e3 | date:"medium"}}
|
||||
td.oneliner
|
||||
span(editable-text="snapshot.name_label", e-name="name_label", e-form="vmSnap", onbeforesave="saveSnapshot(snapshot.id, $data)")
|
||||
| {{snapshot.name_label}}
|
||||
span(ng-if="snapshot.tags | includes:'quiesce'")
|
||||
i.fa.fa-info-circle(tooltip = "Quiesced snapshot")
|
||||
| {{snapshot.name_label}}
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(tooltip="Export this snapshot", type="button", xo-click="exportVM(snapshot.id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-upload.fa-lg
|
||||
|
||||
34
app/node_modules/angular-no-vnc/index.js
generated
vendored
34
app/node_modules/angular-no-vnc/index.js
generated
vendored
@@ -74,10 +74,14 @@ export default angular.module('no-vnc', [])
|
||||
|
||||
let isSecure = url.protocol === 'wss:'
|
||||
|
||||
let canvas = $element.find('canvas')[0]
|
||||
rfb = new RFB({
|
||||
encrypt: isSecure,
|
||||
target: canvas,
|
||||
wsProtocols: ['chat']
|
||||
wsProtocols: ['chat'],
|
||||
onClipboard (rfb, text) {
|
||||
setClipboard(text)
|
||||
}
|
||||
})
|
||||
|
||||
rfb._onUpdateState = (rfb, state) => {
|
||||
@@ -101,6 +105,11 @@ export default angular.module('no-vnc', [])
|
||||
}
|
||||
|
||||
this.remoteControl = {
|
||||
pasteToClipboard (text) {
|
||||
if (rfb) {
|
||||
rfb.clipboardPasteFrom(text)
|
||||
}
|
||||
},
|
||||
sendCtrlAltDel () {
|
||||
if (rfb) {
|
||||
rfb.sendCtrlAltDel()
|
||||
@@ -108,7 +117,25 @@ export default angular.module('no-vnc', [])
|
||||
}
|
||||
}
|
||||
|
||||
let canvas = $element.find('canvas')[0]
|
||||
const setClipboard = (text) => {
|
||||
this.onClipboardChange({ clipboardContent: text })
|
||||
}
|
||||
|
||||
$scope.unfocus = () => {
|
||||
if (rfb) {
|
||||
rfb.get_keyboard().ungrab()
|
||||
rfb.get_mouse().ungrab()
|
||||
}
|
||||
}
|
||||
$scope.focus = () => {
|
||||
if (rfb) {
|
||||
if (document.activeElement) {
|
||||
document.activeElement.blur()
|
||||
}
|
||||
rfb.get_keyboard().grab()
|
||||
rfb.get_mouse().grab()
|
||||
}
|
||||
}
|
||||
|
||||
$attrs.$observe('url', (url) => {
|
||||
reset(url)
|
||||
@@ -122,7 +149,8 @@ export default angular.module('no-vnc', [])
|
||||
controller: 'NoVncCtrl as noVnc',
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
remoteControl: '='
|
||||
remoteControl: '=',
|
||||
onClipboardChange: '&'
|
||||
},
|
||||
template: view
|
||||
}
|
||||
|
||||
2
app/node_modules/angular-no-vnc/view.jade
generated
vendored
2
app/node_modules/angular-no-vnc/view.jade
generated
vendored
@@ -1,5 +1,7 @@
|
||||
canvas.center-block(
|
||||
height = "{{noVnc.height}}"
|
||||
width = "{{noVnc.width}}"
|
||||
ng-mouseenter = 'focus()'
|
||||
ng-mouseleave = 'unfocus()'
|
||||
)
|
||||
| Sorry, your browser does not support the canvas element.
|
||||
|
||||
2
app/node_modules/iso-device/view.jade
generated
vendored
2
app/node_modules/iso-device/view.jade
generated
vendored
@@ -4,7 +4,7 @@
|
||||
select.form-control(
|
||||
ng-model = 'isoDevice.isos.mounted'
|
||||
ng-change = 'isoDevice.insert(isoDevice.vm, isoDevice.isos.mounted)'
|
||||
ng-options = 'iso.iso.id as iso.label group by iso.sr for iso in isoDevice.isos.opts'
|
||||
ng-options = 'iso.iso.id as iso.label group by iso.sr for iso in isoDevice.isos.opts | orderBy:"label"'
|
||||
unfocus-on-change
|
||||
)
|
||||
option(value = '', disabled) -- CD Drive (empty) --
|
||||
|
||||
314
app/node_modules/scheduler/index.js
generated
vendored
Normal file
314
app/node_modules/scheduler/index.js
generated
vendored
Normal file
@@ -0,0 +1,314 @@
|
||||
import angular from 'angular'
|
||||
import assign from 'lodash.assign'
|
||||
import forEach from 'lodash.foreach'
|
||||
import indexOf from 'lodash.indexof'
|
||||
import later from 'later'
|
||||
import moment from 'moment'
|
||||
import prettyCron from 'prettycron'
|
||||
import remove from 'lodash.remove'
|
||||
|
||||
later.date.localTime()
|
||||
|
||||
import view from './view'
|
||||
|
||||
export default angular.module('xoWebApp.scheduler', [])
|
||||
|
||||
.directive('xoScheduler', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: view,
|
||||
controller: 'XoScheduler as ctrl',
|
||||
bindToController: true,
|
||||
scope: {
|
||||
data: '=',
|
||||
api: '='
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
.controller('XoScheduler', function () {
|
||||
this.init = () => {
|
||||
let i, j
|
||||
|
||||
const minutes = []
|
||||
for (i = 0; i < 6; i++) {
|
||||
minutes[i] = []
|
||||
for (j = 0; j < 10; j++) {
|
||||
minutes[i].push(10 * i + j)
|
||||
}
|
||||
}
|
||||
this.minutes = minutes
|
||||
|
||||
const hours = []
|
||||
for (i = 0; i < 3; i++) {
|
||||
hours[i] = []
|
||||
for (j = 0; j < 8; j++) {
|
||||
hours[i].push(8 * i + j)
|
||||
}
|
||||
}
|
||||
this.hours = hours
|
||||
|
||||
const days = []
|
||||
for (i = 0; i < 4; i++) {
|
||||
days[i] = []
|
||||
for (j = 1; j < 8; j++) {
|
||||
days[i].push(7 * i + j)
|
||||
}
|
||||
}
|
||||
days.push([29, 30, 31])
|
||||
this.days = days
|
||||
|
||||
this.months = [
|
||||
[
|
||||
{v: 1, l: 'Jan'},
|
||||
{v: 2, l: 'Feb'},
|
||||
{v: 3, l: 'Mar'},
|
||||
{v: 4, l: 'Apr'},
|
||||
{v: 5, l: 'May'},
|
||||
{v: 6, l: 'Jun'}
|
||||
],
|
||||
[
|
||||
{v: 7, l: 'Jul'},
|
||||
{v: 8, l: 'Aug'},
|
||||
{v: 9, l: 'Sep'},
|
||||
{v: 10, l: 'Oct'},
|
||||
{v: 11, l: 'Nov'},
|
||||
{v: 12, l: 'Dec'}
|
||||
]
|
||||
]
|
||||
|
||||
this.dayWeeks = [
|
||||
{v: 0, l: 'Sun'},
|
||||
{v: 1, l: 'Mon'},
|
||||
{v: 2, l: 'Tue'},
|
||||
{v: 3, l: 'Wed'},
|
||||
{v: 4, l: 'Thu'},
|
||||
{v: 5, l: 'Fri'},
|
||||
{v: 6, l: 'Sat'}
|
||||
]
|
||||
this.resetData()
|
||||
}
|
||||
|
||||
this.selectMinute = function (minute) {
|
||||
if (this.isSelectedMinute(minute)) {
|
||||
remove(this.data.minSelect, v => String(v) === String(minute))
|
||||
} else {
|
||||
this.data.minSelect.push(minute)
|
||||
}
|
||||
}
|
||||
|
||||
this.isSelectedMinute = function (minute) {
|
||||
return indexOf(this.data.minSelect, minute) > -1 || indexOf(this.data.minSelect, String(minute)) > -1
|
||||
}
|
||||
|
||||
this.selectHour = function (hour) {
|
||||
if (this.isSelectedHour(hour)) {
|
||||
remove(this.data.hourSelect, v => String(v) === String(hour))
|
||||
} else {
|
||||
this.data.hourSelect.push(hour)
|
||||
}
|
||||
}
|
||||
|
||||
this.isSelectedHour = function (hour) {
|
||||
return indexOf(this.data.hourSelect, hour) > -1 || indexOf(this.data.hourSelect, String(hour)) > -1
|
||||
}
|
||||
|
||||
this.selectDay = function (day) {
|
||||
if (this.isSelectedDay(day)) {
|
||||
remove(this.data.daySelect, v => String(v) === String(day))
|
||||
} else {
|
||||
this.data.daySelect.push(day)
|
||||
}
|
||||
}
|
||||
|
||||
this.isSelectedDay = function (day) {
|
||||
return indexOf(this.data.daySelect, day) > -1 || indexOf(this.data.daySelect, String(day)) > -1
|
||||
}
|
||||
|
||||
this.selectMonth = function (month) {
|
||||
if (this.isSelectedMonth(month)) {
|
||||
remove(this.data.monthSelect, v => String(v) === String(month))
|
||||
} else {
|
||||
this.data.monthSelect.push(month)
|
||||
}
|
||||
}
|
||||
|
||||
this.isSelectedMonth = function (month) {
|
||||
return indexOf(this.data.monthSelect, month) > -1 || indexOf(this.data.monthSelect, String(month)) > -1
|
||||
}
|
||||
|
||||
this.selectDayWeek = function (dayWeek) {
|
||||
if (this.isSelectedDayWeek(dayWeek)) {
|
||||
remove(this.data.dayWeekSelect, v => String(v) === String(dayWeek))
|
||||
} else {
|
||||
this.data.dayWeekSelect.push(dayWeek)
|
||||
}
|
||||
}
|
||||
|
||||
this.isSelectedDayWeek = function (dayWeek) {
|
||||
return indexOf(this.data.dayWeekSelect, dayWeek) > -1 || indexOf(this.data.dayWeekSelect, String(dayWeek)) > -1
|
||||
}
|
||||
|
||||
this.noMinutePlan = function (set = false) {
|
||||
if (!set) {
|
||||
// The last part (after &&) of this expression is reliable because we maintain the minSelect array with lodash.remove
|
||||
return this.data.min === 'select' && this.data.minSelect.length === 1 && String(this.data.minSelect[0]) === '0'
|
||||
} else {
|
||||
this.data.minSelect = [0]
|
||||
this.data.min = 'select'
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
this.noHourPlan = function (set = false) {
|
||||
if (!set) {
|
||||
// The last part (after &&) of this expression is reliable because we maintain the hourSelect array with lodash.remove
|
||||
return this.data.hour === 'select' && this.data.hourSelect.length === 1 && String(this.data.hourSelect[0]) === '0'
|
||||
} else {
|
||||
this.data.hourSelect = [0]
|
||||
this.data.hour = 'select'
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
this.resetData = () => {
|
||||
this.data.minRange = 5
|
||||
this.data.hourRange = 2
|
||||
this.data.minSelect = [0]
|
||||
this.data.hourSelect = []
|
||||
this.data.daySelect = []
|
||||
this.data.monthSelect = []
|
||||
this.data.dayWeekSelect = []
|
||||
this.data.min = 'select'
|
||||
this.data.hour = 'all'
|
||||
this.data.day = 'all'
|
||||
this.data.month = 'all'
|
||||
this.data.dayWeek = 'all'
|
||||
this.data.cronPattern = '* * * * *'
|
||||
this.data.summary = []
|
||||
this.data.previewLimit = 0
|
||||
|
||||
this.update()
|
||||
}
|
||||
|
||||
this.update = () => {
|
||||
const d = this.data
|
||||
const i = (d.min === 'all' && '*') ||
|
||||
(d.min === 'range' && ('*/' + d.minRange)) ||
|
||||
(d.min === 'select' && d.minSelect.join(',')) ||
|
||||
'*'
|
||||
const h = (d.hour === 'all' && '*') ||
|
||||
(d.hour === 'range' && ('*/' + d.hourRange)) ||
|
||||
(d.hour === 'select' && d.hourSelect.join(',')) ||
|
||||
'*'
|
||||
const dm = (d.day === 'all' && '*') ||
|
||||
(d.day === 'select' && d.daySelect.join(',')) ||
|
||||
'*'
|
||||
const m = (d.month === 'all' && '*') ||
|
||||
(d.month === 'select' && d.monthSelect.join(',')) ||
|
||||
'*'
|
||||
const dw = (d.dayWeek === 'all' && '*') ||
|
||||
(d.dayWeek === 'select' && d.dayWeekSelect.join(',')) ||
|
||||
'*'
|
||||
this.data.cronPattern = i + ' ' + h + ' ' + dm + ' ' + m + ' ' + dw
|
||||
|
||||
const tabState = {
|
||||
min: {
|
||||
all: d.min === 'all',
|
||||
range: d.min === 'range',
|
||||
select: d.min === 'select'
|
||||
},
|
||||
hour: {
|
||||
all: d.hour === 'all',
|
||||
range: d.hour === 'range',
|
||||
select: d.hour === 'select'
|
||||
},
|
||||
day: {
|
||||
all: d.day === 'all',
|
||||
range: d.day === 'range',
|
||||
select: d.day === 'select'
|
||||
},
|
||||
month: {
|
||||
all: d.month === 'all',
|
||||
select: d.month === 'select'
|
||||
},
|
||||
dayWeek: {
|
||||
all: d.dayWeek === 'all',
|
||||
select: d.dayWeek === 'select'
|
||||
}
|
||||
}
|
||||
this.tabs = tabState
|
||||
this.summarize()
|
||||
}
|
||||
|
||||
this.summarize = () => {
|
||||
const schedule = later.parse.cron(this.data.cronPattern)
|
||||
const occurences = later.schedule(schedule).next(25)
|
||||
this.data.summary = []
|
||||
forEach(occurences, occurence => {
|
||||
this.data.summary.push(moment(occurence).format('LLLL'))
|
||||
})
|
||||
}
|
||||
|
||||
const cronToData = (data, cron) => {
|
||||
const d = Object.create(null)
|
||||
const cronItems = cron.split(' ')
|
||||
|
||||
if (cronItems[0] === '*') {
|
||||
d.min = 'all'
|
||||
} else if (cronItems[0].indexOf('/') !== -1) {
|
||||
d.min = 'range'
|
||||
const [, range] = cronItems[0].split('/')
|
||||
d.minRange = range
|
||||
} else {
|
||||
d.min = 'select'
|
||||
d.minSelect = cronItems[0].split(',')
|
||||
}
|
||||
|
||||
if (cronItems[1] === '*') {
|
||||
d.hour = 'all'
|
||||
} else if (cronItems[1].indexOf('/') !== -1) {
|
||||
d.hour = 'range'
|
||||
const [, range] = cronItems[1].split('/')
|
||||
d.hourRange = range
|
||||
} else {
|
||||
d.hour = 'select'
|
||||
d.hourSelect = cronItems[1].split(',')
|
||||
}
|
||||
|
||||
if (cronItems[2] === '*') {
|
||||
d.day = 'all'
|
||||
} else {
|
||||
d.day = 'select'
|
||||
d.daySelect = cronItems[2].split(',')
|
||||
}
|
||||
|
||||
if (cronItems[3] === '*') {
|
||||
d.month = 'all'
|
||||
} else {
|
||||
d.month = 'select'
|
||||
d.monthSelect = cronItems[3].split(',')
|
||||
}
|
||||
|
||||
if (cronItems[4] === '*') {
|
||||
d.dayWeek = 'all'
|
||||
} else {
|
||||
d.dayWeek = 'select'
|
||||
d.dayWeekSelect = cronItems[4].split(',')
|
||||
}
|
||||
|
||||
assign(data, d)
|
||||
}
|
||||
|
||||
this.prettyCron = prettyCron.toString.bind(prettyCron)
|
||||
|
||||
this.api.setCron = cron => {
|
||||
cronToData(this.data, cron)
|
||||
this.update()
|
||||
}
|
||||
this.api.resetData = this.resetData.bind(this)
|
||||
|
||||
this.init()
|
||||
})
|
||||
|
||||
.name
|
||||
9
app/node_modules/scheduler/package.json
generated
vendored
Normal file
9
app/node_modules/scheduler/package.json
generated
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"private": true,
|
||||
"browserify": {
|
||||
"transform": [
|
||||
"babelify",
|
||||
"browserify-plain-jade"
|
||||
]
|
||||
}
|
||||
}
|
||||
104
app/node_modules/scheduler/view.jade
generated
vendored
Normal file
104
app/node_modules/scheduler/view.jade
generated
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
accordion(ng-if = 'ctrl.data', close-others= 'false', ng-click = 'ctrl.update()')
|
||||
accordion-group
|
||||
accordion-heading Month
|
||||
tabset
|
||||
tab(select = 'ctrl.data.month = "all"', active = 'ctrl.tabs.month.all')
|
||||
tab-heading every month
|
||||
tab(select = 'ctrl.data.month = "select"', active = 'ctrl.tabs.month.select')
|
||||
tab-heading each selected month
|
||||
br
|
||||
table.table.table-bordered
|
||||
tr(ng-repeat = 'line in ctrl.months')
|
||||
td(ng-click = 'ctrl.selectMonth(month.v)', ng-class = '{"bg-success": ctrl.isSelectedMonth(month.v)}',ng-repeat = 'month in line') {{ month.l }}
|
||||
accordion-group
|
||||
accordion-heading Day of the month
|
||||
tabset
|
||||
tab(select = 'ctrl.data.day = "all"', active = 'ctrl.tabs.day.all')
|
||||
tab-heading every day
|
||||
tab(select = 'ctrl.data.day = "select"', active = 'ctrl.tabs.day.select')
|
||||
tab-heading each selected day
|
||||
br
|
||||
p.text-warning
|
||||
i.fa.fa-warning
|
||||
| This selection can restrict or be restricted by "Day of week" selections below. Use the summary preview to ensure your choice.
|
||||
br
|
||||
table.table.table-bordered
|
||||
tr(ng-repeat = 'line in ctrl.days')
|
||||
td(ng-click = 'ctrl.selectDay(day)', ng-class = '{"bg-success": ctrl.isSelectedDay(day)}',ng-repeat = 'day in line') {{ day }}
|
||||
accordion-group
|
||||
accordion-heading Day of week
|
||||
tabset
|
||||
tab(select = 'ctrl.data.dayWeek = "all"', active = 'ctrl.tabs.dayWeek.all')
|
||||
tab-heading every day of week
|
||||
tab(select = 'ctrl.data.dayWeek = "select"', active = 'ctrl.tabs.dayWeek.select')
|
||||
tab-heading each selected day of week
|
||||
br
|
||||
p.text-warning
|
||||
i.fa.fa-warning
|
||||
| This selection can restrict or be restricted by "Day of the month" selections up ahead. Use the summary preview to ensure your choice.
|
||||
br
|
||||
table.table.table-bordered
|
||||
tr
|
||||
td(ng-click = 'ctrl.selectDayWeek(dayWeek.v)', ng-class = '{"bg-success": ctrl.isSelectedDayWeek(dayWeek.v)}',ng-repeat = 'dayWeek in ctrl.dayWeeks') {{ dayWeek.l }}
|
||||
accordion-group
|
||||
accordion-heading Hour
|
||||
button.btn.btn-primary(ng-if = '!ctrl.noHourPlan()', type = 'button', ng-click = 'ctrl.noHourPlan(true)') Plan nothing on a hourly grain
|
||||
button.btn.btn-primary.disabled(ng-if = 'ctrl.noHourPlan()', type = 'button')
|
||||
i.fa.fa-info-circle
|
||||
| Nothing planned on a hourly grain
|
||||
br
|
||||
br
|
||||
tabset
|
||||
tab(select = 'ctrl.data.hour = "all"', active = 'ctrl.tabs.hour.all')
|
||||
tab-heading every hour
|
||||
tab(select = 'ctrl.data.hour = "range"', active = 'ctrl.tabs.hour.range')
|
||||
tab-heading every N hour
|
||||
br
|
||||
.form-group
|
||||
label.col-sm-2.control-label {{ ctrl.data.hourRange }}
|
||||
.col-sm-10
|
||||
input.form-control(type = 'range', min = '2', max = '23', step = '1', ng-model = 'ctrl.data.hourRange', ng-change = 'ctrl.update()')
|
||||
tab(select = 'ctrl.data.hour = "select"', active = 'ctrl.tabs.hour.select')
|
||||
tab-heading each selected hour
|
||||
br
|
||||
table.table.table-bordered
|
||||
tr(ng-repeat = 'line in ctrl.hours')
|
||||
td(ng-click = 'ctrl.selectHour(hour)', ng-class = '{"bg-success": ctrl.isSelectedHour(hour)}',ng-repeat = 'hour in line') {{ hour }}
|
||||
accordion-group
|
||||
accordion-heading Minute
|
||||
button.btn.btn-primary(ng-if = '!ctrl.noMinutePlan()', type = 'button', ng-click = 'ctrl.noMinutePlan(true)') Plan nothing on a minute grain
|
||||
button.btn.btn-primary.disabled(ng-if = 'ctrl.noMinutePlan()', type = 'button')
|
||||
i.fa.fa-info-circle
|
||||
| Nothing planned on a minute grain
|
||||
br
|
||||
br
|
||||
tabset
|
||||
tab(select = 'ctrl.data.min = "all"', active = 'ctrl.tabs.min.all')
|
||||
tab-heading every minute
|
||||
tab(select = 'ctrl.data.min = "range"', active = 'ctrl.tabs.min.range')
|
||||
tab-heading every N minutes
|
||||
br
|
||||
.form-group
|
||||
label.col-sm-2.control-label {{ ctrl.data.minRange }}
|
||||
.col-sm-10
|
||||
input.form-control(type = 'range', min = '2', max = '59', step = '1', ng-model = 'ctrl.data.minRange', ng-change = 'ctrl.update()')
|
||||
tab(select = 'ctrl.data.min = "select"', active = 'ctrl.tabs.min.select')
|
||||
tab-heading each selected minute
|
||||
br
|
||||
table.table.table-bordered
|
||||
tr(ng-repeat = 'line in ctrl.minutes')
|
||||
td(ng-click = 'ctrl.selectMinute(min)', ng-class = '{"bg-success": ctrl.isSelectedMinute(min)}',ng-repeat = 'min in line') {{ min }}
|
||||
input.form-control.hidden(type ='text', readonly, ng-model = 'ctrl.data.cronPattern')
|
||||
.text-center(ng-if = '!ctrl.data'): i.xo-icon-loading
|
||||
div(ng-if = 'ctrl.data')
|
||||
p
|
||||
strong Scheduled to run:
|
||||
| {{ ctrl.prettyCron(ctrl.data.cronPattern) }}
|
||||
.form-inline.container-fluid
|
||||
.form-group
|
||||
label Preview:
|
||||
input.form-control(type = 'range', min = '0', max = '{{ ctrl.data.summary.length - 3 }}', step = '1', ng-model = 'ctrl.data.previewLimit')
|
||||
br
|
||||
ul
|
||||
li(ng-repeat = 'occurence in ctrl.data.summary | limitTo: +ctrl.data.previewLimit+3') {{ occurence }}
|
||||
li ...
|
||||
4
app/node_modules/tag/index.js
generated
vendored
4
app/node_modules/tag/index.js
generated
vendored
@@ -33,6 +33,10 @@ export default angular.module('xoWebApp.tag', [])
|
||||
this.object = get(this.object)
|
||||
}
|
||||
|
||||
$scope.canAdmin = (id) => {
|
||||
return id && xoApi.canInteract(id, 'administrate') || false
|
||||
}
|
||||
|
||||
this.add = (tag) => {
|
||||
tag = trim(tag)
|
||||
if (tag === '') {
|
||||
|
||||
2
app/node_modules/tag/view.jade
generated
vendored
2
app/node_modules/tag/view.jade
generated
vendored
@@ -6,7 +6,7 @@
|
||||
i.fa.fa-times(ng-click = 'ctrl.remove(tag)')
|
||||
|  {{tag}}
|
||||
|  
|
||||
i.fa.add-button(ng-class = '{"fa-plus": !edit, "fa-minus": edit, edit: edit}', ng-click = 'edit = !edit; newTag = ""')
|
||||
i.fa.add-button(ng-class = '{"fa-plus": !edit, "fa-minus": edit, edit: edit}', ng-click = 'edit = !edit; newTag = ""', ng-hide = '!canAdmin(ctrl.object.id)')
|
||||
|  
|
||||
form.form-inline(ng-attr-id = 'xo-tag-{{ctrl.object.id}}',ng-submit = 'ctrl.add(newTag); newTag = ""; edit = false')
|
||||
.input-group(ng-hide = '!edit')
|
||||
|
||||
123
app/node_modules/xo-api/acl.js
generated
vendored
123
app/node_modules/xo-api/acl.js
generated
vendored
@@ -1,123 +0,0 @@
|
||||
// These global variables are not a problem because the algorithm is
|
||||
// synchronous.
|
||||
let permissionsByObject
|
||||
let getObject
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const authorized = () => true // eslint-disable-line no-unused-vars
|
||||
const forbiddden = () => false // eslint-disable-line no-unused-vars
|
||||
|
||||
function and (...checkers) { // eslint-disable-line no-unused-vars
|
||||
return function (object, permission) {
|
||||
for (const checker of checkers) {
|
||||
if (!checker(object, permission)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
function or (...checkers) { // eslint-disable-line no-unused-vars
|
||||
return function (object, permission) {
|
||||
for (const checker of checkers) {
|
||||
if (checker(object, permission)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
function checkMember (memberName) {
|
||||
return function (object, permission) {
|
||||
const member = object[memberName]
|
||||
return checkAuthorization(member, permission)
|
||||
}
|
||||
}
|
||||
|
||||
function checkSelf ({ id }, permission) {
|
||||
const permissionsForObject = permissionsByObject[id]
|
||||
|
||||
return (
|
||||
permissionsForObject &&
|
||||
permissionsForObject[permission]
|
||||
)
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const checkAuthorizationByTypes = {
|
||||
host: or(checkSelf, checkMember('$poolId')),
|
||||
|
||||
message: checkMember('$object'),
|
||||
|
||||
network: or(checkSelf, checkMember('$poolId')),
|
||||
|
||||
SR: or(checkSelf, checkMember('$poolId')),
|
||||
|
||||
task: checkMember('$host'),
|
||||
|
||||
VBD: checkMember('VDI'),
|
||||
|
||||
// Access to a VDI is granted if the user has access to the
|
||||
// containing SR or to a linked VM.
|
||||
VDI (vdi, permission) {
|
||||
// Check authorization for the containing SR.
|
||||
if (checkAuthorization(vdi.$SR, permission)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check authorization for each of the connected VMs.
|
||||
for (const {$VM: vm} of vdi.$VBDs) {
|
||||
if (checkAuthorization(vm, permission)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
|
||||
VIF: or(checkMember('$network'), checkMember('$VM')),
|
||||
|
||||
VM: or(checkSelf, checkMember('$container')),
|
||||
|
||||
'VM-snapshot': checkMember('snapshot_of'),
|
||||
|
||||
'VM-template': authorized
|
||||
}
|
||||
|
||||
function checkAuthorization (objectId, permission) {
|
||||
const object = getObject(objectId)
|
||||
const checker = checkAuthorizationByTypes[object.type] || checkSelf
|
||||
|
||||
return checker(object, permission)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export default function (
|
||||
permissionsByObject_,
|
||||
getObject_,
|
||||
permissions
|
||||
) {
|
||||
// Assign global variables.
|
||||
permissionsByObject = permissionsByObject_
|
||||
getObject = getObject_
|
||||
|
||||
try {
|
||||
for (const [objectId, permission] of permissions) {
|
||||
if (!checkAuthorization(objectId, permission)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
} finally {
|
||||
// Free the global variables.
|
||||
permissionsByObject = getObject = null
|
||||
}
|
||||
}
|
||||
3
app/node_modules/xo-api/index.js
generated
vendored
3
app/node_modules/xo-api/index.js
generated
vendored
@@ -1,5 +1,6 @@
|
||||
import angular from 'angular'
|
||||
import angularCookies from 'angular-cookies'
|
||||
import checkPermissions from 'xo-acl-resolver'
|
||||
import cloneDeep from 'lodash.clonedeep'
|
||||
import forEach from 'lodash.foreach'
|
||||
import indexOf from 'lodash.indexof'
|
||||
@@ -9,8 +10,6 @@ import xoLib from 'xo-lib'
|
||||
import XoUniqueIndex from 'xo-collection/unique-index'
|
||||
import XoView from 'xo-collection/view'
|
||||
|
||||
import checkPermissions from './acl'
|
||||
|
||||
const {defineProperty} = Object
|
||||
const {isArray, isString} = angular
|
||||
|
||||
|
||||
80
app/node_modules/xo-filters/index.js
generated
vendored
80
app/node_modules/xo-filters/index.js
generated
vendored
@@ -1,6 +1,8 @@
|
||||
import angular from 'angular'
|
||||
import forEach from 'lodash.foreach'
|
||||
import includes from 'lodash.includes'
|
||||
import isEmpty from 'lodash.isempty'
|
||||
import isNumber from 'lodash.isnumber'
|
||||
import map from 'lodash.map'
|
||||
import slice from 'lodash.slice'
|
||||
import xoApi from 'xo-api'
|
||||
@@ -17,7 +19,11 @@ export default angular.module('xoWebApp.filters', [
|
||||
.filter('bytesToSize', () => {
|
||||
const powers = ['', 'K', 'M', 'G', 'T', 'P']
|
||||
|
||||
return function bytesToSize (bytes, unit = 'B', base = 1024) {
|
||||
return function bytesToSize (bytes, unit = 'iB', base = 1024) {
|
||||
if (!isNumber(bytes)) {
|
||||
return bytes
|
||||
}
|
||||
|
||||
let i = 0
|
||||
while (bytes >= base) {
|
||||
bytes /= base
|
||||
@@ -31,47 +37,62 @@ export default angular.module('xoWebApp.filters', [
|
||||
// Maximum 1 decimals.
|
||||
bytes = ((bytes * 10) | 0) / 10
|
||||
|
||||
return `${bytes}${powers[i]}${unit}`
|
||||
return `${bytes} ${powers[i]}${unit}`
|
||||
}
|
||||
})
|
||||
|
||||
// Takes a size (eg.: '8 GiB') and returns a number of bytes
|
||||
.filter('sizeToBytes', () => {
|
||||
/* eslint no-multi-spaces: 0 */
|
||||
const powers = ['', 'K', 'M', 'G', 'T', 'P']
|
||||
|
||||
const RE = new RegExp('^' +
|
||||
'(\\d+(?:\\.\\d+)?)' + // digits ('.' digits)?
|
||||
'\\s*' + // Optional spaces between the digits and the unit.
|
||||
'([kmgtp])?' + // Optional unit modifier K/M/G/T/P.
|
||||
'b?' + // Optional unit (“b”), not meaningful.
|
||||
'$', 'i')
|
||||
return function sizeToBytes (size, unit = 'iB', base = 1024) {
|
||||
const split = size.split(' ')
|
||||
if (split.length !== 2) {
|
||||
return size
|
||||
}
|
||||
let number = parseInt(split[0], 10)
|
||||
const powerUnit = split[1]
|
||||
const power = powerUnit.slice(0, 1)
|
||||
const effUnit = powerUnit.slice(1)
|
||||
|
||||
const factors = {
|
||||
k: 1024,
|
||||
m: 1048576,
|
||||
g: 1073741824,
|
||||
t: 1099511627776,
|
||||
p: 1125899906842624
|
||||
if (!includes(powers, power) || unit !== effUnit) {
|
||||
return size
|
||||
}
|
||||
|
||||
let i = 0
|
||||
while (powers[i] !== power) {
|
||||
number *= base
|
||||
++i
|
||||
}
|
||||
|
||||
return number
|
||||
}
|
||||
})
|
||||
|
||||
return function sizeToBytes (size) {
|
||||
const matches = RE.exec(size)
|
||||
// Takes a number of bytes and converts it to the desired unit (GiB, MiB, etc.).
|
||||
.filter('bytesConvert', () => {
|
||||
return function bytesConvert (bytes, powerUnit = 'GiB', unit = 'iB', base = 1024) {
|
||||
let powerUnits = ['', 'K', 'M', 'G', 'T', 'P']
|
||||
powerUnits = powerUnits.map(pu => pu + unit)
|
||||
|
||||
// If the input is invalid, just returns null.
|
||||
if (!matches) {
|
||||
return null
|
||||
if (!isNumber(bytes) || !includes(powerUnits, powerUnit)) {
|
||||
return bytes
|
||||
}
|
||||
|
||||
let value = +matches[1]
|
||||
|
||||
const modifier = matches[2]
|
||||
if (modifier) {
|
||||
const factor = factors[modifier.toLowerCase()]
|
||||
if (factor) {
|
||||
value *= factor
|
||||
}
|
||||
let i = 0
|
||||
while (powerUnit !== powerUnits[i]) {
|
||||
bytes /= base
|
||||
++i
|
||||
}
|
||||
|
||||
return Math.round(value)
|
||||
if (bytes === -1) {
|
||||
return '-'
|
||||
}
|
||||
|
||||
// Maximum 1 decimals.
|
||||
bytes = ((bytes * 10) | 0) / 10
|
||||
|
||||
return bytes
|
||||
}
|
||||
})
|
||||
|
||||
@@ -105,6 +126,7 @@ export default angular.module('xoWebApp.filters', [
|
||||
|
||||
.filter('isEmpty', () => isEmpty)
|
||||
.filter('isNotEmpty', () => (collection) => !isEmpty(collection))
|
||||
.filter('includes', () => includes)
|
||||
|
||||
.filter('duration', () => (n, unit = 'ms') => (n > 0 && moment.duration(n, unit).humanize() || ''))
|
||||
|
||||
|
||||
75
app/node_modules/xo/index.js
generated
vendored
75
app/node_modules/xo/index.js
generated
vendored
@@ -72,9 +72,21 @@ export default angular.module('xo', [
|
||||
console.error('Error for %s:', method, error)
|
||||
|
||||
if (notification !== false) {
|
||||
const message = (error && error.code === 2)
|
||||
? 'You don\'t have the permission.'
|
||||
: 'The action failed for unknown reason.'
|
||||
let message
|
||||
|
||||
if (error) {
|
||||
const code = error.code
|
||||
|
||||
if (code === 2) {
|
||||
message = 'You don\'t have the permission.'
|
||||
} else if (code === 5) {
|
||||
message = error.data
|
||||
}
|
||||
}
|
||||
|
||||
if (!message) {
|
||||
message = 'The action failed for unknown reason.'
|
||||
}
|
||||
|
||||
xoNotify.warning({
|
||||
title: name,
|
||||
@@ -104,6 +116,9 @@ export default angular.module('xo', [
|
||||
disk: {
|
||||
create: action('Create disk', 'disk.create', {
|
||||
argsMapper: (name, size, sr) => ({name, size, sr})
|
||||
}),
|
||||
resize: action('Resize disk', 'disk.resize', {
|
||||
argsMapper: (id, size) => ({id, size})
|
||||
})
|
||||
},
|
||||
|
||||
@@ -113,7 +128,10 @@ export default angular.module('xo', [
|
||||
patch: action('Upload patch', 'pool.patch', {
|
||||
argsMapper: (pool) => ({pool})
|
||||
}),
|
||||
mergeInto: action('Merge pools', 'pool.mergeInto')
|
||||
mergeInto: action('Merge pools', 'pool.mergeInto'),
|
||||
setDefaultSr: action('Set the default SR', 'pool.setDefaultSr', {
|
||||
argsMapper: (pool, sr) => ({pool, sr})
|
||||
})
|
||||
},
|
||||
|
||||
host: {
|
||||
@@ -129,12 +147,15 @@ export default angular.module('xo', [
|
||||
listMissingPatches: action('Check available patches', 'host.listMissingPatches', {
|
||||
argsMapper: (host) => ({host})
|
||||
}),
|
||||
installPatch: action('Install a patch from a patch id', 'host.installPatch', {
|
||||
installPatch: action('Install a patch from a patch id', 'host.installPatch', {
|
||||
argsMapper: (host, patch) => ({host, patch})
|
||||
}),
|
||||
installAllPatches: action('Install all the missing patches on a host', 'host.installAllPatches', {
|
||||
argsMapper: (host) => ({host})
|
||||
}),
|
||||
emergencyShutdownHost: action('Suspend all VMs running on host and shutdown host', 'host.emergencyShutdownHost', {
|
||||
argsMapper: (host) => ({host})
|
||||
}),
|
||||
refreshStats: action('Get Stats', 'host.stats', {
|
||||
notification: false,
|
||||
argsMapper: (host, granularity) => ({host, granularity})
|
||||
@@ -148,6 +169,9 @@ export default angular.module('xo', [
|
||||
logs: {
|
||||
get: action('Returns logs for one namespace', 'log.get', {
|
||||
argsMapper: (namespace) => ({namespace})
|
||||
}),
|
||||
delete: action('Delete on or several logs for one namespace', 'log.delete', {
|
||||
argsMapper: (namespace, id) => ({namespace, id})
|
||||
})
|
||||
},
|
||||
message: {
|
||||
@@ -228,6 +252,12 @@ export default angular.module('xo', [
|
||||
}),
|
||||
restart: action('Restart a Docker Container', 'docker.restart', {
|
||||
argsMapper: (vm, container) => ({vm, container})
|
||||
}),
|
||||
register: action('Register the VM for the Docker plugin', 'docker.register', {
|
||||
argsMapper: (vm) => ({ vm })
|
||||
}),
|
||||
deregister: action('Deregister the VM for the Docker plugin', 'docker.deregister', {
|
||||
argsMapper: (vm) => ({ vm })
|
||||
})
|
||||
},
|
||||
|
||||
@@ -262,16 +292,26 @@ export default angular.module('xo', [
|
||||
import: action('Import VM', 'vm.import', {
|
||||
argsMapper: (host) => ({ host })
|
||||
}),
|
||||
importBackup: action('Imports a VM from a remote point', 'vm.importBackup', {
|
||||
argsMapper: (remote, file, sr) => ({ remote, file, sr })
|
||||
}),
|
||||
importDeltaBackup: action('Imports a delta backup from a remote point', 'vm.importDeltaBackup', {
|
||||
argsMapper: (remote, filePath, sr) => ({ remote, filePath, sr })
|
||||
}),
|
||||
migrate: action('Migrate VM', 'vm.migrate', {
|
||||
argsMapper: (id, host_id) => ({ id, host_id })
|
||||
argsMapper:
|
||||
(vm, targetHost, mapVdisSrs = undefined, mapVifsNetworks = undefined, migrationNetwork = undefined) =>
|
||||
({ vm, targetHost, mapVdisSrs, mapVifsNetworks, migrationNetwork })
|
||||
}),
|
||||
restart: action('Restart VM', 'vm.restart', {
|
||||
argsMapper: (id, force = false) => ({ id, force })
|
||||
}),
|
||||
recoveryStart: action('Start VM in recovery mode', 'vm.recoveryStart'),
|
||||
start: action('Start VM', 'vm.start'),
|
||||
stop: action('Stop VM', 'vm.stop', {
|
||||
argsMapper: (id, force = false) => ({ id, force })
|
||||
}),
|
||||
setBootOrder: action('Set the boot order', 'vm.setBootOrder'),
|
||||
revert: action('Revert snapshot', 'vm.revert'),
|
||||
suspend: action('Suspend VM', 'vm.suspend'),
|
||||
resume: action('Resume VM', 'vm.resume', {
|
||||
@@ -281,6 +321,12 @@ export default angular.module('xo', [
|
||||
notification: false,
|
||||
argsMapper: (id, granularity) => ({id, granularity})
|
||||
}),
|
||||
getCloudInitConfig: action('Get Cloud Init Template', 'vm.getCloudInitConfig', {
|
||||
argsMapper: (template) => ({ template })
|
||||
}),
|
||||
createCloudInitConfigDrive: action('Create Cloud Config Drive', 'vm.createCloudInitConfigDrive', {
|
||||
argsMapper: (vm, sr, config, coreos) => ({ vm, sr, config, coreos })
|
||||
}),
|
||||
// TODO: create/set/pause
|
||||
connectPci: action('Connect PCI device', 'vm.attachPci', {
|
||||
argsMapper: (vm, pciId) => ({vm, pciId})
|
||||
@@ -304,6 +350,9 @@ export default angular.module('xo', [
|
||||
},
|
||||
|
||||
vbd: {
|
||||
setBootable: action('Set bootable VBD', 'vbd.setBootable', {
|
||||
argsMapper: (vbd, bootable) => ({vbd, bootable})
|
||||
}),
|
||||
delete: action('Delete VBD', 'vbd.delete'),
|
||||
disconnect: action('Disconnect VBD', 'vbd.disconnect'),
|
||||
connect: action('Connect VBD', 'vbd.connect')
|
||||
@@ -319,20 +368,24 @@ export default angular.module('xo', [
|
||||
}),
|
||||
delete: action('Delete a job', 'job.delete', {
|
||||
argsMapper: (id) => ({id})
|
||||
}),
|
||||
runSequence: action('Run a sequence of jobs', 'job.runSequence', {
|
||||
argsMapper: (idSequence) => ({idSequence})
|
||||
})
|
||||
},
|
||||
|
||||
schedule: {
|
||||
getAll: action('Get all schedules', 'schedule.getAll'),
|
||||
create: action('Create a schedule', 'schedule.create', {
|
||||
argsMapper: (jobId, cron, enabled) => ({jobId, cron, enabled})
|
||||
argsMapper: (jobId, cron, enabled, name) => ({jobId, cron, enabled, name})
|
||||
}),
|
||||
set: action('Modify a schedule', 'schedule.set', {
|
||||
argsMapper: (id, jobId = undefined, cron = undefined, enabled = undefined) => {
|
||||
argsMapper: (id, jobId = undefined, cron = undefined, enabled = undefined, name = undefined) => {
|
||||
const args = {id}
|
||||
jobId !== undefined && (args.jobId = jobId)
|
||||
cron !== undefined && (args.cron = cron)
|
||||
enabled !== undefined && (args.enabled = enabled)
|
||||
name !== undefined && (args.name = name)
|
||||
return args
|
||||
}
|
||||
}),
|
||||
@@ -370,9 +423,6 @@ export default angular.module('xo', [
|
||||
}),
|
||||
list: action('List files found at the remote point', 'remote.list', {
|
||||
argsMapper: id => ({id})
|
||||
}),
|
||||
importVm: action('Imports a VM form a remote point', 'remote.importVm', {
|
||||
argsMapper: (id, file, host) => ({id, file, host})
|
||||
})
|
||||
},
|
||||
|
||||
@@ -401,6 +451,9 @@ export default angular.module('xo', [
|
||||
}),
|
||||
unload: action('Unloads a plugin', 'plugin.unload', {
|
||||
argsMapper: (id) => ({id})
|
||||
}),
|
||||
purgeConfiguration: action('Purge a plugin configuration', 'plugin.purgeConfiguration', {
|
||||
argsMapper: (id) => ({id})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -445,6 +445,18 @@ html, body, .view-main {
|
||||
background-color: #2e3133;
|
||||
}
|
||||
|
||||
// transparent background for dropdown buttons
|
||||
.filter {
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
border: 0;
|
||||
color: #9d9d9d;
|
||||
box-shadow: none !important;
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0) !important;
|
||||
color: white !important;
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Main view
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -75,7 +75,6 @@ td.grab {padding: 0 !important; margin: 0 !important; width: 6px !important; cur
|
||||
tr:hover .grab {background: url("../images/grip.png") no-repeat scroll 1px 50% transparent !important}
|
||||
|
||||
table { table-layout: fixed; }
|
||||
table th, table td { overflow: hidden; }
|
||||
|
||||
td.vm-power-state {width: 20px; text-align: center;}
|
||||
|
||||
|
||||
11
package.json
11
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xo-web",
|
||||
"version": "4.9.0",
|
||||
"version": "4.13.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Web interface client for Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -27,6 +27,7 @@
|
||||
"browserify": "^12.0.1",
|
||||
"browserify-plain-jade": "^0.2.2",
|
||||
"bundle-collapser": "^1.1.4",
|
||||
"clipboard": "^1.5.5",
|
||||
"coffeeify": "^1.0.0",
|
||||
"d3": "^3.5.5",
|
||||
"event-stream": "^3.3.0",
|
||||
@@ -60,8 +61,10 @@
|
||||
"lodash.indexof": "^3.0.2",
|
||||
"lodash.intersection": "^3.1.0",
|
||||
"lodash.isempty": "^3.0.3",
|
||||
"lodash.isnumber": "^3.0.1",
|
||||
"lodash.keys": "^3.1.2",
|
||||
"lodash.map": "^3.1.2",
|
||||
"lodash.mapvalues": "^3.0.1",
|
||||
"lodash.omit": "^3.1.0",
|
||||
"lodash.pluck": "^3.1.0",
|
||||
"lodash.pull": "^3.0.1",
|
||||
@@ -71,7 +74,7 @@
|
||||
"lodash.sortby": "^3.1.0",
|
||||
"lodash.sum": "^3.6.1",
|
||||
"lodash.throttle": "^3.0.1",
|
||||
"lodash.trim": "^3.0.1",
|
||||
"lodash.trim": "3.0.1",
|
||||
"lodash.union": "^3.1.0",
|
||||
"make-error": "^1.0.2",
|
||||
"marked": "^0.3.5",
|
||||
@@ -85,8 +88,10 @@
|
||||
"vinyl": "^1.1.0",
|
||||
"watchify": "^3.1.1",
|
||||
"ws": "^0.8.0",
|
||||
"xo-acl-resolver": "0.0.0-0",
|
||||
"xo-collection": "^0.4.0",
|
||||
"xo-lib": "^0.7.3"
|
||||
"xo-lib": "^0.7.3",
|
||||
"xo-remote-parser": "^0.1.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
Reference in New Issue
Block a user