488 Commits

Author SHA1 Message Date
Manuel Romero
9ee489ef5a back to how it was 2024-02-26 16:12:30 +01:00
Manuel Romero
c2f26ae478 cookie secure 2024-02-26 15:59:56 +01:00
Manuel Romero
19e94b031a ahora si 2024-02-26 15:39:30 +01:00
Manuel Romero
7843a26962 dev 2024-02-26 15:22:06 +01:00
Manuel Romero
7040d5d030 session storage 2024-02-26 15:21:40 +01:00
Manuel Romero
66111af16f no ahora si que si 2024-02-26 11:53:33 +01:00
Manuel Romero
1eae012704 Ahora si 2024-02-26 11:47:35 +01:00
Manuel Romero
6f7db33e89 Some UI improvements 2024-02-26 11:45:20 +01:00
Manuel Romero
af323e1f91 Test cookie name 2024-02-26 10:34:37 +01:00
Manuel Romero
b85d5d1a2d Merge 2024-02-23 13:44:33 +01:00
Manuel Romero
6c6e1c27b1 venga 2024-02-23 13:29:07 +01:00
Manuel Romero
08714427b3 Merge branch 'master' into dev 2024-02-23 13:26:51 +01:00
Manuel Romero
1aabf5dd45 error page 2024-02-23 13:24:40 +01:00
Manuel Romero
03ed7b74db testing stuff 2024-02-23 13:15:30 +01:00
Manuel Romero
036ab4077c redirect 2024-02-22 15:31:29 +01:00
Manuel Romero
1888a447d7 Merge branch 'dev' 2024-02-22 14:46:56 +01:00
Manuel Romero
4e99e55373 prod 2024-02-22 14:46:08 +01:00
Manuel Romero
a31d3324ad revision navbar 2024-02-22 14:25:01 +01:00
Manuel Romero
092726c3f9 app link update 2024-02-22 11:58:55 +01:00
mjromper
630a35d528 Update index.html 2024-02-21 19:51:09 +00:00
Manuel Romero
f82c844f6b fox 2024-02-21 17:44:36 +01:00
Manuel Romero
24f9c51559 Buttons for guacamole 2024-02-21 17:22:29 +01:00
Manuel Romero
3ceffc97fb fix app used 2024-02-20 13:55:31 +01:00
Manuel Romero
dcc48ae5d3 stuff 2024-02-20 13:24:17 +01:00
Manuel Romero
b03fc86168 Merge branch 'dev' 2024-02-20 11:01:18 +01:00
Manuel Romero
0784f7bdfb No oid 2024-02-20 10:58:31 +01:00
Manuel Romero
71d26dbb9a adding sub to table user 2024-02-19 16:17:57 +01:00
Manuel Romero
dd4487edcd final 2024-02-19 16:10:43 +01:00
Manuel Romero
f805f78ef3 fix4 2024-02-19 16:04:39 +01:00
Manuel Romero
73206b556d fix3 2024-02-19 15:58:32 +01:00
Manuel Romero
397f3c1251 fix 2024-02-19 15:55:22 +01:00
Manuel Romero
d5d7a128d2 OKTA login 2024-02-19 15:37:32 +01:00
Manuel Romero
1f1e9f5b21 Prod 2024-02-13 16:42:28 +01:00
Manuel Romero
d262735c4e dev 2024-02-13 16:19:01 +01:00
Manuel Romero
ecac686e25 guacamole link 2024-02-13 16:18:28 +01:00
Manuel Romero
f1c9a6e87e cosmetics 2024-01-26 13:29:41 +01:00
Manuel Romero
fcba362cc0 no jwt,, just oauth with qlikcloud 2024-01-22 16:24:43 +01:00
Manuel Romero
c3d23d7415 no jwt, using qlikcliud oauth 2024-01-22 15:55:58 +01:00
Manuel Romero
84303d0632 footer 2024-01-22 13:13:20 +01:00
Manuel Romero
44b6a6d658 footer 2024-01-22 13:12:44 +01:00
Manuel Romero
4e90027630 merge 2024-01-22 12:55:09 +01:00
Manuel Romero
b207b9b3c4 New look 2024-01-22 12:54:10 +01:00
Manuel Romero
213867ed77 Merge branch 'master' into dev 2024-01-22 11:52:59 +01:00
Manuel Romero
4412216b6c white stuff 2024-01-22 11:51:56 +01:00
Manuel Romero
70014e9021 new favicon 2024-01-22 11:15:23 +01:00
Manuel Romero
a3704bd4f0 new favicon 2024-01-22 11:13:16 +01:00
Manuel Romero
ca0b30f37e Merge branch 'master' into dev 2024-01-22 10:56:43 +01:00
Manuel Romero
7e9ec60b8e server 2024-01-22 10:56:30 +01:00
Manuel Romero
adb4f54c54 nnew logo 2024-01-22 10:55:48 +01:00
Manuel Romero
eacba70919 fix 2023-12-14 10:34:24 +01:00
Manuel Romero
a68143af97 fix 2023-12-14 09:16:53 +01:00
Manuel Romero
a95e4e691d ei team 2023-12-13 13:54:20 +01:00
Manuel Romero
d4578bef28 start stop logs 2023-12-11 10:28:05 +01:00
Manuel Romero
ae04d5f62d Merge branch 'master' into dev 2023-12-01 11:38:25 +01:00
Manuel Romero
c3ea7ba384 fix db indentifier 2023-12-01 10:23:35 +01:00
Manuel Romero
bca55b62df Some expceptions controlled 2023-12-01 09:54:27 +01:00
Manuel Romero
36741b06ad refix 2023-11-30 16:12:21 +01:00
Manuel Romero
e7cb9b149d fix 2023-11-30 15:20:53 +01:00
Manuel Romero
8129378fc8 fix 2023-11-30 15:04:39 +01:00
Manuel Romero
f3d34321cd Merge 2023-11-30 14:01:30 +01:00
Manuel Romero
2a1525670c identifier 2023-11-30 13:58:38 +01:00
Manuel Romero
a7e36d0119 running awsrds 2023-11-30 10:54:41 +01:00
Manuel Romero
2ba046310f running awsrds 2023-11-30 10:54:04 +01:00
Manuel Romero
6ed942d954 dev 2023-11-30 10:23:02 +01:00
Manuel Romero
0af1c4d4c9 fix table 2023-11-30 10:22:28 +01:00
Manuel Romero
811220f882 Merge 2023-11-30 10:20:52 +01:00
Manuel Romero
caeef42000 Some fixes for RDS aws start stop 2023-11-30 10:17:53 +01:00
Manuel Romero
90a3cf176f better 2023-11-29 15:54:00 +01:00
Manuel Romero
44cec9cc31 aws-rdss stop sstart 2023-11-29 14:00:40 +01:00
Manuel Romero
e04cec01e6 merge 2023-11-23 16:47:02 +01:00
Manuel Romero
9df38c142e Session qs control 2023-11-23 16:09:31 +01:00
Manuel Romero
51bf4c8064 ahora vamos ya esta 2023-11-23 13:40:32 +01:00
Manuel Romero
cca7714f19 ui fix 2023-11-23 13:27:59 +01:00
Manuel Romero
fc6d9437a9 point to prod 2023-11-23 13:11:23 +01:00
Manuel Romero
537123c6aa ahora si 2023-11-23 12:55:28 +01:00
Manuel Romero
373258f402 vengaa 2023-11-23 12:42:21 +01:00
Manuel Romero
6f2a3b75d4 fix fin 2023-11-23 11:05:55 +01:00
Manuel Romero
ed4cd383df fix it 2023-11-23 10:16:22 +01:00
Manuel Romero
3cd7e1d4ae Added also none active 2023-11-22 16:54:04 +01:00
Manuel Romero
7264710bb5 session and stuff 2023-11-22 14:21:59 +01:00
Manuel Romero
004c425738 master into dev 2023-11-21 15:47:21 +01:00
Manuel Romero
cd50c47b4a request new session 2023-11-21 15:42:03 +01:00
Manuel Romero
93d240e9f2 less logs 2023-11-21 12:50:44 +01:00
Manuel Romero
01640adde7 Merge branch 'dev' 2023-11-21 12:03:33 +01:00
Manuel Romero
c3c1c76acb mongo redirect fixed 2023-11-21 12:03:18 +01:00
Manuel Romero
7e58cbc685 log 2023-11-21 11:51:53 +01:00
Manuel Romero
5e7ad89fdb Merge branch 'dev' 2023-11-21 11:42:12 +01:00
Manuel Romero
f1be3b7730 mongo redirect 2023-11-21 11:36:08 +01:00
Manuel Romero
08286e3172 dale 2023-11-21 11:30:05 +01:00
Manuel Romero
f16683a421 Merge branch 'dev' 2023-11-21 11:12:58 +01:00
Manuel Romero
ce7f97839b qmimongo and stuff 2023-11-21 11:12:45 +01:00
Manuel Romero
7e39db8dbd no cors 2023-11-21 09:41:11 +01:00
Manuel Romero
5c029d8976 prod 2023-11-20 16:48:41 +01:00
Manuel Romero
bc76634bca double server 2023-11-20 16:35:43 +01:00
Manuel Romero
fcb8365fe4 try catch 2023-11-20 16:01:43 +01:00
Manuel Romero
b2f4a41c0d vengga 2023-11-20 15:42:36 +01:00
Manuel Romero
811ca7ca70 fixxxx 2023-11-20 15:31:12 +01:00
Manuel Romero
1fcec290b6 fix 2023-11-20 15:09:25 +01:00
Manuel Romero
d9d2ac29b9 no se 2023-11-20 15:07:25 +01:00
Manuel Romero
28e3c12b23 fixxxx 2023-11-20 14:44:06 +01:00
Manuel Romero
1eff5089f2 dix 2023-11-20 14:38:23 +01:00
Manuel Romero
b0520064e8 extracted to componnent 2023-11-20 14:31:13 +01:00
Manuel Romero
b3262d9acb fix 2023-11-20 13:31:27 +01:00
Manuel Romero
272cb32e74 cost to provisions 2023-11-20 13:17:08 +01:00
Manuel Romero
577246830d cost data 2023-11-17 14:12:42 +01:00
Manuel Romero
7fed166c29 fix 2023-11-17 13:45:19 +01:00
Manuel Romero
294a3a8d2d fix 2023-11-17 13:42:04 +01:00
Manuel Romero
835d162ede qlik-embed 2023-11-17 12:23:53 +01:00
Manuel Romero
8f8c18490a fix 2023-10-10 15:38:42 +02:00
Manuel Romero
dbcdd6974b add guacamole link to outputs and email 2023-10-05 11:02:23 +02:00
Manuel Romero
61071e1d00 fix 2023-09-26 15:58:56 +02:00
Manuel Romero
1aa83a1d5b fix delete 2023-09-26 13:22:43 +02:00
Manuel Romero
f9c27e4b3d conn name 2023-09-26 12:31:20 +02:00
Manuel Romero
cb10e425d7 ssh connection 2023-09-26 12:18:30 +02:00
Manuel Romero
8827940789 fix delete 2023-09-26 12:00:34 +02:00
Manuel Romero
30e099ee2e guacamole 2023-09-26 11:58:47 +02:00
Manuel Romero
834aff2a9d talend email 2023-07-12 10:21:07 +02:00
Manuel Romero
975b84a3b2 no sort users table 2023-07-11 15:21:09 +02:00
Manuel Romero
14d1690100 fix 2023-07-05 07:48:57 +02:00
Manuel Romero
4bbd86e82f fix 2023-07-04 15:50:53 +02:00
Manuel Romero
ba30495cc8 filter emails 2023-07-04 15:40:12 +02:00
Manuel Romero
368d58070c fixes chulos 2023-06-15 13:00:28 +02:00
Manuel Romero
54f05b11c4 getUsers inactive 2023-06-15 12:23:42 +02:00
Manuel Romero
77b599ec83 fjx url 2023-06-12 14:49:55 +02:00
Manuel Romero
2cbcb9dd5d fix 2023-06-12 13:48:13 +02:00
Manuel Romero
0f0d050599 fix nextUrl 2023-06-12 13:31:36 +02:00
Manuel Romero
99d9a1a053 fix 2023-06-08 17:15:41 +02:00
Manuel Romero
1c9d5e5ce2 status session 2023-06-08 15:29:59 +02:00
Manuel Romero
56748c4478 output 2023-06-07 10:53:20 +02:00
Manuel Romero
41cdb46fc2 database link 2023-05-25 12:04:14 +02:00
Manuel Romero
515fd749e6 set pendingNextAction to null 2023-05-25 11:04:29 +02:00
Manuel Romero
2574278b68 Merge branch 'master' into dev 2023-05-19 13:27:52 +02:00
Manuel Romero
395c495130 happy 2023-05-19 13:27:35 +02:00
Manuel Romero
075815a6b5 happy 2023-05-19 13:26:47 +02:00
Manuel Romero
29c6bcad1e fix automl and error catch 2023-05-19 12:51:39 +02:00
Manuel Romero
35408b7b1f auto add automations 2023-05-19 11:53:11 +02:00
Manuel Romero
04d8d41b05 automations generation 2023-05-18 17:23:16 +02:00
Manuel Romero
7700675a76 students 2023-05-18 10:52:18 +02:00
Manuel Romero
2b57b2535d post spaces 2023-05-17 14:00:23 +02:00
Manuel Romero
ddc36dbfcf Only active users 2023-05-16 10:20:10 +02:00
Manuel Romero
2ab78335a2 adding session name 2023-05-16 09:42:43 +02:00
Manuel Romero
fed9ad156c edit user and more 2023-05-12 13:09:03 +02:00
Manuel Romero
b8e06f91ef small fix 2023-05-12 10:20:19 +02:00
Manuel Romero
de56713e96 added studetns count 2023-05-12 10:16:39 +02:00
Manuel Romero
3515158bb0 do not delete, set finshed to true 2023-05-11 12:46:11 +02:00
Manuel Romero
a5ed3edd84 cosmetic changes 2023-05-11 11:12:53 +02:00
Manuel Romero
e2d32d954e submit form 2023-05-11 10:44:38 +02:00
Manuel Romero
baf2b3776c submit form 2023-05-10 17:32:50 +02:00
Manuel Romero
f48e32230b small fix 2023-05-10 17:03:07 +02:00
Manuel Romero
3338f66c0b fix form 2023-05-10 14:56:28 +02:00
Manuel Romero
8d34b3d4af more stuff 2023-05-10 14:52:11 +02:00
Manuel Romero
023a3fa89d more trainning stuff 2023-05-10 13:38:22 +02:00
Manuel Romero
ad6d7a1082 new training stuff 2023-05-09 16:23:45 +02:00
Manuel Romero
8e8a23454a fix force externnal access 2023-05-02 12:38:35 +02:00
Manuel Romero
2cea8c6c8a remove logs 2023-03-30 12:43:22 +02:00
Manuel Romero
ce795f99f0 fix 2023-03-30 12:27:10 +02:00
Manuel Romero
fcda1da75b adding jobtitle to ui 2023-03-30 12:15:19 +02:00
Manuel Romero
199d11d22e Adding mail and Jobtitle 2023-03-30 12:03:13 +02:00
Manuel Romero
dd819b17ce fix logs destroy 2023-03-30 09:43:54 +02:00
Manuel Romero
2b93f3f986 fix logs destroy 2023-03-30 09:41:41 +02:00
Manuel Romero
1cf448c823 fix 2023-03-28 11:17:35 +02:00
Manuel Romero
6618b80964 adding active to users 2023-03-24 16:33:12 +01:00
Manuel Romero
3215f559d4 active users 2023-03-24 16:14:32 +01:00
Manuel Romero
cb634636f5 added active field for users 2023-03-24 14:14:44 +01:00
Manuel Romero
c9b8ed3e58 fix error 2023-03-23 16:27:51 +01:00
Manuel Romero
6e6233232c update error provisions 2023-03-14 11:32:05 +01:00
Manuel Romero
642ac66b9f let users see events and shares for a provisioh 2023-03-02 16:36:49 +01:00
Manuel Romero
319ab11274 fix 2023-02-23 12:18:27 +01:00
Manuel Romero
5e928f2e67 fix ui reload provisions 2023-02-10 10:09:29 +01:00
Manuel Romero
26b65b5752 Some events logs improvements 2023-02-10 10:00:11 +01:00
Manuel Romero
b677044fa6 better apis 2023-02-07 17:10:53 +01:00
Manuel Romero
bafcd504f2 stop-auto event 2023-02-07 13:23:10 +01:00
Manuel Romero
f41a9f76c1 mega fix 2023-02-07 12:44:51 +01:00
Manuel Romero
37c8f97493 Simple event types 2023-02-07 12:13:18 +01:00
Manuel Romero
9aac529d70 fix 2023-02-06 16:59:54 +01:00
Manuel Romero
3d59581432 provision admin query update 2023-02-06 16:43:48 +01:00
Manuel Romero
469e1244cc update shares 2023-02-06 15:50:10 +01:00
Manuel Romero
1735edf494 no edit events after owner changes 2023-02-06 15:36:56 +01:00
Manuel Romero
b02b258d77 fixes 2023-02-06 14:51:05 +01:00
Manuel Romero
222056574c Adding events owner 2023-02-06 13:44:57 +01:00
Manuel Romero
6748b3518f more fixes 2023-02-01 17:04:36 +01:00
Manuel Romero
29ccb721f5 fix ui 2023-02-01 16:35:00 +01:00
Manuel Romero
26706a154b fix 2022-11-30 09:38:01 +01:00
Manuel Romero
131be49e5f fix 2022-11-30 09:35:20 +01:00
Manuel Romero
b7e57ef49c better outputs in email 2022-11-29 15:47:10 +01:00
Manuel Romero
6f57285db0 fix 2022-11-22 11:57:38 +01:00
Manuel Romero
6694637fc1 fix ui 2022-11-22 10:52:48 +01:00
Manuel Romero
e84ffa16d0 fix ui 2022-11-22 10:47:26 +01:00
Manuel Romero
10b492b3d2 better ui finished 2022-11-22 10:37:50 +01:00
Manuel Romero
ab0866d36a better ui 2022-11-21 17:13:56 +01:00
Manuel Romero
deb29c0224 fix 2022-11-21 15:26:55 +01:00
Manuel Romero
c8944543cc provision page 2022-11-21 14:50:04 +01:00
Manuel Romero
6ae1472d6c fix 2022-11-18 15:53:42 +01:00
Manuel Romero
9c2c0393e1 ui fixes 2022-11-18 14:21:28 +01:00
Manuel Romero
062e3c89fd ui fixes 2022-11-18 14:04:59 +01:00
Manuel Romero
c08db405f4 ui fixes 2022-11-18 13:49:47 +01:00
Manuel Romero
e7ebfa7f4c ui fixes 2022-11-18 13:29:49 +01:00
Manuel Romero
46a908fb3c ui fixes 2022-11-18 12:56:13 +01:00
Manuel Romero
36a9fbed56 ui fixes 2022-11-18 12:51:31 +01:00
Manuel Romero
63965ec26e ui fixes 2022-11-18 12:42:27 +01:00
Manuel Romero
8b09158148 ui fixes 2022-11-18 12:13:28 +01:00
Manuel Romero
fc10564ab5 fixes 2022-11-17 13:58:22 +01:00
Manuel Romero
4c5b65aa38 better UI 2022-11-17 13:07:10 +01:00
Manuel Romero
94e392d3ef input text 2022-11-17 12:40:47 +01:00
Manuel Romero
ebc7924049 schedule fix 2022-11-14 10:55:25 +01:00
Manuel Romero
689b789864 scripts ref to project path 2022-11-10 12:01:10 +01:00
Manuel Romero
c3b0470c20 scripts ref to project path 2022-11-10 11:23:19 +01:00
Manuel Romero
3501cf72aa fix 2022-11-09 16:44:50 +01:00
Manuel Romero
3464bfa85a distribute snapshots endpoints 2022-11-09 16:24:33 +01:00
Manuel Romero
671b1b79ed distribute snapshots endpoints 2022-11-09 16:07:07 +01:00
Manuel Romero
b823e82411 stop synapse 2022-10-21 14:51:34 +02:00
Manuel Romero
25c1462848 some fixes in ui 2022-10-21 14:05:02 +02:00
Manuel Romero
fb1308556c fix 2022-10-21 13:06:19 +02:00
Manuel Romero
d85f521b53 fix 2022-10-21 12:59:52 +02:00
Manuel Romero
9105755afe synapse start stop ui 2022-10-21 12:42:38 +02:00
Manuel Romero
b3b157641e fix docker image az cli 2022-10-21 11:58:39 +02:00
Manuel Romero
55678c9e91 fix docker image az cli 2022-10-21 11:53:44 +02:00
Manuel Romero
7e173a2a5f resume synapse 2022-10-21 11:41:42 +02:00
Manuel Romero
41b0001886 pause and stop synapse 2022-10-21 11:29:25 +02:00
Manuel Romero
75efcbc3a1 fix stuff 2022-10-13 13:06:20 +02:00
Manuel Romero
3c3641040c test 2022-10-07 12:00:16 +02:00
Manuel Romero
fce686972c injecting aws credentials 2022-10-07 11:42:17 +02:00
Manuel Romero
d28a7b5f26 terraform image for each scenario 2022-10-04 16:10:31 +02:00
Manuel Romero
9372e47589 fix 2022-09-29 10:08:03 +02:00
Manuel Romero
b5af6c0959 fix cli 2022-09-28 11:17:01 +02:00
Manuel Romero
7ec3c6b1c5 fixed cli conditions 2022-09-19 12:49:31 +02:00
Manuel Romero
12f0e627a1 fix2 2022-09-19 11:41:56 +02:00
Manuel Romero
ec6149c127 fixes 2022-09-19 11:24:10 +02:00
Manuel Romero
06050b2796 accomodation for old vmImage POST 2022-09-19 11:10:51 +02:00
Manuel Romero
704319b27a fix cli 2022-09-16 14:13:18 +02:00
Manuel Romero
c868e7000e update versions 2022-09-16 13:46:47 +02:00
Manuel Romero
85f9c826cf envs 2022-09-16 11:39:50 +02:00
Manuel Romero
7d65d736bc fix env 2022-09-16 11:19:25 +02:00
Manuel Romero
2208e362c2 env variables through env.js 2022-09-16 11:17:10 +02:00
Manuel Romero
ac1933d881 some fixes on names 2022-09-16 10:42:23 +02:00
Manuel Romero
b95570b769 more 2022-09-15 14:46:52 +02:00
Manuel Romero
b52b5a0df7 changing versions by values 2022-09-15 14:35:17 +02:00
Manuel Romero
1cedcb791d vmImage by options 2022-09-09 15:54:45 +02:00
Manuel Romero
a20e677954 check is a Qlik email 2022-09-09 09:50:22 +02:00
Manuel Romero
2ea8877a76 Cost text 2022-09-02 13:19:02 +02:00
Manuel Romero
f272b030fb Merge branch 'dev' 2022-09-01 10:30:49 +02:00
Manuel Romero
0de582a750 interceptor 301 2022-09-01 10:11:49 +02:00
Manuel Romero
df6d28fd37 401 error mashup 2022-09-01 09:48:16 +02:00
Manuel Romero
d3149ad1b3 fixeds 2022-06-30 15:17:42 +02:00
Manuel Romero
e35873ab54 fixeds 2022-06-30 15:12:07 +02:00
Manuel Romero
7071eb6907 fix 2022-06-30 12:36:37 +02:00
Manuel Romero
852b68eaba fix 2022-06-30 12:00:55 +02:00
Manuel Romero
9063951aa7 fix 2022-06-30 11:43:19 +02:00
Manuel Romero
f69abbcda9 usin angular 2022-06-30 11:13:18 +02:00
Manuel Romero
45f936ee7c usin angular 2022-06-30 11:12:57 +02:00
Manuel Romero
39663ba9ad usin angular 2022-06-30 11:08:31 +02:00
Manuel Romero
58d50b2552 new version client 2022-06-30 10:32:18 +02:00
Manuel Romero
32a1abce22 revert config 2022-06-30 10:26:16 +02:00
Manuel Romero
30388bf064 New cost analysis stuff 2022-06-30 10:25:28 +02:00
Manuel Romero
a4bc5e4e7b adding subject to users 2022-06-29 10:30:49 +02:00
Manuel Romero
1e450bf019 new certs for barracuda 2022-04-26 11:14:26 +02:00
Manuel Romero
e65215448a new version worker 2022-02-08 17:03:20 +01:00
Manuel Romero
fbd2d8d829 Merge branch 'dev' 2022-02-08 17:02:47 +01:00
Manuel Romero
d531463e67 remove scenario from tf 2022-02-08 15:55:29 +01:00
Manuel Romero
28b792fe76 refactor tf 2022-02-08 15:52:27 +01:00
Manuel Romero
c19e4ca60a refactor tf 2022-02-08 15:51:19 +01:00
Manuel Romero
9749e98aee Merge branch 'dev' 2022-02-03 12:56:20 +01:00
Manuel Romero
b49895045d user_email and user.oid as TF_VARs 2022-02-03 11:54:42 +01:00
Manuel Romero
ca21757199 Merge branch 'dev' 2022-02-01 13:14:32 +01:00
Manuel Romero
2e98be3fca gitbranch to tf execution as env variable 2022-02-01 13:14:11 +01:00
Manuel Romero
3b86ac7777 gitbranch to tf execution as env variable 2022-02-01 12:36:43 +01:00
Manuel Romero
295d1ffb30 Env branch to docker tf 2022-02-01 12:08:23 +01:00
Manuel Romero
26eb86193c Env branch to docker tf 2022-02-01 12:06:43 +01:00
Manuel Romero
9e2cf17ab7 sending email for shared provisions 2022-01-28 16:46:26 +01:00
Manuel Romero
4600969f16 no redshift 90 days expire 2022-01-27 11:42:04 +01:00
Manuel Romero
332df80b1d no redshift 90 days expire 2022-01-27 11:41:34 +01:00
Manuel Romero
0a534eed37 using const 2022-01-25 13:08:03 +01:00
Manuel Romero
900a6abeb7 fix 2022-01-20 10:08:25 +01:00
Manuel Romero
9b81646219 fix multiple warnngs s3 bucket 2022-01-20 10:06:13 +01:00
Manuel Romero
ca8d26c119 Merge branch 'dev' 2022-01-20 10:03:53 +01:00
Manuel Romero
245bd0d90e no needed config 2022-01-11 11:39:38 +01:00
Manuel Romero
67ec379e1d docker image at provision 2022-01-11 11:38:24 +01:00
Manuel Romero
3e73e1a561 fix 2 2022-01-07 16:19:18 +01:00
Manuel Romero
55ccfab66c fis 2022-01-07 15:55:33 +01:00
Manuel Romero
23ae12bf2e test others 2022-01-07 14:53:06 +01:00
Manuel Romero
e8de86da66 fix shared 2021-12-21 09:04:09 +01:00
Manuel Romero
13a297d58d fix 2021-12-20 14:13:01 +01:00
Manuel Romero
f2aa774acd runForever 2021-12-20 14:04:14 +01:00
Manuel Romero
721b478d00 fix admin 2021-12-20 13:51:10 +01:00
Manuel Romero
4ee672ea3b fix 2021-11-30 12:17:21 +01:00
Manuel Romero
acd210da93 Share in user 2021-11-30 11:43:03 +01:00
Manuel Romero
4c853a7b20 fix getUsers 2021-11-29 10:17:13 +01:00
Manuel Romero
51f20a9e2d now one more fix 2021-11-26 14:29:53 +01:00
Manuel Romero
18a17ea761 fix 2021-11-26 13:48:03 +01:00
Manuel Romero
60c5bc543a small fix 2021-11-26 13:40:52 +01:00
Manuel Romero
0aa1dc0c74 do not delete shares 2021-11-26 13:39:13 +01:00
Manuel Romero
f5b85b25ed small fix 2021-11-26 13:10:48 +01:00
Manuel Romero
4245694b10 final 2021-11-26 12:29:06 +01:00
Manuel Romero
24c99df65a new version 2021-11-26 11:12:48 +01:00
Manuel Romero
6283dddb22 Share 2021-11-26 11:08:45 +01:00
Manuel Romero
c3b8bd119f more stuff for sharing provisions 2021-11-25 18:02:37 +01:00
Manuel Romero
2e31fe279c more stuff 2021-11-25 14:12:29 +01:00
Manuel Romero
899fd2fda7 share provisions with others 2021-11-25 12:37:42 +01:00
Manuel Romero
dc116c9f7f fix destroy 2021-11-24 12:06:32 +01:00
Manuel Romero
455cc3e75d fix 2021-11-24 10:34:07 +01:00
Manuel Romero
16f574f744 child being destroyed check 2021-11-24 09:53:28 +01:00
Manuel Romero
1486e2fd21 check if destroyed or destroying 2021-11-24 08:35:06 +01:00
Manuel Romero
c0349f440e fix open in Azure 2021-11-19 17:47:55 +01:00
Manuel Romero
1a99589c8b Del temp apikeys for QDI 2021-11-10 10:01:18 +01:00
Manuel Romero
b54a590f51 fix 2021-11-09 16:10:50 +01:00
Manuel Romero
eafd78f91c fix 2021-11-09 16:04:28 +01:00
Manuel Romero
243c65c64b filter all users 2021-11-09 15:36:35 +01:00
Manuel Romero
672ba4194a revert 2021-11-09 15:19:11 +01:00
Manuel Romero
9f539bd96a prep of apikey for QDI 2021-11-09 14:56:10 +01:00
Manuel Romero
55b9f0c032 improve admin prov query 2021-11-09 11:46:53 +01:00
Manuel Romero
00dcb33872 fix alerts 2021-11-08 18:08:55 +01:00
Manuel Romero
429838cfc5 new version 2021-11-08 14:13:10 +01:00
Manuel Romero
3a2d6eb0b6 fix ui 2021-11-08 14:12:06 +01:00
Manuel Romero
4290406c19 fix 2021-11-08 13:35:02 +01:00
Manuel Romero
9057f58342 ui fixes 2021-11-08 13:20:30 +01:00
Manuel Romero
6d03a4f6ed Fixes ui 2021-11-08 13:02:35 +01:00
Manuel Romero
5d914c890f new laf 2021-11-05 18:03:54 +01:00
Manuel Romero
b1286a1b2b new look and feel 2021-11-05 17:59:57 +01:00
Manuel Romero
a5b5f2a8ca fix 2021-11-05 13:53:22 +01:00
Manuel Romero
8bb7856102 destroy also children provisions 2021-11-05 13:08:00 +01:00
Manuel Romero
8280b32872 user_oid 2021-10-28 11:27:55 +02:00
Manuel Romero
39ab3fe6dd Merge branch 'dev' 2021-10-26 17:33:08 +02:00
Manuel Romero
fbe30b740c synapse user 2021-10-26 14:04:16 +02:00
Manuel Romero
ab5ab80765 Test watchtower 2021-10-06 17:48:02 +02:00
Manuel Romero
180a20c8d6 fix 2021-10-05 10:02:39 +02:00
Manuel Romero
91d7017b03 vm1 stuff 2021-10-04 17:13:08 +02:00
Manuel Romero
5b6cb73b09 vm1 stuff 2021-10-04 16:54:48 +02:00
Manuel Romero
2003a039f2 new version available 2021-08-03 16:19:42 +02:00
Manuel Romero
96b1302520 fix 2021-07-13 10:38:03 +02:00
Manuel Romero
891acb714e Go to user 2021-07-13 10:21:33 +02:00
Manuel Romero
7cf4e7b5e7 adding try catch cli 2021-07-12 14:12:09 +02:00
Manuel Romero
0c78992958 change to promise 2021-07-12 13:02:36 +02:00
Manuel Romero
ed44eb7fef change to promise 2021-07-12 12:59:53 +02:00
Manuel Romero
8ed6612686 fix3 2021-07-09 13:33:58 +02:00
Manuel Romero
793b8f8739 fix2 2021-07-09 13:15:30 +02:00
Manuel Romero
a9b3b57414 fix 2021-07-09 12:48:25 +02:00
Manuel Romero
69b92a64bc awscli 2021-07-09 12:32:29 +02:00
Manuel Romero
f84ecd7212 new barracuda api version 2021-07-09 09:44:04 +02:00
Manuel Romero
5b6bb90119 simple barracuda 2021-06-25 12:00:02 +02:00
Manuel Romero
d5637065c2 fixes for barracuda 2021-06-25 11:30:57 +02:00
Manuel Romero
6d322c3cc2 fix 2021-06-10 10:55:58 +02:00
Manuel Romero
e0ba340ca1 info times 2021-06-10 10:39:01 +02:00
Manuel Romero
79ec7af773 fixed readme 2021-06-09 16:13:01 +02:00
Manuel Romero
752a3329be periods per scenario 2021-06-09 15:46:22 +02:00
Manuel Romero
36a246504f scenario model allowedInnactiveHours 2021-06-09 13:43:34 +02:00
Manuel Romero
b2a4b41023 n/a for non vm provisions 2021-06-04 09:39:40 +02:00
Manuel Romero
852c943f99 Logevent 2021-06-01 15:12:17 +02:00
Manuel Romero
96e33ca765 azurewebhook 2021-06-01 11:36:08 +02:00
Manuel Romero
bae80ec847 azurewebhook 2021-06-01 10:49:52 +02:00
Manuel Romero
ce55788a26 Delete BarracudaApp first 2021-05-27 11:54:14 +02:00
Manuel Romero
17e128099e new barracuda api 0.0.8 2021-05-26 17:40:57 +02:00
Manuel Romero
17f391ffbd new barracuda api version 2021-05-26 17:35:17 +02:00
Manuel Romero
23bf5f58f6 Only cli for provision with VMs 2021-05-07 13:43:03 +02:00
Manuel Romero
b36616f28a fix 2021-04-30 16:51:58 +02:00
Manuel Romero
5e266a4461 ui fixes 2021-04-30 16:23:04 +02:00
Manuel Romero
0dd199d5f8 no vmImage 2021-04-30 15:53:19 +02:00
Manuel Romero
0f020e96a7 fix 2021-04-29 13:31:37 +02:00
Manuel Romero
0dcba31636 fix 2021-04-28 14:06:48 +02:00
Manuel Romero
04221eb03a using lock icon 2021-04-28 10:06:35 +02:00
Manuel Romero
c12039d354 info class 2021-04-23 13:51:06 +02:00
Manuel Romero
c5dcc547a9 QDC barracuda 2021-04-23 13:38:12 +02:00
Manuel Romero
5c3e10f9df Some buttons 2021-04-20 16:22:37 +02:00
Manuel Romero
e1848da829 Adding user dashbooard 2021-04-20 14:40:16 +02:00
Manuel Romero
a4ae065d8b ttl to 60 for cnames 2021-04-19 13:00:06 +02:00
Manuel Romero
8e0ffd72fa create barracuda app at provision 2021-04-16 15:23:08 +02:00
Manuel Romero
fafe697af5 create barracuda app at provision 2021-04-16 15:21:58 +02:00
Manuel Romero
2cf319175d fix 2021-04-16 14:39:50 +02:00
Manuel Romero
643cb9775d change region barracuda 2021-04-16 13:21:16 +02:00
Manuel Romero
66085e49c5 getting barracuda detauls in one 2021-04-16 11:33:50 +02:00
Manuel Romero
fd85d65aeb barracuda app status 2021-04-16 11:12:02 +02:00
Manuel Romero
24a3573f73 logs 2021-04-16 10:09:48 +02:00
Manuel Romero
048fc6d89d gitbranch for scenarios 2021-04-16 09:38:56 +02:00
Manuel Romero
bf20f4efd4 gitbranch for scenarios 2021-04-16 09:32:47 +02:00
Manuel Romero
2fd05a2b54 adding git 2021-04-15 14:43:05 +02:00
Manuel Romero
03ce50c255 Some logs 2021-04-15 14:03:53 +02:00
Manuel Romero
171f552571 Some logs 2021-04-15 14:01:12 +02:00
Manuel Romero
1843907b42 Testing delete barracuda at destroy provision 2021-04-15 13:06:32 +02:00
Manuel Romero
d88a6f2359 fix 2021-04-14 13:05:03 +02:00
Manuel Romero
d273bc937e delete barracuda endpoint 2021-04-13 15:58:30 +02:00
Manuel Romero
6777d2f0d8 delete barracuda endpoint 2021-04-13 15:56:40 +02:00
Manuel Romero
3d2506639d Adding creation of DNS record 2021-04-13 15:41:09 +02:00
Manuel Romero
a9806bd94e Changed dockerfile to install git 2021-04-13 14:50:20 +02:00
Manuel Romero
783642f083 barracuda email password 2021-04-13 13:37:16 +02:00
Manuel Romero
2a7fec03d7 Barrcuda API endpoint 2021-04-13 13:32:13 +02:00
Manuel Romero
aed18cccfa No logs tf output 2021-04-08 10:46:06 +02:00
Manuel Romero
0f12ee6649 testing tags are the same 2021-04-07 11:07:02 +02:00
Manuel Romero
fbc727a66e allow userId as 'me' 2021-03-31 15:29:37 +02:00
Manuel Romero
897fa2a631 trycatch error setting tags 2021-03-31 10:33:41 +02:00
Manuel Romero
43a428592e Merge branch 'dev' 2021-03-25 15:53:32 +01:00
Manuel Romero
c3796b478f Sort updated on Scenarios 2021-03-25 15:53:23 +01:00
Manuel Romero
e12f60516b sort first by updated 2021-03-25 15:31:28 +01:00
Manuel Romero
84fe322c96 sort updated 2021-03-25 15:00:57 +01:00
Manuel Romero
84263b08dd Merge branch 'dev' 2021-03-25 12:21:31 +01:00
Manuel Romero
895e069326 Using new node version 15.12 2021-03-25 12:21:07 +01:00
Manuel Romero
da4c940055 fix azurecli module 2021-03-25 12:02:30 +01:00
Manuel Romero
f9e608c06c fix 2021-03-17 15:31:00 +01:00
Manuel Romero
15e8c12508 readme 2021-03-17 12:45:42 +01:00
Manuel Romero
1e543b1e6f Destroy on aborted for user 2021-03-17 12:34:40 +01:00
Manuel Romero
00739082f2 movedestroyed with date 2021-03-17 12:04:10 +01:00
Manuel Romero
aacf23f57b movedestroyed 2021-03-17 11:31:42 +01:00
Manuel Romero
ca037b42ef updatemany 2021-03-17 09:55:56 +01:00
Manuel Romero
96300e1e99 fix axios final 2021-03-16 17:49:35 +01:00
Manuel Romero
721fbe24b8 fix axios 2021-03-16 17:45:34 +01:00
Manuel Romero
b20a6fe858 fix axios 2021-03-16 17:30:53 +01:00
Manuel Romero
658bd6a131 abort from UI 2021-03-16 17:20:33 +01:00
Manuel Romero
be5c9ef85e fix 2021-03-16 14:51:17 +01:00
Manuel Romero
5cc6f7a3c6 Test queue stop container 2021-03-16 13:30:24 +01:00
Manuel Romero
5fdd26306a abort job 2021-03-16 11:24:06 +01:00
Manuel Romero
0e74d8873f disable cache stats 2021-03-10 09:18:33 +01:00
Manuel Romero
3f8e9290de cached stats 2021-03-09 09:32:33 +01:00
Manuel Romero
034dde5e1d fix 2021-03-05 18:06:36 +01:00
Manuel Romero
d0e274cd5f fix 2021-03-05 18:00:11 +01:00
Manuel Romero
b3de18bbda fix 2021-03-05 17:55:47 +01:00
Manuel Romero
e02ac30cde fix 2021-03-05 17:44:00 +01:00
Manuel Romero
49b444cfc8 fix 2021-03-05 17:32:50 +01:00
Manuel Romero
1ab9055fc5 request up to 6 2021-03-05 17:30:00 +01:00
Manuel Romero
fd8e542467 adding page vms 2021-03-05 17:08:10 +01:00
Manuel Romero
7699978f39 Added nextlink 2021-03-05 16:48:32 +01:00
Manuel Romero
902db15c17 using azurerm new lib 2021-03-05 15:14:14 +01:00
Manuel Romero
49bd59df62 vms stats 2021-03-05 13:50:13 +01:00
Manuel Romero
b7e44f436c separate vms endpoints 2021-03-05 12:25:16 +01:00
Manuel Romero
aa26928716 stats vms 2021-03-05 12:22:41 +01:00
Manuel Romero
8509a520cf Adding stats endpoint 2021-03-05 11:36:37 +01:00
Manuel Romero
0037634b70 New topbar logo 2021-03-03 16:56:23 +01:00
Manuel Romero
6dfab50871 fix assign new users on events 2021-03-03 14:54:51 +01:00
Manuel Romero
fcc46cdb1a fix check 2021-03-01 19:41:14 +01:00
Manuel Romero
06c4fae9c2 fix check 2021-03-01 19:35:36 +01:00
Manuel Romero
8b18a65303 ffix 2021-02-28 20:28:28 +01:00
Manuel Romero
e4ad1dde54 Better events logs 2021-02-28 20:26:48 +01:00
Manuel Romero
a2b82501aa Changed onschedule timer algorithm 2021-02-28 18:14:08 +01:00
Manuel Romero
ad0c4cbbd1 schedule log event 2021-02-26 12:35:51 +01:00
Manuel Romero
1a638c16bf fix events 2021-02-26 11:52:24 +01:00
Manuel Romero
7dd9fc79c2 show events 2021-02-25 20:03:00 +01:00
Manuel Romero
d492c5e4a8 more events 2021-02-25 18:18:24 +01:00
Manuel Romero
c2a14e5f79 Event collection 2021-02-25 18:10:25 +01:00
Manuel Romero
c888c068b4 fix nootifi 2021-02-25 17:23:05 +01:00
Manuel Romero
7b0bacc990 notificates from divvy and user api 2021-02-25 16:49:47 +01:00
Manuel Romero
4d66c40c07 onscheduled running calculation 2021-02-25 12:39:20 +01:00
Manuel Romero
8a86130fe6 fix 2021-02-25 09:37:18 +01:00
Manuel Romero
b7db4a5f00 fix 2021-02-24 15:00:51 +01:00
Manuel Romero
f3b9869d4f fix 2021-02-24 13:00:23 +01:00
Manuel Romero
0801f84b10 queues logs 2021-02-09 13:56:20 +01:00
Manuel Romero
9dd091f982 pipeline status 2021-02-09 12:05:26 +01:00
Manuel Romero
190967a889 Using qlik smpt sender 2021-02-09 10:42:33 +01:00
Manuel Romero
510254922b adding axios 2021-02-05 17:43:03 +01:00
Manuel Romero
91da6199b3 Merge branch 'dev' 2021-02-05 17:16:32 +01:00
Manuel Romero
9374a60e2a send message with BAM 2021-02-05 17:16:25 +01:00
Manuel Romero
0e1817947e fix 2021-02-05 17:01:23 +01:00
Manuel Romero
870ad47cbc fix 2021-02-05 16:57:24 +01:00
Manuel Romero
4304c56685 fix 2021-02-05 16:36:09 +01:00
Manuel Romero
b7dfee506f Send with BAM 2021-02-05 16:31:49 +01:00
Manuel Romero
022b414766 fix 2021-02-05 14:44:08 +01:00
Manuel Romero
05143c78c0 fix 2021-02-05 13:15:13 +01:00
Manuel Romero
04ed3a22ef test email 2021-02-05 13:13:48 +01:00
Manuel Romero
d3f0513224 worker 2021-02-05 11:28:49 +01:00
Manuel Romero
d5efa608bc warning error cli 2020-12-04 13:11:03 +01:00
Manuel Romero
040d2187cb try catch start stop vms 2020-12-04 10:46:00 +01:00
Manuel Romero
2d254ae238 first tags then start 2020-12-03 12:18:05 +01:00
Manuel Romero
82861f2130 wait for updating tags azure 2020-12-03 11:43:21 +01:00
Manuel Romero
e0270e6925 wait for updating tags azure 2020-12-03 11:39:35 +01:00
Manuel Romero
dc2180af03 Using avatar-ui for avatarars 2020-12-01 12:40:06 +01:00
Manuel Romero
d240b06c5c support emails 2020-11-30 16:03:50 +01:00
Manuel Romero
2434d2d157 cosmetics 2020-11-26 14:12:16 +01:00
Manuel Romero
c631fb141e Adding user_email to qdi scenario 2020-11-24 09:34:45 +01:00
Manuel Romero
4e27a35e4e Adding scenario titles 2020-11-18 10:40:28 +01:00
Manuel Romero
1b458ab2df New image 2020-11-13 14:13:40 +01:00
Manuel Romero
082e98c01a Adding init password 2020-11-11 14:19:59 +01:00
Manuel Romero
c6d1f84c57 new versions 2020-11-03 13:51:03 +01:00
Manuel Romero
c3b9e8532f fix2 2020-11-03 13:36:03 +01:00
Manuel Romero
4194973884 fix 2020-11-03 12:56:00 +01:00
Manuel Romero
57a47d7794 fix 2020-11-03 12:52:04 +01:00
Manuel Romero
953930bc42 Synapse databases and others 2020-11-03 12:36:50 +01:00
Manuel Romero
35d78b4db0 Do not display error 2020-10-23 14:05:27 +02:00
Manuel Romero
b8a8b8bace error passport image 2020-10-23 12:08:37 +02:00
Manuel Romero
e09d03f1ef Photos 2020-10-23 10:46:07 +02:00
Manuel Romero
dd2f328f45 fix sort by region 2020-10-22 15:51:24 +02:00
Manuel Romero
b3895be44a Set url for sent emails 2020-10-22 15:39:21 +02:00
Manuel Romero
7516f46903 fix filter scenarioos 2020-10-20 21:22:46 +02:00
Manuel Romero
3c7327ed57 Allowed users 2020-10-20 11:34:20 +02:00
Manuel Romero
8e1c027038 No logs 2020-10-19 18:55:51 +02:00
Manuel Romero
f8fe33039c better ui 2020-10-19 18:51:05 +02:00
Manuel Romero
bf6ea3cd51 better ui 2020-10-19 18:43:45 +02:00
Manuel Romero
e3aeeea51f Allowed users for a scenario 2020-10-19 18:37:27 +02:00
Manuel Romero
1fba40289e Default enable if optional server 2020-10-19 11:17:04 +02:00
Manuel Romero
b32c18a349 Set default terraform image 2020-10-16 09:57:48 +02:00
Manuel Romero
9fc6bdd5a3 fix 2020-10-16 09:12:31 +02:00
Manuel Romero
755fd11f1a logs terraform version 2020-10-15 18:31:19 +02:00
Manuel Romero
82984673aa New package versions 2020-10-15 17:42:29 +02:00
209 changed files with 16464 additions and 16919 deletions

2
.gitignore vendored
View File

@@ -48,4 +48,4 @@ secrets.json
qmi-cloud-tf-modules/
*.pfx
/photos/*

View File

@@ -1,7 +1,7 @@
# Stage 1:
FROM node:13.8-alpine AS sources
FROM node:15.12.0-alpine AS sources
RUN apk --no-cache add yarn
RUN apk --no-cache add yarn git
WORKDIR /var/www/app
@@ -12,7 +12,7 @@ ADD ./qmi-cloud-common ./qmi-cloud-common
RUN yarn install --production
# Stage 2:
FROM node:13.8-alpine AS production
FROM node:15.12.0-alpine AS production
WORKDIR /var/www/app
COPY --from=sources /var/www/app/node_modules ./node_modules
COPY --from=sources /var/www/app/package.json ./package.json

View File

@@ -1,5 +1,7 @@
# QMI Cloud
[![pipeline status](https://gitlab.com/qmi/qmi-cloud/badges/master/pipeline.svg)](https://gitlab.com/qmi/qmi-cloud/-/commits/master)
## Pre-requisites
- Docker
- Docker-Compose
@@ -7,9 +9,9 @@
```json
{
"AZURE_TENANT_ID" : "xxxxxxxx",
"AZURE_CLIENT_ID": "yyyyyyyy",
"AZURE_CLIENT_SECRET": "zzzzzzzz"
"IDENTITY_METADATA" : "xxxxxxxx",
"CLIENT_ID": "yyyyyyyy",
"CLIENT_SECRET": "zzzzzzzz"
}
```

View File

@@ -23,8 +23,10 @@
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.app.json",
"assets": [
"src/favicon.ico",
"src/assets"
"src/favicon.svg",
"src/assets",
"src/env.js",
"src/oauth-callback.html"
],
"styles": [
"node_modules/@fortawesome/fontawesome-free/scss/fontawesome.scss",
@@ -79,7 +81,8 @@
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "qmi-cloud:build"
"browserTarget": "qmi-cloud:build",
"proxyConfig": "proxy.conf.json"
},
"configurations": {
"production": {
@@ -105,7 +108,7 @@
],
"scripts": [],
"assets": [
"src/favicon.ico",
"src/favicon.svg",
"src/assets"
]
}

View File

@@ -10,7 +10,7 @@ Just an assorted selection of questions and answers.
### 2\. Are these QMI scenarios ready to go End-To-End demos?
- Not at this moment, a global environment is being built for PreSales by GEAR.
- Not at this moment, a global environment is being built for PreSales by Innovation & Excellence Team.
- Provided scenarios are meant for (See 1). QMI works by installing licensing the products. QMI completes the main setup for the scenario to properly work where possible. You may need to put custom data, Qlik Sense applications or whatever you need for your customer engagement.
* * *
@@ -117,7 +117,6 @@ Just an assorted selection of questions and answers.
### 15\. How are we managing Costs?
- Excessive use for instances, will need to be justified, every instance logged it tagged with your trigram. The usage of QMI Cloud will be made public within a Qlik Sense Application.
- GEAR is looking at estimating the price for each server that you initiate.
* * *

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

8
dist/qmi-cloud/assets/favicon.svg vendored Normal file
View File

@@ -0,0 +1,8 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M23.3002 25.1984C21.3002 26.7984 18.7002 27.6984 16.0002 27.6984C9.5002 27.7984 4.2002 22.4984 4.2002 15.9984C4.2002 13.1984 5.2002 10.5984 6.8002 8.59844L4.7002 6.39844C2.5002 8.99844 1.2002 12.2984 1.2002 15.9984C1.2002 24.1984 7.8002 30.7984 16.0002 30.7984C19.6002 30.7984 22.9002 29.4984 25.5002 27.3984L23.3002 25.1984Z"
fill="#54565A" />
<path
d="M27.9002 30.0992H32.0002L27.6002 25.2992C29.6002 22.7992 30.8002 19.4992 30.8002 15.9992C30.8002 7.79922 24.2002 1.19922 16.0002 1.19922C11.5002 1.19922 7.4002 3.29922 4.7002 6.49922L6.9002 8.69922C9.0002 5.89922 12.3002 4.19922 16.0002 4.19922C22.5002 4.19922 27.8002 9.49922 27.8002 15.9992C27.8002 19.6992 26.1002 23.0992 23.3002 25.1992L25.3002 27.1992L25.4002 27.2992L27.9002 30.0992Z"
fill="#009845" />
</svg>

After

Width:  |  Height:  |  Size: 914 B

1
dist/qmi-cloud/assets/logo.svg vendored Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="72" height="30" fill="none"><path fill="#54565A" d="M38.188.66h-2.985v28.562h2.985V.66ZM46.298 9.27h-2.974v19.953h2.974V9.27ZM46.842 3.02a2.033 2.033 0 1 0-4.014-.652 2.033 2.033 0 0 0 4.014.651ZM22.408 24.334A11.885 11.885 0 0 1 15 26.907C8.425 26.918 3.093 21.587 3.093 15c0-2.833.984-5.429 2.628-7.473L3.536 5.332A14.912 14.912 0 0 0 0 15c0 8.285 6.716 15 15 15 3.645 0 6.986-1.297 9.582-3.46l-2.174-2.206Z"/><path fill="#009845" d="M27.035 29.232h4.132l-4.456-4.856A14.951 14.951 0 0 0 30 15c0-8.284-6.716-15-15-15A14.974 14.974 0 0 0 3.535 5.332L5.72 7.527a11.878 11.878 0 0 1 9.29-4.445c6.586 0 11.917 5.332 11.917 11.918 0 3.785-1.762 7.16-4.51 9.333l2.045 2.055.086.097.022.022 2.465 2.725Z"/><path fill="#54565A" d="M67.191 9.269H63.05l-8.597 7.527L54.44.66h-2.974v28.562h2.974v-9.214l8.825 9.214h4.13L56.875 18.548l10.317-9.28ZM71.495 27.816v1.417h-.248v-.973l-.422.973h-.13l-.433-.973v.973h-.248v-1.417h.303l.443 1.039.432-1.039h.303ZM69.776 27.816v.206h-.433v1.211h-.248v-1.211h-.433v-.206h1.114Z"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
dist/qmi-cloud/assets/user1.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

5
dist/qmi-cloud/env.js vendored Normal file
View File

@@ -0,0 +1,5 @@
(function (window) {
window.__env = window.__env || {};
window.__env.disabledProvisions = false;
}(this));

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

View File

@@ -5,9 +5,29 @@
<title>QMI Cloud</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/favicon.ico">
<link rel="stylesheet" href="styles.b2e9059fb6aa834811ea.css"></head>
<link rel="icon" href="assets/favicon.svg">
<!-- Load environment variables -->
<script src="env.js"></script>
<!--
<script crossorigin="anonymous"
type="application/javascript"
src="https://cdn.jsdelivr.net/npm/@qlik/embed-web-components"
data-host="https://qmicloud.qliktech.com/qcsproxy"></script>
-->
<script crossorigin="anonymous"
type="application/javascript"
src="https://cdn.jsdelivr.net/npm/@qlik/embed-web-components"
data-host="https://innovation.us.qlikcloud.com"
data-auth-type="Oauth2"
data-client-id="21be5044bba1072c16a803a3e6e4dca0"
data-redirect-uri="https://qmicloud-dev.qliktech.com/oauth-callback.html"
data-access-token-storage="session"
data-auto-redirect="true"></script>
<link rel="stylesheet" href="styles.3b2b6672156f20378f8f.css"></head>
<body>
<app-root></app-root>
<script src="runtime.c51bd5b1c616d9ffddc1.js" defer></script><script src="polyfills-es5.6fef7e679f78bcc42760.js" nomodule defer></script><script src="polyfills.51f5cc3d1309de3a873d.js" defer></script><script src="scripts.1af868998801499c8755.js" defer></script><script src="main.5c4c2f7e4461741ce9f1.js" defer></script></body>
<script src="runtime.c51bd5b1c616d9ffddc1.js" defer></script><script src="polyfills-es5.6fef7e679f78bcc42760.js" nomodule defer></script><script src="polyfills.51f5cc3d1309de3a873d.js" defer></script><script src="scripts.1af868998801499c8755.js" defer></script><script src="main.0eb476bbc192df0d888a.js" defer></script></body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

12
dist/qmi-cloud/oauth-callback.html vendored Normal file
View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<script
crossorigin="anonymous"
type="application/javascript"
data-host="https://innovation.us.qlikcloud.com"
src="https://cdn.jsdelivr.net/npm/@qlik/embed-web-components@0/dist/oauth-callback.js"
></script>
</head>
</html>

File diff suppressed because one or more lines are too long

View File

@@ -77,6 +77,7 @@ services:
- ../qmi-cloud-provisions:/provisions
- ./logs:/logs
- ./costexport:/var/www/app/costexport
- ./photos:/var/www/app/photos
#- ./certs:/var/www/app/server/certs
depends_on:
- mongo
@@ -95,6 +96,7 @@ services:
container_name: qmi-cloud-worker
restart: on-failure
environment:
- HOSTNAME_URL=http://localhost:3000
- REDIS_URL=redis://redis
- MONGO_URI=mongodb://root:example@mongo/qmicloud?authSource=admin
- PROJECT_PATH=%PWD%
@@ -102,11 +104,11 @@ services:
- GIT_TAG=dev
- SSHPATH=/Users/aor/.ssh
- DOCKERIMAGE_AZURE_POWERSHELL=mcr.microsoft.com/azure-powershell:4.2.0-ubuntu-18.04
- DOCKERIMAGE_TERRAFORM=qlikgear/terraform:1.0.1
- DOCKERIMAGE_TERRAFORM=qlikgear/terraform:1.3.5
command: "sh -c 'npm run start:dev'"
volumes:
# -- Dev only volumes
- ./worker:/app/worker
- ./qmi-cloud-worker:/app/worker
# -------------------
- ./logs:/logs
- /var/run/docker.sock:/home/docker.sock

View File

@@ -16,7 +16,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
<link rel="shortcut icon" href="favicon.ico" />
<link rel="shortcut icon" href="favicon.svg" />
<!-- <script type="text/javascript" src="https://qdt-apps.qlik.com/qdt-components/v3/3.0.0/qdt-components.js"></script> -->
<script type="text/javascript" src="qdt-components.js"></script>
<script type="text/javascript" src="index.js"></script>

15214
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "qmi-cloud-app",
"version": "1.1.8",
"version": "5.0.1",
"scripts": {
"start": "node -r esm server/server.js",
"start:dev": "nodemon -r esm server/server.js",
@@ -27,11 +27,13 @@
"adal-angular4": "^4.0.12",
"angular-bootstrap-md": "9.0.0",
"animate.css": "^3.7.2",
"axios": "^0.21.1",
"body-parser": "^1.19.0",
"bootstrap": "^4.3.1",
"bull-arena": "^2.6.4",
"chart.js": "^2.9.3",
"connect-mongo": "^3.0.0",
"cookie": "^0.6.0",
"cookie-parser": "^1.4.4",
"core-js": "^2.5.4",
"esm": "^3.2.25",
@@ -40,20 +42,23 @@
"font-awesome": "^4.7.0",
"fs-extra": "^8.1.0",
"hammerjs": "^2.0.8",
"js-sha1": "^0.6.0",
"jsonwebtoken": "^8.5.1",
"leonardo-ui": "^1.7.1",
"moment": "^2.24.0",
"moment-timezone": "^0.5.31",
"mongoose": "^5.7.4",
"mongoose": "^6.11.1",
"ngx-markdown": "^9.0.0",
"nodemon": "^1.19.1",
"passport": "^0.4.0",
"passport-azure-ad": "^4.1.0",
"passport-openidconnect": "^0.1.2",
"qdt-components": "^2.5.5",
"qmi-cloud-common": "./qmi-cloud-common",
"rxjs": "~6.5.4",
"swagger-jsdoc": "3.5.0",
"swagger-ui-express": "4.1.3",
"uuid": "^9.0.1",
"ws": "^8.14.2",
"zone.js": "~0.10.3"
},
"devDependencies": {

0
photos/.keep Normal file
View File

9
proxy.conf.json Normal file
View File

@@ -0,0 +1,9 @@
{
"/api/*": {
"target": "http://localhost:3000/api",
"secure": false,
"logLevel": "debug",
"changeOrigin": true,
"pathRewrite": { "^/api": "" }
}
}

View File

@@ -1,7 +1,7 @@
# Stage 1: NOTE: context is actually ../
FROM node:13.8-alpine AS sources
FROM node:15.12.0-alpine AS sources
RUN apk --no-cache add yarn
RUN apk --no-cache add yarn git
WORKDIR /app
@@ -11,7 +11,7 @@ ADD ./qmi-cloud-common ../qmi-cloud-common
RUN yarn install --production
# Stage 2:
FROM node:13.8-alpine AS production
FROM node:15.12.0-alpine AS production
WORKDIR /app
COPY --from=sources /app ./

View File

@@ -1,22 +1,36 @@
# Examples
## Check Destroy
```
docker run --net=host \
-e MONGO_URI="mongodb://root:example@localhost:27017/qmicloud?authSource=admin" \
-e API_KEY="c229219ccdd72d11e8ea253fd3876d247e5f489c9c84922cabdfb0cc194d8ff398a8d8d6528d8241efc99add2207e0ec75122a1b2c5598cc340cbe6b7c3c0dbf" \
qlikgear/qmi-cloud-cli:latest -s checkdestroy -t warning -p 4 -r test
qlikgear/qmi-cloud-cli:latest -s checkdestroy -t warning -r test
```
## Init DB
```
docker run --net=host \
-e MONGO_URI="mongodb://root:example@localhost:27017/qmicloud?authSource=admin" \
qlikgear/qmi-cloud-cli:latest -s initdb
```
## Check Destroy
```
docker run --net=host -e MONGO_URI=$MONGO_URI -e API_KEY=$API_KEY qlikgear/qmi-cloud-cli:latest -s checkdestroy -t warning -p 20 -r test
docker run --net=host -e MONGO_URI=$MONGO_URI -e API_KEY=$API_KEY qlikgear/qmi-cloud-cli:latest -s checkdestroy -t warning -r test
```
## Check Stop
```
docker run --net=host -e SMTP_EMAIL_SENDER=http://172.20.16.4:9200/sendemail -e MONGO_URI=$MONGO_URI -e API_KEY=$API_KEY qlikgear/qmi-cloud-cli:latest -s checkstop -t warning -r test
```
## Check Errors
```
docker run --net=host -e MONGO_URI=$MONGO_URI qlikgear/qmi-cloud-cli:latest -s checkerror -r test
```
## Move Destroyed
```
docker run --net=host -e MONGO_URI=$MONGO_URI -v $PWD/qmi-cloud-provisions:/provisions qlikgear/qmi-cloud-cli:latest -s movedestroyed -r "2020-31-12T00:00:00.000Z"
```
docker run --net=host -e MONGO_URI=$MONGO_URI -e API_KEY=$API_KEY qlikgear/qmi-cloud-cli:latest -s checkstop -t warning -p 4 -r test
```

View File

@@ -4,12 +4,12 @@
"isWafPolicyAppGw": true,
"isExternal": true,
"isDisabled": false,
"availableProductVersions": [
"availableProductvalues": [
{
"product": "Qlik Sense",
"vmTypeDefault": "Standard_D4s_v3",
"index": "vm1",
"versions": [
"values": [
{
"name": "Qlik Sense February 2020",
"image": "/subscriptions/62ebff8f-c40b-41be-9239-252d6c0c8ad9/resourceGroups/QMI-Machines/providers/Microsoft.Compute/images/qliksense-base-feb20-2"
@@ -35,12 +35,12 @@
"isWafPolicyAppGw": true,
"isExternal": true,
"isDisabled": false,
"availableProductVersions": [
"availableProductvalues": [
{
"product": "Qlik Sense",
"vmTypeDefault": "Standard_D4s_v3",
"index": "vm1",
"versions": [
"values": [
{
"name": "Qlik Sense February 2020",
"image": "/subscriptions/62ebff8f-c40b-41be-9239-252d6c0c8ad9/resourceGroups/QMI-Machines/providers/Microsoft.Compute/images/qliksense-base-feb20-2"
@@ -55,7 +55,7 @@
"product": "QDC",
"vmTypeDefault": "Standard_D4s_v3",
"index": "vm2",
"versions": [
"values": [
{
"name": "QDC Single Server December 2019",
"image": "/subscriptions/62ebff8f-c40b-41be-9239-252d6c0c8ad9/resourceGroups/QMI-Machines/providers/Microsoft.Compute/images/qdc-base-4.4-2"
@@ -81,12 +81,12 @@
"isWafPolicyAppGw": false,
"isExternal": false,
"isDisabled": false,
"availableProductVersions": [
"availableProductvalues": [
{
"product": "Oracle",
"vmTypeDefault": "Standard_D4s_v3",
"index": "vm1",
"versions": []
"values": []
}
],
"name": "azqmi-database-lkn",
@@ -102,12 +102,12 @@
"isWafPolicyAppGw": false,
"isExternal": false,
"isDisabled": false,
"availableProductVersions": [
"availableProductvalues": [
{
"product": "packer",
"vmTypeDefault": "Standard_D8s_v3",
"index": "vm1",
"versions": []
"values": []
}
],
"name": "azqmi-box-builder",
@@ -123,12 +123,12 @@
"isWafPolicyAppGw": false,
"isExternal": true,
"isDisabled": false,
"availableProductVersions": [
"availableProductvalues": [
{
"product": "QDC",
"vmTypeDefault": "Standard_D4s_v3",
"index": "vm1",
"versions": [
"values": [
{
"name": "QDC Single Server December 2019",
"image": "/subscriptions/62ebff8f-c40b-41be-9239-252d6c0c8ad9/resourceGroups/QMI-Machines/providers/Microsoft.Compute/images/qdc-base-4.4-2"
@@ -154,13 +154,13 @@
"isWafPolicyAppGw": false,
"isExternal": false,
"isDisabled": false,
"availableProductVersions": [
"availableProductvalues": [
{
"product": "Windows",
"vmTypeDefault": "Standard_D2s_v3",
"diskSizeGbDefault": 128,
"index": "vm1",
"versions": []
"values": []
}
],
"vmTypeDefault": "Standard_D2s_v3",
@@ -177,12 +177,12 @@
"isWafPolicyAppGw": false,
"isExternal": false,
"isDisabled": false,
"availableProductVersions": [
"availableProductvalues": [
{
"product": "Windows",
"vmTypeDefault": "Standard_D2s_v3",
"index": "vm1",
"versions": []
"values": []
}
],
"vmTypeDefault": "Standard_D2s_v3",
@@ -198,13 +198,13 @@
"isWafPolicyAppGw": false,
"isExternal": false,
"isDisabled": false,
"availableProductVersions": [
"availableProductvalues": [
{
"product": "Ubuntu 18.04",
"vmTypeDefault": "Standard_B2s",
"diskSizeGbDefault": 128,
"index": "vm1",
"versions": []
"values": []
}
],
"vmTypeDefault": "Standard_B2s",
@@ -221,13 +221,13 @@
"isWafPolicyAppGw": false,
"isExternal": false,
"isDisabled": false,
"availableProductVersions": [
"availableProductvalues": [
{
"product": "Centos 7.5",
"vmTypeDefault": "Standard_B2s",
"diskSizeGbDefault": 128,
"index": "vm1",
"versions": []
"values": []
}
],
"vmTypeDefault": "Standard_B2s",
@@ -244,13 +244,13 @@
"isWafPolicyAppGw": false,
"isExternal": false,
"isDisabled": false,
"availableProductVersions": [
"availableProductvalues": [
{
"product": "Centos 7.5",
"vmTypeDefault": "Standard_D4s_v3",
"diskSizeGbDefault": 128,
"index": "vm1",
"versions": []
"values": []
}
],
"vmTypeDefault": "Standard_D4s_v3",
@@ -268,13 +268,13 @@
"isWafPolicyAppGw": false,
"isExternal": false,
"isDisabled": false,
"availableProductVersions": [
"availableProductvalues": [
{
"product": "Windows",
"vmTypeDefault": "Standard_D8s_v3",
"diskSizeGbDefault": 128,
"index": "vm1",
"versions": []
"values": []
}
],
"vmTypeDefault": "Standard_D8s_v3",
@@ -292,12 +292,12 @@
"isWafPolicyAppGw": false,
"isExternal": false,
"isDisabled": false,
"availableProductVersions": [
"availableProductvalues": [
{
"product": "QlikView",
"vmTypeDefault": "Standard_D4s_v3",
"index": "vm1",
"versions": []
"values": []
}
],
"vmTypeDefault": "Standard_D4s_v3",
@@ -314,13 +314,13 @@
"isWafPolicyAppGw": false,
"isExternal": false,
"isDisabled": false,
"availableProductVersions": [
"availableProductvalues": [
{
"product": "each node",
"vmTypeDefault": "Standard_D8s_v3",
"diskSizeGbDefault": 250,
"index": "vm1",
"versions": []
"values": []
}
],
"vmTypeDefault": "Standard_D8s_v3",
@@ -337,12 +337,12 @@
"isWafPolicyAppGw": true,
"isExternal": true,
"isDisabled": false,
"availableProductVersions": [
"availableProductvalues": [
{
"product": "QIB",
"vmTypeDefault": "Standard_D8s_v3",
"index": "vm1",
"versions": [
"values": [
{
"name": "Qlik Sense Feb 2020 and QIB Feb 2020",
"image": "/subscriptions/62ebff8f-c40b-41be-9239-252d6c0c8ad9/resourceGroups/QMI-Machines/providers/Microsoft.Compute/images/qliksense-qib-base-feb20-1"
@@ -363,12 +363,12 @@
"isWafPolicyAppGw": true,
"isExternal": true,
"isDisabled": false,
"availableProductVersions": [
"availableProductvalues": [
{
"product": "Windows VM",
"vmTypeDefault": "Standard_D8s_v3",
"index": "vm1",
"versions": []
"values": []
}
],
"vmTypeDefault": "Standard_D8s_v3",
@@ -384,13 +384,13 @@
"isWafPolicyAppGw": false,
"isExternal": false,
"isDisabled": false,
"availableProductVersions": [
"availableProductvalues": [
{
"product": "Ubuntu 18.04",
"vmTypeDefault": "Standard_D4s_v3",
"diskSizeGbDefault": 128,
"index": "vm1",
"versions": []
"values": []
}
],
"vmTypeDefault": "Standard_D4s_v3",
@@ -406,12 +406,12 @@
"isWafPolicyAppGw": false,
"isExternal": false,
"isDisabled": false,
"availableProductVersions": [
"availableProductvalues": [
{
"product": "Attunity Replicate/Compose DW",
"vmTypeDefault": "Standard_D2s_v3",
"index": "vm1",
"versions": []
"values": []
}
],
"vmTypeDefault": "Standard_D2s_v3",

View File

@@ -4,16 +4,16 @@
"isWafPolicyAppGw": false,
"isExternal": false,
"isDisabled": false,
"availableProductVersions": [
"availableProductvalues": [
{
"product": "Qlik Sense",
"vmTypeDefault": "Standard_D4s_v3",
"diskSizeGbDefault": "500",
"index": "vm1",
"versions": [
"values": [
{
"name": "Qlik Sense April 2020",
"image": "/subscriptions/62ebff8f-c40b-41be-9239-252d6c0c8ad9/resourceGroups/QMI-Machines/providers/Microsoft.Compute/galleries/QMICloud/images/QlikSenseEnterprise/versions/13.72.3"
"image": "/subscriptions/62ebff8f-c40b-41be-9239-252d6c0c8ad9/resourceGroups/QMI-Machines/providers/Microsoft.Compute/galleries/QMICloud/images/QlikSenseEnterprise/values/13.72.3"
}
]
},
@@ -22,10 +22,10 @@
"vmTypeDefault": "Standard_D4s_v3",
"diskSizeGbDefault": "500",
"index": "vm2",
"versions": [
"values": [
{
"name": "QDC Single Server April 2020",
"image": "/subscriptions/62ebff8f-c40b-41be-9239-252d6c0c8ad9/resourceGroups/QMI-Machines/providers/Microsoft.Compute/galleries/QMICloud/images/QDC/versions/4.5.0"
"image": "/subscriptions/62ebff8f-c40b-41be-9239-252d6c0c8ad9/resourceGroups/QMI-Machines/providers/Microsoft.Compute/galleries/QMICloud/images/QDC/values/4.5.0"
}
]
}
@@ -42,16 +42,16 @@
"isWafPolicyAppGw": false,
"isExternal": false,
"isDisabled": false,
"availableProductVersions": [
"availableProductvalues": [
{
"product": "QDC",
"vmTypeDefault": "Standard_D8s_v3",
"diskSizeGbDefault": 500,
"index": "vm1",
"versions": [
"values": [
{
"name": "QDC Single Server April 2020",
"image": "/subscriptions/62ebff8f-c40b-41be-9239-252d6c0c8ad9/resourceGroups/QMI-Machines/providers/Microsoft.Compute/galleries/QMICloud/images/QDC/versions/4.5.0"
"image": "/subscriptions/62ebff8f-c40b-41be-9239-252d6c0c8ad9/resourceGroups/QMI-Machines/providers/Microsoft.Compute/galleries/QMICloud/images/QDC/values/4.5.0"
}
]
}
@@ -68,20 +68,20 @@
"isWafPolicyAppGw": false,
"isExternal": false,
"isDisabled": false,
"availableProductVersions": [
"availableProductvalues": [
{
"product": "Qlik Sense",
"vmTypeDefault": "Standard_D8s_v3",
"diskSizeGbDefault": 250,
"index": "vm1",
"versions": [
"values": [
{
"name": "Qlik Sense February 2020",
"image": "/subscriptions/62ebff8f-c40b-41be-9239-252d6c0c8ad9/resourceGroups/QMI-Machines/providers/Microsoft.Compute/galleries/QMICloud/images/QlikSenseEnterprise/versions/13.62.0"
"image": "/subscriptions/62ebff8f-c40b-41be-9239-252d6c0c8ad9/resourceGroups/QMI-Machines/providers/Microsoft.Compute/galleries/QMICloud/images/QlikSenseEnterprise/values/13.62.0"
},
{
"name": "Qlik Sense April 2020",
"image": "/subscriptions/62ebff8f-c40b-41be-9239-252d6c0c8ad9/resourceGroups/QMI-Machines/providers/Microsoft.Compute/galleries/QMICloud/images/QlikSenseEnterprise/versions/13.72.3"
"image": "/subscriptions/62ebff8f-c40b-41be-9239-252d6c0c8ad9/resourceGroups/QMI-Machines/providers/Microsoft.Compute/galleries/QMICloud/images/QlikSenseEnterprise/values/13.72.3"
}
]
}

View File

@@ -1,7 +1,7 @@
var myArgs = process.argv.slice(2);
if ( myArgs.length < 3 ) {
if ( myArgs.length < 2 ) {
console.log("Missing args", myArgs);
process.exit(0);
}
@@ -12,9 +12,7 @@ const moment = require('moment');
const fetch = require('node-fetch');
const IS_REAL = myArgs[2] !== 'test';
const STOPPED_PERIOD = myArgs[1]; //20 Days
const STOPPED_PERIOD_IS_EXTERNAL = Math.ceil(myArgs[1]/2); //10 Days
const IS_REAL = myArgs[1] !== 'test';
const WARNING_DAYS = 2;
//---
@@ -52,6 +50,15 @@ function timeRunning(p) {
};
}
function timeFromCreated(p) {
let totalTime = Math.abs(new Date().getTime() - new Date(p.created).getTime());
let duration = moment.duration(totalTime);
p.duration = {
hours: Math.floor(duration.asHours()),
complete: Math.floor(duration.asDays()) +"d "+duration.hours()+"h "+duration.minutes()
};
}
async function asyncForEach(array, callback) {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array);
@@ -63,7 +70,12 @@ async function init(type) {
var filter = {
"isDestroyed":false,
"isDeleted": false,
"statusVms": "Stopped"
"$or": [ {"runForever":{ "$exists": false }}, {"runForever": false}],
"statusVms": "Stopped",
"$and": [{"$or": [
{ "options": {"$exists": true}, "options.vm1": { "$exists": true } },
{ "vmImage": {"$exists": true}, "vmImage.vm1": { "$exists": true } },
]}]
};
if ( type === "warning" ) {
filter.pendingNextAction = {$ne: "destroy"};
@@ -84,11 +96,15 @@ async function init(type) {
typeSchedule = 'OnSchedule';
}
timeRunning(p);
let stoppedPeriod = p._scenarioDoc.allowedInnactiveDays || 20;
let stoppedPeriodExternal = Math.ceil(stoppedPeriod/2);
var limit;
if ( type === "warning" ) {
limit = p.isExternalAccess? 24*(STOPPED_PERIOD_IS_EXTERNAL-WARNING_DAYS) : 24*(STOPPED_PERIOD-WARNING_DAYS);
limit = p.isExternalAccess? 24*(stoppedPeriodExternal-WARNING_DAYS) : 24*(stoppedPeriod-WARNING_DAYS);
} else if ( type === "exec" ) {
limit = p.isExternalAccess? (24*STOPPED_PERIOD_IS_EXTERNAL) : (24*STOPPED_PERIOD);
limit = p.isExternalAccess? (24*stoppedPeriodExternal) : (24*stoppedPeriod);
}
if ( !IS_REAL ) {
console.log(`${p._id} (${typeSchedule}) - limit: ${limit} hs - actual duration: ${p.duration.hours} hs`);
@@ -99,6 +115,55 @@ async function init(type) {
});
}
async function initOthers() {
const WARN_DAYS = 82;
const DEST_DAYS = 91;
var dWarning = new Date();
var dDestroy = new Date();
dWarning.setDate(dWarning.getDate() - WARN_DAYS);
dDestroy.setDate(dDestroy.getDate() - DEST_DAYS);
dWarning = dWarning.getTime();
dDestroy = dDestroy.getTime();
var filter = {
"isDestroyed":false,
"isDeleted": false,
"$or": [ {"runForever":{ "$exists": false }}, {"runForever": false}],
"scenario": { "$in": ["awsqmi-s3bucket"] }
};
let provisions = await db.provision.get(filter);
await asyncForEach(provisions.results, async function(p) {
var typeSchedule = "24x7";
timeFromCreated(p);
let pCreatedAt = new Date(p.created);
pCreatedAt = pCreatedAt.getTime();
var limit;
if ( pCreatedAt <= dDestroy ) {
//TODO Destroy
limit = DEST_DAYS*24;
await doDestroy(p, limit, "24x7");
} else if ( pCreatedAt > dDestroy && pCreatedAt <= dWarning && !p.pendingNextAction) {
//TODO Warning
limit = WARN_DAYS*24;
let msg = `Send warning DESTROY email - ${p.user.displayName} (${p.user.upn}) about provision '${p._scenarioDoc.title}' (${p._id} - ${typeSchedule}) being 'Inactive' more than ${limit} hours, (exactly ${p.duration.complete})`;
console.log(msg);
if ( IS_REAL ) {
db.event.add({ provision: p._id, type: 'vms.warning-destroy' });
await db.provision.update(p._id, {"pendingNextAction": "destroy"});
await db.notification.add({ provision: p._id.toString(), type: 'warningDestroy', message: msg });
await sendEmail.sendWillDestroyInDays(p, p._scenarioDoc, WARN_DAYS, 10);
}
}
});
}
const doSendEmailDestroyWarning = async function(p, limit, typeSchedule) {
if ( p.pendingNextAction === 'destroy') {
console.log(`Warning email Destroy already sent. Wait for pending action to complete.`);
@@ -106,9 +171,11 @@ const doSendEmailDestroyWarning = async function(p, limit, typeSchedule) {
let msg = `Send warning DESTROY email - ${p.user.displayName} (${p.user.upn}) about provision '${p._scenarioDoc.title}' (${p._id} - ${typeSchedule}) being 'Inactive' more than ${limit} hours, (exactly ${p.duration.complete})`;
console.log(msg);
if ( IS_REAL ) {
db.event.add({ provision: p._id, type: 'vms.warning-destroy' });
await db.provision.update(p._id, {"pendingNextAction": "destroy"});
await sendEmail.sendWillDestroyIn24(p, p._scenarioDoc, Math.floor(limit/24), WARNING_DAYS);
await db.notification.add({ provision: p._id.toString(), type: 'warningDestroy', message: msg });
await db.notification.add({ provision: p._id.toString(), type: 'warningDestroy', message: msg });
await sendEmail.sendWillDestroyIn24(p, p._scenarioDoc, Math.floor(limit/24), WARNING_DAYS);
}
}
};
@@ -117,9 +184,10 @@ const doDestroy = async function(p, limit, typeSchedule) {
try {
let msg = `Provision destroyed - ${p.user.displayName} (${p.user.upn}) about provision '${p._scenarioDoc.title}' (${p._id} - ${typeSchedule}) being 'Inactive' more than ${limit} hours (exactly ${p.duration.complete})`
console.log(msg);
if ( IS_REAL ) {
if ( IS_REAL ) {
db.event.add({ provision: p._id, type: 'vms.exec-destroy' });
await postDestroy(p);
await db.notification.add({ provision: p._id.toString(), type: 'destroy', message: msg });
await db.notification.add({ provision: p._id.toString(), type: 'destroy', message: msg });
}
} catch (error) {
console.log("doDestroy Error", error);
@@ -138,6 +206,18 @@ function check(type) {
});
}
function checkOthers() {
initOthers().then(function(){
db.mongoose.connection.close()
process.exit(0);
}).catch(function(e){
db.mongoose.connection.close()
console.log("Error", e);
process.exit(0);
});
}
// --------------------------------
switch (myArgs[0]) {
@@ -147,6 +227,9 @@ switch (myArgs[0]) {
case 'exec':
check("exec");
break;
case 'others':
checkOthers();
break;
default:
console.log('Sorry, that is not something I know how to do.');
process.exit(0);

View File

@@ -0,0 +1,59 @@
var myArgs = process.argv.slice(2);
if ( myArgs.length < 1 ) {
console.log("Missing args", myArgs);
process.exit(0);
}
const IS_REAL = myArgs[0] !== 'test';
// ---
const db = require('qmi-cloud-common/mongo');
const sendEmail = require("qmi-cloud-common/send-email");
async function asyncForEach(array, callback) {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array);
}
}
const doSendEmailErrorProvision = async function(p) {
let msg = `Send prrovision ERROR email - ${p.user.displayName} (${p.user.upn}) about provision '${p._scenarioDoc.title}' (${p._id}) provisioned with 'Errors'`;
console.log(msg);
if ( IS_REAL ) {
db.event.add({ provision: p._id, type: 'vms.warning-error' });
await db.notification.add({ provision: p._id.toString(), type: 'warningError', message: msg });
await sendEmail.sendProvisionError(p, p._scenarioDoc);
}
};
async function init() {
let provisions = await db.provision.get({
"isDestroyed":false,
"isDeleted": false,
"status": "error"
});
await asyncForEach(provisions.results, async function(p) {
await doSendEmailErrorProvision(p);
});
}
function check() {
init().then(function(){
db.mongoose.connection.close()
process.exit(0);
}).catch(function(e){
db.mongoose.connection.close()
console.log("Error", e);
process.exit(0);
});
}
// --------------------------------
check();

View File

@@ -0,0 +1,61 @@
var myArgs = process.argv.slice(2);
if ( myArgs.length < 1 ) {
console.log("Missing args", myArgs);
process.exit(0);
}
console.log("myArgs", myArgs);
const ISODATE = new Date(myArgs[0]).toISOString();
const db = require('qmi-cloud-common/mongo');
const fs = require('fs-extra');
async function asyncForEach(array, callback) {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array);
}
}
async function init() {
let provisions = await db.provision.get({
"isDestroyed":true,
"isDeleted": false,
"created": { "$lt" : ISODATE }
});
await asyncForEach(provisions.results, async function(p) {
await doMoveDestroyed(p);
});
}
const doMoveDestroyed = async function(provision) {
var scenarioFolder = `${provision.scenario}_${provision._id}`;
console.log(`Moving scenario: /provisions/${scenarioFolder}`);
if (fs.existsSync(`/provisions/${scenarioFolder}`)) {
fs.moveSync(`/provisions/${scenarioFolder}`, `/provisions/deleted/${scenarioFolder}`, { overwrite: true })
console.log(`OK.`);
} else {
console.log(`NOK: It does not exist.`);
}
};
function check() {
init().then(function(){
db.mongoose.connection.close()
process.exit(0);
}).catch(function(e){
db.mongoose.connection.close()
console.log("Error", e);
process.exit(0);
});
}
// --------------------------------
check();

View File

@@ -1,15 +1,12 @@
var myArgs = process.argv.slice(2);
if ( myArgs.length < 3 ) {
if ( myArgs.length < 2 ) {
console.log("Missing args", myArgs);
process.exit(0);
}
const IS_REAL = myArgs[2] !== 'test';
const RUNNING_PERIOD = myArgs[1]; // 7 Days
const RUNNING_PERIOD_ON_SCHEDULE = Math.ceil(myArgs[1]/2); // ~ 4 Days
const IS_REAL = myArgs[1] !== 'test';
const WARNING_DAYS = 1;
// ---
@@ -17,7 +14,7 @@ const WARNING_DAYS = 1;
const db = require('qmi-cloud-common/mongo');
const sendEmail = require("qmi-cloud-common/send-email");
const moment = require('moment');
const azurecli = require('qmi-cloud-common/azurecli');
const cli = require('qmi-cloud-common/cli');
function timeRunningIs24x7(p) {
let runningFromTime = p.runningFrom? new Date(p.runningFrom).getTime() : new Date(p.created).getTime();
@@ -41,6 +38,18 @@ function timeRunningOnSchedule(p) {
};
}
function timeRunningOnSchedule2(p) {
let startTimestamp = p.startDateOnSchedule? new Date(p.startDateOnSchedule).getTime() : 0;
let endTimestamp = p.endDateOnSchedule? new Date(p.endDateOnSchedule).getTime() : 0;
let totalTimeOnschedule = Math.abs(endTimestamp - startTimestamp);
let duration = moment.duration(totalTimeOnschedule);
p.duration = {
hours: Math.floor(duration.asHours()),
complete: Math.floor(duration.asDays()) +"d "+duration.hours()+"h "+duration.minutes()
};
}
async function asyncForEach(array, callback) {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array);
@@ -50,12 +59,18 @@ async function asyncForEach(array, callback) {
async function init(type) {
var cb;
var filter = {
"isDestroyed":false,
"isDestroyed": false,
"isDeleted": false,
"statusVms": "Running"
"$or": [ {"runForever":{ "$exists": false }}, {"runForever": false}],
"statusVms": "Running",
"$and": [{"$or": [
{ "options": {"$exists": true}, "options.vm1": { "$exists": true } },
{ "vmImage": {"$exists": true}, "vmImage.vm1": { "$exists": true } },
]}]
};
if ( type === "warning" ) {
filter.pendingNextAction = {$ne: "stopVms"};
filter.pendingNextAction = { $ne: "stopVms" };
cb = doSendEmailWarning;
} else if ( type === "exec" ) {
filter.pendingNextAction = "stopVms";
@@ -69,18 +84,19 @@ async function init(type) {
await asyncForEach(provisions.results, async function(p) {
//Only if 24x7
var typeSchedule = "24x7";
var periodDays = RUNNING_PERIOD;
let typeSchedule = "24x7";
let periodDays = p._scenarioDoc.allowed24x7RunningDays;
if ( !p.schedule || p.schedule.is24x7 ) {
// 24x7
timeRunningIs24x7(p);
periodDays = RUNNING_PERIOD;
} else if ( p.schedule && !p.schedule.is24x7 ) {
timeRunningOnSchedule(p);
periodDays = p._scenarioDoc.allowed24x7RunningDays;
} else if ( p.schedule && !p.schedule.is24x7 ) {
// OnSchedule
timeRunningOnSchedule2(p);
typeSchedule = 'OnSchedule';
let onScheduleRenewed = p.schedule.onScheduleRenewed? p.schedule.onScheduleRenewed : 1;
periodDays = RUNNING_PERIOD_ON_SCHEDULE * onScheduleRenewed;
periodDays = p._scenarioDoc.allowedOnScheduleRunningDays;
}
var limit;
let limit;
if ( type === "warning" ) {
limit = 24*(periodDays-WARNING_DAYS);
} else if ( type === "exec" ) {
@@ -102,9 +118,10 @@ const doSendEmailWarning = async function(p, limit, typeSchedule) {
let msg = `Send warning STOP email - ${p.user.displayName} (${p.user.upn}) about provision '${p._scenarioDoc.title}' (${p._id} - ${typeSchedule}) being 'Running' more than ${limit} hours, (exactly ${p.duration.complete})`;
console.log(msg);
if ( IS_REAL ) {
db.event.add({ provision: p._id, type: 'vms.warning-stop' });
await db.provision.update(p._id, {"pendingNextAction": "stopVms"});
await db.notification.add({ provision: p._id.toString(), type: 'warningStop', message: msg });
await sendEmail.sendWillStopIn24(p, p._scenarioDoc, Math.floor(limit/24), WARNING_DAYS);
await db.notification.add({ provision: p._id.toString(), type: 'warningStop', message: msg });
}
}
};
@@ -118,14 +135,15 @@ const doStop = async function(p, limit, typeSchedule) {
if (p.schedule){
//Disable Divvy
await db.schedule.update(p.schedule._id, {"isStartupTimeEnable": false});
await azurecli.updateVmsTags(p._id, {
await cli.updateVmsTags(p._id, {
"24x7": false,
"StartupTime": false,
"ShutdownTime": false
});
}
//Stop VMs indefinitely
await azurecli.deallocate(p._id, true);
db.event.add({ provision: p._id, type: 'vms.exec-stop' });
await cli.deallocate(p._id, null, true);
await db.notification.add({ provision: p._id.toString(), type: 'stop', message: msg });
}

View File

@@ -57,8 +57,8 @@ async function getProvisions(filter){
console.log(res.results.length);
await asyncForEach(res.results, async function(prov){
if ( prov.vmType ) {
//console.log("vmType", prov.vmType, prov.vmImage)
prov["vmImage"] = {
//console.log("vmType", prov.vmType, prov.options)
prov["options"] = {
"vm1": {
"vmType": prov.vmType,
"diskSizeGb": 128

View File

@@ -1,13 +1,15 @@
{
"name": "qmi-cloud-cli",
"version": "1.1.5",
"version": "2.0.5",
"scripts": {
},
"private": true,
"dependencies": {
"qmi-cloud-common": "../qmi-cloud-common",
"node-fetch": "^2.6.0",
"moment": "^2.24.0"
"moment": "^2.24.0",
"axios": "^0.21.1",
"fs-extra": "^8.1.0"
}
}

View File

@@ -5,18 +5,33 @@ helpFunction()
{
echo ""
echo "Usage: $0 -s script"
echo -e "\t-s Type of script ( checkstop | checkdestroy | initdb )"
echo -e "\t-s Type of script ( checkstop | checkdestroy | checkerror | movedestroyed | initdb )"
exit 1 # Exit script after printing help
}
helpFunctionCheck()
{
echo ""
echo "Usage: $0 -s script -t type -p period -r realortest"
echo -e "\t-s Type of script ( checkstop | checkdestroy | initdb )"
echo -e "\t-t Type of script (warning | exec)"
echo -e "\t-p Period (number of days)"
echo -e "\t-r Type of script (test | real)"
echo "Usage: $0 -s script [-t type -r realortest]"
echo -e "\t-s Type of script ( checkstop | checkdestroy | checkerror )"
echo -e "\t-t Type of script ( warning | exec )"
echo -e "\t-r Indicate whether is for test or real execution ( test | real )"
exit 1 # Exit script after printing help
}
helpFunctionCheck2()
{
echo ""
echo "Usage: $0 -s checkerror [-r realortest]"
echo -e "\t-r Indicate whether is for test or real execution ( test | real )"
exit 1 # Exit script after printing help
}
helpFunctionCheck3()
{
echo ""
echo "Usage: $0 -s movedestroyed [-r isodate]"
echo -e "\t-r An ISODATE (ie: '2020-31-12T00:00:00.000Z') to movedestroyed backwards"
exit 1 # Exit script after printing help
}
@@ -25,7 +40,6 @@ do
case "$opt" in
s ) script="$OPTARG" ;;
t ) type="$OPTARG" ;;
p ) period="$OPTARG" ;;
r ) realortest="$OPTARG" ;;
? ) helpFunction ;; # Print helpFunction in case parameter is non-existent
esac
@@ -33,25 +47,25 @@ done
if [ -z "$script" ]
then
echo "Some or all of the parameters are empty";
echo "Script not recognised";
helpFunction
fi
if [ "$script" = "checkstop" ] || [ "$script" = "checkdestroy" ]
then
if [ -z "$type" ] || [ -z "$period" ] || [ -z "$realortest" ]
if [ -z "$type" ] || [ -z "$realortest" ]
then
echo "Some or all of the parameters are empty";
helpFunctionCheck
else
if [ "$script" = "checkstop" ]
then
$BASEDIR/checkstop.sh $type $period $realortest
$BASEDIR/checkstop.sh $type $realortest
fi
if [ "$script" = "checkdestroy" ]
then
$BASEDIR/checkdestroy.sh $type $period $realortest
$BASEDIR/checkdestroy.sh $type $realortest
fi
fi
elif [ "$script" = "initdb" ]
@@ -64,5 +78,24 @@ then
echo "\$MONGO_URI=$MONGO_URI"
node $BASEDIR/../jobs/db-init.js
fi
elif [ "$script" = "checkerror" ]
then
if [ -z "$realortest" ]
then
echo "Some or all of the parameters are empty";
helpFunctionCheck2
else
node $BASEDIR/../jobs/error5.js $realortest
fi
elif [ "$script" = "movedestroyed" ]
then
if [ -z "$realortest" ]
then
echo "Some or all of the parameters are empty";
helpFunctionCheck3
else
node $BASEDIR/../jobs/move_destroyed.js $realortest
fi
fi

View File

@@ -3,8 +3,7 @@ d=`date`
echo "------ $d"
echo "------ SCRIPT: $0"
echo "------ TYPE: $1"
echo "------ PERIOD: $2"
echo "------ TEST/REAL: $3"
echo "------ TEST/REAL: $2"
if [ -z "$MONGO_URI" ]
then
@@ -22,4 +21,4 @@ else
echo "\$API_KEY=$API_KEY"
fi
node $BASEDIR/../jobs/destroy5.js $1 $2 $3
node $BASEDIR/../jobs/destroy5.js $1 $2

View File

@@ -0,0 +1,14 @@
BASEDIR=$(dirname "$0")
d=`date`
echo "------ $d"
echo "------ SCRIPT: $0"
if [ -z "$MONGO_URI" ]
then
echo "\$MONGO_URI is empty"
exit
else
echo "\$MONGO_URI=$MONGO_URI"
fi
node $BASEDIR/../jobs/error5.js $1

View File

@@ -3,8 +3,7 @@ d=`date`
echo "------ $d"
echo "------ SCRIPT: $0"
echo "------ TYPE: $1"
echo "------ PERIOD: $2"
echo "------ TEST/REAL: $3"
echo "------ TEST/REAL: $2"
if [ -z "$MONGO_URI" ]
then
@@ -22,4 +21,4 @@ else
echo "\$API_KEY=$API_KEY"
fi
node $BASEDIR/../jobs/stop5.js $1 $2 $3
node $BASEDIR/../jobs/stop5.js $1 $2

View File

@@ -2,6 +2,70 @@
# yarn lockfile v1
"@azure/abort-controller@^1.0.0":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@azure/abort-controller/-/abort-controller-1.0.4.tgz#fd3c4d46c8ed67aace42498c8e2270960250eafd"
integrity sha512-lNUmDRVGpanCsiUN3NWxFTdwmdFI53xwhkTFfHDGTYk46ca7Ind3nanJc+U6Zj9Tv+9nTCWRBscWEW1DyKOpTw==
dependencies:
tslib "^2.0.0"
"@azure/arm-compute@^15.0.0":
version "15.0.0"
resolved "https://registry.yarnpkg.com/@azure/arm-compute/-/arm-compute-15.0.0.tgz#5d0d0c1db16adbb6db3d33ca376b120c68e6ae23"
integrity sha512-ElV2MuYZ+B2+ygMx2iiM/u3C7WDRruZjkXzfk5p2O+UbWxjG6j/686OH3T+VSDqmzg+47AnIOTLu2v0u0H8FOw==
dependencies:
"@azure/ms-rest-azure-js" "^2.0.1"
"@azure/ms-rest-js" "^2.0.4"
tslib "^1.10.0"
"@azure/core-auth@^1.1.4":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@azure/core-auth/-/core-auth-1.2.0.tgz#a5a181164e99f8446a3ccf9039345ddc9bb63bb9"
integrity sha512-KUl+Nwn/Sm6Lw5d3U90m1jZfNSL087SPcqHLxwn2T6PupNKmcgsEbDjHB25gDvHO4h7pBsTlrdJAY7dz+Qk8GA==
dependencies:
"@azure/abort-controller" "^1.0.0"
tslib "^2.0.0"
"@azure/ms-rest-azure-env@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@azure/ms-rest-azure-env/-/ms-rest-azure-env-2.0.0.tgz#45809f89763a480924e21d3c620cd40866771625"
integrity sha512-dG76W7ElfLi+fbTjnZVGj+M9e0BIEJmRxU6fHaUQ12bZBe8EJKYb2GV50YWNaP2uJiVQ5+7nXEVj1VN1UQtaEw==
"@azure/ms-rest-azure-js@^2.0.1":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@azure/ms-rest-azure-js/-/ms-rest-azure-js-2.1.0.tgz#8c90b31468aeca3146b06c7144b386fd4827f64c"
integrity sha512-CjZjB8apvXl5h97Ck6SbeeCmU0sk56YPozPtTyGudPp1RGoHXNjFNtoOvwOG76EdpmMpxbK10DqcygI16Lu60Q==
dependencies:
"@azure/core-auth" "^1.1.4"
"@azure/ms-rest-js" "^2.2.0"
tslib "^1.10.0"
"@azure/ms-rest-js@^2.0.4", "@azure/ms-rest-js@^2.2.0":
version "2.2.3"
resolved "https://registry.yarnpkg.com/@azure/ms-rest-js/-/ms-rest-js-2.2.3.tgz#8f0085f7768c69f17b3cdb20ce95728b452dc304"
integrity sha512-sXOhOu/37Tr8428f32Jwuwga975Xw64pYg1UeUwOBMhkNgtn5vUuNRa3fhmem+I6f8EKoi6hOsYDFlaHeZ52jA==
dependencies:
"@azure/core-auth" "^1.1.4"
"@types/node-fetch" "^2.3.7"
"@types/tunnel" "0.0.1"
abort-controller "^3.0.0"
form-data "^2.5.0"
node-fetch "^2.6.0"
tough-cookie "^3.0.1"
tslib "^1.10.0"
tunnel "0.0.6"
uuid "^3.3.2"
xml2js "^0.4.19"
"@azure/ms-rest-nodeauth@^3.0.7":
version "3.0.7"
resolved "https://registry.yarnpkg.com/@azure/ms-rest-nodeauth/-/ms-rest-nodeauth-3.0.7.tgz#73c399b0aef45c75104324b6617aa4e0a6c27875"
integrity sha512-7Q1MyMB+eqUQy8JO+virSIzAjqR2UbKXE/YQZe+53gC8yakm8WOQ5OzGfPP+eyHqeRs6bQESyw2IC5feLWlT2A==
dependencies:
"@azure/ms-rest-azure-env" "^2.0.0"
"@azure/ms-rest-js" "^2.0.4"
adal-node "^0.1.28"
"@hapi/boom@^9.1.0":
version "9.1.0"
resolved "https://registry.yarnpkg.com/@hapi/boom/-/boom-9.1.0.tgz#0d9517657a56ff1e0b42d0aca9da1b37706fec56"
@@ -14,11 +78,38 @@
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.0.4.tgz#e80ad4e8e8d2adc6c77d985f698447e8628b6010"
integrity sha512-EwaJS7RjoXUZ2cXXKZZxZqieGtc7RbvQhUy8FwDoMQtxWVi14tFjeFCYPZAM1mBCpOpiBpyaZbb9NeHc7eGKgw==
"@types/node-fetch@^2.3.7":
version "2.5.8"
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.8.tgz#e199c835d234c7eb0846f6618012e558544ee2fb"
integrity sha512-fbjI6ja0N5ZA8TV53RUqzsKNkl9fv8Oj3T7zxW7FGv1GSH7gwJaNF8dzCjrqKaxKeUpTz4yT1DaJFq/omNpGfw==
dependencies:
"@types/node" "*"
form-data "^3.0.0"
"@types/node@*":
version "14.14.35"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.35.tgz#42c953a4e2b18ab931f72477e7012172f4ffa313"
integrity sha512-Lt+wj8NVPx0zUmUwumiVXapmaLUcAk3yPuHCFVXras9k5VT9TdhJqKqGVUQCD60OTMCl0qxJ57OiTL0Mic3Iag==
"@types/node@^8.0.47":
version "8.10.61"
resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.61.tgz#d299136ce54bcaf1abaa4a487f9e4bedf6b0d393"
integrity sha512-l+zSbvT8TPRaCxL1l9cwHCb0tSqGAGcjPJFItGGYat5oCTiq1uQQKYg5m7AF1mgnEBzFXGLJ2LRmNjtreRX76Q==
"@types/tunnel@0.0.1":
version "0.0.1"
resolved "https://registry.yarnpkg.com/@types/tunnel/-/tunnel-0.0.1.tgz#0d72774768b73df26f25df9184273a42da72b19c"
integrity sha512-AOqu6bQu5MSWwYvehMXLukFHnupHrpZ8nvgae5Ggie9UwzDR1CCwoXgSSWNZJuyOlCdfdsWMA5F2LlmvyoTv8A==
dependencies:
"@types/node" "*"
abort-controller@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==
dependencies:
event-target-shim "^5.0.0"
adal-node@^0.1.28:
version "0.1.28"
resolved "https://registry.yarnpkg.com/adal-node/-/adal-node-0.1.28.tgz#468c4bb3ebbd96b1270669f4b9cba4e0065ea485"
@@ -56,13 +147,6 @@ assert-plus@1.0.0, assert-plus@^1.0.0:
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=
async@2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4"
integrity sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==
dependencies:
lodash "^4.14.0"
async@>=0.6.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720"
@@ -83,13 +167,12 @@ aws4@^1.8.0:
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.10.0.tgz#a17b3a8ea811060e74d47d306122400ad4497ae2"
integrity sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA==
azure-arm-compute@^10.0.0:
version "10.0.0"
resolved "https://registry.yarnpkg.com/azure-arm-compute/-/azure-arm-compute-10.0.0.tgz#ce9ba2e4d6dd6b1174c34da2219700a8080f389b"
integrity sha512-ehafNtcMKI6c00FT+xhPWPtzhYgHCZ675TUsaJ1FJ2bSpznih5EUrpir/4w519T4zbFBigszhnRK6eBkl78I9g==
axios@^0.21.1:
version "0.21.1"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8"
integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==
dependencies:
ms-rest "^2.5.0"
ms-rest-azure "^2.5.5"
follow-redirects "^1.10.0"
bcrypt-pbkdf@^1.0.0:
version "1.0.2"
@@ -147,7 +230,7 @@ cluster-key-slot@^1.1.0:
resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz#30474b2a981fb12172695833052bc0d01336d10d"
integrity sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw==
combined-stream@^1.0.6, combined-stream@~1.0.6:
combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6:
version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
@@ -215,11 +298,6 @@ denque@^1.1.0, denque@^1.4.1:
resolved "https://registry.yarnpkg.com/denque/-/denque-1.4.1.tgz#6744ff7641c148c3f8a69c307e51235c1f4a37cf"
integrity sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ==
duplexer@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1"
integrity sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=
ecc-jsbn@~0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
@@ -261,6 +339,11 @@ es-to-primitive@^1.2.1:
is-date-object "^1.0.1"
is-symbol "^1.0.2"
event-target-shim@^5.0.0:
version "5.0.1"
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
extend@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
@@ -286,11 +369,34 @@ fast-json-stable-stringify@^2.0.0:
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
follow-redirects@^1.10.0:
version "1.13.3"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.3.tgz#e5598ad50174c1bc4e872301e82ac2cd97f90267"
integrity sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA==
forever-agent@~0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=
form-data@^2.5.0:
version "2.5.1"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4"
integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.6"
mime-types "^2.1.12"
form-data@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f"
integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
mime-types "^2.1.12"
form-data@~2.3.2:
version "2.3.3"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
@@ -300,6 +406,15 @@ form-data@~2.3.2:
combined-stream "^1.0.6"
mime-types "^2.1.12"
fs-extra@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"
integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==
dependencies:
graceful-fs "^4.2.0"
jsonfile "^4.0.0"
universalify "^0.1.0"
function-bind@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
@@ -317,6 +432,11 @@ getpass@^0.1.1:
dependencies:
assert-plus "^1.0.0"
graceful-fs@^4.1.6, graceful-fs@^4.2.0:
version "4.2.6"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee"
integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==
har-schema@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
@@ -371,10 +491,10 @@ ioredis@^4.14.1:
redis-parser "^3.0.0"
standard-as-callback "^2.0.1"
is-buffer@^1.1.6:
version "1.1.6"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
ip-regex@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9"
integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=
is-callable@^1.1.4, is-callable@^1.1.5:
version "1.2.0"
@@ -400,11 +520,6 @@ is-regex@^1.0.5:
dependencies:
has-symbols "^1.0.1"
is-stream@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
is-symbol@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937"
@@ -447,6 +562,13 @@ json-stringify-safe@~5.0.1:
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
jsonfile@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=
optionalDependencies:
graceful-fs "^4.1.6"
jsprim@^1.2.2:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
@@ -489,7 +611,7 @@ lodash.flatten@^4.4.0:
resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f"
integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=
lodash@^4.14.0, lodash@^4.17.15:
lodash@^4.17.15:
version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
@@ -518,7 +640,7 @@ moment-timezone@^0.5.31:
dependencies:
moment ">= 2.9.0"
"moment@>= 2.9.0", moment@^2.21.0, moment@^2.22.2, moment@^2.24.0:
"moment@>= 2.9.0", moment@^2.24.0:
version "2.26.0"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.26.0.tgz#5e1f82c6bafca6e83e808b30c8705eed0dcbd39a"
integrity sha512-oIixUO+OamkUkwjhAVE18rAMfRJNsNe/Stid/gwHSOfHrOtw9EhAY2AHvdKZ/k/MggcYELFCJz/Sn2pL8b8JMw==
@@ -574,44 +696,6 @@ mquery@3.2.2:
safe-buffer "5.1.2"
sliced "1.0.1"
ms-rest-azure@^2.5.5:
version "2.6.0"
resolved "https://registry.yarnpkg.com/ms-rest-azure/-/ms-rest-azure-2.6.0.tgz#2098efec529eecfa0c6e215b69143abcaba12140"
integrity sha512-J6386a9krZ4VtU7CRt+Ypgo9RGf8+d3gjMBkH7zbkM4zzkhbbMOYiPRaZ+bHZcfihkKLlktTgA6rjshTjF329A==
dependencies:
adal-node "^0.1.28"
async "2.6.0"
moment "^2.22.2"
ms-rest "^2.3.2"
request "^2.88.0"
uuid "^3.2.1"
ms-rest-azure@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/ms-rest-azure/-/ms-rest-azure-3.0.0.tgz#54d0341c2aeef7b9f17f2a46258788740b2f0ec5"
integrity sha512-cttN01/TtMDB4v3rt/WQ/slgffB6jcUYxcPzcL0VNSB+WFPE1j4y5ICNHMuD1RaNNekCYMI4Pv51BDQ/BXNq7Q==
dependencies:
adal-node "^0.1.28"
async "2.6.0"
moment "^2.22.2"
ms-rest "^2.3.2"
request "^2.88.0"
uuid "^3.2.1"
ms-rest@^2.3.2, ms-rest@^2.5.0:
version "2.5.4"
resolved "https://registry.yarnpkg.com/ms-rest/-/ms-rest-2.5.4.tgz#57b42299cf302e45d5e1a734220bf7d4a110167a"
integrity sha512-VeqCbawxRM6nhw0RKNfj7TWL7SL8PB6MypqwgylXCi+u412uvYoyY/kSmO8n06wyd8nIcnTbYToCmSKFMI1mCg==
dependencies:
duplexer "^0.1.1"
is-buffer "^1.1.6"
is-stream "^1.1.0"
moment "^2.21.0"
request "^2.88.0"
through "^2.3.8"
tunnel "0.0.5"
uuid "^3.2.1"
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
@@ -707,13 +791,14 @@ punycode@^2.1.0, punycode@^2.1.1:
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
qmi-cloud-common@../qmi-cloud-common:
version "1.1.2"
version "1.1.6"
dependencies:
"@azure/arm-compute" "^15.0.0"
"@azure/ms-rest-nodeauth" "^3.0.7"
"@hapi/boom" "^9.1.0"
azure-arm-compute "^10.0.0"
axios "^0.21.1"
bull "^3.11.0"
mongoose "^5.7.4"
ms-rest-azure "^3.0.0"
nodemailer "^6.4.2"
qs@~6.5.2:
@@ -756,7 +841,7 @@ regexp-clone@1.0.0, regexp-clone@^1.0.0:
resolved "https://registry.yarnpkg.com/regexp-clone/-/regexp-clone-1.0.0.tgz#222db967623277056260b992626354a04ce9bf63"
integrity sha512-TuAasHQNamyyJ2hb97IuBEif4qBHGjPHBS64sZwytpLEqtBQ1gPJTnOaQ6qmpET16cK14kkjbazl6+p0RRv0yw==
"request@>= 2.52.0", request@^2.88.0:
"request@>= 2.52.0":
version "2.88.2"
resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
@@ -817,6 +902,11 @@ saslprep@^1.0.0:
dependencies:
sparse-bitfield "^3.0.3"
sax@>=0.6.0:
version "1.2.4"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
semver@^5.1.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
@@ -905,10 +995,14 @@ string_decoder@~1.1.1:
dependencies:
safe-buffer "~5.1.0"
through@^2.3.8:
version "2.3.8"
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
tough-cookie@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-3.0.1.tgz#9df4f57e739c26930a018184887f4adb7dca73b2"
integrity sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==
dependencies:
ip-regex "^2.1.0"
psl "^1.1.28"
punycode "^2.1.1"
tough-cookie@~2.5.0:
version "2.5.0"
@@ -918,6 +1012,16 @@ tough-cookie@~2.5.0:
psl "^1.1.28"
punycode "^2.1.1"
tslib@^1.10.0:
version "1.14.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
tslib@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a"
integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==
tunnel-agent@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
@@ -925,10 +1029,10 @@ tunnel-agent@^0.6.0:
dependencies:
safe-buffer "^5.0.1"
tunnel@0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.5.tgz#d1532254749ed36620fcd1010865495a1fa9d0ae"
integrity sha512-gj5sdqherx4VZKMcBA4vewER7zdK25Td+z1npBqpbDys4eJrLx+SlYjJvq1bDXs2irkuJM5pf8ktaEQVipkrbA==
tunnel@0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c"
integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==
tweetnacl@^0.14.3, tweetnacl@~0.14.0:
version "0.14.5"
@@ -940,6 +1044,11 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0:
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.10.2.tgz#73d6aa3668f3188e4adb0f1943bd12cfd7efaaaf"
integrity sha512-N4P+Q/BuyuEKFJ43B9gYuOj4TQUHXX+j2FqguVOpjkssLUUrnJofCcBccJSCoeturDoZU6GorDTHSvUDlSQbTg==
universalify@^0.1.0:
version "0.1.2"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
uri-js@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"
@@ -962,7 +1071,7 @@ util.promisify@^1.0.1:
has-symbols "^1.0.1"
object.getownpropertydescriptors "^2.1.0"
uuid@^3.1.0, uuid@^3.2.1, uuid@^3.3.2, uuid@^3.4.0:
uuid@^3.1.0, uuid@^3.3.2, uuid@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
@@ -976,6 +1085,19 @@ verror@1.10.0:
core-util-is "1.0.2"
extsprintf "^1.2.0"
xml2js@^0.4.19:
version "0.4.23"
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66"
integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==
dependencies:
sax ">=0.6.0"
xmlbuilder "~11.0.0"
xmlbuilder@~11.0.0:
version "11.0.1"
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3"
integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==
"xmldom@>= 0.1.x":
version "0.3.0"
resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.3.0.tgz#e625457f4300b5df9c2e1ecb776147ece47f3e5a"

222
qmi-cloud-common/awscli.js Normal file
View File

@@ -0,0 +1,222 @@
const AWS = require("aws-sdk");
const db = require("./mongo");
const utils = require("./utils");
function _getRgName(provision) {
let rgName = provision.scenario.toUpperCase();
rgName = rgName.replace(/AZQMI/g, 'QMI');
rgName = rgName + "-" + provision._id.toString();
return rgName;
}
function _getRegion(provision) {
let region = "eu-west-1";
if ( provision.deployOpts.location === 'East US') {
region = "us-east-1";
} else if ( provision.deployOpts.location === 'Southeast Asia' ) {
region = "ap-southeast-1";
}
return region;
}
function _getDbIndentifier(provision) {
var out;
if ( provision.outputs['db_instance_id'] ) {
out = provision.outputs['db_instance_id'];
} else if (provision.outputs['db_instance_endpoint']) {
out = provision.outputs['db_instance_endpoint'].split(".")[0];
}
return out;
}
async function stopDbInstance(provision, triggerUserId, isSendEmailAfter) {
if (provision.statusVms === 'Stopped' || provision.statusVms === 'Stopping'){
return;
}
var identifier = _getDbIndentifier(provision);
if ( !identifier ) {
return;
}
await db.provision.update(provision._id.toString(), {"statusVms": "Stopping" });
db.event.add({ user: triggerUserId, provision: provision._id, type: `db.stopping` });
return new Promise((resolve, reject) => {
let region = _getRegion(provision);
var rds = new AWS.RDS({apiVersion: '2014-10-31', region: region});
var params = {
DBInstanceIdentifier: identifier
};
console.log(`AWSCLI# DB (${identifier}) stopping...`);
rds.stopDBInstance(params, async function(err, data) {
if (err) {
console.log("AWSCLI# ERROR stopping DB: "+identifier, err.stack);
await db.provision.update(provision._id.toString(), {"statusVms": "Running" });
reject(err);
} else {
console.log(`AWSCLI# DB (${identifier}) stopped!`);
resolve(data);
}
});
});
}
async function startDbInstance(provision, triggerUserId) {
if (provision.statusVms === 'Running' || provision.statusVms === 'Starting'){
return;
}
var identifier = _getDbIndentifier(provision);
if ( !identifier ) {
return;
}
await db.provision.update(provision._id.toString(), {"statusVms": "Starting" });
db.event.add({ user: triggerUserId, provision: provision._id, type: `db.starting` });
return new Promise((resolve, reject) => {
let region = _getRegion(provision);
var rds = new AWS.RDS({apiVersion: '2014-10-31', region: region});
var params = {
DBInstanceIdentifier: identifier
};
console.log(`AWSCLI# DB (${identifier}) starting...`);
rds.startDBInstance(params, async function(err, data) {
if (err) {
console.log("AWSCLI# ERROR starting DB: "+identifier, err.stack);
await db.provision.update(provision._id.toString(), {"statusVms": "Stopped" });
reject(err);
} else {
console.log(`AWSCLI# DB (${identifier}) starting (2)...`);
rds.waitFor('dBInstanceAvailable', params, async function(err, data) {
if (err) {
console.log("AWSCLI# ERROR waiting for DB to become available: "+identifier, err.stack);
reject(err);
} else {
console.log(`AWSCLI# DB (${identifier}) started!`);
await utils.afterStartVms( provision, triggerUserId, "db" );
}
});
resolve(data);
}
});
});
}
function deallocate(provision, triggerUserId, isSendEmailAfter) {
let rgName = _getRgName(provision);
let region = _getRegion(provision);
console.log("AWSCLI# Stopping EC2s for resource group: "+rgName);
return new Promise((resolve, reject) => {
var ec2 = new AWS.EC2({apiVersion: '2016-11-15', region: region});
var params = {
DryRun: false,
Filters: [
{
Name: 'tag:Name',
Values: [ `fort-${provision._id}` ]
},
]
};
ec2.describeInstances(params, async function (err, data) {
if (err) {
console.log("AWSCLI# ERROR describing EC2s: "+rgName, err.stack);
await db.provision.update(provision._id.toString(), {"statusVms": "Running" });
reject(err);
} else if (data && data.Reservations && data.Reservations.length) {
var ec2Ids = data.Reservations[0].Instances.map(ec2=> ec2.InstanceId);
console.log("AWSCLI# Stopping Ec2s ids...", ec2Ids);
params = {
DryRun: false,
InstanceIds: ec2Ids
};
ec2.stopInstances(params, async function(err1, data) {
if (err1) {
console.log("AWSCLI# ERROR stopping EC2s: "+rgName, err1.stack);
await db.provision.update(provision._id.toString(), {"statusVms": "Running" });
reject(err1);
} else {
console.log("AWSCLI# Ec2s stopped!");
await utils.afterStopVms( provision, triggerUserId, isSendEmailAfter );
resolve(data);
}
});
} else {
console.log("AWSCLI# No Ec2 Instances found: "+rgName);
resolve(data);
}
});
});
}
function start(provision, triggerUserId) {
let rgName = _getRgName(provision);
let region = _getRegion(provision);
console.log("AWSCLI# Stopping EC2s for resource group: "+rgName);
return new Promise((resolve, reject) => {
var ec2 = new AWS.EC2({apiVersion: '2016-11-15', region: region});
var params = {
DryRun: false,
Filters: [
{
Name: 'tag:Name',
Values: [ `fort-${provision._id}` ]
},
]
};
ec2.describeInstances(params, async function (err, data) {
if (err) {
console.log("AWSCLI# ERROR describing EC2s: "+rgName, err.stack);
await db.provision.update(provision._id.toString(), {"statusVms": "Stopped" });
reject(err);
} else if (data && data.Reservations && data.Reservations.length) {
var ec2Ids = data.Reservations[0].Instances.map(ec2=> ec2.InstanceId);
console.log("AWSCLI# Starting Ec2s ids...", ec2Ids);
params = {
DryRun: false,
InstanceIds: ec2Ids
};
ec2.startInstances(params, async function(err1, data) {
if (err1) {
console.log("AWSCLI# ERROR starting EC2s: "+rgName, err1.stack);
await db.provision.update(provision._id.toString(), {"statusVms": "Stopped" });
reject(err1);
} else {
console.log("AWSCLI# Ec2s started!");
await utils.afterStartVms( provision, triggerUserId );
resolve(data);
}
});
} else {
console.log("AWSCLI# No Ec2 Instances found: "+rgName);
resolve(data);
}
});
});
}
module.exports.deallocate = deallocate;
module.exports.start = start;
module.exports.stopDbInstance = stopDbInstance;
module.exports.startDbInstance = startDbInstance;

View File

@@ -1,7 +1,8 @@
const MsRest = require('ms-rest-azure');
const computeManagementClient = require('azure-arm-compute');
const loginWithVmMSI = require("@azure/ms-rest-nodeauth").loginWithVmMSI;
const computeManagementClient = require("@azure/arm-compute").ComputeManagementClient;
const dnsManagementClient = require("@azure/arm-dns").DnsManagementClient;
const db = require("./mongo");
const sendEmail = require("./send-email");
const utils = require("./utils");
const SUBSCRIPTION_ID = "62ebff8f-c40b-41be-9239-252d6c0c8ad9";
@@ -18,10 +19,22 @@ async function _getClient(scenarioName) {
var scenario = await db.scenario.getOne({"name": scenarioName});
id = scenario.subscription? scenario.subscription.subsId : SUBSCRIPTION_ID;
}
var credentials = await MsRest.loginWithMSI({ port: 50342 });
var credentials = await loginWithVmMSI({ port: 50342 });
//console.log("AzureCLI# authenticated", credentials);
return new computeManagementClient(credentials, id);
return new computeManagementClient(credentials, id);
}
async function _getDNSClient(scenarioName) {
var id = SUBSCRIPTION_ID;
if (scenarioName){
var scenario = await db.scenario.getOne({"name": scenarioName});
id = scenario.subscription? scenario.subscription.subsId : SUBSCRIPTION_ID;
}
var credentials = await loginWithVmMSI({ port: 50342 });
//console.log("AzureCLI# authenticated", credentials);
return new dnsManagementClient(credentials, id);
}
async function asyncForEach(array, callback) {
for (let index = 0; index < array.length; index++) {
@@ -29,60 +42,74 @@ async function asyncForEach(array, callback) {
}
}
async function deallocate(provId, isSendEmailAfter ) {
let provision = await db.provision.getById(provId);
if ( !provision ) return;
async function deallocate(provision, triggerUserId, isSendEmailAfter ) {
let rgName = _getRgName(provision);
console.log("AzureCLI# Deallocating VMs for resource group: "+rgName);
var computeClient = await _getClient(provision.scenario);
let finalResult = await computeClient.virtualMachines.list(rgName);
if ( finalResult && finalResult.length > 0 ) {
db.provision.update(provision._id, {"statusVms": "Stopping"});
}
await asyncForEach(finalResult, async function(vm) {
await computeClient.virtualMachines.deallocate(rgName, vm.name);
});
let timeRunning = db.utils.getNewTimeRunning(provision);
await db.provision.update(provision._id.toString(), {"statusVms": "Stopped", "timeRunning": timeRunning, "stoppedFrom": new Date(), "pendingNextAction": undefined});
if ( isSendEmailAfter && provision._scenarioDoc ) {
await sendEmail.sendVMsStopped(provision, provision._scenarioDoc);
try {
var computeClient = await _getClient(provision.scenario);
let finalResult = await computeClient.virtualMachines.list(rgName);
if ( finalResult && finalResult.length > 0 ) {
db.provision.update(provision._id, {"statusVms": "Stopping"});
}
await asyncForEach(finalResult, async function(vm) {
await computeClient.virtualMachines.deallocate(rgName, vm.name);
});
await utils.afterStopVms(provision, triggerUserId, isSendEmailAfter);
console.log("AzureCLI# All VMs DEALLOCATED for resource group: "+rgName);
} catch ( error ) {
console.log("AzureCLI# ERROR stopping VMs: "+rgName, error);
await db.provision.update(provision._id.toString(), {"statusVms": "Running" });
}
console.log("AzureCLI# All VMs DEALLOCATED for resource group: "+rgName);
}
async function start(provId){
let provision = await db.provision.getById(provId);
if ( !provision ) return;
async function start(provision, triggerUserId){
let rgName = _getRgName(provision);
console.log("AzureCLI# Starting VMs for resource group: "+rgName);
var computeClient = await _getClient(provision.scenario);
let finalResult = await computeClient.virtualMachines.list(rgName);
try {
if ( finalResult && finalResult.length > 0 ) {
db.provision.update(provision._id, {"statusVms": "Starting"});
var computeClient = await _getClient(provision.scenario);
let finalResult = await computeClient.virtualMachines.list(rgName);
if ( finalResult && finalResult.length > 0 ) {
db.provision.update(provision._id, {"statusVms": "Starting"});
}
await asyncForEach(finalResult, async function(vm) {
await computeClient.virtualMachines.start(rgName, vm.name);
});
await utils.afterStartVms( provision, triggerUserId );
console.log("AzureCLI# All VMs RUNNING for resource group: "+rgName);
} catch ( error ) {
console.log("AzureCLI# ERROR starting VMs: "+rgName, error);
await db.provision.update(provision._id.toString(), {"statusVms": "Stopped" });
}
await asyncForEach(finalResult, async function(vm) {
await computeClient.virtualMachines.start(rgName, vm.name);
});
let countExtend = db.utils.getNewCountExtend(provision);
await db.provision.update(provision._id.toString(), {"statusVms": "Running", "runningFrom": new Date(), "countExtend": countExtend, "pendingNextAction": undefined});
console.log("AzureCLI# All VMs RUNNING for resource group: "+rgName);
}
async function getResourceGroupVms(rgName){
var computeClient = await _getClient();
let computeClient = await _getClient();
return await computeClient.virtualMachines.list(rgName);
}
async function getAllVms(){
var computeClient = await _getClient();
return await computeClient.virtualMachines.listAll(rgName);
let computeClient = await _getClient();
console.log("AzureCLI# Retrieving all VMS from subscription");
return await computeClient.virtualMachines.listAll();
}
async function getAllVmsNext(nextLink){
let computeClient = await _getClient();
console.log("AzureCLI# Retrieving all VMS from subscription (next): " +nextLink);
return await computeClient.virtualMachines.listAllNext(nextLink)
}
async function updateVmsTags(provId, tagsEdit) {
@@ -94,8 +121,16 @@ async function updateVmsTags(provId, tagsEdit) {
let rgName = _getRgName(provision);
console.log("AzureCLI# Updating TAGS in VMs for resource group: "+rgName);
var computeClient = await _getClient(provision.scenario);
let finalResult = await computeClient.virtualMachines.list(rgName);
var computeClient;
var finalResult;
try {
computeClient = await _getClient(provision.scenario);
finalResult = await computeClient.virtualMachines.list(rgName);
} catch (e2) {
console.log(`AzureCLI# ERROR: Resource group '${rgName}' could not be found`, e2);
return {};
}
if (finalResult && finalResult.length > 0) {
var toDelete = [];
@@ -111,6 +146,8 @@ async function updateVmsTags(provId, tagsEdit) {
if ( toDelete.length > 0 || toAdd.length > 0 ) {
await asyncForEach(finalResult, async function(vm) {
var vmActualTags = JSON.stringify(vm.tags);
var tags = vm.tags;
toDelete.forEach(t=>{
delete tags[t];
@@ -120,11 +157,23 @@ async function updateVmsTags(provId, tagsEdit) {
});
tags["ProvId"] = provision._id.toString();
result[vm.name] = tags;
console.log(`AzureCLI# VM ${vm.name} new tags: `, tags );
computeClient.virtualMachines.update(rgName, vm.name, {"tags": tags} );
if ( JSON.stringify(tags) !== vmActualTags ) {
try {
console.log(`AzureCLI# setting new tags to VM ${vm.name} (${rgName})`);
await computeClient.virtualMachines.update(rgName, vm.name, {"tags": tags} );
} catch (e1) {
console.log(`AzureCLI# ERROR setting tags to VM ${vm.name} (${rgName})`, e1);
}
} else {
console.log(`AzureCLI# Same tags, no need to update tags for VM ${vm.name} (${rgName})`);
}
});
}
//db.event.add({ user: provision.user._id, provision: provision._id, type: 'provision.vms-tags-update' });
return result;
} else {
@@ -132,8 +181,53 @@ async function updateVmsTags(provId, tagsEdit) {
}
};
/**
* DNS Records
*/
const DNS_RESOURCE_GROUP = "QMI-Infra-DNS";
const DNS_ZONE_NAME = "qmi.qlik-poc.com";
async function getDNSRecords(type = "CNAME") {
let dnsClient = await _getDNSClient();
console.log(`AzureCLI# Retrieving '${type}' DNS records`);
return await dnsClient.recordSets.listByType(DNS_RESOURCE_GROUP, DNS_ZONE_NAME, type);
}
async function createDNSRecord(provision, cname, type = "CNAME"){
if ( !provision ) return;
let rgName = _getRgName(provision).toLowerCase();
let dnsClient = await _getDNSClient();
console.log(`AzureCLI# Creating '${type}' DNS record: ${rgName}.${DNS_ZONE_NAME} --> ${cname}`);
let parameters = {
"cnameRecord": {
"cname": cname
},
"tTL": 60
};
return await dnsClient.recordSets.createOrUpdate(DNS_RESOURCE_GROUP, DNS_ZONE_NAME, rgName, type, parameters );
}
async function deleteDNSRecord(provision, type = "CNAME"){
if ( !provision ) return;
let rgName = _getRgName(provision).toLowerCase();
let dnsClient = await _getDNSClient();
console.log(`AzureCLI# Deleting '${type}' DNS record: ${rgName}.${DNS_ZONE_NAME}`);
return await dnsClient.recordSets.deleteMethod(DNS_RESOURCE_GROUP, DNS_ZONE_NAME, rgName, type );
}
module.exports.start = start;
module.exports.deallocate = deallocate;
module.exports.getResourceGroupVms = getResourceGroupVms;
module.exports.getAllVms = getAllVms;
module.exports.updateVmsTags = updateVmsTags;
module.exports.getAllVmsNext = getAllVmsNext;
module.exports.updateVmsTags = updateVmsTags;
module.exports.getDNSRecords = getDNSRecords;
module.exports.createDNSRecord = createDNSRecord;
module.exports.deleteDNSRecord = deleteDNSRecord

View File

@@ -0,0 +1,85 @@
const BarracudaAPI = require('barracuda-api');
const azurecli = require('./azurecli');
const db = require('./mongo');
const DOMAIN = "qmi.qlik-poc.com";
async function createApp(prov) {
if ( !prov._scenarioDoc.barracudaTemplate || prov._scenarioDoc.barracudaTemplate === 'qs-only-443' ) {
_createApp(prov, "443", "HTTPS");
} else if ( prov._scenarioDoc.barracudaTemplate === 'qdc-only-8443' ) {
_createApp(prov, "8443", "HTTPS");
} else if (prov._scenarioDoc.barracudaTemplate) {
var spl = prov._scenarioDoc.barracudaTemplate.split("-");
_createApp(prov, spl[0], spl[1]);
}
}
async function _createApp(provision, backendPort, backendType) {
if ( !provision || !provision.barracudaAzureFqdn ) {
console.log(`Barracuda# Provision does not exist or does not have a barracudaAzureFqdn value. Do nothing.`);
return;
}
let appName = provision.scenario.toLowerCase();
appName = appName.replace(/azqmi/g, 'qmi');
appName = `${appName}-${provision._id}`
console.log(`Barracuda# Creating Barracuda App for provision (${appName})`);
const client = new BarracudaAPI.QlikSenseNPrintingApp();
client.create(appName, `${appName}.${DOMAIN}`, provision.barracudaAzureFqdn, backendPort, backendType).then(function(result) {
let bApp = result.app;
console.log("Barracuda# Barracuda App created! ID: "+ bApp.id);
let cname = bApp.waas_services && bApp.waas_services.length? bApp.waas_services[0].cname : null;
db.provision.update(provision._id, {"barracudaAppId": bApp.id, "barracudaAppCname":cname});
console.log("Barracuda# Creating DNS record in Azure: "+cname);
azurecli.createDNSRecord(provision, cname);
db.event.add({ provision: provision._id, type: 'provision.is-public' });
}).catch(function(err){
console.log("Barracuda# Error creating Barracuda App", err);
});
}
async function deleteApp(provision) {
if ( !provision || !provision.barracudaAppId ) {
console.log(`Barracuda# Provision does not exist or does not have a barracudaAppId value. Do nothing.`);
return;
}
console.log(`Barracuda# Deleting DNS record in Azure and Barracuda App ID (${provision.barracudaAppId}) for provision (${provision._id})`);
azurecli.deleteDNSRecord(provision);
const client = new BarracudaAPI.QlikSenseNPrintingApp();
client.delete(provision.barracudaAppId).then(function() {
console.log("Barracuda# Barracuda App deleted! ID: "+ provision.barracudaAppId);
db.event.add({ provision: provision._id, type: 'provision.is-private' });
}).catch(function(err){
console.log("Barracuda# Error deleting Barracuda App", err);
});
}
async function getApp(provision) {
if ( !provision || !provision.barracudaAppId ) {
console.log(`Barracuda# Provision does not exist or does not have a barracudaAppId value. Do nothing.`);
return;
}
const client = new BarracudaAPI.QlikSenseNPrintingApp();
return await client.getDetails(provision.barracudaAppId);
}
module.exports.getApp = getApp;
module.exports.createApp = createApp;
module.exports.deleteApp = deleteApp;

72
qmi-cloud-common/cli.js Normal file
View File

@@ -0,0 +1,72 @@
const awscli = require("./awscli");
const azurecli = require("./azurecli");
const db = require("./mongo");
async function deallocate(provId, userId, isSendEmailAfter ) {
try {
let provision = await db.provision.getById(provId);
if ( !provision ) return;
if (provision.scenario === 'azqmi-fort'){
return awscli.deallocate(provision, userId, isSendEmailAfter);
} else {
return azurecli.deallocate(provision, userId, isSendEmailAfter);
}
} catch (err) {
console.log("CLI# ERROR stopping VMs", err);
}
}
async function start(provId, userId){
try {
let provision = await db.provision.getById(provId);
if ( !provision ) return;
if (provision.scenario === 'azqmi-fort'){
return awscli.start(provision, userId);
} else {
return azurecli.start(provision, userId);
}
} catch (err) {
console.log("CLI# ERROR starting VMs", err);
}
}
async function updateVmsTags(provId, tagsEdit) {
try {
return azurecli.updateVmsTags(provId, tagsEdit);
} catch (err) {
console.log("CLI# ERROR updateVmsTags", err);
}
}
async function stopDb(provId, userId, isSendEmailAfter) {
try {
let provision = await db.provision.getById(provId);
if ( !provision ) return;
return awscli.stopDbInstance(provision, userId, isSendEmailAfter);
} catch (err) {
console.log("CLI# ERROR stopping DB", err);
}
}
async function startDb(provId, userId) {
try {
let provision = await db.provision.getById(provId);
if ( !provision ) return;
return awscli.startDbInstance(provision, userId);
} catch (err) {
console.log("CLI# ERROR stopping DB", err);
}
}
module.exports.deallocate = deallocate;
module.exports.start = start;
module.exports.updateVmsTags = updateVmsTags;
module.exports.stopDb = stopDb;
module.exports.startDb = startDb;

View File

@@ -1,13 +1,14 @@
const mongoose = require('mongoose');
mongoose.set('useFindAndModify', false);
const crypto = require("crypto");
//mongoose.set('debug', true)
const schema = new mongoose.Schema({
user: {
type: mongoose.Types.ObjectId, ref: 'User'
},
description: {
type: String
},
created: {
type: Date,
default: Date.now,

View File

@@ -1,6 +1,4 @@
const mongoose = require('mongoose');
mongoose.set('useFindAndModify', false);
//mongoose.set('debug', true)
const destroySchema = new mongoose.Schema({

View File

@@ -0,0 +1,27 @@
const mongoose = require('mongoose');
const schema = new mongoose.Schema({
created: {
type: Date,
default: Date.now,
index : true
},
user: {
type: mongoose.Types.ObjectId,
ref: 'User'
},
provision: {
type: mongoose.Types.ObjectId,
ref: 'Provision',
index : true
},
type: {
type: String
},
message: {
type: String
}
});
module.exports = mongoose.model('Event', schema)

View File

@@ -1,7 +1,4 @@
const mongoose = require('mongoose');
mongoose.set('useFindAndModify', false);
//mongoose.set('debug', true)
const sc = new mongoose.Schema({
created: {

View File

@@ -1,10 +1,9 @@
const mongoose = require('mongoose');
mongoose.set('useFindAndModify', false);
//mongoose.set('debug', true)
const provisionSchema = new mongoose.Schema({
user: {
type: mongoose.Types.ObjectId, ref: 'User',
type: mongoose.Types.ObjectId,
ref: 'User',
index: true
},
created: {
@@ -26,6 +25,7 @@ const provisionSchema = new mongoose.Schema({
},
description: String,
vmImage: Object,
options: Object,
status: {
type: String,
default: "queued"
@@ -38,6 +38,15 @@ const provisionSchema = new mongoose.Schema({
type: Boolean,
default: false
},
barracudaAppId: {
type: String
},
barracudaAppCname: {
type: String
},
barracudaAzureFqdn: {
type: String
},
isDestroyed: {
type: Boolean,
default: false
@@ -66,6 +75,12 @@ const provisionSchema = new mongoose.Schema({
type: Number,
default: 0
},
startDateOnSchedule: {
type: Date
},
endDateOnSchedule: {
type: Date
},
countExtend: {
type: Number,
default: 0
@@ -82,8 +97,21 @@ const provisionSchema = new mongoose.Schema({
ref: "Subscription"
},
terraformImage: {
type: String,
default: "qlikgear/terraform:0.13.4"
type: String
},
version: {
type: Number
},
parent: {
type: mongoose.Types.ObjectId,
ref: 'Provision'
},
runForever: {
type: Boolean,
default: false
},
guacaConnId: {
type: String
}
},{
toObject: {virtuals:true},

View File

@@ -1,6 +1,4 @@
const mongoose = require('mongoose')
mongoose.set('useFindAndModify', false);
//mongoose.set('debug', true)
const scenarioSchema = new mongoose.Schema({
@@ -35,6 +33,10 @@ const scenarioSchema = new mongoose.Schema({
type: Boolean,
default: false
},
forceExternalAccess: {
type: Boolean,
default: false
},
isDivvyEnabled: {
type: Boolean,
default: false
@@ -50,6 +52,9 @@ const scenarioSchema = new mongoose.Schema({
type: String,
required: true
},
gitBranch: {
type: String
},
description: String,
availableProductVersions: Array,
productVersionDefault: String,
@@ -65,7 +70,33 @@ const scenarioSchema = new mongoose.Schema({
}],
labels: [{
type: String
}]
}],
allowedUsers: [{
type: mongoose.Types.ObjectId,
ref: 'User',
required: true
}],
support: [{
type: String
}],
barracudaTemplate: {
type: String
},
allowedInnactiveDays: {
type: Number,
default: 20
},
allowed24x7RunningDays: {
type: Number,
default: 7
},
allowedOnScheduleRunningDays: {
type: Number,
default: 4
},
terraformImage: {
type: String
}
});

View File

@@ -1,6 +1,4 @@
const mongoose = require('mongoose');
mongoose.set('useFindAndModify', false);
//mongoose.set('debug', true)
const sc = new mongoose.Schema({
@@ -13,10 +11,6 @@ const sc = new mongoose.Schema({
type: Boolean,
default: true
},
onScheduleRenewed: {
type: Number,
default: 1
},
localeShutdownTime: {
type: String
},

View File

@@ -0,0 +1,35 @@
const mongoose = require('mongoose')
const schema = new mongoose.Schema({
created: {
type: Date,
default: Date.now,
index : true
},
updated: {
type: Date,
default: Date.now
},
user: {
type: mongoose.Types.ObjectId,
ref: 'User',
index: true
},
provision: {
type: mongoose.Types.ObjectId,
ref: 'Provision',
index: true
},
sharedWithUser: {
type: mongoose.Types.ObjectId,
ref: 'User',
index: true
},
canManage: {
type: Boolean,
default: false
}
});
module.exports = mongoose.model('sharedProvision', schema);

View File

@@ -1,6 +1,4 @@
const mongoose = require('mongoose')
mongoose.set('useFindAndModify', false);
//mongoose.set('debug', true)
const subSchema = new mongoose.Schema({
created: {

View File

@@ -0,0 +1,67 @@
const mongoose = require('mongoose');
const crypto = require("crypto");
const schema = new mongoose.Schema({
description: {
type: String
},
user: {
type: mongoose.Types.ObjectId,
ref: 'User'
},
template: {
type: mongoose.Types.ObjectId,
ref: 'TrainingTemplate'
},
created: {
type: Date,
default: Date.now
},
updated: {
type: Date,
default: Date.now
},
status: {
type: String,
default: "inactive" //active, inactive, terminated
},
qcsTenantHost: {
type: String
},
qcsApiKey: {
type: String
},
qaUrl: {
type: String
},
qaToken: {
type: String
},
cloudshareClass: {
type: String
},
qcsSharedSpace: {
type: String
},
qcsDataSpace: {
type: String
},
passwd: {
type: String,
default: function() {
return crypto.randomBytes(4).toString('hex');
}
},
studentsCount: {
type: Number,
default: 0
},
studentEmailFilter: {
type: String // qlik.com,talend.com,gmail.com
}
});
module.exports = mongoose.model('TrainingSession', schema);

View File

@@ -0,0 +1,22 @@
const mongoose = require('mongoose');
const schema = new mongoose.Schema({
created: {
type: Date,
default: Date.now
},
email: {
type: String
},
session: {
type: mongoose.Types.ObjectId, ref: 'TrainingSession'
},
status: {
type: String,
default: 'pending'
}
});
module.exports = mongoose.model('TrainingStudent', schema);

View File

@@ -0,0 +1,35 @@
const mongoose = require('mongoose');
const schema = new mongoose.Schema({
index: {
type: String
},
created: {
type: Date,
default: Date.now
},
title: {
type: String
},
description: {
type: String
},
publicDescription: {
type: String
},
cloudshare: {
type: Boolean
},
qcs: {
type: Boolean
},
needQcsAutomation: {
type: Array //['main']
},
needQcsApiKey: {
type: Boolean
}
});
module.exports = mongoose.model('TrainingTemplate', schema);

View File

@@ -1,6 +1,4 @@
const mongoose = require('mongoose')
mongoose.set('useFindAndModify', false);
//mongoose.set('debug', true)
const userSchema = new mongoose.Schema({
@@ -14,7 +12,11 @@ const userSchema = new mongoose.Schema({
default: Date.now
},
displayName: String,
upn: String,
upn: {
type: String,
index: true
},
sub: String,
oid: {
type: String,
index: true
@@ -31,6 +33,19 @@ const userSchema = new mongoose.Schema({
},
qcsUserSubject: {
type: String
},
active: {
type: Boolean,
default: true
},
jobTitle: {
type: String
},
mail: {
type: String
},
featureFlags: {
type: Array
}
});

View File

@@ -1,7 +1,4 @@
const mongoose = require('mongoose')
mongoose.set('useFindAndModify', false);
//mongoose.set('debug', true)
const userSchema = new mongoose.Schema({
type: String,

View File

@@ -1,18 +1,10 @@
const mongoose = require('mongoose');
const boom = require('@hapi/boom');
const options = {
loggerLevel: 'error',
useNewUrlParser: true,
//reconnectInterval: 2000,
//reconnectTries: 30, // Retry up to 30 times
useCreateIndex: true,
useUnifiedTopology: true
};
console.log("--- MongoDB connecting... ", process.env.MONGO_URI);
// Connect to DB
mongoose.connect(process.env.MONGO_URI, options);
mongoose.connect(process.env.MONGO_URI);
// When successfully connected
mongoose.connection.on('connected', () => {
@@ -42,7 +34,11 @@ const VmType = require('./models/VmType');
const ApiKey = require('./models/ApiKey');
const Notification = require('./models/Notification');
const Subscription = require('./models/Subscription');
const Event = require('./models/Event');
const SharedProvision = require('./models/SharedProvision');
const TrainingTemplate = require('./models/TrainingTemplate');
const TrainingSession = require('./models/TrainingSession');
const TrainingStudent = require('./models/TrainingStudent');
const getNewCountExtend = function(provision) {
return provision.countExtend !== undefined? (provision.countExtend + 1) : 1;
@@ -93,14 +89,25 @@ const getPage = async ( model, filter, page, populates, select ) => {
if ( model === Provision ) {
exec = exec.populate({ path: 'user', select: 'displayName upn'}).populate({path:'destroy', select: "-user -jobId"}).populate({path:'_scenarioDoc', select: "-availableProductVersions -updated -created"}).populate({path: "schedule"}).populate('deployOpts');
}
if ( model === SharedProvision ) {
exec = exec.populate('provision').populate({path: 'sharedWithUser', select: 'displayName upn oid active'}).populate({path: 'user', select: 'displayName upn'});
}
if ( model === ApiKey ) {
exec = exec.populate('user');
}
if ( model = Scenario ) {
if ( model === Scenario ) {
exec = exec.populate('subscription').populate('deployOpts');
}
if ( model === Event ) {
exec = exec.populate({path: 'user', select: 'displayName'});
}
if ( model === TrainingSession ) {
exec = exec.populate('user').populate('template');
}
}
const entity = await exec;
@@ -128,8 +135,15 @@ const getPage = async ( model, filter, page, populates, select ) => {
const get = async (model, filter, select, skip, limit, populates, reply) => {
var sort = {};
var modelAttributes = Object.keys(model.schema.tree);
if ( model === Scenario ) {
if ( modelAttributes.indexOf("updated") !== -1) {
sort.updated = -1;
}
}
if ( modelAttributes.indexOf("created") !== -1) {
sort = {created: -1};
sort.created = -1;
}
try {
var exec = model.find(filter, select).sort(sort);
@@ -152,13 +166,32 @@ const get = async (model, filter, select, skip, limit, populates, reply) => {
if ( model === Provision ) {
exec = exec.populate({ path: 'user', select: 'displayName upn'}).populate({path:'destroy', select: "-user -jobId"}).populate({path:'_scenarioDoc', select: "-availableProductVersions -updated -created"}).populate({path: "schedule"}).populate('deployOpts');
}
if ( model === SharedProvision ) {
exec = exec.populate({path:'provision', populate: [{
path: 'schedule',
},{
path: '_scenarioDoc',
select: "-availableProductVersions -updated -created"
},{
path: 'destroy',
select: "-user -jobId"
}]}).populate({path: 'sharedWithUser', select: 'displayName upn oid active'}).populate({path: 'user', select: 'displayName upn'});
}
if ( model === ApiKey ) {
exec = exec.populate('user');
}
if ( model = Scenario ) {
exec = exec.populate('subscription').populate('deployOpts');
if ( model === Scenario ) {
exec = exec.populate('subscription').populate('deployOpts').populate('allowedUsers');
}
if ( model === Event ) {
exec = exec.populate({path: 'user', select: 'displayName'});
}
if ( model === TrainingSession ) {
exec = exec.populate('user').populate('template');
}
}
@@ -185,11 +218,20 @@ const getById = async (model, id, reply) => {
if ( model === Provision ) {
exec = exec.populate('user').populate('destroy').populate('_scenarioDoc').populate("schedule").populate('deployOpts');
}
if ( model === SharedProvision ) {
exec = exec.populate('provision').populate({path: 'sharedWithUser', select: 'displayName upn oid active'}).populate({path: 'user', select: 'displayName upn'});
}
if ( model === ApiKey ) {
exec = exec.populate('user');
}
if ( model = Scenario ) {
exec = exec.populate('subscription').populate('deployOpts');
if ( model === Scenario ) {
exec = exec.populate('subscription').populate('deployOpts').populate('allowedUsers');
}
if ( model === Event ) {
exec = exec.populate({path: 'user', select: 'displayName'});
}
if ( model === TrainingSession ) {
exec = exec.populate('user').populate('template');
}
const entity = await exec;
return entity;
@@ -204,11 +246,20 @@ const getOne = async (model, filter, reply) => {
if ( model === Provision ) {
exec = exec.populate('user').populate('destroy').populate('_scenarioDoc').populate("schedule").populate('deployOpts');
}
if ( model === SharedProvision ) {
exec = exec.populate('provision').populate({path: 'sharedWithUser', select: 'displayName upn oid active'}).populate({path: 'user', select: 'displayName upn'});
}
if ( model === ApiKey ) {
exec = exec.populate('user');
}
if ( model = Scenario ) {
exec = exec.populate('subscription').populate('deployOpts');
if ( model === Scenario ) {
exec = exec.populate('subscription').populate('deployOpts').populate('allowedUsers');
}
if ( model === Event ) {
exec = exec.populate({path: 'user', select: 'displayName'});
}
if ( model === TrainingSession ) {
exec = exec.populate('user').populate('template');
}
const entity = await exec;
return entity;
@@ -235,12 +286,18 @@ const update = async (model, id, body, reply) => {
if ( model === Provision ) {
exec = exec.populate('user').populate('destroy').populate('_scenarioDoc').populate("schedule").populate('deployOpts');
}
if ( model === SharedProvision ) {
exec = exec.populate('provision').populate({path: 'sharedWithUser', select: 'displayName upn oid active'}).populate({path: 'user', select: 'displayName upn'});
}
if ( model === ApiKey ) {
exec = exec.populate('user');
}
if ( model = Scenario ) {
if ( model === Scenario ) {
exec = exec.populate('subscription').populate('deployOpts');
}
if ( model === TrainingSession ) {
exec = exec.populate('user').populate('template');
}
const update = await exec;
return update;
} catch (err) {
@@ -248,10 +305,57 @@ const update = async (model, id, body, reply) => {
}
};
const updateMany = async (model, filter, body, reply) => {
try {
const { ...updateData } = body;
updateData.updated = new Date();
var exec = model.updateMany(filter, updateData);
if ( model === Provision ) {
exec = exec.populate('user').populate('destroy').populate('_scenarioDoc').populate("schedule").populate('deployOpts');
}
if ( model === SharedProvision ) {
exec = exec.populate('provision').populate({path: 'sharedWithUser', select: 'displayName upn oid active'}).populate({path: 'user', select: 'displayName upn'});
}
if ( model === ApiKey ) {
exec = exec.populate('user');
}
if ( model === Scenario ) {
exec = exec.populate('subscription').populate('deployOpts');
}
if ( model === TrainingSession ) {
exec = exec.populate('user').populate('template');
}
return await exec;
} catch (err) {
throw boom.boomify(err)
}
};
const del = async (model, id, reply) => {
try {
const entity = await model.findByIdAndRemove(id)
return entity;
return await model.findByIdAndRemove(id);
} catch (err) {
throw boom.boomify(err)
}
}
const delMany = async(model, filter, reply) => {
try {
return await model.deleteMany(filter);
} catch (err) {
throw boom.boomify(err)
}
}
const count = async (model, filter, reply) => {
try {
var totalDocs = await model.countDocuments(filter);
return totalDocs;
} catch (err) {
throw boom.boomify(err)
}
@@ -277,8 +381,17 @@ function _m(model) {
update: async (id, data, reply) => {
return update(model, id, data, reply);
},
updateMany: async(filter, data, reply) => {
return updateMany(model, filter, data, reply);
},
del: async (id, reply) => {
return del(model, id, reply);
},
count: async (filter, reply) => {
return count(model, filter, reply);
},
delMany: async(filter, reply) => {
return delMany(model, filter, reply);
}
}
}
@@ -293,7 +406,12 @@ module.exports = {
apiKey: _m(ApiKey),
notification: _m(Notification),
subscription: _m(Subscription),
event: _m(Event),
user: _m(User),
sharedProvision: _m(SharedProvision),
trainingSession: _m(TrainingSession),
trainingTemplate: _m(TrainingTemplate),
trainingStudent: _m(TrainingStudent),
utils: {
getNewTimeRunning: getNewTimeRunning,
getNewCountExtend: getNewCountExtend
@@ -308,7 +426,12 @@ module.exports = {
VmType: VmType,
Notification: Notification,
ApiKey: ApiKey,
Subscription: Subscription
Subscription: Subscription,
Event: Event,
SharedProvision: SharedProvision,
TrainingSession: TrainingSession,
TrainingTemplate: TrainingTemplate,
TrainingStudent: TrainingStudent
}
};

1315
qmi-cloud-common/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,17 @@
{
"name": "qmi-cloud-common",
"version": "1.1.5",
"version": "2.0.0",
"dependencies": {
"@azure/arm-compute": "^15.0.0",
"@azure/arm-dns": "^4.0.0",
"@azure/ms-rest-nodeauth": "^3.0.7",
"@hapi/boom": "^9.1.0",
"mongoose": "^5.7.4",
"aws-sdk": "^2.942.0",
"axios": "^0.21.1",
"barracuda-api": "https://gitlab.com/qlik_gear/barracuda-api-node.git#1.1.0",
"bull": "^3.11.0",
"mongoose": "^6.11.1",
"nodemailer": "^6.4.2",
"azure-arm-compute": "^10.0.0",
"ms-rest-azure": "^3.0.0",
"bull": "^3.11.0"
"uuid": "^8.3.2"
}
}
}

View File

@@ -4,25 +4,30 @@ const Queue = require('bull');
export const TF_APPLY_QUEUE = 'TF_APPLY_QUEUE';
export const TF_DESTROY_QUEUE = 'TF_DESTROY_QUEUE';
export const TF_APPLY_QSEOK_QUEUE = 'TF_APPLY_QSEOK_QUEUE';
export const STOP_CONTAINER_QUEUE = 'STOP_CONTAINER_QUEUE';
export const SYNAPSE_QUEUE = 'SYNAPSE_QUEUE';
var terraformApplyQueue = new Queue(TF_APPLY_QUEUE, process.env.REDIS_URL);
var terraformDestroyQueue = new Queue(TF_DESTROY_QUEUE, process.env.REDIS_URL);
var terraformApplyQseokQueue = new Queue(TF_APPLY_QSEOK_QUEUE, process.env.REDIS_URL);
var stopContainerQueue = new Queue(STOP_CONTAINER_QUEUE, process.env.REDIS_URL);
var synapseQueue = new Queue(SYNAPSE_QUEUE, process.env.REDIS_URL);
export const queues = {
[TF_APPLY_QUEUE]: terraformApplyQueue,
[TF_DESTROY_QUEUE]: terraformDestroyQueue,
[TF_APPLY_QSEOK_QUEUE]: terraformApplyQseokQueue
[TF_APPLY_QSEOK_QUEUE]: terraformApplyQseokQueue,
[STOP_CONTAINER_QUEUE]: stopContainerQueue,
[SYNAPSE_QUEUE]: synapseQueue
};
for (let key in queues) {
queues[key].on('completed', function(job, result) {
//console.log(`Queues# Job ${job.id} completed! Result`, result);
console.log(`Queues# Job ${job.id} completed!`);
console.log(`Queue ${key}# Job ${job.id} completed!`);
});
@@ -31,13 +36,13 @@ for (let key in queues) {
});
queues[key].on('waiting', function(jobId){
console.log(`Queues# Job ${jobId} is waiting...`);
console.log(`Queue ${key}# Job ${jobId} is waiting...`);
// A Job is waiting to be processed as soon as a worker is idling.
});
queues[key].on('active', function(job, jobPromise){
// A job has started. You can use `jobPromise.cancel()`` to abort it.
console.log(`Queues# Job ${job.id} is now active`);
console.log(`Queue ${key}# Job ${job.id} is now active`);
});
queues[key].on('stalled', function(job){
@@ -47,12 +52,12 @@ for (let key in queues) {
queues[key].on('progress', function(job, progress){
// A job's progress was updated!
console.log(`Queues# Job ${job.id} is ${progress * 100}% ready!`);
console.log(`Queue ${key}# Job ${job.id} is ${progress * 100}% ready!`);
});
queues[key].on('failed', function(job, err){
// A job failed with reason `err`!
console.log(`Queues# Job ${job.id} has failed:`, err);
console.log(`Queue ${key}# Job ${job.id} has failed:`, err);
});
}

View File

@@ -0,0 +1,278 @@
'use strict';
const nodemailer = require('nodemailer');
const FROM = '"Qlik" <no-reply@qlik.com>';
var transporter;
const HOSTNAME_URL = process.env.HOSTNAME_URL || "https://qmicloud.qliktech.com";
const FOOTER = `<div style="color:#404040;font-size:16px;margin:30px 0px">
<p style="margin:0px">Check it out at <a href="${HOSTNAME_URL}">${HOSTNAME_URL}</a></p>
</div>`;
if ( process.env.GMAIL_USERNAME && process.env.GMAIL_PASSWORD ) {
//GMAIL
transporter = nodemailer.createTransport({
service: 'gmail',
port: 587,
secure: false,
auth: {
user: process.env.GMAIL_USERNAME,
pass: process.env.GMAIL_PASSWORD
}
});
} else {
//QLIK
transporter = nodemailer.createTransport({
host: 'smtp.qliktech.com',
port: 587,
secure: false, // true for 465, false for other ports
});
}
async function _doSend(to, subject, htmlText) {
// send mail with defined transport object
try {
let info = await transporter.sendMail( {
from: FROM, // sender address
to: to, // list of receivers
subject: subject, // Subject line
text: subject, // plain text body
html: htmlText // html body
} );
console.log('SendEmail# message id ('+info.messageId+') sent to: ' + to);
return info;
} catch (err) {
console.log('SendEmail# ERROR!! -> could not send email to: ' + to);
console.log(err);
}
}
function _getCommonDetails(provision, scenario){
var description = decodeURI(scenario.description);
var externalAccess = provision.isExternalAccess? 'Yes' : 'No';
var schedule = "";
if ( !provision.schedule || provision.schedule.is24x7 ) {
schedule = "24x7";
} else if ( provision.schedule && !provision.schedule.is24x7 ) {
schedule = `from ${provision.schedule.localeStartupTime}h until ${provision.schedule.localeShutdownTime}h (${provision.schedule.localTimezone})`;
}
return `<div style="color:#404040;font-size:18px;margin:20px 0px">
<p style="margin:0px">Provision information:</p>
</div>
<div>
<span style="color:#404040">ID: </span> ${provision._id}
</div>
<div>
<span style="color:#404040">VMs Running schedule: </span> ${schedule}
</div>
<div>
<span style="color:#404040">Purpose: </span> ${provision.description}
</div>
<div>
<span style="color:#404040">Scenario: </span> ${scenario.title}
</div>
<div>
<span style="color:#404040">With external access: </span> ${externalAccess}
</div>
<div>
<span style="color:#404040">Description: </span> ${description}
</div>`;
}
function getHtmlScenarioDestroyIn24( provision, scenario, period, warningDays) {
var common = _getCommonDetails(provision,scenario);
return`<div style="width:600px;color:black!important;font-family:'Source Sans Pro',sans-serif;padding:50px">
<div style="background-color:white;height:100%;padding:20px 10px">
<div style="color:#404040;font-size:34px;text-align:center;margin:20px">
<p style="margin:0px">QMI Cloud</p>
</div>
<div style="color:#404040;font-size:22px;margin:20px 0px 40px 0px">
<p style="margin:0px">Provision '${scenario.title}' inactive more than ${period} days</p>
</div>
<div style="color:#404040;font-size:18px;margin:10px 0px">
<p style="margin:0px;color: #FF2020">This scenario will be automatically DESTROYED in ${(warningDays*24)} hours.</p>
</div>
<div style="color:#404040;font-size:16px;margin:30px 0px">
<p style="margin:0px">If you don't want this to happen, you've got ${(warningDays*24)} hours (from when this email was sent) as a grace period to get back at 'Running' status this provision.</p>
</div>
${common}
${FOOTER}
</div>
</div>`;
}
function getHtmlScenarioVMsStopped( provision, scenario) {
var common = _getCommonDetails(provision,scenario);
return `<div style="width:600px;color:black!important;font-family:'Source Sans Pro',sans-serif;padding:50px">
<div style="background-color:white;height:100%;padding:20px 10px">
<div style="color:#404040;font-size:34px;text-align:center;margin:20px">
<p style="margin:0px">QMI Cloud</p>
</div>
<div style="color:#404040;font-size:22px;margin:20px 0px 40px 0px">
<p style="margin:0px">Provision '${scenario.title}'</p>
</div>
<div style="color:#404040;font-size:18px;margin:10px 0px">
<p style="margin:0px;color: #FF2020">All VMs for this provision <b>stopped</b> automatically.</p>
</div>
<div style="color:#404040;font-size:16px;margin:30px 0px">
<p style="margin:0px">You can start them up again from <a href="${HOSTNAME_URL}">${HOSTNAME_URL}</a></p>
</div>
${common}
${FOOTER}
</div>
</div>`;
}
function getHtmlScenarioWillStopIn24( provision, scenario, period, warningDays ) {
var common = _getCommonDetails(provision,scenario);
return`<div style="width:600px;color:black!important;font-family:'Source Sans Pro',sans-serif;padding:50px">
<div style="background-color:white;height:100%;padding:20px 10px">
<div style="color:#404040;font-size:34px;text-align:center;margin:20px">
<p style="margin:0px">QMI Cloud</p>
</div>
<div style="color:#404040;font-size:22px;margin:20px 0px 40px 0px">
<p style="margin:0px">Provision '${scenario.title}' - VMs running for ${period} days</p>
</div>
<div style="color:#404040;font-size:18px;margin:10px 0px">
<p style="margin:0px;color: #FF2020">This scenario will automatically stop its VMs in ${warningDays*24} hours.</p>
</div>
<div style="color:#404040;font-size:18px;margin:20px 0px 10px 0px">
<p style="margin:0px;color: #FF2020">Take action and extend the period ${(period+warningDays)} extra days.</p>
</div>
<div style="color:#404040;font-size:16px;margin:30px 0px">
<p style="margin:0px">If you don't want the VMs to automatically stop, you've got ${warningDays*24} hours (from when this email was sent) as a grace period to extend this scenario's <b style="color: #009845">Running</b> VMs for ${(period+warningDays)} extra days.</p>
</div>
${common}
${FOOTER}
</div>
</div>`;
}
function getHtmlNewProvision(provision, scenario) {
var htmlint;
if ( provision && provision.outputs ) {
htmlint = `<div style="color:#404040;font-size:18px;padding: 10px 0px;">Connection resources</div>`;
} else {
htmlint = "";
}
for (let key in provision.outputs) {
htmlint += `<div>
<span style="color:#404040">${key}</span>
<pre style="color:#404040;">${provision.outputs[key]}</pre>
</div>`;
}
var common = _getCommonDetails(provision, scenario);
return `<div style="width:600px;color:black!important;font-family:'Source Sans Pro',sans-serif;padding:50px">
<div style="background-color:white;height:100%;padding:20px 10px">
<div style="color:#404040;font-size:34px;text-align:center;margin:20px">
<p style="margin:0px">QMI Cloud</p>
</div>
<div style="color:#404040;font-size:22px;margin:20px 0px">
<p style="margin:0px">Scenario '${scenario.title}' successfully provisioned!</p>
</div>
${common}
<div style="margin: 30px 0px;">
${htmlint}
</div>
${FOOTER}
</div>
</div>`;
}
function getHtmlErrorProvision(provision, scenario) {
var common = _getCommonDetails(provision, scenario);
return`<div style="width:600px;color:black!important;font-family:'Source Sans Pro',sans-serif;padding:50px">
<div style="background-color:white;height:100%;padding:20px 10px">
<div style="color:#404040;font-size:34px;text-align:center;margin:20px">
<p style="margin:0px">QMI Cloud</p>
</div>
<div style="color:#404040;font-size:20px;margin:20px 0px">
<p style="margin:0px;color: #FF2020">Oops! Something didn't work.</p>
</div>
<div style="color:#404040;font-size:20px;margin:20px 0px 50px 0px">
<p style="margin:0px">Scenario '${scenario.title}' failed during provision.</p>
</div>
<div style="color:#404040;font-size:16px;margin:30px 0px">
<p style="margin:0px">Please, follow these steps:</p>
<ul>
<li>Reach out the person responsible for this scenario for support.</li>
<li>As soon as it's possible, consider destroy this provision since it's taking valuable cloud resources which might imply relevant cost.</li>
</ul>
</div>
${common}
${FOOTER}
</div>
</div>`;
}
function getHtmlDestroyProvision(provision, scenario) {
var common = _getCommonDetails(provision, scenario);
return`<div style="width:600px;color:black!important;font-family:'Source Sans Pro',sans-serif;padding:50px">
<div style="background-color:white;height:100%;padding:20px 10px">
<div style="color:#404040;font-size:34px;text-align:center;margin:20px">
<p style="margin:0px">QMI Cloud</p>
</div>
<div style="color:#404040;font-size:20px;margin:40px 0px">
<p style="margin:0px">Scenario '${scenario.title}' successfully destroyed!</p>
</div>
${common}
${FOOTER}
</div>
</div>`;
}
// async..await is not allowed in global scope, must use a wrapper
async function sendProvisionSuccess( provision, scenario ) {
const htmlText = getHtmlNewProvision(provision, scenario);
await _doSend(provision.user.upn, 'QMI Cloud - Provision finished successfully', htmlText);
}
async function sendProvisionError(provision, scenario ) {
const htmlText = getHtmlErrorProvision(provision, scenario);
await _doSend(provision.user.upn, 'QMI Cloud - Provision with errors', htmlText);
}
async function sendDestroyedSuccess(provision, scenario ) {
const htmlText = getHtmlDestroyProvision(provision, scenario);
await _doSend(provision.user.upn, 'QMI Cloud - Provision destroyed successfully', htmlText);
}
async function sendWillStopIn24( provision, scenario, period, warningDays ) {
const htmlText = getHtmlScenarioWillStopIn24( provision, scenario, period, warningDays);
await _doSend(provision.user.upn, `QMI Cloud - VMs will stop in ${warningDays*24} hours`, htmlText);
}
async function sendWillDestroyIn24( provision, scenario, period, warningDays ) {
const htmlText = getHtmlScenarioDestroyIn24( provision, scenario, period, warningDays);
await _doSend(provision.user.upn, `QMI Cloud - Provision will destroy in ${(warningDays*24)} hours`, htmlText);
}
async function sendVMsStopped( provision, scenario ) {
const htmlText = getHtmlScenarioVMsStopped( provision, scenario);
await _doSend(provision.user.upn, 'QMI Cloud - VMs stopped automatically', htmlText);
}
module.exports.sendProvisionSuccess = sendProvisionSuccess;
module.exports.sendProvisionError = sendProvisionError;
module.exports.sendDestroyedSuccess = sendDestroyedSuccess;
module.exports.sendWillStopIn24 = sendWillStopIn24;
module.exports.sendVMsStopped = sendVMsStopped;
module.exports.sendWillDestroyIn24 = sendWillDestroyIn24;
module.exports._doSend = _doSend;

View File

@@ -1,39 +1,35 @@
'use strict';
const nodemailer = require('nodemailer');
const FROM = '"Qlik" <no-reply@qlik.com>';
var transporter;
const axios = require('axios');
const https = require("https");
const SMTP_EMAIL_SENDER = process.env.SMTP_EMAIL_SENDER;
const HOSTNAME_URL = process.env.HOSTNAME_URL || "https://qmicloud.qliktech.com";
const FOOTER = `<div style="color:#404040;font-size:16px;padding:30px 0px 50px 0px">
<p style="margin:0px">Check it out at <a href="${HOSTNAME_URL}">${HOSTNAME_URL}</a></p>
</div>`;
if ( process.env.GMAIL_USERNAME && process.env.GMAIL_PASSWORD ) {
//GMAIL
transporter = nodemailer.createTransport({
service: 'gmail',
port: 587,
secure: false,
auth: {
user: process.env.GMAIL_USERNAME,
pass: process.env.GMAIL_PASSWORD
}
});
} else {
//QLIK
transporter = nodemailer.createTransport({
host: 'smtp.qliktech.com',
port: 587,
secure: false, // true for 465, false for other ports
});
}
async function _doSend(to, subject, htmlText) {
// send mail with defined transport object
let info = await transporter.sendMail( {
from: FROM, // sender address
to: to, // list of receivers
subject: subject, // Subject line
text: subject, // plain text body
html: htmlText // html body
} );
try {
var body = {
"to": to,
"subject": subject,
"html": htmlText
};
await axios({
url: SMTP_EMAIL_SENDER,
method: "post",
data: body,
httpsAgent: new https.Agent({
rejectUnauthorized: false
})
});
console.log('SendEmail (Qlik SMTP)# message sent to: ' + to);
console.log('SendEmail# message id ('+info.messageId+') sent to: ' + to);
} catch (err) {
// Handle Error Here
console.log("SendEmail (Qlik SMTP) _doSend error: could not send the email to: " +to);
}
}
function _getCommonDetails(provision, scenario){
@@ -85,9 +81,26 @@ function getHtmlScenarioDestroyIn24( provision, scenario, period, warningDays) {
<p style="margin:0px">If you don't want this to happen, you've got ${(warningDays*24)} hours (from when this email was sent) as a grace period to get back at 'Running' status this provision.</p>
</div>
${common}
<div style="color:#404040;font-size:16px;margin:30px 0px">
<p style="margin:0px">Check it out at <a href="https://qmicloud.qliktech.com">https://qmicloud.qliktech.com</a></p>
${FOOTER}
</div>
</div>`;
}
function getHtmlScenarioIndays( provision, scenario, period, warningDays) {
var common = _getCommonDetails(provision,scenario);
return`<div style="width:600px;color:black!important;font-family:'Source Sans Pro',sans-serif;padding:50px">
<div style="background-color:white;height:100%;padding:20px 10px">
<div style="color:#404040;font-size:34px;text-align:center;margin:20px">
<p style="margin:0px">QMI Cloud</p>
</div>
<div style="color:#404040;font-size:22px;margin:20px 0px 40px 0px">
<p style="margin:0px">Provision '${scenario.title}' running more than ${period} days</p>
</div>
<div style="color:#404040;font-size:18px;margin:10px 0px">
<p style="margin:0px;color: #FF2020">This scenario was meant to live active no more than 90 days. It's been 80 days since creation and it will be automatically DESTROYED in ${warningDays} days. Please, consider to backup it up manually if you don't want to lose data.</p>
</div>
${common}
${FOOTER}
</div>
</div>`;
}
@@ -107,12 +120,30 @@ function getHtmlScenarioVMsStopped( provision, scenario) {
<p style="margin:0px;color: #FF2020">All VMs for this provision <b>stopped</b> automatically.</p>
</div>
<div style="color:#404040;font-size:16px;margin:30px 0px">
<p style="margin:0px">You can start them up again from <a href="https://qmicloud.qliktech.com">https://qmicloud.qliktech.com</a></p>
<p style="margin:0px">You can start them up again from <a href="${HOSTNAME_URL}">${HOSTNAME_URL}</a></p>
</div>
${common}
<div style="color:#404040;font-size:16px;margin:30px 0px">
<p style="margin:0px">Check it out at <a href="https://qmicloud.qliktech.com">https://qmicloud.qliktech.com</a></p>
${FOOTER}
</div>
</div>`;
}
function getHtmlSharedProvision( provision, scenario) {
var common = _getCommonDetails(provision,scenario);
return `<div style="width:600px;color:black!important;font-family:'Source Sans Pro',sans-serif;padding:50px">
<div style="background-color:white;height:100%;padding:20px 10px">
<div style="color:#404040;font-size:34px;text-align:center;margin:20px">
<p style="margin:0px">QMI Cloud</p>
</div>
<div style="color:#404040;font-size:22px;margin:20px 0px 40px 0px">
<p style="margin:0px">Provision '${scenario.title}'</p>
</div>
<div style="color:#404040;font-size:18px;margin:30px 0px">
<p style="margin:0px;color: #FF2020">'${provision.user.displayName}' is sharing with you!</p>
</div>
${common}
${FOOTER}
</div>
</div>`;
}
@@ -137,24 +168,27 @@ function getHtmlScenarioWillStopIn24( provision, scenario, period, warningDays )
<p style="margin:0px">If you don't want the VMs to automatically stop, you've got ${warningDays*24} hours (from when this email was sent) as a grace period to extend this scenario's <b style="color: #009845">Running</b> VMs for ${(period+warningDays)} extra days.</p>
</div>
${common}
<div style="color:#404040;font-size:16px;margin:30px 0px">
<p style="margin:0px">Check it out at <a href="https://qmicloud.qliktech.com">https://qmicloud.qliktech.com</a></p>
</div>
${FOOTER}
</div>
</div>`;
}
function getHtmlNewProvision(provision, scenario) {
var htmlint;
if ( provision && provision.outputs ) {
htmlint = `<div style="color:#404040;font-size:18px;padding: 10px 0px;">Connection resources</div>`;
htmlint = `<div style="color:#404040;font-size:20px;padding: 10px 0px;">Connection resources</div>`;
htmlint += `<table style="width:100%" border="0">`;
} else {
htmlint = "";
}
for (let key in provision.outputs) {
htmlint += `<div>
<span style="color:#404040">${key}</span>
<pre style="color:#404040;">${provision.outputs[key]}</pre>
</div>`;
htmlint += `<tr>
<td style="padding-right: 15px"><b style="color:#404040">${key}</b></td>
<td><pre style="color:#404040;">${provision.outputs[key]}</pre></td>
</tr>`;
}
if ( provision && provision.outputs ) {
htmlint += `</table>`;
}
var common = _getCommonDetails(provision, scenario);
@@ -173,9 +207,7 @@ function getHtmlNewProvision(provision, scenario) {
<div style="margin: 30px 0px;">
${htmlint}
</div>
<div style="color:#404040;font-size:16px;margin:30px 0px">
<p style="margin:0px">Check it out at <a href="https://qmicloud.qliktech.com">https://qmicloud.qliktech.com</a></p>
</div>
${FOOTER}
</div>
</div>`;
@@ -194,10 +226,15 @@ function getHtmlErrorProvision(provision, scenario) {
<div style="color:#404040;font-size:20px;margin:20px 0px 50px 0px">
<p style="margin:0px">Scenario '${scenario.title}' failed during provision.</p>
</div>
${common}
<div style="color:#404040;font-size:16px;margin:30px 0px">
<p style="margin:0px">Check it out at <a href="https://qmicloud.qliktech.com">https://qmicloud.qliktech.com</a></p>
<p style="margin:0px">Please, follow these steps:</p>
<ul>
<li>Reach out the person responsible for this scenario for support.</li>
<li>As soon as it's possible, consider destroy this provision since it's taking valuable cloud resources which might imply relevant cost.</li>
</ul>
</div>
${common}
${FOOTER}
</div>
</div>`;
}
@@ -213,26 +250,24 @@ function getHtmlDestroyProvision(provision, scenario) {
<p style="margin:0px">Scenario '${scenario.title}' successfully destroyed!</p>
</div>
${common}
<div style="color:#404040;font-size:16px;margin:30px 0px">
<p style="margin:0px">Check it out at <a href="https://qmicloud.qliktech.com">https://qmicloud.qliktech.com</a></p>
</div>
${FOOTER}
</div>
</div>`;
}
// async..await is not allowed in global scope, must use a wrapper
async function send( provision, scenario ) {
async function sendProvisionSuccess( provision, scenario ) {
const htmlText = getHtmlNewProvision(provision, scenario);
await _doSend(provision.user.upn, 'QMI Cloud - Provision finished successfully', htmlText);
}
async function sendError(provision, scenario ) {
async function sendProvisionError(provision, scenario ) {
const htmlText = getHtmlErrorProvision(provision, scenario);
await _doSend(provision.user.upn, 'QMI Cloud - Provision with failures', htmlText);
await _doSend(provision.user.upn, 'QMI Cloud - Provision with errors', htmlText);
}
async function sendDestroyed(provision, scenario ) {
async function sendDestroyedSuccess(provision, scenario ) {
const htmlText = getHtmlDestroyProvision(provision, scenario);
await _doSend(provision.user.upn, 'QMI Cloud - Provision destroyed successfully', htmlText);
@@ -252,16 +287,30 @@ async function sendWillDestroyIn24( provision, scenario, period, warningDays ) {
await _doSend(provision.user.upn, `QMI Cloud - Provision will destroy in ${(warningDays*24)} hours`, htmlText);
}
async function sendWillDestroyInDays( provision, scenario, period, warningDays ) {
const htmlText = getHtmlScenarioIndays( provision, scenario, period, warningDays);
await _doSend(provision.user.upn, `QMI Cloud - Provision will destroy in ${warningDays} days`, htmlText);
}
async function sendVMsStopped( provision, scenario ) {
const htmlText = getHtmlScenarioVMsStopped( provision, scenario);
await _doSend(provision.user.upn, 'QMI Cloud - VMs stopped automatically', htmlText);
}
module.exports.send = send;
module.exports.sendError = sendError;
module.exports.sendDestroyed = sendDestroyed;
async function sendSharedProvision(provision, shareWithUser) {
const htmlText = getHtmlSharedProvision( provision, provision._scenarioDoc);
await _doSend(shareWithUser.upn, `${provision.user.displayName} shared a QMI Cloud provision with you.`, htmlText);
}
module.exports.sendProvisionSuccess = sendProvisionSuccess;
module.exports.sendProvisionError = sendProvisionError;
module.exports.sendDestroyedSuccess = sendDestroyedSuccess;
module.exports.sendWillStopIn24 = sendWillStopIn24;
module.exports.sendWillDestroyInDays = sendWillDestroyInDays;
module.exports.sendVMsStopped = sendVMsStopped;
module.exports.sendWillDestroyIn24 = sendWillDestroyIn24;
module.exports.sendSharedProvision= sendSharedProvision;
module.exports._doSend = _doSend;

View File

@@ -0,0 +1,7 @@
var sendEmail = require("./send-email");
function test(){
sendEmail._doSend("AOR@qlik.com", "Test subject", "Hi world");
}
test();

74
qmi-cloud-common/utils.js Normal file
View File

@@ -0,0 +1,74 @@
const db = require("./mongo");
const sendEmail = require("./send-email");
async function afterStopVms( provision, triggerUserId, auto = false, type = 'vms' ) {
let timeRunning = db.utils.getNewTimeRunning(provision);
const dateNow = new Date();
let patch = {
"statusVms": "Stopped",
"timeRunning": timeRunning,
"stoppedFrom": dateNow,
"pendingNextAction": null
};
if ( auto && provision._scenarioDoc ) { //From CLI (auto stop)
let msg = `[CLI] TotalTimeRunning: ${timeRunning} mins`;
// Actual onschedule reset
if ( provision.schedule && !provision.schedule.is24x7 ) {
patch["startDateOnSchedule"] = dateNow;
patch["endDateOnSchedule"] = dateNow;
msg += " - (Schedule) accumlated running time has been reset.";
} else {
msg += " - (24x7) accumlated running time was reached.";
}
await db.provision.update(provision._id.toString(), patch);
await sendEmail.sendVMsStopped(provision, provision._scenarioDoc);
db.event.add({ provision: provision._id, type: 'vms.stop', message: msg });
} else { //On Demand stop
if ( provision.schedule && !provision.schedule.is24x7 ) {
patch["endDateOnSchedule"] = dateNow;
//This is temporary, only to make sure there is some initial value soon
if ( !provision["startDateOnSchedule"] ) {
patch["startDateOnSchedule"] = dateNow;
}
}
await db.provision.update(provision._id.toString(), patch);
db.event.add({ user: triggerUserId, provision: provision._id, type: `${type}.stop`, message: `[Manual] TotalTimeRunning: ${timeRunning} mins` });
}
}
async function afterStartVms( provision, triggerUserId, type = 'vms' ) {
const dateNow = new Date();
let countExtend = db.utils.getNewCountExtend(provision);
var patch = {
"statusVms": "Running",
"runningFrom": dateNow,
"countExtend": countExtend,
"pendingNextAction": null
};
// Actual onschedule reset
let msg = `[Manual] TotalTimeRunning: ${provision.timeRunning} mins`;
if ( provision.schedule && !provision.schedule.is24x7 ) {
msg += " - Schedule time has been reset.";
patch["startDateOnSchedule"] = dateNow;
patch["endDateOnSchedule"] = dateNow;
} else {
msg += ` - 24x7, New count extend: ${countExtend}`;
}
await db.provision.update(provision._id.toString(), patch);
db.event.add({ user: triggerUserId, provision: provision._id, type: `${type}.start`, message: msg });
}
module.exports.afterStopVms = afterStopVms;
module.exports.afterStartVms = afterStartVms;

1994
qmi-cloud-common/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
# Stage 1: NOTE: context is actually ../
FROM node:13.8-alpine AS sources
FROM node:15.12.0-alpine AS sources
RUN apk --no-cache add yarn
RUN apk --no-cache add yarn git
WORKDIR /app
@@ -11,8 +11,9 @@ ADD ./qmi-cloud-common ../qmi-cloud-common
RUN yarn install --production
# Stage 2:
FROM node:13.8-alpine AS production
FROM node:15.12.0-alpine AS production
WORKDIR /app
COPY --from=sources /app ./
CMD ["node", "-r", "esm", "index.js"]

View File

@@ -4,8 +4,11 @@ const PROJECT_PATH = process.env.PROJECT_PATH;
const tf = require("./docker/tf");
const azure = require("./docker/azure");
const sendEmail = require("qmi-cloud-common/send-email");
const guaca = require("./guacamole");
module.exports = async function(job) {
const triggerUser = job.data.user;
var prov = await db.provision.update(job.data.id, {
"status": "initializing",
@@ -21,6 +24,8 @@ module.exports = async function(job) {
var idProv = prov._id.toString();
db.event.add({ user: triggerUser._id, provision: prov._id, type: 'provision.init' });
// TERRAFORM INIT
return tf.init(prov)
.then(async function(res) {
@@ -29,7 +34,7 @@ module.exports = async function(job) {
return Promise.reject({"success": false, "error": "Error at Terraform Init", provStatus: "error_init"});
} else {
// TERRAFORM PLAN
return tf.plan(prov, job.data._scenario);
return tf.plan(prov);
}
} )
.then( async function(res) {
@@ -37,38 +42,85 @@ module.exports = async function(job) {
console.log(`ProcessorApply# Error at Terraform PLAN for provision (${idProv}) `);
return Promise.reject({"success": false, "error": "Error at Terraform Plan", provStatus: "error_plan"});
} else {
return await db.provision.update(prov._id,{"status": "provisioning", "statusVms": "Running", "runningFrom": new Date(), "runningTime": 0, "countExtend": 0});
const dateNow = new Date();
let patch = {
"status": "provisioning",
"statusVms": (prov.scenario === 'azqmi-synapse' || prov.scenario === 'awsqmi-rds' || prov.options && prov.options.vm1)? "Running" : "N/A",
"runningFrom": dateNow,
"runningTime": 0,
"countExtend": 0
};
if ( prov.schedule && !prov.schedule.is24x7 ) {
patch["startDateOnSchedule"] = dateNow;
patch["endDateOnSchedule"] = dateNow;
}
return await db.provision.update(prov._id, patch);
}
} ).then( function(prov) {
// TERRAFORM APPLY
return tf.apply(prov);
} ).then( async function(res) {
// Save Provision status
if ( res.statusCode === 1 ) {
console.log(`ProcessorApply# Error at Terraform APPLY for provision (${idProv})`);
} else if ( res.statusCode === 137 ){
console.log(`ProcessorApply# Apply container must have been killed!`);
return Promise.reject({"success": false, "error": "Apply container must have been killed", provStatus: "aborted"});
}
var status = ( res.output.indexOf("Error:") !== -1 )? "error" : "provisioned";
return await db.provision.update(prov._id, {"status": status});
} ).then( async function(prov) {
// Generate Terraform OUTPUTS and store in provision object
return tf.outputs(prov).then( async function(outputs){
return await db.provision.update(prov._id, {"outputs": outputs});
//if ( outputs.barracudaAzureFqdn ) {
// let fqdn = outputs.barracudaAzureFqdn;
// delete outputs['barracudaAzureFqdn'];
// return await db.provision.update(prov._id, {"barracudaAzureFqdn": fqdn, "outputs": outputs});
//} else {
return await db.provision.update(prov._id, {"outputs": outputs});
//}
});
} ).then( async function(prov) {
} )
//.then( async function(prov) {
// if ( prov.isExternalAccess && prov.barracudaAzureFqdn ) {
// console.log(`ProcessorApply# Calling Barracuda service to create App and DNS CName for provision (${prov._id})`);
// barracuda.createApp(prov);
// }
// return prov;
//} )
.then( async function(prov) {
// Application Gateway assign policy
return azure.appgateway(prov, job.data._scenario);
} ).then( async function(prov) {
// Create Image
return azure.createimage(prov, job.data._scenario);
} ).then( async function(prov) {
// Guacamole Web access
return await guaca.setUserConnection(prov, job.data._scenario);
} ).then( function(prov) {
if (prov.status === "provisioned") {
sendEmail.send(prov, job.data._scenario);
db.event.add({ provision: prov._id, type: 'provision.finished' });
sendEmail.sendProvisionSuccess(prov, job.data._scenario);
} else {
sendEmail.sendError(prov, job.data._scenario);
db.event.add({ provision: prov._id, type: 'provision.error' });
sendEmail.sendProvisionError(prov, job.data._scenario);
}
return Promise.resolve({"success": true, provMongo: prov});
} ).catch( function(err) {
console.log("ProcessorApply# Provision: error", err);
db.provision.update(prov._id, {"status": err.provStatus? err.provStatus : 'error'});
sendEmail.sendError(prov, job.data._scenario);
var errormsg = err.provStatus? err.provStatus : 'error'
db.provision.update(prov._id, {"status": errormsg});
db.event.add({ user: triggerUser._id, provision: prov._id, type: 'provision.'+errormsg });
if ( errormsg !== "aborted") {
sendEmail.sendProvisionError(prov, job.data._scenario);
}
return Promise.reject({"success": false, "error": err});
} );
}

View File

@@ -0,0 +1,170 @@
const Docker = require('dockerode');
const docker = new Docker({
'socketPath': '/home/docker.sock'
//'socketPath': '/var/run/docker.sock'
});
const fs = require("fs");
const path = require("path");
const SSHPATH = process.env.SSHPATH;
const DOCKERIMAGE = "mcr.microsoft.com/azure-cli";
const PROJECT_PATH = process.env.PROJECT_PATH;
const pause = function(provision) {
if ( provision.scenario !== 'azqmi-synapse' ) {
console.log(`SynapseAzCLI# provision (${provision._id}) is not AZ SYNAPSE`);
return {"message": `Won't do anything, provision (${provision._id}) is not AZ SYNAPSE`};
}
if ( !provision.outputs ) {
console.log(`SynapseAzCLI# provision (${provision._id}) AZ SYNAPSE has NO outputs`);
return {"message": `Won't do anything, provision (${provision._id}) AZ SYNAPSE has NO outputs`};
}
const name = `qmi-synpause-${provision._id}`;
console.log(`SynapseAzCLI# Pause Synapse: ${name}`);
var processStream = fs.createWriteStream(provision.logFile, {flags:'a'});
var exec = ['synpause', provision.outputs.Synapse_WS_ID, provision.outputs.Synapse_Database];
console.log('SynapseAzCLI# Pause Synapse: exec: '+exec.join(" "));
return docker.run(DOCKERIMAGE, exec, processStream, {
//"Env": vars.envs,
"name": name,
//"WorkingDir": "/app",
"HostConfig": {
"Binds": [
`${provision.path}/synpause.sh:/bin/synpause`,
`${SSHPATH}:/root/.ssh`
]
}
}).then(function(data) {
var container = data[1];
console.log(`SynapseAzCLI# Pause Synapse: '${name}' (${container.id}) has finished with code: ${data[0].StatusCode}`);
return container.remove().then(function(){
console.log(`SynapseAzCLI# Pause Synapse: Container '${name}' removed!`);
return data[0].StatusCode;
});
}).then(async function(statusCode) {
return {"output": fs.readFileSync(provision.logFile), "statusCode": statusCode};
});
}
const resume = function(provision) {
if ( provision.scenario !== 'azqmi-synapse' ) {
console.log(`SynapseAzCLI# provision (${provision._id}) is not AZ SYNAPSE`);
return {"message": `Won't do anything, provision (${provision._id}) is not AZ SYNAPSE`};
}
if ( !provision.outputs ) {
console.log(`SynapseAzCLI# provision (${provision._id}) AZ SYNAPSE has NO outputs`);
return {"message": `Won't do anything, provision (${provision._id}) AZ SYNAPSE has NO outputs`};
}
const name = `qmi-synresume-${provision._id}`;
console.log(`SynapseAzCLI# Resume Synapse: ${name}`);
var processStream = fs.createWriteStream(provision.logFile, {flags:'a'});
var exec = ['synresume', provision.outputs.Synapse_WS_ID, provision.outputs.Synapse_Database];
console.log('SynapseAzCLI# Resume Synapse: exec: '+exec.join(" "));
return docker.run(DOCKERIMAGE, exec, processStream, {
//"Env": vars.envs,
"name": name,
//"WorkingDir": "/app",
"HostConfig": {
"Binds": [
`${provision.path}/synresume.sh:/bin/synresume`,
`${SSHPATH}:/root/.ssh`
]
}
}).then(function(data) {
var container = data[1];
console.log(`SynapseAzCLI# Resume Synapse: '${name}' (${container.id}) has finished with code: ${data[0].StatusCode}`);
return container.remove().then(function(){
console.log(`SynapseAzCLI# Resume Synapse: Container '${name}' removed!`);
return data[0].StatusCode;
});
}).then(async function(statusCode) {
return {"output": fs.readFileSync(provision.logFile), "statusCode": statusCode};
});
}
const copySnapshots = function(data) {
let snapName = data.snapName;
let regions = data.regions;
const name = `qmi-copysnaps`;
console.log(`CopySnapsAzCLI# container: ${name}`);
var processStream = fs.createWriteStream("/logs/general/qmi-snapshots.log", {flags:'a'});
var exec = ['copy-snapshots-to-sa', snapName, regions];
console.log('CopySnapsAzCLI# exec: '+exec.join(" "));
let scriptPath = path.join(PROJECT_PATH,'..');
return docker.run(DOCKERIMAGE, exec, processStream, {
//"Env": vars.envs,
"name": name,
//"WorkingDir": "/app",
"HostConfig": {
"Binds": [
`${scriptPath}/copy-snapshots-to-sa.sh:/bin/copy-snapshots-to-sa`,
`${SSHPATH}:/root/.ssh`
]
}
}).then(function(data) {
var container = data[1];
console.log(`CopySnapsAzCLI# '${name}' (${container.id}) has finished with code: ${data[0].StatusCode}`);
return container.remove().then(function(){
console.log(`CopySnapsAzCLI# Container '${name}' removed!`);
return data[0].StatusCode;
});
}).then(async function(statusCode) {
return {"output": fs.readFileSync("/logs/general/qmi-snapshots.log"), "statusCode": statusCode};
});
}
const checkCopySnapshots = function(data) {
let snapName = data.snapName;
const name = `qmi-checkcopysnaps`;
console.log(`CheckCopySnapsAzCLI# container: ${name}`);
var processStream = fs.createWriteStream("/logs/general/qmi-snapshots.log", {flags:'a'});
var exec = ['checkcopystatus', snapName];
console.log('CheckCopySnapsAzCLI# exec: '+exec.join(" "));
let scriptPath = path.join(PROJECT_PATH,'..');
return docker.run(DOCKERIMAGE, exec, processStream, {
//"Env": vars.envs,
"name": name,
//"WorkingDir": "/app",
"HostConfig": {
"Binds": [
`${scriptPath}/checkcopystatus.sh:/bin/checkcopystatus`,
`${SSHPATH}:/root/.ssh`
]
}
}).then(function(data) {
var container = data[1];
console.log(`CheckCopySnapsAzCLI# '${name}' (${container.id}) has finished with code: ${data[0].StatusCode}`);
return container.remove().then(function(){
console.log(`CheckCopySnapsAzCLI# Container '${name}' removed!`);
return data[0].StatusCode;
});
}).then(async function(statusCode) {
return {"output": fs.readFileSync("/logs/general/qmi-snapshots.log"), "statusCode": statusCode};
});
}
module.exports.pause = pause;
module.exports.resume = resume;
module.exports.copySnapshots = copySnapshots;
module.exports.checkCopySnapshots = checkCopySnapshots;

View File

@@ -5,8 +5,9 @@ const docker = new Docker({
const fs = require('fs');
const GIT_SCENARIOS = process.env.GIT_SCENARIOS;
const GIT_TAG = process.env.GIT_TAG || "master";
const DOCKERIMAGE = process.env.DOCKERIMAGE_TERRAFORM || "qlikgear/terraform:0.12.18";
const SSHPATH = process.env.SSHPATH;
const AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID;
const AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY;
function hook_stdout(callback) {
var old_write = process.stdout.write
@@ -23,94 +24,129 @@ function hook_stdout(callback) {
}
}
function _buildVarsExec( exec, provision, scenario ) {
function _buildVarsExec( exec, provision ) {
let prefix = scenario.name.toUpperCase();
prefix = prefix.replace(/AZQMI/g, 'QMI');
exec.push('-var');
exec.push(`prefix=${prefix}`);
if ( provision.deployOpts && provision.deployOpts.subsId ) {
exec.push('-var');
exec.push(`subscription_id=${provision.deployOpts.subsId}`);
}
//Deprecated
else if ( scenario.subscription && scenario.subscription.subsId ) {
exec.push('-var');
exec.push(`subscription_id=${scenario.subscription.subsId}`);
let gitBranch = GIT_TAG;
if ( provision._scenarioDoc && provision._scenarioDoc.gitBranch && provision._scenarioDoc.gitBranch.trim() !== "") {
gitBranch = provision._scenarioDoc.gitBranch;
}
let prefix = provision.scenario.toUpperCase();
prefix = prefix.replace(/AZQMI/g, 'QMI');
let envs = [
`AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}`,
`AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}`,
`AWS_DEFAULT_REGION=us-east-1`,
`TF_VAR_envbranch=${gitBranch}`,
`TF_VAR_user_email=${provision.user.upn}`,
`TF_VAR_user_oid=${provision.user.oid}`,
`TF_VAR_prefix=${prefix}`
];
if ( provision.deployOpts ) {
if ( provision.deployOpts.subsId ) {
exec.push('-var');
exec.push(`subscription_id=${provision.deployOpts.subsId}`);
}
if ( provision.deployOpts.location ) {
exec.push('-var');
exec.push(`location=${provision.deployOpts.location}`);
}
if ( provision.deployOpts.vnetExists ) {
exec.push('-var');
exec.push(`subnet_id=${provision.deployOpts.subnetId}`);
envs.push(`TF_VAR_subnet_id=${provision.deployOpts.subnetId}`);
if ( provision.isExternalAccess ) {
exec.push('-var');
exec.push(`app_gw_subnet=${provision.deployOpts.appGwSubnetId}`);
envs.push(`TF_VAR_app_gw_subnet=${provision.deployOpts.appGwSubnetId}`);
}
}
}
//Deprecated
else if ( scenario.subscription && scenario.subscription.vnetExists ) {
exec.push('-var');
exec.push(`subnet_id=${scenario.subscription.subnetId}`);
if ( scenario.isExternal || provision.isExternalAccess) {
exec.push('-var');
exec.push(`app_gw_subnet=${scenario.subscription.appGwSubnetId}`);
}
}
exec.push('-var');
exec.push(`provision_id=${provision._id}`);
exec.push('-var');
exec.push(`user_id=${provision.user.displayName}`);
if (!provision.vmImage) {
//Old way
if ( provision.vmType ) {
exec.push('-var');
exec.push(`vm_type=${provision.vmType}`);
}
if ( provision.nodeCount) {
exec.push('-var');
exec.push(`agent_count=${provision.nodeCount}`);
}
} else if ( provision.vmImage ) {
//New way
//DEPRECATED VMIMAGE
if ( provision.vmImage ) {
for ( let key in provision.vmImage ) {
if ( provision.vmImage[key].nodeCount ) {
exec.push('-var');
exec.push(`agent_count_${key}=${provision[key].nodeCount}`);
}
if ( !provision.vmImage[key].disabled ) {
if ( provision.vmImage[key].vmType ) {
if ( provision.vmImage[key].nodeCount ) {
exec.push('-var');
exec.push(`agent_count_${key}=${provision[key].nodeCount}`);
}
if ( !provision.vmImage[key].disabled ) {
if ( provision.vmImage[key].vmType ) {
exec.push('-var');
exec.push(`vm_type_${key}=${provision.vmImage[key].vmType}`);
} else {
exec.push('-var');
exec.push(`vm_type_${key}=ENABLED`);
}
}
if ( provision.vmImage[key].diskSizeGb ) {
exec.push('-var');
exec.push(`disk_size_gb_${key}=${provision.vmImage[key].diskSizeGb}`);
}
if ( provision.vmImage[key].diskSizeGb ) {
exec.push('-var');
exec.push(`disk_size_gb_${key}=${provision.vmImage[key].diskSizeGb}`);
}
if ( provision.vmImage[key].version && provision.vmImage[key].version.image) {
exec.push('-var');
exec.push(`image_reference_${key}=${provision.vmImage[key].version.image}`);
}
if ( provision.vmImage[key].version ) {
exec.push('-var');
exec.push(`image_reference_${key}=${provision.vmImage[key].version.image}`);
}
if ( provision.vmImage[key].version && provision.vmImage[key].version.init_password) {
exec.push('-var');
exec.push(`image_reference_${key}_init_password=${provision.vmImage[key].version.init_password}`);
}
}
}
}
// NEW ATTRS
if ( provision.options ) {
for ( let key in provision.options ) {
if ( !provision.options[key].disabled ) {
if ( key.indexOf("vm") !== -1 ) {
if ( provision.options[key].nodeCount ) {
exec.push('-var');
exec.push(`agent_count_${key}=${provision[key].nodeCount}`);
}
if ( provision.options[key].vmType ) {
exec.push('-var');
exec.push(`vm_type_${key}=${provision.options[key].vmType}`);
}else {
exec.push('-var');
exec.push(`vm_type_${key}=ENABLED`);
}
if ( provision.options[key].diskSizeGb ) {
exec.push('-var');
exec.push(`disk_size_gb_${key}=${provision.options[key].diskSizeGb}`);
}
if ( provision.options[key].selected && provision.options[key].selected.value) {
exec.push('-var');
exec.push(`image_reference_${key}=${provision.options[key].selected.value}`);
}
if ( provision.options[key].selected && provision.options[key].selected.init_password) {
exec.push('-var');
exec.push(`image_reference_${key}_init_password=${provision.options[key].selected.init_password}`);
}
} else {
if ( provision.options[key].selected && provision.options[key].selected.value) {
exec.push('-var');
exec.push(`attr_${key}=${provision.options[key].selected.value}`);
} else {
exec.push('-var');
exec.push(`attr_${key}=ENABLED`);
}
}
}
}
}
@@ -137,7 +173,7 @@ function _buildVarsExec( exec, provision, scenario ) {
}
}
return exec;
return {"exec": exec, "envs": envs};
}
const init = function( provision ) {
@@ -145,11 +181,15 @@ const init = function( provision ) {
const name = `qmi-tf-init-${provision._id}`;
console.log(`Terraform# Init: will spin up container: ${name}`);
var processStream = fs.createWriteStream(provision.logFile, {flags:'a'});
let exec = ['terraform', 'init', '-no-color', `-from-module=${GIT_SCENARIOS}//${provision.scenario}?ref=${GIT_TAG}`];
let gitBranch = GIT_TAG;
if ( provision._scenarioDoc && provision._scenarioDoc.gitBranch && provision._scenarioDoc.gitBranch.trim() !== "") {
gitBranch = provision._scenarioDoc.gitBranch;
}
let exec = ['terraform', 'init', '-no-color', `-from-module=${GIT_SCENARIOS}//${provision.scenario}?ref=${gitBranch}`];
console.log('Terraform# Init: exec: '+exec.join(" "));
var terraformImage = provision.terraformImage? provision.terraformImage : DOCKERIMAGE;
return docker.run(terraformImage, exec, processStream, {
//"Env": ["VAR_ENV=whatever"],
console.log('Terraform# Init: version to use is : '+provision.terraformImage);
return docker.run(provision.terraformImage, exec, processStream, {
//"Env": [],
"name": name,
"WorkingDir": "/app",
"HostConfig": {
@@ -170,18 +210,19 @@ const init = function( provision ) {
});
};
const plan = function( provision, scenario ) {
const plan = function( provision ) {
const name = `qmi-tf-plan-${provision._id}`;
console.log(`Terraform# Plan: will spin up container: ${name}`);
var processStream = fs.createWriteStream(provision.logFile, {flags:'a'});
//var processStream = process.stdout;
var exec = ['terraform', 'plan', '-no-color', '-input=false', '-out=tfplan' ];
exec = _buildVarsExec(exec, provision, scenario);
var vars = _buildVarsExec(exec, provision);
exec = vars.exec;
console.log('Terraform# Plan: exec: '+exec.join(" "));
var terraformImage = provision.terraformImage? provision.terraformImage : DOCKERIMAGE;
return docker.run(terraformImage, exec, processStream, {
//"Env": ["VAR_ENV=whatever"],
console.log('Terraform# Plan: version to use is : '+provision.terraformImage);
return docker.run(provision.terraformImage, exec, processStream, {
"Env": vars.envs,
"name": name,
"WorkingDir": "/app",
"HostConfig": {
@@ -212,9 +253,13 @@ const apply = function( provision ) {
var exec = ['terraform', 'apply', 'tfplan', '-no-color'];
console.log('Terraform# Apply: exec: '+exec.join(" "));
var terraformImage = provision.terraformImage? provision.terraformImage : DOCKERIMAGE;
return docker.run(terraformImage, exec, processStream, {
//"Env": ["VAR_ENV=whatever"],
console.log('Terraform# Apply: version to use is : '+provision.terraformImage);
return docker.run(provision.terraformImage, exec, processStream, {
"Env": [
`AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}`,
`AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}`,
`AWS_DEFAULT_REGION=us-east-1`
],
"name": name,
"WorkingDir": "/app",
"HostConfig": {
@@ -228,7 +273,7 @@ const apply = function( provision ) {
let container = data[1];
console.log(`Terraform# Apply: ${name} (${container.id}) has finished with code: ${data[0].StatusCode}`);
return container.remove().then(function(){
console.log(`Terraform# Apply: ${name} removed!`);
console.log(`Terraform# Apply: Container '${name}' removed!`);
return data[0].StatusCode;
});
}).then(function(statusCode) {
@@ -236,19 +281,19 @@ const apply = function( provision ) {
})
}
const destroy = function(destroyMongo, provision, scenario) {
const destroy = function(destroyMongo, provision) {
const name = `qmi-tf-destroy-${destroyMongo._id}`;
console.log(`Terraform# Destroy: will spin up container: ${name}`);
var processStream = fs.createWriteStream(destroyMongo.logFile, {flags:'a'});
var exec = ['Terraform# terraform', 'destroy', '-auto-approve', '-no-color'];
exec = _buildVarsExec(exec, provision, scenario);
var exec = ['terraform', 'destroy', '-auto-approve', '-no-color'];
var vars = _buildVarsExec(exec, provision);
exec = vars.exec;
console.log('Terraform# Destroy: exec: '+exec.join(" "));
var terraformImage = provision.terraformImage? provision.terraformImage : DOCKERIMAGE;
return docker.run(terraformImage, exec, processStream, {
//"Env": ["VAR_ENV=whatever"],
console.log('Terraform# Destroy: version to use is : '+provision.terraformImage);
return docker.run(provision.terraformImage, exec, processStream, {
"Env": vars.envs,
"name": name,
"WorkingDir": "/app",
"HostConfig": {
@@ -261,7 +306,7 @@ const destroy = function(destroyMongo, provision, scenario) {
var container = data[1];
console.log(`Terraform# Destroy: '${name}' (${container.id}) has finished with code: ${data[0].StatusCode}`);
return container.remove().then(function(){
console.log(`Terraform# Destroy: '${name}' removed!`);
console.log(`Terraform# Destroy: Container '${name}' removed!`);
return data[0].StatusCode;
});
}).then(async function(statusCode) {
@@ -283,11 +328,8 @@ const outputs = function(provision) {
var unhook = hook_stdout(function(string, encoding, fd) {
tfout += string.trim();
});
var terraformImage = provision.terraformImage? provision.terraformImage : DOCKERIMAGE;
return docker.run(terraformImage, exec, process.stdout, {
//"Env": ["VAR_ENV=whatever"],
return docker.run(provision.terraformImage, exec, process.stdout, {
//"Env": [],
"name": name,
"WorkingDir": "/app",
"HostConfig": {
@@ -301,8 +343,8 @@ const outputs = function(provision) {
console.log(`Terraform# Output: '${name}' (${container.id}) has finished with code: ${data[0].StatusCode}`);
return container.remove();
}).then(async function(data) {
console.log(`Terraform# Output: '${name}' removed!`);
console.log("Terraform# Output: tfout: " + tfout);
console.log(`Terraform# Output: Container '${name}' removed!`);
//console.log("Terraform# Output: tfout: " + tfout);
var out = JSON.parse(tfout);
var o = {};
for (var key in out) {
@@ -312,8 +354,48 @@ const outputs = function(provision) {
});
};
const stop = function(provision) {
if ( provision.status !== "provisioning" ) {
console.log(`Terraform# Stop: provision (${provision._id}) is not provisioning`);
return {"message": `Won't stop container: provision (${provision._id}) is not provisioning`};
}
const name = `qmi-tf-apply-${provision._id}`;
console.log(`Terraform# Stop: will try to stop container: ${name}`);
return docker.listContainers().then(function(containers) {
var containerID;
for (let i=0;i<containers.length;i++) {
if ( containers[i].Names && containers[i].Names.length ) {
//console.log(`Terraform# Stop: debug container Names: ${containers[i].Names[0]}`);
if ( containers[i].Names[0] === ("/"+name) ) {
console.log(`Terraform# Stop: container found: ${name}`);
containerID = containers[i].Id;
break;
}
}
}
if ( containerID ) {
console.log(`Terraform# Stop: stopping container: ${name} with ID ${containerID}`);
return docker.getContainer(containerID).kill().then(function(){
console.log(`Terraform# Stop: container stopped!!: ${name}`);
return {"message": `container ${name} stopped`}
});
} else {
console.log(`Terraform# Stop: container ${name} not found!`);
return {"message": `container ${name} not found`};
}
});
}
module.exports.init = init;
module.exports.plan = plan;
module.exports.apply = apply;
module.exports.destroy = destroy;
module.exports.outputs = outputs;
module.exports.outputs = outputs;
module.exports.stop = stop;

View File

@@ -0,0 +1,302 @@
'use strict';
const axios = require('axios');
const https = require("https");
const db = require('qmi-cloud-common/mongo');
const URL = "https://qmicloud-dev.qliktech.com:8443/api/session/data/postgresql";
const GUACA_USERNAME = process.env.GUACA_USERNAME || "guacadmin";
const GUACA_PASSWORD = process.env.GUACA_PASSWORD;
const base64urlEncode = function(value) {
// Translate padded standard base64 to unpadded base64url
return Buffer.from(value).toString('base64').replace(/[+/=]/g,
(str) => ({
'+' : '-',
'/' : '_',
'=' : ''
})[str]
);
};
const guacamoleClientId = function(id, type, dataSource) {
return base64urlEncode([
id,
type,
dataSource
].join('\0'));
};
async function _auth() {
try {
const params = new URLSearchParams();
params.append('username', GUACA_USERNAME);
params.append('password', GUACA_PASSWORD);
var res = await axios.post('https://qmicloud-dev.qliktech.com:8443/api/tokens', params, {
httpsAgent: new https.Agent({
rejectUnauthorized: false
})
});
return res.data.authToken;
} catch (err) {
// Handle Error Here
console.log("Guacamole# Could not auth", err);
return null;
}
}
async function _createUser(email, token) {
try {
var body = {
"username": email,
"attributes": {
"disabled": "",
"expired": "",
"access-window-start": "",
"access-window-end": "",
"valid-from": "",
"valid-until": "",
"timezone": null
}
};
var res = await axios({
url: `${URL}/users`,
method: "post",
data: body,
httpsAgent: new https.Agent({
rejectUnauthorized: false
}),
headers: {
'Guacamole-Token': token
}
});
console.log("Guacamole# User created: ", email);
return email;
} catch (err) {
// Handle Error Here
console.log("Guacamole# User already existed: ", email);
return email;
}
}
async function _createConnection(type, name, ip, username, password, token) {
try {
var body = {
"parentIdentifier": "ROOT",
"name": name,
"protocol": type,
"parameters": {
"port": type === "ssh"? "22" : "3389",
"read-only": "",
"swap-red-blue": "",
"cursor": "",
"color-depth": "",
"force-lossless": "",
"clipboard-encoding": "",
"disable-copy": "",
"disable-paste": "",
"dest-port": "",
"recording-exclude-output": "",
"recording-exclude-mouse": "",
"recording-include-keys": "",
"create-recording-path": "",
"enable-sftp": "",
"sftp-port": "",
"sftp-server-alive-interval": "",
"sftp-disable-download": "",
"sftp-disable-upload": "",
"enable-audio": "",
"wol-send-packet": "",
"wol-udp-port": "",
"wol-wait-time": "",
"security": "nla",
"disable-auth": "",
"ignore-cert": "true",
"gateway-port": "",
"server-layout": "",
"timezone": null,
"enable-touch": "",
"console": "",
"width": "",
"height": "",
"dpi": "",
"resize-method": "",
"normalize-clipboard": "",
"console-audio": "",
"disable-audio": "",
"enable-audio-input": "",
"enable-printing": "",
"enable-drive": "",
"disable-download": "",
"disable-upload": "",
"create-drive-path": "",
"enable-wallpaper": "",
"enable-theming": "",
"enable-font-smoothing": "",
"enable-full-window-drag": "",
"enable-desktop-composition": "",
"enable-menu-animations": "",
"disable-bitmap-caching": "",
"disable-offscreen-caching": "",
"disable-glyph-caching": "",
"preconnection-id": "",
"recording-exclude-touch": "",
"hostname": ip,
"username": username,
"password": password
},
"attributes": {
"max-connections": "2",
"max-connections-per-user": "2",
"weight": "",
"failover-only": "",
"guacd-port": "",
"guacd-encryption": ""
}
};
var res = await axios({
url: `${URL}/connections`,
method: "post",
data: body,
httpsAgent: new https.Agent({
rejectUnauthorized: false
}),
headers: {
'Guacamole-Token': token
}
});
console.log("Guacamole# Connection created: ", name);
return res.data;
} catch (err) {
// Handle Error Here
console.log("Guacamole# Could not create Guaca Connection", err.config.data);
return null;
}
}
async function _addConnectionToUser(email, identifier, token) {
try {
var body = [
{
"op": "add",
"path": "/connectionPermissions/"+identifier,
"value": "READ"
}
];
var res = await axios({
url: `${URL}/users/${email}/permissions`,
method: "patch",
data: body,
httpsAgent: new https.Agent({
rejectUnauthorized: false
}),
headers: {
'Guacamole-Token': token
}
});
console.log("Guacamole# Connection identifier ("+identifier+") added to user: ", email);
return res.data;
} catch (err) {
// Handle Error Here
console.log("Guacamole# Could not add connection "+identifier+" to Guaca User " + email);
return null;
}
}
async function setUserConnection(provision, scenario){
var resultProvision = provision;
if ( provision && provision.status === 'provisioned' && provision.options && provision.options.vm1 ) {
var token = await _auth();
if (token) {
await _createUser(provision.user.mail, token);
let ip = provision.outputs["RDP-ip"] || provision.outputs["Replicate__RDP_IP"] || provision.outputs["Private_IP"] || provision.outputs["IP"];
let credentials = provision.outputs["RDP-credentials"] || provision.outputs["Replicate__RDP-credentials"] || provision.outputs["User-and-Password"] || provision.outputs["All_DBs_User-and-Password"];
if (ip && credentials){
let username = credentials.split(" / ")[0];
let password = credentials.split(" / ")[1];
if (username && password){
let connection;
let connName = `(${scenario.name}_${provision._id}) - ${provision.description}`;
let type;
if ( scenario.labels.indexOf("linux") === -1 ) {
connection = await _createConnection("rdp",connName, ip, username, password, token);
type = "RDP";
} else {
connection = await _createConnection("ssh", connName, ip, username, password, token);
type = "SSH";
}
if (connection) {
let outputs = provision.outputs || {};
const guacClient = guacamoleClientId(connection.identifier,"c", "postgresql");
outputs[`WEB_${type}_ACCESS_WITH_GUACAMOLE`] = `https://qmicloud-dev.qliktech.com:8443/#/client/${guacClient}`;
_addConnectionToUser(provision.user.mail, connection.identifier, token);
resultProvision = await db.provision.update(provision._id, {"guacaConnId": connection.identifier, outputs: outputs});
} else {
console.log("Guacamole# No connection was created for provision: ", provision._id);
}
} else {
console.log("Guacamole# Could not find username or password for provision:", provision._id);
}
} else {
console.log("Guacamole# Could not find ip or credentials for provision:", provision._id);
}
}
}
return resultProvision;
}
async function deleteConnection(provision){
if ( provision.guacaConnId ) {
try {
var token = await _auth();
await axios({
url: `${URL}/connections/${provision.guacaConnId}`,
method: "delete",
httpsAgent: new https.Agent({
rejectUnauthorized: false
}),
headers: {
'Guacamole-Token': token
}
});
} catch (err) {
// Handle Error Here
console.log("Guacamole# Could not delete connection ("+provision.guacaConnId+")");
}
}
}
module.exports.setUserConnection = setUserConnection;
module.exports.deleteConnection = deleteConnection;

View File

@@ -1,4 +1,4 @@
import { queues, TF_APPLY_QUEUE, TF_APPLY_QSEOK_QUEUE, TF_DESTROY_QUEUE } from 'qmi-cloud-common/queues';
import { queues, TF_APPLY_QUEUE, TF_APPLY_QSEOK_QUEUE, TF_DESTROY_QUEUE, STOP_CONTAINER_QUEUE, SYNAPSE_QUEUE } from 'qmi-cloud-common/queues';
var path = require("path");
@@ -6,6 +6,8 @@ var path = require("path");
queues[TF_APPLY_QUEUE].process("tf_apply_job", 10, path.resolve(__dirname, 'processor-apply.js'));
queues[TF_APPLY_QSEOK_QUEUE].process("tf_apply_qseok_job", 10, path.resolve(__dirname, 'processor-apply-qseok.js'));
queues[TF_DESTROY_QUEUE].process("tf_destroy_job", 10, path.resolve(__dirname, 'processor-destroy.js'));
queues[STOP_CONTAINER_QUEUE].process("tf_abort_apply_job", 10, path.resolve(__dirname, 'processor-stop-container.js'));
queues[SYNAPSE_QUEUE].process("synapse_job", 10, path.resolve(__dirname, 'processor-synapse.js'));
console.log(`Worker queues started!`);

View File

@@ -1,6 +1,6 @@
{
"name": "qmi-cloud-worker",
"version": "1.1.6",
"version": "2.1.0",
"scripts": {
"start": "node -r esm index.js",
"start:dev": "nodemon -r esm index.js",
@@ -11,6 +11,7 @@
"qmi-cloud-common": "../qmi-cloud-common",
"dockerode": "^3.0.2",
"esm": "^3.2.25",
"nodemon": "^1.19.1"
"nodemon": "^1.19.1",
"axios": "^0.21.1"
}
}

View File

@@ -2,9 +2,14 @@ const tf = require('./docker/tf');
const db = require('qmi-cloud-common/mongo');
const path = require('path');
const sendEmail = require("qmi-cloud-common/send-email");
const barracuda = require("qmi-cloud-common/barracuda");
const guaca = require("./guacamole");
module.exports = async function(job){
var triggerUser = job.data.user;
var destroyMongo = await db.destroy.update(job.data.id, {
"status": "destroying",
"jobId": job.id,
@@ -17,25 +22,53 @@ module.exports = async function(job){
}
var provMongo = await db.provision.getById(job.data.provId);
// Deleting Barracuda App if existed
if ( provMongo.barracudaAppId ) {
console.log(`ProcessorDestroy# Deleting Barracuda App...` );
barracuda.deleteApp(provMongo);
} else {
console.log(`ProcessorDestroy# There is no barracuda to be deleted` );
}
return tf.destroy(destroyMongo, provMongo, job.data._scenario)
.then(async function(res) {
db.event.add({ user: triggerUser._id, provision: provMongo._id, type: 'provision.destroy-init' });
return tf.destroy(destroyMongo, provMongo).then(async function(res) {
let update, update2;
if ( res.output.indexOf("Error:") !== -1 ) {
update = await db.destroy.update(destroyMongo._id,{"status": "error"});
update2 = await db.provision.update(provMongo._id, {"isDestroyed": false});
} else {
update = await db.destroy.update(destroyMongo._id, {"status": "destroyed"});
let timeRunning = db.utils.getNewTimeRunning(provMongo);
update2 = await db.provision.update(provMongo._id, {"isDestroyed": true, "timeRunning": timeRunning, "pendingNextAction": undefined, "actualDestroyDate": new Date()});
sendEmail.sendDestroyed(update2, job.data._scenario);
update2 = await db.provision.update(provMongo._id, {"isDestroyed": true, "timeRunning": timeRunning, "pendingNextAction": null, "actualDestroyDate": new Date()});
sendEmail.sendDestroyedSuccess(update2, job.data._scenario);
guaca.deleteConnection(provMongo);
//console.log(`ProcessorDestroy# Deleting shares of this provision` );
//db.sharedProvision.delMany({"provision":update2._id});
}
return { destroy: update, provision: update2 };
}).then(async function(res) {
}).then(async function(res) {
if ( res.provision.isDestroyed ) {
console.log(`ProcessorDestroy# Provision (${res.provision._id}) destroyed!` );
db.event.add({ provision: provMongo._id, type: 'provision.destroy-finished' });
if ( res.provision.scenario === "azqmi-qdi" ) {
let tempApiKey = await db.apiKey.getOne({"description": res.provision._id});
if (tempApiKey && tempApiKey._id){
db.apiKey.del(tempApiKey._id);
}
}
} else {
console.log(`ProcessorDestroy# Provision (${res.provision._id}) NOT destroyed - Error!` );
db.event.add({ provision: provMongo._id, type: 'provision.destroy-error' });
}
return Promise.resolve({"success": true, job: res});
}).catch(function(err) {
console.log("ProcessorDestroy# err", err);
db.event.add({ provision: provMongo._id, type: 'provision.destroy-error' });
db.destroy.update(destroyMongo._id, {"status": "error", "isDestroyed": false});
return Promise.reject({"success": false, "err": err});
});

View File

@@ -0,0 +1,21 @@
const db = require('qmi-cloud-common/mongo');
const tf = require("./docker/tf");
module.exports = async function(job) {
var prov = await db.provision.getById(job.data.provId);
if ( !prov ) {
console.log(`ProcessorStopContainer# Error: Not found Provision object in Database (it should exist!), provisionId is: ${job.data.id}` );
return Promise.reject({"success": false, "err": "Not found Provision object in Worker"});
}
// TERRAFORM INIT
return tf.stop(prov)
.then( function(res) {
return Promise.resolve( { "success": true, "output": res });
} ).catch( function(err) {
console.log("ProcessorStopContainer# Error:", err);
return Promise.reject({"success": false, "error": err});
} );
}

View File

@@ -0,0 +1,67 @@
const db = require('qmi-cloud-common/mongo');
const synapse = require("./docker/synapse");
module.exports = async function(job) {
console.log(`ProcessorAzCLI# will execute ${job.data.tasktype} task`);
if (job.data.tasktype === 'pause') {
var prov = await db.provision.getById(job.data.provId);
if ( !prov ) {
console.log(`ProcessorAzCLI# Error: Not found Provision object in Database (it should exist!), provisionId is: ${job.data.id}` );
return Promise.reject({"success": false, "err": "Not found Provision object in Worker"});
}
// Synapse Pause
db.provision.update(prov._id, {"statusVms": "Stopping"});
return synapse.pause(prov)
.then( function(res) {
db.provision.update(prov._id, {"statusVms": "Stopped"});
return Promise.resolve( { "success": true, "output": res });
} ).catch( function(err) {
console.log("ProcessorAzCLI# Error:", err);
db.provision.update(prov._id, {"statusVms": "Running"});
return Promise.reject({"success": false, "error": err});
} );
} else if (job.data.tasktype === 'resume') {
var prov = await db.provision.getById(job.data.provId);
if ( !prov ) {
console.log(`ProcessorAzCLI# Error: Not found Provision object in Database (it should exist!), provisionId is: ${job.data.id}` );
return Promise.reject({"success": false, "err": "Not found Provision object in Worker"});
}
// Synapse Resume
db.provision.update(prov._id, {"statusVms": "Starting"});
return synapse.resume(prov)
.then( function(res) {
db.provision.update(prov._id, {"statusVms": "Running"});
return Promise.resolve( { "success": true, "output": res });
} ).catch( function(err) {
console.log("ProcessorAzCLI# Error:", err);
db.provision.update(prov._id, {"statusVms": "Stopped"});
return Promise.reject({"success": false, "error": err});
} );
} else if (job.data.tasktype === 'copy-snapshots') {
return synapse.copySnapshots(job.data)
.then( function(res) {
return Promise.resolve( { "success": true, "output": res });
} ).catch( function(err) {
console.log("ProcessorAzCLI# Error:", err);
return Promise.reject({"success": false, "error": err});
} );
} else if (job.data.tasktype === 'check-copy-snapshots') {
return synapse.checkCopySnapshots(job.data)
.then( function(res) {
return Promise.resolve( { "success": true, "output": res });
} ).catch( function(err) {
console.log("ProcessorAzCLI# Error:", err);
return Promise.reject({"success": false, "error": err});
} );
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQCzYn0nGRdheWZf
X9Gvz5Fc0RH0oeJe1HfC0E7HrDFlogkmErJyvr6vZMwjx7jxSYDbYgNJtUXmalKZ
a3fgF4nRmlv+aPG1pNjeCThoTHTof7hMS70iAIyrbubU6v+SKvbmcWM/RkEkEXXa
7TCOi/ZbN7S+dFQAA9ObNF6dsYHLWBQO4O8ZoydTcxTc1enc+v1Q6/Bqm7//fBY+
VV8tIi4hiIgLTEGnsBCnH34a3Ie8i6JDXEMmD+NiZjeYLrr5YToniDu4drkrpzIp
2P9X2wbdwI25WC6RcWLwSIkjiRY8Z/jDgowjieCDNXHVCYv47+8peeOx/Z//YuKy
Hul2ob25KjK51k6t55NKNj3kTrzXyRIgGOdI6Q+IBl85lfhIiXPRSCobLoMVbZgG
5qTZpEHyp2RN5CPLGlshslrQigunUzuA6sPGR+qnT8ZwGndiV8n5oaWx7fmIoK1k
SYBe5zKehYHh4hprThvzvqqPfJwzAFAOCSJJsch7SuZGlz/2xSx0P1e27vRJ9at/
zSKL3FIFqHpk3I0FKf/FjNZu29RPmyLIyX8uaB/Iud6cg45THaUs+GXeiLY3mt/u
3BRADZF9AXzJZR02Ubtre1gE5PSu0NOnkudoL+9fnCK5PGFlifdtp68R8UiKAnXj
OjMljbq+yqn5tf6DB0y82rEk0qXCoQIDAQABAoICAAhFqkF4zz1f+fVmTPGfZUko
nPwxJj1ll9uPJpWKSRbc44YdcPGIqUYbpFb2uVPAe3riaLC9SsARjBs7ZkzvrkZ7
qJ1b8orZU2Td0PuXoavXuUR6GPqDQvlkL6yG5QqKWlYNyYaxSIS6HLtf77rLFS0S
Aw7l/LpUHYMikEW+WfQUwjazbw3kGlv8n8F/8ye3wqG4156FmH4sFzcq/FdvpD1I
ogQUsVG4dUl3qCWN9jZ0IU3w6GnOFsLCtZ1EyRDXR8rAkLHKIRzfD274QkInaAgm
09ebC5QKbLEURJAJHNI5SzKc3QswgFQcoqdb0tgZHGgcZlXJZ9eWPvT5fEj2Xsd3
i0Q46D6y7r+dAMhhE9hdZA/Ke1fkcBgANLRRdoHHFUey1en7uQsliGfNzm1i/OR4
AW9XdyNiB5F2LqYfnrRXEsG7FHAnCVkN1k5ur8GsBZpfYtzWof9yKXCaVUQODKqW
0gUndI38omx8liZoFruP5VteRJgVYEIkcyAAfeG3OlIaywGmwdTGyViXhWZcihse
3pJMsUI7P72SkSjGKAjVxCf08bwCIT6ls8V79epiUzhMnORAZC4hZxayZrBphS8U
sVzrMGQeVexlXso3EoqjWCaM7XkbvE/8ZOBbFxyJWAkIMuD4vUuW19Fk8IOTzAL4
VKlcq+vfZxWbA7Mq6w7vAoIBAQDeJF82Lv/IUXL3JkgYmSnze7rGg+rwsuUxBXd5
CNPjrlwbCEIVEreQHUPOzih2Rvhrd2iCVpyCO8Kdo2AE5B7wnPePAitnEwZ6hFcD
xMubuiRqlaILfDi8Ph7juL0mmSaz5D+HkzUdCUfvBUFXxvLyeDsNFjgqBGOahL0b
1E1FDct/6Mltc+ATlavWxQxn+UEs8/3ssy+f4gmeLgwgevt+A8cB3ppwSFCOnu4I
iX/vdti5MHjdUBjJGV/qjTOsUWioRXY1ay87H5RiQju3GpTQ8zlkyRv9MMll0lqi
OQwtVvVpRd6O4qHFmgJOfPt+Z81b6cPrA0StbwAG/t5qYH7/AoIBAQDOucsdyj5k
XmlcDo51VyEr95FsJ/SZOPOBh4MpRm4IYUgLsQTacUueTCiY17TUR6CUc+si76gb
YYAcF0KV/17lvPosoWkdZNsqq1L8nrVq3+z2WsTkCs8m10FZ0aajf3N8WTT0v6VX
DsOrZLsD2xxIU4f3Lf4FnL6PI5BDT83Up5paHSIhE+omAp+x7Bzf0KSpsDu2uZTH
r8yJf1ghet+Z923/yUUTHDM8Vpt8hhoEjXWJd3r96OSIoFUwHzLgNFpiPgHkGJUU
xRZV4Au+GeHuz9nLWPRaYVG5CxX/IbPyMzGJHchVFywIySqYK2l+VvVM17M7AuCq
V4SZ3y7Ky15fAoIBAHahp/MwwEqDLMlOOVxhl2S/Y+yWEIbAkuNODxKlIztJJ0kM
bPYCC+O7rTWpJTSdDBegKkDI7kYikflLgYC7LsbCnPZTa0hdga02NZ3+n9mnW8FL
7cECcu4corRsOR9+1ItnToIhnFDIXxEHlnDA/4d7q9V+UzolI+gmETPmeelxx4ak
k8WPB1COMrm8e7afBy5xkt6whrN0rDw8TR+fbeVLMSEPdxyVkefIekg23grNRkoH
19Qg7Uuf8Hg7NihFRYXvqoQ2nH+Pite6lVdgq6625aSsPfVF85gb8WkG3DjuYpr4
xDU8VLZJXAf8ePZ1itcWDRnZofiY+cPCopbet5MCggEAGb7l3wXrE1D2yjI957s8
NF+WyuOHAPYozX71BNTyqzSCZoJbWmE1y7csbyyeJrns89Aj/qveQdq4u8bh0hCF
3xLUDW7kynZfHUdNBI03huHwfxX643O9LNcuGmOT31TmKxxpDfo4O0lpcRUQfYBy
W0eb7VrbAhPtX6JMOzXbKprdDFAIihoS1T0Kanw/dFhlyYRbS3x9XQk17gHgFftZ
kbFRD8QfSCwA7YjTwIRrBRohA0fQF4NDwwhE08Nu8KFUiFu0nJW7K2UITRWkIL7U
douIUlz3wbHRHbyVtrqZ0JYzmyIMaxyBrW5wUZdGgieOUU2j0rufA1f2+brj9vmw
/QKCAQBDuCwPOBuZ6qFu1zBAfDyElTsIve1Uh32q+jbL5w8kkBhsiGaxFcmC3gG6
WkdtKZKSRaf23wl4LgjvAYD5lPX+p3127pHxG+haWFkXweDMw4WrvGamm/X0H8JD
8CX9PNWi2yxVDjNXiJ5tSbsFnhBf2CijHinrZexxlVTId6yuptOWDaq4XeYnMmKt
FRgpnwnKAkxZKmoKIXOeSQOUVJzzxufkX3QG3CJqu60DGEDsLxM00HoP/Vxa81un
XT+DxB6kgSYLC0a7KsHsXm8CbCoikqVEtANzIvahbsKDLNah/OWmOo9O1UphhYN+
HQe3R2QQtfmWZ320cdhukaok4yj3
-----END PRIVATE KEY-----

View File

@@ -2,16 +2,14 @@
const secrets = require("/run/secrets/config.json");
const HOSTNAME_URL = process.env.HOSTNAME_URL || "http://localhost:3000"
const TENANT_ID = secrets.AZURE_TENANT_ID;
const CLIENT_ID = secrets.AZURE_CLIENT_ID;
const CLIENT_SECRET = secrets.AZURE_CLIENT_SECRET;
const CLIENT_ID = secrets.CLIENT_ID;
const CLIENT_SECRET = secrets.CLIENT_SECRET;
const LOGOUT_URL = HOSTNAME_URL;
const REDIRECT_URL = `${HOSTNAME_URL}/auth/openid/return`;
exports.creds = {
// Required
identityMetadata: `https://login.microsoftonline.com/${TENANT_ID}/.well-known/openid-configuration`,
// or equivalently: 'https://login.microsoftonline.com/${TENANT_ID}/.well-known/openid-configuration'
//identityMetadata: IDENTITY_METADATA,
//
// or you can use the common endpoint
// 'https://login.microsoftonline.com/common/.well-known/openid-configuration'
@@ -85,12 +83,13 @@ exports.creds = {
exports.resourceURL = 'https://graph.microsoft.com';
// The url you need to go to destroy the session with AAD
exports.destroySessionUrl = `https://login.microsoftonline.com/common/oauth2/logout?post_logout_redirect_uri=${LOGOUT_URL}`;
//exports.destroySessionUrl = `https://login.microsoftonline.com/common/oauth2/logout?post_logout_redirect_uri=${LOGOUT_URL}`;
//exports.destroySessionUrl = `https://qlik.okta.com/logout?id_token_hint=${id_token}&post_logout_redirect_uri=${LOGOUT_URL}&state=${state}`
exports.destroySessionUrl = `https://qlik.okta.com/logout?post_logout_redirect_uri=${LOGOUT_URL}`
// If you want to use the mongoDB session store for session middleware, set to true; otherwise we will use the default
// session store provided by express-session.
// Note that the default session store is designed for development purpose only.
exports.useMongoDBSessionStore = true;
exports.useMongoDBSessionStore = false;
// If you want to use mongoDB, provide the uri here for the database.
exports.databaseUri = process.env.MONGO_URI;

View File

@@ -6,6 +6,17 @@ const config = require('./config');
const MongoStore = require('connect-mongo')(expressSession);
const mongoose = require('mongoose');
const db = require("qmi-cloud-common/mongo");
const axios = require('axios');
const path = require('path');
const fs = require('fs');
const sessionStore = new MongoStore({
mongooseConnection: mongoose.connection,
url: process.env.MONGO_URI,
autoRemove: 'interval',
autoRemoveInterval: 10
//clear_interval: config.mongoDBSessionMaxAge
});
// Start QuickStart here
@@ -77,37 +88,85 @@ passport.use(new OIDCStrategy({
cookieEncryptionKeys: config.creds.cookieEncryptionKeys,
clockSkew: config.creds.clockSkew,
},
function(iss, sub, profile, jwtClaims, accessToken, refreshToken, params, done) {
async function(iss, sub, profile, jwtClaims, accessToken, refreshToken, params, done) {
if ( !profile.oid ) {
return done(new Error("No oid found"), null);
}
let email = profile.upn;
let isPermitterEmail = (email.toLowerCase().includes("@qlik") || email.toLowerCase().includes("@talend"));
if ( !email || !isPermitterEmail) {
return done(new Error("Not a valid Qlik email"), null);
}
//console.log("accessToken", accessToken);
//console.log("iss", iss);
//console.log("sub", sub);
//console.log("refreshToken", refreshToken);
//console.log("jwtClaims", jwtClaims);
//console.log("params", params);
console.log(`Passport# new login from: ${profile.upn} (${profile.displayName})` );
console.log(`Passport# new login from: ${profile.upn} (${profile.displayName}) - sub: ${sub}` );
//Save user photo
axios({
method: 'GET',
url: 'https://graph.microsoft.com/v1.0/me/photo/$value',
responseType: 'stream',
headers: { 'Authorization' : 'Bearer '+accessToken }
}).then(function (response) {
response.data.pipe(fs.createWriteStream(path.resolve(__dirname, '..', 'photos', `${profile.oid}.jpg`)));
}).catch(function(err){
console.log('Passport# Warning: No picture found');
});
let mail, jobTitle;
try {
let fullProfile = await axios({
method: 'GET',
url: `https://graph.microsoft.com/v1.0/users/${profile.oid}`,
responseType: 'json',
headers: { 'Authorization' : 'Bearer '+accessToken }
});
if ( fullProfile.data ) {
mail = fullProfile.data.mail;
jobTitle = fullProfile.data.jobTitle;
}
} catch (e) {
console.log('Passport# Warning: Could not get user entire profile');
}
// asynchronous verification, for effect...
process.nextTick(function () {
_findByOid(profile.oid, async function(err, user) {
if (err) {
return done(err);
}
if (!user) {
// "Auto-registration"
user = await db.user.add({
"oid": profile.oid,
"upn": profile.upn,
"displayName": profile.displayName,
"lastLogin": new Date()
_findByOid(profile.oid, async function(err, user) {
if (err) {
return done(err);
}
if (!user) {
// "Auto-registration"
user = await db.user.add({
"oid": profile.oid,
"upn": profile.upn,
"displayName": profile.displayName,
"lastLogin": new Date(),
"sub": sub,
"active": true,
"mail": mail,
"jobTitle": jobTitle
});
return done(null, user);
}
db.user.update(user._id, {
"lastLogin": new Date(),
"sub": sub,
"active": true,
"mail": mail,
"jobTitle": jobTitle
});
return done(null, user);
}
db.user.update(user._id, {"lastLogin": new Date()});
return done(null, user);
});
});
});
}
));
@@ -120,15 +179,16 @@ module.exports.init = function(app){
app.use(expressSession({
secret: 'secret',
cookie: {maxAge: config.mongoDBSessionMaxAge * 1000},
store: new MongoStore({
mongooseConnection: mongoose.connection,
autoRemove: 'interval',
autoRemoveInterval: 10
//clear_interval: config.mongoDBSessionMaxAge
})
store: sessionStore,
resave: true,
saveUninitialized: false
}));
} else {
app.use(expressSession({ secret: 'keyboard cat', resave: true, saveUninitialized: false }));
app.use(expressSession({
secret: 'keyboard cat',
resave: true,
saveUninitialized: false
}));
}
@@ -240,11 +300,14 @@ module.exports.ensureAuthenticatedAndAdmin = async function(req, res, next) {
module.exports.ensureAuthenticatedAndIsMe = async function (req, res, next) {
if ( await isApiKeyAuthenticated(req) || req.isAuthenticated() ) {
if ( req.user._id == req.params.userId || req.user.role === 'admin' || req.user.role === 'superadmin' ) {
var userId = (req.params.userId === 'me')? req.user._id : req.params.userId;
if ( req.user._id == userId || req.user.role === 'admin' || req.user.role === 'superadmin' ) {
return next();
} else {
return res.status(401).send("Error: Unauthorized");
}
}
return res.status(401).send("Error: Unauthorized");
};
};
module.exports.sessionStore = sessionStore;

268
server/passport-okta.js Normal file
View File

@@ -0,0 +1,268 @@
const passport = require('passport');
const expressSession = require('express-session');
const config = require('./config');
// set up database for express session
const MongoStore = require('connect-mongo')(expressSession);
const mongoose = require('mongoose');
const db = require("qmi-cloud-common/mongo");
const sessionStore = config.useMongoDBSessionStore? new MongoStore({
mongooseConnection: mongoose.connection,
url: process.env.MONGO_URI,
autoRemove: 'interval',
autoRemoveInterval: 10
//clear_interval: config.mongoDBSessionMaxAge
}) : new expressSession.MemoryStore();
var OpenIDConnectStrategy = require('passport-openidconnect');
const OKTA_DOMAIN = "qlik.okta.com";
passport.serializeUser(function(user, done) {
done(null, user);
});
passport.deserializeUser(function(user, done) {
_findByUpn(user.upn, function (err, user) {
done(err, user);
});
});
var _findByUpn = async function(upn, fn) {
var mongouser = await db.user.getOne({"upn": { $regex: new RegExp(upn, 'i') } });
if (mongouser){
return fn(null, mongouser);
} else {
return fn(null, null);
}
};
// set up passport
passport.use('oidc', new OpenIDConnectStrategy({
issuer: "https://qlik.okta.com",
authorizationURL: `https://${OKTA_DOMAIN}/oauth2/v1/authorize`,
tokenURL: `https://${OKTA_DOMAIN}/oauth2/v1/token`,
clientID: config.creds.clientID,
clientSecret: config.creds.clientSecret,
callbackURL: config.creds.redirectUrl,
scope: config.creds.scope,
passReqToCallback: true
}, async (req, issuer, profile, context, idToken, accessToken, refreshToken, done) => {
//console.log("OKTA ISSUER ", issuer)
//console.log("OKTA PROFILE ", profile)
//console.log("OKTA context ", context)
//console.log("OKTA idToken ", idToken)
//console.log("OKTA accessToken ", accessToken)
//console.log("OKTA refreshToken ", refreshToken)
req.session.oktaAccessToken = accessToken;
if ( !profile.id ) {
return done(new Error("No profile id found"), null);
}
console.log(`Passport# new login from: ${profile.displayName} - sub: ${profile.id}` );
// asynchronous verification, for effect...
process.nextTick(function () {
_findByUpn(profile.username, async function(err, user) {
if (err) {
return done(err);
}
if (!user) {
// "Auto-registration"
user = await db.user.add({
//"oid": profile.id,
"upn": profile.username.toLowerCase(),
"displayName": profile.displayName,
"lastLogin": new Date(),
"sub": profile.id,
"active": true,
"mail": profile.emails[0].value,
//"jobTitle": jobTitle
});
return done(null, user);
}
db.user.update(user._id, {
"upn": profile.username.toLowerCase(),
"lastLogin": new Date(),
"sub": profile.id,
"active": true,
"mail": profile.emails[0].value,
//"jobTitle": jobTitle
});
return done(null, user);
});
});
//return done(null, profile);
}));
module.exports.init = function(app, isSecure){
const cookieName = 'qmiconnect.sid';
var cookieConf = {
maxAge: config.mongoDBSessionMaxAge * 1000,
httpOnly: false,
secure: isSecure,
sameSite: isSecure? 'none' : undefined
};
console.log("--- SESSION", "Cookie Conf = ", cookieConf , "Cookie Name = " + cookieName);
// set up session middleware
if (config.useMongoDBSessionStore) {
app.use(expressSession({
name: 'qmiconnect.sid',
secret: 'secret',
cookie: cookieConf,
store: sessionStore,
resave: true,
saveUninitialized: false
}));
} else {
app.use(expressSession({
name: 'qmiconnect.sid',
secret: 'keyboard cat',
cookie: cookieConf,
store: sessionStore,
resave: true,
saveUninitialized: false
}));
}
// Initialize Passport! Also use passport.session() middleware, to support
// persistent login sessions (recommended).
app.use(passport.initialize());
app.use(passport.session());
app.get('/sessioninfo', function(req, res){
if (req.session.oktaAccessToken && req.user ) {
res.json({
"oktaAccessToken": req.session.oktaAccessToken,
"user": req.user
});
} else {
res.status(401).json({});
}
});
app.get('/login',
function(req, res, next) {
passport.authenticate('oidc',
{
response: res, // required
resourceURL: config.resourceURL, // optional. Provide a value if you want to specify the resource.
//customState: 'my_state', // optional. Provide a value if you want to provide custom state value.
failureRedirect: '/error',
session: true,
//assignProperty: "weee",
authInfo: true
}
)(req, res, next);
},
function(req, res) {
res.redirect('/home');
}
);
app.get('/auth/openid/return',
function(req, res, next) {
passport.authenticate('oidc', {
response: res,
failureRedirect: '/error' ,
session: true,
//assignProperty: "weee",
authInfo: true
}
)(req, res, next);
},
function(req, res) {
console.log('Passport# We received a return from OKTA ');
res.redirect('/provisions');
}
);
/*app.use('/auth/openid/return', passport.authenticate('oidc', { failureRedirect: '/error' }), (req, res) => {
console.log('Passport# We received a return from OKTA ');
res.redirect('/provisions');
});*/
app.get('/logout', function(req, res){
req.session.destroy(function(err) {
req.logOut();
res.redirect("/home");
});
});
};
async function isApiKeyAuthenticated(req) {
//console.log("oktaToken", req.headers.oktatoken);
//console.log("okta session token", req.session.oktaAccessToken);
if (req.headers && req.headers.oktatoken !== undefined && req.session && req.headers.oktatoken === req.session.oktaAccessToken) {
console.log("OKTA TOKEN PROVIDED and is GOOD!!!!");
return true;
}
let key = req.query.apiKey || req.get('QMI-ApiKey');
if ( key ) {
var result = await db.apiKey.getOne({"apiKey": key});
if ( result ) {
req.user = result.user;
return true;
} else {
return false;
}
} else {
return false;
}
}
module.exports.ensureAuthenticatedDoLogin = async function(req, res, next) {
if ( await isApiKeyAuthenticated(req) || req.isAuthenticated() ) {
return next();
}
res.redirect('/login');
};
module.exports.ensureAuthenticated = async function(req, res, next) {
if ( await isApiKeyAuthenticated(req) || req.isAuthenticated() ) {
return next();
}
res.status(401).send({"error": "Unauthorized"});
};
module.exports.ensureAuthenticatedAndAdmin = async function(req, res, next) {
if ( ( await isApiKeyAuthenticated(req) || req.isAuthenticated()) && (req.user.role === 'admin' || req.user.role === 'superadmin') ) {
return next();
}
res.status(401).send({"error": "Unauthorized"});
};
module.exports.ensureAuthenticatedAndIsMe = async function (req, res, next) {
if ( await isApiKeyAuthenticated(req) || req.isAuthenticated() ) {
var userId = (req.params.userId === 'me')? req.user._id : req.params.userId;
if ( req.user._id == userId || req.user.role === 'admin' || req.user.role === 'superadmin' ) {
return next();
} else {
return res.status(401).send("Error: Unauthorized");
}
}
return res.status(401).send("Error: Unauthorized");
};
module.exports.sessionStore = sessionStore;

View File

@@ -1,7 +1,7 @@
const express = require('express');
const router = express.Router();
const db = require('qmi-cloud-common/mongo');
const passport = require('../passport');
const passport = require('../passport-okta');
/**
@@ -12,6 +12,15 @@ const passport = require('../passport');
* summary: Get all API keys
* tags:
* - admin
* parameters:
* - name: filter
* in: query
* required: false
* type: object
* content:
* application/json:
* schema:
* type: object
* produces:
* - application/json
* responses:
@@ -20,7 +29,8 @@ const passport = require('../passport');
*/
router.get('/', passport.ensureAuthenticatedAndAdmin, async (req, res, next) => {
try {
const result = await db.apiKey.get();
const filter = req.query.filter? JSON.parse(req.query.filter) : {};
const result = await db.apiKey.get(filter);
return res.json(result);
} catch (error) {
next(error);
@@ -42,19 +52,27 @@ router.get('/', passport.ensureAuthenticatedAndAdmin, async (req, res, next) =>
* in: path
* type: string
* required: true
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* description:
* type: string
* responses:
* 200:
* description: API KEY
*/
router.post('/:userId', passport.ensureAuthenticatedAndAdmin, async (req, res, next) => {
try {
const user = await db.user.getById(req.params.userId);
const user = await db.user.getById(req.params.userId);
if ( !user ) {
res.status(404).json({"err": "user not found"});
}
var body = {
user: req.params.userId
}
var body = req.body;
body.user = req.params.userId;
const result = await db.apiKey.add(body);
return res.json(result);
} catch (error) {
@@ -64,10 +82,10 @@ router.post('/:userId', passport.ensureAuthenticatedAndAdmin, async (req, res, n
/**
* @swagger
* /apikeys/{id}:
* /apikeys/{id}/revoke:
* put:
* description: Deactivate API Key
* summary: Deactivate API Key
* description: Revoke API Key
* summary: Revoke API Key
* tags:
* - admin
* produces:
@@ -81,7 +99,7 @@ router.post('/:userId', passport.ensureAuthenticatedAndAdmin, async (req, res, n
* 200:
* description: API KEY
*/
router.put('/:id', passport.ensureAuthenticatedAndAdmin, async (req, res, next) => {
router.put('/:id/revoke', passport.ensureAuthenticatedAndAdmin, async (req, res, next) => {
try {
const result = await db.apiKey.update(req.params.id, {"isActive": false});
return res.json(result);
@@ -90,4 +108,32 @@ router.put('/:id', passport.ensureAuthenticatedAndAdmin, async (req, res, next)
}
});
/**
* @swagger
* /apikeys/{id}:
* delete:
* description: Delete API Key
* summary: Delete API Key
* tags:
* - admin
* produces:
* - application/json
* parameters:
* - name: id
* in: path
* type: string
* required: true
* responses:
* 200:
* description: API KEY
*/
router.delete('/:id', passport.ensureAuthenticatedAndAdmin, async (req, res, next) => {
try {
const result = await db.apiKey.del(req.params.id);
return res.json(result);
} catch (error) {
next(error);
}
});
module.exports = router;

View File

@@ -1,7 +1,7 @@
const express = require('express')
const router = express.Router()
const db = require('qmi-cloud-common/mongo');
const passport = require('../passport');
const passport = require('../passport-okta');
/**

View File

@@ -1,7 +1,7 @@
const express = require('express')
const router = express.Router()
const db = require('qmi-cloud-common/mongo');
const passport = require('../passport');
const passport = require('../passport-okta');
/**

View File

@@ -1,7 +1,10 @@
const express = require('express');
const router = express.Router();
const db = require('qmi-cloud-common/mongo');
const passport = require('../passport');
const passport = require('../passport-okta');
import { queues, SYNAPSE_QUEUE } from 'qmi-cloud-common/queues';
/**
* @swagger
@@ -23,15 +26,18 @@ const passport = require('../passport');
* description: Notifications
*/
router.post('/updates', passport.ensureAuthenticatedAndAdmin, async (req, res, next) => {
const now = new Date().toISOString();
const dateNow = new Date();
const now = dateNow.toISOString();
try {
let event = req.body;
const event = req.body;
let logEvent = event.logEvent || 'DivvyCloud';
if ( event.cloudName === 'QMI Automation' ) {
console.log(`DivvyCloud (${now})# event received for subscription (${event.cloudName}) - provision (${event.provID}) -> new status (${event.instanceState})`);
console.log(`${logEvent} (${now})# event received for subscription (${event.cloudName}) - provision (${event.provID}) -> new status (${event.instanceState})`);
if ( event.provID && event.provID !== 'None' ) {
@@ -39,54 +45,108 @@ router.post('/updates', passport.ensureAuthenticatedAndAdmin, async (req, res, n
if ( provision ) {
var type = "vms";
if ( logEvent.indexOf("RDS")!== -1 ){
type = "db";
}
let id = provision._id.toString();
console.log(`DivvyCloud (${now})# provision (${id}) - scenario is (${provision.scenario} - v${provision.scenarioVersion})`);
console.log(`${logEvent} (${now})# provision (${id}) - scenario is (${provision.scenario} - v${provision.scenarioVersion})`);
if ( provision.status === 'provisioned' ) {
if ( provision.status === 'provisioned' || provision.status === 'error' ) {
if ( event.instanceState === 'Stopped' ) {
//Check active children provisions
let children = await db.provision.get({ "parent": provision._id, "isDestroyed": false, "isDeleted": false });
if (children.results.length > 0 ) {
children.results.forEach(function(child) {
if ( child.scenario === 'azqmi-synapse') {
queues[SYNAPSE_QUEUE].add("synapse_job", {
provId: child._id,
user: req.user,
tasktype: 'pause'
});
}
});
}
if ( provision.statusVms === 'Stopped' ) {
console.log(`DivvyCloud (${now})# provision (${id}) - VMs were already Stopped!`);
console.log(`${logEvent} (${now})# provision (${id}) - were already Stopped!`);
} else {
let timeRunning = db.utils.getNewTimeRunning(provision);
await db.provision.update(id, {"statusVms": "Stopped", "timeRunning": timeRunning, "stoppedFrom": new Date(), "pendingNextAction": undefined});
console.log(`DivvyCloud (${now})# provision (${id}) - VMs changed to Stopped!`);
let patch = {
"statusVms": "Stopped",
"timeRunning": timeRunning,
"stoppedFrom": dateNow,
"pendingNextAction": null
};
if ( provision.schedule && !provision.schedule.is24x7 ) {
patch["endDateOnSchedule"] = dateNow;
//This is temporary, only to make sure there is value
if ( !provision["startDateOnSchedule"] ) {
patch["startDateOnSchedule"] = dateNow;
}
}
await db.provision.update(id, patch);
console.log(`${logEvent} (${now})# provision (${id}) - changed to Stopped!`);
db.event.add({ provision: provision._id, type: `${type}.stop`, message: `[Divvy] TotalTimeRunning: ${timeRunning} mins` });
}
} else if ( event.instanceState === 'Running' ) {
if ( provision.statusVms === 'Running' ) {
console.log(`DivvyCloud (${now})# provision (${id}) - VMs were already Running!`);
console.log(`${logEvent} (${now})# provision (${id}) - were already Running!`);
} else {
await db.provision.update(id, {"statusVms": "Running", "runningFrom": new Date(), "pendingNextAction": undefined});
console.log(`DivvyCloud (${now})# provision (${id}) - VMs changed to Running!`);
let patch = {
"statusVms": "Running",
"runningFrom": dateNow,
"pendingNextAction": null
};
// This is temporary, only to make sure there are values soon
if ( provision.schedule && !provision.schedule.is24x7 ) {
if ( !provision["startDateOnSchedule"] ) {
patch["startDateOnSchedule"] = dateNow;
patch["endDateOnSchedule"] = dateNow;
}
}
await db.provision.update(id, patch);
console.log(`${logEvent} (${now})# provision (${id}) - changed to Running!`);
db.event.add({ provision: provision._id, type: `${type}.start`, message: `[Divvy] TotalTimeRunning: ${provision.timeRunning} mins` });
}
}
} else {
console.log(`DivvyCloud (${now})# provision (${event.provID}) - Scenario not yet 'provisioned'`);
console.log(`${logEvent} (${now})# provision (${event.provID}) - Scenario not yet 'provisioned'`);
}
} else {
console.log(`DivvyCloud (${now})# provision (${event.provID}) - Provision not found.`);
console.log(`${logEvent} (${now})# provision (${event.provID}) - Provision not found.`);
}
} else {
console.log(`DivvyCloud (${now})# 'provID' attribute is missing.`);
console.log(`${logEvent} (${now})# 'provID' attribute is missing.`);
}
} else {
//console.log(`DivvyCloud (${now}): event received for subscription (${event.cloudName}) --> won't be processed`);
//console.log(`${logEvent} (${now}): event received for subscription (${event.cloudName}) --> won't be processed`);
}
return res.json(req.body);
} catch (error) {
console.log(`DivvyCloud (${now})# error!!!!`, error);
console.log(`${logEvent} (${now})# error!!!!`, error);
next(error);
}
});
});
module.exports = router;

View File

@@ -1,7 +1,8 @@
const express = require('express');
const router = express.Router();
const db = require('qmi-cloud-common/mongo');
const passport = require('../passport');
const passport = require('../passport-okta');
const sendEmail = require('qmi-cloud-common/send-email');
/**
@@ -14,6 +15,15 @@ const passport = require('../passport');
* - admin
* produces:
* - application/json
* parameters:
* - name: page
* in: query
* required: false
* type: integer
* - name: size
* in: query
* required: false
* type: integer
* responses:
* 200:
* description: Notifications
@@ -34,4 +44,27 @@ router.get('/', passport.ensureAuthenticatedAndAdmin, async (req, res, next) =>
}
});
/**
* @swagger
* /notifications/testsendemail:
* post:
* description: Test email service
* summary: Test email service
* tags:
* - admin
* produces:
* - application/json
* responses:
* 200:
* description: Notifications
*/
router.post('/testsendemail', passport.ensureAuthenticatedAndAdmin, async (req, res, next) => {
try {
const result = await sendEmail._doSend(req.user.upn, "QMI - Test email", "Hi! This is a test email from QMI Cloud.");
return res.json(result);
} catch (error) {
next(error);
}
});
module.exports = router;

View File

@@ -1,17 +1,19 @@
const express = require('express')
const router = express.Router()
const db = require('qmi-cloud-common/mongo');
const azurecli = require('qmi-cloud-common/azurecli');
const passport = require('../passport');
const passport = require('../passport-okta');
const fs = require('fs-extra');
import { queues, SYNAPSE_QUEUE } from 'qmi-cloud-common/queues';
/**
* @swagger
* /provisions:
* get:
* description: Get all Provisions
* summary: Get all Provisions)
* summary: Get all Provisions
* tags:
* - admin
* produces:
@@ -63,7 +65,8 @@ router.get('/', passport.ensureAuthenticatedAndAdmin, async (req, res, next) =>
const result = await db.provision.getPage(filter, page, req.query.populates, req.query.select);
if (result.next){
result.nextUrl = new URL(req.protocol + '://' + req.get('Host') + req.baseUrl);
result.nextUrl = new URL('https://' + req.hostname + req.baseUrl);
if ( req.query.filter ) {
result.nextUrl.searchParams.append("filter", req.query.filter);
}
@@ -85,10 +88,121 @@ router.get('/', passport.ensureAuthenticatedAndAdmin, async (req, res, next) =>
/**
* @swagger
* /provisions/vmImagesLogs:
* /provisions/copysnapshots:
* post:
* description: Copy snapshots into regions
* summary: Copy snapshots into regions
* tags:
* - admin
* produces:
* - application/json
* parameters:
* - name: snap
* in: query
* required: true
* type: string
* - name: regions
* in: query
* required: true
* type: string
* - name: rg
* in: query
* required: true
* type: string
* responses:
* 200:
* description: OK
* 404:
* description: Not found
*
*/
router.post('/copysnapshots', passport.ensureAuthenticatedAndAdmin, async (req, res, next) => {
try {
queues[SYNAPSE_QUEUE].add("synapse_job", {
snapName: req.query.snap,
rGroup: req.query.rg,
regions: req.query.regions,
tasktype: "copy-snapshots"
});
return res.json({"msg": "Snapshots are being craeted", "success": true});
} catch (error) {
next(error);
}
});
/**
* @swagger
* /provisions/copysnapshots:
* get:
* description: Get all vmImages
* summary: Get all vmImages
* description: Get Status of copy snapshots into regions
* summary: Get Status of copy snapshots into regions
* tags:
* - admin
* produces:
* - application/json
* parameters:
* - name: snap
* in: query
* required: true
* type: string
* responses:
* 200:
* description: OK
* 404:
* description: Not found
*
*/
router.get('/copysnapshots', passport.ensureAuthenticatedAndAdmin, async (req, res, next) => {
try {
queues[SYNAPSE_QUEUE].add("synapse_job", {
snapName: req.query.snap,
tasktype: "check-copy-snapshots"
});
return res.json({"msg": "Snapshots are being checked", "success": true});
} catch (error) {
next(error);
}
});
/**
* @swagger
* /provisions/snapshotslogs:
* get:
* description: Get logs for snapshots
* summary: Get logs for snapshots
* tags:
* - admin
* produces:
* - application/json
* responses:
* 200:
* description: OK
* 404:
* description: Not found
*
*/
router.get('/snapshotslogs', passport.ensureAuthenticatedAndAdmin, async (req, res, next) => {
try {
return res.sendFile("/logs/general/qmi-snapshots.log");
} catch (error) {
next(error);
}
});
/**
* @swagger
* /provisions/optionssLogs:
* get:
* description: Get all optionss
* summary: Get all optionss
* produces:
* - application/json
* parameters:
@@ -112,25 +226,25 @@ router.get('/', passport.ensureAuthenticatedAndAdmin, async (req, res, next) =>
* 200:
* description: JSON Array
*/
router.get('/vmImagesLogs', passport.ensureAuthenticated, async (req, res, next) => {
router.get('/optionssLogs', passport.ensureAuthenticated, async (req, res, next) => {
try {
let filter = req.query.filter? JSON.parse(req.query.filter) : {};
filter.isDeleted = {"$exists": true};
let select = "_id vmImage"
let select = "_id options"
let populates = "[]";
const result = await db.provision.get(filter, select, req.query.skip, req.query.limit, populates);
var final = [];
result.results.forEach(p=>{
for( let key in p.vmImage ) {
for( let key in p.options ) {
var o = {
"_id": p._id.toString(),
"vmIndex": key,
"vmType": p.vmImage[key].vmType? p.vmImage[key].vmType : null,
"diskSizeGb": p.vmImage[key].diskSizeGb? p.vmImage[key].diskSizeGb : null,
"versionProduct": p.vmImage[key].version? p.vmImage[key].version.name : null,
"versionImage": p.vmImage[key].version? p.vmImage[key].version.image : null
"vmType": p.options[key].vmType? p.options[key].vmType : null,
"diskSizeGb": p.options[key].diskSizeGb? p.options[key].diskSizeGb : null,
"versionProduct": p.options[key].selected? p.options[key].selected.name : null,
"versionImage": p.options[key].selected? p.options[key].selected.value : null
}
final.push(o);
}
@@ -140,7 +254,7 @@ router.get('/vmImagesLogs', passport.ensureAuthenticated, async (req, res, next)
count: result.count
};
if ( result.nextSkip && result.nextLimit ) {
out.nextUrl = new URL(req.protocol + '://' + req.get('Host') + req.baseUrl);
out.nextUrl = new URL('https://' + req.hostname + req.baseUrl);
if ( req.query.filter ) {
out.nextUrl.searchParams.append("filter", req.query.filter);
}
@@ -338,7 +452,7 @@ router.post('/:id/updatetagsvms', passport.ensureAuthenticatedAndAdmin, async (r
if (!provision) {
return res.status(404).json({"msg": "Not found provision with id: "+req.params.id, "success": false});
}
var result = await azurecli.updateVmsTags(provision._id, tagsEdit);
var result = await cli.updateVmsTags(provision._id, tagsEdit);
return res.json({"msg": "Tags are being updated", "result": result, "success": true});
} catch (error) {
@@ -348,4 +462,5 @@ router.post('/:id/updatetagsvms', passport.ensureAuthenticatedAndAdmin, async (r
module.exports = router;

View File

@@ -1,7 +1,7 @@
const express = require('express')
const router = express.Router()
const db = require('qmi-cloud-common/mongo');
const passport = require('../passport');
const passport = require('../passport-okta');
router.get('/all', passport.ensureAuthenticated, async (req, res, next) => {
@@ -203,7 +203,7 @@ router.delete('/vmtypes/:id', passport.ensureAuthenticatedAndAdmin, async (req,
* type: string
* index:
* type: string
* versions:
* values:
* type: array
* items:
* type: object

227
server/routes/api-stats.js Normal file
View File

@@ -0,0 +1,227 @@
const express = require('express')
const router = express.Router()
const db = require('qmi-cloud-common/mongo');
const moment = require('moment');
const azurecli = require('qmi-cloud-common/azurecli');
const CACHED_PERIOD = 30; //minutes
var cachedTime;
var cachedTimeVms;
var cachedStats;
var cachedVms;
/**
* @swagger
* /stats:
* get:
* description: Get overall stats
* summary: Get overall stats
* produces:
* - application/json
* responses:
* 200:
* description: Stats
* 404:
* description: Not found
*
*/
router.get('/', async (req, res, next) => {
try {
var now = new Date().getTime();
var nowMinus5mins = now - CACHED_PERIOD*60*1000;
if ( (!req.query.disablecache || req.query.disablecache === 'no') && cachedStats && cachedTime && cachedTime > nowMinus5mins ) {
console.log("APIStats# Stats: return cached value");
cachedTime = now;
return res.json(cachedStats);
} else {
console.log("APIStats# Stats: new value");
let filterActiveP = { "isDestroyed": false, "status": "provisioned" };
let filterPRunning = { "isDestroyed": false, "status": "provisioned", "statusVms" : "Running" };
let initCurrentMonth = moment().startOf('month');
let initLastMonth = moment(initCurrentMonth).add(-1, 'months');
let today = moment();
let todayLastMonth = moment().add(-1, "months");
let filterTotalPCurrentMonth = { "status": "provisioned", "created": {
$gte: initCurrentMonth.toISOString(),
$lt: today.toISOString()
} };
let filterTotalPLastMonth = { "status": "provisioned", "created": {
$gte: initLastMonth.toISOString(),
$lt: todayLastMonth.toISOString()
} };
//Counts
let totalActiveP = await db.provision.count(filterActiveP);
let totalCPRunning = await db.provision.count(filterPRunning);
let totalPCurrentmonth = await db.provision.count(filterTotalPCurrentMonth);
let totalPLastmonth = await db.provision.count(filterTotalPLastMonth);
let totalUsers = await db.user.count({});
let totalAuthUsers = await db.user.count({
"lastLogin": {$gte: moment().add(-12, "hours").toISOString()}
});
let totalScenarios = await db.scenario.count({"isDisabled":false, "isAdminOnly": false});
cachedStats = {
provisions: {
active: totalActiveP,
running: totalCPRunning,
totalCurrentMonthPeriod: totalPCurrentmonth,
totalLastMonthPerdiod:totalPLastmonth
},
users: {
total: totalUsers,
activeNow: totalAuthUsers,
active7days: await db.user.count({
"lastLogin": {$gte: moment().add(-7, "days").toISOString()}
}),
active30days: await db.user.count({
"lastLogin": {$gte: moment().add(-30, "days").toISOString()}
})
},
scenarios: {
total: totalScenarios
}
};
cachedTime = now;
return res.json(cachedStats);
}
} catch (error) {
next(error);
}
});
function appendResult(list, output ){
list.forEach(v=>{
if ( v.tags && v.tags["QMI_user"] ) {
if (v.location) {
if ( !output.locations[v.location] ) {
output.locations[v.location] = 0;
}
output.locations[v.location] += 1;
}
if (v.storageProfile && v.storageProfile.osDisk && v.storageProfile.osDisk.osType) {
if ( !output.types[v.storageProfile.osDisk.osType] ) {
output.types[v.storageProfile.osDisk.osType] = 0;
}
output.types[v.storageProfile.osDisk.osType] += 1;
}
output.out.push(v);
}
});
return output;
}
/**
* @swagger
* /stats/vms:
* get:
* description: List azure vms
* summary: List azure vms
* produces:
* - application/json
* responses:
* 200:
* description: Stats
* 404:
* description: Not found
*
*/
router.get('/vms', async (req, res, next) => {
try {
var now = new Date().getTime();
var nowMinus5mins = now - CACHED_PERIOD*60*1000;
if ( (!req.query.disablecache || req.query.disablecache === 'no') && cachedVms && cachedTimeVms && cachedTimeVms > nowMinus5mins ) {
console.log("APIStats# VMs: return cached value");
cachedTimeVms = now;
return res.json(cachedVms);
} else {
console.log("APIStats# VMs: new value");
var output = {
out : [],
locations: {
"eastus": 0,
"westeurope": 0,
"southeastasia": 0
},
types: {
"Windows": 0,
"Linux": 0
}
}
var result = await azurecli.getAllVms();
output = appendResult(result, output);
if ( result.nextLink ) {
console.log("There is a second page");
result = await azurecli.getAllVmsNext(result.nextLink);
output = appendResult(result, output);
if ( result.nextLink ) {
console.log("There is a third page");
result = await azurecli.getAllVmsNext(result.nextLink);
output = appendResult(result, output);
if ( result.nextLink ) {
console.log("There is a forth page");
result = await azurecli.getAllVmsNext(result.nextLink);
output = appendResult(result, output);
if ( result.nextLink ) {
console.log("There is a fifth page");
result = await azurecli.getAllVmsNext(result.nextLink);
output = appendResult(result, output);
if ( result.nextLink ) {
console.log("There is a sixth page");
result = await azurecli.getAllVmsNext(result.nextLink);
output = appendResult(result, output);
if ( result.nextLink ) {
console.log("There is a seventh page");
}
}
}
}
}
}
cachedTimeVms = now;
cachedVms = {
total: output.out.length,
locations: output.locations,
types: output.types
};
return res.json(cachedVms);
}
} catch (error) {
next(error);
}
});
module.exports = router;

View File

@@ -0,0 +1,446 @@
const express = require('express');
const router = express.Router();
const db = require('qmi-cloud-common/mongo');
const passport = require('../passport-okta');
const cloudshare = require('../training/cloudshare');
const qa = require('../training/automations');
/**
* @swagger
* /training/sessions:
* get:
* description: Get all training sessions (admin)
* summary: Get all training sessions (admin)
* tags:
* - admin
* produces:
* - application/json
* responses:
* 200:
* description: TrainingTemplate
*/
router.get('/sessions', passport.ensureAuthenticatedAndAdmin, async (req, res, next) => {
try {
const filter = req.query.filter? JSON.parse(req.query.filter) : {};
const result = await db.trainingSession.get(filter);
return res.json(result);
} catch (error) {
next(error);
}
});
/**
* @swagger
* /training/templates:
* get:
* description: Get all training templates
* summary: Get all training templates
* produces:
* - application/json
* responses:
* 200:
* description: TrainingTemplate
*/
router.get('/templates', passport.ensureAuthenticated, async (req, res, next) => {
try {
const filter = req.query.filter? JSON.parse(req.query.filter) : {};
const result = await db.trainingTemplate.get(filter);
return res.json(result);
} catch (error) {
next(error);
}
});
/**
* @swagger
* /training/session/{id}:
* get:
* description: Get training session by ID
* summary: Get training session by ID
* produces:
* - application/json
* parameters:
* - name: id
* in: path
* type: string
* required: true
* responses:
* 200:
* description: TrainingTemplate
*/
router.get('/session/:id', async (req, res, next) => {
try {
const result = await db.trainingSession.get({"_id":req.params.id}, 'passwd -user description status created updated studentEmailFilter');
if (!result || !result.results || result.results.length === 0 ){
return res.status(404).json({"msg": "Not found"});
}
return res.json(result.results[0]);
} catch (error) {
next(error);
}
});
/**
* @swagger
* /training/session/{id}:
* post:
* description: Post new student for session by ID
* summary: Post new student for session by ID
* produces:
* - application/json
* parameters:
* - name: id
* in: path
* type: string
* required: true
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* responses:
* 200:
* description: TrainingTemplate
*/
router.post('/session/:id', async (req, res, next) => {
try {
const session = await db.trainingSession.getById(req.params.id);
if (!session){
return res.status(404).json({"msg": "Not found"});
}
let student = await db.trainingStudent.getOne({"email": req.body.email, "session": session._id});
let isNew = false;
if ( !student ) {
let data = req.body;
data.session = session._id;
student = await db.trainingStudent.add(data);
isNew = true;
}
let result1, result2;
if ( session.template.cloudshare && session.cloudshareClass ) {
result1 = await cloudshare.addStudentToClass(session, student.email);
}
if ( session.template.needQcsAutomation && session.qaUrl && session.qaToken ) {
result2 = await qa.runQlikAutomation(session, student.email);
}
if (result1 && result1.error || result2 && result2.error) {
return res.status(500).json({"msg": "Something went wrong when registering studeent", err: result1.error? result1.error : result2.error});
} else {
student = await db.trainingStudent.update(student._id, {"status": "sent", "created": new Date()});
if ( isNew ) {
db.trainingSession.update({ _id: session._id }, { $inc: { studentsCount: 1 } });
}
return res.json(student);
}
} catch (error) {
next(error);
}
});
/**
* @swagger
* /training/session/{id}:
* delete:
* description: Delete session by ID
* summary: Delete session by ID
* tags:
* - admin
* produces:
* - application/json
* parameters:
* - name: id
* in: path
* type: string
* required: true
* responses:
* 200:
* description: TrainingTemplate
*/
router.delete('/session/:id', passport.ensureAuthenticatedAndAdmin, async (req, res, next) => {
try {
const result = await db.trainingSession.update(req.params.id, {status: "terminated"});
return res.json(result);
} catch (error) {
next(error);
}
});
/**
* @swagger
* /training/templates:
* post:
* description: Create new training template
* summary: Create new training template
* tags:
* - admin
* produces:
* - application/json
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* description:
* type: string
* responses:
* 200:
* description: TrainingTemplate
*/
router.post('/templates', passport.ensureAuthenticatedAndAdmin, async (req, res, next) => {
try {
let data = req.body;
const result = await db.trainingTemplate.add(data);
return res.json(result);
} catch (error) {
next(error);
}
});
/**
* @swagger
* /training/{userId}/sessions:
* post:
* description: Add new training session
* summary: Add new training session
* parameters:
* - name: userId
* in: path
* type: string
* required: true
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* produces:
* - application/json
* responses:
* 200:
* description: TrainingSession
*/
router.post('/:userId/sessions', passport.ensureAuthenticatedAndIsMe, async (req, res, next) => {
try {
const userId = req.params.userId === 'me'? req.user._id : req.params.userId;
let data = req.body;
data.user = userId;
let session = await db.trainingSession.add(data);
session = await db.trainingSession.getById(session._id);
let mainAutomation = await qa.getAutomation(session, 'main');
if (!mainAutomation) {
let result = await qa.createAutomations(session);
mainAutomation = result.main;
}
session = await db.trainingSession.update(session._id, {qaToken: mainAutomation.executionToken, qaUrl: `https://${session.qcsTenantHost}/api/v1/automations/${mainAutomation.id}/actions/execute`})
return res.json(session);
} catch (error) {
next(error);
}
});
/**
* @swagger
* /training/{userId}/sessions:
* get:
* description: Get all training session by user
* summary: Get all training session by user
* parameters:
* - name: userId
* in: path
* type: string
* required: true
* produces:
* - application/json
* responses:
* 200:
* description: TrainingSession
*/
router.get('/:userId/sessions', passport.ensureAuthenticatedAndIsMe, async (req, res, next) => {
try {
const userId = req.params.userId === 'me'? req.user._id : req.params.userId;
const result = await db.trainingSession.get({user: userId, status: {"$ne": "terminated"}});
return res.json(result);
} catch (error) {
next(error);
}
});
/**
* @swagger
* /training/{userId}/sessions/{id}:
* get:
* description: Get training session by Id for a user
* summary: Get training session by Id for a user
* parameters:
* - name: userId
* in: path
* type: string
* required: true
* - name: id
* in: path
* type: string
* required: true
* produces:
* - application/json
* responses:
* 200:
* description: TrainingSession
*/
router.get('/:userId/sessions/:id', passport.ensureAuthenticatedAndIsMe, async (req, res, next) => {
try {
const userId = req.params.userId === 'me'? req.user._id : req.params.userId;
const id = req.params.id;
const result = await db.trainingSession.getOne({user: userId, _id: id});
return res.json(result);
} catch (error) {
next(error);
}
});
/**
* @swagger
* /training/{userId}/sessions/{id}/automation:
* get:
* description: Test if automation exists in tenant
* summary: Test if automation exists in tenant
* parameters:
* - name: userId
* in: path
* type: string
* required: true
* - name: id
* in: path
* type: string
* required: true
* produces:
* - application/json
* responses:
* 200:
* description: TrainingSession
*/
router.get('/:userId/sessions/:id/automation', passport.ensureAuthenticatedAndIsMe, async (req, res, next) => {
try {
const id = req.params.id;
let session = await db.trainingSession.getById(id);
let automation = await qa.getAutomation(session);
if (automation) {
session = await db.trainingSession.update(session._id, {qaToken: automation.executionToken, qaUrl: `https://${session.qcsTenantHost}/api/v1/automations/${automation.id}/actions/execute`})
}
return res.json(session);
} catch (error) {
next(error);
}
});
/**
* @swagger
* /training/session/{id}/students:
* get:
* description: Get studetns for a session
* summary: Get studetns for a session
* parameters:
* - name: userId
* in: path
* type: string
* required: true
* - name: id
* in: path
* type: string
* required: true
* produces:
* - application/json
* responses:
* 200:
* description: TrainingSession
*/
router.get('/session/:id/students', passport.ensureAuthenticated, async (req, res, next) => {
try {
const id = req.params.id;
const result = await db.trainingStudent.get({session: id});
return res.json(result);
} catch (error) {
next(error);
}
});
/**
* @swagger
* /training/{userId}/sessions/{id}:
* put:
* description: Update training session
* summary: Update training session
* parameters:
* - name: userId
* in: path
* type: string
* required: true
* - name: id
* in: path
* type: string
* required: true
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* produces:
* - application/json
* responses:
* 200:
* description: TrainingSession
*/
router.put('/:userId/sessions/:id', passport.ensureAuthenticatedAndIsMe, async (req, res, next) => {
try {
const result = await db.trainingSession.update(req.params.id, req.body);
return res.json(result);
} catch (error) {
next(error);
}
});
/**
* @swagger
* /training/{userId}/sessions/{id}:
* put:
* description: Update training session
* summary: Update training session
* parameters:
* - name: userId
* in: path
* type: string
* required: true
* - name: id
* in: path
* type: string
* required: true
* produces:
* - application/json
* responses:
* 200:
* description: TrainingSession
*/
router.delete('/:userId/sessions/:id', passport.ensureAuthenticatedAndIsMe, async (req, res, next) => {
try {
const result = await db.trainingSession.update(req.params.id, {status: "terminated"});
return res.json(result);
} catch (error) {
next(error);
}
});
module.exports = router;

File diff suppressed because it is too large Load Diff

269
server/routes/qsProxy.js Normal file
View File

@@ -0,0 +1,269 @@
const express = require('express')
const router = express.Router()
const jsonwebtoken = require("jsonwebtoken");
const fs = require("fs");
const path = require("path");
const cookie = require("cookie");
const cookieParser = require("cookie-parser");
const axios = require("axios");
const { v4: uuidv4 } = require("uuid");
const ws = require("ws");
const WebSocketServer = ws.WebSocketServer;
const WebSocket = ws.WebSocket;
const passport = require('../passport-okta');
// This is the frontend application uri used for responding to requests.
//const frontendUri = "https://outstanding-desert-gatsby.glitch.me";
const privKey = fs.readFileSync(path.resolve(__dirname, '..', 'certs', 'privateKey.pem'));
const TENANT_DOMAIN = process.env["TENANT_DOMAIN"] || "innovation.us.qlikcloud.com";
const JWT_KEYID = process.env["JWT_KEYID"] || "8889be0d-6cf0-44eb-9fef-24bbc83712c5";
const JWT_ISSUER = process.env["JWT_ISSUER"] || TENANT_DOMAIN;
const JWT_USER_GROUPS = ["AnonJWTGroup"];
console.log("QSProxy# - TENANT_DOMAIN", TENANT_DOMAIN);
const auth = {
generateToken: function (user) {
// kid and issuer have to match with the IDP config and the audience has to be qlik.api/jwt-login-session
const signingOptions = {
keyid: JWT_KEYID,
algorithm: "RS256",
issuer: JWT_ISSUER,
expiresIn: "60m",
notBefore: "0s",
audience: "qlik.api/login/jwt-session",
};
// These are the claims that will be accepted and mapped anything else will be ignored. sub, subtype and name are mandatory.
const uuid = uuidv4();
userEmail = user.mail;
const payload = {
jti: uuid,
sub: `az/${user.sub}`,
subType: "user",
name: user.displayName,
email: userEmail,
email_verified: true,
groups: JWT_USER_GROUPS,
};
const token = jsonwebtoken.sign(payload, privKey, signingOptions);
return token;
},
getQlikSessionCookie: async function (token) {
try {
const resp = await axios(`https://${TENANT_DOMAIN}/login/jwt-session`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
},
});
if (resp.status === 200) {
console.log("QSProxy# - login/jwt-session", resp.headers['set-cookie'])
return resp.headers['set-cookie']
.map((e) => {
return e.split(";")[0];
})
.join(";");
}
return "";
} catch (e) {
console.log("QSProxy# - ERRRR", e);
return "";
}
},
};
const getStoreData = function (sidParsed) {
const store = passport.sessionStore;
return new Promise((resolve) => {
store.get(sidParsed, (err, session) => {
if (err) {
throw err;
}
return resolve(session);
});
});
};
router.get("/*.js", async (req, res) => {
setCors(res);
res.redirect(`https://${TENANT_DOMAIN}${req.path}`);
res.end();
});
// fetch resource from qlik using a redirect instead of proxy
// This endpoint is necessary when your web application uses the capability API.
router.get("/resources/*", async (req, res) => {
setCors(res);
res.redirect(`https://${TENANT_DOMAIN}${req.path}`);
res.end();
});
router.get('/assets/*', async (req, res) => {
setCors(res);
res.redirect(`https://${TENANT_DOMAIN}${req.path}`);
res.end();
});
// Issues the necessary pre-flight request to make sure the browser
// knows how to work with the web application.
router.options("/*", async (req, res) => {
setCors(res);
res.status(200).end();
});
function setCors(res) {
//res.set("Access-Control-Allow-Origin", "http://localhost:3000");
//res.set("Access-Control-Allow-Methods", "GET, OPTIONS");
//res.set("Access-Control-Allow-Headers", "Content-Type, x-proxy-session-id");
//res.set("Access-Control-Allow-Credentials", "true");
}
async function newSession(req){
console.log("QSProxy# - New cookie request for user", req.user.mail);
const jwtToken = auth.generateToken(req.user);
const qlikSession = await auth.getQlikSessionCookie(jwtToken);
return encodeURIComponent(qlikSession);
}
// Intercepts a request to one of Qlik's REST APIs and proxies the request to
// Qlik Cloud.
router.get("/api/v1/*", passport.ensureAuthenticated, async (req, res) => {
setCors(res);
var session = req.session;
var reqHeaders = {};
try {
if (session && session.id && session.qlikSession) {
console.log("QSProxy# - COOKIE FROM session", session.qlikSession);
reqHeaders.cookie = decodeURIComponent(session.qlikSession);
} else {
const newS = await newSession(req);
session.qlikSession = newS;
reqHeaders.cookie = decodeURIComponent(session.qlikSession);
}
try {
console.log("QSProxy# - qlikSession", reqHeaders.cookie);
const { status, data } = await axios(`https://${TENANT_DOMAIN}${req.path}`, {
headers: reqHeaders,
});
res.status(status).end(data);
} catch (e2) {
console.log("QSProxy# Error: QlikSession expired, requesting a new one.");
let newS = await newSession(req);
session.qlikSession = newS;
reqHeaders.cookie = decodeURIComponent(session.qlikSession);
let { status, data } = await axios(`https://${TENANT_DOMAIN}${req.path}`, {
headers: reqHeaders,
});
res.status(status).end(data);
}
} catch (e) {
res.status(500).end("Error obtaining qlik session");
}
});
router.get('/qlik-embed-iframe/*', async (req, res) => {
setCors(res);
res.redirect(`https://${TENANT_DOMAIN}${req.path}`);
res.end();
});
function init (server) {
// Websocket section for intercepting websocket requests from the
// frontend application. When the front end application communicates
// communicates with the backend using websockets, this set of
// functions will be invoked.
const wss = new WebSocketServer({ server });
wss.on("connection", async function connection(ws, req) {
let isOpened = false;
// WebSockets do not have access to session information.
// To get the session you need to parse the 1st-party cookie.
// This will give you access to the Qlik Cloud cookie in order
// to proxy requests.
const cookieString = req.headers.cookie;
let qlikCookie = "";
if (cookieString) {
const cookieParsed = cookie.parse(cookieString);
const appCookie = cookieParsed["connect.sid"];
if (appCookie) {
const sidParsed = cookieParser.signedCookie(appCookie, "secret");
var session = await getStoreData(sidParsed);
qlikCookie = decodeURIComponent(session.qlikSession);
}
}
const appId = req.url.match("/app/(.*)\\?")[1];
if (!qlikCookie){
console.log("QSProxy# - Error in Websocket: NO qlikCookie!");
return;
}
const matchCookie = qlikCookie.match("_csrfToken=(.*);");
if (!matchCookie) {
console.log("QSProxy# - Error in Websocket: cant find _csrfToken= in qlikCookie");
return;
}
const csrfToken = matchCookie[1];
var wsConnUrl = `wss://${TENANT_DOMAIN}/app/${appId}?qlik-csrf-token=${csrfToken}`;
const qlikWebSocket = new WebSocket(
wsConnUrl,
{
headers: {
cookie: qlikCookie,
},
}
);
qlikWebSocket.on("error", console.error);
const openPromise = new Promise((resolve) => {
qlikWebSocket.on("open", function open() {
resolve();
});
});
ws.on("message", async function message(data) {
if (!isOpened) {
await openPromise;
isOpened = true;
}
qlikWebSocket.send(data.toString());
});
qlikWebSocket.on("message", function message(data) {
ws.send(data.toString());
});
});
}
module.exports.router = router;
module.exports.init = init;

View File

@@ -2,7 +2,7 @@ const url = require("url");
const express = require("express");
import Arena from 'bull-arena';
import { TF_APPLY_QUEUE, TF_APPLY_QSEOK_QUEUE, TF_DESTROY_QUEUE } from 'qmi-cloud-common/queues';
import { TF_APPLY_QUEUE, TF_APPLY_QSEOK_QUEUE, TF_DESTROY_QUEUE, STOP_CONTAINER_QUEUE, SYNAPSE_QUEUE } from 'qmi-cloud-common/queues';
const app = express();
const routesApiScenarios = require('./routes/api-scenarios');
@@ -11,8 +11,10 @@ const routesApiProvisions = require('./routes/api-provisions');
const routesApiDestroyProvisions = require('./routes/api-destroyprovisions');
const routesApiNotifications = require('./routes/api-notifications');
const routesApiDivvy = require('./routes/api-divvy');
const routesApiDeployOpts = require('./routes/api-deployopts')
const routesApiApikeys = require('./routes/api-apikeys')
const routesApiDeployOpts = require('./routes/api-deployopts');
const routesApiApikeys = require('./routes/api-apikeys');
const routesApiStats = require('./routes/api-stats');
const routesApiTraining = require('./routes/api-training');
const swaggerUi = require('swagger-ui-express');
const swaggerJsdoc = require('swagger-jsdoc');
const cookieParser = require('cookie-parser');
@@ -21,7 +23,16 @@ const https = require('https');
const fs = require('fs');
const path = require('path');
const passport = require('./passport');
const passport = require('./passport-okta');
const qsProxy = require("./routes/qsProxy");
const BACKEND_LOGS_URL = process.env.BACKEND_LOGS_URL;
const QMI_MONGO_URL = process.env.QMI_MONGO_URL;
const IS_SECURE = process.env.CERT_PFX_PASSWORD && process.env.CERT_PFX_FILENAME;
function _getRedisConfig(redisUrl) {
const redisConfig = url.parse(redisUrl);
@@ -51,6 +62,16 @@ app.use('/arena', Arena(
name: TF_DESTROY_QUEUE,
hostId: 'Worker',
redis: _getRedisConfig(process.env.REDIS_URL)
},
{
name: STOP_CONTAINER_QUEUE,
hostId: 'Worker',
redis: _getRedisConfig(process.env.REDIS_URL)
},
{
name: SYNAPSE_QUEUE,
hostId: 'Worker',
redis: _getRedisConfig(process.env.REDIS_URL)
}
]
},
@@ -75,7 +96,7 @@ app.use(bodyParser.json())
app.use('/',express.static(__dirname + '/../dist/qmi-cloud'));
passport.init(app);
passport.init(app, IS_SECURE? true : false);
app.use("/api/v1/scenarios", routesApiScenarios);
app.use("/api/v1/users", routesApiUsers);
@@ -85,26 +106,46 @@ app.use("/api/v1/notifications", routesApiNotifications);
app.use("/api/v1/divvy", routesApiDivvy);
app.use("/api/v1/deployopts", routesApiDeployOpts);
app.use("/api/v1/apikeys", routesApiApikeys);
app.use("/api/v1/stats", routesApiStats);
app.use("/api/v1/training", routesApiTraining);
app.use("/qcsproxy", qsProxy.router);
function _isAllowedPath(path){
const allowedPaths = [ '/api-docs', '/arena', '/costexport', '/backendlogs', '/photos/user/', '/qmimongo' ];
let isAllowed = false;
for (let i=0; i<allowedPaths.length; i++) {
if ( path.startsWith( allowedPaths[i]) ) {
isAllowed = true;
break;
}
}
return isAllowed;
}
/* Checking allowedPaths */
app.get('/*',(req, res, next) =>{
if (req.originalUrl.indexOf("/api-docs") !== -1 || req.originalUrl.indexOf("/arena") !== -1 || req.originalUrl.indexOf("/costexport") !== -1 || req.originalUrl.indexOf("/backendlogs") !== -1) {
if ( _isAllowedPath(req.originalUrl) ) {
return next();
} else if (req.originalUrl.indexOf("oauth-callback.html") !== -1) {
res.sendFile(path.join(__dirname,'/../dist/qmi-cloud/oauth-callback.html'));
} else {
res.sendFile(path.join(__dirname,'/../dist/qmi-cloud/index.html'));
}
});
/* -----------------------*/
app.get('/login', passport.ensureAuthenticatedDoLogin, function(req, res) {
res.redirect("/");
});
app.get('/logout', function(req, res) {
res.redirect("/");
});
app.get('/backendlogs', function (req, res) {
res.redirect(process.env.BACKEND_LOGS_URL);
})
res.redirect(BACKEND_LOGS_URL);
res.end();
});
app.get('/qmimongo/*', function (req, res) {
let path = req.path.split("/qmimongo")[1];
console.log("QMI-Mongo# Redirect: " + `${QMI_MONGO_URL}${path}`);
res.redirect(`${QMI_MONGO_URL}${path}`);
res.end();
});
const options = {
@@ -116,8 +157,8 @@ const options = {
version: '1.0.0',
description: 'REST API for QMI Cloud solutions',
contact: {
"name": "Qlik GEAR",
"email": "DL-Enterprise-ArchitectsGEAR@qlik.com"
"name": "Qlik Presales - Innovation & Excellence Team",
"email": "DL-PresalesGlobalInnovation@qlik.com"
}
},
servers: [{
@@ -131,25 +172,7 @@ const options = {
name: "apiKey",
in: "query"
}
},
/*schemas: {
"user": {
"properties": {
"displayName": {
"type": "string"
},
"upn": {
"type": "string"
},
"oid": {
"type": "string"
},
"role": {
"type": "string"
}
}
}
}*/
}
},
security: [{
ApiKeyAuth: []
@@ -167,21 +190,35 @@ app.use('/costexport*', passport.ensureAuthenticatedAndAdmin, function(req, res)
res.status(404).send("Not found");
} else {
res.header("Content-Type",'application/json');
res.sendFile(path.resolve(__dirname, '..', 'costexport', req.query.file ));
}
} );
app.use('/photos/user/:oid', passport.ensureAuthenticated, function(req, res){
if ( !req.params.oid ) {
res.status(404).send("Not found");
} else {
var pic = path.resolve(__dirname, '..', 'photos', `${req.params.oid}.jpg` );
if (fs.existsSync(pic)){
res.sendFile(pic);
} else {
res.status(404).send();
}
}
} );
const specs = swaggerJsdoc(options);
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs));
/**
* Create necessary folders
*/
console.log("--- Create necessary folders");
var dirs = ['/logs', '/logs/provision', '/logs/destroy'];
var dirs = ['/logs', '/logs/provision', '/logs/destroy', '/costexports', '/photos'];
dirs.forEach(d => {
if (!fs.existsSync(d)){
console.log(`--- Creating folder '${d}' since it does not exist`);
fs.mkdirSync(d);
}
});
@@ -190,19 +227,26 @@ dirs.forEach(d => {
/**
* Start App
*/
app.listen(3000, () => {
const server = app.listen(3000, () => {
console.log(`Server listening on port 3000`)
});
qsProxy.init(server);
if ( process.env.CERT_PFX_PASSWORD && process.env.CERT_PFX_FILENAME) {
if ( IS_SECURE ) {
var optionsHttps = {
pfx: fs.readFileSync(path.resolve(__dirname, 'certs', process.env.CERT_PFX_FILENAME)),
passphrase: process.env.CERT_PFX_PASSWORD
};
https.createServer(optionsHttps, app).listen(3100, function(){
const httpsServer = https.createServer(optionsHttps, app).listen(3100, function(){
console.log(`Secure server listening on port 3100`);
});
qsProxy.init(httpsServer);
}

View File

@@ -0,0 +1,129 @@
const axios = require("axios");
const AUTOMATION_NAME_PREFIX = "QMI-Training";
async function asyncForEach(array, callback) {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array);
}
}
const _newAutomation = async function(host, apikey, index, workspace) {
let workspaceData = require(`./workspaces/${index}-${workspace}.json`);
let name = `${AUTOMATION_NAME_PREFIX}-${index}-${workspace}`;
let result = await axios({
method: "post",
url: `https://${host}/api/v1/automations/`,
headers: {
"Authorization": `Bearer ${apikey}`,
"Content-Type": "application/json",
"Accept": "application/json"
},
data: {
"id":"create",
"name": name,
"description": workspace === 'main'? "Main automation entry point for QMI training session" : ""
},
});
let data = result.data;
console.log("createAutomation #1", data);
let result2 = await axios({
method: "put",
url: `https://${host}/api/v1/automations/${data.id}`,
headers: {
"Authorization": `Bearer ${apikey}`,
"Content-Type": "application/json",
"Accept": "application/json"
},
data: {
"workspace": workspaceData
},
});
console.log("createAutomation #2", result2.data);
return result2.data;
};
async function runQlikAutomation(session, email) {
let automationUrl = session.qaUrl;
let automationToken = session.qaToken;
try {
var result = await axios({
method: "post",
url: automationUrl,
headers: {
"X-Execution-Token": automationToken,
"Content-Type": "application/json"
},
data: {
email: email,
apiKey: session.qcsApiKey,
sharedSpace: session.qcsSharedSpace? session.qcsSharedSpace : null,
dataSpace: session.qcsDataSpace? session.qcsDataSpace : null
},
});
console.log(`Executed Qlik Automation '${automationUrl}' for user '${email}'`);
return result.data;
} catch (error) {
return {error: error};
}
}
async function getAutomation(session, workspace) {
const name = `${AUTOMATION_NAME_PREFIX}-${session.template.index}-${workspace}`;
console.log(`# GetAutomation: find "${name}"`);
try {
var result = await axios({
method: "get",
url: `https://${session.qcsTenantHost}/api/v1/automations?filter=name eq "${name}"`,
headers: {
"Authorization": `Bearer ${session.qcsApiKey}`,
"Content-Type": "application/json",
"Accept": "application/json"
}
});
if ( result.data && result.data.length ) {
console.log("# GetAutomation", result.data[0]);
return result.data[0];
} else {
console.log("# GetAutomation", 'Not found');
return null;
}
} catch (error) {
return {error: error};
}
}
async function createAutomations(session) {
try {
let result = {};
console.log('asyncForEach', session.template.needQcsAutomation);
await asyncForEach(session.template.needQcsAutomation, async function( workspace ) {
console.log("EXECUTING: ", workspace);
result[workspace] = await _newAutomation(session.qcsTenantHost, session.qcsApiKey, session.template.index, workspace);
});
console.log("Final results automations", result);
return result;
} catch (error) {
return {error: error};
}
}
module.exports.runQlikAutomation = runQlikAutomation;
module.exports.createAutomations = createAutomations;
module.exports.getAutomation = getAutomation;

View File

@@ -0,0 +1,128 @@
const BASEURL = "https://use.cloudshare.com/api/v3/class";
const axios = require("axios");
const sha1 = require("js-sha1");
const API_KEY = process.env.CLOUDSHARE_API_KEY;
const API_ID = process.env.CLOUDSHARE_API_ID;
function randomString(length) {
var chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
var result = "";
for (var i = length; i > 0; --i)
result += chars[Math.floor(Math.random() * chars.length)];
return result;
}
function getRequestAuth(url) {
var timestamp = Math.floor(Date.now() / 1000);
var token = randomString(10);
var hmac = `${API_KEY}${url}${timestamp}${token}`;
var hmacSHA1 = sha1(hmac);
return `cs_sha1 userapiid:${API_ID};timestamp:${timestamp};token:${token};hmac:${hmacSHA1}`;
};
async function _getAllStudentsInClass(classId) {
var url = `${BASEURL}/${classId}/students?isFull=false`;
var auth = getRequestAuth(url);
try {
var result = await axios({
method: "GET",
url: url,
headers: {
Accpet: "application/json",
"Content-Type": "application/json",
Authorization: auth,
},
});
return result.data;
} catch (error) {
return {error: error};
}
};
async function _sendInvitationToUser(classId, userId) {
var url = `${BASEURL}/actions/sendinvitations?isMultiple=true`;
var auth = getRequestAuth(url);
try {
var result = await axios({
method: "POST",
url: url,
headers: {
Accpet: "application/json",
"Content-Type": "application/json",
Authorization: auth,
},
data: {
classId: classId,
studentIds: [userId],
},
});
return result.data;
} catch (error) {
return {error: error};
}
}
async function _addUser(classId, email) {
var url = `${BASEURL}/${classId}/Students`;
var auth = getRequestAuth(url);
var firstName = email.split("@")[0];
var lastName = email.split("@")[1];
try {
var result = await axios({
method: "POST",
url: url,
headers: {
Accpet: "application/json",
"Content-Type": "application/json",
Authorization: auth,
},
data: {
email: email,
firstName: firstName,
lastName: lastName,
properties: {},
},
});
return result.data;
} catch (error) {
return {error: error};
}
}
var addStudentToClass = async function (session, email) {
const classId = session.cloudshareClass;
console.log(`Executed 'Add Student' to class '${classId}' for user '${email}'`);
const students = await _getAllStudentsInClass(classId);
let student = students.find(function (user) {
return user.email === email;
});
if (student) {
console.log(`Student already in class (${classId}), sending invitation: ${student.email}` );
return await _sendInvitationToUser(classId, student.id);
} else {
console.log(`Adding student to class (${classId}): ${email}`);
student = await _addUser(classId, email);
console.log(`Student Added to class (${classId}): ${email}`);
console.log(`Student:`, student);
return await _sendInvitationToUser(classId, student.studentId);
}
};
module.exports.addStudentToClass = addStudentToClass;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,7 @@
<p style="margin-top: 70px;text-align: right;">
<a class="lui-text-info" href="/backendlogs" target="blank"><i class="fas fa-long-arrow-alt-right"></i> Backend Logs (internal)</a>
<a class="lui-text-info" href="/backendlogs" target="_blank"><i class="fas fa-long-arrow-alt-right"></i> Backend Logs (internal)</a>
</p>
<ul class="nav nav-pills nav-fill">
@@ -10,34 +10,29 @@
</li>
</ul>
<div *ngIf="tab === 'Scenarios'">
<!--<h1>Scenarios</h1>-->
<div *ngIf="tab === 'scenarios'">
<table-scenarios></table-scenarios>
</div>
<div *ngIf="tab === 'Scenario Deploy Opts'">
<!--<h1>Scenarios</h1>-->
<div *ngIf="tab === 'scenario-deploy-opts'">
<table-subscriptions></table-subscriptions>
</div>
<div *ngIf="tab === 'Provisions'">
<!--<h1>Provisions</h1>-->
<div *ngIf="tab === 'provisions'">
<table-provisions></table-provisions>
</div>
<div *ngIf="tab === 'Users'">
<!--<h1>Users</h1>-->
<div *ngIf="tab === 'users'">
<table-users></table-users>
</div>
<div *ngIf="tab === 'Notifications'">
<!--<h1>Scenarios</h1>-->
<div *ngIf="tab === 'notifications'">
<table-notifications></table-notifications>
</div>
<div *ngIf="tab === 'API keys'">
<!--<h1>Scenarios</h1>-->
<div *ngIf="tab === 'api-keys'">
<table-apikeys></table-apikeys>
</div>
<div *ngIf="tab === 'VM Types'">
<!--<h1>Scenarios</h1>-->
<div *ngIf="tab === 'vm-types'">
<table-vmtypes></table-vmtypes>
</div>
<qmi-alert></qmi-alert>
<div *ngIf="tab === 'sessions'">
<table-sessions></table-sessions>
</div>

View File

@@ -1,24 +1,35 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
@Component({
selector: 'app-admin',
templateUrl: './admin.component.html',
styleUrls: ['./admin.component.scss']
})
export class AdminComponent implements OnInit {
export class AdminComponent implements OnInit, OnDestroy {
sections = ['Provisions', 'Scenarios', 'Scenario Deploy Opts', 'Users', 'Notifications','API keys', 'VM Types'];
sections = ['provisions', 'scenarios', 'scenario-deploy-opts', 'users', 'notifications','sessions', 'api-keys', 'vm-types'];
tab : string = 'Provisions';
private sub: any;
constructor() { }
constructor( private route: ActivatedRoute, private router: Router ) { }
ngOnInit() {
this.sub = this.route.params.subscribe(params => {
console.log("Params", params);
this.tab = params['tab'] || 'provisions'; // (+) converts string 'id' to a number
});
}
tabSelect($event, tab) {
$event.preventDefault();
this.router.navigate(['/admin', tab]);
/*$event.preventDefault();
$event.stopPropagation();
this.tab = tab;
this.tab = tab;*/
}
ngOnDestroy() {
this.sub.unsubscribe();
}
}

View File

@@ -6,17 +6,33 @@ import { StatsComponent } from './stats/stats.component';
import { HomeComponent } from './home/home.component';
import { AuthGuard } from './services/auth.guard';
import { FaqComponent } from './faq/faq.component';
import { UserDashboardComponent } from './user/user-dashboard.component';
import { ScenariosSectionComponent } from './scenarios/scenarios-section.component';
import { ProvisionsSharedComponent } from './provisions/provisions-shared.component';
import { CostComponent } from './cost/cost.component';
import { ProvComponent } from './provisions/prov.component';
import { TrainingComponent } from './training/training.component';
import { SessionFormComponent } from './sessionform/sessionform.component';
import { FeatureGuard } from './services/feature.guard';
import { ErrorComponent } from './home/error.component';
const routes: Routes = [
{ path: 'home', component: HomeComponent},
{ path: 'faq', component: FaqComponent},
{ path: 'error', component: ErrorComponent},
{ path: 'scenarios', component: ScenariosSectionComponent, canActivate: [AuthGuard]},
{ path: 'provision/:id', component: ProvComponent, canActivate: [AuthGuard]},
{ path: 'provisions', component: ProvisionsComponent, canActivate: [AuthGuard]},
{ path: 'sharedprovision', component: ProvisionsSharedComponent, canActivate: [AuthGuard]},
{ path: 'cost-analysis', component: CostComponent, canActivate: [AuthGuard]},
{ path: 'admin', component: AdminComponent, canActivate: [AuthGuard]},
{ path: 'admin/:tab', component: AdminComponent, canActivate: [AuthGuard]},
{ path: 'stats', component: StatsComponent, canActivate: [AuthGuard]},
{ path: '',
redirectTo: '/home',
pathMatch: 'full'
},
{ path: 'training', component: TrainingComponent, canActivate: [AuthGuard, FeatureGuard]},
{ path: 'training/session/:id/public', component: SessionFormComponent},
{ path: 'user/:id', component: UserDashboardComponent, canActivate: [AuthGuard]},
{ path: '', redirectTo: '/home', pathMatch: 'full'},
{ path: '**', redirectTo: '/home' }
];

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