Compare commits
728 Commits
xo-web/v4.
...
v4.16.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ffd95261c3 | ||
|
|
82f38040c1 | ||
|
|
0eadfd5a58 | ||
|
|
eea34a4f6c | ||
|
|
ca525bd08c | ||
|
|
ac2ffc4586 | ||
|
|
5781269557 | ||
|
|
e4422b9fe7 | ||
|
|
269f76d546 | ||
|
|
540e3f0aaa | ||
|
|
5f64ae28e0 | ||
|
|
f669f64fcb | ||
|
|
be2db2dd8e | ||
|
|
9ccd3438ad | ||
|
|
c6a0874b3b | ||
|
|
9c80470185 | ||
|
|
fd8da5ffba | ||
|
|
e987af87f6 | ||
|
|
0074cc3933 | ||
|
|
5f2ce89316 | ||
|
|
60492c48a6 | ||
|
|
eed2d70017 | ||
|
|
b859adaa8c | ||
|
|
89a587f9ae | ||
|
|
fb56bcff80 | ||
|
|
99eb6907dd | ||
|
|
3743fad899 | ||
|
|
c1e59a7e03 | ||
|
|
b34dee1f83 | ||
|
|
6edd65ad8f | ||
|
|
0959ca6a40 | ||
|
|
1287fa2cd0 | ||
|
|
a5a07f250d | ||
|
|
089fb526f5 | ||
|
|
af58b7593a | ||
|
|
d4508b25ce | ||
|
|
9edc218eaa | ||
|
|
3790f753aa | ||
|
|
8ce3a4f904 | ||
|
|
be0b9c7e53 | ||
|
|
6d75cd9025 | ||
|
|
345d6f369e | ||
|
|
959ea86d85 | ||
|
|
b67a99af3d | ||
|
|
fa3b848d40 | ||
|
|
0f971e9e7d | ||
|
|
c17f76c009 | ||
|
|
bf23b5d295 | ||
|
|
09c7256d42 | ||
|
|
eaee8a2fbb | ||
|
|
3b18dd67be | ||
|
|
c3f87b4248 | ||
|
|
1c79edc52f | ||
|
|
fe2dfd0e8f | ||
|
|
fa6056c1b1 | ||
|
|
d5762c7ad8 | ||
|
|
d9c9dd2a4f | ||
|
|
3a4d945c68 | ||
|
|
f4a364816b | ||
|
|
931bc03cab | ||
|
|
1abd4937cd | ||
|
|
0df8b51c62 | ||
|
|
e5b7190015 | ||
|
|
279b8aacf6 | ||
|
|
9eebaab2f4 | ||
|
|
16e9d60033 | ||
|
|
335b378e9a | ||
|
|
9c41bc33a3 | ||
|
|
7f7d6b4d5d | ||
|
|
cc0e3bbce0 | ||
|
|
2eead65fef | ||
|
|
e664be451f | ||
|
|
d2d8160096 | ||
|
|
3bd503c28d | ||
|
|
aa1df8eb33 | ||
|
|
c1aace45ae | ||
|
|
217a60aadc | ||
|
|
5654f528ca | ||
|
|
4da036a064 | ||
|
|
2256b3d262 | ||
|
|
d84ecc307d | ||
|
|
237313d5fb | ||
|
|
7caf766bca | ||
|
|
0a3f9f5ef1 | ||
|
|
e890b8f7c1 | ||
|
|
dc4d5f0ecb | ||
|
|
a2f0980731 | ||
|
|
0a5c029f8b | ||
|
|
85bb79e4fb | ||
|
|
f18d1e50f8 | ||
|
|
943b10dd5d | ||
|
|
0a48e17c88 | ||
|
|
da1381e14e | ||
|
|
bdffb0ee10 | ||
|
|
7bdb7d2ca8 | ||
|
|
92567561b8 | ||
|
|
335bdcd89d | ||
|
|
4c2fc13abb | ||
|
|
7f8f29daa2 | ||
|
|
8fac845ecb | ||
|
|
d8076e7630 | ||
|
|
b370bc27c4 | ||
|
|
334c3f4488 | ||
|
|
33822109c0 | ||
|
|
b1f18b0f5b | ||
|
|
c0d6284368 | ||
|
|
16e294f6fc | ||
|
|
5f7925b2b8 | ||
|
|
2e001b0ce4 | ||
|
|
c1ca3ff5b5 | ||
|
|
1de33cd4ca | ||
|
|
77b773388f | ||
|
|
3e668ee439 | ||
|
|
0d3ea9af36 | ||
|
|
0d81bc8056 | ||
|
|
e4b532a34d | ||
|
|
61f86c0ac3 | ||
|
|
66fad37116 | ||
|
|
c7bbd8c823 | ||
|
|
dc6f8baf1e | ||
|
|
0e76e65d65 | ||
|
|
877dbed999 | ||
|
|
668fd05fae | ||
|
|
3e49998f41 | ||
|
|
99b183ac17 | ||
|
|
bb04cddc48 | ||
|
|
ba3f095dd8 | ||
|
|
f8438421c8 | ||
|
|
334361860b | ||
|
|
23bd211758 | ||
|
|
4cc8fb9891 | ||
|
|
ed3cd690fe | ||
|
|
9a40c7cdc6 | ||
|
|
ae4c9ce819 | ||
|
|
2f8bae1356 | ||
|
|
2dfcd5b7ef | ||
|
|
739926f64e | ||
|
|
a18dde07de | ||
|
|
6480017a91 | ||
|
|
4467ec52f7 | ||
|
|
072d82a10e | ||
|
|
b50b759f4f | ||
|
|
c1477ad45f | ||
|
|
679d45399b | ||
|
|
0e15a789ff | ||
|
|
dff5b3f497 | ||
|
|
483e49a6ae | ||
|
|
a689b5b917 | ||
|
|
1363d98280 | ||
|
|
405f3dcbdd | ||
|
|
4b48408bc9 | ||
|
|
d6f1e2d7e2 | ||
|
|
c04c8e3aa4 | ||
|
|
ff5a08d3b0 | ||
|
|
d2049c759e | ||
|
|
f940cb0ace | ||
|
|
507f2f4af4 | ||
|
|
deca7099f3 | ||
|
|
46a741825a | ||
|
|
14fdcd3052 | ||
|
|
c76b01608a | ||
|
|
be709d6601 | ||
|
|
7dc8fac198 | ||
|
|
29ae7d57fd | ||
|
|
1bf9ce872b | ||
|
|
0071c9504f | ||
|
|
594a872c84 | ||
|
|
0d1f78e82e | ||
|
|
a12de51897 | ||
|
|
74fa084dd0 | ||
|
|
a1ee258da5 | ||
|
|
c1af171c5d | ||
|
|
e36c9560fa | ||
|
|
5cd19ddc8d | ||
|
|
ac243e5d11 | ||
|
|
b480d019f6 | ||
|
|
cc13ab97d6 | ||
|
|
fee1d2ed04 | ||
|
|
4144d5faa6 | ||
|
|
38a23c0bee | ||
|
|
8fcfebe170 | ||
|
|
f917fa8138 | ||
|
|
f60b611304 | ||
|
|
a54624e5c8 | ||
|
|
370b14b82e | ||
|
|
88205adeb2 | ||
|
|
351ce995d9 | ||
|
|
a9aa92de90 | ||
|
|
9ea665dea2 | ||
|
|
30b52527e7 | ||
|
|
bb4125153b | ||
|
|
f0d5b2b1da | ||
|
|
30c4048e4a | ||
|
|
93770ca9ce | ||
|
|
e788783d12 | ||
|
|
1314444d7c | ||
|
|
2a14664d34 | ||
|
|
07a03940a0 | ||
|
|
5f2f6fff56 | ||
|
|
353548660c | ||
|
|
912f07225c | ||
|
|
a23b7eeff1 | ||
|
|
574f0d71b2 | ||
|
|
a368312035 | ||
|
|
2f88b1ab65 | ||
|
|
f85f97e061 | ||
|
|
30dec13903 | ||
|
|
3057e5c997 | ||
|
|
6c413eb1ba | ||
|
|
dcdd9132e2 | ||
|
|
d9b1c36055 | ||
|
|
a593a247d7 | ||
|
|
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 | ||
|
|
0c173fde53 | ||
|
|
77db2bbfec | ||
|
|
2987185a9d | ||
|
|
7a7baf7175 | ||
|
|
5645cc0af2 | ||
|
|
63a6756fed | ||
|
|
9f408c98a6 | ||
|
|
26d6998d82 | ||
|
|
6bee44acb7 | ||
|
|
441992cf37 | ||
|
|
490c224ac3 | ||
|
|
f5c55048de | ||
|
|
8139e124c2 | ||
|
|
cba73a5139 | ||
|
|
de0c9367e5 | ||
|
|
630060860c | ||
|
|
dccd11fb7b | ||
|
|
b8a4b2cf16 | ||
|
|
e52f55bfba | ||
|
|
1cb99e02a9 | ||
|
|
c9c5c35e56 | ||
|
|
bc7c9f9c01 | ||
|
|
e4bfc4cb8d | ||
|
|
d64995c4a1 | ||
|
|
2952ea7404 | ||
|
|
f34c807a2c | ||
|
|
b1f9704055 | ||
|
|
9382829ba5 | ||
|
|
373a6ea912 | ||
|
|
72eb4e7b3b | ||
|
|
315c0870ed | ||
|
|
200fa621bf | ||
|
|
80348c1980 | ||
|
|
856dd8403c | ||
|
|
0bb9acd4c1 | ||
|
|
047a80917f | ||
|
|
e6e8fe4763 | ||
|
|
6cd212398e | ||
|
|
44ad6d4247 | ||
|
|
7302782853 | ||
|
|
7fa1aba6b8 | ||
|
|
2fed4e3e8b | ||
|
|
bd343c51a3 | ||
|
|
8a05f06efa | ||
|
|
27b049eada | ||
|
|
2d1afb5291 | ||
|
|
63c17a3abf | ||
|
|
94f9bc5fca | ||
|
|
ab273430d2 | ||
|
|
0b3dc315ad | ||
|
|
f26a2d2f13 | ||
|
|
8edf9bf508 | ||
|
|
415381cebd | ||
|
|
59accec1c0 | ||
|
|
dd2699fcc1 | ||
|
|
0986a5f985 | ||
|
|
fa77229b72 | ||
|
|
78b5080c9a | ||
|
|
0641da786c | ||
|
|
3291f3bb3c | ||
|
|
a0cfef8bda | ||
|
|
4d033f4a03 | ||
|
|
562820180c | ||
|
|
a29832207e | ||
|
|
2afd549826 | ||
|
|
8a71b2c6dd | ||
|
|
d633d2691d | ||
|
|
f9b1608fd2 | ||
|
|
4d8ed3f00e | ||
|
|
359e7d0543 | ||
|
|
07bf93e022 | ||
|
|
57e27da0c4 | ||
|
|
9ecbf62d25 | ||
|
|
48ffa591ca | ||
|
|
b7dd617bb1 | ||
|
|
392f9d0775 | ||
|
|
4361b11c68 | ||
|
|
28bccad010 | ||
|
|
29d31a0deb | ||
|
|
1d9960d349 | ||
|
|
2747b241ab | ||
|
|
6b8035b116 | ||
|
|
33ad5f4d45 | ||
|
|
af7ad9251a | ||
|
|
4ec9975aa3 | ||
|
|
c6b0841583 | ||
|
|
9312435076 | ||
|
|
49427f1c54 | ||
|
|
82e7e06dc4 | ||
|
|
76cf82bb19 | ||
|
|
4f8ad2962e | ||
|
|
fe4be48bff | ||
|
|
66fc0b421b | ||
|
|
c0b4867659 | ||
|
|
95253fbc76 | ||
|
|
df519b3042 | ||
|
|
9ed963ef70 | ||
|
|
1dd7993e7a | ||
|
|
386b33b65d | ||
|
|
416deb8711 | ||
|
|
3c7fdac55e | ||
|
|
392a6af47f | ||
|
|
6b03e3f603 | ||
|
|
9397f6beda | ||
|
|
d17b386fd6 | ||
|
|
f6d2e1a447 | ||
|
|
bd95ef5db6 | ||
|
|
6e76c621b8 | ||
|
|
3e58bee0eb | ||
|
|
8c2ed1f581 | ||
|
|
500dd3bfaf | ||
|
|
bc7bacd654 | ||
|
|
fa16b990b6 | ||
|
|
9d5e9dd9e5 | ||
|
|
4046f9dde1 | ||
|
|
fcd82ada14 | ||
|
|
d616da7f67 | ||
|
|
0c81202bbb | ||
|
|
6284bd3f17 | ||
|
|
7adc9d94b4 | ||
|
|
73e030d2f5 | ||
|
|
3a3b45aa04 | ||
|
|
81c19e9964 | ||
|
|
df856bc4a0 | ||
|
|
8558dc7ee4 | ||
|
|
087d5f6e58 | ||
|
|
9540bc350a | ||
|
|
09153c6c30 | ||
|
|
f66d81f147 | ||
|
|
75925143b6 | ||
|
|
e7dc00991e | ||
|
|
dd9da82ed3 | ||
|
|
c995b8fa81 | ||
|
|
e7c2994ea3 | ||
|
|
106997b26c | ||
|
|
fa842c1566 | ||
|
|
be03dd82f9 | ||
|
|
39c46995e1 | ||
|
|
97adc01e8d | ||
|
|
36be881741 | ||
|
|
955cc6dff5 | ||
|
|
2be1399eda | ||
|
|
ef5d2a7654 | ||
|
|
1cd00cab62 | ||
|
|
7652c231f6 | ||
|
|
e17cdf0ca7 | ||
|
|
3317791e68 | ||
|
|
c3871bc2ec | ||
|
|
ebba86f741 | ||
|
|
5ac84a6a02 | ||
|
|
cf3e9704e8 | ||
|
|
37eac8afcf | ||
|
|
692a0535ff | ||
|
|
0f0d804052 | ||
|
|
d189e6b53d | ||
|
|
5da31691a9 | ||
|
|
4059a4fd9a | ||
|
|
e56da71856 | ||
|
|
91e10f627f | ||
|
|
338c686e8d | ||
|
|
0007e9ea2b | ||
|
|
1e09e9b322 | ||
|
|
43c358119a | ||
|
|
7a0f251ebd | ||
|
|
e989321c5f | ||
|
|
56f27e6aaa | ||
|
|
7c4e5aa667 | ||
|
|
d253d826bb | ||
|
|
888fa20ca3 | ||
|
|
598dbb2b7a | ||
|
|
71eb1eab14 | ||
|
|
62a6bd99e8 | ||
|
|
174cdf2149 | ||
|
|
5267fbce7b | ||
|
|
dd814e7e95 | ||
|
|
f806b45d3d | ||
|
|
8575e9eabe | ||
|
|
6ff9e22049 | ||
|
|
caa86fdab7 | ||
|
|
48246716cc | ||
|
|
6e07429e8a | ||
|
|
1a271c32b6 | ||
|
|
3fddec8f20 | ||
|
|
ac3944aece | ||
|
|
958cc2a50c | ||
|
|
058dfcfa9f | ||
|
|
9dbb1ca386 | ||
|
|
4d1def6e9d | ||
|
|
ff763b0278 | ||
|
|
74f611e0fd | ||
|
|
61f8be1c60 | ||
|
|
96b18dab00 | ||
|
|
0a21b239bc | ||
|
|
9c3589aea4 | ||
|
|
2433485d13 | ||
|
|
6b5f254e0a | ||
|
|
e1b41b1e26 | ||
|
|
bd7a265df0 | ||
|
|
039cca9529 | ||
|
|
963347dbc2 | ||
|
|
697cc9f758 | ||
|
|
3892225584 | ||
|
|
a7880a0ef5 | ||
|
|
dd574830f5 | ||
|
|
71a0d15c35 | ||
|
|
8a33c4f09a | ||
|
|
d223ce062a | ||
|
|
39c8f12963 | ||
|
|
bd4ba8c826 | ||
|
|
3d38c8e088 | ||
|
|
ce58c80c6d | ||
|
|
19b3a0781c | ||
|
|
b42c1971b9 | ||
|
|
02440941e0 | ||
|
|
cd2f986c50 | ||
|
|
e7cbd6b31f | ||
|
|
a7f6d5eebd | ||
|
|
4f3b8c0906 | ||
|
|
7126c71943 | ||
|
|
489cf16af8 | ||
|
|
b012f44259 | ||
|
|
5ce765bd27 | ||
|
|
2450edd070 | ||
|
|
fc2a61835c | ||
|
|
d06d73d5f7 | ||
|
|
2c10996bb3 | ||
|
|
b9c85bb1bf | ||
|
|
f436afb9aa | ||
|
|
750efe4152 | ||
|
|
99cee95cd5 | ||
|
|
ec10b84fa6 | ||
|
|
f0442fe2ce | ||
|
|
7907969696 | ||
|
|
8dbab73d2b | ||
|
|
ade8acb4e2 | ||
|
|
9cb78e6954 | ||
|
|
e9127bdbb3 | ||
|
|
9b750bc756 | ||
|
|
c3349e8cc7 | ||
|
|
f4d7c7f739 | ||
|
|
19d51cb1a4 | ||
|
|
cc9983aa16 | ||
|
|
81f8467f66 | ||
|
|
df6b23e3c7 | ||
|
|
4dd81e7d59 | ||
|
|
54ce7067b4 | ||
|
|
2673f790e6 | ||
|
|
69bea2ec9b | ||
|
|
37037cf797 | ||
|
|
2a1586aab3 | ||
|
|
915281d138 | ||
|
|
b53a179ea0 | ||
|
|
7077e8b50e | ||
|
|
d9181277d9 | ||
|
|
810c2d6a1a | ||
|
|
f31113fb90 | ||
|
|
1702b9dd37 | ||
|
|
f8e61c713c | ||
|
|
643132754a | ||
|
|
4f0a131bd2 | ||
|
|
52aa0350cf | ||
|
|
c9884f32fe | ||
|
|
aea3ae3d37 | ||
|
|
10f7c3045f | ||
|
|
bf4e158c30 | ||
|
|
4a3155ed22 | ||
|
|
29f1c89fa5 | ||
|
|
4a92e8a99f | ||
|
|
c6cffb1156 | ||
|
|
f6e4e59905 | ||
|
|
3a0736c4bf | ||
|
|
47455b2029 | ||
|
|
05eb7d765f | ||
|
|
5e786686d0 | ||
|
|
5cb8e3a7c3 |
16
.babelrc
@@ -1,14 +1,12 @@
|
||||
{
|
||||
"comments": false,
|
||||
"compact": true,
|
||||
"optional": [
|
||||
// Experimental features.
|
||||
// "minification.constantFolding",
|
||||
// "minification.deadCodeElimination",
|
||||
|
||||
"es7.asyncFunctions",
|
||||
"es7.decorators",
|
||||
"es7.functionBind"
|
||||
//"runtime"
|
||||
"plugins": [
|
||||
"transform-decorators-legacy",
|
||||
"transform-runtime"
|
||||
],
|
||||
"presets": [
|
||||
"stage-0",
|
||||
"es2015"
|
||||
]
|
||||
}
|
||||
|
||||
10
.travis.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- 'stable'
|
||||
|
||||
# Use containers.
|
||||
# http://docs.travis-ci.com/user/workers/container-based-infrastructure/
|
||||
sudo: false
|
||||
|
||||
before_install:
|
||||
- npm i -g npm
|
||||
390
CHANGELOG.md
@@ -1,5 +1,395 @@
|
||||
# ChangeLog
|
||||
|
||||
## **4.16.0** (2016-04-29)
|
||||
|
||||
Maintenance release
|
||||
|
||||
### Enhancements
|
||||
|
||||
- TOO\_MANY\_PENDING\_TASKS [\#861](https://github.com/vatesfr/xo-web/issues/861)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Incorrect VM target name with continuous replication [\#904](https://github.com/vatesfr/xo-web/issues/904)
|
||||
- Error while deleting users [\#901](https://github.com/vatesfr/xo-web/issues/901)
|
||||
- Use an available path to the SR to create a config drive [\#882](https://github.com/vatesfr/xo-web/issues/882)
|
||||
- VM autoboot don't set the right pool parameter [\#879](https://github.com/vatesfr/xo-web/issues/879)
|
||||
- BUG: ACL with NFS ISO Library not working! [\#870](https://github.com/vatesfr/xo-web/issues/870)
|
||||
- Broken paths in backups in SMB [\#865](https://github.com/vatesfr/xo-web/issues/865)
|
||||
- Plugins page loads users/groups multiple times [\#829](https://github.com/vatesfr/xo-web/issues/829)
|
||||
- "Ghost" VM remains after migration [\#769](https://github.com/vatesfr/xo-web/issues/769)
|
||||
|
||||
## **4.15.0** (2016-03-21)
|
||||
|
||||
Load balancing, SMB delta support, advanced network operations...
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Add the job name inside the backup email report [\#819](https://github.com/vatesfr/xo-web/issues/819)
|
||||
- Delta backup with quiesce [\#812](https://github.com/vatesfr/xo-web/issues/812)
|
||||
- Hosts: No user feedback when error occurs with SR connect / disconnect [\#810](https://github.com/vatesfr/xo-web/issues/810)
|
||||
- Expose components versions [\#807](https://github.com/vatesfr/xo-web/issues/807)
|
||||
- Rework networks/PIFs management [\#805](https://github.com/vatesfr/xo-web/issues/805)
|
||||
- Displaying all SRs and a list of available hosts for creating VM from a pool [\#790](https://github.com/vatesfr/xo-web/issues/790)
|
||||
- Add "Source network" on "VM migration" screen [\#785](https://github.com/vatesfr/xo-web/issues/785)
|
||||
- Migration queue [\#783](https://github.com/vatesfr/xo-web/issues/783)
|
||||
- Match network names for VM migration [\#782](https://github.com/vatesfr/xo-web/issues/782)
|
||||
- Disk names [\#780](https://github.com/vatesfr/xo-web/issues/780)
|
||||
- Self service: should the user be able to set the CPU weight? [\#767](https://github.com/vatesfr/xo-web/issues/767)
|
||||
- host & pool Citrix license status [\#763](https://github.com/vatesfr/xo-web/issues/763)
|
||||
- pool view: Provide "updates" section [\#762](https://github.com/vatesfr/xo-web/issues/762)
|
||||
- XOA ISO image: ambigious root disk label [\#761](https://github.com/vatesfr/xo-web/issues/761)
|
||||
- Host info: provide system serial number [\#760](https://github.com/vatesfr/xo-web/issues/760)
|
||||
- CIFS ISO SR Creation [\#731](https://github.com/vatesfr/xo-web/issues/731)
|
||||
- MAC address not preserved on VM restore [\#707](https://github.com/vatesfr/xo-web/issues/707)
|
||||
- Failing replication job should send reports [\#659](https://github.com/vatesfr/xo-web/issues/659)
|
||||
- Display networks in the Pool view [\#226](https://github.com/vatesfr/xo-web/issues/226)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Broken link to backup remote [\#821](https://github.com/vatesfr/xo-web/issues/821)
|
||||
- Issue with self-signed cert for email plugin [\#817](https://github.com/vatesfr/xo-web/issues/817)
|
||||
- Plugins view, reset form and errors [\#815](https://github.com/vatesfr/xo-web/issues/815)
|
||||
- HVM recovery mode is broken [\#794](https://github.com/vatesfr/xo-web/issues/794)
|
||||
- Disk bug when creating vm from template [\#778](https://github.com/vatesfr/xo-web/issues/778)
|
||||
- Can't mount NFS shares in remote stores [\#775](https://github.com/vatesfr/xo-web/issues/775)
|
||||
- VM disk name and description not passed during creation [\#774](https://github.com/vatesfr/xo-web/issues/774)
|
||||
- NFS mount problem for Windows share [\#771](https://github.com/vatesfr/xo-web/issues/771)
|
||||
- lodash.pluck not installed [\#757](https://github.com/vatesfr/xo-web/issues/757)
|
||||
- this.\_getAuthenticationTokensForUser is not a function [\#755](https://github.com/vatesfr/xo-web/issues/755)
|
||||
- CentOS 6.x 64bit template creates a VM that won't boot [\#733](https://github.com/vatesfr/xo-web/issues/733)
|
||||
- Lot of xo:perf leading to XO crash [\#575](https://github.com/vatesfr/xo-web/issues/575)
|
||||
- New collection checklist [\#262](https://github.com/vatesfr/xo-web/issues/262)
|
||||
|
||||
## **4.14.0** (2016-02-23)
|
||||
|
||||
Self service, custom CloudInit...
|
||||
|
||||
### Enhancements
|
||||
|
||||
- VM creation self service with quotas [\#285](https://github.com/vatesfr/xo-web/issues/285)
|
||||
- Cloud config custom user data [\#706](https://github.com/vatesfr/xo-web/issues/706)
|
||||
- Patches behind a proxy [\#737](https://github.com/vatesfr/xo-web/issues/737)
|
||||
- Remote store status indicator [\#728](https://github.com/vatesfr/xo-web/issues/728)
|
||||
- Patch list order [\#724](https://github.com/vatesfr/xo-web/issues/724)
|
||||
- Enable reporting on additional backup types [\#717](https://github.com/vatesfr/xo-web/issues/717)
|
||||
- Tooltip name for cancel [\#703](https://github.com/vatesfr/xo-web/issues/703)
|
||||
- Portable VHD merging [\#646](https://github.com/vatesfr/xo-web/issues/646)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Avoid merge between two delta vdi backups [\#702](https://github.com/vatesfr/xo-web/issues/702)
|
||||
- Text in table is not cut anymore [\#713](https://github.com/vatesfr/xo-web/issues/713)
|
||||
- Disk size edition issue with float numbers [\#719](https://github.com/vatesfr/xo-web/issues/719)
|
||||
- Create vm, summary is not refreshed [\#721](https://github.com/vatesfr/xo-web/issues/721)
|
||||
- Boot order problem [\#726](https://github.com/vatesfr/xo-web/issues/726)
|
||||
|
||||
## **4.13.0** (2016-02-05)
|
||||
|
||||
Backup checksum, SMB remotes...
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Add SMB mount for remote [\#338](https://github.com/vatesfr/xo-web/issues/338)
|
||||
- Centralize Perm in a lib [\#345](https://github.com/vatesfr/xo-web/issues/345)
|
||||
- Expose interpool migration details [\#567](https://github.com/vatesfr/xo-web/issues/567)
|
||||
- Add checksum for delta backup [\#617](https://github.com/vatesfr/xo-web/issues/617)
|
||||
- Redirect from HTTP to HTTPS [\#626](https://github.com/vatesfr/xo-web/issues/626)
|
||||
- Expose vCPU weight [\#633](https://github.com/vatesfr/xo-web/issues/633)
|
||||
- Avoid metadata in delta backup [\#651](https://github.com/vatesfr/xo-web/issues/651)
|
||||
- Button to clear logs [\#661](https://github.com/vatesfr/xo-web/issues/661)
|
||||
- Units for RAM and disks [\#666](https://github.com/vatesfr/xo-web/issues/666)
|
||||
- Remove multiple VDIs at once [\#676](https://github.com/vatesfr/xo-web/issues/676)
|
||||
- Find orphaned VDI snapshots [\#679](https://github.com/vatesfr/xo-web/issues/679)
|
||||
- New health view in Dashboard [\#680](https://github.com/vatesfr/xo-web/issues/680)
|
||||
- Use physical usage for VDI and SR [\#682](https://github.com/vatesfr/xo-web/issues/682)
|
||||
- TLS configuration [\#685](https://github.com/vatesfr/xo-web/issues/685)
|
||||
- Better VM info on tree view [\#688](https://github.com/vatesfr/xo-web/issues/688)
|
||||
- Absolute values in tooltips for tree view [\#690](https://github.com/vatesfr/xo-web/issues/690)
|
||||
- Absolute values for host memory [\#691](https://github.com/vatesfr/xo-web/issues/691)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Issues on host console screen [\#672](https://github.com/vatesfr/xo-web/issues/672)
|
||||
- NFS remote mount fails in particular case [\#665](https://github.com/vatesfr/xo-web/issues/665)
|
||||
- Unresponsive pages [\#662](https://github.com/vatesfr/xo-web/issues/662)
|
||||
- Live migration fail in the same pool with local SR fails [\#655](https://github.com/vatesfr/xo-web/issues/655)
|
||||
|
||||
## **4.12.0** (2016-01-18)
|
||||
|
||||
Continuous Replication, Continuous Delta backup...
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Continuous VM replication [\#582](https://github.com/vatesfr/xo-web/issues/582)
|
||||
- Continuous Delta Backup [\#576](https://github.com/vatesfr/xo-web/issues/576)
|
||||
- Scheduler should not run job again if previous instance is not finished [\#642](https://github.com/vatesfr/xo-web/issues/642)
|
||||
- Boot VM automatically after creation [\#635](https://github.com/vatesfr/xo-web/issues/635)
|
||||
- Manage existing VIFs in templates [\#630](https://github.com/vatesfr/xo-web/issues/630)
|
||||
- Support templates with existing install repository [\#627](https://github.com/vatesfr/xo-web/issues/627)
|
||||
- Remove running VMs [\#616](https://github.com/vatesfr/xo-web/issues/616)
|
||||
- Prevent a VM to start before delta import is finished [\#613](https://github.com/vatesfr/xo-web/issues/613)
|
||||
- Spawn multiple VMs at once [\#606](https://github.com/vatesfr/xo-web/issues/606)
|
||||
- Fixed `suspendVM` in tree view. [\#619](https://github.com/vatesfr/xo-web/pull/619) ([pdonias](https://github.com/pdonias))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- User defined MAC address is not fetch in VM install [\#643](https://github.com/vatesfr/xo-web/issues/643)
|
||||
- CoreOsCloudConfig is not shown with CoreOS [\#639](https://github.com/vatesfr/xo-web/issues/639)
|
||||
- Plugin activation/deactivation in web UI seems broken [\#637](https://github.com/vatesfr/xo-web/issues/637)
|
||||
- Issue when creating CloudConfig drive [\#636](https://github.com/vatesfr/xo-web/issues/636)
|
||||
- CloudConfig hostname shouldn't have space [\#634](https://github.com/vatesfr/xo-web/issues/634)
|
||||
- Cloned VIFs are not properly deleted on VM creation [\#632](https://github.com/vatesfr/xo-web/issues/632)
|
||||
- Default PV args missing during VM creation [\#628](https://github.com/vatesfr/xo-web/issues/628)
|
||||
- VM creation problems from custom templates [\#625](https://github.com/vatesfr/xo-web/issues/625)
|
||||
- Emergency shutdown race condition [\#622](https://github.com/vatesfr/xo-web/issues/622)
|
||||
- `vm.delete\(\)` should not delete VDIs attached to other VMs [\#621](https://github.com/vatesfr/xo-web/issues/621)
|
||||
- VM creation error from template with a disk [\#581](https://github.com/vatesfr/xo-web/issues/581)
|
||||
- Only delete VDI exports when VM backup is successful [\#644](https://github.com/vatesfr/xo-web/issues/644)
|
||||
- Change the name of an imported VM during the import process [\#641](https://github.com/vatesfr/xo-web/issues/641)
|
||||
- Creating a new VIF in view is partially broken [\#652](https://github.com/vatesfr/xo-web/issues/652)
|
||||
- Grey out the "create button" during VM creation [\#654](https://github.com/vatesfr/xo-web/issues/654)
|
||||
|
||||
## **4.11.0** (2015-12-22)
|
||||
|
||||
Delta backup, CloudInit...
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Visible list of SR inside a VM [\#601](https://github.com/vatesfr/xo-web/issues/601)
|
||||
- VDI move [\#591](https://github.com/vatesfr/xo-web/issues/591)
|
||||
- Edit pre-existing disk configuration during VM creation [\#589](https://github.com/vatesfr/xo-web/issues/589)
|
||||
- Allow disk size edition [\#587](https://github.com/vatesfr/xo-web/issues/587)
|
||||
- Better VDI resize support [\#585](https://github.com/vatesfr/xo-web/issues/585)
|
||||
- Remove manual VM export metadata in UI [\#580](https://github.com/vatesfr/xo-web/issues/580)
|
||||
- Support import VM metadata [\#579](https://github.com/vatesfr/xo-web/issues/579)
|
||||
- Set a default pool SR [\#572](https://github.com/vatesfr/xo-web/issues/572)
|
||||
- ISOs should be sorted by name [\#565](https://github.com/vatesfr/xo-web/issues/565)
|
||||
- Button to boot a VM from a disc once [\#564](https://github.com/vatesfr/xo-web/issues/564)
|
||||
- Ability to boot a PV VM from a disc [\#563](https://github.com/vatesfr/xo-web/issues/563)
|
||||
- Add an option to manually run backup jobs [\#562](https://github.com/vatesfr/xo-web/issues/562)
|
||||
- backups to unmounted storage [\#561](https://github.com/vatesfr/xo-web/issues/561)
|
||||
- Root integer properties cannot be edited in plugins configuration form [\#550](https://github.com/vatesfr/xo-web/issues/550)
|
||||
- Generic CloudConfig drive [\#549](https://github.com/vatesfr/xo-web/issues/549)
|
||||
- Auto-discovery of installed xo-server plugins [\#546](https://github.com/vatesfr/xo-web/issues/546)
|
||||
- Hide info on flat view [\#545](https://github.com/vatesfr/xo-web/issues/545)
|
||||
- Config plugin boolean properties must have a default value \(undefined prohibited\) [\#543](https://github.com/vatesfr/xo-web/issues/543)
|
||||
- Present detailed errors on plugin configuration failures [\#530](https://github.com/vatesfr/xo-web/issues/530)
|
||||
- Do not reset form on failures in plugins configuration [\#529](https://github.com/vatesfr/xo-web/issues/529)
|
||||
- XMPP alert plugin [\#518](https://github.com/vatesfr/xo-web/issues/518)
|
||||
- Hide tag adders depending on ACLs [\#516](https://github.com/vatesfr/xo-web/issues/516)
|
||||
- Choosing a framework for xo-web 5 [\#514](https://github.com/vatesfr/xo-web/issues/514)
|
||||
- Prevent adding a host in an existing XAPI connection [\#466](https://github.com/vatesfr/xo-web/issues/466)
|
||||
- Read only connection to Xen servers/pools [\#439](https://github.com/vatesfr/xo-web/issues/439)
|
||||
- generic notification system [\#391](https://github.com/vatesfr/xo-web/issues/391)
|
||||
- Data architecture review [\#384](https://github.com/vatesfr/xo-web/issues/384)
|
||||
- Make filtering easier to understand/add some "default" filters [\#207](https://github.com/vatesfr/xo-web/issues/207)
|
||||
- Improve performance [\#148](https://github.com/vatesfr/xo-web/issues/148)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- VM metadata export should not require a snapshot [\#615](https://github.com/vatesfr/xo-web/issues/615)
|
||||
- Missing patch for all hosts is continuously refreshed [\#609](https://github.com/vatesfr/xo-web/issues/609)
|
||||
- Backup import memory issue [\#608](https://github.com/vatesfr/xo-web/issues/608)
|
||||
- Host list missing patch is buggy [\#604](https://github.com/vatesfr/xo-web/issues/604)
|
||||
- Servers infos should not been refreshed while a field is being edited [\#595](https://github.com/vatesfr/xo-web/issues/595)
|
||||
- Servers list should not been re-order while a field is being edited [\#594](https://github.com/vatesfr/xo-web/issues/594)
|
||||
- Correctly display size in interface \(binary scale\) [\#592](https://github.com/vatesfr/xo-web/issues/592)
|
||||
- Display failures on VM boot order modification [\#560](https://github.com/vatesfr/xo-web/issues/560)
|
||||
- `vm.setBootOrder\(\)` should throw errors on failures \(non-HVM VMs\) [\#559](https://github.com/vatesfr/xo-web/issues/559)
|
||||
- Hide boot order form for non-HVM VMs [\#558](https://github.com/vatesfr/xo-web/issues/558)
|
||||
- Allow editing PV args even when empty \(but only for PV VMs\) [\#557](https://github.com/vatesfr/xo-web/issues/557)
|
||||
- Crashes when using legacy event system [\#556](https://github.com/vatesfr/xo-web/issues/556)
|
||||
- XenServer patches check error for 6.1 [\#555](https://github.com/vatesfr/xo-web/issues/555)
|
||||
- activation plugin xo-server-transport-email [\#553](https://github.com/vatesfr/xo-web/issues/553)
|
||||
- Server error with JSON on 32 bits Dom0 [\#552](https://github.com/vatesfr/xo-web/issues/552)
|
||||
- Cloud Config drive shouldn't be created on default SR [\#548](https://github.com/vatesfr/xo-web/issues/548)
|
||||
- Deep properties cannot be edited in plugins configuration form [\#521](https://github.com/vatesfr/xo-web/issues/521)
|
||||
- Aborted VM export should cancel the operation [\#490](https://github.com/vatesfr/xo-web/issues/490)
|
||||
- VM missing with same UUID after an inter-pool migration [\#284](https://github.com/vatesfr/xo-web/issues/284)
|
||||
|
||||
## **4.10.0** (2015-11-27)
|
||||
|
||||
Job management, email notifications, CoreOS/Docker, Quiesce snapshots...
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Job management ([xo-web#487](https://github.com/vatesfr/xo-web/issues/487))
|
||||
- Patch upload on all connected servers ([xo-web#168](https://github.com/vatesfr/xo-web/issues/168))
|
||||
- Emergency shutdown ([xo-web#185](https://github.com/vatesfr/xo-web/issues/185))
|
||||
- CoreOS/docker template install ([xo-web#246](https://github.com/vatesfr/xo-web/issues/246))
|
||||
- Email for backups ([xo-web#308](https://github.com/vatesfr/xo-web/issues/308))
|
||||
- Console Clipboard ([xo-web#408](https://github.com/vatesfr/xo-web/issues/408))
|
||||
- Logs from CLI ([xo-web#486](https://github.com/vatesfr/xo-web/issues/486))
|
||||
- Save disconnected servers ([xo-web#489](https://github.com/vatesfr/xo-web/issues/489))
|
||||
- Snapshot with quiesce ([xo-web#491](https://github.com/vatesfr/xo-web/issues/491))
|
||||
- Start VM in reovery mode ([xo-web#495](https://github.com/vatesfr/xo-web/issues/495))
|
||||
- Username in logs ([xo-web#498](https://github.com/vatesfr/xo-web/issues/498))
|
||||
- Delete associated tokens with user ([xo-web#500](https://github.com/vatesfr/xo-web/issues/500))
|
||||
- Validate plugin configuration ([xo-web#503](https://github.com/vatesfr/xo-web/issues/503))
|
||||
- Avoid non configured plugins to be loaded ([xo-web#504](https://github.com/vatesfr/xo-web/issues/504))
|
||||
- Verbose API logs if configured ([xo-web#505](https://github.com/vatesfr/xo-web/issues/505))
|
||||
- Better backup overview ([xo-web#512](https://github.com/vatesfr/xo-web/issues/512))
|
||||
- VM auto power on ([xo-web#519](https://github.com/vatesfr/xo-web/issues/519))
|
||||
- Title property supported in config schema ([xo-web#522](https://github.com/vatesfr/xo-web/issues/522))
|
||||
- Start VM export only when necessary ([xo-web#534](https://github.com/vatesfr/xo-web/issues/534))
|
||||
- Input type should be number ([xo-web#538](https://github.com/vatesfr/xo-web/issues/538))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Numbers/int support in plugins config ([xo-web#531](https://github.com/vatesfr/xo-web/issues/531))
|
||||
- Boolean support in plugins config ([xo-web#528](https://github.com/vatesfr/xo-web/issues/528))
|
||||
- Keyboard unusable outside console ([xo-web#513](https://github.com/vatesfr/xo-web/issues/513))
|
||||
- UsernameField for SAML ([xo-web#513](https://github.com/vatesfr/xo-web/issues/513))
|
||||
- Wrong display of "no plugin found" ([xo-web#508](https://github.com/vatesfr/xo-web/issues/508))
|
||||
- Bower build error ([xo-web#488](https://github.com/vatesfr/xo-web/issues/488))
|
||||
- VM cloning should require SR permission ([xo-web#472](https://github.com/vatesfr/xo-web/issues/472))
|
||||
- Xen tools status ([xo-web#471](https://github.com/vatesfr/xo-web/issues/471))
|
||||
- Can't delete ghost user ([xo-web#464](https://github.com/vatesfr/xo-web/issues/464))
|
||||
- Stats with old versions of Node ([xo-web#463](https://github.com/vatesfr/xo-web/issues/463))
|
||||
|
||||
## **4.9.0** (2015-11-13)
|
||||
|
||||
Automated DR, restore backup, VM copy
|
||||
|
||||
### Enhancements
|
||||
|
||||
- DR: schedule VM export on other host ([xo-web#447](https://github.com/vatesfr/xo-web/issues/447))
|
||||
- Scheduler logs ([xo-web#390](https://github.com/vatesfr/xo-web/issues/390) and [xo-web#477](https://github.com/vatesfr/xo-web/issues/477))
|
||||
- Restore backups ([xo-web#450](https://github.com/vatesfr/xo-web/issues/350))
|
||||
- Disable backup compression ([xo-web#467](https://github.com/vatesfr/xo-web/issues/467))
|
||||
- Copy VM to another SR (even remote) ([xo-web#475](https://github.com/vatesfr/xo-web/issues/475))
|
||||
- VM stats without time sync ([xo-web#460](https://github.com/vatesfr/xo-web/issues/460))
|
||||
- Stats perfs for high CPU numbers ([xo-web#461](https://github.com/vatesfr/xo-web/issues/461))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Rolling backup bug ([xo-web#484](https://github.com/vatesfr/xo-web/issues/484))
|
||||
- vCPUs/CPUs inversion in dashboard ([xo-web#481](https://github.com/vatesfr/xo-web/issues/481))
|
||||
- Machine to template ([xo-web#459](https://github.com/vatesfr/xo-web/issues/459))
|
||||
|
||||
### Misc
|
||||
|
||||
- Console fix in XenServer ([xo-web#406](https://github.com/vatesfr/xo-web/issues/406))
|
||||
|
||||
## **4.8.0** (2015-10-29)
|
||||
|
||||
Fully automated patch system, ACLs inheritance, stats performance improved.
|
||||
|
||||
### Enhancements
|
||||
|
||||
- ACLs inheritance ([xo-web#279](https://github.com/vatesfr/xo-web/issues/279))
|
||||
- Patch automatically all missing updates ([xo-web#281](https://github.com/vatesfr/xo-web/issues/281))
|
||||
- Intelligent stats polling ([xo-web#432](https://github.com/vatesfr/xo-web/issues/432))
|
||||
- Cache latest result of stats request ([xo-web#431](https://github.com/vatesfr/xo-web/issues/431))
|
||||
- Improve stats polling on multiple objects ([xo-web#433](https://github.com/vatesfr/xo-web/issues/433))
|
||||
- Patch upload task should display the patch name ([xo-web#449](https://github.com/vatesfr/xo-web/issues/449))
|
||||
- Backup filename for Windows ([xo-web#448](https://github.com/vatesfr/xo-web/issues/448))
|
||||
- Specific distro icons ([xo-web#446](https://github.com/vatesfr/xo-web/issues/446))
|
||||
- PXE boot for HVM ([xo-web#436](https://github.com/vatesfr/xo-web/issues/436))
|
||||
- Favicon display before sign in ([xo-web#428](https://github.com/vatesfr/xo-web/issues/428))
|
||||
- Registration renewal ([xo-web#424](https://github.com/vatesfr/xo-web/issues/424))
|
||||
- Reconnect to the host if pool merge fails ([xo-web#403](https://github.com/vatesfr/xo-web/issues/403))
|
||||
- Avoid brute force login ([xo-web#339](https://github.com/vatesfr/xo-web/issues/339))
|
||||
- Missing FreeBSD icon ([xo-web#136](https://github.com/vatesfr/xo-web/issues/136))
|
||||
- Hide halted objects in the Health view ([xo-web#457](https://github.com/vatesfr/xo-web/issues/457))
|
||||
- Click on "Remember me" label ([xo-web#438](https://github.com/vatesfr/xo-web/issues/438))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Pool patches in multiple pools not displayed ([xo-web#442](https://github.com/vatesfr/xo-web/issues/442))
|
||||
- VM Import crashes with Chrome ([xo-web#427](https://github.com/vatesfr/xo-web/issues/427))
|
||||
- Cannot open a direct link ([xo-web#371](https://github.com/vatesfr/xo-web/issues/371))
|
||||
- Patch display edge case ([xo-web#309](https://github.com/vatesfr/xo-web/issues/309))
|
||||
- VM snapshot should require user permission on SR ([xo-web#429](https://github.com/vatesfr/xo-web/issues/429))
|
||||
|
||||
## **4.7.0** (2015-10-12)
|
||||
|
||||
Plugin config management and browser notifications.
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Plugin management in the web interface ([xo-web#352](https://github.com/vatesfr/xo-web/issues/352))
|
||||
- Browser notifications ([xo-web#402](https://github.com/vatesfr/xo-web/issues/402))
|
||||
- Graph selector ([xo-web#400](https://github.com/vatesfr/xo-web/issues/400))
|
||||
- Circle packing visualization ([xo-web#374](https://github.com/vatesfr/xo-web/issues/374))
|
||||
- Password generation ([xo-web#397](https://github.com/vatesfr/xo-web/issues/397))
|
||||
- Password reveal during user creation ([xo-web#396](https://github.com/vatesfr/xo-web/issues/396))
|
||||
- Add host to a pool ([xo-web#62](https://github.com/vatesfr/xo-web/issues/62))
|
||||
- Better modal when removing a host from a pool ([xo-web#405](https://github.com/vatesfr/xo-web/issues/405))
|
||||
- Drop focus on CD/ISO selector ([xo-web#290](https://github.com/vatesfr/xo-web/issues/290))
|
||||
- Allow non persistent session ([xo-web#243](https://github.com/vatesfr/xo-web/issues/243))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- VM export permission corrected ([xo-web#410](https://github.com/vatesfr/xo-web/issues/410))
|
||||
- Proper host removal in a pool ([xo-web#402](https://github.com/vatesfr/xo-web/issues/402))
|
||||
- Sub-optimal tooltip placement ([xo-web#421](https://github.com/vatesfr/xo-web/issues/421))
|
||||
- VM migrate host incorrect target ([xo-web#419](https://github.com/vatesfr/xo-web/issues/419))
|
||||
- Alone host can't leave its pool ([xo-web#414](https://github.com/vatesfr/xo-web/issues/414))
|
||||
|
||||
## **4.6.0** (2015-09-25)
|
||||
|
||||
Tags management and new visualization.
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Multigraph for correlation ([xo-web#358](https://github.com/vatesfr/xo-web/issues/358))
|
||||
- Tags management ([xo-web#367](https://github.com/vatesfr/xo-web/issues/367))
|
||||
- Google Provider for authentication ([xo-web#363](https://github.com/vatesfr/xo-web/issues/363))
|
||||
- Password change for users ([xo-web#362](https://github.com/vatesfr/xo-web/issues/362))
|
||||
- Better live migration process ([xo-web#237](https://github.com/vatesfr/xo-web/issues/237))
|
||||
- VDI search filter in SR view ([xo-web#222](https://github.com/vatesfr/xo-web/issues/222))
|
||||
- PV args during VM creation ([xo-web#112](https://github.com/vatesfr/xo-web/issues/330))
|
||||
- PV args management ([xo-web#394](https://github.com/vatesfr/xo-web/issues/394))
|
||||
- Confirmation dialog on important actions ([xo-web#350](https://github.com/vatesfr/xo-web/issues/350))
|
||||
- New favicon ([xo-web#369](https://github.com/vatesfr/xo-web/issues/369))
|
||||
- Filename of VM for exports ([xo-web#370](https://github.com/vatesfr/xo-web/issues/370))
|
||||
- ACLs rights edited on the fly ([xo-web#323](https://github.com/vatesfr/xo-web/issues/323))
|
||||
- Heatmap values now human readable ([xo-web#342](https://github.com/vatesfr/xo-web/issues/342))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Export backup fails if no tags specified ([xo-web#383](https://github.com/vatesfr/xo-web/issues/383))
|
||||
- Wrong login give an obscure error message ([xo-web#373](https://github.com/vatesfr/xo-web/issues/373))
|
||||
- Update view is broken during updates ([xo-web#356](https://github.com/vatesfr/xo-web/issues/356))
|
||||
- Settings/dashboard menu incorrect display ([xo-web#357](https://github.com/vatesfr/xo-web/issues/357))
|
||||
- Console View Not refreshing if the VM restart ([xo-web#107](https://github.com/vatesfr/xo-web/issues/107))
|
||||
|
||||
## **4.5.1** (2015-09-16)
|
||||
|
||||
An issue in `xo-web` with the VM view.
|
||||
|
||||
### Bug fix
|
||||
|
||||
- Attach disk/new disk/create interface is broken ([xo-web#378](https://github.com/vatesfr/xo-web/issues/378))
|
||||
|
||||
## **4.5.0** (2015-09-11)
|
||||
|
||||
A new dataviz (parallel coord), a new provider (GitHub) and faster consoles.
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Parallel coordinates view ([xo-web#333](https://github.com/vatesfr/xo-web/issues/333))
|
||||
- Faster consoles ([xo-web#337](https://github.com/vatesfr/xo-web/issues/337))
|
||||
- Disable/hide button ([xo-web#268](https://github.com/vatesfr/xo-web/issues/268))
|
||||
- More details on missing-guest-tools ([xo-web#304](https://github.com/vatesfr/xo-web/issues/304))
|
||||
- Scheduler meta data export ([xo-web#315](https://github.com/vatesfr/xo-web/issues/315))
|
||||
- Better heatmap ([xo-web#330](https://github.com/vatesfr/xo-web/issues/330))
|
||||
- Faster dashboard ([xo-web#331](https://github.com/vatesfr/xo-web/issues/331))
|
||||
- Faster sunburst ([xo-web#332](https://github.com/vatesfr/xo-web/issues/332))
|
||||
- GitHub provider for auth ([xo-web#334](https://github.com/vatesfr/xo-web/issues/334))
|
||||
- Filter networks for users ([xo-web#347](https://github.com/vatesfr/xo-web/issues/347))
|
||||
- Add networks in ACLs ([xo-web#348](https://github.com/vatesfr/xo-web/issues/348))
|
||||
- Better looking login page ([xo-web#341](https://github.com/vatesfr/xo-web/issues/341))
|
||||
- Real time dataviz (dashboard) ([xo-web#349](https://github.com/vatesfr/xo-web/issues/349))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Typo in dashboard ([xo-web#355](https://github.com/vatesfr/xo-web/issues/355))
|
||||
- Global RAM usage fix ([xo-web#356](https://github.com/vatesfr/xo-web/issues/356))
|
||||
- Re-allowing XO behind a reverse proxy ([xo-web#361](https://github.com/vatesfr/xo-web/issues/361))
|
||||
|
||||
## **4.4.0** (2015-08-28)
|
||||
|
||||
SSO and Dataviz are the main features for this release.
|
||||
|
||||
28
ISSUE_TEMPLATE.md
Normal file
@@ -0,0 +1,28 @@
|
||||
<!--
|
||||
Welcome to the issue section of Xen Orchestra!
|
||||
|
||||
Here you can:
|
||||
- report an issue
|
||||
- propose an enhancement
|
||||
- ask a question
|
||||
|
||||
The template below is only a proposition for your ticket, feel free to
|
||||
change it as appropriate :)
|
||||
-->
|
||||
|
||||
### Context
|
||||
|
||||
- **XO version**: XO appliance / `stable` branch / `next-release` branch
|
||||
|
||||
If from the sources:
|
||||
|
||||
- **Component**: xo-web / xo-server / *unknown*
|
||||
- **Node/npm version**: *just execute `npm version`*
|
||||
|
||||
### Expected behavior
|
||||
|
||||
<!-- What you expect to happen -->
|
||||
|
||||
### Actual behavior
|
||||
|
||||
<!-- What is actually happening -->
|
||||
12
README.md
@@ -33,13 +33,11 @@ $ npm run dev
|
||||
|
||||
If you are certain the bug is exclusively related to XO-Web, you may use the [bugtracker of this repository](https://github.com/vatesfr/xo-web/issues).
|
||||
|
||||
Otherwise, please consider using the [bugtracker of the general repository](https://github.com/vatesfr/xo/issues).
|
||||
|
||||
## Process for new release
|
||||
|
||||
```bash
|
||||
# Switch to the master branch.
|
||||
git checkout master
|
||||
# Switch to the stable branch.
|
||||
git checkout stable
|
||||
|
||||
# Fetches latest changes.
|
||||
git pull --ff-only
|
||||
@@ -53,12 +51,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
|
||||
|
||||
86
app/app.js
@@ -1,7 +1,5 @@
|
||||
// Must be loaded before angular.
|
||||
import 'angular-file-upload'
|
||||
|
||||
import angular from 'angular'
|
||||
import angularChartJs from 'angular-chart.js'
|
||||
import uiBootstrap from'angular-ui-bootstrap'
|
||||
import uiIndeterminate from'angular-ui-indeterminate'
|
||||
import uiRouter from'angular-ui-router'
|
||||
@@ -15,25 +13,26 @@ import xoFilters from 'xo-filters'
|
||||
import xoServices from 'xo-services'
|
||||
|
||||
import aboutState from './modules/about'
|
||||
import backupState from './modules/backup'
|
||||
import consoleState from './modules/console'
|
||||
import dashboardState from './modules/dashboard'
|
||||
import deleteVmsState from './modules/delete-vms'
|
||||
import genericModalState from './modules/generic-modal'
|
||||
import hostState from './modules/host'
|
||||
import listState from './modules/list'
|
||||
import migrateVmState from './modules/migrate-vm'
|
||||
import navbarState from './modules/navbar'
|
||||
import newSrState from './modules/new-sr'
|
||||
import newVmState from './modules/new-vm'
|
||||
import poolState from './modules/pool'
|
||||
import schedulerState from './modules/scheduler'
|
||||
import selfState from './modules/self'
|
||||
import settingsState from './modules/settings'
|
||||
import dashboardState from './modules/dashboard'
|
||||
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'
|
||||
|
||||
import '../dist/bower_components/angular-chart.js/dist/angular-chart.js'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default angular.module('xoWebApp', [
|
||||
@@ -42,6 +41,7 @@ export default angular.module('xoWebApp', [
|
||||
uiRouter,
|
||||
uiSelect,
|
||||
|
||||
angularChartJs,
|
||||
naturalSort,
|
||||
xeditable,
|
||||
|
||||
@@ -50,29 +50,31 @@ export default angular.module('xoWebApp', [
|
||||
xoServices,
|
||||
|
||||
aboutState,
|
||||
backupState,
|
||||
consoleState,
|
||||
dashboardState,
|
||||
deleteVmsState,
|
||||
genericModalState,
|
||||
hostState,
|
||||
listState,
|
||||
migrateVmState,
|
||||
navbarState,
|
||||
newSrState,
|
||||
newVmState,
|
||||
poolState,
|
||||
schedulerState,
|
||||
selfState,
|
||||
settingsState,
|
||||
srState,
|
||||
taskScheduler,
|
||||
treeState,
|
||||
updater,
|
||||
vmState,
|
||||
'chart.js'
|
||||
vmState
|
||||
])
|
||||
|
||||
// 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 (
|
||||
@@ -114,25 +116,57 @@ export default angular.module('xoWebApp', [
|
||||
})
|
||||
.run(function (
|
||||
$anchorScroll,
|
||||
$cookies,
|
||||
$rootScope,
|
||||
$state,
|
||||
editableOptions,
|
||||
editableThemes,
|
||||
modal,
|
||||
notify,
|
||||
updater,
|
||||
xoApi
|
||||
) {
|
||||
// Milliseconds are not necessary.
|
||||
const now = Math.floor(Date.now() / 1e3)
|
||||
const oneWeekAgo = now - 7 * 24 * 3600
|
||||
const previousDisclaimer = $cookies.get('previousDisclaimer')
|
||||
if (
|
||||
!previousDisclaimer ||
|
||||
+previousDisclaimer < oneWeekAgo
|
||||
) {
|
||||
modal.alert({
|
||||
title: 'Xen Orchestra from the sources',
|
||||
htmlMessage: [
|
||||
'You are using XO from the sources! That\'s great for a personal/non-profit usage.',
|
||||
'If you are a company, it\'s better to use it with <a href="https://xen-orchestra.com/#!/xoa">XOA (turnkey appliance)</a> and our dedicated pro support!',
|
||||
'This version is <strong>not bundled with any support nor updates</strong>. Use it with caution for critical tasks.'
|
||||
].map(p => `<p>${p}</p>`).join('')
|
||||
})
|
||||
$cookies.put('previousDisclaimer', now)
|
||||
}
|
||||
|
||||
let requestedStateName, requestedStateParams
|
||||
|
||||
$rootScope.$watch(() => xoApi.user, (user, previous) => {
|
||||
// The user just signed in.
|
||||
if (user && !previous) {
|
||||
$state.go('index')
|
||||
if (requestedStateName) {
|
||||
$state.go(requestedStateName, requestedStateParams)
|
||||
requestedStateName = requestedStateParams = null
|
||||
} else {
|
||||
$state.go('index')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
$rootScope.$on('$stateChangeStart', function (event, state, stateParams) {
|
||||
const {user} = xoApi
|
||||
$rootScope.$on('$stateChangeStart', function (event, state, stateParams, fromState) {
|
||||
const { user } = xoApi
|
||||
if (!user) {
|
||||
event.preventDefault()
|
||||
|
||||
requestedStateName = state.name
|
||||
requestedStateParams = stateParams
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -140,22 +174,28 @@ export default angular.module('xoWebApp', [
|
||||
return
|
||||
}
|
||||
|
||||
// Some pages requires the admin permission.
|
||||
if (state.data && state.data.requireAdmin) {
|
||||
function forbidState () {
|
||||
event.preventDefault()
|
||||
notify.error({
|
||||
title: 'Restricted area',
|
||||
message: 'You do not have the permission to view this page'
|
||||
})
|
||||
|
||||
if (fromState.url === '^') {
|
||||
$state.go('index')
|
||||
}
|
||||
}
|
||||
|
||||
let {id} = stateParams
|
||||
if (id && !xoApi.canAccess(id)) {
|
||||
event.preventDefault()
|
||||
notify.error({
|
||||
title: 'Restricted area',
|
||||
message: 'You do not have the permission to view this page'
|
||||
})
|
||||
// Some pages requires the admin permission.
|
||||
if (state.data && state.data.requireAdmin) {
|
||||
forbidState()
|
||||
return
|
||||
}
|
||||
|
||||
const { id } = stateParams
|
||||
if (id && !xoApi.canInteract(id, 'view')) {
|
||||
forbidState()
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
BIN
app/favicon.ico
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
BIN
app/images/circle1.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
app/images/circle2.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
app/images/parcoords.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
app/images/sunburst.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
app/images/sunburst2.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
@@ -58,5 +58,4 @@ html.no-js(lang="en", dir="ltr")
|
||||
//- Main content (managed by the router).
|
||||
.view-main(ui-view = "")
|
||||
|
||||
script(src="bower_components/Chart.js/Chart.min.js")
|
||||
script(src="app.js")
|
||||
|
||||
@@ -15,7 +15,10 @@ export default angular.module('xoWebApp.about', [
|
||||
template: require('./view')
|
||||
})
|
||||
})
|
||||
.controller('AboutCtrl', function ($scope) {
|
||||
.controller('AboutCtrl', function ($scope, xo) {
|
||||
xo.system.getServerVersion().then(version =>
|
||||
$scope.serverVersion = version
|
||||
)
|
||||
$scope.pkg = pkg
|
||||
})
|
||||
// A module exports its name.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
p.page-title About Xen Orchestra
|
||||
p.text-center ({{pkg.name}} {{pkg.version}})
|
||||
p.text-center ({{pkg.name}} {{pkg.version}} - xo-server {{serverVersion}})
|
||||
.grid-sm
|
||||
//- Vates
|
||||
.panel.panel-default
|
||||
|
||||
275
app/modules/backup/backup/index.js
Normal file
@@ -0,0 +1,275 @@
|
||||
import angular from 'angular'
|
||||
import filter from 'lodash.filter'
|
||||
import find from 'lodash.find'
|
||||
import forEach from 'lodash.foreach'
|
||||
import map from 'lodash.map'
|
||||
import prettyCron from 'prettycron'
|
||||
import size from 'lodash.size'
|
||||
import trim from 'lodash.trim'
|
||||
import uiBootstrap from 'angular-ui-bootstrap'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
import { parse } from 'xo-remote-parser'
|
||||
|
||||
import view from './view'
|
||||
|
||||
// ====================================================================
|
||||
|
||||
export default angular.module('backup.backup', [
|
||||
uiRouter,
|
||||
uiBootstrap
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('backup.backup', {
|
||||
url: '/backup/:id',
|
||||
controller: 'BackupCtrl as ctrl',
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('BackupCtrl', function ($scope, $stateParams, $interval, xo, xoApi, notify, selectHighLevelFilter, filterFilter) {
|
||||
const JOBKEY = 'rollingBackup'
|
||||
|
||||
this.ready = false
|
||||
|
||||
this.running = {}
|
||||
this.comesForEditing = $stateParams.id
|
||||
this.scheduleApi = {}
|
||||
this.formData = {}
|
||||
|
||||
const refreshRemotes = () => {
|
||||
const selectRemoteId = this.formData.remote && this.formData.remote.id
|
||||
return xo.remote.getAll()
|
||||
.then(remotes => {
|
||||
const r = {}
|
||||
forEach(remotes, remote => {
|
||||
remote = parse(remote)
|
||||
r[remote.id] = remote
|
||||
})
|
||||
this.remotes = r
|
||||
if (selectRemoteId) {
|
||||
this.formData.remote = this.remotes[selectRemoteId]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const refreshSchedules = () => {
|
||||
return xo.schedule.getAll()
|
||||
.then(schedules => {
|
||||
const s = {}
|
||||
forEach(schedules, schedule => {
|
||||
this.jobs && this.jobs[schedule.job] && this.jobs[schedule.job].key === JOBKEY && (s[schedule.id] = schedule)
|
||||
})
|
||||
this.schedules = s
|
||||
})
|
||||
}
|
||||
|
||||
const refreshJobs = () => {
|
||||
return xo.job.getAll()
|
||||
.then(jobs => {
|
||||
const j = {}
|
||||
forEach(jobs, job => {
|
||||
j[job.id] = job
|
||||
})
|
||||
this.jobs = j
|
||||
})
|
||||
}
|
||||
|
||||
const refresh = () => refreshRemotes().then(refreshJobs).then(refreshSchedules)
|
||||
|
||||
this.getReady = () => refresh().then(() => this.ready = true)
|
||||
this.getReady()
|
||||
|
||||
const interval = $interval(refresh, 5e3)
|
||||
$scope.$on('$destroy', () => $interval.cancel(interval))
|
||||
|
||||
const toggleState = (toggle, state) => {
|
||||
const selectedVms = this.formData.selectedVms.slice()
|
||||
if (toggle) {
|
||||
const vms = filterFilter(selectHighLevelFilter(this.objects), {type: 'VM'})
|
||||
forEach(vms, vm => {
|
||||
if (vm.power_state === state) {
|
||||
(selectedVms.indexOf(vm) === -1) && selectedVms.push(vm)
|
||||
}
|
||||
})
|
||||
this.formData.selectedVms = selectedVms
|
||||
} else {
|
||||
const keptVms = []
|
||||
for (let index in this.formData.selectedVms) {
|
||||
if (this.formData.selectedVms[index].power_state !== state) {
|
||||
keptVms.push(this.formData.selectedVms[index])
|
||||
}
|
||||
}
|
||||
this.formData.selectedVms = keptVms
|
||||
}
|
||||
}
|
||||
|
||||
this.toggleAllRunning = toggle => toggleState(toggle, 'Running')
|
||||
this.toggleAllHalted = toggle => toggleState(toggle, 'Halted')
|
||||
|
||||
this.edit = schedule => {
|
||||
const vms = filterFilter(selectHighLevelFilter(this.objects), {type: 'VM'})
|
||||
const job = this.jobs[schedule.job]
|
||||
const selectedVms = []
|
||||
forEach(job.paramsVector.items[0].values, value => {
|
||||
const vm = find(vms, vm => vm.id === value.id)
|
||||
vm && selectedVms.push(vm)
|
||||
})
|
||||
const tag = job.paramsVector.items[0].values[0].tag
|
||||
const depth = job.paramsVector.items[0].values[0].depth
|
||||
const _reportWhen = job.paramsVector.items[0].values[0]._reportWhen
|
||||
const cronPattern = schedule.cron
|
||||
const remoteId = job.paramsVector.items[0].values[0].remoteId
|
||||
const onlyMetadata = job.paramsVector.items[0].values[0].onlyMetadata || false
|
||||
let compress = job.paramsVector.items[0].values[0].compress
|
||||
if (compress === undefined) {
|
||||
compress = true // Default value
|
||||
}
|
||||
|
||||
this.resetData()
|
||||
this.formData.selectedVms = selectedVms
|
||||
this.formData.tag = tag
|
||||
this.formData.depth = depth
|
||||
this.formData.scheduleId = schedule.id
|
||||
this.formData._reportWhen = _reportWhen
|
||||
this.formData.remote = this.remotes[remoteId]
|
||||
this.formData.disableCompression = !compress
|
||||
this.formData.onlyMetadata = onlyMetadata
|
||||
this.scheduleApi.setCron(cronPattern)
|
||||
}
|
||||
|
||||
this.save = (id, vms, remoteId, tag, depth, cron, enabled, onlyMetadata, disableCompression, _reportWhen) => {
|
||||
if (!vms.length) {
|
||||
notify.warning({
|
||||
title: 'No Vms selected',
|
||||
message: 'Choose VMs to backup'
|
||||
})
|
||||
return
|
||||
}
|
||||
const _save = (id === undefined) ? saveNew(vms, remoteId, tag, depth, cron, enabled, onlyMetadata, disableCompression, _reportWhen) : save(id, vms, remoteId, tag, depth, cron, onlyMetadata, disableCompression, _reportWhen)
|
||||
return _save
|
||||
.then(() => {
|
||||
notify.info({
|
||||
title: 'Backup',
|
||||
message: 'Job schedule successfuly saved'
|
||||
})
|
||||
this.resetData()
|
||||
})
|
||||
.finally(refresh)
|
||||
}
|
||||
|
||||
const save = (id, vms, remoteId, tag, depth, cron, onlyMetadata, disableCompression, _reportWhen) => {
|
||||
const schedule = this.schedules[id]
|
||||
const job = this.jobs[schedule.job]
|
||||
const values = []
|
||||
forEach(vms, vm => {
|
||||
values.push({
|
||||
id: vm.id,
|
||||
remoteId,
|
||||
tag,
|
||||
depth,
|
||||
onlyMetadata,
|
||||
compress: !disableCompression,
|
||||
_reportWhen
|
||||
})
|
||||
})
|
||||
job.paramsVector.items[0].values = values
|
||||
return xo.job.set(job)
|
||||
.then(response => {
|
||||
if (response) {
|
||||
return xo.schedule.set(schedule.id, undefined, cron, undefined)
|
||||
} else {
|
||||
notify.error({
|
||||
title: 'Update schedule',
|
||||
message: 'Job updating failed'
|
||||
})
|
||||
throw new Error('Job updating failed')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const saveNew = (vms, remoteId, tag, depth, cron, enabled, onlyMetadata, disableCompression, _reportWhen) => {
|
||||
const values = []
|
||||
forEach(vms, vm => {
|
||||
values.push({
|
||||
id: vm.id,
|
||||
remoteId,
|
||||
tag,
|
||||
depth,
|
||||
onlyMetadata,
|
||||
compress: !disableCompression,
|
||||
_reportWhen
|
||||
})
|
||||
})
|
||||
const job = {
|
||||
type: 'call',
|
||||
key: JOBKEY,
|
||||
method: 'vm.rollingBackup',
|
||||
paramsVector: {
|
||||
type: 'crossProduct',
|
||||
items: [{
|
||||
type: 'set',
|
||||
values
|
||||
}]
|
||||
}
|
||||
}
|
||||
return xo.job.create(job)
|
||||
.then(jobId => xo.schedule.create(jobId, cron, enabled))
|
||||
}
|
||||
|
||||
this.delete = schedule => {
|
||||
let jobId = schedule.job
|
||||
return xo.schedule.delete(schedule.id)
|
||||
.then(() => xo.job.delete(jobId))
|
||||
.finally(() => {
|
||||
if (this.formData.scheduleId === schedule.id) {
|
||||
this.resetData()
|
||||
}
|
||||
refresh()
|
||||
})
|
||||
}
|
||||
|
||||
this.run = schedule => {
|
||||
this.running[schedule.id] = true
|
||||
notify.info({
|
||||
title: 'Run Job',
|
||||
message: 'One shot running started. See overview for logs.'
|
||||
})
|
||||
const id = schedule.job
|
||||
return xo.job.runSequence([id]).finally(() => delete this.running[schedule.id])
|
||||
}
|
||||
|
||||
this.sanitizePath = (...paths) => (paths[0] && paths[0].charAt(0) === '/' && '/' || '') + filter(map(paths, s => s && filter(map(s.split('/'), trim)).join('/'))).join('/')
|
||||
|
||||
this.resetData = () => {
|
||||
this.formData.allRunning = false
|
||||
this.formData.allHalted = false
|
||||
this.formData.selectedVms = []
|
||||
this.formData.scheduleId = undefined
|
||||
this.formData.tag = undefined
|
||||
this.formData.path = undefined
|
||||
this.formData.depth = undefined
|
||||
this.formData.enabled = false
|
||||
this.formData._reportWhen = undefined
|
||||
this.formData.remote = undefined
|
||||
this.formData.onlyMetadata = false
|
||||
this.formData.disableCompression = false
|
||||
this.scheduleApi && this.scheduleApi.resetData && this.scheduleApi.resetData()
|
||||
}
|
||||
|
||||
this.size = size
|
||||
this.prettyCron = prettyCron.toString.bind(prettyCron)
|
||||
|
||||
if (!this.comesForEditing) {
|
||||
refresh()
|
||||
} else {
|
||||
refresh()
|
||||
.then(() => {
|
||||
this.edit(this.schedules[this.comesForEditing])
|
||||
delete this.comesForEditing
|
||||
})
|
||||
}
|
||||
this.resetData()
|
||||
this.objects = xoApi.all
|
||||
})
|
||||
|
||||
// A module exports its name.
|
||||
.name
|
||||
154
app/modules/backup/backup/view.jade
Normal file
@@ -0,0 +1,154 @@
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.fa.fa-download(style="color: #e25440;")
|
||||
| Backup
|
||||
form#backupform(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.selectedVms, ctrl.formData.remote.id, ctrl.formData.tag, ctrl.formData.depth, ctrl.formData.cronPattern, ctrl.formData.enabled, ctrl.formData.onlyMetadata, ctrl.formData.disableCompression, ctrl.formData._reportWhen)')
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.xo-icon-vm
|
||||
| VMs to backup
|
||||
.panel-body.form-horizontal
|
||||
.text-center(ng-if = '!ctrl.formData'): i.xo-icon-loading
|
||||
.container-fluid(ng-if = 'ctrl.formData')
|
||||
.alert.alert-info(ng-if = '!ctrl.formData.scheduleId') Creating New Backup
|
||||
.alert.alert-warning(ng-if = 'ctrl.formData.scheduleId') Modifying Backup ID {{ ctrl.formData.scheduleId }}
|
||||
.form-group
|
||||
label.control-label.col-md-2(for = 'tag') Tag
|
||||
.col-md-10
|
||||
input#tag.form-control(form = 'backupform', ng-model = 'ctrl.formData.tag', placeholder = 'Back-up tag', required)
|
||||
.form-group(ng-class = '{"has-warning": !ctrl.formData.selectedVms.length}')
|
||||
label.control-label.col-md-2(for = 'vmlist') VMs
|
||||
.col-md-8
|
||||
ui-select(form = 'backupform', ng-model = 'ctrl.formData.selectedVms', multiple, close-on-select = 'false', required)
|
||||
ui-select-match(placeholder = 'Choose VMs to backup')
|
||||
i.xo-icon-working(ng-if="isVMWorking($item)")
|
||||
i(class="xo-icon-{{$item.power_state | lowercase}}",ng-if="!isVMWorking($item)")
|
||||
| {{$item.name_label}}
|
||||
span(ng-if="$item.$container")
|
||||
| ({{($item.$container | resolve).name_label}})
|
||||
ui-select-choices(repeat = 'vm in ctrl.objects | selectHighLevel | filter:{type: "VM"} | filter:$select.search | orderBy:["$container", "name_label"] track by vm.id')
|
||||
div
|
||||
i.xo-icon-working(ng-if="isVMWorking(vm)", tooltip="{{vm.power_state}} and {{(vm.current_operations | map)[0]}}")
|
||||
i(class="xo-icon-{{vm.power_state | lowercase}}",ng-if="!isVMWorking(vm)", tooltip="{{vm.power_state}}")
|
||||
| {{vm.name_label}}
|
||||
span(ng-if="vm.$container")
|
||||
| ({{(vm.$container | resolve).name_label || ((vm.$container | resolve).master | resolve).name_label}})
|
||||
.col-md-2
|
||||
label(tooltip = 'select/deselect all running VMs', style = 'cursor: pointer')
|
||||
input.hidden(form = 'backupform', type = 'checkbox', ng-model = 'ctrl.formData.allRunning', ng-change = 'ctrl.toggleAllRunning(ctrl.formData.allRunning)')
|
||||
span.fa-stack
|
||||
i.xo-icon-running.fa-stack-1x
|
||||
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allRunning')
|
||||
label(tooltip = 'select/deselect all halted VMs', style = 'cursor: pointer')
|
||||
input.hidden(form = 'backupform', type = 'checkbox', ng-model = 'ctrl.formData.allHalted', ng-change = 'ctrl.toggleAllHalted(ctrl.formData.allHalted)')
|
||||
span.fa-stack
|
||||
i.xo-icon-halted.fa-stack-1x
|
||||
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allHalted')
|
||||
.form-group
|
||||
label.control-label.col-md-2(for = 'depth') Depth
|
||||
.col-md-10
|
||||
input#depth.form-control(form = 'backupform', ng-model = 'ctrl.formData.depth', placeholder = 'How many backups to rollover', type = 'number', min = '1', required)
|
||||
.form-group
|
||||
label.control-label.col-md-2(for = 'remote') Remote
|
||||
.col-md-10
|
||||
select#remote.form-control(form = 'backupform', ng-options = 'remote.name group by remote.type for remote in ctrl.remotes | map | orderBy:["type","name"]', ng-model = 'ctrl.formData.remote' required)
|
||||
option(value = ''): em -- Choose a file system remote point --
|
||||
.form-group
|
||||
.col-md-10.col-md-offset-2
|
||||
a(ui-sref = 'backup.remote')
|
||||
i.fa.fa-pencil
|
||||
| Manage your remote stores
|
||||
.form-group
|
||||
label.control-label.col-md-2(for = 'onlyMetadata')
|
||||
input#onlyMetadata(form = 'backupform', ng-model = 'ctrl.formData.onlyMetadata', type = 'checkbox')
|
||||
.help-block.col-md-10 Only MetaData (no disks export)
|
||||
.form-group
|
||||
label.control-label.col-md-2(for = 'onlyMetadata')
|
||||
input#disableCompression(form = 'backupform', ng-model = 'ctrl.formData.disableCompression', type = 'checkbox')
|
||||
.help-block.col-md-10 Disable compression
|
||||
.form-group(ng-if = '!ctrl.formData.scheduleId')
|
||||
label.control-label.col-md-2(for = 'enabled')
|
||||
input#enabled(form = 'backupform', ng-model = 'ctrl.formData.enabled', type = 'checkbox')
|
||||
.help-block.col-md-10 Enable immediately after creation
|
||||
.form-group
|
||||
label.control-label.col-md-2(for = '_reportWhen') Report
|
||||
.col-md-10
|
||||
select.form-control(ng-model = 'ctrl.formData._reportWhen')
|
||||
option(value = ''): em -- When to send reports --
|
||||
option(value = 'never') Never
|
||||
option(value = 'alway') Always
|
||||
option(value = 'fail') Failure
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-clock-o
|
||||
| Schedule
|
||||
.panel-body.form-horizontal
|
||||
.text-center(ng-if = '!ctrl.formData'): i.xo-icon-loading
|
||||
xo-scheduler(data = 'ctrl.formData', api = 'ctrl.scheduleApi')
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-body
|
||||
fieldset.center(ng-disabled = '!ctrl.ready')
|
||||
button.btn.btn-lg.btn-primary(form = 'backupform', type = 'submit')
|
||||
i.fa.fa-clock-o
|
||||
|
|
||||
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.hidden-xs Only MetaData
|
||||
th.hidden-xs Compression DISABLED
|
||||
th Enabled now
|
||||
th
|
||||
tr(ng-repeat = 'schedule in ctrl.schedules | map | orderBy:"id":true track by schedule.id')
|
||||
td {{ schedule.id }}
|
||||
td {{ ctrl.jobs[schedule.job].paramsVector.items[0].values[0].tag }}
|
||||
td.hidden-xs.hidden-sm
|
||||
div(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values.length == 1')
|
||||
| {{ (ctrl.jobs[schedule.job].paramsVector.items[0].values[0].id | resolve).name_label }}
|
||||
div(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values.length > 1')
|
||||
button.btn.btn-info(type = 'button', ng-click = 'unCollapsed = !unCollapsed')
|
||||
| {{ ctrl.jobs[schedule.job].paramsVector.items[0].values.length }} VMs
|
||||
i.fa(ng-class = '{"fa-chevron-down": !unCollapsed, "fa-chevron-up": unCollapsed}')
|
||||
div(collapse = '!unCollapsed')
|
||||
br
|
||||
ul.list-group
|
||||
li.list-group-item(ng-repeat = 'item in ctrl.jobs[schedule.job].paramsVector.items[0].values')
|
||||
span(ng-if = 'item.id | resolve') {{ (item.id | resolve).name_label }}
|
||||
span(ng-if = '(item.id | resolve).$container') ({{ ((item.id | resolve).$container | resolve).name_label }})
|
||||
td.hidden-xs
|
||||
strong: a(ui-sref = 'backup.remote') {{ ctrl.remotes[ctrl.jobs[schedule.job].paramsVector.items[0].values[0].remoteId].name }}
|
||||
td.hidden-xs {{ ctrl.jobs[schedule.job].paramsVector.items[0].values[0].depth }}
|
||||
td.hidden-xs {{ ctrl.prettyCron(schedule.cron) }}
|
||||
td.hidden-xs.text-center
|
||||
i.fa.fa-check(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values[0].onlyMetadata')
|
||||
td.hidden-xs.text-center
|
||||
i.fa.fa-check(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values[0].compress === false')
|
||||
td.text-center
|
||||
i.fa.fa-check(ng-if = 'schedule.enabled')
|
||||
td.text-right
|
||||
button.btn.btn-primary(type = 'button', ng-click = 'ctrl.edit(schedule)'): i.fa.fa-pencil
|
||||
|
|
||||
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
|
||||
225
app/modules/backup/continuous-replication/index.js
Normal file
@@ -0,0 +1,225 @@
|
||||
import angular from 'angular'
|
||||
import find from 'lodash.find'
|
||||
import forEach from 'lodash.foreach'
|
||||
import later from 'later'
|
||||
import prettyCron from 'prettycron'
|
||||
import uiBootstrap from 'angular-ui-bootstrap'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
|
||||
later.date.localTime()
|
||||
|
||||
import view from './view'
|
||||
|
||||
// ====================================================================
|
||||
|
||||
export default angular.module('backup.continuousReplication', [
|
||||
uiRouter,
|
||||
uiBootstrap
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('backup.continuousReplication', {
|
||||
url: '/continuous-replication/:id',
|
||||
controller: 'ContinuousReplicationCtrl as ctrl',
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('ContinuousReplicationCtrl', function ($scope, $stateParams, $interval, xo, xoApi, notify, selectHighLevelFilter, filterFilter, bytesToSizeFilter) {
|
||||
const JOBKEY = 'continuousReplication'
|
||||
|
||||
this.ready = false
|
||||
|
||||
this.running = {}
|
||||
this.comesForEditing = $stateParams.id
|
||||
this.scheduleApi = {}
|
||||
this.formData = {}
|
||||
|
||||
const refreshSchedules = () => xo.schedule.getAll()
|
||||
.then(schedules => {
|
||||
const s = {}
|
||||
forEach(schedules, schedule => {
|
||||
this.jobs && this.jobs[schedule.job] && this.jobs[schedule.job].key === JOBKEY && (s[schedule.id] = schedule)
|
||||
})
|
||||
this.schedules = s
|
||||
})
|
||||
|
||||
const refreshJobs = () => xo.job.getAll()
|
||||
.then(jobs => {
|
||||
const j = {}
|
||||
forEach(jobs, job => {
|
||||
j[job.id] = job
|
||||
})
|
||||
this.jobs = j
|
||||
})
|
||||
|
||||
const refresh = () => refreshJobs().then(refreshSchedules)
|
||||
const getReady = () => refresh().then(() => this.ready = true)
|
||||
getReady()
|
||||
|
||||
const interval = $interval(refresh, 5e3)
|
||||
$scope.$on('$destroy', () => $interval.cancel(interval))
|
||||
|
||||
const toggleState = (toggle, state) => {
|
||||
const selectedVms = this.formData.selectedVms.slice()
|
||||
if (toggle) {
|
||||
const vms = filterFilter(selectHighLevelFilter(this.objects), {type: 'VM'})
|
||||
forEach(vms, vm => {
|
||||
if (vm.power_state === state) {
|
||||
(selectedVms.indexOf(vm) === -1) && selectedVms.push(vm)
|
||||
}
|
||||
})
|
||||
this.formData.selectedVms = selectedVms
|
||||
} else {
|
||||
const keptVms = []
|
||||
for (let index in this.formData.selectedVms) {
|
||||
if (this.formData.selectedVms[index].power_state !== state) {
|
||||
keptVms.push(this.formData.selectedVms[index])
|
||||
}
|
||||
}
|
||||
this.formData.selectedVms = keptVms
|
||||
}
|
||||
}
|
||||
|
||||
this.toggleAllRunning = toggle => toggleState(toggle, 'Running')
|
||||
this.toggleAllHalted = toggle => toggleState(toggle, 'Halted')
|
||||
|
||||
this.edit = schedule => {
|
||||
const vms = filterFilter(selectHighLevelFilter(this.objects), {type: 'VM'})
|
||||
const job = this.jobs[schedule.job]
|
||||
const selectedVms = []
|
||||
forEach(job.paramsVector.items[0].values, value => {
|
||||
const vm = find(vms, vm => vm.id === value.vm)
|
||||
vm && selectedVms.push(vm)
|
||||
})
|
||||
const tag = job.paramsVector.items[0].values[0].tag
|
||||
const selectedSr = xoApi.get(job.paramsVector.items[0].values[0].sr)
|
||||
const _reportWhen = job.paramsVector.items[0].values[0]._reportWhen
|
||||
const cronPattern = schedule.cron
|
||||
|
||||
this.resetData()
|
||||
// const formData = this.formData
|
||||
this.formData.selectedVms = selectedVms
|
||||
this.formData.tag = tag
|
||||
this.formData.selectedSr = selectedSr
|
||||
this.formData.scheduleId = schedule.id
|
||||
this.formData._reportWhen = _reportWhen
|
||||
this.scheduleApi.setCron(cronPattern)
|
||||
}
|
||||
|
||||
this.save = (id, vms, tag, sr, cron, enabled, _reportWhen) => {
|
||||
if (!vms.length) {
|
||||
notify.warning({
|
||||
title: 'No Vms selected',
|
||||
message: 'Choose VMs to copy'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const _save = (id === undefined) ? saveNew(vms, tag, sr, cron, enabled, _reportWhen) : save(id, vms, tag, sr, cron, _reportWhen)
|
||||
return _save
|
||||
.then(() => {
|
||||
notify.info({
|
||||
title: 'Continuous Replication',
|
||||
message: 'Job schedule successfuly saved'
|
||||
})
|
||||
this.resetData()
|
||||
})
|
||||
.finally(refresh)
|
||||
}
|
||||
|
||||
const save = (id, vms, tag, sr, cron, _reportWhen) => {
|
||||
const schedule = this.schedules[id]
|
||||
const job = this.jobs[schedule.job]
|
||||
const values = []
|
||||
forEach(vms, vm => {
|
||||
values.push({vm: vm.id, tag, sr: sr.id, _reportWhen})
|
||||
})
|
||||
job.paramsVector.items[0].values = values
|
||||
return xo.job.set(job)
|
||||
.then(response => {
|
||||
if (response) {
|
||||
return xo.schedule.set(schedule.id, undefined, cron, undefined)
|
||||
} else {
|
||||
notify.error({
|
||||
title: 'Update schedule',
|
||||
message: 'Job updating failed'
|
||||
})
|
||||
throw new Error('Job updating failed')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const saveNew = (vms, tag, sr, cron, enabled, _reportWhen) => {
|
||||
const values = []
|
||||
forEach(vms, vm => {
|
||||
values.push({vm: vm.id, tag, sr: sr.id, _reportWhen})
|
||||
})
|
||||
const job = {
|
||||
type: 'call',
|
||||
key: JOBKEY,
|
||||
method: 'vm.deltaCopy',
|
||||
paramsVector: {
|
||||
type: 'crossProduct',
|
||||
items: [{
|
||||
type: 'set',
|
||||
values
|
||||
}]
|
||||
}
|
||||
}
|
||||
return xo.job.create(job)
|
||||
.then(jobId => xo.schedule.create(jobId, cron, enabled))
|
||||
}
|
||||
|
||||
this.delete = schedule => {
|
||||
let jobId = schedule.job
|
||||
return xo.schedule.delete(schedule.id)
|
||||
.then(() => xo.job.delete(jobId))
|
||||
.finally(() => {
|
||||
if (this.formData.scheduleId === schedule.id) {
|
||||
this.resetData()
|
||||
}
|
||||
refresh()
|
||||
})
|
||||
}
|
||||
|
||||
this.run = schedule => {
|
||||
this.running[schedule.id] = true
|
||||
notify.info({
|
||||
title: 'Run Job',
|
||||
message: 'One shot running started. See overview for logs.'
|
||||
})
|
||||
const id = schedule.job
|
||||
return xo.job.runSequence([id]).finally(() => delete this.running[schedule.id])
|
||||
}
|
||||
|
||||
this.inTargetPool = vm => vm.$poolId === (this.formData.selectedSr && this.formData.selectedSr.$poolId)
|
||||
|
||||
this.resetData = () => {
|
||||
this.formData.allRunning = false
|
||||
this.formData.allHalted = false
|
||||
this.formData.selectedVms = []
|
||||
this.formData.scheduleId = undefined
|
||||
this.formData.tag = undefined
|
||||
this.formData.selectedSr = undefined
|
||||
this.formData.enabled = false
|
||||
this.formData._reportWhen = undefined
|
||||
this.scheduleApi && this.scheduleApi.resetData && this.scheduleApi.resetData()
|
||||
}
|
||||
|
||||
this.collectionLength = col => Object.keys(col).length
|
||||
this.prettyCron = prettyCron.toString.bind(prettyCron)
|
||||
|
||||
if (!this.comesForEditing) {
|
||||
refresh()
|
||||
} else {
|
||||
refresh()
|
||||
.then(() => {
|
||||
this.edit(this.schedules[this.comesForEditing])
|
||||
delete this.comesForEditing
|
||||
})
|
||||
}
|
||||
this.resetData()
|
||||
this.objects = xoApi.all
|
||||
})
|
||||
|
||||
// A module exports its name.
|
||||
.name
|
||||
143
app/modules/backup/continuous-replication/view.jade
Normal file
@@ -0,0 +1,143 @@
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.fa.fa-map-signs(style="color: #e25440;")
|
||||
| Continuous Replication
|
||||
form#ciform(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.selectedVms, ctrl.formData.tag, ctrl.formData.selectedSr, ctrl.formData.cronPattern, ctrl.formData.enabled, ctrl.formData._reportWhen)')
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.xo-icon-vm(style='color: #e25440;')
|
||||
| VMs to copy
|
||||
.panel-body.form-horizontal
|
||||
.text-center(ng-if = '!ctrl.formData'): i.xo-icon-loading
|
||||
.container-fluid(ng-if = 'ctrl.formData')
|
||||
.alert.alert-info(ng-if = '!ctrl.formData.scheduleId') Creating New Continuous Replication
|
||||
.alert.alert-warning(ng-if = 'ctrl.formData.scheduleId') Modifying Continuous Replication ID {{ ctrl.formData.scheduleId }}
|
||||
.form-group
|
||||
label.control-label.col-md-2(for = 'tag') Tag
|
||||
.col-md-10
|
||||
input#tag.form-control(form = 'ciform', ng-model = 'ctrl.formData.tag', placeholder = 'VM copy tag', required)
|
||||
.form-group(ng-class = '{"has-warning": !ctrl.formData.selectedVms.length}')
|
||||
label.control-label.col-md-2(for = 'vmlist') VMs
|
||||
.col-md-8
|
||||
ui-select#vmlist(form = 'ciform', ng-model = 'ctrl.formData.selectedVms', multiple, close-on-select = 'false', required)
|
||||
ui-select-match(placeholder = 'Choose VMs to copy')
|
||||
span(ng-class = '{"bg-danger": ctrl.inTargetPool($item)}')
|
||||
i.xo-icon-working(ng-if="isVMWorking($item)")
|
||||
i(class="xo-icon-{{$item.power_state | lowercase}}",ng-if="!isVMWorking($item)")
|
||||
| {{$item.name_label}}
|
||||
span(ng-if="$item.$container")
|
||||
| ({{($item.$container | resolve).name_label}})
|
||||
ui-select-choices(repeat = 'vm in ctrl.objects | selectHighLevel | filter:{type: "VM"} | filter:$select.search | orderBy:["$container", "name_label"] track by vm.id')
|
||||
div
|
||||
i.xo-icon-working(ng-if="isVMWorking(vm)", tooltip="{{vm.power_state}} and {{(vm.current_operations | map)[0]}}")
|
||||
i(class="xo-icon-{{vm.power_state | lowercase}}",ng-if="!isVMWorking(vm)", tooltip="{{vm.power_state}}")
|
||||
| {{vm.name_label}}
|
||||
span(ng-if="vm.$container")
|
||||
| ({{(vm.$container | resolve).name_label || ((vm.$container | resolve).master | resolve).name_label}})
|
||||
.col-md-2
|
||||
label(tooltip = 'select/deselect all running VMs', style = 'cursor: pointer')
|
||||
input.hidden(form = 'ciform', type = 'checkbox', ng-model = 'ctrl.formData.allRunning', ng-change = 'ctrl.toggleAllRunning(ctrl.formData.allRunning)')
|
||||
span.fa-stack
|
||||
i.xo-icon-running.fa-stack-1x
|
||||
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allRunning')
|
||||
label(tooltip = 'select/deselect all halted VMs', style = 'cursor: pointer')
|
||||
input.hidden(form = 'ciform', type = 'checkbox', ng-model = 'ctrl.formData.allHalted', ng-change = 'ctrl.toggleAllHalted(ctrl.formData.allHalted)')
|
||||
span.fa-stack
|
||||
i.xo-icon-halted.fa-stack-1x
|
||||
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allHalted')
|
||||
.form-group(ng-if = '(ctrl.formData.selectedVms | filter:ctrl.inTargetPool).length')
|
||||
.col-md-offset-2.col-md-10
|
||||
.alert.alert-warning
|
||||
i.fa.fa-exclamation-triangle
|
||||
| At the moment, the selected VMs displayed in red are in the copy target pool.
|
||||
.form-group
|
||||
label.control-label.col-md-2(for = 'sr') To SR
|
||||
.col-md-10
|
||||
ui-select#sr(form = 'ciform', ng-model = 'ctrl.formData.selectedSr', required)
|
||||
ui-select-match(placeholder = 'Choose destination SR')
|
||||
i(class="xo-icon-sr")
|
||||
| {{$select.selected.name_label}}
|
||||
span(ng-if="$select.selected.$container")
|
||||
| ({{($select.selected.$container | resolve).name_label}})
|
||||
ui-select-choices(repeat = 'sr in ctrl.objects | selectHighLevel | filter:{type: "sr", content_type: "!iso"} | filter:$select.search | orderBy:["$container", "name_label"] track by sr.id')
|
||||
div
|
||||
i(class="xo-icon-sr")
|
||||
| {{sr.name_label}} ({{sr.size - sr.physical_usage | bytesToSize }})
|
||||
span(ng-if="sr.$container")
|
||||
| ({{(sr.$container | resolve).name_label || ((sr.$container | resolve).master | resolve).name_label}})
|
||||
.form-group(ng-if = '!ctrl.formData.scheduleId')
|
||||
label.control-label.col-md-2(for = 'enabled')
|
||||
input#enabled(form = 'ciform', ng-model = 'ctrl.formData.enabled', type = 'checkbox')
|
||||
.help-block.col-md-8 Enable immediately after creation
|
||||
.form-group
|
||||
label.control-label.col-md-2(for = '_reportWhen') Report
|
||||
.col-md-10
|
||||
select.form-control(ng-model = 'ctrl.formData._reportWhen')
|
||||
option(value = ''): em -- When to send reports --
|
||||
option(value = 'never') Never
|
||||
option(value = 'alway') Always
|
||||
option(value = 'fail') Failure
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-clock-o
|
||||
| Schedule
|
||||
.panel-body.form-horizontal
|
||||
.text-center(ng-if = '!ctrl.formData'): i.xo-icon-loading
|
||||
xo-scheduler(data = 'ctrl.formData', api = 'ctrl.scheduleApi')
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-body
|
||||
fieldset.center(ng-disabled = '!ctrl.ready')
|
||||
button.btn.btn-lg.btn-primary(form = 'ciform', type = 'submit')
|
||||
i.fa.fa-clock-o
|
||||
|
|
||||
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 | map | orderBy:"id":true track by schedule.id')
|
||||
td {{ schedule.id }}
|
||||
td {{ ctrl.jobs[schedule.job].paramsVector.items[0].values[0].tag }}
|
||||
td.hidden-xs.hidden-sm
|
||||
div(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values.length == 1')
|
||||
| {{ (ctrl.jobs[schedule.job].paramsVector.items[0].values[0].vm | resolve).name_label }}
|
||||
div(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values.length > 1')
|
||||
button.btn.btn-info(type = 'button', ng-click = 'unCollapsed = !unCollapsed')
|
||||
| {{ ctrl.jobs[schedule.job].paramsVector.items[0].values.length }} VMs
|
||||
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
|
||||
@@ -8,27 +8,29 @@ 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('scheduler.backup', [
|
||||
export default angular.module('backup.deltaBackup', [
|
||||
uiRouter,
|
||||
uiBootstrap
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('scheduler.backup', {
|
||||
url: '/backup/:id',
|
||||
controller: 'BackupCtrl as ctrl',
|
||||
$stateProvider.state('backup.deltaBackup', {
|
||||
url: '/delta-backup/:id',
|
||||
controller: 'DeltaBackupCtrl as ctrl',
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('BackupCtrl', function ($scope, $stateParams, $interval, xo, xoApi, notify, selectHighLevelFilter, filterFilter) {
|
||||
const JOBKEY = 'rollingBackup'
|
||||
.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 = {}
|
||||
@@ -38,7 +40,10 @@ export default angular.module('scheduler.backup', [
|
||||
return xo.remote.getAll()
|
||||
.then(remotes => {
|
||||
const r = {}
|
||||
forEach(remotes, remote => r[remote.id] = remote)
|
||||
forEach(remotes, remote => {
|
||||
remote = parse(remote)
|
||||
r[remote.id] = remote
|
||||
})
|
||||
this.remotes = r
|
||||
if (selectRemoteId) {
|
||||
this.formData.remote = this.remotes[selectRemoteId]
|
||||
@@ -61,27 +66,20 @@ export default angular.module('scheduler.backup', [
|
||||
return xo.job.getAll()
|
||||
.then(jobs => {
|
||||
const j = {}
|
||||
forEach(jobs, job => j[job.id] = job)
|
||||
forEach(jobs, job => {
|
||||
j[job.id] = job
|
||||
})
|
||||
this.jobs = j
|
||||
})
|
||||
}
|
||||
|
||||
const refresh = () => {
|
||||
return refreshRemotes().then(refreshJobs).then(refreshSchedules)
|
||||
}
|
||||
const refresh = () => refreshRemotes().then(refreshJobs).then(refreshSchedules)
|
||||
|
||||
this.getReady = () => {
|
||||
return refresh()
|
||||
.then(() => this.ready = true)
|
||||
}
|
||||
this.getReady = () => refresh().then(() => this.ready = true)
|
||||
this.getReady()
|
||||
|
||||
const interval = $interval(() => {
|
||||
refresh()
|
||||
}, 5e3)
|
||||
$scope.$on('$destroy', () => {
|
||||
$interval.cancel(interval)
|
||||
})
|
||||
const interval = $interval(refresh, 5e3)
|
||||
$scope.$on('$destroy', () => $interval.cancel(interval))
|
||||
|
||||
const toggleState = (toggle, state) => {
|
||||
const selectedVms = this.formData.selectedVms.slice()
|
||||
@@ -112,32 +110,34 @@ export default angular.module('scheduler.backup', [
|
||||
const job = this.jobs[schedule.job]
|
||||
const selectedVms = []
|
||||
forEach(job.paramsVector.items[0].values, value => {
|
||||
const vm = find(vms, vm => vm.id === value.id)
|
||||
const vm = find(vms, vm => vm.id === value.vm)
|
||||
vm && selectedVms.push(vm)
|
||||
})
|
||||
const tag = job.paramsVector.items[0].values[0].tag
|
||||
const depth = job.paramsVector.items[0].values[0].depth
|
||||
const _reportWhen = job.paramsVector.items[0].values[0]._reportWhen
|
||||
const cronPattern = schedule.cron
|
||||
const remoteId = job.paramsVector.items[0].values[0].remoteId
|
||||
const remoteId = job.paramsVector.items[0].values[0].remote
|
||||
|
||||
this.resetData()
|
||||
this.formData.selectedVms = selectedVms
|
||||
this.formData.tag = tag
|
||||
this.formData.depth = depth
|
||||
this.formData.scheduleId = schedule.id
|
||||
this.formData._reportWhen = _reportWhen
|
||||
this.formData.remote = this.remotes[remoteId]
|
||||
this.scheduleApi.setCron(cronPattern)
|
||||
}
|
||||
|
||||
this.save = (id, vms, remoteId, tag, depth, cron, enabled) => {
|
||||
this.save = (id, vms, remoteId, tag, depth, cron, enabled, _reportWhen) => {
|
||||
if (!vms.length) {
|
||||
notify.warning({
|
||||
title: 'No Vms selected',
|
||||
message: 'Choose VMs to back up'
|
||||
message: 'Choose VMs to backup'
|
||||
})
|
||||
return
|
||||
}
|
||||
const _save = (id === undefined) ? saveNew(vms, remoteId, tag, depth, cron, enabled) : save(id, vms, remoteId, tag, depth, cron)
|
||||
const _save = (id === undefined) ? saveNew(vms, remoteId, tag, depth, cron, enabled, _reportWhen) : save(id, vms, remoteId, tag, depth, cron, _reportWhen)
|
||||
return _save
|
||||
.then(() => {
|
||||
notify.info({
|
||||
@@ -146,21 +146,20 @@ export default angular.module('scheduler.backup', [
|
||||
})
|
||||
this.resetData()
|
||||
})
|
||||
.finally(() => {
|
||||
refresh()
|
||||
})
|
||||
.finally(refresh)
|
||||
}
|
||||
|
||||
const save = (id, vms, remoteId, tag, depth, cron) => {
|
||||
const save = (id, vms, remoteId, tag, depth, cron, _reportWhen) => {
|
||||
const schedule = this.schedules[id]
|
||||
const job = this.jobs[schedule.job]
|
||||
const values = []
|
||||
forEach(vms, vm => {
|
||||
values.push({
|
||||
id: vm.id,
|
||||
remoteId,
|
||||
vm: vm.id,
|
||||
remote: remoteId,
|
||||
tag,
|
||||
depth
|
||||
depth,
|
||||
_reportWhen
|
||||
})
|
||||
})
|
||||
job.paramsVector.items[0].values = values
|
||||
@@ -178,45 +177,55 @@ export default angular.module('scheduler.backup', [
|
||||
})
|
||||
}
|
||||
|
||||
const saveNew = (vms, remoteId, tag, depth, cron, enabled) => {
|
||||
const saveNew = (vms, remoteId, tag, depth, cron, enabled, _reportWhen) => {
|
||||
const values = []
|
||||
forEach(vms, vm => {
|
||||
values.push({
|
||||
id: vm.id,
|
||||
remoteId,
|
||||
vm: vm.id,
|
||||
remote: remoteId,
|
||||
tag,
|
||||
depth
|
||||
depth,
|
||||
_reportWhen
|
||||
})
|
||||
})
|
||||
const job = {
|
||||
type: 'call',
|
||||
key: JOBKEY,
|
||||
method: 'vm.rollingBackup',
|
||||
method: 'vm.rollingDeltaBackup',
|
||||
paramsVector: {
|
||||
type: 'crossProduct',
|
||||
items: [
|
||||
{
|
||||
type: 'set',
|
||||
values
|
||||
}
|
||||
]
|
||||
items: [{
|
||||
type: 'set',
|
||||
values
|
||||
}]
|
||||
}
|
||||
}
|
||||
return xo.job.create(job)
|
||||
.then(jobId => {
|
||||
return xo.schedule.create(jobId, cron, enabled)
|
||||
})
|
||||
.then(jobId => xo.schedule.create(jobId, cron, enabled))
|
||||
}
|
||||
|
||||
this.delete = schedule => {
|
||||
let jobId = schedule.job
|
||||
xo.schedule.delete(schedule.id)
|
||||
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 = () => {
|
||||
@@ -228,6 +237,7 @@ export default angular.module('scheduler.backup', [
|
||||
this.formData.path = undefined
|
||||
this.formData.depth = undefined
|
||||
this.formData.enabled = false
|
||||
this.formData._reportWhen = undefined
|
||||
this.formData.remote = undefined
|
||||
this.scheduleApi && this.scheduleApi.resetData && this.scheduleApi.resetData()
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.fa.fa-download(style="color: #e25440;")
|
||||
| Backup
|
||||
form#backupform(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.selectedVms, ctrl.formData.remote.id, ctrl.formData.tag, ctrl.formData.depth, ctrl.formData.cronPattern, ctrl.formData.enabled)')
|
||||
| Delta Backup
|
||||
form#backupform(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.selectedVms, ctrl.formData.remote.id, ctrl.formData.tag, ctrl.formData.depth, ctrl.formData.cronPattern, ctrl.formData.enabled, ctrl.formData._reportWhen)')
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
@@ -16,11 +16,11 @@ form#backupform(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.s
|
||||
.form-group
|
||||
label.control-label.col-md-2(for = 'tag') Tag
|
||||
.col-md-10
|
||||
input#tag.form-control(ng-model = 'ctrl.formData.tag', placeholder = 'Back-up tag', required)
|
||||
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(ng-model = 'ctrl.formData.selectedVms', multiple, close-on-select = 'false', required)
|
||||
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)")
|
||||
@@ -36,33 +36,41 @@ form#backupform(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.s
|
||||
| ({{(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(type = 'checkbox', ng-model = 'ctrl.formData.allRunning', ng-change = 'ctrl.toggleAllRunning(ctrl.formData.allRunning)')
|
||||
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(type = 'checkbox', ng-model = 'ctrl.formData.allHalted', ng-change = 'ctrl.toggleAllHalted(ctrl.formData.allHalted)')
|
||||
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(ng-model = 'ctrl.formData.depth', placeholder = 'How many backups to rollover', type = 'number', min = '1', required)
|
||||
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(ng-options = 'remote.name group by remote.type for remote in ctrl.remotes', ng-model = 'ctrl.formData.remote' required)
|
||||
select#remote.form-control(form = 'backupform', ng-options = 'remote.name group by remote.type for remote in ctrl.remotes | map | orderBy:["type","name"]', ng-model = 'ctrl.formData.remote' required)
|
||||
option(value = ''): em -- Choose a file system remote point --
|
||||
.form-group
|
||||
.col-md-10.col-md-offset-2
|
||||
a.btn.btn-info(ui-sref = 'scheduler.remote')
|
||||
a(ui-sref = 'backup.remote')
|
||||
i.fa.fa-pencil
|
||||
| Manage your remote points
|
||||
| Manage your remote stores
|
||||
.form-group(ng-if = '!ctrl.formData.scheduleId')
|
||||
label.control-label.col-md-2(for = 'enabled')
|
||||
input#enabled(ng-model = 'ctrl.formData.enabled', type = 'checkbox')
|
||||
.help-block.col-md-10 Enable immediatly after creation
|
||||
input#enabled(form = 'backupform', ng-model = 'ctrl.formData.enabled', type = 'checkbox')
|
||||
.help-block.col-md-10 Enable immediately after creation
|
||||
.form-group
|
||||
label.control-label.col-md-2(for = '_reportWhen') Report
|
||||
.col-md-10
|
||||
select.form-control(ng-model = 'ctrl.formData._reportWhen')
|
||||
option(value = ''): em -- When to send reports --
|
||||
option(value = 'never') Never
|
||||
option(value = 'alway') Always
|
||||
option(value = 'fail') Failure
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-clock-o
|
||||
@@ -73,8 +81,8 @@ form#backupform(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.s
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-body
|
||||
p.center
|
||||
button.btn.btn-lg.btn-primary(form = 'backupform', type = 'submit', ng-disabled = '!ctrl.ready')
|
||||
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
|
||||
@@ -82,7 +90,7 @@ form#backupform(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.s
|
||||
i.fa.fa-database
|
||||
| Save
|
||||
|
|
||||
button.btn.btn-lg.btn-default(type = 'button', ng-click = 'ctrl.resetData()', ng-disabled = '!ctrl.ready')
|
||||
button.btn.btn-lg.btn-default(type = 'button', ng-click = 'ctrl.resetData()')
|
||||
| Reset
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
@@ -96,35 +104,37 @@ form#backupform(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.s
|
||||
tr
|
||||
th ID
|
||||
th Tag
|
||||
th VMs to backup
|
||||
th Remote
|
||||
th Depth
|
||||
th Scheduling
|
||||
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')
|
||||
tr(ng-repeat = 'schedule in ctrl.schedules | map | orderBy:"id":true track by schedule.id')
|
||||
td {{ schedule.id }}
|
||||
td {{ ctrl.jobs[schedule.job].paramsVector.items[0].values[0].tag }}
|
||||
td
|
||||
td.hidden-xs.hidden-sm
|
||||
div(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values.length == 1')
|
||||
| {{ (ctrl.jobs[schedule.job].paramsVector.items[0].values[0].id | resolve).name_label }}
|
||||
| {{ (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
|
||||
li(ng-repeat = 'item in ctrl.jobs[schedule.job].paramsVector.items[0].values')
|
||||
span(ng-if = 'item.id | resolve') {{ (item.id | resolve).name_label }}
|
||||
span(ng-if = '(item.id | resolve).$container') ({{ ((item.id | resolve).$container | resolve).name_label }})
|
||||
td
|
||||
strong: a(ui-sref = 'scheduler.remote') {{ ctrl.remotes[ctrl.jobs[schedule.job].paramsVector.items[0].values[0].remoteId].name }}
|
||||
td {{ ctrl.jobs[schedule.job].paramsVector.items[0].values[0].depth }}
|
||||
td {{ ctrl.prettyCron(schedule.cron) }}
|
||||
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 = 'backup.remote') {{ ctrl.remotes[ctrl.jobs[schedule.job].paramsVector.items[0].values[0].remote].name }}
|
||||
td.hidden-xs {{ ctrl.jobs[schedule.job].paramsVector.items[0].values[0].depth }}
|
||||
td.hidden-xs {{ ctrl.prettyCron(schedule.cron) }}
|
||||
td.text-center
|
||||
i.fa.fa-check(ng-if = 'schedule.enabled')
|
||||
td.text-right
|
||||
button.btn.btn-primary(type = 'button', ng-click = 'ctrl.edit(schedule)'): i.fa.fa-pencil
|
||||
|
|
||||
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
|
||||
227
app/modules/backup/disaster-recovery/index.js
Normal file
@@ -0,0 +1,227 @@
|
||||
import angular from 'angular'
|
||||
import find from 'lodash.find'
|
||||
import forEach from 'lodash.foreach'
|
||||
import later from 'later'
|
||||
import prettyCron from 'prettycron'
|
||||
import uiBootstrap from 'angular-ui-bootstrap'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
|
||||
later.date.localTime()
|
||||
|
||||
import view from './view'
|
||||
|
||||
// ====================================================================
|
||||
|
||||
export default angular.module('backup.disasterrecovery', [
|
||||
uiRouter,
|
||||
uiBootstrap
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('backup.disasterrecovery', {
|
||||
url: '/disasterrecovery/:id',
|
||||
controller: 'DisasterRecoveryCtrl as ctrl',
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('DisasterRecoveryCtrl', function ($scope, $stateParams, $interval, xo, xoApi, notify, selectHighLevelFilter, filterFilter) {
|
||||
const JOBKEY = 'disasterRecovery'
|
||||
|
||||
this.ready = false
|
||||
|
||||
this.running = {}
|
||||
this.comesForEditing = $stateParams.id
|
||||
this.scheduleApi = {}
|
||||
this.formData = {}
|
||||
|
||||
const refreshSchedules = () => xo.schedule.getAll()
|
||||
.then(schedules => {
|
||||
const s = {}
|
||||
forEach(schedules, schedule => {
|
||||
this.jobs && this.jobs[schedule.job] && this.jobs[schedule.job].key === JOBKEY && (s[schedule.id] = schedule)
|
||||
})
|
||||
this.schedules = s
|
||||
})
|
||||
|
||||
const refreshJobs = () => xo.job.getAll()
|
||||
.then(jobs => {
|
||||
const j = {}
|
||||
forEach(jobs, job => {
|
||||
j[job.id] = job
|
||||
})
|
||||
this.jobs = j
|
||||
})
|
||||
|
||||
const refresh = () => refreshJobs().then(refreshSchedules)
|
||||
const getReady = () => refresh().then(() => this.ready = true)
|
||||
getReady()
|
||||
|
||||
const interval = $interval(refresh, 5e3)
|
||||
$scope.$on('$destroy', () => $interval.cancel(interval))
|
||||
|
||||
const toggleState = (toggle, state) => {
|
||||
const selectedVms = this.formData.selectedVms.slice()
|
||||
if (toggle) {
|
||||
const vms = filterFilter(selectHighLevelFilter(this.objects), {type: 'VM'})
|
||||
forEach(vms, vm => {
|
||||
if (vm.power_state === state) {
|
||||
(selectedVms.indexOf(vm) === -1) && selectedVms.push(vm)
|
||||
}
|
||||
})
|
||||
this.formData.selectedVms = selectedVms
|
||||
} else {
|
||||
const keptVms = []
|
||||
for (let index in this.formData.selectedVms) {
|
||||
if (this.formData.selectedVms[index].power_state !== state) {
|
||||
keptVms.push(this.formData.selectedVms[index])
|
||||
}
|
||||
}
|
||||
this.formData.selectedVms = keptVms
|
||||
}
|
||||
}
|
||||
|
||||
this.toggleAllRunning = toggle => toggleState(toggle, 'Running')
|
||||
this.toggleAllHalted = toggle => toggleState(toggle, 'Halted')
|
||||
|
||||
this.edit = schedule => {
|
||||
const vms = filterFilter(selectHighLevelFilter(this.objects), {type: 'VM'})
|
||||
const job = this.jobs[schedule.job]
|
||||
const selectedVms = []
|
||||
forEach(job.paramsVector.items[0].values, value => {
|
||||
const vm = find(vms, vm => vm.id === value.id)
|
||||
vm && selectedVms.push(vm)
|
||||
})
|
||||
const tag = job.paramsVector.items[0].values[0].tag
|
||||
const selectedPool = xoApi.get(job.paramsVector.items[0].values[0].pool)
|
||||
const depth = job.paramsVector.items[0].values[0].depth
|
||||
const _reportWhen = job.paramsVector.items[0].values[0]._reportWhen
|
||||
const cronPattern = schedule.cron
|
||||
|
||||
this.resetData()
|
||||
// const formData = this.formData
|
||||
this.formData.selectedVms = selectedVms
|
||||
this.formData.tag = tag
|
||||
this.formData.selectedPool = selectedPool
|
||||
this.formData.depth = depth
|
||||
this.formData.scheduleId = schedule.id
|
||||
this.formData._reportWhen = _reportWhen
|
||||
this.scheduleApi.setCron(cronPattern)
|
||||
}
|
||||
|
||||
this.save = (id, vms, tag, pool, depth, cron, enabled, _reportWhen) => {
|
||||
if (!vms.length) {
|
||||
notify.warning({
|
||||
title: 'No Vms selected',
|
||||
message: 'Choose VMs to copy'
|
||||
})
|
||||
return
|
||||
}
|
||||
const _save = (id === undefined) ? saveNew(vms, tag, pool, depth, cron, enabled, _reportWhen) : save(id, vms, tag, pool, depth, cron, _reportWhen)
|
||||
return _save
|
||||
.then(() => {
|
||||
notify.info({
|
||||
title: 'Disaster Recovery',
|
||||
message: 'Job schedule successfuly saved'
|
||||
})
|
||||
this.resetData()
|
||||
})
|
||||
.finally(refresh)
|
||||
}
|
||||
|
||||
const save = (id, vms, tag, pool, depth, cron, _reportWhen) => {
|
||||
const schedule = this.schedules[id]
|
||||
const job = this.jobs[schedule.job]
|
||||
const values = []
|
||||
forEach(vms, vm => {
|
||||
values.push({id: vm.id, tag, pool: pool.id, depth, _reportWhen})
|
||||
})
|
||||
job.paramsVector.items[0].values = values
|
||||
return xo.job.set(job)
|
||||
.then(response => {
|
||||
if (response) {
|
||||
return xo.schedule.set(schedule.id, undefined, cron, undefined)
|
||||
} else {
|
||||
notify.error({
|
||||
title: 'Update schedule',
|
||||
message: 'Job updating failed'
|
||||
})
|
||||
throw new Error('Job updating failed')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const saveNew = (vms, tag, pool, depth, cron, enabled, _reportWhen) => {
|
||||
const values = []
|
||||
forEach(vms, vm => {
|
||||
values.push({id: vm.id, tag, pool: pool.id, depth, _reportWhen})
|
||||
})
|
||||
const job = {
|
||||
type: 'call',
|
||||
key: JOBKEY,
|
||||
method: 'vm.rollingDrCopy',
|
||||
paramsVector: {
|
||||
type: 'crossProduct',
|
||||
items: [{
|
||||
type: 'set',
|
||||
values
|
||||
}]
|
||||
}
|
||||
}
|
||||
return xo.job.create(job)
|
||||
.then(jobId => xo.schedule.create(jobId, cron, enabled))
|
||||
}
|
||||
|
||||
this.delete = schedule => {
|
||||
let jobId = schedule.job
|
||||
return xo.schedule.delete(schedule.id)
|
||||
.then(() => xo.job.delete(jobId))
|
||||
.finally(() => {
|
||||
if (this.formData.scheduleId === schedule.id) {
|
||||
this.resetData()
|
||||
}
|
||||
refresh()
|
||||
})
|
||||
}
|
||||
|
||||
this.run = schedule => {
|
||||
this.running[schedule.id] = true
|
||||
notify.info({
|
||||
title: 'Run Job',
|
||||
message: 'One shot running started. See overview for logs.'
|
||||
})
|
||||
const id = schedule.job
|
||||
return xo.job.runSequence([id]).finally(() => delete this.running[schedule.id])
|
||||
}
|
||||
|
||||
this.inTargetPool = vm => vm.$poolId === (this.formData.selectedPool && this.formData.selectedPool.id)
|
||||
|
||||
this.resetData = () => {
|
||||
this.formData.allRunning = false
|
||||
this.formData.allHalted = false
|
||||
this.formData.selectedVms = []
|
||||
this.formData.scheduleId = undefined
|
||||
this.formData.tag = undefined
|
||||
this.formData.selectedPool = undefined
|
||||
this.formData.depth = undefined
|
||||
this.formData.enabled = false
|
||||
this.formData._reportWhen = undefined
|
||||
this.scheduleApi && this.scheduleApi.resetData && this.scheduleApi.resetData()
|
||||
}
|
||||
|
||||
this.collectionLength = col => Object.keys(col).length
|
||||
this.prettyCron = prettyCron.toString.bind(prettyCron)
|
||||
|
||||
if (!this.comesForEditing) {
|
||||
refresh()
|
||||
} else {
|
||||
refresh()
|
||||
.then(() => {
|
||||
this.edit(this.schedules[this.comesForEditing])
|
||||
delete this.comesForEditing
|
||||
})
|
||||
}
|
||||
this.resetData()
|
||||
this.objects = xoApi.all
|
||||
})
|
||||
|
||||
// A module exports its name.
|
||||
.name
|
||||
153
app/modules/backup/disaster-recovery/view.jade
Normal file
@@ -0,0 +1,153 @@
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.fa.fa-medkit(style="color: #e25440;")
|
||||
| Disaster Recovery
|
||||
form#drform(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.selectedVms, ctrl.formData.tag, ctrl.formData.selectedPool, ctrl.formData.depth, ctrl.formData.cronPattern, ctrl.formData.enabled, ctrl.formData._reportWhen)')
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.xo-icon-vm(style='color: #e25440;')
|
||||
| VMs to copy
|
||||
.panel-body.form-horizontal
|
||||
.text-center(ng-if = '!ctrl.formData'): i.xo-icon-loading
|
||||
.container-fluid(ng-if = 'ctrl.formData')
|
||||
.alert.alert-info(ng-if = '!ctrl.formData.scheduleId') Creating New Disaster Recovery
|
||||
.alert.alert-warning(ng-if = 'ctrl.formData.scheduleId') Modifying Disaster Recovery ID {{ ctrl.formData.scheduleId }}
|
||||
.form-group
|
||||
label.control-label.col-md-2(for = 'tag') Tag
|
||||
.col-md-10
|
||||
.input-group
|
||||
span.input-group-addon DR_
|
||||
input#tag.form-control(form = 'drform', ng-model = 'ctrl.formData.tag', placeholder = 'VM copy tag', required)
|
||||
.form-group(ng-class = '{"has-warning": !ctrl.formData.selectedVms.length}')
|
||||
label.control-label.col-md-2(for = 'vmlist') VMs
|
||||
.col-md-8
|
||||
ui-select#vmlist(form = 'drform', ng-model = 'ctrl.formData.selectedVms', multiple, close-on-select = 'false', required)
|
||||
ui-select-match(placeholder = 'Choose VMs to copy')
|
||||
span(ng-class = '{"bg-danger": ctrl.inTargetPool($item)}')
|
||||
i.xo-icon-working(ng-if="isVMWorking($item)")
|
||||
i(class="xo-icon-{{$item.power_state | lowercase}}",ng-if="!isVMWorking($item)")
|
||||
| {{$item.name_label}}
|
||||
span(ng-if="$item.$container")
|
||||
| ({{($item.$container | resolve).name_label}})
|
||||
ui-select-choices(repeat = 'vm in ctrl.objects | selectHighLevel | filter:{type: "VM"} | filter:$select.search | orderBy:["$container", "name_label"] track by vm.id')
|
||||
div
|
||||
i.xo-icon-working(ng-if="isVMWorking(vm)", tooltip="{{vm.power_state}} and {{(vm.current_operations | map)[0]}}")
|
||||
i(class="xo-icon-{{vm.power_state | lowercase}}",ng-if="!isVMWorking(vm)", tooltip="{{vm.power_state}}")
|
||||
| {{vm.name_label}}
|
||||
span(ng-if="vm.$container")
|
||||
| ({{(vm.$container | resolve).name_label || ((vm.$container | resolve).master | resolve).name_label}})
|
||||
.col-md-2
|
||||
label(tooltip = 'select/deselect all running VMs', style = 'cursor: pointer')
|
||||
input.hidden(form = 'drform', type = 'checkbox', ng-model = 'ctrl.formData.allRunning', ng-change = 'ctrl.toggleAllRunning(ctrl.formData.allRunning)')
|
||||
span.fa-stack
|
||||
i.xo-icon-running.fa-stack-1x
|
||||
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allRunning')
|
||||
label(tooltip = 'select/deselect all halted VMs', style = 'cursor: pointer')
|
||||
input.hidden(form = 'drform', type = 'checkbox', ng-model = 'ctrl.formData.allHalted', ng-change = 'ctrl.toggleAllHalted(ctrl.formData.allHalted)')
|
||||
span.fa-stack
|
||||
i.xo-icon-halted.fa-stack-1x
|
||||
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allHalted')
|
||||
.form-group(ng-if = '(ctrl.formData.selectedVms | filter:ctrl.inTargetPool).length')
|
||||
.col-md-offset-2.col-md-10
|
||||
.alert.alert-warning
|
||||
i.fa.fa-exclamation-triangle
|
||||
| At the moment, the selected VMs displayed in red are in the copy target pool.
|
||||
.form-group
|
||||
label.control-label.col-md-2(for = 'pool') To Pool
|
||||
.col-md-10
|
||||
ui-select#pool(form = 'drform', ng-model = 'ctrl.formData.selectedPool', required)
|
||||
ui-select-match(placeholder = 'Choose destination pool')
|
||||
i(class="xo-icon-pool")
|
||||
| {{$select.selected.name_label}}
|
||||
span(ng-if="$select.selected.$container")
|
||||
| ({{($select.selected.$container | resolve).name_label}})
|
||||
ui-select-choices(repeat = 'pool in ctrl.objects | selectHighLevel | filter:{type: "pool"} | filter:$select.search | orderBy:["$container", "name_label"] track by pool.id')
|
||||
div
|
||||
i(class="xo-icon-pool")
|
||||
| {{pool.name_label}}
|
||||
span(ng-if="pool.$container")
|
||||
| ({{(pool.$container | resolve).name_label || ((pool.$container | resolve).master | resolve).name_label}})
|
||||
.form-group
|
||||
label.control-label.col-md-2(for = 'depth') Depth
|
||||
.col-md-10
|
||||
input#depth.form-control(form = 'drform', ng-model = 'ctrl.formData.depth', placeholder = 'How many VM copies to rollover', type = 'number', min = '1', required)
|
||||
.form-group(ng-if = '!ctrl.formData.scheduleId')
|
||||
label.control-label.col-md-2(for = 'enabled')
|
||||
input#enabled(form = 'drform', ng-model = 'ctrl.formData.enabled', type = 'checkbox')
|
||||
.help-block.col-md-8 Enable immediately after creation
|
||||
.form-group
|
||||
label.control-label.col-md-2(for = '_reportWhen') Report
|
||||
.col-md-10
|
||||
select.form-control(ng-model = 'ctrl.formData._reportWhen')
|
||||
option(value = ''): em -- When to send reports --
|
||||
option(value = 'never') Never
|
||||
option(value = 'alway') Always
|
||||
option(value = 'fail') Failure
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-clock-o
|
||||
| Schedule
|
||||
.panel-body.form-horizontal
|
||||
.text-center(ng-if = '!ctrl.formData'): i.xo-icon-loading
|
||||
xo-scheduler(data = 'ctrl.formData', api = 'ctrl.scheduleApi')
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-body
|
||||
fieldset.center(ng-disabled = '!ctrl.ready')
|
||||
button.btn.btn-lg.btn-primary(form = 'drform', type = 'submit')
|
||||
i.fa.fa-clock-o
|
||||
|
|
||||
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 Pool
|
||||
th.hidden-xs Depth
|
||||
th.hidden-xs Scheduling
|
||||
th Enabled now
|
||||
th
|
||||
tr(ng-repeat = 'schedule in ctrl.schedules | map | orderBy:"id":true track by schedule.id')
|
||||
td {{ schedule.id }}
|
||||
td
|
||||
span.label.label-default DR_
|
||||
| {{ ctrl.jobs[schedule.job].paramsVector.items[0].values[0].tag }}
|
||||
td.hidden-xs.hidden-sm
|
||||
div(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values.length == 1')
|
||||
| {{ (ctrl.jobs[schedule.job].paramsVector.items[0].values[0].id | resolve).name_label }}
|
||||
div(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values.length > 1')
|
||||
button.btn.btn-info(type = 'button', ng-click = 'unCollapsed = !unCollapsed')
|
||||
| {{ ctrl.jobs[schedule.job].paramsVector.items[0].values.length }} VMs
|
||||
i.fa(ng-class = '{"fa-chevron-down": !unCollapsed, "fa-chevron-up": unCollapsed}')
|
||||
div(collapse = '!unCollapsed')
|
||||
ul.list-group
|
||||
li.list-group-item(ng-repeat = 'item in ctrl.jobs[schedule.job].paramsVector.items[0].values')
|
||||
span(ng-if = 'item.id | resolve') {{ (item.id | resolve).name_label }}
|
||||
span(ng-if = '(item.id | resolve).$container') ({{ ((item.id | resolve).$container | resolve).name_label }})
|
||||
td.hidden-xs {{ (ctrl.jobs[schedule.job].paramsVector.items[0].values[0].pool | resolve).name_label }}
|
||||
td.hidden-xs {{ ctrl.jobs[schedule.job].paramsVector.items[0].values[0].depth }}
|
||||
td.hidden-xs {{ ctrl.prettyCron(schedule.cron) }}
|
||||
td.text-center
|
||||
i.fa.fa-check(ng-if = 'schedule.enabled')
|
||||
td.text-right
|
||||
button.btn.btn-primary(type = 'button', ng-click = 'ctrl.edit(schedule)'): i.fa.fa-pencil
|
||||
|
|
||||
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
|
||||
51
app/modules/backup/index.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import angular from 'angular'
|
||||
import later from 'later'
|
||||
import scheduler from 'scheduler'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
|
||||
later.date.localTime()
|
||||
|
||||
import backup from './backup'
|
||||
import continuousReplication from './continuous-replication'
|
||||
import deltaBackup from './delta-backup'
|
||||
import disasterRecovery from './disaster-recovery'
|
||||
import management from './management'
|
||||
import mount from './remote'
|
||||
import restore from './restore'
|
||||
import rollingSnapshot from './rolling-snapshot'
|
||||
|
||||
import view from './view'
|
||||
|
||||
export default angular.module('backup', [
|
||||
uiRouter,
|
||||
|
||||
backup,
|
||||
continuousReplication,
|
||||
deltaBackup,
|
||||
disasterRecovery,
|
||||
management,
|
||||
mount,
|
||||
restore,
|
||||
rollingSnapshot,
|
||||
scheduler
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('backup', {
|
||||
abstract: true,
|
||||
data: {
|
||||
requireAdmin: true
|
||||
},
|
||||
template: view,
|
||||
url: '/backup'
|
||||
})
|
||||
|
||||
// Redirect to default sub-state.
|
||||
$stateProvider.state('backup.index', {
|
||||
url: '',
|
||||
controller: function ($state) {
|
||||
$state.go('backup.management')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
.name
|
||||
231
app/modules/backup/management/index.js
Normal file
@@ -0,0 +1,231 @@
|
||||
import angular from 'angular'
|
||||
import filter from 'lodash.filter'
|
||||
import find from 'lodash.find'
|
||||
import forEach from 'lodash.foreach'
|
||||
import map from 'lodash.map'
|
||||
import prettyCron from 'prettycron'
|
||||
import uiBootstrap from 'angular-ui-bootstrap'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
import parse from 'xo-remote-parser'
|
||||
|
||||
import view from './view'
|
||||
|
||||
// ====================================================================
|
||||
|
||||
export default angular.module('backup.management', [
|
||||
uiRouter,
|
||||
uiBootstrap
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('backup.management', {
|
||||
url: '/management',
|
||||
controller: 'ManagementCtrl as ctrl',
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('ManagementCtrl', function (
|
||||
$interval,
|
||||
$scope,
|
||||
$state,
|
||||
$stateParams,
|
||||
filterFilter,
|
||||
modal,
|
||||
notify,
|
||||
selectHighLevelFilter,
|
||||
xo,
|
||||
xoApi
|
||||
) {
|
||||
this.running = {}
|
||||
const mapJobKeyToState = {
|
||||
continuousReplication: 'continuousReplication',
|
||||
deltaBackup: 'deltaBackup',
|
||||
disasterRecovery: 'disasterrecovery',
|
||||
rollingBackup: 'backup',
|
||||
rollingSnapshot: 'rollingsnapshot',
|
||||
__none: 'index'
|
||||
}
|
||||
|
||||
const mapJobKeyToJobDisplay = {
|
||||
continuousReplication: 'Continuous Replication',
|
||||
deltaBackup: 'Delta Backup',
|
||||
disasterRecovery: 'Disaster Recovery',
|
||||
rollingBackup: 'Backup',
|
||||
rollingSnapshot: 'Rolling Snapshot',
|
||||
__none: '[unknown]'
|
||||
}
|
||||
|
||||
this.currentLogPage = 1
|
||||
this.logPageSize = 10
|
||||
|
||||
const refreshSchedules = () => {
|
||||
xo.schedule.getAll()
|
||||
.then(schedules => {
|
||||
schedules = filter(schedules, schedule => this.jobs[schedule.job] && this.jobs[schedule.job].key in mapJobKeyToState)
|
||||
this.schedules = this.schedules ? map(schedules, schedule => {
|
||||
schedule.error = find(this.schedules, oldSchedule => schedule.id === oldSchedule.id).error
|
||||
return schedule
|
||||
}) : schedules
|
||||
})
|
||||
xo.scheduler.getScheduleTable()
|
||||
.then(table => this.scheduleTable = table)
|
||||
xo.remote.getAll()
|
||||
.then(remotes => {
|
||||
this.backUpRemotes = map(remotes, parse)
|
||||
forEach(this.schedules, schedule => {
|
||||
const jobRemote = this.jobs[schedule.job].paramsVector.items[0].values[0]
|
||||
const key = this.jobs[schedule.job].key
|
||||
// TODO: Why is the property either 'remote' or 'remoteId'?
|
||||
const remoteId = jobRemote.remoteId || jobRemote.remote
|
||||
const remote = find(remotes, remote => remote.id === remoteId)
|
||||
schedule.error = (!remote || !remote.enabled) && key !== 'continuousReplication' && key !== 'disasterRecovery' && key !== 'rollingSnapshot'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const getLogs = () => {
|
||||
xo.logs.get('jobs').then(logs => {
|
||||
const viewLogs = {}
|
||||
const logsToClear = []
|
||||
forEach(logs, (log, logKey) => {
|
||||
const data = log.data
|
||||
const [time] = logKey.split(':')
|
||||
if (data.event === 'job.start' && data.key in mapJobKeyToState) {
|
||||
logsToClear.push(logKey)
|
||||
viewLogs[logKey] = {
|
||||
logKey,
|
||||
jobId: data.jobId,
|
||||
key: data.key,
|
||||
userId: data.userId,
|
||||
start: time,
|
||||
calls: {},
|
||||
time
|
||||
}
|
||||
} else {
|
||||
const runJobId = data.runJobId
|
||||
const entry = viewLogs[runJobId]
|
||||
if (!entry) {
|
||||
return
|
||||
}
|
||||
logsToClear.push(logKey)
|
||||
if (data.event === 'job.end') {
|
||||
if (data.error) {
|
||||
entry.error = data.error
|
||||
}
|
||||
entry.end = time
|
||||
entry.duration = time - entry.start
|
||||
entry.status = 'Finished'
|
||||
} else if (data.event === 'jobCall.start') {
|
||||
entry.calls[logKey] = {
|
||||
callKey: logKey,
|
||||
params: resolveParams(data.params),
|
||||
method: data.method,
|
||||
time
|
||||
}
|
||||
} else if (data.event === 'jobCall.end') {
|
||||
const call = entry.calls[data.runCallId]
|
||||
|
||||
if (data.error) {
|
||||
call.error = data.error
|
||||
entry.hasErrors = true
|
||||
} else {
|
||||
call.returnedValue = resolveReturn(data.returnedValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
forEach(viewLogs, log => {
|
||||
if (log.end === undefined) {
|
||||
log.status = 'In progress'
|
||||
}
|
||||
})
|
||||
|
||||
this.logs = viewLogs
|
||||
this.logsToClear = logsToClear
|
||||
})
|
||||
}
|
||||
|
||||
const resolveParams = params => {
|
||||
for (let key in params) {
|
||||
const xoObject = xoApi.get(params[key])
|
||||
if (xoObject) {
|
||||
const newKey = xoObject.type || key
|
||||
params[newKey] = xoObject.name_label || xoObject.name || params[key]
|
||||
newKey !== key && delete params[key]
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
const resolveReturn = returnValue => {
|
||||
const xoObject = xoApi.get(returnValue)
|
||||
let xoName = xoObject && (xoObject.name_label || xoObject.name)
|
||||
xoName && (xoName += xoObject.type && ` (${xoObject.type})` || '')
|
||||
returnValue = xoName || returnValue
|
||||
return returnValue
|
||||
}
|
||||
|
||||
this.prettyCron = prettyCron.toString.bind(prettyCron)
|
||||
|
||||
const refreshJobs = () => {
|
||||
return xo.job.getAll()
|
||||
.then(jobs => {
|
||||
const j = {}
|
||||
forEach(jobs, job => j[job.id] = job)
|
||||
this.jobs = j
|
||||
})
|
||||
}
|
||||
|
||||
const refresh = () => {
|
||||
refreshJobs().then(refreshSchedules)
|
||||
getLogs()
|
||||
}
|
||||
|
||||
refresh()
|
||||
const interval = $interval(() => {
|
||||
refresh()
|
||||
}, 5e3)
|
||||
$scope.$on('$destroy', () => {
|
||||
$interval.cancel(interval)
|
||||
})
|
||||
|
||||
this.clearLogs = () => {
|
||||
modal.confirm({
|
||||
title: 'Clear logs',
|
||||
message: 'Are you sure you want to delete all logs ?'
|
||||
})
|
||||
.then(() => xo.logs.delete('jobs', this.logsToClear))
|
||||
}
|
||||
|
||||
this.enable = id => {
|
||||
this.working[id] = true
|
||||
return xo.scheduler.enable(id)
|
||||
.finally(() => { this.working[id] = false })
|
||||
.then(refreshSchedules)
|
||||
}
|
||||
this.disable = id => {
|
||||
this.working[id] = true
|
||||
return xo.scheduler.disable(id)
|
||||
.finally(() => { this.working[id] = false })
|
||||
.then(refreshSchedules)
|
||||
}
|
||||
this.run = schedule => {
|
||||
this.running[schedule.id] = true
|
||||
notify.info({
|
||||
title: 'Run Job',
|
||||
message: 'One shot running started. See overview for logs.'
|
||||
})
|
||||
const id = schedule.job
|
||||
return xo.job.runSequence([id]).finally(() => delete this.running[schedule.id])
|
||||
}
|
||||
this.resolveJobKey = schedule => mapJobKeyToState[this.jobs[schedule.job] && this.jobs[schedule.job].key || '__none']
|
||||
this.displayJobKey = schedule => mapJobKeyToJobDisplay[this.jobs[schedule.job] && this.jobs[schedule.job].key || '__none']
|
||||
this.displayLogKey = log => mapJobKeyToJobDisplay[log.key]
|
||||
this.resolveScheduleJobTag = schedule => this.jobs[schedule.job] && this.jobs[schedule.job].paramsVector && this.jobs[schedule.job].paramsVector.items[0].values[0].tag || schedule.id
|
||||
|
||||
this.collectionLength = col => Object.keys(col).length
|
||||
this.working = {}
|
||||
})
|
||||
|
||||
// A module exports its name.
|
||||
.name
|
||||
90
app/modules/backup/management/view.jade
Normal file
@@ -0,0 +1,90 @@
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.fa.fa-eye(style="color: #e25440;")
|
||||
| Backup Overview
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-clock-o
|
||||
| Schedules
|
||||
.panel-body
|
||||
//- The 2 tables below are here for a "full-width" effect of the content vs the menu (cf sheduler/view.jade)
|
||||
table.table(ng-if = '!ctrl.schedules')
|
||||
tr
|
||||
td.text-center: i.xo-icon-loading
|
||||
table.table(ng-if = 'ctrl.schedules && !ctrl.collectionLength(ctrl.schedules)')
|
||||
tr
|
||||
td.text-center No scheduled jobs
|
||||
table.table.table-hover(ng-if = 'ctrl.schedules && ctrl.collectionLength(ctrl.schedules)')
|
||||
tr
|
||||
th Job
|
||||
th Tag
|
||||
th.hidden-xs Scheduling
|
||||
th State
|
||||
tr(ng-repeat = 'schedule in ctrl.schedules | orderBy:"id":true track by schedule.id')
|
||||
td {{ ctrl.displayJobKey(schedule) }}
|
||||
td: a(ui-sref = 'backup.{{ctrl.resolveJobKey(schedule)}}({id: schedule.id})') {{ ctrl.resolveScheduleJobTag(schedule) }}
|
||||
td.hidden-xs {{ ctrl.prettyCron(schedule.cron) }}
|
||||
td
|
||||
span.label.label-success.hidden-xs(ng-if = 'ctrl.scheduleTable[schedule.id] === true') enabled
|
||||
span.label.label-default.hidden-xs(ng-if = 'ctrl.scheduleTable[schedule.id] === false') disabled
|
||||
span.label.label-warning.hidden-xs(ng-if = 'ctrl.scheduleTable[schedule.id] === undefined') unknown
|
||||
fieldset.pull-right(ng-disabled = 'ctrl.working[schedule.id]')
|
||||
button.btn.btn-danger(ui-sref = 'backup.remote' type = 'button' ng-if = 'schedule.error'): i.fa.fa-exclamation-triangle
|
||||
|
|
||||
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 Job
|
||||
th Start
|
||||
th End
|
||||
th Duration
|
||||
th Status
|
||||
tbody(ng-repeat = 'log in ctrl.logs | map | filter:ctrl.logSearch | orderBy:"-time" | slice:(ctrl.logPageSize * (ctrl.currentLogPage - 1)):(ctrl.logPageSize * ctrl.currentLogPage) track by log.logKey')
|
||||
tr
|
||||
td
|
||||
button.btn.btn-sm(type = 'button', tooltip = 'See calls', ng-click = 'seeCalls = !seeCalls', ng-class = '{"btn-default": !log.hasErrors, "btn-danger": log.hasErrors}'): i.fa(ng-class = '{"fa-caret-down": !seeCalls, "fa-caret-up": seeCalls}')
|
||||
| {{ log.jobId }}
|
||||
td {{ ctrl.displayLogKey(log) }}
|
||||
td {{ log.start | date:'medium' }}
|
||||
td {{ log.end | date:'medium' }}
|
||||
td {{ log.duration | duration}}
|
||||
td
|
||||
span(ng-if = 'log.status === "Finished"')
|
||||
span.label(ng-class = '{"label-success": (!log.error && !log.hasErrors), "label-danger": (log.error || log.hasErrors)}') {{ log.status }}
|
||||
span.label(ng-if = 'log.status !== "Finished"', ng-class = '{"label-warning": log.status === "In progress", "label-default": !log.status}') {{ log.status || "unknown" }}
|
||||
p.text-danger(ng-if = 'log.error') {{ 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 }}
|
||||
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 = '>>')
|
||||
@@ -1,21 +1,20 @@
|
||||
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'
|
||||
|
||||
// ====================================================================
|
||||
|
||||
export default angular.module('scheduler.remote', [
|
||||
export default angular.module('backup.remote', [
|
||||
uiRouter,
|
||||
uiBootstrap
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('scheduler.remote', {
|
||||
$stateProvider.state('backup.remote', {
|
||||
url: '/remote',
|
||||
controller: 'RemoteCtrl as ctrl',
|
||||
template: view
|
||||
@@ -26,7 +25,7 @@ export default angular.module('scheduler.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('scheduler.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('scheduler.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
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.fa.fa-puzzle-piece(style="color: #e25440;")
|
||||
| File system remotes for backups
|
||||
a.btn.btn-default(ui-sref = 'scheduler.backup')
|
||||
i.fa.fa-level-up
|
||||
i.fa.fa-plug(style="color: #e25440;")
|
||||
| Remotes stores for backup
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-body
|
||||
@@ -16,6 +14,7 @@
|
||||
th.text-info Local
|
||||
th Name
|
||||
th Path
|
||||
th
|
||||
th State
|
||||
th Error
|
||||
th
|
||||
@@ -23,11 +22,12 @@
|
||||
td
|
||||
td {{ remote.name }}
|
||||
td {{ remote.path }}
|
||||
td
|
||||
td
|
||||
span(ng-if = 'remote.enabled')
|
||||
span.text-success
|
||||
| Accessible
|
||||
i.fa.fa-puzzle-piece
|
||||
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
|
||||
@@ -39,6 +39,7 @@
|
||||
th.text-info NFS
|
||||
th Name
|
||||
th Device
|
||||
th
|
||||
th State
|
||||
th Error
|
||||
th
|
||||
@@ -46,18 +47,48 @@
|
||||
td
|
||||
td {{ remote.name }}
|
||||
td {{ remote.host }}:{{ remote.share }}
|
||||
td
|
||||
td
|
||||
span(ng-if = 'remote.enabled')
|
||||
span.text-success
|
||||
| Mounted
|
||||
i.fa.fa-puzzle-piece
|
||||
i.fa.fa-check
|
||||
button.btn.btn-warning.pull-right(type = 'button', ng-click = 'ctrl.disable(remote.id)'): i.fa.fa-chain-broken
|
||||
span(ng-if = '!remote.enabled')
|
||||
span.text-muted Unmounted
|
||||
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
|
||||
@@ -66,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
|
||||
118
app/modules/backup/restore/index.js
Normal file
@@ -0,0 +1,118 @@
|
||||
import angular from 'angular'
|
||||
import filter from 'lodash.filter'
|
||||
import find from 'lodash.find'
|
||||
import forEach from 'lodash.foreach'
|
||||
import size from 'lodash.size'
|
||||
import uiBootstrap from 'angular-ui-bootstrap'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
|
||||
import view from './view'
|
||||
|
||||
// ====================================================================
|
||||
|
||||
export default angular.module('backup.restore', [
|
||||
uiRouter,
|
||||
uiBootstrap
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('backup.restore', {
|
||||
url: '/restore',
|
||||
controller: 'RestoreCtrl as ctrl',
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('RestoreCtrl', function ($scope, $interval, xo, xoApi, notify, bytesToSizeFilter) {
|
||||
this.loaded = {}
|
||||
|
||||
const srs = xoApi.getView('SRs').all
|
||||
|
||||
this.bytesToSize = bytesToSizeFilter
|
||||
this.isEmpty = backups => backups && !(Object.keys(backups.delta) || backups.other.length)
|
||||
this.size = size
|
||||
|
||||
const refresh = () => {
|
||||
return xo.remote.getAll()
|
||||
.then(remotes => {
|
||||
forEach(this.backUpRemotes, remote => {
|
||||
if (remote.backups) {
|
||||
const freshRemote = find(remotes, {id: remote.id})
|
||||
freshRemote && (freshRemote.backups = remote.backups)
|
||||
}
|
||||
})
|
||||
this.backUpRemotes = remotes
|
||||
this.writable_SRs = filter(srs, (sr) => sr.content_type !== 'iso')
|
||||
})
|
||||
}
|
||||
|
||||
refresh()
|
||||
|
||||
const interval = $interval(refresh, 5e3)
|
||||
$scope.$on('$destroy', () => {
|
||||
$interval.cancel(interval)
|
||||
})
|
||||
|
||||
const deltaBuilder = (backups, uuid, name, tag, value) => {
|
||||
let deltaBackup = backups[uuid]
|
||||
? backups[uuid]
|
||||
: backups[uuid] = {}
|
||||
|
||||
deltaBackup = deltaBackup[name]
|
||||
? deltaBackup[name]
|
||||
: deltaBackup[name] = {}
|
||||
|
||||
deltaBackup = deltaBackup[tag]
|
||||
? deltaBackup[tag]
|
||||
: deltaBackup[tag] = []
|
||||
|
||||
deltaBackup.push(value)
|
||||
}
|
||||
|
||||
this.list = id => {
|
||||
return xo.remote.list(id)
|
||||
.then(files => {
|
||||
const remote = find(this.backUpRemotes, {id})
|
||||
|
||||
if (remote) {
|
||||
const backups = remote.backups = {
|
||||
delta: {},
|
||||
other: []
|
||||
}
|
||||
|
||||
forEach(files, file => {
|
||||
const arr = /^vm_delta_(.*)_([^\/]+)\/([^_]+)_(.*)$/.exec(file)
|
||||
|
||||
if (arr) {
|
||||
const [ , tag, uuid, date, name ] = arr
|
||||
const value = {
|
||||
path: file,
|
||||
date
|
||||
}
|
||||
deltaBuilder(backups.delta, uuid, name, tag, value)
|
||||
} else {
|
||||
backups.other.push(file)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
this.loaded[remote.id] = true
|
||||
})
|
||||
}
|
||||
|
||||
const notification = {
|
||||
title: 'VM import started',
|
||||
message: 'Starting the VM import'
|
||||
}
|
||||
|
||||
this.importBackup = (id, path, sr) => {
|
||||
notify.info(notification)
|
||||
return xo.vm.importBackup(id, path, sr)
|
||||
}
|
||||
|
||||
this.importDeltaBackup = (id, path, sr) => {
|
||||
notify.info(notification)
|
||||
return xo.vm.importDeltaBackup(id, path, sr)
|
||||
}
|
||||
})
|
||||
|
||||
// A module exports its name.
|
||||
.name
|
||||
68
app/modules/backup/restore/view.jade
Normal file
@@ -0,0 +1,68 @@
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.fa.fa-upload(style="color: #e25440;")
|
||||
| Backup Restore
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-body
|
||||
.text-center(ng-if = '!ctrl.size(ctrl.backUpRemotes)') No remotes
|
||||
.panel.panel-default(ng-repeat = 'remote in ctrl.backUpRemotes | orderBy:["name"] track by remote.id')
|
||||
.panel-body(ng-if = '!remote.enabled || remote.error', ng-class = '{"bg-danger": remote.error, "bg-muted": !remote.error}')
|
||||
a(ui-sref = 'backup.remote') {{ remote.name }}
|
||||
span(ng-if = 'remote.error') (on error)
|
||||
span(ng-if = '!remote.error') (disabled)
|
||||
.panel-body(ng-if = 'remote.enabled')
|
||||
.row
|
||||
.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-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 = 'sr in ctrl.writable_SRs | orderBy:natural("name_label") track by sr.id')
|
||||
a(xo-click = "ctrl.importBackup(remote.id, backup, sr.id)")
|
||||
i.xo-icon-host.fa-fw
|
||||
| To {{sr.name_label}} ({{sr.size - sr.physical_usage | bytesToSize }})
|
||||
span {{ (sr.$container | resolve).name_label }}
|
||||
hr
|
||||
@@ -12,12 +12,12 @@ import view from './view'
|
||||
|
||||
// ====================================================================
|
||||
|
||||
export default angular.module('scheduler.rollingSnapshot', [
|
||||
export default angular.module('backup.rollingSnapshot', [
|
||||
uiRouter,
|
||||
uiBootstrap
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('scheduler.rollingsnapshot', {
|
||||
$stateProvider.state('backup.rollingsnapshot', {
|
||||
url: '/rollingsnapshot/:id',
|
||||
controller: 'RollingSnapshotCtrl as ctrl',
|
||||
template: view
|
||||
@@ -26,6 +26,9 @@ export default angular.module('scheduler.rollingSnapshot', [
|
||||
.controller('RollingSnapshotCtrl', function ($scope, $stateParams, $interval, xo, xoApi, notify, selectHighLevelFilter, filterFilter) {
|
||||
const JOBKEY = 'rollingSnapshot'
|
||||
|
||||
this.ready = false
|
||||
|
||||
this.running = {}
|
||||
this.comesForEditing = $stateParams.id
|
||||
this.scheduleApi = {}
|
||||
this.formData = {}
|
||||
@@ -54,6 +57,9 @@ export default angular.module('scheduler.rollingSnapshot', [
|
||||
return refreshJobs().then(refreshSchedules)
|
||||
}
|
||||
|
||||
this.getReady = () => refresh().then(() => this.ready = true)
|
||||
this.getReady()
|
||||
|
||||
const interval = $interval(() => {
|
||||
refresh()
|
||||
}, 5e3)
|
||||
@@ -95,6 +101,7 @@ export default angular.module('scheduler.rollingSnapshot', [
|
||||
})
|
||||
const tag = job.paramsVector.items[0].values[0].tag
|
||||
const depth = job.paramsVector.items[0].values[0].depth
|
||||
const _reportWhen = job.paramsVector.items[0].values[0]._reportWhen
|
||||
const cronPattern = schedule.cron
|
||||
|
||||
this.resetData()
|
||||
@@ -102,11 +109,12 @@ export default angular.module('scheduler.rollingSnapshot', [
|
||||
this.formData.selectedVms = selectedVms
|
||||
this.formData.tag = tag
|
||||
this.formData.depth = depth
|
||||
this.formData._reportWhen = _reportWhen
|
||||
this.formData.scheduleId = schedule.id
|
||||
this.scheduleApi.setCron(cronPattern)
|
||||
}
|
||||
|
||||
this.save = (id, vms, tag, depth, cron, enabled) => {
|
||||
this.save = (id, vms, tag, depth, cron, enabled, _reportWhen) => {
|
||||
if (!vms.length) {
|
||||
notify.warning({
|
||||
title: 'No Vms selected',
|
||||
@@ -114,7 +122,7 @@ export default angular.module('scheduler.rollingSnapshot', [
|
||||
})
|
||||
return
|
||||
}
|
||||
const _save = (id === undefined) ? saveNew(vms, tag, depth, cron, enabled) : save(id, vms, tag, depth, cron)
|
||||
const _save = (id === undefined) ? saveNew(vms, tag, depth, cron, enabled, _reportWhen) : save(id, vms, tag, depth, cron, _reportWhen)
|
||||
return _save
|
||||
.then(() => {
|
||||
notify.info({
|
||||
@@ -128,7 +136,7 @@ export default angular.module('scheduler.rollingSnapshot', [
|
||||
})
|
||||
}
|
||||
|
||||
const save = (id, vms, tag, depth, cron) => {
|
||||
const save = (id, vms, tag, depth, cron, _reportWhen) => {
|
||||
const schedule = this.schedules[id]
|
||||
const job = this.jobs[schedule.job]
|
||||
const values = []
|
||||
@@ -136,7 +144,8 @@ export default angular.module('scheduler.rollingSnapshot', [
|
||||
values.push({
|
||||
id: vm.id,
|
||||
tag,
|
||||
depth
|
||||
depth,
|
||||
_reportWhen
|
||||
})
|
||||
})
|
||||
job.paramsVector.items[0].values = values
|
||||
@@ -154,13 +163,14 @@ export default angular.module('scheduler.rollingSnapshot', [
|
||||
})
|
||||
}
|
||||
|
||||
const saveNew = (vms, tag, depth, cron, enabled) => {
|
||||
const saveNew = (vms, tag, depth, cron, enabled, _reportWhen) => {
|
||||
const values = []
|
||||
forEach(vms, vm => {
|
||||
values.push({
|
||||
id: vm.id,
|
||||
tag,
|
||||
depth
|
||||
depth,
|
||||
_reportWhen
|
||||
})
|
||||
})
|
||||
const job = {
|
||||
@@ -185,13 +195,26 @@ export default angular.module('scheduler.rollingSnapshot', [
|
||||
|
||||
this.delete = schedule => {
|
||||
let jobId = schedule.job
|
||||
xo.schedule.delete(schedule.id)
|
||||
return xo.schedule.delete(schedule.id)
|
||||
.then(() => xo.job.delete(jobId))
|
||||
.finally(() => {
|
||||
if (this.formData.scheduleId === schedule.id) {
|
||||
this.resetData()
|
||||
}
|
||||
refresh()
|
||||
})
|
||||
}
|
||||
|
||||
this.run = schedule => {
|
||||
this.running[schedule.id] = true
|
||||
notify.info({
|
||||
title: 'Run Job',
|
||||
message: 'One shot running started. See overview for logs.'
|
||||
})
|
||||
const id = schedule.job
|
||||
return xo.job.runSequence([id]).finally(() => delete this.running[schedule.id])
|
||||
}
|
||||
|
||||
this.resetData = () => {
|
||||
this.formData.allRunning = false
|
||||
this.formData.allHalted = false
|
||||
@@ -200,6 +223,7 @@ export default angular.module('scheduler.rollingSnapshot', [
|
||||
this.formData.tag = undefined
|
||||
this.formData.depth = undefined
|
||||
this.formData.enabled = false
|
||||
this.formData._reportWhen = undefined
|
||||
this.scheduleApi && this.scheduleApi.resetData && this.scheduleApi.resetData()
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
p.page-title
|
||||
i.xo-icon-snapshot(style="color: #e25440;")
|
||||
| Rolling snapshots
|
||||
form#snapform(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.selectedVms, ctrl.formData.tag, ctrl.formData.depth, ctrl.formData.cronPattern, ctrl.formData.enabled)')
|
||||
form#snapform(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.selectedVms, ctrl.formData.tag, ctrl.formData.depth, ctrl.formData.cronPattern, ctrl.formData.enabled, ctrl.formData._reportWhen)')
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
@@ -16,11 +16,11 @@ form#snapform(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.sel
|
||||
.form-group
|
||||
label.control-label.col-md-2(for = 'tag') Tag
|
||||
.col-md-10
|
||||
input#tag.form-control(ng-model = 'ctrl.formData.tag', placeholder = 'Rolling snapshot tag', required)
|
||||
input#tag.form-control(form = 'snapform', ng-model = 'ctrl.formData.tag', placeholder = 'Rolling snapshot tag', required)
|
||||
.form-group(ng-class = '{"has-warning": !ctrl.formData.selectedVms.length}')
|
||||
label.control-label.col-md-2(for = 'vmlist') VMs
|
||||
.col-md-8
|
||||
ui-select(ng-model = 'ctrl.formData.selectedVms', multiple, close-on-select = 'false', required)
|
||||
ui-select(form = 'snapform', ng-model = 'ctrl.formData.selectedVms', multiple, close-on-select = 'false', required)
|
||||
ui-select-match(placeholder = 'Choose VMs to snapshot')
|
||||
i.xo-icon-working(ng-if="isVMWorking($item)")
|
||||
i(class="xo-icon-{{$item.power_state | lowercase}}",ng-if="!isVMWorking($item)")
|
||||
@@ -36,23 +36,31 @@ form#snapform(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.sel
|
||||
| ({{(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(type = 'checkbox', ng-model = 'ctrl.formData.allRunning', ng-change = 'ctrl.toggleAllRunning(ctrl.formData.allRunning)')
|
||||
input.hidden(form = 'snapform', type = 'checkbox', ng-model = 'ctrl.formData.allRunning', ng-change = 'ctrl.toggleAllRunning(ctrl.formData.allRunning)')
|
||||
span.fa-stack
|
||||
i.xo-icon-running.fa-stack-1x
|
||||
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allRunning')
|
||||
label(tooltip = 'select/deselect all halted VMs', style = 'cursor: pointer')
|
||||
input.hidden(type = 'checkbox', ng-model = 'ctrl.formData.allHalted', ng-change = 'ctrl.toggleAllHalted(ctrl.formData.allHalted)')
|
||||
input.hidden(form = 'snapform', type = 'checkbox', ng-model = 'ctrl.formData.allHalted', ng-change = 'ctrl.toggleAllHalted(ctrl.formData.allHalted)')
|
||||
span.fa-stack
|
||||
i.xo-icon-halted.fa-stack-1x
|
||||
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allHalted')
|
||||
.form-group
|
||||
label.control-label.col-md-2(for = 'depth') Depth
|
||||
.col-md-10
|
||||
input#depth.form-control(ng-model = 'ctrl.formData.depth', placeholder = 'How many snapshots to rollover', type = 'number', min = '1', required)
|
||||
input#depth.form-control(form = 'snapform', ng-model = 'ctrl.formData.depth', placeholder = 'How many snapshots to rollover', type = 'number', min = '1', required)
|
||||
.form-group(ng-if = '!ctrl.formData.scheduleId')
|
||||
label.control-label.col-md-2(for = 'enabled')
|
||||
input#enabled(ng-model = 'ctrl.formData.enabled', type = 'checkbox')
|
||||
.help-block.col-md-8 Enable immediatly after creation
|
||||
input#enabled(form = 'snapform', ng-model = 'ctrl.formData.enabled', type = 'checkbox')
|
||||
.help-block.col-md-8 Enable immediately after creation
|
||||
.form-group
|
||||
label.control-label.col-md-2(for = '_reportWhen') Report
|
||||
.col-md-10
|
||||
select.form-control(ng-model = 'ctrl.formData._reportWhen')
|
||||
option(value = ''): em -- When to send reports --
|
||||
option(value = 'never') Never
|
||||
option(value = 'alway') Always
|
||||
option(value = 'fail') Failure
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-clock-o
|
||||
@@ -63,7 +71,7 @@ form#snapform(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.sel
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-body
|
||||
p.center
|
||||
fieldset.center(ng-disabled = '!ctrl.ready')
|
||||
button.btn.btn-lg.btn-primary(form = 'snapform', type = 'submit')
|
||||
i.fa.fa-clock-o
|
||||
|
|
||||
@@ -86,15 +94,15 @@ form#snapform(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.sel
|
||||
tr
|
||||
th ID
|
||||
th Tag
|
||||
th VMs to snapshot
|
||||
th Depth
|
||||
th Scheduling
|
||||
th.hidden-xs.hidden-sm VMs to snapshot
|
||||
th.hidden-xs Depth
|
||||
th.hidden-xs Scheduling
|
||||
th Enabled now
|
||||
th
|
||||
tr(ng-repeat = 'schedule in ctrl.schedules | orderBy:"id":true track by schedule.id')
|
||||
tr(ng-repeat = 'schedule in ctrl.schedules | map | orderBy:"id":true track by schedule.id')
|
||||
td {{ schedule.id }}
|
||||
td {{ ctrl.jobs[schedule.job].paramsVector.items[0].values[0].tag }}
|
||||
td
|
||||
td.hidden-xs.hidden-sm
|
||||
div(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values.length == 1')
|
||||
| {{ (ctrl.jobs[schedule.job].paramsVector.items[0].values[0].id | resolve).name_label }}
|
||||
div(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values.length > 1')
|
||||
@@ -103,15 +111,17 @@ form#snapform(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.sel
|
||||
i.fa(ng-class = '{"fa-chevron-down": !unCollapsed, "fa-chevron-up": unCollapsed}')
|
||||
div(collapse = '!unCollapsed')
|
||||
br
|
||||
ul
|
||||
li(ng-repeat = 'item in ctrl.jobs[schedule.job].paramsVector.items[0].values')
|
||||
ul.list-group
|
||||
li.list-group-item(ng-repeat = 'item in ctrl.jobs[schedule.job].paramsVector.items[0].values')
|
||||
span(ng-if = 'item.id | resolve') {{ (item.id | resolve).name_label }}
|
||||
span(ng-if = '(item.id | resolve).$container') ({{ ((item.id | resolve).$container | resolve).name_label }})
|
||||
td {{ ctrl.jobs[schedule.job].paramsVector.items[0].values[0].depth }}
|
||||
td {{ ctrl.prettyCron(schedule.cron) }}
|
||||
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
|
||||
37
app/modules/backup/view.jade
Normal file
@@ -0,0 +1,37 @@
|
||||
.menu-grid
|
||||
.side-menu
|
||||
ul.nav
|
||||
li
|
||||
a(ui-sref = '.management', ui-sref-active = 'active')
|
||||
i.fa.fa-fw.fa-eye.fa-menu
|
||||
span.menu-entry Overview
|
||||
li
|
||||
a(ui-sref = '.rollingsnapshot')
|
||||
i.xo-icon-snapshot.fa-fw.fa-menu
|
||||
span.menu-entry Rolling snapshots
|
||||
li
|
||||
a(ui-sref = '.remote')
|
||||
i.fa.fa-fw.fa-plug.fa-menu
|
||||
span.menu-entry Remote stores
|
||||
li
|
||||
a(ui-sref = '.backup')
|
||||
i.fa.fa-fw.fa-download.fa-menu
|
||||
span.menu-entry Backup
|
||||
li
|
||||
a(ui-sref = '.deltaBackup')
|
||||
i.fa.fa-fw.fa-code-fork.fa-menu
|
||||
span.menu-entry Delta Backup
|
||||
li
|
||||
a(ui-sref = '.restore')
|
||||
i.fa.fa-fw.fa-upload.fa-menu
|
||||
span.menu-entry Restore
|
||||
li
|
||||
a(ui-sref = '.disasterrecovery')
|
||||
i.fa.fa-fw.fa-medkit.fa-menu
|
||||
span.menu-entry Disaster Recovery
|
||||
li
|
||||
a(ui-sref = '.continuousReplication')
|
||||
i.fa.fa-fw.fa-map-signs.fa-menu
|
||||
span.menu-entry Continuous Replication
|
||||
|
||||
.side-content(ui-view = '')
|
||||
@@ -1,14 +1,15 @@
|
||||
angular = require 'angular'
|
||||
forEach = require('lodash.foreach')
|
||||
includes = require('lodash.includes')
|
||||
Clipboard = require('clipboard')
|
||||
|
||||
isoDevice = require('../iso-device')
|
||||
isoDevice = require('iso-device').default
|
||||
|
||||
#=====================================================================
|
||||
|
||||
module.exports = angular.module 'xoWebApp.console', [
|
||||
require 'angular-ui-router'
|
||||
require 'angular-no-vnc'
|
||||
require('angular-no-vnc').default
|
||||
|
||||
isoDevice
|
||||
]
|
||||
@@ -17,7 +18,7 @@ module.exports = angular.module 'xoWebApp.console', [
|
||||
url: '/consoles/:id'
|
||||
controller: 'ConsoleCtrl'
|
||||
template: require './view'
|
||||
.controller 'ConsoleCtrl', ($scope, $stateParams, xoApi, xo, xoHideUnauthorizedFilter) ->
|
||||
.controller 'ConsoleCtrl', ($scope, $stateParams, xoApi, xo, xoHideUnauthorizedFilter, modal) ->
|
||||
{id} = $stateParams
|
||||
{get} = xoApi
|
||||
|
||||
@@ -71,13 +72,48 @@ module.exports = angular.module 'xoWebApp.console', [
|
||||
)
|
||||
|
||||
$scope.startVM = xo.vm.start
|
||||
$scope.stopVM = xo.vm.stop
|
||||
$scope.rebootVM = xo.vm.restart
|
||||
$scope.stopVM = (id) ->
|
||||
modal.confirm
|
||||
title: 'VM shutdown'
|
||||
message: 'Are you sure you want to shutdown this VM ?'
|
||||
.then ->
|
||||
xo.vm.stop id
|
||||
$scope.rebootVM = (id) ->
|
||||
modal.confirm
|
||||
title: 'VM reboot'
|
||||
message: 'Are you sure you want to reboot this VM ?'
|
||||
.then ->
|
||||
xo.vm.restart id
|
||||
|
||||
$scope.eject = ->
|
||||
xo.vm.ejectCd id
|
||||
$scope.insert = (disc_id) ->
|
||||
xo.vm.insertCd id, disc_id, true
|
||||
|
||||
$scope.vmClipboard = ''
|
||||
$scope.setClipboard = (text) ->
|
||||
$scope.vmClipboard = text
|
||||
$scope.$applyAsync()
|
||||
|
||||
$scope.shutdownHost = (id) ->
|
||||
modal.confirm({
|
||||
title: 'Shutdown host'
|
||||
message: 'Are you sure you want to shutdown this host?'
|
||||
}).then ->
|
||||
xo.host.stop id
|
||||
|
||||
$scope.rebootHost = (id) ->
|
||||
modal.confirm({
|
||||
title: 'Reboot host'
|
||||
message: 'Are you sure you want to reboot this host? It will be disabled then rebooted'
|
||||
}).then ->
|
||||
xo.host.restart id
|
||||
|
||||
$scope.startHost = (id) ->
|
||||
xo.host.start id
|
||||
|
||||
clipboard = new Clipboard('.copy')
|
||||
clipboard.on('error', (e) -> console.log('Clipboard', e))
|
||||
|
||||
# A module exports its name.
|
||||
.name
|
||||
|
||||
@@ -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)'
|
||||
)
|
||||
|
||||
@@ -3,314 +3,361 @@
|
||||
import angular from 'angular'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
import uiSelect from 'angular-ui-select'
|
||||
import debounce from 'lodash.debounce'
|
||||
import filter from 'lodash.filter'
|
||||
import foreach from 'lodash.foreach'
|
||||
|
||||
import xoApi from 'xo-api'
|
||||
import xoServices from 'xo-services'
|
||||
|
||||
import 'xo-sunburst-d3'
|
||||
// import 'xo-treemap-d3'
|
||||
import'xo-week-heatmap'
|
||||
import xoCircleD3 from 'xo-circle-d3'
|
||||
import xoParallelD3 from 'xo-parallel-d3'
|
||||
import xoSunburstD3 from 'xo-sunburst-d3'
|
||||
|
||||
import view from './view'
|
||||
|
||||
export default angular.module('dashboard.dataviz', [
|
||||
uiRouter,
|
||||
uiSelect,
|
||||
|
||||
xoApi,
|
||||
xoServices
|
||||
xoCircleD3,
|
||||
xoParallelD3,
|
||||
xoSunburstD3
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('dashboard.dataviz', {
|
||||
controller: 'Dataviz as ctrl',
|
||||
data: {
|
||||
requireAdmin: true
|
||||
},
|
||||
url: '/dataviz',
|
||||
template: view
|
||||
})
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('dashboard.dataviz', {
|
||||
controller: 'Dataviz as ctrl',
|
||||
data: {
|
||||
requireAdmin: true
|
||||
},
|
||||
url: '/dataviz/:chart',
|
||||
template: view
|
||||
})
|
||||
.filter('underStat', () => {
|
||||
let isUnderStat = object => object.type === 'host' || object.type === 'VM'
|
||||
return objects => filter(objects, isUnderStat)
|
||||
})
|
||||
.controller('Dataviz', function () {
|
||||
})
|
||||
.controller('DatavizStorageHierarchical', function DatavizStorageHierarchical (xoApi, $scope, $timeout, $interval, $state, bytesToSizeFilter) {
|
||||
$scope.charts = {
|
||||
selected: {},
|
||||
data: {
|
||||
name: 'storage',
|
||||
children: []
|
||||
},
|
||||
click: function (d) {
|
||||
if (d.non_clickable) {
|
||||
return
|
||||
}
|
||||
switch (d.type) {
|
||||
case 'pool':
|
||||
$state.go('pools_view', {id: d.id})
|
||||
break
|
||||
case 'host':
|
||||
$state.go('hosts_view', {id: d.id})
|
||||
break
|
||||
case 'srs':
|
||||
$state.go('SRs_view', {id: d.id})
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
.filter('type', () => {
|
||||
return function (objects, type) {
|
||||
if (!type) {
|
||||
return objects
|
||||
}
|
||||
return filter(objects, object => object.type === type)
|
||||
}
|
||||
})
|
||||
.controller('Dataviz', function ($scope, $state) {
|
||||
$scope.selectedChart = ''
|
||||
$scope.availablecharts = {
|
||||
sunburst: {
|
||||
name: 'Sunburst charts',
|
||||
imgs: ['images/sunburst.png', 'images/sunburst2.png'],
|
||||
url: '/dataviz/sunburst'
|
||||
},
|
||||
circle: {
|
||||
name: 'Circles charts',
|
||||
imgs: ['images/circle1.png', 'images/circle2.png'],
|
||||
url: '/dataviz/circle'
|
||||
},
|
||||
parcoords: {
|
||||
name: 'VM properties',
|
||||
imgs: ['images/parcoords.png'],
|
||||
url: '/dataviz/parcoords'
|
||||
}
|
||||
}
|
||||
$scope.$on('$stateChangeSuccess', function updatePage () {
|
||||
$scope.selectedChart = $state.params.chart
|
||||
})
|
||||
})
|
||||
.controller('DatavizParcoords', function DatavizParcoords (xoApi, $scope, $timeout, $interval, $state, bytesToSizeFilter) {
|
||||
let hostsByPool, vmsByContainer, data
|
||||
data = []
|
||||
hostsByPool = xoApi.getIndex('hostsByPool')
|
||||
vmsByContainer = xoApi.getIndex('vmsByContainer')
|
||||
/* parallel charts */
|
||||
|
||||
function populateChartsData () {
|
||||
function populatestorage (root, container_id) {
|
||||
let srs = filter(xoApi.getIndex('srsByContainer')[container_id], (one_srs)=>one_srs.SR_type !== 'iso' && one_srs.SR_type !== 'udev')
|
||||
function populateChartsData () {
|
||||
foreach(xoApi.getView('pools').all, function (pool, pool_id) {
|
||||
foreach(hostsByPool[pool_id], function (host, host_id) {
|
||||
console.log(host_id)
|
||||
foreach(vmsByContainer[host_id], function (vm, vm_id) {
|
||||
let nbvdi, vdisize
|
||||
|
||||
foreach(srs, function (one_srs) {
|
||||
let srs_used_size = 0
|
||||
const srs_storage = {
|
||||
name: one_srs.name_label,
|
||||
id: one_srs.id,
|
||||
children: [],
|
||||
size: one_srs.size,
|
||||
textSize: bytesToSizeFilter(one_srs.size),
|
||||
type: 'srs'
|
||||
}
|
||||
nbvdi = 0
|
||||
vdisize = 0
|
||||
foreach(vm.$VBDs, function (vbd_id) {
|
||||
let vbd
|
||||
vbd = xoApi.get(vbd_id)
|
||||
|
||||
root.size += one_srs.size
|
||||
foreach(one_srs.VDIs, function (vdi_id) {
|
||||
let vdi = xoApi.get(vdi_id)
|
||||
if (vdi && vdi.name_label.indexOf('.iso') === -1) {
|
||||
let vdi_storage = {
|
||||
name: vdi.name_label,
|
||||
id: vdi_id,
|
||||
size: vdi.size,
|
||||
textSize: bytesToSizeFilter(vdi.size),
|
||||
type: 'vdi',
|
||||
non_clickable: true
|
||||
}
|
||||
srs_used_size += vdi.size
|
||||
srs_storage.children.push(vdi_storage)
|
||||
if (!vbd.is_cd_drive && vbd.attached) {
|
||||
nbvdi++
|
||||
vdisize += xoApi.get(vbd.VDI).size
|
||||
}
|
||||
})
|
||||
if (one_srs.size > srs_used_size) {// some unallocated space
|
||||
srs_storage.children.push({
|
||||
color: 'white',
|
||||
name: 'Free',
|
||||
id: 'free' + one_srs.id,
|
||||
size: one_srs.size - srs_used_size,
|
||||
textSize: bytesToSizeFilter(one_srs.size - srs_used_size),
|
||||
type: 'vdi',
|
||||
non_clickable: true
|
||||
})
|
||||
}
|
||||
root.children.push(srs_storage)
|
||||
data.push({
|
||||
name: vm.name_label,
|
||||
id: vm_id,
|
||||
vcpus: vm.CPUs.number,
|
||||
vifs: vm.VIFs.length,
|
||||
ram: vm.memory.size / (1024 * 1024 * 1024)/* memory size in GB */,
|
||||
nbvdi: nbvdi,
|
||||
vdisize: vdisize / (1024 * 1024 * 1024)/* disk size in Gb */
|
||||
})
|
||||
})
|
||||
root.textSize = bytesToSizeFilter(root.size)
|
||||
})
|
||||
})
|
||||
|
||||
$scope.charts = {
|
||||
data: data,
|
||||
labels: {
|
||||
vcpus: 'vCPUs number',
|
||||
ram: 'RAM quantity',
|
||||
vifs: 'VIF number',
|
||||
nbvdi: 'VDI number',
|
||||
vdisize: 'Total space'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const debouncedPopulate = debounce(populateChartsData, 300, {leading: true, trailing: true})
|
||||
debouncedPopulate()
|
||||
$scope.$on('$destroy', xoApi.onUpdate(debouncedPopulate))
|
||||
})
|
||||
.controller('DatavizStorageHierarchical', function DatavizStorageHierarchical (xoApi, $scope, $timeout, $interval, $state, bytesToSizeFilter) {
|
||||
$scope.charts = {
|
||||
selected: {},
|
||||
data: {
|
||||
name: 'storage',
|
||||
children: []
|
||||
},
|
||||
click: function (d) {
|
||||
if (d.virtual) {
|
||||
return
|
||||
}
|
||||
switch (d.type) {
|
||||
case 'pool':
|
||||
$state.go('pools_view', {
|
||||
id: d.id
|
||||
})
|
||||
break
|
||||
case 'host':
|
||||
$state.go('hosts_view', {
|
||||
id: d.id
|
||||
})
|
||||
break
|
||||
case 'srs':
|
||||
$state.go('SRs_view', {
|
||||
id: d.id
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function populateChartsData () {
|
||||
function populatestorage (root, container_id) {
|
||||
let srs = filter(xoApi.getIndex('srsByContainer')[container_id], (one_srs) => one_srs.SR_type !== 'iso' && one_srs.SR_type !== 'udev')
|
||||
|
||||
foreach(srs, function (one_srs) {
|
||||
let srs_used_size = 0
|
||||
const srs_storage = {
|
||||
name: one_srs.name_label,
|
||||
id: one_srs.id,
|
||||
children: [],
|
||||
size: one_srs.size,
|
||||
textSize: bytesToSizeFilter(one_srs.size),
|
||||
type: 'srs'
|
||||
}
|
||||
|
||||
root.size += one_srs.size
|
||||
foreach(one_srs.VDIs, function (vdi_id) {
|
||||
let vdi = xoApi.get(vdi_id)
|
||||
if (vdi && vdi.name_label.indexOf('.iso') === -1) {
|
||||
let vdi_storage = {
|
||||
name: vdi.name_label,
|
||||
id: vdi_id,
|
||||
size: vdi.size,
|
||||
textSize: bytesToSizeFilter(vdi.size),
|
||||
type: 'vdi'
|
||||
}
|
||||
srs_used_size += vdi.size
|
||||
srs_storage.children.push(vdi_storage)
|
||||
}
|
||||
})
|
||||
if (one_srs.size > srs_used_size) {// some unallocated space
|
||||
srs_storage.children.push({
|
||||
color: 'white',
|
||||
name: 'Free',
|
||||
id: 'free' + one_srs.id,
|
||||
size: one_srs.size - srs_used_size,
|
||||
textSize: bytesToSizeFilter(one_srs.size - srs_used_size),
|
||||
type: 'vdi',
|
||||
virtual: true
|
||||
})
|
||||
}
|
||||
root.children.push(srs_storage)
|
||||
})
|
||||
root.textSize = bytesToSizeFilter(root.size)
|
||||
}
|
||||
|
||||
let storage_children,
|
||||
pools,
|
||||
hostsByPool,
|
||||
pool_shared_storage
|
||||
|
||||
storage_children = []
|
||||
pools = xoApi.getView('pools')
|
||||
hostsByPool = xoApi.getIndex('hostsByPool')
|
||||
|
||||
foreach(pools.all, function (pool, pool_id) {
|
||||
let pool_storage, hosts
|
||||
pool_storage = {
|
||||
name: pool.name_label || 'no pool',
|
||||
id: pool_id,
|
||||
children: [],
|
||||
size: 0,
|
||||
color: pool.name_label ? null : 'white',
|
||||
type: 'pool',
|
||||
virtual: !pool.name_label
|
||||
}
|
||||
pool_shared_storage = {
|
||||
name: 'Shared',
|
||||
id: 'Shared' + pool_id,
|
||||
children: [],
|
||||
size: 0,
|
||||
type: 'host',
|
||||
virtual: true
|
||||
}
|
||||
|
||||
let storage_children,
|
||||
pools,
|
||||
hostsByPool,
|
||||
srsByContainer,
|
||||
pool_shared_storage
|
||||
populatestorage(pool_shared_storage, pool_id)
|
||||
pool_storage.children.push(pool_shared_storage)
|
||||
pool_storage.size += pool_shared_storage.size
|
||||
|
||||
storage_children = []
|
||||
pools = xoApi.getView('pools')
|
||||
hostsByPool = xoApi.getIndex('hostsByPool')
|
||||
srsByContainer = xoApi.getIndex('srsByContainer')
|
||||
|
||||
foreach(pools.all, function (pool, pool_id) {
|
||||
let pool_storage, pool_ram, hosts
|
||||
pool_storage = {
|
||||
name: pool.name_label || 'no pool',
|
||||
id: pool_id,
|
||||
// by hosts
|
||||
hosts = hostsByPool[pool_id]
|
||||
foreach(hosts, function (host, host_id) {
|
||||
// there's also SR attached top
|
||||
let host_storage = {
|
||||
name: host.name_label,
|
||||
id: host.id,
|
||||
children: [],
|
||||
size: 0,
|
||||
color: !!pool.name_label ? null : 'white',
|
||||
type: 'pool',
|
||||
non_clickable: !pool.name_label
|
||||
type: 'host'
|
||||
}
|
||||
pool_shared_storage = {
|
||||
name: 'Shared',
|
||||
id: 'Shared' + pool_id,
|
||||
children: [],
|
||||
size: 0,
|
||||
type: 'host',
|
||||
non_clickable: true
|
||||
}
|
||||
|
||||
populatestorage(pool_shared_storage, pool_id)
|
||||
pool_storage.children.push(pool_shared_storage)
|
||||
pool_storage.size += pool_shared_storage.size
|
||||
|
||||
// by hosts
|
||||
|
||||
pool_ram = {
|
||||
name: pool.name_label || 'no pool',
|
||||
id: pool_id,
|
||||
children: [],
|
||||
size:0,
|
||||
color: !!pool.name_label ? null : 'white',
|
||||
type:'pool',
|
||||
non_clickable:!pool.name_label
|
||||
}
|
||||
hosts = hostsByPool[pool_id]
|
||||
foreach(hosts, function (host, host_id) {
|
||||
// there's also SR attached top
|
||||
let host_storage={
|
||||
name: host.name_label,
|
||||
id: host.id,
|
||||
children: [],
|
||||
size:0,
|
||||
type:'host'
|
||||
}
|
||||
populatestorage(host_storage,host_id);
|
||||
pool_storage.size += host_storage.size
|
||||
pool_storage.children.push(host_storage)
|
||||
|
||||
|
||||
})
|
||||
|
||||
pool_storage.textSize = bytesToSizeFilter(pool_storage.size)
|
||||
storage_children.push(pool_storage)
|
||||
populatestorage(host_storage, host_id)
|
||||
pool_storage.size += host_storage.size
|
||||
pool_storage.children.push(host_storage)
|
||||
})
|
||||
|
||||
$scope.charts.data.children = storage_children
|
||||
}
|
||||
pool_storage.textSize = bytesToSizeFilter(pool_storage.size)
|
||||
storage_children.push(pool_storage)
|
||||
})
|
||||
|
||||
$scope.charts.data.children = storage_children
|
||||
}
|
||||
|
||||
$scope.$watch(() => xoApi.all, function () {
|
||||
$timeout(function () { // all seemes to be unpopulated for now
|
||||
populateChartsData()
|
||||
}, 0)
|
||||
const debouncedPopulate = debounce(populateChartsData, 300, {leading: true, trailing: true})
|
||||
|
||||
debouncedPopulate()
|
||||
$scope.$on('$destroy', xoApi.onUpdate(debouncedPopulate))
|
||||
})
|
||||
.controller('DatavizRamHierarchical', function DatavizRamHierarchical (xoApi, $scope, $timeout, $state, bytesToSizeFilter) {
|
||||
$scope.charts = {
|
||||
selected: {},
|
||||
data: {
|
||||
name: 'ram',
|
||||
children: []
|
||||
},
|
||||
true)
|
||||
|
||||
})
|
||||
.controller('DatavizRamHierarchical', function DatavizRamHierarchical(xoApi, $scope, $timeout, $state, bytesToSizeFilter) {
|
||||
|
||||
$scope.charts = {
|
||||
selected: {},
|
||||
data: {
|
||||
name: 'ram',
|
||||
children: []
|
||||
},
|
||||
click: function (d) {
|
||||
if(d.non_clickable){
|
||||
return ;
|
||||
}
|
||||
switch(d.type){
|
||||
case 'pool':
|
||||
$state.go('pools_view',{id: d.id});
|
||||
break;
|
||||
case 'host':
|
||||
$state.go('hosts_view',{id: d.id});
|
||||
break;
|
||||
case 'vm':
|
||||
$state.go('VMs_view',{id: d.id});
|
||||
break;
|
||||
}
|
||||
click: function (d) {
|
||||
if (d.virtual) {
|
||||
return
|
||||
}
|
||||
switch (d.type) {
|
||||
case 'pool':
|
||||
$state.go('pools_view', {id: d.id})
|
||||
break
|
||||
case 'host':
|
||||
$state.go('hosts_view', {id: d.id})
|
||||
break
|
||||
case 'vm':
|
||||
$state.go('VMs_view', {id: d.id})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function populateChartsData() {
|
||||
function populateChartsData () {
|
||||
let ram_children,
|
||||
pools,
|
||||
vmsByContainer,
|
||||
hostsByPool
|
||||
|
||||
let ram_children,
|
||||
pools,
|
||||
vmsByContainer,
|
||||
hostsByPool
|
||||
ram_children = []
|
||||
pools = xoApi.getView('pools')
|
||||
vmsByContainer = xoApi.getIndex('vmsByContainer')
|
||||
hostsByPool = xoApi.getIndex('hostsByPool')
|
||||
|
||||
ram_children = []
|
||||
pools = xoApi.getView('pools')
|
||||
vmsByContainer = xoApi.getIndex('vmsByContainer')
|
||||
hostsByPool = xoApi.getIndex('hostsByPool')
|
||||
foreach(pools.all, function (pool, pool_id) {
|
||||
let pool_ram, hosts
|
||||
|
||||
foreach(pools.all, function (pool, pool_id) {
|
||||
let pool_ram, hosts
|
||||
//by hosts
|
||||
// by hosts
|
||||
|
||||
pool_ram = {
|
||||
name: pool.name_label || 'no pool',
|
||||
id: pool_id,
|
||||
pool_ram = {
|
||||
name: pool.name_label || 'no pool',
|
||||
id: pool_id,
|
||||
children: [],
|
||||
size: 0,
|
||||
color: pool.name_label ? null : 'white',
|
||||
type: 'pool',
|
||||
virtual: !pool.name_label
|
||||
}
|
||||
hosts = hostsByPool[pool_id]
|
||||
foreach(hosts, function (host, host_id) {
|
||||
// there's also SR attached top
|
||||
let vm_ram_size = 0
|
||||
let host_ram = {
|
||||
name: host.name_label,
|
||||
id: host_id,
|
||||
children: [],
|
||||
size:0,
|
||||
color: !!pool.name_label ? null : 'white',
|
||||
type:'pool',
|
||||
non_clickable:!pool.name_label
|
||||
size: host.memory.size,
|
||||
type: 'host'
|
||||
}
|
||||
hosts = hostsByPool[pool_id]
|
||||
foreach(hosts, function (host, host_id) {
|
||||
// there's also SR attached top
|
||||
let host_storage={
|
||||
name: host.name_label,
|
||||
id: host.id,
|
||||
children: [],
|
||||
size:0,
|
||||
type:'host'
|
||||
let VMs = vmsByContainer[host_id]
|
||||
foreach(VMs, function (VM, vm_id) {
|
||||
let vm_ram = {
|
||||
name: VM.name_label,
|
||||
id: vm_id,
|
||||
size: VM.memory.size,
|
||||
textSize: bytesToSizeFilter(VM.memory.size),
|
||||
type: 'vm'
|
||||
}
|
||||
let vm_ram_size=0
|
||||
let host_ram = {
|
||||
name: host.name_label,
|
||||
id: host_id,
|
||||
children: [],
|
||||
size:host.memory.size,
|
||||
type:'host'
|
||||
if (vm_ram.size) {
|
||||
vm_ram_size += vm_ram.size
|
||||
host_ram.children.push(vm_ram)
|
||||
}
|
||||
let VMs = vmsByContainer[host_id]
|
||||
foreach(VMs, function (VM, vm_id) {
|
||||
let vm_ram = {
|
||||
name: VM.name_label,
|
||||
id: vm_id,
|
||||
size: VM.memory.size,
|
||||
textSize : bytesToSizeFilter(VM.memory.size),
|
||||
type:'vm'
|
||||
}
|
||||
if (vm_ram.size) {
|
||||
vm_ram_size+=vm_ram.size
|
||||
host_ram.children.push(vm_ram)
|
||||
}
|
||||
})
|
||||
if(host_ram.size != vm_ram_size){
|
||||
host_ram.children.push({
|
||||
color:'white',
|
||||
name: 'Free',
|
||||
id: 'free'+host.id,
|
||||
size: host.memory.size-vm_ram_size ,
|
||||
textSize : bytesToSizeFilter( host.memory.size-vm_ram_size),
|
||||
type:'vm',
|
||||
non_clickable:true
|
||||
})
|
||||
}
|
||||
|
||||
host_ram.textSize = bytesToSizeFilter(host_ram.size)
|
||||
pool_ram.size+=host_ram.size
|
||||
pool_ram.children.push(host_ram)
|
||||
|
||||
})
|
||||
if (pool_ram.children.length) {
|
||||
pool_ram.textSize = bytesToSizeFilter(pool_ram.size)
|
||||
ram_children.push(pool_ram)
|
||||
|
||||
if (host_ram.size !== vm_ram_size) {
|
||||
host_ram.children.push({
|
||||
color: 'white',
|
||||
name: 'Free',
|
||||
id: 'free' + host.id,
|
||||
size: host.memory.size - vm_ram_size,
|
||||
textSize: bytesToSizeFilter(host.memory.size - vm_ram_size),
|
||||
type: 'vm',
|
||||
virtual: true
|
||||
})
|
||||
}
|
||||
|
||||
host_ram.textSize = bytesToSizeFilter(host_ram.size)
|
||||
pool_ram.size += host_ram.size
|
||||
pool_ram.children.push(host_ram)
|
||||
})
|
||||
$scope.charts.data.children = ram_children
|
||||
}
|
||||
if (pool_ram.children.length) {
|
||||
pool_ram.textSize = bytesToSizeFilter(pool_ram.size)
|
||||
ram_children.push(pool_ram)
|
||||
}
|
||||
})
|
||||
$scope.charts.data.children = ram_children
|
||||
}
|
||||
|
||||
const debouncedPopulate = debounce(populateChartsData, 300, {leading: true, trailing: true})
|
||||
|
||||
$scope.$watch(() => xoApi.all, function () {
|
||||
$timeout(function () { // all semmes to be unpopulated for now
|
||||
populateChartsData()
|
||||
}, 0)
|
||||
},
|
||||
true)
|
||||
|
||||
|
||||
|
||||
|
||||
})
|
||||
|
||||
debouncedPopulate()
|
||||
$scope.$on('$destroy', xoApi.onUpdate(debouncedPopulate))
|
||||
})
|
||||
// A module exports its name.
|
||||
.name
|
||||
.name
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
.grid-sm
|
||||
|
||||
.grid-sm(ng-if="!selectedChart")
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
p.page-title
|
||||
i.fa.fa-pie-chart
|
||||
| Dataviz
|
||||
.grid-sm
|
||||
.panel-body.text-center
|
||||
|
||||
.chart-selector(
|
||||
ng-repeat="(id,chart) in availablecharts"
|
||||
ui-sref="dashboard.dataviz({chart:id})")
|
||||
div {{chart.name }}
|
||||
img.img-thumbnail(
|
||||
ng-repeat="img in chart.imgs"
|
||||
ng-src="{{img}}"
|
||||
)
|
||||
|
||||
.grid-sm(ng-if="selectedChart =='sunburst'")
|
||||
.grid-cell
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
@@ -11,6 +23,7 @@
|
||||
| Memory usage
|
||||
.panel-body.text-center(
|
||||
ng-controller="DatavizRamHierarchical as ram"
|
||||
style="position:relative"
|
||||
)
|
||||
sunburst-chart(
|
||||
click="charts.click(d)"
|
||||
@@ -22,24 +35,51 @@
|
||||
i.xo-icon-sr
|
||||
| Storage
|
||||
.panel-body.text-center(
|
||||
ng-controller="DatavizStorageHierarchical as storage"
|
||||
ng-controller="DatavizStorageHierarchical as storage"
|
||||
style="position:relative"
|
||||
)
|
||||
sunburst-chart(
|
||||
click="charts.click(d)"
|
||||
chart-data="charts.data"
|
||||
|
||||
)
|
||||
//- .grid-sm
|
||||
//- .grid-cell
|
||||
//- .panel.panel-default
|
||||
//- .panel-heading.panel-title
|
||||
//- i.xo-icon-memory
|
||||
//- | Storage
|
||||
//- .panel-body.text-center(
|
||||
//- ng-controller="DatavizStorageHierarchical as storage"
|
||||
//- )
|
||||
//- treemap-chart(
|
||||
//- click="charts.click(d)"
|
||||
//- chart-data="charts.data"
|
||||
|
||||
//- )
|
||||
.grid-sm(ng-if="selectedChart =='circle'")
|
||||
.grid-cell
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-memory
|
||||
| Memory usage
|
||||
.panel-body.text-center(
|
||||
ng-controller="DatavizRamHierarchical as ram"
|
||||
style="position:relative"
|
||||
)
|
||||
circle-chart(
|
||||
click="charts.click(d)"
|
||||
chart-data="charts.data"
|
||||
)
|
||||
.grid-cell
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-sr
|
||||
| Storage
|
||||
.panel-body.text-center(
|
||||
ng-controller="DatavizStorageHierarchical as storage"
|
||||
style="position:relative"
|
||||
)
|
||||
circle-chart(
|
||||
click="charts.click(d)"
|
||||
chart-data="charts.data"
|
||||
)
|
||||
.grid-sm(ng-if="selectedChart == 'parcoords'")
|
||||
.grid-cell
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-memory
|
||||
| VMs properties
|
||||
.panel-body.text-center(
|
||||
ng-controller="DatavizParcoords as parcoords"
|
||||
)
|
||||
parallel-chart(
|
||||
click="charts.click(d)"
|
||||
chart-labels="charts.labels"
|
||||
chart-data="charts.data"
|
||||
)
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import angular from 'angular'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
import filter from 'lodash.filter'
|
||||
import forEach from 'lodash.foreach'
|
||||
|
||||
import xoApi from 'xo-api'
|
||||
import xoServices from 'xo-services'
|
||||
|
||||
import view from './view'
|
||||
|
||||
export default angular.module('dashboard.health', [
|
||||
uiRouter,
|
||||
|
||||
xoApi,
|
||||
xoServices
|
||||
xoApi
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('dashboard.health', {
|
||||
@@ -23,123 +21,69 @@ export default angular.module('dashboard.health', [
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('Health', function (xoApi, xo) {
|
||||
// console.log(' in main ')
|
||||
this.charts = {
|
||||
heatmap: null
|
||||
|
||||
.controller('Health', function (xo, xoApi, $scope, modal) {
|
||||
this.currentVdiPage = 1
|
||||
this.currentVmPage = 1
|
||||
|
||||
const vms = xoApi.getView('VM-snapshot').all
|
||||
const vdis = xoApi.getView('VDI-snapshot').all
|
||||
const srs = xoApi.getView('SR').all
|
||||
|
||||
$scope.$watchCollection(() => vdis, () => {
|
||||
const orphanVdiSnapshots = filter(vdis, vdi => vdi && !vdi.$snapshot_of)
|
||||
this.orphanVdiSnapshots = orphanVdiSnapshots
|
||||
})
|
||||
|
||||
$scope.$watchCollection(() => vms, () => {
|
||||
const orphanVmSnapshots = filter(vms, vm => vm && !vm.$snapshot_of)
|
||||
this.orphanVmSnapshots = orphanVmSnapshots
|
||||
})
|
||||
|
||||
$scope.$watchCollection(() => srs, () => {
|
||||
const warningSrs = filter(srs, sr => sr.content_type !== 'iso' && (sr.physical_usage / sr.size) >= 0.8 && (sr.physical_usage / sr.size) < 0.9)
|
||||
const dangerSrs = filter(srs, sr => sr.content_type !== 'iso' && (sr.physical_usage / sr.size) >= 0.9)
|
||||
this.warningSrs = warningSrs
|
||||
this.dangerSrs = dangerSrs
|
||||
})
|
||||
|
||||
this.selectedVdiForDelete = {}
|
||||
this.selectedVmForDelete = {}
|
||||
|
||||
this.deleteVdiSnapshot = function (id) {
|
||||
modal.confirm({
|
||||
title: 'VDI snapshot deletion',
|
||||
message: 'Are you sure you want to delete this snapshot?'
|
||||
}).then(() => xo.vdi.delete(id))
|
||||
}
|
||||
|
||||
this.objects = xoApi.all
|
||||
|
||||
this.prepareMetrics = function (object, notify) {
|
||||
this.metrics = undefined
|
||||
this.selectedMetric = undefined
|
||||
if (object) {
|
||||
this.loadingMetrics = true
|
||||
const apiType = (object.type === 'host' && 'host') || (object.type === 'VM' && 'vm') || undefined
|
||||
if (!apiType) {
|
||||
notify.error({
|
||||
title: 'Unhandled object',
|
||||
message: 'There is no stats available for this type of objects'
|
||||
})
|
||||
}
|
||||
xo[apiType].refreshStats(object.id, 2) // 2: week granularity (7 * 24 hours)
|
||||
.then(result => {
|
||||
const metrics = []
|
||||
forEach(result.cpus, (values, metricKey) => {
|
||||
const mapValues = []
|
||||
forEach(values, (value, key) => {
|
||||
mapValues.push({
|
||||
value: +value,
|
||||
date: +result.date[key] * 1000
|
||||
})
|
||||
})
|
||||
metrics.push({
|
||||
key: 'CPU ' + metricKey,
|
||||
values: mapValues
|
||||
})
|
||||
})
|
||||
forEach(result.vifs, (values, metricKey) => {
|
||||
const mapValues = []
|
||||
forEach(values, (value, key) => {
|
||||
mapValues.push({
|
||||
value: +value,
|
||||
date: +result.date[key] * 1000
|
||||
})
|
||||
})
|
||||
metrics.push({
|
||||
key: 'Network ' + Math.floor(metricKey / 2) + ' ' + (metricKey % 2 ? 'out' : 'in'),
|
||||
values: mapValues
|
||||
})
|
||||
})
|
||||
forEach(result.pifs, (values, metricKey) => {
|
||||
const mapValues = []
|
||||
forEach(values, (value, key) => {
|
||||
mapValues.push({
|
||||
value: +value,
|
||||
date: +result.date[key] * 1000
|
||||
})
|
||||
})
|
||||
metrics.push({
|
||||
key: 'NIC ' + Math.floor(metricKey / 2) + ' ' + (metricKey % 2 ? 'out' : 'in'),
|
||||
values: mapValues
|
||||
})
|
||||
})
|
||||
forEach(result.xvds, (values, key) => {
|
||||
const mapValues = []
|
||||
forEach(values, (value, key) => {
|
||||
mapValues.push({
|
||||
value: +value,
|
||||
date: +result.date[key] * 1000
|
||||
})
|
||||
})
|
||||
metrics.push({
|
||||
key: 'Disk ' + String.fromCharCode(Math.floor(key / 2) + 65) + ' ' + (key % 2 ? 'write' : 'read'),
|
||||
values: mapValues
|
||||
})
|
||||
})
|
||||
if (result.load) {
|
||||
const mapValues = []
|
||||
forEach(result.load, (value, key) => {
|
||||
mapValues.push({
|
||||
value: +value,
|
||||
date: +result.date[key] * 1000
|
||||
})
|
||||
})
|
||||
metrics.push({
|
||||
key: 'Load average',
|
||||
values: mapValues
|
||||
})
|
||||
}
|
||||
if (result.memoryUsed) {
|
||||
const mapValues = []
|
||||
forEach(result.memoryUsed, (value, key) => {
|
||||
mapValues.push({
|
||||
value: +value * (object.type === 'host' ? 1024 : 1),
|
||||
date: +result.date[key] * 1000
|
||||
})
|
||||
})
|
||||
metrics.push({
|
||||
key: 'RAM Used',
|
||||
values: mapValues
|
||||
})
|
||||
}
|
||||
this.loadingMetrics = false
|
||||
this.metrics = metrics
|
||||
})
|
||||
}
|
||||
this.deleteVmSnapshot = function (id) {
|
||||
modal.confirm({
|
||||
title: 'VM snapshot deletion',
|
||||
message: 'Are you sure you want to delete this snapshot? (including its disks)'
|
||||
}).then(() => xo.vm.delete(id, true))
|
||||
}
|
||||
|
||||
this.deleteSelectedVdis = function () {
|
||||
return modal.confirm({
|
||||
title: 'VDI snapshot deletion',
|
||||
message: 'Are you sure you want to delete all selected VDI snapshots? This operation is irreversible.'
|
||||
}).then(() => {
|
||||
forEach(this.selectedVdiForDelete, (selected, id) => console.log(id))
|
||||
forEach(this.selectedVdiForDelete, (selected, id) => { selected && xo.vdi.delete(id) })
|
||||
this.selectedVdiForDelete = {}
|
||||
})
|
||||
}
|
||||
|
||||
this.deleteSelectedVms = function () {
|
||||
return modal.confirm({
|
||||
title: 'VM snapshot deletion',
|
||||
message: 'Are you sure you want to delete all selected VM snapshots? This operation is irreversible.'
|
||||
}).then(() => {
|
||||
forEach(this.selectedVmForDelete, (selected, id) => { selected && xo.vm.delete(id, true) })
|
||||
this.selectedVmForDelete = {}
|
||||
})
|
||||
}
|
||||
// $interval(
|
||||
// function(){
|
||||
// var values = [];
|
||||
// for (var i = 0 ;i < 220 ; i ++){
|
||||
// values.push({
|
||||
// value:Math.random()*1500-750,
|
||||
// date:Date.now()+ i*60*60*1000
|
||||
// })
|
||||
// }
|
||||
// $scope.example = values;
|
||||
// },5000
|
||||
// )
|
||||
})
|
||||
|
||||
.name
|
||||
|
||||
@@ -3,65 +3,86 @@
|
||||
p.page-title
|
||||
i.fa.fa-heartbeat
|
||||
| Health
|
||||
//- .grid-sm
|
||||
//- .grid-cell
|
||||
//- .panel.panel-default
|
||||
//- .panel-heading.panel-title
|
||||
//- i.fa.fa-dashboard
|
||||
//- | CPUs load
|
||||
//- .panel-body.text-center
|
||||
//- p Charts with Load of all hosts CPUs
|
||||
//- .grid-cell
|
||||
//- .panel.panel-default
|
||||
//- .panel-heading.panel-title
|
||||
//- i.xo-icon-memory
|
||||
//- | Memory load
|
||||
//- .panel-body.text-center
|
||||
//- p Charts with RAM used for all hosts RAM?
|
||||
//- .grid-sm
|
||||
//- .grid-cell
|
||||
//- .panel.panel-default
|
||||
//- .panel-heading.panel-title
|
||||
//- i.fa.fa-sitemap
|
||||
//- | Networks
|
||||
//- .panel-body.text-center
|
||||
//- p Charts with Load of all hosts networks
|
||||
//- .grid-cell
|
||||
//- .panel.panel-default
|
||||
//- .panel-heading.panel-title
|
||||
//- i.fa.fa-hdd-o
|
||||
//- | Disk IO
|
||||
//- .panel-body.text-center
|
||||
//- p Charts with IO for all SR?
|
||||
.grid-sm
|
||||
.grid-cell
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-fire
|
||||
| Weekly Heatmap
|
||||
.panel-body.text-center
|
||||
form
|
||||
.grid-sm
|
||||
.grid-cell.grid--gutters
|
||||
.container-fluid
|
||||
.form-group
|
||||
ui-select(ng-model = 'ctrl.selected', ng-change = 'ctrl.prepareMetrics(ctrl.selected)')
|
||||
ui-select-match(placeholder = 'Choose an object')
|
||||
i(class = 'xo-icon-{{ $select.selected.type | lowercase }}')
|
||||
| {{ $select.selected.name_label }}
|
||||
ui-select-choices(repeat = 'object in ctrl.objects | underStat | 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 }})
|
||||
.grid-cell.grid--gutters
|
||||
.container-fluid
|
||||
span(ng-if = 'ctrl.loadingMetrics')
|
||||
| Loading metrics ...
|
||||
i.fa.fa-circle-o-notch.fa-spin
|
||||
.form-group(ng-if = 'ctrl.metrics')
|
||||
ui-select(ng-model = 'ctrl.selectedMetric')
|
||||
ui-select-match(placeholder = 'Choose a metric') {{ $select.selected.key }}
|
||||
ui-select-choices(repeat = 'metric in ctrl.metrics | filter:$select.search | orderBy:["key"]') {{ metric.key }}
|
||||
weekheatmap(ng-if = 'ctrl.selectedMetric', chart-data="ctrl.selectedMetric.values")
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-hdd-o
|
||||
| Orphaned VDI snapshots
|
||||
.panel-body
|
||||
.center(ng-if = 'ctrl.orphanVdiSnapshots | isEmpty') No orphaned snapshots found
|
||||
table.table.table-hover(ng-if = 'ctrl.orphanVdiSnapshots | isNotEmpty')
|
||||
tr
|
||||
th Name
|
||||
th Description
|
||||
th Tags
|
||||
th Size
|
||||
th SR
|
||||
span.pull-right: button.btn.btn-danger(xo-click = 'ctrl.deleteSelectedVdis()', tooltip = 'Delete selected snapshots'): i.fa.fa-trash
|
||||
tr(ng-repeat = 'vdi in ctrl.orphanVdiSnapshots | filter:vdiSearch | orderBy:natural("name_label") | slice:(10*(ctrl.currentVdiPage-1)):(10*ctrl.currentVdiPage) track by vdi.id')
|
||||
td.oneliner {{ vdi.name_label }}
|
||||
td.oneliner {{ vdi.name_description }}
|
||||
td: xo-tag(object = 'vdi')
|
||||
td {{ vdi.size | bytesToSize}}
|
||||
td.oneliner
|
||||
a(xo-sref="SRs_view({id: (vdi.$SR | resolve).id})")
|
||||
| {{(vdi.$SR | resolve).name_label}} ({{((vdi.$SR | resolve).$container | resolve).name_label}})
|
||||
span.pull-right
|
||||
.btn-group.quick-buttons
|
||||
a(xo-click="ctrl.deleteVdiSnapshot(vdi.id)"): i.fa.fa-trash-o.fa-lg(tooltip="Destroy this snapshot")
|
||||
input(type = 'checkbox', ng-model = 'ctrl.selectedVdiForDelete[vdi.id]', tooltip = 'select for deletion')
|
||||
.form-inline
|
||||
.input-group
|
||||
.input-group-addon: i.fa.fa-filter
|
||||
input.form-control(type = 'text', ng-model = 'vdiSearch', placeholder = 'Enter your search here')
|
||||
.center(ng-if = '(ctrl.orphanVdiSnapshots | filter:vdiSearch).length > 10 || ctrl.currentVdiPage > 1')
|
||||
pagination(boundary-links="true", total-items="(ctrl.orphanVdiSnapshots | filter:vdiSearch).length", ng-model="ctrl.currentVdiPage", items-per-page="10", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-camera
|
||||
| Orphaned VM snapshots
|
||||
.panel-body
|
||||
.center(ng-if = 'ctrl.orphanVmSnapshots | isEmpty') No orphaned snapshots found
|
||||
table.table.table-hover(ng-if = 'ctrl.orphanVmSnapshots | isNotEmpty')
|
||||
tr
|
||||
th Name
|
||||
th Description
|
||||
th OS
|
||||
th Container
|
||||
span.pull-right: button.btn.btn-danger(xo-click = 'ctrl.deleteSelectedVms()', tooltip = 'Delete selected snapshots'): i.fa.fa-trash
|
||||
tr(ng-repeat = 'vm in ctrl.orphanVmSnapshots | orderBy:natural("name_label") | slice:(10*(ctrl.currentVmPage-1)):(10*ctrl.currentVmPage) track by vm.id')
|
||||
td.oneliner
|
||||
i.xo-icon-working(ng-if="vm.current_operations | isNotEmpty", tooltip="{{vm.power_state}} and {{(vm.current_operations | map)[0]}}")
|
||||
i(class="xo-icon-{{vm.power_state | lowercase}}",ng-if="vm.current_operations | isEmpty", tooltip="{{vm.power_state}}")
|
||||
| {{ vm.name_label }}
|
||||
td.oneliner {{ vm.name_description }}
|
||||
td.onliner {{ vm.os_version.name }}
|
||||
td.oneliner {{ (vm.$container | resolve).name_label }}
|
||||
span.pull-right
|
||||
.btn-group.quick-buttons
|
||||
a(xo-click="ctrl.deleteVmSnapshot(vm.id)"): i.fa.fa-trash-o.fa-lg(tooltip="Destroy this snapshot")
|
||||
input(type = 'checkbox', ng-model = 'ctrl.selectedVmForDelete[vm.id]', tooltip = 'select for deletion')
|
||||
.form-inline
|
||||
.input-group
|
||||
.input-group-addon: i.fa.fa-filter
|
||||
input.form-control(type = 'text', ng-model = 'vmSearch', placeholder = 'Enter your search here')
|
||||
.center(ng-if = '(ctrl.orphanVmSnapshots | filter:vmSearch).length > 10 || ctrl.currentVmPage > 1')
|
||||
pagination(boundary-links="true", total-items="(ctrl.orphanVmSnapshots | filter:vmSearch).length", ng-model="ctrl.currentVmPage", items-per-page="10", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
|
||||
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-database
|
||||
| SR Warnings
|
||||
.panel-body
|
||||
.center(ng-if = '(ctrl.warningSrs | isEmpty) && (ctrl.dangerSrs | isEmpty)') No warnings found
|
||||
table.table.table-hover(ng-if = '(ctrl.warningSrs | isNotEmpty) || (ctrl.dangerSrs | isNotEmpty)')
|
||||
tr
|
||||
th SR
|
||||
th Physical usage
|
||||
tr(ng-repeat = 'sr in ctrl.dangerSrs')
|
||||
td: a(xo-sref="SRs_view({id: sr.id})") {{ sr.name_label }} ({{ (sr.$container | resolve).name_label }})
|
||||
td: span.label.label-danger {{ [sr.physical_usage, sr.size] | percentage }}
|
||||
tr(ng-repeat = 'sr in ctrl.warningSrs')
|
||||
td: a(xo-sref="SRs_view({id: sr.id})") {{ sr.name_label }} ({{ (sr.$container | resolve).name_label }})
|
||||
td: span.label.label-warning {{ [sr.physical_usage, sr.size] | percentage }}
|
||||
|
||||
@@ -2,16 +2,18 @@ import angular from 'angular'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
|
||||
import dataviz from './dataviz'
|
||||
import filter from 'lodash.filter'
|
||||
import health from './health'
|
||||
import stats from './stats'
|
||||
import overview from './overview'
|
||||
|
||||
import view from './view'
|
||||
|
||||
export default angular.module('dashboard', [
|
||||
uiRouter,
|
||||
|
||||
dataviz,
|
||||
health,
|
||||
stats,
|
||||
overview
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
@@ -32,4 +34,10 @@ export default angular.module('dashboard', [
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
.filter('underStat', () => {
|
||||
let isUnderStat = object => object.type === 'host' || object.type === 'VM'
|
||||
return objects => filter(objects, isUnderStat)
|
||||
})
|
||||
|
||||
.name
|
||||
|
||||
@@ -4,7 +4,7 @@ import uiRouter from 'angular-ui-router'
|
||||
import uiSelect from 'angular-ui-select'
|
||||
|
||||
import clone from 'lodash.clonedeep'
|
||||
import filter from 'lodash.filter'
|
||||
import debounce from 'lodash.debounce'
|
||||
import foreach from 'lodash.foreach'
|
||||
|
||||
import xoApi from 'xo-api'
|
||||
@@ -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,8 +47,34 @@ export default angular.module('dashboard.overview', [
|
||||
cpu: [0, 0],
|
||||
srs: []
|
||||
})
|
||||
function populateChartsData () {
|
||||
|
||||
$scope.installAllPatches = function () {
|
||||
modal.confirm({
|
||||
title: 'Install all the missing patches',
|
||||
message: 'Are you sure you want to install all the missing patches? This could take a while...'
|
||||
}).then(() =>
|
||||
foreach($scope.pools.all, function (pool, pool_id) {
|
||||
let pool_hosts = $scope.hostsByPool[pool_id]
|
||||
foreach(pool_hosts, function (host, host_id) {
|
||||
console.log('Installing all missing patches on host ', host_id)
|
||||
xo.host.installAllPatches(host_id)
|
||||
})
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
$scope.installHostPatches = function (hostId) {
|
||||
modal.confirm({
|
||||
title: 'Update host (' + $scope.nbUpdates[hostId] + ' patch(es))',
|
||||
message: 'Are you sure you want to install all the missing patches on this host? This could take a while...'
|
||||
}).then(() => {
|
||||
console.log('Installing all missing patches on host ', hostId)
|
||||
xo.host.installAllPatches(hostId)
|
||||
})
|
||||
}
|
||||
|
||||
const nbUpdates = $scope.nbUpdates = {}
|
||||
function populateChartsData () {
|
||||
let pools,
|
||||
vmsByContainer,
|
||||
hostsByPool,
|
||||
@@ -74,25 +100,38 @@ export default angular.module('dashboard.overview', [
|
||||
nb_pools = 0
|
||||
srs = []
|
||||
|
||||
//update vdi, set thme to the right host
|
||||
pools = xoApi.getView('pools')
|
||||
// update vdi, set them to the right host
|
||||
$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++
|
||||
let pool_srs = srsByContainer[pool_id]
|
||||
foreach(pool_srs, (one_srs)=> {
|
||||
if(one_srs.SR_type !== 'iso'
|
||||
&& one_srs.SR_type !== 'udev'){
|
||||
one_srs = clone(one_srs)
|
||||
one_srs.ratio = one_srs.size ? one_srs.physical_usage/one_srs.size : 0
|
||||
one_srs.pool_label = pool.name_label
|
||||
srs.push(one_srs)
|
||||
}
|
||||
})
|
||||
let pool_srs = srsByContainer[pool_id]
|
||||
foreach(pool_srs, (one_srs) => {
|
||||
if (one_srs.SR_type !== 'iso' && one_srs.SR_type !== 'udev') {
|
||||
one_srs = clone(one_srs)
|
||||
one_srs.ratio = one_srs.size ? one_srs.physical_usage / one_srs.size : 0
|
||||
one_srs.pool_label = pool.name_label
|
||||
srs.push(one_srs)
|
||||
}
|
||||
})
|
||||
let VMs = vmsByContainer[pool_id]
|
||||
foreach(VMs, function (VM) {
|
||||
// non running VM
|
||||
@@ -102,11 +141,10 @@ export default angular.module('dashboard.overview', [
|
||||
let hosts = hostsByPool[pool_id]
|
||||
foreach(hosts, function (host, host_id) {
|
||||
let hosts_srs = srsByContainer[host_id]
|
||||
foreach(hosts_srs, (one_srs)=> {
|
||||
if(one_srs.SR_type !== 'iso'
|
||||
&& one_srs.SR_type !== 'udev'){
|
||||
foreach(hosts_srs, (one_srs) => {
|
||||
if (one_srs.SR_type !== 'iso' && one_srs.SR_type !== 'udev') {
|
||||
one_srs = clone(one_srs)
|
||||
one_srs.ratio = one_srs.size ? one_srs.physical_usage/one_srs.size : 0
|
||||
one_srs.ratio = one_srs.size ? one_srs.physical_usage / one_srs.size : 0
|
||||
one_srs.host_label = host.name_label
|
||||
one_srs.pool_label = pool.name_label
|
||||
srs.push(one_srs)
|
||||
@@ -125,19 +163,15 @@ export default angular.module('dashboard.overview', [
|
||||
$scope.vms = vms
|
||||
$scope.pools.nb = nb_pools
|
||||
$scope.srs = srs
|
||||
$scope.ram = [xoApi.stats.$memory.size, xoApi.stats.$memory.size - xoApi.stats.$memory.usage]
|
||||
$scope.cpu = [[xoApi.stats.$CPUs], [xoApi.stats.$vCPUs]]
|
||||
$scope.ram = [xoApi.stats.$memory.usage, xoApi.stats.$memory.size - xoApi.stats.$memory.usage]
|
||||
$scope.cpu = [[xoApi.stats.$vCPUs], [xoApi.stats.$CPUs]]
|
||||
}
|
||||
|
||||
populateChartsData()
|
||||
$scope.$watch(() => xoApi.all, function () {
|
||||
console.log(' XOAPI .ALL CHANGHE')
|
||||
$timeout(function () { // all semmes to be unpopulated for now
|
||||
populateChartsData()
|
||||
}, 0)
|
||||
},
|
||||
true)
|
||||
const debouncedPopulate = debounce(populateChartsData, 300, {leading: true, trailing: true})
|
||||
|
||||
debouncedPopulate()
|
||||
|
||||
$scope.$on('$destroy', xoApi.onUpdate(debouncedPopulate))
|
||||
})
|
||||
|
||||
.name
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.fa.fa-dashboard
|
||||
| Dashoard
|
||||
| Dashboard
|
||||
.grid-sm
|
||||
.grid-cell
|
||||
.panel.panel-default
|
||||
@@ -87,7 +87,7 @@
|
||||
th Physical/Allocated usage
|
||||
//- TODO: display PBD status for each SR of this host (connected or not)
|
||||
//- Shared SR
|
||||
tr(xo-sref="SRs_view({id: SR.id})", ng-repeat="SR in srs | orderBy:'-ratio' track by SR.id")
|
||||
tr(xo-sref="SRs_view({id: SR.id})", ng-repeat="SR in srs | map | orderBy:'-ratio' track by SR.id")
|
||||
td.oneliner
|
||||
| {{SR.name_label}}
|
||||
td.oneliner
|
||||
@@ -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 | map | orderBy:'name_label'")
|
||||
tr( ng-repeat="host in hostsByPool[pool.id]" ng-if="nbUpdates[host.id]")
|
||||
td.oneliner
|
||||
| {{ pool.name_label }}
|
||||
td.oneliner
|
||||
| {{ host.name_label }}
|
||||
td.oneliner
|
||||
| {{ host.name_description }}
|
||||
td {{ nbUpdates[host.id] }}
|
||||
td
|
||||
button.btn.btn-success(ng-click="installHostPatches(host.id)" tooltip="Install {{ nbUpdates[host.id] }} patch(es)")
|
||||
| Update host
|
||||
|
||||
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
@@ -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 | map | orderBy:["type", "name_label"] track by object.id')
|
||||
div
|
||||
i(class = 'xo-icon-{{ object.type | lowercase }}')
|
||||
| {{ object.name_label }}
|
||||
span(ng-if='(object.type === "SR" || object.type === "VM") && object.$container')
|
||||
| ({{ (object.$container | resolve).name_label }})
|
||||
//- br
|
||||
.btn-group(role = 'group')
|
||||
button.btn.btn-default(ng-click = 'heatmap.selected = []', tooltip = 'Clear selection')
|
||||
i.fa.fa-times
|
||||
button.btn.btn-default(ng-click = 'heatmap.selectAll("VM")', tooltip = 'Choose all VMs')
|
||||
i.xo-icon-vm
|
||||
button.btn.btn-default(ng-click = 'heatmap.selectAll("host")', tooltip = 'Choose all hosts')
|
||||
i.xo-icon-host
|
||||
button.btn.btn-success(ng-click = 'heatmap.prepareMetrics(heatmap.selected)', tooltip = 'Load metrics')
|
||||
i.fa.fa-check
|
||||
| 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 | map | orderBy:["key"]') {{ metric.key }}
|
||||
br
|
||||
p.text-center(ng-if = 'heatmap.chosen.length')
|
||||
span(ng-repeat = 'object in heatmap.chosen', ng-class = '{"text-danger": object._ignored}')
|
||||
i(class = 'xo-icon-{{ object.type | lowercase }}')
|
||||
|
|
||||
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 | map | orderBy:["type", "name_label"] track by object.id')
|
||||
div
|
||||
i(class = 'xo-icon-{{ object.type | lowercase }}')
|
||||
| {{ object.name_label }}
|
||||
span(ng-if='(object.type === "SR" || object.type === "VM") && object.$container')
|
||||
| ({{ (object.$container | resolve).name_label }})
|
||||
//- br
|
||||
.btn-group(role = 'group')
|
||||
button.btn.btn-default(ng-click = 'horizons.selected = []', tooltip = 'Clear selection')
|
||||
i.fa.fa-times
|
||||
button.btn.btn-default(ng-click = 'horizons.selectAll("VM")', tooltip = 'Choose all VMs')
|
||||
i.xo-icon-vm
|
||||
button.btn.btn-default(ng-click = 'horizons.selectAll("host")', tooltip = 'Choose all hosts')
|
||||
i.xo-icon-host
|
||||
button.btn.btn-success(ng-click = 'horizons.prepareMetrics(horizons.selected)', tooltip = 'Load metrics')
|
||||
i.fa.fa-check
|
||||
| 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 | map | orderBy:["key"]') {{ metric }}
|
||||
br
|
||||
button.btn.btn-primary.pull-right(
|
||||
tooltip="Desynchronize Scale",
|
||||
ng-click="horizons.toggleSynchronizeScale()"
|
||||
ng-if='horizons.synchronizescale && horizons.selectedMetric'
|
||||
)
|
||||
i.fa.fa-balance-scale
|
||||
button.btn.btn-default.pull-right(
|
||||
tooltip="Synchronize Scale",
|
||||
ng-click="horizons.toggleSynchronizeScale()"
|
||||
ng-if='!horizons.synchronizescale && horizons.selectedMetric'
|
||||
)
|
||||
i.fa.fa-balance-scale
|
||||
br
|
||||
p.text-center(ng-if = 'horizons.chosen.length')
|
||||
span(ng-repeat = 'object in horizons.chosen', ng-class = '{"text-danger": object._ignored}')
|
||||
i(class = 'xo-icon-{{ object.type | lowercase }}')
|
||||
|
|
||||
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}}'
|
||||
)
|
||||
@@ -6,9 +6,13 @@
|
||||
i.fa.fa-fw.fa-dashboard.fa-menu
|
||||
span.menu-entry Overview
|
||||
li
|
||||
a(ui-sref = '.dataviz')
|
||||
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
|
||||
|
||||
@@ -1,37 +1,50 @@
|
||||
import angular from 'angular'
|
||||
import uiBootstrap from 'angular-ui-bootstrap'
|
||||
|
||||
import template from './view'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default angular.module('xoWebApp.genericModal', [
|
||||
uiBootstrap
|
||||
])
|
||||
.controller('GenericModalCtrl', function ($scope, $modalInstance, options) {
|
||||
$scope.title = options.title
|
||||
$scope.message = options.message
|
||||
.controller('GenericModalCtrl', function ($modalInstance, $sce, options) {
|
||||
const {
|
||||
htmlMessage,
|
||||
message,
|
||||
noButtonLabel = undefined,
|
||||
title,
|
||||
yesButtonLabel = 'Ok'
|
||||
} = options
|
||||
|
||||
$scope.yesButtonLabel = options.yesButtonLabel || 'Ok'
|
||||
$scope.noButtonLabel = options.noButtonLabel
|
||||
this.title = title
|
||||
this.message = message
|
||||
this.htmlMessage = htmlMessage && $sce.trustAsHtml(htmlMessage)
|
||||
|
||||
this.yesButtonLabel = yesButtonLabel
|
||||
this.noButtonLabel = noButtonLabel
|
||||
})
|
||||
.service('modal', function ($modal) {
|
||||
return {
|
||||
confirm: function (opts) {
|
||||
const modal = $modal.open({
|
||||
controller: 'GenericModalCtrl',
|
||||
template: require('./view'),
|
||||
resolve: {
|
||||
options: function () {
|
||||
return {
|
||||
title: opts.title,
|
||||
message: opts.message,
|
||||
noButtonLabel: 'Cancel'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return modal.result
|
||||
}
|
||||
alert: ({ title, htmlMessage, message }) => $modal.open({
|
||||
controller: 'GenericModalCtrl as $ctrl',
|
||||
template,
|
||||
resolve: {
|
||||
options: () => ({ title, htmlMessage, message })
|
||||
}
|
||||
}).result,
|
||||
confirm: ({ title, htmlMessage, message }) => $modal.open({
|
||||
controller: 'GenericModalCtrl as $ctrl',
|
||||
template,
|
||||
resolve: {
|
||||
options: () => ({
|
||||
title,
|
||||
htmlMessage,
|
||||
message,
|
||||
noButtonLabel: 'Cancel'
|
||||
})
|
||||
}
|
||||
}).result
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
.modal-header
|
||||
h3
|
||||
i.fa.fa-exclamation-triangle.text-danger
|
||||
| {{title}}
|
||||
.modal-body
|
||||
| {{message}}
|
||||
| {{$ctrl.title}}
|
||||
.modal-body(ng-if = "$ctrl.htmlMessage", ng-bind-html = "$ctrl.htmlMessage")
|
||||
.modal-body(ng-if = "!$ctrl.htmlMessage") {{$ctrl.message}}
|
||||
.modal-footer
|
||||
button.btn.btn-primary(type="button", ng-click="$close()")
|
||||
| {{yesButtonLabel}}
|
||||
button.btn.btn-warning(ng-if="noButtonLabel", type="button", ng-click="$dismiss()")
|
||||
| {{noButtonLabel}}
|
||||
| {{$ctrl.yesButtonLabel}}
|
||||
button.btn.btn-warning(ng-if="$ctrl.noButtonLabel", type="button", ng-click="$dismiss()")
|
||||
| {{$ctrl.noButtonLabel}}
|
||||
|
||||
@@ -5,12 +5,15 @@ map = require 'lodash.map'
|
||||
omit = require 'lodash.omit'
|
||||
sum = require 'lodash.sum'
|
||||
throttle = require 'lodash.throttle'
|
||||
find = require 'lodash.find'
|
||||
filter = require 'lodash.filter'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
module.exports = angular.module 'xoWebApp.host', [
|
||||
require 'angular-file-upload'
|
||||
require 'angular-ui-router'
|
||||
require('ng-file-upload')
|
||||
require('tag').default
|
||||
]
|
||||
.config ($stateProvider) ->
|
||||
$stateProvider.state 'hosts_view',
|
||||
@@ -19,10 +22,10 @@ module.exports = angular.module 'xoWebApp.host', [
|
||||
template: require './view'
|
||||
.controller 'HostCtrl', (
|
||||
$scope, $stateParams, $http
|
||||
$upload
|
||||
$window
|
||||
$timeout
|
||||
$window
|
||||
dateFilter
|
||||
Upload
|
||||
xoApi, xo, modal, notify, bytesToSizeFilter
|
||||
) ->
|
||||
do (
|
||||
@@ -61,6 +64,7 @@ module.exports = angular.module 'xoWebApp.host', [
|
||||
$scope.currentLogPage = 1
|
||||
$scope.currentPCIPage = 1
|
||||
$scope.currentGPUPage = 1
|
||||
$scope.currentLicensePage = 1
|
||||
|
||||
$scope.refreshStatControl = refreshStatControl = {
|
||||
baseStatInterval: 5000
|
||||
@@ -111,6 +115,8 @@ module.exports = angular.module 'xoWebApp.host', [
|
||||
$scope.host = host
|
||||
return unless host?
|
||||
|
||||
$scope.hostParams = Object.getOwnPropertyNames(host.license_params)
|
||||
|
||||
pool = $scope.pool = xoApi.get host.$poolId
|
||||
|
||||
SRsToPBDs = $scope.SRsToPBDs = Object.create null
|
||||
@@ -130,7 +136,7 @@ module.exports = angular.module 'xoWebApp.host', [
|
||||
)
|
||||
|
||||
$scope.$watch('vms', (vms) =>
|
||||
$scope.vCPUs = sum(vms, (vm) => vm.CPUs.number)
|
||||
$scope.vCPUs = sum(map(vms, (vm) => +vm.CPUs.number))
|
||||
)
|
||||
|
||||
$scope.cancelTask = (id) ->
|
||||
@@ -155,12 +161,22 @@ module.exports = angular.module 'xoWebApp.host', [
|
||||
$scope.pool_addHost = (id) ->
|
||||
xo.host.attach id
|
||||
|
||||
$scope.pools = xoApi.getView('pools')
|
||||
$scope.hostsByPool = xoApi.getIndex('hostsByPool')
|
||||
$scope.pool_moveHost = (target) ->
|
||||
modal.confirm({
|
||||
title: 'Move host to another pool'
|
||||
message: 'Are you sure you want to move this host?'
|
||||
}).then ->
|
||||
xo.pool.mergeInto({ source: $scope.pool.id, target: target.id })
|
||||
|
||||
$scope.pool_removeHost = (id) ->
|
||||
modal.confirm({
|
||||
title: 'Remove host from pool'
|
||||
message: 'Are you sure you want to detach this host from its pool? It will be automatically rebooted'
|
||||
message: 'Are you sure you want to detach this host from its pool? It will be automatically rebooted AND LOCAL STORAGE WILL BE ERASED.'
|
||||
}).then ->
|
||||
xo.host.detach id
|
||||
|
||||
$scope.rebootHost = (id) ->
|
||||
modal.confirm({
|
||||
title: 'Reboot host'
|
||||
@@ -201,6 +217,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
|
||||
@@ -275,7 +299,7 @@ module.exports = angular.module 'xoWebApp.host', [
|
||||
|
||||
xo.vm.import id
|
||||
.then ({ $sendTo: url }) ->
|
||||
return $upload.http {
|
||||
return Upload.http {
|
||||
method: 'POST'
|
||||
url
|
||||
data: file
|
||||
@@ -295,7 +319,7 @@ module.exports = angular.module 'xoWebApp.host', [
|
||||
}
|
||||
|
||||
params = {
|
||||
host: $scope.host.id
|
||||
pool: $scope.host.$pool
|
||||
name,
|
||||
}
|
||||
|
||||
@@ -304,11 +328,38 @@ module.exports = angular.module 'xoWebApp.host', [
|
||||
if vlan then params.vlan = vlan
|
||||
if description then params.description = description
|
||||
|
||||
xoApi.call 'host.createNetwork', params
|
||||
xoApi.call 'network.create', params
|
||||
.then ->
|
||||
$scope.creatingNetwork = false
|
||||
$scope.createNetworkWaiting = false
|
||||
|
||||
$scope.addIp = (pif, ip, netmask, dns, gateway, ipMethod) ->
|
||||
notify.info {
|
||||
title: 'IP configuration...'
|
||||
message: 'Configuring new IP mode'
|
||||
}
|
||||
xoApi.call('pif.reconfigureIp', {
|
||||
id: pif.id,
|
||||
mode: ipMethod,
|
||||
ip,
|
||||
netmask,
|
||||
dns,
|
||||
gateway
|
||||
})
|
||||
|
||||
$scope.physicalPifs = () ->
|
||||
physicalPifs = []
|
||||
forEach $scope.host.$PIFs, (pif) ->
|
||||
pif = xoApi.get(pif)
|
||||
if pif.physical
|
||||
physicalPifs.push pif.id
|
||||
return physicalPifs
|
||||
|
||||
$scope.isPoolPatch = (patch) ->
|
||||
return false if $scope.poolPatches is undefined
|
||||
return $scope.poolPatches.hasOwnProperty(patch.uuid)
|
||||
|
||||
|
||||
$scope.isPoolPatchApplied = (patch) ->
|
||||
return true if patch.applied
|
||||
hostPatch = intersection(patch.$host_patches, $scope.host.patches)
|
||||
@@ -329,25 +380,63 @@ module.exports = angular.module 'xoWebApp.host', [
|
||||
}
|
||||
xo.host.installPatch id, patchUid
|
||||
|
||||
$scope.installAllPatches = (id) ->
|
||||
modal.confirm({
|
||||
title: 'Install all the missing patches'
|
||||
message: 'Are you sure you want to install all the missing patches on this host? This could take a while...'
|
||||
}).then ->
|
||||
console.log('Installing all patches on host ' + id)
|
||||
xo.host.installAllPatches id
|
||||
|
||||
$scope.refreshStats = (id) ->
|
||||
return xo.host.refreshStats id
|
||||
|
||||
.then (result) ->
|
||||
result.cpuSeries = []
|
||||
forEach result.cpus, (v,k) ->
|
||||
result.cpuSeries.push 'CPU ' + k
|
||||
result.stats.cpuSeries = []
|
||||
|
||||
if result.stats.cpus.length >= 12
|
||||
nValues = result.stats.cpus[0].length
|
||||
nCpus = result.stats.cpus.length
|
||||
cpuAVG = (0 for [1..nValues])
|
||||
|
||||
forEach result.stats.cpus, (cpu) ->
|
||||
forEach cpu, (stat, index) ->
|
||||
cpuAVG[index] += stat
|
||||
return
|
||||
return
|
||||
|
||||
forEach cpuAVG, (cpu, index) ->
|
||||
cpuAVG[index] /= nCpus
|
||||
return
|
||||
|
||||
result.stats.cpus = [cpuAVG]
|
||||
result.stats.cpuSeries.push 'CPU AVG'
|
||||
else
|
||||
forEach result.stats.cpus, (v,k) ->
|
||||
result.stats.cpuSeries.push 'CPU ' + k
|
||||
return
|
||||
|
||||
result.stats.pifSeries = []
|
||||
pifsArray = []
|
||||
forEach result.stats.pifs.rx, (v,k) ->
|
||||
return unless v
|
||||
result.stats.pifSeries.push '#' + k + ' in'
|
||||
result.stats.pifSeries.push '#' + k + ' out'
|
||||
pifsArray.push (v || [])
|
||||
pifsArray.push (result.stats.pifs.tx[k] || [])
|
||||
return
|
||||
result.pifSeries = []
|
||||
forEach result.pifs, (v,k) ->
|
||||
result.pifSeries.push '#' + Math.floor(k/2) + ' ' + if k % 2 then 'out' else 'in'
|
||||
return
|
||||
forEach result.date, (v,k) ->
|
||||
result.date[k] = new Date(v*1000).toLocaleTimeString()
|
||||
forEach result.memoryUsed, (v, k) ->
|
||||
result.memoryUsed[k] = v*1024
|
||||
forEach result.memory, (v, k) ->
|
||||
result.memory[k] = v*1024
|
||||
$scope.stats = result
|
||||
result.stats.pifs = pifsArray
|
||||
|
||||
forEach result.stats.memoryUsed, (v, k) ->
|
||||
result.stats.memoryUsed[k] = v*1024
|
||||
forEach result.stats.memory, (v, k) ->
|
||||
result.stats.memory[k] = v*1024
|
||||
|
||||
result.stats.date = []
|
||||
timestamp = result.endTimestamp
|
||||
for i in [result.stats.memory.length-1..0] by -1
|
||||
result.stats.date.unshift new Date(timestamp*1000).toLocaleTimeString()
|
||||
timestamp -= 5
|
||||
$scope.stats = result.stats
|
||||
|
||||
$scope.statView = {
|
||||
cpuOnly: false,
|
||||
@@ -355,5 +444,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 Edit", ng-click="hostSettings.$cancel()")
|
||||
i.fa.fa-undo.fa-fw
|
||||
.panel-body
|
||||
form(editable-form="", name="hostSettings", onbeforesave="saveHost($data)")
|
||||
dl.dl-horizontal
|
||||
@@ -31,11 +33,8 @@
|
||||
span(editable-select="host.enabled", e-ng-options="ap.v as ap.t for ap in [{v: true, t:'Yes'}, {v: false, t:'No'}]", e-name="enabled", e-form="hostSettings")
|
||||
| {{host.enabled ? 'Yes' : 'No'}}
|
||||
dt Tags
|
||||
dd(ng-if="host.tags.length")
|
||||
span(ng-repeat="tag in host.tags")
|
||||
span.label.label-primary {{tag}}
|
||||
dd(ng-if="!host.tags.length")
|
||||
em No tags.
|
||||
dd
|
||||
xo-tag(ng-if = 'host', object = 'host')
|
||||
dt CPUs
|
||||
dd {{host.CPUs["cpu_count"]}}x {{host.CPUs["modelname"]}}
|
||||
dt Hostname
|
||||
@@ -92,7 +91,7 @@
|
||||
series="['Used RAM', 'Total RAM']"
|
||||
colours="['#ff0000', '#ffbbbb']"
|
||||
legend="true"
|
||||
options=' {responsive: true, maintainAspectRatio: false, tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>", multiTooltipTemplate:"<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%= bytesToSize(value) %>", datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false, pointHitDetectionRadius: 0}'
|
||||
options='{responsive: true, maintainAspectRatio: false, tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>", multiTooltipTemplate:"<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%= bytesToSize(value) %>", datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false, pointHitDetectionRadius: 0}'
|
||||
)
|
||||
div(ng-if="statView.netOnly", ng-click="statView.netOnly = false")
|
||||
p.stat-name
|
||||
@@ -105,7 +104,7 @@
|
||||
series="stats.pifSeries"
|
||||
colours="['#dddd00', '#dddd77', '#777700', '#dddd55', '#555500', '#ffdd00']"
|
||||
legend="true"
|
||||
options=' {responsive: true, maintainAspectRatio: false, tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>", multiTooltipTemplate:"<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%= bytesToSize(value) %>", datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false, pointHitDetectionRadius: 0}'
|
||||
options='{responsive: true, maintainAspectRatio: false, tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>", multiTooltipTemplate:"<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%= bytesToSize(value) %>", datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false, pointHitDetectionRadius: 0}'
|
||||
)
|
||||
div(ng-if="statView.loadOnly", ng-click="statView.loadOnly = false")
|
||||
p.stat-name
|
||||
@@ -118,7 +117,7 @@
|
||||
series="['Load']"
|
||||
colours="['#960094']"
|
||||
legend="true"
|
||||
options=' {responsive: true, maintainAspectRatio: false, multiTooltipTemplate:"<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%= bytesToSize(value) %>", datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false, pointHitDetectionRadius: 0}'
|
||||
options='{responsive: true, maintainAspectRatio: false, multiTooltipTemplate:"<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%= bytesToSize(value) %>", datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false, pointHitDetectionRadius: 0}'
|
||||
)
|
||||
div(ng-if="!statView.netOnly && !statView.loadOnly && !statView.cpuOnly && !statView.ramOnly")
|
||||
.row
|
||||
@@ -189,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
|
||||
@@ -198,44 +197,69 @@
|
||||
.grid-sm.grid--gutters
|
||||
.grid.grid-cell
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Add SR", 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", 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", 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", 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", 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", 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", 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")
|
||||
button.btn(tooltip="Remove from pool", style="width: 90%", type="button", xo-click="pool_removeHost(host.id)")
|
||||
.grid-cell.btn-group(ng-if="pool.name_label && (hostsByPool[pool.id] | count)>1")
|
||||
button.btn(tooltip="Remove from pool", tooltip-placement="top", style="width: 90%", type="button", xo-click="pool_removeHost(host.id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-cloud-upload.fa-2x.fa-fw
|
||||
.grid-cell.btn-group.dropdown(
|
||||
ng-if="pool.name_label && (hostsByPool[pool.id] | count)==1"
|
||||
dropdown
|
||||
)
|
||||
button.btn.dropdown-toggle(
|
||||
ng-if = 'canAdmin()'
|
||||
dropdown-toggle
|
||||
tooltip="Move host to another pool"
|
||||
tooltip-placement="top"
|
||||
type="button"
|
||||
style="width: 90%"
|
||||
)
|
||||
i.fa.fa-cloud-download.fa-2x.fa-fw
|
||||
span.caret
|
||||
ul.dropdown-menu.left(role="menu")
|
||||
li(ng-repeat="p in pools.all | map | orderBy:natural('name_label') track by p.id" ng-if="p!=pool")
|
||||
a(xo-click="pool_moveHost(p)")
|
||||
i.xo-icon-host.fa-fw
|
||||
| To {{p.name_label}}
|
||||
.grid-cell.btn-group(ng-if="!pool.name_label")
|
||||
button.btn(tooltip="Add to pool", 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"
|
||||
style="width: 90%"
|
||||
ng-file-select = 'importVm($files, host.id)'
|
||||
ngf-select = 'importVm($files, host.id)'
|
||||
)
|
||||
i.fa.fa-upload.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(style="margin-bottom: 0.5em")
|
||||
button.btn(
|
||||
tooltip="Host console"
|
||||
tooltip-placement="top"
|
||||
type="button"
|
||||
style="width: 90%"
|
||||
xo-sref="consoles_view({id: controller.id})"
|
||||
@@ -269,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)
|
||||
@@ -281,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
|
||||
@@ -306,72 +329,115 @@
|
||||
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
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-network
|
||||
| Interfaces
|
||||
| PIFs
|
||||
.panel-body
|
||||
table.table.table-hover
|
||||
th.col-md-1 Device
|
||||
th.col-md-1 Network
|
||||
th.col-md-1 VLAN
|
||||
th.col-md-1 Address
|
||||
th.col-md-2 Address
|
||||
th.col-md-2 MAC
|
||||
th.col-md-1 MTU
|
||||
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}}
|
||||
span.label.label-primary(ng-if="PIF.management") XAPI
|
||||
td
|
||||
span(ng-if="PIF.vlan > -1")
|
||||
| {{PIF.vlan}}
|
||||
span(ng-if="PIF.vlan == -1")
|
||||
| -
|
||||
td.oneliner {{PIF.IP}} ({{PIF.mode}})
|
||||
td.oneliner {{PIF.MAC}}
|
||||
td {{PIF.MTU}}
|
||||
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)")
|
||||
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)")
|
||||
i.fa.fa-link.fa-lg
|
||||
a(tooltip="Remove this interface", xo-click="removePIF(PIF.id)")
|
||||
i.fa.fa-trash-o.fa-lg
|
||||
th.col-md-2 Link status
|
||||
tbody(ng-repeat="PIF in host.$PIFs | resolve | orderBy:natural('device') track by PIF.id")
|
||||
tr
|
||||
td
|
||||
| {{PIF.device}}
|
||||
span.label.label-primary(ng-if="PIF.management") XAPI
|
||||
|
|
||||
span.label.label-primary(ng-if="PIF.physical") Phys.
|
||||
td {{(PIF.$network | resolve).name_label}}
|
||||
td
|
||||
span(ng-if="PIF.vlan > -1")
|
||||
| {{PIF.vlan}}
|
||||
span(ng-if="PIF.vlan == -1")
|
||||
| -
|
||||
td {{PIF.ip}} ({{PIF.mode}})
|
||||
span.quick-buttons(ng-click="configuringIp = !configuringIp" tooltip="Configure IP")
|
||||
i.fa.fa-edit.fa-lg
|
||||
td {{PIF.mac}}
|
||||
td {{PIF.mtu}}
|
||||
td
|
||||
span.label.label-default(ng-if="!PIF.attached") Disconnected
|
||||
span.label.label-success(ng-if="PIF.attached") Connected
|
||||
span.pull-right.btn-group.quick-buttons(ng-if="canAdmin()")
|
||||
i.fa.fa-unlink.fa-lg.text-danger(ng-if="PIF.disallowUnplug" tooltip="Disconnection not allowed")
|
||||
i.fa.fa-unlink.fa-lg.text-danger(ng-if="!PIF.disallowUnplug && PIF.management" tooltip="Management PIF")
|
||||
|
|
||||
i.fa.fa-trash.fa-lg.text-danger(ng-if="PIF.disallowUnplug" tooltip="Disconnection not allowed")
|
||||
i.fa.fa-trash.fa-lg.text-danger(ng-if="!PIF.disallowUnplug && PIF.management" tooltip="Management PIF")
|
||||
a(tooltip="Disconnect this interface" xo-click="disconnectPIF(PIF.id)", ng-if = 'PIF.attached && !PIF.disallowUnplug && !PIF.management')
|
||||
i.fa.fa-unlink.fa-lg
|
||||
a(tooltip="Connect this interface" xo-click="connectPIF(PIF.id)", ng-if = '!PIF.attached')
|
||||
i.fa.fa-link.fa-lg
|
||||
a(tooltip="Remove this interface", xo-click="removePIF(PIF.id)", ng-if = '!PIF.physical && !PIF.disallowUnplug && !PIF.management')
|
||||
i.fa.fa-trash-o.fa-lg
|
||||
tr(ng-if="configuringIp")
|
||||
td(colspan="7")
|
||||
form.form-inline#configureIpForm(name = 'configureIpForm', ng-submit = 'addIp(PIF, newIp, newNetmask, newDns, newGateway, ipMethod); $parent.configuringIp=false', ng-init='ipMethod="Static"')
|
||||
label
|
||||
.form-group
|
||||
input(type="radio" name="ipMethod" ng-model="ipMethod" value="Static" checked)
|
||||
.form-group
|
||||
|
|
||||
span(for = 'newIp') IP:
|
||||
input#newIp.form-control(type = 'text', ng-model = 'newIp', placeholder = '{{PIF.ip}}', required, ng-disabled="ipMethod !== 'Static'")
|
||||
|
|
||||
span(for = 'newNetmask') Netmask:
|
||||
input#newNetmask.form-control(type = 'text', ng-model = 'newNetmask', placeholder = '{{PIF.netmask}}', required, ng-disabled="ipMethod !== 'Static'")
|
||||
|
|
||||
span(for = 'newDns') DNS:
|
||||
input#newDns.form-control(type = 'text', ng-model = 'newDns', placeholder = '{{PIF.dns}}', ng-disabled="ipMethod !== 'Static'")
|
||||
|
|
||||
span(for = 'newGateway') Gateway:
|
||||
input#newGateway.form-control(type = 'text', ng-model = 'newGateway', placeholder = '{{PIF.gateway}}', ng-disabled="ipMethod !== 'Static'")
|
||||
|
|
||||
br
|
||||
label
|
||||
.form-group
|
||||
input(type="radio" name="ipMethod" ng-model="ipMethod" value="DHCP")
|
||||
.form-group
|
||||
| Use DHCP
|
||||
br
|
||||
label
|
||||
.form-group
|
||||
input(type="radio" name="ipMethod" ng-model="ipMethod" value="None")
|
||||
.form-group
|
||||
| Remove IP
|
||||
br
|
||||
button.btn.btn-primary(type = 'submit') OK
|
||||
.text-right
|
||||
button.btn(type="button", ng-class = '{"btn-success": creatingNetwork, "btn-primary": !creatingNetwork}', ng-click="creatingNetwork = !creatingNetwork")
|
||||
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')
|
||||
option(value = '', disabled) None
|
||||
select.form-control(ng-model = 'newNetworkPIF', ng-change = 'updateMTU(newNetworkPIF)', ng-options='(PIF | resolve).device for PIF in physicalPifs()')
|
||||
option(value = '') None
|
||||
|
|
||||
.form-group
|
||||
label.control-label(for = 'newNetworkName') Name
|
||||
@@ -392,7 +458,7 @@
|
||||
.form-group
|
||||
button.btn.btn-primary(type = 'submit')
|
||||
i.fa.fa-plus-square
|
||||
| Create
|
||||
| Create
|
||||
span(ng-if = 'createNetworkWaiting')
|
||||
|
|
||||
i.xo-icon-loading-sm
|
||||
@@ -422,7 +488,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)")
|
||||
@@ -434,7 +500,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
|
||||
@@ -445,7 +511,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')
|
||||
@@ -458,6 +524,8 @@
|
||||
| Patches
|
||||
span.quick-edit(ng-click="listMissingPatches(host.id)", tooltip="Check for updates")
|
||||
i.fa.fa-question-circle
|
||||
span.quick-edit(ng-click="installAllPatches(host.id)", tooltip="Install all the missing patches", style="margin-right:5px", ng-if = 'canAdmin()')
|
||||
i.fa.fa-download
|
||||
.panel-body
|
||||
table.table.table-hover(ng-if="poolPatches || updates")
|
||||
th.col-sm-2 Name
|
||||
@@ -465,29 +533,32 @@
|
||||
th.col-sm-3 Applied/Released date
|
||||
th.col-sm-1 Size
|
||||
th.col-sm-1 Status
|
||||
tr(ng-repeat="patch in updates")
|
||||
tr(
|
||||
ng-repeat="patch in updates"
|
||||
ng-if="!isPoolPatch(patch)"
|
||||
)
|
||||
td.oneliner {{patch.name}}
|
||||
td.oneliner
|
||||
a(href="{{patch.documentationUrl}}", target="_blank") {{patch.description}}
|
||||
td.oneliner {{patch.date | date:"medium"}}
|
||||
td -
|
||||
td
|
||||
span(ng-click="installPatch(host.id, patch.uuid)", tooltip="Click to install the patch on this host")
|
||||
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
|
||||
tr(ng-repeat="patch in poolPatches | map | slice:(5*(currentPatchPage-1)):(5*currentPatchPage)")
|
||||
td.oneliner {{patch.name_label}}
|
||||
td.oneliner {{patch.name_description}}
|
||||
span.label.label-danger(ng-if = '!canAdmin()') Missing
|
||||
tr(ng-repeat="patch in poolPatches | map | orderBy:'-name'| slice:(5*(currentPatchPage-1)):(5*currentPatchPage)")
|
||||
td.oneliner {{patch.name}}
|
||||
td.oneliner {{patch.description}}
|
||||
//- TODO: use a proper function for patch date, like poolPatchToHostPatch
|
||||
td.oneliner {{((patch.$host_patches[0]) | resolve).time*1e3 | date:"medium"}}
|
||||
td {{patch.size | bytesToSize}}
|
||||
td
|
||||
span(ng-if="isPoolPatchApplied(patch)")
|
||||
span.label.label-success Applied
|
||||
span(ng-click="installPatch(host.id, patch.id)", ng-if="!isPoolPatchApplied(patch)", tooltip="Click to apply the patch on this host")
|
||||
span.label.label-warning Not applied
|
||||
//- span.pull-right.btn-group.quick-buttons
|
||||
//- a(xo-click="deletePatch(patch.id)")
|
||||
//- i.fa.fa-trash-o.fa-lg(tooltip="Remove this patch")
|
||||
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=">>")
|
||||
|
||||
@@ -518,3 +589,31 @@
|
||||
td.oneliner {{pgpu.device}}
|
||||
.center(ng-if = '(host.$PGPUs | resolve).length > 5')
|
||||
pagination(boundary-links="true", total-items="(host.$PGPUs | resolve).length", ng-model="$parent.currentGPUPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
|
||||
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-asterisk
|
||||
| Misc
|
||||
.panel-body(style="overflow:hidden")
|
||||
.row(ng-repeat="(key, value) in host.bios_strings")
|
||||
label.control-label.col-sm-4
|
||||
| {{key}}:
|
||||
.col-sm-8 {{value}}
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-book
|
||||
| License
|
||||
.panel-body
|
||||
.row
|
||||
label.control-label.col-sm-3
|
||||
| Server:
|
||||
.col-sm-9 {{host.license_server.address}}:{{host.license_server.port}}
|
||||
br
|
||||
.row(ng-repeat="key in hostParams | slice:(10*(currentLicensePage-1)):(10*currentLicensePage) track by key")
|
||||
label.control-label.col-sm-7
|
||||
| {{key}}:
|
||||
.col-sm-5 {{host.license_params[key]}}
|
||||
.center
|
||||
pagination(boundary-links="true", total-items="hostParams.length", ng-model="currentLicensePage", items-per-page="10", max-size="10", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
|
||||
|
||||
|
||||
@@ -1,5 +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'
|
||||
|
||||
@@ -9,7 +11,8 @@ import view from './view'
|
||||
|
||||
export default angular.module('xoWebApp.list', [
|
||||
uiRouter,
|
||||
xoApi
|
||||
xoApi,
|
||||
xoTag
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('list', {
|
||||
@@ -18,7 +21,20 @@ export default angular.module('xoWebApp.list', [
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('ListCtrl', function (xoApi) {
|
||||
.controller('ListCtrl', function (xo, xoApi, $state, $scope, $rootScope) {
|
||||
const user = xoApi.user
|
||||
|
||||
$scope.createButton = user.permission !== 'admin'
|
||||
|
||||
if (user.permission !== 'admin') {
|
||||
$scope.createButton = false
|
||||
xo.resourceSet.getAll()
|
||||
.then(sets => {
|
||||
$scope.resourceSets = sets
|
||||
$scope.createButton = sets.length > 0
|
||||
})
|
||||
}
|
||||
|
||||
this.hosts = xoApi.getView('host')
|
||||
this.pools = xoApi.getView('pool')
|
||||
this.SRs = xoApi.getView('SR')
|
||||
@@ -27,7 +43,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,48 @@
|
||||
.sub-bar
|
||||
.grid(style="margin-left:1em")
|
||||
.btn-group.dropdown.col-sm-1(dropdown)
|
||||
a.btn.navbar-btn.dropdown-toggle.filter(dropdown-toggle)
|
||||
| Types
|
||||
i.fa.fa-caret-down
|
||||
ul.dropdown-menu.inverse(role="menu" style="color:white")
|
||||
li(
|
||||
ng-repeat = "type in ['VM', 'SR', 'Host', 'Pool']"
|
||||
ng-click='onClick(type)'
|
||||
)
|
||||
|  
|
||||
label(ng-click)
|
||||
i.fa.fa-square-o(ng-if='!options[type.toLowerCase()]')
|
||||
i.fa.fa-check-square-o(ng-if='options[type.toLowerCase()]')
|
||||
| {{type}}
|
||||
.btn-group.dropdown.col-sm-1(dropdown)
|
||||
a.btn.navbar-btn.dropdown-toggle.filter(dropdown-toggle)
|
||||
| States
|
||||
i.fa.fa-caret-down
|
||||
ul.dropdown-menu.inverse(role="menu" style="color:white")
|
||||
li(
|
||||
ng-repeat = "state in ['Running', 'Halted', 'Disconnected']"
|
||||
ng-click='onClick(state)'
|
||||
)
|
||||
|  
|
||||
label(ng-click)
|
||||
i.fa.fa-square-o(ng-if='!options[state.toLowerCase()]')
|
||||
i.fa.fa-check-square-o(ng-if='options[state.toLowerCase()]')
|
||||
| {{state}}
|
||||
.btn-group.col-sm-1.col-sm-offset-9(ng-if='createButton')
|
||||
a.btn.navbar-btn.filter(xo-sref='VMs_new()' tooltip = 'Create VM')
|
||||
i.fa.fa-desktop.text-success  
|
||||
i.fa.fa-plus.text-success
|
||||
//- TODO: print a message when no entries.
|
||||
|
||||
//- FIXME: Ugly trick to force the results to be under the sub bar.
|
||||
div(style="margin-top: 50px; visibility: hidden; height: 0") .
|
||||
|
||||
//- If it's a (named) pool.
|
||||
.grid.flat-object(ng-repeat="pool in list.pools.all | xoHideUnauthorized | filter: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
|
||||
@@ -37,14 +78,17 @@
|
||||
.grid
|
||||
.grid-cell
|
||||
.grid-cell.flat-cell-tag
|
||||
i.fa.fa-tag
|
||||
span(ng-repeat="tag in pool.tags")
|
||||
span.label.label-primary {{tag}}
|
||||
i.fa.fa-tag
|
||||
xo-tag(object = 'pool')
|
||||
//- /Tags.
|
||||
//- /Properties & tags.
|
||||
//- /Pool.
|
||||
//- If it's a host.
|
||||
.grid.flat-object(ng-repeat="host in list.hosts.all | xoHideUnauthorized | filter: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}}")
|
||||
@@ -71,14 +115,17 @@
|
||||
.grid
|
||||
.grid-cell
|
||||
.grid-cell.flat-cell-tag
|
||||
i.fa.fa-tag
|
||||
span(ng-repeat="tag in host.tags")
|
||||
span.label.label-primary {{tag}}
|
||||
i.fa.fa-tag
|
||||
xo-tag(object = 'host')
|
||||
//- /Tags.
|
||||
//- /Properties & tags.
|
||||
//- /Host.
|
||||
//- If it's a VM.
|
||||
.grid.flat-object(ng-repeat="VM in list.VMs.all | xoHideUnauthorized | filter: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}}")
|
||||
@@ -96,30 +143,33 @@
|
||||
| {{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}}
|
||||
| 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")
|
||||
| Resident on:
|
||||
a(ui-sref="hosts_view({id: container.id})") {{container.name_label}}
|
||||
small(ng-if="pool.name_label && canView(pool.id)")
|
||||
| (
|
||||
a(ui-sref="pools_view({id: pool.id})") {{pool.name_label}}
|
||||
a(ui-sref="pools_view({id: pool.id})") {{pool.name_label}}
|
||||
| )
|
||||
//- /Properties.
|
||||
//- Tags.
|
||||
.grid
|
||||
.grid-cell
|
||||
.grid-cell.flat-cell-tag
|
||||
i.fa.fa-tag
|
||||
span(ng-repeat="tag in VM.tags")
|
||||
span.label.label-primary {{tag}}
|
||||
i.fa.fa-tag
|
||||
xo-tag(object = 'VM')
|
||||
//- /Tags.
|
||||
//- /Properties & tags.
|
||||
//- /VM.
|
||||
//- If it's a SR.
|
||||
.grid.flat-object(ng-repeat="SR in list.SRs.all | xoHideUnauthorized | filter: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
|
||||
@@ -132,25 +182,24 @@
|
||||
.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)")
|
||||
div(ng-if="'pool' === container.type")
|
||||
strong
|
||||
| Shared on
|
||||
a(ui-sref="pools_view({id: container.id})") {{container.name_label}}
|
||||
| Shared on:
|
||||
a(ui-sref="pools_view({id: container.id})") {{container.name_label}}
|
||||
div(ng-if="'host' === container.type")
|
||||
| Connected to
|
||||
a(ui-sref="hosts_view({id: container.id})") {{container.name_label}}
|
||||
| Connected to:
|
||||
a(ui-sref="hosts_view({id: container.id})") {{container.name_label}}
|
||||
//- /Properties.
|
||||
//- Tags.
|
||||
.grid
|
||||
.grid-cell
|
||||
.grid-cell.flat-cell-tag
|
||||
i.fa.fa-tag
|
||||
span(ng-repeat="tag in SR.tags")
|
||||
span.label.label-primary {{tag}}
|
||||
i.fa.fa-tag
|
||||
xo-tag(object = 'SR')
|
||||
//- /Tags.
|
||||
//- /Properties & tags.
|
||||
//- /SR.
|
||||
|
||||
82
app/modules/migrate-vm/index.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import angular from 'angular'
|
||||
import forEach from 'lodash.foreach'
|
||||
import find from 'lodash.find'
|
||||
|
||||
import uiBootstrap from 'angular-ui-bootstrap'
|
||||
|
||||
import xoServices from 'xo-services'
|
||||
|
||||
import view from './view'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default angular.module('xoWebApp.migrateVm', [
|
||||
uiBootstrap,
|
||||
xoServices
|
||||
])
|
||||
.controller('MigrateVmCtrl', function (
|
||||
$scope,
|
||||
$modalInstance,
|
||||
xoApi,
|
||||
VDIs,
|
||||
srsOnTargetPool,
|
||||
srsOnTargetHost,
|
||||
VIFs,
|
||||
networks,
|
||||
defaults,
|
||||
intraPoolMigration
|
||||
) {
|
||||
$scope.VDIs = VDIs
|
||||
$scope.SRs = srsOnTargetPool.concat(srsOnTargetHost)
|
||||
$scope.VIFs = VIFs
|
||||
$scope.networks = networks
|
||||
$scope.intraPoolMigration = intraPoolMigration
|
||||
|
||||
$scope.selected = {}
|
||||
|
||||
$scope.selected.migrationNetwork = defaults.network
|
||||
|
||||
$scope.selected.vdi = {}
|
||||
forEach($scope.VDIs, (vdi) => {
|
||||
$scope.selected.vdi[vdi.id] = defaults.sr
|
||||
})
|
||||
|
||||
if (!intraPoolMigration) {
|
||||
$scope.selected.vif = {}
|
||||
forEach($scope.VIFs, (vif) => {
|
||||
const network = find($scope.networks, (network) => network.name_label === xoApi.get(vif.$network).name_label)
|
||||
$scope.selected.vif[vif.id] = network
|
||||
// Try to find a target network with the same name
|
||||
? network.id
|
||||
// Otherwise the default network
|
||||
: defaults.network
|
||||
})
|
||||
}
|
||||
|
||||
$scope.migrate = function () {
|
||||
$modalInstance.close($scope.selected)
|
||||
}
|
||||
})
|
||||
.service('migrateVmModal', function ($modal, xo, xoApi) {
|
||||
return function (state, id, hostId, VDIs, srsOnTargetPool, srsOnTargetHost, VIFs, networks, defaults, intraPoolMigration) {
|
||||
return $modal.open({
|
||||
controller: 'MigrateVmCtrl',
|
||||
template: view,
|
||||
resolve: {
|
||||
VDIs: () => VDIs,
|
||||
srsOnTargetPool: () => srsOnTargetPool,
|
||||
srsOnTargetHost: () => srsOnTargetHost,
|
||||
VIFs: () => VIFs,
|
||||
networks: () => networks,
|
||||
defaults: () => defaults,
|
||||
intraPoolMigration: () => intraPoolMigration
|
||||
}
|
||||
}).result.then(function (selected) {
|
||||
let isAdmin = xoApi.user && (xoApi.user.permission === 'admin')
|
||||
state.go(isAdmin ? 'tree' : 'list')
|
||||
return xo.vm.migrate(id, hostId, selected.vdi, intraPoolMigration ? undefined : selected.vif, selected.migrationNetwork)
|
||||
})
|
||||
}
|
||||
})
|
||||
// A module exports its name.
|
||||
.name
|
||||
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 target network:
|
||||
table.table(ng-if="!intraPoolMigration")
|
||||
tr
|
||||
th.col-sm-5 VIF (network)
|
||||
th.col-sm-7 Target Network
|
||||
tbody
|
||||
tr(ng-repeat="vif in (VIFs | orderBy:'device')")
|
||||
td {{vif.MAC}} ({{(vif.$network | resolve).name_label}})
|
||||
td
|
||||
table.table
|
||||
tbody
|
||||
tr
|
||||
select.form-control(
|
||||
ng-options="network.id as network.name_label for network in networks"
|
||||
ng-model="selected.vif[vif.id]"
|
||||
)
|
||||
.modal-footer
|
||||
button.btn.btn-primary(type="submit")
|
||||
| Migrate
|
||||
button.btn.btn-warning(type="button", ng-click="$dismiss()")
|
||||
| Cancel
|
||||
@@ -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.
|
||||
@@ -52,7 +52,7 @@ nav.navbar.navbar-inverse.navbar-fixed-top(role = 'navigation')
|
||||
i.xo-icon-task
|
||||
ul.dropdown-menu.inverse
|
||||
li.task-menu(
|
||||
ng-repeat="task in navbar.tasks.all | orderBy:natural('name_label') track by task.id"
|
||||
ng-repeat="task in navbar.tasks.all | map | orderBy:natural('name_label') track by task.id"
|
||||
)
|
||||
a(
|
||||
ui-sref="hosts_view({id: task.$host})"
|
||||
@@ -91,16 +91,21 @@ nav.navbar.navbar-inverse.navbar-fixed-top(role = 'navigation')
|
||||
)
|
||||
a(ui-sref="dashboard.index")
|
||||
i.fa.fa-dashboard
|
||||
| Dashoard
|
||||
//- li.disabled(ui-sref-active="active")
|
||||
//- a(ui-sref="graph")
|
||||
//- i.fa.fa-sitemap
|
||||
//- | Graphs view
|
||||
| Dashboard
|
||||
li.divider
|
||||
li(ng-class = '{ disabled: navbar.user.permission !== "admin" }')
|
||||
a(ui-sref = 'scheduler.index')
|
||||
i.fa.fa-clock-o
|
||||
| Scheduler
|
||||
a(ui-sref = 'self.index')
|
||||
i.fa.fa-cloud
|
||||
| Self Service
|
||||
li.divider
|
||||
li(ng-class = '{ disabled: navbar.user.permission !== "admin" }')
|
||||
a(ui-sref = 'backup.index')
|
||||
i.fa.fa-archive
|
||||
| Backup
|
||||
li(ng-class = '{ disabled: navbar.user.permission !== "admin" }')
|
||||
a(ui-sref = 'taskscheduler.index')
|
||||
i.fa.fa-cogs
|
||||
| Job Manager
|
||||
li.divider
|
||||
li(
|
||||
ui-sref-active = 'active'
|
||||
@@ -126,7 +131,7 @@ nav.navbar.navbar-inverse.navbar-fixed-top(role = 'navigation')
|
||||
i.fa.fa-exclamation-triangle.text-danger(ng-if = 'navbar.updater.state == "error"', tooltip = 'Can\'t fetch update information')
|
||||
|
||||
li
|
||||
a(ui-sref="settings.users", tooltip="{{navbar.user.email}}")
|
||||
a(ng-if = '!navbar.user.provider', ui-sref="{{navbar.user.provider ? 'settings.users' : 'settings.user'}}", tooltip="{{navbar.user.email}}")
|
||||
i.fa.fa-user
|
||||
span.hidden-sm {{navbar.user.email}}
|
||||
li
|
||||
|
||||
@@ -19,16 +19,14 @@ export default angular.module('xoWebApp.newSr', [
|
||||
})
|
||||
})
|
||||
.controller('NewSrCtrl', function ($scope, $state, $stateParams, xo, xoApi, notify, modal, bytesToSizeFilter) {
|
||||
|
||||
this.reset = function (data = {}) {
|
||||
|
||||
this.data = {}
|
||||
delete this.lockCreation
|
||||
this.lock = !(
|
||||
(data.srType === 'Local') &&
|
||||
(data.srPath && data.srPath.path)
|
||||
(data.srPath && data.srPath.path) ||
|
||||
data.srType === 'SMB'
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
this.resetLists = function () {
|
||||
@@ -48,7 +46,6 @@ export default angular.module('xoWebApp.newSr', [
|
||||
* Loads NFS paths and iScsi iqn`s
|
||||
*/
|
||||
this.populateSettings = function (type, server, auth, user, password) {
|
||||
|
||||
this.reset()
|
||||
this.loading = true
|
||||
|
||||
@@ -82,7 +79,6 @@ export default angular.module('xoWebApp.newSr', [
|
||||
|
||||
xoApi.call('sr.probeIscsiIqns', params)
|
||||
.then(response => {
|
||||
|
||||
if (response.length > 0) {
|
||||
this.data.iqns = response
|
||||
} else {
|
||||
@@ -91,7 +87,6 @@ export default angular.module('xoWebApp.newSr', [
|
||||
message: 'No IQNs found'
|
||||
})
|
||||
}
|
||||
|
||||
})
|
||||
.catch(error => notify.warning({
|
||||
title: 'iSCSI Detection',
|
||||
@@ -101,7 +96,6 @@ export default angular.module('xoWebApp.newSr', [
|
||||
} else {
|
||||
this.loading = false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -124,7 +118,6 @@ export default angular.module('xoWebApp.newSr', [
|
||||
|
||||
xoApi.call('sr.probeIscsiLuns', params)
|
||||
.then(response => {
|
||||
|
||||
forEach(response, item => {
|
||||
item.display = 'LUN ' + item.id + ': ' +
|
||||
item.serial + ' ' + bytesToSizeFilter(item.size) +
|
||||
@@ -220,24 +213,46 @@ export default angular.module('xoWebApp.newSr', [
|
||||
})
|
||||
break
|
||||
|
||||
case 'NFS_ISO':
|
||||
case 'Local':
|
||||
operationToPromise = xoApi.call('sr.createIso', {
|
||||
host: this.container.id,
|
||||
nameLabel: data.srName,
|
||||
nameDescription: data.srDesc,
|
||||
type: 'local',
|
||||
path: data.srPath.path
|
||||
})
|
||||
break
|
||||
|
||||
case 'NFS_ISO':
|
||||
let server = this._parseAddress(data.srServer || '')
|
||||
|
||||
let path = (
|
||||
data.srType === 'NFS_ISO' ?
|
||||
server.host + ':' :
|
||||
''
|
||||
const path = (
|
||||
data.srType === 'NFS_ISO'
|
||||
? server.host + ':'
|
||||
: ''
|
||||
) + data.srPath.path
|
||||
|
||||
operationToPromise = xoApi.call('sr.createIso', {
|
||||
host: this.container.id,
|
||||
nameLabel: data.srName,
|
||||
nameDescription: data.srDesc,
|
||||
type: 'nfs',
|
||||
path
|
||||
})
|
||||
break
|
||||
|
||||
case 'SMB':
|
||||
operationToPromise = xoApi.call('sr.createIso', {
|
||||
host: this.container.id,
|
||||
nameLabel: data.srName,
|
||||
nameDescription: data.srDesc,
|
||||
type: 'smb',
|
||||
path: data.srServer,
|
||||
user: data.user,
|
||||
password: data.password
|
||||
})
|
||||
break
|
||||
|
||||
default:
|
||||
operationToPromise = Bluebird.reject({message: 'Unhanled SR Type'})
|
||||
break
|
||||
@@ -334,13 +349,11 @@ export default angular.module('xoWebApp.newSr', [
|
||||
|
||||
xoApi.call('sr.probeIscsiExists', params)
|
||||
.then(response => {
|
||||
|
||||
if (response.length > 0) {
|
||||
this.data.scsiList = this._processSRList(response)
|
||||
}
|
||||
|
||||
this.lock = !Boolean(data.srIScsiId)
|
||||
|
||||
})
|
||||
.catch(error => {
|
||||
notify.error({
|
||||
@@ -362,13 +375,11 @@ export default angular.module('xoWebApp.newSr', [
|
||||
serverPath: data.srPath.path
|
||||
})
|
||||
.then(response => {
|
||||
|
||||
if (response.length > 0) {
|
||||
this.data.scsiList = this._processSRList(response)
|
||||
this.data.nfsList = this._processSRList(response)
|
||||
}
|
||||
|
||||
this.lock = !Boolean(data.srPath.path)
|
||||
|
||||
})
|
||||
.catch(error => {
|
||||
notify.error({
|
||||
|
||||
@@ -27,6 +27,7 @@ form.form-horizontal(name = 'srForm' ng-submit="newSr.createSR(formData)")
|
||||
optgroup(label="ISO SR")
|
||||
option(value="Local") Local
|
||||
option(value="NFS_ISO") NFS ISO
|
||||
option(value="SMB") SMB
|
||||
.form-group(ng-class = '{"has-error": newSr.data.error.name}')
|
||||
label.col-sm-3.control-label Name
|
||||
.col-sm-9
|
||||
@@ -55,6 +56,23 @@ form.form-horizontal(name = 'srForm' ng-submit="newSr.createSR(formData)")
|
||||
button.btn.btn-default(type = 'button', ng-click = 'newSr.populateSettings(formData.srType, formData.srServer, formData.srAuth, formData.srChapUser, formData.srChapPassword)')
|
||||
i.fa.fa-search
|
||||
|
||||
//- For SMB
|
||||
.form-group(ng-if='formData.srType === "SMB"')
|
||||
label.col-sm-3.control-label
|
||||
| Server
|
||||
.col-sm-9
|
||||
input.form-control(type="text", name='srServer', ng-model='formData.srServer', placeholder='\\\\\\\\<server>\\\\<path>' required)
|
||||
.form-group(ng-if='formData.srType === "SMB"')
|
||||
label.col-sm-3.control-label
|
||||
| User
|
||||
.col-sm-9
|
||||
input.form-control(type="text", name='user', ng-model='formData.user', required)
|
||||
.form-group(ng-if='formData.srType === "SMB"')
|
||||
label.col-sm-3.control-label
|
||||
| Password
|
||||
.col-sm-9
|
||||
input.form-control(type="password", name='password', ng-model='formData.password', required)
|
||||
|
||||
//- For Local LVM
|
||||
.form-group(ng-if = 'formData.srType === "lvm"')
|
||||
label.col-sm-3.control-label Device
|
||||
|
||||
@@ -2,6 +2,9 @@ angular = require 'angular'
|
||||
cloneDeep = require 'lodash.clonedeep'
|
||||
filter = require 'lodash.filter'
|
||||
forEach = require 'lodash.foreach'
|
||||
trim = require 'lodash.trim'
|
||||
includes = require 'lodash.includes'
|
||||
forEach = require 'lodash.foreach'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
@@ -11,7 +14,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 +22,97 @@ module.exports = angular.module 'xoWebApp.newVm', [
|
||||
bytesToSizeFilter, sizeToBytesFilter
|
||||
notify
|
||||
) ->
|
||||
{get} = xoApi
|
||||
$scope.min = Math.min
|
||||
|
||||
user = xoApi.user
|
||||
$scope.isAdmin = user.permission == 'admin'
|
||||
|
||||
userGroups = user.groups
|
||||
|
||||
if !$scope.isAdmin
|
||||
$scope.resourceSets = []
|
||||
$scope.userResourceSets = []
|
||||
$scope.resourceSet = ''
|
||||
xo.resourceSet.getAll()
|
||||
.then (sets) ->
|
||||
$scope.resourceSets = sets
|
||||
$scope.resourceSet = $scope.resourceSets[0]
|
||||
$scope.updateResourceSet($scope.resourceSet)
|
||||
|
||||
$scope.updateResourceSet = (resourceSet) ->
|
||||
$scope.resourceSet = resourceSet
|
||||
$scope.template = ''
|
||||
$scope.templates = []
|
||||
$scope.writable_SRs = []
|
||||
$scope.ISO_SRs = []
|
||||
srs = []
|
||||
$scope.resourceSetNetworks = []
|
||||
$scope.pools = []
|
||||
forEach $scope.resourceSet.objects, (id) ->
|
||||
obj = xoApi.get id
|
||||
if obj.type is 'VM-template'
|
||||
$scope.templates.push(obj)
|
||||
else if obj.type is 'SR'
|
||||
srs.push(obj)
|
||||
else if obj.type is 'network'
|
||||
$scope.resourceSetNetworks.push(obj)
|
||||
$scope.writable_SRs = filter(srs, (sr) => sr.content_type isnt 'iso')
|
||||
$scope.ISO_SRs = filter(srs, (sr) => sr.content_type is 'iso')
|
||||
|
||||
$scope.multipleVmsActive = false
|
||||
$scope.vmsNames = ['VM1', 'VM2']
|
||||
$scope.numberOfVms = 1
|
||||
$scope.newNumberOfVms = 2
|
||||
|
||||
$scope.checkNumberOfVms = ->
|
||||
if $scope.newNumberOfVms && Number.isInteger($scope.newNumberOfVms)
|
||||
$scope.newNumberOfVms = $scope.numberOfVms = Math.min(100,Math.max(2,$scope.newNumberOfVms))
|
||||
else
|
||||
$scope.newNumberOfVms = $scope.numberOfVms = 2
|
||||
|
||||
$scope.refreshNames = ->
|
||||
$scope.defaultName = 'VM'
|
||||
$scope.defaultName = $scope.name_label if $scope.name_label
|
||||
forEach($scope.vmsNames, (name, index) ->
|
||||
$scope.vmsNames[index] = $scope.defaultName + (index+1)
|
||||
)
|
||||
|
||||
$scope.toggleBootAfterCreate = ->
|
||||
$scope.bootAfterCreate = false if $scope.multipleVmsActive
|
||||
|
||||
$scope.configDriveActive = false
|
||||
existingDisks = {}
|
||||
$scope.saveChange = (position, propertyName, value) ->
|
||||
if not existingDisks[position]?
|
||||
existingDisks[position] = {}
|
||||
existingDisks[position][propertyName] = value
|
||||
$scope.updateVdiSize = (position) ->
|
||||
$scope.saveChange(position, 'size', bytesToSizeFilter(sizeToBytesFilter($scope.existingDiskSizeValues[position] + ' ' + $scope.existingDiskSizeUnits[position])))
|
||||
$scope.updateTotalDiskBytes()
|
||||
$scope.initExistingValues = (template) ->
|
||||
$scope.name_label = template.name_label
|
||||
sizes = {}
|
||||
$scope.templateVBDs = []
|
||||
$scope.existingDiskSizeValues = {}
|
||||
$scope.existingDiskSizeUnits = {}
|
||||
forEach xoApi.get(template.$VBDs), (VBD) ->
|
||||
if VBD.is_cd_drive or not VBD.VDI? or not (VDI = xoApi.get(VBD.VDI))?
|
||||
return
|
||||
$scope.templateVBDs.push(VBD)
|
||||
|
||||
sizes[VBD.position] = bytesToSizeFilter VDI.size
|
||||
$scope.existingDiskSizeValues[VBD.position] = +sizes[VBD.position].split(' ')[0]
|
||||
$scope.existingDiskSizeUnits[VBD.position] = sizes[VBD.position].split(' ')[1]
|
||||
$scope.VIFs.length = 0
|
||||
if template.VIFs.length
|
||||
forEach xoApi.get(template.VIFs), (VIF) ->
|
||||
network = xoApi.get(VIF.$network)
|
||||
$scope.addVIF(network)
|
||||
return
|
||||
else $scope.addVIF()
|
||||
$scope.memory = template.memory.size
|
||||
|
||||
{get} = xoApi
|
||||
removeItems = do ->
|
||||
splice = Array::splice.call.bind Array::splice
|
||||
(array, index, n) -> splice array, index, n ? 1
|
||||
@@ -35,6 +127,8 @@ module.exports = angular.module 'xoWebApp.newVm', [
|
||||
|
||||
pool = default_SR = null
|
||||
host = null
|
||||
poolHosts = null
|
||||
hostsSrs = null
|
||||
do (
|
||||
networks = xoApi.getIndex('networksByPool')
|
||||
srsByContainer = xoApi.getIndex('srsByContainer')
|
||||
@@ -49,12 +143,58 @@ module.exports = angular.module 'xoWebApp.newVm', [
|
||||
get: () => pool && networks[pool.id]
|
||||
}
|
||||
})
|
||||
updateSrs = () =>
|
||||
|
||||
$scope.updateSrs = () =>
|
||||
srs = []
|
||||
$scope.selectedLocalSrs = {}
|
||||
Object.defineProperty($scope.selectedLocalSrs, "size", {
|
||||
value: 0,
|
||||
writable: true,
|
||||
enumerable: false
|
||||
})
|
||||
$scope.forcedHost = undefined
|
||||
poolSrs and forEach(poolSrs, (sr) => srs.push(sr))
|
||||
hostSrs and forEach(hostSrs, (sr) => srs.push(sr))
|
||||
$scope.writable_SRs = filter(srs, (sr) => sr.content_type isnt 'iso')
|
||||
$scope.ISO_SRs = filter(srs, (sr) => sr.content_type is 'iso')
|
||||
poolHosts and forEach(poolHosts, (host) =>
|
||||
forEach(hostsSrs[host.id], (sr) ->
|
||||
srs.push(sr))
|
||||
)
|
||||
if pool or $scope.resourceSet
|
||||
selectedSrs = []
|
||||
forEach($scope.templateVBDs, (vbd) ->
|
||||
selectedSrs.push(xoApi.get(vbd.VDI).$SR)
|
||||
)
|
||||
forEach($scope.VDIs, (vdi) ->
|
||||
selectedSrs.push(vdi.SR)
|
||||
)
|
||||
if $scope.resourceSet
|
||||
forEach(selectedSrs, (sr) ->
|
||||
sr = xoApi.get sr
|
||||
container = xoApi.get sr.$container
|
||||
if container.type is 'host'
|
||||
if not $scope.selectedLocalSrs[sr.$container]
|
||||
$scope.selectedLocalSrs[sr.$container] = []
|
||||
$scope.selectedLocalSrs.size++
|
||||
$scope.forcedHost = sr.$container
|
||||
if not includes($scope.selectedLocalSrs[sr.$container], sr.id)
|
||||
$scope.selectedLocalSrs[sr.$container].push(sr.id)
|
||||
)
|
||||
else
|
||||
forEach(poolHosts, (host) ->
|
||||
forEach(hostsSrs[host.id], (sr) ->
|
||||
if includes(selectedSrs, sr.id)
|
||||
if not $scope.selectedLocalSrs[host.id]
|
||||
$scope.selectedLocalSrs[host.id] = []
|
||||
$scope.selectedLocalSrs.size++
|
||||
$scope.forcedHost = host.id
|
||||
if not includes($scope.selectedLocalSrs[host.id], sr.id)
|
||||
$scope.selectedLocalSrs[host.id].push(sr.id)
|
||||
)
|
||||
)
|
||||
if not $scope.resourceSet
|
||||
$scope.writable_SRs = filter(srs, (sr) => sr.content_type isnt 'iso')
|
||||
$scope.ISO_SRs = filter(srs, (sr) => sr.content_type is 'iso')
|
||||
|
||||
updateTemplates = () =>
|
||||
templates = []
|
||||
poolTemplates and forEach(poolTemplates, (template) => templates.push(template))
|
||||
@@ -64,13 +204,13 @@ module.exports = angular.module 'xoWebApp.newVm', [
|
||||
() => pool and srsByContainer[pool.id],
|
||||
(srs) =>
|
||||
poolSrs = srs
|
||||
updateSrs()
|
||||
$scope.updateSrs()
|
||||
)
|
||||
$scope.$watchCollection(
|
||||
() => host and srsByContainer[host.id],
|
||||
(srs) =>
|
||||
hostSrs = srs
|
||||
updateSrs()
|
||||
$scope.updateSrs()
|
||||
)
|
||||
$scope.$watchCollection(
|
||||
() => pool and vmTemplatesByContainer[pool.id],
|
||||
@@ -96,35 +236,68 @@ module.exports = angular.module 'xoWebApp.newVm', [
|
||||
if container.type is 'host'
|
||||
host = container
|
||||
pool = (get container.$poolId) ? {}
|
||||
poolHosts = []
|
||||
hostsSrs = {}
|
||||
else
|
||||
host = {}
|
||||
pool = container
|
||||
objects = filter(xoApi.all, (obj) -> obj.type is 'host' or obj.type is 'SR')
|
||||
poolHosts = filter(objects, (obj) -> obj.type is 'host' and obj.$poolId is pool.id)
|
||||
hostsSrs = {}
|
||||
forEach(poolHosts, (host) ->
|
||||
hostsSrs[host.id] = filter(objects, (obj) -> obj.type is 'SR' and obj.$container is host.id)
|
||||
)
|
||||
|
||||
default_SR = get pool.default_SR
|
||||
default_SR = if default_SR then default_SR.id else ''
|
||||
)
|
||||
|
||||
$scope.availableMethods = {}
|
||||
$scope.CPUs = ''
|
||||
$scope.pv_args = ''
|
||||
$scope.installation_cdrom = ''
|
||||
$scope.installation_method = ''
|
||||
$scope.installation_network = ''
|
||||
$scope.memory = ''
|
||||
$scope.name_description = ''
|
||||
$scope.memory = null
|
||||
$scope.memoryValue = null
|
||||
$scope.units = ['MiB', 'GiB', 'TiB']
|
||||
$scope.memoryUnit = $scope.units[1]
|
||||
$scope.name_description = 'Created by XO'
|
||||
$scope.name_label = ''
|
||||
$scope.template = ''
|
||||
$scope.totalDiskBytes = 0
|
||||
$scope.firstSR = ''
|
||||
$scope.VDIs = []
|
||||
$scope.VIFs = []
|
||||
$scope.isDiskTemplate = false
|
||||
$scope.cloudConfigSshKey = ''
|
||||
$scope.cloudConfigCustom = '#cloud-config\n#hostname: myhostname\n#ssh_authorized_keys:\n# - ssh-rsa <myKey>\n#packages:\n# - htop\n'
|
||||
$scope.cloudConfigLoading = false
|
||||
$scope.cloudConfigError = false
|
||||
$scope.bootAfterCreate = true
|
||||
|
||||
$scope.updateMemory = ->
|
||||
if $scope.memoryValue
|
||||
$scope.memory = sizeToBytesFilter $scope.memoryValue + ' ' + $scope.memoryUnit
|
||||
else
|
||||
$scope.memory = $scope.template.memory.size
|
||||
$scope.updateMemoryUnit = (memoryUnit) ->
|
||||
$scope.memoryUnit = memoryUnit
|
||||
$scope.updateMemory()
|
||||
|
||||
$scope.updateTotalDiskBytes = ->
|
||||
$scope.totalDiskBytes = 0
|
||||
forEach $scope.existingDiskSizeValues, (value, key) ->
|
||||
$scope.totalDiskBytes += sizeToBytesFilter value + ' ' + $scope.existingDiskSizeUnits[key]
|
||||
forEach $scope.VDIs, (VDI) ->
|
||||
$scope.totalDiskBytes += (sizeToBytesFilter VDI.sizeValue + ' ' + VDI.sizeUnit) || 0
|
||||
|
||||
$scope.addVIF = do ->
|
||||
id = 0
|
||||
->
|
||||
(network = '') ->
|
||||
$scope.VIFs.push {
|
||||
id: id++
|
||||
network: ''
|
||||
network
|
||||
}
|
||||
$scope.addVIF()
|
||||
|
||||
$scope.removeVIF = (index) -> removeItems $scope.VIFs, index
|
||||
|
||||
@@ -134,22 +307,39 @@ module.exports = angular.module 'xoWebApp.newVm', [
|
||||
newIndex = index + direction
|
||||
[VDIs[index], VDIs[newIndex]] = [VDIs[newIndex], VDIs[index]]
|
||||
|
||||
$scope.removeVDI = (index) -> removeItems $scope.VDIs, index
|
||||
$scope.removeVDI = (index) ->
|
||||
removeItems $scope.VDIs, index
|
||||
$scope.updateTotalDiskBytes()
|
||||
|
||||
VDI_id = 0
|
||||
$scope.addVDI = ->
|
||||
$scope.VDIs.push {
|
||||
id: VDI_id++
|
||||
bootable: false
|
||||
name_label: $scope.name_label + '_disk' + (VDI_id - 1)
|
||||
name_description: 'Created by XO'
|
||||
size: ''
|
||||
SR: default_SR
|
||||
sizeValue: ''
|
||||
sizeUnit: $scope.units[1]
|
||||
SR: default_SR || $scope.writable_SRs[0] && $scope.writable_SRs[0].id
|
||||
type: 'system'
|
||||
}
|
||||
$scope.updateSrs()
|
||||
|
||||
$scope.$watch('name_label', (newName, oldName) ->
|
||||
forEach $scope.VDIs, (vdi, index) ->
|
||||
if vdi.name_label is oldName + '_disk' + index
|
||||
vdi.name_label = newName + '_disk' + index
|
||||
)
|
||||
|
||||
# When the selected template changes, updates other variables.
|
||||
$scope.$watch 'template', (template) ->
|
||||
return unless template
|
||||
# After each template change, initialize coreOsCloudConfig to empty
|
||||
$scope.coreOsCloudConfig = ''
|
||||
|
||||
# Fetch the PV args
|
||||
$scope.pv_args = template.PV_args
|
||||
{install_methods} = template.template_info
|
||||
availableMethods = $scope.availableMethods = Object.create null
|
||||
for method in install_methods
|
||||
@@ -159,56 +349,132 @@ 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
|
||||
forEach VDIs, (vdi, index) ->
|
||||
vdi.name_label = $scope.name_label + '_disk' + index
|
||||
vdi.name_description = 'Created by XO'
|
||||
|
||||
# if the template has no config disk
|
||||
# nor it's Other install media (specific case)
|
||||
# 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.SR or= default_SR || $scope.writable_SRs[0] && $scope.writable_SRs[0].id
|
||||
VDI.size = bytesToSizeFilter VDI.size
|
||||
VDI.SR or= default_SR
|
||||
VDI.sizeValue = if VDI.size then +VDI.size.split(' ')[0] else null
|
||||
VDI.sizeUnit = VDI.size.split(' ')[1]
|
||||
# if the template is labeled CoreOS
|
||||
# we'll use config drive setup
|
||||
if template.name_label == 'CoreOS'
|
||||
return xo.vm.getCloudInitConfig template.id
|
||||
.then (result) ->
|
||||
$scope.coreOsCloudConfig = result
|
||||
$scope.updateTotalDiskBytes()
|
||||
$scope.updateSrs()
|
||||
|
||||
$scope.createVM = ->
|
||||
$scope.uploadCloudConfig = (file) ->
|
||||
$scope.cloudConfigError = false
|
||||
return unless file
|
||||
reader = new FileReader()
|
||||
reader.onerror = () ->
|
||||
$scope.cloudConfigError = true
|
||||
reader.onload = (event) ->
|
||||
$scope.cloudConfigCustom = event.target.result
|
||||
reader.onloadend = (event) ->
|
||||
$scope.cloudConfigLoading = false
|
||||
if file.size > 2e6
|
||||
reader.onerror()
|
||||
return
|
||||
$scope.cloudConfigLoading = true
|
||||
reader.readAsText(file)
|
||||
|
||||
$scope.createVMs = ->
|
||||
if !$scope.multipleVmsActive
|
||||
$scope.createVM($scope.name_label)
|
||||
return
|
||||
forEach($scope.vmsNames, (name) ->
|
||||
$scope.createVM(name)
|
||||
)
|
||||
# Send the client on the tree view
|
||||
$state.go 'index'
|
||||
|
||||
xenDefaultWeight = 256
|
||||
$scope.weightMap = {
|
||||
'Quarter (1/4)': xenDefaultWeight / 4,
|
||||
'Half (1/2)': xenDefaultWeight / 2,
|
||||
'Normal': xenDefaultWeight,
|
||||
'Double (x2)': xenDefaultWeight * 2
|
||||
}
|
||||
|
||||
$scope.createVM = (name_label) ->
|
||||
{
|
||||
resourceSet
|
||||
CPUs
|
||||
cpuWeight
|
||||
pv_args
|
||||
installation_cdrom
|
||||
installation_method
|
||||
installation_network
|
||||
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
|
||||
# Default VDI name and description
|
||||
VDI.name_label = VDI.name_label || name_label + '_disk' + index
|
||||
VDI.name_description = VDI.name_description || 'Created by XO'
|
||||
|
||||
# TODO: handles invalid values.
|
||||
|
||||
forEach existingDisks, (disk, index) ->
|
||||
if disk.name_label is ''
|
||||
delete disk.name_label
|
||||
if disk.name_description is ''
|
||||
delete disk.name_description
|
||||
|
||||
# Does not edit the displayed data directly.
|
||||
VIFs = cloneDeep VIFs
|
||||
for VIF in VIFs
|
||||
# Removes the dummy identifier used for AngularJS.
|
||||
delete VIF.id
|
||||
|
||||
# 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'
|
||||
@@ -223,17 +489,23 @@ module.exports = angular.module 'xoWebApp.newVm', [
|
||||
method: matches[1].toLowerCase()
|
||||
repository: installation_network
|
||||
}
|
||||
else if installation_method is 'pxe'
|
||||
installation = {
|
||||
method: 'network'
|
||||
repository: 'pxe'
|
||||
}
|
||||
else
|
||||
installation = undefined
|
||||
|
||||
data = {
|
||||
resourceSet: resourceSet && resourceSet.id
|
||||
installation
|
||||
pv_args
|
||||
name_label
|
||||
template: template.id
|
||||
VDIs
|
||||
VIFs
|
||||
existingDisks
|
||||
}
|
||||
|
||||
# TODO:
|
||||
# - disable the form during creation
|
||||
# - indicate the progress of the operation
|
||||
@@ -241,32 +513,74 @@ 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 memory
|
||||
memory = sizeToBytesFilter memory
|
||||
# FIXME: handles invalid entries.
|
||||
data.memory = memory
|
||||
if pv_args
|
||||
data.pv_args = pv_args
|
||||
|
||||
xoApi.call('vm.set', data).then -> id
|
||||
.then (id) ->
|
||||
$state.go 'VMs_view', { id }
|
||||
if memoryValue
|
||||
# FIXME: handles invalid entries.
|
||||
data.memory = memoryValue + ' ' + memoryUnit
|
||||
return xo.vm.set(data)
|
||||
.then () ->
|
||||
# If a CloudConfig drive needs to be created
|
||||
if $scope.coreOsCloudConfig
|
||||
# Use the CoreOS specific Cloud Config creation
|
||||
return xo.vm.createCloudInitConfigDrive(id, $scope.firstSR, $scope.coreOsCloudConfig, true).then ->
|
||||
return xo.docker.register(id)
|
||||
if $scope.configDriveActive
|
||||
# User creation is less universal...
|
||||
# $scope.cloudContent = '#cloud-config\nhostname: ' + name_label + '\nusers:\n - name: olivier\n sudo: ALL=(ALL) NOPASSWD:ALL\n groups: sudo\n shell: /bin/bash\n ssh_authorized_keys:\n - ' + $scope.cloudConfigSshKey + '\n'
|
||||
# So keep it basic for now: hostname and ssh key
|
||||
hostname = name_label
|
||||
# Remove leading and trailing spaces.
|
||||
.replace(/^\s+|\s+$/g, '')
|
||||
# Replace spaces with '-'.
|
||||
.replace(/\s+/g, '-')
|
||||
if $scope.configDriveMethod == 'standard'
|
||||
$scope.cloudContent = '#cloud-config\nhostname: ' + hostname + '\nssh_authorized_keys:\n - ' + $scope.cloudConfigSshKey + '\n'
|
||||
else
|
||||
$scope.cloudContent = $scope.cloudConfigCustom
|
||||
# The first SR for a template with an existing disk
|
||||
$scope.firstSR = (get (get template.$VBDs[0]).VDI).$SR
|
||||
# Use the generic CloudConfig creation
|
||||
return xo.vm.createCloudInitConfigDrive(id, $scope.firstSR, $scope.cloudContent).then ->
|
||||
# Boot directly on disk
|
||||
return xo.vm.setBootOrder({vm: id, order: 'c'})
|
||||
.then () ->
|
||||
if $scope.bootAfterCreate
|
||||
xo.vm.start id
|
||||
if !$scope.multipleVmsActive
|
||||
if resourceSet
|
||||
# FIXME When using self service, ACL permissions are not updated fast enough to access VM view right after creation
|
||||
$state.go 'index'
|
||||
else
|
||||
# Send the client on the VM view
|
||||
$state.go 'VMs_view', { id }
|
||||
.catch (error) ->
|
||||
notify.error {
|
||||
title: 'VM creation'
|
||||
message: 'The creation failed'
|
||||
}
|
||||
$scope.creatingVM = false
|
||||
|
||||
console.log error
|
||||
|
||||
|
||||
@@ -1,14 +1,29 @@
|
||||
.grid
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
.col-sm-4
|
||||
p.page-title.col-sm-4
|
||||
i.xo-icon-vm
|
||||
| Create VM on
|
||||
a(ng-if="'pool' === container.type", ui-sref="pools_view({id: container.id})")
|
||||
| {{container.name_label}}
|
||||
a(ng-if="'host' === container.type", ui-sref="hosts_view({id: container.id})")
|
||||
| {{container.name_label}}
|
||||
| Create VM
|
||||
span(ng-if='isAdmin') on
|
||||
a(ng-if="'pool' === container.type", ui-sref="pools_view({id: container.id})")
|
||||
| {{container.name_label}}
|
||||
a(ng-if="'host' === container.type", ui-sref="hosts_view({id: container.id})")
|
||||
| {{container.name_label}}
|
||||
form.col-sm-4.form-horizontal(ng-if="resourceSet")
|
||||
.form-group(style="margin-top:4px;margin-bottom:4px;")
|
||||
label.col-sm-5.control-label Resource set:
|
||||
.col-sm-7(ng-if='resourceSets.length > 1')
|
||||
select.form-control(
|
||||
style="max-width:20em;"
|
||||
ng-model="$parent.resourceSet"
|
||||
ng-options="resourceSet.name for resourceSet in resourceSets | orderBy:natural('name') track by resourceSet.id"
|
||||
ng-change="updateResourceSet(resourceSet)"
|
||||
required=""
|
||||
)
|
||||
.col-sm-7.form-control-static(ng-if='resourceSets.length === 1')
|
||||
| {{ resourceSet.name }}
|
||||
//- Add server panel
|
||||
form.form-horizontal(ng-submit="createVM()")
|
||||
form.form-horizontal(ng-submit="createVMs()")
|
||||
.grid
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
@@ -18,11 +33,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 | map | orderBy:natural('name_label') track by template.id", required="", ng-change = 'initExistingValues(template)')
|
||||
.form-group
|
||||
label.col-sm-3.control-label Name
|
||||
.col-sm-9
|
||||
input.form-control(type="text", placeholder="Name of your new VM", 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 +51,86 @@ 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' ng-disabled='resourceSet')
|
||||
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" step="0.01" placeholder="{{ template.memory.size | bytesConvert:memoryUnit:'iB' }}" ng-model="memoryValue" ng-change="updateMemory()")
|
||||
span.input-group-btn.dropdown(dropdown)
|
||||
button.btn.btn-default.dropdown-toggle(type = 'button' dropdown-toggle)
|
||||
| {{ memoryUnit }}
|
||||
span.caret
|
||||
ul.dropdown-menu(role = 'menu' style='min-width:0')
|
||||
li(ng-repeat="memoryUnit in units")
|
||||
a(ng-click="updateMemoryUnit(memoryUnit)") {{ memoryUnit }}
|
||||
.grid
|
||||
//- Cloud Config Panel, only for templates with existing disks
|
||||
.panel.panel-default(ng-if="isDiskTemplate")
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-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', ng-click = '$parent.configDriveMethod = "standard"')
|
||||
i.fa(ng-class = '{"fa-toggle-on": $parent.configDriveActive, "text-success": $parent.configDriveActive, "fa-toggle-off": !$parent.configDriveActive}', style = 'font-size: 1.5em;')
|
||||
.panel-body
|
||||
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-group
|
||||
span.input-group-addon
|
||||
input(
|
||||
type = 'radio'
|
||||
name = 'configDriveMethod'
|
||||
ng-model = '$parent.configDriveMethod'
|
||||
value = 'standard'
|
||||
)
|
||||
input.form-control(
|
||||
type="text"
|
||||
placeholder="ssh-rsa AAAA.... you@machine"
|
||||
ng-model="$parent.cloudConfigSshKey"
|
||||
ng-disabled = '$parent.configDriveMethod !== "standard"'
|
||||
name="cloudConfigSshKey"
|
||||
required
|
||||
)
|
||||
.form-group
|
||||
label.col-sm-3.control-label
|
||||
a(href='http://cloudinit.readthedocs.org/en/latest/topics/examples.html', target='_blank') Custom config
|
||||
.col-sm-9
|
||||
.input-group
|
||||
span.input-group-addon
|
||||
input(
|
||||
type = 'radio'
|
||||
name = 'configDriveMethod'
|
||||
ng-model = '$parent.configDriveMethod'
|
||||
value = 'custom'
|
||||
)
|
||||
textarea.form-control(
|
||||
rows='4'
|
||||
style="resize: none;"
|
||||
ng-model="$parent.cloudConfigCustom"
|
||||
ng-disabled = '$parent.configDriveMethod !== "custom"'
|
||||
name="cloudConfigCustom"
|
||||
required
|
||||
)
|
||||
br
|
||||
button.btn.btn-default(
|
||||
type = 'button'
|
||||
ng-disabled = '$parent.configDriveMethod !== "custom"'
|
||||
ngf-select = '$parent.uploadCloudConfig($files[0]); fileName = $files[0].name'
|
||||
) Select file
|
||||
span(style='max-width: 1em' ng-init='fileName = "None"')
|
||||
| Selected file : {{ fileName }}
|
||||
i.fa.fa-spinner.fa-spin(ng-show = 'cloudConfigLoading')
|
||||
i.fa.fa-exclamation-triangle.text-danger(ng-show = 'cloudConfigError' tooltip = 'Error while loading file')
|
||||
//- Install panel, only if an installation method is needed
|
||||
.panel.panel-default(ng-if="!isDiskTemplate")
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-download
|
||||
| Install settings
|
||||
@@ -70,18 +145,18 @@ 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")
|
||||
.form-group(
|
||||
ng-show = '(availableMethods.http || availableMethods.ftp || availableMethods.nfs)'
|
||||
)
|
||||
label.col-sm-3.control-label Network
|
||||
.col-sm-9
|
||||
.input-group
|
||||
@@ -91,18 +166,21 @@ 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")
|
||||
|
||||
//- <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>
|
||||
.form-group(ng-show = 'template.virtualizationMode === "hvm"')
|
||||
label.col-sm-3.control-label PXE
|
||||
.col-sm-9
|
||||
input(
|
||||
type = 'radio'
|
||||
name = 'installation_method'
|
||||
ng-model = '$parent.installation_method'
|
||||
value = 'pxe'
|
||||
)
|
||||
.form-group(ng-show='template.virtualizationMode === "pv"')
|
||||
label.col-sm-3.control-label PV Args
|
||||
.col-sm-9
|
||||
input.form-control(type="text", placeholder="{{template.PV_args}}", ng-model="$parent.pv_args")
|
||||
|
||||
//- Interface panel
|
||||
.panel.panel-default
|
||||
@@ -118,10 +196,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 + " (" + (network.$pool | resolve).name_label + ")") for network in (networks || resourceSetNetworks) | map | orderBy:natural("name_label") track by network.id'
|
||||
ng-model = 'VIF.network'
|
||||
required
|
||||
)
|
||||
@@ -138,8 +216,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(); checkNumberOfVms()', ng-change="toggleBootAfterCreate()")
|
||||
i.fa(ng-class = '{"fa-toggle-on": multipleVmsActive, "text-success": multipleVmsActive, "fa-toggle-off": !multipleVmsActive}', style = 'font-size: 1.5em;')
|
||||
.panel-body(ng-if="multipleVmsActive")
|
||||
.form-group
|
||||
label.col-md-offset-4.col-sm-2.control-label
|
||||
i.fa.fa-refresh(ng-click = "$parent.refreshNames()" tooltip="Set VMs to default names")
|
||||
| Number of VMs
|
||||
.col-sm-2
|
||||
.input-group(style="width:10em")
|
||||
input.form-control(type="number" ng-model="$parent.newNumberOfVms")
|
||||
span.input-group-btn
|
||||
button.btn.btn-default(type="button" ng-click="checkNumberOfVms()")
|
||||
i.fa.fa-arrow-right
|
||||
.col-sm-6(ng-repeat="offset in [0, 1]")
|
||||
.form-group(
|
||||
ng-repeat = "n in [].constructor($parent.numberOfVms).slice(0, ($parent.numberOfVms+1)/2) track by $index"
|
||||
ng-if = "2*$index+offset < $parent.numberOfVms"
|
||||
)
|
||||
label.col-sm-2.control-label VM \#{{ 2*$index+1+offset }}
|
||||
.col-sm-10
|
||||
input.form-control(
|
||||
type = "text"
|
||||
required
|
||||
placeholder = "Name of new VM \#{{ (2*$index+1+offset) }}"
|
||||
ng-model = "$parent.vmsNames[2*$index+offset]"
|
||||
ng-init = "$parent.vmsNames[2*$index+offset] = $parent.defaultName + (2*$index+1+offset)"
|
||||
)
|
||||
|
||||
//- Disk panel
|
||||
.grid(ng-if="!isDiskTemplate")
|
||||
.grid
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-disk
|
||||
@@ -151,16 +277,62 @@ form.form-horizontal(ng-submit="createVM()")
|
||||
th.col-md-1 Bootable?
|
||||
th.col-md-2 Size
|
||||
th.col-md-2 Name
|
||||
th.col-md-4 Description
|
||||
th.col-md-1  
|
||||
th.col-md-3 Description
|
||||
th.col-md-2  
|
||||
//- Buttons
|
||||
tr(ng-repeat="VBD in (templateVBDs | orderBy:'position') track by VBD.id", ng-if="isDiskTemplate")
|
||||
td
|
||||
select.form-control(ng-model="(VBD.VDI | resolve).$SR", ng-options="SR.id as (SR.name_label + ' on ' + (SR.$container | resolve).name_label + ' (' + (SR.size - SR.physical_usage | bytesToSize) + ' free)') for SR in (writable_SRs | orderBy:natural('name_label'))", ng-change = 'saveChange(VBD.position, "$SR", (VBD.VDI | resolve).$SR); updateSrs()', required)
|
||||
option(value = '') Please select
|
||||
td.text-center
|
||||
i.fa.fa-check(ng-if = 'VBD.bootable')
|
||||
td(style = "overflow: visible")
|
||||
.input-group
|
||||
input.form-control(
|
||||
type='number'
|
||||
min="0"
|
||||
step="0.01"
|
||||
placeholder="Size of this virtual disk"
|
||||
ng-model="existingDiskSizeValues[VBD.position]"
|
||||
ng-readonly='!configDriveActive'
|
||||
ng-change = 'updateVdiSize(VBD.position)'
|
||||
required
|
||||
)
|
||||
span.input-group-btn.dropdown(dropdown)
|
||||
button.btn.btn-default.dropdown-toggle(type = 'button' dropdown-toggle ng-disabled='!configDriveActive')
|
||||
| {{ existingDiskSizeUnits[VBD.position] }} 
|
||||
span.caret
|
||||
ul.dropdown-menu(role="menu" style="min-width:0")
|
||||
li(ng-repeat="unit in $parent.units")
|
||||
a(ng-click="existingDiskSizeUnits[VBD.position] = unit; updateVdiSize(VBD.position)") {{ unit }}
|
||||
td
|
||||
input.form-control(type="text", placeholder="Name of this virtual disk", ng-model="(VBD.VDI | resolve).name_label", ng-change = 'saveChange(VBD.position, "name_label", (VBD.VDI | resolve).name_label)')
|
||||
td
|
||||
input.form-control(type="text", placeholder="Description of this virtual disk", ng-model="(VBD.VDI | resolve).name_description", ng-change = 'saveChange(VBD.position, "name_description", (VBD.VDI | resolve).name_description)')
|
||||
td
|
||||
tr(ng-repeat="VDI in VDIs track by VDI.id")
|
||||
td
|
||||
select.form-control(ng-model="VDI.SR", ng-options="SR.id as (SR.name_label + ' (' + (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 + ' on ' + (SR.$container | resolve).name_label + ' (' + (SR.size - SR.physical_usage | bytesToSize) + ' free)') for SR in (writable_SRs | orderBy:natural('name_label'))" ng-change="updateSrs()")
|
||||
td.text-center
|
||||
input(type="checkbox", ng-model="VDI.bootable")
|
||||
td
|
||||
input.form-control(type="text", ng-model="VDI.size", required="")
|
||||
td(style = "overflow: visible")
|
||||
.input-group
|
||||
input.form-control(
|
||||
type='number'
|
||||
min="0"
|
||||
step="0.01"
|
||||
placeholder="Size of this virtual disk"
|
||||
ng-model="VDI.sizeValue"
|
||||
ng-change = 'updateTotalDiskBytes()'
|
||||
required
|
||||
)
|
||||
span.input-group-btn.dropdown(dropdown)
|
||||
button.btn.btn-default.dropdown-toggle(type = 'button' dropdown-toggle)
|
||||
| {{ VDI.sizeUnit }} 
|
||||
span.caret
|
||||
ul.dropdown-menu(role="menu" style="min-width:0")
|
||||
li(ng-repeat="unit in units")
|
||||
a(ng-click="VDI.sizeUnit = unit; updateTotalDiskBytes()") {{ unit }}
|
||||
td
|
||||
input.form-control(type="text", placeholder="Name of this virtual disk", ng-model="VDI.name_label")
|
||||
td
|
||||
@@ -190,7 +362,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
|
||||
@@ -202,19 +376,121 @@ 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="memoryValue") {{memory | bytesToSize}}
|
||||
span(ng-if="!memoryValue") {{template.memory.size | bytesToSize}}
|
||||
|
|
||||
i.xo-icon-memory
|
||||
.grid-cell
|
||||
//- p.stat-name Disks
|
||||
p.center.big(tooltip="Disks")
|
||||
| {{(VDIs.length) || (template.$VBDs.length) || 0}}x
|
||||
| {{(VDIs.length) || (templateVBDs.length) || 0}}x
|
||||
i.xo-icon-disk
|
||||
.grid-cell
|
||||
//- p.stat-name Interfaces
|
||||
p.center.big(tooltip="Network interfaces")
|
||||
| {{(VIFs.length) || (template.VIFs.length) || 0}}x
|
||||
i.xo-icon-network
|
||||
.grid(ng-if="template && resourceSet")
|
||||
.grid-cell
|
||||
.center-block(ng-if="resourceSet.limits.cpus" style="width:60%")
|
||||
.progress
|
||||
.progress-bar(
|
||||
role="progressbar",
|
||||
aria-valuemin="0",
|
||||
aria-valuenow="{{resourceSet.limits.cpus.total - resourceSet.limits.cpus.available}}",
|
||||
aria-valuemax="{{resourceSet.limits.cpus.total}}",
|
||||
style="width: {{[resourceSet.limits.cpus.total - resourceSet.limits.cpus.available, resourceSet.limits.cpus.total] | percentage}}",
|
||||
tooltip="{{resourceSet.limits.cpus.total - resourceSet.limits.cpus.available}} vCPUs already in use"
|
||||
)
|
||||
.progress-bar(
|
||||
ng-class = '{"progress-bar-success": numberOfVms * (CPUs || template.CPUs.number || 0) <= resourceSet.limits.cpus.available, "progress-bar-danger": numberOfVms * (CPUs || template.CPUs.number || 0) > resourceSet.limits.cpus.available}'
|
||||
role="progressbar",
|
||||
aria-valuemin="0",
|
||||
aria-valuenow="{{numberOfVms * (CPUs || template.CPUs.number || 0)}}",
|
||||
aria-valuemax="{{resourceSet.limits.cpus.total}}",
|
||||
style="width: {{[min(numberOfVms * (CPUs || template.CPUs.number || 0), resourceSet.limits.cpus.available), resourceSet.limits.cpus.total] | percentage}}",
|
||||
tooltip="{{numberOfVms * (CPUs || template.CPUs.number || 0)}} vCPUs / {{resourceSet.limits.cpus.available}} remaining"
|
||||
)
|
||||
.grid-cell
|
||||
.center-block(ng-if="resourceSet.limits.memory" style="width:60%")
|
||||
.progress
|
||||
.progress-bar(
|
||||
role="progressbar",
|
||||
aria-valuemin="0",
|
||||
aria-valuenow="{{resourceSet.limits.memory.total - resourceSet.limits.memory.available}}",
|
||||
aria-valuemax="{{resourceSet.limits.memory.total}}",
|
||||
style="width: {{[resourceSet.limits.memory.total - resourceSet.limits.memory.available, resourceSet.limits.memory.total] | percentage}}",
|
||||
tooltip="{{resourceSet.limits.memory.total - resourceSet.limits.memory.available | bytesToSize}} already in use"
|
||||
)
|
||||
.progress-bar(
|
||||
ng-class = '{"progress-bar-success": numberOfVms * memory <= resourceSet.limits.memory.available, "progress-bar-danger": numberOfVms * memory > resourceSet.limits.memory.available}'
|
||||
role="progressbar",
|
||||
aria-valuemin="0",
|
||||
aria-valuenow="{{numberOfVms * memory}}",
|
||||
aria-valuemax="{{resourceSet.limits.memory.total}}",
|
||||
style="width: {{[min(numberOfVms * memory, resourceSet.limits.memory.available), resourceSet.limits.memory.total] | percentage}}",
|
||||
tooltip="{{numberOfVms * memory | bytesToSize}} / {{resourceSet.limits.memory.available | bytesToSize}} remaining"
|
||||
)
|
||||
.grid-cell
|
||||
.center-block(ng-if="resourceSet.limits.disk" style="width:60%")
|
||||
.progress
|
||||
.progress-bar(
|
||||
role="progressbar",
|
||||
aria-valuemin="0",
|
||||
aria-valuenow="{{resourceSet.limits.disk.total - resourceSet.limits.disk.available}}",
|
||||
aria-valuemax="{{resourceSet.limits.disk.total}}",
|
||||
style="width: {{[resourceSet.limits.disk.total - resourceSet.limits.disk.available, resourceSet.limits.disk.total] | percentage}}",
|
||||
tooltip="{{resourceSet.limits.disk.total - resourceSet.limits.disk.available | bytesToSize}} already in use"
|
||||
)
|
||||
.progress-bar(
|
||||
ng-class = '{"progress-bar-success": numberOfVms * totalDiskBytes <= resourceSet.limits.disk.available, "progress-bar-danger": numberOfVms * totalDiskBytes > resourceSet.limits.disk.available}'
|
||||
role="progressbar",
|
||||
aria-valuemin="0",
|
||||
aria-valuenow="{{numberOfVms * totalDiskBytes}}",
|
||||
aria-valuemax="{{resourceSet.limits.disk.total}}",
|
||||
style="width: {{[min(numberOfVms * totalDiskBytes, resourceSet.limits.disk.available), resourceSet.limits.disk.total] | percentage}}",
|
||||
tooltip="{{numberOfVms * totalDiskBytes | bytesToSize}} / {{resourceSet.limits.disk.available | bytesToSize}} remaining"
|
||||
)
|
||||
.grid-cell
|
||||
p.center(ng-if="isDiskTemplate")
|
||||
| Cloud configuration is
|
||||
strong.text-success(ng-if = 'configDriveActive') enabled.
|
||||
strong.text-danger(ng-if = '!configDriveActive') disabled.
|
||||
p.center(ng-if="selectedLocalSrs.size === 1")
|
||||
label
|
||||
| The VM will be created on {{(forcedHost | resolve).name_label}} since
|
||||
span(ng-repeat="sr in selectedLocalSrs[forcedHost]") {{(sr | resolve).name_label}}
|
||||
span(ng-if="$index < selectedLocalSrs[forcedHost].length - 2") ,
|
||||
span(ng-if="$index === selectedLocalSrs[forcedHost].length - 2") and
|
||||
span(ng-if="selectedLocalSrs[forcedHost].length > 1") are
|
||||
span(ng-if="selectedLocalSrs[forcedHost].length === 1") is
|
||||
| on {{(forcedHost | resolve).name_label}}
|
||||
p.text-danger(ng-if="selectedLocalSrs.size > 1")
|
||||
label.control-label Incompatible disks:
|
||||
ul(ng-if="selectedLocalSrs.size > 1")
|
||||
li.text-danger(ng-repeat="(host, srs) in selectedLocalSrs")
|
||||
span(ng-repeat="sr in srs") {{(sr | resolve).name_label}}
|
||||
span(ng-if="$index < srs.length - 2") ,
|
||||
span(ng-if="$index === srs.length - 2") and
|
||||
span(ng-if="srs.length > 1") are
|
||||
span(ng-if="srs.length === 1") is
|
||||
| on {{(host | resolve).name_label}}
|
||||
p.center
|
||||
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',
|
||||
'resourceSet.limits.cpus && (CPUs || template.CPUs.number || 0) > resourceSet.limits.cpus.available',
|
||||
'resourceSet.limits.memory && memory > resourceSet.limits.memory.available',
|
||||
'resourceSet.limits.disk && totalDiskBytes > resourceSet.limits.disk.available',
|
||||
'selectedLocalSrs.size > 1'
|
||||
].join(' || ')
|
||||
)
|
||||
i.fa.fa-play(ng-if = '!creatingVM')
|
||||
i.fa.fa-circle-o-notch.fa-spin(ng-if = 'creatingVM')
|
||||
| Create VM
|
||||
|
||||
@@ -2,12 +2,15 @@ import angular from 'angular'
|
||||
import forEach from 'lodash.foreach'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
|
||||
import xoTag from 'tag'
|
||||
|
||||
import view from './view'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default angular.module('xoWebApp.pool', [
|
||||
uiRouter
|
||||
uiRouter,
|
||||
xoTag
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('pools_view', {
|
||||
@@ -16,14 +19,18 @@ export default angular.module('xoWebApp.pool', [
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('PoolCtrl', function ($scope, $stateParams, xoApi, xo, modal) {
|
||||
.controller('PoolCtrl', function ($scope, $stateParams, xoApi, xo, modal, notify) {
|
||||
{
|
||||
const {id} = $stateParams
|
||||
const hostsByPool = xoApi.getIndex('hostsByPool')
|
||||
const runningHostsByPool = xoApi.getIndex('runningHostsByPool')
|
||||
const srsByContainer = xoApi.getIndex('srsByContainer')
|
||||
const networksByPool = xoApi.getIndex('networksByPool')
|
||||
|
||||
Object.defineProperties($scope, {
|
||||
pool: {
|
||||
get: () => xoApi.get(id)
|
||||
},
|
||||
hosts: {
|
||||
get: () => hostsByPool[id]
|
||||
},
|
||||
@@ -32,12 +39,20 @@ export default angular.module('xoWebApp.pool', [
|
||||
},
|
||||
srs: {
|
||||
get: () => srsByContainer[id]
|
||||
},
|
||||
networks: {
|
||||
get: () => networksByPool[id]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
$scope.$watch(() => xoApi.get($stateParams.id), function (pool) {
|
||||
$scope.pool = pool
|
||||
$scope.$watch(() => $scope.pool && $scope.hosts, result => {
|
||||
if (result) {
|
||||
$scope.listMissingPatches()
|
||||
xo.pool.getLicenseState($scope.pool.id).then(result => {
|
||||
$scope.license = result
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
$scope.currentLogPage = 1
|
||||
@@ -72,16 +87,154 @@ 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)
|
||||
}
|
||||
|
||||
$scope.nbUpdates = {}
|
||||
$scope.totalUpdates = 0
|
||||
$scope.listMissingPatches = () => {
|
||||
forEach($scope.hosts, function (host, host_id) {
|
||||
xo.host.listMissingPatches(host_id)
|
||||
.then(result => {
|
||||
$scope.nbUpdates[host_id] = result.length
|
||||
$scope.totalUpdates += result.length
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
$scope.installAllPatches = function () {
|
||||
modal.confirm({
|
||||
title: 'Install all the missing patches',
|
||||
message: 'Are you sure you want to install all the missing patches? This could take a while...'
|
||||
}).then(() => {
|
||||
forEach($scope.hosts, function (host, host_id) {
|
||||
console.log('Installing all missing patches on host ', host_id)
|
||||
xo.host.installAllPatches(host_id)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
$scope.installHostPatches = function (hostId) {
|
||||
modal.confirm({
|
||||
title: 'Update host (' + $scope.nbUpdates[hostId] + ' patch(es))',
|
||||
message: 'Are you sure you want to install all the missing patches on this host? This could take a while...'
|
||||
}).then(() => {
|
||||
console.log('Installing all missing patches on host ', hostId)
|
||||
xo.host.installAllPatches(hostId)
|
||||
})
|
||||
}
|
||||
|
||||
$scope.canAdmin = function (id = undefined) {
|
||||
if (id === undefined) {
|
||||
id = $scope.pool && $scope.pool.id
|
||||
}
|
||||
|
||||
return id && xoApi.canInteract(id, 'administrate') || false
|
||||
}
|
||||
|
||||
$scope.connectPIF = function (id) {
|
||||
console.log(`Connect PIF ${id}`)
|
||||
|
||||
xoApi.call('pif.connect', {id: id})
|
||||
}
|
||||
|
||||
$scope.disconnectPIF = function (id) {
|
||||
console.log(`Disconnect PIF ${id}`)
|
||||
|
||||
xoApi.call('pif.disconnect', {id: id})
|
||||
}
|
||||
|
||||
$scope.removePIF = function (id) {
|
||||
console.log(`Remove PIF ${id}`)
|
||||
|
||||
xoApi.call('pif.delete', {id: id})
|
||||
}
|
||||
|
||||
$scope.deleteNetwork = function (id) {
|
||||
return modal.confirm({
|
||||
title: 'Network deletion',
|
||||
message: 'Are you sure you want to delete this network?'
|
||||
}).then(function () {
|
||||
console.log(`Delete network ${id}`)
|
||||
notify.info({
|
||||
title: 'Network deletion...',
|
||||
message: 'Deleting the network'
|
||||
})
|
||||
|
||||
xoApi.call('network.delete', {id: id})
|
||||
})
|
||||
}
|
||||
|
||||
$scope.disallowDelete = function (network) {
|
||||
let disallow = false
|
||||
forEach(network.PIFs, pif => {
|
||||
const PIF = xoApi.get(pif)
|
||||
if (PIF.disallowUnplug || PIF.management) {
|
||||
disallow = true
|
||||
return false
|
||||
}
|
||||
})
|
||||
return disallow
|
||||
}
|
||||
|
||||
$scope.createNetwork = function (name, description, pif, mtu, vlan) {
|
||||
$scope.createNetworkWaiting = true
|
||||
notify.info({
|
||||
title: 'Network creation...',
|
||||
message: 'Creating the network'
|
||||
})
|
||||
const params = {
|
||||
pool: $scope.pool.id,
|
||||
name: name
|
||||
}
|
||||
if (mtu) {
|
||||
params.mtu = mtu
|
||||
}
|
||||
if (pif) {
|
||||
params.pif = pif
|
||||
}
|
||||
if (vlan) {
|
||||
params.vlan = vlan
|
||||
}
|
||||
if (description) {
|
||||
params.description = description
|
||||
}
|
||||
return xoApi.call('network.create', params).then(function () {
|
||||
$scope.creatingNetwork = false
|
||||
$scope.createNetworkWaiting = false
|
||||
})
|
||||
}
|
||||
|
||||
$scope.physicalPifs = () => {
|
||||
const physicalPifs = []
|
||||
const host = xoApi.get($scope.pool.master)
|
||||
forEach(host.$PIFs, pif => {
|
||||
pif = xoApi.get(pif)
|
||||
if (pif.physical) {
|
||||
physicalPifs.push(pif.id)
|
||||
}
|
||||
})
|
||||
return physicalPifs
|
||||
}
|
||||
|
||||
// $scope.patchPool = ($files, id) ->
|
||||
// file = $files[0]
|
||||
// xo.pool.patch id
|
||||
// .then ({ $sendTo: url }) ->
|
||||
// return $upload.http {
|
||||
// return Upload.http {
|
||||
// method: 'POST'
|
||||
// url
|
||||
// data: file
|
||||
@@ -100,7 +253,6 @@ export default angular.module('xoWebApp.pool', [
|
||||
// notify.info
|
||||
// title: 'Upload patch'
|
||||
// message: 'Success'
|
||||
|
||||
})
|
||||
|
||||
// A module exports its name.
|
||||
|
||||
@@ -28,8 +28,7 @@
|
||||
| {{master.name_label}}
|
||||
dt Tags
|
||||
dd
|
||||
span(ng-repeat="tag in pool.tags")
|
||||
span.label.label-primary {{tag}}
|
||||
xo-tag(ng-if = 'pool', object = 'pool')
|
||||
dt(ng-if="pool.default_SR") Default SR
|
||||
dd(ng-if="pool.default_SR", ng-init="default_SR = (pool.default_SR | resolve)")
|
||||
a(ui-sref="SRs_view({id: default_SR.id})") {{default_SR.name_label}}
|
||||
@@ -69,19 +68,19 @@
|
||||
.grid-cell.text-center
|
||||
.grid
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Add SR", type="button", style="width: 90%", disabled)
|
||||
button.btn(tooltip="Add SR", tooltip-placement="top", type="button", style="width: 90%", disabled)
|
||||
i.xo-icon-sr.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Add VM", type="button", style="width: 90%", xo-sref="VMs_new({container: pool.id})")
|
||||
button.btn(tooltip="Add VM", tooltip-placement="top", type="button", style="width: 90%", xo-sref="VMs_new({container: pool.id})")
|
||||
i.xo-icon-vm.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Patch the pool", type="button", style="width: 90%", ng-file-select = "patchPool($files, pool.id)")
|
||||
button.btn(tooltip="Patch the pool", tooltip-placement="top", type="button", style="width: 90%", ngf-select = "patchPool($files, pool.id)")
|
||||
i.fa.fa-file-code-o.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Add Host", type="button", style="width: 90%")
|
||||
button.btn(tooltip="Add Host", tooltip-placement="top", type="button", style="width: 90%")
|
||||
i.xo-icon-host.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Disconnect", type="button", style="width: 90%; margin-bottom: 0.5em")
|
||||
button.btn(tooltip="Disconnect", tooltip-placement="top", type="button", style="width: 90%; margin-bottom: 0.5em")
|
||||
i.fa.fa-unlink.fa-2x.fa-fw
|
||||
|
||||
//- Hosts panel
|
||||
@@ -100,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
|
||||
@@ -115,19 +121,143 @@
|
||||
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")
|
||||
//- Networks/Interfaces panel
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-network
|
||||
| Networks
|
||||
.panel-body
|
||||
table.table.table-hover
|
||||
th.col-md-2 Name
|
||||
th.col-md-2 Description
|
||||
th.col-md-7 PIFs
|
||||
th.col-md-1
|
||||
tr(ng-repeat="network in networks track by network.id" ng-init="showPIFs = false")
|
||||
td {{network.name_label}}
|
||||
td {{network.name_description}}
|
||||
td
|
||||
a(ng-if="network.PIFs.length && !showPIFs" ng-click="$parent.showPIFs=true") show PIFs ({{network.PIFs.length}})
|
||||
a(ng-if="network.PIFs.length && showPIFs" ng-click="$parent.showPIFs=false") hide PIFs
|
||||
table.table.table-sm.table-hover(ng-if="network.PIFs.length && showPIFs")
|
||||
th.col-md-2 Device
|
||||
th.col-md-2 Host
|
||||
th.col-md-1 VLAN
|
||||
th.col-md-2 Address
|
||||
th.col-md-2 MAC
|
||||
th.col-md-2 Link status
|
||||
tr(ng-repeat="PIF in network.PIFs | resolve | orderBy:natural('($host | resolve).name_label')")
|
||||
td
|
||||
| {{PIF.device}}
|
||||
span.label.label-primary(ng-if="PIF.management") XAPI
|
||||
|
|
||||
span.label.label-primary(ng-if="PIF.physical") Phys.
|
||||
td {{(PIF.$host | resolve).name_label}}
|
||||
td
|
||||
span(ng-if="PIF.vlan > -1") {{PIF.vlan}}
|
||||
span(ng-if="PIF.vlan == -1") -
|
||||
td {{PIF.ip}} ({{PIF.mode}})
|
||||
td {{PIF.mac}}
|
||||
td
|
||||
span.label.label-default(ng-if="!PIF.attached") Disconnected
|
||||
span.label.label-success(ng-if="PIF.attached") Connected
|
||||
span.pull-right.btn-group.quick-buttons(ng-if="canAdmin()")
|
||||
i.fa.fa-unlink.fa-lg.text-danger(ng-if="PIF.disallowUnplug" tooltip="Disconnection not allowed")
|
||||
i.fa.fa-unlink.fa-lg.text-danger(ng-if="!PIF.disallowUnplug && PIF.management" tooltip="Management PIF")
|
||||
a(tooltip="Disconnect this interface" xo-click="disconnectPIF(PIF.id)", ng-if = 'PIF.attached && !PIF.disallowUnplug && !PIF.management')
|
||||
i.fa.fa-unlink.fa-lg
|
||||
a(tooltip="Connect this interface" xo-click="connectPIF(PIF.id)", ng-if = '!PIF.attached')
|
||||
i.fa.fa-link.fa-lg
|
||||
span(ng-if="!network.PIFs.length") No PIF attached to this network
|
||||
td
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
i.fa.fa-trash-o.text-danger.fa-lg(ng-if="disallowDelete(network)" tooltip="Some PIFs cannot be disconnected")
|
||||
i.fa.fa-trash-o.text-danger.fa-lg(ng-if="network.name_label === 'Host internal management network'" tooltip="Management network")
|
||||
a(tooltip="Remove network" xo-click="$parent.deleteNetwork(network.id)", ng-if = 'canAdmin() && network.name_label !== "Host internal management network" && !disallowDelete(network)')
|
||||
i.fa.fa-trash-o.fa-lg
|
||||
.text-right
|
||||
button.btn(type="button", ng-class = '{"btn-success": creatingNetwork, "btn-primary": !creatingNetwork}', ng-click="creatingNetwork = !creatingNetwork", ng-hide = '!canAdmin()', ng-disabled = '!canAdmin()')
|
||||
i.fa.fa-plus(ng-if = '!creatingNetwork')
|
||||
i.fa.fa-minus(ng-if = 'creatingNetwork')
|
||||
| Create Network
|
||||
br
|
||||
form.form-inline.text-right#createNetworkForm(ng-if = 'creatingNetwork', name = 'createNetworkForm', ng-submit = 'createNetwork(newNetworkName, newNetworkDescription, newNetworkPIF, newNetworkMTU, newNetworkVlan)')
|
||||
fieldset(ng-disabled = 'createNetworkWaiting || !canAdmin()')
|
||||
.form-group
|
||||
label(for = 'newNetworkPIF') Interface
|
||||
select.form-control(ng-model = 'newNetworkPIF', ng-change = 'updateMTU(newNetworkPIF)', ng-options='(PIF | resolve).device for PIF in physicalPifs()')
|
||||
option(value = '') None
|
||||
|
|
||||
.form-group
|
||||
label.control-label(for = 'newNetworkName') Name
|
||||
input#newNetworkName.form-control(type = 'text', ng-model = 'newNetworkName', required)
|
||||
|
|
||||
.form-group
|
||||
label.control-label(for = 'newNetworkDescription') Description
|
||||
input#newNetworkDescription.form-control(type = 'text', ng-model = 'newNetworkDescription', placeholder= 'Network created with Xen Orchestra')
|
||||
|
|
||||
.form-group
|
||||
label.control-label(for = 'newNetworkVlan') VLAN
|
||||
input#newNetworkVlan.form-control(type = 'text', ng-model = 'newNetworkVlan', placeholder = 'Defaut: no VLAN')
|
||||
|
|
||||
.form-group
|
||||
label(for = 'newNetworkMTU') MTU
|
||||
input#newNetworkMTU.form-control(type = 'text', ng-model = 'newNetworkMTU', placeholder = 'Default: 1500')
|
||||
|
|
||||
.form-group
|
||||
button.btn.btn-primary(type = 'submit')
|
||||
i.fa.fa-plus-square
|
||||
| Create
|
||||
span(ng-if = 'createNetworkWaiting')
|
||||
|
|
||||
i.xo-icon-loading-sm
|
||||
.grid-sm
|
||||
.grid-cell
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-refresh
|
||||
| Updates
|
||||
span.quick-edit(
|
||||
ng-if="totalUpdates"
|
||||
tooltip="Update all"
|
||||
ng-click="installAllPatches()"
|
||||
)
|
||||
i.fa.fa-download.fa-fw
|
||||
.panel-body
|
||||
p.center(ng-if="!totalUpdates") Everything up to date
|
||||
table.table.table-hover(ng-if="totalUpdates")
|
||||
tr
|
||||
th Host
|
||||
th Description
|
||||
th Missing patches
|
||||
th Install
|
||||
tr( ng-repeat="host in hosts" ng-if="nbUpdates[host.id]")
|
||||
td.oneliner
|
||||
| {{ host.name_label }}
|
||||
td.oneliner
|
||||
| {{ host.name_description }}
|
||||
td {{ nbUpdates[host.id] }}
|
||||
td
|
||||
button.btn.btn-success(ng-click="installHostPatches(host.id)" tooltip="Install {{ nbUpdates[host.id] }} patch(es)")
|
||||
| Update host
|
||||
//- Logs panel
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
@@ -150,3 +280,12 @@
|
||||
i.fa.fa-trash-o.fa-lg(tooltip="Remove this log entry")
|
||||
.center(ng-if = '(pool.messages | count) > 5 || currentLogPage > 1')
|
||||
pagination(boundary-links="true", total-items="pool.messages | count", ng-model="$parent.currentLogPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-book
|
||||
| License
|
||||
.panel-body
|
||||
.row(ng-repeat="(key, value) in license")
|
||||
label.control-label.col-sm-3
|
||||
| {{key}}:
|
||||
.col-sm-9 {{value}}
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
import angular from 'angular'
|
||||
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'
|
||||
|
||||
// ====================================================================
|
||||
|
||||
export default angular.module('scheduler.management', [
|
||||
uiRouter,
|
||||
uiBootstrap
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('scheduler.management', {
|
||||
url: '/management',
|
||||
controller: 'ManagementCtrl as ctrl',
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('ManagementCtrl', function ($scope, $state, $stateParams, $interval, xo, xoApi, notify, selectHighLevelFilter, filterFilter) {
|
||||
const mapJobKeyToState = {
|
||||
'rollingSnapshot': 'rollingsnapshot',
|
||||
'rollingBackup': 'backup'
|
||||
}
|
||||
|
||||
const refreshSchedules = () => {
|
||||
xo.schedule.getAll()
|
||||
.then(schedules => this.schedules = schedules)
|
||||
xo.scheduler.getScheduleTable()
|
||||
.then(table => this.scheduleTable = table)
|
||||
}
|
||||
|
||||
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 = () => {
|
||||
return refreshJobs().then(refreshSchedules)
|
||||
}
|
||||
|
||||
refresh()
|
||||
|
||||
const interval = $interval(() => {
|
||||
refresh()
|
||||
}, 5e3)
|
||||
$scope.$on('$destroy', () => {
|
||||
$interval.cancel(interval)
|
||||
})
|
||||
|
||||
this.enable = id => {
|
||||
this.working[id] = true
|
||||
return xo.scheduler.enable(id)
|
||||
.finally(() => {this.working[id] = false})
|
||||
.then(refreshSchedules)
|
||||
}
|
||||
this.disable = id => {
|
||||
this.working[id] = true
|
||||
return xo.scheduler.disable(id)
|
||||
.finally(() => {this.working[id] = false})
|
||||
.then(refreshSchedules)
|
||||
}
|
||||
this.resolveJobKey = schedule => {
|
||||
return mapJobKeyToState[this.jobs[schedule.job].key]
|
||||
}
|
||||
|
||||
this.collectionLength = col => Object.keys(col).length
|
||||
this.working = {}
|
||||
})
|
||||
|
||||
// A module exports its name.
|
||||
.name
|
||||
@@ -1,34 +0,0 @@
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.fa.fa-clock-o(style="color: #e25440;")
|
||||
| Scheduler
|
||||
.panel.panel-default
|
||||
.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
|
||||
tr
|
||||
td.text-center(ng-if = '!ctrl.schedules'): i.xo-icon-loading
|
||||
table.table
|
||||
tr
|
||||
td.text-center(ng-if = 'ctrl.schedules && !ctrl.collectionLength(ctrl.schedules)', style = 'width: 100%;') No scheduled jobs
|
||||
table.table.table-hover(ng-if = 'ctrl.schedules && ctrl.collectionLength(ctrl.schedules)')
|
||||
tr
|
||||
th ID
|
||||
th Job
|
||||
th Scheduling
|
||||
th State
|
||||
th
|
||||
tr(ng-repeat = 'schedule in ctrl.schedules | orderBy:"id":true track by schedule.id')
|
||||
td: a(ui-sref = 'scheduler.{{ctrl.resolveJobKey(schedule)}}({id: schedule.id})') {{ schedule.id }}
|
||||
td {{ ctrl.jobs[schedule.job].key }}
|
||||
td {{ 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
|
||||
@@ -1,16 +0,0 @@
|
||||
.menu-grid
|
||||
.side-menu
|
||||
ul.nav
|
||||
li
|
||||
a(ui-sref = '.management', ui-sref-active = 'active')
|
||||
i.fa.fa-fw.fa-clock-o.fa-menu
|
||||
span.menu-entry Scheduler
|
||||
li
|
||||
a(ui-sref = '.rollingsnapshot')
|
||||
i.xo-icon-snapshot.fa-fw.fa-menu
|
||||
span.menu-entry Rolling snapshots
|
||||
li
|
||||
a(ui-sref = '.backup')
|
||||
i.fa.fa-fw.fa-download.fa-menu
|
||||
span.menu-entry Backup
|
||||
.side-content(ui-view = '')
|
||||
250
app/modules/self/admin/index.js
Normal file
@@ -0,0 +1,250 @@
|
||||
import angular from 'angular'
|
||||
import uiBootstrap from 'angular-ui-bootstrap'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
|
||||
import assign from 'lodash.assign'
|
||||
import differenceBy from 'lodash.differenceby'
|
||||
import filter from 'lodash.filter'
|
||||
import find from 'lodash.find'
|
||||
import forEach from 'lodash.foreach'
|
||||
import includes from 'lodash.includes'
|
||||
import intersection from 'lodash.intersection'
|
||||
import map from 'lodash.map'
|
||||
|
||||
import view from './view'
|
||||
|
||||
// ====================================================================
|
||||
|
||||
export default angular.module('self.admin', [
|
||||
uiRouter,
|
||||
uiBootstrap
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('self.admin', {
|
||||
url: '/admin',
|
||||
resolve: {
|
||||
users (xo) {
|
||||
return xo.user.getAll()
|
||||
},
|
||||
groups (xo) {
|
||||
return xo.group.getAll()
|
||||
}
|
||||
},
|
||||
controller: 'AdminCtrl as ctrl',
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('AdminCtrl', function (xo, xoApi, $scope, users, groups, sizeToBytesFilter, bytesToSizeFilter) {
|
||||
users.push(...groups)
|
||||
this.sizeUnits = ['MiB', 'GiB', 'TiB']
|
||||
|
||||
let validHosts
|
||||
|
||||
this.resourceSets = {}
|
||||
const loadSets = () => {
|
||||
xo.resourceSet.getAll()
|
||||
.then(sets => this.resourceSets = sets)
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
this.srs = []
|
||||
this.networks = []
|
||||
this.templates = []
|
||||
this.eligibleHosts = []
|
||||
validHosts = []
|
||||
|
||||
delete this.editing
|
||||
|
||||
delete this.selectedNetworks
|
||||
delete this.selectedSrs
|
||||
delete this.selectedTemplates
|
||||
delete this.selectedPools
|
||||
delete this.selectedSubjects
|
||||
delete this.name
|
||||
delete this.cpuMax
|
||||
delete this.memoryMax
|
||||
delete this.diskMax
|
||||
this.memoryUnit = this.sizeUnits[1]
|
||||
this.diskUnit = this.sizeUnits[1]
|
||||
}
|
||||
this.reset = reset
|
||||
|
||||
reset()
|
||||
loadSets()
|
||||
|
||||
this.pools = xoApi.getView('pool').all
|
||||
const hosts = xoApi.getView('host').all
|
||||
const srs = xoApi.getView('SR').all
|
||||
const networks = xoApi.getView('network').all
|
||||
const vmTemplatesByContainer = xoApi.getIndex('vmTemplatesByContainer')
|
||||
|
||||
this.subjects = users
|
||||
|
||||
const collectById = function (array) {
|
||||
const collection = {}
|
||||
forEach(array, item => collection[item.id] = item)
|
||||
return collection
|
||||
}
|
||||
|
||||
this.listSubjects = collectById(users)
|
||||
|
||||
// When a pool selection happens
|
||||
const filterSrs = () => filter(srs, sr => {
|
||||
let found = false
|
||||
forEach(this.selectedPools, pool => !(found = sr.$poolId === pool.id))
|
||||
return found
|
||||
})
|
||||
const gatherTemplates = () => {
|
||||
const vmTemplates = {}
|
||||
forEach(this.selectedPools, pool => {
|
||||
assign(vmTemplates, vmTemplatesByContainer[pool.id])
|
||||
})
|
||||
return vmTemplates
|
||||
}
|
||||
$scope.$watchCollection(() => this.selectedPools, () => {
|
||||
validHosts = filter(hosts, host => {
|
||||
let found = false
|
||||
forEach(this.selectedPools, pool => !(found = host.$poolId === pool.id))
|
||||
return found
|
||||
})
|
||||
this.srs = filterSrs()
|
||||
this.selectedSrs = intersection(this.selectedSrs, this.srs)
|
||||
this.vmTemplates = gatherTemplates()
|
||||
// TODO : Why isn't this working fine? (`intersection` uses SameValueZero as comparison: http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero)
|
||||
// this.selectedTemplates = intersection(this.selectedTemplates, this.vmTemplates)
|
||||
this.selectedTemplates = filter(this.selectedTemplates, (template) => this.vmTemplates.hasOwnProperty(template.id))
|
||||
this.networks = filterNetworks()
|
||||
this.selectedNetworks = intersection(this.selectedNetworks, this.networks)
|
||||
this.eligibleHosts = resolveHosts()
|
||||
})
|
||||
|
||||
const filterNetworks = () => {
|
||||
const selectableHosts = filter(validHosts, host => {
|
||||
let keptBySr
|
||||
forEach(this.selectedSrs, sr => !(keptBySr = intersection(sr.$PBDs, host.$PBDs).length > 0))
|
||||
return keptBySr
|
||||
})
|
||||
return filter(networks, network => {
|
||||
let kept = false
|
||||
forEach(selectableHosts, host => !(kept = intersection(network.PIFs, host.PIFs).length > 0))
|
||||
return kept
|
||||
})
|
||||
}
|
||||
// When a SR selection happens
|
||||
const constraintNetworks = () => {
|
||||
this.networks = filterNetworks()
|
||||
this.selectedNetworks = intersection(this.selectedNetworks, this.networks)
|
||||
resolveHosts()
|
||||
}
|
||||
|
||||
const resolveHosts = () => {
|
||||
const keptHosts = filter(validHosts, host => {
|
||||
let keptBySr = false
|
||||
forEach(this.selectedSrs, sr => !(keptBySr = intersection(sr.$PBDs, host.$PBDs).length > 0))
|
||||
let keptByNetwork
|
||||
forEach(this.selectedNetworks, network => !(keptByNetwork = intersection(network.PIFs, host.PIFs).length > 0))
|
||||
return keptBySr && keptByNetwork
|
||||
})
|
||||
this.eligibleHosts = keptHosts
|
||||
this.excludedHosts = differenceBy(map(hosts), keptHosts, item => item && item.id)
|
||||
}
|
||||
|
||||
$scope.$watchCollection(() => this.selectedSrs, constraintNetworks)
|
||||
$scope.$watchCollection(() => this.selectedNetworks, resolveHosts)
|
||||
|
||||
this.save = function (name, subjects, pools, templates, srs, networks, cpuMax, memoryMax, memoryUnit, diskMax, diskUnit, id) {
|
||||
return save(name, subjects, pools, templates, srs, networks, cpuMax, memoryMax, memoryUnit, diskMax, diskUnit, id)
|
||||
.then(reset)
|
||||
.then(loadSets)
|
||||
}
|
||||
|
||||
this.create = function (name, subjects, pools, templates, srs, networks, cpuMax, memoryMax, memoryUnit, diskMax, diskUnit) {
|
||||
return xo.resourceSet.create(name)
|
||||
.then(set => {
|
||||
save(name, subjects, pools, templates, srs, networks, cpuMax, memoryMax, memoryUnit, diskMax, diskUnit, set.id)
|
||||
})
|
||||
.then(reset)
|
||||
.then(loadSets)
|
||||
}
|
||||
|
||||
const save = function (name, subjects, pools, templates, srs, networks, cpuMax, memoryMax, memoryUnit, diskMax, diskUnit, id) {
|
||||
const limits = {}
|
||||
if (cpuMax) {
|
||||
limits.cpus = cpuMax
|
||||
}
|
||||
if (memoryMax) {
|
||||
limits.memory = sizeToBytesFilter(`${memoryMax} ${memoryUnit}`)
|
||||
}
|
||||
if (diskMax) {
|
||||
limits.disk = sizeToBytesFilter(`${diskMax} ${diskUnit}`)
|
||||
}
|
||||
|
||||
const getIds = arr => map(arr, item => item.id)
|
||||
|
||||
subjects = getIds(subjects)
|
||||
pools = getIds(pools)
|
||||
templates = getIds(templates)
|
||||
srs = getIds(srs)
|
||||
networks = getIds(networks)
|
||||
|
||||
const objects = Array.of(...templates, ...srs, ...networks)
|
||||
|
||||
return xo.resourceSet.set(id, name, subjects, objects, limits)
|
||||
}
|
||||
|
||||
this.edit = id => {
|
||||
window.scroll(0, 0)
|
||||
const set = find(this.resourceSets, rs => rs.id === id)
|
||||
if (set) {
|
||||
this.editing = id
|
||||
|
||||
this.name = set.name
|
||||
|
||||
const getObjects = arr => map(arr, id => xoApi.get(id))
|
||||
const objects = getObjects(set.objects)
|
||||
|
||||
const selectedPools = {}
|
||||
forEach(objects, object => {
|
||||
const poolId = object.poolId || object.$poolId
|
||||
if (poolId) { selectedPools[poolId] = true }
|
||||
})
|
||||
this.selectedPools = getObjects(Object.keys(selectedPools))
|
||||
|
||||
this.selectedSrs = filter(objects, object => object.type === 'SR')
|
||||
this.selectedNetworks = filter(objects, object => object.type === 'network')
|
||||
this.selectedTemplates = filter(objects, object => object.type === 'VM-template')
|
||||
|
||||
this.selectedSubjects = filter(users, user => includes(set.subjects, user.id))
|
||||
|
||||
this.cpuMax = set.limits.cpus && set.limits.cpus.total
|
||||
if (set.limits.memory) {
|
||||
const memory = bytesToSizeFilter(set.limits.memory.total).split(' ')
|
||||
this.memoryMax = +memory[0]
|
||||
this.memoryUnit = memory[1]
|
||||
} else {
|
||||
delete this.memoryMax
|
||||
this.memoryUnit = this.sizeUnits[1]
|
||||
}
|
||||
if (set.limits.disk) {
|
||||
const disk = bytesToSizeFilter(set.limits.disk.total).split(' ')
|
||||
this.diskMax = +disk[0]
|
||||
this.diskUnit = disk[1]
|
||||
} else {
|
||||
delete this.diskMax
|
||||
this.diskUnit = this.sizeUnits[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.delete = id => {
|
||||
xo.resourceSet.delete(id).then(() => {
|
||||
if (id === this.editing) {
|
||||
reset()
|
||||
}
|
||||
loadSets()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// A module exports its name.
|
||||
.name
|
||||
180
app/modules/self/admin/view.jade
Normal file
@@ -0,0 +1,180 @@
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.fa.fa-wrench(style="color: #e25440;")
|
||||
| Administration
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-pencil-square-o
|
||||
| Creation and edition
|
||||
.panel-body
|
||||
.alert.alert-info(ng-if = 'ctrl.editing') Editing an existing set
|
||||
form.form-horizontal(ng-submit = 'ctrl[ctrl.editing ? "save" : "create"](ctrl.name, ctrl.selectedSubjects, ctrl.selectedPools, ctrl.selectedTemplates, ctrl.selectedSrs, ctrl.selectedNetworks, ctrl.cpuMax, ctrl.memoryMax, ctrl.memoryUnit, ctrl.diskMax, ctrl.diskUnit, ctrl.editing)')
|
||||
.form-group
|
||||
.col-sm-4
|
||||
input.form-control(type = 'text', ng-model = 'ctrl.name', placeholder = 'resource set name', required)
|
||||
.col-sm-4
|
||||
ui-select(ng-model = 'ctrl.selectedSubjects', multiple, close-on-select = 'false', required)
|
||||
ui-select-match(placeholder = 'choose user(s) and/or group(s)')
|
||||
span(ng-if = '$item.email')
|
||||
i.xo-icon-user.fa-fw
|
||||
| {{$item.email}}
|
||||
span(ng-if = '$item.name')
|
||||
i.xo-icon-group.fa-fw
|
||||
| {{$item.name}}
|
||||
ui-select-choices(repeat = 'subject in ctrl.subjects | filter:{ permission: "!admin" } | filter:$select.search')
|
||||
div(ng-if = 'subject.email')
|
||||
i.xo-icon-user.fa-fw
|
||||
| {{subject.email}}
|
||||
div(ng-if = 'subject.name')
|
||||
i.xo-icon-group.fa-fw
|
||||
| {{subject.name}}
|
||||
.col-sm-4
|
||||
ui-select(ng-model = 'ctrl.selectedPools', multiple, close-on-select = 'false', required)
|
||||
ui-select-match(placeholder = 'choose pool(s)')
|
||||
i(class = 'xo-icon-{{$item.type | lowercase}}')
|
||||
| {{$item.name_label}}
|
||||
ui-select-choices(repeat = 'pool in ctrl.pools | map | filter:$select.search | orderBy:["type", "name_label"]')
|
||||
div
|
||||
i(class = 'xo-icon-{{pool.type | lowercase}}')
|
||||
| {{pool.name_label}}
|
||||
fieldset(ng-disabled = 'ctrl.selectedPools | isEmpty')
|
||||
.form-group
|
||||
.col-sm-4
|
||||
ui-select(ng-model = 'ctrl.selectedTemplates', multiple, close-on-select = 'false', required)
|
||||
ui-select-match(placeholder = 'choose VM templates')
|
||||
i(class = 'xo-icon-{{$item.type | lowercase}}')
|
||||
| {{$item.name_label}}
|
||||
ui-select-choices(repeat = 'template in ctrl.vmTemplates | map | filter:$select.search | orderBy:["type", "name_label"]')
|
||||
div
|
||||
i(class = 'xo-icon-{{template.type | lowercase}}')
|
||||
| {{template.name_label}}
|
||||
.col-sm-4
|
||||
ui-select(ng-model = 'ctrl.selectedSrs', multiple, close-on-select = 'false', required)
|
||||
ui-select-match(placeholder = 'choose storages')
|
||||
i(class = 'xo-icon-{{$item.type | lowercase}}')
|
||||
| {{$item.name_label}}
|
||||
span(ng-if="$item.$container")
|
||||
| ({{($item.$container | resolve).name_label}})
|
||||
ui-select-choices(repeat = 'sr in ctrl.srs | map | filter:$select.search | orderBy:["type", "name_label"]')
|
||||
div
|
||||
i(class = 'xo-icon-{{sr.type | lowercase}}')
|
||||
| {{sr.name_label}}
|
||||
span(ng-if="sr.$container")
|
||||
| ({{(sr.$container | resolve).name_label}})
|
||||
.col-sm-4
|
||||
fieldset(ng-disabled = 'ctrl.selectedSrs | isEmpty')
|
||||
ui-select(ng-model = 'ctrl.selectedNetworks', multiple, close-on-select = 'false', required)
|
||||
ui-select-match(placeholder = 'choose networks')
|
||||
i(class = 'xo-icon-{{$item.type | lowercase}}')
|
||||
| {{$item.name_label}}
|
||||
span(ng-if="$item.$poolId")
|
||||
| ({{($item.$poolId | resolve).name_label}})
|
||||
ui-select-choices(repeat = 'network in ctrl.networks | map | filter:$select.search | orderBy:["type", "name_label"]')
|
||||
div
|
||||
i(class = 'xo-icon-{{network.type | lowercase}}')
|
||||
| {{network.name_label}}
|
||||
span(ng-if="network.$poolId")
|
||||
| ({{(network.$poolId | resolve).name_label}})
|
||||
.form-group
|
||||
.col-sm-4
|
||||
input.form-control(type = 'number' min = '0' placeholder = 'Maximum CPUs' ng-model = 'ctrl.cpuMax')
|
||||
.col-sm-4
|
||||
.input-group
|
||||
input.form-control(type = 'number' min = '0' placeholder = 'Maximum RAM' ng-model = 'ctrl.memoryMax')
|
||||
span.input-group-btn.dropdown(dropdown)
|
||||
button.btn.btn-default.dropdown-toggle(type = 'button' dropdown-toggle)
|
||||
| {{ ctrl.memoryUnit }}
|
||||
span.caret
|
||||
ul.dropdown-menu(role = 'menu' style='min-width:0')
|
||||
li(ng-repeat = 'unit in ctrl.sizeUnits')
|
||||
a(ng-click = 'ctrl.memoryUnit = unit') {{ unit }}
|
||||
.col-sm-4
|
||||
.input-group
|
||||
input.form-control(type = 'number' min = '0' placeholder = 'Max. disk Space' ng-model = 'ctrl.diskMax')
|
||||
span.input-group-btn.dropdown(dropdown)
|
||||
button.btn.btn-default.dropdown-toggle(type = 'button' dropdown-toggle)
|
||||
| {{ ctrl.diskUnit }}
|
||||
span.caret
|
||||
ul.dropdown-menu(role = 'menu' style='min-width:0')
|
||||
li(ng-repeat = 'unit in ctrl.sizeUnits')
|
||||
a(ng-click = 'ctrl.diskUnit = unit') {{ unit }}
|
||||
.row
|
||||
.col-sm-8
|
||||
h4 Available hosts
|
||||
p.text-muted VMs created from this resource set shall run on the following hosts
|
||||
ul.list-group
|
||||
li.list-group-item(ng-if = 'ctrl.eligibleHosts | isEmpty'): em.text-muted No hosts available
|
||||
li.list-group-item(ng-if = 'ctrl.eligibleHosts | isNotEmpty', ng-repeat = 'host in ctrl.eligibleHosts')
|
||||
| {{ host.name_label }}
|
||||
span(ng-if = '(host.$poolId | resolve)') ({{ (host.$poolId | resolve).name_label }})
|
||||
.col-sm-4
|
||||
h4 Excluded hosts
|
||||
ul.list-group
|
||||
li.list-group-item(ng-repeat = 'host in ctrl.excludedHosts')
|
||||
s
|
||||
| {{ host.name_label }}
|
||||
span(ng-if = '(host.$poolId | resolve)') ({{ (host.$poolId | resolve).name_label }})
|
||||
.form-group
|
||||
.col-sm-10
|
||||
button.btn.btn-lg.btn-primary(type = 'submit', ng-disabled = '(ctrl.selectedSrs | isEmpty) || (ctrl.selectedNetworks | isEmpty) || (ctrl.selectedTemplates | isEmpty) || (ctrl.selectedSubjects | isEmpty)')
|
||||
span(ng-if='!ctrl.editing') Create
|
||||
span(ng-if='ctrl.editing') Edit
|
||||
|
|
||||
button.btn.btn-lg.btn-default(type = 'button', ng-click = 'ctrl.reset()') Reset
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-list-alt
|
||||
| Resource sets
|
||||
.panel-body
|
||||
div(ng-repeat = 's in ctrl.resourceSets | orderBy:"name"')
|
||||
.row
|
||||
.col-sm-9
|
||||
h4 {{ s.name }}
|
||||
.col-sm-3
|
||||
button.btn.btn-primary(type = 'button', ng-click = 'ctrl.edit(s.id)'): i.fa.fa-pencil
|
||||
|
|
||||
button.btn.btn-danger(type = 'button', ng-click = 'ctrl.delete(s.id)'): i.fa.fa-trash
|
||||
.row
|
||||
.col-sm-9
|
||||
ul.list-group
|
||||
li.list-group-item
|
||||
span(ng-repeat = 'subject in s.subjects')
|
||||
span(ng-if = 'ctrl.listSubjects[subject].email')
|
||||
i.fa.fa-user
|
||||
| {{ ctrl.listSubjects[subject].email }}
|
||||
span(ng-if = 'ctrl.listSubjects[subject].name')
|
||||
i.fa.fa-users
|
||||
| {{ ctrl.listSubjects[subject].name }}
|
||||
li.list-group-item
|
||||
span(ng-repeat = 'template in s.objects')
|
||||
span(ng-if = '(template | resolve).type == "VM-template"')
|
||||
i.xo-icon-vm
|
||||
| {{ (template | resolve).name_label }}
|
||||
li.list-group-item
|
||||
span(ng-repeat = 'sr in s.objects')
|
||||
span(ng-if = '(sr | resolve).type == "SR"')
|
||||
i.xo-icon-sr
|
||||
| {{ (sr | resolve).name_label }} ({{ ((sr | resolve).$container | resolve).name_label }})
|
||||
li.list-group-item
|
||||
span(ng-repeat = 'network in s.objects')
|
||||
span(ng-if = '(network | resolve).type == "network"')
|
||||
i.xo-icon-network
|
||||
| {{ (network | resolve).name_label }} ({{ ((network | resolve).$poolId | resolve).name_label }})
|
||||
li.list-group-item(ng-if="s.limits && (s.limits.cpus || s.limits.memory || s.limits.disk)")
|
||||
span(ng-if="s.limits.cpus && s.limits.cpus.total")
|
||||
i.xo-icon-cpu
|
||||
| Max. vCPUs: {{ s.limits.cpus.total }} ({{ s.limits.cpus.available }} remaining)
|
||||
br
|
||||
span(ng-if="s.limits.memory && s.limits.memory.total")
|
||||
i.xo-icon-memory
|
||||
| Max. RAM: {{ s.limits.memory.total | bytesToSize }} ({{ s.limits.memory.available | bytesToSize }} remaining)
|
||||
br
|
||||
span(ng-if="s.limits.disk && s.limits.disk.total")
|
||||
i.xo-icon-disk
|
||||
| Max. disk space: {{ s.limits.disk.total | bytesToSize }} ({{ s.limits.disk.available | bytesToSize }} remaining)
|
||||
.col-sm-3
|
||||
//- ul.list-group
|
||||
li.list-group-item max. CPUS: {{ s.cpuMax }}
|
||||
li.list-group-item max. RAM: {{ s.memoryMax }}
|
||||
li.list-group-item max. Disk space: {{ s.diskMax }}
|
||||
hr
|
||||
100
app/modules/self/dashboard/index.js
Normal file
@@ -0,0 +1,100 @@
|
||||
import angular from 'angular'
|
||||
import uiBootstrap from 'angular-ui-bootstrap'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
import slice from 'lodash.slice'
|
||||
import forEach from 'lodash.foreach'
|
||||
import find from 'lodash.find'
|
||||
|
||||
import view from './view'
|
||||
|
||||
// ====================================================================
|
||||
|
||||
export default angular.module('self.dashboard', [
|
||||
uiRouter,
|
||||
uiBootstrap
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('self.dashboard', {
|
||||
url: '/dashboard',
|
||||
resolve: {
|
||||
users (xo) {
|
||||
return xo.user.getAll()
|
||||
},
|
||||
groups (xo) {
|
||||
return xo.group.getAll()
|
||||
}
|
||||
},
|
||||
controller: 'DashboardCtrl as ctrl',
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('DashboardCtrl', function (xo, xoApi, $scope, $window, users, groups, bytesToSizeFilter) {
|
||||
this.resourceSetsPerPage = 5
|
||||
$window.bytesToSize = bytesToSizeFilter // FIXME dirty workaround to custom a Chart.js tooltip template
|
||||
|
||||
this.get = xoApi.get
|
||||
this.pageIndex = 0
|
||||
this.numberOfPages = 0
|
||||
this.resourceSetsToShow = []
|
||||
|
||||
const loadSets = () => {
|
||||
xo.resourceSet.getAll()
|
||||
.then(sets => {
|
||||
this.resourceSets = sets
|
||||
this.resourceSet = this.resourceSets[0]
|
||||
this.numberOfPages = Math.ceil(sets.length / this.resourceSetsPerPage)
|
||||
this.updateResourceSetsToShow()
|
||||
})
|
||||
}
|
||||
loadSets()
|
||||
|
||||
this.updateResourceSetsToShow = () => {
|
||||
this.resourceSetsToShow = slice(this.resourceSets, this.resourceSetsPerPage * this.pageIndex, this.resourceSetsPerPage * (this.pageIndex + 1))
|
||||
}
|
||||
|
||||
const getList = (ids, list) => {
|
||||
const collection = []
|
||||
forEach(ids, id => {
|
||||
const item = find(list, item => item.id === id)
|
||||
if (item) {
|
||||
collection.push(item)
|
||||
}
|
||||
})
|
||||
return collection
|
||||
}
|
||||
this.getUsers = (ids) => getList(ids, users)
|
||||
this.getGroups = (ids) => getList(ids, groups)
|
||||
|
||||
this.getObjectsByType = (arr) => {
|
||||
const objects = {}
|
||||
forEach(arr, id => {
|
||||
const obj = this.get(id)
|
||||
if (!objects[obj.type]) {
|
||||
objects[obj.type] = []
|
||||
}
|
||||
objects[obj.type].push(obj)
|
||||
})
|
||||
return objects
|
||||
}
|
||||
|
||||
$scope.$watch('ctrl.resourceSet', (resourceSet) => {
|
||||
if (!resourceSet) {
|
||||
return
|
||||
}
|
||||
this.cpusStats = [0, 0]
|
||||
this.memoryStats = [0, 0]
|
||||
this.diskStats = [0, 0]
|
||||
if (resourceSet.limits.cpus) {
|
||||
this.cpusStats = [resourceSet.limits.cpus.total - resourceSet.limits.cpus.available, resourceSet.limits.cpus.available]
|
||||
}
|
||||
if (resourceSet.limits.memory) {
|
||||
this.memoryStats = [resourceSet.limits.memory.total - resourceSet.limits.memory.available, resourceSet.limits.memory.available]
|
||||
}
|
||||
if (resourceSet.limits.disk) {
|
||||
this.diskStats = [resourceSet.limits.disk.total - resourceSet.limits.disk.available, resourceSet.limits.disk.available]
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// A module exports its name.
|
||||
.name
|
||||
123
app/modules/self/dashboard/view.jade
Normal file
@@ -0,0 +1,123 @@
|
||||
.panel.panel-default.alert.alert-danger.text-center(ng-if="!ctrl.resourceSets.length")
|
||||
| No resource set found.
|
||||
a(ui-sref = 'self.admin') Create one here.
|
||||
.grid(ng-if="ctrl.resourceSets.length")
|
||||
.panel.panel-default
|
||||
.col-sm-4(ng-if="ctrl.resourceSets.length <= ctrl.resourceSetsPerPage")
|
||||
button.btn.btn-default.col-sm-1.col-sm-offset-3(
|
||||
ng-if="ctrl.resourceSets.length > ctrl.resourceSetsPerPage"
|
||||
style="margin-top:4px;margin-bottom:4px;"
|
||||
ng-disabled="ctrl.pageIndex === 0"
|
||||
ng-click="ctrl.pageIndex = ctrl.pageIndex - 1; ctrl.updateResourceSetsToShow()"
|
||||
)
|
||||
i.fa.fa-chevron-left
|
||||
p.page-title.col-sm-4
|
||||
i.fa.xo-icon-cpu(style="color: #e25440;")
|
||||
| Dashboard
|
||||
span(ng-if="ctrl.resourceSets.length > ctrl.resourceSetsPerPage" style="font-size: 0.8em") ({{ ctrl.pageIndex + 1 }}/{{ ctrl.numberOfPages }})
|
||||
button.btn.btn-default.col-sm-1(
|
||||
ng-if="ctrl.resourceSets.length > ctrl.resourceSetsPerPage"
|
||||
style="margin-top:4px;margin-bottom:4px;"
|
||||
ng-disabled="ctrl.pageIndex + 1 === ctrl.numberOfPages"
|
||||
ng-click="ctrl.pageIndex = ctrl.pageIndex + 1; ctrl.updateResourceSetsToShow()"
|
||||
)
|
||||
i.fa.fa-chevron-right
|
||||
.well.panel(
|
||||
ng-repeat="resourceSet in ctrl.resourceSetsToShow"
|
||||
ng-init="users = ctrl.getUsers(resourceSet.subjects); groups = ctrl.getGroups(resourceSet.subjects); objects = ctrl.getObjectsByType(resourceSet.objects); showDetails = false"
|
||||
)
|
||||
.panel.panel-default
|
||||
.grid
|
||||
.col-sm-4
|
||||
p.page-title.col-sm-4 {{ resourceSet.name }}
|
||||
.col-sm-4(style="padding-right:4px")
|
||||
button.btn.btn-default.pull-right(ng-click="showDetails = !showDetails" style="margin-top:4px; margin-bottom:4px;")
|
||||
i.fa(ng-class="{'fa-chevron-up': showDetails, 'fa-chevron-down': !showDetails}")
|
||||
.panel.panel-default(ng-if="showDetails")
|
||||
ul.list-group
|
||||
li.list-group-item(ng-if="users.length")
|
||||
.grid
|
||||
label.control-label.col-sm-2
|
||||
i.xo-icon-user
|
||||
| Users:
|
||||
.col-sm-10
|
||||
span(ng-repeat="user in users track by user.id") {{ user.email }}
|
||||
span(ng-if="!$last") ,
|
||||
li.list-group-item(ng-if="groups.length")
|
||||
.grid
|
||||
label.control-label.col-sm-2
|
||||
i.xo-icon-group
|
||||
| Groups:
|
||||
.col-sm-10
|
||||
span(ng-repeat="group in groups track by group.id") {{ group.name }}
|
||||
span(ng-if="!$last") ,
|
||||
li.list-group-item(ng-if="objects['VM-template'].length")
|
||||
.grid
|
||||
label.control-label.col-sm-2
|
||||
i.xo-icon-vm
|
||||
| Templates:
|
||||
.col-sm-10
|
||||
span(ng-repeat="template in objects['VM-template'] track by template.id") {{ template.name_label }}
|
||||
span(ng-if="!$last") ,
|
||||
li.list-group-item(ng-if="objects.SR.length")
|
||||
.grid
|
||||
label.control-label.col-sm-2
|
||||
i.xo-icon-sr
|
||||
| SRs:
|
||||
.col-sm-10
|
||||
span(ng-repeat="sr in objects.SR track by sr.id") {{ sr.name_label }} ({{ ctrl.get(sr.$container).name_label }})
|
||||
span(ng-if="!$last") ,
|
||||
li.list-group-item(ng-if="objects.network.length")
|
||||
.grid
|
||||
label.control-label.col-sm-2
|
||||
i.xo-icon-network
|
||||
| Networks:
|
||||
.col-sm-10
|
||||
span(ng-repeat="network in objects.network track by network.id") {{ network.name_label }} ({{ ctrl.get(network.$pool).name_label }})
|
||||
span(ng-if="!$last") ,
|
||||
.grid-sm
|
||||
.grid-cell
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-cpu
|
||||
| vCPUs
|
||||
span(ng-if="resourceSet.limits.cpus" style="font-variant: normal; font-weight: normal") ({{ resourceSet.limits.cpus.total }})
|
||||
.panel-body.text-center(style="height:185px")
|
||||
canvas(
|
||||
ng-if="resourceSet.limits.cpus"
|
||||
class="chart chart-doughnut"
|
||||
data="[resourceSet.limits.cpus.total - resourceSet.limits.cpus.available, resourceSet.limits.cpus.available]"
|
||||
labels="['Used', 'Available']"
|
||||
options='{responsive: false,tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= value %> vCPUs"}'
|
||||
)
|
||||
p.big-stat(ng-if="!resourceSet.limits.cpus || !resourceSet.limits.cpus.total") ∞
|
||||
.grid-cell
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-memory
|
||||
| Memory
|
||||
span(ng-if="resourceSet.limits.memory" style="font-variant: normal; font-weight: normal") ({{ resourceSet.limits.memory.total | bytesToSize }})
|
||||
.panel-body.text-center(style="height:185px")
|
||||
canvas(
|
||||
ng-if="resourceSet.limits.memory"
|
||||
class="chart chart-doughnut"
|
||||
data="[resourceSet.limits.memory.total - resourceSet.limits.memory.available, resourceSet.limits.memory.available]"
|
||||
labels="['Used', 'Available']"
|
||||
options='{responsive: false,tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>"}'
|
||||
)
|
||||
p.big-stat(ng-if="!resourceSet.limits.memory || !resourceSet.limits.memory.total") ∞
|
||||
.grid-cell
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-sr
|
||||
| Storage
|
||||
span(ng-if="resourceSet.limits.disk" style="font-variant: normal; font-weight: normal") ({{ resourceSet.limits.disk.total | bytesToSize }})
|
||||
.panel-body.text-center(style="height:185px")
|
||||
canvas(
|
||||
ng-if="resourceSet.limits.disk"
|
||||
class="chart chart-doughnut"
|
||||
data="[resourceSet.limits.disk.total - resourceSet.limits.disk.available, resourceSet.limits.disk.available]"
|
||||
labels="['Used', 'Available']"
|
||||
options='{responsive: false,tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>"}'
|
||||
)
|
||||
p.big-stat(ng-if="!resourceSet.limits.disk || !resourceSet.limits.disk.total") ∞
|
||||
34
app/modules/self/index.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import angular from 'angular'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
|
||||
import admin from './admin'
|
||||
import dashboard from './dashboard'
|
||||
|
||||
import view from './view'
|
||||
|
||||
export default angular.module('self', [
|
||||
uiRouter,
|
||||
|
||||
admin,
|
||||
dashboard
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('self', {
|
||||
abstract: true,
|
||||
data: {
|
||||
requireAdmin: true
|
||||
},
|
||||
template: view,
|
||||
url: '/self'
|
||||
})
|
||||
|
||||
// Redirect to default sub-state.
|
||||
$stateProvider.state('self.index', {
|
||||
url: '',
|
||||
controller: function ($state) {
|
||||
$state.go('self.dashboard')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
.name
|
||||
12
app/modules/self/view.jade
Normal file
@@ -0,0 +1,12 @@
|
||||
.menu-grid
|
||||
.side-menu
|
||||
ul.nav
|
||||
li
|
||||
a(ui-sref = '.dashboard', ui-sref-active = 'active')
|
||||
i.fa.fa-fw.xo-icon-cpu.fa-menu
|
||||
span.menu-entry Dashboard
|
||||
li
|
||||
a(ui-sref = '.admin')
|
||||
i.fa.fa-fw.fa-wrench.fa-menu
|
||||
span.menu-entry Administration
|
||||
.side-content(ui-view = '')
|
||||
@@ -16,7 +16,8 @@ const HIGH_LEVEL_OBJECTS = {
|
||||
pool: true,
|
||||
host: true,
|
||||
VM: true,
|
||||
SR: true
|
||||
SR: true,
|
||||
network: true
|
||||
}
|
||||
|
||||
export default angular.module('settings.acls', [
|
||||
@@ -32,9 +33,6 @@ export default angular.module('settings.acls', [
|
||||
controller: 'SettingsAcls as ctrl',
|
||||
url: '/acls',
|
||||
resolve: {
|
||||
acls (xo) {
|
||||
return xo.acl.get()
|
||||
},
|
||||
users (xo) {
|
||||
return xo.user.getAll()
|
||||
},
|
||||
@@ -48,8 +46,14 @@ export default angular.module('settings.acls', [
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('SettingsAcls', function ($scope, acls, users, groups, roles, xoApi, xo, selectHighLevelFilter, filterFilter) {
|
||||
this.acls = acls
|
||||
.controller('SettingsAcls', function ($scope, users, groups, roles, xoApi, xo, selectHighLevelFilter, filterFilter) {
|
||||
const refreshAcls = () => {
|
||||
xo.acl.get().then(acls => {
|
||||
forEach(acls, acl => acl.newRole = acl.action)
|
||||
this.acls = acls
|
||||
})
|
||||
}
|
||||
refreshAcls()
|
||||
|
||||
this.types = Object.keys(HIGH_LEVEL_OBJECTS)
|
||||
this.selectedTypes = {}
|
||||
@@ -76,12 +80,6 @@ export default angular.module('settings.acls', [
|
||||
|
||||
this.objects = xoApi.all
|
||||
|
||||
let refreshAcls = () => {
|
||||
xo.acl.get().then(acls => {
|
||||
this.acls = acls
|
||||
})
|
||||
}
|
||||
|
||||
this.getUser = (id) => {
|
||||
for (let user of this.users) {
|
||||
if (user.id === id) {
|
||||
@@ -101,11 +99,18 @@ export default angular.module('settings.acls', [
|
||||
xo.acl.remove(subject, object, role).then(refreshAcls)
|
||||
}
|
||||
|
||||
this.editAcl = (subject, object, role, newRole) => {
|
||||
console.log(subject, object, role, newRole)
|
||||
xo.acl.remove(subject, object, role)
|
||||
.then(xo.acl.add(subject, object, newRole))
|
||||
.then(refreshAcls)
|
||||
}
|
||||
|
||||
this.toggleType = (toggle, type) => {
|
||||
const selectedObjects = this.selectedObjects && this.selectedObjects.slice() || []
|
||||
if (toggle) {
|
||||
const objects = filterFilter(selectHighLevelFilter(this.objects), {type})
|
||||
forEach(objects, object => {selectedObjects.indexOf(object) === -1 && selectedObjects.push(object)})
|
||||
forEach(objects, object => { selectedObjects.indexOf(object) === -1 && selectedObjects.push(object) })
|
||||
this.selectedObjects = selectedObjects
|
||||
} else {
|
||||
const keptObjects = []
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
p.page-title
|
||||
i.fa.fa-key(style="color: #e25440;")
|
||||
| ACLs
|
||||
.grid-sm
|
||||
.grid-lg
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-plus-circle
|
||||
@@ -33,12 +33,18 @@
|
||||
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')
|
||||
@@ -72,11 +78,11 @@
|
||||
th Object
|
||||
th Role
|
||||
th
|
||||
tr(ng-repeat = 'acl in ctrl.acls')
|
||||
tr(ng-repeat = 'acl in ctrl.acls | orderBy:["subject", "object"] track by acl.id')
|
||||
td {{ ctrl.usersById[acl.subject].email || ctrl.groupsById[acl.subject].name }}
|
||||
td {{(acl.object | resolve).name_label}}
|
||||
td {{ ctrl.rolesById[acl.action].name }}
|
||||
td
|
||||
select.form-control(ng-options = 'role.id as role.name for role in ctrl.roles | orderBy:"name"', ng-model = 'acl.newRole', ng-change = 'ctrl.editAcl(acl.subject, acl.object, acl.action, acl.newRole)')
|
||||
td
|
||||
button.btn.btn-danger(ng-click = 'ctrl.removeAcl(acl.subject, acl.object, acl.action)')
|
||||
i.fa.fa-trash
|
||||
|
||||
|
||||
@@ -4,8 +4,10 @@ import uiRouter from 'angular-ui-router'
|
||||
import acls from './acls'
|
||||
import group from './group'
|
||||
import groups from './groups'
|
||||
import plugins from './plugins'
|
||||
import servers from './servers'
|
||||
import update from './update'
|
||||
import user from './user'
|
||||
import users from './users'
|
||||
|
||||
import view from './view'
|
||||
@@ -16,8 +18,10 @@ export default angular.module('settings', [
|
||||
acls,
|
||||
group,
|
||||
groups,
|
||||
plugins,
|
||||
servers,
|
||||
update,
|
||||
user,
|
||||
users
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
@@ -39,4 +43,3 @@ export default angular.module('settings', [
|
||||
})
|
||||
})
|
||||
.name
|
||||
|
||||
|
||||
213
app/modules/settings/plugins/index.js
Normal file
@@ -0,0 +1,213 @@
|
||||
import angular from 'angular'
|
||||
import find from 'lodash.find'
|
||||
import forEach from 'lodash.foreach'
|
||||
import includes from 'lodash.includes'
|
||||
import trim from 'lodash.trim'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
import remove from 'lodash.remove'
|
||||
|
||||
import xoApi from 'xo-api'
|
||||
import xoServices from 'xo-services'
|
||||
|
||||
import view from './view'
|
||||
|
||||
function loadDefaults (schema, configuration) {
|
||||
if (!schema || !configuration) {
|
||||
return
|
||||
}
|
||||
forEach(schema.properties, (item, key) => {
|
||||
if (item.type === 'boolean' && !(key in configuration)) { // String default values are used as placeholders in view
|
||||
configuration[key] = Boolean(item && item.default)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function setOptionalProperties (configurationSchema) {
|
||||
if (!configurationSchema) {
|
||||
return
|
||||
}
|
||||
|
||||
forEach(configurationSchema.properties, (property, key) => {
|
||||
let { required } = configurationSchema
|
||||
|
||||
if (!required) {
|
||||
required = configurationSchema.required = []
|
||||
}
|
||||
|
||||
property.optional = !includes(required, key)
|
||||
|
||||
const { type, items } = property
|
||||
|
||||
if (type === 'object') {
|
||||
setOptionalProperties(property)
|
||||
} else if (type === 'array' && items && items.type === 'object') {
|
||||
setOptionalProperties(items)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function cleanUpConfiguration (schema, configuration, dump = {}) {
|
||||
if (!schema || !configuration) {
|
||||
return
|
||||
}
|
||||
|
||||
function sanitizeItem (item) {
|
||||
if (typeof item === 'string') {
|
||||
item = trim(item)
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
function keepItem (item) {
|
||||
return !(item == null || item === '' || (Array.isArray(item) && item.length === 0))
|
||||
}
|
||||
|
||||
forEach(configuration, (item, key) => {
|
||||
item = sanitizeItem(item)
|
||||
configuration[key] = item
|
||||
dump[key] = item
|
||||
|
||||
if (!keepItem(item) || !schema.properties || !(key in schema.properties)) {
|
||||
delete dump[key]
|
||||
} else if (schema.properties && schema.properties[key]) {
|
||||
const type = schema.properties[key].type
|
||||
|
||||
if (type === 'integer' || type === 'number') {
|
||||
dump[key] = +dump[key]
|
||||
} else if (type === 'object') {
|
||||
dump[key] = {}
|
||||
cleanUpConfiguration(schema.properties[key], item, dump[key])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default angular.module('settings.plugins', [
|
||||
uiRouter,
|
||||
|
||||
xoApi,
|
||||
xoServices
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('settings.plugins', {
|
||||
controller: 'SettingsPlugins as ctrl',
|
||||
url: '/plugins',
|
||||
data: {
|
||||
requireAdmin: true
|
||||
},
|
||||
resolve: {
|
||||
},
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('SettingsPlugins', function (xo, notify, modal) {
|
||||
this.disabled = {}
|
||||
|
||||
const preparePluginForView = plugin => {
|
||||
const { configurationSchema } = plugin
|
||||
|
||||
plugin._loaded = plugin.loaded
|
||||
plugin._autoload = plugin.autoload
|
||||
|
||||
if (!plugin.configuration) {
|
||||
plugin.configuration = {}
|
||||
}
|
||||
|
||||
setOptionalProperties(configurationSchema)
|
||||
loadDefaults(configurationSchema, plugin.configuration)
|
||||
}
|
||||
|
||||
const refreshPlugin = id => {
|
||||
return xo.plugin.get()
|
||||
.then(plugins => {
|
||||
const plugin = find(plugins, plugin => plugin.id === id)
|
||||
if (plugin) {
|
||||
preparePluginForView(plugin)
|
||||
remove(this.plugins, plugin => plugin.id === id)
|
||||
this.plugins.push(plugin)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const refreshPlugins = () => xo.plugin.get().then(plugins => {
|
||||
forEach(plugins, preparePluginForView)
|
||||
this.plugins = plugins
|
||||
})
|
||||
refreshPlugins()
|
||||
|
||||
const _execPluginMethod = (id, method, ...args) => {
|
||||
this.disabled[id] = true
|
||||
return xo.plugin[method](...args)
|
||||
.finally(() => {
|
||||
this.disabled[id] = false
|
||||
})
|
||||
}
|
||||
|
||||
this.configure = (plugin) => {
|
||||
const newConfiguration = {}
|
||||
plugin.errors = []
|
||||
|
||||
cleanUpConfiguration(plugin.configurationSchema, plugin.configuration, newConfiguration)
|
||||
_execPluginMethod(plugin.id, 'configure', plugin.id, newConfiguration)
|
||||
.then(() => {
|
||||
notify.info({
|
||||
title: 'Plugin configuration',
|
||||
message: 'Successfully saved'
|
||||
})
|
||||
refreshPlugin(plugin.id)
|
||||
})
|
||||
.catch(err => {
|
||||
forEach(err.data, data => {
|
||||
const fieldPath = data.field.split('.').slice(1)
|
||||
const fieldPathTitles = []
|
||||
let groupObject = plugin.configurationSchema
|
||||
forEach(fieldPath, groupName => {
|
||||
groupObject = groupObject.properties[groupName]
|
||||
fieldPathTitles.push(groupObject.title || groupName)
|
||||
})
|
||||
plugin.errors.push(`${fieldPathTitles.join(' > ')} ${data.message}`)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
this.purgeConfiguration = (plugin) => {
|
||||
modal.confirm({
|
||||
title: 'Purge configuration',
|
||||
message: 'Are you sure you want to purge this configuration ?'
|
||||
}).then(() => {
|
||||
_execPluginMethod(plugin.id, 'purgeConfiguration', plugin.id).then(() => {
|
||||
refreshPlugin(plugin.id).then(() =>
|
||||
notify.info({
|
||||
title: 'Purge configuration',
|
||||
message: 'This plugin config is now purged.'
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
this.toggleAutoload = (plugin) => {
|
||||
let method
|
||||
if (!plugin._autoload && plugin.autoload) {
|
||||
method = 'disableAutoload'
|
||||
} else if (plugin._autoload && !plugin.autoload) {
|
||||
method = 'enableAutoload'
|
||||
}
|
||||
if (method) {
|
||||
_execPluginMethod(plugin.id, method, plugin.id)
|
||||
}
|
||||
}
|
||||
this.toggleLoad = (plugin) => {
|
||||
let method
|
||||
if (!plugin._loaded && plugin.loaded && plugin.unloadable !== false) {
|
||||
method = 'unload'
|
||||
} else if (plugin._loaded && !plugin.loaded) {
|
||||
method = 'load'
|
||||
}
|
||||
if (method) {
|
||||
_execPluginMethod(plugin.id, method, plugin.id)
|
||||
refreshPlugin(plugin.id)
|
||||
}
|
||||
}
|
||||
})
|
||||
.name
|
||||
52
app/modules/settings/plugins/view.jade
Normal file
@@ -0,0 +1,52 @@
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.xo-icon-plugin(style="color: #e25440;")
|
||||
| Plugins
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-body
|
||||
p.text-center(ng-if = '!ctrl.plugins || !ctrl.plugins.length') No plugins found
|
||||
div(ng-repeat = 'plugin in ctrl.plugins | orderBy:"name" track by plugin.id')
|
||||
h3.form-inline.clearfix
|
||||
.checkbox.small
|
||||
label
|
||||
i.fa.fa-2x(ng-class = '{"fa-toggle-on": plugin.loaded, "fa-toggle-off": !plugin.loaded, "text-success": plugin.loaded}')
|
||||
span(ng-if = 'plugin.loaded && plugin.unloadable === false')
|
||||
|
|
||||
i.fa.fa-2x.fa-lock(tooltip = 'This plugin cannot be unloaded without a server restart')
|
||||
input.hidden(type = 'checkbox', ng-model = 'plugin._loaded', ng-change = 'ctrl.toggleLoad(plugin)', ng-disabled = 'plugin.unloadable === false && plugin.loaded || ctrl.disabled[plugin.id]')
|
||||
|
|
||||
span.text-info {{ plugin.name }}
|
||||
span(style="font-size:0.7em") (v{{ plugin.version }})
|
||||
.checkbox.small
|
||||
label
|
||||
| Auto-load at server start
|
||||
input(type = 'checkbox', ng-model = 'plugin._autoload', ng-change = 'ctrl.toggleAutoload(plugin)', ng-disabled = 'ctrl.disabled[plugin.id]')
|
||||
.form-group.pull-right.small
|
||||
button.btn.btn-default(type = 'button', ng-click = 'isExpanded = !isExpanded'): i.fa(ng-class = '{"fa-plus": !isExpanded, "fa-minus": isExpanded}')
|
||||
hr
|
||||
div(collapse = '!isExpanded')
|
||||
p(ng-if = '!plugin.configurationSchema') This plugin has no specific configuration
|
||||
form.form-horizontal(form = 'pluginform' ng-if = 'plugin.configurationSchema', ng-submit = 'ctrl.configure(plugin)')
|
||||
fieldset(ng-disabled = 'ctrl.disabled[plugin.id]')
|
||||
object-input(
|
||||
form = '"pluginform"',
|
||||
property = 'plugin.configurationSchema',
|
||||
model = 'plugin.configuration',
|
||||
group = ''
|
||||
)
|
||||
.form-group
|
||||
.col-md-offset-2.col-md-10.text-danger(ng-repeat = "err in plugin.errors")
|
||||
| {{ err }}
|
||||
.form-group
|
||||
.col-md-offset-2.col-md-10
|
||||
.btn-toolbar
|
||||
.btn-group
|
||||
button.btn.btn-primary(type = 'submit')
|
||||
| Save configuration
|
||||
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,26 +36,24 @@ export default angular.module('settings.update', [
|
||||
this.updater = updater
|
||||
|
||||
this.updater.isRegistered()
|
||||
.then(() => this.updater.on('end', () => {
|
||||
if (this.updater.state === 'registerNeeded' && this.updater.registerState !== 'unregistered' && this.updater.registerState !== 'error') {
|
||||
this.updater.isRegistered()
|
||||
}
|
||||
}))
|
||||
.then(() => this.updater.on('end', () => this.updater.isRegistered()))
|
||||
.catch(err => console.error(err))
|
||||
|
||||
this.updater.getConfiguration()
|
||||
.then(configuration => this.configuration = _assign({}, configuration))
|
||||
.then(() => this.withAuth = Boolean(this.configuration.proxyUser))
|
||||
.catch(error => notify.error({
|
||||
title: 'XOA Updater',
|
||||
message: error.message
|
||||
}))
|
||||
|
||||
this.registerXoa = (email, password) => {
|
||||
this.registerXoa = (email, password, renewRegister) => {
|
||||
this.regPwd = ''
|
||||
this.updater.register(email, password)
|
||||
this.updater.register(email, password, renewRegister)
|
||||
.tap(() => this.renewRegister = false)
|
||||
.then(() => this.updater.update())
|
||||
.catch(AuthenticationFailed, () => {})
|
||||
.catch(() => {})
|
||||
.catch(err => console.error(err))
|
||||
}
|
||||
|
||||
this.update = () => {
|
||||
@@ -74,12 +72,19 @@ export default angular.module('settings.update', [
|
||||
}))
|
||||
}
|
||||
|
||||
this.configure = (host, port) => {
|
||||
this.configure = (host, port, username, password) => {
|
||||
const config = {}
|
||||
if (!this.withAuth) {
|
||||
username = null
|
||||
password = null
|
||||
}
|
||||
config.proxyHost = host && host.trim() || null
|
||||
config.proxyPort = port && port.trim() || null
|
||||
config.proxyUser = username || null
|
||||
config.proxyPassword = password || null
|
||||
return this.updater.configure(config)
|
||||
.then(configuration => this.configuration = _assign({}, configuration))
|
||||
.then(() => this.withAuth = Boolean(this.configuration.proxyUser))
|
||||
.catch(error => notify.error({
|
||||
title: 'XOA Updater',
|
||||
message: error.message
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
p(ng-repeat = 'entry in ctrl.updater._log')
|
||||
span(ng-class = '{"text-danger": entry.level === "error", "text-muted": entry.level === "info", "text-warning": entry.level === "warning", "text-success": entry.level === "success"}') {{ entry.date }}
|
||||
| :
|
||||
span(ng-bind-html = 'entry.message | ansitohtml')
|
||||
span(style = 'word-wrap: break-word;', ng-bind-html = 'entry.message | ansitohtml')
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-pencil
|
||||
@@ -57,8 +57,11 @@
|
||||
button.btn.btn-default(type = 'button', ng-click = 'ctrl.updater.isRegistered()')
|
||||
i.fa.fa-refresh
|
||||
| Refresh
|
||||
form(ng-if = 'ctrl.updater.state && ctrl.updater.registerState === "unregistered"', ng-submit = 'ctrl.registerXoa(ctrl.regEmail, ctrl.regPwd)')
|
||||
p.form-static-control XOA is not registered.
|
||||
form(ng-if = 'ctrl.updater.state && (ctrl.renewRegister || ctrl.updater.registerState === "unregistered")', ng-submit = 'ctrl.registerXoa(ctrl.regEmail, ctrl.regPwd, ctrl.renewRegister)')
|
||||
p.form-static-control(ng-if = '!ctrl.renewRegister') XOA is not registered.
|
||||
p.form-static-control(ng-if = 'ctrl.renewRegister')
|
||||
| Forget previous registration ?
|
||||
button.btn.btn-default(type = 'button', ng-click = 'ctrl.renewRegister = false') Cancel
|
||||
p.small Your xen-orchestra.com email and password
|
||||
.form-group
|
||||
.input-group
|
||||
@@ -75,36 +78,50 @@
|
||||
i.fa.fa-check
|
||||
| Register
|
||||
p.form-static-control.text-danger {{ ctrl.updater.registerError }}
|
||||
p(ng-if = 'ctrl.updater.state && ctrl.updater.registerState === "registered"')
|
||||
p(ng-if = 'ctrl.updater.state && ctrl.updater.registerState === "registered" && !ctrl.renewRegister')
|
||||
| Your Xen Orchestra appliance is registered to
|
||||
span.text-success {{ ctrl.updater.token.registrationEmail }}
|
||||
| .
|
||||
br
|
||||
span.big-stat
|
||||
i.fa.fa-check-circle.text-success
|
||||
br
|
||||
i.fa.fa-check-circle.fa-3x.text-success
|
||||
br
|
||||
br
|
||||
button.btn.btn-default(type = 'button', ng-click = 'ctrl.renewRegister = true') Register to someone else ?
|
||||
|
||||
.grid-sm(ng-if = 'ctrl.updater.state && ctrl.configuration')
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-cogs
|
||||
| Settings
|
||||
.panel-body
|
||||
form.form-inline(ng-submit = 'ctrl.configure(ctrl.configuration.proxyHost, ctrl.configuration.proxyPort)')
|
||||
form(ng-submit = 'ctrl.configure(ctrl.configuration.proxyHost, ctrl.configuration.proxyPort, ctrl.configuration.proxyUser, ctrl.configuration.proxyPassword)')
|
||||
h4
|
||||
i.fa.fa-globe
|
||||
| Proxy settings
|
||||
p
|
||||
| If you need a proxy to access the Internet 
|
||||
label
|
||||
input(type = 'checkbox', ng-model = 'ctrl.withAuth')
|
||||
| with authentication
|
||||
fieldset.form-inline
|
||||
.form-group
|
||||
//- label.control-label Host:
|
||||
input.form-control(type = 'text', ng-model = 'ctrl.configuration.proxyHost', placeholder = 'Host (myproxy.example.org)')
|
||||
|
|
||||
.form-group
|
||||
//- label.control-label Port:
|
||||
input.form-control(type = 'text', ng-model = 'ctrl.configuration.proxyPort', placeholder = 'Port (3128 ?...)')
|
||||
br
|
||||
div(ng-hide = '!ctrl.withAuth')
|
||||
fieldset.form-inline(ng-disabled = '!ctrl.withAuth')
|
||||
.form-group
|
||||
input.form-control(type = 'text', ng-model = 'ctrl.configuration.proxyUser', placeholder = 'User name', required)
|
||||
|
|
||||
.form-group
|
||||
input.form-control(type = 'password', ng-model = 'ctrl.configuration.proxyPassword', placeholder = 'Password')
|
||||
br
|
||||
fieldset
|
||||
h4
|
||||
i.fa.fa-globe
|
||||
| Proxy settings
|
||||
p If you need a proxy to access the Internet
|
||||
.form-group
|
||||
label.control-label Host:
|
||||
input.form-control(type = 'text', ng-model = 'ctrl.configuration.proxyHost', placeholder = 'myproxy.example.org')
|
||||
|
|
||||
|
|
||||
.form-group
|
||||
label.control-label Port:
|
||||
input.form-control(type = 'text', ng-model = 'ctrl.configuration.proxyPort', placeholder = '3128')
|
||||
|
|
||||
|
|
||||
|
|
||||
.form-group
|
||||
button.btn.btn-primary(type = 'submit')
|
||||
i.fa.fa-floppy-o
|
||||
|
||||
48
app/modules/settings/user/index.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import angular from 'angular'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
|
||||
import xoApi from 'xo-api'
|
||||
import xoServices from 'xo-services'
|
||||
|
||||
import view from './view'
|
||||
|
||||
export default angular.module('settings.user', [
|
||||
uiRouter,
|
||||
|
||||
xoApi,
|
||||
xoServices
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('settings.user', {
|
||||
controller: 'SettingsUser as ctrl',
|
||||
url: '/user',
|
||||
data: {
|
||||
requireAdmin: false
|
||||
},
|
||||
resolve: {
|
||||
},
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('SettingsUser', function (xo, notify) {
|
||||
this.changePassword = function (oldPassword, newPassword) {
|
||||
this.working = true
|
||||
xo.user.changePassword(oldPassword, newPassword)
|
||||
.then(() => {
|
||||
this.oldPassword = ''
|
||||
this.newPassword = ''
|
||||
this.confirmPassword = ''
|
||||
notify.info({
|
||||
title: 'Change password',
|
||||
message: 'Password has been successfully change'
|
||||
})
|
||||
})
|
||||
.catch(error => notify.error({
|
||||
title: 'Change password',
|
||||
message: error.message
|
||||
}))
|
||||
.finally(() => this.working = false)
|
||||
}
|
||||
})
|
||||
|
||||
.name
|
||||
21
app/modules/settings/user/view.jade
Normal file
@@ -0,0 +1,21 @@
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.xo-icon-user(style="color: #e25440;")
|
||||
| Profile
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-body
|
||||
.row
|
||||
.col-sm-6
|
||||
form(ng-submit = 'ctrl.changePassword(ctrl.oldPassword, ctrl.newPassword)')
|
||||
fieldset(ng-disabled = 'ctrl.working')
|
||||
legend Change password
|
||||
.form-group
|
||||
input.form-control(type = 'password', ng-model = 'ctrl.oldPassword', placeholder = 'Current password', required)
|
||||
.form-group
|
||||
input.form-control(type = 'password', ng-model = 'ctrl.newPassword', placeholder = 'New password', required)
|
||||
.form-group(ng-class = '{"has-error": ctrl.confirmPassword && ctrl.newPassword && (ctrl.confirmPassword !== ctrl.newPassword)}')
|
||||
input.form-control(type = 'password', ng-model = 'ctrl.confirmPassword', placeholder = 'Confirm password', required)
|
||||
.form-group
|
||||
button.btn.btn-primary(type = 'submit', ng-disabled = '!ctrl.oldPassword || !ctrl.newPassword || ctrl.newPassword !== ctrl.confirmPassword') Save password
|
||||
@@ -1,4 +1,5 @@
|
||||
import angular from 'angular'
|
||||
import passwordGenerator from 'password-generator'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
import uiSelect from 'angular-ui-select'
|
||||
import uiEvent from 'angular-ui-event'
|
||||
@@ -121,6 +122,11 @@ export default angular.module('settings.users', [
|
||||
this.editingUser = editing => {
|
||||
this._editingUser = editing
|
||||
}
|
||||
|
||||
this.generatePassword = (user) => {
|
||||
// Generate password of 8 letters/numbers/underscore
|
||||
user.password = passwordGenerator(8, false)
|
||||
}
|
||||
})
|
||||
|
||||
.name
|
||||
|
||||
@@ -16,13 +16,49 @@
|
||||
i.fa.fa-trash-o.fa-lg(tooltip="Remove user")
|
||||
tr(ng-repeat="user in ctrl.users | orderBy:natural('id') track by user.id")
|
||||
td
|
||||
input.form-control(type="text", ng-model="user.email", ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}')
|
||||
input.form-control(
|
||||
type="text"
|
||||
ng-model="user.email"
|
||||
ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}'
|
||||
)
|
||||
td
|
||||
select.form-control(ng-options="p.value as p.label for p in ctrl.permissions", ng-model="user.permission", ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}')
|
||||
select.form-control(
|
||||
ng-options="p.value as p.label for p in ctrl.permissions"
|
||||
ng-model="user.permission"
|
||||
ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}'
|
||||
)
|
||||
td
|
||||
input.form-control(type="password", ng-model="user.password", placeholder="Fill to change the password", ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}')
|
||||
div.input-group
|
||||
span.input-group-btn
|
||||
button.btn.btn-default(
|
||||
type = "button"
|
||||
tooltip = "Generate random password"
|
||||
ng-click = "ctrl.generatePassword(user); showPassword = true"
|
||||
)
|
||||
i.fa.fa-key
|
||||
input.form-control(
|
||||
type = "{{ showPassword ? 'text' : 'password' }}"
|
||||
ng-model = "user.password"
|
||||
placeholder = "Fill to change the password"
|
||||
ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}'
|
||||
)
|
||||
span.input-group-btn
|
||||
button.btn.btn-default(
|
||||
type = "button"
|
||||
tooltip = "Reveal password"
|
||||
ng-show = "user.password.length > 0"
|
||||
ng-mousedown = "showPassword = true"
|
||||
ng-mouseup = "showPassword = false"
|
||||
ng-mouseleave = "showPassword = false"
|
||||
)
|
||||
i.fa.fa-eye(ng-if = "showPassword")
|
||||
i.fa.fa-eye-slash(ng-if = "!showPassword")
|
||||
td.text-center
|
||||
input(type="checkbox", ng-model="ctrl.selectedUsers[user.id]", ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}')
|
||||
input(
|
||||
type="checkbox"
|
||||
ng-model="ctrl.selectedUsers[user.id]"
|
||||
ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}'
|
||||
)
|
||||
tr(ng-repeat="user in ctrl.newUsers")
|
||||
td
|
||||
input.form-control(
|
||||
@@ -39,13 +75,32 @@
|
||||
ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}'
|
||||
)
|
||||
td
|
||||
input.form-control(
|
||||
type = "password"
|
||||
ng-model = "user.password"
|
||||
ng-required = "user.email"
|
||||
placeholder = "password"
|
||||
ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}'
|
||||
)
|
||||
div.input-group
|
||||
span.input-group-btn
|
||||
button.btn.btn-default(
|
||||
type = "button"
|
||||
tooltip = "Generate random password"
|
||||
ng-click = "ctrl.generatePassword(user); showPassword = true"
|
||||
)
|
||||
i.fa.fa-key
|
||||
input.form-control(
|
||||
type = "{{ showPassword ? 'text' : 'password' }}"
|
||||
ng-model = "user.password"
|
||||
ng-required = "user.email"
|
||||
placeholder = "password"
|
||||
ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}'
|
||||
)
|
||||
span.input-group-btn
|
||||
button.btn.btn-default(
|
||||
type = "button"
|
||||
tooltip = "Reveal password"
|
||||
ng-show = "user.password.length > 0"
|
||||
ng-mousedown = "showPassword = true"
|
||||
ng-mouseup = "showPassword = false"
|
||||
ng-mouseleave = "showPassword = false"
|
||||
)
|
||||
i.fa.fa-eye(ng-if = "showPassword")
|
||||
i.fa.fa-eye-slash(ng-if = "!showPassword")
|
||||
td  
|
||||
p.text-center
|
||||
button.btn.btn-primary(type="submit")
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
a(ui-sref = '.acls')
|
||||
i.fa.fa-fw.fa-key.fa-menu
|
||||
span.menu-entry ACLs
|
||||
li
|
||||
a(ui-sref = '.plugins')
|
||||
i.xo-icon-plugin.fa-fw.fa-menu
|
||||
span.menu-entry Plugins
|
||||
li
|
||||
a(ui-sref = '.update')
|
||||
i.fa.fa-fw.fa-refresh.fa-menu
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
import angular from 'angular'
|
||||
import escapeRegExp from 'lodash.escaperegexp'
|
||||
import filter from 'lodash.filter'
|
||||
import forEach from 'lodash.foreach'
|
||||
import isEmpty from 'lodash.isempty'
|
||||
import trim from 'lodash.trim'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
|
||||
import Bluebird from 'bluebird'
|
||||
|
||||
import xoTag from 'tag'
|
||||
|
||||
import view from './view'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default angular.module('xoWebApp.sr', [
|
||||
uiRouter
|
||||
uiRouter,
|
||||
xoTag
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('SRs_view', {
|
||||
@@ -19,18 +25,52 @@ export default angular.module('xoWebApp.sr', [
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('SrCtrl', function ($scope, $stateParams, $state, $q, notify, xoApi, xo, modal, $window, bytesToSizeFilter) {
|
||||
|
||||
.filter('vdiFilter', (xoApi, filterFilter) => {
|
||||
return (input, search) => {
|
||||
search && (search = trim(search).toLowerCase())
|
||||
return filter(input, vdi => {
|
||||
let vbd, vm
|
||||
let vmName = vdi.$VBDs && vdi.$VBDs[0] && (vbd = xoApi.get(vdi.$VBDs[0])) && (vm = xoApi.get(vbd.VM)) && vm.name_label
|
||||
vmName && (vmName = vmName.toLowerCase())
|
||||
return !search || (vmName && (vmName.search(escapeRegExp(search)) !== -1) || filterFilter([vdi], search).length)
|
||||
})
|
||||
}
|
||||
})
|
||||
.controller('SrCtrl', function ($scope, $stateParams, $state, $q, notify, xoApi, xo, modal, $window, bytesToSizeFilter, sizeToBytesFilter) {
|
||||
$window.bytesToSize = bytesToSizeFilter // FIXME dirty workaround to custom a Chart.js tooltip template
|
||||
|
||||
$scope.units = ['MiB', 'GiB', 'TiB']
|
||||
|
||||
$scope.currentLogPage = 1
|
||||
$scope.currentVDIPage = 1
|
||||
|
||||
let {get} = xoApi
|
||||
$scope.$watch(() => xoApi.get($stateParams.id), function (SR) {
|
||||
const VDIs = []
|
||||
if (SR) {
|
||||
forEach(SR.VDIs, vdi => {
|
||||
vdi = xoApi.get(vdi)
|
||||
if (vdi) {
|
||||
const size = bytesToSizeFilter(vdi.size)
|
||||
VDIs.push({...vdi, size, sizeValue: size.split(' ')[0], sizeUnit: size.split(' ')[1]})
|
||||
}
|
||||
})
|
||||
}
|
||||
$scope.SR = SR
|
||||
$scope.VDIs = VDIs
|
||||
})
|
||||
|
||||
$scope.selectedForDelete = {}
|
||||
$scope.deleteSelectedVdis = function () {
|
||||
return modal.confirm({
|
||||
title: 'VDI deletion',
|
||||
message: 'Are you sure you want to delete all selected VDIs? This operation is irreversible.'
|
||||
}).then(function () {
|
||||
forEach($scope.selectedForDelete, (selected, id) => selected && xo.vdi.delete(id))
|
||||
$scope.selectedForDelete = {}
|
||||
})
|
||||
}
|
||||
|
||||
$scope.saveSR = function ($data) {
|
||||
let {SR} = $scope
|
||||
let {name_label, name_description} = $data
|
||||
@@ -62,19 +102,24 @@ export default angular.module('xoWebApp.sr', [
|
||||
$scope.disconnectVBD = function (id) {
|
||||
console.log('Disconnect VBD', id)
|
||||
|
||||
return xoApi.call('vbd.disconnect', {id: id})
|
||||
return modal.confirm({
|
||||
title: 'VDI disconnection',
|
||||
message: 'Are you sure you want to disconnect this VDI?'
|
||||
}).then(function () {
|
||||
return xoApi.call('vbd.disconnect', {id: id})
|
||||
})
|
||||
}
|
||||
|
||||
$scope.connectPBD = function (id) {
|
||||
console.log('Connect PBD', id)
|
||||
|
||||
return xoApi.call('pbd.connect', {id: id})
|
||||
return xo.pbd.connect(id)
|
||||
}
|
||||
|
||||
$scope.disconnectPBD = function (id) {
|
||||
console.log('Disconnect PBD', id)
|
||||
|
||||
return xoApi.call('pbd.disconnect', {id: id})
|
||||
return xo.pbd.disconnect(id)
|
||||
}
|
||||
|
||||
$scope.reconnectAllHosts = function () {
|
||||
@@ -155,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('/')
|
||||
|
||||
@@ -164,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 Edit", 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
|
||||
@@ -24,11 +26,8 @@
|
||||
dt Content type:
|
||||
dd {{SR.SR_type}}
|
||||
dt Tags
|
||||
dd(ng-if="SR.tags.length")
|
||||
span(ng-repeat="tag in SR.tags")
|
||||
span.label.label-primary {{tag}}
|
||||
dd(ng-if="!SR.tags.length")
|
||||
em No tags.
|
||||
dd
|
||||
xo-tag(ng-if = 'SR', object = 'SR')
|
||||
dt Shared
|
||||
div(ng-repeat="container in [SR.$container] | resolve")
|
||||
dd(ng-if="'pool' === container.type")
|
||||
@@ -55,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
|
||||
@@ -77,19 +69,19 @@
|
||||
.panel-body.text-center
|
||||
.grid
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Rescan all the VDI", type="button", style="width: 90%", ng-click="rescanSr(SR.id)")
|
||||
button.btn(tooltip="Rescan all the VDI", tooltip-placement="top", type="button", style="width: 90%", ng-click="rescanSr(SR.id)")
|
||||
i.fa.fa-refresh.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Reconnect all hosts", type="button", style="width: 90%", ng-click="reconnectAllHosts()")
|
||||
button.btn(tooltip="Reconnect all hosts", tooltip-placement="top", type="button", style="width: 90%", ng-click="reconnectAllHosts()")
|
||||
i.fa.fa-retweet.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Disconnect all hosts", type="button", style="width: 90%", xo-click="disconnectAllHosts()")
|
||||
button.btn(tooltip="Disconnect all hosts", tooltip-placement="top", type="button", style="width: 90%", xo-click="disconnectAllHosts()")
|
||||
i.fa.fa-power-off.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Forget SR", type="button", style="width: 90%", xo-click="forgetSR(SR.id)")
|
||||
button.btn(tooltip="Forget SR", tooltip-placement="top", type="button", style="width: 90%", xo-click="forgetSR(SR.id)")
|
||||
i.fa.fa-2x.fa-fw.fa-ban
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Remove SR", type="button", style="width: 90%", xo-click="removeSR(SR.id)")
|
||||
button.btn(tooltip="Remove SR", tooltip-placement="top", type="button", style="width: 90%", xo-click="removeSR(SR.id)")
|
||||
i.fa.fa-2x.fa-trash-o
|
||||
//- TODO: Space panel
|
||||
.grid
|
||||
@@ -99,38 +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 Edit"
|
||||
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 Size
|
||||
th Virtual Machine:
|
||||
tr(ng-repeat="VDI in SR.VDIs | resolve | 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,18 +155,34 @@
|
||||
)
|
||||
| {{VDI.name_description}}
|
||||
td
|
||||
//- FIXME: should be editable, but the server needs first
|
||||
//- to accept a human readable string.
|
||||
| {{VDI.size | bytesToSize}}
|
||||
xo-tag(object = 'VDI')
|
||||
td
|
||||
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.
|
||||
.center(ng-if = '(SR.VDIs | resolve).length > 10 || currentVDIPage > 1')
|
||||
pagination(boundary-links="true", total-items="(SR.VDIs | resolve).length", ng-model="$parent.currentVDIPage", items-per-page="10", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
|
||||
.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 = '(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(
|
||||
@@ -185,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
@@ -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
|
||||
350
app/modules/task-scheduler/job/index.js
Normal file
@@ -0,0 +1,350 @@
|
||||
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 jsonSchema from 'json-schema'
|
||||
import mapValues from 'lodash.mapvalues'
|
||||
import trim from 'lodash.trim'
|
||||
import uiBootstrap from 'angular-ui-bootstrap'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
import Bluebird from 'bluebird'
|
||||
Bluebird.longStackTraces()
|
||||
|
||||
import view from './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
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
||||
export default angular.module('xoWebApp.taskscheduler.job', [
|
||||
jsonSchema,
|
||||
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()
|
||||
|
||||
const loadActions = () => xoApi.call('system.getMethodsInfo')
|
||||
.then(response => {
|
||||
const actions = []
|
||||
|
||||
for (let method in response) {
|
||||
if (includes(jobCompliantMethods, method)) {
|
||||
let [group, command] = method.split('.')
|
||||
const properties = response[method].params
|
||||
|
||||
response[method].properties = properties
|
||||
response[method].type = 'object'
|
||||
delete response[method].params
|
||||
|
||||
for (const key in properties) {
|
||||
const property = properties[key]
|
||||
const type = getType(property)
|
||||
|
||||
if (type === 'string') {
|
||||
if (group === 'acl') {
|
||||
if (key === 'object') {
|
||||
property.$type = 'XoObject'
|
||||
} else if (key === 'action') {
|
||||
property.$type = 'XoRole'
|
||||
} else if (key === 'subject') {
|
||||
property.$type = 'XoEntity'
|
||||
}
|
||||
} else if (group === 'host' && key === 'id') {
|
||||
property.$type = 'Host'
|
||||
} else if (group === 'vm' && key === 'id') {
|
||||
property.$type = 'Vm'
|
||||
} else {
|
||||
if (includes(['pool', 'pool_id', 'target_pool_id'], key)) {
|
||||
property.$type = 'Pool'
|
||||
} else if (includes(['sr', 'sr_id', 'target_sr_id'], key)) {
|
||||
property.$type = 'Sr'
|
||||
} else if (includes(['host', 'host_id', 'target_host_id'], key)) {
|
||||
property.$type = 'Host'
|
||||
} else if (includes(['vm'], key)) {
|
||||
property.$type = 'Vm'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
})
|
||||
// A module exports its name.
|
||||
.name
|
||||
72
app/modules/task-scheduler/job/view.jade
Normal file
@@ -0,0 +1,72 @@
|
||||
.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
|
||||
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
@@ -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
@@ -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
@@ -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 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 = '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
@@ -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 = '')
|
||||
@@ -7,13 +7,13 @@ throttle = require 'lodash.throttle'
|
||||
sourceHost = null
|
||||
|
||||
module.exports = angular.module 'xoWebApp.tree', [
|
||||
require 'angular-file-upload'
|
||||
require 'angular-ui-router'
|
||||
require('ng-file-upload')
|
||||
|
||||
require 'xo-api'
|
||||
require 'xo-services'
|
||||
require('xo-api').default
|
||||
require('xo-services').default
|
||||
|
||||
require '../delete-vms'
|
||||
require('../delete-vms').default
|
||||
]
|
||||
.config ($stateProvider) ->
|
||||
$stateProvider.state 'tree',
|
||||
@@ -25,11 +25,11 @@ module.exports = angular.module 'xoWebApp.tree', [
|
||||
url: '/tree'
|
||||
.controller 'TreeCtrl', (
|
||||
$scope
|
||||
$upload
|
||||
dateFilter
|
||||
deleteVmsModal
|
||||
modal
|
||||
notify
|
||||
Upload
|
||||
xo
|
||||
xoApi
|
||||
) ->
|
||||
@@ -105,30 +105,69 @@ module.exports = angular.module 'xoWebApp.tree', [
|
||||
$scope.startHost = (id) ->
|
||||
xo.host.start id
|
||||
|
||||
bulkConfirms = {
|
||||
'stopVM': {
|
||||
title: 'VM shutdown',
|
||||
message: 'Are you sure you want to shutdown all selected VMs ?'
|
||||
},
|
||||
'rebootVM': {
|
||||
title: 'VM reboot',
|
||||
message: 'Are you sure you want to reboot all selected VMs ?'
|
||||
},
|
||||
'suspendVM': {
|
||||
title: 'VM suspend',
|
||||
message: 'Are you sure you want to suspend all selected VMs ?'
|
||||
},
|
||||
'force_rebootVM': {
|
||||
title: 'VM force reboot',
|
||||
message: 'Are you sure you want to force reboot for all selected VMs ?'
|
||||
},
|
||||
'force_stopVM': {
|
||||
title: 'VM force shutdown',
|
||||
message: 'Are you sure you want to force shutdown for all selected VMs ?'
|
||||
},
|
||||
'migrateVM': {
|
||||
title: 'VM migrate',
|
||||
message: 'Are you sure you want to migrate all selected VMs ?'
|
||||
}
|
||||
}
|
||||
|
||||
unitConfirms = {
|
||||
'stopVM': {
|
||||
title: 'VM shutdown',
|
||||
message: 'Are you sure you want to shutdown this VM ?'
|
||||
},
|
||||
'rebootVM': {
|
||||
title: 'VM reboot',
|
||||
message: 'Are you sure you want to reboot this VM ?'
|
||||
},
|
||||
'suspendVM': {
|
||||
title: 'VM suspend',
|
||||
message: 'Are you sure you want to suspend this VM ?'
|
||||
},
|
||||
'force_rebootVM': {
|
||||
title: 'VM force reboot',
|
||||
message: 'Are you sure you want to force reboot for this VM ?'
|
||||
},
|
||||
'force_stopVM': {
|
||||
title: 'VM force shutdown',
|
||||
message: 'Are you sure you want to force shutdown for this VM ?'
|
||||
},
|
||||
'migrateVM': {
|
||||
title: 'VM migrate',
|
||||
message: 'Are you sure you want to migrate this VM ?'
|
||||
}
|
||||
}
|
||||
|
||||
$scope.startVM = xo.vm.start
|
||||
$scope.stopVM = xo.vm.stop
|
||||
$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
|
||||
|
||||
$scope.migrateVM = (id, hostId) ->
|
||||
(xo.vm.migrate id, hostId).catch (error) ->
|
||||
modal.confirm
|
||||
title: 'VM migrate'
|
||||
message: 'This VM can\'t be migrated with Xen Motion to this host because they don\'t share any storage. Do you want to try a Xen Storage Motion?'
|
||||
|
||||
.then ->
|
||||
notify.info {
|
||||
title: 'VM migration'
|
||||
message: 'The migration process started'
|
||||
}
|
||||
|
||||
xo.vm.migratePool {
|
||||
id
|
||||
target_host_id: hostId
|
||||
}
|
||||
$scope.snapshotVM = (id) ->
|
||||
vm = xoApi.get(id)
|
||||
date = dateFilter Date.now(), 'yyyy-MM-ddTHH:mmZ'
|
||||
@@ -207,11 +246,31 @@ module.exports = angular.module 'xoWebApp.tree', [
|
||||
unless angular.isFunction fn
|
||||
throw new Error "invalid action #{action}"
|
||||
|
||||
for id, selected of selected_VMs
|
||||
fn id, args... if selected
|
||||
runBulk = () ->
|
||||
for id, selected of selected_VMs
|
||||
fn id, args... if selected
|
||||
# Unselects all VMs.
|
||||
$scope.selectVMs false
|
||||
|
||||
# Unselects all VMs.
|
||||
$scope.selectVMs false
|
||||
if action of bulkConfirms
|
||||
modal.confirm(bulkConfirms[action])
|
||||
.then runBulk
|
||||
else
|
||||
runBulk()
|
||||
|
||||
$scope.confirmAction = (action, args...) ->
|
||||
fn = $scope[action]
|
||||
unless angular.isFunction fn
|
||||
throw new Error "invalid action #{action}"
|
||||
|
||||
doAction = () ->
|
||||
fn args...
|
||||
|
||||
if action of unitConfirms
|
||||
modal.confirm(unitConfirms[action])
|
||||
.then doAction
|
||||
else
|
||||
doAction()
|
||||
|
||||
$scope.importVm = ($files, id) ->
|
||||
file = $files[0]
|
||||
@@ -222,7 +281,7 @@ module.exports = angular.module 'xoWebApp.tree', [
|
||||
|
||||
xo.vm.import id
|
||||
.then ({ $sendTo: url }) ->
|
||||
return $upload.http {
|
||||
return Upload.http {
|
||||
method: 'POST'
|
||||
url
|
||||
data: file
|
||||
@@ -237,7 +296,7 @@ module.exports = angular.module 'xoWebApp.tree', [
|
||||
file = $files[0]
|
||||
xo.pool.patch id
|
||||
.then ({ $sendTo: url }) ->
|
||||
return $upload.http {
|
||||
return Upload.http {
|
||||
method: 'POST'
|
||||
url
|
||||
data: file
|
||||
@@ -293,25 +352,11 @@ module.exports = angular.module 'xoWebApp.tree', [
|
||||
# sourceHost = event.originalEvent.dataTransfer.getData('host')
|
||||
targetHost = event.currentTarget.getAttribute('host')
|
||||
if sourceHost isnt targetHost
|
||||
notify.info({
|
||||
title: 'VM Migration'
|
||||
message: 'Starting your VM migration'
|
||||
})
|
||||
(xo.vm.migrate vm, targetHost).catch (error) ->
|
||||
modal.confirm
|
||||
title: 'VM migrate'
|
||||
message: 'This VM can\'t be migrated with Xen Motion to this host because they don\'t share any storage. Do you want to try a Xen Storage Motion?'
|
||||
|
||||
.then ->
|
||||
notify.info {
|
||||
title: 'VM migration'
|
||||
message: 'The migration process started'
|
||||
}
|
||||
|
||||
xo.vm.migratePool {
|
||||
id: vm
|
||||
target_host_id: targetHost
|
||||
}
|
||||
modal.confirm
|
||||
title: 'VM migrate'
|
||||
message: 'Are you sure you want to migrate this VM?'
|
||||
.then ->
|
||||
xo.vm.migrate vm, targetHost
|
||||
restrict: 'A'
|
||||
}
|
||||
# A module exports its name.
|
||||
|
||||
@@ -165,8 +165,8 @@ div(style="margin-top: 57px; visibility: hidden; height: 0") .
|
||||
a(xo-sref="VMs_new({container: pool.id})")
|
||||
i.xo-icon-vm.fa-fw
|
||||
| Create VM
|
||||
//- TODO: solve the "a" problem for ng-file-select
|
||||
li(ng-file-select="patchPool($files, pool.id)")
|
||||
//- TODO: solve the "a" problem for ngf-select
|
||||
li(ngf-select="patchPool($files, pool.id)")
|
||||
a
|
||||
i.fa.fa-file-code-o.fa-fw
|
||||
| Patch
|
||||
@@ -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).
|
||||
@@ -229,8 +229,8 @@ div(style="margin-top: 57px; visibility: hidden; height: 0") .
|
||||
a(xo-sref="VMs_new({container: host.id})")
|
||||
i.xo-icon-vm.fa-fw
|
||||
| Create VM
|
||||
//- TODO: solve the "a" problem for ng-file-select
|
||||
li(ng-file-select="importVm($files, host.id)")
|
||||
//- TODO: solve the "a" problem for ngf-select
|
||||
li(ngf-select="importVm($files, host.id)")
|
||||
a
|
||||
i.fa.fa-upload.fa-fw
|
||||
| Import VM
|
||||
@@ -263,18 +263,13 @@ div(style="margin-top: 57px; visibility: hidden; height: 0") .
|
||||
a(xo-click="restartToolStack(host.id)")
|
||||
i.fa.fa-retweet.fa-fw
|
||||
| Restart toolstack
|
||||
li(ng-if="pool.name_label")
|
||||
a(xo-click="pool_removeHost(host.id)")
|
||||
i.fa.fa-cloud-upload.fa-fw
|
||||
| Remove from pool
|
||||
li(ng-if="!pool.name_label")
|
||||
a(xo-click="pool_addHost(host.id)")
|
||||
i.fa.fa-cloud-download.fa-fw
|
||||
| Add to pool
|
||||
//- /Header.
|
||||
//- Stats.
|
||||
ul.list-unstyled.stats
|
||||
//- Warning icon if host is halted or disabled
|
||||
li.text-warning(ng-if="host.power_state === 'Unknown'")
|
||||
i.fa.fa-warning
|
||||
| Unknown
|
||||
li.text-danger(ng-if="host.power_state === 'Halted'")
|
||||
i.fa.fa-warning
|
||||
| Halted
|
||||
@@ -284,7 +279,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
|
||||
@@ -318,22 +313,22 @@ 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="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="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
|
||||
//- Description.
|
||||
td.vm-description.col-md-4.hidden-xs
|
||||
i(class="xo-icon-{{VM.os_version.distro | osFamily}}",ng-if="VM.os_version.distro", tooltip="{{VM.os_version.name}}")
|
||||
i(class="icon-{{VM.os_version.distro | osFamily}}",ng-if="VM.os_version.distro", tooltip="{{VM.os_version.name}}")
|
||||
|
|
||||
i.fa.fa-fw(ng-if="!VM.os_version.distro")
|
||||
| {{VM.name_description}}
|
||||
@@ -341,10 +336,16 @@ 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.PV_drivers && !VM.docker")
|
||||
i.xo-icon-info.fa-fw(ng-if="!VM.PV_drivers", tooltip="Xen tools not installed")
|
||||
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")
|
||||
i.xo-icon-info.fa-fw(ng-if="VM.xenTools === false", tooltip="Xen tools not installed")
|
||||
i.xo-icon-other.fa-fw(ng-if="VM.xenTools === undefined", tooltip="Unknown")
|
||||
//- /Metrics.
|
||||
//- Address.
|
||||
td.text-muted.text-right.col-md-2.hidden-xs
|
||||
@@ -370,24 +371,24 @@ 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
|
||||
i(class="xo-icon-{{VM.os_version.distro | osFamily}}",ng-if="VM.os_version.distro", tooltip="{{VM.os_version.name}}")
|
||||
i(class="icon-{{VM.os_version.distro | osFamily}}",ng-if="VM.os_version.distro", tooltip="{{VM.os_version.name}}")
|
||||
|
|
||||
i.fa.fa-fw(ng-if="!VM.os_version.distro")
|
||||
| {{VM.name_description}}
|
||||
@@ -395,10 +396,16 @@ 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.PV_drivers && !VM.docker")
|
||||
i.xo-icon-info.fa-fw(ng-if="!VM.PV_drivers", tooltip="Xen tools not installed")
|
||||
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")
|
||||
i.xo-icon-info.fa-fw(ng-if="VM.xenTools === false", tooltip="Xen tools not installed")
|
||||
i.xo-icon-other.fa-fw(ng-if="VM.xenTools === undefined", tooltip="Unknown")
|
||||
//- /Metrics.
|
||||
//- Address.
|
||||
td.text-muted.text-right.col-md-2.hidden-xs
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import * as format from '@julien-f/json-rpc/format'
|
||||
import angular from 'angular'
|
||||
import 'angular-bootstrap'
|
||||
import Bluebird from 'bluebird'
|
||||
import makeError from 'make-error'
|
||||
import parse from '@julien-f/json-rpc/parse'
|
||||
import uiBootstrap from 'angular-ui-bootstrap'
|
||||
import WebSocket from 'ws'
|
||||
import {EventEmitter} from 'events'
|
||||
|
||||
@@ -11,6 +11,8 @@ import modal from './modal'
|
||||
|
||||
const calls = {}
|
||||
|
||||
let blockTime
|
||||
|
||||
function jsonRpcCall (socket, method, params = {}) {
|
||||
const req = format.request(method, params)
|
||||
const reqId = req.id
|
||||
@@ -50,8 +52,8 @@ function blockXoaAccess (xoaState) {
|
||||
export const NotRegistered = makeError('NotRegistered')
|
||||
export const AuthenticationFailed = makeError('AuthenticationFailed')
|
||||
export default angular.module('updater', [
|
||||
'ui.bootstrap'
|
||||
])
|
||||
uiBootstrap
|
||||
])
|
||||
.factory('updater', function ($interval, $timeout, $window, $modal) {
|
||||
class Updater extends EventEmitter {
|
||||
constructor () {
|
||||
@@ -214,7 +216,6 @@ export default angular.module('updater', [
|
||||
throw new NotRegistered('Your Xen Orchestra Appliance is not registered')
|
||||
} else {
|
||||
this.registerState = 'registered'
|
||||
this.registerError = ''
|
||||
this.token = token
|
||||
return token
|
||||
}
|
||||
@@ -222,7 +223,6 @@ export default angular.module('updater', [
|
||||
})
|
||||
.catch(NotRegistered, () => {
|
||||
this.registerState = 'unregistered'
|
||||
this.registerError = ''
|
||||
})
|
||||
.catch(error => {
|
||||
this.registerError = error.message
|
||||
@@ -230,12 +230,13 @@ export default angular.module('updater', [
|
||||
})
|
||||
}
|
||||
|
||||
register (email, password) {
|
||||
register (email, password, renew = false) {
|
||||
return this._open()
|
||||
.then(socket => {
|
||||
return jsonRpcCall(socket, 'register', {email, password})
|
||||
return jsonRpcCall(socket, 'register', {email, password, renew})
|
||||
.then(token => {
|
||||
this.registerState = 'registered'
|
||||
this.registerError = ''
|
||||
this.token = token
|
||||
return token
|
||||
})
|
||||
@@ -337,7 +338,6 @@ export default angular.module('updater', [
|
||||
return jsonRpcCall(socket, 'configure', config)
|
||||
.then(configuration => this._configuration = configuration)
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,11 +353,18 @@ export default angular.module('updater', [
|
||||
}
|
||||
let {user} = xoApi
|
||||
let loggedIn = !!user
|
||||
if (!loggedIn || !updater._xoaState || state.name === 'settings.update') {
|
||||
if (!loggedIn || !updater._xoaState || state.name === 'settings.update') { // no reason to block
|
||||
return
|
||||
} else if (blockXoaAccess(updater._xoaState)) {
|
||||
blockTime || (blockTime = updater._xoaStateTS)
|
||||
updater.xoaState()
|
||||
if (Date.now() - blockTime < (60 * 1000)) { // We have 1 min before blocking for real
|
||||
return
|
||||
}
|
||||
event.preventDefault()
|
||||
$state.go('settings.update')
|
||||
} else {
|
||||
blockTime = undefined
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
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'
|
||||
|
||||
isoDevice = require('../iso-device')
|
||||
|
||||
#=====================================================================
|
||||
|
||||
module.exports = angular.module 'xoWebApp.vm', [
|
||||
require 'angular-ui-router',
|
||||
require 'angular-ui-bootstrap'
|
||||
|
||||
isoDevice
|
||||
require('iso-device').default
|
||||
require('tag').default
|
||||
]
|
||||
.config ($stateProvider) ->
|
||||
$stateProvider.state 'VMs_view',
|
||||
@@ -22,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
|
||||
@@ -32,12 +34,28 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
$window.bytesToSize = bytesToSizeFilter # FIXME dirty workaround to custom a Chart.js tooltip template
|
||||
{get} = xoApi
|
||||
|
||||
$scope.canSetCpuWeight = () =>
|
||||
return xoApi.user && xoApi.user.permission is 'admin' || !$scope.VM.other['xo:resource_set']
|
||||
|
||||
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
|
||||
) ->
|
||||
@@ -50,6 +68,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
|
||||
@@ -73,6 +95,8 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
prepareDiskData()
|
||||
)
|
||||
|
||||
$scope.objects = xoApi.all
|
||||
|
||||
$scope.currentLogPage = 1
|
||||
$scope.currentSnapPage = 1
|
||||
$scope.currentPCIPage = 1
|
||||
@@ -98,7 +122,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)
|
||||
@@ -123,6 +148,7 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
}
|
||||
|
||||
$scope.hosts = xoApi.getView('hosts')
|
||||
$scope.hostsByPool = xoApi.getIndex('hostsByPool')
|
||||
|
||||
$scope.$watch(
|
||||
-> get $stateParams.id, 'VM'
|
||||
@@ -130,29 +156,26 @@ 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)
|
||||
|
||||
# build VDI list of this VM
|
||||
VDIs = []
|
||||
for VBD in VM.$VBDs
|
||||
oVbd = get VBD
|
||||
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);
|
||||
$scope.prepareVDIs()
|
||||
|
||||
container = get VM.$container
|
||||
|
||||
if container.type is 'host'
|
||||
host = container
|
||||
pool = (get container.$poolId) ? {}
|
||||
host = $scope.host = container
|
||||
pool = $scope.pool = (get container.$poolId) ? {}
|
||||
else
|
||||
host = {}
|
||||
pool = container
|
||||
host = $scope.host = {}
|
||||
pool = $scope.pool = container
|
||||
|
||||
if VM.power_state is 'Running' && !($scope.isVMWorking($scope.VM))
|
||||
refreshStatControl.start()
|
||||
@@ -160,6 +183,29 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
refreshStatControl.stop()
|
||||
)
|
||||
|
||||
$scope.prepareVDIs = () ->
|
||||
return unless $scope.VM
|
||||
# build VDI list of this VM
|
||||
VDIs = []
|
||||
for VBD in $scope.VM.$VBDs
|
||||
oVbd = get VBD
|
||||
continue unless oVbd
|
||||
oVdi = get oVbd.VDI
|
||||
continue unless oVdi
|
||||
|
||||
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
|
||||
return ''
|
||||
@@ -228,32 +274,70 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
return
|
||||
$scope.savingBootOrder = true
|
||||
paramString = ''
|
||||
forEach(bootParams, (boot) -> boot.v && paramString += boot.e)
|
||||
return xoApi.call 'vm.bootOrder', {vm: id, order: paramString}
|
||||
forEach(bootParams, (boot) ->
|
||||
boot.v && paramString += boot.e
|
||||
return
|
||||
)
|
||||
return xo.vm.setBootOrder {vm: id, order: paramString}
|
||||
.finally () ->
|
||||
$scope.savingBootOrder = false
|
||||
$scope.bootReordering = false
|
||||
|
||||
$scope.refreshStats = (id) ->
|
||||
return xo.vm.refreshStats id
|
||||
|
||||
.then (result) ->
|
||||
result.cpuSeries = []
|
||||
forEach result.cpus, (v,k) ->
|
||||
result.cpuSeries.push 'CPU ' + k
|
||||
result.stats.cpuSeries = []
|
||||
|
||||
if result.stats.cpus.length >= 12
|
||||
nValues = result.stats.cpus[0].length
|
||||
nCpus = result.stats.cpus.length
|
||||
cpuAVG = (0 for [1..nValues])
|
||||
|
||||
forEach result.stats.cpus, (cpu) ->
|
||||
forEach cpu, (stat, index) ->
|
||||
cpuAVG[index] += stat
|
||||
return
|
||||
return
|
||||
|
||||
forEach cpuAVG, (cpu, index) ->
|
||||
cpuAVG[index] /= nCpus
|
||||
return
|
||||
|
||||
result.stats.cpus = [cpuAVG]
|
||||
result.stats.cpuSeries.push 'CPU AVG'
|
||||
else
|
||||
forEach result.stats.cpus, (v,k) ->
|
||||
result.stats.cpuSeries.push 'CPU ' + k
|
||||
return
|
||||
|
||||
result.stats.vifSeries = []
|
||||
vifsArray = []
|
||||
forEach result.stats.vifs.rx, (v,k) ->
|
||||
return unless v
|
||||
result.stats.vifSeries.push '#' + k + ' in'
|
||||
result.stats.vifSeries.push '#' + k + ' out'
|
||||
vifsArray.push (v || [])
|
||||
vifsArray.push (result.stats.vifs.tx[k] || [])
|
||||
return
|
||||
result.vifSeries = []
|
||||
forEach result.vifs, (v,k) ->
|
||||
result.vifSeries.push '#' + Math.floor(k/2) + ' ' + if k % 2 then 'out' else 'in'
|
||||
result.stats.vifs = vifsArray
|
||||
|
||||
result.stats.xvdSeries = []
|
||||
xvdsArray = []
|
||||
forEach result.stats.xvds.r, (v,k) ->
|
||||
return unless v
|
||||
result.stats.xvdSeries.push 'xvd' + k + ' read'
|
||||
result.stats.xvdSeries.push 'xvd' + k + ' write'
|
||||
xvdsArray.push (v || [])
|
||||
xvdsArray.push (result.stats.xvds.w[k] || [])
|
||||
return
|
||||
result.xvdSeries = []
|
||||
forEach result.xvds, (v,k) ->
|
||||
# 97 is ascii code of 'a'
|
||||
result.xvdSeries.push 'xvd' + String.fromCharCode(Math.floor(k/2) + 97, ) + ' ' + if k % 2 then 'write' else 'read'
|
||||
return
|
||||
forEach result.date, (v,k) ->
|
||||
result.date[k] = new Date(v*1000).toLocaleTimeString()
|
||||
$scope.stats = result
|
||||
result.stats.xvds = xvdsArray
|
||||
|
||||
result.stats.date = []
|
||||
timestamp = result.endTimestamp
|
||||
for i in [result.stats.memory.length-1..0] by -1
|
||||
result.stats.date.unshift new Date(timestamp*1000).toLocaleTimeString()
|
||||
timestamp -= 5
|
||||
$scope.stats = result.stats
|
||||
|
||||
$scope.startVM = (id) ->
|
||||
xo.vm.start id
|
||||
@@ -262,40 +346,67 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
message: 'Start VM'
|
||||
}
|
||||
|
||||
$scope.stopVM = (id) ->
|
||||
xo.vm.stop id
|
||||
$scope.recoveryStartVM = (id) ->
|
||||
xo.vm.recoveryStart id
|
||||
notify.info {
|
||||
title: 'VM shutdown...'
|
||||
message: 'Gracefully shutdown the VM'
|
||||
title: 'VM starting...'
|
||||
message: 'Start VM in recovery mode'
|
||||
}
|
||||
|
||||
$scope.stopVM = (id) ->
|
||||
modal.confirm
|
||||
title: 'VM shutdown'
|
||||
message: 'Are you sure you want to shutdown this VM ?'
|
||||
.then ->
|
||||
xo.vm.stop id
|
||||
notify.info {
|
||||
title: 'VM shutdown...'
|
||||
message: 'Gracefully shutdown the VM'
|
||||
}
|
||||
|
||||
$scope.force_stopVM = (id) ->
|
||||
xo.vm.stop id, true
|
||||
notify.info {
|
||||
title: 'VM force shutdown...'
|
||||
message: 'Force shutdown the VM'
|
||||
}
|
||||
modal.confirm
|
||||
title: 'VM force shutdown'
|
||||
message: 'Are you sure you want to force shutdown for this VM ?'
|
||||
.then ->
|
||||
xo.vm.stop id, true
|
||||
notify.info {
|
||||
title: 'VM force shutdown...'
|
||||
message: 'Force shutdown the VM'
|
||||
}
|
||||
|
||||
$scope.rebootVM = (id) ->
|
||||
xo.vm.restart id
|
||||
notify.info {
|
||||
title: 'VM reboot...'
|
||||
message: 'Gracefully reboot the VM'
|
||||
}
|
||||
modal.confirm
|
||||
title: 'VM reboot'
|
||||
message: 'Are you sure you want to reboot this VM ?'
|
||||
.then ->
|
||||
xo.vm.restart id
|
||||
notify.info {
|
||||
title: 'VM reboot...'
|
||||
message: 'Gracefully reboot the VM'
|
||||
}
|
||||
|
||||
$scope.force_rebootVM = (id) ->
|
||||
xo.vm.restart id, true
|
||||
notify.info {
|
||||
title: 'VM reboot...'
|
||||
message: 'Force reboot the VM'
|
||||
}
|
||||
modal.confirm
|
||||
title: 'VM reboot'
|
||||
message: 'Are you sure you want to force reboot for this VM ?'
|
||||
.then ->
|
||||
xo.vm.restart id, true
|
||||
notify.info {
|
||||
title: 'VM reboot...'
|
||||
message: 'Force reboot the VM'
|
||||
}
|
||||
|
||||
$scope.suspendVM = (id) ->
|
||||
xo.vm.suspend id
|
||||
notify.info {
|
||||
title: 'VM suspend...'
|
||||
message: 'Suspend the VM'
|
||||
}
|
||||
modal.confirm
|
||||
title: 'VM suspend'
|
||||
message: 'Are you sure you want to suspend this VM ?'
|
||||
.then ->
|
||||
xo.vm.suspend id
|
||||
notify.info {
|
||||
title: 'VM suspend...'
|
||||
message: 'Suspend the VM'
|
||||
}
|
||||
|
||||
$scope.resumeVM = (id) ->
|
||||
xo.vm.resume id, true
|
||||
@@ -305,21 +416,57 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
}
|
||||
|
||||
$scope.migrateVM = (id, hostId) ->
|
||||
(xo.vm.migrate id, hostId).catch (error) ->
|
||||
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: 'This VM can\'t be migrated with Xen Motion to this host because they don\'t share any storage. Do you want to try a Xen Storage Motion?'
|
||||
|
||||
message: 'Are you sure you want to migrate this VM?'
|
||||
.then ->
|
||||
notify.info {
|
||||
title: 'VM migration'
|
||||
message: 'The migration process started'
|
||||
}
|
||||
xo.vm.migrate id, hostId
|
||||
return
|
||||
|
||||
xo.vm.migratePool {
|
||||
id
|
||||
target_host_id: hostId
|
||||
}
|
||||
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
|
||||
@@ -346,20 +493,34 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
if $data isnt snapshot.name_label
|
||||
result.name_label = $data
|
||||
|
||||
xoApi.call 'vm.set', result
|
||||
xo.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} = $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
|
||||
@@ -368,8 +529,10 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
$data.high_availability = high_availability
|
||||
if auto_poweron isnt VM.auto_poweron
|
||||
$data.auto_poweron = auto_poweron
|
||||
if PV_args isnt VM.PV_args
|
||||
$data.PV_args = PV_args
|
||||
|
||||
xoApi.call 'vm.set', $data
|
||||
xo.vm.set($data)
|
||||
|
||||
#-----------------------------------------------------------------
|
||||
# Disks
|
||||
@@ -384,83 +547,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({
|
||||
@@ -478,14 +673,18 @@ 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) ->
|
||||
id = resolveVBD(vdi)
|
||||
if id?
|
||||
console.log "Disconnect VBD #{id}"
|
||||
xo.vbd.disconnect id
|
||||
modal.confirm({
|
||||
title: 'VBD disconnection'
|
||||
message: 'Are you sure you want to detach this VM disk ?'
|
||||
}).then ->
|
||||
console.log "Disconnect VBD #{id}"
|
||||
xo.vbd.disconnect id
|
||||
|
||||
$scope.connectVBD = (vdi) ->
|
||||
id = resolveVBD(vdi)
|
||||
@@ -496,11 +695,11 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
$scope.deleteVBD = (vdi) ->
|
||||
id = resolveVBD(vdi)
|
||||
if id?
|
||||
console.log "Delete VBD #{id}"
|
||||
modal.confirm({
|
||||
title: 'VBD deletion'
|
||||
message: 'Are you sure you want to delete this VM disk attachment (the disk will NOT be destroyed)?'
|
||||
}).then ->
|
||||
console.log "Delete VBD #{id}"
|
||||
xo.vbd.delete id
|
||||
|
||||
$scope.connectVIF = (id) ->
|
||||
@@ -509,9 +708,13 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
xo.vif.connect id
|
||||
|
||||
$scope.disconnectVIF = (id) ->
|
||||
console.log "Disconnect VIF #{id}"
|
||||
modal.confirm
|
||||
title: 'Disconnect VIF'
|
||||
message: 'Are you sure you want to disconnect this interface ?'
|
||||
.then ->
|
||||
console.log "Disconnect VIF #{id}"
|
||||
|
||||
xo.vif.disconnect id
|
||||
xo.vif.disconnect id
|
||||
|
||||
$scope.deleteVIF = (id) ->
|
||||
console.log "Delete VIF #{id}"
|
||||
@@ -548,15 +751,40 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
}
|
||||
xo.vm.export id
|
||||
.then ({$getFrom: url}) ->
|
||||
window.open url
|
||||
window.open '.' + url
|
||||
|
||||
$scope.copyVM = (id, srId) ->
|
||||
console.log "Copy VM #{id} tp SR #{srId}"
|
||||
notify.info {
|
||||
title: 'VM copy'
|
||||
message: 'VM copy started'
|
||||
}
|
||||
xo.vm.copy id, srId, $scope.VM.name_label + '_COPY'
|
||||
|
||||
$scope.exportOnlyMetadataVM = (id) ->
|
||||
console.log "Export Metadata only for VM #{id}"
|
||||
notify.info {
|
||||
title: 'VM export'
|
||||
message: 'VM export started'
|
||||
}
|
||||
xo.vm.export id, true, true
|
||||
.then ({$getFrom: url}) ->
|
||||
window.open '.' + url
|
||||
|
||||
$scope.convertVM = (id) ->
|
||||
console.log "Convert VM #{id}"
|
||||
modal.confirm({
|
||||
title: 'VM to template'
|
||||
message: 'Are you sure you want to convert this VM into a template?'
|
||||
}).then ->
|
||||
})
|
||||
.then ->
|
||||
xo.vm.convert id
|
||||
.then ->
|
||||
$state.go 'index'
|
||||
notify.info {
|
||||
title: 'VM conversion'
|
||||
message: 'VM is converted to template'
|
||||
}
|
||||
|
||||
$scope.deleteSnapshot = (id) ->
|
||||
console.log "Delete snapshot #{id}"
|
||||
@@ -627,6 +855,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
|
||||
@@ -646,6 +875,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
|
||||
@@ -655,6 +885,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
|
||||
@@ -685,9 +916,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
|
||||
|
||||
@@ -719,5 +951,27 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
diskOnly: false
|
||||
}
|
||||
|
||||
$scope.canAdmin = (id = undefined) ->
|
||||
if id == undefined
|
||||
id = $scope.VM?.id
|
||||
|
||||
return id && xoApi.canInteract(id, 'administrate') || false
|
||||
|
||||
$scope.canOperate = (id = undefined) ->
|
||||
if id == undefined
|
||||
id = $scope.VM?.id
|
||||
|
||||
return id && xoApi.canInteract(id, 'operate') || false
|
||||
|
||||
$scope.canHostOperate = (id) ->
|
||||
|
||||
return id && xoApi.canInteract(id, 'operate') || false
|
||||
|
||||
$scope.canView = (id = undefined) ->
|
||||
if id == undefined
|
||||
id = $scope.VM?.id
|
||||
|
||||
return id && xoApi.canInteract(id, 'view') || false
|
||||
|
||||
# A module exports its name.
|
||||
.name
|
||||
|
||||
@@ -9,8 +9,10 @@
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-cogs
|
||||
| General
|
||||
span.quick-edit(tooltip="Edit General settings", ng-click="vmSettings.$show()")
|
||||
span.quick-edit(tooltip="Edit General settings", ng-click="vmSettings.$show()", ng-if = '!vmSettings.$visible && canAdmin()')
|
||||
i.fa.fa-edit.fa-fw
|
||||
span.quick-edit(ng-if="vmSettings.$visible", tooltip="Cancel Edit", ng-click="vmSettings.$cancel()")
|
||||
i.fa.fa-undo.fa-fw
|
||||
.panel-body
|
||||
form(editable-form="", name="vmSettings", onbeforesave="saveVM($data)")
|
||||
dl.dl-horizontal
|
||||
@@ -25,7 +27,7 @@
|
||||
dt(ng-if="VM.power_state === 'Running' || VM.power_state === 'Paused'") Running on
|
||||
dt(ng-if="VM.power_state == 'Halted' || VM.power_state === 'Suspended'") Resident on
|
||||
dd(ng-repeat="container in [VM.$container] | resolve")
|
||||
span(ng-if = 'container.type === "host"')
|
||||
span(ng-if = 'container.type === "host" && canView(container.id)')
|
||||
a(xo-sref="hosts_view({id: container.id})")
|
||||
| {{container.name_label}}
|
||||
small
|
||||
@@ -34,10 +36,13 @@
|
||||
a(ui-sref="pools_view({id: (container.$poolId | resolve).id})") {{(container.$poolId | resolve).name_label}}
|
||||
| )
|
||||
a(
|
||||
ng-if = 'container.type === "pool"'
|
||||
ng-if = 'container.type === "pool" && canView(container.id)'
|
||||
xo-sref="pools_view({id: container.id})"
|
||||
)
|
||||
| {{container.name_label}}
|
||||
dt Tags
|
||||
dd
|
||||
xo-tag(ng-if = 'VM', object = 'VM')
|
||||
dt Addresses
|
||||
dd(ng-if="!VM.addresses") -
|
||||
dd(ng-repeat="IP in VM.addresses") {{IP}}
|
||||
@@ -66,20 +71,41 @@
|
||||
e-form="vmSettings"
|
||||
)
|
||||
| {{VM.CPUs.number}}
|
||||
dt CPU Weight
|
||||
dd
|
||||
span(
|
||||
e-ng-disabled="!canSetCpuWeight()"
|
||||
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.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}}
|
||||
dt(ng-if="refreshStatControl.running && stats") Xen tools
|
||||
dd(ng-if="refreshStatControl.running && stats")
|
||||
span(ng-if="VM.PV_drivers", style="color:green;") Installed
|
||||
span(ng-if="!VM.PV_drivers") NOT installed
|
||||
span.text-success(ng-if="VM.xenTools === 'up to date'") Installed
|
||||
span(ng-if="!VM.xenTools") NOT installed
|
||||
span.text-warning(ng-if="VM.xenTools === 'out of date'") Outdated
|
||||
dt(ng-if="refreshStatControl.running && stats && VM.os_version") OS
|
||||
dd(ng-if="refreshStatControl.running && stats && VM.os_version")
|
||||
| {{VM.os_version.name}} ({{VM.os_version.distro}})
|
||||
@@ -231,102 +257,128 @@
|
||||
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:
|
||||
p.center.big
|
||||
i(class="xo-icon-{{VM.os_version.distro | osFamily}}",tooltip="{{VM.os_version.name}}", style="color: black;")
|
||||
i(class="icon-{{VM.os_version.distro | osFamily}}",tooltip="{{VM.os_version.name}}", style="color: black;")
|
||||
.grid-cell
|
||||
p.stat-name Xen tools:
|
||||
p.center
|
||||
span(ng-if="VM.PV_drivers", style="color:green;") Installed
|
||||
span(ng-if="!VM.PV_drivers") NOT installed
|
||||
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", type="button", style="width: 90%", xo-click="stopVM(VM.id)")
|
||||
i.fa.fa-stop.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(ng-if="VM.power_state == ('Running')")
|
||||
button.btn(tooltip="Suspend VM", type="button", style="width: 90%", xo-click="suspendVM(VM.id)")
|
||||
i.fa.fa-pause.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(ng-if="VM.power_state == ('Halted')")
|
||||
button.btn(tooltip="Start VM", type="button", style="width: 90%", xo-click="startVM(VM.id)")
|
||||
i.fa.fa-play.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(ng-if="VM.power_state == ('Running' || 'Paused')")
|
||||
button.btn(tooltip="Reboot VM", type="button", style="width: 90%", xo-click="rebootVM(VM.id)")
|
||||
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.dropdown-toggle(
|
||||
dropdown-toggle
|
||||
tooltip="Migrate VM"
|
||||
type="button"
|
||||
style="width: 90%"
|
||||
)
|
||||
i.fa.fa-share.fa-2x.fa-fw
|
||||
span.caret
|
||||
ul.dropdown-menu.left(role="menu")
|
||||
li(ng-repeat="host in hosts.all | orderBy:natural('name_label') track by host.id")
|
||||
a(ng-click="migrateVM(VM.id, host.id)")
|
||||
i.xo-icon-host.fa-fw
|
||||
| To {{host.name_label}}
|
||||
.grid-cell.btn-group(ng-if="VM.power_state == ('Running' || 'Paused')")
|
||||
button.btn(tooltip="Force Reboot", type="button", style="width: 90%", xo-click="force_rebootVM(VM.id)")
|
||||
i.fa.fa-flash.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(ng-if="VM.power_state == ('Halted')")
|
||||
button.btn(tooltip="Delete VM", type="button", style="width: 90%", xo-click="destroyVM(VM.id)")
|
||||
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(
|
||||
dropdown-toggle
|
||||
tooltip="Create a clone"
|
||||
style="width: 90%"
|
||||
type="button"
|
||||
)
|
||||
i.fa.fa-files-o.fa-2x.fa-fw
|
||||
span.caret
|
||||
ul.dropdown-menu.left(role="menu")
|
||||
li
|
||||
a(ng-click="cloneVM(VM.id,VM.name_label,false)")
|
||||
i.fa.fa-code-fork.fa-fw
|
||||
| Fast clone
|
||||
li
|
||||
a(ng-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", type="button", style="width: 90%", xo-click="convertVM(VM.id)")
|
||||
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", type="button", style="width: 90%", xo-click="force_stopVM(VM.id)")
|
||||
i.fa.fa-power-off.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(ng-if="VM.power_state == ('Suspended')")
|
||||
button.btn(tooltip="Resume VM", type="button", style="width: 90%", xo-click="resumeVM(VM.id)")
|
||||
i.fa.fa-play.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Create a snapshot", style="width: 90%", type="button", xo-click="snapshotVM(VM.id,VM.name_label)")
|
||||
i.xo-icon-snapshot.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Export the VM", style="width: 90%", type="button", xo-click="exportVM(VM.id)")
|
||||
i.fa.fa-download.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(style="margin-bottom: 0.5em")
|
||||
button.btn(tooltip="VM Console", 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 | map | orderBy:natural('name_label') track by h.id" ng-if="h!=host && canHostOperate(h.id)")
|
||||
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 writable_SRs | 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
|
||||
@@ -386,27 +438,30 @@
|
||||
i.xo-icon-disk
|
||||
| Disk
|
||||
span.quick-edit(
|
||||
ng-if="!disksForm.$visible"
|
||||
ng-if="!disksForm.$visible && canAdmin()"
|
||||
tooltip="Edit disks"
|
||||
ng-click="disksForm.$show()"
|
||||
)
|
||||
i.fa.fa-edit.fa-fw
|
||||
span.quick-edit(
|
||||
ng-if="disksForm.$visible"
|
||||
tooltip="Cancel Edition"
|
||||
tooltip="Cancel Edit"
|
||||
ng-click="disksForm.$cancel()"
|
||||
)
|
||||
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 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
|
||||
@@ -422,21 +477,46 @@
|
||||
)
|
||||
| {{VDI.name_description}}
|
||||
td
|
||||
//- FIXME: should be editable, but the server needs first
|
||||
//- to accept a human readable string.
|
||||
| {{VDI.size | bytesToSize}}
|
||||
xo-tag(object = 'VDI')
|
||||
td
|
||||
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
|
||||
span.pull-right.btn-group.quick-buttons(ng-if="canAdmin()")
|
||||
a(
|
||||
tooltip="Disconnect this disk"
|
||||
xo-click="disconnectVBD(VDI)"
|
||||
@@ -444,7 +524,7 @@
|
||||
i.fa.fa-unlink.fa-lg
|
||||
td(ng-if="!isConnected(VDI)")
|
||||
span.label.label-default Disconnected
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
span.pull-right.btn-group.quick-buttons(ng-if="canAdmin()")
|
||||
a(
|
||||
tooltip="Plug this disk"
|
||||
ng-if="VM.power_state == ('Running' || 'Paused')"
|
||||
@@ -497,20 +577,20 @@
|
||||
.container-fluid
|
||||
.row
|
||||
.col-sm-5
|
||||
iso-device(ng-if = 'VM && SRs', vm = 'VM', srs = 'SRs')
|
||||
iso-device(ng-if = '(VM && SRs) && canOperate()', vm = 'VM', srs = 'SRs')
|
||||
.col-sm-7.text-right
|
||||
div
|
||||
button.btn(type="button", ng-class = '{"btn-success": adding, "btn-primary": !adding}', ng-disabled="disksForm.$waiting", ng-click="adding = !adding;creatingVdi = false;bootReordering = false")
|
||||
button.btn(type="button", ng-class = '{"btn-success": adding, "btn-primary": !adding}', ng-disabled="disksForm.$waiting", ng-click="adding = !adding;creatingVdi = false;bootReordering = false", ng-hide = '!canAdmin()')
|
||||
i.fa.fa-plus(ng-if = '!adding')
|
||||
i.fa.fa-minus(ng-if = 'adding')
|
||||
| Attach Disk
|
||||
|
|
||||
button.btn(type="button", ng-class = '{"btn-success": creatingVdi, "btn-primary": !creatingVdi}', ng-disabled="disksForm.$waiting", ng-click="creatingVdi = !creatingVdi;adding = false;bootReordering = false")
|
||||
button.btn(type="button", ng-class = '{"btn-success": creatingVdi, "btn-primary": !creatingVdi}', ng-disabled="disksForm.$waiting", ng-click="creatingVdi = !creatingVdi;adding = false;bootReordering = false", ng-hide = '!canAdmin()')
|
||||
i.fa.fa-plus(ng-if = '!creatingVdi')
|
||||
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")
|
||||
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
|
||||
@@ -536,14 +616,14 @@
|
||||
input(ng-if = 'isFreeForWriting(vdiToAdd.vdi)', type='checkbox', ng-model = '$parent.$parent.vdiReadOnly')
|
||||
| Read-only
|
||||
.form-group(ng-if = 'vdiToAdd')
|
||||
button.btn.btn-primary(type = 'submit', ng-disabled="disksForm.$waiting")
|
||||
button.btn.btn-primary(type = 'submit', ng-disabled="disksForm.$waiting", ng-if = 'canAdmin()')
|
||||
| Add
|
||||
span(ng-if = 'addWaiting')
|
||||
|
|
||||
i.xo-icon-loading-sm
|
||||
br
|
||||
form#createDiskForm(ng-if = 'creatingVdi', name = 'createForm', ng-submit = 'createVdi(newDiskName, newDiskSize, newDiskSR, newDiskBootable, newDiskReadonly)')
|
||||
fieldset(ng-attr-disabled = '{{ createWaiting ? true : undefined }}')
|
||||
fieldset(ng-attr-disabled = '{{ createVdiWaiting ? true : undefined }}')
|
||||
.form-inline
|
||||
.form-group
|
||||
//- label(for = 'newDiskName') Name
|
||||
@@ -551,11 +631,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
|
||||
@@ -572,25 +652,25 @@
|
||||
option(value = '', disabled) --
|
||||
|
|
||||
.form-group
|
||||
button.btn.btn-primary(type = 'submit', ng-disabled="disksForm.$waiting")
|
||||
button.btn.btn-primary(type = 'submit', ng-disabled="disksForm.$waiting", ng-if = 'canAdmin()')
|
||||
i.fa.fa-plus-square
|
||||
| Create
|
||||
span(ng-if = 'createWaiting')
|
||||
span(ng-if = 'createVdiWaiting')
|
||||
|
|
||||
i.xo-icon-loading-sm
|
||||
br
|
||||
form#bootOrderForm(ng-if = 'bootReordering', ng-submit = 'saveBootParams(VM.id, bootParams)')
|
||||
fieldset(ng-attr-disabled = '{{ savingBootOrder ? true : undefined }}')
|
||||
fieldset(ng-disabled = 'savingBootOrder')
|
||||
.form-group(ng-repeat = 'elem in bootParams')
|
||||
label
|
||||
span(ng-class = '{"text-muted": !elem.v}') {{ elem.t }}
|
||||
input(type = 'checkbox', ng-model = 'elem.v')
|
||||
|
|
||||
button.btn.btn-default(type = 'button', ng-click = 'bootMove($index, -1)', ng-class = '{disabled: $first}'): i.fa.fa-chevron-up
|
||||
button.btn.btn-default(type = 'button', ng-click = 'bootMove($index, -1)', ng-disabled = '$first'): i.fa.fa-chevron-up
|
||||
|
|
||||
button.btn.btn-default(type = 'button', ng-click = 'bootMove($index, 1)', ng-class = '{disabled: $last}'): i.fa.fa-chevron-down
|
||||
button.btn.btn-default(type = 'button', ng-click = 'bootMove($index, 1)', ng-disabled = '$last'): i.fa.fa-chevron-down
|
||||
.form-group
|
||||
button.btn.btn-primary(type = 'submit', ng-disabled = 'disksForm.$waiting')
|
||||
button.btn.btn-primary(type = 'submit', ng-disabled = 'disksForm.$waiting', ng-if = 'canAdmin()')
|
||||
i.fa.fa-database
|
||||
| Save
|
||||
//- TODO: add interface in this panel
|
||||
@@ -613,21 +693,21 @@
|
||||
td
|
||||
| {{VIF.MTU}}
|
||||
td.oneliner
|
||||
| {{(VIF.$network | resolve).name_label}}
|
||||
span(ng-if = 'canView((VIF.$network | resolve).id)') {{(VIF.$network | resolve).name_label}}
|
||||
td(ng-if="VIF.attached")
|
||||
span.label.label-success Connected
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(tooltip="Disconnect this interface", ng-if="VM.power_state == ('Running' || 'Paused')", xo-click="disconnectVIF(VIF.id)")
|
||||
a(tooltip="Disconnect this interface", ng-if="VM.power_state == ('Running' || 'Paused') && canAdmin()", xo-click="disconnectVIF(VIF.id)")
|
||||
i.fa.fa-unlink.fa-lg
|
||||
td(ng-if="!VIF.attached")
|
||||
span.label.label-default Disconnected
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(tooltip="Connect this interface", xo-click="connectVIF(VIF.id)")
|
||||
a(tooltip="Connect this interface", xo-click="connectVIF(VIF.id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-link.fa-lg
|
||||
a(tooltip="Remove this interface", xo-click="deleteVIF(VIF.id)")
|
||||
a(tooltip="Remove this interface", xo-click="deleteVIF(VIF.id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-trash-o.fa-lg
|
||||
.text-right
|
||||
button.btn(type="button", ng-class = '{"btn-success": creatingVif, "btn-primary": !creatingVif}', ng-click="creatingVif = !creatingVif")
|
||||
button.btn(type="button", ng-class = '{"btn-success": creatingVif, "btn-primary": !creatingVif}', ng-click="creatingVif = !creatingVif", ng-hide = '!canAdmin()')
|
||||
i.fa.fa-plus(ng-if = '!creatingVif')
|
||||
i.fa.fa-minus(ng-if = 'creatingVif')
|
||||
| Create Interface
|
||||
@@ -637,11 +717,11 @@
|
||||
name = 'createInterfaceForm'
|
||||
ng-submit = 'createInterface(newInterfaceNetwork, newInterfaceMTU, autoMac, newInterfaceMAC)'
|
||||
)
|
||||
fieldset(ng-attr-disabled = '{{ createVifWaiting ? true : undefined }}')
|
||||
fieldset(ng-disabled = 'createVifWaiting')
|
||||
.form-group
|
||||
label(for = 'newVifNetwork') Network
|
||||
select.form-control(
|
||||
ng-options='network.name_label for network in networks'
|
||||
ng-options='network.name_label for network in networks | xoHideUnauthorized'
|
||||
ng-model = 'newInterfaceNetwork'
|
||||
ng-change = 'updateMTU(newInterfaceNetwork)'
|
||||
required
|
||||
@@ -649,21 +729,21 @@
|
||||
option(value = '', disabled) --
|
||||
|
|
||||
.form-group
|
||||
fieldset(ng-attr-disabled = '{{ autoMac ? true : undefined }}')
|
||||
fieldset(ng-disabled = 'autoMac')
|
||||
label.control-label(for = 'newInterfaceMAC') MAC address
|
||||
input#newInterfaceMAC.form-control(ng-class = '{hidden: autoMac}', type = 'text', ng-model = 'newInterfaceMAC', ng-required = '!autoMac')
|
||||
|
|
||||
.checkbox
|
||||
label
|
||||
input(type='checkbox', ng-model = 'autoMac')
|
||||
| Auto-generate
|
||||
| Auto-generate
|
||||
|
|
||||
.form-group
|
||||
label(for = 'newInterfaceMTU') MTU
|
||||
input#newInterfaceMTU.form-control(type = 'text', ng-model = 'newInterfaceMTU', required)
|
||||
|
|
||||
.form-group
|
||||
button.btn.btn-primary(type = 'submit')
|
||||
button.btn.btn-primary(type = 'submit', ng-if = 'canAdmin()')
|
||||
i.fa.fa-plus-square
|
||||
| Create
|
||||
span(ng-if = 'createVifWaiting')
|
||||
@@ -676,24 +756,28 @@
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-snapshot
|
||||
| Snapshots
|
||||
span.quick-edit(tooltip="Edit snapshots", ng-click="vmSnap.$show()")
|
||||
span.quick-edit(tooltip="Edit snapshots", ng-click="vmSnap.$show()", ng-if="!vmSnap.$visible && canAdmin()")
|
||||
i.fa.fa-edit.fa-fw
|
||||
span.quick-edit(ng-if="vmSnap.$visible", tooltip="Cancel Edit", ng-click="vmSnap.$cancel()")
|
||||
i.fa.fa-undo.fa-fw
|
||||
.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)")
|
||||
a(tooltip="Export this snapshot", type="button", xo-click="exportVM(snapshot.id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-upload.fa-lg
|
||||
a(tooltip="Revert VM to this snapshot", xo-click="revertSnapshot(snapshot.id)")
|
||||
a(tooltip="Revert VM to this snapshot", xo-click="revertSnapshot(snapshot.id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-undo.fa-lg
|
||||
a(tooltip="Remove this snapshot", xo-click="deleteSnapshot(snapshot.id)")
|
||||
a(tooltip="Remove this snapshot", xo-click="deleteSnapshot(snapshot.id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-trash-o.fa-lg
|
||||
.center(ng-if = '(VM.snapshots | count) > 5 || currentSnapPage > 1')
|
||||
pagination(boundary-links="true", total-items="VM.snapshots | count", ng-model="$parent.currentSnapPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
|
||||
@@ -711,7 +795,7 @@
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-comments
|
||||
| Logs
|
||||
span.quick-edit(ng-if="VM.messages | isNotEmpty", tooltip="Remove all logs", xo-click="deleteAllLog()")
|
||||
span.quick-edit(ng-if="(VM.messages | isNotEmpty) && canAdmin()", tooltip="Remove all logs", xo-click="deleteAllLog()")
|
||||
i.fa.fa-trash-o.fa-fw
|
||||
.panel-body
|
||||
p.center(ng-if="VM.messages | isEmpty") No recent logs
|
||||
@@ -722,12 +806,12 @@
|
||||
td.oneliner {{message.time*1e3 | date:"medium"}}
|
||||
td.oneliner
|
||||
| {{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 = '(VM.messages | count) > 5 || currentLogPage > 1')
|
||||
pagination(boundary-links="true", total-items="VM.messages | count", ng-model="$parent.currentLogPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
|
||||
.grid-sm
|
||||
.grid-sm(ng-if="canAdmin()")
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-plug
|
||||
@@ -744,12 +828,12 @@
|
||||
td(ng-if="pci.pci_id === VM.other.pci")
|
||||
span.label.label-success Attached
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(tooltip="Disconnect this PCI device", xo-click="disconnectPci(VM.id)")
|
||||
a(tooltip="Disconnect this PCI device", xo-click="disconnectPci(VM.id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-unlink.fa-lg
|
||||
td(ng-if="pci.pci_id !== VM.other.pci")
|
||||
span.label.label-default Not attached
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(tooltip="Connect this PCI device", xo-click="connectPci(VM.id, pci.pci_id)")
|
||||
a(tooltip="Connect this PCI device", xo-click="connectPci(VM.id, pci.pci_id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-link.fa-lg
|
||||
.center(ng-if = '((VM.$container | resolve).$PCIs | resolve).length > 5 || currentPCIPage > 1')
|
||||
pagination(boundary-links="true", total-items="((VM.$container | resolve).$PCIs | resolve).length", ng-model="$parent.currentPCIPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
|
||||
|
||||