mirror of
https://github.com/GNS3/gns3-server.git
synced 2025-06-24 17:55:15 +00:00
Compare commits
840 Commits
Author | SHA1 | Date | |
---|---|---|---|
b2db8896ea | |||
a22f7ac3d2 | |||
e19af851c1 | |||
a00f53fbaa | |||
8db81de153 | |||
fa0d7d7529 | |||
bd813b0a53 | |||
b37db57bb9 | |||
31a2cb998d | |||
e83e12b51a | |||
a7da814b85 | |||
11c9802268 | |||
a60e0d424d | |||
db1fb29c63 | |||
ec9dbd20b8 | |||
a5f0dba5cf | |||
754680d8e9 | |||
05773ce189 | |||
aeae9deb01 | |||
f3749e83ec | |||
c2db4e8542 | |||
52a7696618 | |||
7bf17392dd | |||
2718224dde | |||
0122070b29 | |||
345ecb7630 | |||
ae538dd11d | |||
ce4dd17409 | |||
8c7c17b889 | |||
1530bdfc2b | |||
74d07173c1 | |||
ec300d19d4 | |||
fb06eb3c0c | |||
7e902726bf | |||
c4fcb43e78 | |||
753135a80d | |||
c05e1cb8e7 | |||
48b7e6ca50 | |||
dbe2b8a5fb | |||
80487eb62d | |||
6e66b26b88 | |||
9990666076 | |||
97e5a5edc3 | |||
2b5c6ce8fb | |||
0aac62d03a | |||
4058abf16e | |||
4357410b10 | |||
734365b216 | |||
abb7cc2075 | |||
c273a78560 | |||
718269e5b3 | |||
19fd7d2193 | |||
e62ffb1b87 | |||
42808161bf | |||
45ee662c56 | |||
24bfc205db | |||
cb46c0fbcc | |||
35256901b5 | |||
6933c3d938 | |||
8af71ee291 | |||
c0be6875c2 | |||
a1922ef067 | |||
cafdb2522b | |||
f7996d5e98 | |||
996dad2f5c | |||
1332e940fd | |||
19bd953d31 | |||
3a896b6964 | |||
441f0fb1b5 | |||
a02b57698a | |||
7582107ae5 | |||
2dbde5df22 | |||
a7b56739a9 | |||
8ce0cbf7f7 | |||
7b5d123ad8 | |||
d1a7474ef6 | |||
af6f34b2ca | |||
d3138daa23 | |||
967c7d066d | |||
7619080a50 | |||
f1294cf267 | |||
74782d413f | |||
c41c11eb34 | |||
1f09a3e6bc | |||
22f022cc22 | |||
8429494280 | |||
a1666dd247 | |||
3214f52e9e | |||
ddd6235acd | |||
bcc148bbd2 | |||
3792901dc7 | |||
5ffe5fd9b3 | |||
9bcf26b943 | |||
b80e1f3a22 | |||
ba13b0b078 | |||
90c69d95ad | |||
69a5b16bad | |||
21a5c5d3f2 | |||
225779bc11 | |||
b1209155f5 | |||
7cebb9472a | |||
00c3b1abc1 | |||
b1c1c0520c | |||
2bb26075f0 | |||
54e436394f | |||
9046c60811 | |||
24cace8122 | |||
6746ef39be | |||
8889eaa439 | |||
95a4c284bc | |||
9a3bd2ee0c | |||
2416069a6e | |||
eefb3d87b2 | |||
999f41b03e | |||
ea339af1e9 | |||
6951137a25 | |||
cccbca3d5e | |||
8659439829 | |||
677bc389a5 | |||
3f7f5a3cda | |||
59ad5c55ec | |||
4a58ede550 | |||
3f5b0bb514 | |||
6e8e0764c1 | |||
eaa18ad624 | |||
0a32be276e | |||
271c958381 | |||
ca1d99b112 | |||
6b14abc06f | |||
6ed18c561f | |||
90dce03da2 | |||
b48bd92da3 | |||
d54c9db8c3 | |||
b194e48649 | |||
29f848d833 | |||
08154e43aa | |||
45a0644371 | |||
3a4ffb8786 | |||
84efc55d02 | |||
fec0220b55 | |||
a58fb320ac | |||
39057bd781 | |||
0f3b96f134 | |||
df210386dd | |||
3e512e650a | |||
e50e2c90bd | |||
c1f9f1111d | |||
8c1dbf0084 | |||
6f2b12c218 | |||
4b1068e02f | |||
d56acc5844 | |||
d477eb8366 | |||
35f94b0c73 | |||
b82b9a64e6 | |||
901652aaeb | |||
990dabb7e4 | |||
3ab74b8094 | |||
5cd540d94d | |||
d3860ba84b | |||
3e133eda44 | |||
e01256980c | |||
aabfe9eadc | |||
6c9a3b634b | |||
0561aba927 | |||
86a78f53cb | |||
f0bc74ff6e | |||
f81128f717 | |||
9ce505cad3 | |||
314e01d2b4 | |||
6b8635f7d5 | |||
388ffe9f00 | |||
d01129a3cf | |||
535ea0ad9f | |||
ccb5de6df1 | |||
cdcfc652db | |||
7030dadbcb | |||
46a9fdb61d | |||
9958b876cf | |||
259d3a0155 | |||
33499be425 | |||
3fd44f5260 | |||
c076275bd0 | |||
081447ba2e | |||
3df7af12e1 | |||
d9e4ce5639 | |||
689d476b71 | |||
f1701e3f09 | |||
38df4bea2e | |||
35b75f0f7f | |||
2609849507 | |||
448b94f701 | |||
18002a7580 | |||
644e659729 | |||
1c37e8bd32 | |||
1414bdcadf | |||
b6e68ba1a8 | |||
19142d97bb | |||
faf7e09ebb | |||
84f7337cb1 | |||
e11621467b | |||
123123d767 | |||
b3bdeb611e | |||
7a229e33f9 | |||
4959a51ce7 | |||
5b1ba5a57c | |||
bc1a8c99d1 | |||
ae64f15386 | |||
123b5d0800 | |||
e1d3ee12b9 | |||
998898a471 | |||
9b66d939d2 | |||
fd60bda2c2 | |||
c5f4e25756 | |||
16bad6e0b5 | |||
8e661df981 | |||
31c7fe88f1 | |||
03b491f7b1 | |||
fa41d9ba75 | |||
a80aed5420 | |||
e9827653ae | |||
9da8d1ec0f | |||
a53fd2e8b1 | |||
5bab4131e1 | |||
e8a7e80456 | |||
ab05784c33 | |||
6983e686ca | |||
3631202ce5 | |||
8d8a3247c4 | |||
d100a132d6 | |||
52638a9a8d | |||
9a5af82a28 | |||
14a618766d | |||
0137688ba7 | |||
84db3b9996 | |||
800920e3df | |||
14917580ac | |||
4c65426e97 | |||
fd85e49220 | |||
0a08031d85 | |||
1a53c9aabf | |||
1f5085608c | |||
16f72b4d3d | |||
3ced41633f | |||
4fa10be5aa | |||
f050fc7e00 | |||
c93aafc9af | |||
93520b4d6c | |||
1fb0260ae6 | |||
91fd543321 | |||
763ef24108 | |||
17aabd6cda | |||
d94adf4c8f | |||
4dd9cf2da2 | |||
d6f8069739 | |||
0037f31553 | |||
e2a3d391d8 | |||
6c5f54fe57 | |||
082fbee1bd | |||
6d97feaced | |||
8d35089661 | |||
6455f62447 | |||
2fb3b1ebab | |||
58399a9fa8 | |||
907b305ecf | |||
57f92db124 | |||
0f6f943a83 | |||
2cb76b2274 | |||
2d2db52a8b | |||
a8d0818e07 | |||
f7eb2492d9 | |||
218522b08c | |||
470f13f448 | |||
317aa669ac | |||
8d160ad5ed | |||
e3493870b2 | |||
d466c85385 | |||
35d4391fc0 | |||
eea0ab69bd | |||
22ade94118 | |||
08ee40548f | |||
7f05a06766 | |||
9fd2f58ef7 | |||
b9b802ebab | |||
1d86e322e9 | |||
8eb5f10971 | |||
cc4783ab98 | |||
19792f328d | |||
596d1274a3 | |||
2f765747b8 | |||
4fe57b6a15 | |||
1177626a53 | |||
c4cc346864 | |||
8915dfffa5 | |||
ff027ebd17 | |||
27d5ac537f | |||
fe246cd413 | |||
2bbb560b8e | |||
1624c7d6ad | |||
2a3bb81076 | |||
e0a0dd83d7 | |||
16ea395618 | |||
9dd8da4f6e | |||
54abf85523 | |||
385fffec56 | |||
ac86717bc0 | |||
7b7af33920 | |||
85fd3ef4a4 | |||
fbd8f6ce28 | |||
195da24910 | |||
b4c7609aeb | |||
cd9c4c84b6 | |||
2566de0f5a | |||
6a069d0af6 | |||
2789b3802a | |||
9e1a1337d4 | |||
466eb8128e | |||
e41227b5bd | |||
78a3b8e824 | |||
4c149e1614 | |||
473fa98eda | |||
13d9afd8bc | |||
b4edbbbaa7 | |||
02e19800fb | |||
bc1016474e | |||
24bb0ca419 | |||
42f668ffc4 | |||
646f0fd837 | |||
3d383b25f0 | |||
8d28178e56 | |||
0d63bf5128 | |||
37f720ac73 | |||
9509d3a713 | |||
061ee1491d | |||
c8245e81be | |||
ff7e10fa7f | |||
6c5f75bf43 | |||
24d958d9ec | |||
48dab4af61 | |||
67be4259ff | |||
4e9bc190a4 | |||
261dd1d6d5 | |||
e80e80a080 | |||
09ff807055 | |||
303cbf3642 | |||
e367b3a148 | |||
7afdcbb3d3 | |||
de27a57a00 | |||
c88f76b740 | |||
b81cc14cef | |||
aa133c3b8a | |||
7449064ea2 | |||
db315e3c3c | |||
c437482e85 | |||
46d9ada6d8 | |||
aac6fbfc31 | |||
76bd5921c5 | |||
8a208dbf04 | |||
af3a618306 | |||
3c4b5db1ca | |||
5c8abdc6fd | |||
f80f41b5b3 | |||
503373a6e9 | |||
8236c85b5f | |||
531499bbfb | |||
ae825b0080 | |||
7ad3afbdef | |||
191feed669 | |||
cd785e0d17 | |||
e1bd4638c8 | |||
61dede72f9 | |||
13630179b2 | |||
3b22bcfe96 | |||
0f07b9e019 | |||
8676a3dccf | |||
b0dbf59ac4 | |||
0e18762b36 | |||
cbc7e59d3f | |||
14fd165f2a | |||
7c49a9160c | |||
5a6e945db2 | |||
86c0b90951 | |||
23b1c7a989 | |||
399e925f2e | |||
ac11c984cb | |||
0e8e4fd2f7 | |||
ed99a98fe1 | |||
19edc193f9 | |||
3f50319990 | |||
fb6d29aeff | |||
5475eedce7 | |||
d680bbbc77 | |||
248737c34b | |||
240708112c | |||
c759c4e3f6 | |||
34ee75e1f1 | |||
4af5edbc03 | |||
0d15f40930 | |||
f3ad97c398 | |||
a55e1cbc99 | |||
9c369a2597 | |||
4dcb0dce57 | |||
0f0412158d | |||
2f3689588e | |||
44c066ac68 | |||
ffc49ea954 | |||
d610c933fb | |||
8f35b871d5 | |||
29c9d2c2e9 | |||
5ad7761337 | |||
9911b5199b | |||
edcc69f617 | |||
fe8b61373c | |||
87960f1ff0 | |||
3b0d89be5a | |||
5ff3043fb4 | |||
47d3f3f349 | |||
109feeb647 | |||
56839413fa | |||
18012dc18a | |||
46c02ad2f3 | |||
08ac80e451 | |||
6ca15cbcff | |||
722512d851 | |||
737664a3d4 | |||
29f4b238b0 | |||
b6127af067 | |||
5092bd2fdf | |||
a27db6b4eb | |||
66b66cc3e1 | |||
7215b150dd | |||
674381f1be | |||
1ae6d13022 | |||
6886f1f566 | |||
999a47f747 | |||
c1507b4155 | |||
d9214a3c67 | |||
f675f24ba7 | |||
dc1eed42da | |||
2c5a9ef9b7 | |||
e75d151611 | |||
d87cff7fd5 | |||
ffb58a4ed2 | |||
4d7b3fb9d6 | |||
94abdfc9f0 | |||
6f345bb1ec | |||
89ec458f5c | |||
80bc1a726b | |||
6265d3d55f | |||
a96a82821c | |||
ad47ffbe29 | |||
eca1243fc0 | |||
0d834ee2a0 | |||
e1c5c05492 | |||
7534718a1c | |||
63c1defd9a | |||
1f90bb14b6 | |||
ae00dd422f | |||
10eeefc1f5 | |||
702fea89fb | |||
a95dda0d1d | |||
d53ef175f8 | |||
f7d287242f | |||
c11b3c3911 | |||
1ec056c1a6 | |||
285b19d97f | |||
709aa46074 | |||
0c12849d0d | |||
8aa9d1aae6 | |||
66047b3431 | |||
71d06abaf4 | |||
05ef9f2a7c | |||
476658b829 | |||
6ec97a704f | |||
a358369606 | |||
5155aea923 | |||
b0c4fc17ad | |||
e72b07bf5c | |||
170f476733 | |||
0077fd98aa | |||
f3a4ad49f4 | |||
3e0592520b | |||
d3d0cc039d | |||
c2783d355f | |||
d6e1ee5dbb | |||
57197c3d1c | |||
60ce1172e0 | |||
6bd855b3c5 | |||
74cb3be910 | |||
425ad845fc | |||
1ce0c13fc9 | |||
e9e2dc2ca7 | |||
e61ada69bb | |||
a6b0f32b57 | |||
a69feb3682 | |||
090d1c8c84 | |||
df2f96828e | |||
6a614fbd78 | |||
2d7438446c | |||
77d4eabadc | |||
ca48efa5be | |||
b0657b39ef | |||
cefa459721 | |||
66dd8bdadb | |||
bbb0a407e0 | |||
96ce5eac8d | |||
2f2aabeb5a | |||
d9eb61efc4 | |||
1fd8444d22 | |||
f3b6825e40 | |||
719458764f | |||
483db91851 | |||
f3d43aeb39 | |||
1cb433c5bc | |||
17f71f970e | |||
fe90d2b146 | |||
35e1519073 | |||
9f5aece8b6 | |||
08139ebdd1 | |||
cd87ac4474 | |||
77744cc31d | |||
df9d642a56 | |||
fad28c766a | |||
21d9d968c4 | |||
945a2c98cd | |||
5e1792ff4d | |||
a2628042e8 | |||
e616b09028 | |||
958865e919 | |||
7f595b089b | |||
a76d7576c6 | |||
533c4544ea | |||
04a302a389 | |||
5da742394e | |||
962c5eed8e | |||
f4dab1482f | |||
05d594f3cf | |||
d44f6eb2f1 | |||
9c456532d0 | |||
7ef9b7600c | |||
11f51b3809 | |||
1d59afa9fb | |||
c51d71a4fa | |||
af83adfdf6 | |||
658bfb7624 | |||
88a1cef21f | |||
1ced862c08 | |||
6097166055 | |||
46b8ee3279 | |||
d82a07600b | |||
2991899d50 | |||
9238c52f97 | |||
38ee79c15f | |||
3f691b60c9 | |||
a6ca7c4e36 | |||
b76d2c2150 | |||
d2a0632ec4 | |||
fde5f5cbe2 | |||
7dc5628845 | |||
723b519e8f | |||
9c653f7dbf | |||
c226b2a9ef | |||
c5c4e1ad56 | |||
8e2989d747 | |||
c181df6935 | |||
24c03406e7 | |||
6c8c5c1787 | |||
f541c03b94 | |||
0a14a08d54 | |||
9357ac6035 | |||
785eff869d | |||
24db2a039d | |||
bfb317a302 | |||
29b4e89d21 | |||
bbb68cb148 | |||
427bbc40b9 | |||
4b791d4924 | |||
6f132a2e09 | |||
d5cfb85de5 | |||
d366d77ff7 | |||
21049d73a5 | |||
c290d75eec | |||
40c265152b | |||
e5eeab662e | |||
a7a946c067 | |||
b762d1e60d | |||
1ff23348d3 | |||
3dbc714f37 | |||
682af449f8 | |||
60f1d76eab | |||
16b619066a | |||
d44038305f | |||
3641742930 | |||
4bf7838543 | |||
998fe6d8a4 | |||
aa79f4cb25 | |||
2602602193 | |||
8d969349c8 | |||
bf67fec242 | |||
a070a72f66 | |||
71d1aefb65 | |||
0b6436d2cb | |||
77f5044aec | |||
088ae699de | |||
21dc2a8dbc | |||
aae7bf9865 | |||
4338afab7f | |||
ef62eba4e8 | |||
cc4ead40e2 | |||
67943c4727 | |||
045c0c4c14 | |||
a0943b0b5a | |||
84c886b058 | |||
e59b5fb95b | |||
cc4d6759e3 | |||
e45467af57 | |||
c6d6fcfe66 | |||
73bcd019cc | |||
2361e138ec | |||
0f02cff5f2 | |||
72af145a23 | |||
8cc0244c5a | |||
b3dfb87622 | |||
d03e7ac9c5 | |||
af2fc8c111 | |||
25c03b7823 | |||
b4bfb24a80 | |||
d6ea546ff7 | |||
68f9c55f3d | |||
9868c28bc6 | |||
f3f7921525 | |||
65cc12c850 | |||
161f62d083 | |||
af2b5c3448 | |||
1591ca114d | |||
d6141d4652 | |||
8bf5eb2754 | |||
ce220e5ae4 | |||
791ce6a56e | |||
66157a335d | |||
513bc0fb16 | |||
ffe35b6559 | |||
f08ce9d3f1 | |||
2bb71838ac | |||
f347e21100 | |||
3fb138b9a1 | |||
9de847fc45 | |||
ea550508f6 | |||
267c4cbbbb | |||
c33a13fd45 | |||
ceb8208002 | |||
42767eff56 | |||
0406083991 | |||
91dba66800 | |||
69b741a01e | |||
8903ab8a47 | |||
4d62c6943b | |||
2a66f67b5f | |||
8ef59c7832 | |||
cbdab1f0d7 | |||
7968ee7ff2 | |||
be557abba9 | |||
3040e87a7d | |||
1f85abb036 | |||
d867e1f1c1 | |||
1490e458b6 | |||
30c85703c8 | |||
4bdd405bc5 | |||
229168367b | |||
e257fb425e | |||
fac224ac48 | |||
62c2ca9be1 | |||
f98a60fc74 | |||
42a5f1956e | |||
13df828ca7 | |||
19a8d1caed | |||
290df5290a | |||
fbe10360c2 | |||
ac042b02c4 | |||
e44999f588 | |||
952b4e7249 | |||
3b0336bd7e | |||
7689b7841f | |||
bf5970b904 | |||
2eca92e34d | |||
70d3f991ed | |||
77b6ef1a1b | |||
aaa6ca9445 | |||
2b6bec1b93 | |||
0f49911432 | |||
9c158848bd | |||
3a6ff9b44f | |||
83cb8693e6 | |||
e5ecdd270d | |||
260ac44e87 | |||
1fa4632b55 | |||
a0cf711634 | |||
b83645c531 | |||
a7daae1c6a | |||
a7d19fd89a | |||
3c2ae5363c | |||
24d1aeee32 | |||
8d56b7b18c | |||
90d7478679 | |||
fb4c04ac41 | |||
dc1b98a1d0 | |||
c57b0cbb53 | |||
ab1f9a4385 | |||
ce3bf9930c | |||
a033080418 | |||
af9b883c08 | |||
f09594ae4c | |||
5a3b502024 | |||
997e0ad6ee | |||
eb3fc1bb3a | |||
e9c95ff299 | |||
78c301653f | |||
2a392ef991 | |||
535f89e29e | |||
72e3d8f0c1 | |||
27d9063e56 | |||
f8f5d7ec07 | |||
d0141c351b | |||
8e2992fbc7 | |||
8986f10506 | |||
10f3adcb60 | |||
5459543eb5 | |||
e15c36f05c | |||
da7c7d16e4 | |||
8d69f7f792 | |||
297ada529c | |||
ae200d9add | |||
eb1b70456f | |||
9132002b80 | |||
771a9a5ddb | |||
04ba3b6549 | |||
8f9800f444 | |||
90c971ed74 | |||
d2ad9dc5e2 | |||
e5c8ae4bde | |||
5bccf4841d | |||
b3a6b9173b | |||
2d6b260188 | |||
c814245426 | |||
d787f38c21 | |||
343022c63b | |||
1148dbc48e | |||
85679aaa94 | |||
5bcc247881 | |||
b8d595928b | |||
3804249d89 | |||
c56a8ef8f7 | |||
076e85ddb3 | |||
2550fb3495 | |||
f837912ebc | |||
830cc108d0 | |||
36ffe2bb59 | |||
f8739eb5e1 | |||
5bc030688e | |||
9f75a49e0a | |||
7f440f43d8 | |||
3e29ae4276 | |||
91b50eb5f2 | |||
c6e31d98db | |||
9a7b3bed25 | |||
8ad7b3f613 | |||
00691390eb | |||
17e0b2e259 | |||
5666b43ee0 | |||
84f256dd88 | |||
1bbecf1ceb | |||
72a4d0df4e | |||
93449c942f | |||
649173b293 | |||
1c183e660f | |||
634e6a3549 | |||
a91ed34f43 | |||
5209009df9 | |||
5ba526e175 | |||
d17c243b13 | |||
9e4ae6bc24 | |||
4b410cfa42 | |||
b9e415b6fe | |||
d2d327cc76 | |||
7643185fa2 | |||
263febecbc | |||
0a046e43d7 | |||
a7036d14d5 | |||
1aca7dbe04 | |||
338128ca3e | |||
e165cc48e3 | |||
c80a55b18c | |||
284a4b62fb | |||
d33fefa183 | |||
dfb48c884b | |||
18ba60d1a3 | |||
521132726e | |||
f80aca7633 | |||
84914ecfa4 | |||
5696d1ccb7 | |||
18b0863ba8 | |||
ece47dc279 | |||
6e1d49d8ca | |||
2e550d839e | |||
a4b24eaceb | |||
f04702d607 | |||
6f11df6189 | |||
9cf2e4f5a6 | |||
9316876f97 | |||
f3b8f43689 | |||
2d74d1ad94 | |||
ec50cc7c0d | |||
f8ee3b3d50 | |||
bbf9c04d20 | |||
67e42f9353 | |||
31a490211b | |||
c7df8331ed | |||
de1654a50f | |||
a6959de1f8 | |||
8cfedce468 | |||
77dd772314 | |||
9ff302592c | |||
6b3d4feab8 | |||
78b2b93540 | |||
0303e21059 | |||
e20d1adb0e | |||
e5bee8ff52 | |||
58471ea7f4 | |||
9fb0ba5a71 | |||
3d738e83f5 | |||
19de2732f2 | |||
40c22e389e | |||
0419c081fe | |||
dced70a565 | |||
9a7222b83e | |||
b0e646b97d | |||
e8973b9c44 | |||
eeae27ae0d | |||
3634cc8307 | |||
3b108563a0 | |||
790bf4521d |
16
.github/workflows/add-new-issues-to-project.yml
vendored
Normal file
16
.github/workflows/add-new-issues-to-project.yml
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
name: Add new issues to GNS3 project
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- opened
|
||||
|
||||
jobs:
|
||||
add-to-project:
|
||||
name: Add issue to project
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/add-to-project@v1.0.1
|
||||
with:
|
||||
project-url: https://github.com/orgs/GNS3/projects/3
|
||||
github-token: ${{ secrets.ADD_NEW_ISSUES_TO_PROJECT }}
|
79
.github/workflows/codeql-analysis.yml
vendored
79
.github/workflows/codeql-analysis.yml
vendored
@ -13,58 +13,81 @@ name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ master ]
|
||||
branches: [ "master" ]
|
||||
schedule:
|
||||
- cron: '44 1 * * 3'
|
||||
- cron: '21 12 * * 4'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
name: Analyze (${{ matrix.language }})
|
||||
# Runner size impacts CodeQL analysis time. To learn more, please see:
|
||||
# - https://gh.io/recommended-hardware-resources-for-running-codeql
|
||||
# - https://gh.io/supported-runners-and-hardware-resources
|
||||
# - https://gh.io/using-larger-runners (GitHub.com only)
|
||||
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
|
||||
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
|
||||
timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
|
||||
permissions:
|
||||
# required for all workflows
|
||||
security-events: write
|
||||
|
||||
# required to fetch internal or private CodeQL packs
|
||||
packages: read
|
||||
|
||||
# only required for workflows in private repositories
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'python' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||
# Learn more about CodeQL language support at https://git.io/codeql-language-support
|
||||
|
||||
include:
|
||||
- language: python
|
||||
build-mode: none
|
||||
# CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
|
||||
# Use `c-cpp` to analyze code written in C, C++ or both
|
||||
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
|
||||
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
|
||||
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
|
||||
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
|
||||
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
|
||||
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
# If the analyze step fails for one of the languages you are analyzing with
|
||||
# "We were unable to automatically build your code", modify the matrix above
|
||||
# to set the build mode to "manual" for that language. Then modify this step
|
||||
# to build your code.
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
- if: matrix.build-mode == 'manual'
|
||||
shell: bash
|
||||
run: |
|
||||
echo 'If you are using a "manual" build mode for one or more of the' \
|
||||
'languages you are analyzing, replace this with the commands to build' \
|
||||
'your code, for example:'
|
||||
echo ' make bootstrap'
|
||||
echo ' make release'
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
@ -12,25 +12,22 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: "gh-pages"
|
||||
- uses: actions/setup-python@v2
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.7
|
||||
python-version: 3.8
|
||||
- name: Merge changes from 3.0 branch
|
||||
run: |
|
||||
git config user.name github-actions
|
||||
git config user.email github-actions@github.com
|
||||
git merge origin/3.0 -X theirs
|
||||
- name: Install dependencies
|
||||
- name: Install GNS3 server and dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||
- name: Install GNS3 server
|
||||
run: |
|
||||
python setup.py install
|
||||
python -m pip install .
|
||||
- name: Generate the API documentation
|
||||
run: |
|
||||
cd scripts
|
26
.github/workflows/testing.yml
vendored
26
.github/workflows/testing.yml
vendored
@ -13,16 +13,21 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.7", "3.8", "3.9", "3.10"]
|
||||
os: ["ubuntu-latest"]
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
|
||||
#include:
|
||||
# only test with Python 3.10 on Windows
|
||||
# - os: windows-latest
|
||||
# python-version: "3.10"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Display Python version
|
||||
@ -30,10 +35,15 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install -r dev-requirements.txt
|
||||
- name: Install Windows dependencies
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: python -m pip install -r win-requirements.txt
|
||||
python -m pip install .[dev]
|
||||
|
||||
- name: Install Windows specific dependencies
|
||||
if: runner.os == 'Windows'
|
||||
run: |
|
||||
python -m pip install -r win-requirements.txt
|
||||
curl -O "http://www.win10pcap.org/download/Win10Pcap-v10.2-5002.msi"
|
||||
msiexec /i "Win10Pcap-v10.2-5002.msi" /qn /norestart
|
||||
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
# stop the build if there are Python syntax errors or undefined names
|
||||
|
@ -2,7 +2,8 @@
|
||||
"scanSettings": {
|
||||
"configMode": "AUTO",
|
||||
"configExternalURL": "",
|
||||
"projectToken" : ""
|
||||
"projectToken" : "",
|
||||
"baseBranches": ["master", "2.2", "3.0"]
|
||||
},
|
||||
"checkRunSettings": {
|
||||
"vulnerableCheckRunConclusionLevel": "failure"
|
||||
|
373
CHANGELOG
373
CHANGELOG
@ -1,5 +1,378 @@
|
||||
# Change Log
|
||||
|
||||
## 3.0.0rc2 20/11/2024
|
||||
|
||||
* Bundle web-ui v3.0.0rc2
|
||||
* Fix error 500 on PUT for cloud, nat, vmware and vpcs nodes. Fixes #2426
|
||||
* Add a duplicated project in the same resource pools as the original project if it is in any
|
||||
* Upgrade FastAPI to v0.115.5
|
||||
* Overwrite user resources when the originals have changed.
|
||||
* Relax setuptools requirement to allow for easier Debian packaging on Ubuntu Focal & Jammy
|
||||
* Increase SQLite timeout. Ref #2422
|
||||
* Fix test user with wrong creds
|
||||
* Upgrade dependencies and fix Pydantic warnings
|
||||
* Upgrade aiohttp to v3.10.10. Fixes #2411
|
||||
* Replace aiohttp.web.HTTPConflict()
|
||||
* Python 3.13 support
|
||||
|
||||
## 2.2.51 07/11/2024
|
||||
|
||||
* Catch error when cannot resize Docker container TTY.
|
||||
* Do not use "ide" if there is a disk image and no interface type has been explicitly configured.
|
||||
* Use @locking when sending uBridge commands. Ref https://github.com/GNS3/gns3-gui/issues/3651
|
||||
* Fix run Docker containers with user namespaces enabled. Fixes #2414
|
||||
* Python 3.13 support
|
||||
* Upgrade dependencies
|
||||
* Fix errors in init.sh. Fixes #2431
|
||||
|
||||
## 2.2.50 21/10/2024
|
||||
|
||||
* Bundle web-ui v2.2.50
|
||||
* Symbolic links support for project export/import
|
||||
* Add comment to indicate sentry-sdk is optional. Ref https://github.com/GNS3/gns3-server/issues/2423
|
||||
* Fix issues with recent busybox versions
|
||||
* Support to reset MAC addresses for Docker nodes and some adjustments for fast duplication.
|
||||
* Update README.md to change the minimum required Python version.
|
||||
* Faster project duplication for local projects (no remote compute)
|
||||
* Improve error message when a project cannot be parsed.
|
||||
* Fix for running Docker containers with user namespaces enabled
|
||||
* Support for configuring MAC address in Docker containers
|
||||
* Upgrade aiohttp to v3.10.3
|
||||
|
||||
## 3.0.0rc1 11/08/2024
|
||||
|
||||
* Bundle web-ui v3.0.0rc1
|
||||
* Convert topologies < 3.0 to have valid node hostnames
|
||||
* Fix to access resources_path and install_builtin_appliances settings
|
||||
|
||||
## 2.2.49 06/08/2024
|
||||
|
||||
* Bundle web-ui v2.2.49
|
||||
* Forbid -nic and -nicdev in Qemu additional options. Fixes https://github.com/GNS3/gns3-server/issues/2397
|
||||
* Upgrade jsonschema and sentry-sdk packages
|
||||
* Update IOU base configs to use "no ip domain lookup". Fixes #2404
|
||||
|
||||
## 2.2.48.1 12/07/2024
|
||||
|
||||
* Bundle web-ui v2.2.48.1
|
||||
|
||||
## 2.2.48 08/07/2024
|
||||
|
||||
* Bundle web-ui v2.2.48
|
||||
* Add 'install_builtin_appliances' and 'resources_path' settings in the server config
|
||||
* Option to keep the compute IDs unchanged when exporting a project
|
||||
* Forbid unsafe Qemu additional options
|
||||
* Fix error when snapshot exists with an underscore in the name
|
||||
* Upgrade sentry-sdk, psutil and aiofiles packages
|
||||
* Fix check for IPv6 enabled on host
|
||||
|
||||
## 3.0.0b3 19/05/2024
|
||||
|
||||
* Bundle web-ui v3.0.0b3
|
||||
* Fix to allow duplicating IOS routers
|
||||
* Fix to allow changing the auxiliary console for IOS router nodes.
|
||||
* Replace deprecated method datetime.utcnow()
|
||||
* Upgrade FastAPI to v0.111.0
|
||||
* Fix reset console for non running IOU devices
|
||||
* Do not allow to create a builtin template using the API.
|
||||
* Use cryptography backend for python-jose. Ref #2372
|
||||
|
||||
## 2.2.47 15/05/2024
|
||||
|
||||
* Fix update-bundled-web-ui.sh script
|
||||
* Bundle web-ui v2.2.47
|
||||
* Change sentry-sdk version
|
||||
* Upgrade aiohttp, sentry-sdk and truststore
|
||||
* Upgrade jsonschema and aiohttp
|
||||
* Drop Python 3.7
|
||||
* Remove dev requirements for Python 3.6
|
||||
* Do not run Docker VM tests on Windows
|
||||
* Do not wait for the server to close when shutting down.
|
||||
* Fix test create image with not supported characters by filesystem. Fixes #2375
|
||||
* Allow listing x86_64 IOU images. Fixes #2376
|
||||
* Upgrade Jinja2 to version 3.1.4. Fixes #2378
|
||||
* Fix link capture for ATM switch. Fixes https://github.com/GNS3/gns3-gui/issues/3570
|
||||
* Fix tests after updating error message when busybox is not installed.
|
||||
* Add more details to error message when busybox is not installed. Fixes https://github.com/GNS3/gns3-gui/issues/3569
|
||||
* Fix invalid escape sequences
|
||||
* Add NAT symbols
|
||||
* Fix cannot stop Docker VM while console connection is still active.
|
||||
* Upgrade sentry-sdk to version 1.40.6
|
||||
|
||||
## 3.0.0b2 07/04/2024
|
||||
|
||||
* Bundle web-ui v3.0.0b2
|
||||
* Fix cannot stop Docker VM while console connection is still active.
|
||||
* Support for custom Qemu path in templates and nodes
|
||||
* Fix CPU fractional values for Docker VMs.
|
||||
* Use bcrypt directly instead of passlib
|
||||
* Update CORS policy
|
||||
* Do not stop searching for Qemu binaries if one binary cannot be executed. Ref #2306
|
||||
* Fix Ethernet switch and Ethernet hub port validations. Fixes #2334
|
||||
* Update CORS policy
|
||||
|
||||
|
||||
## 2.2.46 26/02/2024
|
||||
|
||||
* Bundle web-ui v2.2.46
|
||||
* Save empty directories when exporting a project
|
||||
* Backport from v3: install Docker resources in a writable location at runtime.
|
||||
* Use Docker API v1.24 to get version.
|
||||
* Drop support for Python 3.6
|
||||
* Address the telnet console bug.
|
||||
* Update welcome.py
|
||||
* Update remote-install.sh
|
||||
* Use Python 3.8 to publish API doc
|
||||
* Upgrade sentry-sdk, psutil and distro dependencies
|
||||
|
||||
## 2.2.45 12/01/2024
|
||||
|
||||
* Bundle web-ui v2.2.45
|
||||
* Fix mouse offset issues with VNC in Qemu. Fixes #2335
|
||||
* Add project.created, project.opened and project.deleted controller notification stream. Move project.updated and project.closed from project notification to controller notification stream.
|
||||
* Do not stop searching for Qemu binaries if one binary cannot be executed. Ref #2306
|
||||
* Fix Ethernet switch and Ethernet hub port validations. Fixes #2334
|
||||
* Update CORS policy
|
||||
* Add custom executable paths on Windows
|
||||
* Upgrade sentry-sdk and aiohttp
|
||||
|
||||
## 3.0.0b1 27/11/2023
|
||||
|
||||
* Bundle web-ui v3.0.0b1
|
||||
* Upgrade sentry-sdk to v1.37.1
|
||||
* Upgrade aiohttp to v3.9.1
|
||||
* Fix bug when listing endpoints for opened project
|
||||
* Make images executable after importing a project
|
||||
* Disable IOS hostname check for Dynamips ghost instances
|
||||
|
||||
## 3.0.0a6 15/11/2023
|
||||
|
||||
* Bundle web-ui v3.0.0a6
|
||||
* Upgrade to aiohttp v3.9.0rc0
|
||||
* Install Docker resources in writable location
|
||||
* Default compute username is "gns3"
|
||||
* Non-blocking checksums computation when server starts. Fixes #2228
|
||||
* Fix timeout issue when creating Qemu disk image. Fixes https://github.com/GNS3/gns3-server/issues/2313
|
||||
* Fix broken link to Web UI in 3.0 branch. Fixes #2312
|
||||
* Fix sample config: VMware section declared twice. Fixes #2311
|
||||
* Fix ws console and packet capture over SSL
|
||||
* Support for web socket console over HTTPS
|
||||
* Allow disabling hardware virtualization check
|
||||
|
||||
## 2.2.44.1 07/11/2023
|
||||
|
||||
* Catch exceptions when computing image checksums. Ref https://github.com/GNS3/gns3-server/issues/2228
|
||||
* Add freeze_support() for multiprocessing
|
||||
|
||||
## 2.2.44 06/11/2023
|
||||
|
||||
* Bundle web-ui v2.2.44
|
||||
* Non-blocking checksums computation when server starts. Fixes #2228
|
||||
* Fix timeout issue when creating Qemu disk image. Fixes https://github.com/GNS3/gns3-server/issues/2313
|
||||
* Support for web socket console over HTTPS
|
||||
* Add back script create_cert.sh
|
||||
* Allow disabling hardware virtualization check
|
||||
* Fix L2IOU "failed code signing checks" when IOU base file name is >= 63 characters
|
||||
* Change "ip cef" to "no ip cef" in IOU default configs. Fixes #2298
|
||||
* Add Qemu IGB network device
|
||||
* Add Python 3.12 support.
|
||||
* Fix issue with importlib.resources.files() and Python 3.9
|
||||
|
||||
## 3.0.0a5 27/10/2023
|
||||
|
||||
* Bundle web-ui v3.0.0a5
|
||||
* Fix L2IOU "failed code signing checks" when IOU base file name is >= 63 characters
|
||||
* Python 3.12 support
|
||||
* Add igb Qemu adapter
|
||||
* Change "ip cef" to "no ip cef" in IOU default configs. Fixes #2298
|
||||
* Drop support for Python 3.7 and upgrade dependencies
|
||||
* Fix compute authentication for websocket endpoints
|
||||
* Add Qemu IGB network device
|
||||
|
||||
## 3.0.0a4 18/10/2023
|
||||
|
||||
* Bundle web-ui v3.0.0a4
|
||||
* Do not enforce Compute.Audit and Template.Audit privileges due to current web-ui limitations
|
||||
* Support to create empty disk images on the controller
|
||||
* Fix issue with importlib.resources.files() and Python 3.9
|
||||
* New RBAC system with resource pools support.
|
||||
* Use controller vars file to store version and appliance etag
|
||||
* Pydantic v2 migration
|
||||
* Allow connection to ws console over IPv6
|
||||
* Allow computes to be dynamically or manually allocated
|
||||
* Add UEFI boot mode option for Qemu VMs
|
||||
* Mark VMware and VirtualBox support as deprecated
|
||||
* Make port name for custom adapters optional. Fixes https://github.com/GNS3/gns3-web-ui/issues/1430
|
||||
* Support for database schema migrations using alembic
|
||||
* Add config option to change the server name. Ref #2149
|
||||
* Option to disable image discovery and do not scan parent directory
|
||||
* Allow raw images by default. Fixes https://github.com/GNS3/gns3-server/issues/2097
|
||||
* Fix bug when creating Dynamips router with chassis setting
|
||||
* Stricter checks to create/update an Ethernet switch and add tests
|
||||
* Fix schema for removing WICs from Cisco routers. Fixes #3392
|
||||
* Fix some issues with HTTP notification streams
|
||||
* API endpoint to get the locked status of a project
|
||||
* Global project lock and unlock
|
||||
* Require name for custom adapters. Fixes #2098
|
||||
* Allow empty adapter slots for Dynamips templates. Ref https://github.com/GNS3/gns3-gui/issues/3373
|
||||
* Custom adapters should not be in node (compute) properties returned to clients. Fixes https://github.com/GNS3/gns3-gui/issues/3366
|
||||
* Optionally allow Qemu raw images
|
||||
* Ignore image detection for IOU user libraries in image directory
|
||||
* Checks for valid hostname on server side for Dynamips, IOU, Qemu and Docker nodes
|
||||
* Only check files (not directories) when looking for new images on file system.
|
||||
* Support user defined loader/libraries to run IOU
|
||||
* Remove explicit Response for VPCS endpoints returning HTTP 204 status code
|
||||
* Remove explicit Response for endpoints returning HTTP 204 status code
|
||||
* Make 'vendor_url' and 'maintainer_email' optional for template validation.
|
||||
* Allow auth token to be passed as a URL param
|
||||
* Add controller endpoints to get VirtualBox VMs, VMware VMs and Docker images
|
||||
* Detect new images added to the default image directory. * Images can be present before the server starts or while it is running * Images are recorded in the database
|
||||
* Support delete Qemu disk image from API Return the real disk image name in the 'hdx_disk_image_backed' property for Qemu VMs
|
||||
* Fix ComputeConflictError import
|
||||
* Handle creating Qemu disk images and resizing
|
||||
* Finish to clean up local setting usage. Ref #1460
|
||||
* "Local" command line parameter is only for stopping a server that has been started by the desktop GUI
|
||||
* Fix AsyncSession handling after breaking changes in FastAPI 0.74.0 See https://github.com/tiangolo/fastapi/releases/tag/0.74.0 for details.
|
||||
* Detect image type instead of requesting it from user
|
||||
* Add connect endpoint for computes Param to connect to compute after creation Report compute unauthorized HTTP errors to client
|
||||
* Replace CORS origins by origin regex
|
||||
* Allow empty compute_id. Ref #1657
|
||||
* Secure controller to compute communication using HTTP basic authentication
|
||||
* Secure websocket endpoints
|
||||
* Allocate compute when compute_id is unset
|
||||
* Return the current controller hostname/IP from any compute
|
||||
* Remove Qemu legacy networking support
|
||||
* Appliance management refactoring: * Install an appliance based on selected version * Each template have unique name and version * Allow to download an appliance file
|
||||
* Add isolate and unisolate endpoints. Ref https://github.com/GNS3/gns3-gui/issues/3190
|
||||
* Allow images to be stored in subdirs and used by templates.
|
||||
* Use uuid5 to create new compute_id. Fixes #1641 #1887
|
||||
* Migrate PCAP streaming code to work with FastAPI.
|
||||
* Refactor WebSocket console code to work with FastAPI. Fix endpoint routes.
|
||||
|
||||
|
||||
## 2.2.43 19/09/2023
|
||||
|
||||
* Force English output for VBoxManage. Fixes #2266
|
||||
* Automatically add vboxnet and DHCP server if not present for VirtualBox GNS3 VM. Ref #2266
|
||||
* Fix issue with controller config saved before checking current version with previous one
|
||||
* Prevent X11 socket file to be modified by Docker container
|
||||
* Use the user data dir to store built-in appliances
|
||||
* Catch ConnectionResetError exception when client disconnects
|
||||
* Upgrade to PyQt 5.15.9 and pywin32
|
||||
|
||||
## 2.2.42 09/08/2023
|
||||
|
||||
* Bundle web-ui v2.2.42
|
||||
* Handle API version key in VirtualBox 7. Fixes #2266
|
||||
* Enable system certificate store for SSL connections
|
||||
* Use DEFAULT_BUFFER_SIZE for md5sum
|
||||
* Fix version check when installing appliances. Ref https://github.com/GNS3/gns3-gui/issues/3486
|
||||
* Allow connection to ws console over IPv6. Fixes https://github.com/GNS3/gns3-web-ui/issues/1400
|
||||
* Support for Python 3.12
|
||||
* Remove import urllib3 and let sentry_sdk import and patch it. Fixes https://github.com/GNS3/gns3-gui/issues/3498
|
||||
|
||||
## 2.2.41 12/07/2023
|
||||
|
||||
* Bundle web-ui v2.2.41
|
||||
* Catch urllib3 exceptions when sending crash report. Ref https://github.com/GNS3/gns3-gui/issues/3483
|
||||
* Only fetch Qemu version once when starting Qemu + only add speed/duplex for virtio-net-pci with Qemu version >= 2.12
|
||||
* Use recent OVMF firmware (stable-202305) and use flash drives to configure Qemu command line
|
||||
* Remove the useless executable permissions to the file gns3server/disks/empty8G.qcow2
|
||||
* Backport UEFI boot mode support for Qemu VMs
|
||||
|
||||
## 2.2.40.1 10/06/2023
|
||||
|
||||
* Re-bundle Web-Ui v2.2.40. Fixes #2239
|
||||
|
||||
## 2.2.40 06/06/2023
|
||||
|
||||
* qemu : with network adapter_type equal to "virtio-net-pci", fix the speed to 10000 and duplex to full. The values are actually fake. (https://github.com/GNS3/gns3-gui/issues/3476)
|
||||
* Parse name for request to node creation from template
|
||||
* Remove Xvfb + x11vnc support
|
||||
* Require a Host-Only Network to start the VirtualBox GNS3 VM on macOS with VirtualBox 7
|
||||
* Properly catch aiohttp client exception. Ref #2228
|
||||
* Catch ConnectionResetError when waiting for the wrap console
|
||||
* Fix open IPv6 address for HTTP consoles on controller. Fixes https://github.com/GNS3/gns3-gui/issues/3448
|
||||
* Use proc.communicate() when checking for subprocess output As recommended in https://docs.python.org/3/library/asyncio-subprocess.html#asyncio.subprocess.Process.stderr
|
||||
|
||||
## 2.2.39 08/05/2023
|
||||
|
||||
* Install web-ui v2.2.39
|
||||
* Add generic function to install resource files
|
||||
* Install empty Qemu disks on first start
|
||||
* Check for colon in project name. Fixes #2203
|
||||
* Upgrade distro and aiohttp dependencies
|
||||
|
||||
## 2.2.38 28/02/2023
|
||||
|
||||
* Bundle web-ui v2.2.38
|
||||
* Fix c7200_i0_log.txt is created in the current directory. Fixes #2191
|
||||
* Check swtpm version and start swtpm before qemu
|
||||
* Fix broken websocket console with Python 3.11
|
||||
* Fix "cannot reopen console". Ref #2182
|
||||
* Fix Qemu binary not set when adding appliance from template
|
||||
|
||||
## 2.2.37 25/01/2023
|
||||
|
||||
* Fix link communication issues on Windows with uBridge
|
||||
* Fix StreamWriter doesn't have the wait_closed() method in Python3.6. Fixes #2170
|
||||
* Install built-in appliances when no previous version has been detected. Fixes #2168
|
||||
* Update documentation to install gns3-server. Fixes #2124
|
||||
* Give udhcpc executable right. Fixes #2159
|
||||
|
||||
## 2.2.36 04/01/2023
|
||||
|
||||
* Install web-ui v2.2.36
|
||||
* Add Trusted Platform Module (TPM) support for Qemu VMs
|
||||
* Require Dynamips 0.2.23 and bind Dynamips hypervisor on 127.0.0.1
|
||||
* Delete the built-in appliance directory before installing updated files
|
||||
* Use a stock BusyBox for the Docker integration
|
||||
* Overwrite built-in appliance files when starting a more recent version of the server
|
||||
* Fix reset console. Fixes #1619
|
||||
* Only use importlib_resources for Python <= 3.9. Fixes #2147
|
||||
* Support when the user field defined in Docker container is an ID. Fixes #2134
|
||||
|
||||
## 3.0.0a3 27/12/2022
|
||||
|
||||
* Add web-ui v3.0.0a3
|
||||
* Add config option to change the server name. Ref #2149
|
||||
* Option to disable image discovery and do not scan parent directory
|
||||
* Allow raw images by default. Fixes https://github.com/GNS3/gns3-server/issues/2097
|
||||
* Fix bug when creating Dynamips router with chassis setting
|
||||
* Stricter checks to create/update an Ethernet switch and add tests
|
||||
* Fix schema for removing WICs from Cisco routers. Fixes #3392
|
||||
* Fix issues with VMnet interface on macOS >= 11.0. Ref #3381
|
||||
* Use importlib_resources instead of pkg_resources and install built-in appliances in config dir.
|
||||
* Fix console vnc don't use configured ports in some case. Fixes #2111
|
||||
* Add missing VMware settings in gns3_server.conf
|
||||
* Make version PEP 440 compliant
|
||||
* Support for Python 3.11
|
||||
* Allow for more dependency versions at patch level
|
||||
* Replace deprecated distro.linux_distribution() call
|
||||
* Update gns3.service.systemd
|
||||
* Fix some issues with HTTP notification streams
|
||||
* gns3.service.openrc: make openrc script posix compliant
|
||||
* fix: use exact match to find interface in windows to avoid get wrong interface
|
||||
|
||||
## 2.2.35.1 10/11/2022
|
||||
|
||||
* Re-release Web-Ui v2.2.35
|
||||
|
||||
## 2.2.35 08/11/2022
|
||||
|
||||
* Release web-ui v2.2.35
|
||||
* Fix issues with VMnet interface on macOS >= 11.0. Ref #3381
|
||||
* Use importlib_resources instead of pkg_resources and install built-in appliances in config dir.
|
||||
* Fix console vnc don't use configured ports in some case. Fixes #2111
|
||||
* Add missing VMware settings in gns3_server.conf
|
||||
* Make version PEP 440 compliant
|
||||
* Support for Python 3.11
|
||||
* Allow for more dependency versions at patch level
|
||||
* Replace deprecated distro.linux_distribution() call
|
||||
* Update gns3.service.systemd
|
||||
* gns3.service.openrc: make openrc script posix compliant
|
||||
* fix: use exact match to find interface in windows to avoid get wrong interface
|
||||
|
||||
## 3.0.0a2 06/09/2022
|
||||
|
||||
* Add web-ui v3.0.0a2
|
||||
|
@ -34,4 +34,4 @@ COPY . /gns3server
|
||||
RUN mkdir -p ~/.config/GNS3/3.0/
|
||||
RUN cp scripts/gns3_server.conf ~/.config/GNS3/3.0/
|
||||
|
||||
RUN python3 setup.py install
|
||||
RUN python3 -m pip install .
|
||||
|
10
MANIFEST.in
10
MANIFEST.in
@ -1,11 +1,7 @@
|
||||
include README.rst
|
||||
include AUTHORS
|
||||
include README.md
|
||||
include LICENSE
|
||||
include MANIFEST.in
|
||||
include requirements.txt
|
||||
include conf/*.conf
|
||||
recursive-include tests *
|
||||
recursive-exclude docs *
|
||||
include CHANGELOG
|
||||
recursive-include gns3server *
|
||||
recursive-exclude docs *
|
||||
recursive-exclude * __pycache__
|
||||
recursive-exclude * *.py[co]
|
||||
|
@ -6,7 +6,7 @@
|
||||
[](https://snyk.io/test/github/GNS3/gns3-server)
|
||||
|
||||
The GNS3 server manages emulators and other virtualization software such as Dynamips, Qemu/KVM, Docker, VPCS, VirtualBox and VMware Workstation.
|
||||
Clients like the [GNS3 GUI](https://github.com/GNS3/gns3-gui/) and the [GNS3 Web UI](https://github.com/GNS3/gns3-web-ui>) control the server using a HTTP REST API.
|
||||
Clients like the [GNS3 GUI](https://github.com/GNS3/gns3-gui/) and the [GNS3 Web UI](https://github.com/GNS3/gns3-web-ui/) control the server using a HTTP REST API.
|
||||
|
||||
## Installation
|
||||
|
||||
@ -85,8 +85,8 @@ cd gns3-server
|
||||
git checkout 3.0
|
||||
python3 -m venv venv-gns3server
|
||||
source venv-gns3server/bin/activate
|
||||
python3 setup.py install
|
||||
python3 -m gns3server --local
|
||||
python3 -m pip install .
|
||||
python3 -m gns3server
|
||||
```
|
||||
|
||||
You will have to manually install other software dependencies (see above), for Dynamips, VPCS and uBridge the easiest is to install from our PPA.
|
||||
|
@ -14,4 +14,4 @@ currently being supported with security updates.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please contact us at security@gns3.net
|
||||
Please use GitHub's report a vulnerability feature. More information can be found in https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability
|
||||
|
@ -1,8 +1,7 @@
|
||||
-r requirements.txt
|
||||
|
||||
pytest==7.1.2
|
||||
flake8==5.0.4
|
||||
pytest-timeout==2.1.0
|
||||
pytest-asyncio==0.19.0
|
||||
requests==2.28.1
|
||||
httpx==0.23.0
|
||||
pytest==8.3.3
|
||||
flake8==7.1.1
|
||||
pytest-timeout==2.3.1
|
||||
pytest-asyncio==0.21.2
|
||||
requests==2.32.3
|
||||
httpx==0.27.2 # version 0.24.1 is required by httpx_ws
|
||||
httpx_ws==0.6.2
|
||||
|
103
gns3server/alembic.ini
Normal file
103
gns3server/alembic.ini
Normal file
@ -0,0 +1,103 @@
|
||||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# path to migration scripts
|
||||
script_location = db_migrations
|
||||
|
||||
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
||||
# Uncomment the line below if you want the files to be prepended with date and time
|
||||
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
|
||||
|
||||
# sys.path path, will be prepended to sys.path if present.
|
||||
# defaults to the current working directory.
|
||||
prepend_sys_path = .
|
||||
|
||||
# timezone to use when rendering the date within the migration file
|
||||
# as well as the filename.
|
||||
# If specified, requires the python-dateutil library that can be
|
||||
# installed by adding `alembic[tz]` to the pip requirements
|
||||
# string value is passed to dateutil.tz.gettz()
|
||||
# leave blank for localtime
|
||||
# timezone =
|
||||
|
||||
# max length of characters to apply to the
|
||||
# "slug" field
|
||||
# truncate_slug_length = 40
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
# set to 'true' to allow .pyc and .pyo files without
|
||||
# a source .py file to be detected as revisions in the
|
||||
# versions/ directory
|
||||
# sourceless = false
|
||||
|
||||
# version location specification; This defaults
|
||||
# to db_migrations/versions. When using multiple version
|
||||
# directories, initial revisions must be specified with --version-path.
|
||||
# The path separator used here should be the separator specified by "version_path_separator" below.
|
||||
# version_locations = %(here)s/bar:%(here)s/bat:db_migrations/versions
|
||||
|
||||
# version path separator; As mentioned above, this is the character used to split
|
||||
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
|
||||
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
|
||||
# Valid values for version_path_separator are:
|
||||
#
|
||||
# version_path_separator = :
|
||||
# version_path_separator = ;
|
||||
# version_path_separator = space
|
||||
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
|
||||
|
||||
# the output encoding used when revision files
|
||||
# are written from script.py.mako
|
||||
# output_encoding = utf-8
|
||||
|
||||
sqlalchemy.url =
|
||||
|
||||
|
||||
[post_write_hooks]
|
||||
# post_write_hooks defines scripts or Python functions that are run
|
||||
# on newly generated revision scripts. See the documentation for further
|
||||
# detail and examples
|
||||
|
||||
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
||||
# hooks = black
|
||||
# black.type = console_scripts
|
||||
# black.entrypoint = black
|
||||
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
@ -58,7 +58,6 @@ log = logging.getLogger(__name__)
|
||||
|
||||
compute_api = FastAPI(
|
||||
title="GNS3 compute API",
|
||||
dependencies=[Depends(compute_authentication)],
|
||||
description="This page describes the private compute API for GNS3. PLEASE DO NOT USE DIRECTLY!",
|
||||
version="v3",
|
||||
)
|
||||
@ -158,11 +157,13 @@ async def http_exception_handler(request: Request, exc: StarletteHTTPException):
|
||||
|
||||
compute_api.include_router(
|
||||
capabilities.router,
|
||||
dependencies=[Depends(compute_authentication)],
|
||||
tags=["Capabilities"]
|
||||
)
|
||||
|
||||
compute_api.include_router(
|
||||
compute.router,
|
||||
dependencies=[Depends(compute_authentication)],
|
||||
tags=["Compute"]
|
||||
)
|
||||
|
||||
@ -173,21 +174,25 @@ compute_api.include_router(
|
||||
|
||||
compute_api.include_router(
|
||||
projects.router,
|
||||
dependencies=[Depends(compute_authentication)],
|
||||
tags=["Projects"]
|
||||
)
|
||||
|
||||
compute_api.include_router(
|
||||
images.router,
|
||||
dependencies=[Depends(compute_authentication)],
|
||||
tags=["Images"]
|
||||
)
|
||||
|
||||
compute_api.include_router(
|
||||
atm_switch_nodes.router,
|
||||
dependencies=[Depends(compute_authentication)],
|
||||
prefix="/projects/{project_id}/atm_switch/nodes",
|
||||
tags=["ATM switch"]
|
||||
)
|
||||
compute_api.include_router(
|
||||
cloud_nodes.router,
|
||||
dependencies=[Depends(compute_authentication)],
|
||||
prefix="/projects/{project_id}/cloud/nodes",
|
||||
tags=["Cloud nodes"]
|
||||
)
|
||||
@ -206,18 +211,21 @@ compute_api.include_router(
|
||||
|
||||
compute_api.include_router(
|
||||
ethernet_hub_nodes.router,
|
||||
dependencies=[Depends(compute_authentication)],
|
||||
prefix="/projects/{project_id}/ethernet_hub/nodes",
|
||||
tags=["Ethernet hub nodes"]
|
||||
)
|
||||
|
||||
compute_api.include_router(
|
||||
ethernet_switch_nodes.router,
|
||||
dependencies=[Depends(compute_authentication)],
|
||||
prefix="/projects/{project_id}/ethernet_switch/nodes",
|
||||
tags=["Ethernet switch nodes"]
|
||||
)
|
||||
|
||||
compute_api.include_router(
|
||||
frame_relay_switch_nodes.router,
|
||||
dependencies=[Depends(compute_authentication)],
|
||||
prefix="/projects/{project_id}/frame_relay_switch/nodes",
|
||||
tags=["Frame Relay switch nodes"]
|
||||
)
|
||||
@ -229,6 +237,7 @@ compute_api.include_router(
|
||||
|
||||
compute_api.include_router(
|
||||
nat_nodes.router,
|
||||
dependencies=[Depends(compute_authentication)],
|
||||
prefix="/projects/{project_id}/nat/nodes",
|
||||
tags=["NAT nodes"]
|
||||
)
|
||||
|
@ -85,7 +85,7 @@ def get_cloud(node: Cloud = Depends(dep_node)) -> schemas.Cloud:
|
||||
|
||||
|
||||
@router.put("/{node_id}", response_model=schemas.Cloud)
|
||||
def update_cloud(node_data: schemas.CloudUpdate, node: Cloud = Depends(dep_node)) -> schemas.Cloud:
|
||||
async def update_cloud(node_data: schemas.CloudUpdate, node: Cloud = Depends(dep_node)) -> schemas.Cloud:
|
||||
"""
|
||||
Update a cloud node.
|
||||
"""
|
||||
|
@ -45,7 +45,7 @@ router = APIRouter()
|
||||
@router.post("/projects/{project_id}/ports/udp", status_code=status.HTTP_201_CREATED)
|
||||
def allocate_udp_port(project_id: UUID) -> dict:
|
||||
"""
|
||||
Allocate an UDP port on the compute.
|
||||
Allocate a UDP port on the compute.
|
||||
"""
|
||||
|
||||
pm = ProjectManager.instance()
|
||||
@ -56,7 +56,7 @@ def allocate_udp_port(project_id: UUID) -> dict:
|
||||
|
||||
|
||||
@router.get("/network/interfaces")
|
||||
def network_interfaces() -> dict:
|
||||
def network_interfaces() -> List[dict]:
|
||||
"""
|
||||
List all the network interfaces available on the compute"
|
||||
"""
|
||||
|
@ -15,12 +15,17 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import secrets
|
||||
import base64
|
||||
import binascii
|
||||
import logging
|
||||
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi import Depends, HTTPException, WebSocket, status
|
||||
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||||
from fastapi.security.utils import get_authorization_scheme_param
|
||||
from gns3server.config import Config
|
||||
from typing import Optional
|
||||
from typing import Optional, Union
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
security = HTTPBasic()
|
||||
|
||||
|
||||
@ -35,3 +40,44 @@ def compute_authentication(credentials: Optional[HTTPBasicCredentials] = Depends
|
||||
detail="Invalid compute username or password",
|
||||
headers={"WWW-Authenticate": "Basic"},
|
||||
)
|
||||
|
||||
async def ws_compute_authentication(websocket: WebSocket) -> Union[None, WebSocket]:
|
||||
"""
|
||||
"""
|
||||
|
||||
await websocket.accept()
|
||||
|
||||
# handle basic HTTP authentication
|
||||
invalid_user_credentials_exc = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": "Basic"},
|
||||
)
|
||||
|
||||
try:
|
||||
authorization = websocket.headers.get("Authorization")
|
||||
scheme, param = get_authorization_scheme_param(authorization)
|
||||
if not authorization or scheme.lower() != "basic":
|
||||
raise invalid_user_credentials_exc
|
||||
try:
|
||||
data = base64.b64decode(param).decode("ascii")
|
||||
except (ValueError, UnicodeDecodeError, binascii.Error):
|
||||
raise invalid_user_credentials_exc
|
||||
|
||||
username, separator, password = data.partition(":")
|
||||
if not separator:
|
||||
raise invalid_user_credentials_exc
|
||||
|
||||
server_settings = Config.instance().settings.Server
|
||||
username = secrets.compare_digest(username, server_settings.compute_username)
|
||||
password = secrets.compare_digest(password, server_settings.compute_password.get_secret_value())
|
||||
if not (username and password):
|
||||
raise invalid_user_credentials_exc
|
||||
|
||||
except HTTPException as e:
|
||||
err_msg = f"Could not authenticate while connecting to compute WebSocket: {e.detail}"
|
||||
websocket_error = {"action": "log.error", "event": {"message": err_msg}}
|
||||
await websocket.send_json(websocket_error)
|
||||
log.error(err_msg)
|
||||
return await websocket.close(code=1008)
|
||||
return websocket
|
||||
|
@ -20,15 +20,18 @@ API routes for Docker nodes.
|
||||
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, WebSocket, Depends, Body, Response, status
|
||||
from fastapi import APIRouter, WebSocket, Depends, Body, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from uuid import UUID
|
||||
from typing import Union
|
||||
|
||||
from gns3server import schemas
|
||||
from gns3server.compute.docker import Docker
|
||||
from gns3server.compute.docker.docker_vm import DockerVM
|
||||
|
||||
from .dependencies.authentication import compute_authentication, ws_compute_authentication
|
||||
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or Docker node"}}
|
||||
|
||||
router = APIRouter(responses=responses)
|
||||
@ -49,6 +52,7 @@ def dep_node(project_id: UUID, node_id: UUID) -> DockerVM:
|
||||
response_model=schemas.Docker,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Docker node"}},
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def create_docker_node(project_id: UUID, node_data: schemas.DockerCreate) -> schemas.Docker:
|
||||
"""
|
||||
@ -65,6 +69,7 @@ async def create_docker_node(project_id: UUID, node_data: schemas.DockerCreate)
|
||||
start_command=node_data.get("start_command"),
|
||||
environment=node_data.get("environment"),
|
||||
adapters=node_data.get("adapters"),
|
||||
mac_address=node_data.get("mac_address"),
|
||||
console=node_data.get("console"),
|
||||
console_type=node_data.get("console_type"),
|
||||
console_resolution=node_data.get("console_resolution", "1024x768"),
|
||||
@ -85,7 +90,11 @@ async def create_docker_node(project_id: UUID, node_data: schemas.DockerCreate)
|
||||
return container.asdict()
|
||||
|
||||
|
||||
@router.get("/{node_id}", response_model=schemas.Docker)
|
||||
@router.get(
|
||||
"/{node_id}",
|
||||
response_model=schemas.Docker,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
def get_docker_node(node: DockerVM = Depends(dep_node)) -> schemas.Docker:
|
||||
"""
|
||||
Return a Docker node.
|
||||
@ -94,7 +103,11 @@ def get_docker_node(node: DockerVM = Depends(dep_node)) -> schemas.Docker:
|
||||
return node.asdict()
|
||||
|
||||
|
||||
@router.put("/{node_id}", response_model=schemas.Docker)
|
||||
@router.put(
|
||||
"/{node_id}",
|
||||
response_model=schemas.Docker,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def update_docker_node(node_data: schemas.DockerUpdate, node: DockerVM = Depends(dep_node)) -> schemas.Docker:
|
||||
"""
|
||||
Update a Docker node.
|
||||
@ -112,6 +125,8 @@ async def update_docker_node(node_data: schemas.DockerUpdate, node: DockerVM = D
|
||||
"start_command",
|
||||
"environment",
|
||||
"adapters",
|
||||
"mac_address",
|
||||
"custom_adapters",
|
||||
"extra_hosts",
|
||||
"extra_volumes",
|
||||
"memory",
|
||||
@ -131,7 +146,11 @@ async def update_docker_node(node_data: schemas.DockerUpdate, node: DockerVM = D
|
||||
return node.asdict()
|
||||
|
||||
|
||||
@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def start_docker_node(node: DockerVM = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Start a Docker node.
|
||||
@ -140,7 +159,11 @@ async def start_docker_node(node: DockerVM = Depends(dep_node)) -> None:
|
||||
await node.start()
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def stop_docker_node(node: DockerVM = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Stop a Docker node.
|
||||
@ -149,7 +172,11 @@ async def stop_docker_node(node: DockerVM = Depends(dep_node)) -> None:
|
||||
await node.stop()
|
||||
|
||||
|
||||
@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def suspend_docker_node(node: DockerVM = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Suspend a Docker node.
|
||||
@ -158,7 +185,11 @@ async def suspend_docker_node(node: DockerVM = Depends(dep_node)) -> None:
|
||||
await node.pause()
|
||||
|
||||
|
||||
@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/reload",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def reload_docker_node(node: DockerVM = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Reload a Docker node.
|
||||
@ -167,7 +198,11 @@ async def reload_docker_node(node: DockerVM = Depends(dep_node)) -> None:
|
||||
await node.restart()
|
||||
|
||||
|
||||
@router.post("/{node_id}/pause", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/pause",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def pause_docker_node(node: DockerVM = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Pause a Docker node.
|
||||
@ -176,7 +211,11 @@ async def pause_docker_node(node: DockerVM = Depends(dep_node)) -> None:
|
||||
await node.pause()
|
||||
|
||||
|
||||
@router.post("/{node_id}/unpause", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/unpause",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def unpause_docker_node(node: DockerVM = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Unpause a Docker node.
|
||||
@ -185,7 +224,11 @@ async def unpause_docker_node(node: DockerVM = Depends(dep_node)) -> None:
|
||||
await node.unpause()
|
||||
|
||||
|
||||
@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.delete(
|
||||
"/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def delete_docker_node(node: DockerVM = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Delete a Docker node.
|
||||
@ -194,7 +237,12 @@ async def delete_docker_node(node: DockerVM = Depends(dep_node)) -> None:
|
||||
await node.delete()
|
||||
|
||||
|
||||
@router.post("/{node_id}/duplicate", response_model=schemas.Docker, status_code=status.HTTP_201_CREATED)
|
||||
@router.post(
|
||||
"/{node_id}/duplicate",
|
||||
response_model=schemas.Docker,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def duplicate_docker_node(
|
||||
destination_node_id: UUID = Body(..., embed=True),
|
||||
node: DockerVM = Depends(dep_node)
|
||||
@ -211,6 +259,7 @@ async def duplicate_docker_node(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def create_docker_node_nio(
|
||||
adapter_number: int, port_number: int, nio_data: schemas.UDPNIO, node: DockerVM = Depends(dep_node)
|
||||
@ -229,6 +278,7 @@ async def create_docker_node_nio(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def update_docker_node_nio(
|
||||
adapter_number: int, port_number: int, nio_data: schemas.UDPNIO, node: DockerVM = Depends(dep_node)
|
||||
@ -245,7 +295,11 @@ async def update_docker_node_nio(
|
||||
return nio.asdict()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.delete(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def delete_docker_node_nio(
|
||||
adapter_number: int,
|
||||
port_number: int,
|
||||
@ -259,7 +313,10 @@ async def delete_docker_node_nio(
|
||||
await node.adapter_remove_nio_binding(adapter_number)
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start")
|
||||
@router.post(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def start_docker_node_capture(
|
||||
adapter_number: int,
|
||||
port_number: int,
|
||||
@ -278,7 +335,8 @@ async def start_docker_node_capture(
|
||||
|
||||
@router.post(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def stop_docker_node_capture(
|
||||
adapter_number: int,
|
||||
@ -293,7 +351,10 @@ async def stop_docker_node_capture(
|
||||
await node.stop_capture(adapter_number)
|
||||
|
||||
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream")
|
||||
@router.get(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def stream_pcap_file(
|
||||
adapter_number: int,
|
||||
port_number: int,
|
||||
@ -310,15 +371,23 @@ async def stream_pcap_file(
|
||||
|
||||
|
||||
@router.websocket("/{node_id}/console/ws")
|
||||
async def console_ws(websocket: WebSocket, node: DockerVM = Depends(dep_node)) -> None:
|
||||
async def console_ws(
|
||||
websocket: Union[None, WebSocket] = Depends(ws_compute_authentication),
|
||||
node: DockerVM = Depends(dep_node)
|
||||
) -> None:
|
||||
"""
|
||||
Console WebSocket.
|
||||
"""
|
||||
|
||||
await node.start_websocket_console(websocket)
|
||||
if websocket:
|
||||
await node.start_websocket_console(websocket)
|
||||
|
||||
|
||||
@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/console/reset",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def reset_console(node: DockerVM = Depends(dep_node)) -> None:
|
||||
|
||||
await node.reset_console()
|
||||
|
@ -20,16 +20,18 @@ API routes for Dynamips nodes.
|
||||
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, WebSocket, Depends, Response, status
|
||||
from fastapi import APIRouter, WebSocket, Body, Depends, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from typing import List
|
||||
from typing import List, Union
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.compute.dynamips import Dynamips
|
||||
from gns3server.compute.dynamips.nodes.router import Router
|
||||
from gns3server import schemas
|
||||
|
||||
from .dependencies.authentication import compute_authentication, ws_compute_authentication
|
||||
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or Dynamips node"}}
|
||||
|
||||
router = APIRouter(responses=responses)
|
||||
@ -53,6 +55,7 @@ def dep_node(project_id: UUID, node_id: UUID) -> Router:
|
||||
response_model=schemas.Dynamips,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Dynamips node"}},
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def create_router(project_id: UUID, node_data: schemas.DynamipsCreate) -> schemas.Dynamips:
|
||||
"""
|
||||
@ -61,9 +64,11 @@ async def create_router(project_id: UUID, node_data: schemas.DynamipsCreate) ->
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
platform = node_data.platform
|
||||
chassis = None
|
||||
print(node_data.chassis, platform in DEFAULT_CHASSIS)
|
||||
if not node_data.chassis and platform in DEFAULT_CHASSIS:
|
||||
chassis = DEFAULT_CHASSIS[platform]
|
||||
else:
|
||||
chassis = node_data.chassis
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
vm = await dynamips_manager.create_node(
|
||||
node_data.pop("name"),
|
||||
@ -82,7 +87,11 @@ async def create_router(project_id: UUID, node_data: schemas.DynamipsCreate) ->
|
||||
return vm.asdict()
|
||||
|
||||
|
||||
@router.get("/{node_id}", response_model=schemas.Dynamips)
|
||||
@router.get(
|
||||
"/{node_id}",
|
||||
response_model=schemas.Dynamips,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
def get_router(node: Router = Depends(dep_node)) -> schemas.Dynamips:
|
||||
"""
|
||||
Return Dynamips router.
|
||||
@ -91,7 +100,11 @@ def get_router(node: Router = Depends(dep_node)) -> schemas.Dynamips:
|
||||
return node.asdict()
|
||||
|
||||
|
||||
@router.put("/{node_id}", response_model=schemas.Dynamips)
|
||||
@router.put(
|
||||
"/{node_id}",
|
||||
response_model=schemas.Dynamips,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def update_router(node_data: schemas.DynamipsUpdate, node: Router = Depends(dep_node)) -> schemas.Dynamips:
|
||||
"""
|
||||
Update a Dynamips router.
|
||||
@ -102,7 +115,11 @@ async def update_router(node_data: schemas.DynamipsUpdate, node: Router = Depend
|
||||
return node.asdict()
|
||||
|
||||
|
||||
@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.delete(
|
||||
"/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def delete_router(node: Router = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Delete a Dynamips router.
|
||||
@ -111,7 +128,11 @@ async def delete_router(node: Router = Depends(dep_node)) -> None:
|
||||
await Dynamips.instance().delete_node(node.id)
|
||||
|
||||
|
||||
@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def start_router(node: Router = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Start a Dynamips router.
|
||||
@ -124,7 +145,11 @@ async def start_router(node: Router = Depends(dep_node)) -> None:
|
||||
await node.start()
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def stop_router(node: Router = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Stop a Dynamips router.
|
||||
@ -133,13 +158,21 @@ async def stop_router(node: Router = Depends(dep_node)) -> None:
|
||||
await node.stop()
|
||||
|
||||
|
||||
@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def suspend_router(node: Router = Depends(dep_node)) -> None:
|
||||
|
||||
await node.suspend()
|
||||
|
||||
|
||||
@router.post("/{node_id}/resume", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/resume",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def resume_router(node: Router = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Resume a suspended Dynamips router.
|
||||
@ -148,7 +181,11 @@ async def resume_router(node: Router = Depends(dep_node)) -> None:
|
||||
await node.resume()
|
||||
|
||||
|
||||
@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/reload",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def reload_router(node: Router = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Reload a suspended Dynamips router.
|
||||
@ -161,6 +198,7 @@ async def reload_router(node: Router = Depends(dep_node)) -> None:
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def create_nio(
|
||||
adapter_number: int,
|
||||
@ -181,6 +219,7 @@ async def create_nio(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def update_nio(
|
||||
adapter_number: int,
|
||||
@ -199,7 +238,11 @@ async def update_nio(
|
||||
return nio.asdict()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.delete(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def delete_nio(adapter_number: int, port_number: int, node: Router = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Delete a NIO (Network Input/Output) from the node.
|
||||
@ -209,7 +252,10 @@ async def delete_nio(adapter_number: int, port_number: int, node: Router = Depen
|
||||
await nio.delete()
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start")
|
||||
@router.post(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def start_capture(
|
||||
adapter_number: int,
|
||||
port_number: int,
|
||||
@ -226,7 +272,9 @@ async def start_capture(
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def stop_capture(adapter_number: int, port_number: int, node: Router = Depends(dep_node)) -> None:
|
||||
"""
|
||||
@ -236,7 +284,10 @@ async def stop_capture(adapter_number: int, port_number: int, node: Router = Dep
|
||||
await node.stop_capture(adapter_number, port_number)
|
||||
|
||||
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream")
|
||||
@router.get(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def stream_pcap_file(
|
||||
adapter_number: int,
|
||||
port_number: int,
|
||||
@ -251,7 +302,10 @@ async def stream_pcap_file(
|
||||
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
|
||||
|
||||
|
||||
@router.get("/{node_id}/idlepc_proposals")
|
||||
@router.get(
|
||||
"/{node_id}/idlepc_proposals",
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def get_idlepcs(node: Router = Depends(dep_node)) -> List[str]:
|
||||
"""
|
||||
Retrieve Dynamips idle-pc proposals
|
||||
@ -261,7 +315,10 @@ async def get_idlepcs(node: Router = Depends(dep_node)) -> List[str]:
|
||||
return await node.get_idle_pc_prop()
|
||||
|
||||
|
||||
@router.get("/{node_id}/auto_idlepc")
|
||||
@router.get(
|
||||
"/{node_id}/auto_idlepc",
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def get_auto_idlepc(node: Router = Depends(dep_node)) -> dict:
|
||||
"""
|
||||
Get an automatically guessed best idle-pc value.
|
||||
@ -271,8 +328,13 @@ async def get_auto_idlepc(node: Router = Depends(dep_node)) -> dict:
|
||||
return {"idlepc": idlepc}
|
||||
|
||||
|
||||
@router.post("/{node_id}/duplicate", response_model=schemas.Dynamips, status_code=status.HTTP_201_CREATED)
|
||||
async def duplicate_router(destination_node_id: UUID, node: Router = Depends(dep_node)) -> schemas.Dynamips:
|
||||
@router.post(
|
||||
"/{node_id}/duplicate",
|
||||
response_model=schemas.Dynamips,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def duplicate_router(destination_node_id: UUID = Body(..., embed=True), node: Router = Depends(dep_node)) -> schemas.Dynamips:
|
||||
"""
|
||||
Duplicate a router.
|
||||
"""
|
||||
@ -282,15 +344,24 @@ async def duplicate_router(destination_node_id: UUID, node: Router = Depends(dep
|
||||
|
||||
|
||||
@router.websocket("/{node_id}/console/ws")
|
||||
async def console_ws(websocket: WebSocket, node: Router = Depends(dep_node)) -> None:
|
||||
async def console_ws(
|
||||
websocket: Union[None, WebSocket] = Depends(ws_compute_authentication),
|
||||
node: Router = Depends(dep_node)
|
||||
|
||||
) -> None:
|
||||
"""
|
||||
Console WebSocket.
|
||||
"""
|
||||
|
||||
await node.start_websocket_console(websocket)
|
||||
if websocket:
|
||||
await node.start_websocket_console(websocket)
|
||||
|
||||
|
||||
@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/console/reset",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def reset_console(node: Router = Depends(dep_node)) -> None:
|
||||
|
||||
await node.reset_console()
|
||||
|
@ -150,12 +150,22 @@ def suspend_ethernet_switch(node: EthernetSwitch = Depends(dep_node)) -> None:
|
||||
pass
|
||||
|
||||
|
||||
@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def reload_ethernet_switch(node: EthernetSwitch = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Reload an Ethernet switch.
|
||||
This endpoint results in no action since Ethernet switch nodes are always on.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
)
|
||||
async def create_nio(
|
||||
async def create_ethernet_switch_nio(
|
||||
*,
|
||||
adapter_number: int = Path(..., ge=0, le=0),
|
||||
port_number: int,
|
||||
@ -169,7 +179,7 @@ async def create_nio(
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_nio(
|
||||
async def delete_ethernet_switch_nio(
|
||||
*,
|
||||
adapter_number: int = Path(..., ge=0, le=0),
|
||||
port_number: int,
|
||||
@ -185,7 +195,7 @@ async def delete_nio(
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start")
|
||||
async def start_capture(
|
||||
async def start_ethernet_switch_capture(
|
||||
*,
|
||||
adapter_number: int = Path(..., ge=0, le=0),
|
||||
port_number: int,
|
||||
@ -205,7 +215,7 @@ async def start_capture(
|
||||
@router.post(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT
|
||||
)
|
||||
async def stop_capture(
|
||||
async def stop_ethernet_switch_capture(
|
||||
*,
|
||||
adapter_number: int = Path(..., ge=0, le=0),
|
||||
port_number: int,
|
||||
|
@ -34,7 +34,7 @@ router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/docker/images")
|
||||
async def get_docker_images() -> List[str]:
|
||||
async def get_docker_images() -> List[dict]:
|
||||
"""
|
||||
Get all Docker images.
|
||||
"""
|
||||
@ -44,7 +44,7 @@ async def get_docker_images() -> List[str]:
|
||||
|
||||
|
||||
@router.get("/dynamips/images")
|
||||
async def get_dynamips_images() -> List[str]:
|
||||
async def get_dynamips_images() -> List[dict]:
|
||||
"""
|
||||
Get all Dynamips images.
|
||||
"""
|
||||
@ -85,7 +85,7 @@ async def download_dynamips_image(filename: str) -> FileResponse:
|
||||
|
||||
|
||||
@router.get("/iou/images")
|
||||
async def get_iou_images() -> List[str]:
|
||||
async def get_iou_images() -> List[dict]:
|
||||
"""
|
||||
Get all IOU images.
|
||||
"""
|
||||
@ -125,7 +125,7 @@ async def download_iou_image(filename: str) -> FileResponse:
|
||||
|
||||
|
||||
@router.get("/qemu/images")
|
||||
async def get_qemu_images() -> List[str]:
|
||||
async def get_qemu_images() -> List[dict]:
|
||||
|
||||
qemu_manager = Qemu.instance()
|
||||
return await qemu_manager.list_images()
|
||||
|
@ -30,6 +30,8 @@ from gns3server import schemas
|
||||
from gns3server.compute.iou import IOU
|
||||
from gns3server.compute.iou.iou_vm import IOUVM
|
||||
|
||||
from .dependencies.authentication import compute_authentication, ws_compute_authentication
|
||||
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or IOU node"}}
|
||||
|
||||
router = APIRouter(responses=responses)
|
||||
@ -50,6 +52,7 @@ def dep_node(project_id: UUID, node_id: UUID) -> IOUVM:
|
||||
response_model=schemas.IOU,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create IOU node"}},
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def create_iou_node(project_id: UUID, node_data: schemas.IOUCreate) -> schemas.IOU:
|
||||
"""
|
||||
@ -82,7 +85,11 @@ async def create_iou_node(project_id: UUID, node_data: schemas.IOUCreate) -> sch
|
||||
return vm.asdict()
|
||||
|
||||
|
||||
@router.get("/{node_id}", response_model=schemas.IOU)
|
||||
@router.get(
|
||||
"/{node_id}",
|
||||
response_model=schemas.IOU,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
def get_iou_node(node: IOUVM = Depends(dep_node)) -> schemas.IOU:
|
||||
"""
|
||||
Return an IOU node.
|
||||
@ -91,7 +98,11 @@ def get_iou_node(node: IOUVM = Depends(dep_node)) -> schemas.IOU:
|
||||
return node.asdict()
|
||||
|
||||
|
||||
@router.put("/{node_id}", response_model=schemas.IOU)
|
||||
@router.put(
|
||||
"/{node_id}",
|
||||
response_model=schemas.IOU,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def update_iou_node(node_data: schemas.IOUUpdate, node: IOUVM = Depends(dep_node)) -> schemas.IOU:
|
||||
"""
|
||||
Update an IOU node.
|
||||
@ -112,7 +123,11 @@ async def update_iou_node(node_data: schemas.IOUUpdate, node: IOUVM = Depends(de
|
||||
return node.asdict()
|
||||
|
||||
|
||||
@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.delete(
|
||||
"/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def delete_iou_node(node: IOUVM = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Delete an IOU node.
|
||||
@ -121,7 +136,12 @@ async def delete_iou_node(node: IOUVM = Depends(dep_node)) -> None:
|
||||
await IOU.instance().delete_node(node.id)
|
||||
|
||||
|
||||
@router.post("/{node_id}/duplicate", response_model=schemas.IOU, status_code=status.HTTP_201_CREATED)
|
||||
@router.post(
|
||||
"/{node_id}/duplicate",
|
||||
response_model=schemas.IOU,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def duplicate_iou_node(
|
||||
destination_node_id: UUID = Body(..., embed=True),
|
||||
node: IOUVM = Depends(dep_node)
|
||||
@ -134,7 +154,11 @@ async def duplicate_iou_node(
|
||||
return new_node.asdict()
|
||||
|
||||
|
||||
@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def start_iou_node(start_data: schemas.IOUStart, node: IOUVM = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Start an IOU node.
|
||||
@ -148,7 +172,11 @@ async def start_iou_node(start_data: schemas.IOUStart, node: IOUVM = Depends(dep
|
||||
await node.start()
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def stop_iou_node(node: IOUVM = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Stop an IOU node.
|
||||
@ -157,7 +185,11 @@ async def stop_iou_node(node: IOUVM = Depends(dep_node)) -> None:
|
||||
await node.stop()
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
def suspend_iou_node(node: IOUVM = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Suspend an IOU node.
|
||||
@ -167,7 +199,11 @@ def suspend_iou_node(node: IOUVM = Depends(dep_node)) -> None:
|
||||
pass
|
||||
|
||||
|
||||
@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/reload",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def reload_iou_node(node: IOUVM = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Reload an IOU node.
|
||||
@ -180,6 +216,7 @@ async def reload_iou_node(node: IOUVM = Depends(dep_node)) -> None:
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def create_iou_node_nio(
|
||||
adapter_number: int,
|
||||
@ -200,6 +237,7 @@ async def create_iou_node_nio(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def update_iou_node_nio(
|
||||
adapter_number: int,
|
||||
@ -218,7 +256,11 @@ async def update_iou_node_nio(
|
||||
return nio.asdict()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.delete(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def delete_iou_node_nio(adapter_number: int, port_number: int, node: IOUVM = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Delete a NIO (Network Input/Output) from the node.
|
||||
@ -227,7 +269,10 @@ async def delete_iou_node_nio(adapter_number: int, port_number: int, node: IOUVM
|
||||
await node.adapter_remove_nio_binding(adapter_number, port_number)
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start")
|
||||
@router.post(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def start_iou_node_capture(
|
||||
adapter_number: int,
|
||||
port_number: int,
|
||||
@ -244,7 +289,9 @@ async def start_iou_node_capture(
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def stop_iou_node_capture(adapter_number: int, port_number: int, node: IOUVM = Depends(dep_node)) -> None:
|
||||
"""
|
||||
@ -254,7 +301,10 @@ async def stop_iou_node_capture(adapter_number: int, port_number: int, node: IOU
|
||||
await node.stop_capture(adapter_number, port_number)
|
||||
|
||||
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream")
|
||||
@router.get(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def stream_pcap_file(
|
||||
adapter_number: int,
|
||||
port_number: int,
|
||||
@ -269,16 +319,26 @@ async def stream_pcap_file(
|
||||
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
|
||||
|
||||
|
||||
@router.websocket("/{node_id}/console/ws")
|
||||
async def console_ws(websocket: WebSocket, node: IOUVM = Depends(dep_node)) -> None:
|
||||
@router.websocket(
|
||||
"/{node_id}/console/ws",
|
||||
)
|
||||
async def console_ws(
|
||||
websocket: Union[None, WebSocket] = Depends(ws_compute_authentication),
|
||||
node: IOUVM = Depends(dep_node)
|
||||
) -> None:
|
||||
"""
|
||||
Console WebSocket.
|
||||
"""
|
||||
|
||||
await node.start_websocket_console(websocket)
|
||||
if websocket:
|
||||
await node.start_websocket_console(websocket)
|
||||
|
||||
|
||||
@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/console/reset",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def reset_console(node: IOUVM = Depends(dep_node)) -> None:
|
||||
|
||||
await node.reset_console()
|
||||
|
@ -80,7 +80,7 @@ def get_nat_node(node: Nat = Depends(dep_node)) -> schemas.NAT:
|
||||
|
||||
|
||||
@router.put("/{node_id}", response_model=schemas.NAT)
|
||||
def update_nat_node(node_data: schemas.NATUpdate, node: Nat = Depends(dep_node)) -> schemas.NAT:
|
||||
async def update_nat_node(node_data: schemas.NATUpdate, node: Nat = Depends(dep_node)) -> schemas.NAT:
|
||||
"""
|
||||
Update a NAT node.
|
||||
"""
|
||||
|
@ -18,14 +18,13 @@
|
||||
API routes for compute notifications.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import binascii
|
||||
|
||||
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, status, HTTPException
|
||||
from fastapi.security.utils import get_authorization_scheme_param
|
||||
from fastapi import APIRouter, Depends, WebSocket, WebSocketDisconnect
|
||||
from typing import Union
|
||||
from websockets.exceptions import ConnectionClosed, WebSocketException
|
||||
|
||||
from gns3server.compute.notification_manager import NotificationManager
|
||||
from .dependencies.authentication import ws_compute_authentication
|
||||
|
||||
import logging
|
||||
|
||||
@ -35,53 +34,23 @@ router = APIRouter()
|
||||
|
||||
|
||||
@router.websocket("/notifications/ws")
|
||||
async def notification_ws(websocket: WebSocket) -> None:
|
||||
async def project_ws_notifications(websocket: Union[None, WebSocket] = Depends(ws_compute_authentication)) -> None:
|
||||
"""
|
||||
Receive project notifications about the project from WebSocket.
|
||||
"""
|
||||
|
||||
await websocket.accept()
|
||||
|
||||
# handle basic HTTP authentication
|
||||
invalid_user_credentials_exc = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid authentication credentials",
|
||||
headers={"WWW-Authenticate": "Basic"},
|
||||
)
|
||||
|
||||
try:
|
||||
authorization = websocket.headers.get("Authorization")
|
||||
scheme, param = get_authorization_scheme_param(authorization)
|
||||
if not authorization or scheme.lower() != "basic":
|
||||
raise invalid_user_credentials_exc
|
||||
if websocket:
|
||||
log.info(f"New client {websocket.client.host}:{websocket.client.port} has connected to compute WebSocket")
|
||||
try:
|
||||
data = base64.b64decode(param).decode("ascii")
|
||||
except (ValueError, UnicodeDecodeError, binascii.Error):
|
||||
raise invalid_user_credentials_exc
|
||||
username, separator, password = data.partition(":")
|
||||
if not separator:
|
||||
raise invalid_user_credentials_exc
|
||||
except invalid_user_credentials_exc as e:
|
||||
websocket_error = {"action": "log.error", "event": {"message": f"Could not authenticate while connecting to "
|
||||
f"compute WebSocket: {e.detail}"}}
|
||||
await websocket.send_json(websocket_error)
|
||||
return await websocket.close(code=1008)
|
||||
with NotificationManager.instance().queue() as queue:
|
||||
while True:
|
||||
notification = await queue.get_json(5)
|
||||
await websocket.send_text(notification)
|
||||
except (ConnectionClosed, WebSocketDisconnect):
|
||||
log.info(f"Client {websocket.client.host}:{websocket.client.port} has disconnected from compute WebSocket")
|
||||
except WebSocketException as e:
|
||||
log.warning(f"Error while sending to controller event to WebSocket client: {e}")
|
||||
|
||||
log.info(f"New client {websocket.client.host}:{websocket.client.port} has connected to compute WebSocket")
|
||||
try:
|
||||
with NotificationManager.instance().queue() as queue:
|
||||
while True:
|
||||
notification = await queue.get_json(5)
|
||||
await websocket.send_text(notification)
|
||||
except (ConnectionClosed, WebSocketDisconnect):
|
||||
log.info(f"Client {websocket.client.host}:{websocket.client.port} has disconnected from compute WebSocket")
|
||||
except WebSocketException as e:
|
||||
log.warning(f"Error while sending to controller event to WebSocket client: {e}")
|
||||
finally:
|
||||
try:
|
||||
await websocket.close()
|
||||
except OSError:
|
||||
pass # ignore OSError: [Errno 107] Transport endpoint is not connected
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
|
@ -20,15 +20,17 @@ API routes for Qemu nodes.
|
||||
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, WebSocket, Depends, Body, Path, Response, status
|
||||
from fastapi import APIRouter, WebSocket, Depends, Body, Path, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from typing import Union
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server import schemas
|
||||
from gns3server.compute.qemu import Qemu
|
||||
from gns3server.compute.qemu.qemu_vm import QemuVM
|
||||
|
||||
from .dependencies.authentication import compute_authentication, ws_compute_authentication
|
||||
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or Qemu node"}}
|
||||
|
||||
@ -50,6 +52,7 @@ def dep_node(project_id: UUID, node_id: UUID) -> QemuVM:
|
||||
response_model=schemas.Qemu,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Qemu node"}},
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def create_qemu_node(project_id: UUID, node_data: schemas.QemuCreate) -> schemas.Qemu:
|
||||
"""
|
||||
@ -78,7 +81,11 @@ async def create_qemu_node(project_id: UUID, node_data: schemas.QemuCreate) -> s
|
||||
return vm.asdict()
|
||||
|
||||
|
||||
@router.get("/{node_id}", response_model=schemas.Qemu)
|
||||
@router.get(
|
||||
"/{node_id}",
|
||||
response_model=schemas.Qemu,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
def get_qemu_node(node: QemuVM = Depends(dep_node)) -> schemas.Qemu:
|
||||
"""
|
||||
Return a Qemu node.
|
||||
@ -87,7 +94,11 @@ def get_qemu_node(node: QemuVM = Depends(dep_node)) -> schemas.Qemu:
|
||||
return node.asdict()
|
||||
|
||||
|
||||
@router.put("/{node_id}", response_model=schemas.Qemu)
|
||||
@router.put(
|
||||
"/{node_id}",
|
||||
response_model=schemas.Qemu,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def update_qemu_node(node_data: schemas.QemuUpdate, node: QemuVM = Depends(dep_node)) -> schemas.Qemu:
|
||||
"""
|
||||
Update a Qemu node.
|
||||
@ -103,7 +114,11 @@ async def update_qemu_node(node_data: schemas.QemuUpdate, node: QemuVM = Depends
|
||||
return node.asdict()
|
||||
|
||||
|
||||
@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.delete(
|
||||
"/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def delete_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Delete a Qemu node.
|
||||
@ -112,7 +127,12 @@ async def delete_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
|
||||
await Qemu.instance().delete_node(node.id)
|
||||
|
||||
|
||||
@router.post("/{node_id}/duplicate", response_model=schemas.Qemu, status_code=status.HTTP_201_CREATED)
|
||||
@router.post(
|
||||
"/{node_id}/duplicate",
|
||||
response_model=schemas.Qemu,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def duplicate_qemu_node(
|
||||
destination_node_id: UUID = Body(..., embed=True),
|
||||
node: QemuVM = Depends(dep_node)
|
||||
@ -127,7 +147,8 @@ async def duplicate_qemu_node(
|
||||
|
||||
@router.post(
|
||||
"/{node_id}/disk_image/{disk_name}",
|
||||
status_code=status.HTTP_204_NO_CONTENT
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def create_qemu_disk_image(
|
||||
disk_name: str,
|
||||
@ -144,7 +165,8 @@ async def create_qemu_disk_image(
|
||||
|
||||
@router.put(
|
||||
"/{node_id}/disk_image/{disk_name}",
|
||||
status_code=status.HTTP_204_NO_CONTENT
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def update_qemu_disk_image(
|
||||
disk_name: str,
|
||||
@ -161,7 +183,8 @@ async def update_qemu_disk_image(
|
||||
|
||||
@router.delete(
|
||||
"/{node_id}/disk_image/{disk_name}",
|
||||
status_code=status.HTTP_204_NO_CONTENT
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def delete_qemu_disk_image(
|
||||
disk_name: str,
|
||||
@ -174,7 +197,11 @@ async def delete_qemu_disk_image(
|
||||
node.delete_disk_image(disk_name)
|
||||
|
||||
|
||||
@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def start_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Start a Qemu node.
|
||||
@ -183,7 +210,11 @@ async def start_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
|
||||
await node.start()
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def stop_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Stop a Qemu node.
|
||||
@ -192,7 +223,11 @@ async def stop_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
|
||||
await node.stop()
|
||||
|
||||
|
||||
@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/reload",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def reload_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Reload a Qemu node.
|
||||
@ -201,7 +236,11 @@ async def reload_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
|
||||
await node.reload()
|
||||
|
||||
|
||||
@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def suspend_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Suspend a Qemu node.
|
||||
@ -210,7 +249,11 @@ async def suspend_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
|
||||
await node.suspend()
|
||||
|
||||
|
||||
@router.post("/{node_id}/resume", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/resume",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def resume_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Resume a Qemu node.
|
||||
@ -223,6 +266,7 @@ async def resume_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def create_qemu_node_nio(
|
||||
*,
|
||||
@ -245,6 +289,7 @@ async def create_qemu_node_nio(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def update_qemu_node_nio(
|
||||
*,
|
||||
@ -267,7 +312,11 @@ async def update_qemu_node_nio(
|
||||
return nio.asdict()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.delete(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def delete_qemu_node_nio(
|
||||
adapter_number: int,
|
||||
port_number: int = Path(..., ge=0, le=0),
|
||||
@ -281,7 +330,10 @@ async def delete_qemu_node_nio(
|
||||
await node.adapter_remove_nio_binding(adapter_number)
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start")
|
||||
@router.post(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def start_qemu_node_capture(
|
||||
*,
|
||||
adapter_number: int,
|
||||
@ -300,7 +352,9 @@ async def start_qemu_node_capture(
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def stop_qemu_node_capture(
|
||||
adapter_number: int,
|
||||
@ -315,7 +369,10 @@ async def stop_qemu_node_capture(
|
||||
await node.stop_capture(adapter_number)
|
||||
|
||||
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream")
|
||||
@router.get(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def stream_pcap_file(
|
||||
adapter_number: int,
|
||||
port_number: int = Path(..., ge=0, le=0),
|
||||
@ -330,16 +387,26 @@ async def stream_pcap_file(
|
||||
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
|
||||
|
||||
|
||||
@router.websocket("/{node_id}/console/ws")
|
||||
async def console_ws(websocket: WebSocket, node: QemuVM = Depends(dep_node)) -> None:
|
||||
@router.websocket(
|
||||
"/{node_id}/console/ws"
|
||||
)
|
||||
async def console_ws(
|
||||
websocket: Union[None, WebSocket] = Depends(ws_compute_authentication),
|
||||
node: QemuVM = Depends(dep_node)
|
||||
) -> None:
|
||||
"""
|
||||
Console WebSocket.
|
||||
"""
|
||||
|
||||
await node.start_websocket_console(websocket)
|
||||
if websocket:
|
||||
await node.start_websocket_console(websocket)
|
||||
|
||||
|
||||
@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/console/reset",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def reset_console(node: QemuVM = Depends(dep_node)) -> None:
|
||||
|
||||
await node.reset_console()
|
||||
|
@ -20,19 +20,22 @@ API routes for VirtualBox nodes.
|
||||
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, WebSocket, Depends, Path, Response, status
|
||||
from fastapi import APIRouter, WebSocket, Depends, Path, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from uuid import UUID
|
||||
from typing import Union
|
||||
|
||||
from gns3server import schemas
|
||||
from gns3server.compute.virtualbox import VirtualBox
|
||||
from gns3server.compute.virtualbox.virtualbox_error import VirtualBoxError
|
||||
from gns3server.compute.virtualbox.virtualbox_vm import VirtualBoxVM
|
||||
|
||||
from .dependencies.authentication import compute_authentication, ws_compute_authentication
|
||||
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or VirtualBox node"}}
|
||||
|
||||
router = APIRouter(responses=responses)
|
||||
router = APIRouter(responses=responses, deprecated=True)
|
||||
|
||||
|
||||
def dep_node(project_id: UUID, node_id: UUID) -> VirtualBoxVM:
|
||||
@ -50,6 +53,7 @@ def dep_node(project_id: UUID, node_id: UUID) -> VirtualBoxVM:
|
||||
response_model=schemas.VirtualBox,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create VirtualBox node"}},
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def create_virtualbox_node(project_id: UUID, node_data: schemas.VirtualBoxCreate) -> schemas.VirtualBox:
|
||||
"""
|
||||
@ -82,7 +86,11 @@ async def create_virtualbox_node(project_id: UUID, node_data: schemas.VirtualBox
|
||||
return vm.asdict()
|
||||
|
||||
|
||||
@router.get("/{node_id}", response_model=schemas.VirtualBox)
|
||||
@router.get(
|
||||
"/{node_id}",
|
||||
response_model=schemas.VirtualBox,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
def get_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> schemas.VirtualBox:
|
||||
"""
|
||||
Return a VirtualBox node.
|
||||
@ -91,7 +99,11 @@ def get_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> schemas.Virtu
|
||||
return node.asdict()
|
||||
|
||||
|
||||
@router.put("/{node_id}", response_model=schemas.VirtualBox)
|
||||
@router.put(
|
||||
"/{node_id}",
|
||||
response_model=schemas.VirtualBox,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def update_virtualbox_node(
|
||||
node_data: schemas.VirtualBoxUpdate,
|
||||
node: VirtualBoxVM = Depends(dep_node)
|
||||
@ -136,7 +148,11 @@ async def update_virtualbox_node(
|
||||
return node.asdict()
|
||||
|
||||
|
||||
@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.delete(
|
||||
"/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def delete_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Delete a VirtualBox node.
|
||||
@ -145,7 +161,11 @@ async def delete_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None
|
||||
await VirtualBox.instance().delete_node(node.id)
|
||||
|
||||
|
||||
@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def start_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Start a VirtualBox node.
|
||||
@ -154,7 +174,11 @@ async def start_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None:
|
||||
await node.start()
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def stop_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Stop a VirtualBox node.
|
||||
@ -163,7 +187,11 @@ async def stop_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None:
|
||||
await node.stop()
|
||||
|
||||
|
||||
@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def suspend_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Suspend a VirtualBox node.
|
||||
@ -172,7 +200,11 @@ async def suspend_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> Non
|
||||
await node.suspend()
|
||||
|
||||
|
||||
@router.post("/{node_id}/resume", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/resume",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def resume_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Resume a VirtualBox node.
|
||||
@ -181,7 +213,11 @@ async def resume_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None
|
||||
await node.resume()
|
||||
|
||||
|
||||
@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/reload",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def reload_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Reload a VirtualBox node.
|
||||
@ -194,6 +230,7 @@ async def reload_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def create_virtualbox_node_nio(
|
||||
*,
|
||||
@ -216,6 +253,7 @@ async def create_virtualbox_node_nio(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def update_virtualbox_node_nio(
|
||||
*,
|
||||
@ -238,7 +276,11 @@ async def update_virtualbox_node_nio(
|
||||
return nio.asdict()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.delete(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def delete_virtualbox_node_nio(
|
||||
adapter_number: int,
|
||||
port_number: int = Path(..., ge=0, le=0),
|
||||
@ -252,7 +294,10 @@ async def delete_virtualbox_node_nio(
|
||||
await node.adapter_remove_nio_binding(adapter_number)
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start")
|
||||
@router.post(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def start_virtualbox_node_capture(
|
||||
*,
|
||||
adapter_number: int,
|
||||
@ -271,7 +316,9 @@ async def start_virtualbox_node_capture(
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def stop_virtualbox_node_capture(
|
||||
adapter_number: int,
|
||||
@ -286,7 +333,10 @@ async def stop_virtualbox_node_capture(
|
||||
await node.stop_capture(adapter_number)
|
||||
|
||||
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream")
|
||||
@router.get(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def stream_pcap_file(
|
||||
adapter_number: int,
|
||||
port_number: int = Path(..., ge=0, le=0),
|
||||
@ -302,8 +352,13 @@ async def stream_pcap_file(
|
||||
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
|
||||
|
||||
|
||||
@router.websocket("/{node_id}/console/ws")
|
||||
async def console_ws(websocket: WebSocket, node: VirtualBoxVM = Depends(dep_node)) -> None:
|
||||
@router.websocket(
|
||||
"/{node_id}/console/ws"
|
||||
)
|
||||
async def console_ws(
|
||||
websocket: Union[None, WebSocket] = Depends(ws_compute_authentication),
|
||||
node: VirtualBoxVM = Depends(dep_node)
|
||||
) -> None:
|
||||
"""
|
||||
Console WebSocket.
|
||||
"""
|
||||
@ -311,7 +366,11 @@ async def console_ws(websocket: WebSocket, node: VirtualBoxVM = Depends(dep_node
|
||||
await node.start_websocket_console(websocket)
|
||||
|
||||
|
||||
@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/console/reset",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def reset_console(node: VirtualBoxVM = Depends(dep_node)) -> None:
|
||||
|
||||
await node.reset_console()
|
||||
|
@ -20,19 +20,21 @@ API routes for VMware nodes.
|
||||
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, WebSocket, Depends, Path, Response, status
|
||||
from fastapi import APIRouter, WebSocket, Depends, Path, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from uuid import UUID
|
||||
from typing import Union
|
||||
|
||||
from gns3server import schemas
|
||||
from gns3server.compute.vmware import VMware
|
||||
from gns3server.compute.project_manager import ProjectManager
|
||||
from gns3server.compute.vmware.vmware_vm import VMwareVM
|
||||
|
||||
from .dependencies.authentication import compute_authentication, ws_compute_authentication
|
||||
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or VMware node"}}
|
||||
|
||||
router = APIRouter(responses=responses)
|
||||
router = APIRouter(responses=responses, deprecated=True)
|
||||
|
||||
|
||||
def dep_node(project_id: UUID, node_id: UUID) -> VMwareVM:
|
||||
@ -50,6 +52,7 @@ def dep_node(project_id: UUID, node_id: UUID) -> VMwareVM:
|
||||
response_model=schemas.VMware,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create VMware node"}},
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def create_vmware_node(project_id: UUID, node_data: schemas.VMwareCreate) -> schemas.VMware:
|
||||
"""
|
||||
@ -76,7 +79,11 @@ async def create_vmware_node(project_id: UUID, node_data: schemas.VMwareCreate)
|
||||
return vm.asdict()
|
||||
|
||||
|
||||
@router.get("/{node_id}", response_model=schemas.VMware)
|
||||
@router.get(
|
||||
"/{node_id}",
|
||||
response_model=schemas.VMware,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
def get_vmware_node(node: VMwareVM = Depends(dep_node)) -> schemas.VMware:
|
||||
"""
|
||||
Return a VMware node.
|
||||
@ -85,8 +92,12 @@ def get_vmware_node(node: VMwareVM = Depends(dep_node)) -> schemas.VMware:
|
||||
return node.asdict()
|
||||
|
||||
|
||||
@router.put("/{node_id}", response_model=schemas.VMware)
|
||||
def update_vmware_node(node_data: schemas.VMwareUpdate, node: VMwareVM = Depends(dep_node)) -> schemas.VMware:
|
||||
@router.put(
|
||||
"/{node_id}",
|
||||
response_model=schemas.VMware,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def update_vmware_node(node_data: schemas.VMwareUpdate, node: VMwareVM = Depends(dep_node)) -> schemas.VMware:
|
||||
"""
|
||||
Update a VMware node.
|
||||
"""
|
||||
@ -102,7 +113,11 @@ def update_vmware_node(node_data: schemas.VMwareUpdate, node: VMwareVM = Depends
|
||||
return node.asdict()
|
||||
|
||||
|
||||
@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.delete(
|
||||
"/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def delete_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Delete a VMware node.
|
||||
@ -111,7 +126,11 @@ async def delete_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
|
||||
await VMware.instance().delete_node(node.id)
|
||||
|
||||
|
||||
@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def start_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Start a VMware node.
|
||||
@ -120,7 +139,11 @@ async def start_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
|
||||
await node.start()
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def stop_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Stop a VMware node.
|
||||
@ -129,7 +152,11 @@ async def stop_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
|
||||
await node.stop()
|
||||
|
||||
|
||||
@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def suspend_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Suspend a VMware node.
|
||||
@ -138,7 +165,11 @@ async def suspend_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
|
||||
await node.suspend()
|
||||
|
||||
|
||||
@router.post("/{node_id}/resume", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/resume",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def resume_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Resume a VMware node.
|
||||
@ -147,7 +178,11 @@ async def resume_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
|
||||
await node.resume()
|
||||
|
||||
|
||||
@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/reload",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def reload_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Reload a VMware node.
|
||||
@ -160,6 +195,7 @@ async def reload_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def create_vmware_node_nio(
|
||||
*,
|
||||
@ -182,6 +218,7 @@ async def create_vmware_node_nio(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def update_vmware_node_nio(
|
||||
*,
|
||||
@ -202,7 +239,11 @@ async def update_vmware_node_nio(
|
||||
return nio.asdict()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.delete(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def delete_vmware_node_nio(
|
||||
adapter_number: int,
|
||||
port_number: int = Path(..., ge=0, le=0),
|
||||
@ -216,7 +257,10 @@ async def delete_vmware_node_nio(
|
||||
await node.adapter_remove_nio_binding(adapter_number)
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start")
|
||||
@router.post(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def start_vmware_node_capture(
|
||||
*,
|
||||
adapter_number: int,
|
||||
@ -235,7 +279,9 @@ async def start_vmware_node_capture(
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def stop_vmware_node_capture(
|
||||
adapter_number: int,
|
||||
@ -250,7 +296,10 @@ async def stop_vmware_node_capture(
|
||||
await node.stop_capture(adapter_number)
|
||||
|
||||
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream")
|
||||
@router.get(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def stream_pcap_file(
|
||||
adapter_number: int,
|
||||
port_number: int = Path(..., ge=0, le=0),
|
||||
@ -266,7 +315,11 @@ async def stream_pcap_file(
|
||||
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
|
||||
|
||||
|
||||
@router.post("/{node_id}/interfaces/vmnet", status_code=status.HTTP_201_CREATED)
|
||||
@router.post(
|
||||
"/{node_id}/interfaces/vmnet",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
def allocate_vmnet(node: VMwareVM = Depends(dep_node)) -> dict:
|
||||
"""
|
||||
Allocate a VMware VMnet interface on the server.
|
||||
@ -280,16 +333,23 @@ def allocate_vmnet(node: VMwareVM = Depends(dep_node)) -> dict:
|
||||
|
||||
|
||||
@router.websocket("/{node_id}/console/ws")
|
||||
async def console_ws(websocket: WebSocket, node: VMwareVM = Depends(dep_node)) -> None:
|
||||
async def console_ws(
|
||||
websocket: Union[None, WebSocket] = Depends(ws_compute_authentication),
|
||||
node: VMwareVM = Depends(dep_node)
|
||||
) -> None:
|
||||
"""
|
||||
Console WebSocket.
|
||||
"""
|
||||
|
||||
await node.start_websocket_console(websocket)
|
||||
if websocket:
|
||||
await node.start_websocket_console(websocket)
|
||||
|
||||
|
||||
@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/console/reset",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def reset_console(node: VMwareVM = Depends(dep_node)) -> None:
|
||||
|
||||
await node.reset_console()
|
||||
|
||||
|
@ -20,15 +20,18 @@ API routes for VPCS nodes.
|
||||
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, WebSocket, Depends, Body, Path, Response, status
|
||||
from fastapi import APIRouter, WebSocket, Depends, Body, Path, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from typing import Union
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server import schemas
|
||||
from gns3server.compute.vpcs import VPCS
|
||||
from gns3server.compute.vpcs.vpcs_vm import VPCSVM
|
||||
|
||||
from .dependencies.authentication import compute_authentication, ws_compute_authentication
|
||||
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or VMware node"}}
|
||||
|
||||
router = APIRouter(responses=responses)
|
||||
@ -49,6 +52,7 @@ def dep_node(project_id: UUID, node_id: UUID) -> VPCSVM:
|
||||
response_model=schemas.VPCS,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create VMware node"}},
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def create_vpcs_node(project_id: UUID, node_data: schemas.VPCSCreate) -> schemas.VPCS:
|
||||
"""
|
||||
@ -69,7 +73,11 @@ async def create_vpcs_node(project_id: UUID, node_data: schemas.VPCSCreate) -> s
|
||||
return vm.asdict()
|
||||
|
||||
|
||||
@router.get("/{node_id}", response_model=schemas.VPCS)
|
||||
@router.get(
|
||||
"/{node_id}",
|
||||
response_model=schemas.VPCS,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
def get_vpcs_node(node: VPCSVM = Depends(dep_node)) -> schemas.VPCS:
|
||||
"""
|
||||
Return a VPCS node.
|
||||
@ -78,21 +86,35 @@ def get_vpcs_node(node: VPCSVM = Depends(dep_node)) -> schemas.VPCS:
|
||||
return node.asdict()
|
||||
|
||||
|
||||
@router.put("/{node_id}", response_model=schemas.VPCS)
|
||||
def update_vpcs_node(node_data: schemas.VPCSUpdate, node: VPCSVM = Depends(dep_node)) -> schemas.VPCS:
|
||||
@router.put(
|
||||
"/{node_id}",
|
||||
response_model=schemas.VPCS,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def update_vpcs_node(node_data: schemas.VPCSUpdate, node: VPCSVM = Depends(dep_node)) -> schemas.VPCS:
|
||||
"""
|
||||
Update a VPCS node.
|
||||
"""
|
||||
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
node.name = node_data.get("name", node.name)
|
||||
node.console = node_data.get("console", node.console)
|
||||
node.console_type = node_data.get("console_type", node.console_type)
|
||||
name = node_data.get("name", node.name)
|
||||
if node.name != name:
|
||||
node.name = name
|
||||
console = node_data.get("console", node.console)
|
||||
if node.console != console:
|
||||
node.console = console
|
||||
console_type = node_data.get("console_type", node.console_type)
|
||||
if node.console_type != console_type:
|
||||
node.console_type = console_type
|
||||
node.updated()
|
||||
return node.asdict()
|
||||
|
||||
|
||||
@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.delete(
|
||||
"/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def delete_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Delete a VPCS node.
|
||||
@ -101,7 +123,12 @@ async def delete_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None:
|
||||
await VPCS.instance().delete_node(node.id)
|
||||
|
||||
|
||||
@router.post("/{node_id}/duplicate", response_model=schemas.VPCS, status_code=status.HTTP_201_CREATED)
|
||||
@router.post(
|
||||
"/{node_id}/duplicate",
|
||||
response_model=schemas.VPCS,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def duplicate_vpcs_node(
|
||||
destination_node_id: UUID = Body(..., embed=True),
|
||||
node: VPCSVM = Depends(dep_node)) -> None:
|
||||
@ -113,7 +140,11 @@ async def duplicate_vpcs_node(
|
||||
return new_node.asdict()
|
||||
|
||||
|
||||
@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def start_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Start a VPCS node.
|
||||
@ -122,7 +153,11 @@ async def start_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None:
|
||||
await node.start()
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def stop_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Stop a VPCS node.
|
||||
@ -131,7 +166,11 @@ async def stop_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None:
|
||||
await node.stop()
|
||||
|
||||
|
||||
@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def suspend_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Suspend a VPCS node.
|
||||
@ -141,7 +180,11 @@ async def suspend_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None:
|
||||
pass
|
||||
|
||||
|
||||
@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/reload",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def reload_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Reload a VPCS node.
|
||||
@ -154,6 +197,7 @@ async def reload_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None:
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def create_vpcs_node_nio(
|
||||
*,
|
||||
@ -176,6 +220,7 @@ async def create_vpcs_node_nio(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def update_vpcs_node_nio(
|
||||
*,
|
||||
@ -196,7 +241,11 @@ async def update_vpcs_node_nio(
|
||||
return nio.asdict()
|
||||
|
||||
|
||||
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.delete(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def delete_vpcs_node_nio(
|
||||
*,
|
||||
adapter_number: int = Path(..., ge=0, le=0),
|
||||
@ -211,7 +260,10 @@ async def delete_vpcs_node_nio(
|
||||
await node.port_remove_nio_binding(port_number)
|
||||
|
||||
|
||||
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start")
|
||||
@router.post(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def start_vpcs_node_capture(
|
||||
*,
|
||||
adapter_number: int = Path(..., ge=0, le=0),
|
||||
@ -230,7 +282,9 @@ async def start_vpcs_node_capture(
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def stop_vpcs_node_capture(
|
||||
*,
|
||||
@ -246,13 +300,10 @@ async def stop_vpcs_node_capture(
|
||||
await node.stop_capture(port_number)
|
||||
|
||||
|
||||
@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def reset_console(node: VPCSVM = Depends(dep_node)) -> None:
|
||||
|
||||
await node.reset_console()
|
||||
|
||||
|
||||
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream")
|
||||
@router.get(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def stream_pcap_file(
|
||||
*,
|
||||
adapter_number: int = Path(..., ge=0, le=0),
|
||||
@ -269,10 +320,24 @@ async def stream_pcap_file(
|
||||
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
|
||||
|
||||
|
||||
@router.websocket("/{node_id}/console/ws")
|
||||
async def console_ws(websocket: WebSocket, node: VPCSVM = Depends(dep_node)) -> None:
|
||||
@router.websocket(
|
||||
"/{node_id}/console/ws"
|
||||
)
|
||||
async def console_ws(
|
||||
websocket: Union[None, WebSocket] = Depends(ws_compute_authentication),
|
||||
node: VPCSVM = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Console WebSocket.
|
||||
"""
|
||||
|
||||
await node.start_websocket_console(websocket)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{node_id}/console/reset",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(compute_authentication)]
|
||||
)
|
||||
async def reset_console(node: VPCSVM = Depends(dep_node)) -> None:
|
||||
|
||||
await node.reset_console()
|
||||
|
@ -23,7 +23,6 @@ from . import drawings
|
||||
from . import gns3vm
|
||||
from . import links
|
||||
from . import nodes
|
||||
from . import notifications
|
||||
from . import projects
|
||||
from . import snapshots
|
||||
from . import symbols
|
||||
@ -32,73 +31,81 @@ from . import images
|
||||
from . import users
|
||||
from . import groups
|
||||
from . import roles
|
||||
from . import permissions
|
||||
from . import acl
|
||||
from . import pools
|
||||
from . import privileges
|
||||
|
||||
from .dependencies.authentication import get_current_active_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
router.include_router(controller.router, tags=["Controller"])
|
||||
router.include_router(users.router, prefix="/users", tags=["Users"])
|
||||
router.include_router(
|
||||
controller.router,
|
||||
tags=["Controller"]
|
||||
)
|
||||
|
||||
router.include_router(
|
||||
users.router,
|
||||
prefix="/access/users",
|
||||
tags=["Users"]
|
||||
)
|
||||
|
||||
router.include_router(
|
||||
groups.router,
|
||||
dependencies=[Depends(get_current_active_user)],
|
||||
prefix="/groups",
|
||||
prefix="/access/groups",
|
||||
tags=["Users groups"]
|
||||
)
|
||||
|
||||
router.include_router(
|
||||
roles.router,
|
||||
dependencies=[Depends(get_current_active_user)],
|
||||
prefix="/roles",
|
||||
prefix="/access/roles",
|
||||
tags=["Roles"]
|
||||
)
|
||||
|
||||
router.include_router(
|
||||
permissions.router,
|
||||
privileges.router,
|
||||
dependencies=[Depends(get_current_active_user)],
|
||||
prefix="/permissions",
|
||||
tags=["Permissions"]
|
||||
prefix="/access/privileges",
|
||||
tags=["Privileges"]
|
||||
)
|
||||
|
||||
router.include_router(
|
||||
acl.router,
|
||||
prefix="/access/acl",
|
||||
tags=["ACL"]
|
||||
)
|
||||
|
||||
router.include_router(
|
||||
images.router,
|
||||
dependencies=[Depends(get_current_active_user)],
|
||||
prefix="/images",
|
||||
tags=["Images"]
|
||||
)
|
||||
|
||||
router.include_router(
|
||||
templates.router,
|
||||
dependencies=[Depends(get_current_active_user)],
|
||||
prefix="/templates",
|
||||
tags=["Templates"]
|
||||
)
|
||||
|
||||
router.include_router(
|
||||
projects.router,
|
||||
dependencies=[Depends(get_current_active_user)],
|
||||
prefix="/projects",
|
||||
tags=["Projects"])
|
||||
|
||||
router.include_router(
|
||||
nodes.router,
|
||||
dependencies=[Depends(get_current_active_user)],
|
||||
prefix="/projects/{project_id}/nodes",
|
||||
tags=["Nodes"]
|
||||
)
|
||||
|
||||
router.include_router(
|
||||
links.router,
|
||||
dependencies=[Depends(get_current_active_user)],
|
||||
prefix="/projects/{project_id}/links",
|
||||
tags=["Links"]
|
||||
)
|
||||
|
||||
router.include_router(
|
||||
drawings.router,
|
||||
dependencies=[Depends(get_current_active_user)],
|
||||
prefix="/projects/{project_id}/drawings",
|
||||
tags=["Drawings"])
|
||||
|
||||
@ -109,7 +116,6 @@ router.include_router(
|
||||
|
||||
router.include_router(
|
||||
snapshots.router,
|
||||
dependencies=[Depends(get_current_active_user)],
|
||||
prefix="/projects/{project_id}/snapshots",
|
||||
tags=["Snapshots"])
|
||||
|
||||
@ -120,23 +126,22 @@ router.include_router(
|
||||
tags=["Computes"]
|
||||
)
|
||||
|
||||
router.include_router(
|
||||
notifications.router,
|
||||
dependencies=[Depends(get_current_active_user)],
|
||||
prefix="/notifications",
|
||||
tags=["Notifications"])
|
||||
|
||||
router.include_router(
|
||||
appliances.router,
|
||||
dependencies=[Depends(get_current_active_user)],
|
||||
prefix="/appliances",
|
||||
tags=["Appliances"]
|
||||
)
|
||||
|
||||
router.include_router(
|
||||
pools.router,
|
||||
prefix="/pools",
|
||||
tags=["Resource pools"]
|
||||
)
|
||||
|
||||
router.include_router(
|
||||
gns3vm.router,
|
||||
deprecated=True,
|
||||
dependencies=[Depends(get_current_active_user)],
|
||||
deprecated=True,
|
||||
prefix="/gns3vm",
|
||||
tags=["GNS3 VM"]
|
||||
)
|
||||
|
268
gns3server/api/routes/controller/acl.py
Normal file
268
gns3server/api/routes/controller/acl.py
Normal file
@ -0,0 +1,268 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2023 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API routes for ACL.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from fastapi import APIRouter, Depends, Request, status
|
||||
from fastapi.routing import APIRoute
|
||||
from uuid import UUID
|
||||
from typing import List
|
||||
|
||||
|
||||
from gns3server import schemas
|
||||
from gns3server.controller.controller_error import (
|
||||
ControllerBadRequestError,
|
||||
ControllerNotFoundError
|
||||
)
|
||||
|
||||
from gns3server.controller import Controller
|
||||
from gns3server.db.repositories.users import UsersRepository
|
||||
from gns3server.db.repositories.rbac import RbacRepository
|
||||
from gns3server.db.repositories.images import ImagesRepository
|
||||
from gns3server.db.repositories.templates import TemplatesRepository
|
||||
from gns3server.db.repositories.pools import ResourcePoolsRepository
|
||||
from .dependencies.database import get_repository
|
||||
from .dependencies.rbac import has_privilege
|
||||
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get(
|
||||
"/endpoints",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
dependencies=[Depends(has_privilege("ACE.Audit"))]
|
||||
)
|
||||
async def endpoints(
|
||||
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
|
||||
images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)),
|
||||
templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)),
|
||||
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository))
|
||||
) -> List[dict]:
|
||||
"""
|
||||
List all endpoints to be used in ACL entries.
|
||||
"""
|
||||
|
||||
controller = Controller.instance()
|
||||
endpoints = [{"endpoint": "/", "name": "All endpoints", "endpoint_type": "root"}]
|
||||
|
||||
def add_to_endpoints(endpoint: str, name: str, endpoint_type: str) -> None:
|
||||
if endpoint not in endpoints:
|
||||
endpoints.append({"endpoint": endpoint, "name": name, "endpoint_type": endpoint_type})
|
||||
|
||||
# projects
|
||||
add_to_endpoints("/projects", "All projects", "project")
|
||||
projects = [p for p in controller.projects.values()]
|
||||
for project in projects:
|
||||
add_to_endpoints(f"/projects/{project.id}", f'Project "{project.name}"', "project")
|
||||
|
||||
if project.status == "closed":
|
||||
nodes = project.nodes.values()
|
||||
links = project.links.values()
|
||||
else:
|
||||
nodes = [v.asdict() for v in project.nodes.values()]
|
||||
links = [v.asdict() for v in project.links.values()]
|
||||
|
||||
# nodes
|
||||
add_to_endpoints(f"/projects/{project.id}/nodes", f'All nodes in project "{project.name}"', "node")
|
||||
for node in nodes:
|
||||
add_to_endpoints(
|
||||
f"/projects/{project.id}/nodes/{node['node_id']}",
|
||||
f'Node "{node["name"]}" in project "{project.name}"',
|
||||
endpoint_type="node"
|
||||
)
|
||||
|
||||
# links
|
||||
add_to_endpoints(f"/projects/{project.id}/links", f'All links in project "{project.name}"', "link")
|
||||
for link in links:
|
||||
node_id_1 = link["nodes"][0]["node_id"]
|
||||
node_id_2 = link["nodes"][1]["node_id"]
|
||||
node_name_1 = node_name_2 = "N/A"
|
||||
for node in nodes:
|
||||
if node["node_id"] == node_id_1:
|
||||
node_name_1 = node["name"]
|
||||
if node["node_id"] == node_id_2:
|
||||
node_name_2 = node["name"]
|
||||
add_to_endpoints(
|
||||
f"/projects/{project.id}/links/{link['link_id']}",
|
||||
f'Link from "{node_name_1}" to "{node_name_2}" in project "{project.name}"',
|
||||
endpoint_type="link"
|
||||
)
|
||||
|
||||
# users
|
||||
add_to_endpoints("/access/users", "All users", "user")
|
||||
users = await users_repo.get_users()
|
||||
for user in users:
|
||||
add_to_endpoints(f"/users/{user.user_id}", f'User "{user.username}"', "user")
|
||||
|
||||
# groups
|
||||
add_to_endpoints("/access/groups", "All groups", "group")
|
||||
groups = await users_repo.get_user_groups()
|
||||
for group in groups:
|
||||
add_to_endpoints(f"/groups/{group.user_group_id}", f'Group "{group.name}"', "group")
|
||||
|
||||
# roles
|
||||
add_to_endpoints("/access/roles", "All roles", "role")
|
||||
roles = await rbac_repo.get_roles()
|
||||
for role in roles:
|
||||
add_to_endpoints(f"/roles/{role.role_id}", f'Role "{role.name}"', "role")
|
||||
|
||||
# images
|
||||
add_to_endpoints("/images", "All images", "image")
|
||||
images = await images_repo.get_images()
|
||||
for image in images:
|
||||
add_to_endpoints(f"/images/{image.filename}", f'Image "{image.filename}"', "image")
|
||||
|
||||
# templates
|
||||
add_to_endpoints("/templates", "All templates", "template")
|
||||
templates = await templates_repo.get_templates()
|
||||
for template in templates:
|
||||
add_to_endpoints(f"/templates/{template.template_id}", f'Template "{template.name}"', "template")
|
||||
|
||||
# resource pools
|
||||
add_to_endpoints("/pools", "All resource pools", "pool")
|
||||
pools = await pools_repo.get_resource_pools()
|
||||
for pool in pools:
|
||||
add_to_endpoints(f"/pools/{pool.resource_pool_id}", f'Resource pool "{pool.name}"', "pool")
|
||||
return endpoints
|
||||
|
||||
|
||||
@router.get(
|
||||
"",
|
||||
response_model=List[schemas.ACE],
|
||||
dependencies=[Depends(has_privilege("ACE.Audit"))]
|
||||
)
|
||||
async def get_aces(
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
|
||||
) -> List[schemas.ACE]:
|
||||
"""
|
||||
Get all ACL entries.
|
||||
|
||||
Required privilege: ACE.Audit
|
||||
"""
|
||||
|
||||
return await rbac_repo.get_aces()
|
||||
|
||||
|
||||
@router.post(
|
||||
"",
|
||||
response_model=schemas.ACE,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
dependencies=[Depends(has_privilege("ACE.Allocate"))]
|
||||
)
|
||||
async def create_ace(
|
||||
request: Request,
|
||||
ace_create: schemas.ACECreate,
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
|
||||
) -> schemas.ACE:
|
||||
"""
|
||||
Create a new ACL entry.
|
||||
|
||||
Required privilege: ACE.Allocate
|
||||
"""
|
||||
|
||||
for route in request.app.routes:
|
||||
if isinstance(route, APIRoute):
|
||||
|
||||
# remove the prefix (e.g. "/v3") from the route path
|
||||
route_path = re.sub(r"^/v[0-9]", "", route.path)
|
||||
# replace route path ID parameters by a UUID regex
|
||||
route_path = re.sub(r"{\w+_id}", "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}", route_path)
|
||||
# replace remaining route path parameters by a word matching regex
|
||||
route_path = re.sub(r"/{[\w:]+}", r"/\\w+", route_path)
|
||||
|
||||
if re.fullmatch(route_path, ace_create.path):
|
||||
log.info(f"Creating ACE for route path {route_path}")
|
||||
return await rbac_repo.create_ace(ace_create)
|
||||
|
||||
raise ControllerBadRequestError(f"Path '{ace_create.path}' doesn't match any existing endpoint")
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{ace_id}",
|
||||
response_model=schemas.ACE,
|
||||
dependencies=[Depends(has_privilege("ACE.Audit"))]
|
||||
)
|
||||
async def get_ace(
|
||||
ace_id: UUID,
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
|
||||
) -> schemas.ACE:
|
||||
"""
|
||||
Get an ACL entry.
|
||||
|
||||
Required privilege: ACE.Audit
|
||||
"""
|
||||
|
||||
ace = await rbac_repo.get_ace(ace_id)
|
||||
if not ace:
|
||||
raise ControllerNotFoundError(f"ACL entry '{ace_id}' not found")
|
||||
return ace
|
||||
|
||||
|
||||
@router.put(
|
||||
"/{ace_id}",
|
||||
response_model=schemas.ACE,
|
||||
dependencies=[Depends(has_privilege("ACE.Modify"))]
|
||||
)
|
||||
async def update_ace(
|
||||
ace_id: UUID,
|
||||
ace_update: schemas.ACEUpdate,
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
|
||||
) -> schemas.ACE:
|
||||
"""
|
||||
Update an ACL entry.
|
||||
|
||||
Required privilege: ACE.Modify
|
||||
"""
|
||||
|
||||
ace = await rbac_repo.get_ace(ace_id)
|
||||
if not ace:
|
||||
raise ControllerNotFoundError(f"ACL entry '{ace_id}' not found")
|
||||
|
||||
return await rbac_repo.update_ace(ace_id, ace_update)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/{ace_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(has_privilege("ACE.Allocate"))]
|
||||
)
|
||||
async def delete_ace(
|
||||
ace_id: UUID,
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
|
||||
) -> None:
|
||||
"""
|
||||
Delete an ACL entry.
|
||||
|
||||
Required privilege: ACE.Allocate
|
||||
"""
|
||||
|
||||
ace = await rbac_repo.get_ace(ace_id)
|
||||
if not ace:
|
||||
raise ControllerNotFoundError(f"ACL entry '{ace_id}' not found")
|
||||
|
||||
success = await rbac_repo.delete_ace(ace_id)
|
||||
if not success:
|
||||
raise ControllerNotFoundError(f"ACL entry '{ace_id}' could not be deleted")
|
@ -20,7 +20,7 @@ API routes for appliances.
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, Response, status
|
||||
from fastapi import APIRouter, Depends, status
|
||||
from typing import Optional, List
|
||||
from uuid import UUID
|
||||
|
||||
@ -38,19 +38,28 @@ from gns3server.db.repositories.rbac import RbacRepository
|
||||
|
||||
from .dependencies.authentication import get_current_active_user
|
||||
from .dependencies.database import get_repository
|
||||
from .dependencies.rbac import has_privilege
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("")
|
||||
@router.get(
|
||||
"",
|
||||
response_model=List[schemas.Appliance],
|
||||
response_model_exclude_unset=True,
|
||||
dependencies=[Depends(has_privilege("Appliance.Audit"))]
|
||||
)
|
||||
async def get_appliances(
|
||||
update: Optional[bool] = False,
|
||||
symbol_theme: Optional[str] = None
|
||||
) -> List[schemas.Appliance]:
|
||||
"""
|
||||
Return all appliances known by the controller.
|
||||
|
||||
Required privilege: Appliance.Audit
|
||||
"""
|
||||
|
||||
controller = Controller.instance()
|
||||
@ -60,10 +69,17 @@ async def get_appliances(
|
||||
return [c.asdict() for c in controller.appliance_manager.appliances.values()]
|
||||
|
||||
|
||||
@router.get("/{appliance_id}")
|
||||
@router.get(
|
||||
"/{appliance_id}",
|
||||
response_model=schemas.Appliance,
|
||||
response_model_exclude_unset=True,
|
||||
dependencies=[Depends(has_privilege("Appliance.Audit"))]
|
||||
)
|
||||
def get_appliance(appliance_id: UUID) -> schemas.Appliance:
|
||||
"""
|
||||
Get an appliance file.
|
||||
|
||||
Required privilege: Appliance.Audit
|
||||
"""
|
||||
|
||||
controller = Controller.instance()
|
||||
@ -73,10 +89,16 @@ def get_appliance(appliance_id: UUID) -> schemas.Appliance:
|
||||
return appliance.asdict()
|
||||
|
||||
|
||||
@router.post("/{appliance_id}/version", status_code=status.HTTP_201_CREATED)
|
||||
def add_appliance_version(appliance_id: UUID, appliance_version: schemas.ApplianceVersion) -> schemas.Appliance:
|
||||
@router.post(
|
||||
"/{appliance_id}/version",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
dependencies=[Depends(has_privilege("Appliance.Allocate"))]
|
||||
)
|
||||
def add_appliance_version(appliance_id: UUID, appliance_version: schemas.ApplianceVersion) -> dict:
|
||||
"""
|
||||
Add a version to an appliance
|
||||
Add a version to an appliance.
|
||||
|
||||
Required privilege: Appliance.Allocate
|
||||
"""
|
||||
|
||||
controller = Controller.instance()
|
||||
@ -94,11 +116,15 @@ def add_appliance_version(appliance_id: UUID, appliance_version: schemas.Applian
|
||||
if version.get("name") == appliance_version.name:
|
||||
raise ControllerError(message=f"Appliance '{appliance_id}' already has version '{appliance_version.name}'")
|
||||
|
||||
appliance.versions.append(appliance_version.dict(exclude_unset=True))
|
||||
appliance.versions.append(appliance_version.model_dump(exclude_unset=True))
|
||||
return appliance.asdict()
|
||||
|
||||
|
||||
@router.post("/{appliance_id}/install", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{appliance_id}/install",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(has_privilege("Appliance.Allocate"))]
|
||||
)
|
||||
async def install_appliance(
|
||||
appliance_id: UUID,
|
||||
version: Optional[str] = None,
|
||||
@ -109,6 +135,8 @@ async def install_appliance(
|
||||
) -> None:
|
||||
"""
|
||||
Install an appliance.
|
||||
|
||||
Required privilege: Appliance.Allocate
|
||||
"""
|
||||
|
||||
controller = Controller.instance()
|
||||
|
@ -18,16 +18,18 @@
|
||||
API routes for computes.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, Response, status
|
||||
from typing import List, Union, Optional
|
||||
from fastapi import APIRouter, Depends, status
|
||||
from typing import Any, List, Union, Optional
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.controller import Controller
|
||||
from gns3server.db.repositories.computes import ComputesRepository
|
||||
from gns3server.db.repositories.rbac import RbacRepository
|
||||
from gns3server.services.computes import ComputesService
|
||||
from gns3server import schemas
|
||||
|
||||
from .dependencies.database import get_repository
|
||||
from .dependencies.rbac import has_privilege
|
||||
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Compute not found"}}
|
||||
|
||||
@ -43,6 +45,7 @@ router = APIRouter(responses=responses)
|
||||
409: {"model": schemas.ErrorMessage, "description": "Could not create compute"},
|
||||
401: {"model": schemas.ErrorMessage, "description": "Invalid authentication for compute"},
|
||||
},
|
||||
dependencies=[Depends(has_privilege("Compute.Allocate"))]
|
||||
)
|
||||
async def create_compute(
|
||||
compute_create: schemas.ComputeCreate,
|
||||
@ -51,15 +54,23 @@ async def create_compute(
|
||||
) -> schemas.Compute:
|
||||
"""
|
||||
Create a new compute on the controller.
|
||||
|
||||
Required privilege: Compute.Allocate
|
||||
"""
|
||||
|
||||
return await ComputesService(computes_repo).create_compute(compute_create, connect)
|
||||
|
||||
|
||||
@router.post("/{compute_id}/connect", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{compute_id}/connect",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
#dependencies=[Depends(has_privilege("Compute.Audit"))] # FIXME: this is a temporary workaround due to a bug in the web-ui
|
||||
)
|
||||
async def connect_compute(compute_id: Union[str, UUID]) -> None:
|
||||
"""
|
||||
Connect to compute on the controller.
|
||||
|
||||
Required privilege: Compute.Audit
|
||||
"""
|
||||
|
||||
compute = Controller.instance().get_compute(str(compute_id))
|
||||
@ -67,29 +78,48 @@ async def connect_compute(compute_id: Union[str, UUID]) -> None:
|
||||
await compute.connect(report_failed_connection=True)
|
||||
|
||||
|
||||
@router.get("/{compute_id}", response_model=schemas.Compute, response_model_exclude_unset=True)
|
||||
@router.get(
|
||||
"/{compute_id}",
|
||||
response_model=schemas.Compute,
|
||||
response_model_exclude_unset=True,
|
||||
#dependencies=[Depends(has_privilege("Compute.Audit"))] # FIXME: this is a temporary workaround due to a bug in the web-ui
|
||||
)
|
||||
async def get_compute(
|
||||
compute_id: Union[str, UUID], computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository))
|
||||
) -> schemas.Compute:
|
||||
"""
|
||||
Return a compute from the controller.
|
||||
|
||||
Required privilege: Compute.Audit
|
||||
"""
|
||||
|
||||
return await ComputesService(computes_repo).get_compute(compute_id)
|
||||
|
||||
|
||||
@router.get("", response_model=List[schemas.Compute], response_model_exclude_unset=True)
|
||||
@router.get(
|
||||
"",
|
||||
response_model=List[schemas.Compute],
|
||||
response_model_exclude_unset=True,
|
||||
#dependencies=[Depends(has_privilege("Compute.Audit"))] # FIXME: this is a temporary workaround due to a bug in the web-ui
|
||||
)
|
||||
async def get_computes(
|
||||
computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository)),
|
||||
) -> List[schemas.Compute]:
|
||||
"""
|
||||
Return all computes known by the controller.
|
||||
|
||||
Required privilege: Compute.Audit
|
||||
"""
|
||||
|
||||
return await ComputesService(computes_repo).get_computes()
|
||||
|
||||
|
||||
@router.put("/{compute_id}", response_model=schemas.Compute, response_model_exclude_unset=True)
|
||||
@router.put(
|
||||
"/{compute_id}",
|
||||
response_model=schemas.Compute,
|
||||
response_model_exclude_unset=True,
|
||||
dependencies=[Depends(has_privilege("Compute.Modify"))]
|
||||
)
|
||||
async def update_compute(
|
||||
compute_id: Union[str, UUID],
|
||||
compute_update: schemas.ComputeUpdate,
|
||||
@ -97,20 +127,31 @@ async def update_compute(
|
||||
) -> schemas.Compute:
|
||||
"""
|
||||
Update a compute on the controller.
|
||||
|
||||
Required privilege: Compute.Modify
|
||||
"""
|
||||
|
||||
return await ComputesService(computes_repo).update_compute(compute_id, compute_update)
|
||||
|
||||
|
||||
@router.delete("/{compute_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.delete(
|
||||
"/{compute_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(has_privilege("Compute.Allocate"))]
|
||||
)
|
||||
async def delete_compute(
|
||||
compute_id: Union[str, UUID], computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository))
|
||||
compute_id: Union[str, UUID],
|
||||
computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository)),
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
|
||||
) -> None:
|
||||
"""
|
||||
Delete a compute from the controller.
|
||||
|
||||
Required privilege: Compute.Allocate
|
||||
"""
|
||||
|
||||
await ComputesService(computes_repo).delete_compute(compute_id)
|
||||
await rbac_repo.delete_all_ace_starting_with_path(f"/computes/{compute_id}")
|
||||
|
||||
|
||||
@router.get("/{compute_id}/docker/images", response_model=List[schemas.ComputeDockerImage])
|
||||
@ -157,7 +198,7 @@ async def dynamips_autoidlepc(compute_id: Union[str, UUID], auto_idle_pc: schema
|
||||
|
||||
|
||||
@router.get("/{compute_id}/{emulator}/{endpoint_path:path}", deprecated=True)
|
||||
async def forward_get(compute_id: Union[str, UUID], emulator: str, endpoint_path: str) -> dict:
|
||||
async def forward_get(compute_id: Union[str, UUID], emulator: str, endpoint_path: str) -> Any:
|
||||
"""
|
||||
Forward a GET request to a compute.
|
||||
Read the full compute API documentation for available routes.
|
||||
@ -169,7 +210,7 @@ async def forward_get(compute_id: Union[str, UUID], emulator: str, endpoint_path
|
||||
|
||||
|
||||
@router.post("/{compute_id}/{emulator}/{endpoint_path:path}", deprecated=True)
|
||||
async def forward_post(compute_id: Union[str, UUID], emulator: str, endpoint_path: str, compute_data: dict) -> dict:
|
||||
async def forward_post(compute_id: Union[str, UUID], emulator: str, endpoint_path: str, compute_data: dict) -> Any:
|
||||
"""
|
||||
Forward a POST request to a compute.
|
||||
Read the full compute API documentation for available routes.
|
||||
@ -180,7 +221,7 @@ async def forward_post(compute_id: Union[str, UUID], emulator: str, endpoint_pat
|
||||
|
||||
|
||||
@router.put("/{compute_id}/{emulator}/{endpoint_path:path}", deprecated=True)
|
||||
async def forward_put(compute_id: Union[str, UUID], emulator: str, endpoint_path: str, compute_data: dict) -> dict:
|
||||
async def forward_put(compute_id: Union[str, UUID], emulator: str, endpoint_path: str, compute_data: dict) -> Any:
|
||||
"""
|
||||
Forward a PUT request to a compute.
|
||||
Read the full compute API documentation for available routes.
|
||||
|
@ -18,9 +18,12 @@ import asyncio
|
||||
import signal
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, Depends, Request, Response, status
|
||||
from fastapi import APIRouter, Request, Depends, WebSocket, WebSocketDisconnect, status
|
||||
from fastapi.responses import StreamingResponse
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.routing import Mount
|
||||
from websockets.exceptions import ConnectionClosed, WebSocketException
|
||||
|
||||
from typing import List
|
||||
|
||||
from gns3server.config import Config
|
||||
@ -29,7 +32,7 @@ from gns3server.version import __version__
|
||||
from gns3server.controller.controller_error import ControllerError, ControllerForbiddenError
|
||||
from gns3server import schemas
|
||||
|
||||
from .dependencies.authentication import get_current_active_user
|
||||
from .dependencies.authentication import get_current_active_user, get_current_active_user_from_websocket
|
||||
|
||||
import logging
|
||||
|
||||
@ -174,6 +177,52 @@ async def statistics() -> List[dict]:
|
||||
return compute_statistics
|
||||
|
||||
|
||||
@router.get("/notifications", dependencies=[Depends(get_current_active_user)])
|
||||
async def controller_http_notifications(request: Request) -> StreamingResponse:
|
||||
"""
|
||||
Receive controller notifications about the controller from HTTP stream.
|
||||
"""
|
||||
|
||||
from gns3server.api.server import app
|
||||
log.info(f"New client {request.client.host}:{request.client.port} has connected to controller HTTP "
|
||||
f"notification stream")
|
||||
|
||||
async def event_stream():
|
||||
try:
|
||||
with Controller.instance().notification.controller_queue() as queue:
|
||||
while not app.state.exiting:
|
||||
msg = await queue.get_json(5)
|
||||
yield f"{msg}\n".encode("utf-8")
|
||||
finally:
|
||||
log.info(f"Client {request.client.host}:{request.client.port} has disconnected from controller HTTP "
|
||||
f"notification stream")
|
||||
return StreamingResponse(event_stream(), media_type="application/json")
|
||||
|
||||
|
||||
@router.websocket("/notifications/ws")
|
||||
async def controller_ws_notifications(
|
||||
websocket: WebSocket,
|
||||
current_user: schemas.User = Depends(get_current_active_user_from_websocket)
|
||||
) -> None:
|
||||
"""
|
||||
Receive project notifications about the controller from WebSocket.
|
||||
"""
|
||||
|
||||
if current_user is None:
|
||||
return
|
||||
|
||||
log.info(f"New client {websocket.client.host}:{websocket.client.port} has connected to controller WebSocket")
|
||||
try:
|
||||
with Controller.instance().notification.controller_queue() as queue:
|
||||
while True:
|
||||
notification = await queue.get_json(5)
|
||||
await websocket.send_text(notification)
|
||||
except (ConnectionClosed, WebSocketDisconnect):
|
||||
log.info(f"Client {websocket.client.host}:{websocket.client.port} has disconnected from controller WebSocket")
|
||||
except WebSocketException as e:
|
||||
log.warning(f"Error while sending to controller event to WebSocket client: {e}")
|
||||
|
||||
|
||||
# @Route.post(
|
||||
# r"/debug",
|
||||
# description="Dump debug information to disk (debug directory in config directory). Work only for local server",
|
||||
|
@ -14,7 +14,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import re
|
||||
import logging
|
||||
|
||||
from fastapi import Request, Query, Depends, HTTPException, WebSocket, status
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
@ -26,7 +26,8 @@ from gns3server.db.repositories.rbac import RbacRepository
|
||||
from gns3server.services import auth_service
|
||||
from .database import get_repository
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/v3/users/login", auto_error=False)
|
||||
log = logging.getLogger(__name__)
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/v3/access/users/login", auto_error=False)
|
||||
|
||||
|
||||
async def get_user_from_token(
|
||||
@ -74,21 +75,6 @@ async def get_current_active_user(
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
# remove the prefix (e.g. "/v3") from URL path
|
||||
path = re.sub(r"^/v[0-9]", "", request.url.path)
|
||||
|
||||
# special case: always authorize access to the "/users/me" endpoint
|
||||
if path == "/users/me":
|
||||
return current_user
|
||||
|
||||
authorized = await rbac_repo.check_user_is_authorized(current_user.user_id, request.method, path)
|
||||
if not authorized:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=f"User is not authorized '{current_user.user_id}' on {request.method} '{path}'",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
return current_user
|
||||
|
||||
|
||||
@ -96,7 +82,6 @@ async def get_current_active_user_from_websocket(
|
||||
websocket: WebSocket,
|
||||
token: str = Query(...),
|
||||
user_repo: UsersRepository = Depends(get_repository(UsersRepository)),
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
|
||||
) -> Optional[schemas.User]:
|
||||
|
||||
await websocket.accept()
|
||||
@ -121,22 +106,12 @@ async def get_current_active_user_from_websocket(
|
||||
detail=f"'{username}' is not an active user"
|
||||
)
|
||||
|
||||
# remove the prefix (e.g. "/v3") from URL path
|
||||
path = re.sub(r"^/v[0-9]", "", websocket.url.path)
|
||||
|
||||
# there are no HTTP methods for web sockets, assuming "GET"...
|
||||
authorized = await rbac_repo.check_user_is_authorized(user.user_id, "GET", path)
|
||||
if not authorized:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=f"User is not authorized '{user.user_id}' on '{path}'",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
return user
|
||||
|
||||
except HTTPException as e:
|
||||
websocket_error = {"action": "log.error", "event": {"message": f"Could not authenticate while connecting to "
|
||||
f"WebSocket: {e.detail}"}}
|
||||
err_msg = f"Could not authenticate while connecting to controller WebSocket: {e.detail}"
|
||||
websocket_error = {"action": "log.error", "event": {"message": err_msg}}
|
||||
await websocket.send_json(websocket_error)
|
||||
await websocket.close(code=1008)
|
||||
log.error(err_msg)
|
||||
return await websocket.close(code=1008)
|
||||
|
||||
|
78
gns3server/api/routes/controller/dependencies/rbac.py
Normal file
78
gns3server/api/routes/controller/dependencies/rbac.py
Normal file
@ -0,0 +1,78 @@
|
||||
#
|
||||
# Copyright (C) 2023 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import re
|
||||
|
||||
from fastapi import Request, WebSocket, Depends, HTTPException
|
||||
from gns3server import schemas
|
||||
from gns3server.db.repositories.rbac import RbacRepository
|
||||
from .authentication import get_current_active_user, get_current_active_user_from_websocket
|
||||
from .database import get_repository
|
||||
|
||||
import logging
|
||||
|
||||
log = logging.getLogger()
|
||||
|
||||
|
||||
def has_privilege(
|
||||
privilege_name: str
|
||||
):
|
||||
async def get_user_and_check_privilege(
|
||||
request: Request,
|
||||
current_user: schemas.User = Depends(get_current_active_user),
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
|
||||
):
|
||||
if not current_user.is_superadmin:
|
||||
path = re.sub(r"^/v[0-9]", "", request.url.path) # remove the prefix (e.g. "/v3") from URL path
|
||||
log.debug(f"Checking user {current_user.username} has privilege {privilege_name} on '{path}'")
|
||||
if not await rbac_repo.check_user_has_privilege(current_user.user_id, path, privilege_name):
|
||||
raise HTTPException(status_code=403, detail=f"Permission denied (privilege {privilege_name} is required)")
|
||||
return current_user
|
||||
return get_user_and_check_privilege
|
||||
|
||||
|
||||
def has_privilege_on_websocket(
|
||||
privilege_name: str
|
||||
):
|
||||
async def get_user_and_check_privilege(
|
||||
websocket: WebSocket,
|
||||
current_user: schemas.User = Depends(get_current_active_user_from_websocket),
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
|
||||
):
|
||||
if not current_user.is_superadmin:
|
||||
path = re.sub(r"^/v[0-9]", "", websocket.url.path) # remove the prefix (e.g. "/v3") from URL path
|
||||
log.debug(f"Checking user {current_user.username} has privilege {privilege_name} on '{path}'")
|
||||
if not await rbac_repo.check_user_has_privilege(current_user.user_id, path, privilege_name):
|
||||
raise HTTPException(status_code=403, detail=f"Permission denied (privilege {privilege_name} is required)")
|
||||
return current_user
|
||||
return get_user_and_check_privilege
|
||||
|
||||
# class PrivilegeChecker:
|
||||
#
|
||||
# def __init__(self, required_privilege: str) -> None:
|
||||
# self._required_privilege = required_privilege
|
||||
#
|
||||
# async def __call__(
|
||||
# self,
|
||||
# current_user: schemas.User = Depends(get_current_active_user),
|
||||
# rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
|
||||
# ) -> bool:
|
||||
#
|
||||
# if not await rbac_repo.check_user_has_privilege(current_user.user_id, "/projects", self._required_privilege):
|
||||
# raise HTTPException(status_code=403, detail=f"Permission denied (privilege {self._required_privilege} is required)")
|
||||
# return True
|
||||
|
||||
# Depends(PrivilegeChecker("Project.Audit"))
|
@ -18,33 +18,54 @@
|
||||
API routes for drawings.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Response, status
|
||||
from fastapi import APIRouter, Depends, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from typing import List
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.controller import Controller
|
||||
from gns3server.db.repositories.rbac import RbacRepository
|
||||
from gns3server import schemas
|
||||
|
||||
from .dependencies.database import get_repository
|
||||
from .dependencies.rbac import has_privilege
|
||||
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Project or drawing not found"}}
|
||||
|
||||
router = APIRouter(responses=responses)
|
||||
|
||||
|
||||
@router.get("", response_model=List[schemas.Drawing], response_model_exclude_unset=True)
|
||||
@router.get(
|
||||
"",
|
||||
response_model=List[schemas.Drawing],
|
||||
response_model_exclude_unset=True,
|
||||
dependencies=[Depends(has_privilege("Drawing.Audit"))]
|
||||
)
|
||||
async def get_drawings(project_id: UUID) -> List[schemas.Drawing]:
|
||||
"""
|
||||
Return the list of all drawings for a given project.
|
||||
|
||||
Required privilege: Drawing.Audit
|
||||
"""
|
||||
|
||||
project = await Controller.instance().get_loaded_project(str(project_id))
|
||||
if project.status == "closed":
|
||||
# allow to retrieve drawings from a closed project
|
||||
return project.drawings.values()
|
||||
return [v.asdict() for v in project.drawings.values()]
|
||||
|
||||
|
||||
@router.post("", status_code=status.HTTP_201_CREATED, response_model=schemas.Drawing)
|
||||
@router.post(
|
||||
"",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.Drawing,
|
||||
dependencies=[Depends(has_privilege("Drawing.Allocate"))]
|
||||
)
|
||||
async def create_drawing(project_id: UUID, drawing_data: schemas.Drawing) -> schemas.Drawing:
|
||||
"""
|
||||
Create a new drawing.
|
||||
|
||||
Required privilege: Drawing.Allocate
|
||||
"""
|
||||
|
||||
project = await Controller.instance().get_loaded_project(str(project_id))
|
||||
@ -52,10 +73,17 @@ async def create_drawing(project_id: UUID, drawing_data: schemas.Drawing) -> sch
|
||||
return drawing.asdict()
|
||||
|
||||
|
||||
@router.get("/{drawing_id}", response_model=schemas.Drawing, response_model_exclude_unset=True)
|
||||
@router.get(
|
||||
"/{drawing_id}",
|
||||
response_model=schemas.Drawing,
|
||||
response_model_exclude_unset=True,
|
||||
dependencies=[Depends(has_privilege("Drawing.Audit"))]
|
||||
)
|
||||
async def get_drawing(project_id: UUID, drawing_id: UUID) -> schemas.Drawing:
|
||||
"""
|
||||
Return a drawing.
|
||||
|
||||
Required privilege: Drawing.Audit
|
||||
"""
|
||||
|
||||
project = await Controller.instance().get_loaded_project(str(project_id))
|
||||
@ -63,10 +91,17 @@ async def get_drawing(project_id: UUID, drawing_id: UUID) -> schemas.Drawing:
|
||||
return drawing.asdict()
|
||||
|
||||
|
||||
@router.put("/{drawing_id}", response_model=schemas.Drawing, response_model_exclude_unset=True)
|
||||
@router.put(
|
||||
"/{drawing_id}",
|
||||
response_model=schemas.Drawing,
|
||||
response_model_exclude_unset=True,
|
||||
dependencies=[Depends(has_privilege("Drawing.Modify"))]
|
||||
)
|
||||
async def update_drawing(project_id: UUID, drawing_id: UUID, drawing_data: schemas.Drawing) -> schemas.Drawing:
|
||||
"""
|
||||
Update a drawing.
|
||||
|
||||
Required privilege: Drawing.Modify
|
||||
"""
|
||||
|
||||
project = await Controller.instance().get_loaded_project(str(project_id))
|
||||
@ -75,11 +110,22 @@ async def update_drawing(project_id: UUID, drawing_id: UUID, drawing_data: schem
|
||||
return drawing.asdict()
|
||||
|
||||
|
||||
@router.delete("/{drawing_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_drawing(project_id: UUID, drawing_id: UUID) -> None:
|
||||
@router.delete(
|
||||
"/{drawing_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(has_privilege("Drawing.Allocate"))]
|
||||
)
|
||||
async def delete_drawing(
|
||||
project_id: UUID,
|
||||
drawing_id: UUID,
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
|
||||
) -> None:
|
||||
"""
|
||||
Delete a drawing.
|
||||
|
||||
Required privilege: Drawing.Allocate
|
||||
"""
|
||||
|
||||
project = await Controller.instance().get_loaded_project(str(project_id))
|
||||
await project.delete_drawing(str(drawing_id))
|
||||
await rbac_repo.delete_all_ace_starting_with_path(f"/drawings/{drawing_id}")
|
||||
|
@ -19,7 +19,7 @@
|
||||
API routes for user groups.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, Response, status
|
||||
from fastapi import APIRouter, Depends, status
|
||||
from uuid import UUID
|
||||
from typing import List
|
||||
|
||||
@ -33,6 +33,8 @@ from gns3server.controller.controller_error import (
|
||||
|
||||
from gns3server.db.repositories.users import UsersRepository
|
||||
from gns3server.db.repositories.rbac import RbacRepository
|
||||
|
||||
from .dependencies.rbac import has_privilege
|
||||
from .dependencies.database import get_repository
|
||||
|
||||
import logging
|
||||
@ -42,12 +44,18 @@ log = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("", response_model=List[schemas.UserGroup])
|
||||
@router.get(
|
||||
"",
|
||||
response_model=List[schemas.UserGroup],
|
||||
dependencies=[Depends(has_privilege("Group.Audit"))]
|
||||
)
|
||||
async def get_user_groups(
|
||||
users_repo: UsersRepository = Depends(get_repository(UsersRepository))
|
||||
) -> List[schemas.UserGroup]:
|
||||
"""
|
||||
Get all user groups.
|
||||
|
||||
Required privilege: Group.Audit
|
||||
"""
|
||||
|
||||
return await users_repo.get_user_groups()
|
||||
@ -56,7 +64,8 @@ async def get_user_groups(
|
||||
@router.post(
|
||||
"",
|
||||
response_model=schemas.UserGroup,
|
||||
status_code=status.HTTP_201_CREATED
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
dependencies=[Depends(has_privilege("Group.Allocate"))]
|
||||
)
|
||||
async def create_user_group(
|
||||
user_group_create: schemas.UserGroupCreate,
|
||||
@ -64,6 +73,8 @@ async def create_user_group(
|
||||
) -> schemas.UserGroup:
|
||||
"""
|
||||
Create a new user group.
|
||||
|
||||
Required privilege: Group.Allocate
|
||||
"""
|
||||
|
||||
if await users_repo.get_user_group_by_name(user_group_create.name):
|
||||
@ -72,13 +83,19 @@ async def create_user_group(
|
||||
return await users_repo.create_user_group(user_group_create)
|
||||
|
||||
|
||||
@router.get("/{user_group_id}", response_model=schemas.UserGroup)
|
||||
@router.get(
|
||||
"/{user_group_id}",
|
||||
response_model=schemas.UserGroup,
|
||||
dependencies=[Depends(has_privilege("Group.Audit"))]
|
||||
)
|
||||
async def get_user_group(
|
||||
user_group_id: UUID,
|
||||
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
|
||||
) -> schemas.UserGroup:
|
||||
"""
|
||||
Get an user group.
|
||||
Get a user group.
|
||||
|
||||
Required privilege: Group.Audit
|
||||
"""
|
||||
|
||||
user_group = await users_repo.get_user_group(user_group_id)
|
||||
@ -87,14 +104,20 @@ async def get_user_group(
|
||||
return user_group
|
||||
|
||||
|
||||
@router.put("/{user_group_id}", response_model=schemas.UserGroup)
|
||||
@router.put(
|
||||
"/{user_group_id}",
|
||||
response_model=schemas.UserGroup,
|
||||
dependencies=[Depends(has_privilege("Group.Modify"))]
|
||||
)
|
||||
async def update_user_group(
|
||||
user_group_id: UUID,
|
||||
user_group_update: schemas.UserGroupUpdate,
|
||||
users_repo: UsersRepository = Depends(get_repository(UsersRepository))
|
||||
) -> schemas.UserGroup:
|
||||
"""
|
||||
Update an user group.
|
||||
Update a user group.
|
||||
|
||||
Required privilege: Group.Modify
|
||||
"""
|
||||
user_group = await users_repo.get_user_group(user_group_id)
|
||||
if not user_group:
|
||||
@ -108,14 +131,18 @@ async def update_user_group(
|
||||
|
||||
@router.delete(
|
||||
"/{user_group_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(has_privilege("Group.Allocate"))]
|
||||
)
|
||||
async def delete_user_group(
|
||||
user_group_id: UUID,
|
||||
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
|
||||
user_group_id: UUID,
|
||||
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
|
||||
) -> None:
|
||||
"""
|
||||
Delete an user group
|
||||
Delete a user group.
|
||||
|
||||
Required privilege: Group.Allocate
|
||||
"""
|
||||
|
||||
user_group = await users_repo.get_user_group(user_group_id)
|
||||
@ -128,15 +155,22 @@ async def delete_user_group(
|
||||
success = await users_repo.delete_user_group(user_group_id)
|
||||
if not success:
|
||||
raise ControllerError(f"User group '{user_group_id}' could not be deleted")
|
||||
await rbac_repo.delete_all_ace_starting_with_path(f"/groups/{user_group_id}")
|
||||
|
||||
|
||||
@router.get("/{user_group_id}/members", response_model=List[schemas.User])
|
||||
@router.get(
|
||||
"/{user_group_id}/members",
|
||||
response_model=List[schemas.User],
|
||||
dependencies=[Depends(has_privilege("Group.Audit"))]
|
||||
)
|
||||
async def get_user_group_members(
|
||||
user_group_id: UUID,
|
||||
users_repo: UsersRepository = Depends(get_repository(UsersRepository))
|
||||
) -> List[schemas.User]:
|
||||
"""
|
||||
Get all user group members.
|
||||
|
||||
Required privilege: Group.Audit
|
||||
"""
|
||||
|
||||
return await users_repo.get_user_group_members(user_group_id)
|
||||
@ -144,7 +178,8 @@ async def get_user_group_members(
|
||||
|
||||
@router.put(
|
||||
"/{user_group_id}/members/{user_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(has_privilege("Group.Modify"))]
|
||||
)
|
||||
async def add_member_to_group(
|
||||
user_group_id: UUID,
|
||||
@ -152,13 +187,20 @@ async def add_member_to_group(
|
||||
users_repo: UsersRepository = Depends(get_repository(UsersRepository))
|
||||
) -> None:
|
||||
"""
|
||||
Add member to an user group.
|
||||
Add member to a user group.
|
||||
|
||||
Required privilege: Group.Modify
|
||||
"""
|
||||
|
||||
user = await users_repo.get_user(user_id)
|
||||
if not user:
|
||||
raise ControllerNotFoundError(f"User '{user_id}' not found")
|
||||
|
||||
user_groups = await users_repo.get_user_memberships(user_id)
|
||||
for group in user_groups:
|
||||
if group.user_group_id == user_group_id:
|
||||
raise ControllerBadRequestError(f"Username '{user.username}' is already member of group '{group.name}'")
|
||||
|
||||
user_group = await users_repo.add_member_to_user_group(user_group_id, user)
|
||||
if not user_group:
|
||||
raise ControllerNotFoundError(f"User group '{user_group_id}' not found")
|
||||
@ -166,7 +208,8 @@ async def add_member_to_group(
|
||||
|
||||
@router.delete(
|
||||
"/{user_group_id}/members/{user_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(has_privilege("Group.Modify"))]
|
||||
)
|
||||
async def remove_member_from_group(
|
||||
user_group_id: UUID,
|
||||
@ -174,7 +217,9 @@ async def remove_member_from_group(
|
||||
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
|
||||
) -> None:
|
||||
"""
|
||||
Remove member from an user group.
|
||||
Remove member from a user group.
|
||||
|
||||
Required privilege: Group.Modify
|
||||
"""
|
||||
|
||||
user = await users_repo.get_user(user_id)
|
||||
@ -184,61 +229,3 @@ async def remove_member_from_group(
|
||||
user_group = await users_repo.remove_member_from_user_group(user_group_id, user)
|
||||
if not user_group:
|
||||
raise ControllerNotFoundError(f"User group '{user_group_id}' not found")
|
||||
|
||||
|
||||
@router.get("/{user_group_id}/roles", response_model=List[schemas.Role])
|
||||
async def get_user_group_roles(
|
||||
user_group_id: UUID,
|
||||
users_repo: UsersRepository = Depends(get_repository(UsersRepository))
|
||||
) -> List[schemas.Role]:
|
||||
"""
|
||||
Get all user group roles.
|
||||
"""
|
||||
|
||||
return await users_repo.get_user_group_roles(user_group_id)
|
||||
|
||||
|
||||
@router.put(
|
||||
"/{user_group_id}/roles/{role_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT
|
||||
)
|
||||
async def add_role_to_group(
|
||||
user_group_id: UUID,
|
||||
role_id: UUID,
|
||||
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
|
||||
) -> Response:
|
||||
"""
|
||||
Add role to an user group.
|
||||
"""
|
||||
|
||||
role = await rbac_repo.get_role(role_id)
|
||||
if not role:
|
||||
raise ControllerNotFoundError(f"Role '{role_id}' not found")
|
||||
|
||||
user_group = await users_repo.add_role_to_user_group(user_group_id, role)
|
||||
if not user_group:
|
||||
raise ControllerNotFoundError(f"User group '{user_group_id}' not found")
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/{user_group_id}/roles/{role_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT
|
||||
)
|
||||
async def remove_role_from_group(
|
||||
user_group_id: UUID,
|
||||
role_id: UUID,
|
||||
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
|
||||
) -> None:
|
||||
"""
|
||||
Remove role from an user group.
|
||||
"""
|
||||
|
||||
role = await rbac_repo.get_role(role_id)
|
||||
if not role:
|
||||
raise ControllerNotFoundError(f"Role '{role_id}' not found")
|
||||
|
||||
user_group = await users_repo.remove_role_from_user_group(user_group_id, role)
|
||||
if not user_group:
|
||||
raise ControllerNotFoundError(f"User group '{user_group_id}' not found")
|
||||
|
@ -22,14 +22,16 @@ import os
|
||||
import logging
|
||||
import urllib.parse
|
||||
|
||||
from fastapi import APIRouter, Request, Response, Depends, status
|
||||
from fastapi import APIRouter, Request, Depends, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from starlette.requests import ClientDisconnect
|
||||
from sqlalchemy.orm.exc import MultipleResultsFound
|
||||
from typing import List, Optional
|
||||
from gns3server import schemas
|
||||
|
||||
from gns3server.config import Config
|
||||
from gns3server.utils.images import InvalidImageError, write_image
|
||||
from gns3server.compute.qemu import Qemu
|
||||
from gns3server.utils.images import InvalidImageError, write_image, read_image_info, default_images_directory
|
||||
from gns3server.db.repositories.images import ImagesRepository
|
||||
from gns3server.db.repositories.templates import TemplatesRepository
|
||||
from gns3server.db.repositories.rbac import RbacRepository
|
||||
@ -43,25 +45,84 @@ from gns3server.controller.controller_error import (
|
||||
|
||||
from .dependencies.authentication import get_current_active_user
|
||||
from .dependencies.database import get_repository
|
||||
from .dependencies.rbac import has_privilege
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("", response_model=List[schemas.Image])
|
||||
@router.post(
|
||||
"/qemu/{image_path:path}",
|
||||
response_model=schemas.Image,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
dependencies=[Depends(has_privilege("Image.Allocate"))]
|
||||
)
|
||||
async def create_qemu_image(
|
||||
image_path: str,
|
||||
image_data: schemas.QemuDiskImageCreate,
|
||||
images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)),
|
||||
|
||||
) -> schemas.Image:
|
||||
"""
|
||||
Create a new blank Qemu image.
|
||||
|
||||
Required privilege: Image.Allocate
|
||||
"""
|
||||
|
||||
allow_raw_image = Config.instance().settings.Server.allow_raw_images
|
||||
if image_data.format == schemas.QemuDiskImageFormat.raw and not allow_raw_image:
|
||||
raise ControllerBadRequestError("Raw images are not allowed")
|
||||
|
||||
disk_image_path = urllib.parse.unquote(image_path)
|
||||
image_dir, image_name = os.path.split(disk_image_path)
|
||||
# check if the path is within the default images directory
|
||||
base_images_directory = os.path.expanduser(Config.instance().settings.Server.images_path)
|
||||
full_path = os.path.abspath(os.path.join(base_images_directory, image_dir, image_name))
|
||||
if os.path.commonprefix([base_images_directory, full_path]) != base_images_directory:
|
||||
raise ControllerForbiddenError(f"Cannot write disk image, '{disk_image_path}' is forbidden")
|
||||
|
||||
if not image_dir:
|
||||
# put the image in the default images directory for Qemu
|
||||
directory = default_images_directory(image_type="qemu")
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
disk_image_path = os.path.abspath(os.path.join(directory, disk_image_path))
|
||||
|
||||
if await images_repo.get_image(disk_image_path):
|
||||
raise ControllerBadRequestError(f"Disk image '{disk_image_path}' already exists")
|
||||
|
||||
options = jsonable_encoder(image_data, exclude_unset=True)
|
||||
# FIXME: should we have the create_disk_image in the compute code since
|
||||
# this code is used to create images on the controller?
|
||||
await Qemu.instance().create_disk_image(disk_image_path, options)
|
||||
|
||||
image_info = await read_image_info(disk_image_path, "qemu")
|
||||
return await images_repo.add_image(**image_info)
|
||||
|
||||
@router.get(
|
||||
"",
|
||||
response_model=List[schemas.Image],
|
||||
dependencies=[Depends(has_privilege("Image.Audit"))]
|
||||
)
|
||||
async def get_images(
|
||||
images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)),
|
||||
image_type: Optional[schemas.ImageType] = None
|
||||
) -> List[schemas.Image]:
|
||||
"""
|
||||
Return all images.
|
||||
|
||||
Required privilege: Image.Audit
|
||||
"""
|
||||
|
||||
return await images_repo.get_images(image_type)
|
||||
|
||||
|
||||
@router.post("/upload/{image_path:path}", response_model=schemas.Image, status_code=status.HTTP_201_CREATED)
|
||||
@router.post(
|
||||
"/upload/{image_path:path}",
|
||||
response_model=schemas.Image,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
dependencies=[Depends(has_privilege("Image.Allocate"))]
|
||||
)
|
||||
async def upload_image(
|
||||
image_path: str,
|
||||
request: Request,
|
||||
@ -70,13 +131,14 @@ async def upload_image(
|
||||
current_user: schemas.User = Depends(get_current_active_user),
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
|
||||
install_appliances: Optional[bool] = False,
|
||||
allow_raw_image: Optional[bool] = False
|
||||
) -> schemas.Image:
|
||||
"""
|
||||
Upload an image.
|
||||
|
||||
Example: curl -X POST http://host:port/v3/images/upload/my_image_name.qcow2 \
|
||||
-H 'Authorization: Bearer <token>' --data-binary @"/path/to/image.qcow2"
|
||||
|
||||
Required privilege: Image.Allocate
|
||||
"""
|
||||
|
||||
image_path = urllib.parse.unquote(image_path)
|
||||
@ -87,10 +149,13 @@ async def upload_image(
|
||||
if os.path.commonprefix([base_images_directory, full_path]) != base_images_directory:
|
||||
raise ControllerForbiddenError(f"Cannot write image, '{image_path}' is forbidden")
|
||||
|
||||
if await images_repo.get_image(image_path):
|
||||
raise ControllerBadRequestError(f"Image '{image_path}' already exists")
|
||||
image = await images_repo.get_image(image_path)
|
||||
if image:
|
||||
log.warning(f"Image '{image_path}' already exists")
|
||||
return image
|
||||
|
||||
try:
|
||||
allow_raw_image = Config.instance().settings.Server.allow_raw_images
|
||||
image = await write_image(image_path, full_path, request.stream(), images_repo, allow_raw_image=allow_raw_image)
|
||||
except (OSError, InvalidImageError, ClientDisconnect) as e:
|
||||
raise ControllerError(f"Could not save image '{image_path}': {e}")
|
||||
@ -110,13 +175,19 @@ async def upload_image(
|
||||
return image
|
||||
|
||||
|
||||
@router.get("/{image_path:path}", response_model=schemas.Image)
|
||||
@router.get(
|
||||
"/{image_path:path}",
|
||||
response_model=schemas.Image,
|
||||
dependencies=[Depends(has_privilege("Image.Audit"))]
|
||||
)
|
||||
async def get_image(
|
||||
image_path: str,
|
||||
images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)),
|
||||
) -> schemas.Image:
|
||||
"""
|
||||
Return an image.
|
||||
|
||||
Required privilege: Image.Audit
|
||||
"""
|
||||
|
||||
image_path = urllib.parse.unquote(image_path)
|
||||
@ -126,13 +197,19 @@ async def get_image(
|
||||
return image
|
||||
|
||||
|
||||
@router.delete("/{image_path:path}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.delete(
|
||||
"/{image_path:path}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(has_privilege("Image.Allocate"))]
|
||||
)
|
||||
async def delete_image(
|
||||
image_path: str,
|
||||
images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)),
|
||||
) -> None:
|
||||
"""
|
||||
Delete an image.
|
||||
|
||||
Required privilege: Image.Allocate
|
||||
"""
|
||||
|
||||
image_path = urllib.parse.unquote(image_path)
|
||||
@ -161,12 +238,18 @@ async def delete_image(
|
||||
raise ControllerError(f"Image '{image_path}' could not be deleted")
|
||||
|
||||
|
||||
@router.post("/prune", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/prune",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(has_privilege("Image.Allocate"))]
|
||||
)
|
||||
async def prune_images(
|
||||
images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)),
|
||||
) -> None:
|
||||
"""
|
||||
Prune images not attached to any template.
|
||||
|
||||
Required privilege: Image.Allocate
|
||||
"""
|
||||
|
||||
await images_repo.prune_images()
|
||||
|
@ -1,5 +1,5 @@
|
||||
#
|
||||
# Copyright (C) 2016 GNS3 Technologies Inc.
|
||||
# Copyright (C) 2023 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@ -21,7 +21,7 @@ API routes for links.
|
||||
import multidict
|
||||
import aiohttp
|
||||
|
||||
from fastapi import APIRouter, Depends, Request, Response, status
|
||||
from fastapi import APIRouter, Depends, Request, status
|
||||
from fastapi.responses import StreamingResponse
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from typing import List
|
||||
@ -29,10 +29,14 @@ from uuid import UUID
|
||||
|
||||
from gns3server.controller import Controller
|
||||
from gns3server.controller.controller_error import ControllerError
|
||||
from gns3server.db.repositories.rbac import RbacRepository
|
||||
from gns3server.controller.link import Link
|
||||
from gns3server.utils.http_client import HTTPClient
|
||||
from gns3server import schemas
|
||||
|
||||
from .dependencies.database import get_repository
|
||||
from .dependencies.rbac import has_privilege
|
||||
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@ -52,13 +56,23 @@ async def dep_link(project_id: UUID, link_id: UUID) -> Link:
|
||||
return link
|
||||
|
||||
|
||||
@router.get("", response_model=List[schemas.Link], response_model_exclude_unset=True)
|
||||
@router.get(
|
||||
"",
|
||||
response_model=List[schemas.Link],
|
||||
response_model_exclude_unset=True,
|
||||
dependencies=[Depends(has_privilege("Link.Audit"))]
|
||||
)
|
||||
async def get_links(project_id: UUID) -> List[schemas.Link]:
|
||||
"""
|
||||
Return all links for a given project.
|
||||
|
||||
Required privilege: Link.Audit
|
||||
"""
|
||||
|
||||
project = await Controller.instance().get_loaded_project(str(project_id))
|
||||
if project.status == "closed":
|
||||
# allow to retrieve links from a closed project
|
||||
return project.links.values()
|
||||
return [v.asdict() for v in project.links.values()]
|
||||
|
||||
|
||||
@ -70,10 +84,13 @@ async def get_links(project_id: UUID) -> List[schemas.Link]:
|
||||
404: {"model": schemas.ErrorMessage, "description": "Could not find project"},
|
||||
409: {"model": schemas.ErrorMessage, "description": "Could not create link"},
|
||||
},
|
||||
dependencies=[Depends(has_privilege("Link.Allocate"))]
|
||||
)
|
||||
async def create_link(project_id: UUID, link_data: schemas.LinkCreate) -> schemas.Link:
|
||||
"""
|
||||
Create a new link.
|
||||
|
||||
Required privilege: Link.Allocate
|
||||
"""
|
||||
|
||||
project = await Controller.instance().get_loaded_project(str(project_id))
|
||||
@ -99,28 +116,47 @@ async def create_link(project_id: UUID, link_data: schemas.LinkCreate) -> schema
|
||||
return link.asdict()
|
||||
|
||||
|
||||
@router.get("/{link_id}/available_filters")
|
||||
@router.get(
|
||||
"/{link_id}/available_filters",
|
||||
dependencies=[Depends(has_privilege("Link.Audit"))]
|
||||
)
|
||||
async def get_filters(link: Link = Depends(dep_link)) -> List[dict]:
|
||||
"""
|
||||
Return all filters available for a given link.
|
||||
|
||||
Required privilege: Link.Audit
|
||||
"""
|
||||
|
||||
return link.available_filters()
|
||||
|
||||
|
||||
@router.get("/{link_id}", response_model=schemas.Link, response_model_exclude_unset=True)
|
||||
@router.get(
|
||||
"/{link_id}",
|
||||
response_model=schemas.Link,
|
||||
response_model_exclude_unset=True,
|
||||
dependencies=[Depends(has_privilege("Link.Audit"))]
|
||||
)
|
||||
async def get_link(link: Link = Depends(dep_link)) -> schemas.Link:
|
||||
"""
|
||||
Return a link.
|
||||
|
||||
Required privilege: Link.Audit
|
||||
"""
|
||||
|
||||
return link.asdict()
|
||||
|
||||
|
||||
@router.put("/{link_id}", response_model=schemas.Link, response_model_exclude_unset=True)
|
||||
@router.put(
|
||||
"/{link_id}",
|
||||
response_model=schemas.Link,
|
||||
response_model_exclude_unset=True,
|
||||
dependencies=[Depends(has_privilege("Link.Modify"))]
|
||||
)
|
||||
async def update_link(link_data: schemas.LinkUpdate, link: Link = Depends(dep_link)) -> schemas.Link:
|
||||
"""
|
||||
Update a link.
|
||||
|
||||
Required privilege: Link.Modify
|
||||
"""
|
||||
|
||||
link_data = jsonable_encoder(link_data, exclude_unset=True)
|
||||
@ -135,30 +171,54 @@ async def update_link(link_data: schemas.LinkUpdate, link: Link = Depends(dep_li
|
||||
return link.asdict()
|
||||
|
||||
|
||||
@router.delete("/{link_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_link(project_id: UUID, link: Link = Depends(dep_link)) -> None:
|
||||
@router.delete(
|
||||
"/{link_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(has_privilege("Link.Allocate"))]
|
||||
)
|
||||
async def delete_link(
|
||||
project_id: UUID,
|
||||
link: Link = Depends(dep_link),
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
|
||||
) -> None:
|
||||
"""
|
||||
Delete a link.
|
||||
|
||||
Required privilege: Link.Allocate
|
||||
"""
|
||||
|
||||
project = await Controller.instance().get_loaded_project(str(project_id))
|
||||
await project.delete_link(link.id)
|
||||
await rbac_repo.delete_all_ace_starting_with_path(f"/links/{link.id}")
|
||||
|
||||
|
||||
@router.post("/{link_id}/reset", response_model=schemas.Link)
|
||||
@router.post(
|
||||
"/{link_id}/reset",
|
||||
response_model=schemas.Link,
|
||||
dependencies=[Depends(has_privilege("Link.Modify"))]
|
||||
)
|
||||
async def reset_link(link: Link = Depends(dep_link)) -> schemas.Link:
|
||||
"""
|
||||
Reset a link.
|
||||
|
||||
Required privilege: Link.Modify
|
||||
"""
|
||||
|
||||
await link.reset()
|
||||
return link.asdict()
|
||||
|
||||
|
||||
@router.post("/{link_id}/capture/start", status_code=status.HTTP_201_CREATED, response_model=schemas.Link)
|
||||
@router.post(
|
||||
"/{link_id}/capture/start",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.Link,
|
||||
dependencies=[Depends(has_privilege("Link.Capture"))]
|
||||
)
|
||||
async def start_capture(capture_data: dict, link: Link = Depends(dep_link)) -> schemas.Link:
|
||||
"""
|
||||
Start packet capture on the link.
|
||||
|
||||
Required privilege: Link.Capture
|
||||
"""
|
||||
|
||||
await link.start_capture(
|
||||
@ -168,19 +228,30 @@ async def start_capture(capture_data: dict, link: Link = Depends(dep_link)) -> s
|
||||
return link.asdict()
|
||||
|
||||
|
||||
@router.post("/{link_id}/capture/stop", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{link_id}/capture/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(has_privilege("Link.Capture"))]
|
||||
)
|
||||
async def stop_capture(link: Link = Depends(dep_link)) -> None:
|
||||
"""
|
||||
Stop packet capture on the link.
|
||||
|
||||
Required privilege: Link.Capture
|
||||
"""
|
||||
|
||||
await link.stop_capture()
|
||||
|
||||
|
||||
@router.get("/{link_id}/capture/stream")
|
||||
@router.get(
|
||||
"/{link_id}/capture/stream",
|
||||
dependencies=[Depends(has_privilege("Link.Capture"))]
|
||||
)
|
||||
async def stream_pcap(request: Request, link: Link = Depends(dep_link)) -> StreamingResponse:
|
||||
"""
|
||||
Stream the PCAP capture file from compute.
|
||||
|
||||
Required privilege: Link.Capture
|
||||
"""
|
||||
|
||||
if not link.capturing:
|
||||
@ -196,11 +267,13 @@ async def stream_pcap(request: Request, link: Link = Depends(dep_link)) -> Strea
|
||||
async def compute_pcap_stream():
|
||||
|
||||
try:
|
||||
ssl_context = Controller.instance().ssl_context()
|
||||
async with HTTPClient.request(
|
||||
request.method,
|
||||
pcap_streaming_url,
|
||||
user=compute.user,
|
||||
password=compute.password,
|
||||
ssl_context=ssl_context,
|
||||
timeout=None,
|
||||
data=body
|
||||
) as response:
|
||||
|
@ -20,6 +20,7 @@ API routes for nodes.
|
||||
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import ipaddress
|
||||
|
||||
from fastapi import APIRouter, Depends, WebSocket, WebSocketDisconnect, Request, Response, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
@ -28,13 +29,18 @@ from typing import List, Callable
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.controller import Controller
|
||||
from gns3server.config import Config
|
||||
from gns3server.controller.node import Node
|
||||
from gns3server.controller.project import Project
|
||||
from gns3server.utils import force_unix_path
|
||||
from gns3server.utils.http_client import HTTPClient
|
||||
from gns3server.controller.controller_error import ControllerForbiddenError, ControllerBadRequestError
|
||||
from gns3server.db.repositories.rbac import RbacRepository
|
||||
from gns3server import schemas
|
||||
|
||||
from .dependencies.database import get_repository
|
||||
from .dependencies.rbac import has_privilege, has_privilege_on_websocket
|
||||
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@ -107,10 +113,13 @@ async def dep_node(node_id: UUID, project: Project = Depends(dep_project)) -> No
|
||||
404: {"model": schemas.ErrorMessage, "description": "Could not find project"},
|
||||
409: {"model": schemas.ErrorMessage, "description": "Could not create node"},
|
||||
},
|
||||
dependencies=[Depends(has_privilege("Node.Allocate"))]
|
||||
)
|
||||
async def create_node(node_data: schemas.NodeCreate, project: Project = Depends(dep_project)) -> schemas.Node:
|
||||
"""
|
||||
Create a new node.
|
||||
|
||||
Required privilege: Node.Allocate
|
||||
"""
|
||||
|
||||
controller = Controller.instance()
|
||||
@ -120,65 +129,92 @@ async def create_node(node_data: schemas.NodeCreate, project: Project = Depends(
|
||||
return node.asdict()
|
||||
|
||||
|
||||
@router.get("", response_model=List[schemas.Node], response_model_exclude_unset=True)
|
||||
async def get_nodes(project: Project = Depends(dep_project)) -> List[schemas.Node]:
|
||||
@router.get(
|
||||
"",
|
||||
response_model=List[schemas.Node],
|
||||
response_model_exclude_unset=True,
|
||||
dependencies=[Depends(has_privilege("Node.Audit"))]
|
||||
)
|
||||
def get_nodes(project: Project = Depends(dep_project)) -> List[schemas.Node]:
|
||||
"""
|
||||
Return all nodes belonging to a given project.
|
||||
|
||||
Required privilege: Node.Audit
|
||||
"""
|
||||
|
||||
if project.status == "closed":
|
||||
# allow to retrieve nodes from a closed project
|
||||
return project.nodes.values()
|
||||
return [v.asdict() for v in project.nodes.values()]
|
||||
|
||||
|
||||
@router.post("/start", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post("/start", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(has_privilege("Node.PowerMgmt"))])
|
||||
async def start_all_nodes(project: Project = Depends(dep_project)) -> None:
|
||||
"""
|
||||
Start all nodes belonging to a given project.
|
||||
|
||||
Required privilege: Node.PowerMgmt
|
||||
"""
|
||||
|
||||
await project.start_all()
|
||||
|
||||
|
||||
@router.post("/stop", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post("/stop", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(has_privilege("Node.PowerMgmt"))])
|
||||
async def stop_all_nodes(project: Project = Depends(dep_project)) -> None:
|
||||
"""
|
||||
Stop all nodes belonging to a given project.
|
||||
|
||||
Required privilege: Node.PowerMgmt
|
||||
"""
|
||||
|
||||
await project.stop_all()
|
||||
|
||||
|
||||
@router.post("/suspend", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post("/suspend", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(has_privilege("Node.PowerMgmt"))])
|
||||
async def suspend_all_nodes(project: Project = Depends(dep_project)) -> None:
|
||||
"""
|
||||
Suspend all nodes belonging to a given project.
|
||||
|
||||
Required privilege: Node.PowerMgmt
|
||||
"""
|
||||
|
||||
await project.suspend_all()
|
||||
|
||||
|
||||
@router.post("/reload", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post("/reload", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(has_privilege("Node.PowerMgmt"))])
|
||||
async def reload_all_nodes(project: Project = Depends(dep_project)) -> None:
|
||||
"""
|
||||
Reload all nodes belonging to a given project.
|
||||
|
||||
Required privilege: Node.PowerMgmt
|
||||
"""
|
||||
|
||||
await project.stop_all()
|
||||
await project.start_all()
|
||||
|
||||
|
||||
@router.get("/{node_id}", response_model=schemas.Node)
|
||||
@router.get("/{node_id}", response_model=schemas.Node, dependencies=[Depends(has_privilege("Node.Audit"))])
|
||||
def get_node(node: Node = Depends(dep_node)) -> schemas.Node:
|
||||
"""
|
||||
Return a node from a given project.
|
||||
|
||||
Required privilege: Node.Audit
|
||||
"""
|
||||
|
||||
return node.asdict()
|
||||
|
||||
|
||||
@router.put("/{node_id}", response_model=schemas.Node, response_model_exclude_unset=True)
|
||||
@router.put(
|
||||
"/{node_id}",
|
||||
response_model=schemas.Node,
|
||||
response_model_exclude_unset=True,
|
||||
dependencies=[Depends(has_privilege("Node.Modify"))]
|
||||
)
|
||||
async def update_node(node_data: schemas.NodeUpdate, node: Node = Depends(dep_node)) -> schemas.Node:
|
||||
"""
|
||||
Update a node.
|
||||
|
||||
Required privilege: Node.Modify
|
||||
"""
|
||||
|
||||
node_data = jsonable_encoder(node_data, exclude_unset=True)
|
||||
@ -196,85 +232,142 @@ async def update_node(node_data: schemas.NodeUpdate, node: Node = Depends(dep_no
|
||||
"/{node_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={**responses, 409: {"model": schemas.ErrorMessage, "description": "Cannot delete node"}},
|
||||
dependencies=[Depends(has_privilege("Node.Allocate"))]
|
||||
)
|
||||
async def delete_node(node_id: UUID, project: Project = Depends(dep_project)) -> None:
|
||||
async def delete_node(
|
||||
node_id: UUID, project: Project = Depends(dep_project),
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
|
||||
) -> None:
|
||||
"""
|
||||
Delete a node from a project.
|
||||
|
||||
Required privilege: Node.Allocate
|
||||
"""
|
||||
|
||||
await project.delete_node(str(node_id))
|
||||
await rbac_repo.delete_all_ace_starting_with_path(f"/projects/{project.id}/nodes/{node_id}")
|
||||
|
||||
|
||||
@router.post("/{node_id}/duplicate", response_model=schemas.Node, status_code=status.HTTP_201_CREATED)
|
||||
@router.post(
|
||||
"/{node_id}/duplicate",
|
||||
response_model=schemas.Node,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
dependencies=[Depends(has_privilege("Node.Allocate"))]
|
||||
)
|
||||
async def duplicate_node(duplicate_data: schemas.NodeDuplicate, node: Node = Depends(dep_node)) -> schemas.Node:
|
||||
"""
|
||||
Duplicate a node.
|
||||
|
||||
Required privilege: Node.Allocate
|
||||
"""
|
||||
|
||||
new_node = await node.project.duplicate_node(node, duplicate_data.x, duplicate_data.y, duplicate_data.z)
|
||||
return new_node.asdict()
|
||||
|
||||
|
||||
@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/start",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(has_privilege("Node.PowerMgmt"))]
|
||||
)
|
||||
async def start_node(start_data: dict, node: Node = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Start a node.
|
||||
|
||||
Required privilege: Node.PowerMgmt
|
||||
"""
|
||||
|
||||
await node.start(data=start_data)
|
||||
|
||||
|
||||
@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/stop",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(has_privilege("Node.PowerMgmt"))]
|
||||
)
|
||||
async def stop_node(node: Node = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Stop a node.
|
||||
|
||||
Required privilege: Node.PowerMgmt
|
||||
"""
|
||||
|
||||
await node.stop()
|
||||
|
||||
|
||||
@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/suspend",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(has_privilege("Node.PowerMgmt"))]
|
||||
)
|
||||
async def suspend_node(node: Node = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Suspend a node.
|
||||
|
||||
Required privilege: Node.PowerMgmt
|
||||
"""
|
||||
|
||||
await node.suspend()
|
||||
|
||||
|
||||
@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/reload",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(has_privilege("Node.PowerMgmt"))]
|
||||
)
|
||||
async def reload_node(node: Node = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Reload a node.
|
||||
|
||||
Required privilege: Node.PowerMgmt
|
||||
"""
|
||||
|
||||
await node.reload()
|
||||
|
||||
|
||||
@router.post("/{node_id}/isolate", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/isolate",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(has_privilege("Link.Modify"))]
|
||||
)
|
||||
async def isolate_node(node: Node = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Isolate a node (suspend all attached links).
|
||||
|
||||
Required privilege: Link.Modify
|
||||
"""
|
||||
|
||||
for link in node.links:
|
||||
await link.update_suspend(True)
|
||||
|
||||
|
||||
@router.post("/{node_id}/unisolate", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/unisolate",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(has_privilege("Link.Modify"))]
|
||||
)
|
||||
async def unisolate_node(node: Node = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Un-isolate a node (resume all attached suspended links).
|
||||
|
||||
Required privilege: Link.Modify
|
||||
"""
|
||||
|
||||
for link in node.links:
|
||||
await link.update_suspend(False)
|
||||
|
||||
|
||||
@router.get("/{node_id}/links", response_model=List[schemas.Link], response_model_exclude_unset=True)
|
||||
@router.get(
|
||||
"/{node_id}/links",
|
||||
response_model=List[schemas.Link],
|
||||
response_model_exclude_unset=True,
|
||||
dependencies = [Depends(has_privilege("Link.Audit"))]
|
||||
)
|
||||
async def get_node_links(node: Node = Depends(dep_node)) -> List[schemas.Link]:
|
||||
"""
|
||||
Return all the links connected to a node.
|
||||
|
||||
Required privilege: Link.Audit
|
||||
"""
|
||||
|
||||
links = []
|
||||
@ -283,10 +376,12 @@ async def get_node_links(node: Node = Depends(dep_node)) -> List[schemas.Link]:
|
||||
return links
|
||||
|
||||
|
||||
@router.get("/{node_id}/dynamips/auto_idlepc")
|
||||
async def auto_idlepc(node: Node = Depends(dep_node)) -> str:
|
||||
@router.get("/{node_id}/dynamips/auto_idlepc", dependencies=[Depends(has_privilege("Node.Audit"))])
|
||||
async def auto_idlepc(node: Node = Depends(dep_node)) -> dict:
|
||||
"""
|
||||
Compute an Idle-PC value for a Dynamips node
|
||||
|
||||
Required privilege: Node.Audit
|
||||
"""
|
||||
|
||||
if node.node_type != "dynamips":
|
||||
@ -294,10 +389,12 @@ async def auto_idlepc(node: Node = Depends(dep_node)) -> str:
|
||||
return await node.dynamips_auto_idlepc()
|
||||
|
||||
|
||||
@router.get("/{node_id}/dynamips/idlepc_proposals")
|
||||
@router.get("/{node_id}/dynamips/idlepc_proposals", dependencies=[Depends(has_privilege("Node.Audit"))])
|
||||
async def idlepc_proposals(node: Node = Depends(dep_node)) -> List[str]:
|
||||
"""
|
||||
Compute a list of potential idle-pc values for a Dynamips node
|
||||
|
||||
Required privilege: Node.Audit
|
||||
"""
|
||||
|
||||
if node.node_type != "dynamips":
|
||||
@ -305,7 +402,11 @@ async def idlepc_proposals(node: Node = Depends(dep_node)) -> List[str]:
|
||||
return await node.dynamips_idlepc_proposals()
|
||||
|
||||
|
||||
@router.post("/{node_id}/qemu/disk_image/{disk_name}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/qemu/disk_image/{disk_name}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(has_privilege("Node.Allocate"))]
|
||||
)
|
||||
async def create_disk_image(
|
||||
disk_name: str,
|
||||
disk_data: schemas.QemuDiskImageCreate,
|
||||
@ -313,14 +414,20 @@ async def create_disk_image(
|
||||
) -> None:
|
||||
"""
|
||||
Create a Qemu disk image.
|
||||
|
||||
Required privilege: Node.Allocate
|
||||
"""
|
||||
|
||||
if node.node_type != "qemu":
|
||||
raise ControllerBadRequestError("Creating a disk image is only supported on a Qemu node")
|
||||
await node.post(f"/disk_image/{disk_name}", data=disk_data.dict(exclude_unset=True))
|
||||
await node.post(f"/disk_image/{disk_name}", data=disk_data.model_dump(exclude_unset=True))
|
||||
|
||||
|
||||
@router.put("/{node_id}/qemu/disk_image/{disk_name}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.put(
|
||||
"/{node_id}/qemu/disk_image/{disk_name}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(has_privilege("Node.Allocate"))]
|
||||
)
|
||||
async def update_disk_image(
|
||||
disk_name: str,
|
||||
disk_data: schemas.QemuDiskImageUpdate,
|
||||
@ -328,20 +435,28 @@ async def update_disk_image(
|
||||
) -> None:
|
||||
"""
|
||||
Update a Qemu disk image.
|
||||
|
||||
Required privilege: Node.Allocate
|
||||
"""
|
||||
|
||||
if node.node_type != "qemu":
|
||||
raise ControllerBadRequestError("Updating a disk image is only supported on a Qemu node")
|
||||
await node.put(f"/disk_image/{disk_name}", data=disk_data.dict(exclude_unset=True))
|
||||
await node.put(f"/disk_image/{disk_name}", data=disk_data.model_dump(exclude_unset=True))
|
||||
|
||||
|
||||
@router.delete("/{node_id}/qemu/disk_image/{disk_name}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.delete(
|
||||
"/{node_id}/qemu/disk_image/{disk_name}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(has_privilege("Node.Allocate"))]
|
||||
)
|
||||
async def delete_disk_image(
|
||||
disk_name: str,
|
||||
node: Node = Depends(dep_node)
|
||||
) -> None:
|
||||
"""
|
||||
Delete a Qemu disk image.
|
||||
|
||||
Required privilege: Node.Allocate
|
||||
"""
|
||||
|
||||
if node.node_type != "qemu":
|
||||
@ -349,10 +464,12 @@ async def delete_disk_image(
|
||||
await node.delete(f"/disk_image/{disk_name}")
|
||||
|
||||
|
||||
@router.get("/{node_id}/files/{file_path:path}")
|
||||
@router.get("/{node_id}/files/{file_path:path}", dependencies=[Depends(has_privilege("Node.Audit"))])
|
||||
async def get_file(file_path: str, node: Node = Depends(dep_node)) -> Response:
|
||||
"""
|
||||
Return a file in the node directory
|
||||
Return a file from the node directory.
|
||||
|
||||
Required privilege: Node.Audit
|
||||
"""
|
||||
|
||||
path = force_unix_path(file_path)
|
||||
@ -368,10 +485,16 @@ async def get_file(file_path: str, node: Node = Depends(dep_node)) -> Response:
|
||||
return Response(res.body, media_type="application/octet-stream", status_code=res.status)
|
||||
|
||||
|
||||
@router.post("/{node_id}/files/{file_path:path}", status_code=status.HTTP_201_CREATED)
|
||||
@router.post(
|
||||
"/{node_id}/files/{file_path:path}",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
dependencies=[Depends(has_privilege("Node.Modify"))]
|
||||
)
|
||||
async def post_file(file_path: str, request: Request, node: Node = Depends(dep_node)):
|
||||
"""
|
||||
Write a file in the node directory.
|
||||
|
||||
Required privilege: Node.Modify
|
||||
"""
|
||||
|
||||
path = force_unix_path(file_path)
|
||||
@ -389,18 +512,36 @@ async def post_file(file_path: str, request: Request, node: Node = Depends(dep_n
|
||||
|
||||
|
||||
@router.websocket("/{node_id}/console/ws")
|
||||
async def ws_console(websocket: WebSocket, node: Node = Depends(dep_node)) -> None:
|
||||
async def ws_console(
|
||||
websocket: WebSocket,
|
||||
current_user: schemas.User = Depends(has_privilege_on_websocket("Node.Console")),
|
||||
node: Node = Depends(dep_node)
|
||||
) -> None:
|
||||
"""
|
||||
WebSocket console.
|
||||
|
||||
Required privilege: Node.Console
|
||||
"""
|
||||
|
||||
if current_user is None:
|
||||
return
|
||||
|
||||
compute = node.compute
|
||||
await websocket.accept()
|
||||
log.info(
|
||||
f"New client {websocket.client.host}:{websocket.client.port} has connected to controller console WebSocket"
|
||||
)
|
||||
|
||||
compute_host = compute.host
|
||||
try:
|
||||
# handle IPv6 address
|
||||
ip = ipaddress.ip_address(compute_host)
|
||||
if isinstance(ip, ipaddress.IPv6Address):
|
||||
compute_host = '[' + compute_host + ']'
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
ws_console_compute_url = (
|
||||
f"ws://{compute.host}:{compute.port}/v3/compute/projects/"
|
||||
f"{websocket.url.scheme}://{compute_host}:{compute.port}/v3/compute/projects/"
|
||||
f"{node.project.id}/{node.node_type}/nodes/{node.id}/console/ws"
|
||||
)
|
||||
|
||||
@ -423,9 +564,21 @@ async def ws_console(websocket: WebSocket, node: Node = Depends(dep_node)) -> No
|
||||
|
||||
try:
|
||||
# receive WebSocket data from compute console WebSocket and forward to client.
|
||||
async with HTTPClient.get_client().ws_connect(ws_console_compute_url) as ws_console_compute:
|
||||
asyncio.ensure_future(ws_receive(ws_console_compute))
|
||||
async for msg in ws_console_compute:
|
||||
log.info(f"Forwarding console WebSocket to '{ws_console_compute_url}'")
|
||||
server_config = Config.instance().settings.Server
|
||||
user = server_config.compute_username
|
||||
password = server_config.compute_password
|
||||
if not user:
|
||||
raise ControllerForbiddenError("Compute username is not set")
|
||||
user = user.strip()
|
||||
if user and password:
|
||||
auth = aiohttp.BasicAuth(user, password.get_secret_value(), "utf-8")
|
||||
else:
|
||||
auth = aiohttp.BasicAuth(user, "")
|
||||
ssl_context = Controller.instance().ssl_context()
|
||||
async with HTTPClient.get_client().ws_connect(ws_console_compute_url, auth=auth, ssl_context=ssl_context) as ws:
|
||||
asyncio.ensure_future(ws_receive(ws))
|
||||
async for msg in ws:
|
||||
if msg.type == aiohttp.WSMsgType.TEXT:
|
||||
await websocket.send_text(msg.data)
|
||||
elif msg.type == aiohttp.WSMsgType.BINARY:
|
||||
@ -436,16 +589,31 @@ async def ws_console(websocket: WebSocket, node: Node = Depends(dep_node)) -> No
|
||||
log.error(f"Client error received when forwarding to compute console WebSocket: {e}")
|
||||
|
||||
|
||||
@router.post("/console/reset", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/console/reset",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(has_privilege("Node.Console"))]
|
||||
)
|
||||
async def reset_console_all_nodes(project: Project = Depends(dep_project)) -> None:
|
||||
"""
|
||||
Reset console for all nodes belonging to the project.
|
||||
|
||||
Required privilege: Node.Console
|
||||
"""
|
||||
|
||||
await project.reset_console_all()
|
||||
|
||||
|
||||
@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{node_id}/console/reset",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(has_privilege("Node.Console"))]
|
||||
)
|
||||
async def console_reset(node: Node = Depends(dep_node)) -> None:
|
||||
"""
|
||||
Reset a console for a given node.
|
||||
|
||||
Required privilege: Node.Console
|
||||
"""
|
||||
|
||||
await node.post("/console/reset")
|
||||
|
@ -1,78 +0,0 @@
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API routes for controller notifications.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, WebSocket, WebSocketDisconnect
|
||||
from fastapi.responses import StreamingResponse
|
||||
from websockets.exceptions import ConnectionClosed, WebSocketException
|
||||
|
||||
from gns3server.controller import Controller
|
||||
from gns3server import schemas
|
||||
|
||||
from .dependencies.authentication import get_current_active_user, get_current_active_user_from_websocket
|
||||
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("", dependencies=[Depends(get_current_active_user)])
|
||||
async def controller_http_notifications() -> StreamingResponse:
|
||||
"""
|
||||
Receive controller notifications about the controller from HTTP stream.
|
||||
"""
|
||||
|
||||
async def event_stream():
|
||||
with Controller.instance().notification.controller_queue() as queue:
|
||||
while True:
|
||||
msg = await queue.get_json(5)
|
||||
yield f"{msg}\n".encode("utf-8")
|
||||
|
||||
return StreamingResponse(event_stream(), media_type="application/json")
|
||||
|
||||
|
||||
@router.websocket("/ws")
|
||||
async def controller_ws_notifications(
|
||||
websocket: WebSocket,
|
||||
current_user: schemas.User = Depends(get_current_active_user_from_websocket)
|
||||
) -> None:
|
||||
"""
|
||||
Receive project notifications about the controller from WebSocket.
|
||||
"""
|
||||
|
||||
if current_user is None:
|
||||
return
|
||||
|
||||
log.info(f"New client {websocket.client.host}:{websocket.client.port} has connected to controller WebSocket")
|
||||
try:
|
||||
with Controller.instance().notification.controller_queue() as queue:
|
||||
while True:
|
||||
notification = await queue.get_json(5)
|
||||
await websocket.send_text(notification)
|
||||
except (ConnectionClosed, WebSocketDisconnect):
|
||||
log.info(f"Client {websocket.client.host}:{websocket.client.port} has disconnected from controller WebSocket")
|
||||
except WebSocketException as e:
|
||||
log.warning(f"Error while sending to controller event to WebSocket client: {e}")
|
||||
finally:
|
||||
try:
|
||||
await websocket.close()
|
||||
except OSError:
|
||||
pass # ignore OSError: [Errno 107] Transport endpoint is not connected
|
@ -1,161 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2021 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API routes for permissions.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from fastapi import APIRouter, Depends, Response, Request, status
|
||||
from fastapi.routing import APIRoute
|
||||
from uuid import UUID
|
||||
from typing import List
|
||||
|
||||
|
||||
from gns3server import schemas
|
||||
from gns3server.controller.controller_error import (
|
||||
ControllerBadRequestError,
|
||||
ControllerNotFoundError,
|
||||
ControllerForbiddenError,
|
||||
)
|
||||
|
||||
from gns3server.db.repositories.rbac import RbacRepository
|
||||
from .dependencies.database import get_repository
|
||||
from .dependencies.authentication import get_current_active_user
|
||||
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("", response_model=List[schemas.Permission])
|
||||
async def get_permissions(
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
|
||||
) -> List[schemas.Permission]:
|
||||
"""
|
||||
Get all permissions.
|
||||
"""
|
||||
|
||||
return await rbac_repo.get_permissions()
|
||||
|
||||
|
||||
@router.post("", response_model=schemas.Permission, status_code=status.HTTP_201_CREATED)
|
||||
async def create_permission(
|
||||
request: Request,
|
||||
permission_create: schemas.PermissionCreate,
|
||||
current_user: schemas.User = Depends(get_current_active_user),
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
|
||||
) -> schemas.Permission:
|
||||
"""
|
||||
Create a new permission.
|
||||
"""
|
||||
|
||||
# TODO: should we prevent having multiple permissions with same methods/path?
|
||||
#if await rbac_repo.check_permission_exists(permission_create):
|
||||
# raise ControllerBadRequestError(f"Permission '{permission_create.methods} {permission_create.path} "
|
||||
# f"{permission_create.action}' already exists")
|
||||
|
||||
for route in request.app.routes:
|
||||
if isinstance(route, APIRoute):
|
||||
|
||||
# remove the prefix (e.g. "/v3") from the route path
|
||||
route_path = re.sub(r"^/v[0-9]", "", route.path)
|
||||
# replace route path ID parameters by an UUID regex
|
||||
route_path = re.sub(r"{\w+_id}", "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}", route_path)
|
||||
# replace remaining route path parameters by an word matching regex
|
||||
route_path = re.sub(r"/{[\w:]+}", r"/\\w+", route_path)
|
||||
|
||||
# the permission can match multiple routes
|
||||
if permission_create.path.endswith("/*"):
|
||||
route_path += r"/.*"
|
||||
|
||||
if re.fullmatch(route_path, permission_create.path):
|
||||
for method in permission_create.methods:
|
||||
if method in list(route.methods):
|
||||
# check user has the right to add the permission (i.e has already to right on the path)
|
||||
if not await rbac_repo.check_user_is_authorized(current_user.user_id, method, permission_create.path):
|
||||
raise ControllerForbiddenError(f"User '{current_user.username}' doesn't have the rights to "
|
||||
f"add a permission on {method} {permission_create.path} or "
|
||||
f"the endpoint doesn't exist")
|
||||
return await rbac_repo.create_permission(permission_create)
|
||||
|
||||
raise ControllerBadRequestError(f"Permission '{permission_create.methods} {permission_create.path}' "
|
||||
f"doesn't match any existing endpoint")
|
||||
|
||||
|
||||
@router.get("/{permission_id}", response_model=schemas.Permission)
|
||||
async def get_permission(
|
||||
permission_id: UUID,
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
|
||||
) -> schemas.Permission:
|
||||
"""
|
||||
Get a permission.
|
||||
"""
|
||||
|
||||
permission = await rbac_repo.get_permission(permission_id)
|
||||
if not permission:
|
||||
raise ControllerNotFoundError(f"Permission '{permission_id}' not found")
|
||||
return permission
|
||||
|
||||
|
||||
@router.put("/{permission_id}", response_model=schemas.Permission)
|
||||
async def update_permission(
|
||||
permission_id: UUID,
|
||||
permission_update: schemas.PermissionUpdate,
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
|
||||
) -> schemas.Permission:
|
||||
"""
|
||||
Update a permission.
|
||||
"""
|
||||
|
||||
permission = await rbac_repo.get_permission(permission_id)
|
||||
if not permission:
|
||||
raise ControllerNotFoundError(f"Permission '{permission_id}' not found")
|
||||
|
||||
return await rbac_repo.update_permission(permission_id, permission_update)
|
||||
|
||||
|
||||
@router.delete("/{permission_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_permission(
|
||||
permission_id: UUID,
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
|
||||
) -> None:
|
||||
"""
|
||||
Delete a permission.
|
||||
"""
|
||||
|
||||
permission = await rbac_repo.get_permission(permission_id)
|
||||
if not permission:
|
||||
raise ControllerNotFoundError(f"Permission '{permission_id}' not found")
|
||||
|
||||
success = await rbac_repo.delete_permission(permission_id)
|
||||
if not success:
|
||||
raise ControllerNotFoundError(f"Permission '{permission_id}' could not be deleted")
|
||||
|
||||
|
||||
@router.post("/prune", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def prune_permissions(
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
|
||||
) -> None:
|
||||
"""
|
||||
Prune orphaned permissions.
|
||||
"""
|
||||
|
||||
await rbac_repo.prune_permissions()
|
239
gns3server/api/routes/controller/pools.py
Normal file
239
gns3server/api/routes/controller/pools.py
Normal file
@ -0,0 +1,239 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2023 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
API routes for resource pools.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, status
|
||||
from uuid import UUID
|
||||
from typing import List
|
||||
|
||||
from gns3server import schemas
|
||||
from gns3server.controller.controller_error import (
|
||||
ControllerError,
|
||||
ControllerBadRequestError,
|
||||
ControllerNotFoundError
|
||||
)
|
||||
|
||||
from gns3server.controller import Controller
|
||||
from gns3server.db.repositories.rbac import RbacRepository
|
||||
from gns3server.db.repositories.pools import ResourcePoolsRepository
|
||||
|
||||
from .dependencies.rbac import has_privilege
|
||||
from .dependencies.database import get_repository
|
||||
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get(
|
||||
"",
|
||||
response_model=List[schemas.ResourcePool],
|
||||
dependencies=[Depends(has_privilege("Pool.Audit"))]
|
||||
)
|
||||
async def get_resource_pools(
|
||||
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository))
|
||||
) -> List[schemas.ResourcePool]:
|
||||
"""
|
||||
Get all resource pools.
|
||||
|
||||
Required privilege: Pool.Audit
|
||||
"""
|
||||
|
||||
return await pools_repo.get_resource_pools()
|
||||
|
||||
|
||||
@router.post(
|
||||
"",
|
||||
response_model=schemas.ResourcePool,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
dependencies=[Depends(has_privilege("Pool.Allocate"))]
|
||||
)
|
||||
async def create_resource_pool(
|
||||
resource_pool_create: schemas.ResourcePoolCreate,
|
||||
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository))
|
||||
) -> schemas.ResourcePool:
|
||||
"""
|
||||
Create a new resource pool
|
||||
|
||||
Required privilege: Pool.Allocate
|
||||
"""
|
||||
|
||||
if await pools_repo.get_resource_pool_by_name(resource_pool_create.name):
|
||||
raise ControllerBadRequestError(f"Resource pool '{resource_pool_create.name}' already exists")
|
||||
|
||||
return await pools_repo.create_resource_pool(resource_pool_create)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{resource_pool_id}",
|
||||
response_model=schemas.ResourcePool,
|
||||
dependencies=[Depends(has_privilege("Pool.Audit"))]
|
||||
)
|
||||
async def get_resource_pool(
|
||||
resource_pool_id: UUID,
|
||||
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository))
|
||||
) -> schemas.ResourcePool:
|
||||
"""
|
||||
Get a resource pool.
|
||||
|
||||
Required privilege: Pool.Audit
|
||||
"""
|
||||
|
||||
resource_pool = await pools_repo.get_resource_pool(resource_pool_id)
|
||||
if not resource_pool:
|
||||
raise ControllerNotFoundError(f"Resource pool '{resource_pool_id}' not found")
|
||||
return resource_pool
|
||||
|
||||
|
||||
@router.put(
|
||||
"/{resource_pool_id}",
|
||||
response_model=schemas.ResourcePool,
|
||||
dependencies=[Depends(has_privilege("Pool.Modify"))]
|
||||
)
|
||||
async def update_resource_pool(
|
||||
resource_pool_id: UUID,
|
||||
resource_pool_update: schemas.ResourcePoolUpdate,
|
||||
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository))
|
||||
) -> schemas.ResourcePool:
|
||||
"""
|
||||
Update a resource pool.
|
||||
|
||||
Required privilege: Pool.Modify
|
||||
"""
|
||||
|
||||
resource_pool = await pools_repo.get_resource_pool(resource_pool_id)
|
||||
if not resource_pool:
|
||||
raise ControllerNotFoundError(f"Resource pool '{resource_pool_id}' not found")
|
||||
|
||||
return await pools_repo.update_resource_pool(resource_pool_id, resource_pool_update)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/{resource_pool_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(has_privilege("Pool.Allocate"))]
|
||||
)
|
||||
async def delete_resource_pool(
|
||||
resource_pool_id: UUID,
|
||||
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository)),
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
|
||||
) -> None:
|
||||
"""
|
||||
Delete a resource pool.
|
||||
|
||||
Required privilege: Pool.Allocate
|
||||
"""
|
||||
|
||||
resource_pool = await pools_repo.get_resource_pool(resource_pool_id)
|
||||
if not resource_pool:
|
||||
raise ControllerNotFoundError(f"Resource pool '{resource_pool_id}' not found")
|
||||
|
||||
success = await pools_repo.delete_resource_pool(resource_pool_id)
|
||||
if not success:
|
||||
raise ControllerError(f"Resource pool '{resource_pool_id}' could not be deleted")
|
||||
await rbac_repo.delete_all_ace_starting_with_path(f"/pools/{resource_pool_id}")
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{resource_pool_id}/resources",
|
||||
response_model=List[schemas.Resource],
|
||||
dependencies=[Depends(has_privilege("Pool.Audit"))]
|
||||
)
|
||||
async def get_pool_resources(
|
||||
resource_pool_id: UUID,
|
||||
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository)),
|
||||
) -> List[schemas.Resource]:
|
||||
"""
|
||||
Get all resource in a pool.
|
||||
|
||||
Required privilege: Pool.Audit
|
||||
"""
|
||||
|
||||
return await pools_repo.get_pool_resources(resource_pool_id)
|
||||
|
||||
|
||||
@router.put(
|
||||
"/{resource_pool_id}/resources/{resource_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(has_privilege("Pool.Modify"))]
|
||||
)
|
||||
async def add_resource_to_pool(
|
||||
resource_pool_id: UUID,
|
||||
resource_id: UUID,
|
||||
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository)),
|
||||
) -> None:
|
||||
"""
|
||||
Add resource to a resource pool.
|
||||
|
||||
Required privilege: Pool.Modify
|
||||
"""
|
||||
|
||||
resource_pool = await pools_repo.get_resource_pool(resource_pool_id)
|
||||
if not resource_pool:
|
||||
raise ControllerNotFoundError(f"Resource pool '{resource_pool_id}' not found")
|
||||
|
||||
# TODO: consider if a resource can belong to multiple pools
|
||||
resources = await pools_repo.get_pool_resources(resource_pool_id)
|
||||
for resource in resources:
|
||||
if resource.resource_id == resource_id:
|
||||
raise ControllerBadRequestError(f"Resource '{resource_id}' is already in '{resource_pool.name}'")
|
||||
|
||||
# we only support projects in resource pools for now
|
||||
project = Controller.instance().get_project(str(resource_id))
|
||||
|
||||
resource = await pools_repo.get_resource(resource_id)
|
||||
if not resource:
|
||||
# the resource is not in the database yet, create it
|
||||
resource_create = schemas.ResourceCreate(resource_id=resource_id, resource_type="project", name=project.name)
|
||||
resource = await pools_repo.create_resource(resource_create)
|
||||
|
||||
await pools_repo.add_resource_to_pool(resource_pool_id, resource)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/{resource_pool_id}/resources/{resource_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(has_privilege("Pool.Modify"))]
|
||||
)
|
||||
async def remove_resource_from_pool(
|
||||
resource_pool_id: UUID,
|
||||
resource_id: UUID,
|
||||
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository)),
|
||||
) -> None:
|
||||
"""
|
||||
Remove resource from a resource pool.
|
||||
|
||||
Required privilege: Pool.Modify
|
||||
"""
|
||||
|
||||
resource = await pools_repo.get_resource(resource_id)
|
||||
if not resource:
|
||||
raise ControllerNotFoundError(f"Resource '{resource_id}' not found")
|
||||
|
||||
resource_pool = await pools_repo.remove_resource_from_pool(resource_pool_id, resource)
|
||||
if not resource_pool:
|
||||
raise ControllerNotFoundError(f"Resource pool '{resource_pool_id}' not found")
|
||||
|
||||
# TODO: consider if a resource can belong to multiple pools
|
||||
success = await pools_repo.delete_resource(resource.resource_id)
|
||||
if not success:
|
||||
raise ControllerError(f"Resource '{resource_id}' could not be deleted")
|
43
gns3server/api/routes/controller/privileges.py
Normal file
43
gns3server/api/routes/controller/privileges.py
Normal file
@ -0,0 +1,43 @@
|
||||
#
|
||||
# Software Name : GNS3 server
|
||||
# Version: 3
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2023 Orange Business Services
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# This software is distributed under the GPL-3.0 or any later version,
|
||||
# the text of which is available at https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
# or see the "LICENSE" file for more details.
|
||||
#
|
||||
# Author: Sylvain MATHIEU
|
||||
#
|
||||
|
||||
"""
|
||||
API route for privileges
|
||||
"""
|
||||
|
||||
from typing import List
|
||||
from gns3server.db.repositories.rbac import RbacRepository
|
||||
from .dependencies.database import get_repository
|
||||
from fastapi import APIRouter, Depends
|
||||
import logging
|
||||
|
||||
from gns3server import schemas
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get(
|
||||
"",
|
||||
response_model=List[schemas.Privilege],
|
||||
)
|
||||
async def get_privileges(
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
|
||||
) -> List[schemas.Privilege]:
|
||||
"""
|
||||
Get all privileges.
|
||||
|
||||
Required privilege: None
|
||||
"""
|
||||
|
||||
return await rbac_repo.get_privileges()
|
@ -45,11 +45,13 @@ from gns3server.controller.import_project import import_project as import_contro
|
||||
from gns3server.controller.export_project import export_project as export_controller_project
|
||||
from gns3server.utils.asyncio import aiozipstream
|
||||
from gns3server.utils.path import is_safe_path
|
||||
from gns3server.db.repositories.rbac import RbacRepository
|
||||
from gns3server.db.repositories.templates import TemplatesRepository
|
||||
from gns3server.db.repositories.rbac import RbacRepository
|
||||
from gns3server.db.repositories.pools import ResourcePoolsRepository
|
||||
from gns3server.services.templates import TemplatesService
|
||||
|
||||
from .dependencies.authentication import get_current_active_user, get_current_active_user_from_websocket
|
||||
from .dependencies.rbac import has_privilege, has_privilege_on_websocket
|
||||
from .dependencies.authentication import get_current_active_user
|
||||
from .dependencies.database import get_repository
|
||||
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project"}}
|
||||
@ -66,29 +68,39 @@ def dep_project(project_id: UUID) -> Project:
|
||||
return project
|
||||
|
||||
|
||||
CHUNK_SIZE = 1024 * 8 # 8KB
|
||||
|
||||
|
||||
@router.get("", response_model=List[schemas.Project], response_model_exclude_unset=True)
|
||||
@router.get(
|
||||
"",
|
||||
response_model=List[schemas.Project],
|
||||
response_model_exclude_unset=True
|
||||
)
|
||||
async def get_projects(
|
||||
current_user: schemas.User = Depends(get_current_active_user),
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
|
||||
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository))
|
||||
) -> List[schemas.Project]:
|
||||
"""
|
||||
Return all projects.
|
||||
|
||||
Required privilege: Project.Audit
|
||||
"""
|
||||
|
||||
controller = Controller.instance()
|
||||
projects = []
|
||||
|
||||
if current_user.is_superadmin:
|
||||
# super admin sees all projects
|
||||
return [p.asdict() for p in controller.projects.values()]
|
||||
else:
|
||||
user_projects = []
|
||||
for project in controller.projects.values():
|
||||
authorized = await rbac_repo.check_user_is_authorized(
|
||||
current_user.user_id, "GET", f"/projects/{project.id}")
|
||||
if authorized:
|
||||
user_projects.append(project.asdict())
|
||||
return user_projects
|
||||
elif await rbac_repo.check_user_has_privilege(current_user.user_id, "/projects", "Project.Audit"):
|
||||
# user with Project.Audit privilege on '/projects' sees all projects except those in resource pools
|
||||
project_ids_in_pools = [str(r.resource_id) for r in await pools_repo.get_resources() if r.resource_type == "project"]
|
||||
projects.extend([p.asdict() for p in controller.projects.values() if p.id not in project_ids_in_pools])
|
||||
|
||||
# user with Project.Audit privilege on resource pools sees the projects in these pools
|
||||
user_pool_resources = await rbac_repo.get_user_pool_resources(current_user.user_id, "Project.Audit")
|
||||
project_ids_in_pools = [str(r.resource_id) for r in user_pool_resources if r.resource_type == "project"]
|
||||
projects.extend([p.asdict() for p in controller.projects.values() if p.id in project_ids_in_pools])
|
||||
|
||||
return projects
|
||||
|
||||
|
||||
@router.post(
|
||||
@ -97,63 +109,80 @@ async def get_projects(
|
||||
response_model=schemas.Project,
|
||||
response_model_exclude_unset=True,
|
||||
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create project"}},
|
||||
dependencies=[Depends(has_privilege("Project.Allocate"))]
|
||||
)
|
||||
async def create_project(
|
||||
project_data: schemas.ProjectCreate,
|
||||
current_user: schemas.User = Depends(get_current_active_user),
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
|
||||
) -> schemas.Project:
|
||||
"""
|
||||
Create a new project.
|
||||
|
||||
Required privilege: Project.Allocate
|
||||
"""
|
||||
|
||||
controller = Controller.instance()
|
||||
project = await controller.add_project(**jsonable_encoder(project_data, exclude_unset=True))
|
||||
await rbac_repo.add_permission_to_user_with_path(current_user.user_id, f"/projects/{project.id}/*")
|
||||
return project.asdict()
|
||||
|
||||
|
||||
@router.get("/{project_id}", response_model=schemas.Project)
|
||||
@router.get("/{project_id}", response_model=schemas.Project, dependencies=[Depends(has_privilege("Project.Audit"))])
|
||||
def get_project(project: Project = Depends(dep_project)) -> schemas.Project:
|
||||
"""
|
||||
Return a project.
|
||||
|
||||
Required privilege: Project.Audit
|
||||
"""
|
||||
|
||||
return project.asdict()
|
||||
|
||||
|
||||
@router.put("/{project_id}", response_model=schemas.Project, response_model_exclude_unset=True)
|
||||
@router.put(
|
||||
"/{project_id}",
|
||||
response_model=schemas.Project,
|
||||
response_model_exclude_unset=True,
|
||||
dependencies=[Depends(has_privilege("Project.Modify"))]
|
||||
)
|
||||
async def update_project(
|
||||
project_data: schemas.ProjectUpdate,
|
||||
project: Project = Depends(dep_project)
|
||||
) -> schemas.Project:
|
||||
"""
|
||||
Update a project.
|
||||
|
||||
Required privilege: Project.Modify
|
||||
"""
|
||||
|
||||
await project.update(**jsonable_encoder(project_data, exclude_unset=True))
|
||||
return project.asdict()
|
||||
|
||||
|
||||
@router.delete("/{project_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.delete(
|
||||
"/{project_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(has_privilege("Project.Allocate"))]
|
||||
)
|
||||
async def delete_project(
|
||||
project: Project = Depends(dep_project),
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
|
||||
) -> None:
|
||||
"""
|
||||
Delete a project.
|
||||
|
||||
Required privilege: Project.Allocate
|
||||
"""
|
||||
|
||||
controller = Controller.instance()
|
||||
await project.delete()
|
||||
controller.remove_project(project)
|
||||
await rbac_repo.delete_all_permissions_with_path(f"/projects/{project.id}")
|
||||
await rbac_repo.delete_all_ace_starting_with_path(f"/projects/{project.id}")
|
||||
|
||||
|
||||
@router.get("/{project_id}/stats")
|
||||
@router.get("/{project_id}/stats", dependencies=[Depends(has_privilege("Project.Audit"))])
|
||||
def get_project_stats(project: Project = Depends(dep_project)) -> dict:
|
||||
"""
|
||||
Return a project statistics.
|
||||
|
||||
Required privilege: Project.Audit
|
||||
"""
|
||||
|
||||
return project.stats()
|
||||
@ -163,10 +192,13 @@ def get_project_stats(project: Project = Depends(dep_project)) -> dict:
|
||||
"/{project_id}/close",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={**responses, 409: {"model": schemas.ErrorMessage, "description": "Could not close project"}},
|
||||
dependencies=[Depends(has_privilege("Project.Allocate"))]
|
||||
)
|
||||
async def close_project(project: Project = Depends(dep_project)) -> None:
|
||||
"""
|
||||
Close a project.
|
||||
|
||||
Required privilege: Project.Allocate
|
||||
"""
|
||||
|
||||
await project.close()
|
||||
@ -177,10 +209,13 @@ async def close_project(project: Project = Depends(dep_project)) -> None:
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.Project,
|
||||
responses={**responses, 409: {"model": schemas.ErrorMessage, "description": "Could not open project"}},
|
||||
dependencies=[Depends(has_privilege("Project.Allocate"))]
|
||||
)
|
||||
async def open_project(project: Project = Depends(dep_project)) -> schemas.Project:
|
||||
"""
|
||||
Open a project.
|
||||
|
||||
Required privilege: Project.Allocate
|
||||
"""
|
||||
|
||||
await project.open()
|
||||
@ -192,10 +227,13 @@ async def open_project(project: Project = Depends(dep_project)) -> schemas.Proje
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.Project,
|
||||
responses={**responses, 409: {"model": schemas.ErrorMessage, "description": "Could not load project"}},
|
||||
dependencies=[Depends(has_privilege("Project.Allocate"))]
|
||||
)
|
||||
async def load_project(path: str = Body(..., embed=True)) -> schemas.Project:
|
||||
"""
|
||||
Load a project (local server only).
|
||||
|
||||
Required privilege: Project.Allocate
|
||||
"""
|
||||
|
||||
controller = Controller.instance()
|
||||
@ -204,24 +242,27 @@ async def load_project(path: str = Body(..., embed=True)) -> schemas.Project:
|
||||
return project.asdict()
|
||||
|
||||
|
||||
@router.get("/{project_id}/notifications")
|
||||
@router.get("/{project_id}/notifications", dependencies=[Depends(has_privilege("Project.Audit"))])
|
||||
async def project_http_notifications(project_id: UUID) -> StreamingResponse:
|
||||
"""
|
||||
Receive project notifications about the controller from HTTP stream.
|
||||
|
||||
Required privilege: Project.Audit
|
||||
"""
|
||||
|
||||
from gns3server.api.server import app
|
||||
controller = Controller.instance()
|
||||
project = controller.get_project(str(project_id))
|
||||
|
||||
log.info(f"New client has connected to the notification stream for project ID '{project.id}' (HTTP steam method)")
|
||||
log.info(f"New client has connected to the notification stream for project ID '{project.id}' (HTTP stream method)")
|
||||
|
||||
async def event_stream():
|
||||
|
||||
try:
|
||||
with controller.notification.project_queue(project.id) as queue:
|
||||
while True:
|
||||
while not app.state.exiting:
|
||||
msg = await queue.get_json(5)
|
||||
yield (f"{msg}\n").encode("utf-8")
|
||||
yield f"{msg}\n".encode("utf-8")
|
||||
finally:
|
||||
log.info(f"Client has disconnected from notification for project ID '{project.id}' (HTTP stream method)")
|
||||
if project.auto_close:
|
||||
@ -239,10 +280,12 @@ async def project_http_notifications(project_id: UUID) -> StreamingResponse:
|
||||
async def project_ws_notifications(
|
||||
project_id: UUID,
|
||||
websocket: WebSocket,
|
||||
current_user: schemas.User = Depends(get_current_active_user_from_websocket)
|
||||
current_user: schemas.User = Depends(has_privilege_on_websocket("Project.Audit"))
|
||||
) -> None:
|
||||
"""
|
||||
Receive project notifications about the controller from WebSocket.
|
||||
|
||||
Required privilege: Project.Audit
|
||||
"""
|
||||
|
||||
if current_user is None:
|
||||
@ -262,10 +305,6 @@ async def project_ws_notifications(
|
||||
except WebSocketException as e:
|
||||
log.warning(f"Error while sending to project event to WebSocket client: {e}")
|
||||
finally:
|
||||
try:
|
||||
await websocket.close()
|
||||
except OSError:
|
||||
pass # ignore OSError: [Errno 107] Transport endpoint is not connected
|
||||
if project.auto_close:
|
||||
# To avoid trouble with client connecting disconnecting we sleep few seconds before checking
|
||||
# if someone else is not connected
|
||||
@ -275,17 +314,20 @@ async def project_ws_notifications(
|
||||
await project.close()
|
||||
|
||||
|
||||
@router.get("/{project_id}/export")
|
||||
@router.get("/{project_id}/export", dependencies=[Depends(has_privilege("Project.Audit"))])
|
||||
async def export_project(
|
||||
project: Project = Depends(dep_project),
|
||||
include_snapshots: bool = False,
|
||||
include_images: bool = False,
|
||||
reset_mac_addresses: bool = False,
|
||||
keep_compute_ids: bool = False,
|
||||
compression: schemas.ProjectCompression = "zstd",
|
||||
compression_level: int = None,
|
||||
) -> StreamingResponse:
|
||||
"""
|
||||
Export a project as a portable archive.
|
||||
|
||||
Required privilege: Project.Audit
|
||||
"""
|
||||
|
||||
compression_query = compression.lower()
|
||||
@ -325,6 +367,7 @@ async def export_project(
|
||||
tmpdir,
|
||||
include_snapshots=include_snapshots,
|
||||
include_images=include_images,
|
||||
keep_compute_ids=keep_compute_ids,
|
||||
reset_mac_addresses=reset_mac_addresses,
|
||||
)
|
||||
async for chunk in zstream:
|
||||
@ -332,7 +375,7 @@ async def export_project(
|
||||
|
||||
log.info(f"Project '{project.name}' exported in {time.time() - begin:.4f} seconds")
|
||||
|
||||
# Will be raise if you have no space left or permission issue on your temporary directory
|
||||
# Will be raised if you have no space left or permission issue on your temporary directory
|
||||
# RuntimeError: something was wrong during the zip process
|
||||
except (ValueError, OSError, RuntimeError) as e:
|
||||
raise ConnectionError(f"Cannot export project: {e}")
|
||||
@ -341,7 +384,12 @@ async def export_project(
|
||||
return StreamingResponse(streamer(), media_type="application/gns3project", headers=headers)
|
||||
|
||||
|
||||
@router.post("/{project_id}/import", status_code=status.HTTP_201_CREATED, response_model=schemas.Project)
|
||||
@router.post(
|
||||
"/{project_id}/import",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.Project,
|
||||
dependencies=[Depends(has_privilege("Project.Allocate"))]
|
||||
)
|
||||
async def import_project(
|
||||
project_id: UUID,
|
||||
request: Request,
|
||||
@ -349,6 +397,8 @@ async def import_project(
|
||||
) -> schemas.Project:
|
||||
"""
|
||||
Import a project from a portable archive.
|
||||
|
||||
Required privilege: Project.Allocate
|
||||
"""
|
||||
|
||||
controller = Controller.instance()
|
||||
@ -376,56 +426,82 @@ async def import_project(
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.Project,
|
||||
responses={**responses, 409: {"model": schemas.ErrorMessage, "description": "Could not duplicate project"}},
|
||||
dependencies=[Depends(has_privilege("Project.Allocate"))]
|
||||
)
|
||||
async def duplicate_project(
|
||||
project_data: schemas.ProjectDuplicate,
|
||||
project: Project = Depends(dep_project),
|
||||
current_user: schemas.User = Depends(get_current_active_user),
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
|
||||
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository))
|
||||
) -> schemas.Project:
|
||||
"""
|
||||
Duplicate a project.
|
||||
|
||||
Required privilege: Project.Allocate
|
||||
"""
|
||||
|
||||
reset_mac_addresses = project_data.reset_mac_addresses
|
||||
new_project = await project.duplicate(
|
||||
name=project_data.name, reset_mac_addresses=reset_mac_addresses
|
||||
)
|
||||
await rbac_repo.add_permission_to_user_with_path(current_user.user_id, f"/projects/{new_project.id}/*")
|
||||
|
||||
# Add the new project in the same resource pools if the duplicated project is in any
|
||||
pool_memberships = await pools_repo.get_resource_memberships(project.id)
|
||||
if pool_memberships:
|
||||
resource_create = schemas.ResourceCreate(resource_id=new_project.id, resource_type="project", name=new_project.name)
|
||||
resource = await pools_repo.create_resource(resource_create)
|
||||
for pool in pool_memberships:
|
||||
await pools_repo.add_resource_to_pool(pool.resource_pool_id, resource)
|
||||
|
||||
return new_project.asdict()
|
||||
|
||||
|
||||
@router.get("/{project_id}/locked")
|
||||
@router.get("/{project_id}/locked", dependencies=[Depends(has_privilege("Project.Audit"))])
|
||||
async def locked_project(project: Project = Depends(dep_project)) -> bool:
|
||||
"""
|
||||
Returns whether a project is locked or not
|
||||
Returns whether a project is locked or not.
|
||||
|
||||
Required privilege: Project.Audit
|
||||
"""
|
||||
|
||||
return project.locked
|
||||
|
||||
|
||||
@router.post("/{project_id}/lock", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{project_id}/lock",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(has_privilege("Project.Modify"))]
|
||||
)
|
||||
async def lock_project(project: Project = Depends(dep_project)) -> None:
|
||||
"""
|
||||
Lock all drawings and nodes in a given project.
|
||||
|
||||
Required privilege: Project.Audit
|
||||
"""
|
||||
|
||||
project.lock()
|
||||
|
||||
|
||||
@router.post("/{project_id}/unlock", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{project_id}/unlock",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(has_privilege("Project.Modify"))]
|
||||
)
|
||||
async def unlock_project(project: Project = Depends(dep_project)) -> None:
|
||||
"""
|
||||
Unlock all drawings and nodes in a given project.
|
||||
|
||||
Required privilege: Project.Modify
|
||||
"""
|
||||
|
||||
project.unlock()
|
||||
|
||||
|
||||
@router.get("/{project_id}/files/{file_path:path}")
|
||||
@router.get("/{project_id}/files/{file_path:path}", dependencies=[Depends(has_privilege("Project.Audit"))])
|
||||
async def get_file(file_path: str, project: Project = Depends(dep_project)) -> FileResponse:
|
||||
"""
|
||||
Return a file from a project.
|
||||
|
||||
Required privilege: Project.Audit
|
||||
"""
|
||||
|
||||
file_path = urllib.parse.unquote(file_path)
|
||||
@ -442,10 +518,16 @@ async def get_file(file_path: str, project: Project = Depends(dep_project)) -> F
|
||||
return FileResponse(path, media_type="application/octet-stream")
|
||||
|
||||
|
||||
@router.post("/{project_id}/files/{file_path:path}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.post(
|
||||
"/{project_id}/files/{file_path:path}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(has_privilege("Project.Modify"))]
|
||||
)
|
||||
async def write_file(file_path: str, request: Request, project: Project = Depends(dep_project)) -> None:
|
||||
"""
|
||||
Write a file to a project.
|
||||
|
||||
Required privilege: Project.Modify
|
||||
"""
|
||||
|
||||
file_path = urllib.parse.unquote(file_path)
|
||||
@ -474,6 +556,7 @@ async def write_file(file_path: str, request: Request, project: Project = Depend
|
||||
response_model=schemas.Node,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or template"}},
|
||||
dependencies=[Depends(has_privilege("Node.Allocate"))]
|
||||
)
|
||||
async def create_node_from_template(
|
||||
project_id: UUID,
|
||||
@ -483,6 +566,8 @@ async def create_node_from_template(
|
||||
) -> schemas.Node:
|
||||
"""
|
||||
Create a new node from a template.
|
||||
|
||||
Required privilege: Node.Allocate
|
||||
"""
|
||||
|
||||
template = await TemplatesService(templates_repo).get_template(template_id)
|
||||
|
@ -19,7 +19,7 @@
|
||||
API routes for roles.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, Response, status
|
||||
from fastapi import APIRouter, Depends, status
|
||||
from uuid import UUID
|
||||
from typing import List
|
||||
|
||||
@ -33,6 +33,7 @@ from gns3server.controller.controller_error import (
|
||||
|
||||
from gns3server.db.repositories.rbac import RbacRepository
|
||||
from .dependencies.database import get_repository
|
||||
from .dependencies.rbac import has_privilege
|
||||
|
||||
import logging
|
||||
|
||||
@ -41,24 +42,37 @@ log = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("", response_model=List[schemas.Role])
|
||||
@router.get(
|
||||
"",
|
||||
response_model=List[schemas.Role],
|
||||
dependencies=[Depends(has_privilege("Role.Audit"))]
|
||||
)
|
||||
async def get_roles(
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
|
||||
) -> List[schemas.Role]:
|
||||
"""
|
||||
Get all roles.
|
||||
|
||||
Required privilege: Role.Audit
|
||||
"""
|
||||
|
||||
return await rbac_repo.get_roles()
|
||||
|
||||
|
||||
@router.post("", response_model=schemas.Role, status_code=status.HTTP_201_CREATED)
|
||||
@router.post(
|
||||
"",
|
||||
response_model=schemas.Role,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
dependencies=[Depends(has_privilege("Role.Allocate"))]
|
||||
)
|
||||
async def create_role(
|
||||
role_create: schemas.RoleCreate,
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
|
||||
) -> schemas.Role:
|
||||
"""
|
||||
Create a new role.
|
||||
|
||||
Required privilege: Role.Allocate
|
||||
"""
|
||||
|
||||
if await rbac_repo.get_role_by_name(role_create.name):
|
||||
@ -67,13 +81,19 @@ async def create_role(
|
||||
return await rbac_repo.create_role(role_create)
|
||||
|
||||
|
||||
@router.get("/{role_id}", response_model=schemas.Role)
|
||||
@router.get(
|
||||
"/{role_id}",
|
||||
response_model=schemas.Role,
|
||||
dependencies=[Depends(has_privilege("Role.Audit"))]
|
||||
)
|
||||
async def get_role(
|
||||
role_id: UUID,
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
|
||||
) -> schemas.Role:
|
||||
"""
|
||||
Get a role.
|
||||
|
||||
Required privilege: Role.Audit
|
||||
"""
|
||||
|
||||
role = await rbac_repo.get_role(role_id)
|
||||
@ -82,7 +102,11 @@ async def get_role(
|
||||
return role
|
||||
|
||||
|
||||
@router.put("/{role_id}", response_model=schemas.Role)
|
||||
@router.put(
|
||||
"/{role_id}",
|
||||
response_model=schemas.Role,
|
||||
dependencies=[Depends(has_privilege("Role.Modify"))]
|
||||
)
|
||||
async def update_role(
|
||||
role_id: UUID,
|
||||
role_update: schemas.RoleUpdate,
|
||||
@ -90,6 +114,8 @@ async def update_role(
|
||||
) -> schemas.Role:
|
||||
"""
|
||||
Update a role.
|
||||
|
||||
Required privilege: Role.Modify
|
||||
"""
|
||||
|
||||
role = await rbac_repo.get_role(role_id)
|
||||
@ -102,13 +128,19 @@ async def update_role(
|
||||
return await rbac_repo.update_role(role_id, role_update)
|
||||
|
||||
|
||||
@router.delete("/{role_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.delete(
|
||||
"/{role_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(has_privilege("Role.Allocate"))]
|
||||
)
|
||||
async def delete_role(
|
||||
role_id: UUID,
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
|
||||
) -> None:
|
||||
"""
|
||||
Delete a role.
|
||||
|
||||
Required privilege: Role.Allocate
|
||||
"""
|
||||
|
||||
role = await rbac_repo.get_role(role_id)
|
||||
@ -121,59 +153,72 @@ async def delete_role(
|
||||
success = await rbac_repo.delete_role(role_id)
|
||||
if not success:
|
||||
raise ControllerError(f"Role '{role_id}' could not be deleted")
|
||||
await rbac_repo.delete_all_ace_starting_with_path(f"/roles/{role_id}")
|
||||
|
||||
|
||||
@router.get("/{role_id}/permissions", response_model=List[schemas.Permission])
|
||||
async def get_role_permissions(
|
||||
@router.get(
|
||||
"/{role_id}/privileges",
|
||||
response_model=List[schemas.Privilege],
|
||||
dependencies=[Depends(has_privilege("Role.Audit"))]
|
||||
)
|
||||
async def get_role_privileges(
|
||||
role_id: UUID,
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
|
||||
) -> List[schemas.Permission]:
|
||||
) -> List[schemas.Privilege]:
|
||||
"""
|
||||
Get all role permissions.
|
||||
Get all role privileges.
|
||||
|
||||
Required privilege: Role.Audit
|
||||
"""
|
||||
|
||||
return await rbac_repo.get_role_permissions(role_id)
|
||||
return await rbac_repo.get_role_privileges(role_id)
|
||||
|
||||
|
||||
@router.put(
|
||||
"/{role_id}/permissions/{permission_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT
|
||||
"/{role_id}/privileges/{privilege_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(has_privilege("Role.Modify"))]
|
||||
)
|
||||
async def add_permission_to_role(
|
||||
async def add_privilege_to_role(
|
||||
role_id: UUID,
|
||||
permission_id: UUID,
|
||||
privilege_id: UUID,
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
|
||||
) -> None:
|
||||
"""
|
||||
Add a permission to a role.
|
||||
Add a privilege to a role.
|
||||
|
||||
Required privilege: Role.Modify
|
||||
"""
|
||||
|
||||
permission = await rbac_repo.get_permission(permission_id)
|
||||
if not permission:
|
||||
raise ControllerNotFoundError(f"Permission '{permission_id}' not found")
|
||||
privilege = await rbac_repo.get_privilege(privilege_id)
|
||||
if not privilege:
|
||||
raise ControllerNotFoundError(f"Privilege '{privilege_id}' not found")
|
||||
|
||||
role = await rbac_repo.add_permission_to_role(role_id, permission)
|
||||
role = await rbac_repo.add_privilege_to_role(role_id, privilege)
|
||||
if not role:
|
||||
raise ControllerNotFoundError(f"Role '{role_id}' not found")
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/{role_id}/permissions/{permission_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT
|
||||
"/{role_id}/privileges/{privilege_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(has_privilege("Role.Modify"))]
|
||||
)
|
||||
async def remove_permission_from_role(
|
||||
async def remove_privilege_from_role(
|
||||
role_id: UUID,
|
||||
permission_id: UUID,
|
||||
privilege_id: UUID,
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
|
||||
) -> None:
|
||||
"""
|
||||
Remove member from an user group.
|
||||
Remove privilege from a role.
|
||||
|
||||
Required privilege: Role.Modify
|
||||
"""
|
||||
|
||||
permission = await rbac_repo.get_permission(permission_id)
|
||||
if not permission:
|
||||
raise ControllerNotFoundError(f"Permission '{permission_id}' not found")
|
||||
privilege = await rbac_repo.get_privilege(privilege_id)
|
||||
if not privilege:
|
||||
raise ControllerNotFoundError(f"Privilege '{privilege_id}' not found")
|
||||
|
||||
role = await rbac_repo.remove_permission_from_role(role_id, permission)
|
||||
role = await rbac_repo.remove_privilege_from_role(role_id, privilege)
|
||||
if not role:
|
||||
raise ControllerNotFoundError(f"Role '{role_id}' not found")
|
||||
|
@ -23,14 +23,18 @@ import logging
|
||||
|
||||
log = logging.getLogger()
|
||||
|
||||
from fastapi import APIRouter, Depends, Response, status
|
||||
from fastapi import APIRouter, Depends, status
|
||||
from typing import List
|
||||
from uuid import UUID
|
||||
|
||||
from gns3server.controller.project import Project
|
||||
from gns3server.db.repositories.rbac import RbacRepository
|
||||
from gns3server import schemas
|
||||
from gns3server.controller import Controller
|
||||
|
||||
from .dependencies.database import get_repository
|
||||
from .dependencies.rbac import has_privilege
|
||||
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or snapshot"}}
|
||||
|
||||
router = APIRouter(responses=responses)
|
||||
@ -45,42 +49,74 @@ def dep_project(project_id: UUID) -> Project:
|
||||
return project
|
||||
|
||||
|
||||
@router.post("", status_code=status.HTTP_201_CREATED, response_model=schemas.Snapshot)
|
||||
@router.post(
|
||||
"",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.Snapshot,
|
||||
dependencies=[Depends(has_privilege("Snapshot.Allocate"))]
|
||||
)
|
||||
async def create_snapshot(
|
||||
snapshot_data: schemas.SnapshotCreate,
|
||||
project: Project = Depends(dep_project)
|
||||
) -> schemas.Snapshot:
|
||||
"""
|
||||
Create a new snapshot of a project.
|
||||
|
||||
Required privilege: Snapshot.Allocate
|
||||
"""
|
||||
|
||||
snapshot = await project.snapshot(snapshot_data.name)
|
||||
return snapshot.asdict()
|
||||
|
||||
|
||||
@router.get("", response_model=List[schemas.Snapshot], response_model_exclude_unset=True)
|
||||
@router.get(
|
||||
"",
|
||||
response_model=List[schemas.Snapshot],
|
||||
response_model_exclude_unset=True,
|
||||
dependencies=[Depends(has_privilege("Snapshot.Audit"))]
|
||||
)
|
||||
def get_snapshots(project: Project = Depends(dep_project)) -> List[schemas.Snapshot]:
|
||||
"""
|
||||
Return all snapshots belonging to a given project.
|
||||
|
||||
Required privilege: Snapshot.Audit
|
||||
"""
|
||||
|
||||
snapshots = [s for s in project.snapshots.values()]
|
||||
return [s.asdict() for s in sorted(snapshots, key=lambda s: (s.created_at, s.name))]
|
||||
|
||||
|
||||
@router.delete("/{snapshot_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_snapshot(snapshot_id: UUID, project: Project = Depends(dep_project)) -> None:
|
||||
@router.delete(
|
||||
"/{snapshot_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(has_privilege("Snapshot.Allocate"))]
|
||||
)
|
||||
async def delete_snapshot(
|
||||
snapshot_id: UUID,
|
||||
project: Project = Depends(dep_project),
|
||||
rbac_repo=Depends(get_repository(RbacRepository))
|
||||
) -> None:
|
||||
"""
|
||||
Delete a snapshot.
|
||||
|
||||
Required privilege: Snapshot.Allocate
|
||||
"""
|
||||
|
||||
await project.delete_snapshot(str(snapshot_id))
|
||||
await rbac_repo.delete_all_ace_starting_with_path(f"/projects/{project.id}/snapshots/{snapshot_id}")
|
||||
|
||||
|
||||
@router.post("/{snapshot_id}/restore", status_code=status.HTTP_201_CREATED, response_model=schemas.Project)
|
||||
@router.post(
|
||||
"/{snapshot_id}/restore",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.Project,
|
||||
dependencies=[Depends(has_privilege("Snapshot.Restore"))]
|
||||
)
|
||||
async def restore_snapshot(snapshot_id: UUID, project: Project = Depends(dep_project)) -> schemas.Project:
|
||||
"""
|
||||
Restore a snapshot.
|
||||
|
||||
Required privilege: Snapshot.Restore
|
||||
"""
|
||||
|
||||
snapshot = project.get_snapshot(str(snapshot_id))
|
||||
|
@ -29,7 +29,7 @@ from gns3server.controller import Controller
|
||||
from gns3server import schemas
|
||||
from gns3server.controller.controller_error import ControllerError, ControllerNotFoundError
|
||||
|
||||
from .dependencies.authentication import get_current_active_user
|
||||
from .dependencies.rbac import has_privilege
|
||||
|
||||
import logging
|
||||
|
||||
@ -39,19 +39,32 @@ log = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("")
|
||||
def get_symbols() -> List[str]:
|
||||
@router.get(
|
||||
"",
|
||||
dependencies=[Depends(has_privilege("Symbol.Audit"))]
|
||||
)
|
||||
def get_symbols() -> List[dict]:
|
||||
"""
|
||||
Return all symbols.
|
||||
|
||||
Required privilege: Symbol.Audit
|
||||
"""
|
||||
|
||||
controller = Controller.instance()
|
||||
return controller.symbols.list()
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{symbol_id:path}/raw", responses={404: {"model": schemas.ErrorMessage, "description": "Could not find symbol"}}
|
||||
"/{symbol_id:path}/raw",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find symbol"}},
|
||||
# FIXME: this is a temporary workaround due to a bug in the web-ui: https://github.com/GNS3/gns3-web-ui/issues/1466
|
||||
# dependencies=[Depends(has_privilege("Symbol.Audit"))]
|
||||
)
|
||||
async def get_symbol(symbol_id: str) -> FileResponse:
|
||||
"""
|
||||
Download a symbol file.
|
||||
|
||||
Required privilege: Symbol.Audit
|
||||
"""
|
||||
|
||||
controller = Controller.instance()
|
||||
@ -65,10 +78,13 @@ async def get_symbol(symbol_id: str) -> FileResponse:
|
||||
@router.get(
|
||||
"/{symbol_id:path}/dimensions",
|
||||
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find symbol"}},
|
||||
dependencies=[Depends(has_privilege("Symbol.Audit"))]
|
||||
)
|
||||
async def get_symbol_dimensions(symbol_id: str) -> dict:
|
||||
"""
|
||||
Get a symbol dimensions.
|
||||
|
||||
Required privilege: Symbol.Audit
|
||||
"""
|
||||
|
||||
controller = Controller.instance()
|
||||
@ -80,10 +96,12 @@ async def get_symbol_dimensions(symbol_id: str) -> dict:
|
||||
raise ControllerNotFoundError(f"Could not get symbol file: {e}")
|
||||
|
||||
|
||||
@router.get("/default_symbols")
|
||||
@router.get("/default_symbols", dependencies=[Depends(has_privilege("Symbol.Audit"))])
|
||||
def get_default_symbols() -> dict:
|
||||
"""
|
||||
Return all default symbols.
|
||||
|
||||
Required privilege: Symbol.Audit
|
||||
"""
|
||||
|
||||
controller = Controller.instance()
|
||||
@ -92,12 +110,14 @@ def get_default_symbols() -> dict:
|
||||
|
||||
@router.post(
|
||||
"/{symbol_id:path}/raw",
|
||||
dependencies=[Depends(get_current_active_user)],
|
||||
status_code=status.HTTP_204_NO_CONTENT
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(has_privilege("Symbol.Allocate"))]
|
||||
)
|
||||
async def upload_symbol(symbol_id: str, request: Request) -> None:
|
||||
"""
|
||||
Upload a symbol file.
|
||||
|
||||
Required privilege: Symbol.Allocate
|
||||
"""
|
||||
|
||||
controller = Controller.instance()
|
||||
@ -111,4 +131,3 @@ async def upload_symbol(symbol_id: str, request: Request) -> None:
|
||||
|
||||
# Reset the symbol list
|
||||
controller.symbols.list()
|
||||
|
||||
|
@ -36,6 +36,7 @@ from gns3server.db.repositories.rbac import RbacRepository
|
||||
from gns3server.db.repositories.images import ImagesRepository
|
||||
|
||||
from .dependencies.authentication import get_current_active_user
|
||||
from .dependencies.rbac import has_privilege
|
||||
from .dependencies.database import get_repository
|
||||
|
||||
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find template"}}
|
||||
@ -43,24 +44,33 @@ responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find
|
||||
router = APIRouter(responses=responses)
|
||||
|
||||
|
||||
@router.post("", response_model=schemas.Template, status_code=status.HTTP_201_CREATED)
|
||||
@router.post(
|
||||
"",
|
||||
response_model=schemas.Template,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
dependencies=[Depends(has_privilege("Template.Allocate"))]
|
||||
)
|
||||
async def create_template(
|
||||
template_create: schemas.TemplateCreate,
|
||||
templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)),
|
||||
current_user: schemas.User = Depends(get_current_active_user),
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
|
||||
templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository))
|
||||
) -> schemas.Template:
|
||||
"""
|
||||
Create a new template.
|
||||
|
||||
Required privilege: Template.Allocate
|
||||
"""
|
||||
|
||||
template = await TemplatesService(templates_repo).create_template(template_create)
|
||||
template_id = template.get("template_id")
|
||||
await rbac_repo.add_permission_to_user_with_path(current_user.user_id, f"/templates/{template_id}/*")
|
||||
return template
|
||||
|
||||
|
||||
@router.get("/{template_id}", response_model=schemas.Template, response_model_exclude_unset=True)
|
||||
@router.get(
|
||||
"/{template_id}",
|
||||
response_model=schemas.Template,
|
||||
response_model_exclude_unset=True,
|
||||
dependencies=[Depends(get_current_active_user)],
|
||||
#dependencies=[Depends(has_privilege("Template.Audit"))] # FIXME: this is a temporary workaround due to a bug in the web-ui
|
||||
)
|
||||
async def get_template(
|
||||
template_id: UUID,
|
||||
request: Request,
|
||||
@ -69,6 +79,8 @@ async def get_template(
|
||||
) -> schemas.Template:
|
||||
"""
|
||||
Return a template.
|
||||
|
||||
Required privilege: Template.Audit
|
||||
"""
|
||||
|
||||
request_etag = request.headers.get("If-None-Match", "")
|
||||
@ -82,7 +94,12 @@ async def get_template(
|
||||
return template
|
||||
|
||||
|
||||
@router.put("/{template_id}", response_model=schemas.Template, response_model_exclude_unset=True)
|
||||
@router.put(
|
||||
"/{template_id}",
|
||||
response_model=schemas.Template,
|
||||
response_model_exclude_unset=True,
|
||||
dependencies=[Depends(has_privilege("Template.Modify"))]
|
||||
)
|
||||
async def update_template(
|
||||
template_id: UUID,
|
||||
template_update: schemas.TemplateUpdate,
|
||||
@ -90,12 +107,18 @@ async def update_template(
|
||||
) -> schemas.Template:
|
||||
"""
|
||||
Update a template.
|
||||
|
||||
Required privilege: Template.Modify
|
||||
"""
|
||||
|
||||
return await TemplatesService(templates_repo).update_template(template_id, template_update)
|
||||
|
||||
|
||||
@router.delete("/{template_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.delete(
|
||||
"/{template_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(has_privilege("Template.Allocate"))]
|
||||
)
|
||||
async def delete_template(
|
||||
template_id: UUID,
|
||||
prune_images: Optional[bool] = False,
|
||||
@ -105,15 +128,23 @@ async def delete_template(
|
||||
) -> None:
|
||||
"""
|
||||
Delete a template.
|
||||
|
||||
Required privilege: Template.Allocate
|
||||
"""
|
||||
|
||||
await TemplatesService(templates_repo).delete_template(template_id)
|
||||
await rbac_repo.delete_all_permissions_with_path(f"/templates/{template_id}")
|
||||
await rbac_repo.delete_all_ace_starting_with_path(f"/templates/{template_id}")
|
||||
if prune_images:
|
||||
await images_repo.prune_images()
|
||||
|
||||
|
||||
@router.get("", response_model=List[schemas.Template], response_model_exclude_unset=True)
|
||||
@router.get(
|
||||
"",
|
||||
response_model=List[schemas.Template],
|
||||
response_model_exclude_unset=True,
|
||||
dependencies=[Depends(get_current_active_user)],
|
||||
#dependencies=[Depends(has_privilege("Template.Audit"))] # FIXME: this is a temporary workaround due to a bug in the web-ui
|
||||
)
|
||||
async def get_templates(
|
||||
templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)),
|
||||
current_user: schemas.User = Depends(get_current_active_user),
|
||||
@ -121,6 +152,8 @@ async def get_templates(
|
||||
) -> List[schemas.Template]:
|
||||
"""
|
||||
Return all templates.
|
||||
|
||||
Required privilege: Template.Audit
|
||||
"""
|
||||
|
||||
templates = await TemplatesService(templates_repo).get_templates()
|
||||
@ -129,27 +162,31 @@ async def get_templates(
|
||||
else:
|
||||
user_templates = []
|
||||
for template in templates:
|
||||
if template.get("builtin") is True:
|
||||
user_templates.append(template)
|
||||
continue
|
||||
template_id = template.get("template_id")
|
||||
authorized = await rbac_repo.check_user_is_authorized(
|
||||
current_user.user_id, "GET", f"/templates/{template_id}")
|
||||
if authorized:
|
||||
user_templates.append(template)
|
||||
# if template.get("builtin") is True:
|
||||
# user_templates.append(template)
|
||||
# continue
|
||||
# template_id = template.get("template_id")
|
||||
# authorized = await rbac_repo.check_user_is_authorized(
|
||||
# current_user.user_id, "GET", f"/templates/{template_id}")
|
||||
# if authorized:
|
||||
user_templates.append(template)
|
||||
return user_templates
|
||||
|
||||
|
||||
@router.post("/{template_id}/duplicate", response_model=schemas.Template, status_code=status.HTTP_201_CREATED)
|
||||
@router.post(
|
||||
"/{template_id}/duplicate",
|
||||
response_model=schemas.Template,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
dependencies=[Depends(has_privilege("Template.Allocate"))]
|
||||
)
|
||||
async def duplicate_template(
|
||||
template_id: UUID, templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)),
|
||||
current_user: schemas.User = Depends(get_current_active_user),
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
|
||||
template_id: UUID, templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository))
|
||||
) -> schemas.Template:
|
||||
"""
|
||||
Duplicate a template.
|
||||
|
||||
Required privilege: Template.Allocate
|
||||
"""
|
||||
|
||||
template = await TemplatesService(templates_repo).duplicate_template(template_id)
|
||||
await rbac_repo.add_permission_to_user_with_path(current_user.user_id, f"/templates/{template_id}/*")
|
||||
return template
|
||||
|
@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (C) 2020 GNS3 Technologies Inc.
|
||||
# Copyright (C) 2023 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@ -38,6 +38,7 @@ from gns3server.services import auth_service
|
||||
|
||||
from .dependencies.authentication import get_current_active_user
|
||||
from .dependencies.database import get_repository
|
||||
from .dependencies.rbac import has_privilege
|
||||
|
||||
import logging
|
||||
|
||||
@ -53,7 +54,7 @@ async def login(
|
||||
) -> schemas.Token:
|
||||
"""
|
||||
Default user login method using forms (x-www-form-urlencoded).
|
||||
Example: curl http://host:port/v3/users/login -H "Content-Type: application/x-www-form-urlencoded" -d "username=admin&password=admin"
|
||||
Example: curl -X POST http://host:port/v3/access/users/login -H "Content-Type: application/x-www-form-urlencoded" -d "username=admin&password=admin"
|
||||
"""
|
||||
|
||||
user = await users_repo.authenticate_user(username=form_data.username, password=form_data.password)
|
||||
@ -75,7 +76,7 @@ async def authenticate(
|
||||
) -> schemas.Token:
|
||||
"""
|
||||
Alternative authentication method using json.
|
||||
Example: curl http://host:port/v3/users/authenticate -d '{"username": "admin", "password": "admin"}' -H "Content-Type: application/json"
|
||||
Example: curl -X POST http://host:port/v3/access/users/authenticate -d '{"username": "admin", "password": "admin"}' -H "Content-Type: application/json"
|
||||
"""
|
||||
|
||||
user = await users_repo.authenticate_user(username=user_credentials.username, password=user_credentials.password)
|
||||
@ -115,12 +116,18 @@ async def update_logged_in_user(
|
||||
return await users_repo.update_user(current_user.user_id, user_update)
|
||||
|
||||
|
||||
@router.get("", response_model=List[schemas.User], dependencies=[Depends(get_current_active_user)])
|
||||
@router.get(
|
||||
"",
|
||||
response_model=List[schemas.User],
|
||||
dependencies=[Depends(has_privilege("User.Audit"))]
|
||||
)
|
||||
async def get_users(
|
||||
users_repo: UsersRepository = Depends(get_repository(UsersRepository))
|
||||
) -> List[schemas.User]:
|
||||
"""
|
||||
Get all users.
|
||||
|
||||
Required privilege: User.Audit
|
||||
"""
|
||||
|
||||
return await users_repo.get_users()
|
||||
@ -129,8 +136,8 @@ async def get_users(
|
||||
@router.post(
|
||||
"",
|
||||
response_model=schemas.User,
|
||||
dependencies=[Depends(get_current_active_user)],
|
||||
status_code=status.HTTP_201_CREATED
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
dependencies=[Depends(has_privilege("User.Allocate"))]
|
||||
)
|
||||
async def create_user(
|
||||
user_create: schemas.UserCreate,
|
||||
@ -138,6 +145,8 @@ async def create_user(
|
||||
) -> schemas.User:
|
||||
"""
|
||||
Create a new user.
|
||||
|
||||
Required privilege: User.Allocate
|
||||
"""
|
||||
|
||||
if await users_repo.get_user_by_username(user_create.username):
|
||||
@ -149,13 +158,19 @@ async def create_user(
|
||||
return await users_repo.create_user(user_create)
|
||||
|
||||
|
||||
@router.get("/{user_id}", dependencies=[Depends(get_current_active_user)], response_model=schemas.User)
|
||||
@router.get(
|
||||
"/{user_id}",
|
||||
response_model=schemas.User,
|
||||
dependencies=[Depends(has_privilege("User.Audit"))]
|
||||
)
|
||||
async def get_user(
|
||||
user_id: UUID,
|
||||
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
|
||||
) -> schemas.User:
|
||||
"""
|
||||
Get an user.
|
||||
Get a user.
|
||||
|
||||
Required privilege: User.Audit
|
||||
"""
|
||||
|
||||
user = await users_repo.get_user(user_id)
|
||||
@ -164,14 +179,20 @@ async def get_user(
|
||||
return user
|
||||
|
||||
|
||||
@router.put("/{user_id}", dependencies=[Depends(get_current_active_user)], response_model=schemas.User)
|
||||
@router.put(
|
||||
"/{user_id}",
|
||||
response_model=schemas.User,
|
||||
dependencies=[Depends(has_privilege("User.Modify"))]
|
||||
)
|
||||
async def update_user(
|
||||
user_id: UUID,
|
||||
user_update: schemas.UserUpdate,
|
||||
users_repo: UsersRepository = Depends(get_repository(UsersRepository))
|
||||
) -> schemas.User:
|
||||
"""
|
||||
Update an user.
|
||||
Update a user.
|
||||
|
||||
Required privilege: User.Modify
|
||||
"""
|
||||
|
||||
if user_update.username and await users_repo.get_user_by_username(user_update.username):
|
||||
@ -188,15 +209,18 @@ async def update_user(
|
||||
|
||||
@router.delete(
|
||||
"/{user_id}",
|
||||
dependencies=[Depends(get_current_active_user)],
|
||||
status_code=status.HTTP_204_NO_CONTENT
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(has_privilege("User.Allocate"))]
|
||||
)
|
||||
async def delete_user(
|
||||
user_id: UUID,
|
||||
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
|
||||
user_id: UUID,
|
||||
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
|
||||
) -> None:
|
||||
"""
|
||||
Delete an user.
|
||||
Delete a user.
|
||||
|
||||
Required privilege: User.Allocate
|
||||
"""
|
||||
|
||||
user = await users_repo.get_user(user_id)
|
||||
@ -209,12 +233,13 @@ async def delete_user(
|
||||
success = await users_repo.delete_user(user_id)
|
||||
if not success:
|
||||
raise ControllerError(f"User '{user_id}' could not be deleted")
|
||||
await rbac_repo.delete_all_ace_starting_with_path(f"/users/{user_id}")
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{user_id}/groups",
|
||||
dependencies=[Depends(get_current_active_user)],
|
||||
response_model=List[schemas.UserGroup]
|
||||
response_model=List[schemas.UserGroup],
|
||||
dependencies=[Depends(has_privilege("Group.Audit"))]
|
||||
)
|
||||
async def get_user_memberships(
|
||||
user_id: UUID,
|
||||
@ -222,68 +247,8 @@ async def get_user_memberships(
|
||||
) -> List[schemas.UserGroup]:
|
||||
"""
|
||||
Get user memberships.
|
||||
|
||||
Required privilege: Group.Audit
|
||||
"""
|
||||
|
||||
return await users_repo.get_user_memberships(user_id)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{user_id}/permissions",
|
||||
dependencies=[Depends(get_current_active_user)],
|
||||
response_model=List[schemas.Permission]
|
||||
)
|
||||
async def get_user_permissions(
|
||||
user_id: UUID,
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
|
||||
) -> List[schemas.Permission]:
|
||||
"""
|
||||
Get user permissions.
|
||||
"""
|
||||
|
||||
return await rbac_repo.get_user_permissions(user_id)
|
||||
|
||||
|
||||
@router.put(
|
||||
"/{user_id}/permissions/{permission_id}",
|
||||
dependencies=[Depends(get_current_active_user)],
|
||||
status_code=status.HTTP_204_NO_CONTENT
|
||||
)
|
||||
async def add_permission_to_user(
|
||||
user_id: UUID,
|
||||
permission_id: UUID,
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
|
||||
) -> None:
|
||||
"""
|
||||
Add a permission to an user.
|
||||
"""
|
||||
|
||||
permission = await rbac_repo.get_permission(permission_id)
|
||||
if not permission:
|
||||
raise ControllerNotFoundError(f"Permission '{permission_id}' not found")
|
||||
|
||||
user = await rbac_repo.add_permission_to_user(user_id, permission)
|
||||
if not user:
|
||||
raise ControllerNotFoundError(f"User '{user_id}' not found")
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/{user_id}/permissions/{permission_id}",
|
||||
dependencies=[Depends(get_current_active_user)],
|
||||
status_code=status.HTTP_204_NO_CONTENT
|
||||
)
|
||||
async def remove_permission_from_user(
|
||||
user_id: UUID,
|
||||
permission_id: UUID,
|
||||
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
|
||||
) -> None:
|
||||
"""
|
||||
Remove permission from an user.
|
||||
"""
|
||||
|
||||
permission = await rbac_repo.get_permission(permission_id)
|
||||
if not permission:
|
||||
raise ControllerNotFoundError(f"Permission '{permission_id}' not found")
|
||||
|
||||
user = await rbac_repo.remove_permission_from_user(user_id, permission)
|
||||
if not user:
|
||||
raise ControllerNotFoundError(f"User '{user_id}' not found")
|
||||
|
@ -55,6 +55,9 @@ async def web_ui(file_path: str):
|
||||
if static is None or not os.path.exists(static):
|
||||
static = get_resource(os.path.join("static", "web-ui", "index.html"))
|
||||
|
||||
if static is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# guesstype prefers to have text/html type than application/javascript
|
||||
# which results with warnings in Firefox 66 on Windows
|
||||
# Ref. gns3-server#1559
|
||||
|
@ -21,9 +21,10 @@ FastAPI app
|
||||
|
||||
import time
|
||||
|
||||
from fastapi import FastAPI, Request, HTTPException
|
||||
from fastapi import FastAPI, Request, HTTPException, status
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from uvicorn.main import Server as UvicornServer
|
||||
|
||||
@ -87,54 +88,54 @@ UvicornServer.handle_exit = handle_exit
|
||||
|
||||
@app.exception_handler(ControllerError)
|
||||
async def controller_error_handler(request: Request, exc: ControllerError):
|
||||
log.error(f"Controller error: {exc}")
|
||||
log.error(f"Controller error in {request.url.path} ({request.method}): {exc}")
|
||||
return JSONResponse(
|
||||
status_code=409,
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
content={"message": str(exc)},
|
||||
)
|
||||
|
||||
|
||||
@app.exception_handler(ControllerTimeoutError)
|
||||
async def controller_timeout_error_handler(request: Request, exc: ControllerTimeoutError):
|
||||
log.error(f"Controller timeout error: {exc}")
|
||||
log.error(f"Controller timeout error in {request.url.path} ({request.method}): {exc}")
|
||||
return JSONResponse(
|
||||
status_code=408,
|
||||
status_code=status.HTTP_408_REQUEST_TIMEOUT,
|
||||
content={"message": str(exc)},
|
||||
)
|
||||
|
||||
|
||||
@app.exception_handler(ControllerUnauthorizedError)
|
||||
async def controller_unauthorized_error_handler(request: Request, exc: ControllerUnauthorizedError):
|
||||
log.error(f"Controller unauthorized error: {exc}")
|
||||
log.error(f"Controller unauthorized error in {request.url.path} ({request.method}): {exc}")
|
||||
return JSONResponse(
|
||||
status_code=401,
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
content={"message": str(exc)},
|
||||
)
|
||||
|
||||
|
||||
@app.exception_handler(ControllerForbiddenError)
|
||||
async def controller_forbidden_error_handler(request: Request, exc: ControllerForbiddenError):
|
||||
log.error(f"Controller forbidden error: {exc}")
|
||||
log.error(f"Controller forbidden error in {request.url.path} ({request.method}): {exc}")
|
||||
return JSONResponse(
|
||||
status_code=403,
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
content={"message": str(exc)},
|
||||
)
|
||||
|
||||
|
||||
@app.exception_handler(ControllerNotFoundError)
|
||||
async def controller_not_found_error_handler(request: Request, exc: ControllerNotFoundError):
|
||||
log.error(f"Controller not found error: {exc}")
|
||||
log.error(f"Controller not found error in {request.url.path} ({request.method}): {exc}")
|
||||
return JSONResponse(
|
||||
status_code=404,
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
content={"message": str(exc)},
|
||||
)
|
||||
|
||||
|
||||
@app.exception_handler(ControllerBadRequestError)
|
||||
async def controller_bad_request_error_handler(request: Request, exc: ControllerBadRequestError):
|
||||
log.error(f"Controller bad request error: {exc}")
|
||||
log.error(f"Controller bad request error in {request.url.path} ({request.method}): {exc}")
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
content={"message": str(exc)},
|
||||
)
|
||||
|
||||
@ -143,7 +144,7 @@ async def controller_bad_request_error_handler(request: Request, exc: Controller
|
||||
async def compute_conflict_error_handler(request: Request, exc: ComputeConflictError):
|
||||
log.error(f"Controller received error from compute for request '{exc.url()}': {exc}")
|
||||
return JSONResponse(
|
||||
status_code=409,
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
content={"message": str(exc)},
|
||||
)
|
||||
|
||||
@ -160,12 +161,21 @@ async def http_exception_handler(request: Request, exc: HTTPException):
|
||||
|
||||
@app.exception_handler(SQLAlchemyError)
|
||||
async def sqlalchemry_error_handler(request: Request, exc: SQLAlchemyError):
|
||||
log.error(f"Controller database error: {exc}")
|
||||
log.error(f"Controller database error in {request.url.path} ({request.method}): {exc}")
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
content={"message": "Database error detected, please check logs to find details"},
|
||||
)
|
||||
|
||||
|
||||
@app.exception_handler(RequestValidationError)
|
||||
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
||||
log.error(f"Request validation error in {request.url.path} ({request.method}): {exc}")
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
content={"message": str(exc)}
|
||||
)
|
||||
|
||||
# FIXME: do not use this middleware since it creates issue when using StreamingResponse
|
||||
# see https://starlette-context.readthedocs.io/en/latest/middleware.html#why-are-there-two-middlewares-that-do-the-same-thing
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
"vendor_url": "https://www.kali.org/",
|
||||
"documentation_url": "http://www.ipcop.org/docs.html",
|
||||
"product_name": "IP Cop",
|
||||
"registry_version": 3,
|
||||
"registry_version": 4,
|
||||
"status": "stable",
|
||||
"maintainer": "Brent Stewart",
|
||||
"maintainer_email": "brent@stewart.tc",
|
||||
|
@ -7,8 +7,8 @@
|
||||
"vendor_url": "https://www.a10networks.com/",
|
||||
"documentation_url": "https://www.a10networks.com/support",
|
||||
"product_name": "A10 vThunder",
|
||||
"product_url": "https://www.a10networks.com/products/thunder-series-appliances/vthunder-virtualized-application_delivery_controller/",
|
||||
"registry_version": 3,
|
||||
"product_url": "https://www.a10networks.com/products/vthunder-trial/",
|
||||
"registry_version": 4,
|
||||
"status": "stable",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
|
@ -6,7 +6,7 @@
|
||||
"vendor_name": "Ubuntu",
|
||||
"vendor_url": "https://www.ubuntu.com/",
|
||||
"product_name": "AAA",
|
||||
"registry_version": 3,
|
||||
"registry_version": 4,
|
||||
"status": "stable",
|
||||
"maintainer": "Andras Dosztal",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
|
@ -8,7 +8,7 @@
|
||||
"documentation_url": "https://www.alcatel-lucent.com/support",
|
||||
"product_name": "Alcatel 7750",
|
||||
"product_url": "https://www.alcatel-lucent.com/products/7750-service-router",
|
||||
"registry_version": 3,
|
||||
"registry_version": 4,
|
||||
"status": "experimental",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
|
@ -21,30 +21,62 @@
|
||||
"hda_disk_interface": "sata",
|
||||
"arch": "x86_64",
|
||||
"console_type": "telnet",
|
||||
"kvm": "allow"
|
||||
"kvm": "allow",
|
||||
"options": "-cpu host -nographic"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "AlmaLinux-8-GenericCloud-8.5-20211119.x86_64.qcow2",
|
||||
"version": "8.5",
|
||||
"md5sum": "a64ece809ae06180ac59cfa622d98af0",
|
||||
"filesize": 561774592,
|
||||
"download_url": "https://repo.almalinux.org/almalinux/8/cloud/x86_64/images/",
|
||||
"direct_download_url": "https://repo.almalinux.org/almalinux/8/cloud/x86_64/images/AlmaLinux-8-GenericCloud-8.5-20211119.x86_64.qcow2"
|
||||
"filename": "AlmaLinux-9-GenericCloud-9.2-20230513.x86_64.qcow2",
|
||||
"version": "9.2",
|
||||
"md5sum": "c5bc76e8c95ac9f810a3482c80a54cc7",
|
||||
"filesize": 563347456,
|
||||
"download_url": "https://vault.almalinux.org/9.2/cloud/x86_64/images/",
|
||||
"direct_download_url": "https://vault.almalinux.org/9.2/cloud/x86_64/images/AlmaLinux-9-GenericCloud-9.2-20230513.x86_64.qcow2"
|
||||
},
|
||||
{
|
||||
"filename": "AlmaLinux-8-GenericCloud-8.8-20230524.x86_64.qcow2",
|
||||
"version": "8.8",
|
||||
"md5sum": "3958c5fc25770ef63cf97aa5d93f0a0b",
|
||||
"filesize": 565444608,
|
||||
"download_url": "https://vault.almalinux.org/8.8/cloud/x86_64/images/",
|
||||
"direct_download_url": "https://vault.almalinux.org/8.8/cloud/x86_64/images/AlmaLinux-8-GenericCloud-8.8-20230524.x86_64.qcow2"
|
||||
},
|
||||
{
|
||||
"filename": "AlmaLinux-8-GenericCloud-8.7-20221111.x86_64.qcow2",
|
||||
"version": "8.7",
|
||||
"md5sum": "b2b8c7fd3b6869362f3f8ed47549c804",
|
||||
"filesize": 566231040,
|
||||
"download_url": "https://vault.almalinux.org/8.7/cloud/x86_64/images/",
|
||||
"direct_download_url": "https://vault.almalinux.org/8.7/cloud/x86_64/images/AlmaLinux-8-GenericCloud-8.7-20221111.x86_64.qcow2"
|
||||
},
|
||||
{
|
||||
"filename": "almalinux-cloud-init-data.iso",
|
||||
"version": "1.0",
|
||||
"md5sum": "72fb52af76e9561d125dd99224e2c1d1",
|
||||
"filesize": 374784,
|
||||
"download_url": "https://github.com/GNS3/gns3-registry/raw/master/cloud-init/AlmaLinux/almalinux-cloud-init-data.iso"
|
||||
"download_url": "https://github.com/GNS3/gns3-registry/tree/master/cloud-init/AlmaLinux",
|
||||
"direct_download_url": "https://github.com/GNS3/gns3-registry/raw/master/cloud-init/AlmaLinux/almalinux-cloud-init-data.iso"
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "8.5",
|
||||
"name": "9.2",
|
||||
"images": {
|
||||
"hda_disk_image": "AlmaLinux-8-GenericCloud-8.5-20211119.x86_64.qcow2",
|
||||
"hda_disk_image": "AlmaLinux-9-GenericCloud-9.2-20230513.x86_64.qcow2",
|
||||
"cdrom_image": "almalinux-cloud-init-data.iso"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "8.8",
|
||||
"images": {
|
||||
"hda_disk_image": "AlmaLinux-8-GenericCloud-8.8-20230524.x86_64.qcow2",
|
||||
"cdrom_image": "almalinux-cloud-init-data.iso"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "8.7",
|
||||
"images": {
|
||||
"hda_disk_image": "AlmaLinux-8-GenericCloud-8.7-20221111.x86_64.qcow2",
|
||||
"cdrom_image": "almalinux-cloud-init-data.iso"
|
||||
}
|
||||
}
|
||||
|
59
gns3server/appliances/alpine-linux-virt.gns3a
Normal file
59
gns3server/appliances/alpine-linux-virt.gns3a
Normal file
@ -0,0 +1,59 @@
|
||||
{
|
||||
"appliance_id": "3da5c614-772c-4963-af86-f24e058c9216",
|
||||
"name": "Alpine Linux Virt",
|
||||
"category": "guest",
|
||||
"description": "Alpine Linux is a security-oriented, lightweight Linux distribution based on musl libc and busybox.\n\nThis is the qemu version of Alpine Linux, stripped down to the maximum, only the default packages are installed without an SSH server.",
|
||||
"vendor_name": "Alpine Linux Development Team",
|
||||
"vendor_url": "http://alpinelinux.org",
|
||||
"documentation_url": "http://wiki.alpinelinux.org",
|
||||
"product_name": "Alpine Linux Virt",
|
||||
"registry_version": 4,
|
||||
"status": "stable",
|
||||
"availability": "free",
|
||||
"maintainer": "Adnan RIHAN",
|
||||
"maintainer_email": "adnan@rihan.fr",
|
||||
"usage": "Autologin is enabled as \"root\" with no password.\n\nThe network interfaces aren't configured, you can do either of the following:\n- Use alpine's DHCP client: `udhcpc`\n- Configure them manually (ip address add \u2026, ip route add \u2026)\n- Modify interfaces file in /etc/network/interfaces\n- Use alpine's wizard: `setup-interfaces`",
|
||||
"symbol": "alpine-virt-qemu.svg",
|
||||
"port_name_format": "eth{0}",
|
||||
"qemu": {
|
||||
"adapter_type": "virtio-net-pci",
|
||||
"adapters": 1,
|
||||
"ram": 128,
|
||||
"hda_disk_interface": "virtio",
|
||||
"arch": "x86_64",
|
||||
"console_type": "telnet",
|
||||
"kvm": "allow"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "alpine-virt-3.18.4.qcow2",
|
||||
"version": "3.18.4",
|
||||
"md5sum": "99d393c16c870e12c4215aadd82ca998",
|
||||
"filesize": 51066880,
|
||||
"download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/",
|
||||
"direct_download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/alpine-virt-3.18.4.qcow2/download"
|
||||
},
|
||||
{
|
||||
"filename": "alpine-virt-3.16.img",
|
||||
"version": "3.16",
|
||||
"md5sum": "ce90ff64b8f8e5860c49ea4a038e54cc",
|
||||
"filesize": 96468992,
|
||||
"download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/",
|
||||
"direct_download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/alpine-virt-3.16.img/download"
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "3.18.4",
|
||||
"images": {
|
||||
"hda_disk_image": "alpine-virt-3.18.4.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "3.16",
|
||||
"images": {
|
||||
"hda_disk_image": "alpine-virt-3.16.img"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -5,9 +5,10 @@
|
||||
"description": "Alpine Linux is a security-oriented, lightweight Linux distribution based on musl libc and busybox.",
|
||||
"vendor_name": "Alpine Linux Development Team",
|
||||
"vendor_url": "http://alpinelinux.org",
|
||||
"vendor_logo_url": "https://raw.githubusercontent.com/GNS3/gns3-registry/master/vendor-logos/Alpine Linux.png",
|
||||
"documentation_url": "http://wiki.alpinelinux.org",
|
||||
"product_name": "Alpine Linux",
|
||||
"registry_version": 3,
|
||||
"registry_version": 4,
|
||||
"status": "stable",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
|
@ -6,7 +6,7 @@
|
||||
"vendor_name": "Arista",
|
||||
"vendor_url": "http://www.arista.com/",
|
||||
"product_name": "cEOS",
|
||||
"registry_version": 3,
|
||||
"registry_version": 4,
|
||||
"status": "experimental",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
|
@ -8,7 +8,7 @@
|
||||
"documentation_url": "https://www.arista.com/assets/data/docs/Manuals/EOS-4.17.2F-Manual.pdf",
|
||||
"product_name": "vEOS",
|
||||
"product_url": "https://eos.arista.com/",
|
||||
"registry_version": 3,
|
||||
"registry_version": 4,
|
||||
"status": "experimental",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
@ -24,9 +24,101 @@
|
||||
"hdb_disk_interface": "ide",
|
||||
"arch": "x86_64",
|
||||
"console_type": "telnet",
|
||||
"kvm": "require"
|
||||
"kvm": "require",
|
||||
"options": "-cpu host"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "vEOS64-lab-4.32.0F.vmdk",
|
||||
"version": "4.32.0F",
|
||||
"md5sum": "851771260bb18ad3e90fa6956f0c6161",
|
||||
"filesize": 591724544,
|
||||
"download_url": "https://www.arista.com/en/support/software-download"
|
||||
},
|
||||
{
|
||||
"filename": "vEOS64-lab-4.31.3M.vmdk",
|
||||
"version": "4.31.3M",
|
||||
"md5sum": "7df107da137f4a4e752014d4f0e94cd3",
|
||||
"filesize": 577961984,
|
||||
"download_url": "https://www.arista.com/en/support/software-download"
|
||||
},
|
||||
{
|
||||
"filename": "vEOS64-lab-4.30.6M.vmdk",
|
||||
"version": "4.30.6M",
|
||||
"md5sum": "19721aace820b9ebf6d7ae6524803cf5",
|
||||
"filesize": 553123840,
|
||||
"download_url": "https://www.arista.com/en/support/software-download"
|
||||
},
|
||||
{
|
||||
"filename": "vEOS64-lab-4.29.8M.vmdk",
|
||||
"version": "4.29.8M",
|
||||
"md5sum": "131888f74cd63a93894521d40eb4d0b6",
|
||||
"filesize": 548405248,
|
||||
"download_url": "https://www.arista.com/en/support/software-download"
|
||||
},
|
||||
{
|
||||
"filename": "vEOS64-lab-4.28.11M.vmdk",
|
||||
"version": "4.28.11M",
|
||||
"md5sum": "6cac0e7b04a74ee0dc358327a00accfd",
|
||||
"filesize": 513343488,
|
||||
"download_url": "https://www.arista.com/en/support/software-download"
|
||||
},
|
||||
{
|
||||
"filename": "vEOS64-lab-4.27.12M.vmdk",
|
||||
"version": "4.27.12M",
|
||||
"md5sum": "34c4f785c7fc054cda8754dd13c0d7c7",
|
||||
"filesize": 496697344,
|
||||
"download_url": "https://www.arista.com/en/support/software-download"
|
||||
},
|
||||
{
|
||||
"filename": "vEOS-lab-4.32.0F.vmdk",
|
||||
"version": "4.32.0F",
|
||||
"md5sum": "584b901a1249717504050e48f74fb8dd",
|
||||
"filesize": 591396864,
|
||||
"download_url": "https://www.arista.com/en/support/software-download"
|
||||
},
|
||||
{
|
||||
"filename": "vEOS-lab-4.31.3M.vmdk",
|
||||
"version": "4.31.3M",
|
||||
"md5sum": "a2e130697cdf8547006eebebde6eefca",
|
||||
"filesize": 590086144,
|
||||
"download_url": "https://www.arista.com/en/support/software-download"
|
||||
},
|
||||
{
|
||||
"filename": "vEOS-lab-4.30.6M.vmdk",
|
||||
"version": "4.30.6M",
|
||||
"md5sum": "a4467648bcfa7b19640af8a4ad3153c6",
|
||||
"filesize": 565968896,
|
||||
"download_url": "https://www.arista.com/en/support/software-download"
|
||||
},
|
||||
{
|
||||
"filename": "vEOS-lab-4.29.8M.vmdk",
|
||||
"version": "4.29.8M",
|
||||
"md5sum": "1952f6114a4376212c525db9ec8efd5f",
|
||||
"filesize": 558039040,
|
||||
"download_url": "https://www.arista.com/en/support/software-download"
|
||||
},
|
||||
{
|
||||
"filename": "vEOS-lab-4.28.11M.vmdk",
|
||||
"version": "4.28.11M",
|
||||
"md5sum": "5502df24dfc231c45afb33d6018c16d0",
|
||||
"filesize": 521338880,
|
||||
"download_url": "https://www.arista.com/en/support/software-download"
|
||||
},
|
||||
{
|
||||
"filename": "vEOS-lab-4.27.12M.vmdk",
|
||||
"version": "4.27.12M",
|
||||
"md5sum": "e08a97e7c1977993f947fedeb4c6ddd5",
|
||||
"filesize": 504299520,
|
||||
"download_url": "https://www.arista.com/en/support/software-download"
|
||||
},
|
||||
{
|
||||
"filename": "Aboot-veos-serial-8.0.2.iso",
|
||||
"version": "8.0.2",
|
||||
"md5sum": "8d7e754efebca1930a93a2587ff7606c",
|
||||
"filesize": 6291456,
|
||||
"download_url": "https://www.arista.com/en/support/software-download"
|
||||
},
|
||||
{
|
||||
"filename": "vEOS-lab-4.26.2F.vmdk",
|
||||
"version": "4.26.2F",
|
||||
@ -218,6 +310,90 @@
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "4.32.0F",
|
||||
"images": {
|
||||
"hda_disk_image": "Aboot-veos-serial-8.0.2.iso",
|
||||
"hdb_disk_image": "vEOS64-lab-4.32.0F.vmdk"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "4.31.3M",
|
||||
"images": {
|
||||
"hda_disk_image": "Aboot-veos-serial-8.0.2.iso",
|
||||
"hdb_disk_image": "vEOS64-lab-4.31.3M.vmdk"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "4.30.6M",
|
||||
"images": {
|
||||
"hda_disk_image": "Aboot-veos-serial-8.0.2.iso",
|
||||
"hdb_disk_image": "vEOS64-lab-4.30.6M.vmdk"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "4.29.8M",
|
||||
"images": {
|
||||
"hda_disk_image": "Aboot-veos-serial-8.0.2.iso",
|
||||
"hdb_disk_image": "vEOS64-lab-4.29.8M.vmdk"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "4.28.11M",
|
||||
"images": {
|
||||
"hda_disk_image": "Aboot-veos-serial-8.0.2.iso",
|
||||
"hdb_disk_image": "vEOS64-lab-4.28.11M.vmdk"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "4.27.12M",
|
||||
"images": {
|
||||
"hda_disk_image": "Aboot-veos-serial-8.0.2.iso",
|
||||
"hdb_disk_image": "vEOS64-lab-4.27.12M.vmdk"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "4.32.0F",
|
||||
"images": {
|
||||
"hda_disk_image": "Aboot-veos-serial-8.0.2.iso",
|
||||
"hdb_disk_image": "vEOS-lab-4.32.0F.vmdk"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "4.31.3M",
|
||||
"images": {
|
||||
"hda_disk_image": "Aboot-veos-serial-8.0.2.iso",
|
||||
"hdb_disk_image": "vEOS-lab-4.31.3M.vmdk"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "4.30.6M",
|
||||
"images": {
|
||||
"hda_disk_image": "Aboot-veos-serial-8.0.2.iso",
|
||||
"hdb_disk_image": "vEOS-lab-4.30.6M.vmdk"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "4.29.8M",
|
||||
"images": {
|
||||
"hda_disk_image": "Aboot-veos-serial-8.0.2.iso",
|
||||
"hdb_disk_image": "vEOS-lab-4.29.8M.vmdk"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "4.28.11M",
|
||||
"images": {
|
||||
"hda_disk_image": "Aboot-veos-serial-8.0.2.iso",
|
||||
"hdb_disk_image": "vEOS-lab-4.28.11M.vmdk"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "4.27.12M",
|
||||
"images": {
|
||||
"hda_disk_image": "Aboot-veos-serial-8.0.2.iso",
|
||||
"hdb_disk_image": "vEOS-lab-4.27.12M.vmdk"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "4.26.2F",
|
||||
"images": {
|
||||
@ -398,4 +574,4 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -2,10 +2,12 @@
|
||||
"appliance_id": "8f074218-9d61-4e99-ab89-35ca19ad44ee",
|
||||
"name": "ArubaOS-CX Simulation Software",
|
||||
"category": "multilayer_switch",
|
||||
"description": "The ArubaOS-CX Simulation Software is a virtual platform to enable simulation of the ArubaOS-CX Network Operating System. Simulated networks can be created using many of the protocols in the ArubaOS-CX Operating system like OSPF, BGP (inc. EVPN). Key features like the Aruba Network Analytics Engine and the REST API can be simulated, providing a lightweight development platform to building the modern network.",
|
||||
"description": "The Aruba AOS-CX Switch Simulator is a virtual platform to enable simulation of the Aruba AOS-CX Network Operating System. Simulated networks can be created using many of the protocols in the ArubaOS-CX Operating system like OSPF, BGP (inc. EVPN). Key features like the Aruba Network Analytics Engine and the REST API can be simulated, providing a lightweight development platform to building the modern network.",
|
||||
"vendor_name": "HPE Aruba",
|
||||
"vendor_url": "https://www.arubanetworks.com",
|
||||
"product_name": "ArubaOS-CX Simulation Software",
|
||||
"documentation_url": "https://asp.arubanetworks.com/downloads;search=Aruba%20AOS%20CX%20Switch%20Simulator;products=Aruba%20Switches",
|
||||
"product_name": "Aruba AOS-CX Switch Simulator",
|
||||
"product_url": "https://www.arubanetworks.com/products/switches/",
|
||||
"registry_version": 4,
|
||||
"status": "stable",
|
||||
"availability": "service-contract",
|
||||
@ -30,6 +32,48 @@
|
||||
"process_priority": "normal"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "arubaoscx-disk-image-genericx86-p4-20240129204649.vmdk",
|
||||
"version": "10.13.1000",
|
||||
"md5sum": "a1a24b15e3b8a09b0c0f14bdfacc4a75",
|
||||
"filesize": 395342848,
|
||||
"download_url": "https://networkingsupport.hpe.com"
|
||||
},
|
||||
{
|
||||
"filename": "arubaoscx-disk-image-genericx86-p4-20231110145644.vmdk",
|
||||
"version": "10.13.0005",
|
||||
"md5sum": "427fd4580e2ee3eac55a9e7d629d1375",
|
||||
"filesize": 394995200,
|
||||
"download_url": "https://networkingsupport.hpe.com"
|
||||
},
|
||||
{
|
||||
"filename": "arubaoscx-disk-image-genericx86-p4-20230810165021.vmdk",
|
||||
"version": "10.12.1000",
|
||||
"md5sum": "ea89f94dda9d28bf583dc35e0299b106",
|
||||
"filesize": 384622080,
|
||||
"download_url": "https://networkingsupport.hpe.com"
|
||||
},
|
||||
{
|
||||
"filename": "arubaoscx-disk-image-genericx86-p4-20230531220439.vmdk",
|
||||
"version": "10.12.0006",
|
||||
"md5sum": "c4f80fecd02ef93b431b75dd610e0063",
|
||||
"filesize": 384638464,
|
||||
"download_url": "https://asp.arubanetworks.com/"
|
||||
},
|
||||
{
|
||||
"filename": "arubaoscx-disk-image-genericx86-p4-20221130174651.vmdk",
|
||||
"version": "10.11.0001",
|
||||
"md5sum": "ed5434173c898f47f19bfda51000611a",
|
||||
"filesize": 364597760,
|
||||
"download_url": "https://asp.arubanetworks.com/"
|
||||
},
|
||||
{
|
||||
"filename": "arubaoscx-disk-image-genericx86-p4-20220815162137.vmdk",
|
||||
"version": "10.10.1000",
|
||||
"md5sum": "40f9ddf1e12640376af443b5d982f2f6",
|
||||
"filesize": 356162560,
|
||||
"download_url": "https://asp.arubanetworks.com/"
|
||||
},
|
||||
{
|
||||
"filename": "arubaoscx-disk-image-genericx86-p4-20220616193419.vmdk",
|
||||
"version": "10.10.0002",
|
||||
@ -95,6 +139,42 @@
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "10.13.1000",
|
||||
"images": {
|
||||
"hda_disk_image": "arubaoscx-disk-image-genericx86-p4-20240129204649.vmdk"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "10.13.0005",
|
||||
"images": {
|
||||
"hda_disk_image": "arubaoscx-disk-image-genericx86-p4-20231110145644.vmdk"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "10.12.1000",
|
||||
"images": {
|
||||
"hda_disk_image": "arubaoscx-disk-image-genericx86-p4-20230810165021.vmdk"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "10.12.0006",
|
||||
"images": {
|
||||
"hda_disk_image": "arubaoscx-disk-image-genericx86-p4-20230531220439.vmdk"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "10.11.0001",
|
||||
"images": {
|
||||
"hda_disk_image": "arubaoscx-disk-image-genericx86-p4-20221130174651.vmdk"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "10.10.1000",
|
||||
"images": {
|
||||
"hda_disk_image": "arubaoscx-disk-image-genericx86-p4-20220815162137.vmdk"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "10.10.0002",
|
||||
"images": {
|
||||
|
49
gns3server/appliances/asterfusion-vAsterNOS.gns3a
Normal file
49
gns3server/appliances/asterfusion-vAsterNOS.gns3a
Normal file
@ -0,0 +1,49 @@
|
||||
{
|
||||
"appliance_id": "e8001e2b-8ef3-44eb-ace5-79f68f3773e8",
|
||||
"name": "Asterfusion vAsterNOS",
|
||||
"category": "multilayer_switch",
|
||||
"description": "AsterNOS is the core technology of Asterfusion's one-stop turnkey SONiC solution for cloud, enterprise and AI. As an enterprise ready SONiC distribution, AsterNOS features rich functionality enhancement such as MC-LAG, VXLAN, BGP EVPN-Multihoming, RoCEv2(Easy RoCE), and more, making it powerful and easy-to-use in a variety of production scenarios. Currently, AsterNOS is compatible with top commercial switching chips (e.g. Marvell Teralynx, Prestera Falcon/Aldrin/Alleycat, Broadcom Tomahawk/Trident, Intel Tofino and some of NVIDIA's chips.)Through AsterNOS's rich L2/L3 features and enhancements in virtualization and management, cloud architecture built with Asterfusion switching families from 1G-800G (or other standard whitebox switches) can scale to tens of thousands of compute and storage nodes, working smoothly both in underlay nework and overlay cloud fabric with OpenStack integrated, supporting ultra low latency lossless RoCE network in AIGC, distributed storage , or building easily managed access clusters for campus networks.NOTICE: This appliance file is a virtualized version of AsterNOS and is intended to be used only to experience the basic functionality and industry standard CLI (Klish), not for official software testing. For more information about AsterNOS commercial version, please feel free to contact us via Email: bd@cloudswit.ch",
|
||||
"vendor_name": "Asterfusion",
|
||||
"vendor_url": "https://cloudswit.ch/",
|
||||
"vendor_logo_url": "https://raw.githubusercontent.com/GNS3/gns3-registry/master/vendor-logos/asterfusion.png",
|
||||
"documentation_url": "https://help.cloudswit.ch/portal/en/kb/articles/vasternos",
|
||||
"product_name": "vAsterNOS",
|
||||
"product_url": "https://cloudswit.ch/product/sonic-enterprise-distribution",
|
||||
"registry_version": 4,
|
||||
"status": "experimental",
|
||||
"maintainer": "Asterfusion",
|
||||
"maintainer_email": "bd@cloudswit.ch",
|
||||
"usage": "The login is admin, passwd asteros",
|
||||
"symbol": "asterfusion-vAsterNOS.svg",
|
||||
"first_port_name": "eth0",
|
||||
"port_name_format": "Ethernet{0}",
|
||||
"qemu": {
|
||||
"adapter_type": "e1000",
|
||||
"adapters": 10,
|
||||
"ram": 4096,
|
||||
"cpus": 2,
|
||||
"hda_disk_interface": "virtio",
|
||||
"arch": "x86_64",
|
||||
"console_type": "telnet",
|
||||
"boot_priority": "d",
|
||||
"kvm": "require"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "vAsterNOS-V3.1.img",
|
||||
"version": "V3.1",
|
||||
"md5sum": "c323c9c3f60e1a93eca2acdc5034b85c",
|
||||
"filesize": 2724659200,
|
||||
"download_url": "https://drive.cloudswitch.io/external/8ae2e3932ad8bb2ec30dd25be415d288ff3e4a949c557c6bd48ac6e6265bcfc1"
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "V3.1",
|
||||
"images": {
|
||||
"hda_disk_image": "vAsterNOS-V3.1.img"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -8,7 +8,7 @@
|
||||
"documentation_url": "https://wiki.asterisk.org/wiki/display/AST/Installing+AsteriskNOW",
|
||||
"product_name": "AsteriskNOW / FreePBX",
|
||||
"product_url": "http://www.asterisk.org/downloads/asterisknow",
|
||||
"registry_version": 3,
|
||||
"registry_version": 4,
|
||||
"status": "stable",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
|
@ -8,7 +8,7 @@
|
||||
"documentation_url": "http://www.bigswitch.com/support",
|
||||
"product_name": "Big Cloud Fabric",
|
||||
"product_url": "http://www.bigswitch.com/sdn-products/big-cloud-fabrictm",
|
||||
"registry_version": 3,
|
||||
"registry_version": 4,
|
||||
"status": "experimental",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
|
@ -7,11 +7,11 @@
|
||||
"vendor_url": "http://bird.network.cz/",
|
||||
"documentation_url": "http://bird.network.cz/?get_doc&f=bird.html",
|
||||
"product_name": "BIRD internet routing daemon",
|
||||
"registry_version": 3,
|
||||
"registry_version": 4,
|
||||
"status": "stable",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
"usage": "Configure interfaces in /opt/bootlocal.sh, BIRD configuration is done in /usr/local/etc/bird",
|
||||
"usage": "\n*** BIRD v1 is end-of-life ***\nPlease use the BIRD2 appliance.\n\nConfigure interfaces in /opt/bootlocal.sh, BIRD configuration is done in /usr/local/etc/bird",
|
||||
"qemu": {
|
||||
"adapter_type": "e1000",
|
||||
"adapters": 4,
|
||||
|
57
gns3server/appliances/bird2.gns3a
Normal file
57
gns3server/appliances/bird2.gns3a
Normal file
@ -0,0 +1,57 @@
|
||||
{
|
||||
"appliance_id": "8fecbf89-5cd1-4aea-b735-5f36cf0efbb7",
|
||||
"name": "BIRD2",
|
||||
"category": "router",
|
||||
"description": "The BIRD project aims to develop a fully functional dynamic IP routing daemon primarily targeted on (but not limited to) Linux, FreeBSD and other UNIX-like systems and distributed under the GNU General Public License.",
|
||||
"vendor_name": "CZ.NIC Labs",
|
||||
"vendor_url": "https://bird.network.cz",
|
||||
"documentation_url": "https://bird.network.cz/?get_doc&f=bird.html&v=20",
|
||||
"product_name": "BIRD internet routing daemon",
|
||||
"registry_version": 4,
|
||||
"status": "stable",
|
||||
"maintainer": "Bernhard Ehlers",
|
||||
"maintainer_email": "dev-ehlers@mailbox.org",
|
||||
"usage": "Username:\tgns3\nPassword:\tgns3\nTo become root, use \"sudo -s\".\n\nNetwork configuration:\nsudo nano /etc/network/interfaces\nsudo systemctl restart networking\n\nBIRD:\nRestart: sudo systemctl restart bird\nReconfigure: birdc configure",
|
||||
"port_name_format": "eth{0}",
|
||||
"qemu": {
|
||||
"adapter_type": "virtio-net-pci",
|
||||
"adapters": 4,
|
||||
"ram": 512,
|
||||
"hda_disk_interface": "scsi",
|
||||
"arch": "x86_64",
|
||||
"console_type": "telnet",
|
||||
"kvm": "allow"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "bird2-debian-2.14.qcow2",
|
||||
"version": "2.14",
|
||||
"md5sum": "029cf1756201ee79497c169502b08b88",
|
||||
"filesize": 303717376,
|
||||
"download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/",
|
||||
"direct_download_url": "https://downloads.sourceforge.net/project/gns-3/Qemu%20Appliances/bird2-debian-2.14.qcow2"
|
||||
},
|
||||
{
|
||||
"filename": "bird2-debian-2.0.12.qcow2",
|
||||
"version": "2.0.12",
|
||||
"md5sum": "435218a2e90cba921cc7fde1d64a9419",
|
||||
"filesize": 287965184,
|
||||
"download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/",
|
||||
"direct_download_url": "https://downloads.sourceforge.net/project/gns-3/Qemu%20Appliances/bird2-debian-2.0.12.qcow2"
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "2.14",
|
||||
"images": {
|
||||
"hda_disk_image": "bird2-debian-2.14.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "2.0.12",
|
||||
"images": {
|
||||
"hda_disk_image": "bird2-debian-2.0.12.qcow2"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -6,7 +6,7 @@
|
||||
"vendor_name": "Brocade",
|
||||
"vendor_url": "https://www.brocade.com",
|
||||
"product_name": "Virtual ADX",
|
||||
"registry_version": 3,
|
||||
"registry_version": 4,
|
||||
"status": "experimental",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
|
@ -8,7 +8,7 @@
|
||||
"documentation_url": "http://www.brocade.com/en/products-services/software-networking/network-functions-virtualization/vrouter.html",
|
||||
"product_name": "vRouter",
|
||||
"product_url": "http://www.brocade.com/en/products-services/software-networking/network-functions-virtualization/vrouter.html",
|
||||
"registry_version": 3,
|
||||
"registry_version": 4,
|
||||
"status": "stable",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
|
@ -8,7 +8,7 @@
|
||||
"documentation_url": "http://www.brocade.com/en/products-services/software-networking/application-delivery-controllers/virtual-traffic-manager.html",
|
||||
"product_name": "vTM DE",
|
||||
"product_url": "http://www.brocade.com/en/products-services/software-networking/application-delivery-controllers/virtual-traffic-manager.html",
|
||||
"registry_version": 3,
|
||||
"registry_version": 4,
|
||||
"status": "stable",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
|
@ -6,7 +6,7 @@
|
||||
"vendor_name": "Olivier Cochard-Labbe",
|
||||
"vendor_url": "https://bsdrp.net/",
|
||||
"product_name": "BSDRP",
|
||||
"registry_version": 3,
|
||||
"registry_version": 4,
|
||||
"status": "stable",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
|
@ -8,11 +8,11 @@
|
||||
"documentation_url": "https://wiki.centos.org/Documentation",
|
||||
"product_name": "Centos Cloud",
|
||||
"product_url": "https://wiki.centos.org/Cloud",
|
||||
"registry_version": 3,
|
||||
"registry_version": 4,
|
||||
"status": "stable",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
"usage": "Username: centos\nPassword: centos",
|
||||
"usage": "Username: centos or cloud-user\nPassword: centos",
|
||||
"port_name_format": "Ethernet{0}",
|
||||
"qemu": {
|
||||
"adapter_type": "virtio-net-pci",
|
||||
@ -23,39 +23,73 @@
|
||||
"console_type": "telnet",
|
||||
"boot_priority": "c",
|
||||
"kvm": "require",
|
||||
"options": "-nographic"
|
||||
"options": "-cpu host -nographic"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "CentOS-7-x86_64-GenericCloud-2111.qcow2",
|
||||
"version": "7 (2111)",
|
||||
"md5sum": "730b8662695831670721c8245be61dac",
|
||||
"filesize": 897384448,
|
||||
"download_url": "https://cloud.centos.org/centos/7/images/CentOS-7-x86_64-GenericCloud-2111.qcow2"
|
||||
"filename": "CentOS-Stream-GenericCloud-9-20230704.1.x86_64.qcow2",
|
||||
"version": "Stream-9 (20230704.1)",
|
||||
"md5sum": "e04511e019325a97837edd9eafe02b48",
|
||||
"filesize": 1087868416,
|
||||
"download_url": "https://cloud.centos.org/centos/9-stream/x86_64/images",
|
||||
"direct_download_url": "https://cloud.centos.org/centos/9-stream/x86_64/images/CentOS-Stream-GenericCloud-9-20230704.1.x86_64.qcow2"
|
||||
},
|
||||
{
|
||||
"filename": "CentOS-7-x86_64-GenericCloud-1809.qcow2",
|
||||
"version": "7 (1809)",
|
||||
"md5sum": "da79108d1324b27bd1759362b82fbe40",
|
||||
"filesize": 914948096,
|
||||
"download_url": "https://cloud.centos.org/centos/7/images/CentOS-7-x86_64-GenericCloud-1809.qcow2"
|
||||
"filename": "CentOS-Stream-GenericCloud-8-20230710.0.x86_64.qcow2",
|
||||
"version": "Stream-8 (20230710.0)",
|
||||
"md5sum": "83e02ce98c29753c86fb7be7d802aa75",
|
||||
"filesize": 1676164096,
|
||||
"download_url": "https://cloud.centos.org/centos/8-stream/x86_64/images",
|
||||
"direct_download_url": "https://cloud.centos.org/centos/8-stream/x86_64/images/CentOS-Stream-GenericCloud-8-20230710.0.x86_64.qcow2"
|
||||
},
|
||||
{
|
||||
"filename": "CentOS-8-GenericCloud-8.4.2105-20210603.0.x86_64.qcow2",
|
||||
"version": "8.4 (2105)",
|
||||
"md5sum": "032eed270415526546eac07628905a62",
|
||||
"filesize": 1309652992,
|
||||
"download_url": "https://cloud.centos.org/centos/8/x86_64/images/CentOS-8-GenericCloud-8.4.2105-20210603.0.x86_64.qcow2"
|
||||
"download_url": "https://cloud.centos.org/centos/8/x86_64/images",
|
||||
"direct_download_url": "https://cloud.centos.org/centos/8/x86_64/images/CentOS-8-GenericCloud-8.4.2105-20210603.0.x86_64.qcow2"
|
||||
},
|
||||
{
|
||||
"filename": "CentOS-7-x86_64-GenericCloud-2111.qcow2",
|
||||
"version": "7 (2111)",
|
||||
"md5sum": "730b8662695831670721c8245be61dac",
|
||||
"filesize": 897384448,
|
||||
"download_url": "https://cloud.centos.org/centos/7/images",
|
||||
"direct_download_url": "https://cloud.centos.org/centos/7/images/CentOS-7-x86_64-GenericCloud-2111.qcow2"
|
||||
},
|
||||
{
|
||||
"filename": "CentOS-7-x86_64-GenericCloud-1809.qcow2",
|
||||
"version": "7 (1809)",
|
||||
"md5sum": "da79108d1324b27bd1759362b82fbe40",
|
||||
"filesize": 914948096,
|
||||
"download_url": "https://cloud.centos.org/centos/7/images",
|
||||
"direct_download_url": "https://cloud.centos.org/centos/7/images/CentOS-7-x86_64-GenericCloud-1809.qcow2"
|
||||
},
|
||||
{
|
||||
"filename": "centos-cloud-init-data.iso",
|
||||
"version": "1.0",
|
||||
"md5sum": "15ca60c12db6d13b8eeae1a19613fd6e",
|
||||
"filesize": 378880,
|
||||
"download_url": "https://github.com/asenci/gns3-centos-cloud-init-data/raw/master/centos-cloud-init-data.iso"
|
||||
"version": "1.1",
|
||||
"md5sum": "59ea8223fd659d8bce9081ff175912e9",
|
||||
"filesize": 374784,
|
||||
"download_url": "https://github.com/GNS3/gns3-registry/tree/master/cloud-init/centos-cloud",
|
||||
"direct_download_url": "https://github.com/GNS3/gns3-registry/raw/master/cloud-init/centos-cloud/centos-cloud-init-data.iso"
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "Stream-9 (20230704.1)",
|
||||
"images": {
|
||||
"hda_disk_image": "CentOS-Stream-GenericCloud-9-20230704.1.x86_64.qcow2",
|
||||
"cdrom_image": "centos-cloud-init-data.iso"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Stream-8 (20230710.0)",
|
||||
"images": {
|
||||
"hda_disk_image": "CentOS-Stream-GenericCloud-8-20230710.0.x86_64.qcow2",
|
||||
"cdrom_image": "centos-cloud-init-data.iso"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "8.4 (2105)",
|
||||
"images": {
|
||||
|
@ -5,8 +5,9 @@
|
||||
"description": "The chromium browser",
|
||||
"vendor_name": "Chromium",
|
||||
"vendor_url": "https://www.chromium.org/",
|
||||
"vendor_logo_url": "https://raw.githubusercontent.com/GNS3/gns3-registry/master/vendor-logos/Chromium.jpg",
|
||||
"product_name": "Chromium",
|
||||
"registry_version": 3,
|
||||
"registry_version": 4,
|
||||
"status": "stable",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
|
@ -7,7 +7,7 @@
|
||||
"vendor_url": "http://www.cisco.com",
|
||||
"documentation_url": "http://www.cisco.com/c/en/us/support/index.html",
|
||||
"product_name": "1700",
|
||||
"registry_version": 3,
|
||||
"registry_version": 4,
|
||||
"status": "experimental",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
|
@ -7,7 +7,7 @@
|
||||
"vendor_url": "http://www.cisco.com",
|
||||
"documentation_url": "http://www.cisco.com/c/en/us/support/index.html",
|
||||
"product_name": "2600",
|
||||
"registry_version": 3,
|
||||
"registry_version": 4,
|
||||
"status": "experimental",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
|
@ -7,7 +7,7 @@
|
||||
"vendor_url": "http://www.cisco.com",
|
||||
"documentation_url": "http://www.cisco.com/c/en/us/support/index.html",
|
||||
"product_name": "2691",
|
||||
"registry_version": 3,
|
||||
"registry_version": 4,
|
||||
"status": "experimental",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
|
@ -7,7 +7,7 @@
|
||||
"vendor_url": "http://www.cisco.com",
|
||||
"documentation_url": "http://www.cisco.com/c/en/us/support/index.html",
|
||||
"product_name": "3620",
|
||||
"registry_version": 3,
|
||||
"registry_version": 4,
|
||||
"status": "experimental",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
|
@ -7,7 +7,7 @@
|
||||
"vendor_url": "http://www.cisco.com",
|
||||
"documentation_url": "http://www.cisco.com/c/en/us/support/index.html",
|
||||
"product_name": "3640",
|
||||
"registry_version": 3,
|
||||
"registry_version": 4,
|
||||
"status": "experimental",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
|
@ -7,7 +7,7 @@
|
||||
"vendor_url": "http://www.cisco.com",
|
||||
"documentation_url": "http://www.cisco.com/c/en/us/support/index.html",
|
||||
"product_name": "3660",
|
||||
"registry_version": 3,
|
||||
"registry_version": 4,
|
||||
"status": "experimental",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
|
@ -7,7 +7,7 @@
|
||||
"vendor_url": "http://www.cisco.com",
|
||||
"documentation_url": "http://www.cisco.com/c/en/us/support/index.html",
|
||||
"product_name": "3725",
|
||||
"registry_version": 3,
|
||||
"registry_version": 4,
|
||||
"status": "experimental",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
@ -21,14 +21,14 @@
|
||||
"images": [
|
||||
{
|
||||
"filename": "c3725-adventerprisek9-mz.124-15.T14.image",
|
||||
"version": "124-25.T14",
|
||||
"version": "124-15.T14",
|
||||
"md5sum": "64f8c427ed48fd21bd02cf1ff254c4eb",
|
||||
"filesize": 97859480
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "124-25.T14",
|
||||
"name": "124-15.T14",
|
||||
"idlepc": "0x60c09aa0",
|
||||
"images": {
|
||||
"image": "c3725-adventerprisek9-mz.124-15.T14.image"
|
||||
|
@ -7,7 +7,7 @@
|
||||
"vendor_url": "http://www.cisco.com",
|
||||
"documentation_url": "http://www.cisco.com/c/en/us/support/routers/3745-multiservice-access-router/model.html",
|
||||
"product_name": "3745",
|
||||
"registry_version": 3,
|
||||
"registry_version": 4,
|
||||
"status": "experimental",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
|
@ -7,7 +7,7 @@
|
||||
"vendor_url": "http://www.cisco.com",
|
||||
"documentation_url": "http://www.cisco.com/c/en/us/products/routers/7200-series-routers/index.html",
|
||||
"product_name": "7200",
|
||||
"registry_version": 3,
|
||||
"registry_version": 4,
|
||||
"status": "experimental",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
@ -21,6 +21,18 @@
|
||||
"npe": "npe-400"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "c7200-adventerprisek9-mz.153-3.XB12.image",
|
||||
"version": "153-3.XB12",
|
||||
"md5sum": "3d234a3793331c972776354531f87221",
|
||||
"filesize": 131471340
|
||||
},
|
||||
{
|
||||
"filename": "c7200-advipservicesk9-mz.152-4.S5.image",
|
||||
"version": "152-4.S5",
|
||||
"md5sum": "cbbbea66a253f1dac0fcf81274dc778d",
|
||||
"filesize": 87756936
|
||||
},
|
||||
{
|
||||
"filename": "c7200-adventerprisek9-mz.124-24.T5.image",
|
||||
"version": "124-24.T5",
|
||||
@ -29,6 +41,20 @@
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "153-3.XB12",
|
||||
"idlepc": "0x60630d08",
|
||||
"images": {
|
||||
"image": "c7200-adventerprisek9-mz.153-3.XB12.image"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "152-4.S5",
|
||||
"idlepc": "0x62cc930c",
|
||||
"images": {
|
||||
"image": "c7200-advipservicesk9-mz.152-4.S5.image"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "124-24.T5",
|
||||
"idlepc": "0x606df838",
|
||||
|
@ -7,7 +7,7 @@
|
||||
"vendor_url": "http://www.cisco.com/",
|
||||
"product_name": "ASA",
|
||||
"product_url": "http://www.cisco.com/c/en/us/products/security/adaptive-security-appliance-asa-software/index.html",
|
||||
"registry_version": 3,
|
||||
"registry_version": 4,
|
||||
"status": "broken",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
|
@ -8,7 +8,7 @@
|
||||
"documentation_url": "http://www.cisco.com/c/en/us/support/security/virtual-adaptive-security-appliance-firewall/products-installation-guides-list.html",
|
||||
"product_name": "ASAv",
|
||||
"product_url": "http://www.cisco.com/c/en/us/products/security/virtual-adaptive-security-appliance-firewall/index.html",
|
||||
"registry_version": 3,
|
||||
"registry_version": 4,
|
||||
"status": "stable",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
@ -26,7 +26,14 @@
|
||||
"kvm": "require"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
{
|
||||
"filename": "asav9-18-2.qcow2",
|
||||
"version": "9.18.2 CML",
|
||||
"md5sum": "6f10fe106edfad9163625770a47a6b73",
|
||||
"filesize": 340262912,
|
||||
"download_url": "https://learningnetworkstore.cisco.com/cisco-modeling-labs-personal/cisco-modeling-labs-personal/CML-PERSONAL.html"
|
||||
},
|
||||
{
|
||||
"filename": "asav9-16-2.qcow2",
|
||||
"version": "9.16.2 CML",
|
||||
"md5sum": "1f8db97063a7f738fddc81ac880a906c",
|
||||
@ -119,6 +126,12 @@
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "9.18.2 CML",
|
||||
"images": {
|
||||
"hda_disk_image": "asav9-18-2.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "9.16.2 CML",
|
||||
"images": {
|
||||
|
@ -8,7 +8,7 @@
|
||||
"documentation_url": "https://www.cisco.com/c/en/us/td/docs/routers/C8000V/Configuration/c8000v-installation-configuration-guide.html",
|
||||
"product_name": "c8000v",
|
||||
"product_url": "https://www.cisco.com/c/en/us/support/routers/catalyst-8000v-edge-software/series.html",
|
||||
"registry_version": 3,
|
||||
"registry_version": 4,
|
||||
"status": "stable",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
@ -24,10 +24,31 @@
|
||||
"kvm": "require"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "c8000v-universalk9_8G_serial.17.09.04a.qcow2",
|
||||
"version": "17.09.04a 8G",
|
||||
"md5sum": "d5dcba52b522f41648538737af80cdf9",
|
||||
"filesize": 1858142208,
|
||||
"download_url": "https://software.cisco.com/download/home/286327102/type/282046477/release/Cupertino-17.9.4a"
|
||||
},
|
||||
{
|
||||
"filename": "c8000v-universalk9_8G_serial.17.09.01a.qcow2",
|
||||
"version": "17.09.01a 8G",
|
||||
"md5sum": "a10ae2c4d71f4eb611bc4d83ad7709f0",
|
||||
"filesize": 1856634880,
|
||||
"download_url": "https://software.cisco.com/download/home/286327102/type/282046477/release/Cupertino-17.9.1a"
|
||||
},
|
||||
{
|
||||
"filename": "c8000v-universalk9_8G_serial.17.06.05.qcow2",
|
||||
"version": "17.06.05 8G",
|
||||
"md5sum": "aeb15ab8e1cbd0cd76f7260a81442f98",
|
||||
"filesize": 1777795072,
|
||||
"download_url": "https://software.cisco.com/download/home/286327102/type/282046477/release/Bengaluru-17.6.5"
|
||||
},
|
||||
{
|
||||
"filename": "c8000v-universalk9_8G_serial.17.06.01a.qcow2",
|
||||
"version": "17.06.01a 8G",
|
||||
"md5sum": "d8b8ae633d953ec1b6d8f18a09a4f4e7",
|
||||
"md5sum": "e278fa644295c703976a86f7f1c1cd65",
|
||||
"filesize": 1595277312,
|
||||
"download_url": "https://software.cisco.com/download/home/286327102/type/282046477/release/Bengaluru-17.6.1a"
|
||||
},
|
||||
@ -47,6 +68,24 @@
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "17.09.04a 8G",
|
||||
"images": {
|
||||
"hda_disk_image": "c8000v-universalk9_8G_serial.17.09.04a.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "17.09.01a 8G",
|
||||
"images": {
|
||||
"hda_disk_image": "c8000v-universalk9_8G_serial.17.09.01a.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "17.06.05 8G",
|
||||
"images": {
|
||||
"hda_disk_image": "c8000v-universalk9_8G_serial.17.06.05.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "17.06.01a 8G",
|
||||
"images": {
|
||||
|
58
gns3server/appliances/cisco-cat9k.gns3a
Normal file
58
gns3server/appliances/cisco-cat9k.gns3a
Normal file
@ -0,0 +1,58 @@
|
||||
{
|
||||
"appliance_id": "57a85f0e-b8ae-4820-bd2b-816b2cceb842",
|
||||
"name": "Cisco CAT IOS-XE 9000v",
|
||||
"category": "multilayer_switch",
|
||||
"description": "Cisco IOS-XE 9000v. This appliance requires 16GB of memory to run! Recommend 2 or more vCPUs for faster boot performance",
|
||||
"vendor_name": "Cisco",
|
||||
"vendor_url": "http://www.cisco.com/",
|
||||
"documentation_url": "https://developer.cisco.com/docs/modeling-labs/2-5/#!cml-release-notes",
|
||||
"product_name": "Cisco CAT IOS-XE 9000v",
|
||||
"product_url": "http://virl.cisco.com/",
|
||||
"registry_version": 4,
|
||||
"status": "experimental",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
"usage": "There is no default configuration present. Virtual Switch and Interfaces may take several minutes to be usable after appliance boot.\n\nOnly has basic Layer 2 switching features. Will need to enable advance features and reboot to gain access to things like BGP, etc... \n\nconfigure terminal \nlicense boot level network-advantage addon dna-advantage \nend \nwrite memory \nreload",
|
||||
"first_port_name": "GigabitEthernet0/0",
|
||||
"port_name_format": "GigabitEthernet1/0/{port1}",
|
||||
"qemu": {
|
||||
"adapter_type": "virtio-net-pci",
|
||||
"adapters": 9,
|
||||
"ram": 24576,
|
||||
"cpus": 4,
|
||||
"hda_disk_interface": "virtio",
|
||||
"arch": "x86_64",
|
||||
"console_type": "telnet",
|
||||
"kvm": "require"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "cat9kv-prd-17.12.01prd9.qcow2",
|
||||
"version": "17.12(1)",
|
||||
"md5sum": "e587e92186f42bdf69d7fa27f34425f7",
|
||||
"filesize": 2739404800,
|
||||
"download_url": "https://learningnetworkstore.cisco.com/myaccount"
|
||||
},
|
||||
{
|
||||
"filename": "cat9kv-prd-17.10.01prd7.qcow2",
|
||||
"version": "17.10(1)",
|
||||
"md5sum": "ffdbace33d31deae33e2a920a96b79ef",
|
||||
"filesize": 2155806720,
|
||||
"download_url": "https://learningnetworkstore.cisco.com/myaccount"
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "17.12(1)",
|
||||
"images": {
|
||||
"hda_disk_image": "cat9kv-prd-17.12.01prd9.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "17.10(1)",
|
||||
"images": {
|
||||
"hda_disk_image": "cat9kv-prd-17.10.01prd7.qcow2"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -8,7 +8,7 @@
|
||||
"documentation_url": "http://www.cisco.com/c/en/us/support/routers/cloud-services-router-1000v-series/products-installation-and-configuration-guides-list.html",
|
||||
"product_name": "CSR1000v",
|
||||
"product_url": "http://www.cisco.com/c/en/us/support/routers/cloud-services-router-1000v-series/tsd-products-support-series-home.html",
|
||||
"registry_version": 3,
|
||||
"registry_version": 4,
|
||||
"status": "stable",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
@ -24,6 +24,27 @@
|
||||
"kvm": "require"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "csr1000v-universalk9.17.03.08a-serial.qcow2",
|
||||
"version": "17.03.08a",
|
||||
"md5sum": "6abece87d6db99d9fd6917203e253f91",
|
||||
"filesize": 1421410304,
|
||||
"download_url": "https://software.cisco.com/download/home/286323714/type/282046477/release/Amsterdam-17.3.8a"
|
||||
},
|
||||
{
|
||||
"filename": "csr1000v-universalk9.17.03.06-serial.qcow2",
|
||||
"version": "17.03.06",
|
||||
"md5sum": "086ab9bef6e66de847af0da3910c60e8",
|
||||
"filesize": 1422000128,
|
||||
"download_url": "https://software.cisco.com/download/home/286323714/type/282046477/release/Amsterdam-17.3.6"
|
||||
},
|
||||
{
|
||||
"filename": "csr1000v-ucmk9.16.12.5-serial.qcow2",
|
||||
"version": "16.12.05",
|
||||
"md5sum": "5c0cc217f0f0648407b34b11a1dd5b8e",
|
||||
"filesize": 844103680,
|
||||
"download_url": "https://software.cisco.com/download/home/286323714/type/286321980/release/16.12.5"
|
||||
},
|
||||
{
|
||||
"filename": "csr1000v-universalk9.16.12.03-serial.qcow2",
|
||||
"version": "16.12.3",
|
||||
@ -159,7 +180,25 @@
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "17.03.08a",
|
||||
"images": {
|
||||
"hda_disk_image": "csr1000v-universalk9.17.03.08a-serial.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "17.03.06",
|
||||
"images": {
|
||||
"hda_disk_image": "csr1000v-universalk9.17.03.06-serial.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "16.12.05",
|
||||
"images": {
|
||||
"hda_disk_image": "csr1000v-ucmk9.16.12.5-serial.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "16.12.3",
|
||||
"images": {
|
||||
"hda_disk_image": "csr1000v-universalk9.16.12.03-serial.qcow2"
|
||||
|
@ -8,7 +8,7 @@
|
||||
"documentation_url": "http://www.cisco.com/c/en/us/support/cloud-systems-management/data-center-network-manager-10/model.html",
|
||||
"product_name": "DCNM",
|
||||
"product_url": "http://www.cisco.com/c/en/us/products/cloud-systems-management/prime-data-center-network-manager/index.html",
|
||||
"registry_version": 3,
|
||||
"registry_version": 4,
|
||||
"status": "stable",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
|
@ -77,6 +77,13 @@
|
||||
"md5sum": "4cf5b7fd68075b6f7ee0dd41a4029ca0",
|
||||
"filesize": 2150017536,
|
||||
"download_url": "https://software.cisco.com/download/"
|
||||
},
|
||||
{
|
||||
"filename": "Cisco_Firepower_Management_Center_Virtual-6.2.2-81.qcow2",
|
||||
"version": "6.2.2 (81)",
|
||||
"md5sum": "2f75c9c6c18a6fbb5516f6f451aef3a4",
|
||||
"filesize": 2112356352,
|
||||
"download_url": "https://software.cisco.com/download/"
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
@ -121,6 +128,12 @@
|
||||
"images": {
|
||||
"hda_disk_image": "Cisco_Firepower_Management_Center_Virtual_VMware-6.2.1-342-disk1.vmdk"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "6.2.2 (81)",
|
||||
"images": {
|
||||
"hda_disk_image": "Cisco_Firepower_Management_Center_Virtual-6.2.2-81.qcow2"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -7,7 +7,7 @@
|
||||
"vendor_url": "http://www.cisco.com/",
|
||||
"product_name": "IOSv",
|
||||
"product_url": "http://virl.cisco.com/",
|
||||
"registry_version": 3,
|
||||
"registry_version": 4,
|
||||
"status": "stable",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
@ -32,6 +32,20 @@
|
||||
"download_url": "https://sourceforge.net/projects/gns-3/files",
|
||||
"direct_download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/IOSv_startup_config.img/download"
|
||||
},
|
||||
{
|
||||
"filename": "vios-adventerprisek9-m.spa.159-3.m8.qcow2",
|
||||
"version": "15.9(3)M8",
|
||||
"md5sum": "8d93a185c2fa778178a933f20b02150a",
|
||||
"filesize": 57319424,
|
||||
"download_url": "https://learningnetworkstore.cisco.com/myaccount"
|
||||
},
|
||||
{
|
||||
"filename": "vios-adventerprisek9-m.spa.159-3.m6.qcow2",
|
||||
"version": "15.9(3)M6",
|
||||
"md5sum": "49a6977977263b2774bebc56e4e678ff",
|
||||
"filesize": 57309696,
|
||||
"download_url": "https://learningnetworkstore.cisco.com/myaccount"
|
||||
},
|
||||
{
|
||||
"filename": "vios-adventerprisek9-m.spa.159-3.m4.qcow2",
|
||||
"version": "15.9(3)M4",
|
||||
@ -90,6 +104,20 @@
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "15.9(3)M8",
|
||||
"images": {
|
||||
"hda_disk_image": "vios-adventerprisek9-m.spa.159-3.m8.qcow2",
|
||||
"hdb_disk_image": "IOSv_startup_config.img"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "15.9(3)M6",
|
||||
"images": {
|
||||
"hda_disk_image": "vios-adventerprisek9-m.spa.159-3.m6.qcow2",
|
||||
"hdb_disk_image": "IOSv_startup_config.img"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "15.9(3)M4",
|
||||
"images": {
|
||||
|
@ -7,7 +7,7 @@
|
||||
"vendor_url": "http://www.cisco.com/",
|
||||
"product_name": "IOSvL2",
|
||||
"product_url": "http://virl.cisco.com/",
|
||||
"registry_version": 3,
|
||||
"registry_version": 4,
|
||||
"status": "stable",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
|
@ -8,7 +8,7 @@
|
||||
"documentation_url": "http://www.cisco.com/c/en/us/td/docs/ios_xr_sw/ios_xrv/release/notes/xrv-rn.html",
|
||||
"product_name": "IOS XRv",
|
||||
"product_url": "http://virl.cisco.com/",
|
||||
"registry_version": 3,
|
||||
"registry_version": 4,
|
||||
"status": "stable",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
|
@ -8,7 +8,7 @@
|
||||
"documentation_url": "http://www.cisco.com/c/en/us/td/docs/ios_xr_sw/ios_xrv/release/notes/xrv-rn.html",
|
||||
"product_name": "IOS XRv 9000",
|
||||
"product_url": "http://virl.cisco.com/",
|
||||
"registry_version": 3,
|
||||
"registry_version": 4,
|
||||
"status": "experimental",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
@ -26,6 +26,13 @@
|
||||
"options": "-smp 4 -cpu host"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "xrv9k-fullk9-x-7.7.1.qcow2",
|
||||
"version": "7.7.1",
|
||||
"md5sum": "682fff40d2ff373d8da3342906553b54",
|
||||
"filesize": 1643905024,
|
||||
"download_url": "https://software.cisco.com/download/home/286288939/type/280805694/release/7.1.1"
|
||||
},
|
||||
{
|
||||
"filename": "xrv9k-fullk9-x-7.1.1.qcow2",
|
||||
"version": "7.1.1",
|
||||
@ -105,6 +112,12 @@
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "7.7.1",
|
||||
"images": {
|
||||
"hda_disk_image": "xrv9k-fullk9-x-7.7.1.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "7.1.1",
|
||||
"images": {
|
||||
|
@ -6,7 +6,7 @@
|
||||
"vendor_name": "Cisco",
|
||||
"vendor_url": "http://www.cisco.com",
|
||||
"product_name": "Cisco IOU L2",
|
||||
"registry_version": 3,
|
||||
"registry_version": 4,
|
||||
"status": "experimental",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
@ -18,6 +18,12 @@
|
||||
"startup_config": "iou_l2_base_startup-config.txt"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "x86_64_crb_linux_l2-adventerprisek9-ms.bin",
|
||||
"version": "17.12.1",
|
||||
"md5sum": "2b5055e4cef8fd257416d74a94adb626",
|
||||
"filesize": 240355720
|
||||
},
|
||||
{
|
||||
"filename": "i86bi-linux-l2-ipbasek9-15.1g.bin",
|
||||
"version": "15.1g",
|
||||
@ -38,6 +44,12 @@
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "17.12.1",
|
||||
"images": {
|
||||
"image": "x86_64_crb_linux_l2-adventerprisek9-ms.bin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "15.1g",
|
||||
"images": {
|
||||
|
@ -6,7 +6,7 @@
|
||||
"vendor_name": "Cisco",
|
||||
"vendor_url": "http://www.cisco.com",
|
||||
"product_name": "Cisco IOU L3",
|
||||
"registry_version": 3,
|
||||
"registry_version": 4,
|
||||
"status": "experimental",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
@ -18,6 +18,12 @@
|
||||
"startup_config": "iou_l3_base_startup-config.txt"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "x86_64_crb_linux-adventerprisek9-ms.bin",
|
||||
"version": "17.12.1",
|
||||
"md5sum": "4a2fce8de21d1831fbceffd155e41ae7",
|
||||
"filesize": 288947184
|
||||
},
|
||||
{
|
||||
"filename": "i86bi_LinuxL3-AdvEnterpriseK9-M2_157_3_May_2018.bin",
|
||||
"version": "15.7(3)M2",
|
||||
@ -38,6 +44,12 @@
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "17.12.1",
|
||||
"images": {
|
||||
"image": "x86_64_crb_linux-adventerprisek9-ms.bin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "15.7(3)M2",
|
||||
"images": {
|
||||
|
@ -7,7 +7,7 @@
|
||||
"vendor_url": "http://www.cisco.com/",
|
||||
"product_name": "NX-OSv",
|
||||
"product_url": "http://virl.cisco.com/",
|
||||
"registry_version": 3,
|
||||
"registry_version": 4,
|
||||
"status": "stable",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
|
@ -26,6 +26,13 @@
|
||||
"kvm": "require"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"filename": "nexus9300v64.10.3.1.F.qcow2",
|
||||
"version": "9300v 10.3.1.F",
|
||||
"md5sum": "a6ffd2501a5791c11cee319943b912da",
|
||||
"filesize": 2097086464,
|
||||
"download_url": "https://software.cisco.com/download/home/286312239/type/282088129/release/10.1(1)"
|
||||
},
|
||||
{
|
||||
"filename": "nexus9500v64.10.1.1.qcow2",
|
||||
"version": "9500v 10.1.1",
|
||||
@ -33,6 +40,55 @@
|
||||
"filesize": 1592000512,
|
||||
"download_url": "https://software.cisco.com/download/home/286312239/type/282088129/release/10.1(1)"
|
||||
},
|
||||
{
|
||||
"filename": "nexus9300v.10.1.1.qcow2",
|
||||
"version": "9300v 10.1.1",
|
||||
"md5sum": "4051bdb96aff6e54b72b7e3b06c9d6eb",
|
||||
"filesize": 1990983680,
|
||||
"download_url": "https://software.cisco.com/download/home/286312239/type/282088129/release/10.1(1)"
|
||||
},
|
||||
{
|
||||
"filename": "nexus9500v.9.3.13.qcow2",
|
||||
"version": "9500v 9.3.13",
|
||||
"md5sum": "bacf0f664ee34625c85a9f278b2466a2",
|
||||
"filesize": 2248409088,
|
||||
"download_url": "https://software.cisco.com/download/home/286312239/type/282088129/release/9.3(13)"
|
||||
},
|
||||
{
|
||||
"filename": "nexus9300v.9.3.13.qcow2",
|
||||
"version": "9300v 9.3.13",
|
||||
"md5sum": "d8ce30cb762df02d77ec27786a2435ad",
|
||||
"filesize": 2248343552,
|
||||
"download_url": "https://software.cisco.com/download/home/286312239/type/282088129/release/9.3(13)"
|
||||
},
|
||||
{
|
||||
"filename": "nexus9500v.9.3.12.qcow2",
|
||||
"version": "9500v 9.3.12",
|
||||
"md5sum": "452e5cb2a7a25feaa3ba0624a82ff9ca",
|
||||
"filesize": 1997996032,
|
||||
"download_url": "https://software.cisco.com/download/home/286312239/type/282088129/release/9.3(12)"
|
||||
},
|
||||
{
|
||||
"filename": "nexus9300v.9.3.12.qcow2",
|
||||
"version": "9300v 9.3.12",
|
||||
"md5sum": "7b6b5dad1001e11d6ebb54662616e9f2",
|
||||
"filesize": 1997930496,
|
||||
"download_url": "https://software.cisco.com/download/home/286312239/type/282088129/release/9.3(12)"
|
||||
},
|
||||
{
|
||||
"filename": "nexus9500v.9.3.9.qcow2",
|
||||
"version": "9500v 9.3.9",
|
||||
"md5sum": "30c25039927f89aebe73ea20d15abd6d",
|
||||
"filesize": 1980760064,
|
||||
"download_url": "https://software.cisco.com/download/home/286312239/type/282088129/release/9.3(9)"
|
||||
},
|
||||
{
|
||||
"filename": "nexus9300v.9.3.9.qcow2",
|
||||
"version": "9300v 9.3.9",
|
||||
"md5sum": "e807005cb7d2d2957b4af0e59f368b36",
|
||||
"filesize": 1980563456,
|
||||
"download_url": "https://software.cisco.com/download/home/286312239/type/282088129/release/9.3(9)"
|
||||
},
|
||||
{
|
||||
"filename": "nexus9300v.9.3.8.qcow2",
|
||||
"version": "9300v 9.3.8",
|
||||
@ -167,153 +223,209 @@
|
||||
"download_url": "https://software.cisco.com/download/"
|
||||
},
|
||||
{
|
||||
"filename": "OVMF-20160813.fd",
|
||||
"version": "16.08.13",
|
||||
"md5sum": "8ff0ef1ec56345db5b6bda1a8630e3c6",
|
||||
"filesize": 2097152,
|
||||
"download_url": "",
|
||||
"direct_download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/OVMF-20160813.fd.zip/download",
|
||||
"filename": "OVMF-edk2-stable202305.fd",
|
||||
"version": "stable202305",
|
||||
"md5sum": "6c4cf1519fec4a4b95525d9ae562963a",
|
||||
"filesize": 4194304,
|
||||
"download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/",
|
||||
"direct_download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/OVMF-edk2-stable202305.fd.zip/download",
|
||||
"compression": "zip"
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "9300v 10.3.1.F",
|
||||
"images": {
|
||||
"bios_image": "OVMF-edk2-stable202305.fd",
|
||||
"hda_disk_image": "nexus9300v64.10.3.1.F.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "9500v 10.1.1",
|
||||
"images": {
|
||||
"bios_image": "OVMF-20160813.fd",
|
||||
"bios_image": "OVMF-edk2-stable202305.fd",
|
||||
"hda_disk_image": "nexus9500v64.10.1.1.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
{
|
||||
"name": "9300v 10.1.1",
|
||||
"images": {
|
||||
"bios_image": "OVMF-edk2-stable202305.fd",
|
||||
"hda_disk_image": "nexus9300v.10.1.1.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "9500v 9.3.13",
|
||||
"images": {
|
||||
"bios_image": "OVMF-edk2-stable202305.fd",
|
||||
"hda_disk_image": "nexus9500v.9.3.13.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "9300v 9.3.13",
|
||||
"images": {
|
||||
"bios_image": "OVMF-edk2-stable202305.fd",
|
||||
"hda_disk_image": "nexus9300v.9.3.13.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "9500v 9.3.12",
|
||||
"images": {
|
||||
"bios_image": "OVMF-edk2-stable202305.fd",
|
||||
"hda_disk_image": "nexus9500v.9.3.12.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "9300v 9.3.12",
|
||||
"images": {
|
||||
"bios_image": "OVMF-edk2-stable202305.fd",
|
||||
"hda_disk_image": "nexus9300v.9.3.12.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "9500v 9.3.9",
|
||||
"images": {
|
||||
"bios_image": "OVMF-edk2-stable202305.fd",
|
||||
"hda_disk_image": "nexus9500v.9.3.9.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "9300v 9.3.9",
|
||||
"images": {
|
||||
"bios_image": "OVMF-edk2-stable202305.fd",
|
||||
"hda_disk_image": "nexus9300v.9.3.9.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "9300v 9.3.8",
|
||||
"images": {
|
||||
"bios_image": "OVMF-20160813.fd",
|
||||
"bios_image": "OVMF-edk2-stable202305.fd",
|
||||
"hda_disk_image": "nexus9300v.9.3.8.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "9500v 9.3.7",
|
||||
"images": {
|
||||
"bios_image": "OVMF-20160813.fd",
|
||||
"bios_image": "OVMF-edk2-stable202305.fd",
|
||||
"hda_disk_image": "nexus9500v.9.3.7.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "9500v 9.3.3",
|
||||
"images": {
|
||||
"bios_image": "OVMF-20160813.fd",
|
||||
"bios_image": "OVMF-edk2-stable202305.fd",
|
||||
"hda_disk_image": "nexus9500v.9.3.3.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "9300v 9.3.3",
|
||||
"images": {
|
||||
"bios_image": "OVMF-20160813.fd",
|
||||
"bios_image": "OVMF-edk2-stable202305.fd",
|
||||
"hda_disk_image": "nexus9300v.9.3.3.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "9.3.1",
|
||||
"images": {
|
||||
"bios_image": "OVMF-20160813.fd",
|
||||
"bios_image": "OVMF-edk2-stable202305.fd",
|
||||
"hda_disk_image": "nxosv.9.3.1.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "9.2.3",
|
||||
"images": {
|
||||
"bios_image": "OVMF-20160813.fd",
|
||||
"bios_image": "OVMF-edk2-stable202305.fd",
|
||||
"hda_disk_image": "nxosv-final.9.2.3.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "9.2.2",
|
||||
"images": {
|
||||
"bios_image": "OVMF-20160813.fd",
|
||||
"bios_image": "OVMF-edk2-stable202305.fd",
|
||||
"hda_disk_image": "nxosv-final.9.2.2.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "9.2.1",
|
||||
"images": {
|
||||
"bios_image": "OVMF-20160813.fd",
|
||||
"bios_image": "OVMF-edk2-stable202305.fd",
|
||||
"hda_disk_image": "nxosv-final.9.2.1.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "7.0.3.I7.9",
|
||||
"images": {
|
||||
"bios_image": "OVMF-20160813.fd",
|
||||
"bios_image": "OVMF-edk2-stable202305.fd",
|
||||
"hda_disk_image": "nxosv-final.7.0.3.I7.9.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "7.0.3.I7.7",
|
||||
"images": {
|
||||
"bios_image": "OVMF-20160813.fd",
|
||||
"bios_image": "OVMF-edk2-stable202305.fd",
|
||||
"hda_disk_image": "nxosv-final.7.0.3.I7.7.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "7.0.3.I7.6",
|
||||
"images": {
|
||||
"bios_image": "OVMF-20160813.fd",
|
||||
"bios_image": "OVMF-edk2-stable202305.fd",
|
||||
"hda_disk_image": "nxosv-final.7.0.3.I7.6.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "7.0.3.I7.5",
|
||||
"images": {
|
||||
"bios_image": "OVMF-20160813.fd",
|
||||
"bios_image": "OVMF-edk2-stable202305.fd",
|
||||
"hda_disk_image": "nxosv-final.7.0.3.I7.5.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "7.0.3.I7.4",
|
||||
"images": {
|
||||
"bios_image": "OVMF-20160813.fd",
|
||||
"bios_image": "OVMF-edk2-stable202305.fd",
|
||||
"hda_disk_image": "nxosv-final.7.0.3.I7.4.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "7.0.3.I7.3",
|
||||
"images": {
|
||||
"bios_image": "OVMF-20160813.fd",
|
||||
"bios_image": "OVMF-edk2-stable202305.fd",
|
||||
"hda_disk_image": "nxosv-final.7.0.3.I7.3.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "7.0.3.I7.2",
|
||||
"images": {
|
||||
"bios_image": "OVMF-20160813.fd",
|
||||
"bios_image": "OVMF-edk2-stable202305.fd",
|
||||
"hda_disk_image": "nxosv-final.7.0.3.I7.2.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "7.0.3.I7.1",
|
||||
"images": {
|
||||
"bios_image": "OVMF-20160813.fd",
|
||||
"bios_image": "OVMF-edk2-stable202305.fd",
|
||||
"hda_disk_image": "nxosv-final.7.0.3.I7.1.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "7.0.3.I6.1",
|
||||
"images": {
|
||||
"bios_image": "OVMF-20160813.fd",
|
||||
"bios_image": "OVMF-edk2-stable202305.fd",
|
||||
"hda_disk_image": "nxosv-final.7.0.3.I6.1.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "7.0.3.I5.2",
|
||||
"images": {
|
||||
"bios_image": "OVMF-20160813.fd",
|
||||
"bios_image": "OVMF-edk2-stable202305.fd",
|
||||
"hda_disk_image": "nxosv-final.7.0.3.I5.2.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "7.0.3.I5.1",
|
||||
"images": {
|
||||
"bios_image": "OVMF-20160813.fd",
|
||||
"bios_image": "OVMF-edk2-stable202305.fd",
|
||||
"hda_disk_image": "nxosv-final.7.0.3.I5.1.qcow2"
|
||||
}
|
||||
}
|
||||
|
19
gns3server/appliances/cisco-pyats.gns3a
Normal file
19
gns3server/appliances/cisco-pyats.gns3a
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"appliance_id": "d9ce131e-ecdc-49d2-be7d-d883d3919a06",
|
||||
"name": "Cisco PyATS",
|
||||
"category": "guest",
|
||||
"description": "pyATS is an end-to-end DevOps automation ecosystem. Agnostic by design, pyATS enable network engineers to automate their day-to-day DevOps activities, perform stateful validation of their device operational status, build a safety-net of scalable, data-driven and reusable tests around their network, and visualize everything in a modern, easy to use dashboard.",
|
||||
"vendor_name": "Cisco",
|
||||
"vendor_url": "https://cisco.com",
|
||||
"product_name": "PyATS",
|
||||
"product_url": "https://developer.cisco.com/pyats/",
|
||||
"registry_version": 4,
|
||||
"status": "stable",
|
||||
"maintainer": "Xander Petty",
|
||||
"maintainer_email": "Xander.Petty@protonmail.com",
|
||||
"docker": {
|
||||
"adapters": 1,
|
||||
"image": "gns3/pyats:latest",
|
||||
"console_type": "telnet"
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@
|
||||
"documentation_url": "http://www.cisco.com/c/en/us/products/wireless/wireless-lan-controller/index.html",
|
||||
"product_name": "Virtual Wireless LAN Controller",
|
||||
"product_url": "http://www.cisco.com/c/en/us/support/wireless/virtual-wireless-controller/tsd-products-support-series-home.html",
|
||||
"registry_version": 3,
|
||||
"registry_version": 4,
|
||||
"status": "experimental",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
@ -28,7 +28,21 @@
|
||||
"options": ""
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
{
|
||||
"filename": "MFG_CTVM_8_10_196_0.iso",
|
||||
"version": "8.10.196.0",
|
||||
"md5sum": "6093aca44dcf45c999f83e62dc9aeea2",
|
||||
"filesize": 650809344,
|
||||
"download_url": "https://software.cisco.com/download/release.html?mdfid=284464214&flowid=&softwareid=280926587&release=8.10.196.0"
|
||||
},
|
||||
{
|
||||
"filename": "MFG_CTVM_8_5_182_0.iso",
|
||||
"version": "8.5.182.0",
|
||||
"md5sum": "1cf3c57c2b123e739ab4662ea0abbc34",
|
||||
"filesize": 388579328,
|
||||
"download_url": "https://software.cisco.com/download/home/284464214/type/280926587/release/8.5.182.0"
|
||||
},
|
||||
{
|
||||
"filename": "MFG_CTVM_8_3_102_0.iso",
|
||||
"version": "8.3.102.0",
|
||||
"md5sum": "7f6b7968b5bed04b5ecc119b6ba4e41c",
|
||||
@ -73,6 +87,20 @@
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "8.10.196.0",
|
||||
"images": {
|
||||
"hda_disk_image": "empty8G.qcow2",
|
||||
"cdrom_image": "MFG_CTVM_8_10_196_0.iso"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "8.5.182.0",
|
||||
"images": {
|
||||
"hda_disk_image": "empty8G.qcow2",
|
||||
"cdrom_image": "MFG_CTVM_8_5_182_0.iso"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "8.3.102.0",
|
||||
"images": {
|
||||
|
@ -8,7 +8,7 @@
|
||||
"documentation_url": "http://www.cisco.com/c/en/us/support/security/web-security-appliance/tsd-products-support-series-home.html",
|
||||
"product_name": "Web Security Virtual Appliance",
|
||||
"product_url": "http://www.cisco.com/c/en/us/products/security/web-security-appliance/index.html",
|
||||
"registry_version": 3,
|
||||
"registry_version": 4,
|
||||
"status": "stable",
|
||||
"maintainer": "GNS3 Team",
|
||||
"maintainer_email": "developers@gns3.net",
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user