198 Commits

Author SHA1 Message Date
f70c59f260 Update dependency @mui/x-charts to ^8.11.2
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-09-11 00:40:14 +00:00
809e3c9d4e Merge pull request 'Update dependency vite to ^6.3.6' (#414) from renovate/vite-6.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2025-09-09 00:40:41 +00:00
e6a86b0097 Update dependency vite to ^6.3.6
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-09-09 00:40:30 +00:00
c12f916a94 Merge pull request 'Update dependency @eslint/js to ^9.35.0' (#413) from renovate/eslint-js-9.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2025-09-08 00:41:31 +00:00
2c9a2e1ac0 Update dependency @eslint/js to ^9.35.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-09-08 00:41:28 +00:00
99b25fb97e Merge pull request 'Update Rust crate log to 0.4.28' (#412) from renovate/log-0.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2025-09-07 00:41:37 +00:00
ab5f9e1105 Merge pull request 'Update Rust crate image to 0.25.8' (#411) from renovate/image-0.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2025-09-07 00:41:37 +00:00
a867fa311d Update Rust crate log to 0.4.28
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-09-06 00:41:33 +00:00
5461968282 Update Rust crate image to 0.25.8
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-09-06 00:41:31 +00:00
a5af85057f Merge pull request 'Update Rust crate clap to 4.5.47' (#410) from renovate/clap-4.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2025-09-05 00:27:14 +00:00
79c349ff66 Merge pull request 'Update Rust crate actix-http to 3.11.1' (#409) from renovate/actix-http-3.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2025-09-05 00:27:13 +00:00
ced0e4517d Update Rust crate clap to 4.5.47
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-09-04 00:34:38 +00:00
d0c9c01d63 Update Rust crate actix-http to 3.11.1
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-09-04 00:34:36 +00:00
04400ec31e Merge pull request 'Update Rust crate quick-xml to 0.38.3' (#408) from renovate/quick-xml-0.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2025-09-03 00:33:16 +00:00
85785d3568 Merge pull request 'Update Rust crate actix-files to 0.6.7' (#407) from renovate/actix-files-0.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2025-09-03 00:33:13 +00:00
8c5814307b Update Rust crate quick-xml to 0.38.3
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-09-02 00:33:20 +00:00
e2d4b1e3b1 Update Rust crate actix-files to 0.6.7
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-09-02 00:33:17 +00:00
550e094efe Merge pull request 'Update dependency eslint-plugin-react-x to ^1.52.9' (#406) from renovate/eslint-plugin-react-x-1.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2025-09-01 00:32:07 +00:00
94533e7e71 Update dependency eslint-plugin-react-x to ^1.52.9
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-09-01 00:32:02 +00:00
5cb9bbbc89 Merge pull request 'Update dependency eslint-plugin-react-dom to ^1.52.9' (#405) from renovate/eslint-plugin-react-dom-1.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-31 00:32:45 +00:00
f7da80f4c0 Update dependency eslint-plugin-react-dom to ^1.52.9
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2025-08-31 00:32:42 +00:00
64d095ec5e Merge pull request 'Update dependency eslint-plugin-react-dom to ^1.52.8' (#404) from renovate/eslint-plugin-react-dom-1.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-30 00:31:47 +00:00
5c6caf150e Update dependency eslint-plugin-react-dom to ^1.52.8
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2025-08-30 00:31:43 +00:00
43c09bb0ff Merge pull request 'Update dependency @types/react-dom to ^19.1.9' (#403) from renovate/react into master
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-29 00:33:21 +00:00
93dbad2f41 Update dependency @types/react-dom to ^19.1.9
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2025-08-29 00:33:20 +00:00
a1e23efd99 Fix cargo clippy issues
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-28 08:05:22 +02:00
cd4b8af445 Merge pull request 'Update dependency @types/react to ^19.1.12' (#402) from renovate/react into master
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-28 00:33:29 +00:00
3c2470e942 Update dependency @types/react to ^19.1.12
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2025-08-28 00:33:26 +00:00
de3a1aab1b Merge pull request 'Update dependency @types/react-dom to ^19.1.8' (#401) from renovate/react into master
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-27 00:33:15 +00:00
0149f93721 Update dependency @types/react-dom to ^19.1.8
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2025-08-27 00:33:12 +00:00
8daba3e92f Merge pull request 'Update dependency react-syntax-highlighter to ^15.6.6' (#400) from renovate/react-syntax-highlighter-15.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-26 00:31:59 +00:00
8127bfbdbf Update dependency react-syntax-highlighter to ^15.6.6
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-08-26 00:31:55 +00:00
7cb9a97025 Merge pull request 'Update Rust crate url to 2.5.7' (#399) from renovate/url-2.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-25 00:31:48 +00:00
a2fa115e38 Merge pull request 'Update Rust crate thiserror to 2.0.16' (#398) from renovate/thiserror-2.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-25 00:31:45 +00:00
b8be645121 Update Rust crate url to 2.5.7
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-08-24 00:31:39 +00:00
456ffbe062 Update Rust crate thiserror to 2.0.16
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-08-24 00:31:37 +00:00
5cdd8ba0c5 Merge pull request 'Update dependency @types/react to ^19.1.11' (#397) from renovate/react into master
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-23 00:32:42 +00:00
1e2d499794 Update dependency @types/react to ^19.1.11
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2025-08-23 00:32:39 +00:00
144c9af8c7 Merge pull request 'Update dependency react-syntax-highlighter to ^15.6.3' (#396) from renovate/react-syntax-highlighter-15.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-22 00:32:09 +00:00
b7d56880ff Update dependency react-syntax-highlighter to ^15.6.3
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-08-22 00:32:05 +00:00
20d55be54f Merge pull request 'Update Rust crate serde_json to 1.0.143' (#395) from renovate/serde_json-1.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-21 00:32:49 +00:00
920ee5d636 Merge pull request 'Update Rust crate quick-xml to 0.38.2' (#394) from renovate/quick-xml-0.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-21 00:32:46 +00:00
1cb4700ac1 Update Rust crate serde_json to 1.0.143
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-08-20 00:32:16 +00:00
46de03bcc4 Update Rust crate quick-xml to 0.38.2
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-08-20 00:32:13 +00:00
8b96d4f0ab Merge pull request 'Update dependency eslint-plugin-react-x to ^1.52.6' (#393) from renovate/eslint-plugin-react-x-1.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-19 00:32:29 +00:00
48042e2b59 Update dependency eslint-plugin-react-x to ^1.52.6
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2025-08-19 00:32:26 +00:00
b2d6f92d47 Merge pull request 'Update dependency eslint-plugin-react-dom to ^1.52.6' (#392) from renovate/eslint-plugin-react-dom-1.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-19 00:32:23 +00:00
063a7fc830 Update dependency eslint-plugin-react-dom to ^1.52.6
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2025-08-19 00:32:21 +00:00
0c12822567 Merge pull request 'Update Rust crate thiserror to 2.0.15' (#391) from renovate/thiserror-2.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-18 00:31:11 +00:00
38f0ac840b Merge pull request 'Update Rust crate reqwest to 0.12.23' (#390) from renovate/reqwest-0.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-18 00:31:11 +00:00
8db4fd740d Update Rust crate thiserror to 2.0.15
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-08-17 00:32:46 +00:00
456ab796b3 Update Rust crate reqwest to 0.12.23
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-08-17 00:32:43 +00:00
2fbc5b04a3 Merge pull request 'Update dependency eslint-plugin-react-x to ^1.52.5' (#389) from renovate/eslint-plugin-react-x-1.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-16 00:31:14 +00:00
5c2aa39196 Update dependency eslint-plugin-react-x to ^1.52.5
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2025-08-16 00:31:13 +00:00
513bda8fe7 Merge pull request 'Update dependency eslint-plugin-react-dom to ^1.52.5' (#388) from renovate/eslint-plugin-react-dom-1.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-16 00:31:11 +00:00
48b4e4bb2f Update dependency eslint-plugin-react-dom to ^1.52.5
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-08-16 00:31:06 +00:00
e7cb217e5f Merge pull request 'Update dependency eslint-plugin-react-x to ^1.52.4' (#387) from renovate/eslint-plugin-react-x-1.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-15 00:32:50 +00:00
a672645225 Update dependency eslint-plugin-react-x to ^1.52.4
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2025-08-15 00:32:47 +00:00
3428b5b457 Merge pull request 'Update dependency eslint-plugin-react-dom to ^1.52.4' (#386) from renovate/eslint-plugin-react-dom-1.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-15 00:32:45 +00:00
15eabf7c66 Update dependency eslint-plugin-react-dom to ^1.52.4
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2025-08-15 00:32:43 +00:00
8df6b549ac Merge pull request 'Update Rust crate clap to 4.5.45' (#385) from renovate/clap-4.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-14 00:32:20 +00:00
39a896ea20 Merge pull request 'Update Rust crate anyhow to 1.0.99' (#384) from renovate/anyhow-1.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-14 00:32:19 +00:00
acc6c680d4 Update Rust crate clap to 4.5.45
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-08-13 00:32:59 +00:00
3fd9f7cff8 Update Rust crate anyhow to 1.0.99
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-08-13 00:32:57 +00:00
a672329501 Merge pull request 'Update dependency @types/react to ^19.1.10' (#383) from renovate/react into master
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-12 00:32:02 +00:00
5f119688a3 Update dependency @types/react to ^19.1.10
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2025-08-12 00:31:58 +00:00
dbf7cc7cac Merge pull request 'Update materialui to ^7.3.1' (#382) from renovate/materialui into master
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-11 00:32:19 +00:00
6198a8284e Update materialui to ^7.3.1
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2025-08-11 00:32:16 +00:00
f0547dd9cc Merge pull request 'Update dependency eslint to ^9.33.0' (#381) from renovate/eslint-9.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-10 00:31:14 +00:00
fbc2b6fe2a Update dependency eslint to ^9.33.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2025-08-10 00:31:11 +00:00
2e3059dad6 Merge pull request 'Update dependency @mui/x-data-grid to ^8.10.0' (#380) from renovate/mui-x-data-grid-8.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-09 00:31:50 +00:00
c00668f174 Update dependency @mui/x-data-grid to ^8.10.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2025-08-09 00:31:48 +00:00
55b74becdc Merge pull request 'Update dependency @eslint/js to ^9.33.0' (#379) from renovate/eslint-js-9.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-09 00:31:41 +00:00
d2ca41c59d Update dependency @eslint/js to ^9.33.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2025-08-09 00:31:38 +00:00
4df1f413a4 Merge pull request 'Update dependency @types/jest to v30' (#378) from renovate/major-jest-monorepo into master
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-08 00:19:57 +00:00
3937d6d5de Merge pull request 'Update dependency react-router-dom to ^7.8.0' (#377) from renovate/react-router-dom-7.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-08 00:19:54 +00:00
9d2667bba3 Update dependency react-router-dom to ^7.8.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2025-08-08 00:19:52 +00:00
803f063d64 Update dependency @types/jest to v30
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-08-07 00:20:40 +00:00
f323d54c18 Merge pull request 'Update Rust crate zip to 4.3.0' (#376) from renovate/zip-4.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-07 00:20:30 +00:00
ad3fad44e5 Merge pull request 'Update dependency yaml to ^2.8.1' (#375) from renovate/yaml-2.x into master
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-07 00:20:26 +00:00
fc975a5156 Update Rust crate zip to 4.3.0
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is failing
2025-08-06 00:19:43 +00:00
064e5dfef5 Update dependency yaml to ^2.8.1
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-08-06 00:19:32 +00:00
6a3a24f724 Merge pull request 'Update Rust crate quick-xml to 0.38.1' (#374) from renovate/quick-xml-0.x into master
All checks were successful
continuous-integration/drone/push Build is passing
2025-08-06 00:19:25 +00:00
8bdc7a11ef Update Rust crate quick-xml to 0.38.1
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-08-05 00:19:29 +00:00
2d2602f0f8 Merge pull request 'Update Rust crate tokio to 1.47.1' (#324) from renovate/tokio-1.x into master
All checks were successful
continuous-integration/drone/push Build is passing
2025-08-05 00:19:22 +00:00
095d6d827f Update Rust crate tokio to 1.47.1
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-08-04 00:20:18 +00:00
c810c612de Merge pull request 'Update dependency typescript to ^5.9.2' (#373) from renovate/typescript-5.x into master
All checks were successful
continuous-integration/drone/push Build is passing
2025-08-03 00:22:45 +00:00
dbcfd527b8 Update dependency typescript to ^5.9.2
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is passing
2025-08-03 00:22:39 +00:00
117c79c855 Merge pull request 'Update Rust crate serde_json to 1.0.142' (#372) from renovate/serde_json-1.x into master
All checks were successful
continuous-integration/drone/push Build is passing
2025-08-03 00:22:29 +00:00
8b083e8e26 Update Rust crate serde_json to 1.0.142
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-08-02 00:21:27 +00:00
32f805aa3f Merge pull request 'Update dependency @mui/x-data-grid to ^8.9.2' (#371) from renovate/mui-x-data-grid-8.x into master
All checks were successful
continuous-integration/drone/push Build is passing
2025-08-02 00:21:19 +00:00
50ec6ecd9f Update dependency @mui/x-data-grid to ^8.9.2
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-08-01 00:24:12 +00:00
d3834ff7f7 Merge pull request 'Update Rust crate clap to 4.5.42' (#370) from renovate/clap-4.x into master
All checks were successful
continuous-integration/drone/push Build is passing
2025-08-01 00:24:05 +00:00
64d8420cee Update Rust crate clap to 4.5.42
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-31 00:19:24 +00:00
796cf757d4 Merge pull request 'Update react' (#369) from renovate/react into master
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-31 00:19:17 +00:00
4bb4f65d48 Update react
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-30 00:18:34 +00:00
5b916ec5a9 Merge pull request 'Update dependency @eslint/js to ^9.32.0' (#335) from renovate/eslint-js-9.x into master
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-30 00:18:28 +00:00
c5e43c7fbf Update dependency @eslint/js to ^9.32.0
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-29 00:22:19 +00:00
807dfc6635 Merge pull request 'Update Rust crate uuid to 1.17.0' (#368) from renovate/uuid-1.x into master
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-28 00:20:51 +00:00
4e1a2dcdba Update Rust crate uuid to 1.17.0
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is failing
2025-07-28 00:20:50 +00:00
843e53e83a Merge pull request 'Update Rust crate sysinfo to 0.36.1' (#367) from renovate/sysinfo-0.x into master
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-27 00:20:20 +00:00
5f911e6fb4 Update Rust crate sysinfo to 0.36.1
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is passing
2025-07-27 00:20:16 +00:00
6c42cd62df Merge pull request 'Update dependency eslint to ^9.32.0' (#366) from renovate/eslint-9.x into master
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-26 00:20:00 +00:00
a4a4954344 Update dependency eslint to ^9.32.0
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is failing
2025-07-26 00:19:58 +00:00
5d20081fca Merge pull request 'Update dependency react-router-dom to ^7.7.1' (#365) from renovate/react-router-dom-7.x into master
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-25 00:20:51 +00:00
f46abedd1a Update dependency react-router-dom to ^7.7.1
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is passing
2025-07-25 00:20:48 +00:00
0548e480b4 Merge pull request 'Update Rust crate quick-xml to 0.38.0' (#364) from renovate/quick-xml-0.x into master
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-24 00:21:06 +00:00
6b9455f6ea Update Rust crate quick-xml to 0.38.0
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is passing
2025-07-24 00:21:04 +00:00
17108a52a3 Merge pull request 'Update dependency typescript-eslint to ^8.38.0' (#363) from renovate/typescript-eslint-8.x into master
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-23 00:20:29 +00:00
47ebc77458 Update dependency typescript-eslint to ^8.38.0
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is passing
2025-07-23 00:20:28 +00:00
87e57dea56 Merge pull request 'Update dependency @mui/x-data-grid to ^8.9.1' (#362) from renovate/mui-x-data-grid-8.x into master
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-22 00:25:56 +00:00
24b4c02d9f Update dependency @mui/x-data-grid to ^8.9.1
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is passing
2025-07-22 00:25:42 +00:00
f2a3e16a1c Merge pull request 'Update Rust crate rand to 0.9.2' (#361) from renovate/rand-0.x into master
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-21 00:21:31 +00:00
6530a6d8e9 Update Rust crate rand to 0.9.2
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is passing
2025-07-21 00:21:29 +00:00
b741d79ba7 Merge pull request 'Update dependency @vitejs/plugin-react to ^4.7.0' (#360) from renovate/vitejs-plugin-react-4.x into master
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-20 00:18:51 +00:00
eeda3ca4ec Update dependency @vitejs/plugin-react to ^4.7.0
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is passing
2025-07-20 00:18:48 +00:00
992f91bb8a Merge pull request 'Update Rust crate serde_json to 1.0.141' (#359) from renovate/serde_json-1.x into master
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-19 00:22:33 +00:00
2f3ad231c7 Update Rust crate serde_json to 1.0.141
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is passing
2025-07-19 00:22:23 +00:00
6e41d3c6df Merge pull request 'Update materialui to ^7.2.0' (#358) from renovate/materialui into master
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-18 02:21:31 +00:00
f57f3f3c9a Update materialui to ^7.2.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-07-18 00:19:40 +00:00
34fda42766 Merge pull request 'Update dependency react-router-dom to ^7.7.0' (#357) from renovate/react-router-dom-7.x into master
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-17 02:41:12 +00:00
a9536f4091 Update dependency react-router-dom to ^7.7.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-07-17 00:19:55 +00:00
7770ad3ca7 Merge pull request 'Update dependency typescript-eslint to ^8.37.0' (#356) from renovate/typescript-eslint-8.x into master
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-16 02:28:11 +00:00
02b3373f51 Update dependency typescript-eslint to ^8.37.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-07-16 00:19:50 +00:00
5153af0618 Merge pull request 'Update dependency eslint to ^9.31.0' (#355) from renovate/eslint-9.x into master
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-15 02:15:03 +00:00
d50c3a00a5 Update dependency eslint to ^9.31.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-07-15 00:19:02 +00:00
4618c2c081 Merge pull request 'Update dependency eslint-plugin-react-x to ^1.52.3' (#354) from renovate/eslint-plugin-react-x-1.x into master
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-14 02:12:21 +00:00
34d78085c8 Update dependency eslint-plugin-react-x to ^1.52.3
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-07-14 00:18:46 +00:00
0381b73635 Merge pull request 'Update dependency eslint-plugin-react-dom to ^1.52.3' (#353) from renovate/eslint-plugin-react-dom-1.x into master
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-13 02:20:28 +00:00
2e88529e3d Update dependency eslint-plugin-react-dom to ^1.52.3
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-07-13 00:18:37 +00:00
84e3f61902 Merge pull request 'Update dependency @mui/x-data-grid to ^8.8.0' (#352) from renovate/mui-x-data-grid-8.x into master
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-12 02:35:41 +00:00
9623493aee Update dependency @mui/x-data-grid to ^8.8.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-07-12 00:18:58 +00:00
e5ad7d2fe4 Merge pull request 'Update dependency humanize-duration to ^3.33.0' (#351) from renovate/humanize-duration-3.x into master
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-11 02:28:44 +00:00
940179ffe5 Update dependency humanize-duration to ^3.33.0
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-11 00:19:12 +00:00
f7b27a527b Merge pull request 'Update Rust crate clap to 4.5.41' (#350) from renovate/clap-4.x into master
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-10 02:45:14 +00:00
caac7dc1bf Update Rust crate clap to 4.5.41
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-07-10 00:20:01 +00:00
a0f815534b Merge pull request 'Update dependency globals to ^16.3.0' (#349) from renovate/globals-16.x into master
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-09 02:31:14 +00:00
928c2c5cb4 Update dependency globals to ^16.3.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-07-09 00:19:18 +00:00
ddc8b65f8a Merge pull request 'Update dependency eslint to ^9.30.1' (#348) from renovate/eslint-9.x into master
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-08 02:47:12 +00:00
0b67659efa Update dependency eslint to ^9.30.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-07-08 00:19:22 +00:00
0601d9cad9 Merge pull request 'Update dependency @mui/x-data-grid to ^8.7.0' (#347) from renovate/mui-x-data-grid-8.x into master
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-07 02:12:52 +00:00
a1b6ebd9f5 Update dependency @mui/x-data-grid to ^8.7.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-07-07 00:18:43 +00:00
b8eedaab51 Merge pull request 'Update Rust crate reqwest to 0.12.22' (#346) from renovate/reqwest-0.x into master
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-06 02:00:23 +00:00
0afc3252c6 Update Rust crate reqwest to 0.12.22
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-06 00:18:48 +00:00
685f1bc502 Merge pull request 'Update dependency react-router-dom to ^7.6.3' (#345) from renovate/react-router-dom-7.x into master
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-05 02:15:49 +00:00
3e642dd638 Update dependency react-router-dom to ^7.6.3
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-07-05 00:12:33 +00:00
22ad68e43e Update dependency @emotion/styled to ^11.14.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-07-04 00:12:22 +00:00
1dd86807fd Fix cargo clippy issues
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-03 08:01:36 +02:00
96747bda89 Update dependency eslint-plugin-react-x to ^1.52.2
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is failing
2025-06-26 00:12:11 +00:00
e15514dd4f Update dependency eslint-plugin-react-dom to ^1.52.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-06-25 00:12:04 +00:00
7556ee2c06 Update dependency @vitejs/plugin-react to ^4.6.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-06-24 00:14:13 +00:00
992a902590 Update dependency eslint to ^9.29.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-06-23 00:24:41 +00:00
100f12e7c1 Update dependency @vitejs/plugin-react to ^4.5.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-06-22 00:23:00 +00:00
3de66a5873 Fix a bug in sysinfo route with docker containers
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-21 19:06:43 +02:00
49360188f5 Update dependency @mui/x-data-grid to ^8.5.3
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-06-21 00:23:42 +00:00
35c48ba846 Update materialui to ^7.1.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-06-20 00:25:00 +00:00
1ad4262086 Update Rust crate sysinfo to 0.35.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-06-19 00:23:07 +00:00
b633694f74 Add api client to release
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-06-18 19:07:43 +02:00
ab16bd7bcf Can export entire server configuration
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-17 21:17:25 +02:00
1080ab5cb2 Fix boolean config
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-17 19:13:43 +02:00
a2845ddafe Enable word wrapping in Monaco editors
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-16 21:53:43 +02:00
c968b64b51 Fix ESLint issues
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-16 21:52:00 +02:00
12833dc6da Fix cloud init command
Some checks failed
continuous-integration/drone/push Build is failing
2025-06-16 21:48:06 +02:00
8c4f2a9f2d Fix issue with Ubuntu cloud images
Some checks failed
continuous-integration/drone/push Build is failing
2025-06-16 21:31:33 +02:00
9a6b6cfb2d Can change default username
Some checks failed
continuous-integration/drone/push Build is failing
2025-06-16 19:56:04 +02:00
b28ca5f27d Add new options
Some checks failed
continuous-integration/drone/push Build is failing
2025-06-16 19:42:57 +02:00
92f187bf91 Start to auto-fill cloudinit fields
Some checks failed
continuous-integration/drone/push Build is failing
2025-06-16 19:31:13 +02:00
9f1f4b44ca Make network configuration editable 2025-06-16 18:51:33 +02:00
b00d46105b Update Rust crate reqwest to 0.12.20
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-06-15 00:24:45 +00:00
7d6ccd4ce6 Update dependency @types/react to ^19.1.8
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-06-14 00:25:09 +00:00
b5371421e1 Update dependency @fontsource/roboto to ^5.2.6
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-06-13 00:25:12 +00:00
07d305c54e Update Rust crate clap to 4.5.40
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-06-12 00:26:01 +00:00
78c525f47a Update Rust crate actix-http to 3.11.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-06-11 00:26:04 +00:00
978a881372 Basic check of user data structure for errors
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-10 21:57:58 +02:00
ee733b04f3 Fix ESLint issue
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-10 21:49:05 +02:00
0d8ef226c1 Can edit Cloud init user data from UI
Some checks failed
continuous-integration/drone/push Build is failing
2025-06-10 21:46:50 +02:00
feca07558e Can customize from UI Cloud init metadata
Some checks failed
continuous-integration/drone/push Build is failing
2025-06-10 20:30:51 +02:00
0de15af10e Normalize disk size input
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-09 18:11:49 +02:00
d4bc92f562 Do not offer to expand disk if disk deletion is request by user
Some checks failed
continuous-integration/drone/push Build is failing
2025-06-09 18:09:55 +02:00
a1439689dd Can resize existing disks
Some checks failed
continuous-integration/drone/push Build is failing
2025-06-09 18:00:23 +02:00
63126c75fa Prevent shrinking attempts
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-09 17:48:17 +02:00
940302a825 Can resize disk image when adding a new disk image to a VM
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-09 17:42:36 +02:00
9c374f849b Shwo a message when some lists are empty
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-09 17:17:54 +02:00
2fa4d0e11b Can compress XZ files
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-09 17:05:47 +02:00
d7796e1459 Can decompress XZ files 2025-06-09 17:04:35 +02:00
759361d9f6 Add qcow2.xz file format support 2025-06-09 16:58:21 +02:00
b2529c250a Minor improvements
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-09 16:43:27 +02:00
8a7712ec42 Add dsmode cloud-init metadata
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-07 11:50:22 +02:00
9609cfb33a Check if file can be loaded during disk image upload
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-07 11:28:39 +02:00
1fe7c60f36 Automatically generate cloud disk image when updating domains configuration
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-07 11:15:55 +02:00
f1339f0711 Generate cloud init disk image
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-07 10:32:39 +02:00
b3f56cea81 Add cloud init configuration to VM definition
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-06 17:54:56 +02:00
9bd702d60f Add cloud-localds as a required program
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-06 17:19:11 +02:00
c8b42626a9 Update dependency @types/react-dom to ^19.1.6
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-06-05 00:24:08 +00:00
4ef15507d9 Update dependency react-router-dom to ^7.6.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-06-04 00:25:09 +00:00
8a4b3a4db6 Update materialui to ^7.1.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-06-03 00:25:45 +00:00
8bce9ca9b7 Update dependency @types/react to ^19.1.6
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-06-02 00:23:25 +00:00
5574037b73 Update dependency eslint-plugin-react-hooks to ^5.2.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-06-01 00:23:13 +00:00
48 changed files with 2738 additions and 932 deletions

View File

@@ -46,8 +46,9 @@ steps:
- cd virtweb_backend
- mv /tmp/web_build/dist static
- cargo build --release
- ls -lah target/release/virtweb_backend
- cp target/release/virtweb_backend /tmp/release
- cargo build --release --example api_curl
- ls -lah target/release/virtweb_backend target/release/examples/api_curl
- cp target/release/virtweb_backend target/release/examples/api_curl /tmp/release
- name: gitea_release
image: plugins/gitea-release

File diff suppressed because it is too large Load Diff

View File

@@ -6,9 +6,9 @@ edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
log = "0.4.27"
log = "0.4.28"
env_logger = "0.11.8"
clap = { version = "4.5.38", features = ["derive", "env"] }
clap = { version = "4.5.47", features = ["derive", "env"] }
light-openid = { version = "1.0.4", features = ["crypto-wrapper"] }
lazy_static = "1.5.0"
actix = "0.13.5"
@@ -17,26 +17,27 @@ actix-remote-ip = "0.1.0"
actix-session = { version = "0.10.1", features = ["cookie-session"] }
actix-identity = "0.8.0"
actix-cors = "0.7.1"
actix-files = "0.6.6"
actix-files = "0.6.7"
actix-ws = "0.3.0"
actix-http = "3.10.0"
actix-http = "3.11.1"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
quick-xml = { version = "0.37.5", features = ["serialize", "overlapped-lists"] }
serde_json = "1.0.143"
serde_yml = "0.0.12"
quick-xml = { version = "0.38.3", features = ["serialize", "overlapped-lists"] }
futures-util = "0.3.31"
anyhow = "1.0.98"
anyhow = "1.0.99"
actix-multipart = "0.7.2"
tempfile = "3.20.0"
reqwest = { version = "0.12.15", features = ["stream"] }
url = "2.5.4"
reqwest = { version = "0.12.23", features = ["stream"] }
url = "2.5.7"
virt = "0.4.2"
sysinfo = { version = "0.35.1", features = ["serde"] }
uuid = { version = "1.16.0", features = ["v4", "serde"] }
sysinfo = { version = "0.36.1", features = ["serde"] }
uuid = { version = "1.17.0", features = ["v4", "serde"] }
lazy-regex = "3.4.1"
thiserror = "2.0.12"
image = "0.25.6"
rand = "0.9.1"
tokio = { version = "1.45.0", features = ["rt", "time", "macros"] }
thiserror = "2.0.16"
image = "0.25.8"
rand = "0.9.2"
tokio = { version = "1.47.1", features = ["rt", "time", "macros"] }
futures = "0.3.31"
ipnetwork = { version = "0.21.1", features = ["serde"] }
num = "0.4.3"
@@ -44,3 +45,5 @@ rust-embed = { version = "8.7.2", features = ["mime-guess"] }
dotenvy = "0.15.7"
nix = { version = "0.30.1", features = ["net"] }
basic-jwt = "0.3.0"
zip = "4.3.0"
chrono = "0.4.41"

View File

@@ -27,10 +27,7 @@ impl LibVirtActor {
/// Connect to hypervisor
pub async fn connect() -> anyhow::Result<Self> {
let hypervisor_uri = AppConfig::get().hypervisor_uri.as_deref().unwrap_or("");
log::info!(
"Will connect to hypvervisor at address '{}'",
hypervisor_uri
);
log::info!("Will connect to hypvervisor at address '{hypervisor_uri}'",);
let conn = Connect::open(Some(hypervisor_uri))?;
Ok(Self { m: conn })
@@ -102,7 +99,7 @@ impl Handler<GetDomainXMLReq> for LibVirtActor {
log::debug!("Get domain XML:\n{}", msg.0.as_string());
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);
log::debug!("XML = {xml}");
DomainXML::parse_xml(&xml)
}
}
@@ -131,7 +128,7 @@ impl Handler<DefineDomainReq> for LibVirtActor {
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);
log::debug!("Define domain:\n{xml}");
let domain = Domain::define_xml(&self.m, &xml)?;
let uuid = XMLUuid::parse_from_str(&domain.get_uuid_string()?)?;
@@ -182,6 +179,13 @@ impl Handler<DeleteDomainReq> for LibVirtActor {
false => sys::VIR_DOMAIN_UNDEFINE_NVRAM,
})?;
// Delete associated cloud init disk
let cloud_init_disk = AppConfig::get().cloud_init_disk_path_for_vm(&domain_name);
if cloud_init_disk.exists() {
std::fs::remove_file(cloud_init_disk)?;
}
// If requested, delete block storage associated with the VM
if !msg.keep_files {
log::info!("Delete storage associated with the domain");
let path = AppConfig::get().vm_storage_path(msg.id);
@@ -439,7 +443,7 @@ impl Handler<GetNetworkXMLReq> for LibVirtActor {
log::debug!("Get network XML:\n{}", msg.0.as_string());
let network = Network::lookup_by_uuid_string(&self.m, &msg.0.as_string())?;
let xml = network.get_xml_desc(0)?;
log::debug!("XML = {}", xml);
log::debug!("XML = {xml}");
NetworkXML::parse_xml(&xml)
}
}
@@ -595,7 +599,7 @@ impl Handler<GetNWFilterXMLReq> for LibVirtActor {
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);
log::debug!("XML = {xml}");
NetworkFilterXML::parse_xml(xml)
}
}
@@ -610,7 +614,7 @@ impl Handler<DefineNWFilterReq> for LibVirtActor {
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);
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()?)?;

View File

@@ -104,10 +104,10 @@ impl Token {
/// 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;
}
if let Some(max_inactivity) = self.max_inactivity
&& max_inactivity + self.last_used < time()
{
return true;
}
false
@@ -188,10 +188,10 @@ impl NewToken {
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!");
}
if let Some(t) = self.max_inactivity
&& t < 3600
{
return Some("API tokens shall be valid for at least 1 hour!");
}
None

View File

@@ -250,6 +250,19 @@ impl AppConfig {
self.storage_path().join("iso")
}
/// Get the path where generated cloud init disk image are stored
pub fn cloud_init_disk_storage_path(&self) -> PathBuf {
self.storage_path().join("cloud_init_disks")
}
/// Get the path where the disk image of a VM is stored
pub fn cloud_init_disk_path_for_vm(&self, name: &str) -> PathBuf {
self.cloud_init_disk_storage_path().join(format!(
"{}-{name}.iso",
constants::CLOUD_INIT_IMAGE_PREFIX_NAME
))
}
/// Get disk images storage directory
pub fn disk_images_storage_path(&self) -> PathBuf {
self.storage_path().join("disk_images")
@@ -267,7 +280,7 @@ impl AppConfig {
/// Get VM vnc sockets path for domain
pub fn vnc_socket_for_domain(&self, name: &str) -> PathBuf {
self.vnc_sockets_path().join(format!("vnc-{}", name))
self.vnc_sockets_path().join(format!("vnc-{name}"))
}
/// Get VM root disks storage directory

View File

@@ -30,8 +30,9 @@ pub const ALLOWED_ISO_MIME_TYPES: [&str; 4] = [
pub const ISO_MAX_SIZE: FileSize = FileSize::from_gb(10);
/// Allowed uploaded disk images formats
pub const ALLOWED_DISK_IMAGES_MIME_TYPES: [&str; 3] = [
pub const ALLOWED_DISK_IMAGES_MIME_TYPES: [&str; 4] = [
"application/x-qemu-disk",
"application/x-raw-disk-image",
"application/gzip",
"application/octet-stream",
];
@@ -57,6 +58,9 @@ pub const DISK_SIZE_MIN: FileSize = FileSize::from_mb(50);
/// Disk size max (B)
pub const DISK_SIZE_MAX: FileSize = FileSize::from_gb(20000);
/// Cloud init generated disk image prefix
pub const CLOUD_INIT_IMAGE_PREFIX_NAME: &str = "virtweb-cloudinit-autogen-image";
/// Net nat entry comment max size
pub const NET_NAT_COMMENT_MAX_SIZE: usize = 250;
@@ -122,19 +126,25 @@ pub const API_TOKEN_DESCRIPTION_MAX_LENGTH: usize = 30;
pub const API_TOKEN_RIGHT_PATH_MAX_LENGTH: usize = 255;
/// Qemu image program path
pub const QEMU_IMAGE_PROGRAM: &str = "/usr/bin/qemu-img";
pub const PROGRAM_QEMU_IMAGE: &str = "/usr/bin/qemu-img";
/// IP program path
pub const IP_PROGRAM: &str = "/usr/sbin/ip";
pub const PROGRAM_IP: &str = "/usr/sbin/ip";
/// Copy program path
pub const COPY_PROGRAM: &str = "/bin/cp";
pub const PROGRAM_COPY: &str = "/bin/cp";
/// Gzip program path
pub const GZIP_PROGRAM: &str = "/usr/bin/gzip";
pub const PROGRAM_GZIP: &str = "/usr/bin/gzip";
/// XZ program path
pub const PROGRAM_XZ: &str = "/usr/bin/xz";
/// Bash program
pub const BASH_PROGRAM: &str = "/usr/bin/bash";
pub const PROGRAM_BASH: &str = "/usr/bin/bash";
/// DD program
pub const DD_PROGRAM: &str = "/usr/bin/dd";
pub const PROGRAM_DD: &str = "/usr/bin/dd";
/// cloud-localds program
pub const PROGRAM_CLOUD_LOCALDS: &str = "/usr/bin/cloud-localds";

View File

@@ -31,13 +31,12 @@ pub async fn upload(MultipartForm(mut form): MultipartForm<UploadDiskImageForm>)
}
// Check file mime type
if let Some(mime_type) = file.content_type {
if !constants::ALLOWED_DISK_IMAGES_MIME_TYPES.contains(&mime_type.as_ref()) {
return Ok(HttpResponse::BadRequest().json(format!(
"Unsupported file type for disk upload: {}",
mime_type
)));
}
if let Some(mime_type) = file.content_type
&& !constants::ALLOWED_DISK_IMAGES_MIME_TYPES.contains(&mime_type.as_ref())
{
return Ok(HttpResponse::BadRequest().json(format!(
"Unsupported file type for disk upload: {mime_type}"
)));
}
// Extract and check file name
@@ -55,7 +54,15 @@ pub async fn upload(MultipartForm(mut form): MultipartForm<UploadDiskImageForm>)
}
// Copy the file to the destination
file.file.persist(dest_path)?;
file.file.persist(&dest_path)?;
// Check if file information can be loaded
if let Err(e) = DiskFileInfo::load_file(&dest_path) {
log::error!("Failed to get information about uploaded disk file! {e}");
std::fs::remove_file(&dest_path)?;
return Ok(HttpResponse::InternalServerError()
.json(format!("Unable to process uploaded file! {e}")));
}
Ok(HttpResponse::Ok().json("Successfully uploaded disk image!"))
}

View File

@@ -31,11 +31,11 @@ pub async fn upload_file(MultipartForm(mut form): MultipartForm<UploadIsoForm>)
return Ok(HttpResponse::BadRequest().json("File is too large!"));
}
if let Some(m) = &file.content_type {
if !constants::ALLOWED_ISO_MIME_TYPES.contains(&m.to_string().as_str()) {
log::error!("Uploaded ISO file has an invalid mimetype!");
return Ok(HttpResponse::BadRequest().json("Invalid mimetype!"));
}
if let Some(m) = &file.content_type
&& !constants::ALLOWED_ISO_MIME_TYPES.contains(&m.to_string().as_str())
{
log::error!("Uploaded ISO file has an invalid mimetype!");
return Ok(HttpResponse::BadRequest().json("Invalid mimetype!"));
}
let file_name = match &file.file_name {
@@ -52,7 +52,7 @@ pub async fn upload_file(MultipartForm(mut form): MultipartForm<UploadIsoForm>)
}
let dest_file = AppConfig::get().iso_storage_path().join(file_name);
log::info!("Will save ISO file {:?}", dest_file);
log::info!("Will save ISO file {dest_file:?}");
if dest_file.exists() {
log::error!("Conflict with uploaded iso file name!");
@@ -87,16 +87,16 @@ pub async fn upload_from_url(req: web::Json<DownloadFromURLReq>) -> HttpResult {
let response = reqwest::get(&req.url).await?;
if let Some(len) = response.content_length() {
if len > constants::ISO_MAX_SIZE.as_bytes() as u64 {
return Ok(HttpResponse::BadRequest().json("File is too large!"));
}
if let Some(len) = response.content_length()
&& len > constants::ISO_MAX_SIZE.as_bytes() as u64
{
return Ok(HttpResponse::BadRequest().json("File is too large!"));
}
if let Some(ct) = response.headers().get("content-type") {
if !constants::ALLOWED_ISO_MIME_TYPES.contains(&ct.to_str()?) {
return Ok(HttpResponse::BadRequest().json("Invalid file mimetype!"));
}
if let Some(ct) = response.headers().get("content-type")
&& !constants::ALLOWED_ISO_MIME_TYPES.contains(&ct.to_str()?)
{
return Ok(HttpResponse::BadRequest().json("Invalid file mimetype!"));
}
let mut stream = response.bytes_stream();

View File

@@ -4,6 +4,7 @@ use actix_web::body::BoxBody;
use actix_web::{HttpResponse, web};
use std::error::Error;
use std::fmt::{Display, Formatter};
use zip::result::ZipError;
pub mod api_tokens_controller;
pub mod auth_controller;
@@ -42,7 +43,7 @@ impl actix_web::error::ResponseError for HttpErr {
}
}
fn error_response(&self) -> HttpResponse<BoxBody> {
log::error!("Error while processing request! {}", self);
log::error!("Error while processing request! {self}");
HttpResponse::InternalServerError().body("Failed to execute request!")
}
@@ -102,6 +103,12 @@ impl From<actix_web::Error> for HttpErr {
}
}
impl From<ZipError> for HttpErr {
fn from(value: ZipError) -> Self {
HttpErr::Err(std::io::Error::other(value.to_string()).into())
}
}
impl From<HttpResponse> for HttpErr {
fn from(value: HttpResponse) -> Self {
HttpErr::HTTPResponse(value)

View File

@@ -1,14 +1,24 @@
use crate::actors::vnc_tokens_actor::VNC_TOKEN_LIFETIME;
use crate::app_config::AppConfig;
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::hypervisor::HypervisorInfo;
use crate::libvirt_rest_structures::net::NetworkInfo;
use crate::libvirt_rest_structures::nw_filter::NetworkFilter;
use crate::libvirt_rest_structures::vm::VMInfo;
use crate::nat::nat_hook;
use crate::utils::net_utils;
use actix_web::{HttpResponse, Responder};
use crate::utils::time_utils::{format_date, time};
use crate::{api_tokens, constants};
use actix_files::NamedFile;
use actix_web::{HttpRequest, HttpResponse, Responder};
use serde::Serialize;
use std::fs::File;
use std::io::Write;
use sysinfo::{Components, Disks, Networks, System};
use zip::ZipWriter;
use zip::write::SimpleFileOptions;
#[derive(serde::Serialize)]
struct StaticConfig {
@@ -199,3 +209,85 @@ pub async fn networks_list() -> HttpResult {
pub async fn bridges_list() -> HttpResult {
Ok(HttpResponse::Ok().json(net_utils::bridges_list()?))
}
/// Add JSON file to ZIP
fn zip_json<E: Serialize, F>(
zip: &mut ZipWriter<File>,
dir: &str,
content: &Vec<E>,
file_name: F,
) -> anyhow::Result<()>
where
F: Fn(&E) -> String,
{
for entry in content {
let file_encoded = serde_json::to_string(&entry)?;
let options = SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Deflated)
.unix_permissions(0o750);
zip.start_file(format!("{dir}/{}.json", file_name(entry)), options)?;
zip.write_all(file_encoded.as_bytes())?;
}
Ok(())
}
/// Export all configuration elements at once
pub async fn export_all_configs(req: HttpRequest, client: LibVirtReq) -> HttpResult {
// Perform extractions
let vms = client
.get_full_domains_list()
.await?
.into_iter()
.map(VMInfo::from_domain)
.collect::<Result<Vec<_>, _>>()?;
let networks = client
.get_full_networks_list()
.await?
.into_iter()
.map(NetworkInfo::from_xml)
.collect::<Result<Vec<_>, _>>()?;
let nw_filters = client
.get_full_network_filters_list()
.await?
.into_iter()
.map(NetworkFilter::lib2rest)
.collect::<Result<Vec<_>, _>>()?;
let tokens = api_tokens::full_list().await?;
// Create ZIP file
let dest_dir = tempfile::tempdir_in(&AppConfig::get().temp_dir)?;
let zip_path = dest_dir.path().join("export.zip");
let file = File::create(&zip_path)?;
let mut zip = ZipWriter::new(file);
// Encode entities to JSON
zip_json(&mut zip, "vms", &vms, |v| v.name.to_string())?;
zip_json(&mut zip, "networks", &networks, |v| v.name.0.to_string())?;
zip_json(
&mut zip,
"nw_filters",
&nw_filters,
|v| match constants::BUILTIN_NETWORK_FILTER_RULES.contains(&v.name.0.as_str()) {
true => format!("builtin/{}", v.name.0),
false => v.name.0.to_string(),
},
)?;
zip_json(&mut zip, "tokens", &tokens, |v| v.id.0.to_string())?;
// Finalize ZIP and return response
zip.finish()?;
let file = File::open(zip_path)?;
let file = NamedFile::from_file(
file,
format!(
"export_{}.zip",
format_date(time() as i64).unwrap().replace('/', "-")
),
)?;
Ok(file.into_response(&req))
}

View File

@@ -109,6 +109,28 @@ pub async fn get_single_src_def(client: LibVirtReq, id: web::Path<SingleVMUUidRe
.body(info))
}
/// Get the generated cloud init configuration disk of a vm
pub async fn get_cloud_init_disk(client: LibVirtReq, id: web::Path<SingleVMUUidReq>) -> HttpResult {
let info = match client.get_single_domain(id.uid).await {
Ok(i) => i,
Err(e) => {
log::error!("Failed to get domain information! {e}");
return Ok(HttpResponse::InternalServerError().json(e.to_string()));
}
};
let vm = VMInfo::from_domain(info)?;
let disk = vm.cloud_init.generate_nocloud_disk()?;
Ok(HttpResponse::Ok()
.content_type("application/x-iso9660-image")
.insert_header((
"Content-Disposition",
format!("attachment; filename=\"cloud_init_{}.iso\"", vm.name),
))
.body(disk))
}
/// Update a VM information
pub async fn update(
client: LibVirtReq,

View File

@@ -128,21 +128,21 @@ impl FromRequest for ApiAuthExtractor {
));
}
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 let Some(ip) = token.ip_restriction
&& !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!"));
}
if token.should_update_last_activity()
&& 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 })

View File

@@ -1,4 +1,5 @@
use crate::libvirt_lib_structures::XMLUuid;
use crate::utils::cloud_init_utils::CloudInitConfig;
/// VirtWeb specific metadata
#[derive(serde::Serialize, serde::Deserialize, Default, Debug, Clone)]
@@ -8,6 +9,8 @@ pub struct DomainMetadataVirtWebXML {
pub ns: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub group: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cloud_init: Option<CloudInitConfig>,
}
/// Domain metadata

View File

@@ -13,4 +13,6 @@ enum LibVirtStructError {
ParseFilteringChain(String),
#[error("NetworkFilterExtractionError: {0}")]
NetworkFilterExtraction(String),
#[error("CloudInitConfigurationError: {0}")]
CloudInitConfiguration(String),
}

View File

@@ -96,28 +96,28 @@ impl NetworkInfo {
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(n) = &self.title
&& 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(dev) = &self.device
&& !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(bridge) = &self.bridge_name
&& !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());
}
if let Some(domain) = &self.domain
&& !regex!("^[a-zA-Z0-9.]+$").is_match(domain)
{
return Err(StructureExtraction("Domain name is invalid!").into());
}
let mut ips = Vec::with_capacity(2);
@@ -303,16 +303,16 @@ impl NetworkInfo {
/// 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(ipv4) = &self.ip_v4
&& ipv4.nat.is_some()
{
return true;
}
if let Some(ipv6) = &self.ip_v6 {
if ipv6.nat.is_some() {
return true;
}
if let Some(ipv6) = &self.ip_v6
&& ipv6.nat.is_some()
{
return true;
}
false

View File

@@ -43,14 +43,12 @@ impl From<&String> for NetworkFilterMacAddressOrVar {
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());
}
if let Some(mac) = n
&& !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()))
@@ -83,34 +81,34 @@ impl<const V: usize> From<&String> for NetworkFilterIPOrVar<V> {
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());
}
if let Some(ip) = n
&& !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());
}
if let Some(mask) = n
&& !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());
}
if let Some(comment) = n
&& (comment.len() > 256 || comment.contains('\"') || comment.contains('\n'))
{
return Err(NetworkFilterExtraction(format!("Invalid comment! {comment}")).into());
}
Ok(n.clone())
@@ -869,12 +867,10 @@ impl NetworkFilter {
);
}
if let Some(priority) = self.priority {
if !(-1000..=1000).contains(&priority) {
return Err(
NetworkFilterExtraction("Network priority is invalid!".to_string()).into(),
);
}
if let Some(priority) = self.priority
&& !(-1000..=1000).contains(&priority)
{
return Err(NetworkFilterExtraction("Network priority is invalid!".to_string()).into());
}
for fref in &self.join_filters {

View File

@@ -3,7 +3,10 @@ use crate::constants;
use crate::libvirt_lib_structures::XMLUuid;
use crate::libvirt_lib_structures::domain::*;
use crate::libvirt_rest_structures::LibVirtStructError;
use crate::libvirt_rest_structures::LibVirtStructError::StructureExtraction;
use crate::libvirt_rest_structures::LibVirtStructError::{
CloudInitConfiguration, StructureExtraction,
};
use crate::utils::cloud_init_utils::CloudInitConfig;
use crate::utils::file_size_utils::FileSize;
use crate::utils::files_utils;
use crate::utils::vm_file_disks_utils::{VMDiskBus, VMDiskFormat, VMFileDisk};
@@ -94,6 +97,9 @@ pub struct VMInfo {
pub tpm_module: bool,
/// Strings injected as OEM Strings in SMBios configuration
pub oem_strings: Vec<String>,
/// Cloud init configuration
#[serde(default)]
pub cloud_init: CloudInitConfig,
}
impl VMInfo {
@@ -112,22 +118,22 @@ impl VMInfo {
XMLUuid::new_random()
};
if let Some(n) = &self.genid {
if !n.is_valid() {
return Err(StructureExtraction("VM genid is invalid!").into());
}
if let Some(n) = &self.genid
&& !n.is_valid()
{
return Err(StructureExtraction("VM genid is invalid!").into());
}
if let Some(n) = &self.title {
if n.contains('\n') {
return Err(StructureExtraction("VM title contain newline char!").into());
}
if let Some(n) = &self.title
&& n.contains('\n')
{
return Err(StructureExtraction("VM title contain newline char!").into());
}
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 let Some(group) = &self.group
&& !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 {
@@ -138,9 +144,26 @@ impl VMInfo {
return Err(StructureExtraction("Invalid number of vCPU specified!").into());
}
let mut disks = vec![];
if let Some(e) = self.cloud_init.check_error() {
return Err(CloudInitConfiguration(e).into());
}
// Add ISO files
let mut iso_absolute_files = vec![];
// Process cloud init image
if self.cloud_init.attach_config {
let cloud_init_disk_path = AppConfig::get().cloud_init_disk_path_for_vm(&self.name);
// Apply latest cloud init configuration
std::fs::write(
&cloud_init_disk_path,
self.cloud_init.generate_nocloud_disk()?,
)?;
iso_absolute_files.push(cloud_init_disk_path);
}
// Process uploaded ISO files
for iso_file in &self.iso_files {
if !files_utils::check_file_name(iso_file) {
return Err(StructureExtraction("ISO filename is invalid!").into());
@@ -152,6 +175,13 @@ impl VMInfo {
return Err(StructureExtraction("Specified ISO file does not exists!").into());
}
iso_absolute_files.push(path);
}
let mut disks = vec![];
// Add ISO disk files
for iso_path in iso_absolute_files {
disks.push(DiskXML {
r#type: "file".to_string(),
device: "cdrom".to_string(),
@@ -161,7 +191,7 @@ impl VMInfo {
cache: "none".to_string(),
},
source: DiskSourceXML {
file: path.to_string_lossy().to_string(),
file: iso_path.to_string_lossy().to_string(),
},
target: DiskTargetXML {
dev: format!(
@@ -178,6 +208,7 @@ impl VMInfo {
})
}
// Configure VNC access, if requested
let (vnc_graphics, vnc_video) = match self.vnc_access {
true => (
Some(GraphicsXML {
@@ -340,6 +371,7 @@ impl VMInfo {
virtweb: DomainMetadataVirtWebXML {
ns: "https://virtweb.communiquons.org".to_string(),
group: self.group.clone().map(|g| g.0),
cloud_init: Some(self.cloud_init.clone()),
},
}),
os: OSXML {
@@ -490,6 +522,7 @@ impl VMInfo {
.iter()
.filter(|d| d.device == "cdrom")
.map(|d| d.source.file.rsplit_once('/').unwrap().1.to_string())
.filter(|d| !d.starts_with(constants::CLOUD_INIT_IMAGE_PREFIX_NAME))
.collect(),
file_disks: domain
@@ -582,6 +615,13 @@ impl VMInfo {
.and_then(|s| s.oem_strings)
.map(|s| s.entries.iter().map(|o| o.content.to_string()).collect())
.unwrap_or_default(),
cloud_init: domain
.metadata
.clone()
.unwrap_or_default()
.virtweb
.cloud_init
.unwrap_or_default(),
})
}
}

View File

@@ -47,16 +47,22 @@ async fn main() -> std::io::Result<()> {
log::debug!("Checking for required programs");
exec_utils::check_program(
constants::QEMU_IMAGE_PROGRAM,
constants::PROGRAM_QEMU_IMAGE,
"QEMU disk image utility is required to manipulate QCow2 files!",
);
exec_utils::check_program(
constants::IP_PROGRAM,
constants::PROGRAM_IP,
"ip is required to access bridges information!",
);
exec_utils::check_program(
constants::PROGRAM_CLOUD_LOCALDS,
"cloud-localds from package cloud-image-utils is required to build cloud-init images!",
);
log::debug!("Create required directory, if missing");
files_utils::create_directory_if_missing(AppConfig::get().iso_storage_path()).unwrap();
files_utils::create_directory_if_missing(AppConfig::get().cloud_init_disk_storage_path())
.unwrap();
files_utils::create_directory_if_missing(AppConfig::get().disk_images_storage_path()).unwrap();
files_utils::create_directory_if_missing(AppConfig::get().vnc_sockets_path()).unwrap();
files_utils::set_file_permission(AppConfig::get().vnc_sockets_path(), 0o777).unwrap();
@@ -151,6 +157,10 @@ async fn main() -> std::io::Result<()> {
"/api/server/bridges",
web::get().to(server_controller::bridges_list),
)
.route(
"/api/server/export_configs",
web::get().to(server_controller::export_all_configs),
)
// Auth controller
.route(
"/api/auth/local",
@@ -198,6 +208,10 @@ async fn main() -> std::io::Result<()> {
"/api/vm/{uid}/src",
web::get().to(vm_controller::get_single_src_def),
)
.route(
"/api/vm/{uid}/cloud_init_disk",
web::get().to(vm_controller::get_cloud_init_disk),
)
.route(
"/api/vm/{uid}/autostart",
web::get().to(vm_controller::get_autostart),

View File

@@ -69,8 +69,7 @@ where
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
"An attempt to access VirtWeb from an unauthorized network has been intercepted! {remote_ip:?}"
);
return Ok(req
.into_response(

View File

@@ -60,10 +60,10 @@ pub struct Nat<IPv> {
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 NatSourceIP::Interface { name } = &self.host_ip
&& !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 {
@@ -84,10 +84,10 @@ impl<IPv> Nat<IPv> {
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());
}
if let Some(comment) = &self.comment
&& comment.len() > constants::NET_NAT_COMMENT_MAX_SIZE
{
return Err(NatDefError::InvalidNatDef("Comment is too large!").into());
}
Ok(())

View File

@@ -0,0 +1,117 @@
use crate::app_config::AppConfig;
use crate::constants;
use std::process::Command;
/// Cloud init DS Mode
#[derive(Copy, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum CloudInitDSMode {
/// Networking is required
Net,
/// Does not require networking to be up before user-data actions are run
Local,
}
/// VM Cloud Init configuration
///
/// RedHat documentation: https://docs.redhat.com/fr/documentation/red_hat_enterprise_linux/9/html/configuring_and_managing_cloud-init_for_rhel_9/configuring-cloud-init_cloud-content
/// cloud-localds source code: https://github.com/canonical/cloud-utils/blob/main/bin/cloud-localds
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Default)]
pub struct CloudInitConfig {
pub attach_config: bool,
/// Main user data
pub user_data: String,
/// Instance ID, set in metadata file
#[serde(skip_serializing_if = "Option::is_none")]
pub instance_id: Option<String>,
/// Local hostname, set in metadata file
#[serde(skip_serializing_if = "Option::is_none")]
pub local_hostname: Option<String>,
/// Data source mode
#[serde(skip_serializing_if = "Option::is_none")]
pub dsmode: Option<CloudInitDSMode>,
/// Network configuration
#[serde(skip_serializing_if = "Option::is_none")]
pub network_configuration: Option<String>,
}
impl CloudInitConfig {
/// Check cloud init configuration
pub fn check_error(&self) -> Option<String> {
if !self.user_data.is_empty() {
// Check YAML content
if let Err(e) = serde_yml::from_str::<serde_json::Value>(&self.user_data) {
return Some(format!(
"user data is an invalid YAML file! Deserialization error: {e}"
));
}
// Check first line
if !self.user_data.starts_with("#cloud-config\n") {
return Some(
"user data file MUST start with '#cloud-config' as first line!".to_string(),
);
}
}
None
}
/// Generate disk image for nocloud usage
pub fn generate_nocloud_disk(&self) -> anyhow::Result<Vec<u8>> {
let temp_path = tempfile::tempdir_in(&AppConfig::get().temp_dir)?;
let mut cmd = Command::new(constants::PROGRAM_CLOUD_LOCALDS);
// ISO destination path
let temp_iso = temp_path.path().join("disk.iso");
cmd.arg(&temp_iso);
// Process network configuration
if let Some(net_conf) = &self.network_configuration {
let net_conf_path = temp_path.path().join("network");
std::fs::write(&net_conf_path, net_conf)?;
cmd.arg("--network-config").arg(&net_conf_path);
}
// Process user data
let user_data_path = temp_path.path().join("user-data");
std::fs::write(&user_data_path, &self.user_data)?;
cmd.arg(user_data_path);
// Process metadata
let mut metadatas = vec![];
if let Some(inst_id) = &self.instance_id {
metadatas.push(format!("instance-id: {inst_id}"));
}
if let Some(local_hostname) = &self.local_hostname {
metadatas.push(format!("local-hostname: {local_hostname}"));
}
if let Some(dsmode) = &self.dsmode {
metadatas.push(format!(
"dsmode: {}",
match dsmode {
CloudInitDSMode::Net => "net",
CloudInitDSMode::Local => "local",
}
));
}
let meta_data_path = temp_path.path().join("meta-data");
std::fs::write(&meta_data_path, metadatas.join("\n"))?;
cmd.arg(meta_data_path);
// Execute command
let output = cmd.output()?;
if !output.status.success() {
anyhow::bail!(
"{} exited with status {}!\nStdout: {}\nStderr: {}",
constants::PROGRAM_CLOUD_LOCALDS,
output.status,
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
// Read generated ISO file
Ok(std::fs::read(temp_iso)?)
}
}

View File

@@ -28,8 +28,10 @@ pub enum DiskFileFormat {
#[serde(default)]
virtual_size: FileSize,
},
CompressedRaw,
CompressedQCow2,
GzCompressedRaw,
GzCompressedQCow2,
XzCompressedRaw,
XzCompressedQCow2,
}
impl DiskFileFormat {
@@ -37,8 +39,10 @@ impl DiskFileFormat {
match self {
DiskFileFormat::Raw { .. } => &["raw", ""],
DiskFileFormat::QCow2 { .. } => &["qcow2"],
DiskFileFormat::CompressedRaw => &["raw.gz"],
DiskFileFormat::CompressedQCow2 => &["qcow2.gz"],
DiskFileFormat::GzCompressedRaw => &["raw.gz"],
DiskFileFormat::GzCompressedQCow2 => &["qcow2.gz"],
DiskFileFormat::XzCompressedRaw => &["raw.xz"],
DiskFileFormat::XzCompressedQCow2 => &["qcow2.xz"],
}
}
}
@@ -81,9 +85,14 @@ impl DiskFileInfo {
},
"gz" if name.ends_with(".qcow2") => {
name = name.strip_suffix(".qcow2").unwrap_or(&name).to_string();
DiskFileFormat::CompressedQCow2
DiskFileFormat::GzCompressedQCow2
}
"gz" => DiskFileFormat::CompressedRaw,
"gz" => DiskFileFormat::GzCompressedRaw,
"xz" if name.ends_with(".qcow2") => {
name = name.strip_suffix(".qcow2").unwrap_or(&name).to_string();
DiskFileFormat::XzCompressedQCow2
}
"xz" => DiskFileFormat::XzCompressedRaw,
_ => anyhow::bail!("Unsupported disk extension: {ext}!"),
};
@@ -124,7 +133,7 @@ impl DiskFileInfo {
}
DiskFileFormat::QCow2 { virtual_size } => {
let mut cmd = Command::new(constants::QEMU_IMAGE_PROGRAM);
let mut cmd = Command::new(constants::PROGRAM_QEMU_IMAGE);
cmd.arg("create")
.arg("-f")
.arg("qcow2")
@@ -159,9 +168,9 @@ impl DiskFileInfo {
// Prepare the conversion
let mut cmd = match (self.format, dest_format) {
// Decompress QCow2
(DiskFileFormat::CompressedQCow2, DiskFileFormat::QCow2 { .. }) => {
let mut cmd = Command::new(constants::GZIP_PROGRAM);
// Decompress QCow2 (GZIP)
(DiskFileFormat::GzCompressedQCow2, DiskFileFormat::QCow2 { .. }) => {
let mut cmd = Command::new(constants::PROGRAM_GZIP);
cmd.arg("--keep")
.arg("--decompress")
.arg("--to-stdout")
@@ -170,9 +179,30 @@ impl DiskFileInfo {
cmd
}
// Compress QCow2
(DiskFileFormat::QCow2 { .. }, DiskFileFormat::CompressedQCow2) => {
let mut cmd = Command::new(constants::GZIP_PROGRAM);
// Decompress QCow2 (XZ)
(DiskFileFormat::XzCompressedQCow2, DiskFileFormat::QCow2 { .. }) => {
let mut cmd = Command::new(constants::PROGRAM_XZ);
cmd.arg("--stdout")
.arg("--keep")
.arg("--decompress")
.arg(&self.file_path)
.stdout(File::create(&temp_file)?);
cmd
}
// Compress QCow2 (Gzip)
(DiskFileFormat::QCow2 { .. }, DiskFileFormat::GzCompressedQCow2) => {
let mut cmd = Command::new(constants::PROGRAM_GZIP);
cmd.arg("--keep")
.arg("--to-stdout")
.arg(&self.file_path)
.stdout(File::create(&temp_file)?);
cmd
}
// Compress QCow2 (Xz)
(DiskFileFormat::QCow2 { .. }, DiskFileFormat::XzCompressedQCow2) => {
let mut cmd = Command::new(constants::PROGRAM_XZ);
cmd.arg("--keep")
.arg("--to-stdout")
.arg(&self.file_path)
@@ -182,7 +212,7 @@ impl DiskFileInfo {
// Convert QCow2 to Raw file
(DiskFileFormat::QCow2 { .. }, DiskFileFormat::Raw { is_sparse }) => {
let mut cmd = Command::new(constants::QEMU_IMAGE_PROGRAM);
let mut cmd = Command::new(constants::PROGRAM_QEMU_IMAGE);
cmd.arg("convert")
.arg("-f")
.arg("qcow2")
@@ -201,7 +231,7 @@ impl DiskFileInfo {
// Clone a QCow file, using qemu-image instead of cp might improve "sparsification" of
// file
(DiskFileFormat::QCow2 { .. }, DiskFileFormat::QCow2 { .. }) => {
let mut cmd = Command::new(constants::QEMU_IMAGE_PROGRAM);
let mut cmd = Command::new(constants::PROGRAM_QEMU_IMAGE);
cmd.arg("convert")
.arg("-f")
.arg("qcow2")
@@ -214,7 +244,7 @@ impl DiskFileInfo {
// Convert Raw to QCow2 file
(DiskFileFormat::Raw { .. }, DiskFileFormat::QCow2 { .. }) => {
let mut cmd = Command::new(constants::QEMU_IMAGE_PROGRAM);
let mut cmd = Command::new(constants::PROGRAM_QEMU_IMAGE);
cmd.arg("convert")
.arg("-f")
.arg("raw")
@@ -228,7 +258,7 @@ impl DiskFileInfo {
// Render raw file non sparse
(DiskFileFormat::Raw { is_sparse: true }, DiskFileFormat::Raw { is_sparse: false }) => {
let mut cmd = Command::new(constants::COPY_PROGRAM);
let mut cmd = Command::new(constants::PROGRAM_COPY);
cmd.arg("--sparse=never")
.arg(&self.file_path)
.arg(&temp_file);
@@ -237,16 +267,16 @@ impl DiskFileInfo {
// Render raw file sparse
(DiskFileFormat::Raw { is_sparse: false }, DiskFileFormat::Raw { is_sparse: true }) => {
let mut cmd = Command::new(constants::DD_PROGRAM);
let mut cmd = Command::new(constants::PROGRAM_DD);
cmd.arg("conv=sparse")
.arg(format!("if={}", self.file_path.display()))
.arg(format!("of={}", temp_file.display()));
cmd
}
// Compress Raw
(DiskFileFormat::Raw { .. }, DiskFileFormat::CompressedRaw) => {
let mut cmd = Command::new(constants::GZIP_PROGRAM);
// Compress Raw (Gz)
(DiskFileFormat::Raw { .. }, DiskFileFormat::GzCompressedRaw) => {
let mut cmd = Command::new(constants::PROGRAM_GZIP);
cmd.arg("--keep")
.arg("--to-stdout")
.arg(&self.file_path)
@@ -254,9 +284,29 @@ impl DiskFileInfo {
cmd
}
// Decompress Raw to not sparse file
(DiskFileFormat::CompressedRaw, DiskFileFormat::Raw { is_sparse: false }) => {
let mut cmd = Command::new(constants::GZIP_PROGRAM);
// Compress Raw (Xz)
(DiskFileFormat::Raw { .. }, DiskFileFormat::XzCompressedRaw) => {
let mut cmd = Command::new(constants::PROGRAM_XZ);
cmd.arg("--keep")
.arg("--to-stdout")
.arg(&self.file_path)
.stdout(File::create(&temp_file)?);
cmd
}
// Decompress Raw (Gz) to not sparse file
(DiskFileFormat::GzCompressedRaw, DiskFileFormat::Raw { is_sparse: false }) => {
let mut cmd = Command::new(constants::PROGRAM_GZIP);
cmd.arg("--keep")
.arg("--decompress")
.arg("--to-stdout")
.arg(&self.file_path)
.stdout(File::create(&temp_file)?);
cmd
}
// Decompress Raw (Xz) to not sparse file
(DiskFileFormat::XzCompressedRaw, DiskFileFormat::Raw { is_sparse: false }) => {
let mut cmd = Command::new(constants::PROGRAM_XZ);
cmd.arg("--keep")
.arg("--decompress")
.arg("--to-stdout")
@@ -265,15 +315,29 @@ impl DiskFileInfo {
cmd
}
// Decompress Raw to sparse file
// Decompress Raw (Gz) to sparse file
// https://benou.fr/www/ben/decompressing-sparse-files.html
(DiskFileFormat::CompressedRaw, DiskFileFormat::Raw { is_sparse: true }) => {
let mut cmd = Command::new(constants::BASH_PROGRAM);
(DiskFileFormat::GzCompressedRaw, DiskFileFormat::Raw { is_sparse: true }) => {
let mut cmd = Command::new(constants::PROGRAM_BASH);
cmd.arg("-c").arg(format!(
"{} -d -c {} | {} conv=sparse of={}",
constants::GZIP_PROGRAM,
"{} --decompress --to-stdout {} | {} conv=sparse of={}",
constants::PROGRAM_GZIP,
self.file_path.display(),
constants::DD_PROGRAM,
constants::PROGRAM_DD,
temp_file.display()
));
cmd
}
// Decompress Raw (XZ) to sparse file
// https://benou.fr/www/ben/decompressing-sparse-files.html
(DiskFileFormat::XzCompressedRaw, DiskFileFormat::Raw { is_sparse: true }) => {
let mut cmd = Command::new(constants::PROGRAM_BASH);
cmd.arg("-c").arg(format!(
"{} --decompress --to-stdout {} | {} conv=sparse of={}",
constants::PROGRAM_XZ,
self.file_path.display(),
constants::PROGRAM_DD,
temp_file.display()
));
cmd
@@ -281,7 +345,7 @@ impl DiskFileInfo {
// Dumb copy of file
(a, b) if a == b => {
let mut cmd = Command::new(constants::COPY_PROGRAM);
let mut cmd = Command::new(constants::PROGRAM_COPY);
cmd.arg("--sparse=auto")
.arg(&self.file_path)
.arg(&temp_file);
@@ -330,6 +394,44 @@ impl DiskFileInfo {
Ok(())
}
/// Get disk virtual size, if available
pub fn virtual_size(&self) -> Option<FileSize> {
match self.format {
DiskFileFormat::Raw { .. } => Some(self.file_size),
DiskFileFormat::QCow2 { virtual_size } => Some(virtual_size),
_ => None,
}
}
/// Resize disk
pub fn resize(&self, new_size: FileSize) -> anyhow::Result<()> {
if new_size <= self.virtual_size().unwrap_or(new_size) {
anyhow::bail!("Shrinking disk image file is not supported!");
}
let mut cmd = Command::new(constants::PROGRAM_QEMU_IMAGE);
cmd.arg("resize")
.arg("-f")
.arg(match self.format {
DiskFileFormat::QCow2 { .. } => "qcow2",
DiskFileFormat::Raw { .. } => "raw",
f => anyhow::bail!("Unsupported disk format for resize: {f:?}"),
})
.arg(&self.file_path)
.arg(new_size.as_bytes().to_string());
let output = cmd.output()?;
if !output.status.success() {
anyhow::bail!(
"{} info failed, status: {}, stderr: {}",
constants::PROGRAM_QEMU_IMAGE,
output.status,
String::from_utf8_lossy(&output.stderr)
);
}
Ok(())
}
}
#[derive(serde::Deserialize)]
@@ -341,7 +443,7 @@ struct QCowInfoOutput {
/// Get QCow2 virtual size
fn qcow_virt_size(path: &Path) -> anyhow::Result<FileSize> {
// Run qemu-img
let mut cmd = Command::new(constants::QEMU_IMAGE_PROGRAM);
let mut cmd = Command::new(constants::PROGRAM_QEMU_IMAGE);
cmd.args([
"info",
path.to_str().unwrap_or(""),
@@ -353,7 +455,7 @@ fn qcow_virt_size(path: &Path) -> anyhow::Result<FileSize> {
if !output.status.success() {
anyhow::bail!(
"{} info failed, status: {}, stderr: {}",
constants::QEMU_IMAGE_PROGRAM,
constants::PROGRAM_QEMU_IMAGE,
output.status,
String::from_utf8_lossy(&output.stderr)
);

View File

@@ -1,3 +1,4 @@
pub mod cloud_init_utils;
pub mod exec_utils;
pub mod file_disks_utils;
pub mod file_size_utils;

View File

@@ -145,13 +145,13 @@ struct IPBridgeInfo {
/// Get the list of bridge interfaces
pub fn bridges_list() -> anyhow::Result<Vec<String>> {
let mut cmd = Command::new(constants::IP_PROGRAM);
let mut cmd = Command::new(constants::PROGRAM_IP);
cmd.args(["-json", "link", "show", "type", "bridge"]);
let output = cmd.output()?;
if !output.status.success() {
anyhow::bail!(
"{} failed, status: {}, stderr: {}",
constants::IP_PROGRAM,
constants::PROGRAM_IP,
output.status,
String::from_utf8_lossy(&output.stderr)
);

View File

@@ -1,3 +1,4 @@
use chrono::Datelike;
use std::time::{SystemTime, UNIX_EPOCH};
/// Get the current time since epoch
@@ -13,3 +14,15 @@ pub fn time() -> u64 {
.unwrap()
.as_secs()
}
/// Format given UNIX time in a simple format
pub fn format_date(time: i64) -> anyhow::Result<String> {
let date = chrono::DateTime::from_timestamp(time, 0).ok_or(anyhow::anyhow!("invalid date"))?;
Ok(format!(
"{:0>2}/{:0>2}/{}",
date.day(),
date.month(),
date.year()
))
}

View File

@@ -44,6 +44,9 @@ pub struct VMFileDisk {
/// When creating a new disk, specify the disk image template to use
#[serde(skip_serializing_if = "Option::is_none")]
pub from_image: Option<String>,
/// Set this variable to true to resize disk image
#[serde(skip_serializing_if = "Option::is_none")]
pub resize: Option<bool>,
/// Set this variable to true to delete the disk
pub delete: bool,
}
@@ -78,6 +81,7 @@ impl VMFileDisk {
delete: false,
from_image: None,
resize: None,
})
}
@@ -144,28 +148,40 @@ impl VMFileDisk {
if file.exists() {
log::debug!("File {file:?} does not exists, so it was not touched");
return Ok(());
}
// Create disk if required
else {
// Determine file format
let format = match self.format {
VMDiskFormat::Raw { is_sparse } => DiskFileFormat::Raw { is_sparse },
VMDiskFormat::QCow2 => DiskFileFormat::QCow2 {
virtual_size: self.size,
},
};
// Create / Restore disk file
match &self.from_image {
// Create disk file
None => {
DiskFileInfo::create(&file, format, self.size)?;
}
// Restore disk image template
Some(disk_img) => {
let src_file =
DiskFileInfo::load_file(&AppConfig::get().disk_images_file_path(disk_img))?;
src_file.convert(&file, format)?;
}
}
}
let format = match self.format {
VMDiskFormat::Raw { is_sparse } => DiskFileFormat::Raw { is_sparse },
VMDiskFormat::QCow2 => DiskFileFormat::QCow2 {
virtual_size: self.size,
},
};
// Resize disk file if requested
if self.resize == Some(true) {
let disk = DiskFileInfo::load_file(&file)?;
// Create / Restore disk file
match &self.from_image {
// Create disk file
None => {
DiskFileInfo::create(&file, format, self.size)?;
}
// Restore disk image template
Some(disk_img) => {
let src_file =
DiskFileInfo::load_file(&AppConfig::get().disk_images_file_path(disk_img))?;
src_file.convert(&file, format)?;
// Can only increase disk size
if let Err(e) = disk.resize(self.size) {
log::error!("Failed to resize disk file {}: {e:?}", self.name);
}
}

View File

@@ -5,9 +5,9 @@
sudo apt install libvirt-dev
```
2. Libvirt must also be installed:
2. Libvirt and cloud image utilities must also be installed:
```bash
sudo apt install qemu-kvm libvirt-daemon-system
sudo apt install qemu-kvm libvirt-daemon-system cloud-image-utils
```
3. Allow the current user to manage VMs:

View File

@@ -12,10 +12,10 @@ The release file will be available in `virtweb_backend/target/release/virtweb_ba
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`:
In order to work properly, VirtWeb relies on `libvirt`, `qemu`, `kvm` and `cloud-localds`:
```bash
sudo apt install qemu-kvm libvirt-daemon-system libvirt0 libvirt-clients libvirt-daemon bridge-utils
sudo apt install qemu-kvm libvirt-daemon-system libvirt0 libvirt-clients libvirt-daemon bridge-utils cloud-image-utils
```
## Dedicated user

File diff suppressed because it is too large Load Diff

View File

@@ -11,42 +11,46 @@
},
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@fontsource/roboto": "^5.2.5",
"@emotion/styled": "^11.14.1",
"@fontsource/roboto": "^5.2.6",
"@mdi/js": "^7.4.47",
"@mdi/react": "^1.6.1",
"@mui/icons-material": "^7.1.0",
"@mui/material": "^7.1.0",
"@mui/x-charts": "^8.3.1",
"@mui/x-data-grid": "^8.3.1",
"@monaco-editor/react": "^4.7.0",
"@mui/icons-material": "^7.3.1",
"@mui/material": "^7.3.1",
"@mui/x-charts": "^8.11.2",
"@mui/x-data-grid": "^8.10.0",
"date-and-time": "^3.6.0",
"filesize": "^10.1.6",
"humanize-duration": "^3.32.2",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router-dom": "^7.6.0",
"react-syntax-highlighter": "^15.6.1",
"humanize-duration": "^3.33.0",
"monaco-editor": "^0.52.2",
"monaco-yaml": "^5.4.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-router-dom": "^7.8.0",
"react-syntax-highlighter": "^15.6.6",
"react-vnc": "^3.1.0",
"uuid": "^11.1.0",
"xml-formatter": "^3.6.6"
"xml-formatter": "^3.6.6",
"yaml": "^2.8.1"
},
"devDependencies": {
"@eslint/js": "^9.27.0",
"@eslint/js": "^9.35.0",
"@types/humanize-duration": "^3.27.4",
"@types/jest": "^29.5.14",
"@types/react": "^19.1.5",
"@types/react-dom": "^19.1.5",
"@types/jest": "^30.0.0",
"@types/react": "^19.1.12",
"@types/react-dom": "^19.1.9",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/uuid": "^10.0.0",
"@vitejs/plugin-react": "^4.4.1",
"eslint": "^9.27.0",
"eslint-plugin-react-dom": "^1.49.0",
"eslint-plugin-react-hooks": "^5.1.0",
"@vitejs/plugin-react": "^4.7.0",
"eslint": "^9.33.0",
"eslint-plugin-react-dom": "^1.52.9",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"eslint-plugin-react-x": "^1.49.0",
"globals": "^16.1.0",
"typescript": "^5.8.3",
"typescript-eslint": "^8.32.1",
"vite": "^6.3.5"
"eslint-plugin-react-x": "^1.52.9",
"globals": "^16.3.0",
"typescript": "^5.9.2",
"typescript-eslint": "^8.38.0",
"vite": "^6.3.6"
}
}

View File

@@ -4,8 +4,10 @@ import { VMFileDisk, VMInfo } from "./VMApi";
export type DiskImageFormat =
| { format: "Raw"; is_sparse: boolean }
| { format: "QCow2"; virtual_size?: number }
| { format: "CompressedQCow2" }
| { format: "CompressedRaw" };
| { format: "GzCompressedQCow2" }
| { format: "GzCompressedRaw" }
| { format: "XzCompressedQCow2" }
| { format: "XzCompressedRaw" };
export type DiskImage = {
file_size: number;

View File

@@ -232,4 +232,16 @@ export class ServerApi {
})
).data;
}
/**
* Export all server configs
*/
static async ExportServerConfigs(): Promise<Blob> {
return (
await APIClient.exec({
method: "GET",
uri: "/server/export_configs",
})
).data;
}
}

View File

@@ -31,8 +31,12 @@ export interface BaseFileVMDisk {
// For new disk only
from_image?: string;
// Resize disk image after clone
resize?: boolean;
// application attributes
new?: boolean;
originalSize?: number;
deleteType?: "keepfile" | "deletefile";
}
@@ -82,6 +86,15 @@ export interface VMNetBridge {
bridge: string;
}
export interface VMCloudInit {
attach_config: boolean;
user_data: string;
instance_id?: string;
local_hostname?: string;
dsmode?: "Net" | "Local";
network_configuration?: string;
}
export type VMBootType = "UEFI" | "UEFISecureBoot" | "Legacy";
interface VMInfoInterface {
@@ -101,6 +114,7 @@ interface VMInfoInterface {
networks: VMNetInterface[];
tpm_module: boolean;
oem_strings: string[];
cloud_init: VMCloudInit;
}
export class VMInfo implements VMInfoInterface {
@@ -120,6 +134,7 @@ export class VMInfo implements VMInfoInterface {
networks: VMNetInterface[];
tpm_module: boolean;
oem_strings: string[];
cloud_init: VMCloudInit;
constructor(int: VMInfoInterface) {
this.name = int.name;
@@ -138,6 +153,7 @@ export class VMInfo implements VMInfoInterface {
this.networks = int.networks;
this.tpm_module = int.tpm_module;
this.oem_strings = int.oem_strings;
this.cloud_init = int.cloud_init;
}
static NewEmpty(): VMInfo {
@@ -153,6 +169,7 @@ export class VMInfo implements VMInfoInterface {
networks: [],
tpm_module: true,
oem_strings: [],
cloud_init: { attach_config: false, user_data: "" },
});
}

View File

@@ -42,13 +42,15 @@ export function ConvertDiskImageDialog(
setFormat({ format: value ?? ("QCow2" as any) });
if (value === "QCow2") setFilename(`${origFilename}.qcow2`);
if (value === "CompressedQCow2") setFilename(`${origFilename}.qcow2.gz`);
if (value === "GzCompressedQCow2") setFilename(`${origFilename}.qcow2.gz`);
if (value === "XzCompressedQCow2") setFilename(`${origFilename}.qcow2.xz`);
if (value === "Raw") {
setFilename(`${origFilename}.raw`);
// Check sparse checkbox by default
setFormat({ format: "Raw", is_sparse: true });
}
if (value === "CompressedRaw") setFilename(`${origFilename}.raw.gz`);
if (value === "GzCompressedRaw") setFilename(`${origFilename}.raw.gz`);
if (value === "XzCompressedRaw") setFilename(`${origFilename}.raw.xz`);
};
const handleSubmit = async () => {
@@ -104,8 +106,10 @@ export function ConvertDiskImageDialog(
options={[
{ value: "QCow2" },
{ value: "Raw" },
{ value: "CompressedRaw" },
{ value: "CompressedQCow2" },
{ value: "GzCompressedRaw" },
{ value: "XzCompressedRaw" },
{ value: "GzCompressedQCow2" },
{ value: "XzCompressedQCow2" },
]}
/>

View File

@@ -3,16 +3,44 @@ import "@fontsource/roboto/400.css";
import "@fontsource/roboto/500.css";
import "@fontsource/roboto/700.css";
import { ThemeProvider, createTheme } from "@mui/material";
import React from "react";
import ReactDOM from "react-dom/client";
import { App } from "./App";
import { AlertDialogProvider } from "./hooks/providers/AlertDialogProvider";
import { ConfirmDialogProvider } from "./hooks/providers/ConfirmDialogProvider";
import { LoadingMessageProvider } from "./hooks/providers/LoadingMessageProvider";
import { SnackbarProvider } from "./hooks/providers/SnackbarProvider";
import "./index.css";
import { LoadServerConfig } from "./widgets/LoadServerConfig";
import { ThemeProvider, createTheme } from "@mui/material";
import { LoadingMessageProvider } from "./hooks/providers/LoadingMessageProvider";
import { AlertDialogProvider } from "./hooks/providers/AlertDialogProvider";
import { SnackbarProvider } from "./hooks/providers/SnackbarProvider";
import { ConfirmDialogProvider } from "./hooks/providers/ConfirmDialogProvider";
import { loader } from "@monaco-editor/react";
import * as monaco from "monaco-editor";
import EditorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
import { configureMonacoYaml } from "monaco-yaml";
import YamlWorker from "monaco-yaml/yaml.worker?worker";
// This allows to use a self hosted instance of Monaco editor
loader.config({ monaco });
// Add YAML support to Monaco
configureMonacoYaml(monaco, {
enableSchemaRequest: false,
});
/// YAML worker
window.MonacoEnvironment = {
getWorker(_moduleId, label) {
switch (label) {
case "editorWorkerService":
return new EditorWorker();
case "yaml":
return new YamlWorker();
default:
throw new Error(`Unknown label ${label}`);
}
},
};
const darkTheme = createTheme({
palette: {
@@ -20,9 +48,7 @@ const darkTheme = createTheme({
},
});
const root = ReactDOM.createRoot(
document.getElementById("root")!
);
const root = ReactDOM.createRoot(document.getElementById("root")!);
root.render(
<React.StrictMode>
<ThemeProvider theme={darkTheme}>

View File

@@ -9,18 +9,21 @@ import {
import Icon from "@mdi/react";
import {
Box,
IconButton,
LinearProgress,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
Tooltip,
Typography,
} from "@mui/material";
import Grid from "@mui/material/Grid";
import { PieChart } from "@mui/x-charts";
import { filesize } from "filesize";
import humanizeDuration from "humanize-duration";
import IosShareIcon from "@mui/icons-material/IosShare";
import React from "react";
import {
DiskInfo,
@@ -31,6 +34,8 @@ import {
import { AsyncWidget } from "../widgets/AsyncWidget";
import { VirtWebPaper } from "../widgets/VirtWebPaper";
import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
import { useLoadingMessage } from "../hooks/providers/LoadingMessageProvider";
import { useAlert } from "../hooks/providers/AlertDialogProvider";
export function SysInfoRoute(): React.ReactElement {
const [info, setInfo] = React.useState<ServerSystemInfo>();
@@ -52,6 +57,23 @@ export function SysInfoRoute(): React.ReactElement {
export function SysInfoRouteInner(p: {
info: ServerSystemInfo;
}): React.ReactElement {
const alert = useAlert();
const loadingMessage = useLoadingMessage();
const downloadAllConfig = async () => {
try {
loadingMessage.show("Downloading server config...");
const res = await ServerApi.ExportServerConfigs();
const url = URL.createObjectURL(res);
window.location.href = url;
} catch (e) {
console.error("Failed to download server config!", e);
alert(`Failed to download server config! ${e}`);
} finally {
loadingMessage.hide();
}
};
const sumDiskUsage = p.info.disks.reduce(
(prev, disk) => {
return {
@@ -63,7 +85,16 @@ export function SysInfoRouteInner(p: {
);
return (
<VirtWebRouteContainer label="Sysinfo">
<VirtWebRouteContainer
label="Sysinfo"
actions={
<Tooltip title="Export all server configs">
<IconButton onClick={downloadAllConfig}>
<IosShareIcon />
</IconButton>
</Tooltip>
}
>
<Grid container spacing={2}>
{/* Memory */}
<Grid size={{ xs: 4 }}>
@@ -288,7 +319,7 @@ function DiskDetailsTable(p: { disks: DiskInfo[] }): React.ReactElement {
{p.disks.map((e, c) => (
<TableRow hover key={c}>
<TableCell>{e.name}</TableCell>
<TableCell>{e.DiskKind}</TableCell>
<TableCell>{String(e.DiskKind)}</TableCell>
<TableCell>{e.mount_point}</TableCell>
<TableCell>{filesize(e.total_space)}</TableCell>
<TableCell>{filesize(e.available_space)}</TableCell>

View File

@@ -1,5 +1,5 @@
import VisibilityIcon from '@mui/icons-material/Visibility';
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
import VisibilityIcon from "@mui/icons-material/Visibility";
import VisibilityOffIcon from "@mui/icons-material/VisibilityOff";
import {
Alert,
CircularProgress,
@@ -36,7 +36,9 @@ export function LoginRoute(): React.ReactElement {
const canSubmit = username.length > 0 && password.length > 0;
const [showPassword, setShowPassword] = React.useState(false);
const handleClickShowPassword = () => { setShowPassword((show) => !show); };
const handleClickShowPassword = () => {
setShowPassword((show) => !show);
};
const handleMouseDownPassword = (
event: React.MouseEvent<HTMLButtonElement>
@@ -105,12 +107,14 @@ export function LoginRoute(): React.ReactElement {
label="Username"
name="username"
value={username}
onChange={(e) => { setUsername(e.target.value); }}
onChange={(e) => {
setUsername(e.target.value);
}}
autoComplete="username"
autoFocus
/>
<FormControl fullWidth variant="outlined">
<FormControl required fullWidth variant="outlined">
<InputLabel htmlFor="password">Password</InputLabel>
<OutlinedInput
required
@@ -120,7 +124,9 @@ export function LoginRoute(): React.ReactElement {
type={showPassword ? "text" : "password"}
id="password"
value={password}
onChange={(e) => { setPassword(e.target.value); }}
onChange={(e) => {
setPassword(e.target.value);
}}
autoComplete="current-password"
endAdornment={
<InputAdornment position="end">
@@ -131,7 +137,11 @@ export function LoginRoute(): React.ReactElement {
onMouseDown={handleMouseDownPassword}
edge="end"
>
{showPassword ? <VisibilityOffIcon /> : <VisibilityIcon />}
{showPassword ? (
<VisibilityOffIcon />
) : (
<VisibilityIcon />
)}
</IconButton>
</Tooltip>
</InputAdornment>

View File

@@ -17,7 +17,9 @@ export function CheckboxInput(p: {
<Checkbox
disabled={!p.editable}
checked={p.checked}
onChange={(e) => { p.onValueChange(e.target.checked); }}
onChange={(e) => {
p.onValueChange(e.target.checked);
}}
/>
}
label={p.label}

View File

@@ -0,0 +1,341 @@
/* eslint-disable @typescript-eslint/no-base-to-string */
import Editor from "@monaco-editor/react";
import BookIcon from "@mui/icons-material/Book";
import RefreshIcon from "@mui/icons-material/Refresh";
import { Grid, IconButton, InputAdornment, Tooltip } from "@mui/material";
import React from "react";
import { v4 as uuidv4 } from "uuid";
import YAML from "yaml";
import { VMInfo } from "../../api/VMApi";
import { RouterLink } from "../RouterLink";
import { CheckboxInput } from "./CheckboxInput";
import { EditSection } from "./EditSection";
import { SelectInput } from "./SelectInput";
import { TextInput } from "./TextInput";
interface CloudInitProps {
vm: VMInfo;
onChange?: () => void;
editable: boolean;
}
export function CloudInitEditor(p: CloudInitProps): React.ReactElement {
return (
<>
<EditSection>
{/* Attach cloud init disk */}
<CheckboxInput
{...p}
label="Attach Cloud Init disk"
checked={p.vm.cloud_init.attach_config}
onValueChange={(v) => {
p.vm.cloud_init.attach_config = v;
p.onChange?.();
}}
/>
</EditSection>
<Grid container spacing={2}>
<CloudInitMetadata
{...p}
editable={p.editable && p.vm.cloud_init.attach_config}
/>
<CloudInitRawUserData
{...p}
editable={p.editable && p.vm.cloud_init.attach_config}
/>
<CloudInitNetworkConfig
{...p}
editable={p.editable && p.vm.cloud_init.attach_config}
/>
<CloudInitUserDataAssistant
{...p}
editable={p.editable && p.vm.cloud_init.attach_config}
/>
</Grid>
</>
);
}
function CloudInitMetadata(p: CloudInitProps): React.ReactElement {
// Regenerate instance id
const reGenerateInstanceId = () => {
p.vm.cloud_init.instance_id = uuidv4();
p.onChange?.();
};
return (
<EditSection title="Metadata">
{/* Instance ID */}
<TextInput
{...p}
label="Instance ID"
value={p.vm.cloud_init.instance_id}
onValueChange={(v) => {
p.vm.cloud_init.instance_id = v;
p.onChange?.();
}}
endAdornment={
p.editable ? (
<InputAdornment position="end">
<Tooltip title="Generate a new instance ID">
<IconButton onClick={reGenerateInstanceId}>
<RefreshIcon />
</IconButton>
</Tooltip>
</InputAdornment>
) : (
<></>
)
}
/>
{/* Instance hostname */}
<TextInput
{...p}
label="Local hostname"
value={p.vm.cloud_init.local_hostname}
onValueChange={(v) => {
p.vm.cloud_init.local_hostname = v;
p.onChange?.();
}}
/>
{/* Data source mode */}
<SelectInput
{...p}
label="Data source mode"
value={p.vm.cloud_init.dsmode}
onValueChange={(v) => {
p.vm.cloud_init.dsmode = v as any;
p.onChange?.();
}}
options={[
{ label: "None", value: undefined },
{ value: "Net" },
{ value: "Local" },
]}
/>
</EditSection>
);
}
function CloudInitRawUserData(p: CloudInitProps): React.ReactElement {
return (
<EditSection
title="User data"
actions={
<RouterLink
target="_blank"
to="https://cloudinit.readthedocs.io/en/latest/reference/index.html"
>
<Tooltip title="Official reference">
<IconButton size="small">
<BookIcon />
</IconButton>
</Tooltip>
</RouterLink>
}
>
<Editor
theme="vs-dark"
options={{
readOnly: !p.editable,
quickSuggestions: { other: true, comments: true, strings: true },
wordWrap: "on",
}}
language="yaml"
height={"30vh"}
value={p.vm.cloud_init.user_data}
onChange={(v) => {
p.vm.cloud_init.user_data = v ?? "";
p.onChange?.();
}}
/>
</EditSection>
);
}
function CloudInitNetworkConfig(p: CloudInitProps): React.ReactElement {
if (!p.editable && !p.vm.cloud_init.network_configuration) return <></>;
return (
<EditSection
title="Network configuration"
actions={
<RouterLink
target="_blank"
to="https://cloudinit.readthedocs.io/en/latest/reference/network-config-format-v2.html"
>
<Tooltip title="Official network configuration reference">
<IconButton size="small">
<BookIcon />
</IconButton>
</Tooltip>
</RouterLink>
}
>
<Editor
theme="vs-dark"
options={{
readOnly: !p.editable,
quickSuggestions: { other: true, comments: true, strings: true },
wordWrap: "on",
}}
language="yaml"
height={"30vh"}
value={p.vm.cloud_init.network_configuration ?? ""}
onChange={(v) => {
if (v && v !== "") p.vm.cloud_init.network_configuration = v;
else p.vm.cloud_init.network_configuration = undefined;
p.onChange?.();
}}
/>
</EditSection>
);
}
function CloudInitUserDataAssistant(p: CloudInitProps): React.ReactElement {
const user_data = React.useMemo(() => {
return YAML.parseDocument(p.vm.cloud_init.user_data);
}, [p.vm.cloud_init.user_data]);
const onChange = () => {
p.vm.cloud_init.user_data = user_data.toString();
if (!p.vm.cloud_init.user_data.startsWith("#cloud-config"))
p.vm.cloud_init.user_data = `#cloud-config\n${p.vm.cloud_init.user_data}`;
p.onChange?.();
};
const SYSTEMD_NOT_SERIAL = `/bin/sh -c "rm -f /etc/default/grub.d/50-cloudimg-settings.cfg && sed -i 's/quiet splash//g' /etc/default/grub && update-grub"`;
return (
<EditSection title="User data assistant">
<CloudInitTextInput
editable={p.editable}
name="Default user name"
refUrl="https://cloudinit.readthedocs.io/en/latest/reference/modules.html#set-passwords"
attrPath={["user", "name"]}
onChange={onChange}
yaml={user_data}
/>
<CloudInitTextInput
editable={p.editable}
name="Default user password"
refUrl="https://cloudinit.readthedocs.io/en/latest/reference/modules.html#set-passwords"
attrPath={["password"]}
onChange={onChange}
yaml={user_data}
/>
<CloudInitBooleanInput
editable={p.editable}
name="Expire password to require new password on next login"
yaml={user_data}
attrPath={["chpasswd", "expire"]}
onChange={onChange}
refUrl="https://cloudinit.readthedocs.io/en/latest/reference/modules.html#set-passwords"
/>
<br />
<CloudInitBooleanInput
editable={p.editable}
name="Enable SSH password auth"
yaml={user_data}
attrPath={["ssh_pwauth"]}
onChange={onChange}
refUrl="https://cloudinit.readthedocs.io/en/latest/reference/modules.html#set-passwords"
/>
<CloudInitTextInput
editable={p.editable}
name="Keyboard layout"
refUrl="https://cloudinit.readthedocs.io/en/latest/reference/modules.html#keyboard"
attrPath={["keyboard", "layout"]}
onChange={onChange}
yaml={user_data}
/>
<CloudInitTextInput
editable={p.editable}
name="Final message"
refUrl="https://cloudinit.readthedocs.io/en/latest/reference/modules.html#final-message"
attrPath={["final_message"]}
onChange={onChange}
yaml={user_data}
/>
{/* /bin/sh -c "rm -f /etc/default/grub.d/50-cloudimg-settings.cfg && update-grub" */}
<CheckboxInput
editable={p.editable}
label="Show all startup messages on tty1, not serial"
checked={
!!(user_data.get("runcmd") as any)?.items.find(
(a: any) => a.value === SYSTEMD_NOT_SERIAL
)
}
onValueChange={(c) => {
if (!user_data.getIn(["runcmd"])) user_data.addIn(["runcmd"], []);
const runcmd = user_data.getIn(["runcmd"]) as any;
if (c) {
runcmd.addIn([], SYSTEMD_NOT_SERIAL);
} else {
const idx = runcmd.items.findIndex(
(o: any) => o.value === SYSTEMD_NOT_SERIAL
);
runcmd.items.splice(idx, 1);
}
onChange();
}}
/>
</EditSection>
);
}
function CloudInitTextInput(p: {
editable: boolean;
name: string;
refUrl: string;
attrPath: Iterable<unknown>;
yaml: YAML.Document;
onChange?: () => void;
}): React.ReactElement {
return (
<TextInput
editable={p.editable}
label={p.name}
value={String(p.yaml.getIn(p.attrPath) ?? "")}
onValueChange={(v) => {
if (v !== undefined) p.yaml.setIn(p.attrPath, v);
else p.yaml.deleteIn(p.attrPath);
p.onChange?.();
}}
endAdornment={
<RouterLink to={p.refUrl} target="_blank">
<IconButton size="small">
<BookIcon />
</IconButton>
</RouterLink>
}
/>
);
}
function CloudInitBooleanInput(p: {
editable: boolean;
name: string;
refUrl: string;
attrPath: Iterable<unknown>;
yaml: YAML.Document;
onChange?: () => void;
}): React.ReactElement {
return (
<CheckboxInput
editable={p.editable}
label={p.name}
checked={p.yaml.getIn(p.attrPath) === true}
onValueChange={(v) => {
p.yaml.setIn(p.attrPath, v);
p.onChange?.();
}}
/>
);
}

View File

@@ -0,0 +1,25 @@
import { ServerApi } from "../../api/ServerApi";
import { TextInput } from "./TextInput";
export function DiskSizeInput(p: {
editable: boolean;
label?: string;
value: number;
onChange?: (size: number) => void;
}): React.ReactElement {
return (
<TextInput
editable={p.editable}
label={p.label ?? "Disk size (GB)"}
size={{
min: ServerApi.Config.constraints.disk_size.min / (1000 * 1000 * 1000),
max: ServerApi.Config.constraints.disk_size.max / (1000 * 1000 * 1000),
}}
value={(p.value / (1000 * 1000 * 1000)).toString()}
onValueChange={(v) => {
p.onChange?.(Number(v ?? "0") * 1000 * 1000 * 1000);
}}
type="number"
/>
);
}

View File

@@ -19,13 +19,10 @@ export function EditSection(
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "15px",
}}
>
{p.title && (
<Typography variant="h5" style={{ marginBottom: "15px" }}>
{p.title}
</Typography>
)}
{p.title && <Typography variant="h5">{p.title}</Typography>}
{p.actions}
</span>
)}

View File

@@ -25,6 +25,8 @@ export function OEMStringFormWidget(p: {
p.onChange?.();
};
if (!p.editable && p.vm.oem_strings.length === 0) return <></>;
return (
<EditSection
title="SMBIOS OEM Strings"

View File

@@ -18,6 +18,7 @@ export function TextInput(p: {
style?: React.CSSProperties;
helperText?: string;
disabled?: boolean;
endAdornment?: React.ReactNode;
}): React.ReactElement {
if (!p.editable && (p.value ?? "") === "") return <></>;
@@ -51,6 +52,7 @@ export function TextInput(p: {
input: {
readOnly: !p.editable,
type: p.type,
endAdornment: p.endAdornment,
},
}}
variant={"standard"}

View File

@@ -2,7 +2,8 @@ import { mdiHarddiskPlus } from "@mdi/js";
import Icon from "@mdi/react";
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
import DeleteIcon from "@mui/icons-material/Delete";
import { Button, IconButton, Paper, Tooltip } from "@mui/material";
import ExpandIcon from "@mui/icons-material/Expand";
import { Button, IconButton, Paper, Tooltip, Typography } from "@mui/material";
import React from "react";
import { DiskImage } from "../../api/DiskImageApi";
import { ServerApi } from "../../api/ServerApi";
@@ -13,6 +14,7 @@ import { VMDiskFileWidget } from "../vms/VMDiskFileWidget";
import { CheckboxInput } from "./CheckboxInput";
import { DiskBusSelect } from "./DiskBusSelect";
import { DiskImageSelect } from "./DiskImageSelect";
import { DiskSizeInput } from "./DiskSizeInput";
import { SelectInput } from "./SelectInput";
import { TextInput } from "./TextInput";
@@ -67,6 +69,12 @@ export function VMDisksList(p: {
/>
))}
{p.vm.file_disks.length === 0 && (
<Typography style={{ textAlign: "center", paddingTop: "25px" }}>
No disk file yet!
</Typography>
)}
{p.editable && <Button onClick={addNewDisk}>Add new disk</Button>}
{/* Disk backup */}
@@ -93,6 +101,19 @@ function DiskInfo(p: {
diskImagesList: DiskImage[];
}): React.ReactElement {
const confirm = useConfirm();
const expandDisk = () => {
if (p.disk.resize === true) {
p.disk.resize = false;
p.disk.size = p.disk.originalSize!;
} else {
p.disk.resize = true;
p.disk.originalSize = p.disk.size!;
}
p.onChange?.();
};
const deleteDisk = async () => {
if (p.disk.deleteType) {
p.disk.deleteType = undefined;
@@ -115,42 +136,75 @@ function DiskInfo(p: {
if (!p.editable || !p.disk.new)
return (
<VMDiskFileWidget
{...p}
secondaryAction={
<>
{p.editable && (
<IconButton
edge="end"
aria-label="delete disk"
onClick={deleteDisk}
>
{p.disk.deleteType ? (
<Tooltip title="Cancel disk removal">
<CheckCircleIcon />
</Tooltip>
) : (
<Tooltip title="Remove disk">
<DeleteIcon />
</Tooltip>
)}
</IconButton>
)}
{p.canBackup && (
<Tooltip title="Backup this disk">
<>
<VMDiskFileWidget
{...p}
secondaryAction={
<>
{p.editable && !p.disk.deleteType && (
<IconButton
onClick={() => {
p.onRequestBackup(p.disk);
}}
edge="end"
aria-label="expand disk"
onClick={expandDisk}
>
<Icon path={mdiHarddiskPlus} size={1} />
{p.disk.resize === true ? (
<Tooltip title="Cancel disk expansion">
<ExpandIcon color="error" />
</Tooltip>
) : (
<Tooltip title="Increase disk size">
<ExpandIcon />
</Tooltip>
)}
</IconButton>
</Tooltip>
)}
</>
}
/>
)}
{p.editable && (
<IconButton
edge="end"
aria-label="delete disk"
onClick={deleteDisk}
>
{p.disk.deleteType ? (
<Tooltip title="Cancel disk removal">
<CheckCircleIcon />
</Tooltip>
) : (
<Tooltip title="Remove disk">
<DeleteIcon />
</Tooltip>
)}
</IconButton>
)}
{p.canBackup && (
<Tooltip title="Backup this disk">
<IconButton
onClick={() => {
p.onRequestBackup(p.disk);
}}
>
<Icon path={mdiHarddiskPlus} size={1} />
</IconButton>
</Tooltip>
)}
</>
}
/>
{/* New disk size*/}
{p.disk.resize && !p.disk.deleteType && (
<DiskSizeInput
editable
label="New disk size (GB)"
value={p.disk.size}
onChange={(v) => {
p.disk.size = v;
p.onChange?.();
}}
/>
)}
</>
);
return (
@@ -212,24 +266,32 @@ function DiskInfo(p: {
/>
)}
<TextInput
editable={true}
label="Disk size (GB)"
size={{
min:
ServerApi.Config.constraints.disk_size.min / (1000 * 1000 * 1000),
max:
ServerApi.Config.constraints.disk_size.max / (1000 * 1000 * 1000),
}}
value={(p.disk.size / (1000 * 1000 * 1000)).toString()}
onValueChange={(v) => {
p.disk.size = Number(v ?? "0") * 1000 * 1000 * 1000;
p.onChange?.();
}}
type="number"
disabled={!!p.disk.from_image}
/>
{/* Resize disk image */}
{!!p.disk.from_image && (
<CheckboxInput
editable
checked={p.disk.resize}
label="Resize disk file"
onValueChange={(v) => {
p.disk.resize = v;
p.onChange?.();
}}
/>
)}
{/* Disk size */}
{(!p.disk.from_image || p.disk.resize === true) && (
<DiskSizeInput
editable
value={p.disk.size}
onChange={(v) => {
p.disk.size = v;
p.onChange?.();
}}
/>
)}
{/* Disk image selection */}
<DiskImageSelect
label="Use disk image as template"
list={p.diskImagesList}

View File

@@ -10,6 +10,7 @@ import {
ListItemAvatar,
ListItemText,
Tooltip,
Typography,
} from "@mui/material";
import Grid from "@mui/material/Grid";
import { NWFilter } from "../../api/NWFilterApi";
@@ -49,6 +50,12 @@ export function VMNetworksList(p: {
</div>
)}
{p.vm.networks.length === 0 && (
<Typography style={{ textAlign: "center", paddingTop: "25px" }}>
No network interface defined yet!
</Typography>
)}
<Grid container spacing={2}>
{/* networks list */}
{p.vm.networks.map((n, num) => (

View File

@@ -60,6 +60,7 @@ export function TokenRightsEditor(p: {
<TableCell align="center">Get XML definition</TableCell>
<TableCell align="center">Get autostart</TableCell>
<TableCell align="center">Set autostart</TableCell>
<TableCell align="center">Get CloudInit disk</TableCell>
<TableCell align="center">Backup disk</TableCell>
</TableRow>
</TableHead>
@@ -84,6 +85,13 @@ export function TokenRightsEditor(p: {
{...p}
right={{ verb: "PUT", path: "/api/vm/*/autostart" }}
/>
<CellRight
{...p}
right={{
verb: "GET",
path: "/api/vm/*/cloud_init_disk",
}}
/>
<CellRight
{...p}
right={{ verb: "POST", path: "/api/vm/*/disk/*/backup" }}
@@ -123,7 +131,15 @@ export function TokenRightsEditor(p: {
{...p}
right={{ verb: "PUT", path: `/api/vm/${v.uuid}/autostart` }}
parent={{ verb: "PUT", path: "/api/vm/*/autostart" }}
/>{" "}
/>
<CellRight
{...p}
right={{
verb: "GET",
path: `/api/vm/${v.uuid}/cloud_init_disk`,
}}
parent={{ verb: "GET", path: "/api/vm/*/cloud_init_disk" }}
/>
<CellRight
{...p}
right={{
@@ -783,6 +799,11 @@ export function TokenRightsEditor(p: {
right={{ verb: "GET", path: "/api/server/bridges" }}
label="Get list of network bridges"
/>
<RouteRight
{...p}
right={{ verb: "GET", path: "/api/server/export_configs" }}
label="Export all configurations"
/>
</RightsSection>
</>
);

View File

@@ -5,6 +5,7 @@ import Grid from "@mui/material/Grid";
import React from "react";
import { useNavigate } from "react-router-dom";
import { validate as validateUUID } from "uuid";
import { DiskImage, DiskImageApi } from "../../api/DiskImageApi";
import { GroupApi } from "../../api/GroupApi";
import { IsoFile, IsoFilesApi } from "../../api/IsoFilesApi";
import { NWFilter, NWFilterApi } from "../../api/NWFilterApi";
@@ -18,6 +19,7 @@ import { AsyncWidget } from "../AsyncWidget";
import { TabsWidget } from "../TabsWidget";
import { XMLAsyncWidget } from "../XMLWidget";
import { CheckboxInput } from "../forms/CheckboxInput";
import { CloudInitEditor } from "../forms/CloudInitEditor";
import { EditSection } from "../forms/EditSection";
import { OEMStringFormWidget } from "../forms/OEMStringFormWidget";
import { ResAutostartInput } from "../forms/ResAutostartInput";
@@ -27,7 +29,6 @@ import { VMDisksList } from "../forms/VMDisksList";
import { VMNetworksList } from "../forms/VMNetworksList";
import { VMSelectIsoInput } from "../forms/VMSelectIsoInput";
import { VMScreenshot } from "./VMScreenshot";
import { DiskImage, DiskImageApi } from "../../api/DiskImageApi";
interface DetailsProps {
vm: VMInfo;
@@ -89,6 +90,7 @@ enum VMTab {
General = 0,
Storage,
Network,
CloudInit,
Advanced,
XML,
Danger,
@@ -116,6 +118,11 @@ function VMDetailsInner(p: DetailsInnerProps): React.ReactElement {
{ label: "General", value: VMTab.General, visible: true },
{ label: "Storage", value: VMTab.Storage, visible: true },
{ label: "Network", value: VMTab.Network, visible: true },
{
label: "Cloud Init",
value: VMTab.CloudInit,
visible: p.editable || p.vm.cloud_init.attach_config,
},
{ label: "Avanced", value: VMTab.Advanced, visible: true },
{
@@ -135,6 +142,7 @@ function VMDetailsInner(p: DetailsInnerProps): React.ReactElement {
{currTab === VMTab.General && <VMDetailsTabGeneral {...p} />}
{currTab === VMTab.Storage && <VMDetailsTabStorage {...p} />}
{currTab === VMTab.Network && <VMDetailsTabNetwork {...p} />}
{currTab === VMTab.CloudInit && <VMDetailsTabCloudInit {...p} />}
{currTab === VMTab.Advanced && <VMDetailsTabAdvanced {...p} />}
{currTab === VMTab.XML && <VMDetailsTabXML {...p} />}
{currTab === VMTab.Danger && <VMDetailsTabDanger {...p} />}
@@ -381,6 +389,10 @@ function VMDetailsTabNetwork(p: DetailsInnerProps): React.ReactElement {
return <VMNetworksList {...p} />;
}
function VMDetailsTabCloudInit(p: DetailsInnerProps): React.ReactElement {
return <CloudInitEditor {...p} />;
}
function VMDetailsTabAdvanced(p: DetailsInnerProps): React.ReactElement {
return (
<Grid container spacing={2}>