Compare commits
1055 Commits
v3.9.1
...
xo-web/v4.
| 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 | ||
|
|
84bd077eac | ||
|
|
db39b27119 | ||
|
|
f2d2b35543 | ||
|
|
5dfd5766f2 | ||
|
|
0e4c3e1e92 | ||
|
|
221f42606c | ||
|
|
742f092ed3 | ||
|
|
36bffa1475 | ||
|
|
936abc1b1a | ||
|
|
584bdd545f | ||
|
|
99debc18d7 | ||
|
|
56b896eda0 | ||
|
|
cab102528d | ||
|
|
1875cdcda2 | ||
|
|
386dcc8d43 | ||
|
|
e6d59a47b1 | ||
|
|
2659393f33 | ||
|
|
916b2363d9 | ||
|
|
bb513790b5 | ||
|
|
e5ab15a727 | ||
|
|
bbaa750fda | ||
|
|
a46e19210a | ||
|
|
9d6772edd1 | ||
|
|
3aeaa564a2 | ||
|
|
8baad494e3 | ||
|
|
fb04753d52 | ||
|
|
ea37c4ccd8 | ||
|
|
80f02b52e1 | ||
|
|
55464845d6 | ||
|
|
8aef4bb455 | ||
|
|
d8a2adbca2 | ||
|
|
02e56da08a | ||
|
|
9d9f857e73 | ||
|
|
b140c1e65f | ||
|
|
81ff03462e | ||
|
|
2ea7c09c84 | ||
|
|
4163ed212c | ||
|
|
c83722c2df | ||
|
|
700db655e6 | ||
|
|
226428f631 | ||
|
|
e2b293e49b | ||
|
|
1f6e9d4660 | ||
|
|
ecf2ee888f | ||
|
|
1e8eeadb1d | ||
|
|
e7ceccdd83 | ||
|
|
5390b4a4b3 | ||
|
|
f25ec34bc3 | ||
|
|
53ece86816 | ||
|
|
9b128cdfcc | ||
|
|
c047386755 | ||
|
|
b0ffb272b3 | ||
|
|
956e21c8db | ||
|
|
95057a2b09 | ||
|
|
788aa24a80 | ||
|
|
0a72ef91cc | ||
|
|
f0f4e0985a | ||
|
|
a6bedea4b6 | ||
|
|
055316e1ca | ||
|
|
25fece5947 | ||
|
|
5ec3cdbcc5 | ||
|
|
b530ab2ef6 | ||
|
|
5164f60c98 | ||
|
|
19ba3015f7 | ||
|
|
fb00b2672c | ||
|
|
3e0f936d2a | ||
|
|
f5be146dbb | ||
|
|
95431a0874 | ||
|
|
f76b130ca4 | ||
|
|
d19f8259d0 | ||
|
|
68cd62d756 | ||
|
|
ea4a55d3dd | ||
|
|
788bdcd35b | ||
|
|
ebd7e24830 | ||
|
|
0b7fbffa0a | ||
|
|
2afda9a055 | ||
|
|
28dd275bd8 | ||
|
|
9a3cf182ac | ||
|
|
dd278c28be | ||
|
|
37572122b0 | ||
|
|
5348f75b5e | ||
|
|
6b8873d385 | ||
|
|
8db18d87e5 | ||
|
|
444920d15c | ||
|
|
8aa2fab603 | ||
|
|
9ded4386cc | ||
|
|
cc6b1b5aa1 | ||
|
|
74f20da82f | ||
|
|
4c9c838b70 | ||
|
|
9a9d27d37a | ||
|
|
0347d4cec4 | ||
|
|
b1b189288e | ||
|
|
b3220f981b | ||
|
|
a5573e62c6 | ||
|
|
c4ccee8df6 | ||
|
|
fbcf803d06 | ||
|
|
5247b7a9af | ||
|
|
dc218cc992 | ||
|
|
c21761d9d4 | ||
|
|
36c0bf06d7 | ||
|
|
ccdab2b083 | ||
|
|
15a8a56807 | ||
|
|
385d42281b | ||
|
|
b0dc933021 | ||
|
|
b73ee1f638 | ||
|
|
51c2a54179 | ||
|
|
2d71a916a2 | ||
|
|
5f9cf47003 | ||
|
|
16b39185dc | ||
|
|
6f0410f26e | ||
|
|
0b86845852 | ||
|
|
d5f914bd2f | ||
|
|
663c65e42e | ||
|
|
b9de86f96c | ||
|
|
bd9c0ffb25 | ||
|
|
9d763773cf | ||
|
|
540f977146 | ||
|
|
d16b09d3fc | ||
|
|
6f8a8d3b90 | ||
|
|
00ef4166c7 | ||
|
|
b88414735e | ||
|
|
af092fae9b | ||
|
|
b889efc913 | ||
|
|
877dd68a6b | ||
|
|
2805a1c7bc | ||
|
|
c5c000ea6f | ||
|
|
673f1072bf | ||
|
|
d0e93b9b9f | ||
|
|
f239088bcb | ||
|
|
32642f105c | ||
|
|
4adaf6d355 | ||
|
|
291e2a5e40 | ||
|
|
05bdb56203 | ||
|
|
cb71df8345 | ||
|
|
c6c5f5188b | ||
|
|
a7b6ca0914 | ||
|
|
30ba062695 | ||
|
|
a595af7b3f | ||
|
|
b2ee3172d8 | ||
|
|
73992ee8e9 | ||
|
|
78885fd00a | ||
|
|
ce55ac6ccb | ||
|
|
8ce0951e5f | ||
|
|
7788fa9d3e | ||
|
|
7f36552c71 | ||
|
|
16f9437b29 | ||
|
|
0beaff718e | ||
|
|
9b6f37b5d0 | ||
|
|
3d6d4aea6a | ||
|
|
2356a21e54 | ||
|
|
a55e7ed34f | ||
|
|
e355e4d35d | ||
|
|
6dcaf80f3f | ||
|
|
a465114d36 | ||
|
|
07fbcb3488 | ||
|
|
534fbe1b6e | ||
|
|
f5c9c1ba0e | ||
|
|
5d5485f569 | ||
|
|
3d3fa5d18a | ||
|
|
312c41f229 | ||
|
|
2df1dc9028 | ||
|
|
222f245e63 | ||
|
|
2aa7702aed | ||
|
|
0b185c35c2 | ||
|
|
48dcec3cc3 | ||
|
|
8567179fa3 | ||
|
|
79d15ecd7e | ||
|
|
837c7e4bc7 | ||
|
|
2ae7e9920d | ||
|
|
8cf955b674 | ||
|
|
33f897d43e | ||
|
|
ddb0946a0d | ||
|
|
0f5beac4a8 | ||
|
|
974e2f71f9 | ||
|
|
3c427d7e28 | ||
|
|
0f10c8f5df | ||
|
|
7840b51f5c | ||
|
|
6578855182 | ||
|
|
58d68497a4 | ||
|
|
bddcf42a54 | ||
|
|
6318f4e7ac | ||
|
|
0c6cced7ee | ||
|
|
925bf47c9e | ||
|
|
8472b991ff | ||
|
|
ed59c32d96 | ||
|
|
b1981d7499 | ||
|
|
8983dfea57 | ||
|
|
5231b9b22b | ||
|
|
55846a2314 | ||
|
|
1c94f5749d | ||
|
|
90bacd9d31 | ||
|
|
0053cbf782 | ||
|
|
5d120a79e8 | ||
|
|
3389569ea0 | ||
|
|
f546606de0 | ||
|
|
fef95b3aae | ||
|
|
5ba2b72439 | ||
|
|
4bb849f7c9 | ||
|
|
21b5e7e701 | ||
|
|
34a1965497 | ||
|
|
1701682636 | ||
|
|
5d826972f1 | ||
|
|
2467b336e5 | ||
|
|
4f78414c7f | ||
|
|
4532714bae | ||
|
|
352c23b0ba | ||
|
|
8e432ee818 | ||
|
|
47bb2d24f5 | ||
|
|
f3fd4c607d | ||
|
|
0610ceafdf | ||
|
|
032fcdce40 | ||
|
|
636bacd637 | ||
|
|
3f3fbd8bbc | ||
|
|
955e88b4fb | ||
|
|
5954b552c9 | ||
|
|
aaad4c5d20 | ||
|
|
a24c8526ea | ||
|
|
a533535520 | ||
|
|
badded3aa4 | ||
|
|
3055e612d4 | ||
|
|
525cb1a2b6 | ||
|
|
4dd70abc3b | ||
|
|
2ea4c214df | ||
|
|
0a0174a79d | ||
|
|
3db031be1b | ||
|
|
6d3a87fe7d | ||
|
|
8cfd2cdd79 | ||
|
|
9e874e076f | ||
|
|
28192bf184 | ||
|
|
a54957b4de | ||
|
|
f4b1a076b7 | ||
|
|
27a3296d6e | ||
|
|
1aaaee128f | ||
|
|
15a16a2c35 | ||
|
|
db23fe5a58 | ||
|
|
620c88b615 | ||
|
|
99f2fb9764 | ||
|
|
d5a3e67dbd | ||
|
|
55ef81f3e7 | ||
|
|
41699fab1e | ||
|
|
32a1195157 | ||
|
|
f53db2ddfa | ||
|
|
e060f9172b | ||
|
|
4adef88e61 | ||
|
|
d734f2cf89 | ||
|
|
3e81d14bd8 | ||
|
|
e88a94d9e0 | ||
|
|
f4f16e4e87 | ||
|
|
6268f3a3d9 | ||
|
|
06e7c8d19a | ||
|
|
32395232ea | ||
|
|
65d6ef91ff | ||
|
|
4aecc875d1 | ||
|
|
0e649a626c | ||
|
|
5fa249b0f3 | ||
|
|
24ca86aad3 | ||
|
|
8a4f413289 | ||
|
|
6dbad4501d | ||
|
|
9ab6490fee | ||
|
|
a413efa550 | ||
|
|
cd337d444c | ||
|
|
45e1ce0a42 | ||
|
|
e5ef1e6efe | ||
|
|
b1ce3be3d2 | ||
|
|
e13ab73a29 | ||
|
|
aede952b12 | ||
|
|
acc1476b29 | ||
|
|
138bf56624 | ||
|
|
c608de4183 | ||
|
|
ccb6c02c31 | ||
|
|
5cc457b28c | ||
|
|
a353b3d40d | ||
|
|
6f7aca8e5b | ||
|
|
92b0d4561e | ||
|
|
ef8b8346dc | ||
|
|
058058a015 | ||
|
|
fddba7315a | ||
|
|
a5e964ea19 | ||
|
|
3d2152e559 | ||
|
|
50f9c68c26 | ||
|
|
b40207b367 | ||
|
|
6c9305d2b1 | ||
|
|
9fda3c911d | ||
|
|
473c3601ef | ||
|
|
fde8a3720d | ||
|
|
13a6d6b458 | ||
|
|
29d9ba0446 | ||
|
|
71e271774e | ||
|
|
c9db49e255 | ||
|
|
22f35f0e86 | ||
|
|
375f3ac3ac | ||
|
|
b60a02bc34 | ||
|
|
5a4d821c98 | ||
|
|
cd0305c71d | ||
|
|
371459ff5e | ||
|
|
5a8a7c6a0f | ||
|
|
69db541300 | ||
|
|
94949866ee | ||
|
|
c22b3e7449 | ||
|
|
096dde922b | ||
|
|
00f26d854f | ||
|
|
6c8ff1717e | ||
|
|
c7288c1d8a | ||
|
|
2e52fe369d | ||
|
|
df3430add5 | ||
|
|
7af848c94b | ||
|
|
f57c462b5f | ||
|
|
6018035908 | ||
|
|
c8b0351786 | ||
|
|
26cc812f82 | ||
|
|
67f98950e6 | ||
|
|
8ba8537b9f | ||
|
|
a7f05a68e0 | ||
|
|
a0db228154 | ||
|
|
eec6fabe58 | ||
|
|
501c038f97 | ||
|
|
e0a0f717fd | ||
|
|
4dd3f7487c | ||
|
|
99d1cddaa5 | ||
|
|
2158e1a47e | ||
|
|
059238759a | ||
|
|
8d3ea7548a | ||
|
|
221b411b63 | ||
|
|
e2c173990f | ||
|
|
a609a8d5d6 | ||
|
|
c5c2afddc2 | ||
|
|
409d87f210 | ||
|
|
78baa4b01e |
12
.babelrc
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"comments": false,
|
||||
"compact": true,
|
||||
"plugins": [
|
||||
"transform-decorators-legacy",
|
||||
"transform-runtime"
|
||||
],
|
||||
"presets": [
|
||||
"stage-0",
|
||||
"es2015"
|
||||
]
|
||||
}
|
||||
@@ -46,7 +46,7 @@ indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
# Less
|
||||
[*.js]
|
||||
[*.less]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
|
||||
4
.gitignore
vendored
@@ -4,3 +4,7 @@
|
||||
/node_modules/*
|
||||
!/node_modules/*.js
|
||||
/node_modules/*.js/
|
||||
|
||||
jsconfig.json
|
||||
.idea
|
||||
npm-debug.log
|
||||
|
||||
94
.jshintrc
@@ -1,94 +0,0 @@
|
||||
{
|
||||
// Julien Fontanet JSHint configuration
|
||||
// https://gist.github.com/julien-f/8095615
|
||||
//
|
||||
// Changes from defaults:
|
||||
// - all enforcing options (except `++` & `--`) enabled
|
||||
// - single quotes
|
||||
// - indentation set to 2 instead of 4
|
||||
// - almost all relaxing options disabled
|
||||
// - allow expression statements (necessary for chai.expect())
|
||||
// - environments are set to Browserify, mocha & Node.js
|
||||
//
|
||||
// See http://jshint.com/docs/ for more details
|
||||
|
||||
"maxerr" : 50, // {int} Maximum error before stopping
|
||||
|
||||
// Enforcing
|
||||
"bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.)
|
||||
"camelcase" : true, // true: Identifiers must be in camelCase
|
||||
"curly" : true, // true: Require {} for every new block or scope
|
||||
"eqeqeq" : true, // true: Require triple equals (===) for comparison
|
||||
"forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty()
|
||||
"freeze" : true, // true: Prohibit overwriting prototypes of native objects (Array, Date, ...)
|
||||
"immed" : true, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());`
|
||||
"indent" : 2, // {int} Number of spaces to use for indentation
|
||||
"latedef" : true, // true: Require variables/functions to be defined before being used
|
||||
"newcap" : true, // true: Require capitalization of all constructor functions e.g. `new F()`
|
||||
"noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee`
|
||||
"noempty" : true, // true: Prohibit use of empty blocks
|
||||
"nonbsp" : true, // true: Prohibit use of non breakable spaces
|
||||
"nonew" : true, // true: Prohibit use of constructors for side-effects (without assignment)
|
||||
"plusplus" : false, // true: Prohibit use of `++` & `--`
|
||||
"quotmark" : "single", // Quotation mark consistency:
|
||||
// false : do nothing (default)
|
||||
// true : ensure whatever is used is consistent
|
||||
// "single" : require single quotes
|
||||
// "double" : require double quotes
|
||||
"undef" : true, // true: Require all non-global variables to be declared (prevents global leaks)
|
||||
"unused" : true, // true: Require all defined variables be used
|
||||
"strict" : false, // true: Requires all functions run in ES5 Strict Mode
|
||||
"maxcomplexity" : 7, // {int} Max cyclomatic complexity per function
|
||||
"maxdepth" : 3, // {int} Max depth of nested blocks (within functions)
|
||||
"maxlen" : 80, // {int} Max number of characters per line
|
||||
"maxparams" : 4, // {int} Max number of formal params allowed per function
|
||||
"maxstatements" : 20, // {int} Max number statements per function
|
||||
|
||||
// Relaxing
|
||||
"asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons)
|
||||
"boss" : false, // true: Tolerate assignments where comparisons would be expected
|
||||
"debug" : false, // true: Allow debugger statements e.g. browser breakpoints.
|
||||
"eqnull" : false, // true: Tolerate use of `== null`
|
||||
"esnext" : true, // true: Allow ES.next (ES6) syntax (ex: `const`)
|
||||
"evil" : false, // true: Tolerate use of `eval` and `new Function()`
|
||||
"expr" : false, // true: Tolerate `ExpressionStatement` as Programs
|
||||
"funcscope" : false, // true: Tolerate defining variables inside control statements
|
||||
"globalstrict" : false, // true: Allow global "use strict" (also enables 'strict')
|
||||
"iterator" : false, // true: Tolerate using the `__iterator__` property
|
||||
"lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block
|
||||
"laxbreak" : false, // true: Tolerate possibly unsafe line breakings
|
||||
"laxcomma" : false, // true: Tolerate comma-first style coding
|
||||
"loopfunc" : false, // true: Tolerate functions being defined in loops
|
||||
"moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features)
|
||||
// (ex: `for each`, multiple try/catch, function expression…)
|
||||
"multistr" : false, // true: Tolerate multi-line strings
|
||||
"notypeof" : false, // true: Tolerate typeof comparison with unknown values.
|
||||
"proto" : false, // true: Tolerate using the `__proto__` property
|
||||
"scripturl" : false, // true: Tolerate script-targeted URLs
|
||||
"shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;`
|
||||
"sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation
|
||||
"supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;`
|
||||
"validthis" : false, // true: Tolerate using this in a non-constructor function
|
||||
"noyield" : false, // true: Tolerate generators without yields
|
||||
|
||||
// Environments
|
||||
"browser" : false, // Web Browser (window, document, etc)
|
||||
"browserify" : true, // Browserify (node.js code in the browser)
|
||||
"couch" : false, // CouchDB
|
||||
"devel" : false, // Development/debugging (alert, confirm, etc)
|
||||
"dojo" : false, // Dojo Toolkit
|
||||
"jquery" : false, // jQuery
|
||||
"mocha" : false, // mocha
|
||||
"mootools" : false, // MooTools
|
||||
"node" : false, // Node.js
|
||||
"nonstandard" : false, // Widely adopted globals (escape, unescape, etc)
|
||||
"phantom" : false, // PhantomJS
|
||||
"prototypejs" : false, // Prototype and Scriptaculous
|
||||
"rhino" : false, // Rhino
|
||||
"worker" : false, // Web Workers
|
||||
"wsh" : false, // Windows Scripting Host
|
||||
"yui" : false, // Yahoo User Interface
|
||||
|
||||
// Custom Globals
|
||||
"globals" : {} // additional predefined global variables
|
||||
}
|
||||
9
.npmignore
Normal file
@@ -0,0 +1,9 @@
|
||||
/examples/
|
||||
example.js
|
||||
*.example.js
|
||||
*.example.js.map
|
||||
|
||||
/test/
|
||||
/tests/
|
||||
*.spec.js
|
||||
*.spec.js.map
|
||||
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
|
||||
512
CHANGELOG.md
@@ -1,11 +1,513 @@
|
||||
# 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.
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Dataviz storage usage ([xo-web#311](https://github.com/vatesfr/xo-web/issues/311))
|
||||
- Heatmap in health view ([xo-web#329](https://github.com/vatesfr/xo-web/issues/329))
|
||||
- SSO for SAML and other providers ([xo-web#327](https://github.com/vatesfr/xo-web/issues/327))
|
||||
- Better UI for ACL objects attribution ([xo-web#320](https://github.com/vatesfr/xo-web/issues/320))
|
||||
- Refresh the browser after an update ([xo-web#318](https://github.com/vatesfr/xo-web/issues/318))
|
||||
- Clean CSS and Flexbox usage ([xo-web#239](https://github.com/vatesfr/xo-web/issues/239))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Admin only accessible views ([xo-web#328](https://github.com/vatesfr/xo-web/issues/328))
|
||||
- Hide "base copy" VDIs ([xo-web#324](https://github.com/vatesfr/xo-web/issues/324))
|
||||
- ACLs on VIFs for non-admins ([xo-web#322](https://github.com/vatesfr/xo-web/issues/322))
|
||||
- Updater display problems ([xo-web#313](https://github.com/vatesfr/xo-web/issues/313))
|
||||
|
||||
## **4.3.0** (2015-07-22)
|
||||
|
||||
Scheduler for rolling backups
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Rolling backup scheduler ([xo-web#278](https://github.com/vatesfr/xo-web/issues/278))
|
||||
- Clean snapshots of removed VMs ([xo-web#301](https://github.com/vatesfr/xo-web/issues/301))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- VM export ([xo-web#307](https://github.com/vatesfr/xo-web/issues/307))
|
||||
- Remove VM VDIs ([xo-web#303](https://github.com/vatesfr/xo-web/issues/303))
|
||||
- Pagination fails ([xo-web#302](https://github.com/vatesfr/xo-web/issues/302))
|
||||
|
||||
## **4.2.0** (2015-06-29)
|
||||
|
||||
Huge performance boost, scheduler for rolling snapshots and backward compatibility for XS 5.x series
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Rolling snapshots scheduler ([xo-web#176](https://github.com/vatesfr/xo-web/issues/176))
|
||||
- Huge perf boost ([xen-api#1](https://github.com/julien-f/js-xen-api/issues/1))
|
||||
- Backward compatibility ([xo-web#296](https://github.com/vatesfr/xo-web/issues/296))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- VDI attached on a VM missing in SR view ([xo-web#294](https://github.com/vatesfr/xo-web/issues/294))
|
||||
- Better VM creation process ([xo-web#292](https://github.com/vatesfr/xo-web/issues/292))
|
||||
|
||||
## **4.1.0** (2015-06-10)
|
||||
|
||||
Add the drag'n drop support from VM live migration, better ACLs groups UI.
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Drag'n drop VM in tree view for live migration ([xo-web#277](https://github.com/vatesfr/xo-web/issues/277))
|
||||
- Better group view with objects ACLs ([xo-web#276](https://github.com/vatesfr/xo-web/issues/276))
|
||||
- Hide non-visible objects ([xo-web#272](https://github.com/vatesfr/xo-web/issues/272))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Convert to template displayed when the VM is not halted ([xo-web#286](https://github.com/vatesfr/xo-web/issues/286))
|
||||
- Lost some data when refresh some views ([xo-web#271](https://github.com/vatesfr/xo-web/issues/271))
|
||||
- Suspend button don't trigger any permission message ([xo-web#270](https://github.com/vatesfr/xo-web/issues/270))
|
||||
- Create network interfaces shouldn't call xoApi directly ([xo-web#269](https://github.com/vatesfr/xo-web/issues/269))
|
||||
- Don't plug automatically a disk or a VIF if the VM is not running ([xo-web#287](https://github.com/vatesfr/xo-web/issues/287))
|
||||
|
||||
## **4.0.2** (2015-06-01)
|
||||
|
||||
An issue in `xo-server` with the password of default admin account and also a UI fix.
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Cannot modify admin account ([xo-web#265](https://github.com/vatesfr/xo-web/issues/265))
|
||||
- Password field seems to keep empty/reset itself after 1-2 seconds ([xo-web#264](https://github.com/vatesfr/xo-web/issues/264))
|
||||
|
||||
## **4.0.1** (2015-05-30)
|
||||
|
||||
An issue with the updater in HTTPS was left in the *4.0.0*. This patch release fixed
|
||||
it.
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- allow updater to work in HTTPS ([xo-web#266](https://github.com/vatesfr/xo-web/issues/266))
|
||||
|
||||
## **4.0.0** (2015-05-29)
|
||||
|
||||
[Blog post of this release](https://xen-orchestra.com/blog/xen-orchestra-4-0).
|
||||
|
||||
### Enhancements
|
||||
|
||||
- advanced ACLs ([xo-web#209](https://github.com/vatesfr/xo-web/issues/209))
|
||||
- xenserver update management ([xo-web#174](https://github.com/vatesfr/xo-web/issues/174) & [xo-web#259](https://github.com/vatesfr/xo-web/issues/259))
|
||||
- docker control ([xo-web#211](https://github.com/vatesfr/xo-web/issues/211))
|
||||
- better responsive design ([xo-web#252](https://github.com/vatesfr/xo-web/issues/252))
|
||||
- host stats ([xo-web#255](https://github.com/vatesfr/xo-web/issues/255))
|
||||
- pagination ([xo-web#221](https://github.com/vatesfr/xo-web/issues/221))
|
||||
- web updater
|
||||
- better VM creation process([xo-web#256](https://github.com/vatesfr/xo-web/issues/256))
|
||||
- VM boot order([xo-web#251](https://github.com/vatesfr/xo-web/issues/251))
|
||||
- new mapped collection([xo-server#47](https://github.com/vatesfr/xo-server/issues/47))
|
||||
- resource location in ACL view ([xo-web#245](https://github.com/vatesfr/xo-web/issues/245))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- wrong calulation of RAM amounts ([xo-web#51](https://github.com/vatesfr/xo-web/issues/51))
|
||||
- checkbox not aligned ([xo-web#253](https://github.com/vatesfr/xo-web/issues/253))
|
||||
- VM stats behavior more robust ([xo-web#250](https://github.com/vatesfr/xo-web/issues/250))
|
||||
- XO not on the root of domain ([xo-web#254](https://github.com/vatesfr/xo-web/issues/254))
|
||||
|
||||
|
||||
## **3.9.1** (2015-04-21)
|
||||
|
||||
A few bugs hve made their way into *3.9.0*, this minor release fixes
|
||||
them.
|
||||
|
||||
## Bug fixes
|
||||
### Bug fixes
|
||||
|
||||
- correctly keep the VM guest metrics up to date ([xo-web#172](https://github.com/vatesfr/xo-web/issues/172))
|
||||
- fix edition of a VM snapshot ([b04111c](https://github.com/vatesfr/xo-server/commit/b04111c79ba8937778b84cb861bb7c2431162c11))
|
||||
@@ -18,7 +520,7 @@ them.
|
||||
|
||||
[Blog post of this release](https://xen-orchestra.com/blog/xen-orchestra-3-9).
|
||||
|
||||
## Enhancements
|
||||
### Enhancements
|
||||
|
||||
- ability to manually connect/disconnect a server ([xo-web#88](https://github.com/vatesfr/xo-web/issues/88) & [xo-web#234](https://github.com/vatesfr/xo-web/issues/234))
|
||||
- display the connection status of a server ([xo-web#103](https://github.com/vatesfr/xo-web/issues/103))
|
||||
@@ -32,7 +534,7 @@ them.
|
||||
- XO-Server sources are compiled to JS prior distribution: less bugs & faster startups ([xo-server#50](https://github.com/vatesfr/xo-server/issues/50))
|
||||
- use XAPI `event.from()` instead of `event.next()` which leads to faster connection ([xo-server#52](https://github.com/vatesfr/xo-server/issues/52))
|
||||
|
||||
## Bug fixes
|
||||
### Bug fixes
|
||||
|
||||
- removed servers are properly disconnected ([xo-web#61](https://github.com/vatesfr/xo-web/issues/61))
|
||||
- fix VM creation with multiple interfaces ([xo-wb#229](https://github.com/vatesfr/xo-wb/issues/229))
|
||||
@@ -42,7 +544,7 @@ them.
|
||||
|
||||
[Blog post of this release](https://xen-orchestra.com/blog/xen-orchestra-3-8).
|
||||
|
||||
## Enhancements
|
||||
### Enhancements
|
||||
|
||||
- initial plugin system ([xo-server#37](https://github.com/vatesfr/xo-server/issues/37))
|
||||
- new authentication system based on providers ([xo-server#39](https://github.com/vatesfr/xo-server/issues/39))
|
||||
@@ -51,7 +553,7 @@ them.
|
||||
- network creation on the VM page ([xo-web#216](https://github.com/vatesfr/xo-web/issues/216))
|
||||
- charts on the host and SR pages ([xo-web#217](https://github.com/vatesfr/xo-web/issues/217))
|
||||
|
||||
## Bug fixes
|
||||
### Bug fixes
|
||||
|
||||
- fix *Invalid parameter(s)* message on the settings page ([xo-server#49](https://github.com/vatesfr/xo-server/issues/49))
|
||||
- fix mouse clicks in console ([xo-web#205](https://github.com/vatesfr/xo-web/issues/205))
|
||||
|
||||
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 -->
|
||||
17
README.md
@@ -1,5 +1,7 @@
|
||||
# Xen Orchestra Web
|
||||
|
||||

|
||||
|
||||
XO-Web is part of [Xen Orchestra](https://github.com/vatesfr/xo), a web interface for XenServer or XAPI enabled hosts.
|
||||
|
||||
It is a web client for [XO-Server](https://github.com/vatesfr/xo-server).
|
||||
@@ -31,13 +33,14 @@ $ 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
|
||||
|
||||
# Merge changes of the next-release branch.
|
||||
git merge next-release
|
||||
@@ -48,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 pull --fast-forward master
|
||||
git merge --ff-only stable
|
||||
|
||||
# Push the changes on git.
|
||||
git push origin master:master next-release:next-release
|
||||
git push --follow-tags origin stable next-release
|
||||
|
||||
# Publish this release to npm.
|
||||
npm publish
|
||||
|
||||
201
app/app.js
@@ -1,39 +1,39 @@
|
||||
// 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'
|
||||
import uiSelect from'angular-ui-select'
|
||||
|
||||
import angular from 'angular';
|
||||
import uiBootstrap from'angular-ui-bootstrap';
|
||||
import uiIndeterminate from'angular-ui-indeterminate';
|
||||
import uiRouter from'angular-ui-router';
|
||||
import uiSelect from'angular-ui-select';
|
||||
import naturalSort from 'angular-natural-sort'
|
||||
import xeditable from 'angular-xeditable'
|
||||
|
||||
import naturalSort from 'angular-natural-sort';
|
||||
import xeditable from 'angular-xeditable';
|
||||
import xoDirectives from 'xo-directives'
|
||||
import xoFilters from 'xo-filters'
|
||||
import xoServices from 'xo-services'
|
||||
|
||||
import xoDirectives from 'xo-directives';
|
||||
import xoFilters from 'xo-filters';
|
||||
import xoServices from 'xo-services';
|
||||
import aboutState from './modules/about'
|
||||
import backupState from './modules/backup'
|
||||
import consoleState from './modules/console'
|
||||
import dashboardState from './modules/dashboard'
|
||||
import deleteVmsState from './modules/delete-vms'
|
||||
import genericModalState from './modules/generic-modal'
|
||||
import hostState from './modules/host'
|
||||
import listState from './modules/list'
|
||||
import migrateVmState from './modules/migrate-vm'
|
||||
import navbarState from './modules/navbar'
|
||||
import newSrState from './modules/new-sr'
|
||||
import newVmState from './modules/new-vm'
|
||||
import poolState from './modules/pool'
|
||||
import selfState from './modules/self'
|
||||
import settingsState from './modules/settings'
|
||||
import srState from './modules/sr'
|
||||
import taskScheduler from './modules/task-scheduler'
|
||||
import treeState from './modules/tree'
|
||||
import updater from './modules/updater'
|
||||
import vmState from './modules/vm'
|
||||
|
||||
import aboutState from './modules/about';
|
||||
import consoleState from './modules/console';
|
||||
import deleteVmsState from './modules/delete-vms';
|
||||
import genericModalState from './modules/generic-modal';
|
||||
import hostState from './modules/host';
|
||||
import listState from './modules/list';
|
||||
import loginState from './modules/login';
|
||||
import navbarState from './modules/navbar';
|
||||
import newSrState from './modules/new-sr';
|
||||
import newVmState from './modules/new-vm';
|
||||
import poolState from './modules/pool';
|
||||
import settingsState from './modules/settings';
|
||||
import srState from './modules/sr';
|
||||
import treeState from './modules/tree';
|
||||
import vmState from './modules/vm';
|
||||
import isoDevice from './modules/iso-device';
|
||||
|
||||
import '../dist/bower_components/angular-chart.js/dist/angular-chart.js';
|
||||
|
||||
//====================================================================
|
||||
// ===================================================================
|
||||
|
||||
export default angular.module('xoWebApp', [
|
||||
uiBootstrap,
|
||||
@@ -41,6 +41,7 @@ export default angular.module('xoWebApp', [
|
||||
uiRouter,
|
||||
uiSelect,
|
||||
|
||||
angularChartJs,
|
||||
naturalSort,
|
||||
xeditable,
|
||||
|
||||
@@ -49,28 +50,31 @@ export default angular.module('xoWebApp', [
|
||||
xoServices,
|
||||
|
||||
aboutState,
|
||||
backupState,
|
||||
consoleState,
|
||||
dashboardState,
|
||||
deleteVmsState,
|
||||
genericModalState,
|
||||
hostState,
|
||||
listState,
|
||||
loginState,
|
||||
migrateVmState,
|
||||
navbarState,
|
||||
newSrState,
|
||||
newVmState,
|
||||
poolState,
|
||||
selfState,
|
||||
settingsState,
|
||||
srState,
|
||||
taskScheduler,
|
||||
treeState,
|
||||
vmState,
|
||||
isoDevice,
|
||||
'chart.js'
|
||||
updater,
|
||||
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 (
|
||||
@@ -86,96 +90,123 @@ export default angular.module('xoWebApp', [
|
||||
// the console.
|
||||
//
|
||||
// See https://docs.angularjs.org/guide/production
|
||||
$compileProvider.debugInfoEnabled(false);
|
||||
$compileProvider.debugInfoEnabled(false)
|
||||
|
||||
// Redirect to default state.
|
||||
$stateProvider.state('index', {
|
||||
url: '/',
|
||||
controller: function ($state, xoApi) {
|
||||
let isAdmin = xoApi.user && (xoApi.user.permission === 'admin');
|
||||
let isAdmin = xoApi.user && (xoApi.user.permission === 'admin')
|
||||
|
||||
$state.go(isAdmin ? 'tree' : 'list');
|
||||
},
|
||||
});
|
||||
$state.go(isAdmin ? 'tree' : 'list')
|
||||
}
|
||||
})
|
||||
|
||||
// Redirects unmatched URLs to `/`.
|
||||
$urlRouterProvider.otherwise('/');
|
||||
$urlRouterProvider.otherwise('/')
|
||||
|
||||
// Changes the default settings for the tooltips.
|
||||
$tooltipProvider.options({
|
||||
appendToBody: true,
|
||||
placement: 'bottom',
|
||||
});
|
||||
placement: 'bottom'
|
||||
})
|
||||
|
||||
uiSelectConfig.theme = 'bootstrap';
|
||||
uiSelectConfig.resetSearchInput = true;
|
||||
uiSelectConfig.theme = 'bootstrap'
|
||||
uiSelectConfig.resetSearchInput = true
|
||||
})
|
||||
.run(function (
|
||||
$anchorScroll,
|
||||
$cookies,
|
||||
$rootScope,
|
||||
$state,
|
||||
editableOptions,
|
||||
editableThemes,
|
||||
modal,
|
||||
notify,
|
||||
xoApi,
|
||||
xo
|
||||
updater,
|
||||
xoApi
|
||||
) {
|
||||
$rootScope.$on('$stateChangeStart', function (event, state, stateParams) {
|
||||
let {user} = xoApi;
|
||||
let loggedIn = !!user;
|
||||
// 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)
|
||||
}
|
||||
|
||||
if (state.name === 'login') {
|
||||
if (loggedIn) {
|
||||
event.preventDefault();
|
||||
$state.go('index');
|
||||
let requestedStateName, requestedStateParams
|
||||
|
||||
$rootScope.$watch(() => xoApi.user, (user, previous) => {
|
||||
// The user just signed in.
|
||||
if (user && !previous) {
|
||||
if (requestedStateName) {
|
||||
$state.go(requestedStateName, requestedStateParams)
|
||||
requestedStateName = requestedStateParams = null
|
||||
} else {
|
||||
$state.go('index')
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
})
|
||||
|
||||
if (!loggedIn) {
|
||||
event.preventDefault();
|
||||
$rootScope.$on('$stateChangeStart', function (event, state, stateParams, fromState) {
|
||||
const { user } = xoApi
|
||||
if (!user) {
|
||||
event.preventDefault()
|
||||
|
||||
// FIXME: find a better way to pass info to the login controller.
|
||||
$rootScope._login = { state, stateParams };
|
||||
requestedStateName = state.name
|
||||
requestedStateParams = stateParams
|
||||
|
||||
$state.go('login');
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
if (user.permission === 'admin') {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
// The user must have the `admin` permission to access the
|
||||
// settings pages.
|
||||
if (/^settings\..*|tree$/.test(state.name)) {
|
||||
event.preventDefault();
|
||||
function forbidState () {
|
||||
event.preventDefault()
|
||||
notify.error({
|
||||
title: 'Restricted area',
|
||||
message: 'You do not have the permission to view this page',
|
||||
});
|
||||
message: 'You do not have the permission to view this page'
|
||||
})
|
||||
|
||||
if (fromState.url === '^') {
|
||||
$state.go('index')
|
||||
}
|
||||
}
|
||||
|
||||
let {id} = stateParams;
|
||||
if (id && !xo.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
|
||||
}
|
||||
})
|
||||
|
||||
// Work around UI Router bug (https://github.com/angular-ui/ui-router/issues/1509)
|
||||
$rootScope.$on('$stateChangeSuccess', function () {
|
||||
$anchorScroll();
|
||||
});
|
||||
$anchorScroll()
|
||||
})
|
||||
|
||||
editableThemes.bs3.inputClass = 'input-sm';
|
||||
editableThemes.bs3.buttonsClass = 'btn-sm';
|
||||
editableOptions.theme = 'bs3';
|
||||
editableThemes.bs3.inputClass = 'input-sm'
|
||||
editableThemes.bs3.buttonsClass = 'btn-sm'
|
||||
editableOptions.theme = 'bs3'
|
||||
})
|
||||
|
||||
.name
|
||||
;
|
||||
|
||||
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 |
@@ -46,7 +46,6 @@ html.no-js(lang="en", dir="ltr")
|
||||
|
||||
//- Place favicon.ico and apple-touch-icon.png in the root directory
|
||||
link(rel="stylesheet", href="styles/main.css")
|
||||
link(rel="stylesheet", href="bower_components/angular-chart.js/dist/angular-chart.css")
|
||||
body(
|
||||
ng-app = 'xoWebApp'
|
||||
)
|
||||
@@ -59,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")
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
import angular from 'angular';
|
||||
import uiRouter from 'angular-ui-router';
|
||||
import angular from 'angular'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
|
||||
import pkg from '../../../package';
|
||||
import pkg from '../../../package'
|
||||
|
||||
//====================================================================
|
||||
// ===================================================================
|
||||
|
||||
module.exports = angular.module('xoWebApp.about', [
|
||||
uiRouter,
|
||||
export default angular.module('xoWebApp.about', [
|
||||
uiRouter
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('about', {
|
||||
url: '/about',
|
||||
controller: 'AboutCtrl',
|
||||
template: require('./view'),
|
||||
});
|
||||
template: require('./view')
|
||||
})
|
||||
})
|
||||
.controller('AboutCtrl', function ($scope) {
|
||||
$scope.pkg = pkg;
|
||||
.controller('AboutCtrl', function ($scope, xo) {
|
||||
xo.system.getServerVersion().then(version =>
|
||||
$scope.serverVersion = version
|
||||
)
|
||||
$scope.pkg = pkg
|
||||
})
|
||||
// A module exports its name.
|
||||
.name
|
||||
;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
//- TODO: lots of stuff.
|
||||
.grid
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
p.page-title About Xen Orchestra
|
||||
p.text-center ({{pkg.name}} {{pkg.version}})
|
||||
.grid
|
||||
p.text-center ({{pkg.name}} {{pkg.version}} - xo-server {{serverVersion}})
|
||||
.grid-sm
|
||||
//- Vates
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-lightbulb-o(style="color: #e25440;")
|
||||
i.fa.fa-lightbulb-o
|
||||
| Vates
|
||||
.panel-body
|
||||
p.text-center
|
||||
@@ -22,7 +22,7 @@
|
||||
//- Open Source
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-thumbs-up(style="color: #e25440;")
|
||||
i.fa.fa-thumbs-up
|
||||
| Open Source
|
||||
.panel-body
|
||||
p.text-center
|
||||
@@ -37,7 +37,7 @@
|
||||
//- Pro support
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-truck(style="color: #e25440;")
|
||||
i.fa.fa-truck
|
||||
| Pro Support Delivered
|
||||
.panel-body
|
||||
p.text-center
|
||||
@@ -45,6 +45,6 @@
|
||||
p.text-center
|
||||
img(src="images/support.png")
|
||||
p.text-center
|
||||
a.btn.btn-primary(href="https://xen-orchestra.com/services/")
|
||||
a.btn.btn-primary(href="https://vates.fr/services.html")
|
||||
i.fa.fa-envelope
|
||||
| Get services
|
||||
|
||||
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
|
||||
262
app/modules/backup/delta-backup/index.js
Normal file
@@ -0,0 +1,262 @@
|
||||
import angular from 'angular'
|
||||
import filter from 'lodash.filter'
|
||||
import find from 'lodash.find'
|
||||
import forEach from 'lodash.foreach'
|
||||
import map from 'lodash.map'
|
||||
import prettyCron from 'prettycron'
|
||||
import size from 'lodash.size'
|
||||
import trim from 'lodash.trim'
|
||||
import uiBootstrap from 'angular-ui-bootstrap'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
import { parse } from 'xo-remote-parser'
|
||||
|
||||
import view from './view'
|
||||
|
||||
// ====================================================================
|
||||
|
||||
export default angular.module('backup.deltaBackup', [
|
||||
uiRouter,
|
||||
uiBootstrap
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('backup.deltaBackup', {
|
||||
url: '/delta-backup/:id',
|
||||
controller: 'DeltaBackupCtrl as ctrl',
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('DeltaBackupCtrl', function ($scope, $stateParams, $interval, xo, xoApi, notify, selectHighLevelFilter, filterFilter) {
|
||||
const JOBKEY = 'deltaBackup'
|
||||
|
||||
this.ready = false
|
||||
|
||||
this.running = {}
|
||||
this.comesForEditing = $stateParams.id
|
||||
this.scheduleApi = {}
|
||||
this.formData = {}
|
||||
|
||||
const refreshRemotes = () => {
|
||||
const selectRemoteId = this.formData.remote && this.formData.remote.id
|
||||
return xo.remote.getAll()
|
||||
.then(remotes => {
|
||||
const r = {}
|
||||
forEach(remotes, remote => {
|
||||
remote = parse(remote)
|
||||
r[remote.id] = remote
|
||||
})
|
||||
this.remotes = r
|
||||
if (selectRemoteId) {
|
||||
this.formData.remote = this.remotes[selectRemoteId]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const refreshSchedules = () => {
|
||||
return xo.schedule.getAll()
|
||||
.then(schedules => {
|
||||
const s = {}
|
||||
forEach(schedules, schedule => {
|
||||
this.jobs && this.jobs[schedule.job] && this.jobs[schedule.job].key === JOBKEY && (s[schedule.id] = schedule)
|
||||
})
|
||||
this.schedules = s
|
||||
})
|
||||
}
|
||||
|
||||
const refreshJobs = () => {
|
||||
return xo.job.getAll()
|
||||
.then(jobs => {
|
||||
const j = {}
|
||||
forEach(jobs, job => {
|
||||
j[job.id] = job
|
||||
})
|
||||
this.jobs = j
|
||||
})
|
||||
}
|
||||
|
||||
const refresh = () => refreshRemotes().then(refreshJobs).then(refreshSchedules)
|
||||
|
||||
this.getReady = () => refresh().then(() => this.ready = true)
|
||||
this.getReady()
|
||||
|
||||
const interval = $interval(refresh, 5e3)
|
||||
$scope.$on('$destroy', () => $interval.cancel(interval))
|
||||
|
||||
const toggleState = (toggle, state) => {
|
||||
const selectedVms = this.formData.selectedVms.slice()
|
||||
if (toggle) {
|
||||
const vms = filterFilter(selectHighLevelFilter(this.objects), {type: 'VM'})
|
||||
forEach(vms, vm => {
|
||||
if (vm.power_state === state) {
|
||||
(selectedVms.indexOf(vm) === -1) && selectedVms.push(vm)
|
||||
}
|
||||
})
|
||||
this.formData.selectedVms = selectedVms
|
||||
} else {
|
||||
const keptVms = []
|
||||
for (let index in this.formData.selectedVms) {
|
||||
if (this.formData.selectedVms[index].power_state !== state) {
|
||||
keptVms.push(this.formData.selectedVms[index])
|
||||
}
|
||||
}
|
||||
this.formData.selectedVms = keptVms
|
||||
}
|
||||
}
|
||||
|
||||
this.toggleAllRunning = toggle => toggleState(toggle, 'Running')
|
||||
this.toggleAllHalted = toggle => toggleState(toggle, 'Halted')
|
||||
|
||||
this.edit = schedule => {
|
||||
const vms = filterFilter(selectHighLevelFilter(this.objects), {type: 'VM'})
|
||||
const job = this.jobs[schedule.job]
|
||||
const selectedVms = []
|
||||
forEach(job.paramsVector.items[0].values, value => {
|
||||
const vm = find(vms, vm => vm.id === value.vm)
|
||||
vm && selectedVms.push(vm)
|
||||
})
|
||||
const tag = job.paramsVector.items[0].values[0].tag
|
||||
const depth = job.paramsVector.items[0].values[0].depth
|
||||
const _reportWhen = job.paramsVector.items[0].values[0]._reportWhen
|
||||
const cronPattern = schedule.cron
|
||||
const remoteId = job.paramsVector.items[0].values[0].remote
|
||||
|
||||
this.resetData()
|
||||
this.formData.selectedVms = selectedVms
|
||||
this.formData.tag = tag
|
||||
this.formData.depth = depth
|
||||
this.formData.scheduleId = schedule.id
|
||||
this.formData._reportWhen = _reportWhen
|
||||
this.formData.remote = this.remotes[remoteId]
|
||||
this.scheduleApi.setCron(cronPattern)
|
||||
}
|
||||
|
||||
this.save = (id, vms, remoteId, tag, depth, cron, enabled, _reportWhen) => {
|
||||
if (!vms.length) {
|
||||
notify.warning({
|
||||
title: 'No Vms selected',
|
||||
message: 'Choose VMs to backup'
|
||||
})
|
||||
return
|
||||
}
|
||||
const _save = (id === undefined) ? saveNew(vms, remoteId, tag, depth, cron, enabled, _reportWhen) : save(id, vms, remoteId, tag, depth, cron, _reportWhen)
|
||||
return _save
|
||||
.then(() => {
|
||||
notify.info({
|
||||
title: 'Backup',
|
||||
message: 'Job schedule successfuly saved'
|
||||
})
|
||||
this.resetData()
|
||||
})
|
||||
.finally(refresh)
|
||||
}
|
||||
|
||||
const save = (id, vms, remoteId, tag, depth, cron, _reportWhen) => {
|
||||
const schedule = this.schedules[id]
|
||||
const job = this.jobs[schedule.job]
|
||||
const values = []
|
||||
forEach(vms, vm => {
|
||||
values.push({
|
||||
vm: vm.id,
|
||||
remote: remoteId,
|
||||
tag,
|
||||
depth,
|
||||
_reportWhen
|
||||
})
|
||||
})
|
||||
job.paramsVector.items[0].values = values
|
||||
return xo.job.set(job)
|
||||
.then(response => {
|
||||
if (response) {
|
||||
return xo.schedule.set(schedule.id, undefined, cron, undefined)
|
||||
} else {
|
||||
notify.error({
|
||||
title: 'Update schedule',
|
||||
message: 'Job updating failed'
|
||||
})
|
||||
throw new Error('Job updating failed')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const saveNew = (vms, remoteId, tag, depth, cron, enabled, _reportWhen) => {
|
||||
const values = []
|
||||
forEach(vms, vm => {
|
||||
values.push({
|
||||
vm: vm.id,
|
||||
remote: remoteId,
|
||||
tag,
|
||||
depth,
|
||||
_reportWhen
|
||||
})
|
||||
})
|
||||
const job = {
|
||||
type: 'call',
|
||||
key: JOBKEY,
|
||||
method: 'vm.rollingDeltaBackup',
|
||||
paramsVector: {
|
||||
type: 'crossProduct',
|
||||
items: [{
|
||||
type: 'set',
|
||||
values
|
||||
}]
|
||||
}
|
||||
}
|
||||
return xo.job.create(job)
|
||||
.then(jobId => xo.schedule.create(jobId, cron, enabled))
|
||||
}
|
||||
|
||||
this.delete = schedule => {
|
||||
let jobId = schedule.job
|
||||
return xo.schedule.delete(schedule.id)
|
||||
.then(() => xo.job.delete(jobId))
|
||||
.finally(() => {
|
||||
if (this.formData.scheduleId === schedule.id) {
|
||||
this.resetData()
|
||||
}
|
||||
refresh()
|
||||
})
|
||||
}
|
||||
|
||||
this.run = schedule => {
|
||||
this.running[schedule.id] = true
|
||||
notify.info({
|
||||
title: 'Run Job',
|
||||
message: 'One shot running started. See overview for logs.'
|
||||
})
|
||||
const id = schedule.job
|
||||
return xo.job.runSequence([id]).finally(() => delete this.running[schedule.id])
|
||||
}
|
||||
|
||||
this.sanitizePath = (...paths) => (paths[0] && paths[0].charAt(0) === '/' && '/' || '') + filter(map(paths, s => s && filter(map(s.split('/'), trim)).join('/'))).join('/')
|
||||
|
||||
this.resetData = () => {
|
||||
this.formData.allRunning = false
|
||||
this.formData.allHalted = false
|
||||
this.formData.selectedVms = []
|
||||
this.formData.scheduleId = undefined
|
||||
this.formData.tag = undefined
|
||||
this.formData.path = undefined
|
||||
this.formData.depth = undefined
|
||||
this.formData.enabled = false
|
||||
this.formData._reportWhen = undefined
|
||||
this.formData.remote = undefined
|
||||
this.scheduleApi && this.scheduleApi.resetData && this.scheduleApi.resetData()
|
||||
}
|
||||
|
||||
this.size = size
|
||||
this.prettyCron = prettyCron.toString.bind(prettyCron)
|
||||
|
||||
if (!this.comesForEditing) {
|
||||
refresh()
|
||||
} else {
|
||||
refresh()
|
||||
.then(() => {
|
||||
this.edit(this.schedules[this.comesForEditing])
|
||||
delete this.comesForEditing
|
||||
})
|
||||
}
|
||||
this.resetData()
|
||||
this.objects = xoApi.all
|
||||
})
|
||||
|
||||
// A module exports its name.
|
||||
.name
|
||||
140
app/modules/backup/delta-backup/view.jade
Normal file
@@ -0,0 +1,140 @@
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.fa.fa-download(style="color: #e25440;")
|
||||
| Delta Backup
|
||||
form#backupform(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.selectedVms, ctrl.formData.remote.id, ctrl.formData.tag, ctrl.formData.depth, ctrl.formData.cronPattern, ctrl.formData.enabled, ctrl.formData._reportWhen)')
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.xo-icon-vm
|
||||
| VMs to backup
|
||||
.panel-body.form-horizontal
|
||||
.text-center(ng-if = '!ctrl.formData'): i.xo-icon-loading
|
||||
.container-fluid(ng-if = 'ctrl.formData')
|
||||
.alert.alert-info(ng-if = '!ctrl.formData.scheduleId') Creating New Backup
|
||||
.alert.alert-warning(ng-if = 'ctrl.formData.scheduleId') Modifying Backup ID {{ ctrl.formData.scheduleId }}
|
||||
.form-group
|
||||
label.control-label.col-md-2(for = 'tag') Tag
|
||||
.col-md-10
|
||||
input#tag.form-control(form = 'backupform', ng-model = 'ctrl.formData.tag', placeholder = 'Back-up tag', required)
|
||||
.form-group(ng-class = '{"has-warning": !ctrl.formData.selectedVms.length}')
|
||||
label.control-label.col-md-2(for = 'vmlist') VMs
|
||||
.col-md-8
|
||||
ui-select(form = 'backupform', ng-model = 'ctrl.formData.selectedVms', multiple, close-on-select = 'false', required)
|
||||
ui-select-match(placeholder = 'Choose VMs to backup')
|
||||
i.xo-icon-working(ng-if="isVMWorking($item)")
|
||||
i(class="xo-icon-{{$item.power_state | lowercase}}",ng-if="!isVMWorking($item)")
|
||||
| {{$item.name_label}}
|
||||
span(ng-if="$item.$container")
|
||||
| ({{($item.$container | resolve).name_label}})
|
||||
ui-select-choices(repeat = 'vm in ctrl.objects | selectHighLevel | filter:{type: "VM"} | filter:$select.search | orderBy:["$container", "name_label"] track by vm.id')
|
||||
div
|
||||
i.xo-icon-working(ng-if="isVMWorking(vm)", tooltip="{{vm.power_state}} and {{(vm.current_operations | map)[0]}}")
|
||||
i(class="xo-icon-{{vm.power_state | lowercase}}",ng-if="!isVMWorking(vm)", tooltip="{{vm.power_state}}")
|
||||
| {{vm.name_label}}
|
||||
span(ng-if="vm.$container")
|
||||
| ({{(vm.$container | resolve).name_label || ((vm.$container | resolve).master | resolve).name_label}})
|
||||
.col-md-2
|
||||
label(tooltip = 'select/deselect all running VMs', style = 'cursor: pointer')
|
||||
input.hidden(form = 'backupform', type = 'checkbox', ng-model = 'ctrl.formData.allRunning', ng-change = 'ctrl.toggleAllRunning(ctrl.formData.allRunning)')
|
||||
span.fa-stack
|
||||
i.xo-icon-running.fa-stack-1x
|
||||
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allRunning')
|
||||
label(tooltip = 'select/deselect all halted VMs', style = 'cursor: pointer')
|
||||
input.hidden(form = 'backupform', type = 'checkbox', ng-model = 'ctrl.formData.allHalted', ng-change = 'ctrl.toggleAllHalted(ctrl.formData.allHalted)')
|
||||
span.fa-stack
|
||||
i.xo-icon-halted.fa-stack-1x
|
||||
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allHalted')
|
||||
.form-group
|
||||
label.control-label.col-md-2(for = 'depth') Depth
|
||||
.col-md-10
|
||||
input#depth.form-control(form = 'backupform', ng-model = 'ctrl.formData.depth', placeholder = 'How many backups to rollover', type = 'number', min = '1', required)
|
||||
.form-group
|
||||
label.control-label.col-md-2(for = 'remote') Remote
|
||||
.col-md-10
|
||||
select#remote.form-control(form = 'backupform', ng-options = 'remote.name group by remote.type for remote in ctrl.remotes | map | orderBy:["type","name"]', ng-model = 'ctrl.formData.remote' required)
|
||||
option(value = ''): em -- Choose a file system remote point --
|
||||
.form-group
|
||||
.col-md-10.col-md-offset-2
|
||||
a(ui-sref = 'backup.remote')
|
||||
i.fa.fa-pencil
|
||||
| Manage your remote stores
|
||||
.form-group(ng-if = '!ctrl.formData.scheduleId')
|
||||
label.control-label.col-md-2(for = 'enabled')
|
||||
input#enabled(form = 'backupform', ng-model = 'ctrl.formData.enabled', type = 'checkbox')
|
||||
.help-block.col-md-10 Enable immediately after creation
|
||||
.form-group
|
||||
label.control-label.col-md-2(for = '_reportWhen') Report
|
||||
.col-md-10
|
||||
select.form-control(ng-model = 'ctrl.formData._reportWhen')
|
||||
option(value = ''): em -- When to send reports --
|
||||
option(value = 'never') Never
|
||||
option(value = 'alway') Always
|
||||
option(value = 'fail') Failure
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-clock-o
|
||||
| Schedule
|
||||
.panel-body.form-horizontal
|
||||
.text-center(ng-if = '!ctrl.formData'): i.xo-icon-loading
|
||||
xo-scheduler(data = 'ctrl.formData', api = 'ctrl.scheduleApi')
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-body
|
||||
fieldset.center(ng-disabled = '!ctrl.ready')
|
||||
button.btn.btn-lg.btn-primary(form = 'backupform', type = 'submit')
|
||||
i.fa.fa-clock-o
|
||||
|
|
||||
i.fa.fa-arrow-right
|
||||
|
|
||||
i.fa.fa-database
|
||||
| Save
|
||||
|
|
||||
button.btn.btn-lg.btn-default(type = 'button', ng-click = 'ctrl.resetData()')
|
||||
| Reset
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-list-ul
|
||||
| Schedules
|
||||
.panel-body
|
||||
.text-center(ng-if = '!ctrl.schedules'): i.xo-icon-loading
|
||||
.text-center(ng-if = 'ctrl.schedules && !ctrl.size(ctrl.schedules)') No scheduled jobs
|
||||
table.table.table-hover(ng-if = 'ctrl.schedules && ctrl.size(ctrl.schedules)')
|
||||
tr
|
||||
th ID
|
||||
th Tag
|
||||
th.hidden-xs.hidden-sm VMs to backup
|
||||
th.hidden-xs Remote
|
||||
th.hidden-xs Depth
|
||||
th.hidden-xs Scheduling
|
||||
th Enabled now
|
||||
th
|
||||
tr(ng-repeat = 'schedule in ctrl.schedules | 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')
|
||||
br
|
||||
ul.list-group
|
||||
li.list-group-item(ng-repeat = 'item in ctrl.jobs[schedule.job].paramsVector.items[0].values')
|
||||
span(ng-if = 'item.vm | resolve') {{ (item.vm | resolve).name_label }}
|
||||
span(ng-if = '(item.vm | resolve).$container') ({{ ((item.vm | resolve).$container | resolve).name_label }})
|
||||
td.hidden-xs
|
||||
strong: a(ui-sref = '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 = '>>')
|
||||
58
app/modules/backup/remote/index.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import angular from 'angular'
|
||||
import map from 'lodash.map'
|
||||
import size from 'lodash.size'
|
||||
import uiBootstrap from 'angular-ui-bootstrap'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
import {format, parse} from 'xo-remote-parser'
|
||||
|
||||
import view from './view'
|
||||
|
||||
// ====================================================================
|
||||
|
||||
export default angular.module('backup.remote', [
|
||||
uiRouter,
|
||||
uiBootstrap
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('backup.remote', {
|
||||
url: '/remote',
|
||||
controller: 'RemoteCtrl as ctrl',
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('RemoteCtrl', function ($scope, $state, $stateParams, $interval, xo, xoApi, notify, selectHighLevelFilter, filterFilter) {
|
||||
this.ready = false
|
||||
|
||||
const refresh = () => {
|
||||
return xo.remote.getAll()
|
||||
.then(remotes => this.backUpRemotes = map(remotes, parse))
|
||||
}
|
||||
|
||||
this.getReady = () => {
|
||||
return refresh()
|
||||
.then(() => this.ready = true)
|
||||
}
|
||||
this.getReady()
|
||||
|
||||
const interval = $interval(refresh, 5e3)
|
||||
$scope.$on('$destroy', () => {
|
||||
$interval.cancel(interval)
|
||||
})
|
||||
|
||||
this.prepareUrl = (type, host, path, username, password, domain) => format({type, host, path, username, password, domain})
|
||||
|
||||
const reset = () => {
|
||||
this.path = this.host = this.name = undefined
|
||||
this.remoteType = 'file'
|
||||
}
|
||||
this.add = (name, url) => xo.remote.create(name, url).then(reset).then(refresh)
|
||||
this.remove = id => xo.remote.delete(id).then(refresh)
|
||||
this.enable = id => xo.remote.set(id, undefined, undefined, true).then(refresh)
|
||||
this.disable = id => xo.remote.set(id, undefined, undefined, false).then(refresh)
|
||||
this.size = size
|
||||
|
||||
reset()
|
||||
})
|
||||
|
||||
// A module exports its name.
|
||||
.name
|
||||
143
app/modules/backup/remote/view.jade
Normal file
@@ -0,0 +1,143 @@
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.fa.fa-plug(style="color: #e25440;")
|
||||
| Remotes stores for backup
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-body
|
||||
//- {{ ctrl.backUpRemotes }} {{ ctrl.size(ctrl.backUpRemotes) }}
|
||||
.text-center(ng-if = '!ctrl.size(ctrl.backUpRemotes)') No remotes
|
||||
table.table.table-hover(ng-if = 'ctrl.size(ctrl.backUpRemotes)')
|
||||
tbody(ng-if = '(ctrl.backUpRemotes | filter:{type:"local"}).length')
|
||||
tr
|
||||
th.text-info Local
|
||||
th Name
|
||||
th Path
|
||||
th
|
||||
th State
|
||||
th Error
|
||||
th
|
||||
tr(ng-repeat = 'remote in ctrl.backUpRemotes | filter:{type:"local"} | orderBy:["name"] track by remote.id')
|
||||
td
|
||||
td {{ remote.name }}
|
||||
td {{ remote.path }}
|
||||
td
|
||||
td
|
||||
span(ng-if = 'remote.enabled')
|
||||
span.text-success
|
||||
| Accessible
|
||||
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
|
||||
tbody(ng-if = '(ctrl.backUpRemotes | filter:{type:"nfs"}).length')
|
||||
tr
|
||||
th.text-info NFS
|
||||
th Name
|
||||
th Device
|
||||
th
|
||||
th State
|
||||
th Error
|
||||
th
|
||||
tr(ng-repeat = 'remote in ctrl.backUpRemotes | filter:{type:"nfs"} | orderBy:["name"] track by remote.id')
|
||||
td
|
||||
td {{ remote.name }}
|
||||
td {{ remote.host }}:{{ remote.share }}
|
||||
td
|
||||
td
|
||||
span(ng-if = 'remote.enabled')
|
||||
span.text-success
|
||||
| Mounted
|
||||
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
|
||||
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
|
||||
.form-group
|
||||
label.sr-only Type
|
||||
select.form-control(ng-model = 'ctrl.remoteType')
|
||||
option(value = 'file') Local
|
||||
option(value = 'nfs') NFS
|
||||
option(value = 'smb') SMB
|
||||
|
|
||||
.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(ng-if = 'ctrl.remoteType !== "smb"')
|
||||
span.input-group-addon /
|
||||
label.sr-only Path
|
||||
input.form-control(type = 'text', ng-model = 'ctrl.path', placeholder = 'path/to/backup')
|
||||
.form-group(ng-if = 'ctrl.remoteType === "smb"')
|
||||
.input-group
|
||||
span.input-group-addon \\
|
||||
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
|
||||
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
|
||||
247
app/modules/backup/rolling-snapshot/index.js
Normal file
@@ -0,0 +1,247 @@
|
||||
import angular from 'angular'
|
||||
import find from 'lodash.find'
|
||||
import forEach from 'lodash.foreach'
|
||||
import later from 'later'
|
||||
import prettyCron from 'prettycron'
|
||||
import uiBootstrap from 'angular-ui-bootstrap'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
|
||||
later.date.localTime()
|
||||
|
||||
import view from './view'
|
||||
|
||||
// ====================================================================
|
||||
|
||||
export default angular.module('backup.rollingSnapshot', [
|
||||
uiRouter,
|
||||
uiBootstrap
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('backup.rollingsnapshot', {
|
||||
url: '/rollingsnapshot/:id',
|
||||
controller: 'RollingSnapshotCtrl as ctrl',
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('RollingSnapshotCtrl', function ($scope, $stateParams, $interval, xo, xoApi, notify, selectHighLevelFilter, filterFilter) {
|
||||
const JOBKEY = 'rollingSnapshot'
|
||||
|
||||
this.ready = false
|
||||
|
||||
this.running = {}
|
||||
this.comesForEditing = $stateParams.id
|
||||
this.scheduleApi = {}
|
||||
this.formData = {}
|
||||
|
||||
const refreshSchedules = () => {
|
||||
return xo.schedule.getAll()
|
||||
.then(schedules => {
|
||||
const s = {}
|
||||
forEach(schedules, schedule => {
|
||||
this.jobs && this.jobs[schedule.job] && this.jobs[schedule.job].key === JOBKEY && (s[schedule.id] = schedule)
|
||||
})
|
||||
this.schedules = s
|
||||
})
|
||||
}
|
||||
|
||||
const refreshJobs = () => {
|
||||
return xo.job.getAll()
|
||||
.then(jobs => {
|
||||
const j = {}
|
||||
forEach(jobs, job => j[job.id] = job)
|
||||
this.jobs = j
|
||||
})
|
||||
}
|
||||
|
||||
const refresh = () => {
|
||||
return refreshJobs().then(refreshSchedules)
|
||||
}
|
||||
|
||||
this.getReady = () => refresh().then(() => this.ready = true)
|
||||
this.getReady()
|
||||
|
||||
const interval = $interval(() => {
|
||||
refresh()
|
||||
}, 5e3)
|
||||
$scope.$on('$destroy', () => {
|
||||
$interval.cancel(interval)
|
||||
})
|
||||
|
||||
const toggleState = (toggle, state) => {
|
||||
const selectedVms = this.formData.selectedVms.slice()
|
||||
if (toggle) {
|
||||
const vms = filterFilter(selectHighLevelFilter(this.objects), {type: 'VM'})
|
||||
forEach(vms, vm => {
|
||||
if (vm.power_state === state) {
|
||||
(selectedVms.indexOf(vm) === -1) && selectedVms.push(vm)
|
||||
}
|
||||
})
|
||||
this.formData.selectedVms = selectedVms
|
||||
} else {
|
||||
const keptVms = []
|
||||
for (let index in this.formData.selectedVms) {
|
||||
if (this.formData.selectedVms[index].power_state !== state) {
|
||||
keptVms.push(this.formData.selectedVms[index])
|
||||
}
|
||||
}
|
||||
this.formData.selectedVms = keptVms
|
||||
}
|
||||
}
|
||||
|
||||
this.toggleAllRunning = toggle => toggleState(toggle, 'Running')
|
||||
this.toggleAllHalted = toggle => toggleState(toggle, 'Halted')
|
||||
|
||||
this.edit = schedule => {
|
||||
const vms = filterFilter(selectHighLevelFilter(this.objects), {type: 'VM'})
|
||||
const job = this.jobs[schedule.job]
|
||||
const selectedVms = []
|
||||
forEach(job.paramsVector.items[0].values, value => {
|
||||
const vm = find(vms, vm => vm.id === value.id)
|
||||
vm && selectedVms.push(vm)
|
||||
})
|
||||
const tag = job.paramsVector.items[0].values[0].tag
|
||||
const depth = job.paramsVector.items[0].values[0].depth
|
||||
const _reportWhen = job.paramsVector.items[0].values[0]._reportWhen
|
||||
const cronPattern = schedule.cron
|
||||
|
||||
this.resetData()
|
||||
// const formData = this.formData
|
||||
this.formData.selectedVms = selectedVms
|
||||
this.formData.tag = tag
|
||||
this.formData.depth = depth
|
||||
this.formData._reportWhen = _reportWhen
|
||||
this.formData.scheduleId = schedule.id
|
||||
this.scheduleApi.setCron(cronPattern)
|
||||
}
|
||||
|
||||
this.save = (id, vms, tag, depth, cron, enabled, _reportWhen) => {
|
||||
if (!vms.length) {
|
||||
notify.warning({
|
||||
title: 'No Vms selected',
|
||||
message: 'Choose VMs to snapshot'
|
||||
})
|
||||
return
|
||||
}
|
||||
const _save = (id === undefined) ? saveNew(vms, tag, depth, cron, enabled, _reportWhen) : save(id, vms, tag, depth, cron, _reportWhen)
|
||||
return _save
|
||||
.then(() => {
|
||||
notify.info({
|
||||
title: 'Rolling snapshot',
|
||||
message: 'Job schedule successfuly saved'
|
||||
})
|
||||
this.resetData()
|
||||
})
|
||||
.finally(() => {
|
||||
refresh()
|
||||
})
|
||||
}
|
||||
|
||||
const save = (id, vms, tag, depth, cron, _reportWhen) => {
|
||||
const schedule = this.schedules[id]
|
||||
const job = this.jobs[schedule.job]
|
||||
const values = []
|
||||
forEach(vms, vm => {
|
||||
values.push({
|
||||
id: vm.id,
|
||||
tag,
|
||||
depth,
|
||||
_reportWhen
|
||||
})
|
||||
})
|
||||
job.paramsVector.items[0].values = values
|
||||
return xo.job.set(job)
|
||||
.then(response => {
|
||||
if (response) {
|
||||
return xo.schedule.set(schedule.id, undefined, cron, undefined)
|
||||
} else {
|
||||
notify.error({
|
||||
title: 'Update schedule',
|
||||
message: 'Job updating failed'
|
||||
})
|
||||
throw new Error('Job updating failed')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const saveNew = (vms, tag, depth, cron, enabled, _reportWhen) => {
|
||||
const values = []
|
||||
forEach(vms, vm => {
|
||||
values.push({
|
||||
id: vm.id,
|
||||
tag,
|
||||
depth,
|
||||
_reportWhen
|
||||
})
|
||||
})
|
||||
const job = {
|
||||
type: 'call',
|
||||
key: JOBKEY,
|
||||
method: 'vm.rollingSnapshot',
|
||||
paramsVector: {
|
||||
type: 'crossProduct',
|
||||
items: [
|
||||
{
|
||||
type: 'set',
|
||||
values
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
return xo.job.create(job)
|
||||
.then(jobId => {
|
||||
return xo.schedule.create(jobId, cron, enabled)
|
||||
})
|
||||
}
|
||||
|
||||
this.delete = schedule => {
|
||||
let jobId = schedule.job
|
||||
return xo.schedule.delete(schedule.id)
|
||||
.then(() => xo.job.delete(jobId))
|
||||
.finally(() => {
|
||||
if (this.formData.scheduleId === schedule.id) {
|
||||
this.resetData()
|
||||
}
|
||||
refresh()
|
||||
})
|
||||
}
|
||||
|
||||
this.run = schedule => {
|
||||
this.running[schedule.id] = true
|
||||
notify.info({
|
||||
title: 'Run Job',
|
||||
message: 'One shot running started. See overview for logs.'
|
||||
})
|
||||
const id = schedule.job
|
||||
return xo.job.runSequence([id]).finally(() => delete this.running[schedule.id])
|
||||
}
|
||||
|
||||
this.resetData = () => {
|
||||
this.formData.allRunning = false
|
||||
this.formData.allHalted = false
|
||||
this.formData.selectedVms = []
|
||||
this.formData.scheduleId = undefined
|
||||
this.formData.tag = undefined
|
||||
this.formData.depth = undefined
|
||||
this.formData.enabled = false
|
||||
this.formData._reportWhen = undefined
|
||||
this.scheduleApi && this.scheduleApi.resetData && this.scheduleApi.resetData()
|
||||
}
|
||||
|
||||
this.collectionLength = col => Object.keys(col).length
|
||||
this.prettyCron = prettyCron.toString.bind(prettyCron)
|
||||
|
||||
if (!this.comesForEditing) {
|
||||
refresh()
|
||||
} else {
|
||||
refresh()
|
||||
.then(() => {
|
||||
this.edit(this.schedules[this.comesForEditing])
|
||||
delete this.comesForEditing
|
||||
})
|
||||
}
|
||||
this.resetData()
|
||||
this.objects = xoApi.all
|
||||
})
|
||||
|
||||
// A module exports its name.
|
||||
.name
|
||||
127
app/modules/backup/rolling-snapshot/view.jade
Normal file
@@ -0,0 +1,127 @@
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.xo-icon-snapshot(style="color: #e25440;")
|
||||
| Rolling snapshots
|
||||
form#snapform(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.selectedVms, ctrl.formData.tag, ctrl.formData.depth, ctrl.formData.cronPattern, ctrl.formData.enabled, ctrl.formData._reportWhen)')
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.xo-icon-vm(style='color: #e25440;')
|
||||
| VMs to snapshot
|
||||
.panel-body.form-horizontal
|
||||
.text-center(ng-if = '!ctrl.formData'): i.xo-icon-loading
|
||||
.container-fluid(ng-if = 'ctrl.formData')
|
||||
.alert.alert-info(ng-if = '!ctrl.formData.scheduleId') Creating New Rolling Snapshot
|
||||
.alert.alert-warning(ng-if = 'ctrl.formData.scheduleId') Modifying Rolling Snapshot ID {{ ctrl.formData.scheduleId }}
|
||||
.form-group
|
||||
label.control-label.col-md-2(for = 'tag') Tag
|
||||
.col-md-10
|
||||
input#tag.form-control(form = 'snapform', ng-model = 'ctrl.formData.tag', placeholder = 'Rolling snapshot tag', required)
|
||||
.form-group(ng-class = '{"has-warning": !ctrl.formData.selectedVms.length}')
|
||||
label.control-label.col-md-2(for = 'vmlist') VMs
|
||||
.col-md-8
|
||||
ui-select(form = 'snapform', ng-model = 'ctrl.formData.selectedVms', multiple, close-on-select = 'false', required)
|
||||
ui-select-match(placeholder = 'Choose VMs to snapshot')
|
||||
i.xo-icon-working(ng-if="isVMWorking($item)")
|
||||
i(class="xo-icon-{{$item.power_state | lowercase}}",ng-if="!isVMWorking($item)")
|
||||
| {{$item.name_label}}
|
||||
span(ng-if="$item.$container")
|
||||
| ({{($item.$container | resolve).name_label}})
|
||||
ui-select-choices(repeat = 'vm in ctrl.objects | selectHighLevel | filter:{type: "VM"} | filter:$select.search | orderBy:["$container", "name_label"] track by vm.id')
|
||||
div
|
||||
i.xo-icon-working(ng-if="isVMWorking(vm)", tooltip="{{vm.power_state}} and {{(vm.current_operations | map)[0]}}")
|
||||
i(class="xo-icon-{{vm.power_state | lowercase}}",ng-if="!isVMWorking(vm)", tooltip="{{vm.power_state}}")
|
||||
| {{vm.name_label}}
|
||||
span(ng-if="vm.$container")
|
||||
| ({{(vm.$container | resolve).name_label || ((vm.$container | resolve).master | resolve).name_label}})
|
||||
.col-md-2
|
||||
label(tooltip = 'select/deselect all running VMs', style = 'cursor: pointer')
|
||||
input.hidden(form = 'snapform', type = 'checkbox', ng-model = 'ctrl.formData.allRunning', ng-change = 'ctrl.toggleAllRunning(ctrl.formData.allRunning)')
|
||||
span.fa-stack
|
||||
i.xo-icon-running.fa-stack-1x
|
||||
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allRunning')
|
||||
label(tooltip = 'select/deselect all halted VMs', style = 'cursor: pointer')
|
||||
input.hidden(form = 'snapform', type = 'checkbox', ng-model = 'ctrl.formData.allHalted', ng-change = 'ctrl.toggleAllHalted(ctrl.formData.allHalted)')
|
||||
span.fa-stack
|
||||
i.xo-icon-halted.fa-stack-1x
|
||||
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allHalted')
|
||||
.form-group
|
||||
label.control-label.col-md-2(for = 'depth') Depth
|
||||
.col-md-10
|
||||
input#depth.form-control(form = 'snapform', ng-model = 'ctrl.formData.depth', placeholder = 'How many snapshots to rollover', type = 'number', min = '1', required)
|
||||
.form-group(ng-if = '!ctrl.formData.scheduleId')
|
||||
label.control-label.col-md-2(for = 'enabled')
|
||||
input#enabled(form = 'snapform', ng-model = 'ctrl.formData.enabled', type = 'checkbox')
|
||||
.help-block.col-md-8 Enable immediately after creation
|
||||
.form-group
|
||||
label.control-label.col-md-2(for = '_reportWhen') Report
|
||||
.col-md-10
|
||||
select.form-control(ng-model = 'ctrl.formData._reportWhen')
|
||||
option(value = ''): em -- When to send reports --
|
||||
option(value = 'never') Never
|
||||
option(value = 'alway') Always
|
||||
option(value = 'fail') Failure
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-clock-o
|
||||
| Schedule
|
||||
.panel-body.form-horizontal
|
||||
.text-center(ng-if = '!ctrl.formData'): i.xo-icon-loading
|
||||
xo-scheduler(data = 'ctrl.formData', api = 'ctrl.scheduleApi')
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-body
|
||||
fieldset.center(ng-disabled = '!ctrl.ready')
|
||||
button.btn.btn-lg.btn-primary(form = 'snapform', type = 'submit')
|
||||
i.fa.fa-clock-o
|
||||
|
|
||||
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 snapshot
|
||||
th.hidden-xs Depth
|
||||
th.hidden-xs Scheduling
|
||||
th Enabled now
|
||||
th
|
||||
tr(ng-repeat = 'schedule in ctrl.schedules | map | orderBy:"id":true track by schedule.id')
|
||||
td {{ schedule.id }}
|
||||
td {{ ctrl.jobs[schedule.job].paramsVector.items[0].values[0].tag }}
|
||||
td.hidden-xs.hidden-sm
|
||||
div(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values.length == 1')
|
||||
| {{ (ctrl.jobs[schedule.job].paramsVector.items[0].values[0].id | resolve).name_label }}
|
||||
div(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values.length > 1')
|
||||
button.btn.btn-info(type = 'button', ng-click = 'unCollapsed = !unCollapsed')
|
||||
| {{ ctrl.jobs[schedule.job].paramsVector.items[0].values.length }} VMs
|
||||
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 {{ 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
|
||||
104
app/modules/backup/scheduler.jade
Normal file
@@ -0,0 +1,104 @@
|
||||
accordion(ng-if = 'ctrl.data', close-others= 'false', ng-click = 'ctrl.update()')
|
||||
accordion-group
|
||||
accordion-heading Month
|
||||
tabset
|
||||
tab(select = 'ctrl.data.month = "all"', active = 'ctrl.tabs.month.all')
|
||||
tab-heading every month
|
||||
tab(select = 'ctrl.data.month = "select"', active = 'ctrl.tabs.month.select')
|
||||
tab-heading each selected month
|
||||
br
|
||||
table.table.table-bordered
|
||||
tr(ng-repeat = 'line in ctrl.months')
|
||||
td(ng-click = 'ctrl.selectMonth(month.v)', ng-class = '{"bg-success": ctrl.isSelectedMonth(month.v)}',ng-repeat = 'month in line') {{ month.l }}
|
||||
accordion-group
|
||||
accordion-heading Day of the month
|
||||
tabset
|
||||
tab(select = 'ctrl.data.day = "all"', active = 'ctrl.tabs.day.all')
|
||||
tab-heading every day
|
||||
tab(select = 'ctrl.data.day = "select"', active = 'ctrl.tabs.day.select')
|
||||
tab-heading each selected day
|
||||
br
|
||||
p.text-warning
|
||||
i.fa.fa-warning
|
||||
| This selection can restrict or be restricted by "Day of week" selections below. Use the summary preview to ensure your choice.
|
||||
br
|
||||
table.table.table-bordered
|
||||
tr(ng-repeat = 'line in ctrl.days')
|
||||
td(ng-click = 'ctrl.selectDay(day)', ng-class = '{"bg-success": ctrl.isSelectedDay(day)}',ng-repeat = 'day in line') {{ day }}
|
||||
accordion-group
|
||||
accordion-heading Day of week
|
||||
tabset
|
||||
tab(select = 'ctrl.data.dayWeek = "all"', active = 'ctrl.tabs.dayWeek.all')
|
||||
tab-heading every day of week
|
||||
tab(select = 'ctrl.data.dayWeek = "select"', active = 'ctrl.tabs.dayWeek.select')
|
||||
tab-heading each selected day of week
|
||||
br
|
||||
p.text-warning
|
||||
i.fa.fa-warning
|
||||
| This selection can restrict or be restricted by "Day of the month" selections up ahead. Use the summary preview to ensure your choice.
|
||||
br
|
||||
table.table.table-bordered
|
||||
tr
|
||||
td(ng-click = 'ctrl.selectDayWeek(dayWeek.v)', ng-class = '{"bg-success": ctrl.isSelectedDayWeek(dayWeek.v)}',ng-repeat = 'dayWeek in ctrl.dayWeeks') {{ dayWeek.l }}
|
||||
accordion-group
|
||||
accordion-heading Hour
|
||||
button.btn.btn-primary(ng-if = '!ctrl.noHourPlan()', type = 'button', ng-click = 'ctrl.noHourPlan(true)') Plan nothing on a hourly grain
|
||||
button.btn.btn-primary.disabled(ng-if = 'ctrl.noHourPlan()', type = 'button')
|
||||
i.fa.fa-info-circle
|
||||
| Nothing planned on a hourly grain
|
||||
br
|
||||
br
|
||||
tabset
|
||||
tab(select = 'ctrl.data.hour = "all"', active = 'ctrl.tabs.hour.all')
|
||||
tab-heading every hour
|
||||
tab(select = 'ctrl.data.hour = "range"', active = 'ctrl.tabs.hour.range')
|
||||
tab-heading every N hour
|
||||
br
|
||||
.form-group
|
||||
label.col-sm-2.control-label {{ ctrl.data.hourRange }}
|
||||
.col-sm-10
|
||||
input.form-control(type = 'range', min = '2', max = '23', step = '1', ng-model = 'ctrl.data.hourRange', ng-change = 'ctrl.update()')
|
||||
tab(select = 'ctrl.data.hour = "select"', active = 'ctrl.tabs.hour.select')
|
||||
tab-heading each selected hour
|
||||
br
|
||||
table.table.table-bordered
|
||||
tr(ng-repeat = 'line in ctrl.hours')
|
||||
td(ng-click = 'ctrl.selectHour(hour)', ng-class = '{"bg-success": ctrl.isSelectedHour(hour)}',ng-repeat = 'hour in line') {{ hour }}
|
||||
accordion-group
|
||||
accordion-heading Minute
|
||||
button.btn.btn-primary(ng-if = '!ctrl.noMinutePlan()', type = 'button', ng-click = 'ctrl.noMinutePlan(true)') Plan nothing on a minute grain
|
||||
button.btn.btn-primary.disabled(ng-if = 'ctrl.noMinutePlan()', type = 'button')
|
||||
i.fa.fa-info-circle
|
||||
| Nothing planned on a minute grain
|
||||
br
|
||||
br
|
||||
tabset
|
||||
tab(select = 'ctrl.data.min = "all"', active = 'ctrl.tabs.min.all')
|
||||
tab-heading every minute
|
||||
tab(select = 'ctrl.data.min = "range"', active = 'ctrl.tabs.min.range')
|
||||
tab-heading every N minutes
|
||||
br
|
||||
.form-group
|
||||
label.col-sm-2.control-label {{ ctrl.data.minRange }}
|
||||
.col-sm-10
|
||||
input.form-control(type = 'range', min = '2', max = '59', step = '1', ng-model = 'ctrl.data.minRange', ng-change = 'ctrl.update()')
|
||||
tab(select = 'ctrl.data.min = "select"', active = 'ctrl.tabs.min.select')
|
||||
tab-heading each selected minute
|
||||
br
|
||||
table.table.table-bordered
|
||||
tr(ng-repeat = 'line in ctrl.minutes')
|
||||
td(ng-click = 'ctrl.selectMinute(min)', ng-class = '{"bg-success": ctrl.isSelectedMinute(min)}',ng-repeat = 'min in line') {{ min }}
|
||||
input.form-control.hidden(type ='text', readonly, ng-model = 'ctrl.data.cronPattern')
|
||||
.text-center(ng-if = '!ctrl.data'): i.xo-icon-loading
|
||||
div(ng-if = 'ctrl.data')
|
||||
p
|
||||
strong Scheduled to run:
|
||||
| {{ ctrl.prettyCron(ctrl.data.cronPattern) }}
|
||||
.form-inline.container-fluid
|
||||
.form-group
|
||||
label Preview:
|
||||
input.form-control(type = 'range', min = '0', max = '{{ ctrl.data.summary.length - 3 }}', step = '1', ng-model = 'ctrl.data.previewLimit')
|
||||
br
|
||||
ul
|
||||
li(ng-repeat = 'occurence in ctrl.data.summary | limitTo: +ctrl.data.previewLimit+3') {{ occurence }}
|
||||
li ...
|
||||
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,29 +1,51 @@
|
||||
angular = require 'angular'
|
||||
|
||||
forEach = require('lodash.foreach')
|
||||
includes = require('lodash.includes')
|
||||
Clipboard = require('clipboard')
|
||||
|
||||
isoDevice = require('iso-device').default
|
||||
|
||||
#=====================================================================
|
||||
|
||||
module.exports = angular.module 'xoWebApp.console', [
|
||||
require 'angular-ui-router'
|
||||
require('angular-no-vnc').default
|
||||
|
||||
require 'angular-no-vnc'
|
||||
isoDevice
|
||||
]
|
||||
.config ($stateProvider) ->
|
||||
$stateProvider.state 'consoles_view',
|
||||
url: '/consoles/:id'
|
||||
controller: 'ConsoleCtrl'
|
||||
template: require './view'
|
||||
.controller 'ConsoleCtrl', ($scope, $stateParams, xoApi, xo) ->
|
||||
.controller 'ConsoleCtrl', ($scope, $stateParams, xoApi, xo, xoHideUnauthorizedFilter, modal) ->
|
||||
{id} = $stateParams
|
||||
{get} = xoApi
|
||||
push = Array::push.apply.bind Array::push
|
||||
merge = do ->
|
||||
(args...) ->
|
||||
result = []
|
||||
for arg in args
|
||||
push result, arg if arg?
|
||||
result
|
||||
|
||||
pool = null
|
||||
host = null
|
||||
do (
|
||||
srsByContainer = xoApi.getIndex('srsByContainer')
|
||||
poolSrs = null
|
||||
hostSrs = null
|
||||
) ->
|
||||
updateSrs = () =>
|
||||
srs = []
|
||||
poolSrs and forEach(poolSrs, (sr) => srs.push(sr))
|
||||
hostSrs and forEach(hostSrs, (sr) => srs.push(sr))
|
||||
$scope.SRs = xoHideUnauthorizedFilter(srs)
|
||||
$scope.$watchCollection(
|
||||
() => pool and srsByContainer[pool.id],
|
||||
(srs) =>
|
||||
poolSrs = srs
|
||||
updateSrs()
|
||||
)
|
||||
$scope.$watchCollection(
|
||||
() => host and srsByContainer[host.id],
|
||||
(srs) =>
|
||||
hostSrs = srs
|
||||
updateSrs()
|
||||
)
|
||||
|
||||
$scope.$watch(
|
||||
-> xoApi.get id
|
||||
@@ -41,41 +63,57 @@ module.exports = angular.module 'xoWebApp.console', [
|
||||
not includes(VM.current_operations, 'clean_reboot')
|
||||
)
|
||||
|
||||
pool = get VM.poolRef
|
||||
pool = get VM.$poolId
|
||||
return unless pool
|
||||
|
||||
$scope.consoleUrl = "/consoles/#{id}"
|
||||
$scope.consoleUrl = "./api/consoles/#{id}"
|
||||
|
||||
host = get VM.$container # host because the VM is running.
|
||||
return unless host
|
||||
|
||||
# FIXME: We should filter on connected SRs (PBDs)!
|
||||
SRs = get (merge host.SRs, pool.SRs)
|
||||
$scope.VDIs = do ->
|
||||
VDIs = []
|
||||
for SR in SRs
|
||||
push VDIs, SR.VDIs if SR.content_type is 'iso'
|
||||
get VDIs
|
||||
|
||||
cdDrive = do ->
|
||||
return VBD for VBD in (get VM.$VBDs) when VBD.is_cd_drive
|
||||
null
|
||||
|
||||
$scope.mountedIso =
|
||||
if cdDrive and cdDrive.VDI and (VDI = get cdDrive.VDI)
|
||||
VDI.UUID
|
||||
else
|
||||
''
|
||||
)
|
||||
|
||||
$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,59 +7,84 @@
|
||||
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.UUID})'
|
||||
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: .input-group
|
||||
select.form-control(
|
||||
ng-model = 'mountedIso'
|
||||
ng-change = 'insert(mountedIso)'
|
||||
ng-options = 'VDI.UUID as VDI.name_label group by (VDI.$SR | resolve).name_label for VDI in VDIs | orderBy:natural("name_label")'
|
||||
)
|
||||
.input-group-btn
|
||||
button.btn.btn-default(
|
||||
ng-click = 'eject()'
|
||||
ng-disabled = '!mountedIso'
|
||||
)
|
||||
i.fa.fa-eject
|
||||
.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"
|
||||
type = "button"
|
||||
xo-click = "stopVM(VM.UUID)"
|
||||
xo-click = "stopVM(VM.id)"
|
||||
)
|
||||
i.fa.fa-stop.fa-fw
|
||||
button.btn.btn-default.inversed(
|
||||
ng-if = "VM.power_state == ('Halted')"
|
||||
tooltip = "Start VM"
|
||||
type = "button"
|
||||
xo-click = "startVM(VM.UUID)"
|
||||
xo-click = "startVM(VM.id)"
|
||||
)
|
||||
i.fa.fa-play.fa-fw
|
||||
button.btn.btn-default.inversed(
|
||||
ng-if = "VM.power_state == ('Running' || 'Paused')"
|
||||
tooltip = "Reboot VM"
|
||||
type = "button"
|
||||
xo-click = "rebootVM(VM.UUID)"
|
||||
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)'
|
||||
)
|
||||
|
||||
363
app/modules/dashboard/dataviz/index.js
Normal file
@@ -0,0 +1,363 @@
|
||||
'use strict'
|
||||
|
||||
import angular from 'angular'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
import uiSelect from 'angular-ui-select'
|
||||
import debounce from 'lodash.debounce'
|
||||
import filter from 'lodash.filter'
|
||||
import foreach from 'lodash.foreach'
|
||||
|
||||
import xoApi from 'xo-api'
|
||||
import xoCircleD3 from 'xo-circle-d3'
|
||||
import xoParallelD3 from 'xo-parallel-d3'
|
||||
import xoSunburstD3 from 'xo-sunburst-d3'
|
||||
|
||||
import view from './view'
|
||||
|
||||
export default angular.module('dashboard.dataviz', [
|
||||
uiRouter,
|
||||
uiSelect,
|
||||
xoApi,
|
||||
xoCircleD3,
|
||||
xoParallelD3,
|
||||
xoSunburstD3
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('dashboard.dataviz', {
|
||||
controller: 'Dataviz as ctrl',
|
||||
data: {
|
||||
requireAdmin: true
|
||||
},
|
||||
url: '/dataviz/:chart',
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.filter('type', () => {
|
||||
return function (objects, type) {
|
||||
if (!type) {
|
||||
return objects
|
||||
}
|
||||
return filter(objects, object => object.type === type)
|
||||
}
|
||||
})
|
||||
.controller('Dataviz', function ($scope, $state) {
|
||||
$scope.selectedChart = ''
|
||||
$scope.availablecharts = {
|
||||
sunburst: {
|
||||
name: 'Sunburst charts',
|
||||
imgs: ['images/sunburst.png', 'images/sunburst2.png'],
|
||||
url: '/dataviz/sunburst'
|
||||
},
|
||||
circle: {
|
||||
name: 'Circles charts',
|
||||
imgs: ['images/circle1.png', 'images/circle2.png'],
|
||||
url: '/dataviz/circle'
|
||||
},
|
||||
parcoords: {
|
||||
name: 'VM properties',
|
||||
imgs: ['images/parcoords.png'],
|
||||
url: '/dataviz/parcoords'
|
||||
}
|
||||
}
|
||||
$scope.$on('$stateChangeSuccess', function updatePage () {
|
||||
$scope.selectedChart = $state.params.chart
|
||||
})
|
||||
})
|
||||
.controller('DatavizParcoords', function DatavizParcoords (xoApi, $scope, $timeout, $interval, $state, bytesToSizeFilter) {
|
||||
let hostsByPool, vmsByContainer, data
|
||||
data = []
|
||||
hostsByPool = xoApi.getIndex('hostsByPool')
|
||||
vmsByContainer = xoApi.getIndex('vmsByContainer')
|
||||
/* parallel charts */
|
||||
|
||||
function populateChartsData () {
|
||||
foreach(xoApi.getView('pools').all, function (pool, pool_id) {
|
||||
foreach(hostsByPool[pool_id], function (host, host_id) {
|
||||
console.log(host_id)
|
||||
foreach(vmsByContainer[host_id], function (vm, vm_id) {
|
||||
let nbvdi, vdisize
|
||||
|
||||
nbvdi = 0
|
||||
vdisize = 0
|
||||
foreach(vm.$VBDs, function (vbd_id) {
|
||||
let vbd
|
||||
vbd = xoApi.get(vbd_id)
|
||||
|
||||
if (!vbd.is_cd_drive && vbd.attached) {
|
||||
nbvdi++
|
||||
vdisize += xoApi.get(vbd.VDI).size
|
||||
}
|
||||
})
|
||||
data.push({
|
||||
name: vm.name_label,
|
||||
id: vm_id,
|
||||
vcpus: vm.CPUs.number,
|
||||
vifs: vm.VIFs.length,
|
||||
ram: vm.memory.size / (1024 * 1024 * 1024)/* memory size in GB */,
|
||||
nbvdi: nbvdi,
|
||||
vdisize: vdisize / (1024 * 1024 * 1024)/* disk size in Gb */
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
$scope.charts = {
|
||||
data: data,
|
||||
labels: {
|
||||
vcpus: 'vCPUs number',
|
||||
ram: 'RAM quantity',
|
||||
vifs: 'VIF number',
|
||||
nbvdi: 'VDI number',
|
||||
vdisize: 'Total space'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const debouncedPopulate = debounce(populateChartsData, 300, {leading: true, trailing: true})
|
||||
debouncedPopulate()
|
||||
$scope.$on('$destroy', xoApi.onUpdate(debouncedPopulate))
|
||||
})
|
||||
.controller('DatavizStorageHierarchical', function DatavizStorageHierarchical (xoApi, $scope, $timeout, $interval, $state, bytesToSizeFilter) {
|
||||
$scope.charts = {
|
||||
selected: {},
|
||||
data: {
|
||||
name: 'storage',
|
||||
children: []
|
||||
},
|
||||
click: function (d) {
|
||||
if (d.virtual) {
|
||||
return
|
||||
}
|
||||
switch (d.type) {
|
||||
case 'pool':
|
||||
$state.go('pools_view', {
|
||||
id: d.id
|
||||
})
|
||||
break
|
||||
case 'host':
|
||||
$state.go('hosts_view', {
|
||||
id: d.id
|
||||
})
|
||||
break
|
||||
case 'srs':
|
||||
$state.go('SRs_view', {
|
||||
id: d.id
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function populateChartsData () {
|
||||
function populatestorage (root, container_id) {
|
||||
let srs = filter(xoApi.getIndex('srsByContainer')[container_id], (one_srs) => one_srs.SR_type !== 'iso' && one_srs.SR_type !== 'udev')
|
||||
|
||||
foreach(srs, function (one_srs) {
|
||||
let srs_used_size = 0
|
||||
const srs_storage = {
|
||||
name: one_srs.name_label,
|
||||
id: one_srs.id,
|
||||
children: [],
|
||||
size: one_srs.size,
|
||||
textSize: bytesToSizeFilter(one_srs.size),
|
||||
type: 'srs'
|
||||
}
|
||||
|
||||
root.size += one_srs.size
|
||||
foreach(one_srs.VDIs, function (vdi_id) {
|
||||
let vdi = xoApi.get(vdi_id)
|
||||
if (vdi && vdi.name_label.indexOf('.iso') === -1) {
|
||||
let vdi_storage = {
|
||||
name: vdi.name_label,
|
||||
id: vdi_id,
|
||||
size: vdi.size,
|
||||
textSize: bytesToSizeFilter(vdi.size),
|
||||
type: 'vdi'
|
||||
}
|
||||
srs_used_size += vdi.size
|
||||
srs_storage.children.push(vdi_storage)
|
||||
}
|
||||
})
|
||||
if (one_srs.size > srs_used_size) {// some unallocated space
|
||||
srs_storage.children.push({
|
||||
color: 'white',
|
||||
name: 'Free',
|
||||
id: 'free' + one_srs.id,
|
||||
size: one_srs.size - srs_used_size,
|
||||
textSize: bytesToSizeFilter(one_srs.size - srs_used_size),
|
||||
type: 'vdi',
|
||||
virtual: true
|
||||
})
|
||||
}
|
||||
root.children.push(srs_storage)
|
||||
})
|
||||
root.textSize = bytesToSizeFilter(root.size)
|
||||
}
|
||||
|
||||
let storage_children,
|
||||
pools,
|
||||
hostsByPool,
|
||||
pool_shared_storage
|
||||
|
||||
storage_children = []
|
||||
pools = xoApi.getView('pools')
|
||||
hostsByPool = xoApi.getIndex('hostsByPool')
|
||||
|
||||
foreach(pools.all, function (pool, pool_id) {
|
||||
let pool_storage, hosts
|
||||
pool_storage = {
|
||||
name: pool.name_label || 'no pool',
|
||||
id: pool_id,
|
||||
children: [],
|
||||
size: 0,
|
||||
color: pool.name_label ? null : 'white',
|
||||
type: 'pool',
|
||||
virtual: !pool.name_label
|
||||
}
|
||||
pool_shared_storage = {
|
||||
name: 'Shared',
|
||||
id: 'Shared' + pool_id,
|
||||
children: [],
|
||||
size: 0,
|
||||
type: 'host',
|
||||
virtual: true
|
||||
}
|
||||
|
||||
populatestorage(pool_shared_storage, pool_id)
|
||||
pool_storage.children.push(pool_shared_storage)
|
||||
pool_storage.size += pool_shared_storage.size
|
||||
|
||||
// by hosts
|
||||
hosts = hostsByPool[pool_id]
|
||||
foreach(hosts, function (host, host_id) {
|
||||
// there's also SR attached top
|
||||
let host_storage = {
|
||||
name: host.name_label,
|
||||
id: host.id,
|
||||
children: [],
|
||||
size: 0,
|
||||
type: 'host'
|
||||
}
|
||||
populatestorage(host_storage, host_id)
|
||||
pool_storage.size += host_storage.size
|
||||
pool_storage.children.push(host_storage)
|
||||
})
|
||||
|
||||
pool_storage.textSize = bytesToSizeFilter(pool_storage.size)
|
||||
storage_children.push(pool_storage)
|
||||
})
|
||||
|
||||
$scope.charts.data.children = storage_children
|
||||
}
|
||||
|
||||
const debouncedPopulate = debounce(populateChartsData, 300, {leading: true, trailing: true})
|
||||
|
||||
debouncedPopulate()
|
||||
$scope.$on('$destroy', xoApi.onUpdate(debouncedPopulate))
|
||||
})
|
||||
.controller('DatavizRamHierarchical', function DatavizRamHierarchical (xoApi, $scope, $timeout, $state, bytesToSizeFilter) {
|
||||
$scope.charts = {
|
||||
selected: {},
|
||||
data: {
|
||||
name: 'ram',
|
||||
children: []
|
||||
},
|
||||
click: function (d) {
|
||||
if (d.virtual) {
|
||||
return
|
||||
}
|
||||
switch (d.type) {
|
||||
case 'pool':
|
||||
$state.go('pools_view', {id: d.id})
|
||||
break
|
||||
case 'host':
|
||||
$state.go('hosts_view', {id: d.id})
|
||||
break
|
||||
case 'vm':
|
||||
$state.go('VMs_view', {id: d.id})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function populateChartsData () {
|
||||
let ram_children,
|
||||
pools,
|
||||
vmsByContainer,
|
||||
hostsByPool
|
||||
|
||||
ram_children = []
|
||||
pools = xoApi.getView('pools')
|
||||
vmsByContainer = xoApi.getIndex('vmsByContainer')
|
||||
hostsByPool = xoApi.getIndex('hostsByPool')
|
||||
|
||||
foreach(pools.all, function (pool, pool_id) {
|
||||
let pool_ram, hosts
|
||||
|
||||
// by hosts
|
||||
|
||||
pool_ram = {
|
||||
name: pool.name_label || 'no pool',
|
||||
id: pool_id,
|
||||
children: [],
|
||||
size: 0,
|
||||
color: pool.name_label ? null : 'white',
|
||||
type: 'pool',
|
||||
virtual: !pool.name_label
|
||||
}
|
||||
hosts = hostsByPool[pool_id]
|
||||
foreach(hosts, function (host, host_id) {
|
||||
// there's also SR attached top
|
||||
let vm_ram_size = 0
|
||||
let host_ram = {
|
||||
name: host.name_label,
|
||||
id: host_id,
|
||||
children: [],
|
||||
size: host.memory.size,
|
||||
type: 'host'
|
||||
}
|
||||
let VMs = vmsByContainer[host_id]
|
||||
foreach(VMs, function (VM, vm_id) {
|
||||
let vm_ram = {
|
||||
name: VM.name_label,
|
||||
id: vm_id,
|
||||
size: VM.memory.size,
|
||||
textSize: bytesToSizeFilter(VM.memory.size),
|
||||
type: 'vm'
|
||||
}
|
||||
if (vm_ram.size) {
|
||||
vm_ram_size += vm_ram.size
|
||||
host_ram.children.push(vm_ram)
|
||||
}
|
||||
})
|
||||
if (host_ram.size !== vm_ram_size) {
|
||||
host_ram.children.push({
|
||||
color: 'white',
|
||||
name: 'Free',
|
||||
id: 'free' + host.id,
|
||||
size: host.memory.size - vm_ram_size,
|
||||
textSize: bytesToSizeFilter(host.memory.size - vm_ram_size),
|
||||
type: 'vm',
|
||||
virtual: true
|
||||
})
|
||||
}
|
||||
|
||||
host_ram.textSize = bytesToSizeFilter(host_ram.size)
|
||||
pool_ram.size += host_ram.size
|
||||
pool_ram.children.push(host_ram)
|
||||
})
|
||||
if (pool_ram.children.length) {
|
||||
pool_ram.textSize = bytesToSizeFilter(pool_ram.size)
|
||||
ram_children.push(pool_ram)
|
||||
}
|
||||
})
|
||||
$scope.charts.data.children = ram_children
|
||||
}
|
||||
|
||||
const debouncedPopulate = debounce(populateChartsData, 300, {leading: true, trailing: true})
|
||||
|
||||
debouncedPopulate()
|
||||
$scope.$on('$destroy', xoApi.onUpdate(debouncedPopulate))
|
||||
})
|
||||
// A module exports its name.
|
||||
.name
|
||||
85
app/modules/dashboard/dataviz/view.jade
Normal file
@@ -0,0 +1,85 @@
|
||||
|
||||
.grid-sm(ng-if="!selectedChart")
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.fa.fa-pie-chart
|
||||
| Dataviz
|
||||
.panel-body.text-center
|
||||
|
||||
.chart-selector(
|
||||
ng-repeat="(id,chart) in availablecharts"
|
||||
ui-sref="dashboard.dataviz({chart:id})")
|
||||
div {{chart.name }}
|
||||
img.img-thumbnail(
|
||||
ng-repeat="img in chart.imgs"
|
||||
ng-src="{{img}}"
|
||||
)
|
||||
|
||||
.grid-sm(ng-if="selectedChart =='sunburst'")
|
||||
.grid-cell
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-memory
|
||||
| Memory usage
|
||||
.panel-body.text-center(
|
||||
ng-controller="DatavizRamHierarchical as ram"
|
||||
style="position:relative"
|
||||
)
|
||||
sunburst-chart(
|
||||
click="charts.click(d)"
|
||||
chart-data="charts.data"
|
||||
)
|
||||
.grid-cell
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-sr
|
||||
| Storage
|
||||
.panel-body.text-center(
|
||||
ng-controller="DatavizStorageHierarchical as storage"
|
||||
style="position:relative"
|
||||
)
|
||||
sunburst-chart(
|
||||
click="charts.click(d)"
|
||||
chart-data="charts.data"
|
||||
)
|
||||
.grid-sm(ng-if="selectedChart =='circle'")
|
||||
.grid-cell
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-memory
|
||||
| Memory usage
|
||||
.panel-body.text-center(
|
||||
ng-controller="DatavizRamHierarchical as ram"
|
||||
style="position:relative"
|
||||
)
|
||||
circle-chart(
|
||||
click="charts.click(d)"
|
||||
chart-data="charts.data"
|
||||
)
|
||||
.grid-cell
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-sr
|
||||
| Storage
|
||||
.panel-body.text-center(
|
||||
ng-controller="DatavizStorageHierarchical as storage"
|
||||
style="position:relative"
|
||||
)
|
||||
circle-chart(
|
||||
click="charts.click(d)"
|
||||
chart-data="charts.data"
|
||||
)
|
||||
.grid-sm(ng-if="selectedChart == 'parcoords'")
|
||||
.grid-cell
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-memory
|
||||
| VMs properties
|
||||
.panel-body.text-center(
|
||||
ng-controller="DatavizParcoords as parcoords"
|
||||
)
|
||||
parallel-chart(
|
||||
click="charts.click(d)"
|
||||
chart-labels="charts.labels"
|
||||
chart-data="charts.data"
|
||||
)
|
||||
89
app/modules/dashboard/health/index.js
Normal file
@@ -0,0 +1,89 @@
|
||||
import angular from 'angular'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
import filter from 'lodash.filter'
|
||||
import forEach from 'lodash.foreach'
|
||||
|
||||
import xoApi from 'xo-api'
|
||||
|
||||
import view from './view'
|
||||
|
||||
export default angular.module('dashboard.health', [
|
||||
uiRouter,
|
||||
xoApi
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('dashboard.health', {
|
||||
controller: 'Health as ctrl',
|
||||
data: {
|
||||
requireAdmin: true
|
||||
},
|
||||
url: '/health',
|
||||
template: view
|
||||
})
|
||||
})
|
||||
|
||||
.controller('Health', function (xo, xoApi, $scope, modal) {
|
||||
this.currentVdiPage = 1
|
||||
this.currentVmPage = 1
|
||||
|
||||
const vms = xoApi.getView('VM-snapshot').all
|
||||
const vdis = xoApi.getView('VDI-snapshot').all
|
||||
const srs = xoApi.getView('SR').all
|
||||
|
||||
$scope.$watchCollection(() => vdis, () => {
|
||||
const orphanVdiSnapshots = filter(vdis, vdi => vdi && !vdi.$snapshot_of)
|
||||
this.orphanVdiSnapshots = orphanVdiSnapshots
|
||||
})
|
||||
|
||||
$scope.$watchCollection(() => vms, () => {
|
||||
const orphanVmSnapshots = filter(vms, vm => vm && !vm.$snapshot_of)
|
||||
this.orphanVmSnapshots = orphanVmSnapshots
|
||||
})
|
||||
|
||||
$scope.$watchCollection(() => srs, () => {
|
||||
const warningSrs = filter(srs, sr => sr.content_type !== 'iso' && (sr.physical_usage / sr.size) >= 0.8 && (sr.physical_usage / sr.size) < 0.9)
|
||||
const dangerSrs = filter(srs, sr => sr.content_type !== 'iso' && (sr.physical_usage / sr.size) >= 0.9)
|
||||
this.warningSrs = warningSrs
|
||||
this.dangerSrs = dangerSrs
|
||||
})
|
||||
|
||||
this.selectedVdiForDelete = {}
|
||||
this.selectedVmForDelete = {}
|
||||
|
||||
this.deleteVdiSnapshot = function (id) {
|
||||
modal.confirm({
|
||||
title: 'VDI snapshot deletion',
|
||||
message: 'Are you sure you want to delete this snapshot?'
|
||||
}).then(() => xo.vdi.delete(id))
|
||||
}
|
||||
|
||||
this.deleteVmSnapshot = function (id) {
|
||||
modal.confirm({
|
||||
title: 'VM snapshot deletion',
|
||||
message: 'Are you sure you want to delete this snapshot? (including its disks)'
|
||||
}).then(() => xo.vm.delete(id, true))
|
||||
}
|
||||
|
||||
this.deleteSelectedVdis = function () {
|
||||
return modal.confirm({
|
||||
title: 'VDI snapshot deletion',
|
||||
message: 'Are you sure you want to delete all selected VDI snapshots? This operation is irreversible.'
|
||||
}).then(() => {
|
||||
forEach(this.selectedVdiForDelete, (selected, id) => console.log(id))
|
||||
forEach(this.selectedVdiForDelete, (selected, id) => { selected && xo.vdi.delete(id) })
|
||||
this.selectedVdiForDelete = {}
|
||||
})
|
||||
}
|
||||
|
||||
this.deleteSelectedVms = function () {
|
||||
return modal.confirm({
|
||||
title: 'VM snapshot deletion',
|
||||
message: 'Are you sure you want to delete all selected VM snapshots? This operation is irreversible.'
|
||||
}).then(() => {
|
||||
forEach(this.selectedVmForDelete, (selected, id) => { selected && xo.vm.delete(id, true) })
|
||||
this.selectedVmForDelete = {}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
.name
|
||||
88
app/modules/dashboard/health/view.jade
Normal file
@@ -0,0 +1,88 @@
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.fa.fa-heartbeat
|
||||
| Health
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-hdd-o
|
||||
| Orphaned VDI snapshots
|
||||
.panel-body
|
||||
.center(ng-if = 'ctrl.orphanVdiSnapshots | isEmpty') No orphaned snapshots found
|
||||
table.table.table-hover(ng-if = 'ctrl.orphanVdiSnapshots | isNotEmpty')
|
||||
tr
|
||||
th Name
|
||||
th Description
|
||||
th Tags
|
||||
th Size
|
||||
th SR
|
||||
span.pull-right: button.btn.btn-danger(xo-click = 'ctrl.deleteSelectedVdis()', tooltip = 'Delete selected snapshots'): i.fa.fa-trash
|
||||
tr(ng-repeat = 'vdi in ctrl.orphanVdiSnapshots | filter:vdiSearch | orderBy:natural("name_label") | slice:(10*(ctrl.currentVdiPage-1)):(10*ctrl.currentVdiPage) track by vdi.id')
|
||||
td.oneliner {{ vdi.name_label }}
|
||||
td.oneliner {{ vdi.name_description }}
|
||||
td: xo-tag(object = 'vdi')
|
||||
td {{ vdi.size | bytesToSize}}
|
||||
td.oneliner
|
||||
a(xo-sref="SRs_view({id: (vdi.$SR | resolve).id})")
|
||||
| {{(vdi.$SR | resolve).name_label}} ({{((vdi.$SR | resolve).$container | resolve).name_label}})
|
||||
span.pull-right
|
||||
.btn-group.quick-buttons
|
||||
a(xo-click="ctrl.deleteVdiSnapshot(vdi.id)"): i.fa.fa-trash-o.fa-lg(tooltip="Destroy this snapshot")
|
||||
input(type = 'checkbox', ng-model = 'ctrl.selectedVdiForDelete[vdi.id]', tooltip = 'select for deletion')
|
||||
.form-inline
|
||||
.input-group
|
||||
.input-group-addon: i.fa.fa-filter
|
||||
input.form-control(type = 'text', ng-model = 'vdiSearch', placeholder = 'Enter your search here')
|
||||
.center(ng-if = '(ctrl.orphanVdiSnapshots | filter:vdiSearch).length > 10 || ctrl.currentVdiPage > 1')
|
||||
pagination(boundary-links="true", total-items="(ctrl.orphanVdiSnapshots | filter:vdiSearch).length", ng-model="ctrl.currentVdiPage", items-per-page="10", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-camera
|
||||
| Orphaned VM snapshots
|
||||
.panel-body
|
||||
.center(ng-if = 'ctrl.orphanVmSnapshots | isEmpty') No orphaned snapshots found
|
||||
table.table.table-hover(ng-if = 'ctrl.orphanVmSnapshots | isNotEmpty')
|
||||
tr
|
||||
th Name
|
||||
th Description
|
||||
th OS
|
||||
th Container
|
||||
span.pull-right: button.btn.btn-danger(xo-click = 'ctrl.deleteSelectedVms()', tooltip = 'Delete selected snapshots'): i.fa.fa-trash
|
||||
tr(ng-repeat = 'vm in ctrl.orphanVmSnapshots | orderBy:natural("name_label") | slice:(10*(ctrl.currentVmPage-1)):(10*ctrl.currentVmPage) track by vm.id')
|
||||
td.oneliner
|
||||
i.xo-icon-working(ng-if="vm.current_operations | isNotEmpty", tooltip="{{vm.power_state}} and {{(vm.current_operations | map)[0]}}")
|
||||
i(class="xo-icon-{{vm.power_state | lowercase}}",ng-if="vm.current_operations | isEmpty", tooltip="{{vm.power_state}}")
|
||||
| {{ 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 }}
|
||||
43
app/modules/dashboard/index.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import angular from 'angular'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
|
||||
import dataviz from './dataviz'
|
||||
import filter from 'lodash.filter'
|
||||
import health from './health'
|
||||
import stats from './stats'
|
||||
import overview from './overview'
|
||||
|
||||
import view from './view'
|
||||
|
||||
export default angular.module('dashboard', [
|
||||
uiRouter,
|
||||
dataviz,
|
||||
health,
|
||||
stats,
|
||||
overview
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('dashboard', {
|
||||
abstract: true,
|
||||
data: {
|
||||
requireAdmin: true
|
||||
},
|
||||
template: view,
|
||||
url: '/dashboard'
|
||||
})
|
||||
|
||||
// Redirect to default sub-state.
|
||||
$stateProvider.state('dashboard.index', {
|
||||
url: '',
|
||||
controller: function ($state) {
|
||||
$state.go('dashboard.overview')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
.filter('underStat', () => {
|
||||
let isUnderStat = object => object.type === 'host' || object.type === 'VM'
|
||||
return objects => filter(objects, isUnderStat)
|
||||
})
|
||||
|
||||
.name
|
||||
177
app/modules/dashboard/overview/index.js
Normal file
@@ -0,0 +1,177 @@
|
||||
'use strict'
|
||||
import angular from 'angular'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
import uiSelect from 'angular-ui-select'
|
||||
|
||||
import clone from 'lodash.clonedeep'
|
||||
import debounce from 'lodash.debounce'
|
||||
import foreach from 'lodash.foreach'
|
||||
|
||||
import xoApi from 'xo-api'
|
||||
import xoServices from 'xo-services'
|
||||
|
||||
import view from './view'
|
||||
|
||||
export default angular.module('dashboard.overview', [
|
||||
uiRouter,
|
||||
uiSelect,
|
||||
xoApi,
|
||||
xoServices
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('dashboard.overview', {
|
||||
controller: 'Overview as ctrl',
|
||||
data: {
|
||||
requireAdmin: true
|
||||
},
|
||||
url: '/overview',
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('Overview', function ($scope, $window, xoApi, xo, $timeout, bytesToSizeFilter, modal) {
|
||||
$window.bytesToSize = bytesToSizeFilter // FIXME dirty workaround to custom a Chart.js tooltip template
|
||||
angular.extend($scope, {
|
||||
pools: {
|
||||
nb: 0
|
||||
},
|
||||
hosts: {
|
||||
nb: 0
|
||||
},
|
||||
vms: {
|
||||
nb: 0,
|
||||
running: 0,
|
||||
halted: 0,
|
||||
action: 0
|
||||
},
|
||||
ram: [0, 0],
|
||||
cpu: [0, 0],
|
||||
srs: []
|
||||
})
|
||||
|
||||
$scope.installAllPatches = function () {
|
||||
modal.confirm({
|
||||
title: 'Install all the missing patches',
|
||||
message: 'Are you sure you want to install all the missing patches? This could take a while...'
|
||||
}).then(() =>
|
||||
foreach($scope.pools.all, function (pool, pool_id) {
|
||||
let pool_hosts = $scope.hostsByPool[pool_id]
|
||||
foreach(pool_hosts, function (host, host_id) {
|
||||
console.log('Installing all missing patches on host ', host_id)
|
||||
xo.host.installAllPatches(host_id)
|
||||
})
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
$scope.installHostPatches = function (hostId) {
|
||||
modal.confirm({
|
||||
title: 'Update host (' + $scope.nbUpdates[hostId] + ' patch(es))',
|
||||
message: 'Are you sure you want to install all the missing patches on this host? This could take a while...'
|
||||
}).then(() => {
|
||||
console.log('Installing all missing patches on host ', hostId)
|
||||
xo.host.installAllPatches(hostId)
|
||||
})
|
||||
}
|
||||
|
||||
const nbUpdates = $scope.nbUpdates = {}
|
||||
function populateChartsData () {
|
||||
let pools,
|
||||
vmsByContainer,
|
||||
hostsByPool,
|
||||
nb_hosts,
|
||||
nb_pools,
|
||||
vms,
|
||||
srsByContainer,
|
||||
srs
|
||||
|
||||
nb_pools = 0
|
||||
nb_hosts = 0
|
||||
vms = {
|
||||
nb: 0,
|
||||
states: [0, 0, 0, 0]
|
||||
}
|
||||
const runningStateToIndex = {
|
||||
Running: 0,
|
||||
Halted: 1,
|
||||
Suspended: 2,
|
||||
Action: 3
|
||||
}
|
||||
|
||||
nb_pools = 0
|
||||
srs = []
|
||||
|
||||
// update vdi, set them to the right host
|
||||
$scope.pools = pools = xoApi.getView('pools')
|
||||
|
||||
srsByContainer = xoApi.getIndex('srsByContainer')
|
||||
vmsByContainer = xoApi.getIndex('vmsByContainer')
|
||||
$scope.hostsByPool = hostsByPool = xoApi.getIndex('hostsByPool')
|
||||
foreach(pools.all, function (pool, pool_id) {
|
||||
let pool_hosts = hostsByPool[pool_id]
|
||||
foreach(pool_hosts, function (host, host_id) {
|
||||
if (host_id in nbUpdates) {
|
||||
return
|
||||
}
|
||||
|
||||
xo.host.listMissingPatches(host_id)
|
||||
.then(result => {
|
||||
nbUpdates[host_id] = result.length
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
foreach(pools.all, function (pool, pool_id) {
|
||||
nb_pools++
|
||||
let pool_srs = srsByContainer[pool_id]
|
||||
foreach(pool_srs, (one_srs) => {
|
||||
if (one_srs.SR_type !== 'iso' && one_srs.SR_type !== 'udev') {
|
||||
one_srs = clone(one_srs)
|
||||
one_srs.ratio = one_srs.size ? one_srs.physical_usage / one_srs.size : 0
|
||||
one_srs.pool_label = pool.name_label
|
||||
srs.push(one_srs)
|
||||
}
|
||||
})
|
||||
let VMs = vmsByContainer[pool_id]
|
||||
foreach(VMs, function (VM) {
|
||||
// non running VM
|
||||
vms.states[runningStateToIndex[VM['power_state']]]++
|
||||
vms.nb++
|
||||
})
|
||||
let hosts = hostsByPool[pool_id]
|
||||
foreach(hosts, function (host, host_id) {
|
||||
let hosts_srs = srsByContainer[host_id]
|
||||
foreach(hosts_srs, (one_srs) => {
|
||||
if (one_srs.SR_type !== 'iso' && one_srs.SR_type !== 'udev') {
|
||||
one_srs = clone(one_srs)
|
||||
one_srs.ratio = one_srs.size ? one_srs.physical_usage / one_srs.size : 0
|
||||
one_srs.host_label = host.name_label
|
||||
one_srs.pool_label = pool.name_label
|
||||
srs.push(one_srs)
|
||||
}
|
||||
})
|
||||
nb_hosts++
|
||||
let VMs = vmsByContainer[host_id]
|
||||
foreach(VMs, function (VM) {
|
||||
vms.states[runningStateToIndex[VM['power_state']]]++
|
||||
vms.nb++
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
$scope.hosts.nb = nb_hosts
|
||||
$scope.vms = vms
|
||||
$scope.pools.nb = nb_pools
|
||||
$scope.srs = srs
|
||||
$scope.ram = [xoApi.stats.$memory.usage, xoApi.stats.$memory.size - xoApi.stats.$memory.usage]
|
||||
$scope.cpu = [[xoApi.stats.$vCPUs], [xoApi.stats.$CPUs]]
|
||||
}
|
||||
|
||||
const debouncedPopulate = debounce(populateChartsData, 300, {leading: true, trailing: true})
|
||||
|
||||
debouncedPopulate()
|
||||
|
||||
$scope.$on('$destroy', xoApi.onUpdate(debouncedPopulate))
|
||||
})
|
||||
|
||||
.name
|
||||
139
app/modules/dashboard/overview/view.jade
Normal file
@@ -0,0 +1,139 @@
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.fa.fa-dashboard
|
||||
| Dashboard
|
||||
.grid-sm
|
||||
.grid-cell
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-cloud
|
||||
| Pools
|
||||
.panel-body.text-center
|
||||
p.big-stat {{pools.nb}}
|
||||
.grid-cell
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-server
|
||||
| Hosts
|
||||
.panel-body.text-center
|
||||
p.big-stat {{hosts.nb}}
|
||||
.grid-cell
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-desktop
|
||||
| VMs
|
||||
.panel-body.text-center
|
||||
p.big-stat {{vms.nb}}
|
||||
.grid-sm
|
||||
.grid-cell
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-memory
|
||||
| Global RAM usage
|
||||
.panel-body.text-center
|
||||
canvas(
|
||||
id="doughnut"
|
||||
class="chart chart-doughnut"
|
||||
data="ram"
|
||||
labels="['Used', 'Free']"
|
||||
options='{responsive: false,tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>"}'
|
||||
)
|
||||
|
||||
.grid-cell
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-dashboard
|
||||
| vCPUs/CPUs
|
||||
.panel-body.text-center
|
||||
canvas(
|
||||
id="bar"
|
||||
class="chart chart-bar"
|
||||
data="cpu"
|
||||
labels="['']"
|
||||
series="['vCPUs','CPUs']"
|
||||
options="{scaleShowGridLines: false, barDatasetSpacing : 10, showScale: false, responsive: false}"
|
||||
)
|
||||
|
||||
|
||||
.grid-cell
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-question-circle
|
||||
| VMs power state
|
||||
.panel-body.text-center
|
||||
canvas(
|
||||
id="pie"
|
||||
class="chart chart-pie"
|
||||
data="vms.states"
|
||||
labels="['Running', 'Halted', 'Suspended', 'Action']"
|
||||
colours="['#5cb85c', '#d9534f', '#5bc0de', '#f0ad4e']"
|
||||
options="{responsive: false}"
|
||||
)
|
||||
.grid-sm
|
||||
.grid-cell
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-database
|
||||
| Storage
|
||||
.panel-body
|
||||
table.table.table-hover
|
||||
tr
|
||||
th Name
|
||||
th pool
|
||||
th host
|
||||
th Format
|
||||
th Size
|
||||
th Physical/Allocated usage
|
||||
//- TODO: display PBD status for each SR of this host (connected or not)
|
||||
//- Shared SR
|
||||
tr(xo-sref="SRs_view({id: SR.id})", ng-repeat="SR in srs | map | orderBy:'-ratio' track by SR.id")
|
||||
td.oneliner
|
||||
| {{SR.name_label}}
|
||||
td.oneliner
|
||||
| {{SR.pool_label}}
|
||||
td.oneliner
|
||||
| {{SR.host_label}}
|
||||
td {{SR.SR_type}}
|
||||
td {{SR.size | bytesToSize}}
|
||||
td
|
||||
.progress-condensed
|
||||
.progress-bar(
|
||||
role="progressbar",
|
||||
aria-valuemin="0",
|
||||
aria-valuenow="{{SR.physical_usage}}",
|
||||
aria-valuemax="{{SR.size}}",
|
||||
style="width: {{[SR.physical_usage, SR.size] | percentage}}",
|
||||
tooltip="Physical usage: {{[SR.physical_usage, SR.size] | percentage}}"
|
||||
)
|
||||
.grid-sm
|
||||
.grid-cell
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-refresh
|
||||
| Updates
|
||||
span.quick-edit(
|
||||
tooltip="Update all"
|
||||
ng-click="installAllPatches()"
|
||||
)
|
||||
i.fa.fa-download.fa-fw
|
||||
.panel-body
|
||||
table.table.table-hover
|
||||
tr
|
||||
th Pool
|
||||
th Host
|
||||
th Description
|
||||
th Missing patches
|
||||
th Install
|
||||
tbody(ng-repeat="pool in pools.all | map | orderBy:'name_label'")
|
||||
tr( ng-repeat="host in hostsByPool[pool.id]" ng-if="nbUpdates[host.id]")
|
||||
td.oneliner
|
||||
| {{ pool.name_label }}
|
||||
td.oneliner
|
||||
| {{ host.name_label }}
|
||||
td.oneliner
|
||||
| {{ host.name_description }}
|
||||
td {{ nbUpdates[host.id] }}
|
||||
td
|
||||
button.btn.btn-success(ng-click="installHostPatches(host.id)" tooltip="Install {{ nbUpdates[host.id] }} patch(es)")
|
||||
| Update host
|
||||
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}}'
|
||||
)
|
||||
20
app/modules/dashboard/view.jade
Normal file
@@ -0,0 +1,20 @@
|
||||
.menu-grid
|
||||
.side-menu
|
||||
ul.nav
|
||||
li
|
||||
a(ui-sref = '.overview', ui-sref-active = 'active')
|
||||
i.fa.fa-fw.fa-dashboard.fa-menu
|
||||
span.menu-entry Overview
|
||||
li
|
||||
a(ui-sref = '.dataviz({chart:""})')
|
||||
i.fa.fa-fw.fa-pie-chart.fa-menu
|
||||
span.menu-entry Dataviz
|
||||
li
|
||||
a(ui-sref = '.stats')
|
||||
i.fa.fa-fw.fa-bar-chart.fa-menu
|
||||
span.menu-entry Stats
|
||||
li
|
||||
a(ui-sref = '.health')
|
||||
i.fa.fa-fw.fa-heartbeat.fa-menu
|
||||
span.menu-entry Health
|
||||
.side-content(ui-view = '')
|
||||
@@ -1,16 +1,19 @@
|
||||
import angular from 'angular';
|
||||
import uiBootstrap from 'angular-ui-bootstrap';
|
||||
// TODO: should be integrated xo.deleteVms()
|
||||
|
||||
import xoServices from 'xo-services';
|
||||
import angular from 'angular'
|
||||
import forEach from 'lodash.foreach'
|
||||
import uiBootstrap from 'angular-ui-bootstrap'
|
||||
|
||||
import view from './view';
|
||||
import xoServices from 'xo-services'
|
||||
|
||||
//====================================================================
|
||||
import view from './view'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default angular.module('xoWebApp.deleteVms', [
|
||||
uiBootstrap,
|
||||
|
||||
xoServices,
|
||||
xoServices
|
||||
])
|
||||
.controller('DeleteVmsCtrl', function (
|
||||
$scope,
|
||||
@@ -19,23 +22,23 @@ export default angular.module('xoWebApp.deleteVms', [
|
||||
VMsIds
|
||||
) {
|
||||
$scope.$watchCollection(() => xoApi.get(VMsIds), function (VMs) {
|
||||
$scope.VMs = VMs;
|
||||
});
|
||||
$scope.VMs = VMs
|
||||
})
|
||||
|
||||
// Do disks have to be deleted for a given VM.
|
||||
let disks = $scope.disks = {};
|
||||
angular.forEach(VMsIds, id => {
|
||||
disks[id] = true;
|
||||
});
|
||||
let disks = $scope.disks = {}
|
||||
forEach(VMsIds, id => {
|
||||
disks[id] = true
|
||||
})
|
||||
|
||||
$scope.delete = function () {
|
||||
let value = [];
|
||||
angular.forEach(VMsIds, id => {
|
||||
value.push([id, disks[id]]);
|
||||
});
|
||||
let value = []
|
||||
forEach(VMsIds, id => {
|
||||
value.push([id, disks[id]])
|
||||
})
|
||||
|
||||
$modalInstance.close(value);
|
||||
};
|
||||
$modalInstance.close(value)
|
||||
}
|
||||
})
|
||||
.service('deleteVmsModal', function ($modal, xo) {
|
||||
return function (ids) {
|
||||
@@ -46,16 +49,15 @@ export default angular.module('xoWebApp.deleteVms', [
|
||||
VMsIds: () => ids
|
||||
}
|
||||
}).result.then(function (toDelete) {
|
||||
let promises = [];
|
||||
let promises = []
|
||||
|
||||
angular.forEach(toDelete, ([id, deleteDisks]) => {
|
||||
promises.push(xo.vm.delete(id, deleteDisks));
|
||||
});
|
||||
forEach(toDelete, ([id, deleteDisks]) => {
|
||||
promises.push(xo.vm.delete(id, deleteDisks))
|
||||
})
|
||||
|
||||
return promises;
|
||||
});
|
||||
};
|
||||
return promises
|
||||
})
|
||||
}
|
||||
})
|
||||
// A module exports its name.
|
||||
.name
|
||||
;
|
||||
|
||||
@@ -12,11 +12,14 @@ form(ng-submit="delete()")
|
||||
th.col-sm-6 Description
|
||||
th.col-sm-3 Delete disks?
|
||||
tbody
|
||||
tr(ng-repeat="VM in VMs | orderBy:natural('name_label') track by VM.UUID")
|
||||
tr(ng-repeat="VM in VMs | orderBy:natural('name_label') track by VM.id")
|
||||
td {{VM.name_label}}
|
||||
td {{VM.name_description}}
|
||||
td
|
||||
input(type="checkbox", ng-model="disks[VM.UUID]")
|
||||
input(type="checkbox", ng-model="disks[VM.id]")
|
||||
p
|
||||
i.fa.fa-exclamation-triangle
|
||||
| All snapshots will be deleted too
|
||||
.modal-footer
|
||||
button.btn.btn-primary(type="submit")
|
||||
| Delete
|
||||
|
||||
@@ -1,40 +1,52 @@
|
||||
import angular from 'angular';
|
||||
import uiBootstrap from 'angular-ui-bootstrap';
|
||||
import angular from 'angular'
|
||||
import uiBootstrap from 'angular-ui-bootstrap'
|
||||
|
||||
//====================================================================
|
||||
import template from './view'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default angular.module('xoWebApp.genericModal', [
|
||||
uiBootstrap,
|
||||
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) {
|
||||
var 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
|
||||
}
|
||||
})
|
||||
|
||||
// A module exports its name.
|
||||
.name
|
||||
;
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
angular = require 'angular'
|
||||
forEach = require 'lodash.foreach'
|
||||
intersection = require 'lodash.intersection'
|
||||
map = require 'lodash.map'
|
||||
omit = require 'lodash.omit'
|
||||
sum = require 'lodash.sum'
|
||||
throttle = require 'lodash.throttle'
|
||||
find = require 'lodash.find'
|
||||
filter = require 'lodash.filter'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
module.exports = angular.module 'xoWebApp.host', [
|
||||
require 'angular-file-upload'
|
||||
require 'angular-ui-router'
|
||||
require('ng-file-upload')
|
||||
require('tag').default
|
||||
]
|
||||
.config ($stateProvider) ->
|
||||
$stateProvider.state 'hosts_view',
|
||||
@@ -13,20 +21,103 @@ module.exports = angular.module 'xoWebApp.host', [
|
||||
controller: 'HostCtrl'
|
||||
template: require './view'
|
||||
.controller 'HostCtrl', (
|
||||
$scope, $stateParams
|
||||
$upload
|
||||
$scope, $stateParams, $http
|
||||
$timeout
|
||||
$window
|
||||
dateFilter
|
||||
Upload
|
||||
xoApi, xo, modal, notify, bytesToSizeFilter
|
||||
) ->
|
||||
do (
|
||||
hostId = $stateParams.id
|
||||
controllers = xoApi.getIndex('vmControllersByContainer')
|
||||
poolPatches = xoApi.getIndex('poolPatchesByPool')
|
||||
srs = xoApi.getIndex('srsByContainer')
|
||||
tasks = xoApi.getIndex('runningTasksByHost')
|
||||
vms = xoApi.getIndex('vmsByContainer')
|
||||
) ->
|
||||
Object.defineProperties($scope, {
|
||||
controller: {
|
||||
get: () => controllers[hostId]
|
||||
},
|
||||
poolPatches: {
|
||||
get: () => $scope.host && poolPatches[$scope.host.$poolId]
|
||||
},
|
||||
sharedSrs: {
|
||||
get: () => $scope.host && srs[$scope.host.$poolId]
|
||||
},
|
||||
srs: {
|
||||
get: () => srs[hostId]
|
||||
},
|
||||
tasks: {
|
||||
get: () => tasks[hostId]
|
||||
},
|
||||
vms: {
|
||||
get: () => vms[hostId]
|
||||
}
|
||||
})
|
||||
|
||||
$window.bytesToSize = bytesToSizeFilter # FIXME dirty workaround to custom a Chart.js tooltip template
|
||||
host = null
|
||||
|
||||
$scope.currentPatchPage = 1
|
||||
$scope.currentLogPage = 1
|
||||
$scope.currentPCIPage = 1
|
||||
$scope.currentGPUPage = 1
|
||||
$scope.currentLicensePage = 1
|
||||
|
||||
$scope.refreshStatControl = refreshStatControl = {
|
||||
baseStatInterval: 5000
|
||||
baseTimeOut: 10000
|
||||
period: null
|
||||
running: false
|
||||
attempt: 0
|
||||
|
||||
start: () ->
|
||||
return if this.running
|
||||
this.stop()
|
||||
this.running = true
|
||||
this._reset()
|
||||
$scope.$on('$destroy', () => this.stop())
|
||||
return this._trig(Date.now())
|
||||
_trig: (t1) ->
|
||||
if this.running
|
||||
timeoutSecurity = $timeout(
|
||||
() => this.stop(),
|
||||
this.baseTimeOut
|
||||
)
|
||||
return $scope.refreshStats($scope.host.id)
|
||||
.then () => this._reset()
|
||||
.catch (err) =>
|
||||
if !this.running || this.attempt >= 2 || $scope.host.power_state isnt 'Running' || $scope.isVMWorking($scope.host)
|
||||
return this.stop()
|
||||
else
|
||||
this.attempt++
|
||||
.finally () =>
|
||||
$timeout.cancel(timeoutSecurity)
|
||||
if this.running
|
||||
t2 = Date.now()
|
||||
return this.period = $timeout(
|
||||
() => this._trig(t2),
|
||||
Math.max(this.baseStatInterval - (t2 - t1), 0)
|
||||
)
|
||||
_reset: () ->
|
||||
this.attempt = 0
|
||||
stop: () ->
|
||||
if this.period
|
||||
$timeout.cancel(this.period)
|
||||
this.running = false
|
||||
return
|
||||
}
|
||||
$scope.$watch(
|
||||
-> xoApi.get $stateParams.id
|
||||
(host) ->
|
||||
$scope.host = host
|
||||
return unless host?
|
||||
|
||||
$scope.pool = xoApi.get host.poolRef
|
||||
$scope.hostParams = Object.getOwnPropertyNames(host.license_params)
|
||||
|
||||
pool = $scope.pool = xoApi.get host.$poolId
|
||||
|
||||
SRsToPBDs = $scope.SRsToPBDs = Object.create null
|
||||
for PBD in host.$PBDs
|
||||
@@ -36,9 +127,17 @@ module.exports = angular.module 'xoWebApp.host', [
|
||||
continue unless PBD
|
||||
|
||||
SRsToPBDs[PBD.SR] = PBD
|
||||
$scope.listMissingPatches($scope.host.id)
|
||||
|
||||
if host.power_state is 'Running'
|
||||
refreshStatControl.start()
|
||||
else
|
||||
refreshStatControl.stop()
|
||||
)
|
||||
|
||||
$scope.removeMessage = xo.message.delete
|
||||
$scope.$watch('vms', (vms) =>
|
||||
$scope.vCPUs = sum(map(vms, (vm) => +vm.CPUs.number))
|
||||
)
|
||||
|
||||
$scope.cancelTask = (id) ->
|
||||
modal.confirm({
|
||||
@@ -62,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'
|
||||
@@ -108,19 +217,32 @@ 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
|
||||
|
||||
$data = {
|
||||
id: host.UUID
|
||||
id: host.id
|
||||
}
|
||||
if name_label isnt host.name_label
|
||||
$data.name_label = name_label
|
||||
if name_description isnt host.name_description
|
||||
$data.name_description = name_description
|
||||
if enabled isnt host.enabled
|
||||
$data.enabled = host.enabled
|
||||
if host.enabled
|
||||
$scope.disableHost($data.id)
|
||||
else
|
||||
$scope.enableHost($data.id)
|
||||
# enabled is not set via the "set" method, so we remove it before send it
|
||||
delete $data.enabled
|
||||
|
||||
xoApi.call 'host.set', $data
|
||||
|
||||
@@ -129,43 +251,44 @@ module.exports = angular.module 'xoWebApp.host', [
|
||||
title: 'Log deletion'
|
||||
message: 'Are you sure you want to delete all the logs?'
|
||||
}).then ->
|
||||
for log in $scope.host.messages
|
||||
console.log "Remove log #{log}"
|
||||
xo.log.delete log
|
||||
forEach $scope.host.messages, (log) ->
|
||||
console.log "Remove log #{log.id}"
|
||||
xo.log.delete log.id
|
||||
return
|
||||
|
||||
$scope.deleteLog = (id) ->
|
||||
console.log "Remove log #{id}"
|
||||
xo.log.delete id
|
||||
|
||||
$scope.connectPBD = (UUID) ->
|
||||
console.log "Connect PBD #{UUID}"
|
||||
$scope.connectPBD = (id) ->
|
||||
console.log "Connect PBD #{id}"
|
||||
|
||||
xoApi.call 'pbd.connect', {id: UUID}
|
||||
xoApi.call 'pbd.connect', {id: id}
|
||||
|
||||
$scope.disconnectPBD = (UUID) ->
|
||||
console.log "Disconnect PBD #{UUID}"
|
||||
$scope.disconnectPBD = (id) ->
|
||||
console.log "Disconnect PBD #{id}"
|
||||
|
||||
xoApi.call 'pbd.disconnect', {id: UUID}
|
||||
xoApi.call 'pbd.disconnect', {id: id}
|
||||
|
||||
$scope.removePBD = (UUID) ->
|
||||
console.log "Remove PBD #{UUID}"
|
||||
$scope.removePBD = (id) ->
|
||||
console.log "Remove PBD #{id}"
|
||||
|
||||
xoApi.call 'pbd.delete', {id: UUID}
|
||||
xoApi.call 'pbd.delete', {id: id}
|
||||
|
||||
$scope.connectPIF = (UUID) ->
|
||||
console.log "Connect PIF #{UUID}"
|
||||
$scope.connectPIF = (id) ->
|
||||
console.log "Connect PIF #{id}"
|
||||
|
||||
xoApi.call 'pif.connect', {id: UUID}
|
||||
xoApi.call 'pif.connect', {id: id}
|
||||
|
||||
$scope.disconnectPIF = (UUID) ->
|
||||
console.log "Disconnect PIF #{UUID}"
|
||||
$scope.disconnectPIF = (id) ->
|
||||
console.log "Disconnect PIF #{id}"
|
||||
|
||||
xoApi.call 'pif.disconnect', {id: UUID}
|
||||
xoApi.call 'pif.disconnect', {id: id}
|
||||
|
||||
$scope.removePIF = (UUID) ->
|
||||
console.log "Remove PIF #{UUID}"
|
||||
$scope.removePIF = (id) ->
|
||||
console.log "Remove PIF #{id}"
|
||||
|
||||
xoApi.call 'pif.delete', {id: UUID}
|
||||
xoApi.call 'pif.delete', {id: id}
|
||||
|
||||
$scope.importVm = ($files, id) ->
|
||||
file = $files[0]
|
||||
@@ -176,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
|
||||
@@ -196,7 +319,7 @@ module.exports = angular.module 'xoWebApp.host', [
|
||||
}
|
||||
|
||||
params = {
|
||||
host: $scope.host.UUID
|
||||
pool: $scope.host.$pool
|
||||
name,
|
||||
}
|
||||
|
||||
@@ -205,10 +328,139 @@ 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)
|
||||
return false if not hostPatch.length
|
||||
hostPatch = xoApi.get(hostPatch[0])
|
||||
return hostPatch.applied
|
||||
|
||||
$scope.listMissingPatches = (id) ->
|
||||
return xo.host.listMissingPatches id
|
||||
.then (result) ->
|
||||
$scope.updates = omit(result,map($scope.poolPatches,'id'))
|
||||
|
||||
$scope.installPatch = (id, patchUid) ->
|
||||
console.log("Install patch "+patchUid+" on "+id)
|
||||
notify.info {
|
||||
title: 'Patch host'
|
||||
message: "Patching the host, please wait..."
|
||||
}
|
||||
xo.host.installPatch id, patchUid
|
||||
|
||||
$scope.installAllPatches = (id) ->
|
||||
modal.confirm({
|
||||
title: 'Install all the missing patches'
|
||||
message: 'Are you sure you want to install all the missing patches on this host? This could take a while...'
|
||||
}).then ->
|
||||
console.log('Installing all patches on host ' + id)
|
||||
xo.host.installAllPatches id
|
||||
|
||||
$scope.refreshStats = (id) ->
|
||||
return xo.host.refreshStats id
|
||||
.then (result) ->
|
||||
result.stats.cpuSeries = []
|
||||
|
||||
if result.stats.cpus.length >= 12
|
||||
nValues = result.stats.cpus[0].length
|
||||
nCpus = result.stats.cpus.length
|
||||
cpuAVG = (0 for [1..nValues])
|
||||
|
||||
forEach result.stats.cpus, (cpu) ->
|
||||
forEach cpu, (stat, index) ->
|
||||
cpuAVG[index] += stat
|
||||
return
|
||||
return
|
||||
|
||||
forEach cpuAVG, (cpu, index) ->
|
||||
cpuAVG[index] /= nCpus
|
||||
return
|
||||
|
||||
result.stats.cpus = [cpuAVG]
|
||||
result.stats.cpuSeries.push 'CPU AVG'
|
||||
else
|
||||
forEach result.stats.cpus, (v,k) ->
|
||||
result.stats.cpuSeries.push 'CPU ' + k
|
||||
return
|
||||
|
||||
result.stats.pifSeries = []
|
||||
pifsArray = []
|
||||
forEach result.stats.pifs.rx, (v,k) ->
|
||||
return unless v
|
||||
result.stats.pifSeries.push '#' + k + ' in'
|
||||
result.stats.pifSeries.push '#' + k + ' out'
|
||||
pifsArray.push (v || [])
|
||||
pifsArray.push (result.stats.pifs.tx[k] || [])
|
||||
return
|
||||
result.stats.pifs = pifsArray
|
||||
|
||||
forEach result.stats.memoryUsed, (v, k) ->
|
||||
result.stats.memoryUsed[k] = v*1024
|
||||
forEach result.stats.memory, (v, k) ->
|
||||
result.stats.memory[k] = v*1024
|
||||
|
||||
result.stats.date = []
|
||||
timestamp = result.endTimestamp
|
||||
for i in [result.stats.memory.length-1..0] by -1
|
||||
result.stats.date.unshift new Date(timestamp*1000).toLocaleTimeString()
|
||||
timestamp -= 5
|
||||
$scope.stats = result.stats
|
||||
|
||||
$scope.statView = {
|
||||
cpuOnly: false,
|
||||
ramOnly: false,
|
||||
netOnly: false,
|
||||
loadOnly: false
|
||||
}
|
||||
|
||||
$scope.canAdmin = (id = undefined) ->
|
||||
if id == undefined
|
||||
id = $scope.host && $scope.host.id
|
||||
|
||||
return id && xoApi.canInteract(id, 'administrate') || false
|
||||
|
||||
$scope.canOperate = (id = undefined) ->
|
||||
if id == undefined
|
||||
id = $scope.host && $scope.host.id
|
||||
|
||||
return id && xoApi.canInteract(id, 'operate') || false
|
||||
|
||||
$scope.canView = (id = undefined) ->
|
||||
if id == undefined
|
||||
id = $scope.host && $scope.host.id
|
||||
|
||||
return id && xoApi.canInteract(id, 'view') || false
|
||||
# A module exports its name.
|
||||
.name
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
.grid
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.xo-icon-host(class="xo-color-{{host.power_state | lowercase}}")
|
||||
| {{host.name_label}}
|
||||
small(ng-if="pool.name_label")
|
||||
| (
|
||||
a(ui-sref="pools_view({id: pool.UUID})") {{pool.name_label}}
|
||||
a(ui-sref="pools_view({id: pool.id})") {{pool.name_label}}
|
||||
| )
|
||||
p.center {{host.bios_strings["system-manufacturer"]}} {{host.bios_strings["system-product-name"]}}
|
||||
.grid
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-cogs(style="color: #e25440;")
|
||||
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
|
||||
@@ -28,14 +30,11 @@
|
||||
| {{host.name_description}}
|
||||
dt Enabled
|
||||
dd
|
||||
span(editable-checkbox="host.enabled", e-name="enabled", e-form="hostSettings")
|
||||
| {{host.enabled}}
|
||||
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
|
||||
@@ -45,6 +44,12 @@
|
||||
dd {{host.UUID}}
|
||||
dt iQN
|
||||
dd {{host.iSCSI_name}}
|
||||
dt(ng-if="refreshStatControl.running && stats") vCPUs/CPUs:
|
||||
dd(ng-if="refreshStatControl.running && stats") {{vCPUs}}/{{host.CPUs['cpu_count']}}
|
||||
dt(ng-if="refreshStatControl.running && stats") Running VMs:
|
||||
dd(ng-if="refreshStatControl.running && stats") {{vms | count}}
|
||||
dt(ng-if="refreshStatControl.running && stats") RAM (used/free):
|
||||
dd(ng-if="refreshStatControl.running && stats") {{host.memory.usage | bytesToSize}}/{{host.memory.size | bytesToSize}}
|
||||
.btn-form(ng-show="hostSettings.$visible")
|
||||
p.center
|
||||
button.btn.btn-default(type="button", ng-disabled="hostSettings.$waiting", ng-click="hostSettings.$cancel()")
|
||||
@@ -56,94 +61,231 @@
|
||||
| Save
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-stats(style="color: #e25440;")
|
||||
i.xo-icon-stats
|
||||
| Stats
|
||||
.panel-body
|
||||
.grid
|
||||
.grid-cell
|
||||
.panel-body(ng-if="refreshStatControl.running && stats")
|
||||
div(ng-if="statView.cpuOnly", ng-click="statView.cpuOnly = false")
|
||||
p.stat-name
|
||||
i.xo-icon-cpu
|
||||
| CPU usage
|
||||
canvas.chart.chart-line.chart-stat-full(
|
||||
id="bigCpu"
|
||||
data="stats.cpus"
|
||||
labels="stats.date"
|
||||
series="stats.cpuSeries"
|
||||
colours="['#0000ff', '#9999ff', '#000099', '#5555ff', '#000055']"
|
||||
legend="true"
|
||||
options='{responsive: true, maintainAspectRatio: false, tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= Math.round(10*value)/10 %>", multiTooltipTemplate:"<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%= Math.round(10*value)/10 %>", pointDot: false, showScale: false, animation: false, datasetStrokeWidth: 0.8, scaleOverride: true, scaleSteps: 100, scaleStartValue: 0, scaleStepWidth: 1, pointHitDetectionRadius: 0}'
|
||||
)
|
||||
div(ng-if="statView.ramOnly", ng-click="statView.ramOnly = false")
|
||||
p.stat-name
|
||||
i.xo-icon-memory
|
||||
//- i.fa.fa-bar-chart
|
||||
//- i.fa.fa-tasks
|
||||
//- i.fa.fa-server
|
||||
| RAM usage
|
||||
canvas.chart.chart-line.chart-stat-full(
|
||||
id="bigRam"
|
||||
data="[stats.memoryUsed,stats.memory]"
|
||||
labels="stats.date"
|
||||
series="['Used RAM', 'Total RAM']"
|
||||
colours="['#ff0000', '#ffbbbb']"
|
||||
legend="true"
|
||||
options='{responsive: true, maintainAspectRatio: false, tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>", multiTooltipTemplate:"<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%= bytesToSize(value) %>", datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false, pointHitDetectionRadius: 0}'
|
||||
)
|
||||
div(ng-if="statView.netOnly", ng-click="statView.netOnly = false")
|
||||
p.stat-name
|
||||
i.xo-icon-network
|
||||
| Network I/O
|
||||
canvas.chart.chart-line.chart-stat-full(
|
||||
id="bigNet"
|
||||
data="stats.pifs"
|
||||
labels="stats.date"
|
||||
series="stats.pifSeries"
|
||||
colours="['#dddd00', '#dddd77', '#777700', '#dddd55', '#555500', '#ffdd00']"
|
||||
legend="true"
|
||||
options='{responsive: true, maintainAspectRatio: false, tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>", multiTooltipTemplate:"<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%= bytesToSize(value) %>", datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false, pointHitDetectionRadius: 0}'
|
||||
)
|
||||
div(ng-if="statView.loadOnly", ng-click="statView.loadOnly = false")
|
||||
p.stat-name
|
||||
i.fa.fa-cogs
|
||||
| Load Average
|
||||
canvas.chart.chart-line.chart-stat-full(
|
||||
id="bigLoad"
|
||||
data="[stats.load]"
|
||||
labels="stats.date"
|
||||
series="['Load']"
|
||||
colours="['#960094']"
|
||||
legend="true"
|
||||
options='{responsive: true, maintainAspectRatio: false, multiTooltipTemplate:"<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%= bytesToSize(value) %>", datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false, pointHitDetectionRadius: 0}'
|
||||
)
|
||||
div(ng-if="!statView.netOnly && !statView.loadOnly && !statView.cpuOnly && !statView.ramOnly")
|
||||
.row
|
||||
.col-md-6(ng-click="statView.cpuOnly=true")
|
||||
p.stat-name
|
||||
i.xo-icon-cpu
|
||||
| CPU usage
|
||||
canvas.chart.chart-line.chart-stat-preview(
|
||||
id="smallCpu"
|
||||
data="stats.cpus"
|
||||
labels="stats.date"
|
||||
series="stats.cpuSeries"
|
||||
colours="['#0000ff', '#9999ff', '#000099', '#5555ff', '#000055']"
|
||||
options='{responsive: true, maintainAspectRatio: false, showTooltips: false, pointDot: false, showScale: false, animation: false, datasetStrokeWidth: 0.8, scaleOverride: true, scaleSteps: 100, scaleStartValue: 0, scaleStepWidth: 1}'
|
||||
)
|
||||
.col-md-6(ng-click="statView.ramOnly=true")
|
||||
p.stat-name
|
||||
i.xo-icon-memory
|
||||
//- i.fa.fa-bar-chart
|
||||
//- i.fa.fa-tasks
|
||||
//- i.fa.fa-server
|
||||
| RAM usage
|
||||
canvas.chart.chart-line.chart-stat-preview(
|
||||
id="smallRam"
|
||||
data="[stats.memoryUsed,stats.memory]"
|
||||
labels="stats.date"
|
||||
series="['Used RAM', 'Total RAM']"
|
||||
colours="['#ff0000', '#ffbbbb']"
|
||||
options="{responsive: true, maintainAspectRatio: false, showTooltips: false, datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false}"
|
||||
)
|
||||
.row
|
||||
.col-md-6(ng-click="statView.netOnly=true")
|
||||
p.stat-name
|
||||
i.xo-icon-network
|
||||
| Network I/O
|
||||
canvas.chart.chart-line.chart-stat-preview(
|
||||
id="smallNet"
|
||||
data="stats.pifs"
|
||||
labels="stats.date"
|
||||
series="stats.pifSeries"
|
||||
colours="['#dddd00', '#dddd77', '#777700', '#dddd55', '#555500', '#ffdd00']"
|
||||
options="{responsive: true, maintainAspectRatio: false, showTooltips: false, datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false}"
|
||||
)
|
||||
.col-md-6(ng-click="statView.loadOnly=true")
|
||||
p.stat-name
|
||||
i.fa.fa-cogs
|
||||
| Load Average
|
||||
canvas.chart.chart-line.chart-stat-preview(
|
||||
id="smallDisk"
|
||||
data="[stats.load]"
|
||||
labels="stats.date"
|
||||
series="['Load']"
|
||||
colours="['#960094']"
|
||||
options="{responsive: true, maintainAspectRatio: false, showTooltips: false, datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false}"
|
||||
)
|
||||
.panel-body(ng-if="!refreshStatControl.running || !stats")
|
||||
.row
|
||||
.col-sm-4
|
||||
p.stat-name CPU usage:
|
||||
canvas(
|
||||
id="bar"
|
||||
class="chart chart-bar"
|
||||
data="[[host.$vCPUs], [host.CPUs['cpu_count']]]"
|
||||
labels="['']"
|
||||
series="['vCPUs','CPUs']"
|
||||
options="{scaleShowGridLines: false, barDatasetSpacing : 10, showScale: false}"
|
||||
)
|
||||
.grid-cell
|
||||
p.center.mid-stat {{vCPUs}}/{{host.CPUs['cpu_count']}}
|
||||
.col-sm-4
|
||||
p.stat-name RAM used:
|
||||
canvas(id="doughnut", class="chart chart-doughnut", data="[(host.memory.usage), (host.memory.size - host.memory.usage)]", labels="['Used', 'Free']", options='{tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>"}')
|
||||
.grid-cell
|
||||
p.center.mid-stat {{host.memory.usage | bytesToSize}}
|
||||
.col-sm-4
|
||||
p.stat-name Running VMs:
|
||||
p.center.big-stat {{host.VMs.length}}
|
||||
p.center.mid-stat {{vms | count}}
|
||||
p.center(ng-if="refreshStatControl.running")
|
||||
i.xo-icon-loading
|
||||
| Fetching stats...
|
||||
//- Action panel
|
||||
.grid
|
||||
.grid-sm(ng-if = 'canOperate()')
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-flash(style="color: #e25440;")
|
||||
i.fa.fa-flash
|
||||
| Actions
|
||||
.panel-body.text-center
|
||||
.grid
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Add SR", type="button", style="width: 90%", xo-sref="SRs_new({container: host.UUID})")
|
||||
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.UUID})")
|
||||
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.UUID)")
|
||||
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.UUID)")
|
||||
i.fa.fa-power-off.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(ng-if="host.enabled")
|
||||
button.btn(tooltip="Disable host", type="button", style="width: 90%", xo-click="disableHost(host.UUID)")
|
||||
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.UUID)")
|
||||
i.fa.fa-check-circle.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Restart toolstack", type="button", style="width: 90%", xo-click="restartToolStack(host.UUID)")
|
||||
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.UUID)")
|
||||
i.fa.fa-cloud-upload.fa-2x.fa-fw
|
||||
.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.UUID)")
|
||||
i.fa.fa-cloud-download.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(style="margin-bottom: 0.5em")
|
||||
button.btn(
|
||||
tooltip="Import VM"
|
||||
type="button"
|
||||
style="width: 90%"
|
||||
ng-file-select = 'importVm($files, host.UUID)'
|
||||
.grid-sm.grid--gutters
|
||||
.grid.grid-cell
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Add SR", tooltip-placement="top", type="button", style="width: 90%", xo-sref="SRs_new({container: host.id})", ng-if = 'canAdmin()')
|
||||
i.xo-icon-sr.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Add VM", tooltip-placement="top", type="button", style="width: 90%", xo-sref="VMs_new({container: host.id})", ng-if = 'canAdmin()')
|
||||
i.xo-icon-vm.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Reboot host", tooltip-placement="top", type="button", style="width: 90%", xo-click="rebootHost(host.id)", ng-if = 'canOperate()')
|
||||
i.fa.fa-refresh.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Shutdown host", tooltip-placement="top", type="button", style="width: 90%", xo-click="shutdownHost(host.id)", ng-if = 'canOperate()')
|
||||
i.fa.fa-power-off.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Suspend all VMs and shutdown host", tooltip-placement="top", type="button", style="width: 90%", xo-click="emergencyShutdownHost(host.id)", ng-if = 'canOperate()')
|
||||
i.fa.fa-exclamation-triangle.fa-2x.fa-fw
|
||||
.grid.grid-cell
|
||||
.grid-cell.btn-group(ng-if="host.enabled")
|
||||
button.btn(tooltip="Disable host", tooltip-placement="top", type="button", style="width: 90%", xo-click="disableHost(host.id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-times-circle.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(ng-if="!host.enabled")
|
||||
button.btn(tooltip="Enable host", tooltip-placement="top", type="button", style="width: 90%", xo-click="enableHost(host.id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-check-circle.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Restart toolstack", tooltip-placement="top", type="button", style="width: 90%", xo-click="restartToolStack(host.id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-retweet.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(ng-if="pool.name_label && (hostsByPool[pool.id] | count)>1")
|
||||
button.btn(tooltip="Remove from pool", tooltip-placement="top", style="width: 90%", type="button", xo-click="pool_removeHost(host.id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-cloud-upload.fa-2x.fa-fw
|
||||
.grid-cell.btn-group.dropdown(
|
||||
ng-if="pool.name_label && (hostsByPool[pool.id] | count)==1"
|
||||
dropdown
|
||||
)
|
||||
i.fa.fa-upload.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(style="margin-bottom: 0.5em")
|
||||
button.btn(tooltip="Host console", type="button", style="width: 90%", ng-repeat="controller in [host.controller] | resolve track by controller.UUID", xo-sref="consoles_view({id: controller.UUID})")
|
||||
i.xo-icon-console.fa-2x.fa-fw
|
||||
|
||||
button.btn.dropdown-toggle(
|
||||
ng-if = 'canAdmin()'
|
||||
dropdown-toggle
|
||||
tooltip="Move host to another pool"
|
||||
tooltip-placement="top"
|
||||
type="button"
|
||||
style="width: 90%"
|
||||
)
|
||||
i.fa.fa-cloud-download.fa-2x.fa-fw
|
||||
span.caret
|
||||
ul.dropdown-menu.left(role="menu")
|
||||
li(ng-repeat="p in pools.all | map | orderBy:natural('name_label') track by p.id" ng-if="p!=pool")
|
||||
a(xo-click="pool_moveHost(p)")
|
||||
i.xo-icon-host.fa-fw
|
||||
| To {{p.name_label}}
|
||||
.grid-cell.btn-group(ng-if="!pool.name_label")
|
||||
button.btn(tooltip="Add to pool", tooltip-placement="top", style="width: 90%", type="button", xo-click="pool_addHost(host.id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-cloud-download.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(style="margin-bottom: 0.5em")
|
||||
button.btn(
|
||||
ng-if = 'canAdmin()'
|
||||
tooltip="Import VM"
|
||||
tooltip-placement="top"
|
||||
type="button"
|
||||
style="width: 90%"
|
||||
ngf-select = 'importVm($files, host.id)'
|
||||
)
|
||||
i.fa.fa-upload.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(style="margin-bottom: 0.5em")
|
||||
button.btn(
|
||||
tooltip="Host console"
|
||||
tooltip-placement="top"
|
||||
type="button"
|
||||
style="width: 90%"
|
||||
xo-sref="consoles_view({id: controller.id})"
|
||||
)
|
||||
i.xo-icon-console.fa-2x.fa-fw
|
||||
//- TODO: Memory panel
|
||||
.grid
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-memory(style="color: #e25440;")
|
||||
i.xo-icon-memory
|
||||
| Memory
|
||||
.panel-body.text-center
|
||||
.progress
|
||||
.progress-bar-host(ng-repeat="controller in [host.controller] | resolve track by controller.UUID", role="progressbar", aria-valuemin="0", aria-valuenow="{{controller.memory.size}}", aria-valuemax="{{host.memory.size}}", style="width: {{[controller.memory.size, host.memory.size] | %}}", tooltip="{{host.name_label}}: {{[controller.memory.size, host.memory.size] | %}}")
|
||||
.progress-bar-host(role="progressbar", aria-valuemin="0", aria-valuenow="{{controller.memory.size}}", aria-valuemax="{{host.memory.size}}", style="width: {{[controller.memory.size, host.memory.size] | percentage}}", tooltip="{{host.name_label}}: {{[controller.memory.size, host.memory.size] | percentage}}")
|
||||
small {{host.name_label}}
|
||||
.progress-bar.progress-bar-vm(ng-repeat="VM in host.VMs | resolve | orderBy:natural('name_label') track by VM.UUID", role="progressbar", aria-valuemin="0", aria-valuenow="{{VM.memory.size}}", aria-valuemax="{{host.memory.size}}", style="width: {{[VM.memory.size, host.memory.size] | %}}", xo-sref="VMs_view({id: VM.UUID})", tooltip="{{VM.name_label}}: {{[VM.memory.size, host.memory.size] | %}}")
|
||||
.progress-bar.progress-bar-vm(ng-repeat="VM in vms | map | orderBy:natural('name_label') track by VM.id", role="progressbar", aria-valuemin="0", aria-valuenow="{{VM.memory.size}}", aria-valuemax="{{host.memory.size}}", style="width: {{[VM.memory.size, host.memory.size] | percentage}}", xo-sref="VMs_view({id: VM.id})", tooltip="{{VM.name_label}}: {{[VM.memory.size, host.memory.size] | percentage}}")
|
||||
small {{VM.name_label}}
|
||||
ul.list-inline.text-center
|
||||
li Total: {{host.memory.size | bytesToSize}}
|
||||
li Currently used: {{host.memory.usage | bytesToSize}}
|
||||
li Available: {{host.memory.size-host.memory.usage | bytesToSize}}
|
||||
//- SR panel
|
||||
.grid
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-sr(style="color: #e25440;")
|
||||
i.xo-icon-sr
|
||||
| Storage
|
||||
.panel-body
|
||||
table.table.table-hover
|
||||
@@ -151,109 +293,151 @@
|
||||
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)
|
||||
//- Shared SR
|
||||
tr(xo-sref="SRs_view({id: SR.UUID})", ng-repeat="SR in pool.SRs | resolve | orderBy:natural('name_label') track by SR.UUID")
|
||||
td
|
||||
tr(xo-sref="SRs_view({id: SR.id})", ng-repeat="SR in sharedSrs | map | orderBy:natural('name_label') track by SR.id")
|
||||
td.oneliner
|
||||
| {{SR.name_label}}
|
||||
td {{SR.SR_type}}
|
||||
td {{SR.size | bytesToSize}}
|
||||
td
|
||||
.progress-condensed
|
||||
.progress-bar(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.usage}}", aria-valuemax="{{SR.size}}", style="width: {{[SR.physical_usage, SR.size] | %}}", tooltip="Physical usage: {{[SR.physical_usage, SR.size] | %}}")
|
||||
.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] | %}}", tooltip="Allocated: {{[(SR.usage), SR.size] | %}}")
|
||||
.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.ref].attached")
|
||||
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.ref].ref)")
|
||||
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.ref].attached")
|
||||
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.ref].ref)")
|
||||
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.ref].ref)")
|
||||
a(tooltip="Forget this SR", xo-click="removePBD(SRsToPBDs[SR.id].id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-ban.fa-lg
|
||||
//- Local SR
|
||||
//- TODO: migrate to SRs and not PBDs when implemented in xo-server spec
|
||||
tr(xo-sref="SRs_view({id: SR.UUID})", ng-repeat="SR in host.SRs | resolve | orderBy:natural('name_label') track by SR.UUID")
|
||||
tr(xo-sref="SRs_view({id: SR.id})", ng-repeat="SR in srs | map | orderBy:natural('name_label') track by SR.id")
|
||||
td
|
||||
| {{SR.name_label}}
|
||||
td {{SR.SR_type}}
|
||||
td {{SR.size | bytesToSize}}
|
||||
td
|
||||
.progress-condensed
|
||||
.progress-bar(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.usage}}", aria-valuemax="{{SR.size}}", style="width: {{[SR.physical_usage, SR.size] | %}}", tooltip="Physical usage: {{[SR.physical_usage, SR.size] | %}}")
|
||||
.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] | %}}", tooltip="Allocated: {{[(SR.usage), SR.size] | %}}")
|
||||
.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.ref].attached")
|
||||
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.ref].ref)")
|
||||
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.ref].attached")
|
||||
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.ref].ref)")
|
||||
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.ref].ref)")
|
||||
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
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-network(style="color: #e25440;")
|
||||
| Interfaces
|
||||
i.xo-icon-network
|
||||
| PIFs
|
||||
.panel-body
|
||||
table.table.table-hover
|
||||
th.col-md-1 Device
|
||||
th.col-md-1 Network
|
||||
th.col-md-1 VLAN
|
||||
th.col-md-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.UUID")
|
||||
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 {{PIF.IP}} ({{PIF.mode}})
|
||||
td {{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.ref)")
|
||||
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.ref)")
|
||||
i.fa.fa-link.fa-lg
|
||||
a(tooltip="Remove this interface", xo-click="removePIF(PIF.ref)")
|
||||
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
|
||||
@@ -274,109 +458,162 @@
|
||||
.form-group
|
||||
button.btn.btn-primary(type = 'submit')
|
||||
i.fa.fa-plus-square
|
||||
| Create
|
||||
| Create
|
||||
span(ng-if = 'createNetworkWaiting')
|
||||
|
|
||||
i.fa.fa-spin.fa-circle-o-notch
|
||||
i.xo-icon-loading-sm
|
||||
br
|
||||
//- CPU and Logs panels
|
||||
.grid
|
||||
.grid-sm
|
||||
//- Task panel
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title(ng-if="host.tasks.length")
|
||||
i.fa.fa-spinner.fa-pulse(style="color: #e25440;")
|
||||
.panel-heading.panel-title(ng-if="tasks | isNotEmpty")
|
||||
i.fa.fa-spinner.fa-pulse
|
||||
| Pending tasks
|
||||
.panel-heading.panel-title(ng-if="!host.tasks.length")
|
||||
i.fa.fa-spinner(style="color: #e25440;")
|
||||
.panel-heading.panel-title(ng-if="tasks | isEmpty")
|
||||
i.fa.fa-spinner
|
||||
| Pending tasks
|
||||
.panel-body
|
||||
p.center(ng-if="!host.tasks.length") No recent tasks
|
||||
table.table.table-hover(ng-if="host.tasks.length")
|
||||
p.center(ng-if="tasks | isEmpty") No recent tasks
|
||||
table.table.table-hover(ng-if="tasks | isNotEmpty")
|
||||
th Date
|
||||
th Progress
|
||||
th Name
|
||||
//- TODO: working reverse order, from recent to oldest
|
||||
tr(ng-repeat="task in host.tasks | resolve | orderBy:'created':true track by task.UUID")
|
||||
td {{task.created}}
|
||||
tr(ng-repeat="task in tasks | map | orderBy:'created':true track by task.id")
|
||||
td.oneliner {{task.created * 1e3 | date:'medium'}}
|
||||
td
|
||||
.progress-condensed
|
||||
.progress-bar.progress-bar-success.progress-bar-striped.active.progress-bar-black(role="progressbar", aria-valuemin="0", aria-valuenow="{{task.progress*100}}", aria-valuemax="100", style="width: {{task.progress*100}}%", tooltip="Progress: {{task.progress*100 | number:1}}%")
|
||||
| {{task.progress*100 | number:1}}%
|
||||
td
|
||||
td.oneliner
|
||||
| {{task.name_label}}
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(xo-click="cancelTask(task.UUID)")
|
||||
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.UUID)")
|
||||
a(xo-click="destroyTask(task.id)")
|
||||
i.fa.fa-trash-o.fa-lg(tooltip="Destroy this task")
|
||||
|
||||
|
||||
//- Logs panel
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-comments(style="color: #e25440;")
|
||||
i.fa.fa-comments
|
||||
| Logs
|
||||
span.quick-edit(ng-if="host.messages.length", 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.length") No recent logs
|
||||
table.table.table-hover(ng-if="host.messages.length")
|
||||
p.center(ng-if="host.messages | isEmpty") No recent logs
|
||||
table.table.table-hover(ng-if="host.messages | isNotEmpty")
|
||||
th Date
|
||||
th Name
|
||||
tr(ng-repeat="message in host.messages | resolve | orderBy:'-time' track by message.UUID")
|
||||
tr(ng-repeat="message in host.messages | map | orderBy:'-time' | slice:(5*(currentLogPage-1)):(5*currentLogPage) track by message.id")
|
||||
td {{message.time*1e3 | date:"medium"}}
|
||||
td
|
||||
| {{message.name}}
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(xo-click="deleteLog(message.UUID)")
|
||||
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")
|
||||
|
||||
.grid
|
||||
.center(ng-if = '(host.messages | count) > 5 || currentLogPage > 1')
|
||||
pagination(boundary-links="true", total-items="host.messages | count", ng-model="$parent.currentLogPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
|
||||
.grid-sm
|
||||
//- Patches panel
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-file-code-o(style="color: #e25440;")
|
||||
i.fa.fa-file-code-o
|
||||
| Patches
|
||||
span.quick-edit(ng-click="listMissingPatches(host.id)", tooltip="Check for updates")
|
||||
i.fa.fa-question-circle
|
||||
span.quick-edit(ng-click="installAllPatches(host.id)", tooltip="Install all the missing patches", style="margin-right:5px", ng-if = 'canAdmin()')
|
||||
i.fa.fa-download
|
||||
.panel-body
|
||||
p.center(ng-if="!host.patches.length") No patches
|
||||
table.table.table-hover(ng-if="host.patches.length")
|
||||
th Applied on
|
||||
th Name
|
||||
th Description
|
||||
th Status
|
||||
tr(ng-repeat="patch in host.patches | resolve | orderBy:'-time'")
|
||||
td {{patch.time*1e3 | date:"medium"}}
|
||||
td {{(patch.pool_patch | resolve).name_label}}
|
||||
td {{(patch.pool_patch | resolve).name_description}}
|
||||
//- TODO: allow patch application and removal
|
||||
table.table.table-hover(ng-if="poolPatches || updates")
|
||||
th.col-sm-2 Name
|
||||
th.col-sm-5 Description
|
||||
th.col-sm-3 Applied/Released date
|
||||
th.col-sm-1 Size
|
||||
th.col-sm-1 Status
|
||||
tr(
|
||||
ng-repeat="patch in updates"
|
||||
ng-if="!isPoolPatch(patch)"
|
||||
)
|
||||
td.oneliner {{patch.name}}
|
||||
td.oneliner
|
||||
a(href="{{patch.documentationUrl}}", target="_blank") {{patch.description}}
|
||||
td.oneliner {{patch.date | date:"medium"}}
|
||||
td -
|
||||
td
|
||||
span(ng-if="patch.applied")
|
||||
span(ng-click="installPatch(host.id, patch.uuid)", tooltip="Click to install the patch on this host", ng-if = 'canAdmin()')
|
||||
span.label.label-danger Missing
|
||||
span.label.label-danger(ng-if = '!canAdmin()') Missing
|
||||
tr(ng-repeat="patch in poolPatches | map | orderBy:'-name'| slice:(5*(currentPatchPage-1)):(5*currentPatchPage)")
|
||||
td.oneliner {{patch.name}}
|
||||
td.oneliner {{patch.description}}
|
||||
//- TODO: use a proper function for patch date, like poolPatchToHostPatch
|
||||
td.oneliner {{((patch.$host_patches[0]) | resolve).time*1e3 | date:"medium"}}
|
||||
td {{patch.size | bytesToSize}}
|
||||
td
|
||||
span(ng-if="isPoolPatchApplied(patch)")
|
||||
span.label.label-success Applied
|
||||
span(ng-if="!patch.applied")
|
||||
span.label.label-error Not applied
|
||||
//- span.pull-right.btn-group.quick-buttons
|
||||
//- a(xo-click="deletePatch(patch.UUID)")
|
||||
//- i.fa.fa-trash-o.fa-lg(tooltip="Remove this patch")
|
||||
.grid
|
||||
span(ng-if="!isPoolPatchApplied(patch)")
|
||||
span(ng-click="installPatch(host.id, patch.uuid)", tooltip="Click to apply the patch on this host", ng-if = 'canAdmin()')
|
||||
span.label.label-warning Not applied
|
||||
span.label.label-warning(ng-if = '!canAdmin()') Not applied
|
||||
.center(ng-if = '(poolPatches | count) > 5 || currentPatchPage > 1')
|
||||
pagination(boundary-links="true", total-items="poolPatches | count", ng-model="$parent.currentPatchPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
|
||||
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-plug(style="color: #e25440;")
|
||||
i.fa.fa-plug
|
||||
| PCI Devices
|
||||
.panel-body
|
||||
p.center(ng-if="!host.$PCIs") No PCI devices available
|
||||
table.table.table-hover(ng-if="host.$PCIs")
|
||||
th PCI Info
|
||||
th Device Name
|
||||
tr(ng-repeat="pci in host.$PCIs | resolve | orderBy:'pci_id' track by pci.UUID")
|
||||
td {{pci.pci_id}} ({{pci.class_name}})
|
||||
td {{pci.device_name}}
|
||||
tr(ng-repeat="pci in host.$PCIs | resolve | orderBy:'pci_id' | slice:(5*(currentPCIPage-1)):(5*currentPCIPage) track by pci.id")
|
||||
td.oneliner {{pci.pci_id}} ({{pci.class_name}})
|
||||
td.oneliner {{pci.device_name}}
|
||||
.center(ng-if = '(host.$PCIs | resolve).length > 5')
|
||||
pagination(boundary-links="true", total-items="(host.$PCIs | resolve).length", ng-model="$parent.currentPCIPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-desktop(style="color: #e25440;")
|
||||
i.fa.fa-desktop
|
||||
| GPUs
|
||||
.panel-body
|
||||
p.center(ng-if="host.$PGPUs.length === 0") No GPUs available
|
||||
table.table.table-hover(ng-if="host.$PGPUs.length !== 0")
|
||||
th Device
|
||||
tr(ng-repeat="pgpu in host.$PGPUs | resolve | orderBy:'device' track by pgpu.UUID")
|
||||
td {{pgpu.device}}
|
||||
tr(ng-repeat="pgpu in host.$PGPUs | resolve | orderBy:'device' | slice:(5*(currentGPUPage-1)):(5*currentGPUPage) track by pgpu.id")
|
||||
td.oneliner {{pgpu.device}}
|
||||
.center(ng-if = '(host.$PGPUs | resolve).length > 5')
|
||||
pagination(boundary-links="true", total-items="(host.$PGPUs | resolve).length", ng-model="$parent.currentGPUPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
|
||||
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-asterisk
|
||||
| Misc
|
||||
.panel-body(style="overflow:hidden")
|
||||
.row(ng-repeat="(key, value) in host.bios_strings")
|
||||
label.control-label.col-sm-4
|
||||
| {{key}}:
|
||||
.col-sm-8 {{value}}
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-book
|
||||
| License
|
||||
.panel-body
|
||||
.row
|
||||
label.control-label.col-sm-3
|
||||
| Server:
|
||||
.col-sm-9 {{host.license_server.address}}:{{host.license_server.port}}
|
||||
br
|
||||
.row(ng-repeat="key in hostParams | slice:(10*(currentLicensePage-1)):(10*currentLicensePage) track by key")
|
||||
label.control-label.col-sm-7
|
||||
| {{key}}:
|
||||
.col-sm-5 {{host.license_params[key]}}
|
||||
.center
|
||||
pagination(boundary-links="true", total-items="hostParams.length", ng-model="currentLicensePage", items-per-page="10", max-size="10", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
angular = require 'angular'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
module.exports = angular.module 'xoWebApp.isoDevice', []
|
||||
|
||||
.directive 'isoDevice', -> {
|
||||
restrict: 'E'
|
||||
template: require './view'
|
||||
scope: {
|
||||
isos: '='
|
||||
vm: '='
|
||||
}
|
||||
controller: 'IsoDevice as isoDevice'
|
||||
bindToController: true
|
||||
}
|
||||
|
||||
.controller 'IsoDevice', (xo) ->
|
||||
|
||||
this.eject = (VM) ->
|
||||
xo.vm.ejectCd VM.UUID
|
||||
|
||||
this.insert = (VM, disc_id) ->
|
||||
xo.vm.insertCd VM.UUID, disc_id, true
|
||||
|
||||
# A module exports its name.
|
||||
.name
|
||||
@@ -1,27 +1,126 @@
|
||||
import angular from 'angular';
|
||||
import uiRouter from 'angular-ui-router';
|
||||
import angular from 'angular'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
import xoTag from 'tag'
|
||||
import includes from 'lodash.includes'
|
||||
|
||||
import xoApi from 'xo-api';
|
||||
import xoApi from 'xo-api'
|
||||
|
||||
import view from './view';
|
||||
import view from './view'
|
||||
|
||||
//====================================================================
|
||||
// ===================================================================
|
||||
|
||||
export default angular.module('xoWebApp.list', [
|
||||
uiRouter,
|
||||
xoApi,
|
||||
xoTag
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('list', {
|
||||
url: '/list',
|
||||
controller: 'ListCtrl as list',
|
||||
template: view,
|
||||
});
|
||||
})
|
||||
.controller('ListCtrl', function (xoApi) {
|
||||
this.byTypes = xoApi.byTypes;
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('ListCtrl', function (xo, xoApi, $state, $scope, $rootScope) {
|
||||
const user = xoApi.user
|
||||
|
||||
$scope.createButton = user.permission !== 'admin'
|
||||
|
||||
if (user.permission !== 'admin') {
|
||||
$scope.createButton = false
|
||||
xo.resourceSet.getAll()
|
||||
.then(sets => {
|
||||
$scope.resourceSets = sets
|
||||
$scope.createButton = sets.length > 0
|
||||
})
|
||||
}
|
||||
|
||||
this.hosts = xoApi.getView('host')
|
||||
this.pools = xoApi.getView('pool')
|
||||
this.SRs = xoApi.getView('SR')
|
||||
this.VMs = xoApi.getView('VM')
|
||||
|
||||
this.hostsByPool = xoApi.getIndex('hostsByPool')
|
||||
this.runningHostsByPool = xoApi.getIndex('runningHostsByPool')
|
||||
this.vmsByContainer = xoApi.getIndex('vmsByContainer')
|
||||
$scope.canView = function (id) {
|
||||
return xoApi.canInteract(id, 'view')
|
||||
}
|
||||
|
||||
$scope.shouldAppear = (obj) => {
|
||||
// States
|
||||
const powerState = obj.power_state
|
||||
// If there is a search option on the power state (running or halted),
|
||||
// then objects that do not have a power_state (eg: SRs) are not displayed
|
||||
if (($scope.states['running'] !== 2 || $scope.states['halted'] !== 2) && !powerState) return false
|
||||
if (powerState) {
|
||||
if ($scope.states[powerState.toLowerCase()] === 0) return false
|
||||
if (($scope.states['running'] === 1 || $scope.states['halted'] === 1) &&
|
||||
$scope.states[powerState.toLowerCase()] !== 1) return false
|
||||
}
|
||||
|
||||
if ($scope.states['disconnected'] !== 2 && !obj.$PBDs) return false
|
||||
let disconnected = false
|
||||
if (obj.$PBDs) {
|
||||
for (const id of obj.$PBDs) {
|
||||
const pbd = xoApi.get(id)
|
||||
disconnected |= !pbd.attached
|
||||
}
|
||||
if ($scope.states['disconnected'] === 0 && disconnected) return false
|
||||
if ($scope.states['disconnected'] === 1 && !disconnected) return false
|
||||
}
|
||||
|
||||
// Types
|
||||
if ($scope.types[obj.type.toLowerCase()] === 0) return false
|
||||
if ($scope.types[obj.type.toLowerCase()] === 2 && includes($scope.types, 1)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const _initOptions = () => {
|
||||
$scope.types = {
|
||||
'host': 2,
|
||||
'pool': 2,
|
||||
'sr': 2,
|
||||
'vm': 2
|
||||
}
|
||||
$scope.states = {
|
||||
'running': 2,
|
||||
'halted': 2,
|
||||
'disconnected': 2
|
||||
}
|
||||
}
|
||||
_initOptions()
|
||||
|
||||
$scope.parsedListFilter = $scope.listFilter
|
||||
$rootScope.searchParse = () => {
|
||||
let keyWords = []
|
||||
const words = $scope.listFilter ? $scope.listFilter.split(' ') : ['']
|
||||
_initOptions()
|
||||
for (const word of words) {
|
||||
let isOption = word.charAt(0) === '*'
|
||||
const isNegation = word.charAt(0) === '!'
|
||||
// as long as there is a '!', it is an option. ie !vm <=> !*vm
|
||||
isOption = isOption || isNegation
|
||||
let option = (isNegation ? word.substring(1, word.length) : word).toLowerCase()
|
||||
option = option.charAt(0) === '*' ? option.substring(1, option.length) : option
|
||||
if (!isOption) {
|
||||
if (option !== '') keyWords.push(option)
|
||||
} else {
|
||||
if ($scope.types.hasOwnProperty(option)) {
|
||||
$scope.types[option] = isNegation ? 0 : 1
|
||||
} else if ($scope.states.hasOwnProperty(option)) {
|
||||
$scope.states[option] = isNegation ? 0 : 1
|
||||
}
|
||||
}
|
||||
}
|
||||
$scope.parsedListFilter = keyWords.join(' ')
|
||||
}
|
||||
|
||||
$scope.onClick = (type) => {
|
||||
$rootScope.options[type.toLowerCase()] = !$rootScope.options[type.toLowerCase()]
|
||||
$rootScope.updateListFilter(type.toLowerCase())
|
||||
}
|
||||
})
|
||||
// A module exports its name.
|
||||
.name
|
||||
;
|
||||
|
||||
@@ -1,164 +1,205 @@
|
||||
.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.byTypes.pool | xoHideUnauthorized | filter:listFilter | orderBy:natural('name_label') track by pool.UUID", ng-if="pool.name_label", xo-sref="pools_view({id: pool.UUID})")
|
||||
.grid.flat-object(
|
||||
ng-repeat="pool in list.pools.all | xoHideUnauthorized | filter:parsedListFilter | orderBy:natural('name_label') track by pool.id"
|
||||
ng-if="pool.name_label && shouldAppear(pool)"
|
||||
xo-sref="pools_view({id: pool.id})"
|
||||
)
|
||||
//- Icon.
|
||||
.grid-cell.flat-cell.flat-cell-type
|
||||
i.xo-icon-pool
|
||||
//- Properties & tags.
|
||||
.grid-cell
|
||||
//- Properties.
|
||||
.grid
|
||||
.grid-cell
|
||||
.grid
|
||||
.grid-cell.flat-cell.flat-cell-name
|
||||
| {{pool.name_label}}
|
||||
.grid-cell.flat-cell.flat-cell-description
|
||||
i {{pool.name_description}}
|
||||
.grid-cell.flat-cell(ng-init="default_SR = (pool.default_SR | resolve)")
|
||||
div(ng-if="default_SR")
|
||||
| Default SR:
|
||||
a(ui-sref="SRs_view({id: default_SR.UUID})") {{default_SR.name_label}}
|
||||
div(ng-if="!default_SR")
|
||||
em No default SR.
|
||||
.grid-cell.flat-cell(ng-init="master = (pool.master | resolve)")
|
||||
div(ng-if="master")
|
||||
| Master:
|
||||
a(ui-sref="hosts_view({id: master.UUID})") {{master.name_label}}
|
||||
div(ng-if="!master")
|
||||
em Unknown master.
|
||||
.grid-cell.flat-cell
|
||||
div(ng-if="pool.HA_enabled")
|
||||
| HA enabled
|
||||
div(ng-if="!pool.HA_enabled")
|
||||
| HA disabled
|
||||
.grid-cell.flat-cell
|
||||
| {{pool.$running_hosts.length}}/{{pool.hosts.length}} hosts
|
||||
.grid-sm
|
||||
.grid-cell.flat-cell.flat-cell-name
|
||||
| {{pool.name_label}}
|
||||
.grid-cell.flat-cell.flat-cell-description
|
||||
i {{pool.name_description}}
|
||||
.grid-cell.flat-cell(ng-init="default_SR = (pool.default_SR | resolve)")
|
||||
div(ng-if="default_SR")
|
||||
| Default SR:
|
||||
a(ui-sref="SRs_view({id: default_SR.id})") {{default_SR.name_label}}
|
||||
div(ng-if="!default_SR")
|
||||
em No default SR.
|
||||
.grid-cell.flat-cell(ng-init="master = (pool.master | resolve)")
|
||||
div(ng-if="master")
|
||||
| Master:
|
||||
a(ui-sref="hosts_view({id: master.id})") {{master.name_label}}
|
||||
div(ng-if="!master")
|
||||
em Unknown master.
|
||||
.grid-cell.flat-cell
|
||||
div(ng-if="pool.HA_enabled")
|
||||
| HA enabled
|
||||
div(ng-if="!pool.HA_enabled")
|
||||
| HA disabled
|
||||
.grid-cell.flat-cell
|
||||
| {{list.runningHostsByPool[pool.id] | count}}/{{list.hostsByPool[pool.id] | count}} hosts
|
||||
//- /Properties.
|
||||
//- Tags.
|
||||
.grid
|
||||
.grid-cell
|
||||
.grid-cell.flat-cell-tag
|
||||
i.fa.fa-tag
|
||||
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.byTypes.host | xoHideUnauthorized | filter:listFilter | orderBy:natural('name_label') track by host.UUID", xo-sref="hosts_view({id: host.UUID})")
|
||||
.grid.flat-object(
|
||||
ng-repeat="host in list.hosts.all | xoHideUnauthorized | filter:parsedListFilter | orderBy:natural('name_label') track by host.id"
|
||||
ng-if="shouldAppear(host)"
|
||||
xo-sref="hosts_view({id: host.id})"
|
||||
)
|
||||
//- Icon.
|
||||
.grid-cell.flat-cell.flat-cell-type
|
||||
i.xo-icon-host(class="xo-color-{{host.power_state | lowercase}}")
|
||||
//- Properties & tags.
|
||||
.grid-cell
|
||||
//- Properties.
|
||||
.grid
|
||||
.grid-cell
|
||||
.grid
|
||||
.grid-cell.flat-cell.flat-cell-name
|
||||
| {{host.name_label}}
|
||||
.grid-cell.flat-cell.flat-cell-description
|
||||
i {{host.name_description}}
|
||||
.grid-cell.flat-cell
|
||||
| Address: {{host.address}}
|
||||
//- .grid-cell.flat-cell
|
||||
//- | {{host.$vCPUs}} vCPUs used on {{host.CPUs["cpu_count"]}} cores
|
||||
.grid-cell.flat-cell
|
||||
.progress-condensed
|
||||
.progress-bar(role="progressbar", aria-valuemin="0", aria-valuenow="{{100*host.memory.usage/host.memory.size}}", aria-valuemax="100", style="width: {{[host.memory.usage, host.memory.size] | %}}", tooltip="RAM: {{[host.memory.usage, host.memory.size] | %}} allocated")
|
||||
| {{[host.memory.usage, host.memory.size] | %}}
|
||||
.grid-cell.flat-cell
|
||||
| {{host.VMs.length}} VMs running
|
||||
.grid-sm
|
||||
.grid-cell.flat-cell.flat-cell-name
|
||||
| {{host.name_label}}
|
||||
.grid-cell.flat-cell.flat-cell-description
|
||||
i {{host.name_description}}
|
||||
.grid-cell.flat-cell
|
||||
| Address: {{host.address}}
|
||||
//- .grid-cell.flat-cell
|
||||
//- | {{host.$vCPUs}} vCPUs used on {{host.CPUs["cpu_count"]}} cores
|
||||
.grid-cell.flat-cell
|
||||
.progress-condensed
|
||||
.progress-bar(role="progressbar", aria-valuemin="0", aria-valuenow="{{100*host.memory.usage/host.memory.size}}", aria-valuemax="100", style="width: {{[host.memory.usage, host.memory.size] | percentage}}", tooltip="RAM: {{[host.memory.usage, host.memory.size] | percentage}} allocated")
|
||||
| {{[host.memory.usage, host.memory.size] | percentage}}
|
||||
.grid-cell.flat-cell
|
||||
| {{list.vmsByContainer[host.id] | count}} VMs running
|
||||
//- /Properties.
|
||||
//- Tags.
|
||||
.grid
|
||||
.grid-cell
|
||||
.grid-cell.flat-cell-tag
|
||||
i.fa.fa-tag
|
||||
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.byTypes.VM | xoHideUnauthorized | filter:listFilter | orderBy:natural('name_label') track by VM.UUID", xo-sref="VMs_view({id: VM.UUID})")
|
||||
.grid.flat-object(
|
||||
ng-repeat="VM in list.VMs.all | xoHideUnauthorized | filter:parsedListFilter | orderBy:natural('name_label') track by VM.id"
|
||||
ng-if="shouldAppear(VM)"
|
||||
xo-sref="VMs_view({id: VM.id})"
|
||||
)
|
||||
//- Icon.
|
||||
.grid-cell.flat-cell.flat-cell-type
|
||||
i.xo-icon-vm(class="xo-color-{{VM.power_state | lowercase}}")
|
||||
//- Properties & tags.
|
||||
.grid-cell
|
||||
//- Properties.
|
||||
.grid
|
||||
.grid-cell
|
||||
.grid
|
||||
.grid-cell.flat-cell.flat-cell-name
|
||||
| {{VM.name_label}}
|
||||
.grid-cell.flat-cell.flat-cell-description
|
||||
i {{VM.name_description}}
|
||||
.grid-cell.flat-cell
|
||||
| Address: {{VM.addresses["0/ip"]}}
|
||||
.grid-cell.flat-cell
|
||||
| {{VM.CPUs.number}} vCPUs
|
||||
.grid-cell.flat-cell
|
||||
| {{VM.memory.size | bytesToSize}} RAM
|
||||
.grid-cell.flat-cell(ng-init="container = (VM.$container | resolve)")
|
||||
div(ng-if="'pool' === container.type")
|
||||
| Resident on:
|
||||
a(ui-sref="pools_view({id: container.UUID})") {{container.name_label}}
|
||||
div(ng-if="'host' === container.type", ng-init="pool = (container.poolRef | resolve)")
|
||||
| Resident on:
|
||||
a(ui-sref="hosts_view({id: container.UUID})") {{container.name_label}}
|
||||
small(ng-if="pool.name_label")
|
||||
| (
|
||||
a(ui-sref="pools_view({id: pool.UUID})") {{pool.name_label}}
|
||||
| )
|
||||
.grid-sm
|
||||
.grid-cell.flat-cell.flat-cell-name
|
||||
| {{VM.name_label}}
|
||||
.grid-cell.flat-cell.flat-cell-description
|
||||
i {{VM.name_description}}
|
||||
.grid-cell.flat-cell
|
||||
| Address: {{VM.addresses["0/ip"]}}
|
||||
.grid-cell.flat-cell
|
||||
| {{VM.CPUs.number}} vCPUs
|
||||
.grid-cell.flat-cell
|
||||
| {{VM.memory.size | bytesToSize}} RAM
|
||||
.grid-cell.flat-cell(ng-init="container = (VM.$container | resolve)", ng-if="canView((VM.$container | resolve).id)")
|
||||
div(ng-if="'pool' === container.type")
|
||||
| Resident on:
|
||||
a(ui-sref="pools_view({id: container.id})") {{container.name_label}}
|
||||
div(ng-if="'host' === container.type", ng-init="pool = (container.$poolId | resolve)")
|
||||
| Resident on:
|
||||
a(ui-sref="hosts_view({id: container.id})") {{container.name_label}}
|
||||
small(ng-if="pool.name_label && canView(pool.id)")
|
||||
| (
|
||||
a(ui-sref="pools_view({id: pool.id})") {{pool.name_label}}
|
||||
| )
|
||||
//- /Properties.
|
||||
//- Tags.
|
||||
.grid
|
||||
.grid-cell
|
||||
.grid-cell.flat-cell-tag
|
||||
i.fa.fa-tag
|
||||
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.byTypes.SR | xoHideUnauthorized | filter:listFilter | orderBy:natural('name_label') track by SR.UUID", xo-sref="SRs_view({id: SR.UUID})")
|
||||
.grid.flat-object(
|
||||
ng-repeat="SR in list.SRs.all | xoHideUnauthorized | filter:parsedListFilter | orderBy:natural('name_label') track by SR.id"
|
||||
ng-if="shouldAppear(SR)"
|
||||
xo-sref="SRs_view({id: SR.id})"
|
||||
)
|
||||
//- Icon.
|
||||
.grid-cell.flat-cell.flat-cell-type
|
||||
i.xo-icon-sr
|
||||
//- Properties & tags.
|
||||
.grid-cell
|
||||
//- Properties.
|
||||
.grid
|
||||
.grid-cell
|
||||
.grid
|
||||
.grid-cell.flat-cell.flat-cell-name
|
||||
| {{SR.name_label}}
|
||||
.grid-cell.flat-cell.flat-cell-description
|
||||
i {{SR.name_description}}
|
||||
.grid-cell.flat-cell
|
||||
| Usage: {{[SR.usage, SR.size] | %}} ({{SR.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.UUID})") {{container.name_label}}
|
||||
div(ng-if="'host' === container.type")
|
||||
| Connected to
|
||||
a(ui-sref="hosts_view({id: container.UUID})") {{container.name_label}}
|
||||
.grid-sm
|
||||
.grid-cell.flat-cell.flat-cell-name
|
||||
| {{SR.name_label}}
|
||||
.grid-cell.flat-cell.flat-cell-description
|
||||
i {{SR.name_description}}
|
||||
.grid-cell.flat-cell
|
||||
span(ng-if="SR.content_type !== 'disk'") Usage: {{[SR.physical_usage, SR.size] | percentage}} ({{SR.physical_usage | bytesToSize}}/{{SR.size | bytesToSize}})
|
||||
.grid-cell.flat-cell
|
||||
| Type: {{SR.SR_type}}
|
||||
.grid-cell.flat-cell(ng-init="container = (SR.$container | resolve)")
|
||||
div(ng-if="'pool' === container.type")
|
||||
strong
|
||||
| Shared on:
|
||||
a(ui-sref="pools_view({id: container.id})") {{container.name_label}}
|
||||
div(ng-if="'host' === container.type")
|
||||
| Connected to:
|
||||
a(ui-sref="hosts_view({id: container.id})") {{container.name_label}}
|
||||
//- /Properties.
|
||||
//- Tags.
|
||||
.grid
|
||||
.grid-cell
|
||||
.grid-cell.flat-cell-tag
|
||||
i.fa.fa-tag
|
||||
span(ng-repeat="tag in SR.tags")
|
||||
span.label.label-primary {{tag}}
|
||||
i.fa.fa-tag
|
||||
xo-tag(object = 'SR')
|
||||
//- /Tags.
|
||||
//- /Properties & tags.
|
||||
//- /SR.
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import angular from 'angular';
|
||||
import uiRouter from 'angular-ui-router';
|
||||
|
||||
import view from './view';
|
||||
|
||||
//====================================================================
|
||||
|
||||
export default angular.module('xoWebApp.login', [
|
||||
uiRouter,
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('login', {
|
||||
url: '/login',
|
||||
controller: 'LoginCtrl',
|
||||
template: view,
|
||||
});
|
||||
})
|
||||
.controller('LoginCtrl', function($scope, $state, $rootScope, xoApi, notify) {
|
||||
var toState, toStateParams;
|
||||
{
|
||||
let tmp = $rootScope._login;
|
||||
if (tmp) {
|
||||
toState = tmp.state.name;
|
||||
toStateParams = tmp.stateParams;
|
||||
delete $rootScope._login;
|
||||
} else {
|
||||
toState = 'index';
|
||||
}
|
||||
}
|
||||
|
||||
$scope.$watch(() => xoApi.user, function (user) {
|
||||
// When the user is logged in, go the wanted view, fallbacks on
|
||||
// the index view if necessary.
|
||||
if (user) {
|
||||
$state.go(toState, toStateParams).catch(function () {
|
||||
$state.go('index');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Object.defineProperties($scope, {
|
||||
user: {
|
||||
get() {
|
||||
return xoApi.user;
|
||||
},
|
||||
},
|
||||
status: {
|
||||
get() {
|
||||
return xoApi.status;
|
||||
}
|
||||
},
|
||||
});
|
||||
$scope.logIn = (...args) => {
|
||||
xoApi.logIn(...args).catch(error => {
|
||||
notify.warning({
|
||||
title: 'Authentication failed',
|
||||
message: error.message,
|
||||
});
|
||||
});
|
||||
};
|
||||
})
|
||||
|
||||
// A module exports its name.
|
||||
.name
|
||||
;
|
||||
@@ -1,54 +0,0 @@
|
||||
//- Hide the navbar for this view.
|
||||
style.
|
||||
.navbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.container
|
||||
div.row-login
|
||||
div.page-header
|
||||
img(src = 'images/logo_small.png')
|
||||
h2 Xen Orchestra
|
||||
form.form-horizontal(
|
||||
ng-submit = '$broadcast("fixAutofill"); logIn(email, password, true)'
|
||||
)
|
||||
fieldset
|
||||
legend.login: h3 Sign in
|
||||
div.form-group
|
||||
div.col-sm-12
|
||||
.input-group
|
||||
span.input-group-addon: i.fa.fa-user.fa-fw
|
||||
input.form-control.input-sm(
|
||||
name = 'email'
|
||||
type = 'text'
|
||||
placeholder = 'Username'
|
||||
ng-model = 'email'
|
||||
required
|
||||
fix-autofill
|
||||
)
|
||||
div.form-group
|
||||
div.col-sm-12
|
||||
.input-group
|
||||
span.input-group-addon: i.fa.fa-key.fa-fw
|
||||
input.form-control.input-sm(
|
||||
name = 'password'
|
||||
type = 'password'
|
||||
placeholder = 'Password'
|
||||
ng-model = 'password'
|
||||
required
|
||||
fix-autofill
|
||||
)
|
||||
div.form-group
|
||||
div.col-sm-12
|
||||
button.btn.btn-login.btn-block.btn-success(
|
||||
id = 'login'
|
||||
name = 'login'
|
||||
)
|
||||
i.fa.fa-sign-in
|
||||
| Login
|
||||
p.status(ng-if = '"disconnected" === status')
|
||||
i.xo-icon-error.fa-2x(tooltip = 'You are not connected to XO-Server')
|
||||
p.status(ng-if = '"connecting" === status')
|
||||
i.fa.fa-refresh.fa-spin.fa-2x(tooltip = 'Connecting to XO-Server')
|
||||
p.status(ng-if = '"connected" === status')
|
||||
i.xo-icon-success.fa-2x(tooltip = 'You are connected to XO-Server')
|
||||
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
|
||||
@@ -1,59 +1,125 @@
|
||||
import angular from 'angular';
|
||||
import filter from 'lodash.filter';
|
||||
import uiRouter from 'angular-ui-router';
|
||||
import angular from 'angular'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
|
||||
import xoServices from 'xo-services';
|
||||
import updater from '../updater'
|
||||
import xoServices from 'xo-services'
|
||||
import includes from 'lodash.includes'
|
||||
|
||||
import view from './view';
|
||||
import view from './view'
|
||||
|
||||
//====================================================================
|
||||
// ===================================================================
|
||||
|
||||
export default angular.module('xoWebApp.navbar', [
|
||||
uiRouter,
|
||||
|
||||
xoServices,
|
||||
updater,
|
||||
xoServices
|
||||
])
|
||||
.controller('NavbarCtrl', function ($state, xoApi, xo, $scope) {
|
||||
.controller('NavbarCtrl', function ($state, xoApi, xo, $scope, updater, $rootScope) {
|
||||
this.updater = updater
|
||||
// TODO: It would make sense to inject xoApi in the scope.
|
||||
Object.defineProperties(this, {
|
||||
status: {
|
||||
get: () => xoApi.status,
|
||||
get: () => xoApi.status
|
||||
},
|
||||
user: {
|
||||
get: () => xoApi.user,
|
||||
},
|
||||
});
|
||||
this.logIn = xoApi.logIn;
|
||||
get: () => xoApi.user
|
||||
}
|
||||
})
|
||||
this.logIn = xoApi.logIn
|
||||
this.logOut = function () {
|
||||
xoApi.logOut();
|
||||
$state.go('login');
|
||||
};
|
||||
xoApi.logOut()
|
||||
}
|
||||
|
||||
// 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 ALIVE_STATUS = {
|
||||
cancelling: true,
|
||||
pending: true,
|
||||
};
|
||||
let {canAccess} = xo;
|
||||
let sieve = (task) => ALIVE_STATUS[task.status] && canAccess(task.$host);
|
||||
$scope.$watchCollection(() => xoApi.byTypes.task, (tasks) => {
|
||||
this.tasks = filter(tasks, sieve);
|
||||
});
|
||||
const _isOption = function (word, option) {
|
||||
if (word === '*' + option || word === '!' + option || word === '!*' + option) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
const _removeOption = function (option) {
|
||||
if (!$scope.$root.listFilter) {
|
||||
return
|
||||
}
|
||||
const words = $scope.$root.listFilter.split(' ')
|
||||
$scope.$root.listFilter = ''
|
||||
for (const word of words) {
|
||||
if (!_isOption(word, option) && word !== '') {
|
||||
$scope.$root.listFilter += word + ' '
|
||||
}
|
||||
}
|
||||
}
|
||||
const _addOption = function (option) {
|
||||
if (!$scope.$root.listFilter) {
|
||||
$scope.$root.listFilter = '*' + option + ' '
|
||||
return
|
||||
}
|
||||
const words = $scope.$root.listFilter.split(' ')
|
||||
if (!includes(words, '*' + option) && !includes(words, '!' + option) && !includes(words, '!*' + option)) {
|
||||
if ($scope.$root.listFilter.charAt($scope.$root.listFilter.length - 1) !== ' ') {
|
||||
$scope.$root.listFilter += ' '
|
||||
}
|
||||
$scope.$root.listFilter += '*' + option + ' '
|
||||
}
|
||||
}
|
||||
|
||||
$rootScope.options = {
|
||||
'vm': false,
|
||||
'sr': false,
|
||||
'host': false,
|
||||
'pool': false,
|
||||
'running': false,
|
||||
'halted': false,
|
||||
'disconnected': false
|
||||
}
|
||||
// Checkboxes --> Text
|
||||
// Update text field after a checkbox has been clicked
|
||||
$rootScope.updateListFilter = function (option) {
|
||||
if ($rootScope.options[option]) {
|
||||
_addOption(option)
|
||||
} else {
|
||||
_removeOption(option)
|
||||
}
|
||||
$scope.ensureListView($scope.$root.listFilter)
|
||||
}
|
||||
// Text --> Checkboxes
|
||||
// Update checkboxes after the text field has been changed
|
||||
$scope.updateOptions = function () {
|
||||
const words = $scope.$root.listFilter ? $scope.$root.listFilter.split(' ') : ['']
|
||||
for (const opt in $rootScope.options) {
|
||||
$rootScope.options[opt] = false
|
||||
for (let word of words) {
|
||||
if (_isOption(word, opt)) {
|
||||
$rootScope.options[opt] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.tasks = xoApi.getView('runningTasks')
|
||||
})
|
||||
.directive('navbar', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
controller: 'NavbarCtrl as navbar',
|
||||
template: view,
|
||||
scope: {},
|
||||
};
|
||||
scope: {}
|
||||
}
|
||||
})
|
||||
|
||||
// A module exports its name.
|
||||
.name
|
||||
;
|
||||
|
||||
@@ -21,20 +21,20 @@ 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.
|
||||
ul.nav.navbar-nav
|
||||
li
|
||||
a(href="https://xen-orchestra.com/#/pricing?pk_campaign=xoa_source", target="_blank")
|
||||
a(href="https://xen-orchestra.com/#/pricing?pk_campaign=xoa_source", target="_blank", tooltip="Source version without Pro support. Use in production at your own risk.")
|
||||
i.xo-icon-info.text-danger
|
||||
| Unregistered version: no support provided!
|
||||
span.hidden-sm No Pro Support!
|
||||
//- Right items of the navbar.
|
||||
ul.nav.navbar-nav.navbar-right
|
||||
li.navbar-text(ng-if="'disconnected' === navbar.status")
|
||||
@@ -44,15 +44,15 @@ nav.navbar.navbar-inverse.navbar-fixed-top(role = 'navigation')
|
||||
i.fa.fa-refresh.fa-spin
|
||||
| Connecting to XO-Server
|
||||
//- Running tasks
|
||||
li.disabled(ng-if="!navbar.tasks.length", tooltip="No running tasks")
|
||||
li.disabled(ng-if="!navbar.tasks.size", tooltip="No running tasks")
|
||||
a.dropdown-toggle.inverse
|
||||
i.xo-icon-task
|
||||
li.dropdown(dropdown, ng-if="navbar.tasks.length")
|
||||
li.dropdown(dropdown, ng-if="navbar.tasks.size")
|
||||
a.dropdown-toggle.inverse(dropdown-toggle)
|
||||
i.xo-icon-task
|
||||
ul.dropdown-menu.inverse
|
||||
li.task-menu(
|
||||
ng-repeat="task in navbar.tasks | 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})"
|
||||
@@ -85,15 +85,28 @@ nav.navbar.navbar-inverse.navbar-fixed-top(role = 'navigation')
|
||||
a(ui-sref="list")
|
||||
i.fa.fa-align-justify
|
||||
| Flat view
|
||||
//- li.disabled(ui-sref-active="active")
|
||||
//- a(ui-sref="graph")
|
||||
//- i.fa.fa-sitemap
|
||||
//- | Graphs view
|
||||
li(
|
||||
ui-sref-active="active"
|
||||
ng-class = '{ disabled: navbar.user.permission !== "admin" }'
|
||||
)
|
||||
a(ui-sref="dashboard.index")
|
||||
i.fa.fa-dashboard
|
||||
| Dashboard
|
||||
li.divider
|
||||
li(ng-class = '{ disabled: navbar.user.permission !== "admin" }')
|
||||
a(ui-sref = 'self.index')
|
||||
i.fa.fa-cloud
|
||||
| Self Service
|
||||
li.divider
|
||||
li(ng-class = '{ disabled: navbar.user.permission !== "admin" }')
|
||||
a(ui-sref = 'backup.index')
|
||||
i.fa.fa-archive
|
||||
| Backup
|
||||
li(ng-class = '{ disabled: navbar.user.permission !== "admin" }')
|
||||
a(ui-sref = 'taskscheduler.index')
|
||||
i.fa.fa-cogs
|
||||
| Job Manager
|
||||
li.divider
|
||||
//- li.disabled
|
||||
//- a
|
||||
//- i.fa.fa-clock-o
|
||||
//- | Scheduler
|
||||
li(
|
||||
ui-sref-active = 'active'
|
||||
ng-class = '{ disabled: navbar.user.permission !== "admin" }'
|
||||
@@ -109,9 +122,18 @@ nav.navbar.navbar-inverse.navbar-fixed-top(role = 'navigation')
|
||||
//- /Main menu.
|
||||
|
||||
li
|
||||
a
|
||||
a(ui-sref="settings.update")
|
||||
i.fa.fa-question-circle.text-warning(ng-if = '!navbar.updater.state', tooltip = 'No update information available')
|
||||
i.fa.fa-question-circle.text-info(ng-if = 'navbar.updater.state == "connected"', tooltip = 'Update information may be available')
|
||||
i.fa.fa-check.text-success(ng-if = 'navbar.updater.state == "upToDate"', tooltip = 'Your XOA is up-to-date')
|
||||
i.fa.fa-bell.text-primary(ng-if = 'navbar.updater.state == "upgradeNeeded"', tooltip = 'You need to update your XOA (new version is available)')
|
||||
i.fa.fa-bell-slash.text-warning(ng-if = 'navbar.updater.state == "registerNeeded"', tooltip = 'Your XOA is not registered for updates')
|
||||
i.fa.fa-exclamation-triangle.text-danger(ng-if = 'navbar.updater.state == "error"', tooltip = 'Can\'t fetch update information')
|
||||
|
||||
li
|
||||
a(ng-if = '!navbar.user.provider', ui-sref="{{navbar.user.provider ? 'settings.users' : 'settings.user'}}", tooltip="{{navbar.user.email}}")
|
||||
i.fa.fa-user
|
||||
| {{navbar.user.email}}
|
||||
span.hidden-sm {{navbar.user.email}}
|
||||
li
|
||||
a(ng-click = 'navbar.logOut()')
|
||||
i.fa.fa-sign-out
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import angular from 'angular';
|
||||
import uiRouter from 'angular-ui-router';
|
||||
import Bluebird from 'bluebird';
|
||||
import angular from 'angular'
|
||||
import Bluebird from 'bluebird'
|
||||
import forEach from 'lodash.foreach'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
|
||||
import view from './view';
|
||||
import _indexOf from 'lodash.indexof';
|
||||
import view from './view'
|
||||
import _indexOf from 'lodash.indexof'
|
||||
|
||||
//====================================================================
|
||||
// ===================================================================
|
||||
|
||||
export default angular.module('xoWebApp.newSr', [
|
||||
uiRouter
|
||||
@@ -14,463 +15,431 @@ export default angular.module('xoWebApp.newSr', [
|
||||
$stateProvider.state('SRs_new', {
|
||||
url: '/srs/new/:container',
|
||||
controller: 'NewSrCtrl as newSr',
|
||||
template: view,
|
||||
});
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('NewSrCtrl', function ($scope, $state, $stateParams, xo, xoApi, notify, modal, bytesToSizeFilter) {
|
||||
|
||||
this.reset = function (data = {}) {
|
||||
|
||||
this.data = {};
|
||||
delete this.lockCreation;
|
||||
this.data = {}
|
||||
delete this.lockCreation
|
||||
this.lock = !(
|
||||
('Local' === data.srType) &&
|
||||
(data.srPath && data.srPath.path)
|
||||
);
|
||||
(data.srType === 'Local') &&
|
||||
(data.srPath && data.srPath.path) ||
|
||||
data.srType === 'SMB'
|
||||
)
|
||||
}
|
||||
|
||||
};
|
||||
this.resetLists = function () {
|
||||
delete this.data.nfsList
|
||||
delete this.data.scsiList
|
||||
delete this.lockCreation
|
||||
this.lock = true
|
||||
|
||||
this.resetLists = function() {
|
||||
|
||||
delete this.data.nfsList;
|
||||
delete this.data.scsiList;
|
||||
delete this.lockCreation;
|
||||
this.lock = true;
|
||||
|
||||
this.resetErrors();
|
||||
|
||||
};
|
||||
this.resetErrors()
|
||||
}
|
||||
|
||||
this.resetErrors = function () {
|
||||
|
||||
delete this.data.error;
|
||||
|
||||
};
|
||||
delete this.data.error
|
||||
}
|
||||
|
||||
/*
|
||||
* Loads NFS paths and iScsi iqn`s
|
||||
*/
|
||||
this.populateSettings = function (type, server, auth, user, password) {
|
||||
this.reset()
|
||||
this.loading = true
|
||||
|
||||
this.reset();
|
||||
this.loading = true;
|
||||
|
||||
server = this._parseAddress(server);
|
||||
|
||||
if ('NFS' === type || 'NFS_ISO' === type) {
|
||||
server = this._parseAddress(server)
|
||||
|
||||
if (type === 'NFS' || type === 'NFS_ISO') {
|
||||
xoApi.call('sr.probeNfs', {
|
||||
host: this.container.UUID,
|
||||
host: this.container.id,
|
||||
server: server.host
|
||||
})
|
||||
.then(response => this.data.paths = response)
|
||||
.catch(error => notify.warning({
|
||||
title : 'NFS Detection',
|
||||
message : error.message
|
||||
title: 'NFS Detection',
|
||||
message: error.message
|
||||
}))
|
||||
.finally(() => this.loading = false)
|
||||
;
|
||||
|
||||
} else if ('iSCSI' === type) {
|
||||
|
||||
} else if (type === 'iSCSI') {
|
||||
let params = {
|
||||
host: this.container.UUID
|
||||
};
|
||||
|
||||
if (auth) {
|
||||
params.chapUser = user;
|
||||
params.chapPassword = password;
|
||||
host: this.container.id
|
||||
}
|
||||
|
||||
params.target = server.host;
|
||||
if (auth) {
|
||||
params.chapUser = user
|
||||
params.chapPassword = password
|
||||
}
|
||||
|
||||
params.target = server.host
|
||||
if (server.port) {
|
||||
params.port = server.port;
|
||||
params.port = server.port
|
||||
}
|
||||
|
||||
xoApi.call('sr.probeIscsiIqns', params)
|
||||
.then(response => {
|
||||
|
||||
if (response.length > 0) {
|
||||
this.data.iqns = response;
|
||||
this.data.iqns = response
|
||||
} else {
|
||||
notify.warning({
|
||||
title : 'iSCSI Detection',
|
||||
message : 'No IQNs found'
|
||||
});
|
||||
title: 'iSCSI Detection',
|
||||
message: 'No IQNs found'
|
||||
})
|
||||
}
|
||||
|
||||
})
|
||||
.catch(error => notify.warning({
|
||||
title : 'iSCSI Detection',
|
||||
message : error.message
|
||||
title: 'iSCSI Detection',
|
||||
message: error.message
|
||||
}))
|
||||
.finally(() => this.loading = false)
|
||||
;
|
||||
|
||||
} else {
|
||||
|
||||
this.loading = false;
|
||||
|
||||
this.loading = false
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* Loads iScsi LUNs
|
||||
*/
|
||||
this.populateIScsiIds = function (iqn, auth, user, password) {
|
||||
|
||||
delete this.data.iScsiIds;
|
||||
this.loading = true;
|
||||
delete this.data.iScsiIds
|
||||
this.loading = true
|
||||
|
||||
let params = {
|
||||
host: this.container.UUID,
|
||||
host: this.container.id,
|
||||
target: iqn.ip,
|
||||
targetIqn: iqn.iqn
|
||||
};
|
||||
}
|
||||
|
||||
if (auth) {
|
||||
params.chapUser = user;
|
||||
params.chapPassword = password;
|
||||
params.chapUser = user
|
||||
params.chapPassword = password
|
||||
}
|
||||
|
||||
xoApi.call('sr.probeIscsiLuns', params)
|
||||
.then(response => {
|
||||
|
||||
response.forEach(item => {
|
||||
forEach(response, item => {
|
||||
item.display = 'LUN ' + item.id + ': ' +
|
||||
item.serial + ' ' + bytesToSizeFilter(item.size) +
|
||||
' (' + item.vendor + ')';
|
||||
});
|
||||
' (' + item.vendor + ')'
|
||||
})
|
||||
|
||||
this.data.iScsiIds = response;
|
||||
this.data.iScsiIds = response
|
||||
})
|
||||
.catch(error => notify.warning({
|
||||
title : 'LUNs Detection',
|
||||
message : error.message
|
||||
title: 'LUNs Detection',
|
||||
message: error.message
|
||||
}))
|
||||
.finally(() => this.loading = false)
|
||||
;
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
this._parseAddress = function (address) {
|
||||
|
||||
let index = address.indexOf(':');
|
||||
let port = false;
|
||||
let host = address;
|
||||
if (-1 < index) {
|
||||
port = address.substring(index + 1);
|
||||
host = address.substring(0, index);
|
||||
let index = address.indexOf(':')
|
||||
let port = false
|
||||
let host = address
|
||||
if (index > -1) {
|
||||
port = address.substring(index + 1)
|
||||
host = address.substring(0, index)
|
||||
}
|
||||
return {
|
||||
host,
|
||||
port
|
||||
};
|
||||
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
this._prepareNfsParams = function (data) {
|
||||
|
||||
let server = this._parseAddress(data.srServer);
|
||||
let server = this._parseAddress(data.srServer)
|
||||
|
||||
let params = {
|
||||
host: this.container.UUID,
|
||||
host: this.container.id,
|
||||
nameLabel: data.srName,
|
||||
nameDescription: data.srDesc,
|
||||
server: server.host,
|
||||
serverPath: data.srPath.path
|
||||
};
|
||||
}
|
||||
|
||||
return params;
|
||||
|
||||
};
|
||||
|
||||
this._prepareScsiParams = function(data) {
|
||||
return params
|
||||
}
|
||||
|
||||
this._prepareScsiParams = function (data) {
|
||||
let params = {
|
||||
host: this.container.UUID,
|
||||
host: this.container.id,
|
||||
nameLabel: data.srName,
|
||||
nameDescription: data.srDesc,
|
||||
target: data.srIqn.ip,
|
||||
targetIqn: data.srIqn.iqn,
|
||||
scsiId: data.srIScsiId.scsiId,
|
||||
};
|
||||
scsiId: data.srIScsiId.scsiId
|
||||
}
|
||||
|
||||
let server = this._parseAddress(data.srServer);
|
||||
let server = this._parseAddress(data.srServer)
|
||||
if (server.port) {
|
||||
params.port = server.port;
|
||||
params.port = server.port
|
||||
}
|
||||
if (data.srAuth) {
|
||||
params.chapUser = data.srChapUser;
|
||||
params.chapPassword = data.srChapPassword;
|
||||
params.chapUser = data.srChapUser
|
||||
params.chapPassword = data.srChapPassword
|
||||
}
|
||||
|
||||
return params;
|
||||
|
||||
};
|
||||
return params
|
||||
}
|
||||
|
||||
this.createSR = function (data) {
|
||||
this.lock = true
|
||||
this.creating = true
|
||||
|
||||
this.lock = true;
|
||||
this.creating = true;
|
||||
let operationToPromise
|
||||
|
||||
let operationToPromise;
|
||||
|
||||
switch(data.srType) {
|
||||
switch (data.srType) {
|
||||
case 'NFS':
|
||||
|
||||
let nfsParams = this._prepareNfsParams(data);
|
||||
let nfsParams = this._prepareNfsParams(data)
|
||||
operationToPromise = this._checkNfsExistence(nfsParams)
|
||||
.then(() => xoApi.call('sr.createNfs', nfsParams))
|
||||
;
|
||||
break;
|
||||
.then(() => xoApi.call('sr.createNfs', nfsParams))
|
||||
break
|
||||
|
||||
case 'iSCSI':
|
||||
|
||||
let scsiParams = this._prepareScsiParams(data);
|
||||
operationToPromise = this._checkScsiExistence(scsiParams)
|
||||
.then(() => xoApi.call('sr.createIscsi', scsiParams))
|
||||
;
|
||||
break;
|
||||
let scsiParams = this._prepareScsiParams(data)
|
||||
operationToPromise = this._checkScsiExistence(scsiParams)
|
||||
.then(() => xoApi.call('sr.createIscsi', scsiParams))
|
||||
break
|
||||
|
||||
case 'lvm':
|
||||
|
||||
let device = data.srDevice.device;
|
||||
let device = data.srDevice.device
|
||||
|
||||
operationToPromise = xoApi.call('sr.createLvm', {
|
||||
host: this.container.UUID,
|
||||
host: this.container.id,
|
||||
nameLabel: data.srName,
|
||||
nameDescription: data.srDesc,
|
||||
device
|
||||
});
|
||||
break;
|
||||
})
|
||||
break
|
||||
|
||||
case 'NFS_ISO':
|
||||
case 'Local':
|
||||
|
||||
let server = this._parseAddress(data.srServer || '');
|
||||
|
||||
let path = (('NFS_ISO' === data.srType) ?
|
||||
server.host + ':' :
|
||||
'') + data.srPath.path;
|
||||
|
||||
operationToPromise = xoApi.call('sr.createIso', {
|
||||
host: this.container.UUID,
|
||||
host: this.container.id,
|
||||
nameLabel: data.srName,
|
||||
nameDescription: data.srDesc,
|
||||
path
|
||||
});
|
||||
break;
|
||||
default:
|
||||
type: 'local',
|
||||
path: data.srPath.path
|
||||
})
|
||||
break
|
||||
|
||||
operationToPromise = Bluebird.reject({message: 'Unhanled SR Type'});
|
||||
break;
|
||||
case 'NFS_ISO':
|
||||
let server = this._parseAddress(data.srServer || '')
|
||||
|
||||
const path = (
|
||||
data.srType === 'NFS_ISO'
|
||||
? server.host + ':'
|
||||
: ''
|
||||
) + data.srPath.path
|
||||
|
||||
operationToPromise = xoApi.call('sr.createIso', {
|
||||
host: this.container.id,
|
||||
nameLabel: data.srName,
|
||||
nameDescription: data.srDesc,
|
||||
type: 'nfs',
|
||||
path
|
||||
})
|
||||
break
|
||||
|
||||
case 'SMB':
|
||||
operationToPromise = xoApi.call('sr.createIso', {
|
||||
host: this.container.id,
|
||||
nameLabel: data.srName,
|
||||
nameDescription: data.srDesc,
|
||||
type: 'smb',
|
||||
path: data.srServer,
|
||||
user: data.user,
|
||||
password: data.password
|
||||
})
|
||||
break
|
||||
|
||||
default:
|
||||
operationToPromise = Bluebird.reject({message: 'Unhanled SR Type'})
|
||||
break
|
||||
}
|
||||
|
||||
operationToPromise
|
||||
.then(id => {
|
||||
$state.go('SRs_view', {id});
|
||||
$state.go('SRs_view', {id})
|
||||
})
|
||||
.catch(error => {
|
||||
notify.error({
|
||||
title : 'Storage Creation Error',
|
||||
message : error.message
|
||||
});
|
||||
title: 'Storage Creation Error',
|
||||
message: error.message
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
this.lock = false;
|
||||
this.creating = false;
|
||||
this.lock = false
|
||||
this.creating = false
|
||||
})
|
||||
;
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
this._checkScsiExistence = function (params) {
|
||||
|
||||
this.resetLists();
|
||||
this.resetLists()
|
||||
|
||||
return xoApi.call('sr.probeIscsiExists', params)
|
||||
.then(response => {
|
||||
if (response.length > 0) {
|
||||
this.data.scsiList = response;
|
||||
this.data.scsiList = response
|
||||
return modal.confirm({
|
||||
title: 'Previous LUN Usage',
|
||||
message: 'This LUN has been previously used as a Storage by a XenServer host. All data will be lost if you choose to continue the SR creation. Are you sure?'
|
||||
});
|
||||
} else {
|
||||
return Bluebird.resolve(true);
|
||||
})
|
||||
}
|
||||
})
|
||||
;
|
||||
|
||||
};
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
this._checkNfsExistence = function (params) {
|
||||
|
||||
this.resetLists();
|
||||
this.resetLists()
|
||||
|
||||
return xoApi.call('sr.probeNfsExists', params)
|
||||
.then(response => {
|
||||
if (response.length > 0) {
|
||||
this.data.nfsList = response;
|
||||
this.data.nfsList = response
|
||||
return modal.confirm({
|
||||
title: 'Previous Path Usage',
|
||||
message: 'This path has been previously used as a Storage by a XenServer host. All data will be lost if you choose to continue the SR creation. Are you sure?'
|
||||
});
|
||||
} else {
|
||||
return Bluebird.resolve(true);
|
||||
})
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
;
|
||||
}
|
||||
|
||||
};
|
||||
const hostsByPool = xoApi.getIndex('hostsByPool')
|
||||
const srsByContainer = xoApi.getIndex('srsByContainer')
|
||||
this._gatherConnectedUuids = function () {
|
||||
const srIds = []
|
||||
|
||||
this._gatherConnectedUuids = function() {
|
||||
// Shared SRs.
|
||||
forEach(srsByContainer[this.container.$poolId], sr => {
|
||||
srIds.push(sr.id)
|
||||
})
|
||||
|
||||
let SRs = [];
|
||||
// Local SRs.
|
||||
forEach(hostsByPool[this.container.$poolId], host => {
|
||||
forEach(srsByContainer[host.id], sr => {
|
||||
srIds.push(sr.id)
|
||||
})
|
||||
})
|
||||
|
||||
let pool = xoApi.get(this.container.poolRef);
|
||||
pool.SRs.forEach(ref => SRs.push(xoApi.get(ref).UUID));
|
||||
let hosts = [];
|
||||
pool.hosts.forEach(ref => hosts.push(xoApi.get(ref)));
|
||||
hosts.forEach(h => h.SRs.forEach(ref => SRs.push(xoApi.get(ref).UUID)));
|
||||
|
||||
return SRs;
|
||||
|
||||
};
|
||||
return srIds
|
||||
}
|
||||
|
||||
this._processSRList = function (list) {
|
||||
let inUse = false
|
||||
let SRs = this._gatherConnectedUuids()
|
||||
|
||||
let inUse = false;
|
||||
let SRs = this._gatherConnectedUuids();
|
||||
forEach(list, item => {
|
||||
inUse = (item.used = _indexOf(SRs, item.uuid) > -1) || inUse
|
||||
})
|
||||
|
||||
list.forEach(item => {
|
||||
inUse = (item.used = _indexOf(SRs, item.uuid) > -1) || inUse;
|
||||
});
|
||||
this.lockCreation = inUse
|
||||
|
||||
this.lockCreation = inUse;
|
||||
return list
|
||||
}
|
||||
|
||||
return list;
|
||||
this.loadScsiList = function (data) {
|
||||
this.resetLists()
|
||||
this.loading = true
|
||||
|
||||
};
|
||||
|
||||
this.loadScsiList = function(data) {
|
||||
|
||||
this.resetLists();
|
||||
|
||||
let params = this._prepareScsiParams(data);
|
||||
let params = this._prepareScsiParams(data)
|
||||
|
||||
xoApi.call('sr.probeIscsiExists', params)
|
||||
.then(response => {
|
||||
|
||||
if (response.length > 0) {
|
||||
this.data.scsiList = this._processSRList(response);
|
||||
this.data.scsiList = this._processSRList(response)
|
||||
}
|
||||
|
||||
this.lock = !Boolean(data.srIScsiId);
|
||||
|
||||
this.lock = !Boolean(data.srIScsiId)
|
||||
})
|
||||
.catch(error => {
|
||||
notify.error({
|
||||
title : 'iSCSI Error',
|
||||
message : error.message
|
||||
});
|
||||
title: 'iSCSI Error',
|
||||
message: error.message
|
||||
})
|
||||
})
|
||||
;
|
||||
|
||||
};
|
||||
.finally(() => this.loading = false)
|
||||
}
|
||||
|
||||
this.loadNfsList = function (data) {
|
||||
this.resetLists()
|
||||
|
||||
this.resetLists();
|
||||
|
||||
let server = this._parseAddress(data.srServer);
|
||||
let server = this._parseAddress(data.srServer)
|
||||
|
||||
xoApi.call('sr.probeNfsExists', {
|
||||
host: this.container.UUID,
|
||||
host: this.container.id,
|
||||
server: server.host,
|
||||
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);
|
||||
|
||||
this.lock = !Boolean(data.srPath.path)
|
||||
})
|
||||
.catch(error => {
|
||||
notify.error({
|
||||
title : 'NFS error',
|
||||
message : error.message
|
||||
});
|
||||
title: 'NFS error',
|
||||
message: error.message
|
||||
})
|
||||
})
|
||||
;
|
||||
};
|
||||
}
|
||||
|
||||
this.reattachNfs = function (uuid, {name, nameError}, {desc, descError}, iso) {
|
||||
|
||||
this._reattach(uuid, 'nfs', {name, nameError}, {desc, descError}, iso);
|
||||
|
||||
};
|
||||
this._reattach(uuid, 'nfs', {name, nameError}, {desc, descError}, iso)
|
||||
}
|
||||
|
||||
this.reattachIScsi = function (uuid, {name, nameError}, {desc, descError}) {
|
||||
this._reattach(uuid, 'iscsi', {name, nameError}, {desc, descError})
|
||||
}
|
||||
|
||||
this._reattach(uuid, 'iscsi', {name, nameError}, {desc, descError});
|
||||
|
||||
};
|
||||
|
||||
this._reattach = function(uuid, type, {name, nameError}, {desc, descError}, iso = false) {
|
||||
|
||||
this.resetErrors();
|
||||
let method = 'sr.reattach' + (iso ? 'Iso' : '');
|
||||
this._reattach = function (uuid, type, {name, nameError}, {desc, descError}, iso = false) {
|
||||
this.resetErrors()
|
||||
let method = 'sr.reattach' + (iso ? 'Iso' : '')
|
||||
|
||||
if (nameError || descError) {
|
||||
this.data.error = {
|
||||
name: nameError,
|
||||
desc: descError
|
||||
};
|
||||
}
|
||||
notify.warning({
|
||||
title: 'Missing parameters',
|
||||
message: 'Complete the General section information, please'
|
||||
});
|
||||
})
|
||||
} else {
|
||||
this.lock = true;
|
||||
this.attaching = true;
|
||||
this.lock = true
|
||||
this.attaching = true
|
||||
xoApi.call(method, {
|
||||
host: this.container.UUID,
|
||||
host: this.container.id,
|
||||
uuid,
|
||||
nameLabel: name,
|
||||
nameDescription: desc,
|
||||
type
|
||||
})
|
||||
.then(id => {
|
||||
$state.go('SRs_view', {id});
|
||||
$state.go('SRs_view', {id})
|
||||
})
|
||||
.catch(error => notify.error({
|
||||
title : 'reattach',
|
||||
message : error.message
|
||||
})
|
||||
)
|
||||
title: 'reattach',
|
||||
message: error.message
|
||||
}))
|
||||
.finally(() => {
|
||||
this.lock = false;
|
||||
this.attaching = false;
|
||||
this.lock = false
|
||||
this.attaching = false
|
||||
})
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
this.reset();
|
||||
this.reset()
|
||||
|
||||
$scope.$watch(() => xoApi.get($stateParams.container), container => {
|
||||
this.container = container;
|
||||
});
|
||||
|
||||
this.container = container
|
||||
})
|
||||
})
|
||||
|
||||
// A module exports its name.
|
||||
.name
|
||||
;
|
||||
|
||||
@@ -3,16 +3,16 @@
|
||||
p.page-title
|
||||
i.xo-icon-sr
|
||||
| Add SR on
|
||||
a(ng-if="'pool' === newSr.container.type", ui-sref="pools_view({id: newSr.container.UUID})")
|
||||
a(ng-if="'pool' === newSr.container.type", ui-sref="pools_view({id: newSr.container.id})")
|
||||
| {{newSr.container.name_label}}
|
||||
a(ng-if="'host' === newSr.container.type", ui-sref="hosts_view({id: newSr.container.UUID})")
|
||||
a(ng-if="'host' === newSr.container.type", ui-sref="hosts_view({id: newSr.container.id})")
|
||||
| {{newSr.container.name_label}}
|
||||
form.form-horizontal(name = 'srForm' ng-submit="newSr.createSR(formData)")
|
||||
.grid
|
||||
//- Choose SR type panel
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-info-circle(style="color: #e25440;")
|
||||
i.fa.fa-info-circle
|
||||
| General
|
||||
.panel-body
|
||||
.form-group
|
||||
@@ -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
|
||||
@@ -38,10 +39,10 @@ form.form-horizontal(name = 'srForm' ng-submit="newSr.createSR(formData)")
|
||||
//- Choose SR details
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-cogs(style="color: #e25440;")
|
||||
i.fa.fa-cogs
|
||||
| Settings
|
||||
.panel-body
|
||||
.form-group(ng-if = 'formData.srType === "NFS" || formData.srType === "iSCSI"')
|
||||
.form-group(ng-if = 'formData.srType === "NFS" || formData.srType === "iSCSI" || formData.srType === "NFS_ISO"')
|
||||
label.col-sm-3.control-label
|
||||
| Server
|
||||
span(ng-if = 'formData.srType === "iSCSI"')
|
||||
@@ -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
|
||||
@@ -109,12 +127,12 @@ form.form-horizontal(name = 'srForm' ng-submit="newSr.createSR(formData)")
|
||||
select.form-control(name = 'srIScsiId', ng-change = 'newSr.loadScsiList(formData)', ng-model = 'formData.srIScsiId', ng-options = 'item.display for item in newSr.data.iScsiIds', required)
|
||||
option(value = '', disabled) -- Choose LUN --
|
||||
.form-group.text-center(ng-if = 'newSr.loading')
|
||||
i.fa.fa-circle-o-notch.fa-spin.fa-2x
|
||||
i.xo-icon-loading
|
||||
|
||||
.grid(ng-if = 'newSr.data.nfsList && newSr.data.nfsList.length > 0')
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-eye(style="color: #e25440;")
|
||||
i.fa.fa-eye
|
||||
| NFS storage use
|
||||
.panel-body
|
||||
table.table.table-condensed
|
||||
@@ -130,12 +148,12 @@ form.form-horizontal(name = 'srForm' ng-submit="newSr.createSR(formData)")
|
||||
i.fa.fa-eye
|
||||
| In use
|
||||
p.text-center(ng-if = 'newSr.attaching')
|
||||
i.fa.fa-circle-o-notch.fa-spin.fa-2x
|
||||
i.xo-icon-loading
|
||||
|
||||
.grid(ng-if = 'newSr.data.scsiList && newSr.data.scsiList.length > 0')
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-eye(style="color: #e25440;")
|
||||
i.fa.fa-eye
|
||||
| iSCSI storage use
|
||||
.panel-body
|
||||
table.table.table-condensed
|
||||
@@ -151,13 +169,13 @@ form.form-horizontal(name = 'srForm' ng-submit="newSr.createSR(formData)")
|
||||
i.fa.fa-eye
|
||||
| In use
|
||||
p.text-center(ng-if = 'newSr.attaching')
|
||||
i.fa.fa-circle-o-notch.fa-spin.fa-2x
|
||||
i.xo-icon-loading
|
||||
|
||||
//- Summary
|
||||
.grid
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-flag-checkered(style="color: #e25440;")
|
||||
i.fa.fa-flag-checkered
|
||||
| Summary
|
||||
.panel-body
|
||||
.grid
|
||||
@@ -180,4 +198,4 @@ form.form-horizontal(name = 'srForm' ng-submit="newSr.createSR(formData)")
|
||||
button.btn.btn-lg.btn-primary(type="submit", ng-disabled = 'newSr.lock || newSr.lockCreation')
|
||||
i.fa.fa-play
|
||||
| Create SR
|
||||
i.fa.fa-circle-o-notch.fa-spin(ng-if = 'newSr.creating')
|
||||
i.xo-icon-loading-sm(ng-if = 'newSr.creating')
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
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'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
@@ -8,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
|
||||
@@ -16,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
|
||||
@@ -31,6 +126,105 @@ module.exports = angular.module 'xoWebApp.newVm', [
|
||||
result
|
||||
|
||||
pool = default_SR = null
|
||||
host = null
|
||||
poolHosts = null
|
||||
hostsSrs = null
|
||||
do (
|
||||
networks = xoApi.getIndex('networksByPool')
|
||||
srsByContainer = xoApi.getIndex('srsByContainer')
|
||||
vmTemplatesByContainer = xoApi.getIndex('vmTemplatesByContainer')
|
||||
poolSrs = null
|
||||
hostSrs = null
|
||||
poolTemplates = null
|
||||
hostTemplates = null
|
||||
) ->
|
||||
Object.defineProperties($scope, {
|
||||
networks: {
|
||||
get: () => pool && networks[pool.id]
|
||||
}
|
||||
})
|
||||
|
||||
$scope.updateSrs = () =>
|
||||
srs = []
|
||||
$scope.selectedLocalSrs = {}
|
||||
Object.defineProperty($scope.selectedLocalSrs, "size", {
|
||||
value: 0,
|
||||
writable: true,
|
||||
enumerable: false
|
||||
})
|
||||
$scope.forcedHost = undefined
|
||||
poolSrs and forEach(poolSrs, (sr) => srs.push(sr))
|
||||
hostSrs and forEach(hostSrs, (sr) => srs.push(sr))
|
||||
poolHosts and forEach(poolHosts, (host) =>
|
||||
forEach(hostsSrs[host.id], (sr) ->
|
||||
srs.push(sr))
|
||||
)
|
||||
if pool or $scope.resourceSet
|
||||
selectedSrs = []
|
||||
forEach($scope.templateVBDs, (vbd) ->
|
||||
selectedSrs.push(xoApi.get(vbd.VDI).$SR)
|
||||
)
|
||||
forEach($scope.VDIs, (vdi) ->
|
||||
selectedSrs.push(vdi.SR)
|
||||
)
|
||||
if $scope.resourceSet
|
||||
forEach(selectedSrs, (sr) ->
|
||||
sr = xoApi.get sr
|
||||
container = xoApi.get sr.$container
|
||||
if container.type is 'host'
|
||||
if not $scope.selectedLocalSrs[sr.$container]
|
||||
$scope.selectedLocalSrs[sr.$container] = []
|
||||
$scope.selectedLocalSrs.size++
|
||||
$scope.forcedHost = sr.$container
|
||||
if not includes($scope.selectedLocalSrs[sr.$container], sr.id)
|
||||
$scope.selectedLocalSrs[sr.$container].push(sr.id)
|
||||
)
|
||||
else
|
||||
forEach(poolHosts, (host) ->
|
||||
forEach(hostsSrs[host.id], (sr) ->
|
||||
if includes(selectedSrs, sr.id)
|
||||
if not $scope.selectedLocalSrs[host.id]
|
||||
$scope.selectedLocalSrs[host.id] = []
|
||||
$scope.selectedLocalSrs.size++
|
||||
$scope.forcedHost = host.id
|
||||
if not includes($scope.selectedLocalSrs[host.id], sr.id)
|
||||
$scope.selectedLocalSrs[host.id].push(sr.id)
|
||||
)
|
||||
)
|
||||
if not $scope.resourceSet
|
||||
$scope.writable_SRs = filter(srs, (sr) => sr.content_type isnt 'iso')
|
||||
$scope.ISO_SRs = filter(srs, (sr) => sr.content_type is 'iso')
|
||||
|
||||
updateTemplates = () =>
|
||||
templates = []
|
||||
poolTemplates and forEach(poolTemplates, (template) => templates.push(template))
|
||||
hostTemplates and forEach(hostTemplates, (template) => templates.push(template))
|
||||
$scope.templates = templates
|
||||
$scope.$watchCollection(
|
||||
() => pool and srsByContainer[pool.id],
|
||||
(srs) =>
|
||||
poolSrs = srs
|
||||
$scope.updateSrs()
|
||||
)
|
||||
$scope.$watchCollection(
|
||||
() => host and srsByContainer[host.id],
|
||||
(srs) =>
|
||||
hostSrs = srs
|
||||
$scope.updateSrs()
|
||||
)
|
||||
$scope.$watchCollection(
|
||||
() => pool and vmTemplatesByContainer[pool.id],
|
||||
(templates) =>
|
||||
poolTemplates = templates
|
||||
updateTemplates()
|
||||
)
|
||||
$scope.$watchCollection(
|
||||
() => host and vmTemplatesByContainer[host.id],
|
||||
(templates) =>
|
||||
hostTemplates = templates
|
||||
updateTemplates()
|
||||
)
|
||||
|
||||
$scope.$watch(
|
||||
-> get $stateParams.container
|
||||
(container) ->
|
||||
@@ -41,54 +235,69 @@ module.exports = angular.module 'xoWebApp.newVm', [
|
||||
|
||||
if container.type is 'host'
|
||||
host = container
|
||||
pool = (get container.poolRef) ? {}
|
||||
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
|
||||
default_SR.UUID
|
||||
else
|
||||
''
|
||||
|
||||
# Computes the list of templates.
|
||||
$scope.templates = get (merge pool.templates, host.templates)
|
||||
|
||||
# FIXME: We should filter on connected SRs (PBDs)!
|
||||
# Computes the list of SRs.
|
||||
SRs = get (merge pool.SRs, host.SRs)
|
||||
|
||||
# Computes the list of ISO SRs.
|
||||
$scope.ISO_SRs = (SR for SR in SRs when SR.content_type is 'iso')
|
||||
|
||||
# Computes the list of writable SRs.
|
||||
$scope.writable_SRs = (SR for SR in SRs when SR.content_type isnt 'iso')
|
||||
|
||||
# Computes the list of networks.
|
||||
$scope.networks = get pool.networks
|
||||
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
|
||||
|
||||
@@ -98,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
|
||||
@@ -123,50 +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 = angular.copy template.template_info.disks
|
||||
VDIs = $scope.VDIs = cloneDeep template.template_info.disks
|
||||
forEach VDIs, (vdi, index) ->
|
||||
vdi.name_label = $scope.name_label + '_disk' + index
|
||||
vdi.name_description = 'Created by XO'
|
||||
|
||||
# if the template has no config disk
|
||||
# nor it's Other install media (specific case)
|
||||
if VDIs.length is 0 and template.name_label isnt 'Other install media'
|
||||
$scope.isDiskTemplate = true
|
||||
else $scope.isDiskTemplate = false
|
||||
for VDI in VDIs
|
||||
VDI.id = VDI_id++
|
||||
VDI.SR or= default_SR || $scope.writable_SRs[0] && $scope.writable_SRs[0].id
|
||||
VDI.size = bytesToSizeFilter VDI.size
|
||||
VDI.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 = angular.copy VDIs
|
||||
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 = angular.copy VIFs
|
||||
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'
|
||||
@@ -181,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.UUID
|
||||
template: template.id
|
||||
VDIs
|
||||
VIFs
|
||||
existingDisks
|
||||
}
|
||||
|
||||
# TODO:
|
||||
# - disable the form during creation
|
||||
# - indicate the progress of the operation
|
||||
@@ -199,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,50 +1,138 @@
|
||||
.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.UUID})")
|
||||
| {{container.name_label}}
|
||||
a(ng-if="'host' === container.type", ui-sref="hosts_view({id: container.UUID})")
|
||||
| {{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
|
||||
i.fa.fa-info-circle(style="color: #e25440;")
|
||||
i.fa.fa-info-circle
|
||||
| VM info
|
||||
.panel-body
|
||||
.form-group
|
||||
label.col-sm-3.control-label Template
|
||||
.col-sm-9
|
||||
select.form-control(ng-model="template", ng-options="template.name_label for template in templates | orderBy:natural('name_label') track by template.UUID", 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
|
||||
input.form-control(type="text", placeholder="Optional description of you new VM", ng-model="name_description")
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-dashboard(style="color: #e25440;")
|
||||
i.fa.fa-dashboard
|
||||
| Performances
|
||||
.panel-body
|
||||
.form-group
|
||||
label.col-sm-3.control-label vCPUs
|
||||
.col-sm-9
|
||||
input.form-control(type="text", placeholder="{{template.CPUs.number}}", ng-model="CPUs")
|
||||
.form-group
|
||||
label.col-sm-3.control-label CPU Weight
|
||||
.col-sm-9
|
||||
select.form-control(ng-model = "cpuWeight", ng-options='value as key for (key, value) in weightMap track by value' ng-disabled='resourceSet')
|
||||
option(value = '') default
|
||||
.form-group
|
||||
label.col-sm-3.control-label RAM
|
||||
.col-sm-9
|
||||
input.form-control(type="text", placeholder="{{template.memory.size | bytesToSize}}", ng-model="memory")
|
||||
.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
|
||||
//- Install panel
|
||||
.panel.panel-default
|
||||
//- Cloud Config Panel, only for templates with existing disks
|
||||
.panel.panel-default(ng-if="isDiskTemplate")
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-download(style="color: #e25440;")
|
||||
i.fa.fa-cloud
|
||||
| Config Drive
|
||||
span.pull-right
|
||||
label(style = 'cursor: pointer;')
|
||||
input.hidden(type = 'checkbox', ng-model = '$parent.configDriveActive', ng-click = '$parent.configDriveMethod = "standard"')
|
||||
i.fa(ng-class = '{"fa-toggle-on": $parent.configDriveActive, "text-success": $parent.configDriveActive, "fa-toggle-off": !$parent.configDriveActive}', style = 'font-size: 1.5em;')
|
||||
.panel-body
|
||||
fieldset(ng-disabled = '!$parent.configDriveActive')
|
||||
.form-group
|
||||
label.col-sm-3.control-label SSH Key
|
||||
.col-sm-9
|
||||
.input-group
|
||||
span.input-group-addon
|
||||
input(
|
||||
type = 'radio'
|
||||
name = 'configDriveMethod'
|
||||
ng-model = '$parent.configDriveMethod'
|
||||
value = 'standard'
|
||||
)
|
||||
input.form-control(
|
||||
type="text"
|
||||
placeholder="ssh-rsa AAAA.... you@machine"
|
||||
ng-model="$parent.cloudConfigSshKey"
|
||||
ng-disabled = '$parent.configDriveMethod !== "standard"'
|
||||
name="cloudConfigSshKey"
|
||||
required
|
||||
)
|
||||
.form-group
|
||||
label.col-sm-3.control-label
|
||||
a(href='http://cloudinit.readthedocs.org/en/latest/topics/examples.html', target='_blank') Custom config
|
||||
.col-sm-9
|
||||
.input-group
|
||||
span.input-group-addon
|
||||
input(
|
||||
type = 'radio'
|
||||
name = 'configDriveMethod'
|
||||
ng-model = '$parent.configDriveMethod'
|
||||
value = 'custom'
|
||||
)
|
||||
textarea.form-control(
|
||||
rows='4'
|
||||
style="resize: none;"
|
||||
ng-model="$parent.cloudConfigCustom"
|
||||
ng-disabled = '$parent.configDriveMethod !== "custom"'
|
||||
name="cloudConfigCustom"
|
||||
required
|
||||
)
|
||||
br
|
||||
button.btn.btn-default(
|
||||
type = 'button'
|
||||
ng-disabled = '$parent.configDriveMethod !== "custom"'
|
||||
ngf-select = '$parent.uploadCloudConfig($files[0]); fileName = $files[0].name'
|
||||
) Select file
|
||||
span(style='max-width: 1em' ng-init='fileName = "None"')
|
||||
| Selected file : {{ fileName }}
|
||||
i.fa.fa-spinner.fa-spin(ng-show = 'cloudConfigLoading')
|
||||
i.fa.fa-exclamation-triangle.text-danger(ng-show = 'cloudConfigError' tooltip = 'Error while loading file')
|
||||
//- Install panel, only if an installation method is needed
|
||||
.panel.panel-default(ng-if="!isDiskTemplate")
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-download
|
||||
| Install settings
|
||||
.panel-body
|
||||
.form-group(ng-show="availableMethods.cdrom")
|
||||
@@ -55,20 +143,20 @@ form.form-horizontal(ng-submit="createVM()")
|
||||
input(
|
||||
type = 'radio'
|
||||
name = 'installation_method'
|
||||
ng-model = 'installation_method'
|
||||
ng-model = '$parent.installation_method'
|
||||
value = 'cdrom'
|
||||
required
|
||||
)
|
||||
select.form-control.disabled(
|
||||
ng-disabled="'cdrom' !== installation_method"
|
||||
ng-model="installation_cdrom"
|
||||
required
|
||||
ng-model="$parent.installation_cdrom"
|
||||
)
|
||||
option(value = '') Please select
|
||||
optgroup(ng-repeat="SR in ISO_SRs | orderBy:natural('name_label') track by SR.UUID", ng-if="SR.VDIs.length", label="{{SR.name_label}}")
|
||||
option(ng-repeat="VDI in SR.VDIs | resolve | orderBy:natural('name_label') track by VDI.UUID", ng-value="VDI.UUID")
|
||||
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
|
||||
@@ -76,25 +164,28 @@ form.form-horizontal(ng-submit="createVM()")
|
||||
input(
|
||||
type = 'radio'
|
||||
name = 'installation_method'
|
||||
ng-model = '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="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>
|
||||
input.form-control(type="text", ng-disabled="'network' !== installation_method", placeholder="e.g: http://ftp.debian.org/debian", ng-model="$parent.installation_network")
|
||||
.form-group(ng-show = 'template.virtualizationMode === "hvm"')
|
||||
label.col-sm-3.control-label PXE
|
||||
.col-sm-9
|
||||
input(
|
||||
type = 'radio'
|
||||
name = 'installation_method'
|
||||
ng-model = '$parent.installation_method'
|
||||
value = 'pxe'
|
||||
)
|
||||
.form-group(ng-show='template.virtualizationMode === "pv"')
|
||||
label.col-sm-3.control-label PV Args
|
||||
.col-sm-9
|
||||
input.form-control(type="text", placeholder="{{template.PV_args}}", ng-model="$parent.pv_args")
|
||||
|
||||
//- Interface panel
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-network(style="color: #e25440;")
|
||||
i.xo-icon-network
|
||||
| Interfaces
|
||||
.panel-body
|
||||
table.table.table-hover
|
||||
@@ -105,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.UUID as network.name_label for network in (networks | orderBy:natural("name_label"))'
|
||||
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
|
||||
)
|
||||
@@ -125,11 +216,59 @@ 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
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-sr(style="color: #e25440;")
|
||||
i.xo-icon-disk
|
||||
| Disks
|
||||
.panel-body
|
||||
table.table.table-hover
|
||||
@@ -138,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.UUID 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
|
||||
@@ -172,32 +357,140 @@ form.form-horizontal(ng-submit="createVM()")
|
||||
.grid
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-flag-checkered(style="color: #e25440;")
|
||||
i.fa.fa-flag-checkered
|
||||
| Summary
|
||||
.panel-body
|
||||
.grid
|
||||
.grid-cell
|
||||
p.stat-name
|
||||
| Name:
|
||||
p.center.big {{name_label}}
|
||||
.grid-cell
|
||||
p.stat-name
|
||||
| Template:
|
||||
p.center {{template.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
|
||||
.grid-cell
|
||||
p.stat-name vCPUs
|
||||
p.center.big {{CPUs || template.CPUs.number}}
|
||||
//- p.stat-name vCPUs
|
||||
p.center.big(tooltip="vCPUs")
|
||||
| {{CPUs || template.CPUs.number || 0}}x
|
||||
i.xo-icon-cpu
|
||||
.grid-cell
|
||||
p.stat-name RAM
|
||||
p.center.big {{(memory) || (template.memory.size | bytesToSize)}}
|
||||
//- p.stat-name RAM
|
||||
p.center.big(tooltip="RAM")
|
||||
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 {{(VDIs.length) || (template.$VBDs.length) || 0}}
|
||||
//- p.stat-name Disks
|
||||
p.center.big(tooltip="Disks")
|
||||
| {{(VDIs.length) || (templateVBDs.length) || 0}}x
|
||||
i.xo-icon-disk
|
||||
.grid-cell
|
||||
p.stat-name Interfaces
|
||||
p.center.big {{VIFs.length}}
|
||||
//- 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
|
||||
|
||||
@@ -1,66 +1,240 @@
|
||||
import angular from 'angular';
|
||||
import angular from 'angular'
|
||||
import forEach from 'lodash.foreach'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
|
||||
import uiRouter from 'angular-ui-router';
|
||||
import xoTag from 'tag'
|
||||
|
||||
import view from './view';
|
||||
import view from './view'
|
||||
|
||||
//====================================================================
|
||||
// ===================================================================
|
||||
|
||||
export default angular.module('xoWebApp.pool', [
|
||||
uiRouter,
|
||||
xoTag
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('pools_view', {
|
||||
url: '/pools/:id',
|
||||
controller: 'PoolCtrl',
|
||||
template: view,
|
||||
});
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('PoolCtrl', function ($scope, $stateParams, xoApi, xo, modal) {
|
||||
$scope.$watch(() => xoApi.get($stateParams.id), function (pool) {
|
||||
$scope.pool = pool;
|
||||
});
|
||||
.controller('PoolCtrl', function ($scope, $stateParams, xoApi, xo, modal, notify) {
|
||||
{
|
||||
const {id} = $stateParams
|
||||
const hostsByPool = xoApi.getIndex('hostsByPool')
|
||||
const runningHostsByPool = xoApi.getIndex('runningHostsByPool')
|
||||
const srsByContainer = xoApi.getIndex('srsByContainer')
|
||||
const networksByPool = xoApi.getIndex('networksByPool')
|
||||
|
||||
Object.defineProperties($scope, {
|
||||
pool: {
|
||||
get: () => xoApi.get(id)
|
||||
},
|
||||
hosts: {
|
||||
get: () => hostsByPool[id]
|
||||
},
|
||||
runningHosts: {
|
||||
get: () => runningHostsByPool[id]
|
||||
},
|
||||
srs: {
|
||||
get: () => srsByContainer[id]
|
||||
},
|
||||
networks: {
|
||||
get: () => networksByPool[id]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
$scope.$watch(() => $scope.pool && $scope.hosts, result => {
|
||||
if (result) {
|
||||
$scope.listMissingPatches()
|
||||
xo.pool.getLicenseState($scope.pool.id).then(result => {
|
||||
$scope.license = result
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
$scope.currentLogPage = 1
|
||||
|
||||
$scope.savePool = function ($data) {
|
||||
let {pool} = $scope;
|
||||
let {name_label, name_description} = $data;
|
||||
let {pool} = $scope
|
||||
let {name_label, name_description} = $data
|
||||
|
||||
$data = {
|
||||
id: pool.UUID,
|
||||
id: pool.id
|
||||
}
|
||||
if (name_label !== pool.name_label) {
|
||||
$data.name_label = name_label;
|
||||
$data.name_label = name_label
|
||||
}
|
||||
if (name_description !== pool.name_description) {
|
||||
$data.name_description = name_description;
|
||||
$data.name_description = name_description
|
||||
}
|
||||
|
||||
xoApi.call('pool.set', $data);
|
||||
};
|
||||
xoApi.call('pool.set', $data)
|
||||
}
|
||||
|
||||
$scope.deleteAllLog = function () {
|
||||
return modal.confirm({
|
||||
title: 'Log deletion',
|
||||
message: 'Are you sure you want to delete all the logs?',
|
||||
message: 'Are you sure you want to delete all the logs?'
|
||||
}).then(function () {
|
||||
// TODO: return all promises.
|
||||
angular.forEach($scope.pool.messages, function(value, key) {
|
||||
xo.log.delete(value);
|
||||
console.log('Remove log', value);
|
||||
});
|
||||
});
|
||||
};
|
||||
forEach($scope.pool.messages, function (message) {
|
||||
xo.log.delete(message.id)
|
||||
console.log('Remove log', message.id)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
$scope.setDefaultSr = function (id) {
|
||||
let {pool} = $scope
|
||||
return modal.confirm({
|
||||
title: 'Set default SR',
|
||||
message: 'Are you sure you want to set this SR as default?'
|
||||
}).then(function () {
|
||||
return xo.pool.setDefaultSr(pool.id, id)
|
||||
})
|
||||
}
|
||||
|
||||
$scope.deleteLog = function (id) {
|
||||
console.log('Remove log', id);
|
||||
return xo.log.delete(id);
|
||||
};
|
||||
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
|
||||
@@ -79,9 +253,7 @@ export default angular.module('xoWebApp.pool', [
|
||||
// notify.info
|
||||
// title: 'Upload patch'
|
||||
// message: 'Success'
|
||||
|
||||
})
|
||||
|
||||
// A module exports its name.
|
||||
.name
|
||||
;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
//- TODO: lots of stuff.
|
||||
.grid
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.xo-icon-pool
|
||||
| {{pool.name_label}}
|
||||
.grid
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-cogs(style="color: #e25440;")
|
||||
i.fa.fa-cogs
|
||||
| General
|
||||
span.quick-edit(tooltip="Edit General settings", ng-click="poolSettings.$show()")
|
||||
i.fa.fa-edit.fa-fw
|
||||
@@ -24,15 +24,14 @@
|
||||
| {{pool.name_description}}
|
||||
dt Master
|
||||
dd(ng-repeat="master in [pool.master] | resolve")
|
||||
a(ui-sref="hosts_view({id: master.UUID})")
|
||||
a(ui-sref="hosts_view({id: master.id})")
|
||||
| {{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.UUID})") {{default_SR.name_label}}
|
||||
a(ui-sref="SRs_view({id: default_SR.id})") {{default_SR.name_label}}
|
||||
dt HA
|
||||
dd
|
||||
| {{pool.HA_enabled}}
|
||||
@@ -49,64 +48,71 @@
|
||||
| Save
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-stats(style="color: #e25440;")
|
||||
i.xo-icon-stats
|
||||
| Stats
|
||||
.grid
|
||||
.grid-cell
|
||||
.row
|
||||
.col-xs-6
|
||||
p.stat-name Hosts:
|
||||
p.center.big-stat {{pool.hosts.length}}
|
||||
.grid-cell
|
||||
p.center.big-stat {{hosts | count}}
|
||||
.col-xs-6
|
||||
p.stat-name Running:
|
||||
p.center.big-stat {{pool.$running_hosts.length}}
|
||||
p.center.big-stat {{runningHosts | count}}
|
||||
|
||||
//- Action panel
|
||||
.grid
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-flash(style="color: #e25440;")
|
||||
i.fa.fa-flash
|
||||
| Actions
|
||||
.panel-body
|
||||
.grid-cell.text-center
|
||||
.grid
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Add SR", type="button", style="width: 90%")
|
||||
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.UUID})")
|
||||
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.UUID)")
|
||||
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
|
||||
.grid
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-host(style="color: #e25440;")
|
||||
i.xo-icon-host
|
||||
| Hosts
|
||||
.panel-body
|
||||
table.table.table-hover.table-condensed
|
||||
th Name
|
||||
th.col-md-4 Description
|
||||
th.col-md-6 Memory
|
||||
tr(xo-sref="hosts_view({id: host.UUID})", ng-repeat="host in pool.hosts | resolve | orderBy:natural('name_label') track by host.UUID")
|
||||
td {{host.name_label}}
|
||||
td {{host.name_description}}
|
||||
tr(xo-sref="hosts_view({id: host.id})", ng-repeat="host in hosts | map | orderBy:natural('name_label') track by host.id")
|
||||
td.oneliner {{host.name_label}}
|
||||
td.oneliner {{host.name_description}}
|
||||
td
|
||||
.progress-condensed
|
||||
.progress-bar(role="progressbar", aria-valuemin="0", aria-valuenow="{{host.memory.usage}}", aria-valuemax="{{host.memory.size}}", style="width: {{[host.memory.usage, host.memory.size] | %}}")
|
||||
.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
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-sr(style="color: #e25440;")
|
||||
i.xo-icon-sr
|
||||
| Shared SR
|
||||
.panel-body
|
||||
table.table.table-hover
|
||||
@@ -115,33 +121,171 @@
|
||||
th Type
|
||||
th Size
|
||||
th.col-md-4 Physical/Allocated usage
|
||||
tr(xo-sref="SRs_view({id: SR.UUID})", ng-repeat="SR in pool.SRs | resolve | orderBy:natural('name_label') track by SR.UUID")
|
||||
td {{SR.name_label}}
|
||||
td {{SR.name_description}}
|
||||
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}}
|
||||
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] | %}}", tooltip="Physical usage: {{[SR.physical_usage, SR.size] | %}}")
|
||||
.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] | %}}", tooltip="Allocated: {{[(SR.usage), SR.size] | %}}")
|
||||
|
||||
//- Logs panel
|
||||
.grid
|
||||
.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.fa.fa-comments(style="color: #e25440;")
|
||||
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
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-comments
|
||||
| Logs
|
||||
span.quick-edit(ng-if="pool.messages.length", tooltip="Remove all logs", xo-click="deleteAllLog()")
|
||||
span.quick-edit(ng-if="pool.messages | isNotEmpty", tooltip="Remove all logs", xo-click="deleteAllLog()")
|
||||
i.fa.fa-trash-o.fa-fw
|
||||
.panel-body
|
||||
p.center(ng-if="!pool.messages.length") No recent logs
|
||||
table.table.table-hover(ng-if="pool.messages.length")
|
||||
p.center(ng-if="pool.messages | isEmpty") No recent logs
|
||||
table.table.table-hover(ng-if="pool.messages | isNotEmpty")
|
||||
th Date
|
||||
th Name
|
||||
tr(ng-repeat="message in pool.messages | resolve | orderBy:'-time' track by message.UUID")
|
||||
tr(ng-repeat="message in pool.messages | map | orderBy:'-time' | slice:(5*(currentLogPage-1)):(5*currentLogPage) track by message.id")
|
||||
td {{message.time*1e3 | date:"medium"}}
|
||||
td
|
||||
| {{message.name}}
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(xo-click="deleteLog(message.UUID)")
|
||||
a(xo-click="deleteLog(message.id)")
|
||||
i.fa.fa-trash-o.fa-lg(tooltip="Remove this log entry")
|
||||
.center(ng-if = '(pool.messages | count) > 5 || currentLogPage > 1')
|
||||
pagination(boundary-links="true", total-items="pool.messages | count", ng-model="$parent.currentLogPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-book
|
||||
| License
|
||||
.panel-body
|
||||
.row(ng-repeat="(key, value) in license")
|
||||
label.control-label.col-sm-3
|
||||
| {{key}}:
|
||||
.col-sm-9 {{value}}
|
||||
|
||||
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 = '')
|
||||
@@ -1,80 +1,131 @@
|
||||
import angular from 'angular';
|
||||
import uiRouter from 'angular-ui-router';
|
||||
import uiSelect from 'angular-ui-select';
|
||||
import angular from 'angular'
|
||||
import uiBootstrap from 'angular-ui-bootstrap'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
import uiSelect from 'angular-ui-select'
|
||||
|
||||
import filter from 'lodash.filter';
|
||||
import Bluebird from 'bluebird'
|
||||
import filter from 'lodash.filter'
|
||||
import forEach from 'lodash.foreach'
|
||||
|
||||
import xoApi from 'xo-api';
|
||||
import xoServices from 'xo-services';
|
||||
import xoApi from 'xo-api'
|
||||
import xoServices from 'xo-services'
|
||||
|
||||
import view from './view';
|
||||
import view from './view'
|
||||
|
||||
const HIGH_LEVEL_OBJECTS = {
|
||||
pool: true,
|
||||
host: true,
|
||||
VM: true,
|
||||
SR: true,
|
||||
network: true
|
||||
}
|
||||
|
||||
export default angular.module('settings.acls', [
|
||||
uiBootstrap,
|
||||
uiRouter,
|
||||
uiSelect,
|
||||
|
||||
xoApi,
|
||||
xoServices,
|
||||
xoServices
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('settings.acls', {
|
||||
controller: 'SettingsAcls as ctrl',
|
||||
url: '/acls',
|
||||
resolve: {
|
||||
acls(xo) {
|
||||
return xo.acl.get();
|
||||
users (xo) {
|
||||
return xo.user.getAll()
|
||||
},
|
||||
users(xo) {
|
||||
return xo.user.getAll();
|
||||
groups (xo) {
|
||||
return xo.group.getAll()
|
||||
},
|
||||
roles (xo) {
|
||||
return xo.role.getAll()
|
||||
}
|
||||
},
|
||||
template: view,
|
||||
});
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('SettingsAcls', function ($scope, acls, users, xoApi, xo) {
|
||||
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.users = users;
|
||||
this.types = Object.keys(HIGH_LEVEL_OBJECTS)
|
||||
this.selectedTypes = {}
|
||||
|
||||
this.users = users
|
||||
this.roles = roles
|
||||
this.groups = groups
|
||||
{
|
||||
let usersById = this.usersById = Object.create(null);
|
||||
let usersById = this.usersById = Object.create(null)
|
||||
for (let user of users) {
|
||||
usersById[user.id] = user;
|
||||
usersById[user.id] = user
|
||||
}
|
||||
let groupsById = this.groupsById = Object.create(null)
|
||||
for (let group of groups) {
|
||||
groupsById[group.id] = group
|
||||
}
|
||||
let rolesById = this.rolesById = Object.create(null)
|
||||
for (let role of roles) {
|
||||
rolesById[role.id] = role
|
||||
}
|
||||
}
|
||||
|
||||
this.objects = xoApi.all;
|
||||
this.entities = this.users.concat(this.groups)
|
||||
|
||||
let refreshAcls = () => {
|
||||
xo.acl.get().then(acls => {
|
||||
this.acls = acls;
|
||||
});
|
||||
};
|
||||
this.objects = xoApi.all
|
||||
|
||||
this.getUser = (id) => {
|
||||
for (let user of this.users) {
|
||||
if (user.id === id) {
|
||||
return user;
|
||||
return user
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
this.addAcl = () => {
|
||||
xo.acl.add(this.subject.id, this.object.id).then(refreshAcls);
|
||||
};
|
||||
this.removeAcl = (subject, object) => {
|
||||
xo.acl.remove(subject, object).then(refreshAcls);
|
||||
};
|
||||
const promises = []
|
||||
forEach(this.selectedObjects, object => promises.push(xo.acl.add(this.subject.id, object.id, this.role.id)))
|
||||
this.subject = this.selectedObjects = this.role = null
|
||||
Bluebird.all(promises).then(refreshAcls)
|
||||
}
|
||||
|
||||
this.removeAcl = (subject, object, role) => {
|
||||
xo.acl.remove(subject, object, role).then(refreshAcls)
|
||||
}
|
||||
|
||||
this.editAcl = (subject, object, role, newRole) => {
|
||||
console.log(subject, object, role, newRole)
|
||||
xo.acl.remove(subject, object, role)
|
||||
.then(xo.acl.add(subject, object, newRole))
|
||||
.then(refreshAcls)
|
||||
}
|
||||
|
||||
this.toggleType = (toggle, type) => {
|
||||
const selectedObjects = this.selectedObjects && this.selectedObjects.slice() || []
|
||||
if (toggle) {
|
||||
const objects = filterFilter(selectHighLevelFilter(this.objects), {type})
|
||||
forEach(objects, object => { selectedObjects.indexOf(object) === -1 && selectedObjects.push(object) })
|
||||
this.selectedObjects = selectedObjects
|
||||
} else {
|
||||
const keptObjects = []
|
||||
for (let index in this.selectedObjects) {
|
||||
const object = this.selectedObjects[index]
|
||||
if (object.type !== type) {
|
||||
keptObjects.push(object)
|
||||
}
|
||||
}
|
||||
this.selectedObjects = keptObjects
|
||||
}
|
||||
}
|
||||
})
|
||||
.filter('selectHighLevel', () => {
|
||||
const HIGH_LEVEL_OBJECTS = {
|
||||
pool: true,
|
||||
host: true,
|
||||
VM: true,
|
||||
SR: true,
|
||||
};
|
||||
let isHighLevel = (object) => HIGH_LEVEL_OBJECTS[object.type];
|
||||
|
||||
return (objects) => filter(objects, isHighLevel);
|
||||
let isHighLevel = (object) => HIGH_LEVEL_OBJECTS[object.type]
|
||||
return (objects) => filter(objects, isHighLevel)
|
||||
})
|
||||
.name
|
||||
;
|
||||
|
||||
@@ -1,66 +1,88 @@
|
||||
.grid
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.fa.fa-key(style="color: #e25440;")
|
||||
| ACLs
|
||||
.grid
|
||||
.grid-lg
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-plus-circle(style="color: #e25440;")
|
||||
i.fa.fa-plus-circle
|
||||
| Create
|
||||
.panel-body.text-center
|
||||
form(
|
||||
ng-submit = 'ctrl.addAcl()'
|
||||
)
|
||||
.panel-body
|
||||
form(ng-submit = 'ctrl.addAcl()')
|
||||
.form-group
|
||||
ui-select(
|
||||
ng-model = 'ctrl.subject'
|
||||
)
|
||||
ui-select-match(
|
||||
placeholder = 'Choose a user'
|
||||
)
|
||||
ui-select(ng-model = 'ctrl.subject')
|
||||
ui-select-match(placeholder = 'Choose a user or group')
|
||||
div
|
||||
i.fa.fa-user
|
||||
| {{$select.selected.email}}
|
||||
ui-select-choices(
|
||||
repeat = 'user in ctrl.users | filter:{ permission: "!admin" } | filter:$select.search'
|
||||
)
|
||||
span(ng-if = '$select.selected.email')
|
||||
i.xo-icon-user.fa-fw
|
||||
| {{$select.selected.email}}
|
||||
span(ng-if = '$select.selected.name')
|
||||
i.xo-icon-group.fa-fw
|
||||
| {{$select.selected.name}}
|
||||
ui-select-choices(repeat = 'entity in ctrl.entities | filter:{ permission: "!admin" } | filter:$select.search')
|
||||
div
|
||||
i.fa.fa-user
|
||||
| {{user.email}}
|
||||
span(ng-if = 'entity.email')
|
||||
i.xo-icon-user.fa-fw
|
||||
| {{entity.email}}
|
||||
span(ng-if = 'entity.name')
|
||||
i.xo-icon-group.fa-fw
|
||||
| {{entity.name}}
|
||||
.form-group
|
||||
ui-select(
|
||||
ng-model = 'ctrl.object'
|
||||
)
|
||||
ui-select-match(
|
||||
placeholder = 'Choose an object'
|
||||
)
|
||||
div
|
||||
i(class = 'xo-icon-{{$select.selected.type | lowercase}}')
|
||||
| {{$select.selected.name_label}}
|
||||
ui-select-choices(
|
||||
repeat = 'object in ctrl.objects | selectHighLevel | filter:$select.search | orderBy:["type", "name_label"]'
|
||||
)
|
||||
ui-select(ng-model = 'ctrl.selectedObjects', multiple, close-on-select = 'false', required)
|
||||
ui-select-match(placeholder = 'Choose an object')
|
||||
i(class = 'xo-icon-{{$item.type | lowercase}}')
|
||||
| {{$item.name_label}}
|
||||
span(ng-if="($item.type === 'SR' || $item.type === 'VM') && $item.$container")
|
||||
| ({{($item.$container | resolve).name_label}})
|
||||
span(ng-if="$item.type === 'network'")
|
||||
| ({{($item.$poolId | resolve).name_label}})
|
||||
ui-select-choices(repeat = 'object in ctrl.objects | selectHighLevel | filter:$select.search | orderBy:["type", "name_label"]')
|
||||
div
|
||||
i(class = 'xo-icon-{{object.type | lowercase}}')
|
||||
| {{object.name_label}}
|
||||
button.btn.btn-success
|
||||
i.fa.fa-plus
|
||||
| Create
|
||||
span(ng-if="(object.type === 'SR' || object.type === 'VM') && object.$container")
|
||||
| ({{(object.$container | resolve).name_label}})
|
||||
span(ng-if="object.type === 'network'")
|
||||
| ({{(object.$poolId | resolve).name_label}})
|
||||
.text-center
|
||||
span(ng-repeat = 'type in ctrl.types')
|
||||
label(tooltip = 'select/deselect all {{type}}s', style = 'cursor: pointer')
|
||||
input.hidden(type = 'checkbox', ng-model = 'ctrl.selectedTypes[type]', ng-change = 'ctrl.toggleType(ctrl.selectedTypes[type], type)')
|
||||
span.fa-stack
|
||||
i(class = 'xo-icon-{{type | lowercase}}').fa-stack-1x
|
||||
i.fa.fa-square-o.fa-stack-2x.text-info(ng-if = 'ctrl.selectedTypes[type]')
|
||||
.form-group
|
||||
ui-select(ng-model = 'ctrl.role')
|
||||
ui-select-match(placeholder = 'Choose a role')
|
||||
div
|
||||
i(class = 'xo-icon-{{$select.selected.type | lowercase}}')
|
||||
| {{$select.selected.name}}
|
||||
ui-select-choices(repeat = 'role in ctrl.roles | filter:$select.search | orderBy:"name"')
|
||||
div
|
||||
i(class = 'xo-icon-{{role.type | lowercase}}')
|
||||
| {{role.name}}
|
||||
.text-center
|
||||
button.btn.btn-success
|
||||
i.fa.fa-plus
|
||||
| Create
|
||||
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-street-view(style="color: #e25440;")
|
||||
i.fa.fa-street-view
|
||||
| Manage
|
||||
.panel-body
|
||||
table.table.table-hover
|
||||
tr
|
||||
th User
|
||||
th Object
|
||||
th Action
|
||||
tr(ng-repeat = 'acl in ctrl.acls')
|
||||
td {{ctrl.usersById[acl.subject].email}}
|
||||
th Role
|
||||
th
|
||||
tr(ng-repeat = 'acl in ctrl.acls | orderBy:["subject", "object"] track by acl.id')
|
||||
td {{ ctrl.usersById[acl.subject].email || ctrl.groupsById[acl.subject].name }}
|
||||
td {{(acl.object | resolve).name_label}}
|
||||
td
|
||||
button.btn.btn-sm.btn-danger(ng-click = 'ctrl.removeAcl(acl.subject, acl.object)')
|
||||
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
|
||||
|
||||
160
app/modules/settings/group/index.js
Normal file
@@ -0,0 +1,160 @@
|
||||
import angular from 'angular'
|
||||
import filter from 'lodash.filter'
|
||||
import find from 'lodash.find'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
import uiSelect from 'angular-ui-select'
|
||||
import uiEvent from 'angular-ui-event'
|
||||
|
||||
import xoApi from 'xo-api'
|
||||
import xoServices from 'xo-services'
|
||||
|
||||
import view from './view'
|
||||
|
||||
export default angular.module('settings.group', [
|
||||
uiRouter,
|
||||
uiSelect,
|
||||
uiEvent,
|
||||
|
||||
xoApi,
|
||||
xoServices
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('settings.group', {
|
||||
controller: 'SettingsGroup as ctrl',
|
||||
url: '/group/:groupId',
|
||||
resolve: {
|
||||
acls (xo) {
|
||||
return xo.acl.get()
|
||||
},
|
||||
groups (xo) {
|
||||
return xo.group.getAll()
|
||||
},
|
||||
roles (xo) {
|
||||
return xo.role.getAll()
|
||||
},
|
||||
users (xo) {
|
||||
return xo.user.getAll()
|
||||
}
|
||||
},
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('SettingsGroup', function ($scope, $state, $stateParams, $interval, acls, groups, roles, users, xoApi, xo) {
|
||||
this.acls = acls
|
||||
this.roles = roles
|
||||
this.users = users
|
||||
this.userEmails = Object.create(null)
|
||||
this.users.forEach(user => {
|
||||
this.userEmails[user.id] = user.email
|
||||
})
|
||||
{
|
||||
let rolesById = Object.create(null)
|
||||
for (let role of roles) {
|
||||
rolesById[role.id] = role
|
||||
}
|
||||
this.rolesById = rolesById
|
||||
}
|
||||
|
||||
this.objects = xoApi.all
|
||||
this.removals = Object.create(null)
|
||||
|
||||
const findGroup = groups => {
|
||||
this.group = filter(groups, gr => gr.id === $stateParams.groupId).pop()
|
||||
if (!this.group) {
|
||||
$state.go('settings.groups')
|
||||
}
|
||||
}
|
||||
findGroup(groups)
|
||||
|
||||
const refreshUsers = () => {
|
||||
xo.user.getAll().then(users => {
|
||||
this.users = users
|
||||
this.userEmails = Object.create(null)
|
||||
this.users.forEach(user => {
|
||||
this.userEmails[user.id] = user.email
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const refreshGroups = () => {
|
||||
if (!this.isModified()) {
|
||||
xo.group.getAll().then(groups => findGroup(groups))
|
||||
}
|
||||
}
|
||||
|
||||
const refreshAcls = () => {
|
||||
xo.acl.get().then(acls => {
|
||||
this.acls = acls
|
||||
})
|
||||
}
|
||||
|
||||
const interval = $interval(() => {
|
||||
refreshUsers()
|
||||
refreshGroups()
|
||||
}, 5e3)
|
||||
$scope.$on('$destroy', () => {
|
||||
$interval.cancel(interval)
|
||||
})
|
||||
|
||||
this.addUserToGroup = (group, user) => {
|
||||
if (user !== null) {
|
||||
group.users.push(user.id)
|
||||
this.addedUser = null
|
||||
this.modified = true
|
||||
}
|
||||
}
|
||||
|
||||
this.saveGroup = (group) => {
|
||||
const users = []
|
||||
group.users.forEach(user => {
|
||||
let remove = this.removals && this.removals[user]
|
||||
if (!remove) {
|
||||
users.push(user)
|
||||
}
|
||||
})
|
||||
this.removals = Object.create(null)
|
||||
xo.group.setUsers(group.id, users)
|
||||
.then(() => {
|
||||
group.users = users
|
||||
this.modified = false
|
||||
})
|
||||
}
|
||||
|
||||
this.cancelEdition = () => {
|
||||
this.modified = false
|
||||
this.removals = Object.create(null)
|
||||
refreshGroups()
|
||||
}
|
||||
|
||||
this.isModified = () => this.modified || Object.keys(this.removals).length
|
||||
this.matchesGroup = acl => {
|
||||
return acl.subject === this.group.id
|
||||
}
|
||||
|
||||
this.removeAcl = (object, role) => {
|
||||
xo.acl.remove(this.group.id, object, role).then(refreshAcls)
|
||||
}
|
||||
})
|
||||
.filter('notInGroup', function () {
|
||||
return function (users, group) {
|
||||
const filtered = []
|
||||
users.forEach(user => {
|
||||
if (!group.users || group.users.indexOf(user.id) === -1) {
|
||||
filtered.push(user)
|
||||
}
|
||||
})
|
||||
return filtered
|
||||
}
|
||||
})
|
||||
.filter('canAccess', () => {
|
||||
return (objects, group, acls) => {
|
||||
const accessed = []
|
||||
const groupAcls = filter(acls, acl => acl.subject === group.id)
|
||||
groupAcls.forEach(acl => {
|
||||
const found = find(objects, object => object.id === acl.object)
|
||||
found && accessed.push(found)
|
||||
})
|
||||
return accessed
|
||||
}
|
||||
})
|
||||
.name
|
||||
69
app/modules/settings/group/view.jade
Normal file
@@ -0,0 +1,69 @@
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.xo-icon-group(style="color: #e25440;")
|
||||
| {{ ctrl.group.name }}
|
||||
a.btn.btn-default(ui-sref = 'settings.groups')
|
||||
i.fa.fa-level-up
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-street-view
|
||||
| Members
|
||||
span(ng-if = 'ctrl.isModified()') (*)
|
||||
.panel-body
|
||||
ul.list-group(ng-if = '!ctrl.group.users.length')
|
||||
li.list-group-item.disabled: em (empty)
|
||||
ul.list-group(ng-if = 'ctrl.group.users.length')
|
||||
li.list-group-item(ng-repeat = 'user in ctrl.group.users')
|
||||
span(ng-if = '!ctrl.removals[user]') {{ ctrl.userEmails[user] }}
|
||||
del(ng-if = 'ctrl.removals[user]') {{ ctrl.userEmails[user] }}
|
||||
span.pull-right
|
||||
label
|
||||
input.hidden(type = 'checkbox', ng-model = 'ctrl.removals[user]')
|
||||
|
|
||||
i.fa.fa-trash-o(tooltip="Remove user from group", style = 'cursor: pointer')
|
||||
p
|
||||
ui-select(ng-if = '(ctrl.users | notInGroup:ctrl.group).length', ng-model = 'ctrl.addedUser', on-select = 'ctrl.addUserToGroup(ctrl.group, ctrl.addedUser)')
|
||||
ui-select-match(
|
||||
placeholder = 'Choose a user to add'
|
||||
) {{$select.selected.email}}
|
||||
ui-select-choices(
|
||||
repeat = 'addedUser in ctrl.users | notInGroup:ctrl.group | filter:$select.search'
|
||||
) {{addedUser.email}}
|
||||
em.text-muted(ng-if = '!(ctrl.users | notInGroup:ctrl.group).length') No available users to add
|
||||
button.btn.btn-primary(ng-if = 'ctrl.isModified()', type="button", ng-click = 'ctrl.saveGroup(ctrl.group)')
|
||||
i.fa.fa-save
|
||||
| Save
|
||||
|
|
||||
button.btn.btn-default(ng-if = 'ctrl.isModified()', type="button", ng-click = 'ctrl.cancelEdition()')
|
||||
i.fa.fa-times
|
||||
| Cancel
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-key
|
||||
| ACLs
|
||||
.panel-body
|
||||
table.table.table-hover
|
||||
tr
|
||||
th Object
|
||||
th Role
|
||||
th
|
||||
tr(ng-repeat = 'acl in ctrl.acls | filter:ctrl.matchesGroup track by acl.id')
|
||||
td {{(acl.object | resolve).name_label}}
|
||||
td {{ ctrl.rolesById[acl.action].name }}
|
||||
td
|
||||
button.btn.btn-danger(ng-click = 'ctrl.removeAcl(acl.object, acl.action)')
|
||||
i.fa.fa-trash
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-eye
|
||||
| Accessible objects
|
||||
.panel-body
|
||||
p(ng-repeat = 'object in ctrl.objects | selectHighLevel | canAccess:ctrl.group:ctrl.acls | orderBy:["type", "name_label"]')
|
||||
i(class = 'xo-icon-{{object.type | lowercase}}')
|
||||
| {{object.name_label}}
|
||||
span(ng-if="(object.type === 'SR' || object.type === 'VM') && object.$container")
|
||||
| ({{(object.$container | resolve).name_label}})
|
||||
|
||||
189
app/modules/settings/groups/index.js
Normal file
@@ -0,0 +1,189 @@
|
||||
import angular from 'angular'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
import uiSelect from 'angular-ui-select'
|
||||
import uiEvent from 'angular-ui-event'
|
||||
|
||||
import xoApi from 'xo-api'
|
||||
import xoServices from 'xo-services'
|
||||
|
||||
import view from './view'
|
||||
import modal from './modal'
|
||||
|
||||
export default angular.module('settings.groups', [
|
||||
uiRouter,
|
||||
uiSelect,
|
||||
uiEvent,
|
||||
|
||||
xoApi,
|
||||
xoServices
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('settings.groups', {
|
||||
controller: 'SettingsGroups as ctrl',
|
||||
url: '/groups',
|
||||
resolve: {
|
||||
users (xo) {
|
||||
return xo.user.getAll()
|
||||
},
|
||||
groups (xo) {
|
||||
return xo.group.getAll()
|
||||
}
|
||||
},
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('SettingsGroups', function ($scope, $interval, users, groups, xoApi, xo, $modal) {
|
||||
this.uiCollapse = Object.create(null)
|
||||
this.addedUsers = []
|
||||
|
||||
this.users = users
|
||||
this.userEmails = Object.create(null)
|
||||
this.users.forEach(user => {
|
||||
this.userEmails[user.id] = user.email
|
||||
})
|
||||
this.groups = groups
|
||||
|
||||
const selectedGroups = this.selectedGroups = {}
|
||||
this.newGroups = []
|
||||
|
||||
const refreshUsers = () => {
|
||||
xo.user.getAll().then(users => {
|
||||
this.users = users
|
||||
this.userEmails = Object.create(null)
|
||||
this.users.forEach(user => {
|
||||
this.userEmails[user.id] = user.email
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const refreshGroups = () => {
|
||||
if (!this._editingGroup && !this.modified) {
|
||||
return xo.group.getAll().then(groups => this.groups = groups)
|
||||
} else {
|
||||
return this.groups
|
||||
}
|
||||
}
|
||||
|
||||
const interval = $interval(() => {
|
||||
refreshUsers()
|
||||
refreshGroups()
|
||||
}, 5e3)
|
||||
$scope.$on('$destroy', () => {
|
||||
$interval.cancel(interval)
|
||||
})
|
||||
|
||||
this.addGroup = () => {
|
||||
this.newGroups.push({
|
||||
// Fake (unique) id needed by Angular.JS
|
||||
id: Math.random()
|
||||
})
|
||||
}
|
||||
if (!this.groups.length) {
|
||||
this.addGroup()
|
||||
}
|
||||
|
||||
this.deleteGroup = id => {
|
||||
const modalInstance = $modal.open({
|
||||
template: modal,
|
||||
backdrop: false
|
||||
})
|
||||
return modalInstance.result
|
||||
.then(() => {
|
||||
return xo.group.delete(id)
|
||||
.then(() => {
|
||||
return refreshGroups()
|
||||
})
|
||||
.then(groups => {
|
||||
if (!groups.length) {
|
||||
this.addGroup()
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
this.saveGroups = () => {
|
||||
const newGroups = this.newGroups
|
||||
const groups = this.groups
|
||||
const updateGroups = []
|
||||
|
||||
for (let i = 0, len = groups.length; i < len; i++) {
|
||||
const group = groups[i]
|
||||
const {id} = group
|
||||
if (selectedGroups[id]) {
|
||||
delete selectedGroups[id]
|
||||
xo.group.delete(id)
|
||||
} else {
|
||||
xo.group.set(group)
|
||||
updateGroups.push(group)
|
||||
}
|
||||
}
|
||||
for (let i = 0, len = newGroups.length; i < len; i++) {
|
||||
const group = newGroups[i]
|
||||
const {name} = group
|
||||
if (!name) {
|
||||
continue
|
||||
}
|
||||
xo.group.create({name})
|
||||
.then(function (id) {
|
||||
group.id = id
|
||||
group.users = []
|
||||
})
|
||||
updateGroups.push(group)
|
||||
}
|
||||
|
||||
this.groups = updateGroups
|
||||
this.newGroups.length = 0
|
||||
this.modified = false
|
||||
if (!this.groups.length) {
|
||||
this.addGroup()
|
||||
}
|
||||
}
|
||||
|
||||
this.addUserToGroup = (group, index) => {
|
||||
group.users.push(this.addedUsers[index].id)
|
||||
delete this.addedUsers[index]
|
||||
}
|
||||
|
||||
this.flagUserRemoval = (group, index, remove) => {
|
||||
group.removals || (group.removals = {})
|
||||
group.removals[group.users[index]] = remove
|
||||
}
|
||||
|
||||
this.saveGroup = (group) => {
|
||||
const users = []
|
||||
group.users.forEach(user => {
|
||||
let remove = group.removals && group.removals[user]
|
||||
if (!remove) {
|
||||
users.push(user)
|
||||
}
|
||||
})
|
||||
group.removals && delete group.removals
|
||||
xo.group.setUsers(group.id, users)
|
||||
.then(() => {
|
||||
group.users = users
|
||||
this.uiCollapse[group.id] = false
|
||||
})
|
||||
}
|
||||
|
||||
this.editingGroup = (editing = undefined) => editing !== undefined && (this._editingGroup = editing) || this._editingGroup
|
||||
|
||||
this.cancelModifications = () => {
|
||||
this.newGroups.length = 0
|
||||
this.editingGroup(false)
|
||||
this.modified = false
|
||||
refreshGroups()
|
||||
}
|
||||
})
|
||||
.filter('notInGroup', function () {
|
||||
return function (users, group) {
|
||||
const filtered = []
|
||||
users.forEach(user => {
|
||||
if (!group.users || group.users.indexOf(user.id) === -1) {
|
||||
filtered.push(user)
|
||||
}
|
||||
})
|
||||
return filtered
|
||||
}
|
||||
})
|
||||
.name
|
||||
12
app/modules/settings/groups/modal.jade
Normal file
@@ -0,0 +1,12 @@
|
||||
.modal-header
|
||||
button.close(
|
||||
type = 'button',
|
||||
ng-click = '$dismiss()'
|
||||
)
|
||||
span(aria-hidden = 'true') ×
|
||||
h4.modal-title Confirm group suppression
|
||||
.modal-body
|
||||
p Are you sure you want to delete this group ? It's user list and associated ACLs will be lost after that.
|
||||
button.btn.btn-default(type = 'button', ng-click = '$close()') Ok
|
||||
|  
|
||||
button.btn.btn-default(type = 'button', ng-click = '$dismiss()') Cancel
|
||||
49
app/modules/settings/groups/view.jade
Normal file
@@ -0,0 +1,49 @@
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.xo-icon-group(style="color: #e25440;")
|
||||
| Groups
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
form(ng-submit="ctrl.saveGroups()", autocomplete="off").panel-body
|
||||
table.table.table-hover
|
||||
tr
|
||||
th.col-md-5 Name
|
||||
th.col-md-5 Information
|
||||
th.col-md-2
|
||||
tr(ng-repeat="group in ctrl.groups | orderBy:natural('id') track by group.id")
|
||||
td
|
||||
input.form-control(type="text", ng-model="group.name", ui-event = '{focus: "ctrl.editingGroup(true)", blur: "ctrl.editingGroup(false)"}', ng-change = 'ctrl.modified = true')
|
||||
td
|
||||
span(ng-if = '!group.users.length'): em (empty)
|
||||
span(ng-if = 'group.users.length')
|
||||
strong {{ group.users.length }} members:
|
||||
span(ng-repeat = 'user in group.users | limitTo:4')
|
||||
| {{ ctrl.userEmails[user] }}{{ $last ? (group.users.length > 4 ? ',...' : '') : ', ' }}
|
||||
|
|
||||
td
|
||||
a.btn.btn-primary(ui-sref = 'settings.group({groupId: group.id})')
|
||||
| Edit
|
||||
i.fa.fa-pencil
|
||||
|
|
||||
button.btn.btn-danger(type = 'button', ng-click = 'ctrl.deleteGroup(group.id)')
|
||||
i.fa.fa-trash
|
||||
tr(ng-repeat="group in ctrl.newGroups")
|
||||
td
|
||||
input.form-control(type = "text", ng-model = "group.name", placeholder = "New group name", ng-change = 'ctrl.modified = true')
|
||||
td
|
||||
button.btn.btn-btn-default(type = 'button', ng-click = 'ctrl.newGroups.splice($index, 1)')
|
||||
i.fa.fa-times
|
||||
td  
|
||||
p
|
||||
button.btn.btn-success(type="button", ng-click="ctrl.addGroup()")
|
||||
i.fa.fa-plus
|
||||
|
|
||||
span(ng-if = 'ctrl.modified')
|
||||
button.btn.btn-primary(type="submit")
|
||||
i.fa.fa-save
|
||||
| Save
|
||||
|
|
||||
button.btn.btn-default(type="button", ng-click = "ctrl.cancelModifications()")
|
||||
i.fa.fa-times
|
||||
| Cancel
|
||||
@@ -1,33 +1,45 @@
|
||||
import angular from 'angular';
|
||||
import uiRouter from 'angular-ui-router';
|
||||
import angular from 'angular'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
|
||||
import acls from './acls';
|
||||
import servers from './servers';
|
||||
import users from './users';
|
||||
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';
|
||||
import view from './view'
|
||||
|
||||
export default angular.module('settings', [
|
||||
uiRouter,
|
||||
|
||||
acls,
|
||||
group,
|
||||
groups,
|
||||
plugins,
|
||||
servers,
|
||||
users,
|
||||
update,
|
||||
user,
|
||||
users
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('settings', {
|
||||
abstract: true,
|
||||
data: {
|
||||
requireAdmin: true
|
||||
},
|
||||
template: view,
|
||||
url: '/settings',
|
||||
});
|
||||
url: '/settings'
|
||||
})
|
||||
|
||||
// Redirect to default sub-state.
|
||||
$stateProvider.state('settings.index', {
|
||||
url: '',
|
||||
controller: function ($state) {
|
||||
$state.go('settings.servers');
|
||||
$state.go('settings.servers')
|
||||
}
|
||||
});
|
||||
})
|
||||
})
|
||||
.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,127 +1,148 @@
|
||||
import angular from 'angular';
|
||||
import uiRouter from 'angular-ui-router';
|
||||
import uiSelect from 'angular-ui-select';
|
||||
import angular from 'angular'
|
||||
import forEach from 'lodash.foreach'
|
||||
import includes from 'lodash.includes'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
import uiSelect from 'angular-ui-select'
|
||||
|
||||
import filter from 'lodash.filter';
|
||||
import xoApi from 'xo-api'
|
||||
import xoServices from 'xo-services'
|
||||
|
||||
import xoApi from 'xo-api';
|
||||
import xoServices from 'xo-services';
|
||||
|
||||
import view from './view';
|
||||
import view from './view'
|
||||
|
||||
export default angular.module('settings.servers', [
|
||||
uiRouter,
|
||||
uiSelect,
|
||||
|
||||
xoApi,
|
||||
xoServices,
|
||||
xoServices
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('settings.servers', {
|
||||
controller: 'SettingsServers as ctrl',
|
||||
url: '/servers',
|
||||
resolve: {
|
||||
servers(xo) {
|
||||
return xo.server.getAll();
|
||||
},
|
||||
servers (xo) {
|
||||
return xo.server.getAll()
|
||||
}
|
||||
},
|
||||
template: view,
|
||||
});
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('SettingsServers', function ($scope, $interval, servers, xoApi, xo, notify) {
|
||||
this.servers = servers;
|
||||
const selected = this.selectedServers = {};
|
||||
const newServers = this.newServers = [];
|
||||
.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)
|
||||
})
|
||||
|
||||
this.connectServer = (id) => {
|
||||
notify.info ({
|
||||
title: 'Server connect',
|
||||
message: 'Connecting the server...'
|
||||
});
|
||||
notify.info({
|
||||
title: 'Server connect',
|
||||
message: 'Connecting the server...'
|
||||
})
|
||||
xo.server.connect(id).catch(error => {
|
||||
notify.error({
|
||||
title: 'Server connection error',
|
||||
message: error.message
|
||||
});
|
||||
});
|
||||
};
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
this.disconnectServer = (id) => {
|
||||
notify.info ({
|
||||
title: 'Server disconnect',
|
||||
message: 'Disconnecting the server...'
|
||||
});
|
||||
xo.server.disconnect(id);
|
||||
};
|
||||
notify.info({
|
||||
title: 'Server disconnect',
|
||||
message: 'Disconnecting the server...'
|
||||
})
|
||||
xo.server.disconnect(id)
|
||||
}
|
||||
|
||||
this.addServer = () => {
|
||||
newServers.push({
|
||||
// Fake (unique) id needed by Angular.JS
|
||||
id: Math.random(),
|
||||
status: 'connecting'
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
this.addServer();
|
||||
this.addServer()
|
||||
this.saveServers = () => {
|
||||
const newServers = this.newServers;
|
||||
const servers = this.servers;
|
||||
const updateServers = [];
|
||||
const addresses = []
|
||||
forEach(xoApi.getView('host').all, host => addresses.push(host.address))
|
||||
|
||||
const newServers = this.newServers
|
||||
const servers = this.servers
|
||||
const updateServers = []
|
||||
|
||||
for (let i = 0, len = servers.length; i < len; i++) {
|
||||
const server = servers[i];
|
||||
const {id} = server;
|
||||
const server = servers[i]
|
||||
const {id} = server
|
||||
if (selected[id]) {
|
||||
delete selected[id];
|
||||
xo.server.remove(id);
|
||||
}
|
||||
else {
|
||||
delete selected[id]
|
||||
xo.server.remove(id)
|
||||
} else {
|
||||
if (!server.password) {
|
||||
delete server.password;
|
||||
delete server.password
|
||||
}
|
||||
xo.server.set(server);
|
||||
delete server.password;
|
||||
updateServers.push(server);
|
||||
server.readOnly = $scope.readOnly[id]
|
||||
xo.server.set(server)
|
||||
delete server.password
|
||||
updateServers.push(server)
|
||||
}
|
||||
}
|
||||
for (let i = 0, len = newServers.length; i < len; i++) {
|
||||
const server = newServers[i];
|
||||
const {host, username, password} = server;
|
||||
const server = newServers[i]
|
||||
const {host, username, password, readOnly} = server
|
||||
if (!host) {
|
||||
continue;
|
||||
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,
|
||||
autoConnect: false,
|
||||
}).then(function(id) {
|
||||
server.id = id;
|
||||
readOnly,
|
||||
autoConnect: false
|
||||
}).then(function (id) {
|
||||
server.id = id
|
||||
$scope.readOnly[id] = Boolean(readOnly)
|
||||
xo.server.connect(id).catch(error => {
|
||||
notify.error({
|
||||
title: 'Server connection error',
|
||||
message: error.message
|
||||
});
|
||||
});
|
||||
});
|
||||
delete server.password;
|
||||
updateServers.push(server);
|
||||
})
|
||||
})
|
||||
})
|
||||
delete server.password
|
||||
updateServers.push(server)
|
||||
}
|
||||
this.servers = updateServers;
|
||||
this.newServers.length = 0;
|
||||
this.addServer();
|
||||
};
|
||||
this.servers = updateServers
|
||||
this.newServers.length = 0
|
||||
this.addServer()
|
||||
}
|
||||
})
|
||||
.name
|
||||
;
|
||||
|
||||
@@ -1,36 +1,50 @@
|
||||
.grid
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.fa.fa-cloud(style="color: #e25440;")
|
||||
| Servers
|
||||
.grid
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
//- .panel-heading.panel-title
|
||||
//- i.fa.fa-cloud(style="color: #e25440;")
|
||||
//- | Connections
|
||||
form(ng-submit="ctrl.saveServers()", autocomplete="off").panel-body
|
||||
table.table.table-hover
|
||||
tr
|
||||
th.col-md-5 Host
|
||||
th.col-md-2 User
|
||||
th.col-md-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(ng-if="server.status === 'connected'")
|
||||
i.fa.fa-check-circle.fa-lg.text-success(tooltip="Connected")
|
||||
span.input-group-addon(ng-if="server.status === 'disconnected'")
|
||||
i.fa.fa-times-circle.fa-lg.text-danger(tooltip="Disconnected")
|
||||
span.input-group-addon(ng-if="server.status === 'connecting'")
|
||||
span.input-group-addon.hidden-xs(ng-if="server.status === 'connected'")
|
||||
i.xo-icon-success.fa-lg(tooltip="Connected")
|
||||
span.input-group-addon.hidden-xs(ng-if="server.status === 'disconnected'")
|
||||
i.xo-icon-failure.fa-lg(tooltip="Disconnected")
|
||||
span.input-group-addon.hidden-xs(ng-if="server.status === 'connecting'")
|
||||
i.fa.fa-cog.fa-lg.fa-spin(tooltip="Connecting...")
|
||||
input.form-control(type="text", ng-model="server.host")
|
||||
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'",
|
||||
@@ -46,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")
|
||||
@@ -70,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")
|
||||
|
||||
99
app/modules/settings/update/index.js
Normal file
@@ -0,0 +1,99 @@
|
||||
import angular from 'angular'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
|
||||
import _assign from 'lodash.assign'
|
||||
import ansiUp from 'ansi_up'
|
||||
import updater from '../../updater'
|
||||
import xoApi from 'xo-api'
|
||||
import xoServices from 'xo-services'
|
||||
import {AuthenticationFailed} from '../../updater'
|
||||
|
||||
import view from './view'
|
||||
|
||||
export default angular.module('settings.update', [
|
||||
uiRouter,
|
||||
|
||||
updater,
|
||||
xoApi,
|
||||
xoServices
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('settings.update', {
|
||||
controller: 'SettingsUpdate as ctrl',
|
||||
url: '/update',
|
||||
onExit: updater => {
|
||||
updater.removeAllListeners('end')
|
||||
},
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.filter('ansitohtml', function ($sce) {
|
||||
return function (input) {
|
||||
return $sce.trustAsHtml(ansiUp.ansi_to_html(input))
|
||||
}
|
||||
})
|
||||
.controller('SettingsUpdate', function (xoApi, xo, updater, notify) {
|
||||
this.updater = updater
|
||||
|
||||
this.updater.isRegistered()
|
||||
.then(() => this.updater.on('end', () => this.updater.isRegistered()))
|
||||
.catch(err => console.error(err))
|
||||
|
||||
this.updater.getConfiguration()
|
||||
.then(configuration => this.configuration = _assign({}, configuration))
|
||||
.then(() => this.withAuth = Boolean(this.configuration.proxyUser))
|
||||
.catch(error => notify.error({
|
||||
title: 'XOA Updater',
|
||||
message: error.message
|
||||
}))
|
||||
|
||||
this.registerXoa = (email, password, renewRegister) => {
|
||||
this.regPwd = ''
|
||||
this.updater.register(email, password, renewRegister)
|
||||
.tap(() => this.renewRegister = false)
|
||||
.then(() => this.updater.update())
|
||||
.catch(AuthenticationFailed, () => {})
|
||||
.catch(err => console.error(err))
|
||||
}
|
||||
|
||||
this.update = () => {
|
||||
this.updater.update()
|
||||
.catch(error => notify.error({
|
||||
title: 'XOA Updater',
|
||||
message: error.message
|
||||
}))
|
||||
}
|
||||
|
||||
this.upgrade = () => {
|
||||
this.updater.upgrade()
|
||||
.catch(error => notify.error({
|
||||
title: 'XOA Updater',
|
||||
message: error.message
|
||||
}))
|
||||
}
|
||||
|
||||
this.configure = (host, port, username, password) => {
|
||||
const config = {}
|
||||
if (!this.withAuth) {
|
||||
username = null
|
||||
password = null
|
||||
}
|
||||
config.proxyHost = host && host.trim() || null
|
||||
config.proxyPort = port && port.trim() || null
|
||||
config.proxyUser = username || null
|
||||
config.proxyPassword = password || null
|
||||
return this.updater.configure(config)
|
||||
.then(configuration => this.configuration = _assign({}, configuration))
|
||||
.then(() => this.withAuth = Boolean(this.configuration.proxyUser))
|
||||
.catch(error => notify.error({
|
||||
title: 'XOA Updater',
|
||||
message: error.message
|
||||
}))
|
||||
.finally(() => this.update())
|
||||
}
|
||||
|
||||
this.valid = trial => {
|
||||
return trial && trial.end && Date.now() < trial.end
|
||||
}
|
||||
})
|
||||
.name
|
||||
128
app/modules/settings/update/view.jade
Normal file
@@ -0,0 +1,128 @@
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.fa.fa-refresh(style="color: #e25440;")
|
||||
| Update
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-globe
|
||||
| Status
|
||||
.panel-body
|
||||
p(ng-if = '!ctrl.updater.state')
|
||||
a.btn.btn-warning: i.fa.fa-question-circle(ng-if = '!ctrl.updater.state', tooltip = 'No update information available')
|
||||
| No update information available
|
||||
a.btn.btn-default(ng-class = '{disabled: ctrl.updater.isConnected}', ng-click = 'ctrl.update()')
|
||||
i.fa.fa-refresh(ng-class = '{"fa-spin": ctrl.updater.isConnected}')
|
||||
.form-group(ng-if = 'ctrl.updater.state && ctrl.updater.state === "registerNeeded"')
|
||||
a.btn.btn-warning(ng-if = 'ctrl.updater.state === "registerNeeded"'): i.fa.fa-bell-slash(tooltip = 'Your XOA is not registered for updates')
|
||||
| Registration needed
|
||||
button.btn.btn-default(ng-if = 'ctrl.updater.registerState === "registered"', ng-click = 'ctrl.updater.update()', ng-class = '{disabled: ctrl.updater.updating || ctrl.updater.upgrading}'): i.fa.fa-refresh(ng-class = '{"fa-spin": ctrl.updater.updating || ctrl.updater.upgrading}')
|
||||
.form-group(ng-if = 'ctrl.updater.state && ctrl.updater.state !== "registerNeeded"')
|
||||
a.btn.btn-info(ng-if = 'ctrl.updater.state === "connected"'): i.fa.fa-question-circle(tooltip = 'Update information may be available')
|
||||
a.btn.btn-success(ng-if = 'ctrl.updater.state === "upToDate"'): i.fa.fa-check(tooltip = 'Your XOA is up-to-date')
|
||||
a.btn.btn-primary(ng-if = 'ctrl.updater.state === "upgradeNeeded"'): i.fa.fa-bell(tooltip = 'You need to update your XOA (new version is available)')
|
||||
a.btn.btn-danger(ng-if = 'ctrl.updater.state === "error"'): i.fa.fa-exclamation-triangle(tooltip = 'Can\'t fetch update information')
|
||||
|
|
||||
button#update.btn.btn-info(type = 'button', ng-click = 'ctrl.update()', ng-class = '{disabled: ctrl.updater.updating || ctrl.updater.upgrading}')
|
||||
| Check for updates
|
||||
i.fa.fa-refresh(ng-class = '{"fa-spin": ctrl.updater.updating}')
|
||||
|
|
||||
button#upgrade.btn.btn-primary(ng-if = 'ctrl.updater.state === "upgradeNeeded"', type = 'button', ng-click = 'ctrl.upgrade()', ng-class = '{disabled: ctrl.updater.updating || ctrl.updater.upgrading}')
|
||||
| Upgrade
|
||||
i.fa.fa-cog(ng-class = '{"fa-spin": ctrl.updater.upgrading}')
|
||||
div
|
||||
p(ng-repeat = 'entry in ctrl.updater._log')
|
||||
span(ng-class = '{"text-danger": entry.level === "error", "text-muted": entry.level === "info", "text-warning": entry.level === "warning", "text-success": entry.level === "success"}') {{ entry.date }}
|
||||
| :
|
||||
span(style = 'word-wrap: break-word;', ng-bind-html = 'entry.message | ansitohtml')
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-pencil
|
||||
| Registration
|
||||
.panel-body.text-center
|
||||
.text-warning(ng-if = '!ctrl.updater.state || ctrl.updater.registerState === "unknown"')
|
||||
| No registration information available.
|
||||
br
|
||||
span.big-stat
|
||||
i.fa.fa-exclamation-triangle.text-warning
|
||||
div(ng-if = 'ctrl.updater.state && ctrl.updater.registerState === "error"')
|
||||
.text-danger Can't fetch registration information.
|
||||
br
|
||||
span.big-stat
|
||||
i.fa.fa-exclamation-triangle.text-danger
|
||||
br
|
||||
.text-danger {{ ctrl.updater.registerError }}
|
||||
br
|
||||
button.btn.btn-default(type = 'button', ng-click = 'ctrl.updater.isRegistered()')
|
||||
i.fa.fa-refresh
|
||||
| Refresh
|
||||
form(ng-if = 'ctrl.updater.state && (ctrl.renewRegister || ctrl.updater.registerState === "unregistered")', ng-submit = 'ctrl.registerXoa(ctrl.regEmail, ctrl.regPwd, ctrl.renewRegister)')
|
||||
p.form-static-control(ng-if = '!ctrl.renewRegister') XOA is not registered.
|
||||
p.form-static-control(ng-if = 'ctrl.renewRegister')
|
||||
| Forget previous registration ?
|
||||
button.btn.btn-default(type = 'button', ng-click = 'ctrl.renewRegister = false') Cancel
|
||||
p.small Your xen-orchestra.com email and password
|
||||
.form-group
|
||||
.input-group
|
||||
span.input-group-addon: i.fa.fa-envelope-o.fa-fw
|
||||
label.sr-only(for = 'regEmail') Email
|
||||
input#regEmail.form-control(type = 'email', placeholder = 'Email', ng-model = 'ctrl.regEmail', required)
|
||||
.form-group
|
||||
.input-group
|
||||
span.input-group-addon: i.fa.fa-key.fa-fw
|
||||
label.sr-only(for = 'regPwd') Email
|
||||
input#regPwd.form-control(type = 'password', placeholder = 'Password', ng-model = 'ctrl.regPwd', required)
|
||||
.form-group
|
||||
button.btn.btn-primary(type = 'submit')
|
||||
i.fa.fa-check
|
||||
| Register
|
||||
p.form-static-control.text-danger {{ ctrl.updater.registerError }}
|
||||
p(ng-if = 'ctrl.updater.state && ctrl.updater.registerState === "registered" && !ctrl.renewRegister')
|
||||
| Your Xen Orchestra appliance is registered to
|
||||
span.text-success {{ ctrl.updater.token.registrationEmail }}
|
||||
| .
|
||||
br
|
||||
br
|
||||
i.fa.fa-check-circle.fa-3x.text-success
|
||||
br
|
||||
br
|
||||
button.btn.btn-default(type = 'button', ng-click = 'ctrl.renewRegister = true') Register to someone else ?
|
||||
|
||||
.grid-sm(ng-if = 'ctrl.updater.state && ctrl.configuration')
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-cogs
|
||||
| Settings
|
||||
.panel-body
|
||||
form(ng-submit = 'ctrl.configure(ctrl.configuration.proxyHost, ctrl.configuration.proxyPort, ctrl.configuration.proxyUser, ctrl.configuration.proxyPassword)')
|
||||
h4
|
||||
i.fa.fa-globe
|
||||
| Proxy settings
|
||||
p
|
||||
| If you need a proxy to access the Internet 
|
||||
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
|
||||
.form-group
|
||||
button.btn.btn-primary(type = 'submit')
|
||||
i.fa.fa-floppy-o
|
||||
| Save
|
||||
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,35 +1,36 @@
|
||||
import angular from 'angular';
|
||||
import uiRouter from 'angular-ui-router';
|
||||
import uiSelect from 'angular-ui-select';
|
||||
import angular from 'angular'
|
||||
import passwordGenerator from 'password-generator'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
import uiSelect from 'angular-ui-select'
|
||||
import uiEvent from 'angular-ui-event'
|
||||
|
||||
import filter from 'lodash.filter';
|
||||
import xoApi from 'xo-api'
|
||||
import xoServices from 'xo-services'
|
||||
|
||||
import xoApi from 'xo-api';
|
||||
import xoServices from 'xo-services';
|
||||
|
||||
import view from './view';
|
||||
import view from './view'
|
||||
|
||||
export default angular.module('settings.users', [
|
||||
uiRouter,
|
||||
uiSelect,
|
||||
uiEvent,
|
||||
|
||||
xoApi,
|
||||
xoServices,
|
||||
xoServices
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('settings.users', {
|
||||
controller: 'SettingsUsers as ctrl',
|
||||
url: '/users',
|
||||
resolve: {
|
||||
users(xo) {
|
||||
return xo.user.getAll();
|
||||
},
|
||||
users (xo) {
|
||||
return xo.user.getAll()
|
||||
}
|
||||
},
|
||||
template: view,
|
||||
});
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('SettingsUsers', function ($scope, $interval, users, xoApi, xo) {
|
||||
this.users = users;
|
||||
this.users = users
|
||||
this.permissions = [
|
||||
{
|
||||
label: 'User',
|
||||
@@ -39,71 +40,93 @@ export default angular.module('settings.users', [
|
||||
label: 'Admin',
|
||||
value: 'admin'
|
||||
}
|
||||
];
|
||||
const selected = this.selectedUsers = {};
|
||||
const newUsers = this.newUsers = [];
|
||||
]
|
||||
|
||||
const refreshUsers = () => {
|
||||
xo.user.getAll().then(users => {
|
||||
this.users = users;
|
||||
});
|
||||
};
|
||||
const selected = this.selectedUsers = {}
|
||||
this.newUsers = []
|
||||
|
||||
const interval = $interval(refreshUsers, 5e3)
|
||||
$scope.$on('$destroy', () => {
|
||||
$interval.cancel(interval)
|
||||
})
|
||||
|
||||
this.addUser = () => {
|
||||
newUsers.push({
|
||||
// Fake (unique) id needed by Angular.JS
|
||||
id: Math.random(),
|
||||
permission: 'none'
|
||||
});
|
||||
};
|
||||
|
||||
this.addUser();
|
||||
this.saveUsers = () => {
|
||||
const newUsers = this.newUsers;
|
||||
const users = this.users;
|
||||
const updateUsers = [];
|
||||
|
||||
for (let i = 0, len = users.length; i < len; i++) {
|
||||
const user = users[i];
|
||||
const {id} = user;
|
||||
if (selected[id]) {
|
||||
delete selected[id];
|
||||
xo.user.delete(id);
|
||||
const refreshUsers = () => {
|
||||
if (!this._editingUser) {
|
||||
xo.user.getAll().then(users => {
|
||||
this.users = users
|
||||
this.userEmails = Object.create(null)
|
||||
this.users.forEach(user => {
|
||||
this.userEmails[user.id] = user.email
|
||||
})
|
||||
})
|
||||
}
|
||||
else {
|
||||
if (!user.password) {
|
||||
delete user.password;
|
||||
}
|
||||
|
||||
const interval = $interval(() => {
|
||||
refreshUsers()
|
||||
}, 5e3)
|
||||
$scope.$on('$destroy', () => {
|
||||
$interval.cancel(interval)
|
||||
})
|
||||
|
||||
this.addUser = () => {
|
||||
this.newUsers.push({
|
||||
// Fake (unique) id needed by Angular.JS
|
||||
id: Math.random(),
|
||||
permission: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
this.addUser()
|
||||
|
||||
this.saveUsers = () => {
|
||||
const newUsers = this.newUsers
|
||||
const users = this.users
|
||||
const updateUsers = []
|
||||
|
||||
for (let i = 0, len = users.length; i < len; i++) {
|
||||
const user = users[i]
|
||||
const {id} = user
|
||||
if (selected[id]) {
|
||||
delete selected[id]
|
||||
xo.user.delete(id)
|
||||
} else {
|
||||
if (!user.password) {
|
||||
delete user.password
|
||||
}
|
||||
xo.user.set(user)
|
||||
delete user.password
|
||||
updateUsers.push(user)
|
||||
}
|
||||
xo.user.set(user);
|
||||
delete user.password;
|
||||
updateUsers.push(user);
|
||||
}
|
||||
}
|
||||
for (let i = 0, len = newUsers.length; i < len; i++) {
|
||||
const user = newUsers[i];
|
||||
const {email, permission, password} = user;
|
||||
if (!email) {
|
||||
continue;
|
||||
for (let i = 0, len = newUsers.length; i < len; i++) {
|
||||
const user = newUsers[i]
|
||||
const {email, permission, password} = user
|
||||
if (!email) {
|
||||
continue
|
||||
}
|
||||
xo.user.create({
|
||||
email,
|
||||
permission,
|
||||
password
|
||||
}).then(function (id) {
|
||||
user.id = id
|
||||
})
|
||||
delete user.password
|
||||
updateUsers.push(user)
|
||||
}
|
||||
xo.user.create({
|
||||
email,
|
||||
permission,
|
||||
password,
|
||||
}).then(function(id) {
|
||||
user.id = id;
|
||||
});
|
||||
delete user.password;
|
||||
updateUsers.push(user);
|
||||
this.users = updateUsers
|
||||
this.newUsers.length = 0
|
||||
this.userEmails = Object.create(null)
|
||||
this.users.forEach(user => {
|
||||
this.userEmails[user.id] = user.email
|
||||
})
|
||||
this.addUser()
|
||||
}
|
||||
|
||||
this.editingUser = editing => {
|
||||
this._editingUser = editing
|
||||
}
|
||||
|
||||
this.generatePassword = (user) => {
|
||||
// Generate password of 8 letters/numbers/underscore
|
||||
user.password = passwordGenerator(8, false)
|
||||
}
|
||||
this.users = updateUsers;
|
||||
this.newUsers.length = 0;
|
||||
this.addUser();
|
||||
};
|
||||
})
|
||||
|
||||
.name
|
||||
;
|
||||
|
||||
@@ -1,55 +1,111 @@
|
||||
.grid
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.fa.fa-user(style="color: #e25440;")
|
||||
i.xo-icon-user(style="color: #e25440;")
|
||||
| Users
|
||||
.grid
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
//- .panel-heading.panel-title
|
||||
//- i.fa.fa-users(style="color: #e25440;")
|
||||
//- | Users
|
||||
form(ng-submit="ctrl.saveUsers()", autocomplete="off").panel-body
|
||||
table.table.table-hover
|
||||
tr
|
||||
th.col-md-4 Email
|
||||
th.col-md-4 Permissions
|
||||
th.col-md-3 Password
|
||||
th.col-md-1.text-center
|
||||
i.fa.fa-trash-o.fa-lg(tooltip="Remove user")
|
||||
tr(ng-repeat="user in ctrl.users | orderBy:natural('email') track by user.id")
|
||||
td
|
||||
input.form-control(type="text", ng-model="user.email")
|
||||
td
|
||||
select.form-control(ng-options="p.value as p.label for p in ctrl.permissions", ng-model="user.permission")
|
||||
td
|
||||
input.form-control(type="password", ng-model="user.password", placeholder="Fill to change the password")
|
||||
td.text-center
|
||||
input(type="checkbox", ng-model="ctrl.selectedUsers[user.id]")
|
||||
tr(ng-repeat="user in ctrl.newUsers")
|
||||
td
|
||||
input.form-control(
|
||||
type = "text"
|
||||
ng-model = "user.email"
|
||||
placeholder = "email"
|
||||
)
|
||||
td
|
||||
select.form-control(
|
||||
ng-options = "p.value as p.label for p in ctrl.permissions"
|
||||
ng-model = "user.permission"
|
||||
ng-required = "user.email"
|
||||
)
|
||||
td
|
||||
input.form-control(
|
||||
type = "password"
|
||||
ng-model = "user.password"
|
||||
ng-required = "user.email"
|
||||
placeholder = "password"
|
||||
)
|
||||
td  
|
||||
p.text-center
|
||||
button.btn.btn-primary(type="submit")
|
||||
i.fa.fa-save
|
||||
| Save
|
||||
|
|
||||
button.btn.btn-success(type="button", ng-click="ctrl.addUser()")
|
||||
i.fa.fa-plus
|
||||
.panel-body
|
||||
form(ng-submit="ctrl.saveUsers()", autocomplete="off")
|
||||
table.table.table-hover
|
||||
tr
|
||||
th.col-md-4 Email
|
||||
th.col-md-4 Permissions
|
||||
th.col-md-3 Password
|
||||
th.col-md-1.text-center
|
||||
i.fa.fa-trash-o.fa-lg(tooltip="Remove user")
|
||||
tr(ng-repeat="user in ctrl.users | orderBy:natural('id') track by user.id")
|
||||
td
|
||||
input.form-control(
|
||||
type="text"
|
||||
ng-model="user.email"
|
||||
ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}'
|
||||
)
|
||||
td
|
||||
select.form-control(
|
||||
ng-options="p.value as p.label for p in ctrl.permissions"
|
||||
ng-model="user.permission"
|
||||
ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}'
|
||||
)
|
||||
td
|
||||
div.input-group
|
||||
span.input-group-btn
|
||||
button.btn.btn-default(
|
||||
type = "button"
|
||||
tooltip = "Generate random password"
|
||||
ng-click = "ctrl.generatePassword(user); showPassword = true"
|
||||
)
|
||||
i.fa.fa-key
|
||||
input.form-control(
|
||||
type = "{{ showPassword ? 'text' : 'password' }}"
|
||||
ng-model = "user.password"
|
||||
placeholder = "Fill to change the password"
|
||||
ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}'
|
||||
)
|
||||
span.input-group-btn
|
||||
button.btn.btn-default(
|
||||
type = "button"
|
||||
tooltip = "Reveal password"
|
||||
ng-show = "user.password.length > 0"
|
||||
ng-mousedown = "showPassword = true"
|
||||
ng-mouseup = "showPassword = false"
|
||||
ng-mouseleave = "showPassword = false"
|
||||
)
|
||||
i.fa.fa-eye(ng-if = "showPassword")
|
||||
i.fa.fa-eye-slash(ng-if = "!showPassword")
|
||||
td.text-center
|
||||
input(
|
||||
type="checkbox"
|
||||
ng-model="ctrl.selectedUsers[user.id]"
|
||||
ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}'
|
||||
)
|
||||
tr(ng-repeat="user in ctrl.newUsers")
|
||||
td
|
||||
input.form-control(
|
||||
type = "text"
|
||||
ng-model = "user.email"
|
||||
placeholder = "email"
|
||||
ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}'
|
||||
)
|
||||
td
|
||||
select.form-control(
|
||||
ng-options = "p.value as p.label for p in ctrl.permissions"
|
||||
ng-model = "user.permission"
|
||||
ng-required = "user.email"
|
||||
ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}'
|
||||
)
|
||||
td
|
||||
div.input-group
|
||||
span.input-group-btn
|
||||
button.btn.btn-default(
|
||||
type = "button"
|
||||
tooltip = "Generate random password"
|
||||
ng-click = "ctrl.generatePassword(user); showPassword = true"
|
||||
)
|
||||
i.fa.fa-key
|
||||
input.form-control(
|
||||
type = "{{ showPassword ? 'text' : 'password' }}"
|
||||
ng-model = "user.password"
|
||||
ng-required = "user.email"
|
||||
placeholder = "password"
|
||||
ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}'
|
||||
)
|
||||
span.input-group-btn
|
||||
button.btn.btn-default(
|
||||
type = "button"
|
||||
tooltip = "Reveal password"
|
||||
ng-show = "user.password.length > 0"
|
||||
ng-mousedown = "showPassword = true"
|
||||
ng-mouseup = "showPassword = false"
|
||||
ng-mouseleave = "showPassword = false"
|
||||
)
|
||||
i.fa.fa-eye(ng-if = "showPassword")
|
||||
i.fa.fa-eye-slash(ng-if = "!showPassword")
|
||||
td  
|
||||
p.text-center
|
||||
button.btn.btn-primary(type="submit")
|
||||
i.fa.fa-save
|
||||
| Save
|
||||
|
|
||||
button.btn.btn-success(type="button", ng-click="ctrl.addUser()")
|
||||
i.fa.fa-plus
|
||||
|
||||
@@ -1,20 +1,28 @@
|
||||
.grid
|
||||
//- Side menu
|
||||
.settings-menu
|
||||
.menu-grid
|
||||
.side-menu
|
||||
ul.nav
|
||||
li
|
||||
a(ui-sref = '.servers', ui-sref-active = 'active')
|
||||
i.fa.fa-fw.fa-cloud.fa-menu
|
||||
| Servers
|
||||
span.menu-entry Servers
|
||||
li
|
||||
a(ui-sref = '.users')
|
||||
i.fa.fa-fw.fa-user.fa-menu
|
||||
| Users
|
||||
//- a.disabled(ui-sref = '.groups')
|
||||
//- i.fa.fa-fw.fa-users.fa-menu
|
||||
//- | Groups
|
||||
i.xo-icon-user.fa-fw.fa-menu
|
||||
span.menu-entry Users
|
||||
li
|
||||
a(ui-sref = '.groups')
|
||||
i.xo-icon-group.fa-fw.fa-menu
|
||||
span.menu-entry Groups
|
||||
li
|
||||
a(ui-sref = '.acls')
|
||||
i.fa.fa-fw.fa-key.fa-menu
|
||||
| ACLs
|
||||
|
||||
//- Content
|
||||
div.settings-content(ui-view = '')
|
||||
span.menu-entry ACLs
|
||||
li
|
||||
a(ui-sref = '.plugins')
|
||||
i.xo-icon-plugin.fa-fw.fa-menu
|
||||
span.menu-entry Plugins
|
||||
li
|
||||
a(ui-sref = '.update')
|
||||
i.fa.fa-fw.fa-refresh.fa-menu
|
||||
span.menu-entry Update
|
||||
.side-content(ui-view = '')
|
||||
|
||||
@@ -1,189 +1,263 @@
|
||||
import angular from 'angular';
|
||||
import isEmpty from 'isempty';
|
||||
import uiRouter from 'angular-ui-router';
|
||||
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 Bluebird from 'bluebird'
|
||||
|
||||
import view from './view';
|
||||
import xoTag from 'tag'
|
||||
|
||||
//====================================================================
|
||||
import view from './view'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default angular.module('xoWebApp.sr', [
|
||||
uiRouter,
|
||||
xoTag
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('SRs_view', {
|
||||
url: '/srs/:id',
|
||||
controller: 'SrCtrl',
|
||||
template: view,
|
||||
});
|
||||
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
|
||||
|
||||
$window.bytesToSize = bytesToSizeFilter; // FIXME dirty workaround to custom a Chart.js tooltip template
|
||||
$scope.units = ['MiB', 'GiB', 'TiB']
|
||||
|
||||
let {get} = xoApi;
|
||||
$scope.currentLogPage = 1
|
||||
$scope.currentVDIPage = 1
|
||||
|
||||
let {get} = xoApi
|
||||
$scope.$watch(() => xoApi.get($stateParams.id), function (SR) {
|
||||
$scope.SR = 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;
|
||||
let {SR} = $scope
|
||||
let {name_label, name_description} = $data
|
||||
|
||||
$data = {
|
||||
id: SR.UUID,
|
||||
};
|
||||
id: SR.id
|
||||
}
|
||||
if (name_label !== SR.name_label) {
|
||||
$data.name_label = name_label;
|
||||
$data.name_label = name_label
|
||||
}
|
||||
if (name_description !== SR.name_description) {
|
||||
$data.name_description = name_description;
|
||||
$data.name_description = name_description
|
||||
}
|
||||
|
||||
return xoApi.call('sr.set', $data);
|
||||
};
|
||||
return xoApi.call('sr.set', $data)
|
||||
}
|
||||
|
||||
$scope.deleteVDI = function (UUID) {
|
||||
console.log('Delete VDI', UUID);
|
||||
$scope.deleteVDI = function (id) {
|
||||
console.log('Delete VDI', id)
|
||||
|
||||
return modal.confirm({
|
||||
title: 'VDI deletion',
|
||||
message: 'Are you sure you want to delete this VDI? This operation is irreversible.',
|
||||
message: 'Are you sure you want to delete this VDI? This operation is irreversible.'
|
||||
}).then(function () {
|
||||
return xo.vdi.delete(UUID);
|
||||
});
|
||||
};
|
||||
return xo.vdi.delete(id)
|
||||
})
|
||||
}
|
||||
|
||||
$scope.disconnectVBD = function (UUID) {
|
||||
console.log('Disconnect VBD', UUID);
|
||||
$scope.disconnectVBD = function (id) {
|
||||
console.log('Disconnect VBD', id)
|
||||
|
||||
return xoApi.call('vbd.disconnect', {id: UUID});
|
||||
};
|
||||
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 (UUID) {
|
||||
console.log('Connect PBD', UUID);
|
||||
$scope.connectPBD = function (id) {
|
||||
console.log('Connect PBD', id)
|
||||
|
||||
return xoApi.call('pbd.connect', {id: UUID});
|
||||
};
|
||||
return xo.pbd.connect(id)
|
||||
}
|
||||
|
||||
$scope.disconnectPBD = function (UUID) {
|
||||
console.log('Disconnect PBD', UUID);
|
||||
$scope.disconnectPBD = function (id) {
|
||||
console.log('Disconnect PBD', id)
|
||||
|
||||
return xoApi.call('pbd.disconnect', {id: UUID});
|
||||
};
|
||||
return xo.pbd.disconnect(id)
|
||||
}
|
||||
|
||||
$scope.reconnectAllHosts = function () {
|
||||
// TODO: return a Bluebird.all(promises).
|
||||
for (let id of $scope.SR.$PBDs) {
|
||||
let pbd = xoApi.get(id);
|
||||
let pbd = xoApi.get(id)
|
||||
|
||||
xoApi.call('pbd.connect', {id: pbd.ref});
|
||||
xoApi.call('pbd.connect', {id: pbd.id})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
$scope.disconnectAllHosts = function () {
|
||||
return modal.confirm({
|
||||
title: 'Disconnect hosts',
|
||||
message: 'Are you sure you want to disconnect all hosts to this SR?',
|
||||
message: 'Are you sure you want to disconnect all hosts to this SR?'
|
||||
}).then(function () {
|
||||
for (let id of $scope.SR.$PBDs) {
|
||||
let pbd = xoApi.get(id);
|
||||
let pbd = xoApi.get(id)
|
||||
|
||||
xoApi.call('pbd.disconnect', {id: pbd.ref});
|
||||
console.log(pbd.ref)
|
||||
xoApi.call('pbd.disconnect', {id: pbd.id})
|
||||
console.log(pbd.id)
|
||||
}
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
$scope.rescanSr = function (UUID) {
|
||||
console.log('Rescan SR', UUID);
|
||||
$scope.rescanSr = function (id) {
|
||||
console.log('Rescan SR', id)
|
||||
|
||||
return xoApi.call('sr.scan', {id: UUID});
|
||||
};
|
||||
return xoApi.call('sr.scan', {id: id})
|
||||
}
|
||||
|
||||
$scope.removeSR = function (UUID) {
|
||||
console.log('Remove SR', UUID);
|
||||
$scope.removeSR = function (id) {
|
||||
console.log('Remove SR', id)
|
||||
|
||||
return modal.confirm({
|
||||
title: 'SR deletion',
|
||||
message: 'Are you sure you want to delete this SR? This operation is irreversible.',
|
||||
message: 'Are you sure you want to delete this SR? This operation is irreversible.'
|
||||
}).then(function () {
|
||||
return Bluebird.map($scope.SR.$PBDs, pbdId => {
|
||||
let pbd = xoApi.get(pbdId);
|
||||
let pbd = xoApi.get(pbdId)
|
||||
|
||||
return xoApi.call('pbd.disconnect', { id: pbd.id });
|
||||
});
|
||||
return xoApi.call('pbd.disconnect', { id: pbd.id })
|
||||
})
|
||||
}).then(function () {
|
||||
return xoApi.call('sr.destroy', {id: UUID});
|
||||
return xoApi.call('sr.destroy', {id: id})
|
||||
}).then(function () {
|
||||
$state.go('index');
|
||||
$state.go('index')
|
||||
notify.info({
|
||||
title: 'SR remove',
|
||||
message: 'SR is removed',
|
||||
});
|
||||
});
|
||||
};
|
||||
message: 'SR is removed'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
$scope.forgetSR = function (UUID) {
|
||||
console.log('Forget SR', UUID);
|
||||
$scope.forgetSR = function (id) {
|
||||
console.log('Forget SR', id)
|
||||
|
||||
return modal.confirm({
|
||||
title: 'SR forget',
|
||||
message: 'Are you sure you want to forget this SR? No VDI on this SR will be removed.',
|
||||
message: 'Are you sure you want to forget this SR? No VDI on this SR will be removed.'
|
||||
}).then(function () {
|
||||
return Bluebird.map($scope.SR.$PBDs, pbdId => {
|
||||
let pbd = xoApi.get(pbdId);
|
||||
let pbd = xoApi.get(pbdId)
|
||||
|
||||
return xoApi.call('pbd.disconnect', { id: pbd.id });
|
||||
});
|
||||
return xoApi.call('pbd.disconnect', { id: pbd.id })
|
||||
})
|
||||
}).then(function () {
|
||||
return xoApi.call('sr.forget', {id: UUID});
|
||||
return xoApi.call('sr.forget', {id: id})
|
||||
}).then(function () {
|
||||
$state.go('index');
|
||||
$state.go('index')
|
||||
notify.info({
|
||||
title: 'SR forget',
|
||||
message: 'SR is forgotten',
|
||||
});
|
||||
});
|
||||
};
|
||||
message: 'SR is forgotten'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
$scope.saveDisks = function (data) {
|
||||
// Group data by disk.
|
||||
let disks = {};
|
||||
angular.forEach(data, function (value, key) {
|
||||
let i = key.indexOf('/');
|
||||
let disks = {}
|
||||
let sizeChanges = false
|
||||
forEach(data, function (value, key) {
|
||||
let i = key.indexOf('/')
|
||||
|
||||
let id = key.slice(0, i);
|
||||
let prop = key.slice(i + 1);
|
||||
let id = key.slice(0, i)
|
||||
let prop = key.slice(i + 1)
|
||||
|
||||
(disks[id] || (disks[id] = {}))[prop] = value;
|
||||
});
|
||||
;(disks[id] || (disks[id] = {}))[prop] = value
|
||||
})
|
||||
|
||||
let promises = [];
|
||||
angular.forEach(disks, function (attributes, id) {
|
||||
// Keep only changed attributes.
|
||||
let disk = get(id);
|
||||
|
||||
angular.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));
|
||||
forEach(disks, function (attributes, id) {
|
||||
let disk = get(id)
|
||||
attributes.size = bytesToSizeFilter(sizeToBytesFilter(attributes.sizeValue + ' ' + attributes.sizeUnit))
|
||||
if (attributes.size !== bytesToSizeFilter(disk.size)) { // /!\ attributes are provided by a modified copy of disk
|
||||
sizeChanges = true
|
||||
return false
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// A module exports its name.
|
||||
.name
|
||||
;
|
||||
|
||||
@@ -6,10 +6,12 @@
|
||||
.grid
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-cogs(style="color: #e25440;")
|
||||
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,16 +26,13 @@
|
||||
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")
|
||||
| Yes (
|
||||
a(ui-sref="pools_view({id: container.UUID})") {{container.name_label}}
|
||||
a(ui-sref="pools_view({id: container.id})") {{container.name_label}}
|
||||
| )
|
||||
dd(ng-if="'host' === container.type") No
|
||||
dt Size
|
||||
@@ -51,99 +50,139 @@
|
||||
| Save
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-stats(style="color: #e25440;")
|
||||
i.xo-icon-stats
|
||||
| Stats
|
||||
.panel-body
|
||||
.grid
|
||||
.grid-cell
|
||||
p.stat-name Physical Alloc:
|
||||
canvas(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) %>"}')
|
||||
.grid-cell
|
||||
p.stat-name Virtual Alloc:
|
||||
canvas(id="doughnut", class="chart chart-doughnut", data="[(SR.usage), (SR.size - SR.usage)]", labels="['Used', 'Free']", options='{tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>"}')
|
||||
.grid-cell
|
||||
.row
|
||||
.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
|
||||
p.stat-name VDIs:
|
||||
p.center.big-stat {{SR.VDIs.length}}
|
||||
//- Action panel
|
||||
.grid
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-flash(style="color: #e25440;")
|
||||
i.fa.fa-flash
|
||||
| Actions
|
||||
.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.UUID)")
|
||||
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.UUID)")
|
||||
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.UUID)")
|
||||
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
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-memory(style="color: #e25440;")
|
||||
i.xo-icon-memory
|
||||
| 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.UUID", role="progressbar", aria-valuemin="0", aria-valuenow="{{VDI.size}}", aria-valuemax="{{SR.size}}", style="width: {{[VDI.size, SR.size] | %}}", tooltip="{{VDI.name_label}} ({{[VDI.size, SR.size] | %}})")
|
||||
.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.fa.fa-hdd-o(style="color: #e25440;")
|
||||
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(tooltip="Rescan", ng-click="rescanSr(SR.UUID)")
|
||||
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')")
|
||||
td
|
||||
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.UUID}}/name_label'
|
||||
e-name = '{{VDI.id}}/name_label'
|
||||
)
|
||||
| {{VDI.name_label}}
|
||||
span.label.label-info(ng-if="VDI.$snapshot_of") snapshot
|
||||
td
|
||||
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"
|
||||
e-name = '{{VDI.UUID}}/name_description'
|
||||
e-name = '{{VDI.id}}/name_description'
|
||||
)
|
||||
| {{VDI.name_description}}
|
||||
td
|
||||
//- FIXME: should be editable, but the server needs first
|
||||
//- to accept a human readable string.
|
||||
| {{VDI.size | bytesToSize}}
|
||||
td {{((VDI.$VBD | resolve).VM | resolve).name_label}}
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(ng-if="(VDI.$VBD | resolve).attached", xo-click="disconnectVBD(VBD.UUID)")
|
||||
i.fa.fa-unlink.fa-lg(tooltip="Disconnect this disk")
|
||||
a(ng-if="!(VDI.$VBD | resolve).attached", xo-click="deleteVDI(VDI.UUID)")
|
||||
i.fa.fa-trash-o.fa-lg(tooltip="Destroy this disk")
|
||||
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")
|
||||
input(ng-if = '!(VDI.$VBDs[0] | resolve).attached', type = 'checkbox', ng-model = 'selectedForDelete[VDI.id]', tooltip = 'select for deletion')
|
||||
//- TODO: Ability to create new VDIs.
|
||||
.form-inline
|
||||
.input-group
|
||||
.input-group-addon: i.fa.fa-filter
|
||||
input.form-control(type = 'text', ng-model = 'vdiSearch', placeholder = 'Enter your search here')
|
||||
.center(ng-if = '(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(
|
||||
@@ -165,7 +204,7 @@
|
||||
.grid
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-link(style="color: #e25440;")
|
||||
i.fa.fa-link
|
||||
| Connected hosts
|
||||
span.quick-edit(tooltip="Reconnect all hosts", ng-click="reconnectAllHosts()")
|
||||
i.fa.fa-plus-square.fa-fw
|
||||
@@ -173,31 +212,33 @@
|
||||
table.table.table-hover
|
||||
th Name
|
||||
th Status
|
||||
tr(ng-repeat="PBD in SR.$PBDs | resolve", xo-sref="hosts_view({id: (PBD.host | resolve).UUID})")
|
||||
tr(ng-repeat="PBD in SR.$PBDs | resolve", xo-sref="hosts_view({id: (PBD.host | resolve).id})")
|
||||
td {{(PBD.host | resolve).name_label}}
|
||||
td(ng-if="PBD.attached")
|
||||
span.label.label-success Connected
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(xo-click="disconnectPBD(PBD.UUID)")
|
||||
i.fa.fa-unlink.fa-lg(tooltip="Disconnect to this host")
|
||||
a(xo-click="disconnectPBD(PBD.id)")
|
||||
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
|
||||
a(xo-click="connectPBD(PBD.UUID)")
|
||||
a(xo-click="connectPBD(PBD.id)")
|
||||
i.fa.fa-link.fa-lg(tooltip="Reconnect to this host")
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-comments(style="color: #e25440;")
|
||||
i.fa.fa-comments
|
||||
| Logs
|
||||
.panel-body
|
||||
p.center(ng-if="!SR.messages.length") No recent logs
|
||||
table.table.table-hover(ng-if="SR.messages.length")
|
||||
p.center(ng-if="SR.messages | isEmpty") No recent logs
|
||||
table.table.table-hover(ng-if="SR.messages | isNotEmpty")
|
||||
th.col-md-1 Date
|
||||
th.col-md-1 Name
|
||||
tr(ng-repeat="message in SR.messages | resolve | orderBy:'-time' track by message.UUID")
|
||||
tr(ng-repeat="message in SR.messages | map | orderBy:'-time' | slice:(5*(currentLogPage-1)):(5*currentLogPage) track by message.id")
|
||||
td {{message.time*1e3 | date:"medium"}}
|
||||
td
|
||||
| {{message.name}}
|
||||
a.quick-remove(tooltip="Remove log")
|
||||
i.fa.fa-trash-o.fa-fw
|
||||
.center(ng-if = '(SR.messages | count) > 5 || currentLogPage > 1')
|
||||
pagination(boundary-links="true", total-items="SR.messages | count", ng-model="$parent.currentLogPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
|
||||
//- /Hosts.
|
||||
|
||||
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
|
||||