Compare commits
	
		
			385 Commits
		
	
	
		
			20231912
			...
			99533fabf4
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 99533fabf4 | |||
| 38ac6e575b | |||
| 51010fdea3 | |||
| 9b6704c39b | |||
| 8de0718650 | |||
| 10b1355c2c | |||
| ec56af6a8e | |||
| 0549eb04d7 | |||
| dd49c692b1 | |||
| 024360ac65 | |||
| c2f04e41f3 | |||
| b8d2c6ef5a | |||
| a7b4a132cd | |||
| 94c9057a12 | |||
| e7bc32d38f | |||
| a8d0520ec5 | |||
| 63d13f4c27 | |||
| 0c8d37033a | |||
| fd604e2869 | |||
| 990057be0d | |||
| d65ebdcb0f | |||
| cd7252b864 | |||
| 284f9d3a94 | |||
| 98f26c2bf7 | |||
| d7db6a56d3 | |||
| 8389c026fa | |||
| 6cb5b37c23 | |||
| 058c2a4abb | |||
| 839a7d271b | |||
| d53c5d45cf | |||
| be4fa6f0d7 | |||
| deced2492f | |||
| 0eb6567eb8 | |||
| 2c40d50435 | |||
| 5c5c54bbd7 | |||
| 772e14cc3d | |||
| a2802647f2 | |||
| 79b2db0987 | |||
| 9cbbcc3bea | |||
| f4571af3c5 | |||
| 5808589cf8 | |||
| 48e4d9a84d | |||
| 5ee1abde46 | |||
| aca1c69f32 | |||
| 19f71e7d68 | |||
| 7583b547c2 | |||
| f67201eaa9 | |||
| ba7129b67f | |||
| 0642e2910e | |||
| e5968f2444 | |||
| 1bbdbcbbaf | |||
| 5a85e9a91f | |||
| 20342a5cc9 | |||
| 4259327fbf | |||
| 9cdd0920a4 | |||
| feb52889c1 | |||
| 70ef1a3717 | |||
| b13712a430 | |||
| 8e350d4c60 | |||
| 824d9ba4ff | |||
| 5da98d3d12 | |||
| a77faafdf7 | |||
| 61f4dab638 | |||
| 66fd3954c7 | |||
| 772e2270c4 | |||
| 2955dfa5c0 | |||
| 48e9c1d42f | |||
| 044b7d0de4 | |||
| a969248744 | |||
| 7d046e607d | |||
| ee77bd11c9 | |||
| 821021e66f | |||
| 68bd5a6c67 | |||
| e49e69de88 | |||
| a3f9ad17c0 | |||
| b943691d18 | |||
| bc051ee678 | |||
| c7a2d1af23 | |||
| 93fbb31273 | |||
| b4eb6f7ea4 | |||
| 00ff6f0b50 | |||
| 324042f956 | |||
| e466d03ec5 | |||
| 89ba09f872 | |||
| a322c46ca4 | |||
| 0915a3e2d9 | |||
| 07eceaf72f | |||
| 0e1396e177 | |||
| e59f21984f | |||
| 8c508acd32 | |||
| 26e7af7675 | |||
| 2fadf53dea | |||
| 2b58ce4d5e | |||
| 9755bacc55 | |||
| 8b16ce0c5d | |||
| 20e6d7931e | |||
| c908d00c62 | |||
| 55b49699eb | |||
| 91fe291341 | |||
| eec6bbb598 | |||
| d2243fa1c2 | |||
| 6e7dd7c1c4 | |||
| e40e15287b | |||
| 800969b9cc | |||
| 5917068add | |||
| 9b14d62830 | |||
| 25503a688b | |||
| 868adc6cee | |||
| 528e30f3dc | |||
| cc42d20e67 | |||
| d189470539 | |||
| 6fdd9f91fa | |||
| 69c2d12fcd | |||
| 174e4a2c79 | |||
| 847ab20a63 | |||
| 09c32a5555 | |||
| 220c943642 | |||
| e5d709c34f | |||
| 1e359a3b8e | |||
| dbff6358db | |||
| d5c05a0cdd | |||
| 1d24d2a84c | |||
| d35dac2de8 | |||
| 01141f77e2 | |||
| 56f765a15a | |||
| 639b7f4b38 | |||
| babda3acd1 | |||
| 197b72cad0 | |||
| 1910c7081b | |||
| eda0fc80b0 | |||
| f6e5356109 | |||
| 11da25b4c0 | |||
| 2599032581 | |||
| ed58d60e84 | |||
| a126e76eef | |||
| c472dfe807 | |||
| c883f13bf8 | |||
| b320f0b326 | |||
| 9812120ed6 | |||
| 9ebd3b0315 | |||
| 24afa12be2 | |||
| 310689312c | |||
| e7f4bc44e7 | |||
| 165937f88b | |||
| a5d81de62b | |||
| ba2b3494cf | |||
| 1944415371 | |||
| 4130fdda1c | |||
| e4ef4c43bd | |||
| afdf639d9b | |||
| f2d6b9a5dd | |||
| e3b61baf11 | |||
| 20732860cf | |||
| 7f14ab8a54 | |||
| 87d4c5b0fd | |||
| 0f58f82e52 | |||
| 16b73a2030 | |||
| a32954785d | |||
| 2789fc299f | |||
| 0257ecba0b | |||
| 17fc64b1fe | |||
| fbc818b5f3 | |||
| a4292795d1 | |||
| 529e16c0c7 | |||
| e1adc1456f | |||
| f1f4a88ae3 | |||
| 8fdbb0f442 | |||
| 9efb1b29df | |||
| 2c07f5f121 | |||
| 953f6fdcf2 | |||
| d66e384137 | |||
| 80bf70502f | |||
| 7f6cf26617 | |||
| c9cf39bb76 | |||
| 93afb646ca | |||
| 4b358acbde | |||
| b97dbc8149 | |||
| e1292ae922 | |||
| 3e812b5530 | |||
| b1e268bf63 | |||
| 887c4608b4 | |||
| 49e33cfd57 | |||
| 842733caa3 | |||
| b6b56fdba8 | |||
| 8163d5e52f | |||
| 7aca0aee13 | |||
| 39fc34ef26 | |||
| be06339bd7 | |||
| 00c1047734 | |||
| a6c54ada50 | |||
| 8803c6755b | |||
| cdab9df5c1 | |||
| 75b8c1d9e9 | |||
| 557fb7d97b | |||
| bb85e58008 | |||
| b8c1375f4f | |||
| a96f6f33df | |||
| a55061a2cd | |||
| e6d3dd926c | |||
| 95dc089943 | |||
| dafef923f0 | |||
| 5095a701eb | |||
| a157484105 | |||
| 0e4bf4414c | |||
| 0a2a9d66e1 | |||
| c4ff5d0621 | |||
| ff1391694d | |||
| 368ae4e89d | |||
| a539c092f5 | |||
| dbf44e6204 | |||
| 448b029c17 | |||
| f06082ce82 | |||
| 272763bdc3 | |||
| 1dd2dfc684 | |||
| b5cb76cd7d | |||
| 4f7161ae9e | |||
| f3d184e06d | |||
| 12404cc9a0 | |||
| 0eabdec559 | |||
| 8646837035 | |||
| a164c6adb5 | |||
| 7de2c01418 | |||
| 7f11076f45 | |||
| 3f32aab8bd | |||
| 275e706ee5 | |||
| 7608a7cb18 | |||
| e6293e3015 | |||
| a44bc0a4fc | |||
| a2221b0903 | |||
| 6ab4111182 | |||
| 8fb044b61d | |||
| 06ec35e1e7 | |||
| e94b08827c | |||
| 5d1ab3be67 | |||
| 383b29ce21 | |||
| 85c9e0f4c6 | |||
| 7e3c105d78 | |||
| 6a3f1f40f9 | |||
| b33c660c3e | |||
| cd04e04d34 | |||
| 7dfbed0186 | |||
| 3dbefc8d84 | |||
| 077b385c0f | |||
| 3f203966d4 | |||
| 0ab8b23de4 | |||
| a18787efcb | |||
| 68465270bf | |||
| b88eb08ec2 | |||
| 8995b5e874 | |||
| 9fe4c67aa0 | |||
| d6e2a10e59 | |||
| 03c7dbc357 | |||
| 27f33038a9 | |||
| 57b0957d3e | |||
| 2174ececd1 | |||
| a61b38b4d3 | |||
| ea84ebdda7 | |||
| a972ea51aa | |||
| f89a4f4481 | |||
| 0e07ca6bd3 | |||
| aaba9f2f80 | |||
| 0ba70330db | |||
| bb55ec4cfe | |||
| 90f8b46c84 | |||
| 51b34131d2 | |||
| 06374dc5ea | |||
| 151c1fc157 | |||
| 696b09f508 | |||
| 270fe60c1d | |||
| 50a224c9f6 | |||
| 2efe5877a4 | |||
| c1de6d9621 | |||
| 2ae2717a5b | |||
| 0a25dc5730 | |||
| 1d6d6e6796 | |||
| 4e8b79deca | |||
| 0ddc4362a0 | |||
| 9d423e3443 | |||
| 1c3d3d57a4 | |||
| edbfe53d1a | |||
| 81c1044bae | |||
| 3777d73c50 | |||
| 9365e9afdf | |||
| 9d738285ab | |||
| c7de64cc02 | |||
| 149e3f4d72 | |||
| f1ba3bc5ab | |||
| cea123f5b0 | |||
| 927414dbaf | |||
| 2ac16fd1cb | |||
| 59c64e4633 | |||
| 74924cff88 | |||
| f0328a8912 | |||
| afc1b5cca6 | |||
| b0ca64b2ff | |||
| d8e5aa17f3 | |||
| 9002b0c4b1 | |||
| 33eaaf2e6b | |||
| bdb15e16af | |||
| 0ec491bf3e | |||
| 389d03e699 | |||
| e2210d247a | |||
| 586a60ab96 | |||
| 104b369fdd | |||
| c96b8cbad1 | |||
| f16176e927 | |||
| 0c5a8d56c9 | |||
| 7428512222 | |||
| 759a9b2dbf | |||
| 1ed23317cb | |||
| 9b55f1f29c | |||
| 5d26426074 | |||
| 39ff53f2ba | |||
| 96d264d15f | |||
| 6c23951d74 | |||
| 6ea8a927a3 | |||
| 4d0b4929c5 | |||
| d6c8964380 | |||
| ed25eed31e | |||
| 6fdcc8c07c | |||
| f82925dbcb | |||
| 71e22bc328 | |||
| e86b29c03a | |||
| 672e866897 | |||
| 80ecb3c5d2 | |||
| 134e27feb6 | |||
| 524ab50df7 | |||
| 8cd32d35e2 | |||
| 307e5d1b50 | |||
| ff66a5cf97 | |||
| dcf6cdab9b | |||
| 2649bfbd25 | |||
| 3eab3ba4b5 | |||
| 975b4ab395 | |||
| c40ee037da | |||
| 719ab3b265 | |||
| ad45c0d654 | |||
| 7d7a052f5f | |||
| aafa4bf145 | |||
| baa0adf529 | |||
| fdd005a3ec | |||
| ed48b22f7f | |||
| a7bfb80547 | |||
| 0710c61909 | |||
| 85dcb06014 | |||
| c880c5e6bb | |||
| 22f5acd0ff | |||
| 706bce0fd8 | |||
| ffac6991c4 | |||
| f890cba5a4 | |||
| e561942cf7 | |||
| 219fc184ee | |||
| 3407c068e1 | |||
| afe5db1fcd | |||
| 085deff4f7 | |||
| 0175726696 | |||
| a8046ebff8 | |||
| 767d2015df | |||
| d4ef389852 | |||
| 2b145ebeff | |||
| 06ddf57b5c | |||
| b4f65a6703 | |||
| d741e12653 | |||
| 9256b76495 | |||
| e638829da7 | |||
| 81f60ce766 | |||
| 388a1ed478 | |||
| c6c1ce26d3 | |||
| b3f89309c4 | |||
| 8182ecd7f6 | |||
| 7b74e7b75a | |||
| 61c567846d | |||
| 246f5ef842 | |||
| 9d4f19822d | |||
| f7777fe085 | |||
| 3849b0d51d | |||
| b4f765d486 | |||
| d8a6b58c52 | |||
| d053490a47 | |||
| 66dcf668f0 | |||
| af1e406945 | |||
| f49b947884 | |||
| 483acde546 | |||
| 3a7b2445a6 | |||
| cd55e6867e | 
							
								
								
									
										52
									
								
								.drone.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								.drone.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| --- | ||||
| kind: pipeline | ||||
| type: docker | ||||
| name: default | ||||
|  | ||||
| steps: | ||||
| - name: web_build | ||||
|   image: node:23 | ||||
|   volumes: | ||||
|     - name: web_app | ||||
|       path: /tmp/web_build | ||||
|   commands: | ||||
|   - cd virtweb_frontend | ||||
|   - npm install | ||||
|   - npm run build | ||||
|   - mv dist /tmp/web_build | ||||
|  | ||||
| - name: backend_check | ||||
|   image: rust | ||||
|   volumes: | ||||
|   - name: rust_registry | ||||
|     path: /usr/local/cargo/registry | ||||
|   commands: | ||||
|   - apt update && apt install -y libvirt-dev | ||||
|   - rustup component add clippy | ||||
|   - cd virtweb_backend | ||||
|   - cargo clippy -- -D warnings | ||||
|   - cargo test | ||||
|  | ||||
| - name: backend_compile | ||||
|   image: rust | ||||
|   volumes: | ||||
|   - name: rust_registry | ||||
|     path: /usr/local/cargo/registry | ||||
|   - name: web_app | ||||
|     path: /tmp/web_build | ||||
|   depends_on: | ||||
|   - backend_check | ||||
|   - web_build | ||||
|   commands: | ||||
|   - apt update && apt install -y libvirt-dev | ||||
|   - cd virtweb_backend | ||||
|   - mv /tmp/web_build/dist static | ||||
|   - cargo build --release | ||||
|   - ls -lah target/release/virtweb_backend | ||||
|  | ||||
|  | ||||
| volumes: | ||||
| - name: rust_registry | ||||
|   temp: {} | ||||
| - name: web_app | ||||
|   temp: {} | ||||
							
								
								
									
										674
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										674
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,674 @@ | ||||
|                     GNU GENERAL PUBLIC LICENSE | ||||
|                        Version 3, 29 June 2007 | ||||
|  | ||||
|  Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> | ||||
|  Everyone is permitted to copy and distribute verbatim copies | ||||
|  of this license document, but changing it is not allowed. | ||||
|  | ||||
|                             Preamble | ||||
|  | ||||
|   The GNU General Public License is a free, copyleft license for | ||||
| software and other kinds of works. | ||||
|  | ||||
|   The licenses for most software and other practical works are designed | ||||
| to take away your freedom to share and change the works.  By contrast, | ||||
| the GNU General Public License is intended to guarantee your freedom to | ||||
| share and change all versions of a program--to make sure it remains free | ||||
| software for all its users.  We, the Free Software Foundation, use the | ||||
| GNU General Public License for most of our software; it applies also to | ||||
| any other work released this way by its authors.  You can apply it to | ||||
| your programs, too. | ||||
|  | ||||
|   When we speak of free software, we are referring to freedom, not | ||||
| price.  Our General Public Licenses are designed to make sure that you | ||||
| have the freedom to distribute copies of free software (and charge for | ||||
| them if you wish), that you receive source code or can get it if you | ||||
| want it, that you can change the software or use pieces of it in new | ||||
| free programs, and that you know you can do these things. | ||||
|  | ||||
|   To protect your rights, we need to prevent others from denying you | ||||
| these rights or asking you to surrender the rights.  Therefore, you have | ||||
| certain responsibilities if you distribute copies of the software, or if | ||||
| you modify it: responsibilities to respect the freedom of others. | ||||
|  | ||||
|   For example, if you distribute copies of such a program, whether | ||||
| gratis or for a fee, you must pass on to the recipients the same | ||||
| freedoms that you received.  You must make sure that they, too, receive | ||||
| or can get the source code.  And you must show them these terms so they | ||||
| know their rights. | ||||
|  | ||||
|   Developers that use the GNU GPL protect your rights with two steps: | ||||
| (1) assert copyright on the software, and (2) offer you this License | ||||
| giving you legal permission to copy, distribute and/or modify it. | ||||
|  | ||||
|   For the developers' and authors' protection, the GPL clearly explains | ||||
| that there is no warranty for this free software.  For both users' and | ||||
| authors' sake, the GPL requires that modified versions be marked as | ||||
| changed, so that their problems will not be attributed erroneously to | ||||
| authors of previous versions. | ||||
|  | ||||
|   Some devices are designed to deny users access to install or run | ||||
| modified versions of the software inside them, although the manufacturer | ||||
| can do so.  This is fundamentally incompatible with the aim of | ||||
| protecting users' freedom to change the software.  The systematic | ||||
| pattern of such abuse occurs in the area of products for individuals to | ||||
| use, which is precisely where it is most unacceptable.  Therefore, we | ||||
| have designed this version of the GPL to prohibit the practice for those | ||||
| products.  If such problems arise substantially in other domains, we | ||||
| stand ready to extend this provision to those domains in future versions | ||||
| of the GPL, as needed to protect the freedom of users. | ||||
|  | ||||
|   Finally, every program is threatened constantly by software patents. | ||||
| States should not allow patents to restrict development and use of | ||||
| software on general-purpose computers, but in those that do, we wish to | ||||
| avoid the special danger that patents applied to a free program could | ||||
| make it effectively proprietary.  To prevent this, the GPL assures that | ||||
| patents cannot be used to render the program non-free. | ||||
|  | ||||
|   The precise terms and conditions for copying, distribution and | ||||
| modification follow. | ||||
|  | ||||
|                        TERMS AND CONDITIONS | ||||
|  | ||||
|   0. Definitions. | ||||
|  | ||||
|   "This License" refers to version 3 of the GNU General Public License. | ||||
|  | ||||
|   "Copyright" also means copyright-like laws that apply to other kinds of | ||||
| works, such as semiconductor masks. | ||||
|  | ||||
|   "The Program" refers to any copyrightable work licensed under this | ||||
| License.  Each licensee is addressed as "you".  "Licensees" and | ||||
| "recipients" may be individuals or organizations. | ||||
|  | ||||
|   To "modify" a work means to copy from or adapt all or part of the work | ||||
| in a fashion requiring copyright permission, other than the making of an | ||||
| exact copy.  The resulting work is called a "modified version" of the | ||||
| earlier work or a work "based on" the earlier work. | ||||
|  | ||||
|   A "covered work" means either the unmodified Program or a work based | ||||
| on the Program. | ||||
|  | ||||
|   To "propagate" a work means to do anything with it that, without | ||||
| permission, would make you directly or secondarily liable for | ||||
| infringement under applicable copyright law, except executing it on a | ||||
| computer or modifying a private copy.  Propagation includes copying, | ||||
| distribution (with or without modification), making available to the | ||||
| public, and in some countries other activities as well. | ||||
|  | ||||
|   To "convey" a work means any kind of propagation that enables other | ||||
| parties to make or receive copies.  Mere interaction with a user through | ||||
| a computer network, with no transfer of a copy, is not conveying. | ||||
|  | ||||
|   An interactive user interface displays "Appropriate Legal Notices" | ||||
| to the extent that it includes a convenient and prominently visible | ||||
| feature that (1) displays an appropriate copyright notice, and (2) | ||||
| tells the user that there is no warranty for the work (except to the | ||||
| extent that warranties are provided), that licensees may convey the | ||||
| work under this License, and how to view a copy of this License.  If | ||||
| the interface presents a list of user commands or options, such as a | ||||
| menu, a prominent item in the list meets this criterion. | ||||
|  | ||||
|   1. Source Code. | ||||
|  | ||||
|   The "source code" for a work means the preferred form of the work | ||||
| for making modifications to it.  "Object code" means any non-source | ||||
| form of a work. | ||||
|  | ||||
|   A "Standard Interface" means an interface that either is an official | ||||
| standard defined by a recognized standards body, or, in the case of | ||||
| interfaces specified for a particular programming language, one that | ||||
| is widely used among developers working in that language. | ||||
|  | ||||
|   The "System Libraries" of an executable work include anything, other | ||||
| than the work as a whole, that (a) is included in the normal form of | ||||
| packaging a Major Component, but which is not part of that Major | ||||
| Component, and (b) serves only to enable use of the work with that | ||||
| Major Component, or to implement a Standard Interface for which an | ||||
| implementation is available to the public in source code form.  A | ||||
| "Major Component", in this context, means a major essential component | ||||
| (kernel, window system, and so on) of the specific operating system | ||||
| (if any) on which the executable work runs, or a compiler used to | ||||
| produce the work, or an object code interpreter used to run it. | ||||
|  | ||||
|   The "Corresponding Source" for a work in object code form means all | ||||
| the source code needed to generate, install, and (for an executable | ||||
| work) run the object code and to modify the work, including scripts to | ||||
| control those activities.  However, it does not include the work's | ||||
| System Libraries, or general-purpose tools or generally available free | ||||
| programs which are used unmodified in performing those activities but | ||||
| which are not part of the work.  For example, Corresponding Source | ||||
| includes interface definition files associated with source files for | ||||
| the work, and the source code for shared libraries and dynamically | ||||
| linked subprograms that the work is specifically designed to require, | ||||
| such as by intimate data communication or control flow between those | ||||
| subprograms and other parts of the work. | ||||
|  | ||||
|   The Corresponding Source need not include anything that users | ||||
| can regenerate automatically from other parts of the Corresponding | ||||
| Source. | ||||
|  | ||||
|   The Corresponding Source for a work in source code form is that | ||||
| same work. | ||||
|  | ||||
|   2. Basic Permissions. | ||||
|  | ||||
|   All rights granted under this License are granted for the term of | ||||
| copyright on the Program, and are irrevocable provided the stated | ||||
| conditions are met.  This License explicitly affirms your unlimited | ||||
| permission to run the unmodified Program.  The output from running a | ||||
| covered work is covered by this License only if the output, given its | ||||
| content, constitutes a covered work.  This License acknowledges your | ||||
| rights of fair use or other equivalent, as provided by copyright law. | ||||
|  | ||||
|   You may make, run and propagate covered works that you do not | ||||
| convey, without conditions so long as your license otherwise remains | ||||
| in force.  You may convey covered works to others for the sole purpose | ||||
| of having them make modifications exclusively for you, or provide you | ||||
| with facilities for running those works, provided that you comply with | ||||
| the terms of this License in conveying all material for which you do | ||||
| not control copyright.  Those thus making or running the covered works | ||||
| for you must do so exclusively on your behalf, under your direction | ||||
| and control, on terms that prohibit them from making any copies of | ||||
| your copyrighted material outside their relationship with you. | ||||
|  | ||||
|   Conveying under any other circumstances is permitted solely under | ||||
| the conditions stated below.  Sublicensing is not allowed; section 10 | ||||
| makes it unnecessary. | ||||
|  | ||||
|   3. Protecting Users' Legal Rights From Anti-Circumvention Law. | ||||
|  | ||||
|   No covered work shall be deemed part of an effective technological | ||||
| measure under any applicable law fulfilling obligations under article | ||||
| 11 of the WIPO copyright treaty adopted on 20 December 1996, or | ||||
| similar laws prohibiting or restricting circumvention of such | ||||
| measures. | ||||
|  | ||||
|   When you convey a covered work, you waive any legal power to forbid | ||||
| circumvention of technological measures to the extent such circumvention | ||||
| is effected by exercising rights under this License with respect to | ||||
| the covered work, and you disclaim any intention to limit operation or | ||||
| modification of the work as a means of enforcing, against the work's | ||||
| users, your or third parties' legal rights to forbid circumvention of | ||||
| technological measures. | ||||
|  | ||||
|   4. Conveying Verbatim Copies. | ||||
|  | ||||
|   You may convey verbatim copies of the Program's source code as you | ||||
| receive it, in any medium, provided that you conspicuously and | ||||
| appropriately publish on each copy an appropriate copyright notice; | ||||
| keep intact all notices stating that this License and any | ||||
| non-permissive terms added in accord with section 7 apply to the code; | ||||
| keep intact all notices of the absence of any warranty; and give all | ||||
| recipients a copy of this License along with the Program. | ||||
|  | ||||
|   You may charge any price or no price for each copy that you convey, | ||||
| and you may offer support or warranty protection for a fee. | ||||
|  | ||||
|   5. Conveying Modified Source Versions. | ||||
|  | ||||
|   You may convey a work based on the Program, or the modifications to | ||||
| produce it from the Program, in the form of source code under the | ||||
| terms of section 4, provided that you also meet all of these conditions: | ||||
|  | ||||
|     a) The work must carry prominent notices stating that you modified | ||||
|     it, and giving a relevant date. | ||||
|  | ||||
|     b) The work must carry prominent notices stating that it is | ||||
|     released under this License and any conditions added under section | ||||
|     7.  This requirement modifies the requirement in section 4 to | ||||
|     "keep intact all notices". | ||||
|  | ||||
|     c) You must license the entire work, as a whole, under this | ||||
|     License to anyone who comes into possession of a copy.  This | ||||
|     License will therefore apply, along with any applicable section 7 | ||||
|     additional terms, to the whole of the work, and all its parts, | ||||
|     regardless of how they are packaged.  This License gives no | ||||
|     permission to license the work in any other way, but it does not | ||||
|     invalidate such permission if you have separately received it. | ||||
|  | ||||
|     d) If the work has interactive user interfaces, each must display | ||||
|     Appropriate Legal Notices; however, if the Program has interactive | ||||
|     interfaces that do not display Appropriate Legal Notices, your | ||||
|     work need not make them do so. | ||||
|  | ||||
|   A compilation of a covered work with other separate and independent | ||||
| works, which are not by their nature extensions of the covered work, | ||||
| and which are not combined with it such as to form a larger program, | ||||
| in or on a volume of a storage or distribution medium, is called an | ||||
| "aggregate" if the compilation and its resulting copyright are not | ||||
| used to limit the access or legal rights of the compilation's users | ||||
| beyond what the individual works permit.  Inclusion of a covered work | ||||
| in an aggregate does not cause this License to apply to the other | ||||
| parts of the aggregate. | ||||
|  | ||||
|   6. Conveying Non-Source Forms. | ||||
|  | ||||
|   You may convey a covered work in object code form under the terms | ||||
| of sections 4 and 5, provided that you also convey the | ||||
| machine-readable Corresponding Source under the terms of this License, | ||||
| in one of these ways: | ||||
|  | ||||
|     a) Convey the object code in, or embodied in, a physical product | ||||
|     (including a physical distribution medium), accompanied by the | ||||
|     Corresponding Source fixed on a durable physical medium | ||||
|     customarily used for software interchange. | ||||
|  | ||||
|     b) Convey the object code in, or embodied in, a physical product | ||||
|     (including a physical distribution medium), accompanied by a | ||||
|     written offer, valid for at least three years and valid for as | ||||
|     long as you offer spare parts or customer support for that product | ||||
|     model, to give anyone who possesses the object code either (1) a | ||||
|     copy of the Corresponding Source for all the software in the | ||||
|     product that is covered by this License, on a durable physical | ||||
|     medium customarily used for software interchange, for a price no | ||||
|     more than your reasonable cost of physically performing this | ||||
|     conveying of source, or (2) access to copy the | ||||
|     Corresponding Source from a network server at no charge. | ||||
|  | ||||
|     c) Convey individual copies of the object code with a copy of the | ||||
|     written offer to provide the Corresponding Source.  This | ||||
|     alternative is allowed only occasionally and noncommercially, and | ||||
|     only if you received the object code with such an offer, in accord | ||||
|     with subsection 6b. | ||||
|  | ||||
|     d) Convey the object code by offering access from a designated | ||||
|     place (gratis or for a charge), and offer equivalent access to the | ||||
|     Corresponding Source in the same way through the same place at no | ||||
|     further charge.  You need not require recipients to copy the | ||||
|     Corresponding Source along with the object code.  If the place to | ||||
|     copy the object code is a network server, the Corresponding Source | ||||
|     may be on a different server (operated by you or a third party) | ||||
|     that supports equivalent copying facilities, provided you maintain | ||||
|     clear directions next to the object code saying where to find the | ||||
|     Corresponding Source.  Regardless of what server hosts the | ||||
|     Corresponding Source, you remain obligated to ensure that it is | ||||
|     available for as long as needed to satisfy these requirements. | ||||
|  | ||||
|     e) Convey the object code using peer-to-peer transmission, provided | ||||
|     you inform other peers where the object code and Corresponding | ||||
|     Source of the work are being offered to the general public at no | ||||
|     charge under subsection 6d. | ||||
|  | ||||
|   A separable portion of the object code, whose source code is excluded | ||||
| from the Corresponding Source as a System Library, need not be | ||||
| included in conveying the object code work. | ||||
|  | ||||
|   A "User Product" is either (1) a "consumer product", which means any | ||||
| tangible personal property which is normally used for personal, family, | ||||
| or household purposes, or (2) anything designed or sold for incorporation | ||||
| into a dwelling.  In determining whether a product is a consumer product, | ||||
| doubtful cases shall be resolved in favor of coverage.  For a particular | ||||
| product received by a particular user, "normally used" refers to a | ||||
| typical or common use of that class of product, regardless of the status | ||||
| of the particular user or of the way in which the particular user | ||||
| actually uses, or expects or is expected to use, the product.  A product | ||||
| is a consumer product regardless of whether the product has substantial | ||||
| commercial, industrial or non-consumer uses, unless such uses represent | ||||
| the only significant mode of use of the product. | ||||
|  | ||||
|   "Installation Information" for a User Product means any methods, | ||||
| procedures, authorization keys, or other information required to install | ||||
| and execute modified versions of a covered work in that User Product from | ||||
| a modified version of its Corresponding Source.  The information must | ||||
| suffice to ensure that the continued functioning of the modified object | ||||
| code is in no case prevented or interfered with solely because | ||||
| modification has been made. | ||||
|  | ||||
|   If you convey an object code work under this section in, or with, or | ||||
| specifically for use in, a User Product, and the conveying occurs as | ||||
| part of a transaction in which the right of possession and use of the | ||||
| User Product is transferred to the recipient in perpetuity or for a | ||||
| fixed term (regardless of how the transaction is characterized), the | ||||
| Corresponding Source conveyed under this section must be accompanied | ||||
| by the Installation Information.  But this requirement does not apply | ||||
| if neither you nor any third party retains the ability to install | ||||
| modified object code on the User Product (for example, the work has | ||||
| been installed in ROM). | ||||
|  | ||||
|   The requirement to provide Installation Information does not include a | ||||
| requirement to continue to provide support service, warranty, or updates | ||||
| for a work that has been modified or installed by the recipient, or for | ||||
| the User Product in which it has been modified or installed.  Access to a | ||||
| network may be denied when the modification itself materially and | ||||
| adversely affects the operation of the network or violates the rules and | ||||
| protocols for communication across the network. | ||||
|  | ||||
|   Corresponding Source conveyed, and Installation Information provided, | ||||
| in accord with this section must be in a format that is publicly | ||||
| documented (and with an implementation available to the public in | ||||
| source code form), and must require no special password or key for | ||||
| unpacking, reading or copying. | ||||
|  | ||||
|   7. Additional Terms. | ||||
|  | ||||
|   "Additional permissions" are terms that supplement the terms of this | ||||
| License by making exceptions from one or more of its conditions. | ||||
| Additional permissions that are applicable to the entire Program shall | ||||
| be treated as though they were included in this License, to the extent | ||||
| that they are valid under applicable law.  If additional permissions | ||||
| apply only to part of the Program, that part may be used separately | ||||
| under those permissions, but the entire Program remains governed by | ||||
| this License without regard to the additional permissions. | ||||
|  | ||||
|   When you convey a copy of a covered work, you may at your option | ||||
| remove any additional permissions from that copy, or from any part of | ||||
| it.  (Additional permissions may be written to require their own | ||||
| removal in certain cases when you modify the work.)  You may place | ||||
| additional permissions on material, added by you to a covered work, | ||||
| for which you have or can give appropriate copyright permission. | ||||
|  | ||||
|   Notwithstanding any other provision of this License, for material you | ||||
| add to a covered work, you may (if authorized by the copyright holders of | ||||
| that material) supplement the terms of this License with terms: | ||||
|  | ||||
|     a) Disclaiming warranty or limiting liability differently from the | ||||
|     terms of sections 15 and 16 of this License; or | ||||
|  | ||||
|     b) Requiring preservation of specified reasonable legal notices or | ||||
|     author attributions in that material or in the Appropriate Legal | ||||
|     Notices displayed by works containing it; or | ||||
|  | ||||
|     c) Prohibiting misrepresentation of the origin of that material, or | ||||
|     requiring that modified versions of such material be marked in | ||||
|     reasonable ways as different from the original version; or | ||||
|  | ||||
|     d) Limiting the use for publicity purposes of names of licensors or | ||||
|     authors of the material; or | ||||
|  | ||||
|     e) Declining to grant rights under trademark law for use of some | ||||
|     trade names, trademarks, or service marks; or | ||||
|  | ||||
|     f) Requiring indemnification of licensors and authors of that | ||||
|     material by anyone who conveys the material (or modified versions of | ||||
|     it) with contractual assumptions of liability to the recipient, for | ||||
|     any liability that these contractual assumptions directly impose on | ||||
|     those licensors and authors. | ||||
|  | ||||
|   All other non-permissive additional terms are considered "further | ||||
| restrictions" within the meaning of section 10.  If the Program as you | ||||
| received it, or any part of it, contains a notice stating that it is | ||||
| governed by this License along with a term that is a further | ||||
| restriction, you may remove that term.  If a license document contains | ||||
| a further restriction but permits relicensing or conveying under this | ||||
| License, you may add to a covered work material governed by the terms | ||||
| of that license document, provided that the further restriction does | ||||
| not survive such relicensing or conveying. | ||||
|  | ||||
|   If you add terms to a covered work in accord with this section, you | ||||
| must place, in the relevant source files, a statement of the | ||||
| additional terms that apply to those files, or a notice indicating | ||||
| where to find the applicable terms. | ||||
|  | ||||
|   Additional terms, permissive or non-permissive, may be stated in the | ||||
| form of a separately written license, or stated as exceptions; | ||||
| the above requirements apply either way. | ||||
|  | ||||
|   8. Termination. | ||||
|  | ||||
|   You may not propagate or modify a covered work except as expressly | ||||
| provided under this License.  Any attempt otherwise to propagate or | ||||
| modify it is void, and will automatically terminate your rights under | ||||
| this License (including any patent licenses granted under the third | ||||
| paragraph of section 11). | ||||
|  | ||||
|   However, if you cease all violation of this License, then your | ||||
| license from a particular copyright holder is reinstated (a) | ||||
| provisionally, unless and until the copyright holder explicitly and | ||||
| finally terminates your license, and (b) permanently, if the copyright | ||||
| holder fails to notify you of the violation by some reasonable means | ||||
| prior to 60 days after the cessation. | ||||
|  | ||||
|   Moreover, your license from a particular copyright holder is | ||||
| reinstated permanently if the copyright holder notifies you of the | ||||
| violation by some reasonable means, this is the first time you have | ||||
| received notice of violation of this License (for any work) from that | ||||
| copyright holder, and you cure the violation prior to 30 days after | ||||
| your receipt of the notice. | ||||
|  | ||||
|   Termination of your rights under this section does not terminate the | ||||
| licenses of parties who have received copies or rights from you under | ||||
| this License.  If your rights have been terminated and not permanently | ||||
| reinstated, you do not qualify to receive new licenses for the same | ||||
| material under section 10. | ||||
|  | ||||
|   9. Acceptance Not Required for Having Copies. | ||||
|  | ||||
|   You are not required to accept this License in order to receive or | ||||
| run a copy of the Program.  Ancillary propagation of a covered work | ||||
| occurring solely as a consequence of using peer-to-peer transmission | ||||
| to receive a copy likewise does not require acceptance.  However, | ||||
| nothing other than this License grants you permission to propagate or | ||||
| modify any covered work.  These actions infringe copyright if you do | ||||
| not accept this License.  Therefore, by modifying or propagating a | ||||
| covered work, you indicate your acceptance of this License to do so. | ||||
|  | ||||
|   10. Automatic Licensing of Downstream Recipients. | ||||
|  | ||||
|   Each time you convey a covered work, the recipient automatically | ||||
| receives a license from the original licensors, to run, modify and | ||||
| propagate that work, subject to this License.  You are not responsible | ||||
| for enforcing compliance by third parties with this License. | ||||
|  | ||||
|   An "entity transaction" is a transaction transferring control of an | ||||
| organization, or substantially all assets of one, or subdividing an | ||||
| organization, or merging organizations.  If propagation of a covered | ||||
| work results from an entity transaction, each party to that | ||||
| transaction who receives a copy of the work also receives whatever | ||||
| licenses to the work the party's predecessor in interest had or could | ||||
| give under the previous paragraph, plus a right to possession of the | ||||
| Corresponding Source of the work from the predecessor in interest, if | ||||
| the predecessor has it or can get it with reasonable efforts. | ||||
|  | ||||
|   You may not impose any further restrictions on the exercise of the | ||||
| rights granted or affirmed under this License.  For example, you may | ||||
| not impose a license fee, royalty, or other charge for exercise of | ||||
| rights granted under this License, and you may not initiate litigation | ||||
| (including a cross-claim or counterclaim in a lawsuit) alleging that | ||||
| any patent claim is infringed by making, using, selling, offering for | ||||
| sale, or importing the Program or any portion of it. | ||||
|  | ||||
|   11. Patents. | ||||
|  | ||||
|   A "contributor" is a copyright holder who authorizes use under this | ||||
| License of the Program or a work on which the Program is based.  The | ||||
| work thus licensed is called the contributor's "contributor version". | ||||
|  | ||||
|   A contributor's "essential patent claims" are all patent claims | ||||
| owned or controlled by the contributor, whether already acquired or | ||||
| hereafter acquired, that would be infringed by some manner, permitted | ||||
| by this License, of making, using, or selling its contributor version, | ||||
| but do not include claims that would be infringed only as a | ||||
| consequence of further modification of the contributor version.  For | ||||
| purposes of this definition, "control" includes the right to grant | ||||
| patent sublicenses in a manner consistent with the requirements of | ||||
| this License. | ||||
|  | ||||
|   Each contributor grants you a non-exclusive, worldwide, royalty-free | ||||
| patent license under the contributor's essential patent claims, to | ||||
| make, use, sell, offer for sale, import and otherwise run, modify and | ||||
| propagate the contents of its contributor version. | ||||
|  | ||||
|   In the following three paragraphs, a "patent license" is any express | ||||
| agreement or commitment, however denominated, not to enforce a patent | ||||
| (such as an express permission to practice a patent or covenant not to | ||||
| sue for patent infringement).  To "grant" such a patent license to a | ||||
| party means to make such an agreement or commitment not to enforce a | ||||
| patent against the party. | ||||
|  | ||||
|   If you convey a covered work, knowingly relying on a patent license, | ||||
| and the Corresponding Source of the work is not available for anyone | ||||
| to copy, free of charge and under the terms of this License, through a | ||||
| publicly available network server or other readily accessible means, | ||||
| then you must either (1) cause the Corresponding Source to be so | ||||
| available, or (2) arrange to deprive yourself of the benefit of the | ||||
| patent license for this particular work, or (3) arrange, in a manner | ||||
| consistent with the requirements of this License, to extend the patent | ||||
| license to downstream recipients.  "Knowingly relying" means you have | ||||
| actual knowledge that, but for the patent license, your conveying the | ||||
| covered work in a country, or your recipient's use of the covered work | ||||
| in a country, would infringe one or more identifiable patents in that | ||||
| country that you have reason to believe are valid. | ||||
|  | ||||
|   If, pursuant to or in connection with a single transaction or | ||||
| arrangement, you convey, or propagate by procuring conveyance of, a | ||||
| covered work, and grant a patent license to some of the parties | ||||
| receiving the covered work authorizing them to use, propagate, modify | ||||
| or convey a specific copy of the covered work, then the patent license | ||||
| you grant is automatically extended to all recipients of the covered | ||||
| work and works based on it. | ||||
|  | ||||
|   A patent license is "discriminatory" if it does not include within | ||||
| the scope of its coverage, prohibits the exercise of, or is | ||||
| conditioned on the non-exercise of one or more of the rights that are | ||||
| specifically granted under this License.  You may not convey a covered | ||||
| work if you are a party to an arrangement with a third party that is | ||||
| in the business of distributing software, under which you make payment | ||||
| to the third party based on the extent of your activity of conveying | ||||
| the work, and under which the third party grants, to any of the | ||||
| parties who would receive the covered work from you, a discriminatory | ||||
| patent license (a) in connection with copies of the covered work | ||||
| conveyed by you (or copies made from those copies), or (b) primarily | ||||
| for and in connection with specific products or compilations that | ||||
| contain the covered work, unless you entered into that arrangement, | ||||
| or that patent license was granted, prior to 28 March 2007. | ||||
|  | ||||
|   Nothing in this License shall be construed as excluding or limiting | ||||
| any implied license or other defenses to infringement that may | ||||
| otherwise be available to you under applicable patent law. | ||||
|  | ||||
|   12. No Surrender of Others' Freedom. | ||||
|  | ||||
|   If conditions are imposed on you (whether by court order, agreement or | ||||
| otherwise) that contradict the conditions of this License, they do not | ||||
| excuse you from the conditions of this License.  If you cannot convey a | ||||
| covered work so as to satisfy simultaneously your obligations under this | ||||
| License and any other pertinent obligations, then as a consequence you may | ||||
| not convey it at all.  For example, if you agree to terms that obligate you | ||||
| to collect a royalty for further conveying from those to whom you convey | ||||
| the Program, the only way you could satisfy both those terms and this | ||||
| License would be to refrain entirely from conveying the Program. | ||||
|  | ||||
|   13. Use with the GNU Affero General Public License. | ||||
|  | ||||
|   Notwithstanding any other provision of this License, you have | ||||
| permission to link or combine any covered work with a work licensed | ||||
| under version 3 of the GNU Affero General Public License into a single | ||||
| combined work, and to convey the resulting work.  The terms of this | ||||
| License will continue to apply to the part which is the covered work, | ||||
| but the special requirements of the GNU Affero General Public License, | ||||
| section 13, concerning interaction through a network will apply to the | ||||
| combination as such. | ||||
|  | ||||
|   14. Revised Versions of this License. | ||||
|  | ||||
|   The Free Software Foundation may publish revised and/or new versions of | ||||
| the GNU General Public License from time to time.  Such new versions will | ||||
| be similar in spirit to the present version, but may differ in detail to | ||||
| address new problems or concerns. | ||||
|  | ||||
|   Each version is given a distinguishing version number.  If the | ||||
| Program specifies that a certain numbered version of the GNU General | ||||
| Public License "or any later version" applies to it, you have the | ||||
| option of following the terms and conditions either of that numbered | ||||
| version or of any later version published by the Free Software | ||||
| Foundation.  If the Program does not specify a version number of the | ||||
| GNU General Public License, you may choose any version ever published | ||||
| by the Free Software Foundation. | ||||
|  | ||||
|   If the Program specifies that a proxy can decide which future | ||||
| versions of the GNU General Public License can be used, that proxy's | ||||
| public statement of acceptance of a version permanently authorizes you | ||||
| to choose that version for the Program. | ||||
|  | ||||
|   Later license versions may give you additional or different | ||||
| permissions.  However, no additional obligations are imposed on any | ||||
| author or copyright holder as a result of your choosing to follow a | ||||
| later version. | ||||
|  | ||||
|   15. Disclaimer of Warranty. | ||||
|  | ||||
|   THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY | ||||
| APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT | ||||
| HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY | ||||
| OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, | ||||
| THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR | ||||
| PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM | ||||
| IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF | ||||
| ALL NECESSARY SERVICING, REPAIR OR CORRECTION. | ||||
|  | ||||
|   16. Limitation of Liability. | ||||
|  | ||||
|   IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING | ||||
| WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS | ||||
| THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY | ||||
| GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE | ||||
| USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF | ||||
| DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD | ||||
| PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), | ||||
| EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF | ||||
| SUCH DAMAGES. | ||||
|  | ||||
|   17. Interpretation of Sections 15 and 16. | ||||
|  | ||||
|   If the disclaimer of warranty and limitation of liability provided | ||||
| above cannot be given local legal effect according to their terms, | ||||
| reviewing courts shall apply local law that most closely approximates | ||||
| an absolute waiver of all civil liability in connection with the | ||||
| Program, unless a warranty or assumption of liability accompanies a | ||||
| copy of the Program in return for a fee. | ||||
|  | ||||
|                      END OF TERMS AND CONDITIONS | ||||
|  | ||||
|             How to Apply These Terms to Your New Programs | ||||
|  | ||||
|   If you develop a new program, and you want it to be of the greatest | ||||
| possible use to the public, the best way to achieve this is to make it | ||||
| free software which everyone can redistribute and change under these terms. | ||||
|  | ||||
|   To do so, attach the following notices to the program.  It is safest | ||||
| to attach them to the start of each source file to most effectively | ||||
| state the exclusion of warranty; and each file should have at least | ||||
| the "copyright" line and a pointer to where the full notice is found. | ||||
|  | ||||
|     <one line to give the program's name and a brief idea of what it does.> | ||||
|     Copyright (C) <year>  <name of author> | ||||
|  | ||||
|     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 <https://www.gnu.org/licenses/>. | ||||
|  | ||||
| Also add information on how to contact you by electronic and paper mail. | ||||
|  | ||||
|   If the program does terminal interaction, make it output a short | ||||
| notice like this when it starts in an interactive mode: | ||||
|  | ||||
|     <program>  Copyright (C) <year>  <name of author> | ||||
|     This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. | ||||
|     This is free software, and you are welcome to redistribute it | ||||
|     under certain conditions; type `show c' for details. | ||||
|  | ||||
| The hypothetical commands `show w' and `show c' should show the appropriate | ||||
| parts of the General Public License.  Of course, your program's commands | ||||
| might be different; for a GUI interface, you would use an "about box". | ||||
|  | ||||
|   You should also get your employer (if you work as a programmer) or school, | ||||
| if any, to sign a "copyright disclaimer" for the program, if necessary. | ||||
| For more information on this, and how to apply and follow the GNU GPL, see | ||||
| <https://www.gnu.org/licenses/>. | ||||
|  | ||||
|   The GNU General Public License does not permit incorporating your program | ||||
| into proprietary programs.  If your program is a subroutine library, you | ||||
| may consider it more useful to permit linking proprietary applications with | ||||
| the library.  If this is what you want to do, use the GNU Lesser General | ||||
| Public License instead of this License.  But first, please read | ||||
| <https://www.gnu.org/licenses/why-not-lgpl.html>. | ||||
							
								
								
									
										134
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										134
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,123 +1,23 @@ | ||||
| # VirtWEB | ||||
| WIP project | ||||
|  | ||||
| ## Development requirements | ||||
| 1. The `libvirt-dev` package must be installed: | ||||
|  | ||||
| ```bash | ||||
| sudo apt install libvirt-dev | ||||
| ``` | ||||
|  | ||||
| 2. Libvirt must also be installed: | ||||
| ```bash | ||||
| sudo apt install qemu-kvm libvirt-daemon-system | ||||
| ``` | ||||
|  | ||||
| 3. Allow the current user to manage VMs: | ||||
| ``` | ||||
| sudo adduser $USER libvirt | ||||
| sudo adduser $USER kvm  | ||||
| ``` | ||||
|  | ||||
| > Note: You will need to login again for this change to take effect. | ||||
| Open Source Web interface for LibVirt. Simplify the management of VM. | ||||
|  | ||||
| ## Setup for dev | ||||
| Please refer to this guide: [virtweb_docs/SETUP_DEV.md](virtweb_docs/SETUP_DEV.md) | ||||
|  | ||||
| ## Production requirements | ||||
| ### TODO | ||||
| TODO | ||||
| Please refer to this guide: [virtweb_docs/SETUP_PROD.md](virtweb_docs/SETUP_PROD.md) | ||||
|  | ||||
| ### Manual port forwarding without a LibVirt HOOK | ||||
| * Allow ip forwarding in the kernel: edit `/etc/sysctl.conf` and uncomment the following line: | ||||
| ## Features | ||||
| * Only Qemu / KVM is supported! | ||||
| * Basic auth / OpenID auth | ||||
| * Create, update & delete VM | ||||
| * noVNC control of VMs | ||||
| * Start, stop, suspend, resume, reset & kill VMs | ||||
| * Create, update & delete networks | ||||
| * Start & stop networks | ||||
| * Create, update & delete network filters | ||||
| * Upload ISO for easy VM installation | ||||
| * API tokens for system interconnection | ||||
|  | ||||
| ``` | ||||
| net.ipv4.ip_forward=1 | ||||
| ``` | ||||
|  | ||||
| * To reload `sysctl` without reboot: | ||||
|  | ||||
| ``` | ||||
| sudo sysctl -p /etc/sysctl.conf | ||||
| ``` | ||||
|  | ||||
| * Create the following IPTables rules: | ||||
|  | ||||
| ``` | ||||
| UP_DEV=$(ip a | grep "192.168.1." -B 2 | head -n 1 | cut -d ':' -f 2 | | ||||
|  tr -d ' ') | ||||
| LOCAL_DEV=$(ip a | grep "192.168.25." -B 2 | head -n 1 | cut -d ':' -f 2 | tr -d ' ') | ||||
| echo "$UP_DEV -> $LOCAL_DEV" | ||||
|  | ||||
| GUEST_IP=192.168.25.189 | ||||
| HOST_PORT=8085 | ||||
| GUEST_PORT=8085 | ||||
|  | ||||
| # connections from outside | ||||
| sudo iptables -I FORWARD -o $LOCAL_DEV -d  $GUEST_IP -j ACCEPT | ||||
| sudo iptables -t nat -I PREROUTING -p tcp --dport $HOST_PORT -j DNAT --to $GUEST_IP:$GUEST_PORT | ||||
| ``` | ||||
|  | ||||
| * Theses rules can be persisted using `iptables-save` then, or using a libvirt hook. | ||||
|  | ||||
|  | ||||
| ### Manual port forwarding with a LibVirt HOOK | ||||
| * Allow ip forwarding in the kernel: edit `/etc/sysctl.conf` and uncomment the following line: | ||||
|  | ||||
| ``` | ||||
| net.ipv4.ip_forward=1 | ||||
| ``` | ||||
|  | ||||
| * To reload `sysctl` without reboot: | ||||
|  | ||||
| ``` | ||||
| sudo sysctl -p /etc/sysctl.conf | ||||
| ``` | ||||
|  | ||||
| * Get the following information, using the web ui or `virsh`: | ||||
| 	* The name of the target guest | ||||
| 	* The IP and port of the guest who will receive the connection | ||||
| 	* The port of the host that will be forwarded to the guest | ||||
|  | ||||
| * Stop the guest if its running, either using `virsh` or from the web ui | ||||
|  | ||||
| * Create or append the following content to the file `/etc/libvirt/hooks/qemu`: | ||||
|  | ||||
| ```bash | ||||
| #!/bin/bash | ||||
|  | ||||
| # IMPORTANT: Change the "VM NAME" string to match your actual VM Name. | ||||
| # In order to create rules to other VMs, just duplicate the below block and configure | ||||
| # it accordingly. | ||||
| if [ "${1}" = "VM NAME" ]; then | ||||
|  | ||||
|  # Update the following variables to fit your setup | ||||
|  GUEST_IP= | ||||
|  GUEST_PORT= | ||||
|  HOST_PORT= | ||||
|  | ||||
|  if [ "${2}" = "stopped" ] || [ "${2}" = "reconnect" ]; then | ||||
|   /sbin/iptables -D FORWARD -o virbr0 -p tcp -d $GUEST_IP --dport $GUEST_PORT -j ACCEPT | ||||
|   /sbin/iptables -t nat -D PREROUTING -p tcp --dport $HOST_PORT -j DNAT --to $GUEST_IP:$GUEST_PORT | ||||
|  fi | ||||
|  if [ "${2}" = "start" ] || [ "${2}" = "reconnect" ]; then | ||||
|   /sbin/iptables -I FORWARD -o virbr0 -p tcp -d $GUEST_IP --dport $GUEST_PORT -j ACCEPT | ||||
|   /sbin/iptables -t nat -I PREROUTING -p tcp --dport $HOST_PORT -j DNAT --to $GUEST_IP:$GUEST_PORT | ||||
|  fi | ||||
| fi | ||||
| ``` | ||||
|  | ||||
| * Make the hook executable: | ||||
|  | ||||
| ```bash | ||||
| sudo chmod +x /etc/libvirt/hooks/qemu | ||||
| ``` | ||||
|  | ||||
| * Restart the `libvirtd` service: | ||||
|  | ||||
| ```bash | ||||
| sudo systemctl restart libvirtd.service | ||||
| ``` | ||||
|  | ||||
| * Start the guest | ||||
|  | ||||
|  | ||||
| > Note: this guide is based on https://wiki.libvirt.org/Networking.html | ||||
| ## Screenshot | ||||
|  | ||||
|   | ||||
							
								
								
									
										9
									
								
								renovate.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								renovate.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| { | ||||
|   "$schema": "https://docs.renovatebot.com/renovate-schema.json", | ||||
|   "packageRules": [ | ||||
|     { | ||||
|       "matchUpdateTypes": ["major", "minor", "patch"], | ||||
|       "automerge": true | ||||
|     } | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										2462
									
								
								virtweb_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2462
									
								
								virtweb_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -6,41 +6,43 @@ edition = "2021" | ||||
| # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||||
|  | ||||
| [dependencies] | ||||
| log = "0.4.19" | ||||
| env_logger = "0.10.1" | ||||
| clap = { version = "4.4.11", features = ["derive", "env"] } | ||||
| light-openid = { version = "1.0.1", features = ["crypto-wrapper"] } | ||||
| lazy_static = "1.4.0" | ||||
| actix = "0.13.1" | ||||
| actix-web = "4" | ||||
| log = "0.4.21" | ||||
| env_logger = "0.11.3" | ||||
| clap = { version = "4.5.20", features = ["derive", "env"] } | ||||
| light-openid = { version = "1.0.2", features = ["crypto-wrapper"] } | ||||
| lazy_static = "1.5.0" | ||||
| actix = "0.13.3" | ||||
| actix-web = "4.9.0" | ||||
| actix-remote-ip = "0.1.0" | ||||
| actix-session = { version = "0.8.0", features = ["cookie-session"] } | ||||
| actix-identity = "0.6.0" | ||||
| actix-cors = "0.6.5" | ||||
| actix-files = "0.6.2" | ||||
| actix-web-actors = "4.2.0" | ||||
| actix-http = "3.4.0" | ||||
| serde = { version = "1.0.193", features = ["derive"] } | ||||
| serde_json = "1.0.108" | ||||
| serde-xml-rs = "0.6.0" | ||||
| futures-util = "0.3.28" | ||||
| anyhow = "1.0.75" | ||||
| actix-multipart = "0.6.1" | ||||
| tempfile = "3.8.1" | ||||
| reqwest = { version = "0.11.23", features = ["stream"] } | ||||
| actix-session = { version = "0.10.0", features = ["cookie-session"] } | ||||
| actix-identity = "0.8.0" | ||||
| actix-cors = "0.7.0" | ||||
| actix-files = "0.6.5" | ||||
| actix-web-actors = "4.3.0" | ||||
| actix-http = "3.9.0" | ||||
| serde = { version = "1.0.215", features = ["derive"] } | ||||
| serde_json = "1.0.132" | ||||
| quick-xml = { version = "0.37.1", features = ["serialize", "overlapped-lists"] } | ||||
| futures-util = "0.3.31" | ||||
| anyhow = "1.0.93" | ||||
| actix-multipart = "0.7.0" | ||||
| tempfile = "3.14.0" | ||||
| reqwest = { version = "0.12.9", features = ["stream"] } | ||||
| url = "2.5.0" | ||||
| virt = "0.3.1" | ||||
| sysinfo = { version = "0.29.11", features = ["serde"] } | ||||
| uuid = { version = "1.6.1", features = ["v4", "serde"] } | ||||
| lazy-regex = "3.1.0" | ||||
| thiserror = "1.0.51" | ||||
| image = "0.24.7" | ||||
| virt = "0.4.1" | ||||
| sysinfo = { version = "0.32.0", features = ["serde"] } | ||||
| uuid = { version = "1.11.0", features = ["v4", "serde"] } | ||||
| lazy-regex = "3.3.0" | ||||
| thiserror = "2.0.0" | ||||
| image = "0.25.4" | ||||
| rand = "0.8.5" | ||||
| bytes = "1.5.0" | ||||
| tokio = "1.35.0" | ||||
| futures = "0.3.29" | ||||
| bytes = "1.8.0" | ||||
| tokio = "1.41.1" | ||||
| futures = "0.3.31" | ||||
| ipnetwork = "0.20.0" | ||||
| num = "0.4.1" | ||||
| rust-embed = { version = "8.1.0" } | ||||
| num = "0.4.2" | ||||
| rust-embed = { version = "8.5.0" } | ||||
| mime_guess = "2.0.4" | ||||
| dotenvy = "0.15.7" | ||||
| dotenvy = "0.15.7" | ||||
| nix = { version = "0.29.0", features = ["net"] } | ||||
| basic-jwt = "0.2.0" | ||||
|   | ||||
| @@ -1,9 +1,8 @@ | ||||
| services: | ||||
|   oidc: | ||||
|     image: qlik/simple-oidc-provider | ||||
|     environment: | ||||
|     - REDIRECTS=http://localhost:3000/oidc_cb | ||||
|     - PORT=9001 | ||||
|     image: dexidp/dex | ||||
|     ports: | ||||
|     - 9001:9001 | ||||
|      | ||||
|       - 9001:9001 | ||||
|     volumes: | ||||
|       - ./docker/dex:/conf:ro | ||||
|     command: [ "dex", "serve", "/conf/dex.config.yaml" ] | ||||
							
								
								
									
										27
									
								
								virtweb_backend/docker/dex/dex.config.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								virtweb_backend/docker/dex/dex.config.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| issuer: http://127.0.0.1:9001/dex | ||||
|  | ||||
| storage: | ||||
|   type: memory | ||||
|  | ||||
| web: | ||||
|   http: 0.0.0.0:9001 | ||||
|  | ||||
| oauth2: | ||||
|   # Automate some clicking | ||||
|   # Note: this might actually make some tests pass that otherwise wouldn't. | ||||
|   skipApprovalScreen: false | ||||
|  | ||||
| connectors: | ||||
|   # Note: this might actually make some tests pass that otherwise wouldn't. | ||||
|   - type: mockCallback | ||||
|     id: mock | ||||
|     name: Example | ||||
|  | ||||
| # Basic OP test suite requires two clients. | ||||
| staticClients: | ||||
|   - id: foo | ||||
|     secret: bar | ||||
|     redirectURIs: | ||||
|       - http://localhost:3000/oidc_cb | ||||
|       - http://localhost:5173/oidc_cb | ||||
|     name: Project | ||||
							
								
								
									
										66
									
								
								virtweb_backend/examples/api_curl.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								virtweb_backend/examples/api_curl.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | ||||
| use basic_jwt::JWTPrivateKey; | ||||
| use clap::Parser; | ||||
| use std::os::unix::prelude::CommandExt; | ||||
| use std::process::Command; | ||||
| use std::str::FromStr; | ||||
| use virtweb_backend::api_tokens::TokenVerb; | ||||
| use virtweb_backend::extractors::api_auth_extractor::TokenClaims; | ||||
| use virtweb_backend::utils::time_utils::time; | ||||
|  | ||||
| /// cURL wrapper to query Virtweb backend API | ||||
| #[derive(Parser, Debug)] | ||||
| #[command(version, about, long_about = None)] | ||||
| struct Args { | ||||
|     /// URL of VirtWeb | ||||
|     #[arg(short('u'), long, env, default_value = "http://localhost:8000")] | ||||
|     virtweb_url: String, | ||||
|  | ||||
|     /// Token ID | ||||
|     #[arg(short('i'), long, env)] | ||||
|     token_id: String, | ||||
|  | ||||
|     /// Token private key | ||||
|     #[arg(short('t'), long, env)] | ||||
|     token_key: String, | ||||
|  | ||||
|     /// Request verb | ||||
|     #[arg(short('X'), long, default_value = "GET")] | ||||
|     verb: String, | ||||
|  | ||||
|     /// Request URI | ||||
|     uri: String, | ||||
|  | ||||
|     /// Command line arguments to pass to cURL | ||||
|     #[clap(trailing_var_arg = true, allow_hyphen_values = true)] | ||||
|     run: Vec<String>, | ||||
| } | ||||
|  | ||||
| fn main() { | ||||
|     let args = Args::parse(); | ||||
|  | ||||
|     let full_url = format!("{}{}", args.virtweb_url, args.uri); | ||||
|     log::debug!("Full URL: {full_url}"); | ||||
|  | ||||
|     let key = JWTPrivateKey::ES384 { | ||||
|         r#priv: args.token_key, | ||||
|     }; | ||||
|     let claims = TokenClaims { | ||||
|         sub: args.token_id.to_string(), | ||||
|         iat: time() as usize, | ||||
|         exp: time() as usize + 50, | ||||
|         verb: TokenVerb::from_str(&args.verb).expect("Invalid request verb!"), | ||||
|         path: args.uri, | ||||
|         nonce: uuid::Uuid::new_v4().to_string(), | ||||
|     }; | ||||
|  | ||||
|     let jwt = key.sign_jwt(&claims).expect("Failed to sign JWT!"); | ||||
|  | ||||
|     Command::new("curl") | ||||
|         .args(["-X", &args.verb]) | ||||
|         .args(["-H", &format!("x-token-id: {}", args.token_id)]) | ||||
|         .args(["-H", &format!("x-token-content: {jwt}")]) | ||||
|         .args(args.run) | ||||
|         .arg(full_url) | ||||
|         .exec(); | ||||
|     panic!("Failed to run curl!") | ||||
| } | ||||
| @@ -1,12 +1,20 @@ | ||||
| use crate::app_config::AppConfig; | ||||
| use crate::libvirt_lib_structures::{DomainState, DomainXML, NetworkXML, XMLUuid}; | ||||
| use crate::libvirt_rest_structures::*; | ||||
| use crate::libvirt_lib_structures::domain::*; | ||||
| use crate::libvirt_lib_structures::network::*; | ||||
| use crate::libvirt_lib_structures::nwfilter::*; | ||||
| use crate::libvirt_lib_structures::*; | ||||
| use crate::libvirt_rest_structures::hypervisor::*; | ||||
| use crate::libvirt_rest_structures::net::*; | ||||
| use crate::libvirt_rest_structures::nw_filter::{NetworkFilter, NetworkFilterName}; | ||||
| use crate::libvirt_rest_structures::vm::*; | ||||
| use crate::nat::nat_lib; | ||||
| use actix::{Actor, Context, Handler, Message}; | ||||
| use image::ImageOutputFormat; | ||||
| use image::ImageFormat; | ||||
| use std::io::Cursor; | ||||
| use virt::connect::Connect; | ||||
| use virt::domain::Domain; | ||||
| use virt::network::Network; | ||||
| use virt::nwfilter::NWFilter; | ||||
| use virt::stream::Stream; | ||||
| use virt::sys; | ||||
| use virt::sys::VIR_DOMAIN_XML_SECURE; | ||||
| @@ -23,7 +31,7 @@ impl LibVirtActor { | ||||
|             "Will connect to hypvervisor at address '{}'", | ||||
|             hypervisor_uri | ||||
|         ); | ||||
|         let conn = Connect::open(hypervisor_uri)?; | ||||
|         let conn = Connect::open(Some(hypervisor_uri))?; | ||||
|  | ||||
|         Ok(Self { m: conn }) | ||||
|     } | ||||
| @@ -95,7 +103,7 @@ impl Handler<GetDomainXMLReq> for LibVirtActor { | ||||
|         let domain = Domain::lookup_by_uuid_string(&self.m, &msg.0.as_string())?; | ||||
|         let xml = domain.get_xml_desc(VIR_DOMAIN_XML_SECURE)?; | ||||
|         log::debug!("XML = {}", xml); | ||||
|         Ok(serde_xml_rs::from_str(&xml)?) | ||||
|         DomainXML::parse_xml(&xml) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -115,17 +123,24 @@ impl Handler<GetSourceDomainXMLReq> for LibVirtActor { | ||||
|  | ||||
| #[derive(Message)] | ||||
| #[rtype(result = "anyhow::Result<XMLUuid>")] | ||||
| pub struct DefineDomainReq(pub DomainXML); | ||||
| pub struct DefineDomainReq(pub VMInfo, pub DomainXML); | ||||
|  | ||||
| impl Handler<DefineDomainReq> for LibVirtActor { | ||||
|     type Result = anyhow::Result<XMLUuid>; | ||||
|  | ||||
|     fn handle(&mut self, msg: DefineDomainReq, _ctx: &mut Self::Context) -> Self::Result { | ||||
|         let xml = msg.0.into_xml()?; | ||||
|     fn handle(&mut self, mut msg: DefineDomainReq, _ctx: &mut Self::Context) -> Self::Result { | ||||
|         let xml = msg.1.as_xml()?; | ||||
|  | ||||
|         log::debug!("Define domain:\n{}", xml); | ||||
|         let domain = Domain::define_xml(&self.m, &xml)?; | ||||
|         XMLUuid::parse_from_str(&domain.get_uuid_string()?) | ||||
|         let uuid = XMLUuid::parse_from_str(&domain.get_uuid_string()?)?; | ||||
|  | ||||
|         // Save a copy of the source definition | ||||
|         msg.0.uuid = Some(uuid); | ||||
|         let json = serde_json::to_string(&msg.0)?; | ||||
|         std::fs::write(AppConfig::get().vm_definition_path(&msg.0.name), json)?; | ||||
|  | ||||
|         Ok(uuid) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -155,6 +170,12 @@ impl Handler<DeleteDomainReq> for LibVirtActor { | ||||
|             std::fs::remove_file(vnc_socket)?; | ||||
|         } | ||||
|  | ||||
|         // Remove backup definition | ||||
|         let backup_definition = AppConfig::get().vm_definition_path(&domain_name); | ||||
|         if backup_definition.exists() { | ||||
|             std::fs::remove_file(backup_definition)?; | ||||
|         } | ||||
|  | ||||
|         // Delete the domain | ||||
|         domain.undefine_flags(match msg.keep_files { | ||||
|             true => sys::VIR_DOMAIN_UNDEFINE_KEEP_NVRAM, | ||||
| @@ -316,7 +337,7 @@ impl Handler<ScreenshotDomainReq> for LibVirtActor { | ||||
|  | ||||
|         let image = image::load_from_memory(&screen_out)?; | ||||
|         let mut png_out = Cursor::new(Vec::new()); | ||||
|         image.write_to(&mut png_out, ImageOutputFormat::Png)?; | ||||
|         image.write_to(&mut png_out, ImageFormat::Png)?; | ||||
|  | ||||
|         Ok(png_out.into_inner()) | ||||
|     } | ||||
| @@ -360,20 +381,30 @@ impl Handler<SetDomainAutostart> for LibVirtActor { | ||||
|  | ||||
| #[derive(Message)] | ||||
| #[rtype(result = "anyhow::Result<XMLUuid>")] | ||||
| pub struct DefineNetwork(pub NetworkXML); | ||||
| pub struct DefineNetwork(pub NetworkInfo, pub NetworkXML); | ||||
|  | ||||
| impl Handler<DefineNetwork> for LibVirtActor { | ||||
|     type Result = anyhow::Result<XMLUuid>; | ||||
|  | ||||
|     fn handle(&mut self, msg: DefineNetwork, _ctx: &mut Self::Context) -> Self::Result { | ||||
|         log::debug!("Define network: {:?}", msg.0); | ||||
|     fn handle(&mut self, mut msg: DefineNetwork, _ctx: &mut Self::Context) -> Self::Result { | ||||
|         log::debug!("Define network: {:?}", msg.1); | ||||
|  | ||||
|         log::debug!("Source network structure: {:#?}", msg.0); | ||||
|         let network_xml = msg.0.into_xml()?; | ||||
|         log::debug!("Source network structure: {:#?}", msg.1); | ||||
|         let network_xml = msg.1.as_xml()?; | ||||
|         log::debug!("Define network XML: {network_xml}"); | ||||
|  | ||||
|         let network = Network::define_xml(&self.m, &network_xml)?; | ||||
|         XMLUuid::parse_from_str(&network.get_uuid_string()?) | ||||
|         let uuid = XMLUuid::parse_from_str(&network.get_uuid_string()?)?; | ||||
|  | ||||
|         // Save NAT definition | ||||
|         nat_lib::save_nat_def(&msg.0)?; | ||||
|  | ||||
|         // Save a copy of the source definition | ||||
|         msg.0.uuid = Some(uuid); | ||||
|         let json = serde_json::to_string(&msg.0)?; | ||||
|         std::fs::write(AppConfig::get().net_definition_path(&msg.0.name), json)?; | ||||
|  | ||||
|         Ok(uuid) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -409,7 +440,7 @@ impl Handler<GetNetworkXMLReq> for LibVirtActor { | ||||
|         let network = Network::lookup_by_uuid_string(&self.m, &msg.0.as_string())?; | ||||
|         let xml = network.get_xml_desc(0)?; | ||||
|         log::debug!("XML = {}", xml); | ||||
|         Ok(serde_xml_rs::from_str(&xml)?) | ||||
|         NetworkXML::parse_xml(&xml) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -437,7 +468,18 @@ impl Handler<DeleteNetwork> for LibVirtActor { | ||||
|     fn handle(&mut self, msg: DeleteNetwork, _ctx: &mut Self::Context) -> Self::Result { | ||||
|         log::debug!("Delete network: {}\n", msg.0.as_string()); | ||||
|         let network = Network::lookup_by_uuid_string(&self.m, &msg.0.as_string())?; | ||||
|         let network_name = NetworkName(network.get_name()?); | ||||
|         network.undefine()?; | ||||
|  | ||||
|         // Remove NAT definition, if any | ||||
|         nat_lib::remove_nat_def(&network_name)?; | ||||
|  | ||||
|         // Remove backup definition | ||||
|         let backup_definition = AppConfig::get().net_definition_path(&network_name); | ||||
|         if backup_definition.exists() { | ||||
|             std::fs::remove_file(backup_definition)?; | ||||
|         } | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
| @@ -521,3 +563,107 @@ impl Handler<StopNetwork> for LibVirtActor { | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Message)] | ||||
| #[rtype(result = "anyhow::Result<Vec<XMLUuid>>")] | ||||
| pub struct GetNWFiltersListReq; | ||||
|  | ||||
| impl Handler<GetNWFiltersListReq> for LibVirtActor { | ||||
|     type Result = anyhow::Result<Vec<XMLUuid>>; | ||||
|  | ||||
|     fn handle(&mut self, _msg: GetNWFiltersListReq, _ctx: &mut Self::Context) -> Self::Result { | ||||
|         log::debug!("Get full list of network filters"); | ||||
|         let networks = self.m.list_all_nw_filters(0)?; | ||||
|         let mut ids = Vec::with_capacity(networks.len()); | ||||
|  | ||||
|         for d in networks { | ||||
|             ids.push(XMLUuid::parse_from_str(&d.get_uuid_string()?)?); | ||||
|         } | ||||
|  | ||||
|         Ok(ids) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Message)] | ||||
| #[rtype(result = "anyhow::Result<NetworkFilterXML>")] | ||||
| pub struct GetNWFilterXMLReq(pub XMLUuid); | ||||
|  | ||||
| impl Handler<GetNWFilterXMLReq> for LibVirtActor { | ||||
|     type Result = anyhow::Result<NetworkFilterXML>; | ||||
|  | ||||
|     fn handle(&mut self, msg: GetNWFilterXMLReq, _ctx: &mut Self::Context) -> Self::Result { | ||||
|         log::debug!("Get network filter XML:\n{}", msg.0.as_string()); | ||||
|         let filter = NWFilter::lookup_by_uuid_string(&self.m, &msg.0.as_string())?; | ||||
|         let xml = filter.get_xml_desc(0)?; | ||||
|         log::debug!("XML = {}", xml); | ||||
|         NetworkFilterXML::parse_xml(xml) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Message)] | ||||
| #[rtype(result = "anyhow::Result<XMLUuid>")] | ||||
| pub struct DefineNWFilterReq(pub NetworkFilter, pub NetworkFilterXML); | ||||
|  | ||||
| impl Handler<DefineNWFilterReq> for LibVirtActor { | ||||
|     type Result = anyhow::Result<XMLUuid>; | ||||
|  | ||||
|     fn handle(&mut self, mut msg: DefineNWFilterReq, _ctx: &mut Self::Context) -> Self::Result { | ||||
|         let xml = msg.1.into_xml()?; | ||||
|  | ||||
|         log::debug!("Define network filter:\n{}", xml); | ||||
|         let filter = NWFilter::define_xml(&self.m, &xml)?; | ||||
|         let uuid = XMLUuid::parse_from_str(&filter.get_uuid_string()?)?; | ||||
|  | ||||
|         // Save a copy of the source definition | ||||
|         msg.0.uuid = Some(uuid); | ||||
|         let json = serde_json::to_string(&msg.0)?; | ||||
|         std::fs::write( | ||||
|             AppConfig::get().net_filter_definition_path(&msg.0.name), | ||||
|             json, | ||||
|         )?; | ||||
|  | ||||
|         Ok(uuid) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Message)] | ||||
| #[rtype(result = "anyhow::Result<()>")] | ||||
| pub struct DeleteNetworkFilter(pub XMLUuid); | ||||
|  | ||||
| impl Handler<DeleteNetworkFilter> for LibVirtActor { | ||||
|     type Result = anyhow::Result<()>; | ||||
|  | ||||
|     fn handle(&mut self, msg: DeleteNetworkFilter, _ctx: &mut Self::Context) -> Self::Result { | ||||
|         log::debug!("Delete network filter: {}\n", msg.0.as_string()); | ||||
|         let nw_filter = NWFilter::lookup_by_uuid_string(&self.m, &msg.0.as_string())?; | ||||
|         let nw_filter_name = nw_filter.get_name()?; | ||||
|         nw_filter.undefine()?; | ||||
|  | ||||
|         // Remove backup definition | ||||
|         let backup_definition = | ||||
|             AppConfig::get().net_filter_definition_path(&NetworkFilterName(nw_filter_name)); | ||||
|         if backup_definition.exists() { | ||||
|             std::fs::remove_file(backup_definition)?; | ||||
|         } | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Message)] | ||||
| #[rtype(result = "anyhow::Result<String>")] | ||||
| pub struct GetSourceNetworkFilterXMLReq(pub XMLUuid); | ||||
|  | ||||
| impl Handler<GetSourceNetworkFilterXMLReq> for LibVirtActor { | ||||
|     type Result = anyhow::Result<String>; | ||||
|  | ||||
|     fn handle( | ||||
|         &mut self, | ||||
|         msg: GetSourceNetworkFilterXMLReq, | ||||
|         _ctx: &mut Self::Context, | ||||
|     ) -> Self::Result { | ||||
|         log::debug!("Get nw filter source XML:\n{}", msg.0.as_string()); | ||||
|         let nwfilter = NWFilter::lookup_by_uuid_string(&self.m, &msg.0.as_string())?; | ||||
|         Ok(nwfilter.get_xml_desc(0)?) | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										299
									
								
								virtweb_backend/src/api_tokens.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										299
									
								
								virtweb_backend/src/api_tokens.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,299 @@ | ||||
| //! # API tokens management | ||||
|  | ||||
| use crate::app_config::AppConfig; | ||||
| use crate::constants; | ||||
| use crate::utils::time_utils::time; | ||||
| use actix_http::Method; | ||||
| use basic_jwt::{JWTPrivateKey, JWTPublicKey}; | ||||
| use std::path::Path; | ||||
| use std::str::FromStr; | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug)] | ||||
| pub struct TokenID(pub uuid::Uuid); | ||||
|  | ||||
| impl TokenID { | ||||
|     /// Parse a string as a token id | ||||
|     pub fn parse(t: &str) -> anyhow::Result<Self> { | ||||
|         Ok(Self(uuid::Uuid::parse_str(t)?)) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq)] | ||||
| pub struct TokenRight { | ||||
|     verb: TokenVerb, | ||||
|     path: String, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] | ||||
| pub struct TokenRights(Vec<TokenRight>); | ||||
|  | ||||
| impl TokenRights { | ||||
|     pub fn check_error(&self) -> Option<&'static str> { | ||||
|         for r in &self.0 { | ||||
|             if !r.path.starts_with("/api/") { | ||||
|                 return Some("All API rights shall start with /api/"); | ||||
|             } | ||||
|  | ||||
|             if r.path.len() > constants::API_TOKEN_RIGHT_PATH_MAX_LENGTH { | ||||
|                 return Some("An API path shall not exceed maximum URL size!"); | ||||
|             } | ||||
|         } | ||||
|         None | ||||
|     } | ||||
|  | ||||
|     pub fn contains(&self, verb: TokenVerb, path: &str) -> bool { | ||||
|         let req_path_split = path.split('/').collect::<Vec<_>>(); | ||||
|  | ||||
|         'root: for r in &self.0 { | ||||
|             if r.verb != verb { | ||||
|                 continue 'root; | ||||
|             } | ||||
|  | ||||
|             let mut last_idx = 0; | ||||
|             for (idx, part) in r.path.split('/').enumerate() { | ||||
|                 if idx >= req_path_split.len() { | ||||
|                     continue 'root; | ||||
|                 } | ||||
|  | ||||
|                 if part != "*" && part != req_path_split[idx] { | ||||
|                     continue 'root; | ||||
|                 } | ||||
|  | ||||
|                 last_idx = idx; | ||||
|             } | ||||
|  | ||||
|             // Check we visited the whole path | ||||
|             if last_idx + 1 == req_path_split.len() { | ||||
|                 return true; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         false | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] | ||||
| pub struct Token { | ||||
|     pub id: TokenID, | ||||
|     pub name: String, | ||||
|     pub description: String, | ||||
|     created: u64, | ||||
|     updated: u64, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub pub_key: Option<JWTPublicKey>, | ||||
|     pub rights: TokenRights, | ||||
|     pub last_used: u64, | ||||
|     pub ip_restriction: Option<ipnetwork::IpNetwork>, | ||||
|     pub max_inactivity: Option<u64>, | ||||
| } | ||||
|  | ||||
| impl Token { | ||||
|     /// Turn the token into a JSON string | ||||
|     fn save(&self) -> anyhow::Result<()> { | ||||
|         let json = serde_json::to_string(self)?; | ||||
|  | ||||
|         std::fs::write(AppConfig::get().api_token_definition_path(self.id), json)?; | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     /// Load token information from a file | ||||
|     fn load_from_file(path: &Path) -> anyhow::Result<Self> { | ||||
|         Ok(serde_json::from_str(&std::fs::read_to_string(path)?)?) | ||||
|     } | ||||
|  | ||||
|     /// Check whether a token is expired or not | ||||
|     pub fn is_expired(&self) -> bool { | ||||
|         if let Some(max_inactivity) = self.max_inactivity { | ||||
|             if max_inactivity + self.last_used < time() { | ||||
|                 return true; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         false | ||||
|     } | ||||
|  | ||||
|     /// Check whether last_used shall be updated or not | ||||
|     pub fn should_update_last_activity(&self) -> bool { | ||||
|         self.last_used + 3600 < time() | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, Eq, PartialEq)] | ||||
| pub enum TokenVerb { | ||||
|     GET, | ||||
|     POST, | ||||
|     PUT, | ||||
|     PATCH, | ||||
|     DELETE, | ||||
| } | ||||
|  | ||||
| impl TokenVerb { | ||||
|     pub fn as_method(&self) -> Method { | ||||
|         match self { | ||||
|             TokenVerb::GET => Method::GET, | ||||
|             TokenVerb::POST => Method::POST, | ||||
|             TokenVerb::PUT => Method::PUT, | ||||
|             TokenVerb::PATCH => Method::PATCH, | ||||
|             TokenVerb::DELETE => Method::DELETE, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl FromStr for TokenVerb { | ||||
|     type Err = (); | ||||
|  | ||||
|     fn from_str(s: &str) -> Result<Self, Self::Err> { | ||||
|         match s { | ||||
|             "GET" => Ok(TokenVerb::GET), | ||||
|             "POST" => Ok(TokenVerb::POST), | ||||
|             "PUT" => Ok(TokenVerb::PUT), | ||||
|             "PATCH" => Ok(TokenVerb::PATCH), | ||||
|             "DELETE" => Ok(TokenVerb::DELETE), | ||||
|             _ => Err(()), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Structure used to create a token | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] | ||||
| pub struct NewToken { | ||||
|     pub name: String, | ||||
|     pub description: String, | ||||
|     pub rights: TokenRights, | ||||
|     pub ip_restriction: Option<ipnetwork::IpNetwork>, | ||||
|     pub max_inactivity: Option<u64>, | ||||
| } | ||||
|  | ||||
| impl NewToken { | ||||
|     /// Check for error in token | ||||
|     pub fn check_error(&self) -> Option<&'static str> { | ||||
|         if self.name.len() < constants::API_TOKEN_NAME_MIN_LENGTH { | ||||
|             return Some("Name is too short!"); | ||||
|         } | ||||
|  | ||||
|         if self.name.len() > constants::API_TOKEN_NAME_MAX_LENGTH { | ||||
|             return Some("Name is too long!"); | ||||
|         } | ||||
|  | ||||
|         if self.description.len() < constants::API_TOKEN_DESCRIPTION_MIN_LENGTH { | ||||
|             return Some("Description is too short!"); | ||||
|         } | ||||
|  | ||||
|         if self.description.len() > constants::API_TOKEN_DESCRIPTION_MAX_LENGTH { | ||||
|             return Some("Description is too long!"); | ||||
|         } | ||||
|  | ||||
|         if let Some(err) = self.rights.check_error() { | ||||
|             return Some(err); | ||||
|         } | ||||
|  | ||||
|         if let Some(t) = self.max_inactivity { | ||||
|             if t < 3600 { | ||||
|                 return Some("API tokens shall be valid for at least 1 hour!"); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         None | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Create a new Token | ||||
| pub async fn create(t: &NewToken) -> anyhow::Result<(Token, JWTPrivateKey)> { | ||||
|     let priv_key = JWTPrivateKey::generate_ec384_signing_key()?; | ||||
|     let pub_key = priv_key.to_public_key()?; | ||||
|  | ||||
|     let token = Token { | ||||
|         name: t.name.to_string(), | ||||
|         description: t.description.to_string(), | ||||
|         id: TokenID(uuid::Uuid::new_v4()), | ||||
|         created: time(), | ||||
|         updated: time(), | ||||
|         pub_key: Some(pub_key), | ||||
|         rights: t.rights.clone(), | ||||
|         last_used: time(), | ||||
|         ip_restriction: t.ip_restriction, | ||||
|         max_inactivity: t.max_inactivity, | ||||
|     }; | ||||
|  | ||||
|     token.save()?; | ||||
|  | ||||
|     Ok((token, priv_key)) | ||||
| } | ||||
|  | ||||
| /// Get the entire list of api tokens | ||||
| pub async fn full_list() -> anyhow::Result<Vec<Token>> { | ||||
|     let mut list = Vec::new(); | ||||
|     for f in std::fs::read_dir(AppConfig::get().api_tokens_path())? { | ||||
|         list.push(Token::load_from_file(&f?.path())?); | ||||
|     } | ||||
|     Ok(list) | ||||
| } | ||||
|  | ||||
| /// Get the information about a single token | ||||
| pub async fn get_single(id: TokenID) -> anyhow::Result<Token> { | ||||
|     Token::load_from_file(&AppConfig::get().api_token_definition_path(id)) | ||||
| } | ||||
|  | ||||
| /// Update API tokens rights | ||||
| pub async fn update_rights(id: TokenID, rights: TokenRights) -> anyhow::Result<()> { | ||||
|     let mut token = get_single(id).await?; | ||||
|     token.rights = rights; | ||||
|     token.updated = time(); | ||||
|     token.save()?; | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| /// Set last_used value of token | ||||
| pub async fn refresh_last_used(id: TokenID) -> anyhow::Result<()> { | ||||
|     let mut token = get_single(id).await?; | ||||
|     token.last_used = time(); | ||||
|     token.save()?; | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| /// Delete an API token | ||||
| pub async fn delete(id: TokenID) -> anyhow::Result<()> { | ||||
|     let path = AppConfig::get().api_token_definition_path(id); | ||||
|     if path.exists() { | ||||
|         std::fs::remove_file(path)?; | ||||
|     } | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| #[cfg(test)] | ||||
| mod test { | ||||
|     use crate::api_tokens::{TokenRight, TokenRights, TokenVerb}; | ||||
|  | ||||
|     #[test] | ||||
|     fn test_rights_patch() { | ||||
|         let rights = TokenRights(vec![ | ||||
|             TokenRight { | ||||
|                 path: "/api/vm/*".to_string(), | ||||
|                 verb: TokenVerb::GET, | ||||
|             }, | ||||
|             TokenRight { | ||||
|                 path: "/api/vm/a".to_string(), | ||||
|                 verb: TokenVerb::PUT, | ||||
|             }, | ||||
|             TokenRight { | ||||
|                 path: "/api/vm/a/other".to_string(), | ||||
|                 verb: TokenVerb::DELETE, | ||||
|             }, | ||||
|             TokenRight { | ||||
|                 path: "/api/net/create".to_string(), | ||||
|                 verb: TokenVerb::POST, | ||||
|             }, | ||||
|         ]); | ||||
|  | ||||
|         assert!(rights.contains(TokenVerb::GET, "/api/vm/ab")); | ||||
|         assert!(!rights.contains(TokenVerb::GET, "/api/vm")); | ||||
|         assert!(!rights.contains(TokenVerb::GET, "/api/vm/ab/c")); | ||||
|         assert!(rights.contains(TokenVerb::PUT, "/api/vm/a")); | ||||
|         assert!(!rights.contains(TokenVerb::PUT, "/api/vm/other")); | ||||
|         assert!(rights.contains(TokenVerb::POST, "/api/net/create")); | ||||
|         assert!(!rights.contains(TokenVerb::GET, "/api/net/create")); | ||||
|         assert!(!rights.contains(TokenVerb::POST, "/api/net/b")); | ||||
|         assert!(!rights.contains(TokenVerb::POST, "/api/net/create/b")); | ||||
|     } | ||||
| } | ||||
| @@ -1,4 +1,8 @@ | ||||
| use crate::api_tokens::TokenID; | ||||
| use crate::constants; | ||||
| use crate::libvirt_lib_structures::XMLUuid; | ||||
| use crate::libvirt_rest_structures::net::NetworkName; | ||||
| use crate::libvirt_rest_structures::nw_filter::NetworkFilterName; | ||||
| use clap::Parser; | ||||
| use std::net::IpAddr; | ||||
| use std::path::{Path, PathBuf}; | ||||
| @@ -64,7 +68,7 @@ pub struct AppConfig { | ||||
|     #[arg( | ||||
|         long, | ||||
|         env, | ||||
|         default_value = "http://localhost:9001/.well-known/openid-configuration" | ||||
|         default_value = "http://localhost:9001/dex/.well-known/openid-configuration" | ||||
|     )] | ||||
|     pub oidc_configuration_url: String, | ||||
|  | ||||
| @@ -99,10 +103,15 @@ pub struct AppConfig { | ||||
|     #[arg(short = 'H', long, env)] | ||||
|     pub hypervisor_uri: Option<String>, | ||||
|  | ||||
|     /// Trusted network. If set, a client from a different will not be able to perform request other | ||||
|     /// than those with GET verb (aside for login) | ||||
|     /// Trusted network. If set, a client (user) from a different network will not be able to perform | ||||
|     /// request other than those with GET verb (aside for login) | ||||
|     #[arg(short = 'T', long, env)] | ||||
|     pub trusted_network: Vec<String>, | ||||
|  | ||||
|     /// Comma-separated list of allowed networks. If set, a client (user or API token) from a | ||||
|     /// different network will not be able to access VirtWeb | ||||
|     #[arg(short = 'A', long, env)] | ||||
|     pub allowed_networks: Vec<String>, | ||||
| } | ||||
|  | ||||
| lazy_static::lazy_static! { | ||||
| @@ -134,14 +143,19 @@ impl AppConfig { | ||||
|  | ||||
|     /// Get auth cookie domain | ||||
|     pub fn cookie_domain(&self) -> Option<String> { | ||||
|         let domain = self.website_origin.split_once("://")?.1; | ||||
|         Some( | ||||
|             domain | ||||
|                 .split_once(':') | ||||
|                 .map(|s| s.0) | ||||
|                 .unwrap_or(domain) | ||||
|                 .to_string(), | ||||
|         ) | ||||
|         if cfg!(debug_assertions) { | ||||
|             let domain = self.website_origin.split_once("://")?.1; | ||||
|             Some( | ||||
|                 domain | ||||
|                     .split_once(':') | ||||
|                     .map(|s| s.0) | ||||
|                     .unwrap_or(domain) | ||||
|                     .to_string(), | ||||
|             ) | ||||
|         } else { | ||||
|             // In release mode, the web app is hosted on the same origin as the API | ||||
|             None | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Get app secret | ||||
| @@ -181,6 +195,25 @@ impl AppConfig { | ||||
|         false | ||||
|     } | ||||
|  | ||||
|     /// Check if an IP belongs to an allowed network or not | ||||
|     pub fn is_allowed_ip(&self, ip: IpAddr) -> bool { | ||||
|         if self.allowed_networks.is_empty() { | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         for i in &self.allowed_networks { | ||||
|             for sub_i in i.split(',') { | ||||
|                 let net = | ||||
|                     ipnetwork::IpNetwork::from_str(sub_i).expect("Allowed network is invalid!"); | ||||
|                 if net.contains(ip) { | ||||
|                     return true; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         false | ||||
|     } | ||||
|  | ||||
|     /// Get OpenID providers configuration | ||||
|     pub fn openid_provider(&self) -> Option<OIDCProvider<'_>> { | ||||
|         if self.disable_oidc { | ||||
| @@ -235,6 +268,39 @@ impl AppConfig { | ||||
|     pub fn vm_storage_path(&self, id: XMLUuid) -> PathBuf { | ||||
|         self.disks_storage_path().join(id.as_string()) | ||||
|     } | ||||
|  | ||||
|     pub fn definitions_path(&self) -> PathBuf { | ||||
|         self.storage_path().join("definitions") | ||||
|     } | ||||
|  | ||||
|     pub fn vm_definition_path(&self, name: &str) -> PathBuf { | ||||
|         self.definitions_path().join(format!("vm-{name}.json")) | ||||
|     } | ||||
|  | ||||
|     pub fn net_definition_path(&self, name: &NetworkName) -> PathBuf { | ||||
|         self.definitions_path().join(format!("net-{}.json", name.0)) | ||||
|     } | ||||
|  | ||||
|     pub fn nat_path(&self) -> PathBuf { | ||||
|         self.storage_path().join(constants::STORAGE_NAT_DIR) | ||||
|     } | ||||
|  | ||||
|     pub fn net_nat_path(&self, name: &NetworkName) -> PathBuf { | ||||
|         self.nat_path().join(name.nat_file_name()) | ||||
|     } | ||||
|  | ||||
|     pub fn net_filter_definition_path(&self, name: &NetworkFilterName) -> PathBuf { | ||||
|         self.definitions_path() | ||||
|             .join(format!("nwfilter-{}.json", name.0)) | ||||
|     } | ||||
|  | ||||
|     pub fn api_tokens_path(&self) -> PathBuf { | ||||
|         self.storage_path().join(constants::STORAGE_TOKENS_DIR) | ||||
|     } | ||||
|  | ||||
|     pub fn api_token_definition_path(&self, id: TokenID) -> PathBuf { | ||||
|         self.api_tokens_path().join(format!("{}.json", id.0)) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, serde::Serialize)] | ||||
|   | ||||
| @@ -44,5 +44,66 @@ pub const DISK_SIZE_MIN: usize = 100; | ||||
| /// Disk size max (MB) | ||||
| pub const DISK_SIZE_MAX: usize = 1000 * 1000 * 2; | ||||
|  | ||||
| /// Net nat entry comment max size | ||||
| pub const NET_NAT_COMMENT_MAX_SIZE: usize = 250; | ||||
|  | ||||
| /// Network mac address default prefix | ||||
| pub const NET_MAC_ADDR_PREFIX: &str = "52:54:00"; | ||||
|  | ||||
| /// Built-in network filter rules | ||||
| pub const BUILTIN_NETWORK_FILTER_RULES: [&str; 24] = [ | ||||
|     "allow-arp", | ||||
|     "allow-dhcp", | ||||
|     "allow-dhcp-server", | ||||
|     "allow-dhcpv6", | ||||
|     "allow-dhcpv6-server", | ||||
|     "allow-incoming-ipv4", | ||||
|     "allow-incoming-ipv6", | ||||
|     "allow-ipv4", | ||||
|     "allow-ipv6", | ||||
|     "clean-traffic", | ||||
|     "clean-traffic-gateway", | ||||
|     "no-arp-ip-spoofing", | ||||
|     "no-arp-mac-spoofing", | ||||
|     "no-arp-spoofing", | ||||
|     "no-ip-multicast", | ||||
|     "no-ip-spoofing", | ||||
|     "no-ipv6-multicast", | ||||
|     "no-ipv6-spoofing", | ||||
|     "no-mac-broadcast", | ||||
|     "no-mac-spoofing", | ||||
|     "no-other-l2-traffic", | ||||
|     "no-other-rarp-traffic", | ||||
|     "qemu-announce-self", | ||||
|     "qemu-announce-self-rarp", | ||||
| ]; | ||||
|  | ||||
| /// List of valid network chains | ||||
| pub const NETWORK_CHAINS: [&str; 8] = ["root", "mac", "stp", "vlan", "arp", "rarp", "ipv4", "ipv6"]; | ||||
|  | ||||
| /// Directory where nat rules are stored, inside storage directory | ||||
| pub const STORAGE_NAT_DIR: &str = "nat"; | ||||
|  | ||||
| /// Environment variable that is set to run VirtWeb in NAT configuration mode | ||||
| pub const NAT_MODE_ENV_VAR_NAME: &str = "NAT_MODE"; | ||||
|  | ||||
| /// Nat hook file path | ||||
| pub const NAT_HOOK_PATH: &str = "/etc/libvirt/hooks/network"; | ||||
|  | ||||
| /// Directory where API tokens are stored, inside storage directory | ||||
| pub const STORAGE_TOKENS_DIR: &str = "tokens"; | ||||
|  | ||||
| /// API token name min length | ||||
| pub const API_TOKEN_NAME_MIN_LENGTH: usize = 3; | ||||
|  | ||||
| /// API token name max length | ||||
| pub const API_TOKEN_NAME_MAX_LENGTH: usize = 30; | ||||
|  | ||||
| /// API token description min length | ||||
| pub const API_TOKEN_DESCRIPTION_MIN_LENGTH: usize = 5; | ||||
|  | ||||
| /// API token description max length | ||||
| pub const API_TOKEN_DESCRIPTION_MAX_LENGTH: usize = 30; | ||||
|  | ||||
| /// API token right path max length | ||||
| pub const API_TOKEN_RIGHT_PATH_MAX_LENGTH: usize = 255; | ||||
|   | ||||
							
								
								
									
										100
									
								
								virtweb_backend/src/controllers/api_tokens_controller.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								virtweb_backend/src/controllers/api_tokens_controller.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,100 @@ | ||||
| //! # API tokens management | ||||
|  | ||||
| use crate::api_tokens; | ||||
| use crate::api_tokens::{NewToken, TokenID, TokenRights}; | ||||
| use crate::controllers::api_tokens_controller::rest_token::RestToken; | ||||
| use crate::controllers::HttpResult; | ||||
| use actix_web::{web, HttpResponse}; | ||||
| use basic_jwt::JWTPrivateKey; | ||||
|  | ||||
| /// Create a special module for REST token to enforce usage of constructor function | ||||
| mod rest_token { | ||||
|     use crate::api_tokens::Token; | ||||
|  | ||||
|     #[derive(serde::Serialize)] | ||||
|     pub struct RestToken { | ||||
|         #[serde(flatten)] | ||||
|         token: Token, | ||||
|     } | ||||
|  | ||||
|     impl RestToken { | ||||
|         pub fn new(mut token: Token) -> Self { | ||||
|             token.pub_key = None; | ||||
|             Self { token } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize)] | ||||
| struct CreateTokenResult { | ||||
|     token: RestToken, | ||||
|     priv_key: JWTPrivateKey, | ||||
| } | ||||
|  | ||||
| /// Create a new API token | ||||
| pub async fn create(new_token: web::Json<NewToken>) -> HttpResult { | ||||
|     if let Some(err) = new_token.check_error() { | ||||
|         log::error!("Failed to validate new API token information! {err}"); | ||||
|         return Ok(HttpResponse::BadRequest().json(format!( | ||||
|             "Failed to validate new API token information! {err}" | ||||
|         ))); | ||||
|     } | ||||
|  | ||||
|     let (token, priv_key) = api_tokens::create(&new_token).await?; | ||||
|  | ||||
|     Ok(HttpResponse::Ok().json(CreateTokenResult { | ||||
|         token: RestToken::new(token), | ||||
|         priv_key, | ||||
|     })) | ||||
| } | ||||
|  | ||||
| /// Get the list of API tokens | ||||
| pub async fn list() -> HttpResult { | ||||
|     let list = api_tokens::full_list() | ||||
|         .await? | ||||
|         .into_iter() | ||||
|         .map(RestToken::new) | ||||
|         .collect::<Vec<_>>(); | ||||
|  | ||||
|     Ok(HttpResponse::Ok().json(list)) | ||||
| } | ||||
|  | ||||
| #[derive(serde::Deserialize)] | ||||
| pub struct TokenIDInPath { | ||||
|     uid: TokenID, | ||||
| } | ||||
|  | ||||
| /// Get the information about a single token | ||||
| pub async fn get_single(path: web::Path<TokenIDInPath>) -> HttpResult { | ||||
|     let token = api_tokens::get_single(path.uid).await?; | ||||
|  | ||||
|     Ok(HttpResponse::Ok().json(RestToken::new(token))) | ||||
| } | ||||
|  | ||||
| #[derive(serde::Deserialize)] | ||||
| pub struct UpdateTokenBody { | ||||
|     rights: TokenRights, | ||||
| } | ||||
|  | ||||
| /// Update a token | ||||
| pub async fn update( | ||||
|     path: web::Path<TokenIDInPath>, | ||||
|     body: web::Json<UpdateTokenBody>, | ||||
| ) -> HttpResult { | ||||
|     if let Some(err) = body.rights.check_error() { | ||||
|         log::error!("Failed to validate updated API token information! {err}"); | ||||
|         return Ok(HttpResponse::BadRequest() | ||||
|             .json(format!("Failed to validate API token information! {err}"))); | ||||
|     } | ||||
|  | ||||
|     api_tokens::update_rights(path.uid, body.0.rights).await?; | ||||
|  | ||||
|     Ok(HttpResponse::Accepted().finish()) | ||||
| } | ||||
|  | ||||
| /// Delete a token | ||||
| pub async fn delete(path: web::Path<TokenIDInPath>) -> HttpResult { | ||||
|     api_tokens::delete(path.uid).await?; | ||||
|  | ||||
|     Ok(HttpResponse::Accepted().finish()) | ||||
| } | ||||
							
								
								
									
										148
									
								
								virtweb_backend/src/controllers/groups_controller.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										148
									
								
								virtweb_backend/src/controllers/groups_controller.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,148 @@ | ||||
| use crate::controllers::{HttpResult, LibVirtReq}; | ||||
| use crate::extractors::group_vm_id_extractor::GroupVmIdExtractor; | ||||
| use crate::libvirt_rest_structures::vm::VMInfo; | ||||
| use actix_web::HttpResponse; | ||||
| use std::collections::HashMap; | ||||
|  | ||||
| /// Get the list of groups | ||||
| pub async fn list(client: LibVirtReq) -> HttpResult { | ||||
|     let groups = match client.get_full_groups_list().await { | ||||
|         Err(e) => { | ||||
|             log::error!("Failed to get the list of groups! {e}"); | ||||
|             return Ok(HttpResponse::InternalServerError() | ||||
|                 .json(format!("Failed to get the list of groups! {e}"))); | ||||
|         } | ||||
|         Ok(l) => l, | ||||
|     }; | ||||
|  | ||||
|     Ok(HttpResponse::Ok().json(groups)) | ||||
| } | ||||
|  | ||||
| /// Get information about the VMs of a group | ||||
| pub async fn vm_info(vms_xml: GroupVmIdExtractor) -> HttpResult { | ||||
|     let mut vms = Vec::new(); | ||||
|     for vm in vms_xml.0 { | ||||
|         vms.push(VMInfo::from_domain(vm)?) | ||||
|     } | ||||
|     Ok(HttpResponse::Ok().json(vms)) | ||||
| } | ||||
|  | ||||
| #[derive(Default, serde::Serialize)] | ||||
| pub struct TreatmentResult { | ||||
|     ok: usize, | ||||
|     failed: usize, | ||||
| } | ||||
|  | ||||
| /// Start the VMs of a group | ||||
| pub async fn vm_start(client: LibVirtReq, vms: GroupVmIdExtractor) -> HttpResult { | ||||
|     let mut res = TreatmentResult::default(); | ||||
|     for vm in vms.0 { | ||||
|         if let Some(uuid) = vm.uuid { | ||||
|             match client.start_domain(uuid).await { | ||||
|                 Ok(_) => res.ok += 1, | ||||
|                 Err(_) => res.failed += 1, | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     Ok(HttpResponse::Ok().json(res)) | ||||
| } | ||||
|  | ||||
| /// Shutdown the VMs of a group | ||||
| pub async fn vm_shutdown(client: LibVirtReq, vms: GroupVmIdExtractor) -> HttpResult { | ||||
|     let mut res = TreatmentResult::default(); | ||||
|     for vm in vms.0 { | ||||
|         if let Some(uuid) = vm.uuid { | ||||
|             match client.shutdown_domain(uuid).await { | ||||
|                 Ok(_) => res.ok += 1, | ||||
|                 Err(_) => res.failed += 1, | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     Ok(HttpResponse::Ok().json(res)) | ||||
| } | ||||
|  | ||||
| /// Suspend the VMs of a group | ||||
| pub async fn vm_suspend(client: LibVirtReq, vms: GroupVmIdExtractor) -> HttpResult { | ||||
|     let mut res = TreatmentResult::default(); | ||||
|     for vm in vms.0 { | ||||
|         if let Some(uuid) = vm.uuid { | ||||
|             match client.suspend_domain(uuid).await { | ||||
|                 Ok(_) => res.ok += 1, | ||||
|                 Err(_) => res.failed += 1, | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     Ok(HttpResponse::Ok().json(res)) | ||||
| } | ||||
|  | ||||
| /// Resume the VMs of a group | ||||
| pub async fn vm_resume(client: LibVirtReq, vms: GroupVmIdExtractor) -> HttpResult { | ||||
|     let mut res = TreatmentResult::default(); | ||||
|     for vm in vms.0 { | ||||
|         if let Some(uuid) = vm.uuid { | ||||
|             match client.resume_domain(uuid).await { | ||||
|                 Ok(_) => res.ok += 1, | ||||
|                 Err(_) => res.failed += 1, | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     Ok(HttpResponse::Ok().json(res)) | ||||
| } | ||||
|  | ||||
| /// Kill the VMs of a group | ||||
| pub async fn vm_kill(client: LibVirtReq, vms: GroupVmIdExtractor) -> HttpResult { | ||||
|     let mut res = TreatmentResult::default(); | ||||
|     for vm in vms.0 { | ||||
|         if let Some(uuid) = vm.uuid { | ||||
|             match client.kill_domain(uuid).await { | ||||
|                 Ok(_) => res.ok += 1, | ||||
|                 Err(_) => res.failed += 1, | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     Ok(HttpResponse::Ok().json(res)) | ||||
| } | ||||
|  | ||||
| /// Reset the VMs of a group | ||||
| pub async fn vm_reset(client: LibVirtReq, vms: GroupVmIdExtractor) -> HttpResult { | ||||
|     let mut res = TreatmentResult::default(); | ||||
|     for vm in vms.0 { | ||||
|         if let Some(uuid) = vm.uuid { | ||||
|             match client.reset_domain(uuid).await { | ||||
|                 Ok(_) => res.ok += 1, | ||||
|                 Err(_) => res.failed += 1, | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     Ok(HttpResponse::Ok().json(res)) | ||||
| } | ||||
|  | ||||
| /// Get the screenshot of the VMs of a group | ||||
| pub async fn vm_screenshot(client: LibVirtReq, vms: GroupVmIdExtractor) -> HttpResult { | ||||
|     if vms.0.is_empty() { | ||||
|         return Ok(HttpResponse::NoContent().finish()); | ||||
|     } | ||||
|  | ||||
|     let image = if vms.0.len() == 1 { | ||||
|         client.screenshot_domain(vms.0[0].uuid.unwrap()).await? | ||||
|     } else { | ||||
|         return Ok( | ||||
|             HttpResponse::UnprocessableEntity().json("Cannot return multiple VM screenshots!!") | ||||
|         ); | ||||
|     }; | ||||
|  | ||||
|     Ok(HttpResponse::Ok().content_type("image/png").body(image)) | ||||
| } | ||||
|  | ||||
| /// Get the state of the VMs | ||||
| pub async fn vm_state(client: LibVirtReq, vms: GroupVmIdExtractor) -> HttpResult { | ||||
|     let mut states = HashMap::new(); | ||||
|  | ||||
|     for vm in vms.0 { | ||||
|         if let Some(uuid) = vm.uuid { | ||||
|             states.insert(uuid, client.get_domain_state(uuid).await?); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     Ok(HttpResponse::Ok().json(states)) | ||||
| } | ||||
| @@ -1,13 +1,17 @@ | ||||
| use crate::libvirt_client::LibVirtClient; | ||||
| use actix_http::StatusCode; | ||||
| use actix_web::body::BoxBody; | ||||
| use actix_web::{web, HttpResponse}; | ||||
| use std::error::Error; | ||||
| use std::fmt::{Display, Formatter}; | ||||
| use std::io::ErrorKind; | ||||
|  | ||||
| pub mod api_tokens_controller; | ||||
| pub mod auth_controller; | ||||
| pub mod groups_controller; | ||||
| pub mod iso_controller; | ||||
| pub mod network_controller; | ||||
| pub mod nwfilter_controller; | ||||
| pub mod server_controller; | ||||
| pub mod static_controller; | ||||
| pub mod vm_controller; | ||||
| @@ -31,8 +35,15 @@ impl Display for HttpErr { | ||||
| } | ||||
|  | ||||
| impl actix_web::error::ResponseError for HttpErr { | ||||
|     fn status_code(&self) -> StatusCode { | ||||
|         match self { | ||||
|             HttpErr::Err(_) => StatusCode::INTERNAL_SERVER_ERROR, | ||||
|             HttpErr::HTTPResponse(r) => r.status(), | ||||
|         } | ||||
|     } | ||||
|     fn error_response(&self) -> HttpResponse<BoxBody> { | ||||
|         log::error!("Error while processing request! {}", self); | ||||
|  | ||||
|         HttpResponse::InternalServerError().body("Failed to execute request!") | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| use crate::controllers::{HttpResult, LibVirtReq}; | ||||
| use crate::libvirt_lib_structures::XMLUuid; | ||||
| use crate::libvirt_rest_structures::NetworkInfo; | ||||
| use crate::libvirt_rest_structures::net::NetworkInfo; | ||||
| use actix_web::{web, HttpResponse}; | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| @@ -10,7 +10,7 @@ pub struct NetworkID { | ||||
|  | ||||
| /// Create a new network | ||||
| pub async fn create(client: LibVirtReq, req: web::Json<NetworkInfo>) -> HttpResult { | ||||
|     let network = match req.0.to_virt_network() { | ||||
|     let network = match req.0.as_virt_network() { | ||||
|         Ok(d) => d, | ||||
|         Err(e) => { | ||||
|             log::error!("Failed to extract network info! {e}"); | ||||
| @@ -20,7 +20,7 @@ pub async fn create(client: LibVirtReq, req: web::Json<NetworkInfo>) -> HttpResu | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     let uid = match client.update_network(network).await { | ||||
|     let uid = match client.update_network(req.0, network).await { | ||||
|         Ok(u) => u, | ||||
|         Err(e) => { | ||||
|             log::error!("Failed to update network! {e}"); | ||||
| @@ -71,7 +71,7 @@ pub async fn update( | ||||
|     path: web::Path<NetworkID>, | ||||
|     body: web::Json<NetworkInfo>, | ||||
| ) -> HttpResult { | ||||
|     let mut network = match body.0.to_virt_network() { | ||||
|     let mut network = match body.0.as_virt_network() { | ||||
|         Ok(n) => n, | ||||
|         Err(e) => { | ||||
|             log::error!("Failed to extract network info! {e}"); | ||||
| @@ -82,7 +82,7 @@ pub async fn update( | ||||
|     }; | ||||
|     network.uuid = Some(path.uid); | ||||
|  | ||||
|     if let Err(e) = client.update_network(network).await { | ||||
|     if let Err(e) = client.update_network(body.0, network).await { | ||||
|         log::error!("Failed to update network! {e}"); | ||||
|         return Ok( | ||||
|             HttpResponse::InternalServerError().json(format!("Failed to update network!\n${e}")) | ||||
|   | ||||
							
								
								
									
										113
									
								
								virtweb_backend/src/controllers/nwfilter_controller.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								virtweb_backend/src/controllers/nwfilter_controller.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,113 @@ | ||||
| use crate::constants; | ||||
| use crate::controllers::{HttpResult, LibVirtReq}; | ||||
| use crate::libvirt_lib_structures::XMLUuid; | ||||
| use crate::libvirt_rest_structures::nw_filter::NetworkFilter; | ||||
| use actix_web::{web, HttpResponse}; | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| pub struct NetworkFilterID { | ||||
|     uid: XMLUuid, | ||||
| } | ||||
|  | ||||
| /// Create a new nw filter | ||||
| pub async fn create(client: LibVirtReq, req: web::Json<NetworkFilter>) -> HttpResult { | ||||
|     let network = match req.0.rest2lib() { | ||||
|         Ok(d) => d, | ||||
|         Err(e) => { | ||||
|             log::error!("Failed to extract network filter info! {e}"); | ||||
|             return Ok(HttpResponse::BadRequest() | ||||
|                 .json(format!("Failed to extract network filter info! {e}"))); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     if constants::BUILTIN_NETWORK_FILTER_RULES.contains(&network.name.as_str()) { | ||||
|         return Ok(HttpResponse::ExpectationFailed() | ||||
|             .json("Builtin network filter rules shall not be modified!")); | ||||
|     } | ||||
|  | ||||
|     let uid = match client.update_network_filter(req.0, network).await { | ||||
|         Ok(u) => u, | ||||
|         Err(e) => { | ||||
|             log::error!("Failed to update network filter! {e}"); | ||||
|             return Ok(HttpResponse::InternalServerError() | ||||
|                 .json(format!("Failed to update network filter! {e}"))); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     Ok(HttpResponse::Ok().json(NetworkFilterID { uid })) | ||||
| } | ||||
|  | ||||
| /// Get the list of network filters | ||||
| pub async fn list(client: LibVirtReq) -> HttpResult { | ||||
|     let networks = match client.get_full_network_filters_list().await { | ||||
|         Err(e) => { | ||||
|             log::error!("Failed to get the list of network filters! {e}"); | ||||
|             return Ok(HttpResponse::InternalServerError() | ||||
|                 .json(format!("Failed to get the list of networks! {e}"))); | ||||
|         } | ||||
|         Ok(l) => l, | ||||
|     }; | ||||
|  | ||||
|     let networks = networks | ||||
|         .into_iter() | ||||
|         .map(|n| NetworkFilter::lib2rest(n).unwrap()) | ||||
|         .collect::<Vec<_>>(); | ||||
|  | ||||
|     Ok(HttpResponse::Ok().json(networks)) | ||||
| } | ||||
|  | ||||
| /// Get the information about a single network filter | ||||
| pub async fn get_single(client: LibVirtReq, req: web::Path<NetworkFilterID>) -> HttpResult { | ||||
|     let nwfilter = NetworkFilter::lib2rest(client.get_single_network_filter(req.uid).await?)?; | ||||
|     Ok(HttpResponse::Ok().json(nwfilter)) | ||||
| } | ||||
|  | ||||
| /// Get the XML source description of a single network filter | ||||
| pub async fn single_src(client: LibVirtReq, req: web::Path<NetworkFilterID>) -> HttpResult { | ||||
|     let xml = client.get_single_network_filter_xml(req.uid).await?; | ||||
|     Ok(HttpResponse::Ok().content_type("application/xml").body(xml)) | ||||
| } | ||||
|  | ||||
| /// Update the information about a single network filter | ||||
| pub async fn update( | ||||
|     client: LibVirtReq, | ||||
|     path: web::Path<NetworkFilterID>, | ||||
|     body: web::Json<NetworkFilter>, | ||||
| ) -> HttpResult { | ||||
|     let mut network = match body.0.rest2lib() { | ||||
|         Ok(n) => n, | ||||
|         Err(e) => { | ||||
|             log::error!("Failed to extract network filter info! {e}"); | ||||
|             return Ok(HttpResponse::BadRequest() | ||||
|                 .json(format!("Failed to extract network filter info!\n${e}"))); | ||||
|         } | ||||
|     }; | ||||
|     network.uuid = Some(path.uid); | ||||
|  | ||||
|     if constants::BUILTIN_NETWORK_FILTER_RULES.contains(&network.name.as_str()) { | ||||
|         return Ok(HttpResponse::ExpectationFailed() | ||||
|             .json("Builtin network filter rules shall not be modified!")); | ||||
|     } | ||||
|  | ||||
|     if let Err(e) = client.update_network_filter(body.0, network).await { | ||||
|         log::error!("Failed to update network filter! {e}"); | ||||
|         return Ok(HttpResponse::InternalServerError() | ||||
|             .json(format!("Failed to update network filter!\n${e}"))); | ||||
|     } | ||||
|  | ||||
|     Ok(HttpResponse::Ok().json("Network filter updated")) | ||||
| } | ||||
|  | ||||
| /// Delete a network filter | ||||
| pub async fn delete(client: LibVirtReq, path: web::Path<NetworkFilterID>) -> HttpResult { | ||||
|     // Prevent deletion of default rules | ||||
|     let network = client.get_single_network_filter(path.uid).await?; | ||||
|     if constants::BUILTIN_NETWORK_FILTER_RULES.contains(&network.name.as_str()) { | ||||
|         return Ok(HttpResponse::ExpectationFailed() | ||||
|             .json("Builtin network filter rules shall not be deleted!")); | ||||
|     } | ||||
|  | ||||
|     client.delete_network_filter(path.uid).await?; | ||||
|  | ||||
|     Ok(HttpResponse::Ok().json("Network deleted")) | ||||
| } | ||||
| @@ -4,9 +4,11 @@ use crate::constants; | ||||
| use crate::constants::{DISK_NAME_MAX_LEN, DISK_NAME_MIN_LEN, DISK_SIZE_MAX, DISK_SIZE_MIN}; | ||||
| use crate::controllers::{HttpResult, LibVirtReq}; | ||||
| use crate::extractors::local_auth_extractor::LocalAuthEnabled; | ||||
| use crate::libvirt_rest_structures::HypervisorInfo; | ||||
| use crate::libvirt_rest_structures::hypervisor::HypervisorInfo; | ||||
| use crate::nat::nat_hook; | ||||
| use crate::utils::net_utils; | ||||
| use actix_web::{HttpResponse, Responder}; | ||||
| use sysinfo::{NetworksExt, System, SystemExt}; | ||||
| use sysinfo::{Components, Disks, Networks, System}; | ||||
|  | ||||
| #[derive(serde::Serialize)] | ||||
| struct StaticConfig { | ||||
| @@ -15,6 +17,8 @@ struct StaticConfig { | ||||
|     oidc_auth_enabled: bool, | ||||
|     iso_mimetypes: &'static [&'static str], | ||||
|     net_mac_prefix: &'static str, | ||||
|     builtin_nwfilter_rules: &'static [&'static str], | ||||
|     nwfilter_chains: &'static [&'static str], | ||||
|     constraints: ServerConstraints, | ||||
| } | ||||
|  | ||||
| @@ -24,18 +28,33 @@ struct LenConstraints { | ||||
|     max: usize, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize)] | ||||
| struct SLenConstraints { | ||||
|     min: i64, | ||||
|     max: i64, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize)] | ||||
| struct ServerConstraints { | ||||
|     iso_max_size: usize, | ||||
|     vnc_token_duration: u64, | ||||
|     vm_name_size: LenConstraints, | ||||
|     vm_title_size: LenConstraints, | ||||
|     group_id_size: LenConstraints, | ||||
|     memory_size: LenConstraints, | ||||
|     disk_name_size: LenConstraints, | ||||
|     disk_size: LenConstraints, | ||||
|     net_name_size: LenConstraints, | ||||
|     net_title_size: LenConstraints, | ||||
|     net_nat_comment_size: LenConstraints, | ||||
|     dhcp_reservation_host_name: LenConstraints, | ||||
|     nwfilter_name_size: LenConstraints, | ||||
|     nwfilter_comment_size: LenConstraints, | ||||
|     nwfilter_priority: SLenConstraints, | ||||
|     nwfilter_selectors_count: LenConstraints, | ||||
|     api_token_name_size: LenConstraints, | ||||
|     api_token_description_size: LenConstraints, | ||||
|     api_token_right_path_size: LenConstraints, | ||||
| } | ||||
|  | ||||
| pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder { | ||||
| @@ -45,6 +64,8 @@ pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder { | ||||
|         oidc_auth_enabled: !AppConfig::get().disable_oidc, | ||||
|         iso_mimetypes: &constants::ALLOWED_ISO_MIME_TYPES, | ||||
|         net_mac_prefix: constants::NET_MAC_ADDR_PREFIX, | ||||
|         builtin_nwfilter_rules: &constants::BUILTIN_NETWORK_FILTER_RULES, | ||||
|         nwfilter_chains: &constants::NETWORK_CHAINS, | ||||
|         constraints: ServerConstraints { | ||||
|             iso_max_size: constants::ISO_MAX_SIZE, | ||||
|  | ||||
| @@ -52,6 +73,7 @@ pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder { | ||||
|  | ||||
|             vm_name_size: LenConstraints { min: 2, max: 50 }, | ||||
|             vm_title_size: LenConstraints { min: 0, max: 50 }, | ||||
|             group_id_size: LenConstraints { min: 3, max: 50 }, | ||||
|             memory_size: LenConstraints { | ||||
|                 min: constants::MIN_VM_MEMORY, | ||||
|                 max: constants::MAX_VM_MEMORY, | ||||
| @@ -67,8 +89,35 @@ pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder { | ||||
|  | ||||
|             net_name_size: LenConstraints { min: 2, max: 50 }, | ||||
|             net_title_size: LenConstraints { min: 0, max: 50 }, | ||||
|             net_nat_comment_size: LenConstraints { | ||||
|                 min: 0, | ||||
|                 max: constants::NET_NAT_COMMENT_MAX_SIZE, | ||||
|             }, | ||||
|  | ||||
|             dhcp_reservation_host_name: LenConstraints { min: 2, max: 250 }, | ||||
|  | ||||
|             nwfilter_name_size: LenConstraints { min: 2, max: 250 }, | ||||
|             nwfilter_comment_size: LenConstraints { min: 0, max: 256 }, | ||||
|             nwfilter_priority: SLenConstraints { | ||||
|                 min: -1000, | ||||
|                 max: 1000, | ||||
|             }, | ||||
|             nwfilter_selectors_count: LenConstraints { min: 0, max: 1 }, | ||||
|  | ||||
|             api_token_name_size: LenConstraints { | ||||
|                 min: constants::API_TOKEN_NAME_MIN_LENGTH, | ||||
|                 max: constants::API_TOKEN_NAME_MAX_LENGTH, | ||||
|             }, | ||||
|  | ||||
|             api_token_description_size: LenConstraints { | ||||
|                 min: constants::API_TOKEN_DESCRIPTION_MIN_LENGTH, | ||||
|                 max: constants::API_TOKEN_DESCRIPTION_MAX_LENGTH, | ||||
|             }, | ||||
|  | ||||
|             api_token_right_path_size: LenConstraints { | ||||
|                 min: 0, | ||||
|                 max: constants::API_TOKEN_RIGHT_PATH_MAX_LENGTH, | ||||
|             }, | ||||
|         }, | ||||
|     }) | ||||
| } | ||||
| @@ -77,24 +126,54 @@ pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder { | ||||
| struct ServerInfo { | ||||
|     hypervisor: HypervisorInfo, | ||||
|     system: System, | ||||
|     components: Components, | ||||
|     disks: Disks, | ||||
|     networks: Networks, | ||||
| } | ||||
|  | ||||
| pub async fn server_info(client: LibVirtReq) -> HttpResult { | ||||
|     let mut system = System::new(); | ||||
|     system.refresh_disks_list(); | ||||
|     system.refresh_components_list(); | ||||
|     system.refresh_networks_list(); | ||||
|     system.refresh_all(); | ||||
|  | ||||
|     let mut components = Components::new(); | ||||
|     components.refresh_list(); | ||||
|     components.refresh(); | ||||
|  | ||||
|     let mut disks = Disks::new(); | ||||
|     disks.refresh_list(); | ||||
|     disks.refresh(); | ||||
|  | ||||
|     let mut networks = Networks::new(); | ||||
|     networks.refresh_list(); | ||||
|     networks.refresh(); | ||||
|  | ||||
|     Ok(HttpResponse::Ok().json(ServerInfo { | ||||
|         hypervisor: client.get_info().await?, | ||||
|         system, | ||||
|         components, | ||||
|         disks, | ||||
|         networks, | ||||
|     })) | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize)] | ||||
| struct NetworkHookStatus { | ||||
|     installed: bool, | ||||
|     content: String, | ||||
|     path: &'static str, | ||||
| } | ||||
|  | ||||
| pub async fn network_hook_status() -> HttpResult { | ||||
|     Ok(HttpResponse::Ok().json(NetworkHookStatus { | ||||
|         installed: nat_hook::is_installed()?, | ||||
|         content: nat_hook::hook_content()?, | ||||
|         path: constants::NAT_HOOK_PATH, | ||||
|     })) | ||||
| } | ||||
|  | ||||
| pub async fn number_vcpus() -> HttpResult { | ||||
|     let mut system = System::new(); | ||||
|     system.refresh_cpu(); | ||||
|     system.refresh_cpu_all(); | ||||
|     let number_cpus = system.cpus().len(); | ||||
|     assert_ne!(number_cpus, 0, "Got invlid number of CPU!"); | ||||
|  | ||||
| @@ -110,14 +189,5 @@ pub async fn number_vcpus() -> HttpResult { | ||||
| } | ||||
|  | ||||
| pub async fn networks_list() -> HttpResult { | ||||
|     let mut system = System::new(); | ||||
|     system.refresh_networks_list(); | ||||
|  | ||||
|     Ok(HttpResponse::Ok().json( | ||||
|         system | ||||
|             .networks() | ||||
|             .iter() | ||||
|             .map(|n| n.0.to_string()) | ||||
|             .collect::<Vec<_>>(), | ||||
|     )) | ||||
|     Ok(HttpResponse::Ok().json(net_utils::net_list())) | ||||
| } | ||||
|   | ||||
| @@ -1,8 +1,9 @@ | ||||
| use crate::actors::vnc_actor::VNCActor; | ||||
| use crate::actors::vnc_tokens_actor::VNCTokensManager; | ||||
| use crate::controllers::{HttpResult, LibVirtReq}; | ||||
| use crate::libvirt_lib_structures::{DomainState, XMLUuid}; | ||||
| use crate::libvirt_rest_structures::VMInfo; | ||||
| use crate::libvirt_lib_structures::domain::DomainState; | ||||
| use crate::libvirt_lib_structures::XMLUuid; | ||||
| use crate::libvirt_rest_structures::vm::VMInfo; | ||||
| use actix_web::{web, HttpRequest, HttpResponse}; | ||||
| use actix_web_actors::ws; | ||||
|  | ||||
| @@ -20,7 +21,7 @@ struct VMUuid { | ||||
|  | ||||
| /// Create a new VM | ||||
| pub async fn create(client: LibVirtReq, req: web::Json<VMInfo>) -> HttpResult { | ||||
|     let domain = match req.0.to_domain() { | ||||
|     let domain = match req.0.as_domain() { | ||||
|         Ok(d) => d, | ||||
|         Err(e) => { | ||||
|             log::error!("Failed to extract domain info! {e}"); | ||||
| @@ -29,7 +30,7 @@ pub async fn create(client: LibVirtReq, req: web::Json<VMInfo>) -> HttpResult { | ||||
|             ); | ||||
|         } | ||||
|     }; | ||||
|     let id = match client.update_domain(domain).await { | ||||
|     let id = match client.update_domain(req.0, domain).await { | ||||
|         Ok(i) => i, | ||||
|         Err(e) => { | ||||
|             log::error!("Failed to update domain info! {e}"); | ||||
| @@ -82,6 +83,8 @@ pub async fn get_single(client: LibVirtReq, id: web::Path<SingleVMUUidReq>) -> H | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     log::debug!("INFO={info:#?}"); | ||||
|  | ||||
|     let state = client.get_domain_state(id.uid).await?; | ||||
|  | ||||
|     Ok(HttpResponse::Ok().json(VMInfoAndState { | ||||
| @@ -111,13 +114,18 @@ pub async fn update( | ||||
|     id: web::Path<SingleVMUUidReq>, | ||||
|     req: web::Json<VMInfo>, | ||||
| ) -> HttpResult { | ||||
|     let mut domain = req.0.to_domain().map_err(|e| { | ||||
|         log::error!("Failed to extract domain info! {e}"); | ||||
|         HttpResponse::BadRequest().json(format!("Failed to extract domain info! {e}")) | ||||
|     })?; | ||||
|     let mut domain = match req.0.as_domain() { | ||||
|         Ok(d) => d, | ||||
|         Err(e) => { | ||||
|             log::error!("Failed to extract domain info! {e}"); | ||||
|             return Ok( | ||||
|                 HttpResponse::BadRequest().json(format!("Failed to extract domain info! {e}")) | ||||
|             ); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     domain.uuid = Some(id.uid); | ||||
|     if let Err(e) = client.update_domain(domain).await { | ||||
|     if let Err(e) = client.update_domain(req.0, domain).await { | ||||
|         log::error!("Failed to update domain info! {e}"); | ||||
|         return Ok(HttpResponse::BadRequest().json(format!("Failed to update domain info!\n{e}"))); | ||||
|     } | ||||
|   | ||||
							
								
								
									
										151
									
								
								virtweb_backend/src/extractors/api_auth_extractor.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								virtweb_backend/src/extractors/api_auth_extractor.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,151 @@ | ||||
| use crate::api_tokens::{Token, TokenID, TokenVerb}; | ||||
|  | ||||
| use crate::api_tokens; | ||||
| use crate::utils::time_utils::time; | ||||
| use actix_remote_ip::RemoteIP; | ||||
| use actix_web::dev::Payload; | ||||
| use actix_web::error::{ErrorBadRequest, ErrorUnauthorized}; | ||||
| use actix_web::{Error, FromRequest, HttpRequest}; | ||||
| use std::future::Future; | ||||
| use std::pin::Pin; | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug)] | ||||
| pub struct TokenClaims { | ||||
|     pub sub: String, | ||||
|     pub iat: usize, | ||||
|     pub exp: usize, | ||||
|     pub verb: TokenVerb, | ||||
|     pub path: String, | ||||
|     pub nonce: String, | ||||
| } | ||||
|  | ||||
| pub struct ApiAuthExtractor { | ||||
|     pub token: Token, | ||||
|     pub claims: TokenClaims, | ||||
| } | ||||
|  | ||||
| impl FromRequest for ApiAuthExtractor { | ||||
|     type Error = Error; | ||||
|     type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>; | ||||
|  | ||||
|     fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { | ||||
|         let req = req.clone(); | ||||
|  | ||||
|         let remote_ip = match RemoteIP::from_request(&req, payload).into_inner() { | ||||
|             Ok(ip) => ip, | ||||
|             Err(e) => return Box::pin(async { Err(e) }), | ||||
|         }; | ||||
|  | ||||
|         Box::pin(async move { | ||||
|             let (token_id, token_jwt) = match ( | ||||
|                 req.headers().get("x-token-id"), | ||||
|                 req.headers().get("x-token-content"), | ||||
|             ) { | ||||
|                 (Some(id), Some(jwt)) => ( | ||||
|                     id.to_str().unwrap_or("").to_string(), | ||||
|                     jwt.to_str().unwrap_or("").to_string(), | ||||
|                 ), | ||||
|                 (_, _) => { | ||||
|                     return Err(ErrorBadRequest("API auth headers were not all specified!")); | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             let token_id = match TokenID::parse(&token_id) { | ||||
|                 Ok(t) => t, | ||||
|                 Err(e) => { | ||||
|                     log::error!("Failed to parse token id! {e}"); | ||||
|                     return Err(ErrorBadRequest("Unable to validate token ID!")); | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             let token = match api_tokens::get_single(token_id).await { | ||||
|                 Ok(t) => t, | ||||
|                 Err(e) => { | ||||
|                     log::error!("Failed to retrieve token: {e}"); | ||||
|                     return Err(ErrorBadRequest("Unable to validate token!")); | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             if token.is_expired() { | ||||
|                 log::error!("Token has expired (not been used for too long)!"); | ||||
|                 return Err(ErrorBadRequest("Unable to validate token!")); | ||||
|             } | ||||
|  | ||||
|             let claims = match token | ||||
|                 .pub_key | ||||
|                 .as_ref() | ||||
|                 .expect("All tokens shall have public key!") | ||||
|                 .validate_jwt::<TokenClaims>(&token_jwt) | ||||
|             { | ||||
|                 Ok(c) => c, | ||||
|                 Err(e) => { | ||||
|                     log::error!("Failed to validate JWT: {e}"); | ||||
|                     return Err(ErrorBadRequest("Unable to validate token!")); | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             if claims.sub != token.id.0.to_string() { | ||||
|                 log::error!("JWT sub mismatch (should equal to token id)!"); | ||||
|                 return Err(ErrorBadRequest( | ||||
|                     "JWT sub mismatch (should equal to token id)!", | ||||
|                 )); | ||||
|             } | ||||
|  | ||||
|             if time() + 60 * 15 < claims.iat as u64 { | ||||
|                 log::error!("iat is in the future!"); | ||||
|                 return Err(ErrorBadRequest("iat is in the future!")); | ||||
|             } | ||||
|  | ||||
|             if claims.exp < claims.iat { | ||||
|                 log::error!("exp shall not be smaller than iat!"); | ||||
|                 return Err(ErrorBadRequest("exp shall not be smaller than iat!")); | ||||
|             } | ||||
|  | ||||
|             if claims.exp - claims.iat > 1800 { | ||||
|                 log::error!("JWT shall not be valid more than 30 minutes!"); | ||||
|                 return Err(ErrorBadRequest( | ||||
|                     "JWT shall not be valid more than 30 minutes!", | ||||
|                 )); | ||||
|             } | ||||
|  | ||||
|             if claims.path != req.path() { | ||||
|                 log::error!("JWT path mismatch!"); | ||||
|                 return Err(ErrorBadRequest("JWT path mismatch!")); | ||||
|             } | ||||
|  | ||||
|             if claims.verb.as_method() != req.method() { | ||||
|                 log::error!("JWT method mismatch!"); | ||||
|                 return Err(ErrorBadRequest("JWT method mismatch!")); | ||||
|             } | ||||
|  | ||||
|             if !token.rights.contains(claims.verb, req.path()) { | ||||
|                 log::error!( | ||||
|                     "Attempt to use a token for an unauthorized route! (token_id={})", | ||||
|                     token.id.0 | ||||
|                 ); | ||||
|                 return Err(ErrorUnauthorized( | ||||
|                     "Token cannot be used to query this route!", | ||||
|                 )); | ||||
|             } | ||||
|  | ||||
|             if let Some(ip) = token.ip_restriction { | ||||
|                 if !ip.contains(remote_ip.0) { | ||||
|                     log::error!( | ||||
|                         "Attempt to use a token for an unauthorized IP! {remote_ip:?} token_id={}", | ||||
|                         token.id.0 | ||||
|                     ); | ||||
|                     return Err(ErrorUnauthorized("Token cannot be used from this IP!")); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             if token.should_update_last_activity() { | ||||
|                 if let Err(e) = api_tokens::refresh_last_used(token.id).await { | ||||
|                     log::error!("Could not update token last activity! {e}"); | ||||
|                     return Err(ErrorBadRequest("Couldn't refresh token last activity!")); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             Ok(ApiAuthExtractor { token, claims }) | ||||
|         }) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										66
									
								
								virtweb_backend/src/extractors/group_vm_id_extractor.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								virtweb_backend/src/extractors/group_vm_id_extractor.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | ||||
| use crate::controllers::LibVirtReq; | ||||
| use crate::libvirt_lib_structures::domain::DomainXML; | ||||
| use crate::libvirt_lib_structures::XMLUuid; | ||||
| use crate::libvirt_rest_structures::vm::VMGroupId; | ||||
| use actix_http::Payload; | ||||
| use actix_web::error::ErrorBadRequest; | ||||
| use actix_web::web::Query; | ||||
| use actix_web::{web, Error, FromRequest, HttpRequest}; | ||||
| use std::future::Future; | ||||
| use std::pin::Pin; | ||||
|  | ||||
| pub struct GroupVmIdExtractor(pub Vec<DomainXML>); | ||||
|  | ||||
| #[derive(serde::Deserialize)] | ||||
| struct GroupIDInPath { | ||||
|     gid: VMGroupId, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Deserialize)] | ||||
| struct FilterVM { | ||||
|     vm_id: Option<XMLUuid>, | ||||
| } | ||||
|  | ||||
| impl FromRequest for GroupVmIdExtractor { | ||||
|     type Error = Error; | ||||
|     type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>; | ||||
|  | ||||
|     fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future { | ||||
|         let req = req.clone(); | ||||
|  | ||||
|         Box::pin(async move { | ||||
|             let Ok(group_id) = | ||||
|                 web::Path::<GroupIDInPath>::from_request(&req, &mut Payload::None).await | ||||
|             else { | ||||
|                 return Err(ErrorBadRequest("Group ID not specified in path!")); | ||||
|             }; | ||||
|             let group_id = group_id.into_inner().gid; | ||||
|  | ||||
|             let filter_vm = match Query::<FilterVM>::from_request(&req, &mut Payload::None).await { | ||||
|                 Ok(v) => v, | ||||
|                 Err(e) => { | ||||
|                     log::error!("Failed to extract VM id from request! {e}"); | ||||
|                     return Err(ErrorBadRequest("Failed to extract VM id from request!")); | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             let Ok(client) = LibVirtReq::from_request(&req, &mut Payload::None).await else { | ||||
|                 return Err(ErrorBadRequest("Failed to extract client handle!")); | ||||
|             }; | ||||
|  | ||||
|             let vms = match client.get_full_group_vm_list(&group_id).await { | ||||
|                 Ok(vms) => vms, | ||||
|                 Err(e) => { | ||||
|                     log::error!("Failed to get the VMs of the group {group_id:?}: {e}"); | ||||
|                     return Err(ErrorBadRequest("Failed to get the VMs of the group!")); | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             // Filter (if requested by the user) | ||||
|             Ok(GroupVmIdExtractor(match filter_vm.vm_id { | ||||
|                 None => vms, | ||||
|                 Some(id) => vms.into_iter().filter(|vms| vms.uuid == Some(id)).collect(), | ||||
|             })) | ||||
|         }) | ||||
|     } | ||||
| } | ||||
| @@ -1,2 +1,4 @@ | ||||
| pub mod api_auth_extractor; | ||||
| pub mod auth_extractor; | ||||
| pub mod group_vm_id_extractor; | ||||
| pub mod local_auth_extractor; | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| pub mod actors; | ||||
| pub mod api_tokens; | ||||
| pub mod app_config; | ||||
| pub mod constants; | ||||
| pub mod controllers; | ||||
| @@ -7,4 +8,5 @@ pub mod libvirt_client; | ||||
| pub mod libvirt_lib_structures; | ||||
| pub mod libvirt_rest_structures; | ||||
| pub mod middlewares; | ||||
| pub mod nat; | ||||
| pub mod utils; | ||||
|   | ||||
| @@ -1,8 +1,15 @@ | ||||
| use crate::actors::libvirt_actor; | ||||
| use crate::actors::libvirt_actor::LibVirtActor; | ||||
| use crate::libvirt_lib_structures::{DomainState, DomainXML, NetworkXML, XMLUuid}; | ||||
| use crate::libvirt_rest_structures::HypervisorInfo; | ||||
| use crate::libvirt_lib_structures::domain::{DomainState, DomainXML}; | ||||
| use crate::libvirt_lib_structures::network::NetworkXML; | ||||
| use crate::libvirt_lib_structures::nwfilter::NetworkFilterXML; | ||||
| use crate::libvirt_lib_structures::XMLUuid; | ||||
| use crate::libvirt_rest_structures::hypervisor::HypervisorInfo; | ||||
| use crate::libvirt_rest_structures::net::NetworkInfo; | ||||
| use crate::libvirt_rest_structures::nw_filter::NetworkFilter; | ||||
| use crate::libvirt_rest_structures::vm::{VMGroupId, VMInfo}; | ||||
| use actix::Addr; | ||||
| use std::collections::HashSet; | ||||
|  | ||||
| #[derive(Clone)] | ||||
| pub struct LibVirtClient(pub Addr<LibVirtActor>); | ||||
| @@ -36,8 +43,10 @@ impl LibVirtClient { | ||||
|     } | ||||
|  | ||||
|     /// Update a domain | ||||
|     pub async fn update_domain(&self, xml: DomainXML) -> anyhow::Result<XMLUuid> { | ||||
|         self.0.send(libvirt_actor::DefineDomainReq(xml)).await? | ||||
|     pub async fn update_domain(&self, vm_def: VMInfo, xml: DomainXML) -> anyhow::Result<XMLUuid> { | ||||
|         self.0 | ||||
|             .send(libvirt_actor::DefineDomainReq(vm_def, xml)) | ||||
|             .await? | ||||
|     } | ||||
|  | ||||
|     /// Delete a domain | ||||
| @@ -99,9 +108,44 @@ impl LibVirtClient { | ||||
|             .await? | ||||
|     } | ||||
|  | ||||
|     /// Get the full list of groups | ||||
|     pub async fn get_full_groups_list(&self) -> anyhow::Result<Vec<VMGroupId>> { | ||||
|         let domains = self.get_full_domains_list().await?; | ||||
|         let mut out = HashSet::new(); | ||||
|         for d in domains { | ||||
|             if let Some(g) = VMInfo::from_domain(d)?.group { | ||||
|                 out.insert(g); | ||||
|             } | ||||
|         } | ||||
|         let mut out: Vec<_> = out.into_iter().collect(); | ||||
|         out.sort(); | ||||
|         Ok(out) | ||||
|     } | ||||
|  | ||||
|     /// Get the full list of VMs of a given group | ||||
|     pub async fn get_full_group_vm_list( | ||||
|         &self, | ||||
|         group: &VMGroupId, | ||||
|     ) -> anyhow::Result<Vec<DomainXML>> { | ||||
|         let vms = self.get_full_domains_list().await?; | ||||
|         let mut out = Vec::new(); | ||||
|         for vm in vms { | ||||
|             if VMInfo::from_domain(vm.clone())?.group == Some(group.clone()) { | ||||
|                 out.push(vm); | ||||
|             } | ||||
|         } | ||||
|         Ok(out) | ||||
|     } | ||||
|  | ||||
|     /// Update a network configuration | ||||
|     pub async fn update_network(&self, network: NetworkXML) -> anyhow::Result<XMLUuid> { | ||||
|         self.0.send(libvirt_actor::DefineNetwork(network)).await? | ||||
|     pub async fn update_network( | ||||
|         &self, | ||||
|         net_def: NetworkInfo, | ||||
|         network: NetworkXML, | ||||
|     ) -> anyhow::Result<XMLUuid> { | ||||
|         self.0 | ||||
|             .send(libvirt_actor::DefineNetwork(net_def, network)) | ||||
|             .await? | ||||
|     } | ||||
|  | ||||
|     /// Get the full list of networks | ||||
| @@ -157,4 +201,42 @@ impl LibVirtClient { | ||||
|     pub async fn stop_network(&self, id: XMLUuid) -> anyhow::Result<()> { | ||||
|         self.0.send(libvirt_actor::StopNetwork(id)).await? | ||||
|     } | ||||
|  | ||||
|     /// Get the full list of network filters | ||||
|     pub async fn get_full_network_filters_list(&self) -> anyhow::Result<Vec<NetworkFilterXML>> { | ||||
|         let ids = self.0.send(libvirt_actor::GetNWFiltersListReq).await??; | ||||
|         let mut info = Vec::with_capacity(ids.len()); | ||||
|         for id in ids { | ||||
|             info.push(self.get_single_network_filter(id).await?) | ||||
|         } | ||||
|         Ok(info) | ||||
|     } | ||||
|  | ||||
|     /// Get the information about a single domain | ||||
|     pub async fn get_single_network_filter(&self, id: XMLUuid) -> anyhow::Result<NetworkFilterXML> { | ||||
|         self.0.send(libvirt_actor::GetNWFilterXMLReq(id)).await? | ||||
|     } | ||||
|  | ||||
|     /// Get the source XML configuration of a single network filter | ||||
|     pub async fn get_single_network_filter_xml(&self, id: XMLUuid) -> anyhow::Result<String> { | ||||
|         self.0 | ||||
|             .send(libvirt_actor::GetSourceNetworkFilterXMLReq(id)) | ||||
|             .await? | ||||
|     } | ||||
|  | ||||
|     /// Update the information about a single domain | ||||
|     pub async fn update_network_filter( | ||||
|         &self, | ||||
|         nwf_def: NetworkFilter, | ||||
|         xml: NetworkFilterXML, | ||||
|     ) -> anyhow::Result<XMLUuid> { | ||||
|         self.0 | ||||
|             .send(libvirt_actor::DefineNWFilterReq(nwf_def, xml)) | ||||
|             .await? | ||||
|     } | ||||
|  | ||||
|     /// Delete a network filter | ||||
|     pub async fn delete_network_filter(&self, id: XMLUuid) -> anyhow::Result<()> { | ||||
|         self.0.send(libvirt_actor::DeleteNetworkFilter(id)).await? | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,540 +0,0 @@ | ||||
| use std::net::{IpAddr, Ipv4Addr}; | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug)] | ||||
| pub struct XMLUuid(pub uuid::Uuid); | ||||
|  | ||||
| impl XMLUuid { | ||||
|     pub fn parse_from_str(s: &str) -> anyhow::Result<Self> { | ||||
|         Ok(Self(uuid::Uuid::parse_str(s)?)) | ||||
|     } | ||||
|  | ||||
|     pub fn new_random() -> Self { | ||||
|         Self(uuid::Uuid::new_v4()) | ||||
|     } | ||||
|     pub fn as_string(&self) -> String { | ||||
|         self.0.to_string() | ||||
|     } | ||||
|  | ||||
|     pub fn is_valid(&self) -> bool { | ||||
|         log::debug!("UUID version ({}): {}", self.0, self.0.get_version_num()); | ||||
|         self.0.get_version_num() == 4 | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// OS information | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "os")] | ||||
| pub struct OSXML { | ||||
|     #[serde(rename(serialize = "@firmware"), default)] | ||||
|     pub firmware: String, | ||||
|     pub r#type: OSTypeXML, | ||||
|     pub loader: Option<OSLoaderXML>, | ||||
| } | ||||
|  | ||||
| /// OS Type information | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "os")] | ||||
| pub struct OSTypeXML { | ||||
|     #[serde(rename(serialize = "@arch"))] | ||||
|     pub arch: String, | ||||
|     #[serde(rename(serialize = "@machine"))] | ||||
|     pub machine: String, | ||||
|     #[serde(rename = "$value")] | ||||
|     pub body: String, | ||||
| } | ||||
|  | ||||
| /// OS Loader information | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "loader")] | ||||
| pub struct OSLoaderXML { | ||||
|     #[serde(rename(serialize = "@secure"))] | ||||
|     pub secure: String, | ||||
| } | ||||
|  | ||||
| /// Hypervisor features | ||||
| #[derive(serde::Serialize, serde::Deserialize, Default)] | ||||
| #[serde(rename = "features")] | ||||
| pub struct FeaturesXML { | ||||
|     pub acpi: ACPIXML, | ||||
| } | ||||
|  | ||||
| /// ACPI feature | ||||
| #[derive(serde::Serialize, serde::Deserialize, Default)] | ||||
| #[serde(rename = "acpi")] | ||||
| pub struct ACPIXML {} | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "mac")] | ||||
| pub struct NetMacAddress { | ||||
|     #[serde(rename(serialize = "@address"))] | ||||
|     pub address: String, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "source")] | ||||
| pub struct NetIntSourceXML { | ||||
|     #[serde(rename(serialize = "@network"))] | ||||
|     pub network: String, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "interface")] | ||||
| pub struct DomainNetInterfaceXML { | ||||
|     #[serde(rename(serialize = "@type"))] | ||||
|     pub r#type: String, | ||||
|     pub mac: NetMacAddress, | ||||
|     pub source: Option<NetIntSourceXML>, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "input")] | ||||
| pub struct DomainInputXML { | ||||
|     #[serde(rename(serialize = "@type"))] | ||||
|     pub r#type: String, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "backend")] | ||||
| pub struct TPMBackendXML { | ||||
|     #[serde(rename(serialize = "@type"))] | ||||
|     pub r#type: String, | ||||
|  | ||||
|     #[serde(rename(serialize = "@version"))] | ||||
|     pub r#version: String, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "tpm")] | ||||
| pub struct TPMDeviceXML { | ||||
|     #[serde(rename(serialize = "@model"))] | ||||
|     pub model: String, | ||||
|     pub backend: TPMBackendXML, | ||||
| } | ||||
|  | ||||
| /// Devices information | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "devices")] | ||||
| pub struct DevicesXML { | ||||
|     /// Graphics (used for VNC) | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub graphics: Option<GraphicsXML>, | ||||
|  | ||||
|     /// Graphics (used for VNC) | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub video: Option<VideoXML>, | ||||
|  | ||||
|     /// Disks (used for storage) | ||||
|     #[serde(default, rename = "disk", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub disks: Vec<DiskXML>, | ||||
|  | ||||
|     /// Networks cards | ||||
|     #[serde(default, rename = "interface", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub net_interfaces: Vec<DomainNetInterfaceXML>, | ||||
|  | ||||
|     /// Input devices | ||||
|     #[serde(default, rename = "input", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub inputs: Vec<DomainInputXML>, | ||||
|  | ||||
|     /// TPM device | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub tpm: Option<TPMDeviceXML>, | ||||
| } | ||||
|  | ||||
| /// Graphics information | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "graphics")] | ||||
| pub struct GraphicsXML { | ||||
|     #[serde(rename(serialize = "@type"))] | ||||
|     pub r#type: String, | ||||
|     #[serde(rename(serialize = "@socket"))] | ||||
|     pub socket: String, | ||||
| } | ||||
|  | ||||
| /// Video device information | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "video")] | ||||
| pub struct VideoXML { | ||||
|     pub model: VideoModelXML, | ||||
| } | ||||
|  | ||||
| /// Video model device information | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "model")] | ||||
| pub struct VideoModelXML { | ||||
|     #[serde(rename(serialize = "@type"))] | ||||
|     pub r#type: String, | ||||
| } | ||||
|  | ||||
| /// Disk information | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "disk")] | ||||
| pub struct DiskXML { | ||||
|     #[serde(rename(serialize = "@type"))] | ||||
|     pub r#type: String, | ||||
|     #[serde(rename(serialize = "@device"))] | ||||
|     pub r#device: String, | ||||
|  | ||||
|     pub driver: DiskDriverXML, | ||||
|     pub source: DiskSourceXML, | ||||
|     pub target: DiskTargetXML, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub readonly: Option<DiskReadOnlyXML>, | ||||
|     pub boot: DiskBootXML, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub address: Option<DiskAddressXML>, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "driver")] | ||||
| pub struct DiskDriverXML { | ||||
|     #[serde(rename(serialize = "@name"))] | ||||
|     pub name: String, | ||||
|     #[serde(rename(serialize = "@type"))] | ||||
|     pub r#type: String, | ||||
|     #[serde(default, rename(serialize = "@cache"))] | ||||
|     pub r#cache: String, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "source")] | ||||
| pub struct DiskSourceXML { | ||||
|     #[serde(rename(serialize = "@file"))] | ||||
|     pub file: String, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "target")] | ||||
| pub struct DiskTargetXML { | ||||
|     #[serde(rename(serialize = "@dev"))] | ||||
|     pub dev: String, | ||||
|     #[serde(rename(serialize = "@bus"))] | ||||
|     pub bus: String, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "readonly")] | ||||
| pub struct DiskReadOnlyXML {} | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "boot")] | ||||
| pub struct DiskBootXML { | ||||
|     #[serde(rename(serialize = "@order"))] | ||||
|     pub order: String, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "address")] | ||||
| pub struct DiskAddressXML { | ||||
|     #[serde(rename(serialize = "@type"))] | ||||
|     pub r#type: String, | ||||
|     #[serde( | ||||
|         default, | ||||
|         skip_serializing_if = "Option::is_none", | ||||
|         rename(serialize = "@controller") | ||||
|     )] | ||||
|     pub r#controller: Option<String>, | ||||
|     #[serde(rename(serialize = "@bus"))] | ||||
|     pub r#bus: String, | ||||
|     #[serde( | ||||
|         default, | ||||
|         skip_serializing_if = "Option::is_none", | ||||
|         rename(serialize = "@target") | ||||
|     )] | ||||
|     pub r#target: Option<String>, | ||||
|     #[serde( | ||||
|         default, | ||||
|         skip_serializing_if = "Option::is_none", | ||||
|         rename(serialize = "@unit") | ||||
|     )] | ||||
|     pub r#unit: Option<String>, | ||||
| } | ||||
|  | ||||
| /// Domain RAM information | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "memory")] | ||||
| pub struct DomainMemoryXML { | ||||
|     #[serde(rename(serialize = "@unit"))] | ||||
|     pub unit: String, | ||||
|  | ||||
|     #[serde(rename = "$value")] | ||||
|     pub memory: usize, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "topology")] | ||||
| pub struct DomainCPUTopology { | ||||
|     #[serde(rename(serialize = "@sockets"))] | ||||
|     pub sockets: usize, | ||||
|     #[serde(rename(serialize = "@cores"))] | ||||
|     pub cores: usize, | ||||
|     #[serde(rename(serialize = "@threads"))] | ||||
|     pub threads: usize, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "cpu")] | ||||
| pub struct DomainVCPUXML { | ||||
|     #[serde(rename = "$value")] | ||||
|     pub body: usize, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "cpu")] | ||||
| pub struct DomainCPUXML { | ||||
|     #[serde(rename(serialize = "@mode"))] | ||||
|     pub mode: String, | ||||
|     pub topology: Option<DomainCPUTopology>, | ||||
| } | ||||
|  | ||||
| /// Domain information, see https://libvirt.org/formatdomain.html | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "domain")] | ||||
| pub struct DomainXML { | ||||
|     /// Domain type (kvm) | ||||
|     #[serde(rename(serialize = "@type"))] | ||||
|     pub r#type: String, | ||||
|  | ||||
|     pub name: String, | ||||
|     pub uuid: Option<XMLUuid>, | ||||
|     pub genid: Option<uuid::Uuid>, | ||||
|     pub title: Option<String>, | ||||
|     pub description: Option<String>, | ||||
|     pub os: OSXML, | ||||
|     #[serde(default)] | ||||
|     pub features: FeaturesXML, | ||||
|     pub devices: DevicesXML, | ||||
|  | ||||
|     /// The maximum allocation of memory for the guest at boot time | ||||
|     pub memory: DomainMemoryXML, | ||||
|  | ||||
|     /// Number of vCPU | ||||
|     pub vcpu: DomainVCPUXML, | ||||
|  | ||||
|     /// CPU information | ||||
|     pub cpu: DomainCPUXML, | ||||
|  | ||||
|     pub on_poweroff: String, | ||||
|     pub on_reboot: String, | ||||
|     pub on_crash: String, | ||||
| } | ||||
|  | ||||
| impl DomainXML { | ||||
|     /// Turn this domain into its XML definition | ||||
|     pub fn into_xml(mut self) -> anyhow::Result<String> { | ||||
|         // A issue with the disks & network interface definition serialization needs them to be serialized aside | ||||
|         let mut devices_xml = Vec::with_capacity(self.devices.disks.len()); | ||||
|         for disk in self.devices.disks { | ||||
|             let disk_xml = serde_xml_rs::to_string(&disk)?; | ||||
|             let start_offset = disk_xml.find("<disk").unwrap(); | ||||
|             devices_xml.push(disk_xml[start_offset..].to_string()); | ||||
|         } | ||||
|         for network in self.devices.net_interfaces { | ||||
|             let network_xml = serde_xml_rs::to_string(&network)?; | ||||
|             let start_offset = network_xml.find("<interface").unwrap(); | ||||
|             devices_xml.push(network_xml[start_offset..].to_string()); | ||||
|         } | ||||
|         for input in self.devices.inputs { | ||||
|             let input_xml = serde_xml_rs::to_string(&input)?; | ||||
|             let start_offset = input_xml.find("<input").unwrap(); | ||||
|             devices_xml.push(input_xml[start_offset..].to_string()); | ||||
|         } | ||||
|  | ||||
|         self.devices.disks = vec![]; | ||||
|         self.devices.net_interfaces = vec![]; | ||||
|         self.devices.inputs = vec![]; | ||||
|  | ||||
|         let mut xml = serde_xml_rs::to_string(&self)?; | ||||
|         let disks_xml = devices_xml.join("\n"); | ||||
|         xml = xml.replacen("<devices>", &format!("<devices>{disks_xml}"), 1); | ||||
|         Ok(xml) | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Domain state | ||||
| #[derive(serde::Serialize, Debug, Copy, Clone)] | ||||
| pub enum DomainState { | ||||
|     NoState, | ||||
|     Running, | ||||
|     Blocked, | ||||
|     Paused, | ||||
|     Shutdown, | ||||
|     Shutoff, | ||||
|     Crashed, | ||||
|     PowerManagementSuspended, | ||||
|     Other, | ||||
| } | ||||
|  | ||||
| /// Network forward information | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug)] | ||||
| #[serde(rename = "forward")] | ||||
| pub struct NetworkForwardXML { | ||||
|     #[serde(rename(serialize = "@mode"))] | ||||
|     pub mode: String, | ||||
|     #[serde( | ||||
|         default, | ||||
|         rename(serialize = "@dev"), | ||||
|         skip_serializing_if = "String::is_empty" | ||||
|     )] | ||||
|     pub dev: String, | ||||
| } | ||||
|  | ||||
| /// Network bridge information | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug)] | ||||
| #[serde(rename = "bridge")] | ||||
| pub struct NetworkBridgeXML { | ||||
|     #[serde(rename(serialize = "@name"))] | ||||
|     pub name: String, | ||||
| } | ||||
|  | ||||
| /// Network DNS information | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug)] | ||||
| #[serde(rename = "dns")] | ||||
| pub struct NetworkDNSXML { | ||||
|     pub forwarder: NetworkDNSForwarderXML, | ||||
| } | ||||
|  | ||||
| /// Network DNS information | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug)] | ||||
| #[serde(rename = "forwarder")] | ||||
| pub struct NetworkDNSForwarderXML { | ||||
|     /// Address of the DNS server | ||||
|     #[serde(rename(serialize = "@addr"))] | ||||
|     pub addr: Ipv4Addr, | ||||
| } | ||||
|  | ||||
| /// Network DNS information | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug)] | ||||
| #[serde(rename = "domain")] | ||||
| pub struct NetworkDomainXML { | ||||
|     #[serde(rename(serialize = "@name"))] | ||||
|     pub name: String, | ||||
| } | ||||
|  | ||||
| fn invalid_prefix() -> u32 { | ||||
|     u32::MAX | ||||
| } | ||||
|  | ||||
| fn invalid_ip() -> IpAddr { | ||||
|     IpAddr::V4(Ipv4Addr::BROADCAST) | ||||
| } | ||||
|  | ||||
| /// Network ip information | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug)] | ||||
| #[serde(rename = "ip")] | ||||
| pub struct NetworkIPXML { | ||||
|     #[serde(default, rename(serialize = "@family"))] | ||||
|     pub family: String, | ||||
|     #[serde(rename(serialize = "@address"))] | ||||
|     pub address: IpAddr, | ||||
|     /// Network Prefix | ||||
|     #[serde(rename(serialize = "@prefix"), default = "invalid_prefix")] | ||||
|     pub prefix: u32, | ||||
|     /// Network Netmask. This field is never serialized, but because we can't know if LibVirt will | ||||
|     /// provide us netmask or prefix, we need to handle both of these fields | ||||
|     #[serde( | ||||
|         rename(serialize = "@netmask"), | ||||
|         default = "invalid_ip", | ||||
|         skip_serializing | ||||
|     )] | ||||
|     pub netmask: IpAddr, | ||||
|     pub dhcp: Option<NetworkDHCPXML>, | ||||
| } | ||||
|  | ||||
| impl NetworkIPXML { | ||||
|     pub fn into_xml(mut self) -> anyhow::Result<String> { | ||||
|         let mut hosts_xml = vec![]; | ||||
|  | ||||
|         if let Some(dhcp) = &mut self.dhcp { | ||||
|             for host in &dhcp.hosts { | ||||
|                 let mut host_xml = serde_xml_rs::to_string(&host)?; | ||||
|  | ||||
|                 // In case of IPv6, mac address should not be specified | ||||
|                 host_xml = host_xml.replace("mac=\"\"", ""); | ||||
|  | ||||
|                 // strip xml tag | ||||
|                 let start_offset = host_xml.find("<host").unwrap(); | ||||
|                 hosts_xml.push(host_xml[start_offset..].to_string()); | ||||
|             } | ||||
|  | ||||
|             dhcp.hosts = vec![]; | ||||
|         } | ||||
|  | ||||
|         let mut res = serde_xml_rs::to_string(&self)?; | ||||
|         let hosts_xml = hosts_xml.join("\n"); | ||||
|         res = res.replace("</dhcp>", &format!("{hosts_xml}</dhcp>")); | ||||
|         Ok(res) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug)] | ||||
| #[serde(rename = "dhcp")] | ||||
| pub struct NetworkDHCPXML { | ||||
|     pub range: NetworkDHCPRangeXML, | ||||
|     #[serde(default, rename = "host", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub hosts: Vec<NetworkDHCPHostXML>, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug)] | ||||
| #[serde(rename = "host")] | ||||
| pub struct NetworkDHCPHostXML { | ||||
|     #[serde(rename(serialize = "@mac"), default)] | ||||
|     pub mac: String, | ||||
|     #[serde(rename(serialize = "@name"))] | ||||
|     pub name: String, | ||||
|     #[serde(rename(serialize = "@ip"))] | ||||
|     pub ip: IpAddr, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug)] | ||||
| #[serde(rename = "dhcp")] | ||||
| pub struct NetworkDHCPRangeXML { | ||||
|     #[serde(rename(serialize = "@start"))] | ||||
|     pub start: IpAddr, | ||||
|     #[serde(rename(serialize = "@end"))] | ||||
|     pub end: IpAddr, | ||||
| } | ||||
|  | ||||
| /// Network information, see https://libvirt.org/formatnetwork.html | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug)] | ||||
| #[serde(rename = "network")] | ||||
| pub struct NetworkXML { | ||||
|     pub name: String, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub uuid: Option<XMLUuid>, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub title: Option<String>, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub description: Option<String>, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub forward: Option<NetworkForwardXML>, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub bridge: Option<NetworkBridgeXML>, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub dns: Option<NetworkDNSXML>, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub domain: Option<NetworkDomainXML>, | ||||
|     #[serde(default, rename = "ip")] | ||||
|     pub ips: Vec<NetworkIPXML>, | ||||
| } | ||||
|  | ||||
| impl NetworkXML { | ||||
|     pub fn into_xml(mut self) -> anyhow::Result<String> { | ||||
|         // A issue with the IPs definition serialization needs them to be serialized aside | ||||
|         let mut ips_xml = Vec::with_capacity(self.ips.len()); | ||||
|         for ip in self.ips { | ||||
|             log::debug!("Serialize {ip:?}"); | ||||
|             let ip_xml = ip.into_xml()?; | ||||
|             // strip xml tag | ||||
|             let start_offset = ip_xml.find("<ip").unwrap(); | ||||
|             ips_xml.push(ip_xml[start_offset..].to_string()); | ||||
|         } | ||||
|         self.ips = vec![]; | ||||
|  | ||||
|         let mut network_xml = serde_xml_rs::to_string(&self)?; | ||||
|         log::trace!("Serialize network XML start: {network_xml}"); | ||||
|  | ||||
|         let ips_xml = ips_xml.join("\n"); | ||||
|         network_xml = network_xml.replacen("</network>", &format!("{ips_xml}</network>"), 1); | ||||
|         Ok(network_xml) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										389
									
								
								virtweb_backend/src/libvirt_lib_structures/domain.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										389
									
								
								virtweb_backend/src/libvirt_lib_structures/domain.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,389 @@ | ||||
| use crate::libvirt_lib_structures::XMLUuid; | ||||
|  | ||||
| /// VirtWeb specific metadata | ||||
| #[derive(serde::Serialize, serde::Deserialize, Default, Debug, Clone)] | ||||
| #[serde(rename = "virtweb", default)] | ||||
| pub struct DomainMetadataVirtWebXML { | ||||
|     #[serde(rename = "@xmlns:virtweb", default)] | ||||
|     pub ns: String, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub group: Option<String>, | ||||
| } | ||||
|  | ||||
| /// Domain metadata | ||||
| #[derive(serde::Serialize, serde::Deserialize, Default, Debug, Clone)] | ||||
| #[serde(rename = "metadata")] | ||||
| pub struct DomainMetadataXML { | ||||
|     #[serde(rename = "virtweb:metadata", default)] | ||||
|     pub virtweb: DomainMetadataVirtWebXML, | ||||
| } | ||||
|  | ||||
| /// OS information | ||||
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "os")] | ||||
| pub struct OSXML { | ||||
|     #[serde(rename = "@firmware", default)] | ||||
|     pub firmware: String, | ||||
|     pub r#type: OSTypeXML, | ||||
|     pub loader: Option<OSLoaderXML>, | ||||
| } | ||||
|  | ||||
| /// OS Type information | ||||
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "os")] | ||||
| pub struct OSTypeXML { | ||||
|     #[serde(rename = "@arch")] | ||||
|     pub arch: String, | ||||
|     #[serde(rename = "@machine")] | ||||
|     pub machine: String, | ||||
|     #[serde(rename = "$value")] | ||||
|     pub body: String, | ||||
| } | ||||
|  | ||||
| /// OS Loader information | ||||
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "loader")] | ||||
| pub struct OSLoaderXML { | ||||
|     #[serde(rename = "@secure")] | ||||
|     pub secure: String, | ||||
| } | ||||
|  | ||||
| /// Hypervisor features | ||||
| #[derive(serde::Serialize, serde::Deserialize, Clone, Default, Debug)] | ||||
| #[serde(rename = "features")] | ||||
| pub struct FeaturesXML { | ||||
|     pub acpi: ACPIXML, | ||||
| } | ||||
|  | ||||
| /// ACPI feature | ||||
| #[derive(serde::Serialize, serde::Deserialize, Clone, Default, Debug)] | ||||
| #[serde(rename = "acpi")] | ||||
| pub struct ACPIXML {} | ||||
|  | ||||
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "mac")] | ||||
| pub struct NetMacAddress { | ||||
|     #[serde(rename = "@address")] | ||||
|     pub address: String, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "source")] | ||||
| pub struct NetIntSourceXML { | ||||
|     #[serde(rename = "@network")] | ||||
|     pub network: String, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "model")] | ||||
| pub struct NetIntModelXML { | ||||
|     #[serde(rename = "@type")] | ||||
|     pub r#type: String, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "filterref")] | ||||
| pub struct NetIntFilterParameterXML { | ||||
|     #[serde(rename = "@name")] | ||||
|     pub name: String, | ||||
|     #[serde(rename = "@value")] | ||||
|     pub value: String, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "filterref")] | ||||
| pub struct NetIntfilterRefXML { | ||||
|     #[serde(rename = "@filter")] | ||||
|     pub filter: String, | ||||
|     #[serde(rename = "parameter", default)] | ||||
|     pub parameters: Vec<NetIntFilterParameterXML>, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "interface")] | ||||
| pub struct DomainNetInterfaceXML { | ||||
|     #[serde(rename = "@type")] | ||||
|     pub r#type: String, | ||||
|     pub mac: NetMacAddress, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub source: Option<NetIntSourceXML>, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub model: Option<NetIntModelXML>, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub filterref: Option<NetIntfilterRefXML>, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "input")] | ||||
| pub struct DomainInputXML { | ||||
|     #[serde(rename = "@type")] | ||||
|     pub r#type: String, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "backend")] | ||||
| pub struct TPMBackendXML { | ||||
|     #[serde(rename = "@type")] | ||||
|     pub r#type: String, | ||||
|  | ||||
|     #[serde(rename = "@version")] | ||||
|     pub r#version: String, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "tpm")] | ||||
| pub struct TPMDeviceXML { | ||||
|     #[serde(rename = "@model")] | ||||
|     pub model: String, | ||||
|     pub backend: TPMBackendXML, | ||||
| } | ||||
|  | ||||
| /// Devices information | ||||
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "devices")] | ||||
| pub struct DevicesXML { | ||||
|     /// Graphics (used for VNC) | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub graphics: Option<GraphicsXML>, | ||||
|  | ||||
|     /// Graphics (used for VNC) | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub video: Option<VideoXML>, | ||||
|  | ||||
|     /// Disks (used for storage) | ||||
|     #[serde(default, rename = "disk", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub disks: Vec<DiskXML>, | ||||
|  | ||||
|     /// Networks cards | ||||
|     #[serde(default, rename = "interface", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub net_interfaces: Vec<DomainNetInterfaceXML>, | ||||
|  | ||||
|     /// Input devices | ||||
|     #[serde(default, rename = "input", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub inputs: Vec<DomainInputXML>, | ||||
|  | ||||
|     /// TPM device | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub tpm: Option<TPMDeviceXML>, | ||||
| } | ||||
|  | ||||
| /// Graphics information | ||||
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "graphics")] | ||||
| pub struct GraphicsXML { | ||||
|     #[serde(rename = "@type")] | ||||
|     pub r#type: String, | ||||
|     #[serde(rename = "@socket")] | ||||
|     pub socket: String, | ||||
| } | ||||
|  | ||||
| /// Video device information | ||||
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "video")] | ||||
| pub struct VideoXML { | ||||
|     pub model: VideoModelXML, | ||||
| } | ||||
|  | ||||
| /// Video model device information | ||||
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "model")] | ||||
| pub struct VideoModelXML { | ||||
|     #[serde(rename = "@type")] | ||||
|     pub r#type: String, | ||||
| } | ||||
|  | ||||
| /// Disk information | ||||
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "disk")] | ||||
| pub struct DiskXML { | ||||
|     #[serde(rename = "@type")] | ||||
|     pub r#type: String, | ||||
|     #[serde(rename = "@device")] | ||||
|     pub r#device: String, | ||||
|  | ||||
|     pub driver: DiskDriverXML, | ||||
|     pub source: DiskSourceXML, | ||||
|     pub target: DiskTargetXML, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub readonly: Option<DiskReadOnlyXML>, | ||||
|     pub boot: DiskBootXML, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub address: Option<DiskAddressXML>, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "driver")] | ||||
| pub struct DiskDriverXML { | ||||
|     #[serde(rename = "@name")] | ||||
|     pub name: String, | ||||
|     #[serde(rename = "@type")] | ||||
|     pub r#type: String, | ||||
|     #[serde(default, rename = "@cache")] | ||||
|     pub r#cache: String, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "source")] | ||||
| pub struct DiskSourceXML { | ||||
|     #[serde(rename = "@file")] | ||||
|     pub file: String, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "target")] | ||||
| pub struct DiskTargetXML { | ||||
|     #[serde(rename = "@dev")] | ||||
|     pub dev: String, | ||||
|     #[serde(rename = "@bus")] | ||||
|     pub bus: String, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "readonly")] | ||||
| pub struct DiskReadOnlyXML {} | ||||
|  | ||||
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "boot")] | ||||
| pub struct DiskBootXML { | ||||
|     #[serde(rename = "@order")] | ||||
|     pub order: String, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "address")] | ||||
| pub struct DiskAddressXML { | ||||
|     #[serde(rename = "@type")] | ||||
|     pub r#type: String, | ||||
|     #[serde( | ||||
|         default, | ||||
|         skip_serializing_if = "Option::is_none", | ||||
|         rename = "@controller" | ||||
|     )] | ||||
|     pub r#controller: Option<String>, | ||||
|     #[serde(rename = "@bus")] | ||||
|     pub r#bus: String, | ||||
|     #[serde(default, skip_serializing_if = "Option::is_none", rename = "@target")] | ||||
|     pub r#target: Option<String>, | ||||
|     #[serde(default, skip_serializing_if = "Option::is_none", rename = "@unit")] | ||||
|     pub r#unit: Option<String>, | ||||
| } | ||||
|  | ||||
| /// Domain RAM information | ||||
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "memory")] | ||||
| pub struct DomainMemoryXML { | ||||
|     #[serde(rename = "@unit")] | ||||
|     pub unit: String, | ||||
|  | ||||
|     #[serde(rename = "$value")] | ||||
|     pub memory: usize, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "topology")] | ||||
| pub struct DomainCPUTopology { | ||||
|     #[serde(rename = "@sockets")] | ||||
|     pub sockets: usize, | ||||
|     #[serde(rename = "@cores")] | ||||
|     pub cores: usize, | ||||
|     #[serde(rename = "@threads")] | ||||
|     pub threads: usize, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "cpu")] | ||||
| pub struct DomainVCPUXML { | ||||
|     #[serde(rename = "$value")] | ||||
|     pub body: usize, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "cpu")] | ||||
| pub struct DomainCPUXML { | ||||
|     #[serde(rename = "@mode")] | ||||
|     pub mode: String, | ||||
|     pub topology: Option<DomainCPUTopology>, | ||||
| } | ||||
|  | ||||
| /// Domain information, see https://libvirt.org/formatdomain.html | ||||
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | ||||
| #[serde(rename = "domain")] | ||||
| pub struct DomainXML { | ||||
|     /// Domain type (kvm) | ||||
|     #[serde(rename = "@type")] | ||||
|     pub r#type: String, | ||||
|  | ||||
|     pub name: String, | ||||
|     pub uuid: Option<XMLUuid>, | ||||
|     pub genid: Option<uuid::Uuid>, | ||||
|     pub title: Option<String>, | ||||
|     pub description: Option<String>, | ||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] | ||||
|     pub metadata: Option<DomainMetadataXML>, | ||||
|  | ||||
|     pub os: OSXML, | ||||
|     #[serde(default)] | ||||
|     pub features: FeaturesXML, | ||||
|     pub devices: DevicesXML, | ||||
|  | ||||
|     /// The maximum allocation of memory for the guest at boot time | ||||
|     pub memory: DomainMemoryXML, | ||||
|  | ||||
|     /// Number of vCPU | ||||
|     pub vcpu: DomainVCPUXML, | ||||
|  | ||||
|     /// CPU information | ||||
|     pub cpu: DomainCPUXML, | ||||
|  | ||||
|     pub on_poweroff: String, | ||||
|     pub on_reboot: String, | ||||
|     pub on_crash: String, | ||||
| } | ||||
|  | ||||
| const METADATA_START_MARKER: &str = | ||||
|     "<virtweb:metadata xmlns:virtweb=\"https://virtweb.communiquons.org\">"; | ||||
| const METADATA_END_MARKER: &str = "</virtweb:metadata>"; | ||||
|  | ||||
| impl DomainXML { | ||||
|     /// Decode Domain structure from XML definition | ||||
|     pub fn parse_xml(xml: &str) -> anyhow::Result<Self> { | ||||
|         let mut res: Self = quick_xml::de::from_str(xml)?; | ||||
|  | ||||
|         // Handle custom metadata parsing issue | ||||
|         // | ||||
|         // https://github.com/tafia/quick-xml/pull/797 | ||||
|         if xml.contains(METADATA_START_MARKER) && xml.contains(METADATA_END_MARKER) { | ||||
|             let s = xml | ||||
|                 .split_once(METADATA_START_MARKER) | ||||
|                 .unwrap() | ||||
|                 .1 | ||||
|                 .split_once(METADATA_END_MARKER) | ||||
|                 .unwrap() | ||||
|                 .0; | ||||
|             let s = format!("<virtweb>{s}</virtweb>"); | ||||
|             let metadata: DomainMetadataVirtWebXML = quick_xml::de::from_str(&s)?; | ||||
|             res.metadata = Some(DomainMetadataXML { virtweb: metadata }); | ||||
|         } | ||||
|  | ||||
|         Ok(res) | ||||
|     } | ||||
|  | ||||
|     /// Turn this domain into its XML definition | ||||
|     pub fn as_xml(&self) -> anyhow::Result<String> { | ||||
|         Ok(quick_xml::se::to_string(self)?) | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Domain state | ||||
| #[derive(serde::Serialize, Debug, Copy, Clone)] | ||||
| pub enum DomainState { | ||||
|     NoState, | ||||
|     Running, | ||||
|     Blocked, | ||||
|     Paused, | ||||
|     Shutdown, | ||||
|     Shutoff, | ||||
|     Crashed, | ||||
|     PowerManagementSuspended, | ||||
|     Other, | ||||
| } | ||||
							
								
								
									
										24
									
								
								virtweb_backend/src/libvirt_lib_structures/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								virtweb_backend/src/libvirt_lib_structures/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| #[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug, Eq, PartialEq, Hash)] | ||||
| pub struct XMLUuid(pub uuid::Uuid); | ||||
|  | ||||
| impl XMLUuid { | ||||
|     pub fn parse_from_str(s: &str) -> anyhow::Result<Self> { | ||||
|         Ok(Self(uuid::Uuid::parse_str(s)?)) | ||||
|     } | ||||
|  | ||||
|     pub fn new_random() -> Self { | ||||
|         Self(uuid::Uuid::new_v4()) | ||||
|     } | ||||
|     pub fn as_string(&self) -> String { | ||||
|         self.0.to_string() | ||||
|     } | ||||
|  | ||||
|     pub fn is_valid(&self) -> bool { | ||||
|         log::debug!("UUID version ({}): {}", self.0, self.0.get_version_num()); | ||||
|         self.0.get_version_num() == 4 | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub mod domain; | ||||
| pub mod network; | ||||
| pub mod nwfilter; | ||||
							
								
								
									
										124
									
								
								virtweb_backend/src/libvirt_lib_structures/network.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								virtweb_backend/src/libvirt_lib_structures/network.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,124 @@ | ||||
| use crate::libvirt_lib_structures::XMLUuid; | ||||
| use std::net::{IpAddr, Ipv4Addr}; | ||||
|  | ||||
| /// Network forward information | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug)] | ||||
| #[serde(rename = "forward")] | ||||
| pub struct NetworkForwardXML { | ||||
|     #[serde(rename = "@mode")] | ||||
|     pub mode: String, | ||||
|     #[serde(default, rename = "@dev", skip_serializing_if = "String::is_empty")] | ||||
|     pub dev: String, | ||||
| } | ||||
|  | ||||
| /// Network bridge information | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug)] | ||||
| #[serde(rename = "bridge")] | ||||
| pub struct NetworkBridgeXML { | ||||
|     #[serde(rename = "@name")] | ||||
|     pub name: String, | ||||
| } | ||||
|  | ||||
| /// Network DNS information | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug)] | ||||
| #[serde(rename = "dns")] | ||||
| pub struct NetworkDNSXML { | ||||
|     pub forwarder: NetworkDNSForwarderXML, | ||||
| } | ||||
|  | ||||
| /// Network DNS information | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug)] | ||||
| #[serde(rename = "forwarder")] | ||||
| pub struct NetworkDNSForwarderXML { | ||||
|     /// Address of the DNS server | ||||
|     #[serde(rename = "@addr")] | ||||
|     pub addr: Ipv4Addr, | ||||
| } | ||||
|  | ||||
| /// Network DNS information | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug)] | ||||
| #[serde(rename = "domain")] | ||||
| pub struct NetworkDomainXML { | ||||
|     #[serde(rename = "@name")] | ||||
|     pub name: String, | ||||
| } | ||||
|  | ||||
| /// Network ip information | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug)] | ||||
| #[serde(rename = "ip")] | ||||
| pub struct NetworkIPXML { | ||||
|     #[serde(default, rename = "@family")] | ||||
|     pub family: String, | ||||
|     #[serde(rename = "@address")] | ||||
|     pub address: IpAddr, | ||||
|     /// Network Prefix | ||||
|     #[serde(rename = "@prefix")] | ||||
|     pub prefix: Option<u8>, | ||||
|     /// Network Netmask. This field is never serialized, but because we can't know if LibVirt will | ||||
|     /// provide us netmask or prefix, we need to handle both of these fields | ||||
|     #[serde(rename = "@netmask", skip_serializing)] | ||||
|     pub netmask: Option<IpAddr>, | ||||
|  | ||||
|     pub dhcp: Option<NetworkDHCPXML>, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug)] | ||||
| #[serde(rename = "dhcp")] | ||||
| pub struct NetworkDHCPXML { | ||||
|     pub range: NetworkDHCPRangeXML, | ||||
|     #[serde(default, rename = "host", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub hosts: Vec<NetworkDHCPHostXML>, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug)] | ||||
| #[serde(rename = "host")] | ||||
| pub struct NetworkDHCPHostXML { | ||||
|     #[serde(rename = "@mac", default, skip_serializing_if = "Option::is_none")] | ||||
|     pub mac: Option<String>, | ||||
|     #[serde(rename = "@name")] | ||||
|     pub name: String, | ||||
|     #[serde(rename = "@ip")] | ||||
|     pub ip: IpAddr, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug)] | ||||
| #[serde(rename = "dhcp")] | ||||
| pub struct NetworkDHCPRangeXML { | ||||
|     #[serde(rename = "@start")] | ||||
|     pub start: IpAddr, | ||||
|     #[serde(rename = "@end")] | ||||
|     pub end: IpAddr, | ||||
| } | ||||
|  | ||||
| /// Network information, see https://libvirt.org/formatnetwork.html | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug)] | ||||
| #[serde(rename = "network")] | ||||
| pub struct NetworkXML { | ||||
|     pub name: String, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub uuid: Option<XMLUuid>, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub title: Option<String>, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub description: Option<String>, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub forward: Option<NetworkForwardXML>, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub bridge: Option<NetworkBridgeXML>, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub dns: Option<NetworkDNSXML>, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub domain: Option<NetworkDomainXML>, | ||||
|     #[serde(default, rename = "ip")] | ||||
|     pub ips: Vec<NetworkIPXML>, | ||||
| } | ||||
|  | ||||
| impl NetworkXML { | ||||
|     pub fn parse_xml(xml: &str) -> anyhow::Result<Self> { | ||||
|         Ok(quick_xml::de::from_str(xml)?) | ||||
|     } | ||||
|  | ||||
|     pub fn as_xml(&self) -> anyhow::Result<String> { | ||||
|         Ok(quick_xml::se::to_string(self)?) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										240
									
								
								virtweb_backend/src/libvirt_lib_structures/nwfilter.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										240
									
								
								virtweb_backend/src/libvirt_lib_structures/nwfilter.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,240 @@ | ||||
| use crate::libvirt_lib_structures::XMLUuid; | ||||
| use std::fmt::Display; | ||||
| use std::net::{Ipv4Addr, Ipv6Addr}; | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug)] | ||||
| #[serde(rename = "filterref")] | ||||
| pub struct NetworkFilterRefXML { | ||||
|     #[serde(rename = "@filter")] | ||||
|     pub filter: String, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug)] | ||||
| #[serde(rename = "mac")] | ||||
| pub struct NetworkFilterRuleProtocolMac { | ||||
|     #[serde(rename = "@srcmacaddr", skip_serializing_if = "Option::is_none")] | ||||
|     pub srcmacaddr: Option<String>, | ||||
|     #[serde(rename = "@scmacmask", skip_serializing_if = "Option::is_none")] | ||||
|     pub srcmacmask: Option<String>, | ||||
|     #[serde(rename = "@dstmacaddr", skip_serializing_if = "Option::is_none")] | ||||
|     pub dstmacaddr: Option<String>, | ||||
|     #[serde(rename = "@dstmacmask", skip_serializing_if = "Option::is_none")] | ||||
|     pub dstmacmask: Option<String>, | ||||
|     #[serde(rename = "@comment", skip_serializing_if = "Option::is_none")] | ||||
|     pub comment: Option<String>, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug)] | ||||
| #[serde(rename = "arp")] | ||||
| pub struct NetworkFilterRuleProtocolArpXML { | ||||
|     #[serde(rename = "@srcmacaddr", skip_serializing_if = "Option::is_none")] | ||||
|     pub srcmacaddr: Option<String>, | ||||
|     #[serde(rename = "@srcmacmask", skip_serializing_if = "Option::is_none")] | ||||
|     pub srcmacmask: Option<String>, | ||||
|     #[serde(rename = "@dstmacaddr", skip_serializing_if = "Option::is_none")] | ||||
|     pub dstmacaddr: Option<String>, | ||||
|     #[serde(rename = "@dstmacmask", skip_serializing_if = "Option::is_none")] | ||||
|     pub dstmacmask: Option<String>, | ||||
|     #[serde(rename = "@arpsrcipaddr", skip_serializing_if = "Option::is_none")] | ||||
|     pub arpsrcipaddr: Option<String>, | ||||
|     #[serde(rename = "@arpsrcipmask", skip_serializing_if = "Option::is_none")] | ||||
|     pub arpsrcipmask: Option<u8>, | ||||
|     #[serde(rename = "@arpdstipaddr", skip_serializing_if = "Option::is_none")] | ||||
|     pub arpdstipaddr: Option<String>, | ||||
|     #[serde(rename = "@arpdstipmask", skip_serializing_if = "Option::is_none")] | ||||
|     pub arpdstipmask: Option<u8>, | ||||
|     #[serde(rename = "@comment", skip_serializing_if = "Option::is_none")] | ||||
|     pub comment: Option<String>, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug)] | ||||
| #[serde(rename = "ipvx")] | ||||
| pub struct NetworkFilterRuleProtocolIpvx { | ||||
|     #[serde(rename = "@srcmacaddr", skip_serializing_if = "Option::is_none")] | ||||
|     pub srcmacaddr: Option<String>, | ||||
|     #[serde(rename = "@srcmacmask", skip_serializing_if = "Option::is_none")] | ||||
|     pub srcmacmask: Option<String>, | ||||
|     #[serde(rename = "@dstmacaddr", skip_serializing_if = "Option::is_none")] | ||||
|     pub dstmacaddr: Option<String>, | ||||
|     #[serde(rename = "@dstmacmask", skip_serializing_if = "Option::is_none")] | ||||
|     pub dstmacmask: Option<String>, | ||||
|     #[serde(rename = "@srcipaddr", skip_serializing_if = "Option::is_none")] | ||||
|     pub srcipaddr: Option<String>, | ||||
|     #[serde(rename = "@srcipmask", skip_serializing_if = "Option::is_none")] | ||||
|     pub srcipmask: Option<u8>, | ||||
|     #[serde(rename = "@dstipaddr", skip_serializing_if = "Option::is_none")] | ||||
|     pub dstipaddr: Option<String>, | ||||
|     #[serde(rename = "@dstipmask", skip_serializing_if = "Option::is_none")] | ||||
|     pub dstipmask: Option<u8>, | ||||
|  | ||||
|     #[serde(rename = "@comment", skip_serializing_if = "Option::is_none")] | ||||
|     pub comment: Option<String>, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug)] | ||||
| #[serde(rename = "layer4")] | ||||
| pub struct NetworkFilterRuleProtocolLayer4<IPv> { | ||||
|     #[serde(rename = "@srcmacaddr", skip_serializing_if = "Option::is_none")] | ||||
|     pub srcmacaddr: Option<String>, | ||||
|     #[serde(rename = "@srcipaddr", skip_serializing_if = "Option::is_none")] | ||||
|     pub srcipaddr: Option<IPv>, | ||||
|     #[serde(rename = "@srcipmask", skip_serializing_if = "Option::is_none")] | ||||
|     pub srcipmask: Option<u8>, | ||||
|     #[serde(rename = "@dstipaddr", skip_serializing_if = "Option::is_none")] | ||||
|     pub dstipaddr: Option<IPv>, | ||||
|     #[serde(rename = "@dstipmask", skip_serializing_if = "Option::is_none")] | ||||
|     pub dstipmask: Option<u8>, | ||||
|     /// Start of range of source IP address | ||||
|     #[serde(rename = "@srcipfrom", skip_serializing_if = "Option::is_none")] | ||||
|     pub srcipfrom: Option<IPv>, | ||||
|     /// End of range of source IP address | ||||
|     #[serde(rename = "@srcipto", skip_serializing_if = "Option::is_none")] | ||||
|     pub srcipto: Option<IPv>, | ||||
|     /// Start of range of destination IP address | ||||
|     #[serde(rename = "@dstipfrom", skip_serializing_if = "Option::is_none")] | ||||
|     pub dstipfrom: Option<IPv>, | ||||
|     /// End of range of destination IP address | ||||
|     #[serde(rename = "@dstipto", skip_serializing_if = "Option::is_none")] | ||||
|     pub dstipto: Option<IPv>, | ||||
|     #[serde(rename = "@srcportstart", skip_serializing_if = "Option::is_none")] | ||||
|     pub srcportstart: Option<u16>, | ||||
|     #[serde(rename = "@srcportend", skip_serializing_if = "Option::is_none")] | ||||
|     pub srcportend: Option<u16>, | ||||
|     #[serde(rename = "@dstportstart", skip_serializing_if = "Option::is_none")] | ||||
|     pub dstportstart: Option<u16>, | ||||
|     #[serde(rename = "@dstportend", skip_serializing_if = "Option::is_none")] | ||||
|     pub dstportend: Option<u16>, | ||||
|     #[serde(rename = "@state", skip_serializing_if = "Option::is_none")] | ||||
|     pub state: Option<String>, | ||||
|     #[serde(rename = "@comment", skip_serializing_if = "Option::is_none")] | ||||
|     pub comment: Option<String>, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug)] | ||||
| #[serde(rename = "all")] | ||||
| pub struct NetworkFilterRuleProtocolAllXML<IPv> { | ||||
|     #[serde(rename = "@srcmacaddr", skip_serializing_if = "Option::is_none")] | ||||
|     pub srcmacaddr: Option<String>, | ||||
|     #[serde(rename = "@srcipaddr", skip_serializing_if = "Option::is_none")] | ||||
|     pub srcipaddr: Option<IPv>, | ||||
|     #[serde(rename = "@srcipmask", skip_serializing_if = "Option::is_none")] | ||||
|     pub srcipmask: Option<u8>, | ||||
|     #[serde(rename = "@dstipaddr", skip_serializing_if = "Option::is_none")] | ||||
|     pub dstipaddr: Option<IPv>, | ||||
|     #[serde(rename = "@dstipmask", skip_serializing_if = "Option::is_none")] | ||||
|     pub dstipmask: Option<u8>, | ||||
|     /// Start of range of source IP address | ||||
|     #[serde(rename = "@srcipfrom", skip_serializing_if = "Option::is_none")] | ||||
|     pub srcipfrom: Option<IPv>, | ||||
|     /// End of range of source IP address | ||||
|     #[serde(rename = "@srcipto", skip_serializing_if = "Option::is_none")] | ||||
|     pub srcipto: Option<IPv>, | ||||
|     /// Start of range of destination IP address | ||||
|     #[serde(rename = "@dstipfrom", skip_serializing_if = "Option::is_none")] | ||||
|     pub dstipfrom: Option<IPv>, | ||||
|     /// End of range of destination IP address | ||||
|     #[serde(rename = "@dstipto", skip_serializing_if = "Option::is_none")] | ||||
|     pub dstipto: Option<IPv>, | ||||
|     #[serde(rename = "@state", skip_serializing_if = "Option::is_none")] | ||||
|     pub state: Option<String>, | ||||
|     #[serde(rename = "@comment", skip_serializing_if = "Option::is_none")] | ||||
|     pub comment: Option<String>, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug, Default)] | ||||
| #[serde(rename = "rule")] | ||||
| pub struct NetworkFilterRuleXML { | ||||
|     #[serde(rename = "@action")] | ||||
|     pub action: String, | ||||
|     #[serde(rename = "@direction")] | ||||
|     pub direction: String, | ||||
|     #[serde(rename = "@priority")] | ||||
|     pub priority: Option<i32>, | ||||
|  | ||||
|     /// Match mac protocol | ||||
|     #[serde(default, rename = "mac", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub mac_selectors: Vec<NetworkFilterRuleProtocolMac>, | ||||
|  | ||||
|     /// Match arp protocol | ||||
|     #[serde(default, rename = "arp", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub arp_selectors: Vec<NetworkFilterRuleProtocolArpXML>, | ||||
|  | ||||
|     /// Match rarp protocol | ||||
|     #[serde(default, rename = "rarp", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub rarp_selectors: Vec<NetworkFilterRuleProtocolArpXML>, | ||||
|  | ||||
|     /// Match IPv4 protocol | ||||
|     #[serde(default, rename = "ip", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub ipv4_selectors: Vec<NetworkFilterRuleProtocolIpvx>, | ||||
|  | ||||
|     /// Match IPv6 protocol | ||||
|     #[serde(default, rename = "ipv6", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub ipv6_selectors: Vec<NetworkFilterRuleProtocolIpvx>, | ||||
|  | ||||
|     /// Match TCP protocol | ||||
|     #[serde(default, rename = "tcp", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub tcp_selectors: Vec<NetworkFilterRuleProtocolLayer4<Ipv4Addr>>, | ||||
|  | ||||
|     /// Match UDP protocol | ||||
|     #[serde(default, rename = "udp", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub udp_selectors: Vec<NetworkFilterRuleProtocolLayer4<Ipv4Addr>>, | ||||
|  | ||||
|     /// Match SCTP protocol | ||||
|     #[serde(default, rename = "sctp", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub sctp_selectors: Vec<NetworkFilterRuleProtocolLayer4<Ipv4Addr>>, | ||||
|  | ||||
|     /// Match ICMP protocol | ||||
|     #[serde(default, rename = "icmp", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub icmp_selectors: Vec<NetworkFilterRuleProtocolLayer4<Ipv4Addr>>, | ||||
|  | ||||
|     /// Match all protocols | ||||
|     #[serde(default, rename = "all", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub all_selectors: Vec<NetworkFilterRuleProtocolAllXML<Ipv4Addr>>, | ||||
|  | ||||
|     /// Match TCP IPv6 protocol | ||||
|     #[serde(default, rename = "tcp-ipv6", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub tcp_ipv6_selectors: Vec<NetworkFilterRuleProtocolLayer4<Ipv6Addr>>, | ||||
|  | ||||
|     /// Match UDP IPv6 protocol | ||||
|     #[serde(default, rename = "udp-ipv6", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub udp_ipv6_selectors: Vec<NetworkFilterRuleProtocolLayer4<Ipv6Addr>>, | ||||
|  | ||||
|     /// Match SCTP IPv6 protocol | ||||
|     #[serde(default, rename = "sctp-ipv6", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub sctp_ipv6_selectors: Vec<NetworkFilterRuleProtocolLayer4<Ipv6Addr>>, | ||||
|  | ||||
|     /// Match ICMP IPv6 protocol | ||||
|     #[serde(default, rename = "icmpv6", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub imcp_ipv6_selectors: Vec<NetworkFilterRuleProtocolLayer4<Ipv6Addr>>, | ||||
|  | ||||
|     /// Match all ipv6 protocols | ||||
|     #[serde(default, rename = "all-ipv6", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub all_ipv6_selectors: Vec<NetworkFilterRuleProtocolAllXML<Ipv6Addr>>, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug)] | ||||
| #[serde(rename = "filter")] | ||||
| pub struct NetworkFilterXML { | ||||
|     #[serde(rename = "@name")] | ||||
|     pub name: String, | ||||
|     #[serde(rename = "@chain", skip_serializing_if = "Option::is_none", default)] | ||||
|     pub chain: Option<String>, | ||||
|     #[serde(skip_serializing_if = "Option::is_none", rename = "@priority", default)] | ||||
|     pub priority: Option<i32>, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub uuid: Option<XMLUuid>, | ||||
|     #[serde(default, rename = "filterref")] | ||||
|     pub filterrefs: Vec<NetworkFilterRefXML>, | ||||
|     #[serde(default, rename = "rule", skip_serializing_if = "Vec::is_empty")] | ||||
|     pub rules: Vec<NetworkFilterRuleXML>, | ||||
| } | ||||
|  | ||||
| impl NetworkFilterXML { | ||||
|     pub fn parse_xml<D: Display>(xml: D) -> anyhow::Result<Self> { | ||||
|         Ok(quick_xml::de::from_str(&xml.to_string())?) | ||||
|     } | ||||
|  | ||||
|     pub fn into_xml(self) -> anyhow::Result<String> { | ||||
|         Ok(quick_xml::se::to_string(&self)?) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										23
									
								
								virtweb_backend/src/libvirt_rest_structures/hypervisor.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								virtweb_backend/src/libvirt_rest_structures/hypervisor.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| #[derive(serde::Serialize)] | ||||
| pub struct HypervisorInfo { | ||||
|     pub r#type: String, | ||||
|     pub hyp_version: u32, | ||||
|     pub lib_version: u32, | ||||
|     pub capabilities: String, | ||||
|     pub free_memory: u64, | ||||
|     pub hostname: String, | ||||
|     pub node: HypervisorNodeInfo, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize)] | ||||
| pub struct HypervisorNodeInfo { | ||||
|     pub cpu_model: String, | ||||
|     /// Memory size in kilobytes | ||||
|     pub memory_size: u64, | ||||
|     pub number_of_active_cpus: u32, | ||||
|     pub cpu_frequency_mhz: u32, | ||||
|     pub number_of_numa_cell: u32, | ||||
|     pub number_of_cpu_socket_per_node: u32, | ||||
|     pub number_of_core_per_sockets: u32, | ||||
|     pub number_of_threads_per_core: u32, | ||||
| } | ||||
							
								
								
									
										16
									
								
								virtweb_backend/src/libvirt_rest_structures/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								virtweb_backend/src/libvirt_rest_structures/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| pub mod hypervisor; | ||||
| pub mod net; | ||||
| pub mod nw_filter; | ||||
| pub mod vm; | ||||
|  | ||||
| #[derive(thiserror::Error, Debug)] | ||||
| enum LibVirtStructError { | ||||
|     #[error("StructureExtractionError: {0}")] | ||||
|     StructureExtraction(&'static str), | ||||
|     #[error("DomainExtractionError: {0}")] | ||||
|     DomainExtraction(String), | ||||
|     #[error("ParseFilteringChain: {0}")] | ||||
|     ParseFilteringChain(String), | ||||
|     #[error("NetworkFilterExtractionError: {0}")] | ||||
|     NetworkFilterExtraction(String), | ||||
| } | ||||
							
								
								
									
										320
									
								
								virtweb_backend/src/libvirt_rest_structures/net.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										320
									
								
								virtweb_backend/src/libvirt_rest_structures/net.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,320 @@ | ||||
| use crate::libvirt_lib_structures::network::*; | ||||
| use crate::libvirt_lib_structures::XMLUuid; | ||||
| use crate::libvirt_rest_structures::LibVirtStructError::StructureExtraction; | ||||
| use crate::nat::nat_definition::Nat; | ||||
| use crate::nat::nat_lib; | ||||
| use crate::utils::net_utils; | ||||
| use crate::utils::net_utils::{extract_ipv4, extract_ipv6}; | ||||
| use ipnetwork::{Ipv4Network, Ipv6Network}; | ||||
| use lazy_regex::regex; | ||||
| use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Copy, Clone, Debug)] | ||||
| pub enum NetworkForwardMode { | ||||
|     NAT, | ||||
|     Isolated, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] | ||||
| pub struct DHCPv4HostReservation { | ||||
|     mac: String, | ||||
|     name: String, | ||||
|     ip: Ipv4Addr, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] | ||||
| pub struct IPv4DHCPConfig { | ||||
|     pub start: Ipv4Addr, | ||||
|     pub end: Ipv4Addr, | ||||
|     pub hosts: Vec<DHCPv4HostReservation>, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] | ||||
| pub struct IPV4Config { | ||||
|     pub bridge_address: Ipv4Addr, | ||||
|     pub prefix: u8, | ||||
|     pub dhcp: Option<IPv4DHCPConfig>, | ||||
|     pub nat: Option<Vec<Nat<Ipv4Addr>>>, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] | ||||
| pub struct DHCPv6HostReservation { | ||||
|     pub name: String, | ||||
|     pub ip: Ipv6Addr, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] | ||||
| pub struct IPv6DHCPConfig { | ||||
|     pub start: Ipv6Addr, | ||||
|     pub end: Ipv6Addr, | ||||
|     pub hosts: Vec<DHCPv6HostReservation>, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] | ||||
| pub struct IPV6Config { | ||||
|     pub bridge_address: Ipv6Addr, | ||||
|     pub prefix: u8, | ||||
|     pub dhcp: Option<IPv6DHCPConfig>, | ||||
|     pub nat: Option<Vec<Nat<Ipv6Addr>>>, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] | ||||
| pub struct NetworkName(pub String); | ||||
|  | ||||
| impl NetworkName { | ||||
|     /// Get the name of the file that will store the NAT configuration of this network | ||||
|     pub fn nat_file_name(&self) -> String { | ||||
|         format!("nat-{}.json", self.0) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl NetworkName { | ||||
|     pub fn is_valid(&self) -> bool { | ||||
|         regex!("^[a-zA-Z0-9]+$").is_match(&self.0) | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Network configuration | ||||
| #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] | ||||
| pub struct NetworkInfo { | ||||
|     pub name: NetworkName, | ||||
|     pub uuid: Option<XMLUuid>, | ||||
|     pub title: Option<String>, | ||||
|     pub description: Option<String>, | ||||
|     pub forward_mode: NetworkForwardMode, | ||||
|     pub device: Option<String>, | ||||
|     pub bridge_name: Option<String>, | ||||
|     pub dns_server: Option<Ipv4Addr>, | ||||
|     pub domain: Option<String>, | ||||
|     pub ip_v4: Option<IPV4Config>, | ||||
|     pub ip_v6: Option<IPV6Config>, | ||||
| } | ||||
|  | ||||
| impl NetworkInfo { | ||||
|     pub fn as_virt_network(&self) -> anyhow::Result<NetworkXML> { | ||||
|         if !self.name.is_valid() { | ||||
|             return Err(StructureExtraction("network name is invalid!").into()); | ||||
|         } | ||||
|  | ||||
|         if let Some(n) = &self.title { | ||||
|             if n.contains('\n') { | ||||
|                 return Err(StructureExtraction("Network title contain newline char!").into()); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if let Some(dev) = &self.device { | ||||
|             if !regex!("^[a-zA-Z0-9]+$").is_match(dev) { | ||||
|                 return Err(StructureExtraction("Network device name is invalid!").into()); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if let Some(bridge) = &self.bridge_name { | ||||
|             if !regex!("^[a-zA-Z0-9]+$").is_match(bridge) { | ||||
|                 return Err(StructureExtraction("Network bridge name is invalid!").into()); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if let Some(domain) = &self.domain { | ||||
|             if !regex!("^[a-zA-Z0-9.]+$").is_match(domain) { | ||||
|                 return Err(StructureExtraction("Domain name is invalid!").into()); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         let mut ips = Vec::with_capacity(2); | ||||
|  | ||||
|         if let Some(ipv4) = &self.ip_v4 { | ||||
|             if !net_utils::is_ipv4_mask_valid(ipv4.prefix) { | ||||
|                 return Err(StructureExtraction("IPv4 prefix is invalid!").into()); | ||||
|             } | ||||
|  | ||||
|             if let Some(nat) = &ipv4.nat { | ||||
|                 for n in nat { | ||||
|                     n.check()?; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             ips.push(NetworkIPXML { | ||||
|                 family: "ipv4".to_string(), | ||||
|                 address: IpAddr::V4(ipv4.bridge_address), | ||||
|                 prefix: Some(ipv4.prefix), | ||||
|                 netmask: Some( | ||||
|                     Ipv4Network::new(ipv4.bridge_address, ipv4.prefix) | ||||
|                         .unwrap() | ||||
|                         .mask() | ||||
|                         .into(), | ||||
|                 ), | ||||
|                 dhcp: ipv4.dhcp.as_ref().map(|dhcp| NetworkDHCPXML { | ||||
|                     range: NetworkDHCPRangeXML { | ||||
|                         start: IpAddr::V4(dhcp.start), | ||||
|                         end: IpAddr::V4(dhcp.end), | ||||
|                     }, | ||||
|                     hosts: dhcp | ||||
|                         .hosts | ||||
|                         .iter() | ||||
|                         .map(|c| NetworkDHCPHostXML { | ||||
|                             mac: Some(c.mac.to_string()), | ||||
|                             name: c.name.to_string(), | ||||
|                             ip: c.ip.into(), | ||||
|                         }) | ||||
|                         .collect::<Vec<_>>(), | ||||
|                 }), | ||||
|             }) | ||||
|         } | ||||
|  | ||||
|         if let Some(ipv6) = &self.ip_v6 { | ||||
|             if !net_utils::is_ipv6_mask_valid(ipv6.prefix) { | ||||
|                 return Err(StructureExtraction("IPv6 prefix is invalid!").into()); | ||||
|             } | ||||
|  | ||||
|             if let Some(nat) = &ipv6.nat { | ||||
|                 for n in nat { | ||||
|                     n.check()?; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             ips.push(NetworkIPXML { | ||||
|                 family: "ipv6".to_string(), | ||||
|                 address: IpAddr::V6(ipv6.bridge_address), | ||||
|                 prefix: Some(ipv6.prefix), | ||||
|                 netmask: Some( | ||||
|                     Ipv6Network::new(ipv6.bridge_address, ipv6.prefix) | ||||
|                         .unwrap() | ||||
|                         .mask() | ||||
|                         .into(), | ||||
|                 ), | ||||
|                 dhcp: ipv6.dhcp.as_ref().map(|dhcp| NetworkDHCPXML { | ||||
|                     range: NetworkDHCPRangeXML { | ||||
|                         start: IpAddr::V6(dhcp.start), | ||||
|                         end: IpAddr::V6(dhcp.end), | ||||
|                     }, | ||||
|                     hosts: dhcp | ||||
|                         .hosts | ||||
|                         .iter() | ||||
|                         .map(|h| NetworkDHCPHostXML { | ||||
|                             mac: None, | ||||
|                             name: h.name.to_string(), | ||||
|                             ip: h.ip.into(), | ||||
|                         }) | ||||
|                         .collect(), | ||||
|                 }), | ||||
|             }) | ||||
|         } | ||||
|  | ||||
|         Ok(NetworkXML { | ||||
|             name: self.name.0.to_string(), | ||||
|             uuid: self.uuid, | ||||
|             title: self.title.clone(), | ||||
|             description: self.description.clone(), | ||||
|             forward: match self.forward_mode { | ||||
|                 NetworkForwardMode::NAT => Some(NetworkForwardXML { | ||||
|                     mode: "nat".to_string(), | ||||
|                     dev: self.device.clone().unwrap_or_default(), | ||||
|                 }), | ||||
|                 NetworkForwardMode::Isolated => None, | ||||
|             }, | ||||
|             bridge: self.bridge_name.clone().map(|b| NetworkBridgeXML { | ||||
|                 name: b.to_string(), | ||||
|             }), | ||||
|             dns: self.dns_server.map(|addr| NetworkDNSXML { | ||||
|                 forwarder: NetworkDNSForwarderXML { addr }, | ||||
|             }), | ||||
|             domain: self.domain.clone().map(|name| NetworkDomainXML { name }), | ||||
|             ips, | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     pub fn from_xml(xml: NetworkXML) -> anyhow::Result<Self> { | ||||
|         let name = NetworkName(xml.name); | ||||
|  | ||||
|         let nat = nat_lib::load_nat_def(&name)?; | ||||
|  | ||||
|         Ok(Self { | ||||
|             name, | ||||
|             uuid: xml.uuid, | ||||
|             title: xml.title, | ||||
|             description: xml.description, | ||||
|             forward_mode: match xml.forward { | ||||
|                 None => NetworkForwardMode::Isolated, | ||||
|                 Some(_) => NetworkForwardMode::NAT, | ||||
|             }, | ||||
|             device: xml | ||||
|                 .forward | ||||
|                 .map(|f| match f.dev.is_empty() { | ||||
|                     true => None, | ||||
|                     false => Some(f.dev), | ||||
|                 }) | ||||
|                 .unwrap_or(None), | ||||
|             bridge_name: xml.bridge.map(|b| b.name), | ||||
|             dns_server: xml.dns.map(|d| d.forwarder.addr), | ||||
|             domain: xml.domain.map(|d| d.name), | ||||
|             ip_v4: xml | ||||
|                 .ips | ||||
|                 .iter() | ||||
|                 .find(|i| i.family != "ipv6") | ||||
|                 .map(|i| IPV4Config { | ||||
|                     bridge_address: extract_ipv4(i.address), | ||||
|                     prefix: match i.prefix { | ||||
|                         None => ipnetwork::ipv4_mask_to_prefix(extract_ipv4(i.netmask.unwrap())) | ||||
|                             .expect("Failed to convert IPv4 netmask to network"), | ||||
|                         Some(p) => p, | ||||
|                     }, | ||||
|                     dhcp: i.dhcp.as_ref().map(|d| IPv4DHCPConfig { | ||||
|                         start: extract_ipv4(d.range.start), | ||||
|                         end: extract_ipv4(d.range.end), | ||||
|                         hosts: d | ||||
|                             .hosts | ||||
|                             .iter() | ||||
|                             .map(|h| DHCPv4HostReservation { | ||||
|                                 mac: h.mac.clone().unwrap_or_default(), | ||||
|                                 name: h.name.to_string(), | ||||
|                                 ip: extract_ipv4(h.ip), | ||||
|                             }) | ||||
|                             .collect(), | ||||
|                     }), | ||||
|                     nat: nat.ipv4, | ||||
|                 }), | ||||
|             ip_v6: xml | ||||
|                 .ips | ||||
|                 .iter() | ||||
|                 .find(|i| i.family == "ipv6") | ||||
|                 .map(|i| IPV6Config { | ||||
|                     bridge_address: extract_ipv6(i.address), | ||||
|                     prefix: match i.prefix { | ||||
|                         None => ipnetwork::ipv6_mask_to_prefix(extract_ipv6(i.netmask.unwrap())) | ||||
|                             .expect("Failed to convert IPv6 netmask to network"), | ||||
|                         Some(p) => p, | ||||
|                     }, | ||||
|                     dhcp: i.dhcp.as_ref().map(|d| IPv6DHCPConfig { | ||||
|                         start: extract_ipv6(d.range.start), | ||||
|                         end: extract_ipv6(d.range.end), | ||||
|                         hosts: d | ||||
|                             .hosts | ||||
|                             .iter() | ||||
|                             .map(|h| DHCPv6HostReservation { | ||||
|                                 name: h.name.to_string(), | ||||
|                                 ip: extract_ipv6(h.ip), | ||||
|                             }) | ||||
|                             .collect(), | ||||
|                     }), | ||||
|                     nat: nat.ipv6, | ||||
|                 }), | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     /// Check if at least one NAT definition was specified on this interface | ||||
|     pub fn has_nat_def(&self) -> bool { | ||||
|         if let Some(ipv4) = &self.ip_v4 { | ||||
|             if ipv4.nat.is_some() { | ||||
|                 return true; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if let Some(ipv6) = &self.ip_v6 { | ||||
|             if ipv6.nat.is_some() { | ||||
|                 return true; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         false | ||||
|     } | ||||
| } | ||||
							
								
								
									
										925
									
								
								virtweb_backend/src/libvirt_rest_structures/nw_filter.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										925
									
								
								virtweb_backend/src/libvirt_rest_structures/nw_filter.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,925 @@ | ||||
| use crate::libvirt_lib_structures::nwfilter::{ | ||||
|     NetworkFilterRefXML, NetworkFilterRuleProtocolAllXML, NetworkFilterRuleProtocolArpXML, | ||||
|     NetworkFilterRuleProtocolIpvx, NetworkFilterRuleProtocolLayer4, NetworkFilterRuleProtocolMac, | ||||
|     NetworkFilterRuleXML, NetworkFilterXML, | ||||
| }; | ||||
| use crate::libvirt_lib_structures::XMLUuid; | ||||
| use crate::libvirt_rest_structures::LibVirtStructError; | ||||
| use crate::libvirt_rest_structures::LibVirtStructError::{ | ||||
|     NetworkFilterExtraction, StructureExtraction, | ||||
| }; | ||||
| use crate::utils::net_utils; | ||||
| use lazy_regex::regex; | ||||
| use std::net::{Ipv4Addr, Ipv6Addr}; | ||||
|  | ||||
| pub fn is_var_def(var: &str) -> bool { | ||||
|     lazy_regex::regex!("^\\$[a-zA-Z0-9_]+$").is_match(var) | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] | ||||
| pub struct NetworkFilterName(pub String); | ||||
|  | ||||
| impl NetworkFilterName { | ||||
|     pub fn is_valid(&self) -> bool { | ||||
|         regex!("^[a-zA-Z0-9-_]+$").is_match(&self.0) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] | ||||
| pub struct NetworkFilterMacAddressOrVar(pub String); | ||||
|  | ||||
| impl NetworkFilterMacAddressOrVar { | ||||
|     pub fn is_valid(&self) -> bool { | ||||
|         is_var_def(&self.0) || net_utils::is_mac_address_valid(&self.0) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<&String> for NetworkFilterMacAddressOrVar { | ||||
|     fn from(value: &String) -> Self { | ||||
|         Self(value.to_string()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn extract_mac_address_or_var( | ||||
|     n: &Option<NetworkFilterMacAddressOrVar>, | ||||
| ) -> anyhow::Result<Option<String>> { | ||||
|     if let Some(mac) = n { | ||||
|         if !mac.is_valid() { | ||||
|             return Err(NetworkFilterExtraction(format!( | ||||
|                 "Invalid mac address or variable! {}", | ||||
|                 mac.0 | ||||
|             )) | ||||
|             .into()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     Ok(n.as_ref().map(|n| n.0.to_string())) | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] | ||||
| pub struct NetworkFilterIPOrVar<const V: usize>(pub String); | ||||
| pub type NetworkFilterIPv4OrVar = NetworkFilterIPOrVar<4>; | ||||
|  | ||||
| impl<const V: usize> NetworkFilterIPOrVar<V> { | ||||
|     pub fn is_valid(&self) -> bool { | ||||
|         is_var_def(&self.0) | ||||
|             || match V { | ||||
|                 4 => net_utils::is_ipv4_address_valid(&self.0), | ||||
|                 6 => net_utils::is_ipv6_address_valid(&self.0), | ||||
|                 _ => panic!("Unsupported IP version!"), | ||||
|             } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl<const V: usize> From<&String> for NetworkFilterIPOrVar<V> { | ||||
|     fn from(value: &String) -> Self { | ||||
|         if V != 4 && V != 6 { | ||||
|             panic!("Unsupported IP version!"); | ||||
|         } | ||||
|         Self(value.to_string()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn extract_ip_or_var<const V: usize>( | ||||
|     n: &Option<NetworkFilterIPOrVar<V>>, | ||||
| ) -> anyhow::Result<Option<String>> { | ||||
|     if let Some(ip) = n { | ||||
|         if !ip.is_valid() { | ||||
|             return Err(NetworkFilterExtraction(format!( | ||||
|                 "Invalid IPv{V} address or variable! {}", | ||||
|                 ip.0 | ||||
|             )) | ||||
|             .into()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     Ok(n.as_ref().map(|n| n.0.to_string())) | ||||
| } | ||||
|  | ||||
| fn extract_ip_mask<const V: usize>(n: Option<u8>) -> anyhow::Result<Option<u8>> { | ||||
|     if let Some(mask) = n { | ||||
|         if !net_utils::is_mask_valid(V, mask) { | ||||
|             return Err(NetworkFilterExtraction(format!("Invalid IPv{V} mask! {mask}")).into()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     Ok(n) | ||||
| } | ||||
|  | ||||
| fn extract_nw_filter_comment(n: &Option<String>) -> anyhow::Result<Option<String>> { | ||||
|     if let Some(comment) = n { | ||||
|         if comment.len() > 256 || comment.contains('\"') || comment.contains('\n') { | ||||
|             return Err(NetworkFilterExtraction(format!("Invalid comment! {}", comment)).into()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     Ok(n.clone()) | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug, Copy, Clone)] | ||||
| #[serde(rename_all = "lowercase")] | ||||
| pub enum NetworkFilterChainProtocol { | ||||
|     Root, | ||||
|     Mac, | ||||
|     STP, | ||||
|     VLAN, | ||||
|     ARP, | ||||
|     RARP, | ||||
|     IPv4, | ||||
|     IPv6, | ||||
| } | ||||
|  | ||||
| impl NetworkFilterChainProtocol { | ||||
|     pub fn from_xml(xml: &str) -> anyhow::Result<Self> { | ||||
|         Ok(match xml { | ||||
|             "root" => Self::Root, | ||||
|             "mac" => Self::Mac, | ||||
|             "stp" => Self::STP, | ||||
|             "vlan" => Self::VLAN, | ||||
|             "arp" => Self::ARP, | ||||
|             "rarp" => Self::RARP, | ||||
|             "ipv4" => Self::IPv4, | ||||
|             "ipv6" => Self::IPv6, | ||||
|             _ => { | ||||
|                 return Err(LibVirtStructError::ParseFilteringChain(format!( | ||||
|                     "Unknown filtering chain: {xml}! " | ||||
|                 )) | ||||
|                 .into()); | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     pub fn to_xml(&self) -> String { | ||||
|         match self { | ||||
|             Self::Root => "root", | ||||
|             Self::Mac => "mac", | ||||
|             Self::STP => "stp", | ||||
|             Self::VLAN => "vlan", | ||||
|             Self::ARP => "arp", | ||||
|             Self::RARP => "rarp", | ||||
|             Self::IPv4 => "ipv4", | ||||
|             Self::IPv6 => "ipv6", | ||||
|         } | ||||
|         .to_string() | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] | ||||
| pub struct NetworkFilterChain { | ||||
|     protocol: NetworkFilterChainProtocol, | ||||
|     suffix: Option<String>, | ||||
| } | ||||
|  | ||||
| impl NetworkFilterChain { | ||||
|     pub fn from_xml(xml: &str) -> anyhow::Result<Self> { | ||||
|         Ok(match xml.split_once('-') { | ||||
|             None => Self { | ||||
|                 protocol: NetworkFilterChainProtocol::from_xml(xml)?, | ||||
|                 suffix: None, | ||||
|             }, | ||||
|             Some((prefix, suffix)) => Self { | ||||
|                 protocol: NetworkFilterChainProtocol::from_xml(prefix)?, | ||||
|                 suffix: Some(suffix.to_string()), | ||||
|             }, | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     pub fn to_xml(&self) -> String { | ||||
|         match &self.suffix { | ||||
|             None => self.protocol.to_xml(), | ||||
|             Some(s) => format!("{}-{s}", self.protocol.to_xml()), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug, Copy, Clone)] | ||||
| #[serde(rename_all = "lowercase")] | ||||
| pub enum NetworkFilterAction { | ||||
|     /// matching the rule silently discards the packet with no further analysis | ||||
|     Drop, | ||||
|     /// matching the rule generates an ICMP reject message with no further analysis | ||||
|     Reject, | ||||
|     /// matching the rule accepts the packet with no further analysis | ||||
|     Accept, | ||||
|     /// matching the rule passes this filter, but returns control to the calling filter for further | ||||
|     /// analysis | ||||
|     Return, | ||||
|     /// matching the rule goes on to the next rule for further analysis | ||||
|     Continue, | ||||
| } | ||||
|  | ||||
| impl NetworkFilterAction { | ||||
|     pub fn from_xml(xml: &str) -> anyhow::Result<Self> { | ||||
|         Ok(match xml { | ||||
|             "drop" => Self::Drop, | ||||
|             "reject" => Self::Reject, | ||||
|             "accept" => Self::Accept, | ||||
|             "return" => Self::Return, | ||||
|             "continue" => Self::Continue, | ||||
|             s => { | ||||
|                 return Err(LibVirtStructError::ParseFilteringChain(format!( | ||||
|                     "Unkown filter action {s}!" | ||||
|                 )) | ||||
|                 .into()); | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     pub fn to_xml(&self) -> String { | ||||
|         match self { | ||||
|             Self::Drop => "drop", | ||||
|             Self::Reject => "reject", | ||||
|             Self::Accept => "accept", | ||||
|             Self::Return => "return", | ||||
|             Self::Continue => "continue", | ||||
|         } | ||||
|         .to_string() | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] | ||||
| #[serde(rename_all = "lowercase")] | ||||
| pub enum NetworkFilterDirection { | ||||
|     In, | ||||
|     Out, | ||||
|     InOut, | ||||
| } | ||||
|  | ||||
| impl NetworkFilterDirection { | ||||
|     pub fn from_xml(xml: &str) -> anyhow::Result<Self> { | ||||
|         Ok(match xml { | ||||
|             "in" => Self::In, | ||||
|             "out" => Self::Out, | ||||
|             "inout" => Self::InOut, | ||||
|             s => { | ||||
|                 return Err(LibVirtStructError::ParseFilteringChain(format!( | ||||
|                     "Unkown filter direction {s}!" | ||||
|                 )) | ||||
|                 .into()); | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     pub fn to_xml(&self) -> String { | ||||
|         match self { | ||||
|             NetworkFilterDirection::In => "in", | ||||
|             NetworkFilterDirection::Out => "out", | ||||
|             NetworkFilterDirection::InOut => "inout", | ||||
|         } | ||||
|         .to_string() | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug, Copy, Clone)] | ||||
| pub enum Layer4State { | ||||
|     NEW, | ||||
|     ESTABLISHED, | ||||
|     RELATED, | ||||
|     INVALID, | ||||
|     NONE, | ||||
| } | ||||
|  | ||||
| impl Layer4State { | ||||
|     pub fn from_xml(xml: &str) -> anyhow::Result<Self> { | ||||
|         Ok(match xml { | ||||
|             "NEW" => Self::NEW, | ||||
|             "ESTABLISHED" => Self::ESTABLISHED, | ||||
|             "RELATED" => Self::RELATED, | ||||
|             "INVALID" => Self::INVALID, | ||||
|             "NONE" => Self::NONE, | ||||
|             s => { | ||||
|                 return Err(LibVirtStructError::ParseFilteringChain(format!( | ||||
|                     "Unkown layer4 state '{s}'!" | ||||
|                 )) | ||||
|                 .into()); | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     pub fn to_xml(&self) -> String { | ||||
|         match self { | ||||
|             Self::NEW => "NEW", | ||||
|             Self::ESTABLISHED => "ESTABLISHED", | ||||
|             Self::RELATED => "RELATED", | ||||
|             Self::INVALID => "INVALID", | ||||
|             Self::NONE => "NONE", | ||||
|         } | ||||
|         .to_string() | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] | ||||
| pub struct NetworkSelectorMac { | ||||
|     src_mac_addr: Option<NetworkFilterMacAddressOrVar>, | ||||
|     src_mac_mask: Option<NetworkFilterMacAddressOrVar>, | ||||
|     dst_mac_addr: Option<NetworkFilterMacAddressOrVar>, | ||||
|     dst_mac_mask: Option<NetworkFilterMacAddressOrVar>, | ||||
|     comment: Option<String>, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] | ||||
| pub struct NetworkSelectorARP { | ||||
|     srcmacaddr: Option<NetworkFilterMacAddressOrVar>, | ||||
|     srcmacmask: Option<NetworkFilterMacAddressOrVar>, | ||||
|     dstmacaddr: Option<NetworkFilterMacAddressOrVar>, | ||||
|     dstmacmask: Option<NetworkFilterMacAddressOrVar>, | ||||
|     arpsrcipaddr: Option<NetworkFilterIPv4OrVar>, | ||||
|     arpsrcipmask: Option<u8>, | ||||
|     arpdstipaddr: Option<NetworkFilterIPv4OrVar>, | ||||
|     arpdstipmask: Option<u8>, | ||||
|     comment: Option<String>, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] | ||||
| pub struct NetworkFilterSelectorIP<const V: usize> { | ||||
|     srcmacaddr: Option<NetworkFilterMacAddressOrVar>, | ||||
|     srcmacmask: Option<NetworkFilterMacAddressOrVar>, | ||||
|     dstmacaddr: Option<NetworkFilterMacAddressOrVar>, | ||||
|     dstmacmask: Option<NetworkFilterMacAddressOrVar>, | ||||
|     srcipaddr: Option<NetworkFilterIPOrVar<V>>, | ||||
|     srcipmask: Option<u8>, | ||||
|     dstipaddr: Option<NetworkFilterIPOrVar<V>>, | ||||
|     dstipmask: Option<u8>, | ||||
|     comment: Option<String>, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] | ||||
| pub struct NetworkFilterSelectorLayer4<IPv> { | ||||
|     srcmacaddr: Option<NetworkFilterMacAddressOrVar>, | ||||
|     srcipaddr: Option<IPv>, | ||||
|     srcipmask: Option<u8>, | ||||
|     dstipaddr: Option<IPv>, | ||||
|     dstipmask: Option<u8>, | ||||
|     /// Start of range of source IP address | ||||
|     srcipfrom: Option<IPv>, | ||||
|     /// End of range of source IP address | ||||
|     srcipto: Option<IPv>, | ||||
|     /// Start of range of destination IP address | ||||
|     dstipfrom: Option<IPv>, | ||||
|     /// End of range of destination IP address | ||||
|     dstipto: Option<IPv>, | ||||
|     srcportstart: Option<u16>, | ||||
|     srcportend: Option<u16>, | ||||
|     dstportstart: Option<u16>, | ||||
|     dstportend: Option<u16>, | ||||
|     state: Option<Layer4State>, | ||||
|     comment: Option<String>, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] | ||||
| pub struct NetworkSelectorAll<IPv> { | ||||
|     comment: Option<String>, | ||||
|     srcmacaddr: Option<NetworkFilterMacAddressOrVar>, | ||||
|     srcipaddr: Option<IPv>, | ||||
|     srcipmask: Option<u8>, | ||||
|     dstipaddr: Option<IPv>, | ||||
|     dstipmask: Option<u8>, | ||||
|     /// Start of range of source IP address | ||||
|     srcipfrom: Option<IPv>, | ||||
|     /// End of range of source IP address | ||||
|     srcipto: Option<IPv>, | ||||
|     /// Start of range of destination IP address | ||||
|     dstipfrom: Option<IPv>, | ||||
|     /// End of range of destination IP address | ||||
|     dstipto: Option<IPv>, | ||||
|     state: Option<Layer4State>, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] | ||||
| #[serde(tag = "type", rename_all = "lowercase")] | ||||
| pub enum NetworkFilterSelector { | ||||
|     Mac(NetworkSelectorMac), | ||||
|     Arp(NetworkSelectorARP), | ||||
|     Rarp(NetworkSelectorARP), | ||||
|     IPv4(NetworkFilterSelectorIP<4>), | ||||
|     IPv6(NetworkFilterSelectorIP<6>), | ||||
|     TCP(NetworkFilterSelectorLayer4<Ipv4Addr>), | ||||
|     UDP(NetworkFilterSelectorLayer4<Ipv4Addr>), | ||||
|     SCTP(NetworkFilterSelectorLayer4<Ipv4Addr>), | ||||
|     ICMP(NetworkFilterSelectorLayer4<Ipv4Addr>), | ||||
|     All(NetworkSelectorAll<Ipv4Addr>), | ||||
|     TCPipv6(NetworkFilterSelectorLayer4<Ipv6Addr>), | ||||
|     UDPipv6(NetworkFilterSelectorLayer4<Ipv6Addr>), | ||||
|     SCTPipv6(NetworkFilterSelectorLayer4<Ipv6Addr>), | ||||
|     ICMPipv6(NetworkFilterSelectorLayer4<Ipv6Addr>), | ||||
|     Allipv6(NetworkSelectorAll<Ipv6Addr>), | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] | ||||
| pub struct NetworkFilterRule { | ||||
|     action: NetworkFilterAction, | ||||
|     direction: NetworkFilterDirection, | ||||
|     /// optional; the priority of the rule controls the order in which the rule will be instantiated | ||||
|     /// relative to other rules | ||||
|     /// | ||||
|     /// Valid values are in the range of -1000 to 1000. | ||||
|     priority: Option<i32>, | ||||
|     selectors: Vec<NetworkFilterSelector>, | ||||
| } | ||||
|  | ||||
| /// Network filter definition | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] | ||||
| pub struct NetworkFilter { | ||||
|     pub name: NetworkFilterName, | ||||
|     chain: Option<NetworkFilterChain>, | ||||
|     priority: Option<i32>, | ||||
|     pub uuid: Option<XMLUuid>, | ||||
|     /// Referenced filters rules | ||||
|     join_filters: Vec<NetworkFilterName>, | ||||
|     rules: Vec<NetworkFilterRule>, | ||||
| } | ||||
|  | ||||
| impl NetworkFilter { | ||||
|     fn lib2rest_process_mac_rule(n: &NetworkFilterRuleProtocolMac) -> NetworkFilterSelector { | ||||
|         NetworkFilterSelector::Mac(NetworkSelectorMac { | ||||
|             src_mac_addr: n.srcmacaddr.as_ref().map(|v| v.into()), | ||||
|             src_mac_mask: n.srcmacmask.as_ref().map(|v| v.into()), | ||||
|             dst_mac_addr: n.dstmacaddr.as_ref().map(|v| v.into()), | ||||
|             dst_mac_mask: n.dstmacmask.as_ref().map(|v| v.into()), | ||||
|             comment: n.comment.clone(), | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     fn lib2rest_process_arp_rule(n: &NetworkFilterRuleProtocolArpXML) -> NetworkSelectorARP { | ||||
|         NetworkSelectorARP { | ||||
|             srcmacaddr: n.srcmacaddr.as_ref().map(|v| v.into()), | ||||
|             srcmacmask: n.srcmacmask.as_ref().map(|v| v.into()), | ||||
|             dstmacaddr: n.dstmacaddr.as_ref().map(|v| v.into()), | ||||
|             dstmacmask: n.dstmacmask.as_ref().map(|v| v.into()), | ||||
|             arpsrcipaddr: n.arpsrcipaddr.as_ref().map(|v| v.into()), | ||||
|             arpsrcipmask: n.arpsrcipmask, | ||||
|             arpdstipaddr: n.arpdstipaddr.as_ref().map(|v| v.into()), | ||||
|             arpdstipmask: n.arpdstipmask, | ||||
|             comment: n.comment.clone(), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn lib2rest_process_ip_rule<const V: usize>( | ||||
|         n: &NetworkFilterRuleProtocolIpvx, | ||||
|     ) -> NetworkFilterSelectorIP<V> { | ||||
|         NetworkFilterSelectorIP { | ||||
|             srcmacaddr: n.srcmacaddr.as_ref().map(|v| v.into()), | ||||
|             srcmacmask: n.srcmacmask.as_ref().map(|v| v.into()), | ||||
|             dstmacaddr: n.dstmacaddr.as_ref().map(|v| v.into()), | ||||
|             dstmacmask: n.dstmacmask.as_ref().map(|v| v.into()), | ||||
|             srcipaddr: n.srcipaddr.as_ref().map(|v| v.into()), | ||||
|             srcipmask: n.srcipmask, | ||||
|             dstipaddr: n.dstipaddr.as_ref().map(|v| v.into()), | ||||
|             dstipmask: n.dstipmask, | ||||
|             comment: n.comment.clone(), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn lib2rest_process_layer4_rule<IPv: Copy>( | ||||
|         n: &NetworkFilterRuleProtocolLayer4<IPv>, | ||||
|     ) -> anyhow::Result<NetworkFilterSelectorLayer4<IPv>> { | ||||
|         Ok(NetworkFilterSelectorLayer4 { | ||||
|             srcmacaddr: n.srcmacaddr.as_ref().map(|v| v.into()), | ||||
|             srcipaddr: n.srcipaddr, | ||||
|             srcipmask: n.srcipmask, | ||||
|             dstipaddr: n.dstipaddr, | ||||
|             dstipmask: n.dstipmask, | ||||
|             srcipfrom: n.srcipfrom, | ||||
|             srcipto: n.srcipto, | ||||
|             dstipfrom: n.dstipfrom, | ||||
|             dstipto: n.dstipto, | ||||
|             srcportstart: n.srcportstart, | ||||
|             srcportend: n.srcportend, | ||||
|             dstportstart: n.dstportstart, | ||||
|             dstportend: n.dstportend, | ||||
|             state: n.state.as_deref().map(Layer4State::from_xml).transpose()?, | ||||
|             comment: n.comment.clone(), | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     fn lib2rest_process_all_rule<IPv: Copy>( | ||||
|         n: &NetworkFilterRuleProtocolAllXML<IPv>, | ||||
|     ) -> anyhow::Result<NetworkSelectorAll<IPv>> { | ||||
|         Ok(NetworkSelectorAll { | ||||
|             srcmacaddr: n.srcmacaddr.as_ref().map(|v| v.into()), | ||||
|             srcipaddr: n.srcipaddr, | ||||
|             srcipmask: n.srcipmask, | ||||
|             dstipaddr: n.dstipaddr, | ||||
|             dstipmask: n.dstipmask, | ||||
|             srcipfrom: n.srcipfrom, | ||||
|             srcipto: n.srcipto, | ||||
|             dstipfrom: n.dstipfrom, | ||||
|             dstipto: n.dstipto, | ||||
|             state: n.state.as_deref().map(Layer4State::from_xml).transpose()?, | ||||
|             comment: n.comment.clone(), | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     pub fn lib2rest(xml: NetworkFilterXML) -> anyhow::Result<Self> { | ||||
|         let mut rules = Vec::with_capacity(xml.rules.len()); | ||||
|         for rule in &xml.rules { | ||||
|             let mut selectors = Vec::new(); | ||||
|  | ||||
|             // Mac selectors | ||||
|             selectors.append( | ||||
|                 &mut rule | ||||
|                     .mac_selectors | ||||
|                     .iter() | ||||
|                     .map(Self::lib2rest_process_mac_rule) | ||||
|                     .collect(), | ||||
|             ); | ||||
|  | ||||
|             // ARP - RARP selectors | ||||
|             selectors.append( | ||||
|                 &mut rule | ||||
|                     .arp_selectors | ||||
|                     .iter() | ||||
|                     .map(|r| NetworkFilterSelector::Arp(Self::lib2rest_process_arp_rule(r))) | ||||
|                     .collect(), | ||||
|             ); | ||||
|             selectors.append( | ||||
|                 &mut rule | ||||
|                     .rarp_selectors | ||||
|                     .iter() | ||||
|                     .map(|r| NetworkFilterSelector::Rarp(Self::lib2rest_process_arp_rule(r))) | ||||
|                     .collect(), | ||||
|             ); | ||||
|  | ||||
|             // IPv4 - IPv6 selectors | ||||
|             selectors.append( | ||||
|                 &mut rule | ||||
|                     .ipv4_selectors | ||||
|                     .iter() | ||||
|                     .map(|r| NetworkFilterSelector::IPv4(Self::lib2rest_process_ip_rule(r))) | ||||
|                     .collect(), | ||||
|             ); | ||||
|             selectors.append( | ||||
|                 &mut rule | ||||
|                     .ipv6_selectors | ||||
|                     .iter() | ||||
|                     .map(|r| NetworkFilterSelector::IPv6(Self::lib2rest_process_ip_rule(r))) | ||||
|                     .collect(), | ||||
|             ); | ||||
|  | ||||
|             // Layer 4 protocols selectors | ||||
|             selectors.append( | ||||
|                 &mut rule | ||||
|                     .tcp_selectors | ||||
|                     .iter() | ||||
|                     .map(|r| { | ||||
|                         Ok(NetworkFilterSelector::TCP( | ||||
|                             Self::lib2rest_process_layer4_rule(r)?, | ||||
|                         )) | ||||
|                     }) | ||||
|                     .collect::<Result<Vec<_>, anyhow::Error>>()?, | ||||
|             ); | ||||
|             selectors.append( | ||||
|                 &mut rule | ||||
|                     .udp_selectors | ||||
|                     .iter() | ||||
|                     .map(|r| { | ||||
|                         Ok(NetworkFilterSelector::UDP( | ||||
|                             Self::lib2rest_process_layer4_rule(r)?, | ||||
|                         )) | ||||
|                     }) | ||||
|                     .collect::<Result<Vec<_>, anyhow::Error>>()?, | ||||
|             ); | ||||
|             selectors.append( | ||||
|                 &mut rule | ||||
|                     .sctp_selectors | ||||
|                     .iter() | ||||
|                     .map(|r| { | ||||
|                         Ok(NetworkFilterSelector::SCTP( | ||||
|                             Self::lib2rest_process_layer4_rule(r)?, | ||||
|                         )) | ||||
|                     }) | ||||
|                     .collect::<Result<Vec<_>, anyhow::Error>>()?, | ||||
|             ); | ||||
|             selectors.append( | ||||
|                 &mut rule | ||||
|                     .icmp_selectors | ||||
|                     .iter() | ||||
|                     .map(|r| { | ||||
|                         Ok(NetworkFilterSelector::ICMP( | ||||
|                             Self::lib2rest_process_layer4_rule(r)?, | ||||
|                         )) | ||||
|                     }) | ||||
|                     .collect::<Result<Vec<_>, anyhow::Error>>()?, | ||||
|             ); | ||||
|  | ||||
|             selectors.append( | ||||
|                 &mut rule | ||||
|                     .tcp_ipv6_selectors | ||||
|                     .iter() | ||||
|                     .map(|r| { | ||||
|                         Ok(NetworkFilterSelector::TCPipv6( | ||||
|                             Self::lib2rest_process_layer4_rule(r)?, | ||||
|                         )) | ||||
|                     }) | ||||
|                     .collect::<Result<Vec<_>, anyhow::Error>>()?, | ||||
|             ); | ||||
|             selectors.append( | ||||
|                 &mut rule | ||||
|                     .udp_ipv6_selectors | ||||
|                     .iter() | ||||
|                     .map(|r| { | ||||
|                         Ok(NetworkFilterSelector::UDPipv6( | ||||
|                             Self::lib2rest_process_layer4_rule(r)?, | ||||
|                         )) | ||||
|                     }) | ||||
|                     .collect::<Result<Vec<_>, anyhow::Error>>()?, | ||||
|             ); | ||||
|             selectors.append( | ||||
|                 &mut rule | ||||
|                     .sctp_ipv6_selectors | ||||
|                     .iter() | ||||
|                     .map(|r| { | ||||
|                         Ok(NetworkFilterSelector::SCTPipv6( | ||||
|                             Self::lib2rest_process_layer4_rule(r)?, | ||||
|                         )) | ||||
|                     }) | ||||
|                     .collect::<Result<Vec<_>, anyhow::Error>>()?, | ||||
|             ); | ||||
|             selectors.append( | ||||
|                 &mut rule | ||||
|                     .imcp_ipv6_selectors | ||||
|                     .iter() | ||||
|                     .map(|r| { | ||||
|                         Ok(NetworkFilterSelector::ICMPipv6( | ||||
|                             Self::lib2rest_process_layer4_rule(r)?, | ||||
|                         )) | ||||
|                     }) | ||||
|                     .collect::<Result<Vec<_>, anyhow::Error>>()?, | ||||
|             ); | ||||
|  | ||||
|             // All selectors | ||||
|             selectors.append( | ||||
|                 &mut rule | ||||
|                     .all_selectors | ||||
|                     .iter() | ||||
|                     .map(|r| { | ||||
|                         Ok(NetworkFilterSelector::All(Self::lib2rest_process_all_rule( | ||||
|                             r, | ||||
|                         )?)) | ||||
|                     }) | ||||
|                     .collect::<Result<Vec<_>, anyhow::Error>>()?, | ||||
|             ); | ||||
|  | ||||
|             selectors.append( | ||||
|                 &mut rule | ||||
|                     .all_ipv6_selectors | ||||
|                     .iter() | ||||
|                     .map(|r| { | ||||
|                         Ok(NetworkFilterSelector::Allipv6( | ||||
|                             Self::lib2rest_process_all_rule(r)?, | ||||
|                         )) | ||||
|                     }) | ||||
|                     .collect::<Result<Vec<_>, anyhow::Error>>()?, | ||||
|             ); | ||||
|  | ||||
|             rules.push(NetworkFilterRule { | ||||
|                 action: NetworkFilterAction::from_xml(&rule.action)?, | ||||
|                 direction: NetworkFilterDirection::from_xml(&rule.direction)?, | ||||
|                 priority: rule.priority, | ||||
|                 selectors, | ||||
|             }) | ||||
|         } | ||||
|  | ||||
|         Ok(Self { | ||||
|             name: NetworkFilterName(xml.name), | ||||
|             uuid: xml.uuid, | ||||
|             chain: xml | ||||
|                 .chain | ||||
|                 .as_deref() | ||||
|                 .map(NetworkFilterChain::from_xml) | ||||
|                 .transpose()?, | ||||
|             priority: xml.priority, | ||||
|             join_filters: xml | ||||
|                 .filterrefs | ||||
|                 .iter() | ||||
|                 .map(|i| NetworkFilterName(i.filter.to_string())) | ||||
|                 .collect(), | ||||
|             rules, | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     fn rest2lib_process_arp_selector( | ||||
|         selector: &NetworkSelectorARP, | ||||
|     ) -> anyhow::Result<NetworkFilterRuleProtocolArpXML> { | ||||
|         Ok(NetworkFilterRuleProtocolArpXML { | ||||
|             srcmacaddr: extract_mac_address_or_var(&selector.srcmacaddr)?, | ||||
|             srcmacmask: extract_mac_address_or_var(&selector.srcmacmask)?, | ||||
|             dstmacaddr: extract_mac_address_or_var(&selector.dstmacaddr)?, | ||||
|             dstmacmask: extract_mac_address_or_var(&selector.dstmacmask)?, | ||||
|             arpsrcipaddr: extract_ip_or_var(&selector.arpsrcipaddr)?, | ||||
|             arpsrcipmask: selector.arpsrcipmask, | ||||
|             arpdstipaddr: extract_ip_or_var(&selector.arpdstipaddr)?, | ||||
|             arpdstipmask: selector.arpdstipmask, | ||||
|             comment: extract_nw_filter_comment(&selector.comment)?, | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     fn rest2lib_process_ip_selector<const V: usize>( | ||||
|         selector: &NetworkFilterSelectorIP<V>, | ||||
|     ) -> anyhow::Result<NetworkFilterRuleProtocolIpvx> { | ||||
|         Ok(NetworkFilterRuleProtocolIpvx { | ||||
|             srcmacaddr: extract_mac_address_or_var(&selector.srcmacaddr)?, | ||||
|             srcmacmask: extract_mac_address_or_var(&selector.srcmacmask)?, | ||||
|             dstmacaddr: extract_mac_address_or_var(&selector.dstmacaddr)?, | ||||
|             dstmacmask: extract_mac_address_or_var(&selector.dstmacmask)?, | ||||
|             srcipaddr: extract_ip_or_var(&selector.srcipaddr)?, | ||||
|             srcipmask: extract_ip_mask::<V>(selector.srcipmask)?, | ||||
|             dstipaddr: extract_ip_or_var(&selector.dstipaddr)?, | ||||
|             dstipmask: extract_ip_mask::<V>(selector.dstipmask)?, | ||||
|             comment: extract_nw_filter_comment(&selector.comment)?, | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     fn rest2lib_process_layer4_selector<IPv: Copy>( | ||||
|         selector: &NetworkFilterSelectorLayer4<IPv>, | ||||
|     ) -> anyhow::Result<NetworkFilterRuleProtocolLayer4<IPv>> { | ||||
|         Ok(NetworkFilterRuleProtocolLayer4 { | ||||
|             srcmacaddr: extract_mac_address_or_var(&selector.srcmacaddr)?, | ||||
|             srcipaddr: selector.srcipaddr, | ||||
|             // This IP mask is not checked | ||||
|             srcipmask: selector.srcipmask, | ||||
|             dstipaddr: selector.dstipaddr, | ||||
|             // This IP mask is not checked | ||||
|             dstipmask: selector.dstipmask, | ||||
|             srcipfrom: selector.srcipfrom, | ||||
|             srcipto: selector.srcipto, | ||||
|             dstipfrom: selector.dstipfrom, | ||||
|             dstipto: selector.dstipto, | ||||
|             srcportstart: selector.srcportstart, | ||||
|             srcportend: selector.srcportend, | ||||
|             dstportstart: selector.dstportstart, | ||||
|             dstportend: selector.dstportend, | ||||
|             state: selector.state.map(|s| s.to_xml()), | ||||
|             comment: extract_nw_filter_comment(&selector.comment)?, | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     fn rest2lib_process_all_selector<IPv: Copy>( | ||||
|         selector: &NetworkSelectorAll<IPv>, | ||||
|     ) -> anyhow::Result<NetworkFilterRuleProtocolAllXML<IPv>> { | ||||
|         Ok(NetworkFilterRuleProtocolAllXML { | ||||
|             srcmacaddr: extract_mac_address_or_var(&selector.srcmacaddr)?, | ||||
|             srcipaddr: selector.srcipaddr, | ||||
|             // This IP mask is not checked | ||||
|             srcipmask: selector.srcipmask, | ||||
|             dstipaddr: selector.dstipaddr, | ||||
|             // This IP mask is not checked | ||||
|             dstipmask: selector.dstipmask, | ||||
|             srcipfrom: selector.srcipfrom, | ||||
|             srcipto: selector.srcipto, | ||||
|             dstipfrom: selector.dstipfrom, | ||||
|             dstipto: selector.dstipto, | ||||
|             state: selector.state.map(|s| s.to_xml()), | ||||
|             comment: extract_nw_filter_comment(&selector.comment)?, | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     fn rest2lib_process_rule(rule: &NetworkFilterRule) -> anyhow::Result<NetworkFilterRuleXML> { | ||||
|         let mut rule_xml = NetworkFilterRuleXML { | ||||
|             action: rule.action.to_xml(), | ||||
|             direction: rule.direction.to_xml(), | ||||
|             priority: rule.priority, | ||||
|             ..Default::default() | ||||
|         }; | ||||
|  | ||||
|         for sel in &rule.selectors { | ||||
|             match sel { | ||||
|                 NetworkFilterSelector::Mac(mac) => { | ||||
|                     rule_xml.mac_selectors.push(NetworkFilterRuleProtocolMac { | ||||
|                         srcmacaddr: extract_mac_address_or_var(&mac.src_mac_addr)?, | ||||
|                         srcmacmask: extract_mac_address_or_var(&mac.src_mac_mask)?, | ||||
|                         dstmacaddr: extract_mac_address_or_var(&mac.dst_mac_addr)?, | ||||
|                         dstmacmask: extract_mac_address_or_var(&mac.dst_mac_mask)?, | ||||
|                         comment: extract_nw_filter_comment(&mac.comment)?, | ||||
|                     }) | ||||
|                 } | ||||
|  | ||||
|                 NetworkFilterSelector::Arp(a) => { | ||||
|                     rule_xml | ||||
|                         .arp_selectors | ||||
|                         .push(Self::rest2lib_process_arp_selector(a)?); | ||||
|                 } | ||||
|  | ||||
|                 NetworkFilterSelector::Rarp(a) => { | ||||
|                     rule_xml | ||||
|                         .rarp_selectors | ||||
|                         .push(Self::rest2lib_process_arp_selector(a)?); | ||||
|                 } | ||||
|  | ||||
|                 NetworkFilterSelector::IPv4(ip) => rule_xml | ||||
|                     .ipv4_selectors | ||||
|                     .push(Self::rest2lib_process_ip_selector(ip)?), | ||||
|                 NetworkFilterSelector::IPv6(ip) => rule_xml | ||||
|                     .ipv6_selectors | ||||
|                     .push(Self::rest2lib_process_ip_selector(ip)?), | ||||
|  | ||||
|                 NetworkFilterSelector::TCP(tcp) => rule_xml | ||||
|                     .tcp_selectors | ||||
|                     .push(Self::rest2lib_process_layer4_selector(tcp)?), | ||||
|  | ||||
|                 NetworkFilterSelector::UDP(udp) => rule_xml | ||||
|                     .udp_selectors | ||||
|                     .push(Self::rest2lib_process_layer4_selector(udp)?), | ||||
|  | ||||
|                 NetworkFilterSelector::SCTP(sctp) => rule_xml | ||||
|                     .sctp_selectors | ||||
|                     .push(Self::rest2lib_process_layer4_selector(sctp)?), | ||||
|  | ||||
|                 NetworkFilterSelector::ICMP(icmp) => rule_xml | ||||
|                     .icmp_selectors | ||||
|                     .push(Self::rest2lib_process_layer4_selector(icmp)?), | ||||
|  | ||||
|                 NetworkFilterSelector::All(all) => { | ||||
|                     rule_xml | ||||
|                         .all_selectors | ||||
|                         .push(Self::rest2lib_process_all_selector(all)?); | ||||
|                 } | ||||
|  | ||||
|                 NetworkFilterSelector::TCPipv6(tcpv6) => rule_xml | ||||
|                     .tcp_ipv6_selectors | ||||
|                     .push(Self::rest2lib_process_layer4_selector(tcpv6)?), | ||||
|  | ||||
|                 NetworkFilterSelector::UDPipv6(udpv6) => rule_xml | ||||
|                     .udp_ipv6_selectors | ||||
|                     .push(Self::rest2lib_process_layer4_selector(udpv6)?), | ||||
|  | ||||
|                 NetworkFilterSelector::SCTPipv6(sctpv6) => rule_xml | ||||
|                     .sctp_ipv6_selectors | ||||
|                     .push(Self::rest2lib_process_layer4_selector(sctpv6)?), | ||||
|  | ||||
|                 NetworkFilterSelector::ICMPipv6(icmpv6) => rule_xml | ||||
|                     .imcp_ipv6_selectors | ||||
|                     .push(Self::rest2lib_process_layer4_selector(icmpv6)?), | ||||
|  | ||||
|                 NetworkFilterSelector::Allipv6(all) => { | ||||
|                     rule_xml | ||||
|                         .all_ipv6_selectors | ||||
|                         .push(Self::rest2lib_process_all_selector(all)?); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         Ok(rule_xml) | ||||
|     } | ||||
|  | ||||
|     pub fn rest2lib(&self) -> anyhow::Result<NetworkFilterXML> { | ||||
|         if !self.name.is_valid() { | ||||
|             return Err( | ||||
|                 NetworkFilterExtraction("Network filter name is invalid!".to_string()).into(), | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         if let Some(priority) = self.priority { | ||||
|             if !(-1000..=1000).contains(&priority) { | ||||
|                 return Err( | ||||
|                     NetworkFilterExtraction("Network priority is invalid!".to_string()).into(), | ||||
|                 ); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         for fref in &self.join_filters { | ||||
|             if !fref.is_valid() { | ||||
|                 return Err( | ||||
|                     StructureExtraction("Referenced network filter name is invalid!").into(), | ||||
|                 ); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         let mut rules = Vec::with_capacity(self.rules.len()); | ||||
|  | ||||
|         for rule in &self.rules { | ||||
|             rules.push(Self::rest2lib_process_rule(rule)?); | ||||
|         } | ||||
|  | ||||
|         Ok(NetworkFilterXML { | ||||
|             name: self.name.0.to_string(), | ||||
|             uuid: self.uuid, | ||||
|             chain: self.chain.as_ref().map(|c| c.to_xml()), | ||||
|             priority: self.priority, | ||||
|             filterrefs: self | ||||
|                 .join_filters | ||||
|                 .iter() | ||||
|                 .map(|jf| NetworkFilterRefXML { | ||||
|                     filter: jf.0.to_string(), | ||||
|                 }) | ||||
|                 .collect::<Vec<_>>(), | ||||
|             rules, | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[cfg(test)] | ||||
| mod test { | ||||
|     use crate::libvirt_rest_structures::nw_filter::is_var_def; | ||||
|  | ||||
|     #[test] | ||||
|     pub fn var_def() { | ||||
|         assert!(is_var_def("$MAC")); | ||||
|         assert!(is_var_def("$MAC_ADDRESS")); | ||||
|  | ||||
|         assert!(!is_var_def("$$MAC")); | ||||
|         assert!(!is_var_def("$$MACé")); | ||||
|         assert!(!is_var_def("$$MAC@")); | ||||
|         assert!(!is_var_def("$$MAC TEST")); | ||||
|     } | ||||
| } | ||||
| @@ -1,56 +1,19 @@ | ||||
| use crate::app_config::AppConfig; | ||||
| use crate::constants; | ||||
| use crate::libvirt_lib_structures::{ | ||||
|     DevicesXML, DiskBootXML, DiskDriverXML, DiskReadOnlyXML, DiskSourceXML, DiskTargetXML, DiskXML, | ||||
|     DomainCPUTopology, DomainCPUXML, DomainInputXML, DomainMemoryXML, DomainNetInterfaceXML, | ||||
|     DomainVCPUXML, DomainXML, FeaturesXML, GraphicsXML, NetIntSourceXML, NetMacAddress, | ||||
|     NetworkBridgeXML, NetworkDHCPHostXML, NetworkDHCPRangeXML, NetworkDHCPXML, | ||||
|     NetworkDNSForwarderXML, NetworkDNSXML, NetworkDomainXML, NetworkForwardXML, NetworkIPXML, | ||||
|     NetworkXML, OSLoaderXML, OSTypeXML, TPMBackendXML, TPMDeviceXML, VideoModelXML, VideoXML, | ||||
|     XMLUuid, ACPIXML, OSXML, | ||||
| }; | ||||
| use crate::libvirt_lib_structures::domain::*; | ||||
| use crate::libvirt_lib_structures::XMLUuid; | ||||
| use crate::libvirt_rest_structures::LibVirtStructError; | ||||
| use crate::libvirt_rest_structures::LibVirtStructError::StructureExtraction; | ||||
| use crate::utils::disks_utils::Disk; | ||||
| use crate::utils::files_utils; | ||||
| use ipnetwork::{Ipv4Network, Ipv6Network}; | ||||
| use crate::utils::files_utils::convert_size_unit_to_mb; | ||||
| use lazy_regex::regex; | ||||
| use num::Integer; | ||||
| use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; | ||||
| use std::ops::{Div, Mul}; | ||||
| 
 | ||||
| #[derive(thiserror::Error, Debug)] | ||||
| enum LibVirtStructError { | ||||
|     #[error("StructureExtractionError: {0}")] | ||||
|     StructureExtraction(&'static str), | ||||
|     #[error("DomainExtractionError: {0}")] | ||||
|     DomainExtraction(String), | ||||
|     #[error("MBConvertError: {0}")] | ||||
|     MBConvert(String), | ||||
| } | ||||
| 
 | ||||
| #[derive(serde::Serialize)] | ||||
| pub struct HypervisorInfo { | ||||
|     pub r#type: String, | ||||
|     pub hyp_version: u32, | ||||
|     pub lib_version: u32, | ||||
|     pub capabilities: String, | ||||
|     pub free_memory: u64, | ||||
|     pub hostname: String, | ||||
|     pub node: HypervisorNodeInfo, | ||||
| } | ||||
| 
 | ||||
| #[derive(serde::Serialize)] | ||||
| pub struct HypervisorNodeInfo { | ||||
|     pub cpu_model: String, | ||||
|     /// Memory size in kilobytes
 | ||||
|     pub memory_size: u64, | ||||
|     pub number_of_active_cpus: u32, | ||||
|     pub cpu_frequency_mhz: u32, | ||||
|     pub number_of_numa_cell: u32, | ||||
|     pub number_of_cpu_socket_per_node: u32, | ||||
|     pub number_of_core_per_sockets: u32, | ||||
|     pub number_of_threads_per_core: u32, | ||||
| } | ||||
| #[derive(
 | ||||
|     Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, Hash, Ord, PartialOrd, | ||||
| )] | ||||
| pub struct VMGroupId(pub String); | ||||
| 
 | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| pub enum BootType { | ||||
| @@ -66,11 +29,24 @@ pub enum VMArchitecture { | ||||
|     X86_64, | ||||
| } | ||||
| 
 | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| pub struct NWFilterParam { | ||||
|     name: String, | ||||
|     value: String, | ||||
| } | ||||
| 
 | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| pub struct NWFilterRef { | ||||
|     name: String, | ||||
|     parameters: Vec<NWFilterParam>, | ||||
| } | ||||
| 
 | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| pub struct Network { | ||||
|     mac: String, | ||||
|     #[serde(flatten)] | ||||
|     r#type: NetworkType, | ||||
|     mac: String, | ||||
|     nwfilterref: Option<NWFilterRef>, | ||||
| } | ||||
| 
 | ||||
| #[derive(serde::Serialize, serde::Deserialize)] | ||||
| @@ -88,6 +64,9 @@ pub struct VMInfo { | ||||
|     pub genid: Option<XMLUuid>, | ||||
|     pub title: Option<String>, | ||||
|     pub description: Option<String>, | ||||
|     /// Group associated with the VM (VirtWeb specific field)
 | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub group: Option<VMGroupId>, | ||||
|     pub boot_type: BootType, | ||||
|     pub architecture: VMArchitecture, | ||||
|     /// VM allocated memory, in megabytes
 | ||||
| @@ -108,7 +87,7 @@ pub struct VMInfo { | ||||
| 
 | ||||
| impl VMInfo { | ||||
|     /// Turn this VM into a domain
 | ||||
|     pub fn to_domain(self) -> anyhow::Result<DomainXML> { | ||||
|     pub fn as_domain(&self) -> anyhow::Result<DomainXML> { | ||||
|         if !regex!("^[a-zA-Z0-9]+$").is_match(&self.name) { | ||||
|             return Err(StructureExtraction("VM name is invalid!").into()); | ||||
|         } | ||||
| @@ -134,6 +113,12 @@ impl VMInfo { | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if let Some(group) = &self.group { | ||||
|             if !regex!("^[a-zA-Z0-9]+$").is_match(&group.0) { | ||||
|                 return Err(StructureExtraction("VM group name is invalid!").into()); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if self.memory < constants::MIN_VM_MEMORY || self.memory > constants::MAX_VM_MEMORY { | ||||
|             return Err(StructureExtraction("VM memory is invalid!").into()); | ||||
|         } | ||||
| @@ -199,6 +184,67 @@ impl VMInfo { | ||||
|             false => (None, None), | ||||
|         }; | ||||
| 
 | ||||
|         // Process network card
 | ||||
|         let mut networks = vec![]; | ||||
|         for n in &self.networks { | ||||
|             let mac = NetMacAddress { | ||||
|                 address: n.mac.to_string(), | ||||
|             }; | ||||
| 
 | ||||
|             let model = Some(NetIntModelXML { | ||||
|                 r#type: "virtio".to_string(), | ||||
|             }); | ||||
| 
 | ||||
|             let filterref = if let Some(n) = &n.nwfilterref { | ||||
|                 if !regex!("^[a-zA-Z0-9\\_\\-]+$").is_match(&n.name) { | ||||
|                     log::error!("Filter ref name {} is invalid", n.name); | ||||
|                     return Err(StructureExtraction("Network filter ref name is invalid!").into()); | ||||
|                 } | ||||
| 
 | ||||
|                 for p in &n.parameters { | ||||
|                     if !regex!("^[a-zA-Z0-9_-]+$").is_match(&p.name) { | ||||
|                         return Err(StructureExtraction( | ||||
|                             "Network filter ref parameter name is invalid!", | ||||
|                         ) | ||||
|                         .into()); | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 Some(NetIntfilterRefXML { | ||||
|                     filter: n.name.to_string(), | ||||
|                     parameters: n | ||||
|                         .parameters | ||||
|                         .iter() | ||||
|                         .map(|f| NetIntFilterParameterXML { | ||||
|                             name: f.name.to_string(), | ||||
|                             value: f.value.to_string(), | ||||
|                         }) | ||||
|                         .collect(), | ||||
|                 }) | ||||
|             } else { | ||||
|                 None | ||||
|             }; | ||||
| 
 | ||||
|             networks.push(match &n.r#type { | ||||
|                 NetworkType::UserspaceSLIRPStack => DomainNetInterfaceXML { | ||||
|                     mac, | ||||
|                     r#type: "user".to_string(), | ||||
|                     source: None, | ||||
|                     model, | ||||
|                     filterref, | ||||
|                 }, | ||||
|                 NetworkType::DefinedNetwork { network } => DomainNetInterfaceXML { | ||||
|                     mac, | ||||
|                     r#type: "network".to_string(), | ||||
|                     source: Some(NetIntSourceXML { | ||||
|                         network: network.to_string(), | ||||
|                     }), | ||||
|                     model, | ||||
|                     filterref, | ||||
|                 }, | ||||
|             }) | ||||
|         } | ||||
| 
 | ||||
|         // Check disks name for duplicates
 | ||||
|         for disk in &self.disks { | ||||
|             if self.disks.iter().filter(|d| d.name == disk.name).count() > 1 { | ||||
| @@ -206,8 +252,9 @@ impl VMInfo { | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // Apply disks configuration
 | ||||
|         for disk in self.disks { | ||||
|         // Apply disks configuration. Starting from now, the function should ideally never fail due to
 | ||||
|         // bad user input
 | ||||
|         for disk in &self.disks { | ||||
|             disk.check_config()?; | ||||
|             disk.apply_config(uuid)?; | ||||
| 
 | ||||
| @@ -241,30 +288,20 @@ impl VMInfo { | ||||
|             }) | ||||
|         } | ||||
| 
 | ||||
|         let mut networks = vec![]; | ||||
|         for n in self.networks { | ||||
|             networks.push(match n.r#type { | ||||
|                 NetworkType::UserspaceSLIRPStack => DomainNetInterfaceXML { | ||||
|                     mac: NetMacAddress { address: n.mac }, | ||||
|                     r#type: "user".to_string(), | ||||
|                     source: None, | ||||
|                 }, | ||||
|                 NetworkType::DefinedNetwork { network } => DomainNetInterfaceXML { | ||||
|                     mac: NetMacAddress { address: n.mac }, | ||||
|                     r#type: "network".to_string(), | ||||
|                     source: Some(NetIntSourceXML { network }), | ||||
|                 }, | ||||
|             }) | ||||
|         } | ||||
| 
 | ||||
|         Ok(DomainXML { | ||||
|             r#type: "kvm".to_string(), | ||||
|             name: self.name, | ||||
|             name: self.name.to_string(), | ||||
|             uuid: Some(uuid), | ||||
|             genid: self.genid.map(|i| i.0), | ||||
|             title: self.title, | ||||
|             description: self.description, | ||||
|             title: self.title.clone(), | ||||
|             description: self.description.clone(), | ||||
| 
 | ||||
|             metadata: Some(DomainMetadataXML { | ||||
|                 virtweb: DomainMetadataVirtWebXML { | ||||
|                     ns: "https://virtweb.communiquons.org".to_string(), | ||||
|                     group: self.group.clone().map(|g| g.0), | ||||
|                 }, | ||||
|             }), | ||||
|             os: OSXML { | ||||
|                 r#type: OSTypeXML { | ||||
|                     arch: match self.architecture { | ||||
| @@ -352,6 +389,13 @@ impl VMInfo { | ||||
|             genid: domain.genid.map(XMLUuid), | ||||
|             title: domain.title, | ||||
|             description: domain.description, | ||||
|             group: domain | ||||
|                 .metadata | ||||
|                 .clone() | ||||
|                 .unwrap_or_default() | ||||
|                 .virtweb | ||||
|                 .group | ||||
|                 .map(VMGroupId), | ||||
|             boot_type: match domain.os.loader { | ||||
|                 None => BootType::UEFI, | ||||
|                 Some(l) => match l.secure.as_str() { | ||||
| @@ -370,7 +414,7 @@ impl VMInfo { | ||||
|                 } | ||||
|             }, | ||||
|             number_vcpu: domain.vcpu.body, | ||||
|             memory: convert_to_mb(&domain.memory.unit, domain.memory.memory)?, | ||||
|             memory: convert_size_unit_to_mb(&domain.memory.unit, domain.memory.memory)?, | ||||
|             vnc_access: domain.devices.graphics.is_some(), | ||||
|             iso_files: domain | ||||
|                 .devices | ||||
| @@ -406,6 +450,17 @@ impl VMInfo { | ||||
|                                 ))); | ||||
|                             } | ||||
|                         }, | ||||
|                         nwfilterref: d.filterref.as_ref().map(|f| NWFilterRef { | ||||
|                             name: f.filter.to_string(), | ||||
|                             parameters: f | ||||
|                                 .parameters | ||||
|                                 .iter() | ||||
|                                 .map(|p| NWFilterParam { | ||||
|                                     name: p.name.to_string(), | ||||
|                                     value: p.value.to_string(), | ||||
|                                 }) | ||||
|                                 .collect(), | ||||
|                         }), | ||||
|                     }) | ||||
|                 }) | ||||
|                 .collect::<Result<Vec<_>, _>>()?, | ||||
| @@ -414,309 +469,3 @@ impl VMInfo { | ||||
|         }) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// Convert unit to MB
 | ||||
| fn convert_to_mb(unit: &str, value: usize) -> anyhow::Result<usize> { | ||||
|     let fact = match unit { | ||||
|         "bytes" | "b" => 1f64, | ||||
|         "KB" => 1000f64, | ||||
|         "MB" => 1000f64 * 1000f64, | ||||
|         "GB" => 1000f64 * 1000f64 * 1000f64, | ||||
|         "TB" => 1000f64 * 1000f64 * 1000f64 * 1000f64, | ||||
| 
 | ||||
|         "k" | "KiB" => 1024f64, | ||||
|         "M" | "MiB" => 1024f64 * 1024f64, | ||||
|         "G" | "GiB" => 1024f64 * 1024f64 * 1024f64, | ||||
|         "T" | "TiB" => 1024f64 * 1024f64 * 1024f64 * 1024f64, | ||||
| 
 | ||||
|         _ => { | ||||
|             return Err(LibVirtStructError::MBConvert(format!("Unknown size unit: {unit}")).into()); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     Ok((value as f64).mul(fact.div((1000 * 1000) as f64)).ceil() as usize) | ||||
| } | ||||
| 
 | ||||
| #[derive(serde::Serialize, serde::Deserialize, Copy, Clone, Debug)] | ||||
| pub enum NetworkForwardMode { | ||||
|     NAT, | ||||
|     Isolated, | ||||
| } | ||||
| 
 | ||||
| #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] | ||||
| pub struct DHCPv4HostReservation { | ||||
|     mac: String, | ||||
|     name: String, | ||||
|     ip: Ipv4Addr, | ||||
| } | ||||
| 
 | ||||
| #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] | ||||
| pub struct IPv4DHCPConfig { | ||||
|     start: Ipv4Addr, | ||||
|     end: Ipv4Addr, | ||||
|     hosts: Vec<DHCPv4HostReservation>, | ||||
| } | ||||
| 
 | ||||
| #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] | ||||
| pub struct IPV4Config { | ||||
|     bridge_address: Ipv4Addr, | ||||
|     prefix: u32, | ||||
|     dhcp: Option<IPv4DHCPConfig>, | ||||
| } | ||||
| 
 | ||||
| #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] | ||||
| pub struct DHCPv6HostReservation { | ||||
|     name: String, | ||||
|     ip: Ipv6Addr, | ||||
| } | ||||
| 
 | ||||
| #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] | ||||
| pub struct IPv6DHCPConfig { | ||||
|     start: Ipv6Addr, | ||||
|     end: Ipv6Addr, | ||||
|     hosts: Vec<DHCPv6HostReservation>, | ||||
| } | ||||
| 
 | ||||
| #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] | ||||
| pub struct IPV6Config { | ||||
|     bridge_address: Ipv6Addr, | ||||
|     prefix: u32, | ||||
|     dhcp: Option<IPv6DHCPConfig>, | ||||
| } | ||||
| 
 | ||||
| /// Network configuration
 | ||||
| #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] | ||||
| pub struct NetworkInfo { | ||||
|     name: String, | ||||
|     uuid: Option<XMLUuid>, | ||||
|     title: Option<String>, | ||||
|     description: Option<String>, | ||||
|     forward_mode: NetworkForwardMode, | ||||
|     device: Option<String>, | ||||
|     bridge_name: Option<String>, | ||||
|     dns_server: Option<Ipv4Addr>, | ||||
|     domain: Option<String>, | ||||
|     ip_v4: Option<IPV4Config>, | ||||
|     ip_v6: Option<IPV6Config>, | ||||
| } | ||||
| 
 | ||||
| impl NetworkInfo { | ||||
|     pub fn to_virt_network(self) -> anyhow::Result<NetworkXML> { | ||||
|         if !regex!("^[a-zA-Z0-9]+$").is_match(&self.name) { | ||||
|             return Err(StructureExtraction("network name is invalid!").into()); | ||||
|         } | ||||
| 
 | ||||
|         if let Some(n) = &self.title { | ||||
|             if n.contains('\n') { | ||||
|                 return Err(StructureExtraction("Network title contain newline char!").into()); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if let Some(dev) = &self.device { | ||||
|             if !regex!("^[a-zA-Z0-9]+$").is_match(dev) { | ||||
|                 return Err(StructureExtraction("Network device name is invalid!").into()); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if let Some(bridge) = &self.bridge_name { | ||||
|             if !regex!("^[a-zA-Z0-9]+$").is_match(bridge) { | ||||
|                 return Err(StructureExtraction("Network bridge name is invalid!").into()); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if let Some(domain) = &self.domain { | ||||
|             if !regex!("^[a-zA-Z0-9.]+$").is_match(domain) { | ||||
|                 return Err(StructureExtraction("Domain name is invalid!").into()); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         let mut ips = Vec::with_capacity(2); | ||||
| 
 | ||||
|         if let Some(ipv4) = self.ip_v4 { | ||||
|             if ipv4.prefix > 32 { | ||||
|                 return Err(StructureExtraction("IPv4 prefix is invalid!").into()); | ||||
|             } | ||||
| 
 | ||||
|             ips.push(NetworkIPXML { | ||||
|                 family: "ipv4".to_string(), | ||||
|                 address: IpAddr::V4(ipv4.bridge_address), | ||||
|                 prefix: ipv4.prefix, | ||||
|                 netmask: Ipv4Network::new(ipv4.bridge_address, ipv4.prefix as u8) | ||||
|                     .unwrap() | ||||
|                     .mask() | ||||
|                     .into(), | ||||
|                 dhcp: ipv4.dhcp.map(|dhcp| NetworkDHCPXML { | ||||
|                     range: NetworkDHCPRangeXML { | ||||
|                         start: IpAddr::V4(dhcp.start), | ||||
|                         end: IpAddr::V4(dhcp.end), | ||||
|                     }, | ||||
|                     hosts: dhcp | ||||
|                         .hosts | ||||
|                         .into_iter() | ||||
|                         .map(|c| NetworkDHCPHostXML { | ||||
|                             mac: c.mac, | ||||
|                             name: c.name, | ||||
|                             ip: c.ip.into(), | ||||
|                         }) | ||||
|                         .collect::<Vec<_>>(), | ||||
|                 }), | ||||
|             }) | ||||
|         } | ||||
| 
 | ||||
|         if let Some(ipv6) = self.ip_v6 { | ||||
|             ips.push(NetworkIPXML { | ||||
|                 family: "ipv6".to_string(), | ||||
|                 address: IpAddr::V6(ipv6.bridge_address), | ||||
|                 prefix: ipv6.prefix, | ||||
|                 netmask: Ipv6Network::new(ipv6.bridge_address, ipv6.prefix as u8) | ||||
|                     .unwrap() | ||||
|                     .mask() | ||||
|                     .into(), | ||||
|                 dhcp: ipv6.dhcp.map(|dhcp| NetworkDHCPXML { | ||||
|                     range: NetworkDHCPRangeXML { | ||||
|                         start: IpAddr::V6(dhcp.start), | ||||
|                         end: IpAddr::V6(dhcp.end), | ||||
|                     }, | ||||
|                     hosts: dhcp | ||||
|                         .hosts | ||||
|                         .into_iter() | ||||
|                         .map(|h| NetworkDHCPHostXML { | ||||
|                             mac: "".to_string(), | ||||
|                             name: h.name, | ||||
|                             ip: h.ip.into(), | ||||
|                         }) | ||||
|                         .collect(), | ||||
|                 }), | ||||
|             }) | ||||
|         } | ||||
| 
 | ||||
|         Ok(NetworkXML { | ||||
|             name: self.name, | ||||
|             uuid: self.uuid, | ||||
|             title: self.title, | ||||
|             description: self.description, | ||||
|             forward: match self.forward_mode { | ||||
|                 NetworkForwardMode::NAT => Some(NetworkForwardXML { | ||||
|                     mode: "nat".to_string(), | ||||
|                     dev: self.device.unwrap_or_default(), | ||||
|                 }), | ||||
|                 NetworkForwardMode::Isolated => None, | ||||
|             }, | ||||
|             bridge: self.bridge_name.map(|b| NetworkBridgeXML { | ||||
|                 name: b.to_string(), | ||||
|             }), | ||||
|             dns: self.dns_server.map(|addr| NetworkDNSXML { | ||||
|                 forwarder: NetworkDNSForwarderXML { addr }, | ||||
|             }), | ||||
|             domain: self.domain.map(|name| NetworkDomainXML { name }), | ||||
|             ips, | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     pub fn from_xml(xml: NetworkXML) -> anyhow::Result<Self> { | ||||
|         Ok(Self { | ||||
|             name: xml.name, | ||||
|             uuid: xml.uuid, | ||||
|             title: xml.title, | ||||
|             description: xml.description, | ||||
|             forward_mode: match xml.forward { | ||||
|                 None => NetworkForwardMode::Isolated, | ||||
|                 Some(_) => NetworkForwardMode::NAT, | ||||
|             }, | ||||
|             device: xml | ||||
|                 .forward | ||||
|                 .map(|f| match f.dev.is_empty() { | ||||
|                     true => None, | ||||
|                     false => Some(f.dev), | ||||
|                 }) | ||||
|                 .unwrap_or(None), | ||||
|             bridge_name: xml.bridge.map(|b| b.name), | ||||
|             dns_server: xml.dns.map(|d| d.forwarder.addr), | ||||
|             domain: xml.domain.map(|d| d.name), | ||||
|             ip_v4: xml | ||||
|                 .ips | ||||
|                 .iter() | ||||
|                 .find(|i| i.family != "ipv6") | ||||
|                 .map(|i| IPV4Config { | ||||
|                     bridge_address: extract_ipv4(i.address), | ||||
|                     prefix: match i.prefix { | ||||
|                         u32::MAX => ipnetwork::ipv4_mask_to_prefix(extract_ipv4(i.netmask)) | ||||
|                             .expect("Failed to convert IPv4 netmask to network") | ||||
|                             as u32, | ||||
|                         p => p, | ||||
|                     }, | ||||
|                     dhcp: i.dhcp.as_ref().map(|d| IPv4DHCPConfig { | ||||
|                         start: extract_ipv4(d.range.start), | ||||
|                         end: extract_ipv4(d.range.end), | ||||
|                         hosts: d | ||||
|                             .hosts | ||||
|                             .iter() | ||||
|                             .map(|h| DHCPv4HostReservation { | ||||
|                                 mac: h.mac.to_string(), | ||||
|                                 name: h.name.to_string(), | ||||
|                                 ip: extract_ipv4(h.ip), | ||||
|                             }) | ||||
|                             .collect(), | ||||
|                     }), | ||||
|                 }), | ||||
|             ip_v6: xml | ||||
|                 .ips | ||||
|                 .iter() | ||||
|                 .find(|i| i.family == "ipv6") | ||||
|                 .map(|i| IPV6Config { | ||||
|                     bridge_address: extract_ipv6(i.address), | ||||
|                     prefix: match i.prefix { | ||||
|                         u32::MAX => ipnetwork::ipv6_mask_to_prefix(extract_ipv6(i.netmask)) | ||||
|                             .expect("Failed to convert IPv6 netmask to network") | ||||
|                             as u32, | ||||
|                         p => p, | ||||
|                     }, | ||||
|                     dhcp: i.dhcp.as_ref().map(|d| IPv6DHCPConfig { | ||||
|                         start: extract_ipv6(d.range.start), | ||||
|                         end: extract_ipv6(d.range.end), | ||||
|                         hosts: d | ||||
|                             .hosts | ||||
|                             .iter() | ||||
|                             .map(|h| DHCPv6HostReservation { | ||||
|                                 name: h.name.to_string(), | ||||
|                                 ip: extract_ipv6(h.ip), | ||||
|                             }) | ||||
|                             .collect(), | ||||
|                     }), | ||||
|                 }), | ||||
|         }) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| fn extract_ipv4(ip: IpAddr) -> Ipv4Addr { | ||||
|     match ip { | ||||
|         IpAddr::V4(i) => i, | ||||
|         IpAddr::V6(_) => { | ||||
|             panic!("IPv6 found in IPv4 definition!") | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| fn extract_ipv6(ip: IpAddr) -> Ipv6Addr { | ||||
|     match ip { | ||||
|         IpAddr::V4(_) => { | ||||
|             panic!("IPv4 found in IPv6 definition!") | ||||
|         } | ||||
|         IpAddr::V6(i) => i, | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[cfg(test)] | ||||
| mod test { | ||||
|     use crate::libvirt_rest_structures::convert_to_mb; | ||||
| 
 | ||||
|     #[test] | ||||
|     fn convert_units_mb() { | ||||
|         assert_eq!(convert_to_mb("MB", 1).unwrap(), 1); | ||||
|         assert_eq!(convert_to_mb("MB", 1000).unwrap(), 1000); | ||||
|         assert_eq!(convert_to_mb("GB", 1000).unwrap(), 1000 * 1000); | ||||
|         assert_eq!(convert_to_mb("GB", 1).unwrap(), 1000); | ||||
|         assert_eq!(convert_to_mb("GiB", 3).unwrap(), 3222); | ||||
|         assert_eq!(convert_to_mb("KiB", 488281).unwrap(), 500); | ||||
|     } | ||||
| } | ||||
| @@ -22,17 +22,24 @@ use virtweb_backend::constants::{ | ||||
|     MAX_INACTIVITY_DURATION, MAX_SESSION_DURATION, SESSION_COOKIE_NAME, | ||||
| }; | ||||
| use virtweb_backend::controllers::{ | ||||
|     auth_controller, iso_controller, network_controller, server_controller, static_controller, | ||||
|     vm_controller, | ||||
|     api_tokens_controller, auth_controller, groups_controller, iso_controller, network_controller, | ||||
|     nwfilter_controller, server_controller, static_controller, vm_controller, | ||||
| }; | ||||
| use virtweb_backend::libvirt_client::LibVirtClient; | ||||
| use virtweb_backend::middlewares::auth_middleware::AuthChecker; | ||||
| use virtweb_backend::nat::nat_conf_mode; | ||||
| use virtweb_backend::utils::files_utils; | ||||
|  | ||||
| #[actix_web::main] | ||||
| async fn main() -> std::io::Result<()> { | ||||
|     env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); | ||||
|  | ||||
|     // Run in NAT configuration mode, if requested | ||||
|     if std::env::var(constants::NAT_MODE_ENV_VAR_NAME).is_ok() { | ||||
|         nat_conf_mode::sub_main().await.unwrap(); | ||||
|         return Ok(()); | ||||
|     } | ||||
|  | ||||
|     // Load additional config from file, if requested | ||||
|     AppConfig::parse_env_file().unwrap(); | ||||
|  | ||||
| @@ -41,6 +48,9 @@ async fn main() -> std::io::Result<()> { | ||||
|     files_utils::create_directory_if_missing(AppConfig::get().vnc_sockets_path()).unwrap(); | ||||
|     files_utils::set_file_permission(AppConfig::get().vnc_sockets_path(), 0o777).unwrap(); | ||||
|     files_utils::create_directory_if_missing(AppConfig::get().disks_storage_path()).unwrap(); | ||||
|     files_utils::create_directory_if_missing(AppConfig::get().nat_path()).unwrap(); | ||||
|     files_utils::create_directory_if_missing(AppConfig::get().definitions_path()).unwrap(); | ||||
|     files_utils::create_directory_if_missing(AppConfig::get().api_tokens_path()).unwrap(); | ||||
|  | ||||
|     let conn = Data::new(LibVirtClient( | ||||
|         LibVirtActor::connect() | ||||
| @@ -75,7 +85,7 @@ async fn main() -> std::io::Result<()> { | ||||
|  | ||||
|         let mut cors = Cors::default() | ||||
|             .allowed_origin(&AppConfig::get().website_origin) | ||||
|             .allowed_methods(vec!["GET", "POST", "DELETE", "PUT"]) | ||||
|             .allowed_methods(vec!["GET", "POST", "DELETE", "PUT", "PATCH"]) | ||||
|             .allowed_headers(vec![header::AUTHORIZATION, header::ACCEPT]) | ||||
|             .allowed_header(header::CONTENT_TYPE) | ||||
|             .supports_credentials() | ||||
| @@ -109,6 +119,10 @@ async fn main() -> std::io::Result<()> { | ||||
|                 "/api/server/info", | ||||
|                 web::get().to(server_controller::server_info), | ||||
|             ) | ||||
|             .route( | ||||
|                 "/api/server/network_hook_status", | ||||
|                 web::get().to(server_controller::network_hook_status), | ||||
|             ) | ||||
|             .route( | ||||
|                 "/api/server/number_vcpus", | ||||
|                 web::get().to(server_controller::number_vcpus), | ||||
| @@ -196,6 +210,44 @@ async fn main() -> std::io::Result<()> { | ||||
|                 web::get().to(vm_controller::vnc_token), | ||||
|             ) | ||||
|             .route("/api/vnc", web::get().to(vm_controller::vnc)) | ||||
|             // Groups controller | ||||
|             .route("/api/group/list", web::get().to(groups_controller::list)) | ||||
|             .route( | ||||
|                 "/api/group/{gid}/vm/info", | ||||
|                 web::get().to(groups_controller::vm_info), | ||||
|             ) | ||||
|             .route( | ||||
|                 "/api/group/{gid}/vm/start", | ||||
|                 web::get().to(groups_controller::vm_start), | ||||
|             ) | ||||
|             .route( | ||||
|                 "/api/group/{gid}/vm/shutdown", | ||||
|                 web::get().to(groups_controller::vm_shutdown), | ||||
|             ) | ||||
|             .route( | ||||
|                 "/api/group/{gid}/vm/suspend", | ||||
|                 web::get().to(groups_controller::vm_suspend), | ||||
|             ) | ||||
|             .route( | ||||
|                 "/api/group/{gid}/vm/resume", | ||||
|                 web::get().to(groups_controller::vm_resume), | ||||
|             ) | ||||
|             .route( | ||||
|                 "/api/group/{gid}/vm/kill", | ||||
|                 web::get().to(groups_controller::vm_kill), | ||||
|             ) | ||||
|             .route( | ||||
|                 "/api/group/{gid}/vm/reset", | ||||
|                 web::get().to(groups_controller::vm_reset), | ||||
|             ) | ||||
|             .route( | ||||
|                 "/api/group/{gid}/vm/screenshot", | ||||
|                 web::get().to(groups_controller::vm_screenshot), | ||||
|             ) | ||||
|             .route( | ||||
|                 "/api/group/{gid}/vm/state", | ||||
|                 web::get().to(groups_controller::vm_state), | ||||
|             ) | ||||
|             // Network controller | ||||
|             .route( | ||||
|                 "/api/network/create", | ||||
| @@ -238,6 +290,52 @@ async fn main() -> std::io::Result<()> { | ||||
|                 "/api/network/{uid}/stop", | ||||
|                 web::get().to(network_controller::stop), | ||||
|             ) | ||||
|             // Network filters controller | ||||
|             .route( | ||||
|                 "/api/nwfilter/create", | ||||
|                 web::post().to(nwfilter_controller::create), | ||||
|             ) | ||||
|             .route( | ||||
|                 "/api/nwfilter/list", | ||||
|                 web::get().to(nwfilter_controller::list), | ||||
|             ) | ||||
|             .route( | ||||
|                 "/api/nwfilter/{uid}", | ||||
|                 web::get().to(nwfilter_controller::get_single), | ||||
|             ) | ||||
|             .route( | ||||
|                 "/api/nwfilter/{uid}/src", | ||||
|                 web::get().to(nwfilter_controller::single_src), | ||||
|             ) | ||||
|             .route( | ||||
|                 "/api/nwfilter/{uid}", | ||||
|                 web::put().to(nwfilter_controller::update), | ||||
|             ) | ||||
|             .route( | ||||
|                 "/api/nwfilter/{uid}", | ||||
|                 web::delete().to(nwfilter_controller::delete), | ||||
|             ) | ||||
|             // API tokens controller | ||||
|             .route( | ||||
|                 "/api/token/create", | ||||
|                 web::post().to(api_tokens_controller::create), | ||||
|             ) | ||||
|             .route( | ||||
|                 "/api/token/list", | ||||
|                 web::get().to(api_tokens_controller::list), | ||||
|             ) | ||||
|             .route( | ||||
|                 "/api/token/{uid}", | ||||
|                 web::get().to(api_tokens_controller::get_single), | ||||
|             ) | ||||
|             .route( | ||||
|                 "/api/token/{uid}", | ||||
|                 web::patch().to(api_tokens_controller::update), | ||||
|             ) | ||||
|             .route( | ||||
|                 "/api/token/{uid}", | ||||
|                 web::delete().to(api_tokens_controller::delete), | ||||
|             ) | ||||
|             // Static assets | ||||
|             .route("/", web::get().to(static_controller::root_index)) | ||||
|             .route( | ||||
|   | ||||
| @@ -3,6 +3,7 @@ use std::rc::Rc; | ||||
|  | ||||
| use crate::app_config::AppConfig; | ||||
| use crate::constants; | ||||
| use crate::extractors::api_auth_extractor::ApiAuthExtractor; | ||||
| use crate::extractors::auth_extractor::AuthExtractor; | ||||
| use actix_web::body::EitherBody; | ||||
| use actix_web::dev::Payload; | ||||
| @@ -66,10 +67,40 @@ where | ||||
|                     .await | ||||
|                     .unwrap(); | ||||
|  | ||||
|             if !AppConfig::get().is_allowed_ip(remote_ip.0) { | ||||
|                 log::error!("An attempt to access VirtWeb from an unauthorized network has been intercepted! {:?}", remote_ip); | ||||
|                 return Ok(req | ||||
|                     .into_response( | ||||
|                         HttpResponse::MethodNotAllowed() | ||||
|                             .json("I am sorry, but your IP is not allowed to access this service!"), | ||||
|                     ) | ||||
|                     .map_into_right_body()); | ||||
|             } | ||||
|  | ||||
|             let auth_disabled = AppConfig::get().unsecure_disable_auth; | ||||
|  | ||||
|             // Check authentication, if required | ||||
|             if !auth_disabled | ||||
|             // Check API authentication | ||||
|             if req.headers().get("x-token-id").is_some() { | ||||
|                 let auth = | ||||
|                     match ApiAuthExtractor::from_request(req.request(), &mut Payload::None).await { | ||||
|                         Ok(auth) => auth, | ||||
|                         Err(e) => { | ||||
|                             log::error!( | ||||
|                             "Failed to extract API authentication information from request! {e}" | ||||
|                         ); | ||||
|                             return Ok(req | ||||
|                                 .into_response(HttpResponse::PreconditionFailed().finish()) | ||||
|                                 .map_into_right_body()); | ||||
|                         } | ||||
|                     }; | ||||
|  | ||||
|                 log::info!( | ||||
|                     "Using API token '{}' to perform the request", | ||||
|                     auth.token.name | ||||
|                 ); | ||||
|             } | ||||
|             // Check user authentication, if required | ||||
|             else if !auth_disabled | ||||
|                 && !constants::ROUTES_WITHOUT_AUTH.contains(&req.path()) | ||||
|                 && req.path().starts_with("/api/") | ||||
|             { | ||||
|   | ||||
							
								
								
									
										4
									
								
								virtweb_backend/src/nat/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								virtweb_backend/src/nat/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| pub mod nat_conf_mode; | ||||
| pub mod nat_definition; | ||||
| pub mod nat_hook; | ||||
| pub mod nat_lib; | ||||
							
								
								
									
										232
									
								
								virtweb_backend/src/nat/nat_conf_mode.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										232
									
								
								virtweb_backend/src/nat/nat_conf_mode.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,232 @@ | ||||
| use crate::constants; | ||||
| use crate::libvirt_rest_structures::net::NetworkName; | ||||
| use crate::nat::nat_definition::{Nat, NatSourceIP, NetNat}; | ||||
| use crate::utils::net_utils; | ||||
| use clap::Parser; | ||||
| use std::collections::HashMap; | ||||
| use std::net::IpAddr; | ||||
| use std::path::{Path, PathBuf}; | ||||
| use std::process::{Command, ExitStatus}; | ||||
|  | ||||
| #[derive(thiserror::Error, Debug)] | ||||
| enum NatConfModeError { | ||||
|     #[error("UpdateFirewall failed!")] | ||||
|     UpdateFirewall, | ||||
| } | ||||
|  | ||||
| /// VirtWeb NAT configuration mode. This executable should never be executed manually | ||||
| #[derive(Parser, Debug, Clone)] | ||||
| #[clap(author, version, about, long_about = None)] | ||||
| struct NatArgs { | ||||
|     /// Storage directory | ||||
|     #[clap(short, long)] | ||||
|     storage: String, | ||||
|  | ||||
|     /// Network name | ||||
|     #[clap(short, long)] | ||||
|     network_name: String, | ||||
|  | ||||
|     /// Operation | ||||
|     #[clap(short, long)] | ||||
|     operation: String, | ||||
|  | ||||
|     /// Sub operation | ||||
|     #[clap(long)] | ||||
|     sub_operation: String, | ||||
| } | ||||
|  | ||||
| impl NatArgs { | ||||
|     pub fn network_file(&self) -> PathBuf { | ||||
|         let network_name = NetworkName(self.network_name.to_string()); | ||||
|         Path::new(&self.storage) | ||||
|             .join(constants::STORAGE_NAT_DIR) | ||||
|             .join(network_name.nat_file_name()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// NAT sub main function | ||||
| pub async fn sub_main() -> anyhow::Result<()> { | ||||
|     let args = NatArgs::parse(); | ||||
|  | ||||
|     if !args.network_file().exists() { | ||||
|         log::warn!("Cannot do anything for the network, because the NAT configuration file does not exixsts!"); | ||||
|         return Ok(()); | ||||
|     } | ||||
|  | ||||
|     let conf_json = std::fs::read_to_string(args.network_file())?; | ||||
|     let conf: NetNat = serde_json::from_str(&conf_json)?; | ||||
|  | ||||
|     let nic_ips = net_utils::net_list_and_ips()?; | ||||
|  | ||||
|     match (args.operation.as_str(), args.sub_operation.as_str()) { | ||||
|         ("started", "begin") => { | ||||
|             log::info!("Enable port forwarding for network"); | ||||
|             trigger_nat_forwarding(true, &conf, &nic_ips).await? | ||||
|         } | ||||
|         ("stopped", "end") => { | ||||
|             log::info!("Disable port forwarding for network"); | ||||
|             trigger_nat_forwarding(false, &conf, &nic_ips).await? | ||||
|         } | ||||
|         _ => log::debug!( | ||||
|             "Operation {} - {} not supported", | ||||
|             args.operation, | ||||
|             args.sub_operation | ||||
|         ), | ||||
|     } | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| async fn trigger_nat_forwarding( | ||||
|     enable: bool, | ||||
|     conf: &NetNat, | ||||
|     nic_ips: &HashMap<String, Vec<IpAddr>>, | ||||
| ) -> anyhow::Result<()> { | ||||
|     if let Some(ipv4) = &conf.ipv4 { | ||||
|         trigger_nat_forwarding_nat_ipv( | ||||
|             enable, | ||||
|             &conf.interface, | ||||
|             &ipv4.iter().map(|i| i.generalize()).collect::<Vec<_>>(), | ||||
|             nic_ips, | ||||
|         ) | ||||
|         .await?; | ||||
|     } | ||||
|  | ||||
|     if let Some(ipv6) = &conf.ipv6 { | ||||
|         trigger_nat_forwarding_nat_ipv( | ||||
|             enable, | ||||
|             &conf.interface, | ||||
|             &ipv6.iter().map(|i| i.generalize()).collect::<Vec<_>>(), | ||||
|             nic_ips, | ||||
|         ) | ||||
|         .await?; | ||||
|     } | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| async fn trigger_nat_forwarding_nat_ipv( | ||||
|     enable: bool, | ||||
|     net_interface: &str, | ||||
|     rules: &[Nat<IpAddr>], | ||||
|     nic_ips: &HashMap<String, Vec<IpAddr>>, | ||||
| ) -> anyhow::Result<()> { | ||||
|     for r in rules { | ||||
|         let host_ips = match &r.host_ip { | ||||
|             NatSourceIP::Interface { name } => nic_ips.get(name).cloned().unwrap_or_default(), | ||||
|             NatSourceIP::Ip { ip } => vec![*ip], | ||||
|         }; | ||||
|  | ||||
|         for host_ip in host_ips { | ||||
|             let mut guest_port = r.guest_port; | ||||
|             for host_port in r.host_port.as_seq() { | ||||
|                 if r.protocol.has_tcp() { | ||||
|                     toggle_port_forwarding( | ||||
|                         enable, | ||||
|                         false, | ||||
|                         host_ip, | ||||
|                         host_port, | ||||
|                         net_interface, | ||||
|                         r.guest_ip, | ||||
|                         guest_port, | ||||
|                     )? | ||||
|                 } | ||||
|  | ||||
|                 if r.protocol.has_udp() { | ||||
|                     toggle_port_forwarding( | ||||
|                         enable, | ||||
|                         true, | ||||
|                         host_ip, | ||||
|                         host_port, | ||||
|                         net_interface, | ||||
|                         r.guest_ip, | ||||
|                         guest_port, | ||||
|                     )? | ||||
|                 } | ||||
|  | ||||
|                 guest_port += 1; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| fn check_cmd(s: ExitStatus) -> anyhow::Result<()> { | ||||
|     if !s.success() { | ||||
|         log::error!("Failed to update firewall rules!"); | ||||
|         return Err(NatConfModeError::UpdateFirewall.into()); | ||||
|     } | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| fn toggle_port_forwarding( | ||||
|     enable: bool, | ||||
|     is_udp: bool, | ||||
|     host_ip: IpAddr, | ||||
|     host_port: u16, | ||||
|     net_interface: &str, | ||||
|     guest_ip: IpAddr, | ||||
|     guest_port: u16, | ||||
| ) -> anyhow::Result<()> { | ||||
|     if host_ip.is_ipv4() != guest_ip.is_ipv4() { | ||||
|         log::trace!("Skipping invalid combination {host_ip} -> {guest_ip}"); | ||||
|         return Ok(()); | ||||
|     } | ||||
|  | ||||
|     let program = match host_ip.is_ipv4() { | ||||
|         true => "/sbin/iptables", | ||||
|         false => "/sbin/ip6tables", | ||||
|     }; | ||||
|  | ||||
|     let protocol = match is_udp { | ||||
|         true => "udp", | ||||
|         false => "tcp", | ||||
|     }; | ||||
|  | ||||
|     log::info!("Forward (add={enable}) incoming {protocol} connections for {host_ip}:{host_port} to {guest_ip}:{guest_port} int {net_interface}"); | ||||
|  | ||||
|     // Rule 1 | ||||
|     let cmd = Command::new(program) | ||||
|         .arg(match enable { | ||||
|             true => "-I", | ||||
|             false => "-D", | ||||
|         }) | ||||
|         .arg("FORWARD") | ||||
|         .arg("-o") | ||||
|         .arg(net_interface) | ||||
|         .arg("-p") | ||||
|         .arg(protocol) | ||||
|         .arg("-d") | ||||
|         .arg(guest_ip.to_string()) | ||||
|         .arg("--dport") | ||||
|         .arg(guest_port.to_string()) | ||||
|         .arg("-j") | ||||
|         .arg("ACCEPT") | ||||
|         .status()?; | ||||
|     check_cmd(cmd)?; | ||||
|  | ||||
|     // Rule 2 | ||||
|     let cmd = Command::new(program) | ||||
|         .arg("-t") | ||||
|         .arg("nat") | ||||
|         .arg(match enable { | ||||
|             true => "-I", | ||||
|             false => "-D", | ||||
|         }) | ||||
|         .arg("PREROUTING") | ||||
|         .arg("-p") | ||||
|         .arg(protocol) | ||||
|         .arg("-d") | ||||
|         .arg(host_ip.to_string()) | ||||
|         .arg("--dport") | ||||
|         .arg(host_port.to_string()) | ||||
|         .arg("-j") | ||||
|         .arg("DNAT") | ||||
|         .arg("--to") | ||||
|         .arg(format!("{guest_ip}:{guest_port}")) | ||||
|         .status()?; | ||||
|     check_cmd(cmd)?; | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
							
								
								
									
										142
									
								
								virtweb_backend/src/nat/nat_definition.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								virtweb_backend/src/nat/nat_definition.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,142 @@ | ||||
| use crate::constants; | ||||
| use crate::utils::net_utils; | ||||
| use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; | ||||
|  | ||||
| #[derive(thiserror::Error, Debug)] | ||||
| enum NatDefError { | ||||
|     #[error("Invalid nat definition: {0}")] | ||||
|     InvalidNatDef(&'static str), | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | ||||
| #[serde(tag = "type", rename_all = "lowercase")] | ||||
| pub enum NatSourceIP<IPv> { | ||||
|     Interface { name: String }, | ||||
|     Ip { ip: IPv }, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize)] | ||||
| pub enum NatProtocol { | ||||
|     TCP, | ||||
|     UDP, | ||||
|     Both, | ||||
| } | ||||
|  | ||||
| impl NatProtocol { | ||||
|     pub fn has_tcp(&self) -> bool { | ||||
|         !matches!(&self, NatProtocol::UDP) | ||||
|     } | ||||
|  | ||||
|     pub fn has_udp(&self) -> bool { | ||||
|         !matches!(&self, NatProtocol::TCP) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | ||||
| #[serde(tag = "type", rename_all = "lowercase")] | ||||
| pub enum NatHostPort { | ||||
|     Single { port: u16 }, | ||||
|     Range { start: u16, end: u16 }, | ||||
| } | ||||
|  | ||||
| impl NatHostPort { | ||||
|     pub fn as_seq(&self) -> Vec<u16> { | ||||
|         match self { | ||||
|             NatHostPort::Single { port } => vec![*port], | ||||
|             NatHostPort::Range { start, end } => (*start..(*end + 1)).collect(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | ||||
| pub struct Nat<IPv> { | ||||
|     pub protocol: NatProtocol, | ||||
|     pub host_ip: NatSourceIP<IPv>, | ||||
|     pub host_port: NatHostPort, | ||||
|     pub guest_ip: IPv, | ||||
|     pub guest_port: u16, | ||||
|     pub comment: Option<String>, | ||||
| } | ||||
|  | ||||
| impl<IPv> Nat<IPv> { | ||||
|     pub fn check(&self) -> anyhow::Result<()> { | ||||
|         if let NatSourceIP::Interface { name } = &self.host_ip { | ||||
|             if !net_utils::is_net_interface_name_valid(name) { | ||||
|                 return Err(NatDefError::InvalidNatDef("Invalid nat interface name!").into()); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if let NatHostPort::Range { start, end } = &self.host_port { | ||||
|             if *start == 0 { | ||||
|                 return Err(NatDefError::InvalidNatDef("Invalid start range!").into()); | ||||
|             } | ||||
|  | ||||
|             if start > end { | ||||
|                 return Err(NatDefError::InvalidNatDef("Invalid port range!").into()); | ||||
|             } | ||||
|  | ||||
|             if u16::MAX - (end - start) < self.guest_port { | ||||
|                 return Err(NatDefError::InvalidNatDef("Guest port is too high!").into()); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if self.guest_port == 0 { | ||||
|             return Err(NatDefError::InvalidNatDef("Invalid guest port!").into()); | ||||
|         } | ||||
|  | ||||
|         if let Some(comment) = &self.comment { | ||||
|             if comment.len() > constants::NET_NAT_COMMENT_MAX_SIZE { | ||||
|                 return Err(NatDefError::InvalidNatDef("Comment is too large!").into()); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Nat<Ipv4Addr> { | ||||
|     pub fn generalize(&self) -> Nat<IpAddr> { | ||||
|         Nat { | ||||
|             protocol: self.protocol, | ||||
|             host_ip: match &self.host_ip { | ||||
|                 NatSourceIP::Ip { ip } => NatSourceIP::Ip { | ||||
|                     ip: IpAddr::V4(*ip), | ||||
|                 }, | ||||
|                 NatSourceIP::Interface { name } => NatSourceIP::Interface { | ||||
|                     name: name.to_string(), | ||||
|                 }, | ||||
|             }, | ||||
|             host_port: self.host_port.clone(), | ||||
|             guest_ip: IpAddr::V4(self.guest_ip), | ||||
|             guest_port: self.guest_port, | ||||
|             comment: self.comment.clone(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Nat<Ipv6Addr> { | ||||
|     pub fn generalize(&self) -> Nat<IpAddr> { | ||||
|         Nat { | ||||
|             protocol: self.protocol, | ||||
|             host_ip: match &self.host_ip { | ||||
|                 NatSourceIP::Ip { ip } => NatSourceIP::Ip { | ||||
|                     ip: IpAddr::V6(*ip), | ||||
|                 }, | ||||
|                 NatSourceIP::Interface { name } => NatSourceIP::Interface { | ||||
|                     name: name.to_string(), | ||||
|                 }, | ||||
|             }, | ||||
|             host_port: self.host_port.clone(), | ||||
|             guest_ip: IpAddr::V6(self.guest_ip), | ||||
|             guest_port: self.guest_port, | ||||
|             comment: self.comment.clone(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] | ||||
| pub struct NetNat { | ||||
|     pub interface: String, | ||||
|     pub ipv4: Option<Vec<Nat<Ipv4Addr>>>, | ||||
|     pub ipv6: Option<Vec<Nat<Ipv6Addr>>>, | ||||
| } | ||||
							
								
								
									
										29
									
								
								virtweb_backend/src/nat/nat_hook.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								virtweb_backend/src/nat/nat_hook.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| use crate::app_config::AppConfig; | ||||
| use crate::constants; | ||||
| use std::path::Path; | ||||
|  | ||||
| /// Check out whether NAT hook has been installed or not | ||||
| pub fn is_installed() -> anyhow::Result<bool> { | ||||
|     let hook_file = Path::new(constants::NAT_HOOK_PATH); | ||||
|  | ||||
|     if !hook_file.exists() { | ||||
|         return Ok(false); | ||||
|     } | ||||
|  | ||||
|     let exe = std::env::current_exe()?; | ||||
|     let hook_content = std::fs::read_to_string(hook_file)?; | ||||
|  | ||||
|     Ok(hook_content.contains(exe.to_string_lossy().as_ref())) | ||||
| } | ||||
|  | ||||
| /// Get nat hook expected content | ||||
| pub fn hook_content() -> anyhow::Result<String> { | ||||
|     let exe = std::env::current_exe()?; | ||||
|  | ||||
|     Ok(format!( | ||||
|         "#!/bin/bash\n\ | ||||
|     NAT_MODE=1 {} --storage {} --network-name \"$1\" --operation \"$2\" --sub-operation \"$3\"", | ||||
|         exe.to_string_lossy(), | ||||
|         AppConfig::get().storage | ||||
|     )) | ||||
| } | ||||
							
								
								
									
										61
									
								
								virtweb_backend/src/nat/nat_lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								virtweb_backend/src/nat/nat_lib.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | ||||
| use crate::app_config::AppConfig; | ||||
| use crate::libvirt_rest_structures::net::{NetworkInfo, NetworkName}; | ||||
| use crate::nat::nat_definition::NetNat; | ||||
|  | ||||
| #[derive(thiserror::Error, Debug)] | ||||
| enum NatLibError { | ||||
|     #[error("Could not save nat definition, because network bridge name was not specified!")] | ||||
|     MissingNetworkBridgeName, | ||||
| } | ||||
|  | ||||
| /// Save nat definition | ||||
| pub fn save_nat_def(net: &NetworkInfo) -> anyhow::Result<()> { | ||||
|     let nat = match net.has_nat_def() { | ||||
|         true => NetNat { | ||||
|             interface: net | ||||
|                 .bridge_name | ||||
|                 .as_ref() | ||||
|                 .ok_or(NatLibError::MissingNetworkBridgeName)? | ||||
|                 .to_string(), | ||||
|             ipv4: net | ||||
|                 .ip_v4 | ||||
|                 .as_ref() | ||||
|                 .map(|i| i.nat.clone()) | ||||
|                 .unwrap_or_default(), | ||||
|             ipv6: net | ||||
|                 .ip_v6 | ||||
|                 .as_ref() | ||||
|                 .map(|i| i.nat.clone()) | ||||
|                 .unwrap_or_default(), | ||||
|         }, | ||||
|         false => NetNat::default(), | ||||
|     }; | ||||
|  | ||||
|     let json = serde_json::to_string(&nat)?; | ||||
|  | ||||
|     std::fs::write(AppConfig::get().net_nat_path(&net.name), json)?; | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| /// Remove nat definition, if existing | ||||
| pub fn remove_nat_def(name: &NetworkName) -> anyhow::Result<()> { | ||||
|     let nat_file = AppConfig::get().net_nat_path(name); | ||||
|     if nat_file.exists() { | ||||
|         std::fs::remove_file(nat_file)?; | ||||
|     } | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| /// Load nat definition, if available | ||||
| pub fn load_nat_def(name: &NetworkName) -> anyhow::Result<NetNat> { | ||||
|     let nat_file = AppConfig::get().net_nat_path(name); | ||||
|     if !nat_file.exists() { | ||||
|         return Ok(NetNat::default()); | ||||
|     } | ||||
|  | ||||
|     let file = std::fs::read_to_string(nat_file)?; | ||||
|  | ||||
|     Ok(serde_json::from_str(&file)?) | ||||
| } | ||||
| @@ -1,6 +1,13 @@ | ||||
| use std::ops::{Div, Mul}; | ||||
| use std::os::unix::fs::PermissionsExt; | ||||
| use std::path::Path; | ||||
|  | ||||
| #[derive(thiserror::Error, Debug)] | ||||
| enum FilesUtilsError { | ||||
|     #[error("UnitConvertError: {0}")] | ||||
|     UnitConvert(String), | ||||
| } | ||||
|  | ||||
| const INVALID_CHARS: [&str; 19] = [ | ||||
|     "@", "\\", "/", ":", ",", "<", ">", "%", "'", "\"", "?", "{", "}", "$", "*", "|", ";", "=", | ||||
|     "\t", | ||||
| @@ -28,9 +35,31 @@ pub fn set_file_permission<P: AsRef<Path>>(path: P, mode: u32) -> anyhow::Result | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| /// Convert size unit to MB | ||||
| pub fn convert_size_unit_to_mb(unit: &str, value: usize) -> anyhow::Result<usize> { | ||||
|     let fact = match unit { | ||||
|         "bytes" | "b" => 1f64, | ||||
|         "KB" => 1000f64, | ||||
|         "MB" => 1000f64 * 1000f64, | ||||
|         "GB" => 1000f64 * 1000f64 * 1000f64, | ||||
|         "TB" => 1000f64 * 1000f64 * 1000f64 * 1000f64, | ||||
|  | ||||
|         "k" | "KiB" => 1024f64, | ||||
|         "M" | "MiB" => 1024f64 * 1024f64, | ||||
|         "G" | "GiB" => 1024f64 * 1024f64 * 1024f64, | ||||
|         "T" | "TiB" => 1024f64 * 1024f64 * 1024f64 * 1024f64, | ||||
|  | ||||
|         _ => { | ||||
|             return Err(FilesUtilsError::UnitConvert(format!("Unknown size unit: {unit}")).into()); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     Ok((value as f64).mul(fact.div((1000 * 1000) as f64)).ceil() as usize) | ||||
| } | ||||
|  | ||||
| #[cfg(test)] | ||||
| mod test { | ||||
|     use crate::utils::files_utils::check_file_name; | ||||
|     use crate::utils::files_utils::{check_file_name, convert_size_unit_to_mb}; | ||||
|  | ||||
|     #[test] | ||||
|     fn empty_file_name() { | ||||
| @@ -56,4 +85,14 @@ mod test { | ||||
|     fn valid_file_name() { | ||||
|         assert!(check_file_name("test.iso")); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn convert_units_mb() { | ||||
|         assert_eq!(convert_size_unit_to_mb("MB", 1).unwrap(), 1); | ||||
|         assert_eq!(convert_size_unit_to_mb("MB", 1000).unwrap(), 1000); | ||||
|         assert_eq!(convert_size_unit_to_mb("GB", 1000).unwrap(), 1000 * 1000); | ||||
|         assert_eq!(convert_size_unit_to_mb("GB", 1).unwrap(), 1000); | ||||
|         assert_eq!(convert_size_unit_to_mb("GiB", 3).unwrap(), 3222); | ||||
|         assert_eq!(convert_size_unit_to_mb("KiB", 488281).unwrap(), 500); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| pub mod disks_utils; | ||||
| pub mod files_utils; | ||||
| pub mod net_utils; | ||||
| pub mod rand_utils; | ||||
| pub mod time_utils; | ||||
| pub mod url_utils; | ||||
|   | ||||
							
								
								
									
										200
									
								
								virtweb_backend/src/utils/net_utils.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										200
									
								
								virtweb_backend/src/utils/net_utils.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,200 @@ | ||||
| use nix::sys::socket::{AddressFamily, SockaddrLike}; | ||||
| use std::collections::HashMap; | ||||
| use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; | ||||
| use std::str::FromStr; | ||||
| use sysinfo::Networks; | ||||
|  | ||||
| pub fn extract_ipv4(ip: IpAddr) -> Ipv4Addr { | ||||
|     match ip { | ||||
|         IpAddr::V4(i) => i, | ||||
|         IpAddr::V6(_) => { | ||||
|             panic!("IPv6 found in IPv4 definition!") | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub fn extract_ipv6(ip: IpAddr) -> Ipv6Addr { | ||||
|     match ip { | ||||
|         IpAddr::V4(_) => { | ||||
|             panic!("IPv4 found in IPv6 definition!") | ||||
|         } | ||||
|         IpAddr::V6(i) => i, | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub fn is_ipv4_address_valid<D: AsRef<str>>(ip: D) -> bool { | ||||
|     Ipv4Addr::from_str(ip.as_ref()).is_ok() | ||||
| } | ||||
|  | ||||
| pub fn is_ipv6_address_valid<D: AsRef<str>>(ip: D) -> bool { | ||||
|     Ipv6Addr::from_str(ip.as_ref()).is_ok() | ||||
| } | ||||
|  | ||||
| pub fn is_ipv4_mask_valid(mask: u8) -> bool { | ||||
|     mask <= 32 | ||||
| } | ||||
|  | ||||
| pub fn is_ipv6_mask_valid(mask: u8) -> bool { | ||||
|     mask <= 128 | ||||
| } | ||||
|  | ||||
| pub fn is_mask_valid(ipv: usize, mask: u8) -> bool { | ||||
|     match ipv { | ||||
|         4 => is_ipv4_mask_valid(mask), | ||||
|         6 => is_ipv6_mask_valid(mask), | ||||
|         _ => panic!("Unsupported IP version"), | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub fn is_mac_address_valid<D: AsRef<str>>(mac: D) -> bool { | ||||
|     lazy_regex::regex!("^([a-fA-F0-9]{2}[:-]){5}[a-fA-F0-9]{2}$").is_match(mac.as_ref()) | ||||
| } | ||||
|  | ||||
| pub fn is_net_interface_name_valid<D: AsRef<str>>(int: D) -> bool { | ||||
|     lazy_regex::regex!("^[a-zA-Z0-9]+$").is_match(int.as_ref()) | ||||
| } | ||||
|  | ||||
| /// Get the list of available network interfaces | ||||
| pub fn net_list() -> Vec<String> { | ||||
|     let mut networks = Networks::new(); | ||||
|     networks.refresh_list(); | ||||
|  | ||||
|     networks | ||||
|         .list() | ||||
|         .iter() | ||||
|         .map(|n| n.0.to_string()) | ||||
|         .collect::<Vec<_>>() | ||||
| } | ||||
|  | ||||
| /// Get the list of available network interfaces associated with their IP address | ||||
| pub fn net_list_and_ips() -> anyhow::Result<HashMap<String, Vec<IpAddr>>> { | ||||
|     let addrs = nix::ifaddrs::getifaddrs().unwrap(); | ||||
|  | ||||
|     let mut res = HashMap::new(); | ||||
|  | ||||
|     for ifaddr in addrs { | ||||
|         let address = match ifaddr.address { | ||||
|             Some(address) => address, | ||||
|             None => { | ||||
|                 log::debug!( | ||||
|                     "Interface {} has an unsupported address family", | ||||
|                     ifaddr.interface_name | ||||
|                 ); | ||||
|                 continue; | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         let addr_str = match address.family() { | ||||
|             Some(AddressFamily::Inet) => { | ||||
|                 let address = address.to_string(); | ||||
|                 address | ||||
|                     .split_once(':') | ||||
|                     .map(|a| a.0) | ||||
|                     .unwrap_or(&address) | ||||
|                     .to_string() | ||||
|             } | ||||
|             Some(AddressFamily::Inet6) => { | ||||
|                 let address = address.to_string(); | ||||
|                 let address = address | ||||
|                     .split_once(']') | ||||
|                     .map(|a| a.0) | ||||
|                     .unwrap_or(&address) | ||||
|                     .to_string(); | ||||
|  | ||||
|                 let address = address | ||||
|                     .split_once('%') | ||||
|                     .map(|a| a.0) | ||||
|                     .unwrap_or(&address) | ||||
|                     .to_string(); | ||||
|  | ||||
|                 address.strip_prefix('[').unwrap_or(&address).to_string() | ||||
|             } | ||||
|             _ => { | ||||
|                 log::debug!( | ||||
|                     "Interface {} has an unsupported address family {:?}", | ||||
|                     ifaddr.interface_name, | ||||
|                     address.family() | ||||
|                 ); | ||||
|                 continue; | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         log::debug!( | ||||
|             "Process ip {addr_str} for interface {}", | ||||
|             ifaddr.interface_name | ||||
|         ); | ||||
|  | ||||
|         let ip = IpAddr::from_str(&addr_str)?; | ||||
|  | ||||
|         if !res.contains_key(&ifaddr.interface_name) { | ||||
|             res.insert(ifaddr.interface_name.to_string(), Vec::with_capacity(1)); | ||||
|         } | ||||
|  | ||||
|         res.get_mut(&ifaddr.interface_name).unwrap().push(ip); | ||||
|     } | ||||
|  | ||||
|     Ok(res) | ||||
| } | ||||
|  | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|     use crate::utils::net_utils::{ | ||||
|         is_ipv4_address_valid, is_ipv6_address_valid, is_mac_address_valid, is_mask_valid, | ||||
|         is_net_interface_name_valid, | ||||
|     }; | ||||
|  | ||||
|     #[test] | ||||
|     fn test_is_mac_address_valid() { | ||||
|         assert!(is_mac_address_valid("FF:FF:FF:FF:FF:FF")); | ||||
|         assert!(is_mac_address_valid("02:42:a4:6e:f2:be")); | ||||
|  | ||||
|         assert!(!is_mac_address_valid("tata")); | ||||
|         assert!(!is_mac_address_valid("FF:FF:FF:FF:FF:FZ")); | ||||
|         assert!(!is_mac_address_valid("FF:FF:FF:FF:FF:FF:FF")); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_is_ipv4_address_valid() { | ||||
|         assert!(is_ipv4_address_valid("10.0.0.1")); | ||||
|         assert!(is_ipv4_address_valid("2.56.58.156")); | ||||
|  | ||||
|         assert!(!is_ipv4_address_valid("tata")); | ||||
|         assert!(!is_ipv4_address_valid("1.25.25.288")); | ||||
|         assert!(!is_ipv4_address_valid("5.5.5.5.5")); | ||||
|         assert!(!is_ipv4_address_valid("fe80::")); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_is_ipv6_address_valid() { | ||||
|         assert!(is_ipv6_address_valid("fe80::")); | ||||
|         assert!(is_ipv6_address_valid("fe80:dd::")); | ||||
|         assert!(is_ipv6_address_valid("00:00:00:00:00::")); | ||||
|         assert!(is_ipv6_address_valid("0:0:0:0:0:0:0:0")); | ||||
|  | ||||
|         assert!(!is_ipv6_address_valid("tata")); | ||||
|         assert!(!is_ipv6_address_valid("2.56.58.156")); | ||||
|         assert!(!is_ipv6_address_valid("fe::dd::dd")); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_is_mask_valid() { | ||||
|         assert!(is_mask_valid(4, 25)); | ||||
|         assert!(is_mask_valid(4, 32)); | ||||
|         assert!(is_mask_valid(6, 32)); | ||||
|         assert!(is_mask_valid(6, 34)); | ||||
|  | ||||
|         assert!(!is_mask_valid(4, 34)); | ||||
|         assert!(is_mask_valid(6, 69)); | ||||
|         assert!(is_mask_valid(6, 128)); | ||||
|         assert!(!is_mask_valid(6, 129)); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_is_net_interface_name_valid() { | ||||
|         assert!(is_net_interface_name_valid("eth0")); | ||||
|         assert!(is_net_interface_name_valid("enp0s25")); | ||||
|  | ||||
|         assert!(!is_net_interface_name_valid("enp0s25 ")); | ||||
|         assert!(!is_net_interface_name_valid("@enp0s25 ")); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										46
									
								
								virtweb_docs/SETUP_DEV.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								virtweb_docs/SETUP_DEV.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| # Setup for developpment | ||||
| 1. The `libvirt-dev` package must be installed: | ||||
|  | ||||
| ```bash | ||||
| sudo apt install libvirt-dev | ||||
| ``` | ||||
|  | ||||
| 2. Libvirt must also be installed: | ||||
| ```bash | ||||
| sudo apt install qemu-kvm libvirt-daemon-system | ||||
| ``` | ||||
|  | ||||
| 3. Allow the current user to manage VMs: | ||||
| ```bash | ||||
| sudo adduser $USER libvirt | ||||
| sudo adduser $USER kvm  | ||||
| ``` | ||||
|  | ||||
| > Note: You will need to login again for this change to take effect. | ||||
|  | ||||
| 4. Install required developpment tools: | ||||
| * Rust: https://www.rust-lang.org/learn/get-started | ||||
| * NodeJS: https://nodejs.org/en/download/current | ||||
|  | ||||
|  | ||||
| 5. Run sample OpenID service | ||||
| ```bash | ||||
| cd virtweb_backend | ||||
| docker compose up | ||||
| ``` | ||||
|  | ||||
| 6. Run the backend: | ||||
| ```bash | ||||
| sudo mkdir /var/virtweb | ||||
| sudo chown $USER:$USER /var/virtweb | ||||
| cd virtweb_backend | ||||
| cargo fmt && cargo clippy && cargo run -- -s /var/virtweb --hypervisor-uri "qemu:///system" | ||||
| ``` | ||||
|  | ||||
| 7. Run the frontend | ||||
| ```bash | ||||
| cd virtweb_frontend | ||||
| npm run start | ||||
| ``` | ||||
|  | ||||
| Have fun with your development! | ||||
							
								
								
									
										149
									
								
								virtweb_docs/SETUP_PROD.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										149
									
								
								virtweb_docs/SETUP_PROD.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,149 @@ | ||||
| # Setup for prod | ||||
|  | ||||
| ## Build VirtWeb for production | ||||
| Open a terminal in the root directory of the VirtWeb project, and run the following command: | ||||
|  | ||||
| ```bash | ||||
| make | ||||
| ``` | ||||
|  | ||||
| The release file will be available in `virtweb_backend/target/release/virtweb_backend`.  | ||||
|  | ||||
| This is the only artifact that must be copied to the server. It is recommended to copy it to the `/usr/local/bin` directory. | ||||
|  | ||||
| ## Install requirements | ||||
| In order to work properly, VirtWeb relies on `libvirt`, `qemu` and `kvm`: | ||||
|  | ||||
| ```bash | ||||
| sudo apt install qemu-kvm libvirt-daemon-system libvirt0 libvirt-clients libvirt-daemon bridge-utils | ||||
| ``` | ||||
|  | ||||
| ## Dedicated user | ||||
| It is recommended to have a dedicated non-root user to run LibVirt: | ||||
|  | ||||
| ```bash | ||||
| sudo adduser --disabled-login virtweb | ||||
| sudo adduser virtweb libvirt | ||||
| sudo adduser virtweb kvm | ||||
| ``` | ||||
|  | ||||
| When executing this command as this user, it is possible to use the following command:; | ||||
|  | ||||
| ```bash | ||||
| sudo -u virtweb bash | ||||
| ``` | ||||
|  | ||||
| ## Create Virtweb configuration & storage directory | ||||
| Inside the newly created user, create an environment file that will contain the configuration of the VirtWeb software: | ||||
|  | ||||
| ```bash | ||||
| sudo touch /home/virtweb/virtweb-env | ||||
| sudo chmod 600 /home/virtweb/virtweb-env | ||||
| sudo chown virtweb:virtweb /home/virtweb/virtweb-env | ||||
|  | ||||
| sudo mkdir /home/virtweb/storage | ||||
| sudo chown virtweb:kvm /home/virtweb/storage | ||||
|  | ||||
| # Fix storage access permission issue | ||||
| sudo chmod a+rx /home/virtweb | ||||
| ``` | ||||
|  | ||||
| Edit the configuration content: | ||||
|  | ||||
| ```conf | ||||
| LISTEN_ADDRESS=0.0.0.0:8000 | ||||
| WEBSITE_ORIGIN=http://localhost:8000 | ||||
| SECRET=<rand> | ||||
| AUTH_USERNAME=user | ||||
| AUTH_PASSWORD=changeme | ||||
| DISABLE_OIDC=true | ||||
| STORAGE=/home/virtweb/storage | ||||
| HYPERVISOR_URI=qemu:///system | ||||
| ``` | ||||
|  | ||||
| > Note: `HYPERVISOR_URI=qemu:///system` is used to sepcify that we want to use the main hypervisor. | ||||
|  | ||||
| ## Register Virtweb service | ||||
| Before registering service, check that the configuration works correctly: | ||||
|  | ||||
| ```bash | ||||
| sudo -u virtweb virtweb_backend -c /home/virtweb/virtweb-env | ||||
| ``` | ||||
|  | ||||
| Create now a service in the file `/etc/systemd/system/virtweb.service`: | ||||
|  | ||||
| ```conf | ||||
| [Unit] | ||||
| Description=VirtWeb | ||||
| After=syslog.target | ||||
| After=network.target | ||||
|  | ||||
| [Service] | ||||
| RestartSec=2s | ||||
| Type=simple | ||||
| User=virtweb | ||||
| Group=virtweb | ||||
| WorkingDirectory=/home/virtweb | ||||
| ExecStart=/usr/local/bin/virtweb_backend -c /home/virtweb/virtweb-env | ||||
| Restart=always | ||||
| Environment=USER=virtweb  | ||||
| HOME=/home/virtweb | ||||
|  | ||||
| [Install] | ||||
| WantedBy=multi-user.target | ||||
| ``` | ||||
|  | ||||
| Enable and start the created service: | ||||
|  | ||||
| ```bash | ||||
| sudo systemctl enable virtweb | ||||
| sudo systemctl start virtweb | ||||
| ``` | ||||
|  | ||||
| You should now be able to create VMs! | ||||
|  | ||||
|  | ||||
| ## Configure port forwarding | ||||
| * Allow ip forwarding in the kernel: edit `/etc/sysctl.conf` and uncomment the following line: | ||||
|  | ||||
| ``` | ||||
| net.ipv4.ip_forward=1 | ||||
| ``` | ||||
|  | ||||
| * To reload `sysctl` without reboot: | ||||
|  | ||||
| ``` | ||||
| sudo sysctl -p /etc/sysctl.conf | ||||
| ``` | ||||
|  | ||||
| * Configure apparmore service. Create or update a file named `/etc/apparmor.d/local/usr.sbin.libvirtd` with the following content: | ||||
|  | ||||
| ``` | ||||
| /usr/local/bin/virtweb_backend ux, | ||||
| ``` | ||||
|  | ||||
| * Update Apparmor configuration: | ||||
|  | ||||
| ```bash | ||||
| sudo apparmor_parser -r /etc/apparmor.d/usr.sbin.libvirtd | ||||
| ``` | ||||
|  | ||||
| * Create VirtWeb hook. Set the following content inside `/etc/libvirt/hooks/network`: | ||||
|  | ||||
| ```bash | ||||
| #!/bin/bash | ||||
| NAT_MODE=1 /usr/local/bin/virtweb_backend --storage /home/virtweb/storage --network-name "$1" --operation "$2" --sub-operation "$3" | ||||
| ``` | ||||
|  | ||||
| * Make the script executable: | ||||
|  | ||||
| ```bash | ||||
| sudo chmod +x /etc/libvirt/hooks/network | ||||
| ``` | ||||
|  | ||||
| * Restart `libvirtd` and `VirtWeb`: | ||||
|  | ||||
| ```bash | ||||
| sudo systemctl restart libvirtd | ||||
| sudo systemctl restart virtweb | ||||
| ``` | ||||
							
								
								
									
										2
									
								
								virtweb_frontend/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								virtweb_frontend/.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,5 +1,7 @@ | ||||
| # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | ||||
|  | ||||
| dist/ | ||||
|  | ||||
| # dependencies | ||||
| /node_modules | ||||
| /.pnp | ||||
|   | ||||
							
								
								
									
										8746
									
								
								virtweb_frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8746
									
								
								virtweb_frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -4,38 +4,39 @@ | ||||
|   "type": "module", | ||||
|   "private": true, | ||||
|   "dependencies": { | ||||
|     "@emotion/react": "^11.11.1", | ||||
|     "@emotion/styled": "^11.11.0", | ||||
|     "@fontsource/roboto": "^5.0.8", | ||||
|     "@emotion/react": "^11.13.5", | ||||
|     "@emotion/styled": "^11.13.5", | ||||
|     "@fontsource/roboto": "^5.1.0", | ||||
|     "@mdi/js": "^7.2.96", | ||||
|     "@mdi/react": "^1.6.1", | ||||
|     "@mui/icons-material": "^5.14.7", | ||||
|     "@mui/material": "^5.14.7", | ||||
|     "@mui/x-charts": "^6.0.0-alpha.9", | ||||
|     "@mui/x-data-grid": "^6.12.1", | ||||
|     "@testing-library/jest-dom": "^5.17.0", | ||||
|     "@testing-library/react": "^13.4.0", | ||||
|     "@testing-library/user-event": "^13.5.0", | ||||
|     "@mui/icons-material": "^6.1.8", | ||||
|     "@mui/material": "^6.1.8", | ||||
|     "@mui/x-charts": "^7.22.3", | ||||
|     "@mui/x-data-grid": "^7.22.3", | ||||
|     "@testing-library/jest-dom": "^6.6.3", | ||||
|     "@testing-library/react": "^16.0.0", | ||||
|     "@testing-library/user-event": "^14.5.2", | ||||
|     "@types/humanize-duration": "^3.27.1", | ||||
|     "@types/jest": "^27.5.2", | ||||
|     "@types/react": "^18.2.21", | ||||
|     "@types/react-dom": "^18.2.7", | ||||
|     "@types/react-syntax-highlighter": "^15.5.11", | ||||
|     "@types/uuid": "^9.0.5", | ||||
|     "@vitejs/plugin-react": "^4.2.1", | ||||
|     "filesize": "^10.0.12", | ||||
|     "@types/jest": "^29.5.14", | ||||
|     "@types/react": "^18.3.12", | ||||
|     "@types/react-dom": "^18.3.1", | ||||
|     "@types/react-syntax-highlighter": "^15.5.13", | ||||
|     "@types/uuid": "^10.0.0", | ||||
|     "@vitejs/plugin-react": "^4.3.3", | ||||
|     "date-and-time": "^3.6.0", | ||||
|     "filesize": "^10.1.6", | ||||
|     "humanize-duration": "^3.29.0", | ||||
|     "mui-file-input": "^3.0.1", | ||||
|     "mui-file-input": "^6.0.0", | ||||
|     "react": "^18.2.0", | ||||
|     "react-dom": "^18.2.0", | ||||
|     "react-router-dom": "^6.15.0", | ||||
|     "react-syntax-highlighter": "^15.5.0", | ||||
|     "react-vnc": "^1.0.0", | ||||
|     "react-router-dom": "^7.0.1", | ||||
|     "react-syntax-highlighter": "^15.6.1", | ||||
|     "react-vnc": "^2.0.2", | ||||
|     "typescript": "^4.9.5", | ||||
|     "uuid": "^9.0.1", | ||||
|     "vite": "^5.0.8", | ||||
|     "vite-tsconfig-paths": "^4.2.2", | ||||
|     "web-vitals": "^2.1.4", | ||||
|     "uuid": "^11.0.3", | ||||
|     "vite": "^6.0.0", | ||||
|     "vite-tsconfig-paths": "^5.1.3", | ||||
|     "web-vitals": "^3.5.2", | ||||
|     "xml-formatter": "^3.6.0" | ||||
|   }, | ||||
|   "scripts": { | ||||
|   | ||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 2.1 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 8.3 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 14 KiB | 
| @@ -8,26 +8,36 @@ import { | ||||
| import "./App.css"; | ||||
| import { AuthApi } from "./api/AuthApi"; | ||||
| import { ServerApi } from "./api/ServerApi"; | ||||
| import { | ||||
|   CreateApiTokenRoute, | ||||
|   EditApiTokenRoute, | ||||
| } from "./routes/EditAPITokenRoute"; | ||||
| import { | ||||
|   CreateNWFilterRoute, | ||||
|   EditNWFilterRoute, | ||||
| } from "./routes/EditNWFilterRoute"; | ||||
| import { | ||||
|   CreateNetworkRoute, | ||||
|   EditNetworkRoute, | ||||
| } from "./routes/EditNetworkRoute"; | ||||
| import { CreateVMRoute, EditVMRoute } from "./routes/EditVMRoute"; | ||||
| import { HomeRoute } from "./routes/HomeRoute"; | ||||
| import { IsoFilesRoute } from "./routes/IsoFilesRoute"; | ||||
| import { NetworkFiltersListRoute } from "./routes/NetworkFiltersListRoute"; | ||||
| import { NetworksListRoute } from "./routes/NetworksListRoute"; | ||||
| import { NotFoundRoute } from "./routes/NotFound"; | ||||
| import { SysInfoRoute } from "./routes/SysInfoRoute"; | ||||
| import { TokensListRoute } from "./routes/TokensListRoute"; | ||||
| import { VMListRoute } from "./routes/VMListRoute"; | ||||
| import { VMRoute } from "./routes/VMRoute"; | ||||
| import { VNCRoute } from "./routes/VNCRoute"; | ||||
| import { ViewApiTokenRoute } from "./routes/ViewApiTokenRoute"; | ||||
| import { ViewNWFilterRoute } from "./routes/ViewNWFilterRoute"; | ||||
| import { ViewNetworkRoute } from "./routes/ViewNetworkRoute"; | ||||
| import { LoginRoute } from "./routes/auth/LoginRoute"; | ||||
| import { OIDCCbRoute } from "./routes/auth/OIDCCbRoute"; | ||||
| import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage"; | ||||
| import { BaseLoginPage } from "./widgets/BaseLoginPage"; | ||||
| import { ViewNetworkRoute } from "./routes/ViewNetworkRoute"; | ||||
| import { VMXMLRoute } from "./routes/VMXMLRoute"; | ||||
| import { NetXMLRoute } from "./routes/NetXMLRoute"; | ||||
| import { HomeRoute } from "./routes/HomeRoute"; | ||||
|  | ||||
| interface AuthContext { | ||||
|   signedIn: boolean; | ||||
| @@ -57,13 +67,21 @@ export function App() { | ||||
|           <Route path="vm/:uuid" element={<VMRoute />} /> | ||||
|           <Route path="vm/:uuid/edit" element={<EditVMRoute />} /> | ||||
|           <Route path="vm/:uuid/vnc" element={<VNCRoute />} /> | ||||
|           <Route path="vm/:uuid/xml" element={<VMXMLRoute />} /> | ||||
|  | ||||
|           <Route path="net" element={<NetworksListRoute />} /> | ||||
|           <Route path="net/new" element={<CreateNetworkRoute />} /> | ||||
|           <Route path="net/:uuid" element={<ViewNetworkRoute />} /> | ||||
|           <Route path="net/:uuid/edit" element={<EditNetworkRoute />} /> | ||||
|           <Route path="net/:uuid/xml" element={<NetXMLRoute />} /> | ||||
|  | ||||
|           <Route path="nwfilter" element={<NetworkFiltersListRoute />} /> | ||||
|           <Route path="nwfilter/new" element={<CreateNWFilterRoute />} /> | ||||
|           <Route path="nwfilter/:uuid" element={<ViewNWFilterRoute />} /> | ||||
|           <Route path="nwfilter/:uuid/edit" element={<EditNWFilterRoute />} /> | ||||
|  | ||||
|           <Route path="tokens" element={<TokensListRoute />} /> | ||||
|           <Route path="token/new" element={<CreateApiTokenRoute />} /> | ||||
|           <Route path="token/:id" element={<ViewApiTokenRoute />} /> | ||||
|           <Route path="token/:id/edit" element={<EditApiTokenRoute />} /> | ||||
|  | ||||
|           <Route path="sysinfo" element={<SysInfoRoute />} /> | ||||
|           <Route path="*" element={<NotFoundRoute />} /> | ||||
|   | ||||
							
								
								
									
										15
									
								
								virtweb_frontend/src/api/GroupApi.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								virtweb_frontend/src/api/GroupApi.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| import { APIClient } from "./ApiClient"; | ||||
|  | ||||
| export class GroupApi { | ||||
|   /** | ||||
|    * Get the entire list of networks | ||||
|    */ | ||||
|   static async GetList(): Promise<string[]> { | ||||
|     return ( | ||||
|       await APIClient.exec({ | ||||
|         method: "GET", | ||||
|         uri: "/group/list", | ||||
|       }) | ||||
|     ).data; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										227
									
								
								virtweb_frontend/src/api/NWFilterApi.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										227
									
								
								virtweb_frontend/src/api/NWFilterApi.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,227 @@ | ||||
| import { APIClient } from "./ApiClient"; | ||||
| import { ServerApi } from "./ServerApi"; | ||||
|  | ||||
| export interface NWFilterChain { | ||||
|   protocol: string; | ||||
|   suffix?: string; | ||||
| } | ||||
|  | ||||
| export interface NWFSMac { | ||||
|   type: "mac"; | ||||
|   src_mac_addr?: string; | ||||
|   src_mac_mask?: string; | ||||
|   dst_mac_addr?: string; | ||||
|   dst_mac_mask?: string; | ||||
|   comment?: string; | ||||
| } | ||||
|  | ||||
| export interface NWFSArpOrRARP { | ||||
|   srcmacaddr?: string; | ||||
|   srcmacmask?: string; | ||||
|   dstmacaddr?: string; | ||||
|   dstmacmask?: string; | ||||
|   arpsrcipaddr?: string; | ||||
|   arpsrcipmask?: number; | ||||
|   arpdstipaddr?: string; | ||||
|   arpdstipmask?: number; | ||||
|   comment?: string; | ||||
| } | ||||
|  | ||||
| export type NWFSArp = NWFSArpOrRARP & { | ||||
|   type: "arp"; | ||||
| }; | ||||
|  | ||||
| export type NWFSRArp = NWFSArpOrRARP & { | ||||
|   type: "rarp"; | ||||
| }; | ||||
|  | ||||
| export interface NWFSIPBase { | ||||
|   srcmacaddr?: string; | ||||
|   srcmacmask?: string; | ||||
|   dstmacaddr?: string; | ||||
|   dstmacmask?: string; | ||||
|   srcipaddr?: string; | ||||
|   srcipmask?: number; | ||||
|   dstipaddr?: string; | ||||
|   dstipmask?: number; | ||||
|   comment?: string; | ||||
| } | ||||
|  | ||||
| export type NFWSIPv4 = NWFSIPBase & { type: "ipv4" }; | ||||
| export type NFWSIPv6 = NWFSIPBase & { type: "ipv6" }; | ||||
|  | ||||
| export type Layer4State = | ||||
|   | "NEW" | ||||
|   | "ESTABLISHED" | ||||
|   | "RELATED" | ||||
|   | "INVALID" | ||||
|   | "NONE"; | ||||
|  | ||||
| export interface NWFSLayer4Base { | ||||
|   srcmacaddr?: string; | ||||
|   srcipaddr?: string; | ||||
|   srcipmask?: number; | ||||
|   dstipaddr?: string; | ||||
|   dstipmask?: number; | ||||
|   srcipfrom?: string; | ||||
|   srcipto?: string; | ||||
|   dstipfrom?: string; | ||||
|   dstipto?: string; | ||||
|   srcportstart?: number; | ||||
|   srcportend?: number; | ||||
|   dstportstart?: number; | ||||
|   dstportend?: number; | ||||
|   state?: Layer4State; | ||||
|   comment?: string; | ||||
| } | ||||
|  | ||||
| export type NFWSTCPv4 = NWFSLayer4Base & { type: "tcp" }; | ||||
| export type NFWSUDPv4 = NWFSLayer4Base & { type: "udp" }; | ||||
| export type NFWSSCTPv4 = NWFSLayer4Base & { type: "sctp" }; | ||||
| export type NFWSICMPv4 = NWFSLayer4Base & { type: "icmp" }; | ||||
|  | ||||
| export type NFWSTCPv6 = NWFSLayer4Base & { type: "tcpipv6" }; | ||||
| export type NFWSUDPv6 = NWFSLayer4Base & { type: "udpipv6" }; | ||||
| export type NFWSSCTPv6 = NWFSLayer4Base & { type: "sctpipv6" }; | ||||
| export type NFWSICMPv6 = NWFSLayer4Base & { type: "icmpipv6" }; | ||||
|  | ||||
| export interface NWFSAllBase { | ||||
|   srcmacaddr?: string; | ||||
|   srcipaddr?: string; | ||||
|   srcipmask?: number; | ||||
|   dstipaddr?: string; | ||||
|   dstipmask?: number; | ||||
|   srcipfrom?: string; | ||||
|   srcipto?: string; | ||||
|   dstipfrom?: string; | ||||
|   dstipto?: string; | ||||
|   state?: Layer4State; | ||||
|   comment?: string; | ||||
| } | ||||
|  | ||||
| export type NWFSAll = NWFSAllBase & { | ||||
|   type: "all"; | ||||
| }; | ||||
|  | ||||
| export type NWFSAllIPv6 = NWFSAllBase & { | ||||
|   type: "allipv6"; | ||||
| }; | ||||
|  | ||||
| export type NWFSelector = | ||||
|   | NWFSMac | ||||
|   | NWFSArp | ||||
|   | NWFSRArp | ||||
|   | NFWSIPv4 | ||||
|   | NFWSIPv6 | ||||
|   | NFWSTCPv4 | ||||
|   | NFWSUDPv4 | ||||
|   | NFWSSCTPv4 | ||||
|   | NFWSICMPv4 | ||||
|   | NWFSAll | ||||
|   | NFWSTCPv6 | ||||
|   | NFWSUDPv6 | ||||
|   | NFWSSCTPv6 | ||||
|   | NFWSICMPv6 | ||||
|   | NWFSAllIPv6; | ||||
|  | ||||
| export interface NWFilterRule { | ||||
|   action: "drop" | "reject" | "accept" | "return" | "continue"; | ||||
|   direction: "in" | "out" | "inout"; | ||||
|   priority?: number; | ||||
|   selectors: NWFSelector[]; | ||||
| } | ||||
|  | ||||
| export interface NWFilter { | ||||
|   name: string; | ||||
|   uuid?: string; | ||||
|   chain?: NWFilterChain; | ||||
|   priority?: number; | ||||
|   join_filters: string[]; | ||||
|   rules: NWFilterRule[]; | ||||
| } | ||||
|  | ||||
| export function NWFilterURL(n: NWFilter, edit: boolean = false): string { | ||||
|   return `/nwfilter/${n.uuid}${edit ? "/edit" : ""}`; | ||||
| } | ||||
|  | ||||
| export function NWFilterIsBuiltin(n: NWFilter): boolean { | ||||
|   return ServerApi.Config.builtin_nwfilter_rules.includes(n.name); | ||||
| } | ||||
|  | ||||
| export class NWFilterApi { | ||||
|   /** | ||||
|    * Get the entire list of networks | ||||
|    */ | ||||
|   static async GetList(): Promise<NWFilter[]> { | ||||
|     const list: NWFilter[] = ( | ||||
|       await APIClient.exec({ | ||||
|         method: "GET", | ||||
|         uri: "/nwfilter/list", | ||||
|       }) | ||||
|     ).data; | ||||
|  | ||||
|     list.sort((a, b) => a.name.localeCompare(b.name)); | ||||
|  | ||||
|     return list; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Get the information about a single network filter | ||||
|    */ | ||||
|   static async GetSingle(uuid: string): Promise<NWFilter> { | ||||
|     return ( | ||||
|       await APIClient.exec({ | ||||
|         method: "GET", | ||||
|         uri: `/nwfilter/${uuid}`, | ||||
|       }) | ||||
|     ).data; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Get the source XML configuration of a network filter for debugging purposes | ||||
|    */ | ||||
|   static async GetSingleXML(uuid: string): Promise<string> { | ||||
|     return ( | ||||
|       await APIClient.exec({ | ||||
|         uri: `/nwfilter/${uuid}/src`, | ||||
|         method: "GET", | ||||
|       }) | ||||
|     ).data; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Create a new network filter | ||||
|    */ | ||||
|   static async Create(n: NWFilter): Promise<{ uid: string }> { | ||||
|     return ( | ||||
|       await APIClient.exec({ | ||||
|         method: "POST", | ||||
|         uri: "/nwfilter/create", | ||||
|         jsonData: n, | ||||
|       }) | ||||
|     ).data; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Update an existing network filter | ||||
|    */ | ||||
|   static async Update(n: NWFilter): Promise<{ uid: string }> { | ||||
|     return ( | ||||
|       await APIClient.exec({ | ||||
|         method: "PUT", | ||||
|         uri: `/nwfilter/${n.uuid}`, | ||||
|         jsonData: n, | ||||
|       }) | ||||
|     ).data; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Delete a network filter | ||||
|    */ | ||||
|   static async Delete(n: NWFilter): Promise<void> { | ||||
|     await APIClient.exec({ | ||||
|       method: "DELETE", | ||||
|       uri: `/nwfilter/${n.uuid}`, | ||||
|     }); | ||||
|   } | ||||
| } | ||||
| @@ -13,10 +13,28 @@ export interface DHCPConfig { | ||||
|   hosts: DHCPHost[]; | ||||
| } | ||||
|  | ||||
| export type NatSource = | ||||
|   | { type: "interface"; name: string } | ||||
|   | { type: "ip"; ip: string }; | ||||
|  | ||||
| export type NatHostPort = | ||||
|   | { type: "single"; port: number } | ||||
|   | { type: "range"; start: number; end: number }; | ||||
|  | ||||
| export interface NatEntry { | ||||
|   protocol: "TCP" | "UDP" | "Both"; | ||||
|   host_ip: NatSource; | ||||
|   host_port: NatHostPort; | ||||
|   guest_ip: string; | ||||
|   guest_port: number; | ||||
|   comment?: string; | ||||
| } | ||||
|  | ||||
| export interface IpConfig { | ||||
|   bridge_address: string; | ||||
|   prefix: number; | ||||
|   dhcp?: DHCPConfig; | ||||
|   nat?: NatEntry[]; | ||||
| } | ||||
|  | ||||
| export interface NetworkInfo { | ||||
| @@ -39,10 +57,6 @@ export function NetworkURL(n: NetworkInfo, edit: boolean = false): string { | ||||
|   return `/net/${n.uuid}${edit ? "/edit" : ""}`; | ||||
| } | ||||
|  | ||||
| export function NetworkXMLURL(n: NetworkInfo): string { | ||||
|   return `/net/${n.uuid}/xml`; | ||||
| } | ||||
|  | ||||
| export class NetworkApi { | ||||
|   /** | ||||
|    * Create a new network | ||||
| @@ -164,12 +178,10 @@ export class NetworkApi { | ||||
|   /** | ||||
|    * Delete a network | ||||
|    */ | ||||
|   static async Delete(n: NetworkInfo): Promise<NetworkInfo[]> { | ||||
|     return ( | ||||
|       await APIClient.exec({ | ||||
|         method: "DELETE", | ||||
|         uri: `/network/${n.uuid}`, | ||||
|       }) | ||||
|     ).data; | ||||
|   static async Delete(n: NetworkInfo): Promise<void> { | ||||
|     await APIClient.exec({ | ||||
|       method: "DELETE", | ||||
|       uri: `/network/${n.uuid}`, | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -6,6 +6,8 @@ export interface ServerConfig { | ||||
|   oidc_auth_enabled: boolean; | ||||
|   iso_mimetypes: string[]; | ||||
|   net_mac_prefix: string; | ||||
|   builtin_nwfilter_rules: string[]; | ||||
|   nwfilter_chains: string[]; | ||||
|   constraints: ServerConstraints; | ||||
| } | ||||
|  | ||||
| @@ -14,12 +16,21 @@ export interface ServerConstraints { | ||||
|   vnc_token_duration: number; | ||||
|   vm_name_size: LenConstraint; | ||||
|   vm_title_size: LenConstraint; | ||||
|   group_id_size: LenConstraint; | ||||
|   memory_size: LenConstraint; | ||||
|   disk_name_size: LenConstraint; | ||||
|   disk_size: LenConstraint; | ||||
|   net_name_size: LenConstraint; | ||||
|   net_title_size: LenConstraint; | ||||
|   net_nat_comment_size: LenConstraint; | ||||
|   dhcp_reservation_host_name: LenConstraint; | ||||
|   nwfilter_name_size: LenConstraint; | ||||
|   nwfilter_comment_size: LenConstraint; | ||||
|   nwfilter_priority: LenConstraint; | ||||
|   nwfilter_selectors_count: LenConstraint; | ||||
|   api_token_name_size: LenConstraint; | ||||
|   api_token_description_size: LenConstraint; | ||||
|   api_token_right_path_size: LenConstraint; | ||||
| } | ||||
|  | ||||
| export interface LenConstraint { | ||||
| @@ -32,6 +43,9 @@ let config: ServerConfig | null = null; | ||||
| export interface ServerSystemInfo { | ||||
|   hypervisor: HypervisorInfo; | ||||
|   system: SystemInfo; | ||||
|   components: SysComponent; | ||||
|   disks: DiskInfo[]; | ||||
|   networks: NetworkInfo[]; | ||||
| } | ||||
|  | ||||
| interface HypervisorInfo { | ||||
| @@ -60,7 +74,7 @@ interface SystemInfo { | ||||
|     secs: number; | ||||
|     nanos: number; | ||||
|   }; | ||||
|   global_cpu_info: GlobalCPUInfo; | ||||
|   global_cpu_usage: number; | ||||
|   cpus: CpuCore[]; | ||||
|   physical_core_count: number; | ||||
|   total_memory: number; | ||||
| @@ -70,10 +84,6 @@ interface SystemInfo { | ||||
|   total_swap: number; | ||||
|   free_swap: number; | ||||
|   used_swap: number; | ||||
|   components: SysComponent; | ||||
|   users: []; | ||||
|   disks: DiskInfo[]; | ||||
|   networks: NetworkInfo[]; | ||||
|   uptime: number; | ||||
|   boot_time: number; | ||||
|   load_average: SysLoadAverage; | ||||
| @@ -85,14 +95,6 @@ interface SystemInfo { | ||||
|   host_name: string; | ||||
| } | ||||
|  | ||||
| interface GlobalCPUInfo { | ||||
|   cpu_usage: number; | ||||
|   name: string; | ||||
|   vendor_id: string; | ||||
|   brand: string; | ||||
|   frequency: number; | ||||
| } | ||||
|  | ||||
| interface CpuCore { | ||||
|   cpu_usage: number; | ||||
|   name: string; | ||||
| @@ -141,6 +143,12 @@ interface SysLoadAverage { | ||||
|   fifteen: number; | ||||
| } | ||||
|  | ||||
| export interface NetworkHookStatus { | ||||
|   installed: boolean; | ||||
|   content: string; | ||||
|   path: string; | ||||
| } | ||||
|  | ||||
| export class ServerApi { | ||||
|   /** | ||||
|    * Get server configuration | ||||
| @@ -174,6 +182,18 @@ export class ServerApi { | ||||
|     ).data; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Get network hook status | ||||
|    */ | ||||
|   static async NetworkHookStatus(): Promise<NetworkHookStatus> { | ||||
|     return ( | ||||
|       await APIClient.exec({ | ||||
|         method: "GET", | ||||
|         uri: "/server/network_hook_status", | ||||
|       }) | ||||
|     ).data; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Get host supported vCPUs configurations | ||||
|    */ | ||||
|   | ||||
							
								
								
									
										102
									
								
								virtweb_frontend/src/api/TokensApi.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								virtweb_frontend/src/api/TokensApi.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,102 @@ | ||||
| import { time } from "../utils/DateUtils"; | ||||
| import { APIClient } from "./ApiClient"; | ||||
|  | ||||
| export type RightVerb = "POST" | "GET" | "PUT" | "DELETE" | "PATCH"; | ||||
|  | ||||
| export interface TokenRight { | ||||
|   verb: RightVerb; | ||||
|   path: string; | ||||
| } | ||||
|  | ||||
| export interface APIToken { | ||||
|   id: string; | ||||
|   name: string; | ||||
|   description: string; | ||||
|   created: number; | ||||
|   updated: number; | ||||
|   rights: TokenRight[]; | ||||
|   last_used: number; | ||||
|   ip_restriction?: string; | ||||
|   max_inactivity?: number; | ||||
| } | ||||
|  | ||||
| export function APITokenURL(t: APIToken, edit: boolean = false): string { | ||||
|   return `/token/${t.id}${edit ? "/edit" : ""}`; | ||||
| } | ||||
|  | ||||
| export function ExpiredAPIToken(t: APIToken): boolean { | ||||
|   if (!t.max_inactivity) return false; | ||||
|   return t.last_used + t.max_inactivity < time(); | ||||
| } | ||||
|  | ||||
| export interface APITokenPrivateKey { | ||||
|   alg: string; | ||||
|   priv: string; | ||||
| } | ||||
|  | ||||
| export interface CreatedAPIToken { | ||||
|   token: APIToken; | ||||
|   priv_key: APITokenPrivateKey; | ||||
| } | ||||
|  | ||||
| export class TokensApi { | ||||
|   /** | ||||
|    * Create a new API token | ||||
|    */ | ||||
|   static async Create(n: APIToken): Promise<CreatedAPIToken> { | ||||
|     return ( | ||||
|       await APIClient.exec({ | ||||
|         method: "POST", | ||||
|         uri: "/token/create", | ||||
|         jsonData: n, | ||||
|       }) | ||||
|     ).data; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Get the full list of tokens | ||||
|    */ | ||||
|   static async GetList(): Promise<APIToken[]> { | ||||
|     return ( | ||||
|       await APIClient.exec({ | ||||
|         method: "GET", | ||||
|         uri: "/token/list", | ||||
|       }) | ||||
|     ).data; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Get the information about a single token | ||||
|    */ | ||||
|   static async GetSingle(uuid: string): Promise<APIToken> { | ||||
|     return ( | ||||
|       await APIClient.exec({ | ||||
|         method: "GET", | ||||
|         uri: `/token/${uuid}`, | ||||
|       }) | ||||
|     ).data; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Update an existing API token information | ||||
|    */ | ||||
|   static async Update(n: APIToken): Promise<void> { | ||||
|     return ( | ||||
|       await APIClient.exec({ | ||||
|         method: "PATCH", | ||||
|         uri: `/token/${n.id}`, | ||||
|         jsonData: n, | ||||
|       }) | ||||
|     ).data; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Delete an API token | ||||
|    */ | ||||
|   static async Delete(n: APIToken): Promise<void> { | ||||
|     await APIClient.exec({ | ||||
|       method: "DELETE", | ||||
|       uri: `/token/${n.id}`, | ||||
|     }); | ||||
|   } | ||||
| } | ||||
| @@ -30,16 +30,30 @@ export interface VMDisk { | ||||
|   deleteType?: "keepfile" | "deletefile"; | ||||
| } | ||||
|  | ||||
| export type VMNetInterface = VMNetUserspaceSLIRPStack | VMNetDefinedNetwork; | ||||
| export interface VMNetInterfaceFilterParams { | ||||
|   name: string; | ||||
|   value: string; | ||||
| } | ||||
|  | ||||
| export interface VMNetInterfaceFilter { | ||||
|   name: string; | ||||
|   parameters: VMNetInterfaceFilterParams[]; | ||||
| } | ||||
|  | ||||
| export type VMNetInterface = (VMNetUserspaceSLIRPStack | VMNetDefinedNetwork) & | ||||
|   VMNetInterfaceBase; | ||||
|  | ||||
| export interface VMNetInterfaceBase { | ||||
|   mac: string; | ||||
|   nwfilterref?: VMNetInterfaceFilter; | ||||
| } | ||||
|  | ||||
| export interface VMNetUserspaceSLIRPStack { | ||||
|   type: "UserspaceSLIRPStack"; | ||||
|   mac: string; | ||||
| } | ||||
|  | ||||
| export interface VMNetDefinedNetwork { | ||||
|   type: "DefinedNetwork"; | ||||
|   mac: string; | ||||
|   network: string; | ||||
| } | ||||
|  | ||||
| @@ -49,6 +63,7 @@ interface VMInfoInterface { | ||||
|   genid?: string; | ||||
|   title?: string; | ||||
|   description?: string; | ||||
|   group?: string; | ||||
|   boot_type: "UEFI" | "UEFISecureBoot"; | ||||
|   architecture: "i686" | "x86_64"; | ||||
|   memory: number; | ||||
| @@ -66,6 +81,7 @@ export class VMInfo implements VMInfoInterface { | ||||
|   genid?: string; | ||||
|   title?: string; | ||||
|   description?: string; | ||||
|   group?: string; | ||||
|   boot_type: "UEFI" | "UEFISecureBoot"; | ||||
|   architecture: "i686" | "x86_64"; | ||||
|   number_vcpu: number; | ||||
| @@ -82,6 +98,7 @@ export class VMInfo implements VMInfoInterface { | ||||
|     this.genid = int.genid; | ||||
|     this.title = int.title; | ||||
|     this.description = int.description; | ||||
|     this.group = int.group; | ||||
|     this.boot_type = int.boot_type; | ||||
|     this.architecture = int.architecture; | ||||
|     this.number_vcpu = int.number_vcpu; | ||||
| @@ -119,10 +136,6 @@ export class VMInfo implements VMInfoInterface { | ||||
|   get VNCURL(): string { | ||||
|     return `/vm/${this.uuid}/vnc`; | ||||
|   } | ||||
|  | ||||
|   get XMLURL(): string { | ||||
|     return `/vm/${this.uuid}/xml`; | ||||
|   } | ||||
| } | ||||
|  | ||||
| export class VMApi { | ||||
|   | ||||
							
								
								
									
										58
									
								
								virtweb_frontend/src/dialogs/CreatedTokenDialog.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								virtweb_frontend/src/dialogs/CreatedTokenDialog.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | ||||
| import { | ||||
|   Button, | ||||
|   Dialog, | ||||
|   DialogActions, | ||||
|   DialogContent, | ||||
|   DialogTitle, | ||||
|   Typography, | ||||
| } from "@mui/material"; | ||||
| import { useNavigate } from "react-router-dom"; | ||||
| import { APITokenURL, CreatedAPIToken } from "../api/TokensApi"; | ||||
| import { CopyToClipboard } from "../widgets/CopyToClipboard"; | ||||
| import { InlineCode } from "../widgets/InlineCode"; | ||||
|  | ||||
| export function CreatedTokenDialog(p: { | ||||
|   createdToken: CreatedAPIToken; | ||||
| }): React.ReactElement { | ||||
|   const navigate = useNavigate(); | ||||
|  | ||||
|   const close = () => { | ||||
|     navigate(APITokenURL(p.createdToken.token)); | ||||
|   }; | ||||
|   return ( | ||||
|     <Dialog open> | ||||
|       <DialogTitle>Token successfully created</DialogTitle> | ||||
|       <DialogContent> | ||||
|         <Typography> | ||||
|           Your token was successfully created. You need now to copy the private | ||||
|           key, as it will be technically impossible to recover it after closing | ||||
|           this dialog. | ||||
|         </Typography> | ||||
|  | ||||
|         <InfoBlock label="Token ID" value={p.createdToken.token.id} /> | ||||
|         <InfoBlock label="Key algorithm" value={p.createdToken.priv_key.alg} /> | ||||
|         <InfoBlock label="Private key" value={p.createdToken.priv_key.priv} /> | ||||
|       </DialogContent> | ||||
|       <DialogActions> | ||||
|         <Button onClick={close} color="error"> | ||||
|           I copied the key, close this dialog | ||||
|         </Button> | ||||
|       </DialogActions> | ||||
|     </Dialog> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function InfoBlock( | ||||
|   p: React.PropsWithChildren<{ label: string; value: string }> | ||||
| ): React.ReactElement { | ||||
|   return ( | ||||
|     <div | ||||
|       style={{ display: "flex", flexDirection: "column", margin: "20px 10px" }} | ||||
|     > | ||||
|       <Typography variant="overline">{p.label}</Typography> | ||||
|       <CopyToClipboard content={p.value}> | ||||
|         <InlineCode>{p.value}</InlineCode> | ||||
|       </CopyToClipboard> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										161
									
								
								virtweb_frontend/src/routes/EditAPITokenRoute.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								virtweb_frontend/src/routes/EditAPITokenRoute.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,161 @@ | ||||
| import { Button } from "@mui/material"; | ||||
| import React from "react"; | ||||
| import { useNavigate, useParams } from "react-router-dom"; | ||||
| import { | ||||
|   APIToken, | ||||
|   APITokenURL, | ||||
|   CreatedAPIToken, | ||||
|   TokensApi, | ||||
| } from "../api/TokensApi"; | ||||
| import { CreatedTokenDialog } from "../dialogs/CreatedTokenDialog"; | ||||
| import { useAlert } from "../hooks/providers/AlertDialogProvider"; | ||||
| import { useLoadingMessage } from "../hooks/providers/LoadingMessageProvider"; | ||||
| import { useSnackbar } from "../hooks/providers/SnackbarProvider"; | ||||
| import { time } from "../utils/DateUtils"; | ||||
| import { AsyncWidget } from "../widgets/AsyncWidget"; | ||||
| import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; | ||||
| import { | ||||
|   APITokenDetails, | ||||
|   TokenWidgetStatus, | ||||
| } from "../widgets/tokens/APITokenDetails"; | ||||
|  | ||||
| export function CreateApiTokenRoute(): React.ReactElement { | ||||
|   const alert = useAlert(); | ||||
|   const snackbar = useSnackbar(); | ||||
|   const navigate = useNavigate(); | ||||
|  | ||||
|   const [createdToken, setCreatedToken] = React.useState< | ||||
|     CreatedAPIToken | undefined | ||||
|   >(); | ||||
|  | ||||
|   const [token] = React.useState<APIToken>({ | ||||
|     id: "", | ||||
|     name: "", | ||||
|     description: "", | ||||
|     created: time(), | ||||
|     updated: time(), | ||||
|     last_used: time(), | ||||
|     rights: [], | ||||
|   }); | ||||
|  | ||||
|   const createApiToken = async (n: APIToken) => { | ||||
|     try { | ||||
|       const res = await TokensApi.Create(n); | ||||
|       snackbar("The api token was successfully created!"); | ||||
|       setCreatedToken(res); | ||||
|     } catch (e) { | ||||
|       console.error(e); | ||||
|       alert(`Failed to create API token!\n${e}`); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       {createdToken && <CreatedTokenDialog createdToken={createdToken} />} | ||||
|  | ||||
|       <EditApiTokenRouteInner | ||||
|         token={token} | ||||
|         creating={true} | ||||
|         onCancel={() => navigate("/tokens")} | ||||
|         onSave={createApiToken} | ||||
|       /> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| export function EditApiTokenRoute(): React.ReactElement { | ||||
|   const alert = useAlert(); | ||||
|   const snackbar = useSnackbar(); | ||||
|  | ||||
|   const { id } = useParams(); | ||||
|   const navigate = useNavigate(); | ||||
|  | ||||
|   const [token, setToken] = React.useState<APIToken | undefined>(); | ||||
|  | ||||
|   const load = async () => { | ||||
|     setToken(await TokensApi.GetSingle(id!)); | ||||
|   }; | ||||
|  | ||||
|   const updateApiToken = async (n: APIToken) => { | ||||
|     try { | ||||
|       await TokensApi.Update(n); | ||||
|       snackbar("The token was successfully updated!"); | ||||
|       navigate(APITokenURL(token!)); | ||||
|     } catch (e) { | ||||
|       console.error(e); | ||||
|       alert(`Failed to update token!\n${e}`); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <AsyncWidget | ||||
|       loadKey={id} | ||||
|       ready={token !== undefined} | ||||
|       errMsg="Failed to fetch API token informations!" | ||||
|       load={load} | ||||
|       build={() => ( | ||||
|         <EditApiTokenRouteInner | ||||
|           token={token!} | ||||
|           creating={false} | ||||
|           onCancel={() => navigate(`/token/${id}`)} | ||||
|           onSave={updateApiToken} | ||||
|         /> | ||||
|       )} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function EditApiTokenRouteInner(p: { | ||||
|   token: APIToken; | ||||
|   creating: boolean; | ||||
|   onCancel: () => void; | ||||
|   onSave: (token: APIToken) => Promise<void>; | ||||
| }): React.ReactElement { | ||||
|   const loadingMessage = useLoadingMessage(); | ||||
|  | ||||
|   const [changed, setChanged] = React.useState(false); | ||||
|  | ||||
|   const [, updateState] = React.useState<any>(); | ||||
|   const forceUpdate = React.useCallback(() => updateState({}), []); | ||||
|  | ||||
|   const valueChanged = () => { | ||||
|     setChanged(true); | ||||
|     forceUpdate(); | ||||
|   }; | ||||
|  | ||||
|   const save = async () => { | ||||
|     loadingMessage.show("Saving API token configuration..."); | ||||
|     await p.onSave(p.token); | ||||
|     loadingMessage.hide(); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <VirtWebRouteContainer | ||||
|       label={p.creating ? "Create an API Token" : "Edit API Token"} | ||||
|       actions={ | ||||
|         <span> | ||||
|           {changed && ( | ||||
|             <Button | ||||
|               variant="contained" | ||||
|               onClick={save} | ||||
|               style={{ marginRight: "10px" }} | ||||
|             > | ||||
|               {p.creating ? "Create" : "Save"} | ||||
|             </Button> | ||||
|           )} | ||||
|           <Button onClick={p.onCancel} variant="outlined"> | ||||
|             Cancel | ||||
|           </Button> | ||||
|         </span> | ||||
|       } | ||||
|     > | ||||
|       <APITokenDetails | ||||
|         token={p.token} | ||||
|         status={ | ||||
|           p.creating ? TokenWidgetStatus.Create : TokenWidgetStatus.Update | ||||
|         } | ||||
|         onChange={valueChanged} | ||||
|       /> | ||||
|     </VirtWebRouteContainer> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										151
									
								
								virtweb_frontend/src/routes/EditNWFilterRoute.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								virtweb_frontend/src/routes/EditNWFilterRoute.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,151 @@ | ||||
| import { Button } from "@mui/material"; | ||||
| import React from "react"; | ||||
| import { useNavigate, useParams } from "react-router-dom"; | ||||
| import { useAlert } from "../hooks/providers/AlertDialogProvider"; | ||||
| import { useLoadingMessage } from "../hooks/providers/LoadingMessageProvider"; | ||||
| import { useSnackbar } from "../hooks/providers/SnackbarProvider"; | ||||
| import { AsyncWidget } from "../widgets/AsyncWidget"; | ||||
| import { ConfigImportExportButtons } from "../widgets/ConfigImportExportButtons"; | ||||
| import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; | ||||
| import { NWFilter, NWFilterApi, NWFilterURL } from "../api/NWFilterApi"; | ||||
| import { NWFilterDetails } from "../widgets/nwfilter/NWFilterDetails"; | ||||
|  | ||||
| export function CreateNWFilterRoute(): React.ReactElement { | ||||
|   const alert = useAlert(); | ||||
|   const snackbar = useSnackbar(); | ||||
|   const navigate = useNavigate(); | ||||
|  | ||||
|   const [nwfilter, setNWFilter] = React.useState<NWFilter>({ | ||||
|     name: "my-filter", | ||||
|     chain: { protocol: "root" }, | ||||
|     join_filters: [], | ||||
|     rules: [], | ||||
|   }); | ||||
|  | ||||
|   const createNWFilter = async (n: NWFilter) => { | ||||
|     try { | ||||
|       const res = await NWFilterApi.Create(n); | ||||
|       snackbar("The network filter was successfully created!"); | ||||
|       navigate(`/nwfilter/${res.uid}`); | ||||
|     } catch (e) { | ||||
|       console.error(e); | ||||
|       alert(`Failed to create network filter!\n${e}`); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <EditNetworkFilterRouteInner | ||||
|       nwfilter={nwfilter} | ||||
|       creating={true} | ||||
|       onCancel={() => navigate("/nwfilter")} | ||||
|       onSave={createNWFilter} | ||||
|       onReplace={setNWFilter} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| export function EditNWFilterRoute(): React.ReactElement { | ||||
|   const alert = useAlert(); | ||||
|   const snackbar = useSnackbar(); | ||||
|  | ||||
|   const { uuid } = useParams(); | ||||
|   const navigate = useNavigate(); | ||||
|  | ||||
|   const [nwfilter, setNWFilter] = React.useState<NWFilter | undefined>(); | ||||
|  | ||||
|   const load = async () => { | ||||
|     setNWFilter(await NWFilterApi.GetSingle(uuid!)); | ||||
|   }; | ||||
|  | ||||
|   const updateNetworkFilter = async (n: NWFilter) => { | ||||
|     try { | ||||
|       await NWFilterApi.Update(n); | ||||
|       snackbar("The network filter was successfully updated!"); | ||||
|       navigate(NWFilterURL(nwfilter!)); | ||||
|     } catch (e) { | ||||
|       console.error(e); | ||||
|       alert(`Failed to update network filter!\n${e}`); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <AsyncWidget | ||||
|       loadKey={uuid} | ||||
|       ready={nwfilter !== undefined} | ||||
|       errMsg="Failed to fetch network filter information!" | ||||
|       load={load} | ||||
|       build={() => ( | ||||
|         <EditNetworkFilterRouteInner | ||||
|           nwfilter={nwfilter!} | ||||
|           creating={false} | ||||
|           onCancel={() => navigate(`/nwfilter/${uuid}`)} | ||||
|           onSave={updateNetworkFilter} | ||||
|           onReplace={setNWFilter} | ||||
|         /> | ||||
|       )} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function EditNetworkFilterRouteInner(p: { | ||||
|   nwfilter: NWFilter; | ||||
|   creating: boolean; | ||||
|   onCancel: () => void; | ||||
|   onSave: (vm: NWFilter) => Promise<void>; | ||||
|   onReplace: (vm: NWFilter) => void; | ||||
| }): React.ReactElement { | ||||
|   const loadingMessage = useLoadingMessage(); | ||||
|  | ||||
|   const [changed, setChanged] = React.useState(false); | ||||
|  | ||||
|   const [, updateState] = React.useState<any>(); | ||||
|   const forceUpdate = React.useCallback(() => updateState({}), []); | ||||
|  | ||||
|   const valueChanged = () => { | ||||
|     setChanged(true); | ||||
|     forceUpdate(); | ||||
|   }; | ||||
|  | ||||
|   const save = async () => { | ||||
|     loadingMessage.show("Saving network filter configuration..."); | ||||
|     await p.onSave(p.nwfilter); | ||||
|     loadingMessage.hide(); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <VirtWebRouteContainer | ||||
|       label={p.creating ? "Create a Network Filter" : "Edit Network Filter"} | ||||
|       actions={ | ||||
|         <span> | ||||
|           <ConfigImportExportButtons | ||||
|             currentConf={p.nwfilter} | ||||
|             filename={`nwfilter-${p.nwfilter.name}.json`} | ||||
|             importConf={(c) => { | ||||
|               p.onReplace(c); | ||||
|               valueChanged(); | ||||
|             }} | ||||
|           /> | ||||
|  | ||||
|           {changed && ( | ||||
|             <Button | ||||
|               variant="contained" | ||||
|               onClick={save} | ||||
|               style={{ marginRight: "10px" }} | ||||
|             > | ||||
|               {p.creating ? "Create" : "Save"} | ||||
|             </Button> | ||||
|           )} | ||||
|           <Button onClick={p.onCancel} variant="outlined"> | ||||
|             Cancel | ||||
|           </Button> | ||||
|         </span> | ||||
|       } | ||||
|     > | ||||
|       <NWFilterDetails | ||||
|         nwfilter={p.nwfilter} | ||||
|         editable={true} | ||||
|         onChange={valueChanged} | ||||
|       /> | ||||
|     </VirtWebRouteContainer> | ||||
|   ); | ||||
| } | ||||
| @@ -1,19 +1,21 @@ | ||||
| import { Button } from "@mui/material"; | ||||
| import React from "react"; | ||||
| import { useNavigate, useParams } from "react-router-dom"; | ||||
| import { NetworkApi, NetworkInfo, NetworkURL } from "../api/NetworksApi"; | ||||
| import { useAlert } from "../hooks/providers/AlertDialogProvider"; | ||||
| import { useLoadingMessage } from "../hooks/providers/LoadingMessageProvider"; | ||||
| import { useSnackbar } from "../hooks/providers/SnackbarProvider"; | ||||
| import React from "react"; | ||||
| import { AsyncWidget } from "../widgets/AsyncWidget"; | ||||
| import { NetworkDetails } from "../widgets/net/NetworkDetails"; | ||||
| import { ConfigImportExportButtons } from "../widgets/ConfigImportExportButtons"; | ||||
| import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; | ||||
| import { Button } from "@mui/material"; | ||||
| import { NetworkDetails } from "../widgets/net/NetworkDetails"; | ||||
|  | ||||
| export function CreateNetworkRoute(): React.ReactElement { | ||||
|   const alert = useAlert(); | ||||
|   const snackbar = useSnackbar(); | ||||
|   const navigate = useNavigate(); | ||||
|  | ||||
|   const [network] = React.useState<NetworkInfo>({ | ||||
|   const [network, setNetwork] = React.useState<NetworkInfo>({ | ||||
|     name: "NewNetwork", | ||||
|     forward_mode: "Isolated", | ||||
|   }); | ||||
| @@ -35,6 +37,7 @@ export function CreateNetworkRoute(): React.ReactElement { | ||||
|       creating={true} | ||||
|       onCancel={() => navigate("/net")} | ||||
|       onSave={createNetwork} | ||||
|       onReplace={setNetwork} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
| @@ -75,6 +78,7 @@ export function EditNetworkRoute(): React.ReactElement { | ||||
|           creating={false} | ||||
|           onCancel={() => navigate(`/net/${uuid}`)} | ||||
|           onSave={updateNetwork} | ||||
|           onReplace={setNetwork} | ||||
|         /> | ||||
|       )} | ||||
|     /> | ||||
| @@ -86,7 +90,10 @@ function EditNetworkRouteInner(p: { | ||||
|   creating: boolean; | ||||
|   onCancel: () => void; | ||||
|   onSave: (vm: NetworkInfo) => Promise<void>; | ||||
|   onReplace: (vm: NetworkInfo) => void; | ||||
| }): React.ReactElement { | ||||
|   const loadingMessage = useLoadingMessage(); | ||||
|  | ||||
|   const [changed, setChanged] = React.useState(false); | ||||
|  | ||||
|   const [, updateState] = React.useState<any>(); | ||||
| @@ -96,15 +103,31 @@ function EditNetworkRouteInner(p: { | ||||
|     setChanged(true); | ||||
|     forceUpdate(); | ||||
|   }; | ||||
|  | ||||
|   const save = async () => { | ||||
|     loadingMessage.show("Saving network configuration..."); | ||||
|     await p.onSave(p.network); | ||||
|     loadingMessage.hide(); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <VirtWebRouteContainer | ||||
|       label={p.creating ? "Create a Network" : "Edit Network"} | ||||
|       actions={ | ||||
|         <span> | ||||
|           <ConfigImportExportButtons | ||||
|             currentConf={p.network} | ||||
|             filename={`net-${p.network.name}.json`} | ||||
|             importConf={(c) => { | ||||
|               p.onReplace(c); | ||||
|               valueChanged(); | ||||
|             }} | ||||
|           /> | ||||
|  | ||||
|           {changed && ( | ||||
|             <Button | ||||
|               variant="contained" | ||||
|               onClick={() => p.onSave(p.network)} | ||||
|               onClick={save} | ||||
|               style={{ marginRight: "10px" }} | ||||
|             > | ||||
|               {p.creating ? "Create" : "Save"} | ||||
|   | ||||
| @@ -5,15 +5,17 @@ import { VMApi, VMInfo } from "../api/VMApi"; | ||||
| import { useAlert } from "../hooks/providers/AlertDialogProvider"; | ||||
| import { useSnackbar } from "../hooks/providers/SnackbarProvider"; | ||||
| import { AsyncWidget } from "../widgets/AsyncWidget"; | ||||
| import { ConfigImportExportButtons } from "../widgets/ConfigImportExportButtons"; | ||||
| import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; | ||||
| import { VMDetails } from "../widgets/vms/VMDetails"; | ||||
| import { useLoadingMessage } from "../hooks/providers/LoadingMessageProvider"; | ||||
|  | ||||
| export function CreateVMRoute(): React.ReactElement { | ||||
|   const snackbar = useSnackbar(); | ||||
|   const alert = useAlert(); | ||||
|   const navigate = useNavigate(); | ||||
|  | ||||
|   const [vm] = React.useState(VMInfo.NewEmpty); | ||||
|   const [vm, setVM] = React.useState(VMInfo.NewEmpty); | ||||
|  | ||||
|   const create = async (v: VMInfo) => { | ||||
|     try { | ||||
| @@ -30,6 +32,7 @@ export function CreateVMRoute(): React.ReactElement { | ||||
|   return ( | ||||
|     <EditVMInner | ||||
|       vm={vm} | ||||
|       onReplace={setVM} | ||||
|       isCreating={true} | ||||
|       onSave={create} | ||||
|       onCancel={() => navigate("/vms")} | ||||
| @@ -64,7 +67,7 @@ export function EditVMRoute(): React.ReactElement { | ||||
|       navigate(v.ViewURL); | ||||
|     } catch (e) { | ||||
|       console.error(e); | ||||
|       alert("Failed to update VM info!"); | ||||
|       alert(`Failed to update VM info!\n${e}`); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
| @@ -76,6 +79,7 @@ export function EditVMRoute(): React.ReactElement { | ||||
|       build={() => ( | ||||
|         <EditVMInner | ||||
|           vm={vm!} | ||||
|           onReplace={setVM} | ||||
|           isCreating={false} | ||||
|           onCancel={() => { | ||||
|             navigate(vm!.ViewURL); | ||||
| @@ -92,7 +96,10 @@ function EditVMInner(p: { | ||||
|   isCreating: boolean; | ||||
|   onCancel: () => void; | ||||
|   onSave: (vm: VMInfo) => Promise<void>; | ||||
|   onReplace: (vm: VMInfo) => void; | ||||
| }): React.ReactElement { | ||||
|   const loadingMessage = useLoadingMessage(); | ||||
|  | ||||
|   const [changed, setChanged] = React.useState(false); | ||||
|  | ||||
|   const [, updateState] = React.useState<any>(); | ||||
| @@ -102,15 +109,30 @@ function EditVMInner(p: { | ||||
|     setChanged(true); | ||||
|     forceUpdate(); | ||||
|   }; | ||||
|  | ||||
|   const save = async () => { | ||||
|     loadingMessage.show("Saving VM configuration..."); | ||||
|     await p.onSave(p.vm); | ||||
|     loadingMessage.hide(); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <VirtWebRouteContainer | ||||
|       label={p.isCreating ? "Create a Virtual Machine" : "Edit Virtual Machine"} | ||||
|       actions={ | ||||
|         <span> | ||||
|           <ConfigImportExportButtons | ||||
|             filename={`vm-${p.vm.name}.json`} | ||||
|             currentConf={p.vm} | ||||
|             importConf={(conf) => { | ||||
|               p.onReplace(new VMInfo(conf)); | ||||
|               valueChanged(); | ||||
|             }} | ||||
|           /> | ||||
|           {changed && ( | ||||
|             <Button | ||||
|               variant="contained" | ||||
|               onClick={() => p.onSave(p.vm)} | ||||
|               onClick={save} | ||||
|               style={{ marginRight: "10px" }} | ||||
|             > | ||||
|               {p.isCreating ? "Create" : "Save"} | ||||
|   | ||||
| @@ -1,50 +0,0 @@ | ||||
| import ArrowBackIcon from "@mui/icons-material/ArrowBack"; | ||||
| import { IconButton } from "@mui/material"; | ||||
| import React from "react"; | ||||
| import { useParams } from "react-router-dom"; | ||||
| import { NetworkApi, NetworkInfo, NetworkURL } from "../api/NetworksApi"; | ||||
| import { AsyncWidget } from "../widgets/AsyncWidget"; | ||||
| import { RouterLink } from "../widgets/RouterLink"; | ||||
| import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; | ||||
| import { XMLWidget } from "../widgets/XMLWidget"; | ||||
|  | ||||
| export function NetXMLRoute(): React.ReactElement { | ||||
|   const { uuid } = useParams(); | ||||
|  | ||||
|   const [net, setNet] = React.useState<NetworkInfo | undefined>(); | ||||
|   const [src, setSrc] = React.useState<string | undefined>(); | ||||
|  | ||||
|   const load = async () => { | ||||
|     setNet(await NetworkApi.GetSingle(uuid!)); | ||||
|     setSrc(await NetworkApi.GetSingleXML(uuid!)); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <AsyncWidget | ||||
|       loadKey={uuid} | ||||
|       load={load} | ||||
|       errMsg="Failed to load network information!" | ||||
|       build={() => <XMLRouteInner net={net!} src={src!} />} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function XMLRouteInner(p: { | ||||
|   net: NetworkInfo; | ||||
|   src: string; | ||||
| }): React.ReactElement { | ||||
|   return ( | ||||
|     <VirtWebRouteContainer | ||||
|       label={`XML definition of ${p.net.name}`} | ||||
|       actions={ | ||||
|         <RouterLink to={NetworkURL(p.net)}> | ||||
|           <IconButton> | ||||
|             <ArrowBackIcon /> | ||||
|           </IconButton> | ||||
|         </RouterLink> | ||||
|       } | ||||
|     > | ||||
|       <XMLWidget src={p.src} /> | ||||
|     </VirtWebRouteContainer> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										154
									
								
								virtweb_frontend/src/routes/NetworkFiltersListRoute.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								virtweb_frontend/src/routes/NetworkFiltersListRoute.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,154 @@ | ||||
| import VisibilityIcon from "@mui/icons-material/Visibility"; | ||||
| import { | ||||
|   Button, | ||||
|   IconButton, | ||||
|   Paper, | ||||
|   Table, | ||||
|   TableBody, | ||||
|   TableCell, | ||||
|   TableContainer, | ||||
|   TableHead, | ||||
|   TableRow, | ||||
|   ToggleButton, | ||||
|   ToggleButtonGroup, | ||||
|   Typography, | ||||
| } from "@mui/material"; | ||||
| import React from "react"; | ||||
| import { useNavigate } from "react-router-dom"; | ||||
| import { | ||||
|   NWFilter, | ||||
|   NWFilterApi, | ||||
|   NWFilterIsBuiltin, | ||||
|   NWFilterURL, | ||||
| } from "../api/NWFilterApi"; | ||||
| import { AsyncWidget } from "../widgets/AsyncWidget"; | ||||
| import { RouterLink } from "../widgets/RouterLink"; | ||||
| import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; | ||||
|  | ||||
| export function NetworkFiltersListRoute(): React.ReactElement { | ||||
|   const [list, setList] = React.useState<NWFilter[] | undefined>(); | ||||
|  | ||||
|   const [count] = React.useState(1); | ||||
|  | ||||
|   const load = async () => { | ||||
|     setList(await NWFilterApi.GetList()); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <AsyncWidget | ||||
|       loadKey={count} | ||||
|       load={load} | ||||
|       ready={list !== undefined} | ||||
|       errMsg="Failed to load the list of networks!" | ||||
|       build={() => <NetworkFiltersListRouteInner list={list!} />} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| enum VisibleFilters { | ||||
|   All, | ||||
|   Builtin, | ||||
|   Custom, | ||||
| } | ||||
|  | ||||
| function NetworkFiltersListRouteInner(p: { | ||||
|   list: NWFilter[]; | ||||
| }): React.ReactElement { | ||||
|   const navigate = useNavigate(); | ||||
|  | ||||
|   const [visibleFilters, setVisibleFilters] = React.useState( | ||||
|     VisibleFilters.All | ||||
|   ); | ||||
|  | ||||
|   const filteredList = React.useMemo(() => { | ||||
|     if (visibleFilters === VisibleFilters.All) return p.list; | ||||
|  | ||||
|     const onlyBuiltin = visibleFilters === VisibleFilters.Builtin; | ||||
|  | ||||
|     return p.list.filter((f) => NWFilterIsBuiltin(f) === onlyBuiltin); | ||||
|   }, [visibleFilters]); | ||||
|  | ||||
|   return ( | ||||
|     <VirtWebRouteContainer | ||||
|       label="Network filters" | ||||
|       actions={ | ||||
|         <> | ||||
|           <span style={{ flex: 10 }}></span> | ||||
|           <ToggleButtonGroup | ||||
|             size="small" | ||||
|             value={visibleFilters} | ||||
|             exclusive | ||||
|             onChange={(_ev, v) => setVisibleFilters(v)} | ||||
|             aria-label="visible filters" | ||||
|           > | ||||
|             <ToggleButton value={VisibleFilters.All}>All</ToggleButton> | ||||
|             <ToggleButton value={VisibleFilters.Builtin}>Builtin</ToggleButton> | ||||
|             <ToggleButton value={VisibleFilters.Custom}>Custom</ToggleButton> | ||||
|           </ToggleButtonGroup> | ||||
|           <span style={{ flex: 2 }}></span> | ||||
|  | ||||
|           <RouterLink to="/nwfilter/new"> | ||||
|             <Button>New</Button> | ||||
|           </RouterLink> | ||||
|         </> | ||||
|       } | ||||
|     > | ||||
|       <TableContainer component={Paper}> | ||||
|         <Table> | ||||
|           <TableHead> | ||||
|             <TableRow> | ||||
|               <TableCell>Name</TableCell> | ||||
|               <TableCell>Chain</TableCell> | ||||
|               <TableCell>Priority</TableCell> | ||||
|               <TableCell>Referenced filters</TableCell> | ||||
|               <TableCell># of rules</TableCell> | ||||
|               <TableCell>Actions</TableCell> | ||||
|             </TableRow> | ||||
|           </TableHead> | ||||
|           <TableBody> | ||||
|             {filteredList.map((t) => { | ||||
|               return ( | ||||
|                 <TableRow | ||||
|                   key={t.uuid} | ||||
|                   hover | ||||
|                   onDoubleClick={() => navigate(NWFilterURL(t))} | ||||
|                 > | ||||
|                   <TableCell>{t.name}</TableCell> | ||||
|                   <TableCell> | ||||
|                     {t.chain?.protocol ?? ( | ||||
|                       <Typography style={{ fontStyle: "italic" }}> | ||||
|                         None | ||||
|                       </Typography> | ||||
|                     )} | ||||
|                   </TableCell> | ||||
|                   <TableCell> | ||||
|                     {t.priority ?? ( | ||||
|                       <Typography style={{ fontStyle: "italic" }}> | ||||
|                         None | ||||
|                       </Typography> | ||||
|                     )} | ||||
|                   </TableCell> | ||||
|                   <TableCell> | ||||
|                     <ul> | ||||
|                       {t.join_filters.map((f, n) => ( | ||||
|                         <li key={n}>{f}</li> | ||||
|                       ))} | ||||
|                     </ul> | ||||
|                   </TableCell> | ||||
|                   <TableCell>{t.rules.length}</TableCell> | ||||
|                   <TableCell> | ||||
|                     <RouterLink to={NWFilterURL(t)}> | ||||
|                       <IconButton> | ||||
|                         <VisibilityIcon /> | ||||
|                       </IconButton> | ||||
|                     </RouterLink> | ||||
|                   </TableCell> | ||||
|                 </TableRow> | ||||
|               ); | ||||
|             })} | ||||
|           </TableBody> | ||||
|         </Table> | ||||
|       </TableContainer> | ||||
|     </VirtWebRouteContainer> | ||||
|   ); | ||||
| } | ||||
| @@ -1,4 +1,3 @@ | ||||
| import DeleteIcon from "@mui/icons-material/Delete"; | ||||
| import VisibilityIcon from "@mui/icons-material/Visibility"; | ||||
| import { | ||||
|   Button, | ||||
| @@ -13,70 +12,36 @@ import { | ||||
|   Typography, | ||||
| } from "@mui/material"; | ||||
| import React from "react"; | ||||
| import { useNavigate } from "react-router-dom"; | ||||
| import { NetworkApi, NetworkInfo, NetworkURL } from "../api/NetworksApi"; | ||||
| import { AsyncWidget } from "../widgets/AsyncWidget"; | ||||
| import { RouterLink } from "../widgets/RouterLink"; | ||||
| import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; | ||||
| import { useConfirm } from "../hooks/providers/ConfirmDialogProvider"; | ||||
| import { useSnackbar } from "../hooks/providers/SnackbarProvider"; | ||||
| import { useAlert } from "../hooks/providers/AlertDialogProvider"; | ||||
| import { NetworkHookStatusWidget } from "../widgets/net/NetworkHookStatusWidget"; | ||||
| import { NetworkStatusWidget } from "../widgets/net/NetworkStatusWidget"; | ||||
| import { useNavigate } from "react-router-dom"; | ||||
|  | ||||
| export function NetworksListRoute(): React.ReactElement { | ||||
|   const confirm = useConfirm(); | ||||
|   const snackbar = useSnackbar(); | ||||
|   const alert = useAlert(); | ||||
|  | ||||
|   const [list, setList] = React.useState<NetworkInfo[] | undefined>(); | ||||
|  | ||||
|   const [count, setCount] = React.useState(1); | ||||
|   const [count] = React.useState(1); | ||||
|  | ||||
|   const load = async () => { | ||||
|     setList(await NetworkApi.GetList()); | ||||
|   }; | ||||
|  | ||||
|   const reload = () => { | ||||
|     setList(undefined); | ||||
|     setCount(count + 1); | ||||
|   }; | ||||
|  | ||||
|   const requestDelete = async (n: NetworkInfo) => { | ||||
|     try { | ||||
|       if ( | ||||
|         !(await confirm( | ||||
|           "Do you really want to delete this network?", | ||||
|           `Delete network ${n.name}`, | ||||
|           "Delete" | ||||
|         )) | ||||
|       ) | ||||
|         return; | ||||
|  | ||||
|       await NetworkApi.Delete(n); | ||||
|       reload(); | ||||
|       snackbar("The network was successfully deleted!"); | ||||
|     } catch (e) { | ||||
|       console.error(e); | ||||
|       alert(`Failed to delete the network!\n${e}`); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <AsyncWidget | ||||
|       loadKey={count} | ||||
|       load={load} | ||||
|       ready={list !== undefined} | ||||
|       errMsg="Failed to load the list of networks!" | ||||
|       build={() => ( | ||||
|         <NetworksListRouteInner list={list!} onRequestDelete={requestDelete} /> | ||||
|       )} | ||||
|       build={() => <NetworksListRouteInner list={list!} />} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function NetworksListRouteInner(p: { | ||||
|   list: NetworkInfo[]; | ||||
|   onRequestDelete: (n: NetworkInfo) => void; | ||||
| }): React.ReactElement { | ||||
|   const navigate = useNavigate(); | ||||
|  | ||||
| @@ -89,6 +54,8 @@ function NetworksListRouteInner(p: { | ||||
|         </RouterLink> | ||||
|       } | ||||
|     > | ||||
|       <NetworkHookStatusWidget hiddenIfInstalled /> | ||||
|  | ||||
|       <TableContainer component={Paper}> | ||||
|         <Table> | ||||
|           <TableHead> | ||||
| @@ -130,9 +97,6 @@ function NetworksListRouteInner(p: { | ||||
|                         <VisibilityIcon /> | ||||
|                       </IconButton> | ||||
|                     </RouterLink> | ||||
|                     <IconButton onClick={() => p.onRequestDelete(t)}> | ||||
|                       <DeleteIcon /> | ||||
|                     </IconButton> | ||||
|                   </TableCell> | ||||
|                 </TableRow> | ||||
|               ); | ||||
|   | ||||
| @@ -8,7 +8,6 @@ import { | ||||
| import Icon from "@mdi/react"; | ||||
| import { | ||||
|   Box, | ||||
|   Grid, | ||||
|   LinearProgress, | ||||
|   Table, | ||||
|   TableBody, | ||||
| @@ -17,7 +16,10 @@ import { | ||||
|   TableRow, | ||||
|   Typography, | ||||
| } from "@mui/material"; | ||||
| import Grid from "@mui/material/Grid2"; | ||||
| import { PieChart } from "@mui/x-charts"; | ||||
| import { filesize } from "filesize"; | ||||
| import humanizeDuration from "humanize-duration"; | ||||
| import React from "react"; | ||||
| import { | ||||
|   DiskInfo, | ||||
| @@ -28,8 +30,6 @@ import { | ||||
| import { AsyncWidget } from "../widgets/AsyncWidget"; | ||||
| import { VirtWebPaper } from "../widgets/VirtWebPaper"; | ||||
| import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; | ||||
| import humanizeDuration from "humanize-duration"; | ||||
| import { filesize } from "filesize"; | ||||
|  | ||||
| export function SysInfoRoute(): React.ReactElement { | ||||
|   const [info, setInfo] = React.useState<ServerSystemInfo>(); | ||||
| @@ -51,7 +51,7 @@ export function SysInfoRoute(): React.ReactElement { | ||||
| export function SysInfoRouteInner(p: { | ||||
|   info: ServerSystemInfo; | ||||
| }): React.ReactElement { | ||||
|   const sumDiskUsage = p.info.system.disks.reduce( | ||||
|   const sumDiskUsage = p.info.disks.reduce( | ||||
|     (prev, disk) => { | ||||
|       return { | ||||
|         used: prev.used + disk.total_space - disk.available_space, | ||||
| @@ -65,7 +65,7 @@ export function SysInfoRouteInner(p: { | ||||
|     <VirtWebRouteContainer label="Sysinfo"> | ||||
|       <Grid container spacing={2}> | ||||
|         {/* Memory */} | ||||
|         <Grid xs={4}> | ||||
|         <Grid size={{ xs: 4 }}> | ||||
|           <Box flexGrow={1}> | ||||
|             <Typography style={{ textAlign: "center" }}>Memory</Typography> | ||||
|             <PieChart | ||||
| @@ -97,7 +97,7 @@ export function SysInfoRouteInner(p: { | ||||
|         </Grid> | ||||
|  | ||||
|         {/* Disk usage */} | ||||
|         <Grid xs={4}> | ||||
|         <Grid size={{ xs: 4 }}> | ||||
|           <Box flexGrow={1}> | ||||
|             <Typography style={{ textAlign: "center" }}>Disk usage</Typography> | ||||
|             <PieChart | ||||
| @@ -125,7 +125,7 @@ export function SysInfoRouteInner(p: { | ||||
|         </Grid> | ||||
|  | ||||
|         {/* CPU usage */} | ||||
|         <Grid xs={4}> | ||||
|         <Grid size={{ xs: 4 }}> | ||||
|           <Box flexGrow={1}> | ||||
|             <Typography style={{ textAlign: "center" }}>CPU usage</Typography> | ||||
|             <PieChart | ||||
| @@ -134,13 +134,13 @@ export function SysInfoRouteInner(p: { | ||||
|                   data: [ | ||||
|                     { | ||||
|                       id: 1, | ||||
|                       value: 100 - p.info.system.global_cpu_info.cpu_usage, | ||||
|                       value: 100 - p.info.system.global_cpu_usage, | ||||
|                       label: "Free", | ||||
|                     }, | ||||
|  | ||||
|                     { | ||||
|                       id: 2, | ||||
|                       value: p.info.system.global_cpu_info.cpu_usage, | ||||
|                       value: p.info.system.global_cpu_usage, | ||||
|                       label: "Used", | ||||
|                     }, | ||||
|                   ], | ||||
| @@ -180,18 +180,18 @@ export function SysInfoRouteInner(p: { | ||||
|         label="CPU info" | ||||
|         icon={<Icon size={"1rem"} path={mdiMemory} />} | ||||
|         entries={[ | ||||
|           { label: "Brand", value: p.info.system.global_cpu_info.brand }, | ||||
|           { label: "Brand", value: p.info.system.cpus[0].brand }, | ||||
|           { | ||||
|             label: "Vendor ID", | ||||
|             value: p.info.system.global_cpu_info.vendor_id, | ||||
|             value: p.info.system.cpus[0].vendor_id, | ||||
|           }, | ||||
|           { | ||||
|             label: "CPU usage", | ||||
|             value: p.info.system.global_cpu_info.cpu_usage, | ||||
|             value: p.info.system.cpus[0].cpu_usage, | ||||
|           }, | ||||
|           { | ||||
|             label: "Name", | ||||
|             value: p.info.system.global_cpu_info.name, | ||||
|             value: p.info.system.cpus[0].name, | ||||
|           }, | ||||
|           { | ||||
|             label: "CPU model", | ||||
| @@ -227,8 +227,8 @@ export function SysInfoRouteInner(p: { | ||||
|         ]} | ||||
|       /> | ||||
|  | ||||
|       <DiskDetailsTable disks={p.info.system.disks} /> | ||||
|       <NetworksDetailsTable networks={p.info.system.networks} /> | ||||
|       <DiskDetailsTable disks={p.info.disks} /> | ||||
|       <NetworksDetailsTable networks={p.info.networks} /> | ||||
|     </VirtWebRouteContainer> | ||||
|   ); | ||||
| } | ||||
|   | ||||
							
								
								
									
										126
									
								
								virtweb_frontend/src/routes/TokensListRoute.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								virtweb_frontend/src/routes/TokensListRoute.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,126 @@ | ||||
| import VisibilityIcon from "@mui/icons-material/Visibility"; | ||||
| import { | ||||
|   Button, | ||||
|   IconButton, | ||||
|   Paper, | ||||
|   Table, | ||||
|   TableBody, | ||||
|   TableCell, | ||||
|   TableContainer, | ||||
|   TableHead, | ||||
|   TableRow, | ||||
| } from "@mui/material"; | ||||
| import React from "react"; | ||||
| import { useNavigate } from "react-router-dom"; | ||||
| import { | ||||
|   APIToken, | ||||
|   APITokenURL, | ||||
|   ExpiredAPIToken, | ||||
|   TokensApi, | ||||
| } from "../api/TokensApi"; | ||||
| import { AsyncWidget } from "../widgets/AsyncWidget"; | ||||
| import { RouterLink } from "../widgets/RouterLink"; | ||||
| import { TimeWidget, timeDiff } from "../widgets/TimeWidget"; | ||||
| import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; | ||||
|  | ||||
| export function TokensListRoute(): React.ReactElement { | ||||
|   const [list, setList] = React.useState<APIToken[] | undefined>(); | ||||
|  | ||||
|   const [count] = React.useState(1); | ||||
|  | ||||
|   const load = async () => { | ||||
|     setList(await TokensApi.GetList()); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <AsyncWidget | ||||
|       loadKey={count} | ||||
|       load={load} | ||||
|       ready={list !== undefined} | ||||
|       errMsg="Failed to load the list of tokens!" | ||||
|       build={() => <TokensListRouteInner list={list!} />} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| export function TokensListRouteInner(p: { | ||||
|   list: APIToken[]; | ||||
| }): React.ReactElement { | ||||
|   const navigate = useNavigate(); | ||||
|  | ||||
|   return ( | ||||
|     <VirtWebRouteContainer | ||||
|       label="API tokens" | ||||
|       actions={ | ||||
|         <RouterLink to="/token/new"> | ||||
|           <Button>New</Button> | ||||
|         </RouterLink> | ||||
|       } | ||||
|     > | ||||
|       <TableContainer component={Paper}> | ||||
|         <Table> | ||||
|           <TableHead> | ||||
|             <TableRow> | ||||
|               <TableCell>Name</TableCell> | ||||
|               <TableCell>Description</TableCell> | ||||
|               <TableCell>Created</TableCell> | ||||
|               <TableCell>Updated</TableCell> | ||||
|               <TableCell>Last used</TableCell> | ||||
|               <TableCell>IP restriction</TableCell> | ||||
|               <TableCell>Max inactivity</TableCell> | ||||
|               <TableCell>Rights</TableCell> | ||||
|               <TableCell>Actions</TableCell> | ||||
|             </TableRow> | ||||
|           </TableHead> | ||||
|           <TableBody> | ||||
|             {p.list.map((t) => { | ||||
|               return ( | ||||
|                 <TableRow | ||||
|                   key={t.id} | ||||
|                   hover | ||||
|                   onDoubleClick={() => navigate(APITokenURL(t))} | ||||
|                   style={{ backgroundColor: ExpiredAPIToken(t) ? "red" : "" }} | ||||
|                 > | ||||
|                   <TableCell> | ||||
|                     {t.name} {ExpiredAPIToken(t) && <i>(Expired)</i>} | ||||
|                   </TableCell> | ||||
|                   <TableCell>{t.description}</TableCell> | ||||
|                   <TableCell> | ||||
|                     <TimeWidget time={t.created} /> | ||||
|                   </TableCell> | ||||
|                   <TableCell> | ||||
|                     <TimeWidget time={t.updated} /> | ||||
|                   </TableCell> | ||||
|                   <TableCell> | ||||
|                     <TimeWidget time={t.last_used} /> | ||||
|                   </TableCell> | ||||
|                   <TableCell>{t.ip_restriction}</TableCell> | ||||
|                   <TableCell> | ||||
|                     {t.max_inactivity && timeDiff(0, t.max_inactivity)} | ||||
|                   </TableCell> | ||||
|                   <TableCell> | ||||
|                     {t.rights.map((r) => { | ||||
|                       return ( | ||||
|                         <div> | ||||
|                           {r.verb} {r.path} | ||||
|                         </div> | ||||
|                       ); | ||||
|                     })} | ||||
|                   </TableCell> | ||||
|  | ||||
|                   <TableCell> | ||||
|                     <RouterLink to={APITokenURL(t)}> | ||||
|                       <IconButton> | ||||
|                         <VisibilityIcon /> | ||||
|                       </IconButton> | ||||
|                     </RouterLink> | ||||
|                   </TableCell> | ||||
|                 </TableRow> | ||||
|               ); | ||||
|             })} | ||||
|           </TableBody> | ||||
|         </Table> | ||||
|       </TableContainer> | ||||
|     </VirtWebRouteContainer> | ||||
|   ); | ||||
| } | ||||
| @@ -1,4 +1,5 @@ | ||||
| import DeleteIcon from "@mui/icons-material/Delete"; | ||||
| import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; | ||||
| import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; | ||||
| import VisibilityIcon from "@mui/icons-material/Visibility"; | ||||
| import { | ||||
|   Button, | ||||
| @@ -8,29 +9,35 @@ import { | ||||
|   TableBody, | ||||
|   TableCell, | ||||
|   TableContainer, | ||||
|   TableFooter, | ||||
|   TableHead, | ||||
|   TableRow, | ||||
|   Tooltip, | ||||
| } from "@mui/material"; | ||||
| import { filesize } from "filesize"; | ||||
| import React from "react"; | ||||
| import { VMApi, VMInfo } from "../api/VMApi"; | ||||
| import { useNavigate } from "react-router-dom"; | ||||
| import { GroupApi } from "../api/GroupApi"; | ||||
| import { VMApi, VMInfo, VMState } from "../api/VMApi"; | ||||
| import { AsyncWidget } from "../widgets/AsyncWidget"; | ||||
| import { RouterLink } from "../widgets/RouterLink"; | ||||
| import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; | ||||
| import { VMStatusWidget } from "../widgets/vms/VMStatusWidget"; | ||||
| import { useSnackbar } from "../hooks/providers/SnackbarProvider"; | ||||
| import { useConfirm } from "../hooks/providers/ConfirmDialogProvider"; | ||||
| import { useNavigate } from "react-router-dom"; | ||||
| import { useAlert } from "../hooks/providers/AlertDialogProvider"; | ||||
|  | ||||
| export function VMListRoute(): React.ReactElement { | ||||
|   const [groups, setGroups] = React.useState<Array<string | undefined>>(); | ||||
|   const [list, setList] = React.useState<VMInfo[] | undefined>(); | ||||
|  | ||||
|   const loadKey = React.useRef(1); | ||||
|  | ||||
|   const load = async () => { | ||||
|     setList(await VMApi.GetList()); | ||||
|     const groups: Array<string | undefined> = await GroupApi.GetList(); | ||||
|     const list = await VMApi.GetList(); | ||||
|  | ||||
|     if (list.find((v) => !v.group) !== undefined) groups.push(undefined); | ||||
|  | ||||
|     setGroups(groups); | ||||
|     setList(list); | ||||
|   }; | ||||
|  | ||||
|   const reload = () => { | ||||
| @@ -55,7 +62,7 @@ export function VMListRoute(): React.ReactElement { | ||||
|             </> | ||||
|           } | ||||
|         > | ||||
|           <VMListWidget list={list!} onReload={reload} /> | ||||
|           <VMListWidget list={list!} groups={groups!} onReload={reload} /> | ||||
|         </VirtWebRouteContainer> | ||||
|       )} | ||||
|     /> | ||||
| @@ -63,40 +70,35 @@ export function VMListRoute(): React.ReactElement { | ||||
| } | ||||
|  | ||||
| function VMListWidget(p: { | ||||
|   groups: Array<string | undefined>; | ||||
|   list: VMInfo[]; | ||||
|   onReload: () => void; | ||||
| }): React.ReactElement { | ||||
|   const confirm = useConfirm(); | ||||
|   const alert = useAlert(); | ||||
|   const snackbar = useSnackbar(); | ||||
|   const navigate = useNavigate(); | ||||
|  | ||||
|   const deleteVM = async (v: VMInfo) => { | ||||
|     try { | ||||
|       if ( | ||||
|         !(await confirm( | ||||
|           `Do you really want to delete the vm ${v.name}? The operation CANNOT be undone!`, | ||||
|           "Delete a VM", | ||||
|           "DELETE" | ||||
|         )) | ||||
|       ) | ||||
|         return; | ||||
|   const [hiddenGroups, setHiddenGroups] = React.useState< | ||||
|     Set<string | undefined> | ||||
|   >(new Set()); | ||||
|  | ||||
|       const keepData = !(await confirm( | ||||
|         "Do you want to delete the files of the VM?", | ||||
|         "Delete a VM", | ||||
|         "Delete the data", | ||||
|         "keep the data" | ||||
|       )); | ||||
|   const [runningVMs, setRunningVMs] = React.useState<Set<string>>(new Set()); | ||||
|  | ||||
|       await VMApi.Delete(v, keepData); | ||||
|       snackbar("The VM was successfully deleted!"); | ||||
|   const toggleHiddenGroup = (g: string | undefined) => { | ||||
|     if (hiddenGroups.has(g)) hiddenGroups.delete(g); | ||||
|     else hiddenGroups.add(g); | ||||
|  | ||||
|       p.onReload(); | ||||
|     } catch (e) { | ||||
|       console.error(e); | ||||
|       alert(`Failed to delete VM!\n${e}`); | ||||
|     setHiddenGroups(new Set([...hiddenGroups])); | ||||
|   }; | ||||
|  | ||||
|   const updateVMState = (v: VMInfo, s: VMState) => { | ||||
|     const running = s !== "Shutoff"; | ||||
|     if (runningVMs.has(v.name) === running) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     if (running) runningVMs.add(v.name); | ||||
|     else runningVMs.delete(v.name); | ||||
|  | ||||
|     setRunningVMs(new Set([...runningVMs])); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
| @@ -107,44 +109,100 @@ function VMListWidget(p: { | ||||
|             <TableCell>Name</TableCell> | ||||
|             <TableCell>Description</TableCell> | ||||
|             <TableCell>Memory</TableCell> | ||||
|             <TableCell>vCPU</TableCell> | ||||
|             <TableCell>Status</TableCell> | ||||
|             <TableCell>Actions</TableCell> | ||||
|           </TableRow> | ||||
|         </TableHead> | ||||
|         <TableBody> | ||||
|           {p.list.map((row) => ( | ||||
|             <TableRow | ||||
|               hover | ||||
|               key={row.name} | ||||
|               sx={{ "&:last-child td, &:last-child th": { border: 0 } }} | ||||
|               onDoubleClick={() => navigate(row.ViewURL)} | ||||
|             > | ||||
|               <TableCell component="th" scope="row"> | ||||
|                 {row.name} | ||||
|               </TableCell> | ||||
|               <TableCell>{row.description ?? ""}</TableCell> | ||||
|               <TableCell>{filesize(row.memory * 1000 * 1000)}</TableCell> | ||||
|               <TableCell> | ||||
|                 <VMStatusWidget vm={row} /> | ||||
|               </TableCell> | ||||
|               <TableCell> | ||||
|                 <Tooltip title="View this VM"> | ||||
|                   <RouterLink to={row.ViewURL}> | ||||
|                     <IconButton> | ||||
|                       <VisibilityIcon /> | ||||
|           {p.groups.map((g, num) => ( | ||||
|             <React.Fragment key={num}> | ||||
|               {p.groups.length > 1 && ( | ||||
|                 <TableRow> | ||||
|                   <TableCell | ||||
|                     style={{ paddingBottom: 2, paddingTop: 2 }} | ||||
|                     colSpan={6} | ||||
|                   > | ||||
|                     <IconButton | ||||
|                       size="small" | ||||
|                       onClick={() => toggleHiddenGroup(g)} | ||||
|                     > | ||||
|                       {!hiddenGroups?.has(g) ? ( | ||||
|                         <KeyboardArrowUpIcon /> | ||||
|                       ) : ( | ||||
|                         <KeyboardArrowDownIcon /> | ||||
|                       )} | ||||
|                     </IconButton> | ||||
|                   </RouterLink> | ||||
|                 </Tooltip> | ||||
|                 <Tooltip title="Delete this VM"> | ||||
|                   <IconButton onClick={() => deleteVM(row)}> | ||||
|                     <DeleteIcon /> | ||||
|                   </IconButton> | ||||
|                 </Tooltip> | ||||
|               </TableCell> | ||||
|             </TableRow> | ||||
|                     {g ?? "default"} | ||||
|                   </TableCell> | ||||
|                 </TableRow> | ||||
|               )} | ||||
|  | ||||
|               {!hiddenGroups.has(g) && | ||||
|                 p.list | ||||
|                   .filter((row) => row.group === g) | ||||
|                   .map((row) => ( | ||||
|                     <TableRow | ||||
|                       hover | ||||
|                       key={row.name} | ||||
|                       sx={{ "&:last-child td, &:last-child th": { border: 0 } }} | ||||
|                       onDoubleClick={() => navigate(row.ViewURL)} | ||||
|                     > | ||||
|                       <TableCell component="th" scope="row"> | ||||
|                         {row.name} | ||||
|                       </TableCell> | ||||
|                       <TableCell>{row.description ?? ""}</TableCell> | ||||
|                       <TableCell>{vmMemoryToHuman(row.memory)}</TableCell> | ||||
|                       <TableCell>{row.number_vcpu}</TableCell> | ||||
|                       <TableCell> | ||||
|                         <VMStatusWidget | ||||
|                           vm={row} | ||||
|                           onChange={(s) => updateVMState(row, s)} | ||||
|                         /> | ||||
|                       </TableCell> | ||||
|                       <TableCell> | ||||
|                         <Tooltip title="View this VM"> | ||||
|                           <RouterLink to={row.ViewURL}> | ||||
|                             <IconButton> | ||||
|                               <VisibilityIcon /> | ||||
|                             </IconButton> | ||||
|                           </RouterLink> | ||||
|                         </Tooltip> | ||||
|                       </TableCell> | ||||
|                     </TableRow> | ||||
|                   ))} | ||||
|             </React.Fragment> | ||||
|           ))} | ||||
|         </TableBody> | ||||
|         <TableFooter> | ||||
|           <TableRow> | ||||
|             <TableCell></TableCell> | ||||
|             <TableCell></TableCell> | ||||
|             <TableCell> | ||||
|               {vmMemoryToHuman( | ||||
|                 p.list | ||||
|                   .filter((v) => runningVMs.has(v.name)) | ||||
|                   .reduce((s, v) => s + v.memory, 0) | ||||
|               )} | ||||
|               {" / "} | ||||
|               {vmMemoryToHuman(p.list.reduce((s, v) => s + v.memory, 0))} | ||||
|             </TableCell> | ||||
|             <TableCell> | ||||
|               {p.list | ||||
|                 .filter((v) => runningVMs.has(v.name)) | ||||
|                 .reduce((s, v) => s + v.number_vcpu, 0)} | ||||
|               {" / "} | ||||
|               {p.list.reduce((s, v) => s + v.number_vcpu, 0)} | ||||
|             </TableCell> | ||||
|             <TableCell></TableCell> | ||||
|             <TableCell></TableCell> | ||||
|           </TableRow> | ||||
|         </TableFooter> | ||||
|       </Table> | ||||
|     </TableContainer> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function vmMemoryToHuman(size: number): string { | ||||
|   return filesize(size * 1000 * 1000); | ||||
| } | ||||
|   | ||||
| @@ -1,14 +1,12 @@ | ||||
| import { Button } from "@mui/material"; | ||||
| import React from "react"; | ||||
| import { useNavigate, useParams } from "react-router-dom"; | ||||
| import { VMApi, VMInfo, VMState } from "../api/VMApi"; | ||||
| import React from "react"; | ||||
| import { AsyncWidget } from "../widgets/AsyncWidget"; | ||||
| import { ConfigImportExportButtons } from "../widgets/ConfigImportExportButtons"; | ||||
| import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; | ||||
| import { VMDetails } from "../widgets/vms/VMDetails"; | ||||
| import { VMStatusWidget } from "../widgets/vms/VMStatusWidget"; | ||||
| import { Button, IconButton } from "@mui/material"; | ||||
| import Icon from "@mdi/react"; | ||||
| import { mdiXml } from "@mdi/js"; | ||||
| import { RouterLink } from "../widgets/RouterLink"; | ||||
|  | ||||
| export function VMRoute(): React.ReactElement { | ||||
|   const { uuid } = useParams(); | ||||
| @@ -41,11 +39,10 @@ function VMRouteBody(p: { vm: VMInfo }): React.ReactElement { | ||||
|         <span style={{ display: "inline-flex", alignItems: "center" }}> | ||||
|           <VMStatusWidget vm={p.vm} onChange={setState} /> | ||||
|  | ||||
|           <RouterLink to={p.vm.XMLURL}> | ||||
|             <IconButton size="small"> | ||||
|               <Icon path={mdiXml} style={{ width: "1em" }} /> | ||||
|             </IconButton> | ||||
|           </RouterLink> | ||||
|           <ConfigImportExportButtons | ||||
|             filename={`vm-${p.vm.name}.json`} | ||||
|             currentConf={p.vm} | ||||
|           /> | ||||
|  | ||||
|           {(state === "Shutdown" || state === "Shutoff") && ( | ||||
|             <Button | ||||
|   | ||||
| @@ -1,47 +0,0 @@ | ||||
| import React from "react"; | ||||
| import { useParams } from "react-router-dom"; | ||||
| import { VMApi, VMInfo } from "../api/VMApi"; | ||||
| import { AsyncWidget } from "../widgets/AsyncWidget"; | ||||
| import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; | ||||
| import { IconButton } from "@mui/material"; | ||||
| import { RouterLink } from "../widgets/RouterLink"; | ||||
| import ArrowBackIcon from "@mui/icons-material/ArrowBack"; | ||||
| import { XMLWidget } from "../widgets/XMLWidget"; | ||||
|  | ||||
| export function VMXMLRoute(): React.ReactElement { | ||||
|   const { uuid } = useParams(); | ||||
|  | ||||
|   const [vm, setVM] = React.useState<VMInfo | undefined>(); | ||||
|   const [src, setSrc] = React.useState<string | undefined>(); | ||||
|  | ||||
|   const load = async () => { | ||||
|     setVM(await VMApi.GetSingle(uuid!)); | ||||
|     setSrc(await VMApi.GetSingleXML(uuid!)); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <AsyncWidget | ||||
|       loadKey={uuid} | ||||
|       load={load} | ||||
|       errMsg="Failed to load VM information!" | ||||
|       build={() => <XMLRouteInner vm={vm!} src={src!} />} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function XMLRouteInner(p: { vm: VMInfo; src: string }): React.ReactElement { | ||||
|   return ( | ||||
|     <VirtWebRouteContainer | ||||
|       label={`XML definition of ${p.vm.name}`} | ||||
|       actions={ | ||||
|         <RouterLink to={p.vm.ViewURL}> | ||||
|           <IconButton> | ||||
|             <ArrowBackIcon /> | ||||
|           </IconButton> | ||||
|         </RouterLink> | ||||
|       } | ||||
|     > | ||||
|       <XMLWidget src={p.src} /> | ||||
|     </VirtWebRouteContainer> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										53
									
								
								virtweb_frontend/src/routes/ViewApiTokenRoute.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								virtweb_frontend/src/routes/ViewApiTokenRoute.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| import { Button } from "@mui/material"; | ||||
| import React from "react"; | ||||
| import { useNavigate, useParams } from "react-router-dom"; | ||||
| import { APIToken, APITokenURL, TokensApi } from "../api/TokensApi"; | ||||
| import { AsyncWidget } from "../widgets/AsyncWidget"; | ||||
| import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; | ||||
| import { | ||||
|   APITokenDetails, | ||||
|   TokenWidgetStatus, | ||||
| } from "../widgets/tokens/APITokenDetails"; | ||||
|  | ||||
| export function ViewApiTokenRoute() { | ||||
|   const { id } = useParams(); | ||||
|  | ||||
|   const [token, setToken] = React.useState<APIToken | undefined>(); | ||||
|  | ||||
|   const load = async () => { | ||||
|     setToken(await TokensApi.GetSingle(id!)); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <AsyncWidget | ||||
|       loadKey={id} | ||||
|       ready={token !== undefined} | ||||
|       errMsg="Failed to fetch API token information!" | ||||
|       load={load} | ||||
|       build={() => <ViewAPITokenRouteInner token={token!} />} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function ViewAPITokenRouteInner(p: { token: APIToken }): React.ReactElement { | ||||
|   const navigate = useNavigate(); | ||||
|  | ||||
|   return ( | ||||
|     <VirtWebRouteContainer | ||||
|       label={`API token ${p.token.name}`} | ||||
|       actions={ | ||||
|         <span style={{ display: "flex", alignItems: "center" }}> | ||||
|           <Button | ||||
|             variant="contained" | ||||
|             style={{ marginLeft: "15px" }} | ||||
|             onClick={() => navigate(APITokenURL(p.token, true))} | ||||
|           > | ||||
|             Edit | ||||
|           </Button> | ||||
|         </span> | ||||
|       } | ||||
|     > | ||||
|       <APITokenDetails token={p.token} status={TokenWidgetStatus.Read} /> | ||||
|     </VirtWebRouteContainer> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										65
									
								
								virtweb_frontend/src/routes/ViewNWFilterRoute.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								virtweb_frontend/src/routes/ViewNWFilterRoute.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | ||||
| import { Button } from "@mui/material"; | ||||
| import React from "react"; | ||||
| import { useNavigate, useParams } from "react-router-dom"; | ||||
| import { | ||||
|   NWFilter, | ||||
|   NWFilterApi, | ||||
|   NWFilterIsBuiltin, | ||||
|   NWFilterURL, | ||||
| } from "../api/NWFilterApi"; | ||||
| import { AsyncWidget } from "../widgets/AsyncWidget"; | ||||
| import { ConfigImportExportButtons } from "../widgets/ConfigImportExportButtons"; | ||||
| import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; | ||||
| import { NWFilterDetails } from "../widgets/nwfilter/NWFilterDetails"; | ||||
|  | ||||
| export function ViewNWFilterRoute() { | ||||
|   const { uuid } = useParams(); | ||||
|  | ||||
|   const [nwfilter, setNWFilter] = React.useState<NWFilter | undefined>(); | ||||
|  | ||||
|   const load = async () => { | ||||
|     setNWFilter(await NWFilterApi.GetSingle(uuid!)); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <AsyncWidget | ||||
|       loadKey={uuid} | ||||
|       ready={nwfilter !== undefined} | ||||
|       errMsg="Failed to fetch network filter information!" | ||||
|       load={load} | ||||
|       build={() => <ViewNetworkFilterRouteInner nwfilter={nwfilter!} />} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function ViewNetworkFilterRouteInner(p: { | ||||
|   nwfilter: NWFilter; | ||||
| }): React.ReactElement { | ||||
|   const navigate = useNavigate(); | ||||
|  | ||||
|   return ( | ||||
|     <VirtWebRouteContainer | ||||
|       label={`Network filter ${p.nwfilter.name}`} | ||||
|       actions={ | ||||
|         <span style={{ display: "flex", alignItems: "center" }}> | ||||
|           <ConfigImportExportButtons | ||||
|             filename={`nwfilter-${p.nwfilter.name}.json`} | ||||
|             currentConf={p.nwfilter} | ||||
|           /> | ||||
|  | ||||
|           {!NWFilterIsBuiltin(p.nwfilter) && ( | ||||
|             <Button | ||||
|               variant="contained" | ||||
|               style={{ marginLeft: "15px" }} | ||||
|               onClick={() => navigate(NWFilterURL(p.nwfilter, true))} | ||||
|             > | ||||
|               Edit | ||||
|             </Button> | ||||
|           )} | ||||
|         </span> | ||||
|       } | ||||
|     > | ||||
|       <NWFilterDetails nwfilter={p.nwfilter} editable={false} /> | ||||
|     </VirtWebRouteContainer> | ||||
|   ); | ||||
| } | ||||
| @@ -1,6 +1,4 @@ | ||||
| import { mdiXml } from "@mdi/js"; | ||||
| import Icon from "@mdi/react"; | ||||
| import { Button, IconButton } from "@mui/material"; | ||||
| import { Button } from "@mui/material"; | ||||
| import React from "react"; | ||||
| import { useNavigate, useParams } from "react-router-dom"; | ||||
| import { | ||||
| @@ -8,10 +6,9 @@ import { | ||||
|   NetworkInfo, | ||||
|   NetworkStatus, | ||||
|   NetworkURL, | ||||
|   NetworkXMLURL, | ||||
| } from "../api/NetworksApi"; | ||||
| import { AsyncWidget } from "../widgets/AsyncWidget"; | ||||
| import { RouterLink } from "../widgets/RouterLink"; | ||||
| import { ConfigImportExportButtons } from "../widgets/ConfigImportExportButtons"; | ||||
| import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; | ||||
| import { NetworkDetails } from "../widgets/net/NetworkDetails"; | ||||
| import { NetworkStatusWidget } from "../widgets/net/NetworkStatusWidget"; | ||||
| @@ -47,14 +44,13 @@ function ViewNetworkRouteInner(p: { | ||||
|     <VirtWebRouteContainer | ||||
|       label={`Network ${p.network.name}`} | ||||
|       actions={ | ||||
|         <span> | ||||
|         <span style={{ display: "flex", alignItems: "center" }}> | ||||
|           <NetworkStatusWidget net={p.network} onChange={setNetStatus} /> | ||||
|  | ||||
|           <RouterLink to={NetworkXMLURL(p.network)}> | ||||
|             <IconButton size="small"> | ||||
|               <Icon path={mdiXml} style={{ width: "1em" }} /> | ||||
|             </IconButton> | ||||
|           </RouterLink> | ||||
|           <ConfigImportExportButtons | ||||
|             filename={`net-${p.network.name}.json`} | ||||
|             currentConf={p.network} | ||||
|           /> | ||||
|  | ||||
|           {netStatus === "Stopped" && ( | ||||
|             <Button | ||||
|   | ||||
							
								
								
									
										5
									
								
								virtweb_frontend/src/utils/DebugUtils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								virtweb_frontend/src/utils/DebugUtils.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| export function isDebug(): boolean { | ||||
|   return ( | ||||
|     !import.meta.env.NODE_ENV || import.meta.env.NODE_ENV === "development" | ||||
|   ); | ||||
| } | ||||
| @@ -1,9 +1,11 @@ | ||||
| import { | ||||
|   mdiApi, | ||||
|   mdiBoxShadow, | ||||
|   mdiDisc, | ||||
|   mdiHome, | ||||
|   mdiInformation, | ||||
|   mdiLan | ||||
|   mdiLan, | ||||
|   mdiSecurityNetwork, | ||||
| } from "@mdi/js"; | ||||
| import Icon from "@mdi/react"; | ||||
| import { | ||||
| @@ -15,6 +17,7 @@ import { | ||||
|   ListItemText, | ||||
| } from "@mui/material"; | ||||
| import { Outlet, useLocation } from "react-router-dom"; | ||||
| import { isDebug } from "../utils/DebugUtils"; | ||||
| import { RouterLink } from "./RouterLink"; | ||||
| import { VirtWebAppBar } from "./VirtWebAppBar"; | ||||
|  | ||||
| @@ -60,11 +63,21 @@ export function BaseAuthenticatedPage(): React.ReactElement { | ||||
|             uri="/net" | ||||
|             icon={<Icon path={mdiLan} size={1} />} | ||||
|           /> | ||||
|           <NavLink | ||||
|             label="Network filters" | ||||
|             uri="/nwfilter" | ||||
|             icon={<Icon path={mdiSecurityNetwork} size={1} />} | ||||
|           /> | ||||
|           <NavLink | ||||
|             label="ISO files" | ||||
|             uri="/iso" | ||||
|             icon={<Icon path={mdiDisc} size={1} />} | ||||
|           /> | ||||
|           <NavLink | ||||
|             label="API tokens" | ||||
|             uri="/tokens" | ||||
|             icon={<Icon path={mdiApi} size={1} />} | ||||
|           /> | ||||
|           <NavLink | ||||
|             label="Sysinfo" | ||||
|             uri="/sysinfo" | ||||
|   | ||||
| @@ -3,7 +3,7 @@ import Icon from "@mdi/react"; | ||||
| import Avatar from "@mui/material/Avatar"; | ||||
| import Box from "@mui/material/Box"; | ||||
| import CssBaseline from "@mui/material/CssBaseline"; | ||||
| import Grid from "@mui/material/Grid"; | ||||
| import Grid from "@mui/material/Grid2"; | ||||
| import Paper from "@mui/material/Paper"; | ||||
| import Typography from "@mui/material/Typography"; | ||||
| import { Link, Outlet } from "react-router-dom"; | ||||
| @@ -38,10 +38,7 @@ export function BaseLoginPage() { | ||||
|     <Grid container component="main" sx={{ height: "100vh" }}> | ||||
|       <CssBaseline /> | ||||
|       <Grid | ||||
|         item | ||||
|         xs={false} | ||||
|         sm={4} | ||||
|         md={7} | ||||
|         size={{ xs: false, sm: 4, md: 7 }} | ||||
|         sx={{ | ||||
|           backgroundImage: "url(/login_splash.jpg)", | ||||
|           backgroundRepeat: "no-repeat", | ||||
| @@ -53,7 +50,12 @@ export function BaseLoginPage() { | ||||
|           backgroundPosition: "center", | ||||
|         }} | ||||
|       /> | ||||
|       <Grid item xs={12} sm={8} md={5} component={Paper} elevation={6} square> | ||||
|       <Grid | ||||
|         size={{ xs: 12, sm: 8, md: 5 }} | ||||
|         component={Paper} | ||||
|         elevation={6} | ||||
|         square | ||||
|       > | ||||
|         <Box | ||||
|           sx={{ | ||||
|             my: 8, | ||||
|   | ||||
							
								
								
									
										74
									
								
								virtweb_frontend/src/widgets/ConfigImportExportButtons.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								virtweb_frontend/src/widgets/ConfigImportExportButtons.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | ||||
| import FolderOpenIcon from "@mui/icons-material/FolderOpen"; | ||||
| import IosShareIcon from "@mui/icons-material/IosShare"; | ||||
| import { IconButton, Tooltip } from "@mui/material"; | ||||
| import { useAlert } from "../hooks/providers/AlertDialogProvider"; | ||||
|  | ||||
| export function ConfigImportExportButtons(p: { | ||||
|   filename: string; | ||||
|   currentConf: any; | ||||
|   importConf?: (content: any) => any; | ||||
| }): React.ReactElement { | ||||
|   const alert = useAlert(); | ||||
|  | ||||
|   const exportConf = () => { | ||||
|     const conf = JSON.stringify(p.currentConf); | ||||
|     const blob = new Blob([conf], { type: "application/json" }); | ||||
|  | ||||
|     const a = document.createElement("a"); | ||||
|     a.href = window.URL.createObjectURL(blob); | ||||
|     a.download = p.filename; | ||||
|     document.body.appendChild(a); | ||||
|     a.click(); | ||||
|     document.body.removeChild(a); | ||||
|   }; | ||||
|  | ||||
|   const importConf = async () => { | ||||
|     try { | ||||
|       // Create file element | ||||
|       const fileEl = document.createElement("input"); | ||||
|       fileEl.type = "file"; | ||||
|       fileEl.accept = "application/json"; | ||||
|       fileEl.click(); | ||||
|  | ||||
|       // Wait for a file to be chosen | ||||
|       await new Promise((res, _rej) => | ||||
|         fileEl.addEventListener("change", () => res(null)) | ||||
|       ); | ||||
|  | ||||
|       if ((fileEl.files?.length ?? 0) === 0) return null; | ||||
|  | ||||
|       // Import conf | ||||
|       let file = fileEl.files![0]; | ||||
|       const content = await file.text(); | ||||
|       p.importConf?.(JSON.parse(content)); | ||||
|     } catch (e) { | ||||
|       console.error(e); | ||||
|       alert(`Failed to load config from file!\n${e}`); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <Tooltip title={"Export current config"}> | ||||
|         <IconButton | ||||
|           onClick={exportConf} | ||||
|           size="small" | ||||
|           style={{ paddingBottom: "0px", paddingTop: "0px" }} | ||||
|         > | ||||
|           <IosShareIcon /> | ||||
|         </IconButton> | ||||
|       </Tooltip> | ||||
|       {p.importConf && ( | ||||
|         <Tooltip title={"Import config from file"}> | ||||
|           <IconButton | ||||
|             onClick={importConf} | ||||
|             size="small" | ||||
|             style={{ paddingBottom: "0px", paddingTop: "0px" }} | ||||
|           > | ||||
|             <FolderOpenIcon /> | ||||
|           </IconButton> | ||||
|         </Tooltip> | ||||
|       )} | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										30
									
								
								virtweb_frontend/src/widgets/CopyToClipboard.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								virtweb_frontend/src/widgets/CopyToClipboard.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| import { ButtonBase } from "@mui/material"; | ||||
| import { PropsWithChildren } from "react"; | ||||
| import { useSnackbar } from "../hooks/providers/SnackbarProvider"; | ||||
|  | ||||
| export function CopyToClipboard( | ||||
|   p: PropsWithChildren<{ content: string }> | ||||
| ): React.ReactElement { | ||||
|   const snackbar = useSnackbar(); | ||||
|  | ||||
|   const copy = () => { | ||||
|     navigator.clipboard.writeText(p.content); | ||||
|     snackbar(`${p.content} copied to clipboard.`); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <ButtonBase | ||||
|       onClick={copy} | ||||
|       style={{ | ||||
|         display: "inline-block", | ||||
|         alignItems: "unset", | ||||
|         textAlign: "unset", | ||||
|         position: "relative", | ||||
|         padding: "0px", | ||||
|       }} | ||||
|       disableRipple | ||||
|     > | ||||
|       {p.children} | ||||
|     </ButtonBase> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										18
									
								
								virtweb_frontend/src/widgets/InlineCode.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								virtweb_frontend/src/widgets/InlineCode.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| export function InlineCode(p: React.PropsWithChildren): React.ReactElement { | ||||
|   return ( | ||||
|     <code | ||||
|       style={{ | ||||
|         display: "inline-block", | ||||
|         backgroundColor: "black", | ||||
|         color: "white", | ||||
|         wordBreak: "break-all", | ||||
|         wordWrap: "break-word", | ||||
|         whiteSpace: "pre-wrap", | ||||
|         padding: "0px 7px", | ||||
|         borderRadius: "5px", | ||||
|       }} | ||||
|     > | ||||
|       {p.children} | ||||
|     </code> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										35
									
								
								virtweb_frontend/src/widgets/TabsWidget.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								virtweb_frontend/src/widgets/TabsWidget.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| import { Box, Tab, Tabs } from "@mui/material"; | ||||
|  | ||||
| export interface TabWidgetOption<E> { | ||||
|   label: string; | ||||
|   value: E; | ||||
|   visible: boolean; | ||||
|   color?: string; | ||||
| } | ||||
|  | ||||
| export function TabsWidget<E>(p: { | ||||
|   currTab: E; | ||||
|   options: TabWidgetOption<E>[]; | ||||
|   onTabChange: (v: E) => void; | ||||
| }): React.ReactElement { | ||||
|   const activeOptions = p.options.filter((v) => v.visible); | ||||
|  | ||||
|   const currTabIndex = activeOptions.findIndex((v) => v.value === p.currTab); | ||||
|  | ||||
|   const updateActiveTab = (index: number) => { | ||||
|     p.onTabChange(activeOptions[index].value); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <Box sx={{ borderBottom: 1, borderColor: "divider" }}> | ||||
|       <Tabs | ||||
|         value={currTabIndex} | ||||
|         onChange={(_ev, newVal) => updateActiveTab(newVal)} | ||||
|       > | ||||
|         {activeOptions.map((o, index) => ( | ||||
|           <Tab key={index} label={o.label} style={{ color: o.color }} /> | ||||
|         ))} | ||||
|       </Tabs> | ||||
|     </Box> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										65
									
								
								virtweb_frontend/src/widgets/TimeWidget.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								virtweb_frontend/src/widgets/TimeWidget.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | ||||
| import { Tooltip } from "@mui/material"; | ||||
| import date from "date-and-time"; | ||||
| import { time } from "../utils/DateUtils"; | ||||
|  | ||||
| export function formatDate(time: number): string { | ||||
|   const t = new Date(); | ||||
|   t.setTime(1000 * time); | ||||
|   return date.format(t, "DD/MM/YYYY HH:mm:ss"); | ||||
| } | ||||
|  | ||||
| export function timeDiff(a: number, b: number): string { | ||||
|   let diff = b - a; | ||||
|  | ||||
|   if (diff === 0) return "now"; | ||||
|   if (diff === 1) return "1 second"; | ||||
|  | ||||
|   if (diff < 60) { | ||||
|     return `${diff} seconds`; | ||||
|   } | ||||
|  | ||||
|   diff = Math.floor(diff / 60); | ||||
|  | ||||
|   if (diff === 1) return "1 minute"; | ||||
|   if (diff < 24) { | ||||
|     return `${diff} minutes`; | ||||
|   } | ||||
|  | ||||
|   diff = Math.floor(diff / 60); | ||||
|  | ||||
|   if (diff === 1) return "1 hour"; | ||||
|   if (diff < 24) { | ||||
|     return `${diff} hours`; | ||||
|   } | ||||
|  | ||||
|   const diffDays = Math.floor(diff / 24); | ||||
|  | ||||
|   if (diffDays === 1) return "1 day"; | ||||
|   if (diffDays < 31) { | ||||
|     return `${diffDays} days`; | ||||
|   } | ||||
|  | ||||
|   diff = Math.floor(diffDays / 31); | ||||
|  | ||||
|   if (diff < 12) { | ||||
|     return `${diff} month`; | ||||
|   } | ||||
|  | ||||
|   const diffYears = Math.floor(diffDays / 365); | ||||
|  | ||||
|   if (diffYears === 1) return "1 year"; | ||||
|   return `${diffYears} years`; | ||||
| } | ||||
|  | ||||
| export function timeDiffFromNow(t: number): string { | ||||
|   return timeDiff(t, time()); | ||||
| } | ||||
|  | ||||
| export function TimeWidget(p: { time?: number }): React.ReactElement { | ||||
|   if (!p.time) return <></>; | ||||
|   return ( | ||||
|     <Tooltip title={formatDate(p.time)}> | ||||
|       <span>{timeDiffFromNow(p.time)}</span> | ||||
|     </Tooltip> | ||||
|   ); | ||||
| } | ||||
| @@ -3,6 +3,8 @@ import { dracula } from "react-syntax-highlighter/dist/esm/styles/hljs"; | ||||
| import xmlFormat from "xml-formatter"; | ||||
|  | ||||
| import { Light as SyntaxHighlighter } from "react-syntax-highlighter"; | ||||
| import { AsyncWidget } from "./AsyncWidget"; | ||||
| import React from "react"; | ||||
|  | ||||
| SyntaxHighlighter.registerLanguage("xml", xml); | ||||
|  | ||||
| @@ -19,3 +21,24 @@ export function XMLWidget(p: { src: string }): React.ReactElement { | ||||
|     </SyntaxHighlighter> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| export function XMLAsyncWidget(p: { | ||||
|   identifier: string; | ||||
|   load: () => Promise<string>; | ||||
|   errMsg: string; | ||||
| }): React.ReactElement { | ||||
|   const [src, setSrc] = React.useState<string | undefined>(); | ||||
|  | ||||
|   const load = async () => { | ||||
|     setSrc(await p.load()); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <AsyncWidget | ||||
|       errMsg={p.errMsg} | ||||
|       load={load} | ||||
|       loadKey={p.identifier} | ||||
|       build={() => <XMLWidget src={src!} />} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|   | ||||
| @@ -1,24 +1,33 @@ | ||||
| import { Grid, Paper, Typography } from "@mui/material"; | ||||
| import { Paper, Typography } from "@mui/material"; | ||||
| import React, { PropsWithChildren } from "react"; | ||||
| import Grid from "@mui/material/Grid2"; | ||||
|  | ||||
| export function EditSection( | ||||
|   p: { title: string; actions?: React.ReactElement } & PropsWithChildren | ||||
|   p: { | ||||
|     title?: string; | ||||
|     actions?: React.ReactElement; | ||||
|     fullWidth?: boolean; | ||||
|   } & PropsWithChildren | ||||
| ): React.ReactElement { | ||||
|   return ( | ||||
|     <Grid item sm={12} md={6}> | ||||
|     <Grid size={{ sm: 12, md: p.fullWidth ? 12 : 6 }}> | ||||
|       <Paper style={{ margin: "10px", padding: "10px" }}> | ||||
|         <span | ||||
|           style={{ | ||||
|             display: "flex", | ||||
|             justifyContent: "space-between", | ||||
|             alignItems: "center", | ||||
|           }} | ||||
|         > | ||||
|           <Typography variant="h5" style={{ marginBottom: "15px" }}> | ||||
|             {p.title} | ||||
|           </Typography> | ||||
|           {p.actions} | ||||
|         </span> | ||||
|         {(p.title || p.actions) && ( | ||||
|           <span | ||||
|             style={{ | ||||
|               display: "flex", | ||||
|               justifyContent: "space-between", | ||||
|               alignItems: "center", | ||||
|             }} | ||||
|           > | ||||
|             {p.title && ( | ||||
|               <Typography variant="h5" style={{ marginBottom: "15px" }}> | ||||
|                 {p.title} | ||||
|               </Typography> | ||||
|             )} | ||||
|             {p.actions} | ||||
|           </span> | ||||
|         )} | ||||
|         {p.children} | ||||
|       </Paper> | ||||
|     </Grid> | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import React from "react"; | ||||
| import { TextInput } from "./TextInput"; | ||||
|  | ||||
| export function IPInput(p: { | ||||
| @@ -18,6 +19,53 @@ export function IPInput(p: { | ||||
|   ); | ||||
| } | ||||
|  | ||||
| export function IPInputWithMask(p: { | ||||
|   label: string; | ||||
|   editable: boolean; | ||||
|   ipAndMask?: string; | ||||
|   ip?: string; | ||||
|   mask?: number; | ||||
|   onValueChange?: (ip?: string, mask?: number, ipAndMask?: string) => void; | ||||
|   version: 4 | 6; | ||||
| }): React.ReactElement { | ||||
|   const showSlash = React.useRef(!!p.mask); | ||||
|  | ||||
|   const currValue = | ||||
|     p.ipAndMask ?? | ||||
|     (p.ip ?? "") + (p.mask || showSlash.current ? "/" : "") + (p.mask ?? ""); | ||||
|  | ||||
|   const { onValueChange, ...props } = p; | ||||
|   return ( | ||||
|     <TextInput | ||||
|       onValueChange={(v) => { | ||||
|         showSlash.current = false; | ||||
|         if (!v) { | ||||
|           onValueChange?.(undefined, undefined, undefined); | ||||
|           return; | ||||
|         } | ||||
|  | ||||
|         const split = v?.split("/"); | ||||
|         const ip = | ||||
|           p.version === 4 ? sanitizeIpV4(split[0]) : sanitizeIpV6(split[0]); | ||||
|         let mask = undefined; | ||||
|  | ||||
|         if (split.length > 1) { | ||||
|           showSlash.current = true; | ||||
|           mask = sanitizeMask(p.version, split[1]); | ||||
|         } | ||||
|  | ||||
|         onValueChange?.( | ||||
|           ip, | ||||
|           mask, | ||||
|           mask || showSlash.current ? `${ip}/${mask ?? ""}` : ip | ||||
|         ); | ||||
|       }} | ||||
|       value={currValue} | ||||
|       {...props} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function sanitizeIpV4(s: string | undefined): string | undefined { | ||||
|   if (s === "" || s === undefined) return s; | ||||
|  | ||||
| @@ -77,3 +125,15 @@ function sanitizeIpV6(s: string | undefined): string | undefined { | ||||
|  | ||||
|   return needAnotherIteration ? sanitizeIpV6(res) : res; | ||||
| } | ||||
|  | ||||
| function sanitizeMask(version: 4 | 6, mask?: string): number | undefined { | ||||
|   if (!mask) return undefined; | ||||
|  | ||||
|   const value = Math.floor(Number(mask)); | ||||
|  | ||||
|   if (version === 4) { | ||||
|     return value < 0 || value > 32 ? 32 : value; | ||||
|   } else { | ||||
|     return value < 0 || value > 128 ? 128 : value; | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										27
									
								
								virtweb_frontend/src/widgets/forms/NWFConnStateInput.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								virtweb_frontend/src/widgets/forms/NWFConnStateInput.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| import { Layer4State } from "../../api/NWFilterApi"; | ||||
| import { SelectInput } from "./SelectInput"; | ||||
|  | ||||
| export function NWFConnStateInput(p: { | ||||
|   editable: boolean; | ||||
|   value?: Layer4State; | ||||
|   onChange: (s?: Layer4State) => void; | ||||
| }): React.ReactElement { | ||||
|   return ( | ||||
|     <SelectInput | ||||
|       {...p} | ||||
|       label="Connection state" | ||||
|       value={p.value} | ||||
|       onValueChange={(s) => { | ||||
|         p.onChange?.(s as any); | ||||
|       }} | ||||
|       options={[ | ||||
|         { label: "None", value: undefined }, | ||||
|         { label: "NEW", value: "NEW" }, | ||||
|         { label: "ESTABLISHED", value: "ESTABLISHED" }, | ||||
|         { label: "RELATED", value: "RELATED" }, | ||||
|         { label: "INVALID", value: "INVALID" }, | ||||
|         { label: "NONE", value: "NONE" }, | ||||
|       ]} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
| @@ -0,0 +1,66 @@ | ||||
| import React from "react"; | ||||
| import { useNavigate } from "react-router-dom"; | ||||
| import { NWFilter, NWFilterURL } from "../../api/NWFilterApi"; | ||||
| import { NWFilterItem } from "../nwfilter/NWFilterItem"; | ||||
| import { NWFilterSelectInput } from "./NWFilterSelectInput"; | ||||
|  | ||||
| export function NWFSelectReferencedFilters(p: { | ||||
|   editable: boolean; | ||||
|   selected: string[]; | ||||
|   nwFiltersList: NWFilter[]; | ||||
|   onChange?: () => void; | ||||
|   excludedFilters?: string[]; | ||||
| }): React.ReactElement { | ||||
|   const navigate = useNavigate(); | ||||
|  | ||||
|   const nwfilters = React.useMemo( | ||||
|     () => | ||||
|       p.excludedFilters | ||||
|         ? p.nwFiltersList.filter((f) => !p.excludedFilters!.includes(f.name)) | ||||
|         : p.nwFiltersList, | ||||
|     [p.excludedFilters] | ||||
|   ); | ||||
|  | ||||
|   const selectedFilters = React.useMemo( | ||||
|     () => p.selected.map((f) => p.nwFiltersList.find((s) => s.name === f)), | ||||
|     [p.selected.length] | ||||
|   ); | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       {selectedFilters.map((entry, n) => ( | ||||
|         <NWFilterItem | ||||
|           key={n} | ||||
|           value={entry} | ||||
|           onDelete={ | ||||
|             p.editable | ||||
|               ? () => { | ||||
|                   p.selected.splice(n, 1); | ||||
|                   p.onChange?.(); | ||||
|                 } | ||||
|               : undefined | ||||
|           } | ||||
|           onClick={ | ||||
|             !p.editable && entry | ||||
|               ? () => navigate(NWFilterURL(entry)) | ||||
|               : undefined | ||||
|           } | ||||
|         /> | ||||
|       ))} | ||||
|  | ||||
|       {p.editable && ( | ||||
|         <NWFilterSelectInput | ||||
|           editable={p.editable} | ||||
|           label="Attach a new filter" | ||||
|           canBeNull={false} | ||||
|           nwfilters={nwfilters} | ||||
|           value={""} | ||||
|           onChange={(f) => { | ||||
|             p.selected.push(f!); | ||||
|             p.onChange?.(); | ||||
|           }} | ||||
|         /> | ||||
|       )} | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										22
									
								
								virtweb_frontend/src/widgets/forms/NWFilterPriorityInput.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								virtweb_frontend/src/widgets/forms/NWFilterPriorityInput.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| import { ServerApi } from "../../api/ServerApi"; | ||||
| import { TextInput } from "./TextInput"; | ||||
|  | ||||
| export function NWFilterPriorityInput(p: { | ||||
|   editable: boolean; | ||||
|   label: string; | ||||
|   value?: number; | ||||
|   onChange: (priority?: number) => void; | ||||
| }): React.ReactElement { | ||||
|   return ( | ||||
|     <TextInput | ||||
|       {...p} | ||||
|       value={p.value?.toString()} | ||||
|       type="number" | ||||
|       onValueChange={(v) => { | ||||
|         p.onChange?.(v && v !== "" ? Number(v) : undefined); | ||||
|       }} | ||||
|       size={ServerApi.Config.constraints.nwfilter_priority} | ||||
|       helperText="A lower priority value is accessed before one with a higher value" | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										709
									
								
								virtweb_frontend/src/widgets/forms/NWFilterRules.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										709
									
								
								virtweb_frontend/src/widgets/forms/NWFilterRules.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,709 @@ | ||||
| import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward"; | ||||
| import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward"; | ||||
| import DeleteIcon from "@mui/icons-material/Delete"; | ||||
| import PlaylistAddIcon from "@mui/icons-material/PlaylistAdd"; | ||||
| import { | ||||
|   Button, | ||||
|   Card, | ||||
|   CardActions, | ||||
|   CardContent, | ||||
|   IconButton, | ||||
|   Paper, | ||||
|   Tooltip, | ||||
| } from "@mui/material"; | ||||
| import { | ||||
|   NWFSAllBase, | ||||
|   NWFSArpOrRARP, | ||||
|   NWFSIPBase, | ||||
|   NWFSLayer4Base, | ||||
|   NWFSMac, | ||||
|   NWFSelector, | ||||
|   NWFilterRule, | ||||
| } from "../../api/NWFilterApi"; | ||||
| import { ServerApi } from "../../api/ServerApi"; | ||||
| import { EditSection } from "./EditSection"; | ||||
| import { IPInput, IPInputWithMask } from "./IPInput"; | ||||
| import { MACInput } from "./MACInput"; | ||||
| import { NWFConnStateInput } from "./NWFConnStateInput"; | ||||
| import { NWFilterPriorityInput } from "./NWFilterPriorityInput"; | ||||
| import { PortInput } from "./PortInput"; | ||||
| import { SelectInput } from "./SelectInput"; | ||||
| import { TextInput } from "./TextInput"; | ||||
|  | ||||
| export function NWFilterRules(p: { | ||||
|   editable: boolean; | ||||
|   rules: NWFilterRule[]; | ||||
|   onChange?: () => void; | ||||
| }): React.ReactElement { | ||||
|   const addRule = () => { | ||||
|     p.rules.push({ | ||||
|       action: "drop", | ||||
|       direction: "inout", | ||||
|       selectors: [], | ||||
|     }); | ||||
|     p.onChange?.(); | ||||
|   }; | ||||
|  | ||||
|   const swapRules = (f: number, s: number) => { | ||||
|     const swap = p.rules[f]; | ||||
|     p.rules[f] = p.rules[s]; | ||||
|     p.rules[s] = swap; | ||||
|     p.onChange?.(); | ||||
|   }; | ||||
|  | ||||
|   const deleteRule = (num: number) => { | ||||
|     p.rules.splice(num, 1); | ||||
|     p.onChange?.(); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <EditSection title="Rules"> | ||||
|       {p.rules.map((r, n) => ( | ||||
|         <NWRuleEdit | ||||
|           key={n} | ||||
|           rule={r} | ||||
|           onDelete={() => { | ||||
|             deleteRule(n); | ||||
|           }} | ||||
|           onGoDown={ | ||||
|             n < p.rules.length - 1 ? () => swapRules(n, n + 1) : undefined | ||||
|           } | ||||
|           onGoUp={n > 0 ? () => swapRules(n, n - 1) : undefined} | ||||
|           {...p} | ||||
|         /> | ||||
|       ))} | ||||
|  | ||||
|       <div style={{ textAlign: "right" }}> | ||||
|         {p.editable && <Button onClick={addRule}>Add a new rule</Button>} | ||||
|       </div> | ||||
|     </EditSection> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function NWRuleEdit(p: { | ||||
|   editable: boolean; | ||||
|   rule: NWFilterRule; | ||||
|   onChange?: () => void; | ||||
|   onGoUp?: () => void; | ||||
|   onGoDown?: () => void; | ||||
|   onDelete: () => void; | ||||
| }): React.ReactElement { | ||||
|   const addSelector = () => { | ||||
|     p.rule.selectors.push({ | ||||
|       type: "all", | ||||
|     }); | ||||
|     p.onChange?.(); | ||||
|   }; | ||||
|  | ||||
|   const deleteSelector = (num: number) => { | ||||
|     p.rule.selectors.splice(num, 1); | ||||
|     p.onChange?.(); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <Card style={{ margin: "30px" }} elevation={3}> | ||||
|       <CardContent> | ||||
|         <div style={{ display: "flex" }}> | ||||
|           <SelectInput | ||||
|             editable={p.editable} | ||||
|             label="Action" | ||||
|             value={p.rule.action} | ||||
|             onValueChange={(v) => { | ||||
|               p.rule.action = v as any; | ||||
|               p.onChange?.(); | ||||
|             }} | ||||
|             options={[ | ||||
|               { label: "drop", value: "drop" }, | ||||
|               { label: "reject", value: "reject" }, | ||||
|               { label: "accept", value: "accept" }, | ||||
|               { label: "return", value: "return" }, | ||||
|               { label: "continue", value: "continue" }, | ||||
|             ]} | ||||
|           /> | ||||
|           <span style={{ width: "20px" }}></span> | ||||
|           <SelectInput | ||||
|             editable={p.editable} | ||||
|             label="Direction" | ||||
|             value={p.rule.direction} | ||||
|             onValueChange={(v) => { | ||||
|               p.rule.direction = v as any; | ||||
|               p.onChange?.(); | ||||
|             }} | ||||
|             options={[ | ||||
|               { label: "in", value: "in" }, | ||||
|               { label: "out", value: "out" }, | ||||
|               { label: "inout", value: "inout" }, | ||||
|             ]} | ||||
|           /> | ||||
|           <span style={{ width: "20px" }}></span> | ||||
|           <NWFilterPriorityInput | ||||
|             {...p} | ||||
|             label="Priority" | ||||
|             value={p.rule.priority} | ||||
|             onChange={(v) => { | ||||
|               p.rule.priority = v; | ||||
|               p.onChange?.(); | ||||
|             }} | ||||
|           /> | ||||
|         </div> | ||||
|  | ||||
|         {p.rule.selectors.map((s, n) => ( | ||||
|           <NWFSelectorEdit | ||||
|             key={n} | ||||
|             editable={p.editable} | ||||
|             onChange={p.onChange} | ||||
|             selector={s} | ||||
|             onDelete={() => deleteSelector(n)} | ||||
|           /> | ||||
|         ))} | ||||
|       </CardContent> | ||||
|       <CardActions> | ||||
|         {p.editable && ( | ||||
|           <div style={{ display: "flex", width: "100%" }}> | ||||
|             <Tooltip title="Remove the rule"> | ||||
|               <IconButton color="error" onClick={p.onDelete}> | ||||
|                 <DeleteIcon /> | ||||
|               </IconButton> | ||||
|             </Tooltip> | ||||
|  | ||||
|             <span style={{ flex: 1 }}></span> | ||||
|  | ||||
|             {ServerApi.Config.constraints.nwfilter_selectors_count.max > | ||||
|               p.rule.selectors.length && ( | ||||
|               <Tooltip title="Add a selector"> | ||||
|                 <IconButton onClick={addSelector}> | ||||
|                   <PlaylistAddIcon /> | ||||
|                 </IconButton> | ||||
|               </Tooltip> | ||||
|             )} | ||||
|  | ||||
|             {p.onGoUp && ( | ||||
|               <Tooltip title="Move rule upward"> | ||||
|                 <IconButton onClick={p.onGoUp}> | ||||
|                   <ArrowUpwardIcon /> | ||||
|                 </IconButton> | ||||
|               </Tooltip> | ||||
|             )} | ||||
|  | ||||
|             {p.onGoDown && ( | ||||
|               <Tooltip title="Move rule downward"> | ||||
|                 <IconButton onClick={p.onGoDown}> | ||||
|                   <ArrowDownwardIcon /> | ||||
|                 </IconButton> | ||||
|               </Tooltip> | ||||
|             )} | ||||
|           </div> | ||||
|         )} | ||||
|       </CardActions> | ||||
|     </Card> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function NWFSelectorEdit(p: { | ||||
|   editable: boolean; | ||||
|   selector: NWFSelector; | ||||
|   onDelete: () => void; | ||||
|   onChange?: () => void; | ||||
| }): React.ReactElement { | ||||
|   return ( | ||||
|     <Paper elevation={10} style={{ padding: "10px" }}> | ||||
|       <div style={{ display: "flex", width: "100%" }}> | ||||
|         <div style={{ flex: 1 }}> | ||||
|           <SelectInput | ||||
|             editable={p.editable} | ||||
|             label="Type" | ||||
|             onValueChange={(v) => { | ||||
|               p.selector.type = v! as any; | ||||
|               p.onChange?.(); | ||||
|             }} | ||||
|             value={p.selector.type} | ||||
|             options={[ | ||||
|               { label: "MAC (Ethernet)", value: "mac" }, | ||||
|  | ||||
|               { label: "ARP", value: "arp" }, | ||||
|               { label: "RARP", value: "rarp" }, | ||||
|  | ||||
|               { label: "IPv4", value: "ipv4" }, | ||||
|               { label: "IPv6", value: "ipv6" }, | ||||
|  | ||||
|               { label: "TCP over IPv4", value: "tcp" }, | ||||
|               { label: "UDP over IPv4", value: "udp" }, | ||||
|               { label: "SCTP over IPv4", value: "sctp" }, | ||||
|               { label: "ICMPv4", value: "icmp" }, | ||||
|  | ||||
|               { label: "All over IPv4", value: "all" }, | ||||
|  | ||||
|               { label: "TCP over IPv6", value: "tcpipv6" }, | ||||
|               { label: "UDP over IPv6", value: "udpipv6" }, | ||||
|               { label: "SCTP over IPv6", value: "sctpipv6" }, | ||||
|               { label: "ICMPv6", value: "icmpipv6" }, | ||||
|  | ||||
|               { label: "All over IPv6", value: "allipv6" }, | ||||
|             ]} | ||||
|           /> | ||||
|  | ||||
|           {p.selector.type === "mac" && ( | ||||
|             <NWFSelectorMac {...p} selector={p.selector} /> | ||||
|           )} | ||||
|  | ||||
|           {(p.selector.type === "arp" || p.selector.type === "rarp") && ( | ||||
|             <NWFSelectorArp {...p} selector={p.selector} /> | ||||
|           )} | ||||
|  | ||||
|           {p.selector.type === "ipv4" && ( | ||||
|             <NWFSelectorIP {...p} selector={p.selector} version={4} /> | ||||
|           )} | ||||
|  | ||||
|           {p.selector.type === "ipv6" && ( | ||||
|             <NWFSelectorIP {...p} selector={p.selector} version={6} /> | ||||
|           )} | ||||
|  | ||||
|           {(p.selector.type === "tcp" || | ||||
|             p.selector.type === "udp" || | ||||
|             p.selector.type === "sctp" || | ||||
|             p.selector.type === "icmp") && ( | ||||
|             <NWFSelectorLayer4 {...p} selector={p.selector} version={4} /> | ||||
|           )} | ||||
|  | ||||
|           {p.selector.type === "all" && ( | ||||
|             <NWFSelectorAll {...p} selector={p.selector} version={4} /> | ||||
|           )} | ||||
|  | ||||
|           {(p.selector.type === "tcpipv6" || | ||||
|             p.selector.type === "udpipv6" || | ||||
|             p.selector.type === "sctpipv6" || | ||||
|             p.selector.type === "icmpipv6") && ( | ||||
|             <NWFSelectorLayer4 {...p} selector={p.selector} version={6} /> | ||||
|           )} | ||||
|  | ||||
|           {p.selector.type === "allipv6" && ( | ||||
|             <NWFSelectorAll {...p} selector={p.selector} version={6} /> | ||||
|           )} | ||||
|  | ||||
|           <TextInput | ||||
|             editable={p.editable} | ||||
|             label="Comment" | ||||
|             value={p.selector.comment} | ||||
|             onValueChange={(v) => { | ||||
|               p.selector.comment = v; | ||||
|               p.onChange?.(); | ||||
|             }} | ||||
|             size={ServerApi.Config.constraints.nwfilter_comment_size} | ||||
|           /> | ||||
|         </div> | ||||
|  | ||||
|         {p.editable && ( | ||||
|           <div style={{ display: "flex", justifyContent: "center" }}> | ||||
|             <Tooltip title="Remove the selector"> | ||||
|               <IconButton color="error" onClick={p.onDelete}> | ||||
|                 <DeleteIcon /> | ||||
|               </IconButton> | ||||
|             </Tooltip> | ||||
|           </div> | ||||
|         )} | ||||
|       </div> | ||||
|     </Paper> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| interface SpecificSelectorEditor<E> { | ||||
|   editable: boolean; | ||||
|   selector: E; | ||||
|   onChange?: () => void; | ||||
| } | ||||
|  | ||||
| interface SpecificSelectorEditorWithIPVersion<E> | ||||
|   extends SpecificSelectorEditor<E> { | ||||
|   version: 4 | 6; | ||||
| } | ||||
|  | ||||
| function NWFSelectorMac( | ||||
|   p: SpecificSelectorEditor<NWFSMac> | ||||
| ): React.ReactElement { | ||||
|   return ( | ||||
|     <> | ||||
|       <MACInput | ||||
|         {...p} | ||||
|         label="Src mac address" | ||||
|         value={p.selector.src_mac_addr} | ||||
|         onValueChange={(v) => { | ||||
|           p.selector.src_mac_addr = v; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <MACInput | ||||
|         {...p} | ||||
|         label="Src mac mask" | ||||
|         value={p.selector.src_mac_mask} | ||||
|         onValueChange={(v) => { | ||||
|           p.selector.src_mac_mask = v; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <MACInput | ||||
|         {...p} | ||||
|         label="Dst mac address" | ||||
|         value={p.selector.dst_mac_addr} | ||||
|         onValueChange={(v) => { | ||||
|           p.selector.dst_mac_addr = v; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <MACInput | ||||
|         {...p} | ||||
|         label="Dst mac mask" | ||||
|         value={p.selector.dst_mac_mask} | ||||
|         onValueChange={(v) => { | ||||
|           p.selector.dst_mac_mask = v; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function NWFSelectorArp( | ||||
|   p: SpecificSelectorEditor<NWFSArpOrRARP> | ||||
| ): React.ReactElement { | ||||
|   return ( | ||||
|     <> | ||||
|       <MACInput | ||||
|         {...p} | ||||
|         label="Src mac address" | ||||
|         value={p.selector.srcmacaddr} | ||||
|         onValueChange={(v) => { | ||||
|           p.selector.srcmacaddr = v; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <MACInput | ||||
|         {...p} | ||||
|         label="Src mac mask" | ||||
|         value={p.selector.srcmacmask} | ||||
|         onValueChange={(v) => { | ||||
|           p.selector.srcmacmask = v; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <MACInput | ||||
|         {...p} | ||||
|         label="Dst mac address" | ||||
|         value={p.selector.dstmacaddr} | ||||
|         onValueChange={(v) => { | ||||
|           p.selector.dstmacaddr = v; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <MACInput | ||||
|         {...p} | ||||
|         label="Dst mac mask" | ||||
|         value={p.selector.dstmacmask} | ||||
|         onValueChange={(v) => { | ||||
|           p.selector.dstmacmask = v; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <IPInputWithMask | ||||
|         {...p} | ||||
|         label="ARP src ip" | ||||
|         ip={p.selector.arpsrcipaddr} | ||||
|         mask={p.selector.arpsrcipmask} | ||||
|         version={4} | ||||
|         onValueChange={(ip, mask) => { | ||||
|           p.selector.arpsrcipaddr = ip; | ||||
|           p.selector.arpsrcipmask = mask; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <IPInputWithMask | ||||
|         {...p} | ||||
|         label="ARP dst ip" | ||||
|         ip={p.selector.arpdstipaddr} | ||||
|         mask={p.selector.arpdstipmask} | ||||
|         version={4} | ||||
|         onValueChange={(ip, mask) => { | ||||
|           p.selector.arpdstipaddr = ip; | ||||
|           p.selector.arpdstipmask = mask; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function NWFSelectorIP( | ||||
|   p: SpecificSelectorEditorWithIPVersion<NWFSIPBase> | ||||
| ): React.ReactElement { | ||||
|   return ( | ||||
|     <> | ||||
|       <MACInput | ||||
|         {...p} | ||||
|         label="Src mac address" | ||||
|         value={p.selector.srcmacaddr} | ||||
|         onValueChange={(v) => { | ||||
|           p.selector.srcmacaddr = v; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <MACInput | ||||
|         {...p} | ||||
|         label="Src mac mask" | ||||
|         value={p.selector.srcmacmask} | ||||
|         onValueChange={(v) => { | ||||
|           p.selector.srcmacmask = v; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <MACInput | ||||
|         {...p} | ||||
|         label="Dst mac address" | ||||
|         value={p.selector.dstmacaddr} | ||||
|         onValueChange={(v) => { | ||||
|           p.selector.dstmacaddr = v; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <MACInput | ||||
|         {...p} | ||||
|         label="Dst mac mask" | ||||
|         value={p.selector.dstmacmask} | ||||
|         onValueChange={(v) => { | ||||
|           p.selector.dstmacmask = v; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <IPInputWithMask | ||||
|         {...p} | ||||
|         label="Source IP address / mask" | ||||
|         ip={p.selector.srcipaddr} | ||||
|         mask={p.selector.srcipmask} | ||||
|         version={p.version} | ||||
|         onValueChange={(ip, mask) => { | ||||
|           p.selector.srcipaddr = ip; | ||||
|           p.selector.srcipmask = mask; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <IPInputWithMask | ||||
|         {...p} | ||||
|         label="Destination IP address / mask" | ||||
|         ip={p.selector.dstipaddr} | ||||
|         mask={p.selector.dstipmask} | ||||
|         version={p.version} | ||||
|         onValueChange={(ip, mask) => { | ||||
|           p.selector.dstipaddr = ip; | ||||
|           p.selector.dstipmask = mask; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function NWFSelectorLayer4( | ||||
|   p: SpecificSelectorEditorWithIPVersion<NWFSLayer4Base> | ||||
| ): React.ReactElement { | ||||
|   return ( | ||||
|     <> | ||||
|       <MACInput | ||||
|         {...p} | ||||
|         label="Src mac address" | ||||
|         value={p.selector.srcmacaddr} | ||||
|         onValueChange={(v) => { | ||||
|           p.selector.srcmacaddr = v; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <IPInputWithMask | ||||
|         {...p} | ||||
|         label="Source IP address / mask" | ||||
|         ip={p.selector.srcipaddr} | ||||
|         mask={p.selector.srcipmask} | ||||
|         version={p.version} | ||||
|         onValueChange={(ip, mask) => { | ||||
|           p.selector.srcipaddr = ip; | ||||
|           p.selector.srcipmask = mask; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <IPInputWithMask | ||||
|         {...p} | ||||
|         label="Destination IP address / mask" | ||||
|         ip={p.selector.dstipaddr} | ||||
|         mask={p.selector.dstipmask} | ||||
|         version={p.version} | ||||
|         onValueChange={(ip, mask) => { | ||||
|           p.selector.dstipaddr = ip; | ||||
|           p.selector.dstipmask = mask; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <IPInput | ||||
|         {...p} | ||||
|         label="Source IP from" | ||||
|         value={p.selector.srcipfrom} | ||||
|         onValueChange={(ip) => { | ||||
|           p.selector.srcipfrom = ip; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <IPInput | ||||
|         {...p} | ||||
|         label="Source IP to" | ||||
|         value={p.selector.srcipto} | ||||
|         onValueChange={(ip) => { | ||||
|           p.selector.srcipto = ip; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <IPInput | ||||
|         {...p} | ||||
|         label="Destination IP from" | ||||
|         value={p.selector.dstipfrom} | ||||
|         onValueChange={(ip) => { | ||||
|           p.selector.dstipfrom = ip; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <IPInput | ||||
|         {...p} | ||||
|         label="Destination IP to" | ||||
|         value={p.selector.dstipto} | ||||
|         onValueChange={(ip) => { | ||||
|           p.selector.dstipto = ip; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <PortInput | ||||
|         {...p} | ||||
|         label="Source port start" | ||||
|         value={p.selector.srcportstart} | ||||
|         onChange={(port) => { | ||||
|           p.selector.srcportstart = port; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <PortInput | ||||
|         {...p} | ||||
|         label="Source port end" | ||||
|         value={p.selector.srcportend} | ||||
|         onChange={(port) => { | ||||
|           p.selector.srcportend = port; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <PortInput | ||||
|         {...p} | ||||
|         label="Destination port start" | ||||
|         value={p.selector.dstportstart} | ||||
|         onChange={(port) => { | ||||
|           p.selector.dstportstart = port; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <PortInput | ||||
|         {...p} | ||||
|         label="Destination port end" | ||||
|         value={p.selector.dstportend} | ||||
|         onChange={(port) => { | ||||
|           p.selector.dstportend = port; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <NWFConnStateInput | ||||
|         {...p} | ||||
|         value={p.selector.state} | ||||
|         onChange={(v) => { | ||||
|           p.selector.state = v; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function NWFSelectorAll( | ||||
|   p: SpecificSelectorEditorWithIPVersion<NWFSAllBase> | ||||
| ): React.ReactElement { | ||||
|   return ( | ||||
|     <> | ||||
|       <MACInput | ||||
|         {...p} | ||||
|         label="Src mac address" | ||||
|         value={p.selector.srcmacaddr} | ||||
|         onValueChange={(v) => { | ||||
|           p.selector.srcmacaddr = v; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <IPInputWithMask | ||||
|         {...p} | ||||
|         label="Source IP address / mask" | ||||
|         ip={p.selector.srcipaddr} | ||||
|         mask={p.selector.srcipmask} | ||||
|         version={p.version} | ||||
|         onValueChange={(ip, mask) => { | ||||
|           p.selector.srcipaddr = ip; | ||||
|           p.selector.srcipmask = mask; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <IPInputWithMask | ||||
|         {...p} | ||||
|         label="Destination IP address / mask" | ||||
|         ip={p.selector.dstipaddr} | ||||
|         mask={p.selector.dstipmask} | ||||
|         version={p.version} | ||||
|         onValueChange={(ip, mask) => { | ||||
|           p.selector.dstipaddr = ip; | ||||
|           p.selector.dstipmask = mask; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <IPInput | ||||
|         {...p} | ||||
|         label="Source IP from" | ||||
|         value={p.selector.srcipfrom} | ||||
|         onValueChange={(ip) => { | ||||
|           p.selector.srcipfrom = ip; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <IPInput | ||||
|         {...p} | ||||
|         label="Source IP to" | ||||
|         value={p.selector.srcipto} | ||||
|         onValueChange={(ip) => { | ||||
|           p.selector.srcipto = ip; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <IPInput | ||||
|         {...p} | ||||
|         label="Destination IP from" | ||||
|         value={p.selector.dstipfrom} | ||||
|         onValueChange={(ip) => { | ||||
|           p.selector.dstipfrom = ip; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <IPInput | ||||
|         {...p} | ||||
|         label="Destination IP to" | ||||
|         value={p.selector.dstipto} | ||||
|         onValueChange={(ip) => { | ||||
|           p.selector.dstipto = ip; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|       <NWFConnStateInput | ||||
|         {...p} | ||||
|         value={p.selector.state} | ||||
|         onChange={(v) => { | ||||
|           p.selector.state = v; | ||||
|           p.onChange?.(); | ||||
|         }} | ||||
|       /> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										63
									
								
								virtweb_frontend/src/widgets/forms/NWFilterSelectInput.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								virtweb_frontend/src/widgets/forms/NWFilterSelectInput.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | ||||
| import { Autocomplete, TextField } from "@mui/material"; | ||||
| import React from "react"; | ||||
| import { useNavigate } from "react-router-dom"; | ||||
| import { NWFilter, NWFilterURL } from "../../api/NWFilterApi"; | ||||
| import { NWFilterItem } from "../nwfilter/NWFilterItem"; | ||||
|  | ||||
| export function NWFilterSelectInput(p: { | ||||
|   editable: boolean; | ||||
|   label?: string; | ||||
|   nwfilters: NWFilter[]; | ||||
|   value?: string; | ||||
|   onChange?: (name?: string) => void; | ||||
|   canBeNull: boolean; | ||||
| }): React.ReactElement { | ||||
|   const navigate = useNavigate(); | ||||
|  | ||||
|   const [open, setOpen] = React.useState(false); | ||||
|  | ||||
|   const selectedValue = p.nwfilters.find((o) => o.name === p.value); | ||||
|   if (!p.editable && !selectedValue) return <></>; | ||||
|  | ||||
|   if (selectedValue) | ||||
|     return ( | ||||
|       <NWFilterItem | ||||
|         value={selectedValue} | ||||
|         onDelete={p.editable ? () => p.onChange?.(undefined) : undefined} | ||||
|         onClick={ | ||||
|           !p.editable && selectedValue | ||||
|             ? () => navigate(NWFilterURL(selectedValue)) | ||||
|             : undefined | ||||
|         } | ||||
|       /> | ||||
|     ); | ||||
|  | ||||
|   return ( | ||||
|     <Autocomplete | ||||
|       open={open} | ||||
|       onOpen={() => { | ||||
|         setOpen(true); | ||||
|       }} | ||||
|       onClose={() => { | ||||
|         setOpen(false); | ||||
|       }} | ||||
|       readOnly={!p.editable} | ||||
|       options={[...(p.canBeNull ? [undefined] : []), ...p.nwfilters]} | ||||
|       getOptionLabel={(o) => o?.name ?? "Unspecified"} | ||||
|       value={selectedValue} | ||||
|       renderInput={(params) => ( | ||||
|         <TextField {...params} variant="standard" label={p.label} /> | ||||
|       )} | ||||
|       renderOption={(_props, option, _state) => ( | ||||
|         <NWFilterItem | ||||
|           dense | ||||
|           onClick={() => { | ||||
|             p.onChange?.(option?.name); | ||||
|             setOpen(false); | ||||
|           }} | ||||
|           value={option} | ||||
|         /> | ||||
|       )} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
| @@ -10,15 +10,17 @@ import { | ||||
|   ListItemText, | ||||
|   Paper, | ||||
|   Tooltip, | ||||
|   Typography, | ||||
| } from "@mui/material"; | ||||
| import { DHCPConfig, DHCPHost } from "../../api/NetworksApi"; | ||||
| import { ServerApi } from "../../api/ServerApi"; | ||||
| import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider"; | ||||
| import { IPInput } from "../forms/IPInput"; | ||||
| import { MACInput } from "../forms/MACInput"; | ||||
| import { TextInput } from "../forms/TextInput"; | ||||
| import { IPInput } from "./IPInput"; | ||||
| import { MACInput } from "./MACInput"; | ||||
| import { TextInput } from "./TextInput"; | ||||
| import Grid from "@mui/material/Grid2"; | ||||
| 
 | ||||
| export function DHCPHostReservations(p: { | ||||
| export function NetDHCPHostReservations(p: { | ||||
|   editable: boolean; | ||||
|   dhcp: DHCPConfig; | ||||
|   version: 4 | 6; | ||||
| @@ -35,21 +37,29 @@ export function DHCPHostReservations(p: { | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       {p.dhcp.hosts.map((h, num) => ( | ||||
|         <HostReservationWidget | ||||
|           key={num} | ||||
|           {...p} | ||||
|           onChange={() => { | ||||
|             p.onChange?.(p.dhcp); | ||||
|           }} | ||||
|           host={h} | ||||
|           onRemove={() => { | ||||
|             p.dhcp.hosts.splice(num, 1); | ||||
|             p.onChange?.(p.dhcp); | ||||
|           }} | ||||
|         /> | ||||
|       ))} | ||||
| 
 | ||||
|       <Grid container> | ||||
|         {p.dhcp.hosts.map((h, num) => ( | ||||
|           <Grid key={num} size={{ sm: 12, md: 6 }} style={{ padding: "10px" }}> | ||||
|             <HostReservationWidget | ||||
|               key={num} | ||||
|               {...p} | ||||
|               onChange={() => { | ||||
|                 p.onChange?.(p.dhcp); | ||||
|               }} | ||||
|               host={h} | ||||
|               onRemove={() => { | ||||
|                 p.dhcp.hosts.splice(num, 1); | ||||
|                 p.onChange?.(p.dhcp); | ||||
|               }} | ||||
|             /> | ||||
|           </Grid> | ||||
|         ))} | ||||
|       </Grid> | ||||
|       {p.dhcp.hosts.length === 0 && ( | ||||
|         <Typography style={{ textAlign: "center" }}> | ||||
|           You have not set any DHCP host reservations. | ||||
|         </Typography> | ||||
|       )} | ||||
|       {p.editable && ( | ||||
|         <Button onClick={addHost}>Add new host reservation</Button> | ||||
|       )} | ||||
| @@ -75,7 +85,7 @@ function HostReservationWidget(p: { | ||||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <Paper elevation={3} style={{ padding: "10px", marginTop: "20px" }}> | ||||
|     <Paper elevation={3} style={{ padding: "20px", marginBottom: "20px" }}> | ||||
|       <ListItem | ||||
|         secondaryAction={ | ||||
|           p.editable && ( | ||||
							
								
								
									
										311
									
								
								virtweb_frontend/src/widgets/forms/NetNatConfiguration.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										311
									
								
								virtweb_frontend/src/widgets/forms/NetNatConfiguration.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,311 @@ | ||||
| import DeleteIcon from "@mui/icons-material/Delete"; | ||||
| import { | ||||
|   Alert, | ||||
|   Button, | ||||
|   Card, | ||||
|   CardActions, | ||||
|   CardContent, | ||||
|   IconButton, | ||||
|   Tooltip, | ||||
|   Typography, | ||||
| } from "@mui/material"; | ||||
| import Grid from "@mui/material/Grid2"; | ||||
| import React, { PropsWithChildren } from "react"; | ||||
| import { NatEntry } from "../../api/NetworksApi"; | ||||
| import { ServerApi } from "../../api/ServerApi"; | ||||
| import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider"; | ||||
| import { IPInput } from "./IPInput"; | ||||
| import { PortInput } from "./PortInput"; | ||||
| import { RadioGroupInput } from "./RadioGroupInput"; | ||||
| import { SelectInput } from "./SelectInput"; | ||||
| import { TextInput } from "./TextInput"; | ||||
|  | ||||
| export function NetNatConfiguration(p: { | ||||
|   editable: boolean; | ||||
|   nat: NatEntry[]; | ||||
|   nicsList: string[]; | ||||
|   onChange?: (nat: NatEntry[]) => void; | ||||
|   version: 4 | 6; | ||||
| }): React.ReactElement { | ||||
|   const confirm = useConfirm(); | ||||
|  | ||||
|   const addEntry = () => { | ||||
|     p.nat.push({ | ||||
|       host_ip: { | ||||
|         type: "ip", | ||||
|         ip: p.version === 4 ? "10.0.0.1" : "fd00::", | ||||
|       }, | ||||
|       host_port: { type: "single", port: 80 }, | ||||
|       guest_ip: p.version === 4 ? "10.0.0.100" : "fd00::", | ||||
|       guest_port: 10, | ||||
|       protocol: "TCP", | ||||
|     }); | ||||
|     p.onChange?.(p.nat); | ||||
|   }; | ||||
|  | ||||
|   const onDelete = async (idx: number) => { | ||||
|     if (!(await confirm("Do you really want to delete this entry?"))) return; | ||||
|  | ||||
|     p.nat.splice(idx, 1); | ||||
|     p.onChange?.(p.nat); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       {p.nat.map((e, num) => ( | ||||
|         <NatEntryForm | ||||
|           key={num} | ||||
|           {...p} | ||||
|           entry={e} | ||||
|           onChange={() => p.onChange?.(p.nat)} | ||||
|           onDelete={() => onDelete(num)} | ||||
|         /> | ||||
|       ))} | ||||
|  | ||||
|       {p.nat.length === 0 && ( | ||||
|         <Typography style={{ textAlign: "center" }}> | ||||
|           You have not set any NAT entry yet. | ||||
|         </Typography> | ||||
|       )} | ||||
|  | ||||
|       {p.editable && <Button onClick={addEntry}>Add a new entry</Button>} | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function NatEntryForm(p: { | ||||
|   editable: boolean; | ||||
|   version: 4 | 6; | ||||
|   entry: NatEntry; | ||||
|   onChange?: () => void; | ||||
|   onDelete: () => void; | ||||
|   nicsList: string[]; | ||||
| }): React.ReactElement { | ||||
|   const guestPortEnd = | ||||
|     p.entry.host_port.type === "range" | ||||
|       ? p.entry.host_port.end - p.entry.host_port.start + p.entry.guest_port | ||||
|       : undefined; | ||||
|  | ||||
|   return ( | ||||
|     <Card style={{ margin: "30px" }} elevation={3}> | ||||
|       <CardContent> | ||||
|         <Grid container> | ||||
|           <NATEntryProp> | ||||
|             <SelectInput | ||||
|               {...p} | ||||
|               label="Protocol" | ||||
|               options={[ | ||||
|                 { value: "TCP" }, | ||||
|                 { value: "UDP" }, | ||||
|                 { label: "TCP & UDP", value: "Both" }, | ||||
|               ]} | ||||
|               value={p.entry.protocol} | ||||
|               onValueChange={(v) => { | ||||
|                 p.entry.protocol = v as any; | ||||
|                 p.onChange?.(); | ||||
|               }} | ||||
|             /> | ||||
|           </NATEntryProp> | ||||
|           <NATEntryProp> | ||||
|             <TextInput | ||||
|               {...p} | ||||
|               label="Comment" | ||||
|               value={p.entry.comment} | ||||
|               onValueChange={(v) => { | ||||
|                 p.entry.comment = v; | ||||
|                 p.onChange?.(); | ||||
|               }} | ||||
|               size={ServerApi.Config.constraints.net_nat_comment_size} | ||||
|             /> | ||||
|           </NATEntryProp> | ||||
|  | ||||
|           {/* Host conf */} | ||||
|           <NATEntryProp label="Host configuration"> | ||||
|             <SelectInput | ||||
|               {...p} | ||||
|               label="Host IP address specification" | ||||
|               options={[ | ||||
|                 { | ||||
|                   label: "Specific IP", | ||||
|                   value: "ip", | ||||
|                   description: "Use a pre-defined IP address", | ||||
|                 }, | ||||
|                 { | ||||
|                   label: "Network interface", | ||||
|                   value: "interface", | ||||
|                   description: | ||||
|                     "Use active IP addresses on the selected network interface during network startup to determine host adddress", | ||||
|                 }, | ||||
|               ]} | ||||
|               value={p.entry.host_ip.type} | ||||
|               onValueChange={(v) => { | ||||
|                 p.entry.host_ip.type = v as any; | ||||
|                 p.onChange?.(); | ||||
|               }} | ||||
|             /> | ||||
|  | ||||
|             {p.entry.host_ip.type === "ip" && ( | ||||
|               <IPInput | ||||
|                 {...p} | ||||
|                 label="Host IP address" | ||||
|                 value={p.entry.host_ip.ip} | ||||
|                 onValueChange={(v) => { | ||||
|                   if (p.entry.host_ip.type === "ip") p.entry.host_ip.ip = v!; | ||||
|                   p.onChange?.(); | ||||
|                 }} | ||||
|               /> | ||||
|             )} | ||||
|  | ||||
|             {p.entry.host_ip.type === "interface" && ( | ||||
|               <> | ||||
|                 {p.editable && ( | ||||
|                   <Alert severity="warning" style={{ margin: "10px 0px" }}> | ||||
|                     Warning! All IP addresses may not be inferred on reboot due | ||||
|                     to the fact that the network hook might be executed before | ||||
|                     the network interfaces are fully configured. This might lead | ||||
|                     to incomplete ports exposition! | ||||
|                   </Alert> | ||||
|                 )} | ||||
|                 <SelectInput | ||||
|                   {...p} | ||||
|                   label="Network interface" | ||||
|                   value={p.entry.host_ip.name} | ||||
|                   options={p.nicsList.map((n) => { | ||||
|                     return { | ||||
|                       value: n, | ||||
|                     }; | ||||
|                   })} | ||||
|                   onValueChange={(v) => { | ||||
|                     if (p.entry.host_ip.type === "interface") | ||||
|                       p.entry.host_ip.name = v!; | ||||
|                     p.onChange?.(); | ||||
|                   }} | ||||
|                 /> | ||||
|               </> | ||||
|             )} | ||||
|           </NATEntryProp> | ||||
|  | ||||
|           <NATEntryProp label="Target guest configuration"> | ||||
|             <IPInput | ||||
|               {...p} | ||||
|               label="Guest IP" | ||||
|               value={p.entry.guest_ip} | ||||
|               onValueChange={(v) => { | ||||
|                 p.entry.guest_ip = v!; | ||||
|                 p.onChange?.(); | ||||
|               }} | ||||
|             /> | ||||
|           </NATEntryProp> | ||||
|  | ||||
|           <NATEntryProp> | ||||
|             <RadioGroupInput | ||||
|               {...p} | ||||
|               options={[ | ||||
|                 { label: "Single port", value: "single" }, | ||||
|                 { label: "Range of ports", value: "range" }, | ||||
|               ]} | ||||
|               value={p.entry.host_port.type} | ||||
|               onValueChange={(v) => { | ||||
|                 p.entry.host_port.type = v as any; | ||||
|                 p.onChange?.(); | ||||
|               }} | ||||
|             /> | ||||
|  | ||||
|             {p.entry.host_port.type === "single" && ( | ||||
|               <PortInput | ||||
|                 {...p} | ||||
|                 label="Host port" | ||||
|                 value={p.entry.host_port.port} | ||||
|                 onChange={(v) => { | ||||
|                   if (p.entry.host_port.type === "single") | ||||
|                     p.entry.host_port.port = v!; | ||||
|                   p.onChange?.(); | ||||
|                 }} | ||||
|               /> | ||||
|             )} | ||||
|  | ||||
|             {p.entry.host_port.type === "range" && ( | ||||
|               <div style={{ display: "flex" }}> | ||||
|                 <PortInput | ||||
|                   {...p} | ||||
|                   label="Host port start" | ||||
|                   value={p.entry.host_port.start} | ||||
|                   onChange={(v) => { | ||||
|                     if (p.entry.host_port.type === "range") | ||||
|                       p.entry.host_port.start = v!; | ||||
|                     p.onChange?.(); | ||||
|                   }} | ||||
|                 /> | ||||
|                 <PortSpacer /> | ||||
|                 <PortInput | ||||
|                   {...p} | ||||
|                   label="Host port end" | ||||
|                   value={p.entry.host_port.end} | ||||
|                   onChange={(v) => { | ||||
|                     if (p.entry.host_port.type === "range") | ||||
|                       p.entry.host_port.end = v!; | ||||
|                     p.onChange?.(); | ||||
|                   }} | ||||
|                 /> | ||||
|               </div> | ||||
|             )} | ||||
|           </NATEntryProp> | ||||
|  | ||||
|           <NATEntryProp> | ||||
|             <div style={{ display: "flex", height: "100%", alignItems: "end" }}> | ||||
|               <PortInput | ||||
|                 {...p} | ||||
|                 label={`Guest port ${guestPortEnd ? "start" : ""}`} | ||||
|                 value={p.entry.guest_port} | ||||
|                 onChange={(v) => { | ||||
|                   p.entry.guest_port = v!; | ||||
|                   p.onChange?.(); | ||||
|                 }} | ||||
|               /> | ||||
|               {guestPortEnd && <PortSpacer />} | ||||
|               {guestPortEnd && ( | ||||
|                 <PortInput | ||||
|                   editable={false} | ||||
|                   label={`Guest port end`} | ||||
|                   value={guestPortEnd} | ||||
|                   onChange={(v) => { | ||||
|                     p.entry.guest_port = v!; | ||||
|                     p.onChange?.(); | ||||
|                   }} | ||||
|                 /> | ||||
|               )} | ||||
|             </div> | ||||
|           </NATEntryProp> | ||||
|         </Grid> | ||||
|       </CardContent> | ||||
|       <CardActions> | ||||
|         {p.editable && ( | ||||
|           <Tooltip title="Remove the entry"> | ||||
|             <IconButton color="error" onClick={p.onDelete}> | ||||
|               <DeleteIcon /> | ||||
|             </IconButton> | ||||
|           </Tooltip> | ||||
|         )} | ||||
|       </CardActions> | ||||
|     </Card> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function NATEntryProp( | ||||
|   p: PropsWithChildren<{ label?: string }> | ||||
| ): React.ReactElement { | ||||
|   return ( | ||||
|     <Grid size={{ sm: 12, md: 6 }} style={{ padding: "20px" }}> | ||||
|       {p.label && ( | ||||
|         <Typography variant="h6" style={{ marginBottom: "10px" }}> | ||||
|           {p.label} | ||||
|         </Typography> | ||||
|       )} | ||||
|       {p.children} | ||||
|     </Grid> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function PortSpacer(): React.ReactElement { | ||||
|   return <span style={{ width: "20px" }}></span>; | ||||
| } | ||||
							
								
								
									
										29
									
								
								virtweb_frontend/src/widgets/forms/PortInput.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								virtweb_frontend/src/widgets/forms/PortInput.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| import { TextInput } from "./TextInput"; | ||||
|  | ||||
| export function PortInput(p: { | ||||
|   editable: boolean; | ||||
|   label: string; | ||||
|   value?: number; | ||||
|   onChange: (value: number | undefined) => void; | ||||
| }): React.ReactElement { | ||||
|   return ( | ||||
|     <TextInput | ||||
|       {...p} | ||||
|       value={p.value?.toString() ?? ""} | ||||
|       type="number" | ||||
|       onValueChange={(v) => { | ||||
|         p.onChange?.(sanitizePort(v)); | ||||
|       }} | ||||
|       checkValue={(v) => Number(v) <= 65535} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function sanitizePort(port?: string): number | undefined { | ||||
|   if (port === undefined) return undefined; | ||||
|   const val = Number(port); | ||||
|  | ||||
|   if (val < 0) return 0; | ||||
|   if (val > 65535) return 65535; | ||||
|   return val; | ||||
| } | ||||
							
								
								
									
										40
									
								
								virtweb_frontend/src/widgets/forms/RadioGroupInput.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								virtweb_frontend/src/widgets/forms/RadioGroupInput.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| import { | ||||
|   RadioGroup, | ||||
|   FormControlLabel, | ||||
|   Radio, | ||||
|   FormControl, | ||||
|   FormLabel, | ||||
| } from "@mui/material"; | ||||
|  | ||||
| interface RadioGroupOption { | ||||
|   label: string; | ||||
|   value: string; | ||||
| } | ||||
|  | ||||
| export function RadioGroupInput(p: { | ||||
|   editable: boolean; | ||||
|   label?: string; | ||||
|   options: RadioGroupOption[]; | ||||
|   value: string; | ||||
|   onValueChange: (v: string) => void; | ||||
| }): React.ReactElement { | ||||
|   return ( | ||||
|     <FormControl> | ||||
|       {p.label && <FormLabel>{p.label}</FormLabel>} | ||||
|       <RadioGroup | ||||
|         row | ||||
|         value={p.value} | ||||
|         onChange={(_ev, v) => p.onValueChange?.(v)} | ||||
|       > | ||||
|         {p.options.map((o) => ( | ||||
|           <FormControlLabel | ||||
|             disabled={!p.editable} | ||||
|             value={o.value} | ||||
|             control={<Radio />} | ||||
|             label={o.label} | ||||
|           /> | ||||
|         ))} | ||||
|       </RadioGroup> | ||||
|     </FormControl> | ||||
|   ); | ||||
| } | ||||
| @@ -9,26 +9,27 @@ import { TextInput } from "./TextInput"; | ||||
|  | ||||
| export interface SelectOption { | ||||
|   value?: string; | ||||
|   label: string; | ||||
|   label?: string; | ||||
|   description?: string; | ||||
| } | ||||
|  | ||||
| export function SelectInput(p: { | ||||
|   value?: string; | ||||
|   editable: boolean; | ||||
|   label: string; | ||||
|   label?: string; | ||||
|   options: SelectOption[]; | ||||
|   onValueChange: (o?: string) => void; | ||||
| }): React.ReactElement { | ||||
|   if (!p.editable && !p.value) return <></>; | ||||
|  | ||||
|   if (!p.editable) { | ||||
|     const value = p.options.find((o) => o.value === p.value)?.label; | ||||
|     const value = p.options.find((o) => o.value === p.value)?.label ?? p.value; | ||||
|     return <TextInput label={p.label} editable={p.editable} value={value} />; | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <FormControl fullWidth variant="standard" style={{ marginBottom: "15px" }}> | ||||
|       <InputLabel>{p.label}</InputLabel> | ||||
|       {p.label && <InputLabel>{p.label}</InputLabel>} | ||||
|       <Select | ||||
|         value={p.value ?? ""} | ||||
|         label={p.label} | ||||
| @@ -41,7 +42,7 @@ export function SelectInput(p: { | ||||
|             style={{ fontStyle: e.value === undefined ? "italic" : undefined }} | ||||
|           > | ||||
|             <div> | ||||
|               {e.label} | ||||
|               {e.label ?? e.value} | ||||
|               {e.description && ( | ||||
|                 <Typography | ||||
|                   component={"div"} | ||||
|   | ||||
| @@ -5,7 +5,7 @@ import { LenConstraint } from "../../api/ServerApi"; | ||||
|  * Couple / Member property edition | ||||
|  */ | ||||
| export function TextInput(p: { | ||||
|   label: string; | ||||
|   label?: string; | ||||
|   editable: boolean; | ||||
|   value?: string; | ||||
|   onValueChange?: (newVal: string | undefined) => void; | ||||
| @@ -15,6 +15,8 @@ export function TextInput(p: { | ||||
|   minRows?: number; | ||||
|   maxRows?: number; | ||||
|   type?: React.HTMLInputTypeAttribute; | ||||
|   style?: React.CSSProperties; | ||||
|   helperText?: string; | ||||
| }): React.ReactElement { | ||||
|   if (!p.editable && (p.value ?? "") === "") return <></>; | ||||
|  | ||||
| @@ -48,12 +50,12 @@ export function TextInput(p: { | ||||
|         type: p.type, | ||||
|       }} | ||||
|       variant={"standard"} | ||||
|       style={{ width: "100%", marginBottom: "15px" }} | ||||
|       style={p.style ?? { width: "100%", marginBottom: "15px" }} | ||||
|       multiline={p.multiline} | ||||
|       minRows={p.minRows} | ||||
|       maxRows={p.maxRows} | ||||
|       error={valueError !== undefined} | ||||
|       helperText={valueError} | ||||
|       helperText={valueError ?? p.helperText} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,97 @@ | ||||
| import DeleteIcon from "@mui/icons-material/Delete"; | ||||
| import { | ||||
|   Button, | ||||
|   IconButton, | ||||
|   Paper, | ||||
|   Table, | ||||
|   TableBody, | ||||
|   TableCell, | ||||
|   TableContainer, | ||||
|   TableHead, | ||||
|   TableRow, | ||||
|   Tooltip, | ||||
| } from "@mui/material"; | ||||
| import { VMNetInterfaceFilter } from "../../api/VMApi"; | ||||
| import { TextInput } from "./TextInput"; | ||||
|  | ||||
| export function VMNetworkFilterParameters(p: { | ||||
|   editable: boolean; | ||||
|   filterref: VMNetInterfaceFilter; | ||||
|   onChange?: () => void; | ||||
| }): React.ReactElement { | ||||
|   if (!p.editable && p.filterref.parameters.length === 0) return <></>; | ||||
|  | ||||
|   const addParameter = () => { | ||||
|     p.filterref.parameters.push({ name: "", value: "" }); | ||||
|     p.onChange?.(); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       {p.filterref.parameters.length > 0 && ( | ||||
|         <TableContainer component={Paper}> | ||||
|           <Table size="small" aria-label="nwfilter parameters"> | ||||
|             <TableHead> | ||||
|               <TableRow> | ||||
|                 <TableCell>Name</TableCell> | ||||
|                 <TableCell>Value</TableCell> | ||||
|                 {p.editable && <TableCell></TableCell>} | ||||
|               </TableRow> | ||||
|             </TableHead> | ||||
|             <TableBody> | ||||
|               {p.filterref.parameters.map((row, index) => ( | ||||
|                 <TableRow | ||||
|                   key={index} | ||||
|                   sx={{ "&:last-child td, &:last-child th": { border: 0 } }} | ||||
|                 > | ||||
|                   <TableCell | ||||
|                     component="th" | ||||
|                     scope="row" | ||||
|                     style={{ padding: "0px 5px" }} | ||||
|                   > | ||||
|                     <TextInput | ||||
|                       editable={p.editable} | ||||
|                       value={row.name} | ||||
|                       onValueChange={(v) => { | ||||
|                         row.name = v ?? ""; | ||||
|                         p.onChange?.(); | ||||
|                       }} | ||||
|                     /> | ||||
|                   </TableCell> | ||||
|                   <TableCell scope="row" style={{ padding: "0px 5px" }}> | ||||
|                     <TextInput | ||||
|                       editable={p.editable} | ||||
|                       value={row.value} | ||||
|                       onValueChange={(v) => { | ||||
|                         row.value = v ?? ""; | ||||
|                         p.onChange?.(); | ||||
|                       }} | ||||
|                     /> | ||||
|                   </TableCell> | ||||
|                   {p.editable && ( | ||||
|                     <TableCell style={{ padding: "0px" }}> | ||||
|                       <IconButton | ||||
|                         onClick={() => { | ||||
|                           p.filterref.parameters.splice(index, 1); | ||||
|                           p.onChange?.(); | ||||
|                         }} | ||||
|                       > | ||||
|                         <Tooltip title="Remove parameter"> | ||||
|                           <DeleteIcon /> | ||||
|                         </Tooltip> | ||||
|                       </IconButton> | ||||
|                     </TableCell> | ||||
|                   )} | ||||
|                 </TableRow> | ||||
|               ))} | ||||
|             </TableBody> | ||||
|           </Table> | ||||
|         </TableContainer> | ||||
|       )} | ||||
|  | ||||
|       {p.editable && ( | ||||
|         <Button onClick={addParameter}>Add a filter ref parameter</Button> | ||||
|       )} | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
| @@ -10,19 +10,25 @@ import { | ||||
|   ListItemText, | ||||
|   Tooltip, | ||||
| } from "@mui/material"; | ||||
| import Grid from "@mui/material/Grid2"; | ||||
| import { NWFilter } from "../../api/NWFilterApi"; | ||||
| import { NetworkInfo } from "../../api/NetworksApi"; | ||||
| import { ServerApi } from "../../api/ServerApi"; | ||||
| import { VMInfo, VMNetInterface } from "../../api/VMApi"; | ||||
| import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider"; | ||||
| import { SelectInput } from "./SelectInput"; | ||||
| import { NetworkInfo } from "../../api/NetworksApi"; | ||||
| import { randomMacAddress } from "../../utils/RandUtils"; | ||||
| import { ServerApi } from "../../api/ServerApi"; | ||||
| import { EditSection } from "./EditSection"; | ||||
| import { MACInput } from "./MACInput"; | ||||
| import { NWFilterSelectInput } from "./NWFilterSelectInput"; | ||||
| import { SelectInput } from "./SelectInput"; | ||||
| import { VMNetworkFilterParameters } from "./VMNetworkFilterParameters"; | ||||
|  | ||||
| export function VMNetworksList(p: { | ||||
|   vm: VMInfo; | ||||
|   onChange?: () => void; | ||||
|   editable: boolean; | ||||
|   networksList: NetworkInfo[]; | ||||
|   networkFiltersList: NWFilter[]; | ||||
| }): React.ReactElement { | ||||
|   const addNew = () => { | ||||
|     p.vm.networks.push({ | ||||
| @@ -34,22 +40,28 @@ export function VMNetworksList(p: { | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       {/* networks list */} | ||||
|       {p.vm.networks.map((n, num) => ( | ||||
|         <NetworkInfoWidget | ||||
|           key={num} | ||||
|           network={n} | ||||
|           removeFromList={() => { | ||||
|             p.vm.networks.splice(num, 1); | ||||
|             p.onChange?.(); | ||||
|           }} | ||||
|           {...p} | ||||
|         /> | ||||
|       ))} | ||||
|  | ||||
|       {p.editable && ( | ||||
|         <Button onClick={addNew}>Add a new network interface</Button> | ||||
|         <div style={{ textAlign: "right", marginTop: "5px" }}> | ||||
|           <Button onClick={addNew}>Add a new network interface</Button> | ||||
|         </div> | ||||
|       )} | ||||
|  | ||||
|       <Grid container spacing={2}> | ||||
|         {/* networks list */} | ||||
|         {p.vm.networks.map((n, num) => ( | ||||
|           <EditSection key={num}> | ||||
|             <NetworkInfoWidget | ||||
|               key={num} | ||||
|               network={n} | ||||
|               removeFromList={() => { | ||||
|                 p.vm.networks.splice(num, 1); | ||||
|                 p.onChange?.(); | ||||
|               }} | ||||
|               {...p} | ||||
|             /> | ||||
|           </EditSection> | ||||
|         ))} | ||||
|       </Grid> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
| @@ -60,6 +72,7 @@ function NetworkInfoWidget(p: { | ||||
|   onChange?: () => void; | ||||
|   removeFromList: () => void; | ||||
|   networksList: NetworkInfo[]; | ||||
|   networkFiltersList: NWFilter[]; | ||||
| }): React.ReactElement { | ||||
|   const confirm = useConfirm(); | ||||
|   const deleteNetwork = async () => { | ||||
| @@ -137,28 +150,58 @@ function NetworkInfoWidget(p: { | ||||
|         /> | ||||
|  | ||||
|         {p.network.type === "DefinedNetwork" && ( | ||||
|           <SelectInput | ||||
|             editable={p.editable} | ||||
|             label="Defined network" | ||||
|             options={p.networksList.map((n) => { | ||||
|               const chars = [n.forward_mode.toString()]; | ||||
|               if (n.ip_v4) chars.push("IPv4"); | ||||
|               if (n.ip_v6) chars.push("IPv6"); | ||||
|               if (n.description) chars.push(n.description); | ||||
|           <> | ||||
|             <SelectInput | ||||
|               editable={p.editable} | ||||
|               label="Defined network" | ||||
|               options={p.networksList.map((n) => { | ||||
|                 const chars = [n.forward_mode.toString()]; | ||||
|                 if (n.ip_v4) chars.push("IPv4"); | ||||
|                 if (n.ip_v6) chars.push("IPv6"); | ||||
|                 if (n.description) chars.push(n.description); | ||||
|  | ||||
|               return { | ||||
|                 label: n.name, | ||||
|                 value: n.name, | ||||
|                 description: chars.join(" - "), | ||||
|               }; | ||||
|             })} | ||||
|             value={p.network.network} | ||||
|             onValueChange={(v) => { | ||||
|               if (p.network.type === "DefinedNetwork") | ||||
|                 p.network.network = v as any; | ||||
|               p.onChange?.(); | ||||
|             }} | ||||
|           /> | ||||
|                 return { | ||||
|                   label: n.name, | ||||
|                   value: n.name, | ||||
|                   description: chars.join(" - "), | ||||
|                 }; | ||||
|               })} | ||||
|               value={p.network.network} | ||||
|               onValueChange={(v) => { | ||||
|                 if (p.network.type === "DefinedNetwork") | ||||
|                   p.network.network = v as any; | ||||
|                 p.onChange?.(); | ||||
|               }} | ||||
|             /> | ||||
|  | ||||
|             {/* Network Filter */} | ||||
|             <NWFilterSelectInput | ||||
|               editable={p.editable} | ||||
|               label="Network filter" | ||||
|               value={p.network.nwfilterref?.name} | ||||
|               onChange={(v) => { | ||||
|                 if (v && !p.network.nwfilterref) { | ||||
|                   p.network.nwfilterref = { name: v, parameters: [] }; | ||||
|                 } else if (v) { | ||||
|                   p.network.nwfilterref!.name = v; | ||||
|                 } else { | ||||
|                   p.network.nwfilterref = undefined; | ||||
|                 } | ||||
|                 p.onChange?.(); | ||||
|               }} | ||||
|               canBeNull={true} | ||||
|               nwfilters={p.networkFiltersList} | ||||
|             /> | ||||
|  | ||||
|             {p.network.nwfilterref && ( | ||||
|               <div style={{ margin: "10px" }}> | ||||
|                 <VMNetworkFilterParameters | ||||
|                   filterref={p.network.nwfilterref} | ||||
|                   {...p} | ||||
|                 /> | ||||
|               </div> | ||||
|             )} | ||||
|           </> | ||||
|         )} | ||||
|       </div> | ||||
|     </> | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user