193 Commits
Author SHA1 Message Date
Arthur Lu f9ad56a283 fix instance create pool error mesage 2024-04-23 18:56:43 +00:00
Arthur Lu 0a8bd87ed4 check instance pool matches user allowed pools,
update user allowed nodes format,
add get user ct templates route
2024-04-15 21:52:20 +00:00
Arthur Lu e032038100 update localdb backend interface,
update all references to localdb backend
2024-04-09 21:02:41 +00:00
Arthur Lu 1c41d55ceb add pool selection to instance create,
allow multiple pools per user
2024-04-08 21:19:57 +00:00
Arthur Lu 848eb5d1d1 implement full interface for paasldap backend 2024-04-05 22:33:56 +00:00
Arthur Lu 0b5cfff519 add user backend documentation 2024-04-04 18:53:36 +00:00
Arthur Lu b3c9bb48c1 update backend interfaces 2024-04-03 20:03:31 +00:00
Arthur Lu cea8d80ea2 fix function name in AUTH_BACKEND 2024-03-25 21:30:23 +00:00
Arthur Lu 06d160d9ce fix linting 2024-02-24 20:57:11 +00:00
Arthur Lu 67a17cfef0 fix missing return value in user password set 2024-02-23 22:27:28 +00:00
Arthur Lu fae79e4ab0 add localdb template,
update paasldap backend interface
2024-01-30 20:33:41 +00:00
Arthur Lu c347a3b8c0 add put and delete proxmox proxy routes 2024-01-22 19:47:13 +00:00
Arthur Lu 1ffe9fbe7c fix bugs in paasldap backend and auth password endpoint 2024-01-18 21:37:46 +00:00
Arthur Lu bb047a3782 fix issues in backend implementions,
auth endpoint now fetches all relevant backend tokens
2024-01-17 20:21:55 +00:00
Arthur Lu 0013833bce update README 2024-01-16 22:36:21 +00:00
Arthur Lu c9e4bb7484 fix linting 2024-01-12 01:49:24 +00:00
Arthur Lu 68f92493b7 fix various formatting,
add interface for generic backends,
add interfaces for DB and AUTH type backends,
implement basic user password change method
2024-01-09 00:47:33 +00:00
Arthur Lu 18590011cc major rework of backend loading and usage:
- split config from user data to config.json, add config.hson.template
- moved default user data localdb to root folder
- moved pve, localdb, ldap backend handlers to backends sub folder
- add dynamic loading of all backends
- add dynamic mapping for auth backends to support multiple auth sources
- update affected endpoints
2024-01-06 22:36:18 +00:00
Arthur Lu c801ca99df fix linting 2023-12-15 08:14:39 +00:00
Arthur Lu 3d746791e3 change default api port 2023-12-14 00:26:49 +00:00
Arthur Lu a55b9a58d7 move listenPort option to argv 2023-11-16 23:26:11 +00:00
Arthur Lu e2cf883e64 update localdb template 2023-11-15 20:04:53 +00:00
Arthur Lu c2ab19b6d6 add per instance resource quotas,
move getFullInstanceConfig to utils.js,
rework resource related utilities to use new quota format
2023-11-15 19:57:59 +00:00
Arthur Lu 02764c8f6a simplify auth/password logic 2023-11-14 23:50:24 +00:00
Arthur Lu 6e2b284118 fix bug in net create 2023-11-09 20:46:50 +00:00
Arthur Lu 5989d86ef8 add password change route 2023-11-09 18:57:52 +00:00
Arthur Lu eb71f57427 fix linting 2023-10-25 19:46:37 +00:00
Arthur Lu 2f20992e7c fix issue with localdb,
rename db.js to localdb.js
add option to dynamically load db backend
2023-10-24 19:01:53 +00:00
Arthur Lu bf8c8d1f00 change localdb interface 2023-10-23 22:09:31 +00:00
Arthur Lu 2d7df40b04 improve getUsedResources,
remove unused params for some pve functions
2023-10-21 00:13:29 +00:00
Arthur Lu 75c80fd271 update readme and db template 2023-10-13 18:01:53 +00:00
Arthur Lu ad293d890a fix dashboard in readme 2023-10-05 21:19:15 +00:00
Arthur Lu 31463cb55f update readme with dashboard rename 2023-10-05 21:10:49 +00:00
Arthur Lu e3e2ec272b update readme 2023-10-05 20:30:25 +00:00
Arthur Lu 43988e9815 simplify requestPVE args 2023-09-12 19:16:19 +00:00
Arthur Lu a366918256 update resource data format,
add names to pci and cpu list resources
2023-09-08 20:48:33 +00:00
Arthur Lu 45006171ee update localdb template 2023-08-30 20:50:21 +00:00
Arthur Lu abeb307de1 fix error with empty boot order,
fix error with multiple item boot order
2023-08-28 23:34:43 +00:00
Arthur Lu 33660b9f8c fix linting 2023-08-24 22:28:25 +00:00
Arthur Lu 0e836d50c4 set boot order in instance config handler 2023-08-22 06:06:10 +00:00
Arthur Lu 212d9721f8 dynamically import routes 2023-08-07 18:48:47 +00:00
Arthur Lu 0232b2208e add api package cli arg,
fix comments
2023-08-05 00:38:57 +00:00
Arthur Lu 221c854d50 fix db module imports 2023-08-03 19:51:09 +00:00
Arthur Lu c5f7239813 fix argument parse bug 2023-08-03 19:34:37 +00:00
Arthur Lu c9807668ea add cli arg for localdb path 2023-08-03 19:31:11 +00:00
Arthur Lu 3aaa0d2098 route cluster routes to /api/cluster 2023-08-03 00:36:18 +00:00
Arthur Lu 57dab01d7b finish moving routes to files 2023-08-01 19:07:45 +00:00
Arthur Lu 7fe1d2af5c fix linting errors,
group more routes,
use mergeParams in router init
2023-07-31 19:23:22 +00:00
Arthur Lu 76ccfabf59 add default export to db and package module,
start grouping routes by type,
use dynamic imports and express routers
2023-07-27 20:10:46 +00:00
Arthur Lu fcb2151019 update localdb template 2023-07-25 19:26:14 +00:00
Arthur Lu d172f0a590 add cluster iso route 2023-07-25 00:03:13 +00:00
Arthur Lu 790aa4959d fix interrupt sync desync issues 2023-07-18 20:27:51 +00:00
Arthur Lu 98f2c39e6e fix bug with socket close handling of poolUserMap,
improve socket init,
improve interrupt sync timing progression issues
2023-07-17 19:41:20 +00:00
Arthur Lu e0a121009d fix issue with client sync init 2023-07-15 22:59:18 +00:00
Arthur Lu e869834eb3 implement interrupt sync 2023-07-14 21:48:06 +00:00
Arthur Lu b827ff0025 fix formatting 2023-07-12 06:38:06 +00:00
Arthur Lu 070d7714ca add better client sync schemes initialization,
add client sync scheme selects to localdb,
start on interrupt sync scheme implementation,
change sync endpoints to start with /sync/
2023-07-11 22:06:41 +00:00
Arthur Lu 096be3d032 add /cluster/statushash endpoint 2023-07-11 18:48:45 +00:00
Arthur Lu faa0efa75c fix endpoint comment format 2023-07-10 22:18:08 +00:00
Arthur Lu f0e5340474 comment a bunch of code 2023-07-10 06:50:29 +00:00
Arthur Lu ffdce767ab fix some endpoint comments,
add user node permission check to /node/pci
2023-07-07 00:06:36 +00:00
Arthur Lu 8fb26b274a fix linting issues 2023-07-06 05:13:22 +00:00
Arthur Lu 9e6f4cc499 consolidate user config paths,
move global config values to global key in localdb
2023-07-05 23:14:45 +00:00
Arthur Lu 9da8880163 add basic regex patterns for endpoint path params 2023-07-04 21:11:46 +00:00
Arthur Lu 6a3bca4948 move most endpoint parameters to route 2023-07-04 04:41:25 +00:00
Arthur Lu bc1757c9c3 set brace style to Stroustrup 2023-06-29 22:09:57 +00:00
Arthur Lu 1e8a4a8c29 lint all files 2023-06-29 21:53:20 +00:00
Arthur Lu e39f954fc1 add eslinting, fix linting issues 2023-06-29 21:35:19 +00:00
Arthur Lu 90dbcdbb10 mionor fixes to localdb template 2023-06-29 20:27:05 +00:00
Arthur Lu df8d472f85 improve db format for templates,
add additional per user customization for instance network interfaces,
update tempalte localdb
2023-06-29 02:07:57 +00:00
Arthur Lu 8a8a4c0df6 check storage on disk create only when disk is not ide 2023-06-23 03:37:15 +00:00
Arthur Lu 4fa5fb158f check resource approval only if pci device differs 2023-06-23 03:20:56 +00:00
Arthur Lu 1fb934fd0f implement modify pci endpoint 2023-06-23 03:01:28 +00:00
Arthur Lu 19f38fa25d implement pcie device add and delete endpoints,
change getDeviceInfo to return primary device with sun devices listed,
fix bug in approveResources when checking list type resources
2023-06-22 00:23:34 +00:00
Arthur Lu 49192daac6 add pci device resource to config,
implement endpoints for getting available devices,
update used resource calculation to include pci devices
2023-06-21 05:06:38 +00:00
Arthur Lu 83a03bfd7b update lcoaldb template
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-06-14 22:34:10 +00:00
Arthur Lu 03b3a994c6 improve user config routes,
add glocal resource config route,
remove debug console logs

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-06-14 22:20:30 +00:00
Arthur Lu e9386f5204 implement proc type in /instance/resources 2023-06-14 04:49:43 +00:00
Arthur Lu 8d7d952e21 fix bugs in network methods 2023-06-09 04:24:22 +00:00
Arthur Lu a807e5cb03 move source files to src folder,
move localdb to config folder,
consolidate vars.js with localdb,
move service scripts to service folder
2023-06-09 03:58:38 +00:00
Arthur Lu 0043e3e783 fix formatting 2023-06-09 02:05:26 +00:00
Arthur Lu c110330154 add checking for disk bus prefix in disk create and disk attach 2023-06-09 00:24:37 +00:00
Arthur Lu e356684cd9 improve method comments relating to pve builtin return objects,
add paths to create and delete network interfaces,
check for interface existence in modify interface,
add vlan specification to user config and use in network interface creation,
fix disk path security by checking disk existence or nonexistence,
TODO: check create and mount disk against allowed bus types

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-06-08 23:33:32 +00:00
Arthur Lu 78377c0479 fix handleResponse responding with proxmox task status
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-06-03 00:10:33 +00:00
Arthur Lu 2936d39368 change /api/node/pci to /api/instance/pci
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-06-02 23:14:13 +00:00
Arthur Lu 5eb5642166 fix nodes pci endpoint
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-06-01 17:12:52 +00:00
Arthur Lu 15f4026979 add /api/nodes/pci endpoint (untested)
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-06-01 15:35:20 +00:00
Arthur Lu 310ff8e6ae fix approveResources
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-24 23:10:05 +00:00
Arthur Lu 52d40796e9 fix bugs in utils.js
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-24 22:42:38 +00:00
Arthur Lu 0ad242a557 implement db as class,
improve approveResources

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-24 22:21:00 +00:00
Arthur Lu 7e7f9b2b55 fix api crash on username missing from lcoaldb 2023-05-23 00:11:48 +00:00
Arthur Lu c8c42d3b95 remove console.log
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-19 20:50:45 +00:00
Arthur Lu fd7dec6af3 update localdb.json.template,
fix tempalte resource checking for post /instance

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-19 20:50:07 +00:00
Arthur Lu 87b1b622fb fix comments,
fix handleResponse

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-19 20:03:38 +00:00
Arthur Lu 478a25c9e0 fix issue in swap handling in /instance/resources
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-18 20:19:01 +00:00
Arthur Lu f43e9e50d7 improve response value of /user/nodes endpoint,
add basic documentation of routes,
add swap for lxc instances to /instance/resources endpoint

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-18 20:05:41 +00:00
Arthur Lu 505ea28e8a handle NaN or undefined values in approveResources
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-18 14:58:44 +00:00
Arthur Lu 70953f3194 mitigate some vulnerabilities in disk attach,
handle errors in getDiskInfo as result of nonexistent disk
2023-05-18 08:09:18 +00:00
Arthur Lu 2cd23324e5 fix formatting 2023-05-17 21:38:11 +00:00
Arthur Lu b12d84d4be fix authentication checks for routes involving specific instances 2023-05-17 21:30:36 +00:00
Arthur Lu 8c71ceafcf fix network resource usage calculation
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-16 16:50:24 +00:00
Arthur Lu 34de9788ba fix db template
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-16 16:46:15 +00:00
Arthur Lu 90d399d8b6 implement network interface endpoint
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-16 16:34:39 +00:00
Arthur Lu f415b8f962 fix auth early escape
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-16 15:46:39 +00:00
Arthur Lu 4039cdf92d fix bug in login endpoint
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-15 22:11:24 +00:00
Arthur Lu ef23428753 fix error handling in ticket endpoint
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-15 22:00:46 +00:00
Arthur Lu 0aff37a9c3 remove httponly form username cookie 2023-05-13 07:38:32 +00:00
Arthur Lu a70981afb6 update README 2023-05-13 07:36:13 +00:00
Arthur Lu 08947ca15d implement safer pve ticket endpoint 2023-05-13 07:34:58 +00:00
Arthur Lu a0ebcdf1b0 remove helmet 2023-05-13 05:17:48 +00:00
Arthur Lu 9cfa53172c fix bug in instance post
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-12 21:07:47 +00:00
Arthur Lu e89eeeb5cb separate user data endpoints
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-12 20:51:07 +00:00
Arthur Lu 2a37423dd9 style fix 2023-05-11 07:10:22 +00:00
Arthur Lu 3d72e189d4 update vars.js.template
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-27 17:27:51 +00:00
Arthur Lu 2a70548a43 add node restriction
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-27 15:39:01 +00:00
Arthur Lu 0063b6b2b9 Update README.md 2023-04-26 16:38:04 -07:00
Arthur Lu 8527c0aa80 Update README.md 2023-04-26 16:36:54 -07:00
Arthur Lu 4200bd7df4 Update README.md 2023-04-26 16:36:23 -07:00
Arthur Lu 7ea2160c9b fix /api/version 2023-04-26 21:58:15 +00:00
Arthur Lu cc3c1061ba use consistent version notation 2023-04-26 02:38:46 +00:00
Arthur Lu 51c10734b1 update readme with required nodejs version 2023-04-26 02:38:24 +00:00
Arthur Lu 6fc9113e4f use experimental import assertion,
requires nodejs 18 or higher
2023-04-26 01:40:12 +00:00
Arthur Lu 2deda42ec5 update to es6 and fix some syntax errors 2023-04-26 01:18:05 +00:00
Arthur Lu 631237d857 change /api/resources to /api/instance/resources
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-25 15:30:28 +00:00
Arthur Lu 096dfb10e0 fix bugs from refactor
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-24 21:45:32 +00:00
Arthur Lu 81c290f795 fix bug in /api/user
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-24 21:34:30 +00:00
Arthur Lu c7c1191587 code cleanup,
disk paths now under instance

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-24 21:28:58 +00:00
Arthur Lu 9ad10f6c82 fix bug in /user/instances 2023-04-21 22:40:12 +00:00
Arthur Lu d2d1f25131 add /user/instances path 2023-04-21 22:37:12 +00:00
Arthur Lu 952e7e590d minor change to user data to fit additional user data
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-20 21:39:41 +00:00
Arthur Lu 2399858d29 add vmid range check to instance creation,
add configurable templates to instance creation

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-20 21:27:32 +00:00
Arthur Lu 9943a35d7e simplify db
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-20 20:43:05 +00:00
Arthur Lu f2ccee5119 bug fixes
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-19 20:36:48 +00:00
Arthur Lu f274949523 Update README.md 2023-04-19 05:54:18 +00:00
Arthur Lu 7e0bce1dca add unit return to resources path 2023-04-19 02:46:05 +00:00
Arthur Lu 5862edcf24 fully implement new db 2023-04-19 02:42:35 +00:00
Arthur Lu dacbd965cf implement new db strategy
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-19 01:03:55 +00:00
Arthur Lu c92fc95e92 fix bug in create disk resource allocation
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-11 21:57:14 +00:00
Arthur Lu 3810eef1fa fix spelling
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-11 18:00:04 +00:00
Arthur Lu 1acaa6c864 use a better db json layout
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-11 17:56:48 +00:00
Arthur Lu 0c6fc5ae1f fix bug in resource get
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-03 21:53:11 +00:00
Arthur Lu 8862144524 add path to get remaining resources
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-03 21:45:06 +00:00
Arthur Lu d135cec5ae fix bug in input validation
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-03-24 22:22:00 +00:00
Arthur Lu eed39a007b remove extra consol logs
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-03-24 22:14:21 +00:00
Arthur Lu bd845b47f0 fix bug in validation
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-03-24 22:10:17 +00:00
Arthur Lu 33deb01b74 implement quick input checks
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-03-24 19:35:16 +00:00
Arthur Lu 143e9d3f6e Update README.md 2023-02-28 21:15:17 -08:00
Arthur Lu 3afb302a16 remove extra comment
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-03-01 03:44:42 +00:00
Arthur Lu 38589b1b33 implement disk deallocation in delete instance,
remove console.logs

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-03-01 01:14:03 +00:00
Arthur Lu 51d231e7bf improve return data on success
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-03-01 00:53:29 +00:00
Arthur Lu 4bb9aadd04 almost finish implementing resource check
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-03-01 00:50:24 +00:00
Arthur Lu 96bbd469fb implement resource checking all config paths
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-03-01 00:38:09 +00:00
Arthur Lu 3e3ae6077b add some comments
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-28 23:36:22 +00:00
Arthur Lu 1e4b72cb8b rename getDiskByConfig to getDiskConfig
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-28 23:33:00 +00:00
Arthur Lu 2b311cd1f1 improve api responses
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-28 23:27:32 +00:00
Arthur Lu 4fdb2f1cb8 add resource check on resize
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-28 23:24:49 +00:00
Arthur Lu 976995e2a1 use vars to specify cors domain
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-28 22:04:30 +00:00
Arthur Lu ac08243622 update required api permissions
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-28 02:42:36 +00:00
Arthur Lu eb5b39ea22 assign pool membership of new isntance using user group membership
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-28 02:38:30 +00:00
Arthur Lu 2e6840ce99 fix cors origin
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-28 01:37:46 +00:00
Arthur Lu f0798759c2 fix README instructions
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-28 00:07:24 +00:00
Arthur Lu 537420624e improve local db utils
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-27 23:11:51 +00:00
Arthur Lu aa548d4fa8 add create instance endpoint
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-27 23:04:38 +00:00
Arthur Lu dd40e0a802 improve endpoint args 2023-02-27 02:29:39 +00:00
Arthur Lu 3febe6601c add basic local db utility functions 2023-02-27 01:28:01 +00:00
Arthur Lu 3fe6994064 add additional prototypes in db.js 2023-02-27 01:09:49 +00:00
Arthur Lu 31ce8ae582 rename index.js to main.js
remove unused imports in main.js,
add function prototypes to db.js
2023-02-26 08:36:27 +00:00
Arthur Lu 0cdb152a24 fix proxmox proxy post body parsing issue
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-24 22:34:19 +00:00
Arthur Lu ec929d5f90 add post proxmox proxy endpoint
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-24 22:21:21 +00:00
Arthur Lu 6f2592dcca add no token proxmox api proxy endpoint
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-24 22:19:30 +00:00
Arthur Lu f93a3726ac streamline authentication checking,
start on instance creation

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-24 00:17:28 +00:00
Arthur Lu ab235fe581 add disk create endpoint
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-23 20:39:33 +00:00
Arthur Lu 5f1cf088a0 add instance delete path
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-21 22:35:09 +00:00
Arthur Lu bb7660c202 add changing cpu and memory values
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-17 22:17:00 +00:00
Arthur Lu a97811e516 improve return values 2023-02-13 04:34:07 +00:00
Arthur Lu 623b2087af add upid wait for actions which take significant time 2023-02-13 04:30:41 +00:00
Arthur Lu b3379bfa3c add old got as dependency (is a security vulnerability),
use axios as http request handler
2023-02-13 02:11:55 +00:00
Arthur Lu b69d248560 add start script and sample systemd service
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-10 21:58:22 +00:00
Arthur Lu 47e3cafc8b add disk attach,
add disk delete

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-10 21:41:36 +00:00
Arthur Lu 6460f6fd51 implement POST /api/disk/move
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-02 19:34:51 +00:00
Arthur Lu d479879d2c implement resize endpoint
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-30 23:19:24 +00:00
Arthur Lu f46c1165fa fix detach disk endpoint for lxc 2023-01-26 07:53:56 +00:00
Arthur Lu ee7fdad946 implement disk detach 2023-01-25 02:01:47 +00:00
Arthur Lu 0131e5ef7a add detach disk path,
add pveAPIToken config item
2023-01-25 00:48:15 +00:00
Arthur Lu 5639e01815 add vars.js.template,
use vars config items in api
2023-01-25 00:28:44 +00:00
Arthur Lu e27dd22ff6 add auth path, add token to requestPVE
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-24 20:18:29 +00:00
Arthur Lu d9f1839aab add headers and status to response 2023-01-22 23:17:38 +00:00
Arthur Lu f05726bc3f sample code for interfacing with proxmox api 2023-01-20 06:40:21 +00:00
Arthur Lu 5b03a331ea add echo path,
add cookie-parser to requirements

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-19 20:22:26 +00:00
Arthur Lu ab027bf278 update README.md
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-19 19:50:01 +00:00
Arthur Lu 5a63b26620 add simple version route
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 15:46:42 -08:00
Arthur Lu 96133f5882 add body-parser for validation,
add cors for CORS headers,
add helmet for security,
add morgan for logging

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 15:10:11 -08:00
Arthur Lu c3d50f6a30 add package.json,
add gitignore,
add express as dependency

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 14:57:42 -08:00
19 changed files with 560 additions and 1120 deletions
+1 -72
View File
@@ -19,7 +19,6 @@ In Proxmox VE, follow the following steps:
- Datastore.Allocate, Datastore.AllocateSpace, Datastore.Audit
- User.Modify
- Pool.Audit
- SDN.Use (if instances use SDN networks)
4. Add a new API Token Permission with path: `/`, select the API token created previously, and role: `proxmoxaas-api`
5. Add a new User Permission with path: `/`, select the `proxmoxaas-api` user, and role: `proxmoxaas-api`
@@ -68,74 +67,4 @@ server {
### Result
After these steps, the ProxmoxAAS Dashboard should be available and fully functional at `paas.<FQDN>` or `paas.<FQDN>/dashboard/`.
# Backends
Backend handlers are used to interface with any number and type of backend data source used to store ProxmoxAAS data. Most data involves users, groups, and membership relationships. The default backends are sufficient to run a small cluster, but additional backend handlers can be created.
## Interface
Each backend must implement the following methods:
<table>
<tr>
<td>openSession</td>
<td>opens a session to the backend by creating a session token</td>
</tr>
<tr>
<td>closeSession</td>
<td>closes a session to the backend</td>
</tr>
</table>
Additionally, backends dealing with user data may also need to implement:
<table>
<tr>
<td>addUser</td>
<td>create a user</td>
</tr>
<tr>
<td>getUser</td>
<td>retrieve user data including membership</td>
</tr>
<tr>
<td>setUser</td>
<td>modify a user</td>
</tr>
<tr>
<td>delUser</td>
<td>delete a user</td>
</tr>
<tr>
<td>addGroup</td>
<td>create a group</td>
</tr>
<tr>
<td>getGroup</td>
<td>retrieve group data including members</td>
</tr>
<tr>
<td>setGroup</td>
<td>modify group data except membership</td>
</tr>
<tr>
<td>delGroup</td>
<td>delete group</td>
</tr>
<tr>
<td>addUserToGroup</td>
<td>add user to group as member</td>
</tr>
<tr>
<td>delUserFromGroup</td>
<td>remove user from group</td>
</tr>
</table>
Not all user backends will necessarily implement all the methods fully. For example, backends which do not store group data may not need to implement the group related methods.
Specific documentation can be found in `src/backends/backends.js`.
## Multiple Interfaces
Multiple backends can be specified using the config. During a backend operation involving users, each backend method will be called in the order specified in the config. If the operation is to retrieve user data, the responses will be merged favoring the last backend called.
# Backends
+6 -21
View File
@@ -4,7 +4,6 @@
"import": "pve.js",
"config": {
"url": "https://pve.mydomain.example/api2/json",
"fabric": "http://localhost:8082",
"token": {
"user": "proxmoxaas-api",
"realm": "pam",
@@ -26,29 +25,15 @@
"paasldap": {
"import": "paasldap.js",
"config": {
"url": "http://paasldap.mydomain.example",
"realm": "ldap"
"url": "http://paasldap.mydomain.example"
}
}
},
"handlers": {
"instance": {
"pve": "pve",
"db": "localdb",
"auth": {
"pve": "pve"
},
"users": {
"realm": {
"pve": [
"localdb"
],
"ldap": [
"localdb",
"paasldap"
]
},
"any": [
"localdb",
"paasldap"
]
}
},
"application": {
@@ -78,7 +63,7 @@
"memory": {
"name": "RAM",
"type": "numeric",
"multiplier": 1,
"multiplier": 1048576,
"base": 1024,
"compact": true,
"unit": "B",
@@ -87,7 +72,7 @@
"swap": {
"name": "SWAP",
"type": "numeric",
"multiplier": 1,
"multiplier": 1048576,
"base": 1024,
"compact": true,
"unit": "B",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "proxmoxaas-api",
"version": "1.0.0",
"version": "0.0.1",
"description": "REST API for ProxmoxAAS",
"main": "src/main.js",
"type": "module",
+42 -264
View File
@@ -2,7 +2,7 @@ import path from "path";
import url from "url";
export default async () => {
global.backends = {};
const backends = {};
for (const name in global.config.backends) {
// get files and config
const target = global.config.backends[name].import;
@@ -14,11 +14,17 @@ export default async () => {
const importPath = `./${path.relative(thisPath, targetPath)}`;
// import and add to list of imported handlers
const Backend = (await import(importPath)).default;
global.backends[name] = new Backend(config);
backends[name] = new Backend(config);
console.log(`backends: initialized backend ${name} from ${importPath}`);
}
global.pve = global.backends[global.config.handlers.instance.pve];
global.userManager = new USER_BACKEND_MANAGER(global.config.handlers.users);
// assign backends to handlers by type
const handlers = global.config.handlers;
global.pve = backends[handlers.pve];
global.db = backends[handlers.db];
global.auth = handlers.auth;
Object.keys(global.auth).forEach((e) => {
global.auth[e] = backends[global.auth[e]];
});
};
/**
@@ -28,15 +34,13 @@ export default async () => {
class BACKEND {
/**
* Opens a session with the backend and creates session tokens if needed
* @param {{id: string, realm: string}} user object containing id and realm
* @param {string} password
* @returns {{ok: boolean, status: number, message: string, cookies: {name: string, value: string}[]}} response like object with list of session token objects with token name and value
* @param {{username: string, password: string}} credentials object containing username and password fields
* @returns {{ok: boolean, status: number, cookies: {name: string, value: string}[]}} response like object with list of session token objects with token name and value
*/
openSession (user, password) {
openSession (credentials) {
return {
ok: true,
status: 200,
message: "",
cookies: []
};
}
@@ -54,209 +58,85 @@ class BACKEND {
}
}
export class AtomicChange {
constructor (valid, delta, callback, status = { ok: true, status: 200, message: "" }) {
this.valid = valid;
this.delta = delta;
this.callback = callback;
this.status = status;
}
/**
* Execute the change using the saved delta using the callback function
*/
async commit () {
const res = await this.callback(this.delta);
return res;
}
}
export function doNothingCallback (delta) {
return { ok: true, status: 200, message: "" };
}
/**
* Interface for backend types that store/interact with user & group data.
* Not all backends need to implement all interface methods.
*/
class USER_BACKEND extends BACKEND {
/**
* Validate an add user operation with the following parameters.
* Returns whether the change is valid and a delta object to be used in the operation.
* Add user to backend
* @param {{id: string, realm: string}} user
* @param {Object} attributes user attributes
* @param {Object} params authentication params, usually req.cookies
* @returns {AtomicChange} atomic change object
*/
addUser (user, attributes, params) {}
addUser (user, attributes, params = null) {}
/**
* Get user from backend
* @param {{id: string, realm: string}} user
* @param {Object} params authentication params, usually req.cookies
* @returns {Object} containing user data from this backend, null if user does not exist
*/
getUser (user, params) {}
getUser (user, params = null) {}
/**
* Get all users from backend
* @param {Object} params authentication params, usually req.cookies
* @returns {Array} containing each user data from this backend
*/
getAllUsers (params) {}
/**
* Validate a set user operation with the following parameters.
* Returns whether the change is valid and a delta object to be used in the operation.
* Modify user in backend
* @param {{id: string, realm: string}} user
* @param {Object} attributes new user attributes to modify
* @param {Object} params authentication params, usually req.cookies
* @returns {AtomicChange} atomic change object
*/
setUser (user, attributes, params) {}
setUser (user, attributes, params = null) {}
/**
* Validate a delete user operation with the following parameters.
* Returns whether the change is valid and a delta object to be used in the operation.
* Delete user from backend
* @param {{id: string, realm: string}} user
* @param {Object} params authentication params, usually req.cookies
* @returns {AtomicChange} atomic change object
*/
delUser (user, params) {}
deluser (user, params = null) {}
/**
* Validate an add group operation with the following parameters.
* Returns whether the change is valid and a delta object to be used in the operation.
* @param {{id: string, realm: string}} group
* Add group to backend
* @param {{id: string}} group
* @param {Object} attributes group attributes
* @param {Object} params authentication params, usually req.cookies
* @returns {AtomicChange} atomic change object
*/
addGroup (group, attributes, params) {}
addGroup (group, attributes, params = null) {}
/**
* Get group from backend
* @param {{id: string}} group
* @param {Object} params authentication params, usually req.cookies
* @returns {Object} containing group data from this backend, null if user does not exist
*/
getGroup (group, params) {}
getGroup (group, params = null) {}
/**
* Modify group in backend
* @param {{id: string}} group
* @param {Object} attributes new group attributes to modify
* @param {Object} params authentication params, usually req.cookies
*/
setGroup (group, attributes, params = null) {}
/**
* Delete group from backend
* @param {{id: string}} group
* @param {Object} params authentication params, usually req.cookies
*/
delGroup (group, params = null) {}
/**
* Get all users from backend
* @param {Object} params authentication params, usually req.cookies
* @returns {Array} containing each group data from this backend
*/
getAllGroups (params) {}
/**
* Validate a set group operation with the following parameters.
* Returns whether the change is valid and a delta object to be used in the operation.
* @param {{id: string, realm: string}} group
* @param {Object} attributes group attributes
* @param {Object} params authentication params, usually req.cookies
* @returns {AtomicChange} atomic change object
*/
setGroup (group, attributes, params) {}
/**
* Validate a del group operation with the following parameters.
* Returns whether the change is valid and a delta object to be used in the operation.
* @param {{id: string, realm: string}} group
* @param {Object} params authentication params, usually req.cookies
* @returns {AtomicChange} atomic change object
*/
delGroup (group, attributes, params) {}
/**
* Validate an add user to group operation with the following parameters.
* Returns whether the change is valid and a delta object to be used in the operation.
* Add user to group
* @param {{id: string, realm: string}} user
* @param {{id: string}} group
* @param {Object} params authentication params, usually req.cookies
* @returns {AtomicChange} atomic change object
*/
addUserToGroup (user, group, params) {}
addUserToGroup (user, group, params = null) {}
/**
* Validate a remove user from group operation with the following parameters.
* Returns whether the change is valid and a delta object to be used in the operation.
* Remove user from group
* @param {{id: string, realm: string}} user
* @param {{id: string}} group
* @param {Object} params authentication params, usually req.cookies
* @returns {AtomicChange} atomic change object
*/
delUserFromGroup (user, group, params) {}
delUserFromGroup (user, group, params = null) {}
}
/**
* Interface for proxmox api backends.
*/
export class PVE_BACKEND extends BACKEND {
/**
* Get and return node data.
* Returns the node data or null if the node does not exist.
* @param {string} node node id
* @returns {}
*/
getNode (node) {}
/**
* Send a signal to synchronize a node after some change has been made.
* * @param {string} node node id
*/
syncNode (node) {}
/**
* Get and return instance data.
* Returns the instance data or null if the instance does not exist.
* @param {string} node node id
* @param {string} type instance type
* @param {string} vmid instance id
*/
getInstance (node, type, instance) {}
/**
* Send a signal to synchronize an instance after some change has been made.
* @param {string} node node id
* @param {string} instance instance id
*/
syncInstance (node, instance) {}
/**
* Get meta data for a specific disk. Adds info that is not normally available in a instance's config.
* @param {string} node containing the query disk.
* @param {string} instance with query disk.
* @param {string} disk name of the query disk, ie. sata0.
* @returns {Objetc} k-v pairs of specific disk data, including storage and size of unused disks.
*/
async getDisk (node, instance, disk) {}
/**
* Get meta data for a specific net. Adds info that is not normally available in a instance's config.
* @param {string} node containing the query net.
* @param {string} instance with query net.
* @param {string} netid id number of the query net, ie. 0 -> net0.
* @returns {Objetc} k-v pairs of specific net data, including rate and vlan.
*/
async getNet (node, instance, netid) {}
/**
* Get meta data for a specific device. Adds info that is not normally available in a instance's config.
* @param {string} node containing the query device.
* @param {string} instance with query device.
* @param {string} deviceid id number of the query device, ie. 0 -> pci0.
* @returns {Objetc} k-v pairs of specific device data, including name and manfacturer.
*/
async getDevice (node, instance, deviceid) {}
/**
* Get user resource data including used, available, and maximum resources.
* @param {{id: string, realm: string}} user object of user to get resource data.
* @param {Object} cookies object containing k-v store of cookies
* @returns {{used: Object, avail: Object, max: Object, resources: Object}} used, available, maximum, and resource metadata for the specified user.
*/
getUserResources (user, cookies) {}
}
export class PVE_BACKEND extends BACKEND {}
/**
* Interface for user database backends.
@@ -267,105 +147,3 @@ export class DB_BACKEND extends USER_BACKEND {}
* Interface for user auth backends.
*/
export class AUTH_BACKEND extends USER_BACKEND {}
/**
* Interface combining all user backends into a single interface
* Calling methods will also call sub handler methods
*/
class USER_BACKEND_MANAGER extends USER_BACKEND {
#config = null;
constructor (config) {
super();
this.#config = config;
}
getBackendsByUser (user) {
if (user != null) {
return this.#config.realm[user.realm];
}
else {
return null;
}
}
addUser (user, attributes, params) {}
async getUser (user, params) {
let userData = {};
for (const backend of this.#config.realm[user.realm]) {
const backendData = await global.backends[backend].getUser(user, params);
if (backendData) {
userData = { ...backendData, ...userData };
}
}
return userData;
}
async getAllUsers (params) {
const userData = {};
for (const backend of this.#config.any) {
const backendData = await global.backends[backend].getAllUsers(params);
if (backendData) {
for (const user of Object.keys(backendData)) {
userData[user] = { ...backendData[user], ...userData[user] };
}
}
}
return userData;
}
async setUser (user, attributes, params) {
const atomicChanges = [];
for (const backend of this.#config.realm[user.realm]) {
const atomicChange = await global.backends[backend].setUser(user, attributes, params);
if (atomicChange.valid === false) { // if any fails, preemptively exit
return atomicChange.status;
}
atomicChanges.push(atomicChange); // queue callback into array
}
const response = {
ok: true,
status: 200,
message: "",
allResponses: []
};
for (const atomicChange of atomicChanges) {
const atomicResponse = await atomicChange.commit();
if (atomicResponse.ok === false) {
response.ok = false;
response.status = atomicResponse.status;
response.message = atomicResponse.message;
}
response.allResponses.push(); // execute callback
}
return response;
}
delUser (user, params) {}
addGroup (group, attributes, params) {}
getGroup (group, params) {}
async getAllGroups (params) {
const groupData = {};
for (const backend of this.#config.any) {
const backendData = await global.backends[backend].getAllGroups(params);
if (backendData) {
for (const group of Object.keys(backendData)) {
groupData[group] = { ...backendData[group], ...groupData[group] };
}
}
}
return groupData;
}
setGroup (group, attributes, params) {}
delGroup (group, params) {}
addUserToGroup (user, group, params) {}
delUserFromGroup (user, group, params) {}
}
+50 -58
View File
@@ -1,6 +1,6 @@
import { readFileSync, writeFileSync } from "fs";
import { exit } from "process";
import { AtomicChange, DB_BACKEND, doNothingCallback } from "./backends.js";
import { DB_BACKEND } from "./backends.js";
export default class LocalDB extends DB_BACKEND {
#path = null;
@@ -35,82 +35,74 @@ export default class LocalDB extends DB_BACKEND {
writeFileSync(this.#path, JSON.stringify(this.#data));
}
addUser (user, attributes, params) {}
addUser (user, attributes, params = null) {
const username = `${user.id}@${user.realm}`;
attributes = attributes || this.#defaultuser;
this.#data.users[username] = attributes;
this.#save();
}
getUser (user, params) {
const requestedUser = `${user.id}@${user.realm}`;
const requestingUser = params.username; // assume checkAuth has been run, which already checks that username matches PVE token
// user can access a user's db data if they are an admin OR are requesting own data
const authorized = this.#data.users[requestingUser].cluster.admin || requestingUser === requestedUser;
if (authorized && this.#data.users[requestedUser]) {
return this.#data.users[requestedUser];
getUser (user, params = null) {
const username = `${user.id}@${user.realm}`;
if (this.#data.users[username]) {
return this.#data.users[username];
}
else {
return null;
}
}
async getAllUsers (params) {
const requestingUser = params.username; // assume checkAuth has been run, which already checks that username matches PVE token
if (this.#data.users[requestingUser].cluster.admin === true) {
return this.#data.users;
setUser (user, attributes, params = null) {
const username = `${user.id}@${user.realm}`;
if (this.#data.users[username]) {
this.#data.users[username] = attributes;
this.#save();
return true;
}
else {
return null;
return false;
}
}
setUser (user, attributes, params) {
if (attributes.resources && attributes.cluster && attributes.templates) {
const username = `${user.id}@${user.realm}`;
if (this.#data.users[username]) {
if (this.#data.users[params.username] && this.#data.users[params.username].cluster.admin) {
return new AtomicChange(false,
{
username,
attributes: {
resources: attributes.resources,
cluster: attributes.cluster,
templates: attributes.templates
}
},
(delta) => {
this.#data.users[delta.username] = delta.attributes;
this.#save();
return { ok: true, status: 200, message: "" };
},
{ ok: true, status: 200, message: "" }
);
}
else {
return new AtomicChange(false, {}, doNothingCallback, { ok: false, status: 401, message: `${params.username} is not an admin user in localdb` });
}
}
else {
// return false;
return new AtomicChange(false, {}, doNothingCallback, { ok: false, status: 400, message: `${username} was not found in localdb` });
}
delUser (user, params = null) {
const username = `${user.id}@${user.realm}`;
if (this.#data.users[username]) {
delete this.#data.users[username];
this.#save();
return true;
}
else {
return new AtomicChange(true, {}, doNothingCallback, null);
return false;
}
}
delUser (user, params) {}
// group methods not implemented because db backend does not store groups
addGroup (group, atrributes, params) {}
getGroup (group, params) {}
getAllGroups (params) {
return null;
addGroup (group, atrributes, params = null) {}
getGroup (group, params = null) {}
setGroup (group, attributes, params = null) {}
delGroup (group, params = null) {}
// assume that adding to group also adds to group's pool
addUserToGroup (user, group, params = null) {
const username = `${user.id}@${user.realm}`;
if (this.#data.users[username]) {
this.#data.users[username].cluster.pools[group.id] = true;
return true;
}
else {
return false;
}
}
setGroup (group, attributes, params) {}
delGroup (group, params) {}
// assume that adding to group also adds to group's pool
addUserToGroup (user, group, params) {}
// assume that adding to group also adds to group's pool
delUserFromGroup (user, group, params) {}
delUserFromGroup (user, group, params = null) {
const username = `${user.id}@${user.realm}`;
if (this.#data.users[username] && this.#data.users[username].cluster.pools[group.id]) {
delete this.#data.users[username].cluster.pools[group.id];
return true;
}
else {
return false;
}
}
}
+36 -109
View File
@@ -1,15 +1,13 @@
import axios from "axios";
import { AtomicChange, AUTH_BACKEND, doNothingCallback } from "./backends.js";
import { AUTH_BACKEND } from "./backends.js";
import * as setCookie from "set-cookie-parser";
export default class PAASLDAP extends AUTH_BACKEND {
#url = null;
#realm = null;
constructor (config) {
super();
this.#url = config.url;
this.#realm = config.realm;
}
/**
@@ -17,7 +15,7 @@ export default class PAASLDAP extends AUTH_BACKEND {
* @param {*} path HTTP path, prepended with the paas-LDAP API base url
* @param {*} method HTTP method
* @param {*} body body parameters and data to be sent. Optional.
* @returns {Object} HTTP response object
* @returns {Object} HTTP response object or HTTP error object.
*/
async #request (path, method, auth = null, body = null) {
const url = `${this.#url}${path}`;
@@ -41,23 +39,19 @@ export default class PAASLDAP extends AUTH_BACKEND {
return result;
}
catch (error) {
const result = error.response;
result.ok = result.status === 200;
return result;
error.ok = false;
error.status = 500;
error.data = {
error: error.code
};
return error;
}
}
#handleGenericReturn (res) {
return {
ok: res.ok,
status: res.status,
message: res.ok ? "" : res.data.error
};
}
async openSession (user, password) {
const username = user.id;
const content = { username, password };
async openSession (credentials) {
const userRealm = credentials.username.split("@").at(-1);
const uid = credentials.username.replace(`@${userRealm}`, "");
const content = { uid, password: credentials.password };
const result = await this.#request("/ticket", "POST", null, content);
if (result.ok) {
const cookies = setCookie.parse(result.headers["set-cookie"]);
@@ -67,118 +61,51 @@ export default class PAASLDAP extends AUTH_BACKEND {
return {
ok: true,
status: result.status,
message: "",
cookies
};
}
else {
return {
ok: false,
status: result.status,
message: result.data.error,
cookies: []
};
return result;
}
}
async addUser (user, attributes, params) {}
async getUser (user, params) {
if (!params) { // params required, do nothing if params are missing
return null;
}
const res = await this.#request(`/users/${user.id}`, "GET", params);
if (res.ok) { // if ok, return user data
return res.data.user;
}
else { // else return null
return null;
}
async addUser (user, attributes, params = null) {
return await this.#request(`/users/${user.id}`, "POST", params, attributes);
}
async getAllUsers (params) {
if (!params) {
return null;
}
const res = await this.#request("/users", "GET", params);
if (res.ok) { // if ok, return user data
const users = res.data.users;
const usersFormatted = {};
// label each user object by user@realm
for (const user of users) {
usersFormatted[`${user.attributes.uid}@${this.#realm}`] = user;
}
return usersFormatted;
}
else { // else return null
return null;
}
async getUser (user, params = null) {
return await this.#request(`/users/${user.id}`, "GET", params);
}
async setUser (user, attributes, params) {
if (!attributes.userpassword && !attributes.cn && attributes.sn) {
return new AtomicChange(true, {}, doNothingCallback, null); // change has no ldap attributes
}
const ldapAttributes = {};
if (attributes.userpassword) {
ldapAttributes.userpassword = attributes.userpassword;
}
if (attributes.cn) {
ldapAttributes.cn = attributes.cn;
}
if (attributes.sn) {
ldapAttributes.sn = attributes.sn;
}
return new AtomicChange(
true,
{
user,
ldapAttributes,
params
},
async (delta) => {
const res = await this.#request(`/users/${delta.user.id}`, "POST", delta.params, delta.ldapAttributes);
return this.#handleGenericReturn(res);
},
{ ok: true, status: 200, message: "" }
);
async setUser (user, attributes, params = null) {
return await this.#request(`/users/${user.id}`, "POST", params, attributes);
}
async delUser (user, params) {}
async delUser (user, params = null) {
return await this.#request(`/users/${user.id}`, "DELETE", params);
}
async addGroup (group, attributes, params) {}
async addGroup (group, attributes, params = null) {
return await this.#request(`/groups/${group.id}`, "POST", params);
}
async getGroup (group, params) {
async getGroup (group, params = null) {
return await this.#request(`/groups/${group.id}`, "GET", params);
}
async getAllGroups (params) {
if (!params) {
return null;
}
const res = await this.#request("/groups", "GET", params);
if (res.ok) { // if ok, return user data
const groups = res.data.groups;
const groupsFormatted = {};
// label each user object by user@realm
for (const group of groups) {
groupsFormatted[`${group.attributes.cn}@${this.#realm}`] = group;
}
return groupsFormatted;
}
else { // else return null
return null;
}
}
async setGroup (group, attributes, params) {
async setGroup (group, attributes, params = null) {
// not implemented, LDAP groups do not have any attributes to change
return new AtomicChange(true, {}, doNothingCallback, null); ;
}
async delGroup (group, params) {}
async delGroup (group, params = null) {
return await this.#request(`/groups/${group.id}`, "DELETE", params);
}
async addUserToGroup (user, group, params) {}
async addUserToGroup (user, group, params = null) {
return await this.#request(`/groups/${group.id}/members/${user.id}`, "POST", params);
}
async delUserFromGroup (user, group, params) {}
async delUserFromGroup (user, group, params = null) {
return await this.#request(`/groups/${group.id}/members/${user.id}`, "DELETE", params);
}
}
+67 -119
View File
@@ -5,26 +5,18 @@ export default class PVE extends PVE_BACKEND {
#pveAPIURL = null;
#pveAPIToken = null;
#pveRoot = null;
#paasFabric = null;
constructor (config) {
super();
this.#pveAPIURL = config.url;
this.#pveAPIToken = config.token;
this.#pveRoot = config.root;
this.#paasFabric = config.fabric;
}
async openSession (user, password) {
const credentials = { username: `${user.id}@${user.realm}`, password };
async openSession (credentials) {
const response = await global.pve.requestPVE("/access/ticket", "POST", null, credentials);
if (!(response.status === 200)) {
return {
ok: false,
status: response.status,
message: "Authorization failed",
cookies: []
};
return response;
}
const ticket = response.data.data.ticket;
const csrftoken = response.data.data.CSRFPreventionToken;
@@ -74,22 +66,11 @@ export default class PVE extends PVE_BACKEND {
const token = this.#pveAPIToken;
content.headers.Authorization = `PVEAPIToken=${token.user}@${token.realm}!${token.id}=${token.uuid}`;
}
else if (auth && auth.root) {
const rootauth = await global.pve.requestPVE("/access/ticket", "POST", null, this.#pveRoot);
if (!(rootauth.status === 200)) {
return rootauth.response;
}
const rootcookie = rootauth.data.data.ticket;
const rootcsrf = rootauth.data.data.CSRFPreventionToken;
content.headers.CSRFPreventionToken = rootcsrf;
content.headers.Cookie = `PVEAuthCookie=${rootcookie}; CSRFPreventionToken=${rootcsrf}`;
}
try {
return await axios.request(url, content);
}
catch (error) {
console.log(`backends: error ocuured in pve.requestPVE: ${error}`);
return error.response;
}
}
@@ -104,15 +85,11 @@ export default class PVE extends PVE_BACKEND {
*/
async handleResponse (node, result, res) {
const waitFor = delay => new Promise(resolve => setTimeout(resolve, delay));
if (result.status !== 200) {
res.status(result.status).send({ error: result.statusText });
res.end();
}
else if (result.data.data && typeof (result.data.data) === "string" && result.data.data.startsWith("UPID:")) {
if (result.data.data && typeof (result.data.data) === "string" && result.data.data.startsWith("UPID:")) {
const upid = result.data.data;
let taskStatus = await this.requestPVE(`/nodes/${node}/tasks/${upid}/status`, "GET", { token: true });
while (taskStatus.data.data.status !== "stopped") {
await waitFor(100);
await waitFor(1000);
taskStatus = await this.requestPVE(`/nodes/${node}/tasks/${upid}/status`, "GET", { token: true });
}
if (taskStatus.data.data.exitstatus === "OK") {
@@ -137,111 +114,82 @@ export default class PVE extends PVE_BACKEND {
}
/**
* Send HTTP request to PAAS Fabric
* @param {string} path HTTP path, prepended with the proxmox API base url.
* @param {string} method HTTP method.
* @param {Object} auth authentication method. Set auth.cookies with user cookies or auth.token with PVE API Token. Optional.
* @param {string} body body parameters and data to be sent. Optional.
* @returns {Object} HTTP response object or HTTP error object.
* Get meta data for a specific disk. Adds info that is not normally available in a instance's config.
* @param {string} node containing the query disk.
* @param {string} config of instance with query disk.
* @param {string} disk name of the query disk, ie. sata0.
* @returns {Objetc} k-v pairs of specific disk data, including storage and size of unused disks.
*/
async requestFabric (path, method, body = null) {
const url = `${this.#paasFabric}${path}`;
const content = {
method,
mode: "cors",
credentials: "include",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
data: body
};
async getDiskInfo (node, config, disk) {
try {
return await axios.request(url, content);
const storageID = config[disk].split(":")[0];
const volID = config[disk].split(",")[0];
const volInfo = await this.requestPVE(`/nodes/${node}/storage/${storageID}/content/${volID}`, "GET", { token: true });
volInfo.data.data.storage = storageID;
return volInfo.data.data;
}
catch (error) {
return error;
}
}
async getNode (node) {
const res = await this.requestFabric(`/nodes/${node}`, "GET");
if (res.status !== 200) {
console.error(res);
return null;
}
return res.data.node;
}
async syncNode (node) {
this.requestFabric(`/nodes/${node}/sync`, "POST");
}
async getInstance (node, instance) {
const res = await this.requestFabric(`/nodes/${node}/instances/${instance}`, "GET");
if (res.status !== 200) {
console.error(res);
return null;
}
return res.data.instance;
}
async syncInstance (node, vmid) {
this.requestFabric(`/nodes/${node}/instances/${vmid}/sync`, "POST");
}
async getDisk (node, instance, disk) {
const config = await this.getInstance(node, instance);
if (config != null && config.volumes[disk] != null) {
return config.volumes[disk];
}
else {
catch {
return null;
}
}
async getNet (node, instance, netid) {
const config = await this.getInstance(node, instance);
if (config != null && config.nets[netid] != null) {
return config.nets[netid];
/**
* Get meta data for a specific pci device. Adds info that is not normally available in a instance's config.
* @param {string} node containing the query device.
* @param {string} qid pci bus id number of the query device, ie. 89ab:cd:ef.0.
* @returns {Object} k-v pairs of specific device data, including device name and manufacturer.
*/
async getDeviceInfo (node, qid) {
try {
const result = (await this.requestPVE(`/nodes/${node}/hardware/pci`, "GET", { token: true })).data.data;
const deviceData = [];
result.forEach((element) => {
if (element.id.startsWith(qid)) {
deviceData.push(element);
}
});
deviceData.sort((a, b) => {
return a.id < b.id;
});
const device = deviceData[0];
device.subfn = structuredClone(deviceData.slice(1));
return device;
}
else {
catch {
return null;
}
}
async getDevice (node, instance, deviceid) {
const config = await this.getInstance(node, instance);
if (config != null && config.devices[deviceid] != null) {
return config.devices[deviceid];
/**
* Get available devices on specific node.
* @param {string} node to get devices from.
* @returns {Array.<Object>} array of k-v pairs of specific device data, including device name and manufacturer, which are available on the specified node.
*/
async getNodeAvailDevices (node) {
// get node pci devices
let nodeAvailPci = this.requestPVE(`/nodes/${node}/hardware/pci`, "GET", { token: true });
// for each node container, get its config and remove devices which are already used
const vms = (await this.requestPVE(`/nodes/${node}/qemu`, "GET", { token: true })).data.data;
const promises = [];
for (const vm of vms) {
promises.push(this.requestPVE(`/nodes/${node}/qemu/${vm.vmid}/config`, "GET", { token: true }));
}
else {
return null;
const configs = await Promise.all(promises);
configs.forEach((e, i) => {
configs[i] = e.data.data;
});
nodeAvailPci = (await nodeAvailPci).data.data;
for (const config of configs) {
Object.keys(config).forEach((key) => {
if (key.startsWith("hostpci")) {
const deviceID = config[key].split(",")[0];
nodeAvailPci = nodeAvailPci.filter(element => !element.id.includes(deviceID));
}
});
}
}
async getUserResources (user, cookies) {
// get user resources with vm filter
const res = await this.requestPVE("/cluster/resources?type=vm", "GET", { cookies });
if (res.status !== 200) {
return null;
}
const userPVEResources = res.data.data;
const resources = {};
// for each resource, add to the object
for (const resource of userPVEResources) {
const instance = await this.getInstance(resource.node, resource.vmid);
if (instance) {
instance.node = resource.node;
resources[resource.vmid] = instance;
}
}
return resources;
return nodeAvailPci;
}
}
-41
View File
@@ -1,41 +0,0 @@
import { Router } from "express";
export const router = Router({ mergeParams: true });
const checkAuth = global.utils.checkAuth;
/**
* GET - get all groups
* responses:
* - 200: {auth: true, groups: Array}
* - 401: {auth: false}
*/
router.get("/", async (req, res) => {
// check auth
const auth = await checkAuth(req.cookies, res);
if (!auth) {
return;
}
const groups = await global.userManager.getAllGroups(req.cookies);
res.status(200).send({ groups });
});
/**
* GET - get specific group
* request:
* - groupname: name of group to get
* responses:
* - 200: {auth: true, group: Object}
* - 401: {auth: false}
*/
router.get("/:groupname", async (req, res) => {
const params = {
groupname: req.params.groupname
};
// check auth
const auth = await checkAuth(req.cookies, res);
if (!auth) {
return;
}
const group = await global.userManager.getGroup(params.groupname, req.cookies);
res.status(200).send({ group });
});
-42
View File
@@ -1,42 +0,0 @@
import { Router } from "express";
export const router = Router({ mergeParams: true });
const checkAuth = global.utils.checkAuth;
/**
* GET - get all users
* responses:
* - 200: {auth:true, users: Array}
* - 401: {auth: false}
*/
router.get("/", async (req, res) => {
// check auth
const auth = await checkAuth(req.cookies, res);
if (!auth) {
return;
}
const users = await global.userManager.getAllUsers(req.cookies);
res.status(200).send({ users });
});
/**
* GET - get specific user
* request:
* - username: username (id@realm) of user to get
* responses:
* - 200: {auth: true, user: Object}
* - 401: {auth: false}
*/
router.get("/:username", async (req, res) => {
const params = {
username: req.params.username
};
// check auth
const auth = await checkAuth(req.cookies, res);
if (!auth) {
return;
}
const userObj = global.utils.getUserObjFromUsername(params.username);
const user = await global.userManager.getUser(userObj, req.cookies);
res.status(200).send({ user });
});
+35 -37
View File
@@ -3,8 +3,6 @@ export const router = Router({ mergeParams: true }); ;
const checkAuth = global.utils.checkAuth;
global.utils.recursiveImportRoutes(router, "", "access", import.meta.url);
/**
* GET - check authentication
* responses:
@@ -25,12 +23,12 @@ router.get("/", async (req, res) => {
class CookieFetcher {
#fetchedBackends = [];
#cookies = [];
async fetchBackends (backends, user, password) {
async fetchBackends (backends, credentials) {
for (const backend of backends) {
if (this.#fetchedBackends.indexOf(backend) === -1) {
const response = await global.backends[backend].openSession(user, password);
const response = await backend.openSession(credentials);
if (!response.ok) {
return response.message;
return false;
}
this.#cookies = this.#cookies.concat(response.cookies);
this.#fetchedBackends.push(backend);
@@ -39,7 +37,7 @@ class CookieFetcher {
continue;
}
}
return null;
return true;
}
exportCookies () {
@@ -61,33 +59,25 @@ router.post("/ticket", async (req, res) => {
username: req.body.username,
password: req.body.password
};
const domain = global.config.application.domain;
const userObj = global.utils.getUserObjFromUsername(params.username);
let backends = global.userManager.getBackendsByUser(userObj);
if (backends == null) {
res.status(401).send({ auth: false, error: `${params.username} not found in any ProxmoxAAS backends` });
return;
const userRealm = params.username.split("@").at(-1);
const backends = [global.pve, global.db];
if (userRealm in global.auth) {
backends.push(global.auth[userRealm]);
}
backends = backends.concat(["pve"]);
const cm = new CookieFetcher();
const error = await cm.fetchBackends(backends, userObj, params.password);
if (error) {
res.status(401).send({ auth: false, error });
const success = await cm.fetchBackends(backends, params);
if (!success) {
res.status(401).send({ auth: false });
return;
}
const cookies = cm.exportCookies();
let minimumExpires = Infinity;
for (const cookie of cookies) {
const expiresDate = new Date(Date.now() + cookie.expiresMSFromNow);
res.cookie(cookie.name, cookie.value, { domain, path: "/", httpOnly: true, secure: true, expires: expiresDate, sameSite: "none" });
if (cookie.expiresMSFromNow < minimumExpires) {
minimumExpires = cookie.expiresMSFromNow;
}
res.cookie(cookie.name, cookie.value, { domain, path: "/", httpOnly: true, secure: true, expires: expiresDate });
}
const expiresDate = new Date(Date.now() + minimumExpires);
res.cookie("username", params.username, { domain, path: "/", secure: true, expires: expiresDate, sameSite: "none" });
res.cookie("auth", 1, { domain, path: "/", secure: true, expires: expiresDate, sameSite: "none" });
res.cookie("username", params.username, { domain, path: "/", secure: true });
res.cookie("auth", 1, { domain, path: "/", secure: true });
res.status(200).send({ auth: true });
});
@@ -104,10 +94,10 @@ router.delete("/ticket", async (req, res) => {
const domain = global.config.application.domain;
const expire = new Date(0);
for (const cookie in req.cookies) {
res.cookie(cookie, "", { domain, path: "/", expires: expire, secure: true, sameSite: "none" });
res.cookie(cookie, "", { domain, path: "/", expires: expire });
}
await global.pve.closeSession(req.cookies);
await global.userManager.closeSession(req.cookies);
await global.db.closeSession(req.cookies);
res.status(200).send({ auth: false });
});
@@ -124,16 +114,24 @@ router.post("/password", async (req, res) => {
password: req.body.password
};
// check auth
const auth = await checkAuth(req.cookies, res);
if (!auth) {
return;
const userRealm = params.username.split("@").at(-1);
const authHandlers = global.config.handlers.auth;
const userID = params.username.replace(`@${userRealm}`, "");
const userObj = { id: userID, realm: userRealm };
if (userRealm in authHandlers) {
const handler = authHandlers[userRealm];
const newAttributes = {
userpassword: params.password
};
const response = await handler.setUser(userObj, newAttributes, req.cookies);
if (response.ok) {
res.status(response.status).send(response.data);
}
else {
res.status(response.status).send({ error: response.data.error });
}
}
else {
res.status(501).send({ error: `Auth type ${userRealm} not implemented yet.` });
}
const userObj = global.utils.getUserObjFromUsername(params.username);
const newAttributes = {
userpassword: params.password
};
const response = await global.userManager.setUser(userObj, newAttributes, req.cookies);
res.status(response.status).send(response);
});
+23 -89
View File
@@ -1,6 +1,7 @@
import { Router } from "express";
export const router = Router({ mergeParams: true });
const db = global.db;
const checkAuth = global.utils.checkAuth;
const approveResources = global.utils.approveResources;
const getUserResources = global.utils.getUserResources;
@@ -13,61 +14,6 @@ const basePath = `/:node(${nodeRegexP})/:type(${typeRegexP})/:vmid(${vmidRegexP}
global.utils.recursiveImportRoutes(router, basePath, "cluster", import.meta.url);
/**
* GET - get all available cluster pools
* returns only pool IDs
* responses:
* - 200: List of pools
* - PVE error
*/
router.get("/pools", async (req, res) => {
// check auth
const auth = await checkAuth(req.cookies, res);
if (!auth) {
return;
}
const allPools = await global.pve.requestPVE("/pools", "GET", { token: true });
if (allPools.status === 200) {
const allPoolsIDs = Array.from(allPools.data.data, (x) => x.poolid);
res.status(allPools.status).send({ pools: allPoolsIDs });
res.end();
}
else {
res.status(allPools.status).send({ error: allPools.statusMessage });
res.end();
}
});
/**
* GET - get all available cluster nodes
* uses existing user permissions without elevation
* returns only node IDs
* responses:
* - 200: List of nodes
* - PVE error
*/
router.get("/nodes", async (req, res) => {
// check auth
const auth = await checkAuth(req.cookies, res);
if (!auth) {
return;
}
const allNodes = await global.pve.requestPVE("/nodes", "GET", { cookies: req.cookies });
if (allNodes.status === 200) {
const allNodesIDs = Array.from(allNodes.data.data, (x) => x.node);
res.status(allNodes.status).send({ nodes: allNodesIDs });
res.end();
}
else {
res.status(allNodes.status).send({ error: allNodes.statusMessage });
res.end();
}
});
/**
* GET - get available pcie devices given node and user
* request:
@@ -83,14 +29,16 @@ router.get(`/:node(${nodeRegexP})/pci`, async (req, res) => {
node: req.params.node
};
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
const userRealm = req.cookies.username.split("@").at(-1);
const userID = req.cookies.username.replace(`@${userRealm}`, "");
const userObj = { id: userID, realm: userRealm };
// check auth
const auth = await checkAuth(req.cookies, res);
if (!auth) {
return;
}
const userNodes = (await global.userManager.getUser(userObj, req.cookies)).cluster.nodes;
const userNodes = db.getUser(userObj).cluster.nodes;
if (userNodes[params.node] !== true) {
res.status(401).send({ auth: false, path: params.node });
res.end();
@@ -98,28 +46,13 @@ router.get(`/:node(${nodeRegexP})/pci`, async (req, res) => {
}
// get remaining user resources
const userAvailPci = (await getUserResources(req, userObj)).pci.nodes[params.node];
if (userAvailPci === undefined) { // user has no avaliable devices on this node, so send an empty list
res.status(200).send([]);
res.end();
}
else {
// get node avail devices
const node = await global.pve.getNode(params.node);
let availableDevices = [];
// get each device and filter out only thise which are not reserved
for (const device of Object.values(node.devices)) {
if (device.reserved === false) {
availableDevices.push(device);
}
}
// further filter out only devices which the user has access to
availableDevices = availableDevices.filter(nodeAvail => userAvailPci.some((userAvail) => {
return nodeAvail.device_name && nodeAvail.device_name.includes(userAvail.match) && userAvail.avail > 0;
}));
res.status(200).send(availableDevices);
res.end();
}
// get node avail devices
let nodeAvailPci = await global.pve.getNodeAvailDevices(params.node, req.cookies);
nodeAvailPci = nodeAvailPci.filter(nodeAvail => userAvailPci.some((userAvail) => {
return nodeAvail.device_name && nodeAvail.device_name.includes(userAvail.match) && userAvail.avail > 0;
}));
res.status(200).send(nodeAvailPci);
res.end();
});
/**
@@ -150,7 +83,9 @@ router.post(`${basePath}/resources`, async (req, res) => {
boot: req.body.boot
};
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
const userRealm = req.cookies.username.split("@").at(-1);
const userID = req.cookies.username.replace(`@${userRealm}`, "");
const userObj = { id: userID, realm: userRealm };
// check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
@@ -159,13 +94,13 @@ router.post(`${basePath}/resources`, async (req, res) => {
return;
}
// get current config
const instance = await global.pve.getInstance(params.node, params.vmid);
const currentConfig = await global.pve.requestPVE(`/nodes/${params.node}/${params.type}/${params.vmid}/config`, "GET", { token: true });
const request = {
cores: Number(params.cores) - Number(instance.cores),
memory: Number(params.memory) - Number(instance.memory)
cores: Number(params.cores) - Number(currentConfig.data.data.cores),
memory: Number(params.memory) - Number(currentConfig.data.data.memory)
};
if (params.type === "lxc") {
request.swap = Number(params.swap) - Number(instance.swap);
request.swap = Number(params.swap) - Number(currentConfig.data.data.swap);
}
else if (params.type === "qemu") {
request.cpu = params.proctype;
@@ -189,7 +124,6 @@ router.post(`${basePath}/resources`, async (req, res) => {
// commit action
const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action);
await global.pve.handleResponse(params.node, result, res);
await global.pve.syncInstance(params.node, params.vmid);
});
/**
@@ -231,7 +165,9 @@ router.post(`${basePath}/create`, async (req, res) => {
rootfssize: req.body.rootfssize
};
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
const userRealm = req.cookies.username.split("@").at(-1);
const userID = req.cookies.username.replace(`@${userRealm}`, "");
const userObj = { id: userID, realm: userRealm };
// check auth
const auth = await checkAuth(req.cookies, res);
@@ -239,7 +175,7 @@ router.post(`${basePath}/create`, async (req, res) => {
return;
}
// get user db config
const user = await global.userManager.getUser(userObj, req.cookies);
const user = await db.getUser(userObj);
const vmid = Number.parseInt(params.vmid);
const vmidMin = user.cluster.vmid.min;
const vmidMax = user.cluster.vmid.max;
@@ -311,7 +247,6 @@ router.post(`${basePath}/create`, async (req, res) => {
// commit action
const result = await global.pve.requestPVE(`/nodes/${params.node}/${params.type}`, "POST", { token: true }, action);
await global.pve.handleResponse(params.node, result, res);
await global.pve.syncNode(params.node);
});
/**
@@ -340,5 +275,4 @@ router.delete(`${basePath}/delete`, async (req, res) => {
// commit action
const result = await global.pve.requestPVE(vmpath, "DELETE", { token: true });
await global.pve.handleResponse(params.node, result, res);
await global.pve.syncNode(params.node);
});
+40 -31
View File
@@ -31,9 +31,10 @@ router.post("/:disk/detach", async (req, res) => {
if (!auth) {
return;
}
// get current config
const config = (await global.pve.requestPVE(`${vmpath}/config`, "GET", { cookies: req.cookies })).data.data;
// disk must exist
const disk = await global.pve.getDisk(params.node, params.vmid, params.disk);
if (!disk) {
if (!config[params.disk]) {
res.status(500).send({ error: `Disk ${params.disk} does not exist.` });
res.end();
return;
@@ -48,7 +49,6 @@ router.post("/:disk/detach", async (req, res) => {
const method = params.type === "qemu" ? "POST" : "PUT";
const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action);
await global.pve.handleResponse(params.node, result, res);
await global.pve.syncInstance(params.node, params.vmid);
});
/**
@@ -80,30 +80,29 @@ router.post("/:disk/attach", async (req, res) => {
if (!auth) {
return;
}
// get current config
const config = (await global.pve.requestPVE(`${vmpath}/config`, "GET", { cookies: req.cookies })).data.data;
// disk must exist
const disk = await global.pve.getDisk(params.node, params.vmid, `unused${params.source}`);
if (!disk) {
res.status(500).send({ error: `Requested disk unused${params.source} does not exist.` });
if (!config[`unused${params.source}`]) {
res.status(403).send({ error: `Requested disk unused${params.source} does not exist.` });
res.end();
return;
}
// target disk must be allowed according to source disk's storage options
const diskConfig = await global.pve.getDiskInfo(params.node, config, `unused${params.source}`); // get target disk
const resourceConfig = global.config.resources;
if (!resourceConfig[disk.storage].disks.some(diskPrefix => params.disk.startsWith(diskPrefix))) {
res.status(500).send({ error: `Requested target ${params.disk} is not in allowed list [${resourceConfig[disk.storage].disks}].` });
if (!resourceConfig[diskConfig.storage].disks.some(diskPrefix => params.disk.startsWith(diskPrefix))) {
res.status(500).send({ error: `Requested target ${params.disk} is not in allowed list [${resourceConfig[diskConfig.storage].disks}].` });
res.end();
return;
}
// setup action using source disk info from vm config
const action = {};
action[params.disk] = disk.volid;
action[params.disk] = config[`unused${params.source}`];
const method = params.type === "qemu" ? "POST" : "PUT";
// commit action
const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action);
await global.pve.handleResponse(params.node, result, res);
await global.pve.syncInstance(params.node, params.vmid);
});
/**
@@ -131,7 +130,9 @@ router.post("/:disk/resize", async (req, res) => {
size: req.body.size
};
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
const userRealm = req.cookies.username.split("@").at(-1);
const userID = req.cookies.username.replace(`@${userRealm}`, "");
const userObj = { id: userID, realm: userRealm };
// check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
@@ -139,15 +140,17 @@ router.post("/:disk/resize", async (req, res) => {
if (!auth) {
return;
}
// get current config
const config = (await global.pve.requestPVE(`${vmpath}/config`, "GET", { cookies: req.cookies })).data.data;
// check disk existence
const disk = await global.pve.getDisk(params.node, params.vmid, params.disk); // get target disk
if (!disk) { // exit if disk does not exist
const diskConfig = await global.pve.getDiskInfo(params.node, config, params.disk); // get target disk
if (!diskConfig) { // exit if disk does not exist
res.status(500).send({ error: `requested disk ${params.disk} does not exist.` });
res.end();
return;
}
// setup request
const storage = disk.storage; // get the storage
const storage = diskConfig.storage; // get the storage
const request = {};
request[storage] = Number(params.size * 1024 ** 3); // setup request object
// check request approval
@@ -160,7 +163,6 @@ router.post("/:disk/resize", async (req, res) => {
const action = { disk: params.disk, size: `+${params.size}G` };
const result = await global.pve.requestPVE(`${vmpath}/resize`, "PUT", { token: true }, action);
await global.pve.handleResponse(params.node, result, res);
await global.pve.syncInstance(params.node, params.vmid);
});
/**
@@ -190,7 +192,9 @@ router.post("/:disk/move", async (req, res) => {
delete: req.body.delete
};
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
const userRealm = req.cookies.username.split("@").at(-1);
const userID = req.cookies.username.replace(`@${userRealm}`, "");
const userObj = { id: userID, realm: userRealm };
// check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
@@ -198,15 +202,17 @@ router.post("/:disk/move", async (req, res) => {
if (!auth) {
return;
}
// get current config
const config = (await global.pve.requestPVE(`${vmpath}/config`, "GET", { cookies: req.cookies })).data.data;
// check disk existence
const disk = await global.pve.getDisk(params.node, params.vmid, params.disk); // get target disk
if (!disk) { // exit if disk does not exist
const diskConfig = await global.pve.getDiskInfo(params.node, config, params.disk); // get target disk
if (!diskConfig) { // exit if disk does not exist
res.status(500).send({ error: `requested disk ${params.disk} does not exist.` });
res.end();
return;
}
// setup request
const size = parseInt(disk.size); // get source disk size
const size = parseInt(diskConfig.size); // get source disk size
const dstStorage = params.storage; // get destination storage
const request = {};
if (!params.delete) { // if not delete, then request storage, otherwise it is net 0
@@ -230,7 +236,6 @@ router.post("/:disk/move", async (req, res) => {
// commit action
const result = await global.pve.requestPVE(`${vmpath}/${route}`, "POST", { token: true }, action);
await global.pve.handleResponse(params.node, result, res);
await global.pve.syncInstance(params.node, params.vmid);
});
/**
@@ -260,10 +265,11 @@ router.delete("/:disk/delete", async (req, res) => {
if (!auth) {
return;
}
// get current config
const config = (await global.pve.requestPVE(`${vmpath}/config`, "GET", { cookies: req.cookies })).data.data;
// disk must exist
const disk = await global.pve.getDisk(params.node, params.vmid, params.disk);
if (!disk) {
res.status(500).send({ error: `Disk ${params.disk} does not exist.` });
if (!config[params.disk]) {
res.status(403).send({ error: `Requested disk unused${params.source} does not exist.` });
res.end();
return;
}
@@ -279,7 +285,6 @@ router.delete("/:disk/delete", async (req, res) => {
// commit action
const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action);
await global.pve.handleResponse(params.node, result, res);
await global.pve.syncInstance(params.node, params.vmid);
});
/**
@@ -309,17 +314,22 @@ router.post("/:disk/create", async (req, res) => {
size: req.body.size,
iso: req.body.iso
};
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
const userRealm = req.cookies.username.split("@").at(-1);
const userID = req.cookies.username.replace(`@${userRealm}`, "");
const userObj = { id: userID, realm: userRealm };
// check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
const auth = await checkAuth(req.cookies, res, vmpath);
if (!auth) {
return;
}
// get current config
const config = (await global.pve.requestPVE(`${vmpath}/config`, "GET", { cookies: req.cookies })).data.data;
// disk must not exist
const disk = await global.pve.getDisk(params.node, params.vmid, params.disk);
if (disk) {
res.status(500).send({ error: `Disk ${params.disk} does already exists.` });
if (config[params.disk]) {
res.status(403).send({ error: `Requested disk ${params.disk} already exists.` });
res.end();
return;
}
@@ -357,5 +367,4 @@ router.post("/:disk/create", async (req, res) => {
// commit action
const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action);
await global.pve.handleResponse(params.node, result, res);
await global.pve.syncInstance(params.node, params.vmid);
});
+27 -35
View File
@@ -1,6 +1,7 @@
import { Router } from "express";
export const router = Router({ mergeParams: true }); ;
const db = global.db;
const checkAuth = global.utils.checkAuth;
const approveResources = global.utils.approveResources;
@@ -26,16 +27,14 @@ router.post("/:netid/create", async (req, res) => {
node: req.params.node,
type: req.params.type,
vmid: req.params.vmid,
netid: Number(req.params.netid.replace("net", "")),
netid: req.params.netid.replace("net", ""),
rate: req.body.rate,
name: req.body.name
};
// check netid is a valid number
if (isNaN(params.netid)) {
res.status(500).send({ error: `Network interface id must be a number, got ${req.params.netid}.` });
res.end();
return;
}
const userRealm = req.cookies.username.split("@").at(-1);
const userID = req.cookies.username.replace(`@${userRealm}`, "");
const userObj = { id: userID, realm: userRealm };
// check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
@@ -43,9 +42,10 @@ router.post("/:netid/create", async (req, res) => {
if (!auth) {
return;
}
// get current config
const currentConfig = await global.pve.requestPVE(`/nodes/${params.node}/${params.type}/${params.vmid}/config`, "GET", { token: true });
// net interface must not exist
const net = await global.pve.getNet(params.node, params.vmid, params.netid);
if (net) {
if (currentConfig.data.data[`net${params.netid}`]) {
res.status(500).send({ error: `Network interface net${params.netid} already exists.` });
res.end();
return;
@@ -59,14 +59,13 @@ router.post("/:netid/create", async (req, res) => {
network: Number(params.rate)
};
// check resource approval
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
if (!await approveResources(req, userObj, request, params.node)) {
res.status(500).send({ request, error: `Could not fulfil network request of ${params.rate}MB/s.` });
res.end();
return;
}
// setup action
const nc = (await global.userManager.getUser(userObj, req.cookies)).templates.network[params.type];
const nc = db.getUser(userObj).templates.network[params.type];
const action = {};
if (params.type === "lxc") {
action[`net${params.netid}`] = `name=${params.name},bridge=${nc.bridge},ip=${nc.ip},ip6=${nc.ip6},tag=${nc.vlan},type=${nc.type},rate=${params.rate}`;
@@ -78,7 +77,6 @@ router.post("/:netid/create", async (req, res) => {
// commit action
const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action);
await global.pve.handleResponse(params.node, result, res);
await global.pve.syncInstance(params.node, params.vmid);
});
/**
@@ -102,33 +100,34 @@ router.post("/:netid/modify", async (req, res) => {
node: req.params.node,
type: req.params.type,
vmid: req.params.vmid,
netid: Number(req.params.netid.replace("net", "")),
netid: req.params.netid.replace("net", ""),
rate: req.body.rate
};
// check netid is a valid number
if (isNaN(params.netid)) {
res.status(500).send({ error: `Network interface id must be a number, got ${req.params.netid}.` });
res.end();
return;
}
const userRealm = req.cookies.username.split("@").at(-1);
const userID = req.cookies.username.replace(`@${userRealm}`, "");
const userObj = { id: userID, realm: userRealm };
// check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
const auth = await checkAuth(req.cookies, res, vmpath);
if (!auth) {
return;
}
// get current config
const currentConfig = await global.pve.requestPVE(`/nodes/${params.node}/${params.type}/${params.vmid}/config`, "GET", { token: true });
// net interface must already exist
const net = await global.pve.getNet(params.node, params.vmid, params.netid);
if (!net) {
if (!currentConfig.data.data[`net${params.netid}`]) {
res.status(500).send({ error: `Network interface net${params.netid} does not exist.` });
res.end();
return;
}
const currentNetworkConfig = currentConfig.data.data[`net${params.netid}`];
const currentNetworkRate = currentNetworkConfig.split("rate=")[1].split(",")[0];
const request = {
network: Number(params.rate) - Number(net.rate)
network: Number(params.rate) - Number(currentNetworkRate)
};
// check resource approval
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
if (!await approveResources(req, userObj, request, params.node)) {
res.status(500).send({ request, error: `Could not fulfil network request of ${params.rate}MB/s.` });
res.end();
@@ -136,12 +135,11 @@ router.post("/:netid/modify", async (req, res) => {
}
// setup action
const action = {};
action[`net${params.netid}`] = net.value.replace(`rate=${net.rate}`, `rate=${params.rate}`);
action[`net${params.netid}`] = currentNetworkConfig.replace(`rate=${currentNetworkRate}`, `rate=${params.rate}`);
const method = params.type === "qemu" ? "POST" : "PUT";
// commit action
const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action);
await global.pve.handleResponse(params.node, result, res);
await global.pve.syncInstance(params.node, params.vmid);
});
/**
@@ -163,23 +161,18 @@ router.delete("/:netid/delete", async (req, res) => {
node: req.params.node,
type: req.params.type,
vmid: req.params.vmid,
netid: Number(req.params.netid.replace("net", ""))
netid: req.params.netid.replace("net", "")
};
// check netid is a valid number
if (isNaN(params.netid)) {
res.status(500).send({ error: `Network interface id must be a number, got ${req.params.netid}.` });
res.end();
return;
}
// check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
const auth = await checkAuth(req.cookies, res, vmpath);
if (!auth) {
return;
}
// get current config
const currentConfig = await global.pve.requestPVE(`/nodes/${params.node}/${params.type}/${params.vmid}/config`, "GET", { token: true });
// net interface must already exist
const net = await global.pve.getNet(params.node, params.vmid, params.netid);
if (!net) {
if (!currentConfig.data.data[`net${params.netid}`]) {
res.status(500).send({ error: `Network interface net${params.netid} does not exist.` });
res.end();
return;
@@ -189,5 +182,4 @@ router.delete("/:netid/delete", async (req, res) => {
// commit action
const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, { delete: `net${params.netid}` });
await global.pve.handleResponse(params.node, result, res);
await global.pve.syncInstance(params.node, params.vmid);
});
+85 -71
View File
@@ -22,28 +22,30 @@ router.get("/:hostpci", async (req, res) => {
node: req.params.node,
type: req.params.type,
vmid: req.params.vmid,
hostpci: Number(req.params.hostpci.replace("hostpci", ""))
hostpci: req.params.hostpci.replace("hostpci", "")
};
// check hostpci is a valid number
if (isNaN(params.hostpci)) {
res.status(500).send({ error: `Hostpci id must be a number, got ${req.params.hostpci}.` });
res.end();
return;
}
// check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
const auth = await checkAuth(req.cookies, res, vmpath);
if (!auth) {
return;
}
// get device
const device = await global.pve.getDevice(params.node, params.vmid, params.hostpci);
if (!device) {
// check device is in instance config
const config = (await global.pve.requestPVE(`${vmpath}/config`, "GET", { cookies: req.cookies })).data.data;
if (!config[`hostpci${params.hostpci}`]) {
res.status(500).send({ error: `Could not find hostpci${params.hostpci} in ${params.vmid}.` });
res.end();
return;
}
const device = config[`hostpci${params.hostpci}`].split(",")[0];
// get node's pci devices
const deviceData = await global.pve.getDeviceInfo(params.node, device);
if (!deviceData) {
res.status(500).send({ error: `Could not find hostpci${params.hostpci}=${device} in ${params.node}.` });
res.end();
return;
}
res.status(200).send(device);
res.status(200).send(deviceData);
res.end();
});
@@ -68,16 +70,15 @@ router.post("/:hostpci/modify", async (req, res) => {
node: req.params.node,
type: req.params.type,
vmid: req.params.vmid,
hostpci: Number(req.params.hostpci.replace("hostpci", "")),
hostpci: req.params.hostpci.replace("hostpci", ""),
device: req.body.device,
pcie: req.body.pcie
};
// check hostpci is a valid number
if (isNaN(params.hostpci)) {
res.status(500).send({ error: `Hostpci id must be a number, got ${req.params.hostpci}.` });
res.end();
return;
}
const userRealm = req.cookies.username.split("@").at(-1);
const userID = req.cookies.username.replace(`@${userRealm}`, "");
const userObj = { id: userID, realm: userRealm };
// check if type is qemu
if (params.type !== "qemu") {
res.status(500).send({ error: "Type must be qemu (vm)." });
@@ -92,33 +93,28 @@ router.post("/:hostpci/modify", async (req, res) => {
}
// force all functions
params.device = params.device.split(".")[0];
// device must exist to be modified
const existingDevice = await global.pve.getDevice(params.node, params.vmid, params.hostpci);
if (!existingDevice) {
// get instance config to check if device has not changed
const config = (await global.pve.requestPVE(`/nodes/${params.node}/${params.type}/${params.vmid}/config`, "GET", { token: true })).data.data;
const currentDeviceData = await global.pve.getDeviceInfo(params.node, config[`hostpci${params.hostpci}`].split(",")[0]);
if (!currentDeviceData) {
res.status(500).send({ error: `No device in hostpci${params.hostpci}.` });
res.end();
return;
}
// only check user and node availability if base id is different, we do the split in case of existing partial-function hostpci
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
if (existingDevice.device_id.split(".")[0] !== params.device) {
// only check user and node availability if base id is different
if (currentDeviceData.id.split(".")[0] !== params.device) {
// setup request
const node = await global.pve.getNode(params.node);
const requestedDevice = node.devices[`${params.device}`];
const request = { pci: requestedDevice.device_name };
if (!requestedDevice) {
res.status(500).send({ request, error: `Could not fulfil request for ${params.device}.` });
res.end();
return;
}
const deviceData = await global.pve.getDeviceInfo(params.node, params.device);
const request = { pci: deviceData.device_name };
// check resource approval
if (!await approveResources(req, userObj, request, params.node)) {
res.status(500).send({ request, error: `Could not fulfil request for ${requestedDevice.device_name}.` });
res.status(500).send({ request, error: `Could not fulfil request for ${deviceData.device_name}.` });
res.end();
return;
}
// check node availability
if (!Object.values(node.devices).some(element => element.device_id.split(".")[0] === params.device && element.reserved === false)) {
const nodeAvailPci = await global.pve.getNodeAvailDevices(params.node, req.cookies);
if (!nodeAvailPci.some(element => element.id.split(".")[0] === params.device)) {
res.status(500).send({ error: `Device ${params.device} is already in use on ${params.node}.` });
res.end();
return;
@@ -128,9 +124,18 @@ router.post("/:hostpci/modify", async (req, res) => {
const action = {};
action[`hostpci${params.hostpci}`] = `${params.device},pcie=${params.pcie}`;
// commit action
const result = await global.pve.requestPVE(`${vmpath}/config`, "POST", { root: true }, action);
const rootauth = await global.pve.requestPVE("/access/ticket", "POST", null, global.config.backends.pve.config.root);
if (!(rootauth.status === 200)) {
res.status(rootauth.status).send({ auth: false, error: "API could not authenticate as root user." });
res.end();
return;
}
const rootcookies = {
PVEAuthCookie: rootauth.data.data.ticket,
CSRFPreventionToken: rootauth.data.data.CSRFPreventionToken
};
const result = await global.pve.requestPVE(`${vmpath}/config`, "POST", { cookies: rootcookies }, action);
await global.pve.handleResponse(params.node, result, res);
await global.pve.syncNode(params.node);
});
/**
@@ -147,22 +152,20 @@ router.post("/:hostpci/modify", async (req, res) => {
* - 500: {request: Object, error: string}
* - 500: PVE Task Object
*/
router.post("/:hostpci/create", async (req, res) => {
router.post("/create", async (req, res) => {
req.params = Object.assign({}, req.routeparams, req.params);
const params = {
node: req.params.node,
type: req.params.type,
vmid: req.params.vmid,
hostpci: Number(req.params.hostpci.replace("hostpci", "")),
device: req.body.device,
pcie: req.body.pcie
};
// check hostpci is a valid number
if (isNaN(params.hostpci)) {
res.status(500).send({ error: `Hostpci id must be a number, got ${req.params.hostpci}.` });
res.end();
return;
}
const userRealm = req.cookies.username.split("@").at(-1);
const userID = req.cookies.username.replace(`@${userRealm}`, "");
const userObj = { id: userID, realm: userRealm };
// check if type is qemu
if (params.type !== "qemu") {
res.status(500).send({ error: "Type must be qemu (vm)." });
@@ -177,38 +180,46 @@ router.post("/:hostpci/create", async (req, res) => {
}
// force all functions
params.device = params.device.split(".")[0];
// device must not exist to be added
const existingDevice = await global.pve.getDevice(params.node, params.vmid, params.hostpci);
if (existingDevice) {
res.status(500).send({ error: `Existing device in hostpci${params.hostpci}.` });
res.end();
return;
// get instance config to find next available hostpci slot
const config = global.pve.requestPVE(`/nodes/${params.node}/${params.type}/${params.vmid}/config`, "GET", { cookies: params.cookies });
let hostpci = 0;
while (config[`hostpci${hostpci}`]) {
hostpci++;
}
// setup request
const node = await global.pve.getNode(params.node);
const requestedDevice = node.devices[`${params.device}`];
const request = { pci: requestedDevice.device_name };
const deviceData = await global.pve.getDeviceInfo(params.node, params.device);
const request = {
pci: deviceData.device_name
};
// check resource approval
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
if (!await approveResources(req, userObj, request, params.node)) {
res.status(500).send({ request, error: `Could not fulfil request for ${requestedDevice.device_name}.` });
res.status(500).send({ request, error: `Could not fulfil request for ${deviceData.device_name}.` });
res.end();
return;
}
// check node availability
// const node = await global.pve.getNode(params.node);
if (!Object.values(node.devices).some(element => element.device_id.split(".")[0] === params.device && element.reserved === false)) {
const nodeAvailPci = await global.pve.getNodeAvailDevices(params.node, req.cookies);
if (!nodeAvailPci.some(element => element.id.split(".")[0] === params.device)) {
res.status(500).send({ error: `Device ${params.device} is already in use on ${params.node}.` });
res.end();
return;
}
// setup action
const action = {};
action[`hostpci${params.hostpci}`] = `${params.device},pcie=${params.pcie}`;
action[`hostpci${hostpci}`] = `${params.device},pcie=${params.pcie}`;
// commit action
const result = await global.pve.requestPVE(`${vmpath}/config`, "POST", { root: true }, action);
const rootauth = await global.pve.requestPVE("/access/ticket", "POST", null, global.config.backends.pve.config.root);
if (!(rootauth.status === 200)) {
res.status(rootauth.status).send({ auth: false, error: "API could not authenticate as root user." });
res.end();
return;
}
const rootcookies = {
PVEAuthCookie: rootauth.data.data.ticket,
CSRFPreventionToken: rootauth.data.data.CSRFPreventionToken
};
const result = await global.pve.requestPVE(`${vmpath}/config`, "POST", { cookies: rootcookies }, action);
await global.pve.handleResponse(params.node, result, res);
await global.pve.syncNode(params.node);
});
/**
@@ -230,14 +241,8 @@ router.delete("/:hostpci/delete", async (req, res) => {
node: req.params.node,
type: req.params.type,
vmid: req.params.vmid,
hostpci: Number(req.params.hostpci.replace("hostpci", ""))
hostpci: req.params.hostpci.replace("hostpci", "")
};
// check hostpci is a valid number
if (isNaN(params.hostpci)) {
res.status(500).send({ error: `Hostpci id must be a number, got ${req.params.hostpci}.` });
res.end();
return;
}
// check if type is qemu
if (params.type !== "qemu") {
res.status(500).send({ error: "Type must be qemu (vm)." });
@@ -251,8 +256,8 @@ router.delete("/:hostpci/delete", async (req, res) => {
return;
}
// check device is in instance config
const device = global.pve.getDevice(params.node, params.vmid, params.hostpci);
if (!device) {
const config = (await global.pve.requestPVE(`${vmpath}/config`, "GET", { cookies: req.cookies })).data.data;
if (!config[`hostpci${params.hostpci}`]) {
res.status(500).send({ error: `Could not find hostpci${params.hostpci} in ${params.vmid}.` });
res.end();
return;
@@ -260,7 +265,16 @@ router.delete("/:hostpci/delete", async (req, res) => {
// setup action
const action = { delete: `hostpci${params.hostpci}` };
// commit action, need to use root user here because proxmox api only allows root to modify hostpci for whatever reason
const result = await global.pve.requestPVE(`${vmpath}/config`, "POST", { root: true }, action);
const rootauth = await global.pve.requestPVE("/access/ticket", "POST", null, global.config.backends.pve.config.root);
if (!(rootauth.status === 200)) {
res.status(rootauth.status).send({ auth: false, error: "API could not authenticate as root user." });
res.end();
return;
}
const rootcookies = {
PVEAuthCookie: rootauth.data.data.ticket,
CSRFPreventionToken: rootauth.data.data.CSRFPreventionToken
};
const result = await global.pve.requestPVE(`${vmpath}/config`, "POST", { cookies: rootcookies }, action);
await global.pve.handleResponse(params.node, result, res);
await global.pve.syncNode(params.node);
});
+1 -3
View File
@@ -20,9 +20,7 @@ router.get("/config/:key", async (req, res) => {
const allowKeys = ["resources"];
if (allowKeys.includes(params.key)) {
const config = global.config;
const result = {};
result[params.key] = config[params.key];
res.status(200).send(result);
res.status(200).send(config[params.key]);
}
else {
res.status(401).send({ auth: false, error: `User is not authorized to access /global/config/${params.key}.` });
+6 -4
View File
@@ -51,7 +51,7 @@ if (schemes.hash.enabled) {
if (!auth) {
return;
}
// get current cluster resources - do not use fabric here because fabric is not always updated to changes like up/down state changes
// get current cluster resources
const status = (await global.pve.requestPVE("/cluster/resources", "GET", { cookies: req.cookies })).data.data;
// filter out just state information of resources that are needed
const state = extractClusterState(status, resourceTypes);
@@ -165,10 +165,12 @@ if (schemes.interrupt.enabled) {
socket.destroy();
}
else {
wsServer.handleUpgrade(req, socket, head, async (socket) => {
wsServer.handleUpgrade(req, socket, head, (socket) => {
// get the user pools
const userObj = global.utils.getUserObjFromUsername(cookies.username);
const pools = Object.keys((await global.userManager.getUser(userObj, cookies)).cluster.pools);
const userRealm = cookies.username.split("@").at(-1);
const userID = cookies.username.replace(`@${userRealm}`, "");
const userObj = { id: userID, realm: userRealm };
const pools = Object.keys(global.db.getUser(userObj).cluster.pools);
// emit the connection to initialize socket
wsServer.emit("connection", socket, cookies.username, pools);
});
+9 -8
View File
@@ -3,6 +3,7 @@ export const router = Router({ mergeParams: true }); ;
const config = global.config;
const checkAuth = global.utils.checkAuth;
const getUserResources = global.utils.getUserResources;
/**
* GET - get db user resource information including allocated, free, and maximum resource values along with resource metadata
@@ -11,19 +12,17 @@ const checkAuth = global.utils.checkAuth;
* - 401: {auth: false}
*/
router.get("/dynamic/resources", async (req, res) => {
const params = {
username: req.cookies.username
};
// check auth
const auth = await checkAuth(req.cookies, res);
if (!auth) {
return;
}
const userObj = global.utils.getUserObjFromUsername(params.username);
const userRealm = req.cookies.username.split("@").at(-1);
const userID = req.cookies.username.replace(`@${userRealm}`, "");
const userObj = { id: userID, realm: userRealm };
const resources = await global.utils.getUserResources(req, userObj);
const resources = await getUserResources(req, userObj);
res.status(200).send(resources);
});
@@ -41,7 +40,9 @@ router.get("/config/:key", async (req, res) => {
key: req.params.key
};
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
const userRealm = req.cookies.username.split("@").at(-1);
const userID = req.cookies.username.replace(`@${userRealm}`, "");
const userObj = { id: userID, realm: userRealm };
// check auth
const auth = await checkAuth(req.cookies, res);
@@ -50,7 +51,7 @@ router.get("/config/:key", async (req, res) => {
}
const allowKeys = ["resources", "cluster"];
if (allowKeys.includes(params.key)) {
const config = await global.userManager.getUser(userObj, req.cookies);
const config = global.db.getUser(userObj);
res.status(200).send(config[params.key]);
}
else {
+129 -104
View File
@@ -15,34 +15,18 @@ import { exit } from "process";
export async function checkAuth (cookies, res, vmpath = null) {
let auth = false;
const userObj = getUserObjFromUsername(cookies.username); // check if username exists and is valid
if (!userObj) {
res.status(401).send({ auth, path: vmpath ? `${vmpath}/config` : "/version", error: "Username was missing or invalid." });
const userRealm = cookies.username.split("@").at(-1);
const userID = cookies.username.replace(`@${userRealm}`, "");
const userObj = { id: userID, realm: userRealm };
if (global.db.getUser(userObj) === null) {
auth = false;
res.status(401).send({ auth, path: vmpath ? `${vmpath}/config` : "/version", error: `User ${cookies.username} not found in localdb.` });
res.end();
return false;
}
if (!cookies.PVEAuthCookie) { // check if PVE token exists
res.status(401).send({ auth, path: vmpath ? `${vmpath}/config` : "/version", error: "Token was missing or invalid." });
res.end();
return false;
}
const pveTicket = cookies.PVEAuthCookie;
const result = await global.pve.requestPVE("/access/ticket", "POST", null, { username: cookies.username, password: pveTicket });
if (result.status !== 200) { // check if PVE token is valid by using /access/ticket to validate ticket with Proxmox
res.status(401).send({ auth, path: vmpath ? `${vmpath}/config` : "/version", error: "Username did not match token." });
res.end();
return false;
}
if ((await global.userManager.getUser(userObj, cookies)) === null) { // check if user exists in database
res.status(401).send({ auth, path: vmpath ? `${vmpath}/config` : "/version", error: `User ${cookies.username} not found in database.` });
res.end();
return false;
}
if (vmpath) { // if a path is specified, check the permissions on the path
if (vmpath) {
const result = await global.pve.requestPVE(`/${vmpath}/config`, "GET", { cookies });
auth = result.status === 200;
}
@@ -55,10 +39,72 @@ export async function checkAuth (cookies, res, vmpath = null) {
res.status(401).send({ auth, path: vmpath ? `${vmpath}/config` : "/version", error: "User token did not pass authentication check." });
res.end();
}
return auth;
}
/**
* Get the full config of an instance, including searching disk information.
* @param {Object} req ProxmoxAAS API request object.
* @param {Object} instance to get config as object containing node, type, and id.
* @param {Array} diskprefixes Array containing prefixes for disks.
* @returns
*/
async function getFullInstanceConfig (req, instance, diskprefixes) {
const config = (await global.pve.requestPVE(`/nodes/${instance.node}/${instance.type}/${instance.vmid}/config`, "GET", { cookies: req.cookies })).data.data;
// fetch all instance disk and device data concurrently
const promises = [];
const mappings = [];
for (const key in config) {
if (diskprefixes.some(prefix => key.startsWith(prefix))) {
promises.push(global.pve.getDiskInfo(instance.node, config, key));
mappings.push(key);
}
else if (key.startsWith("hostpci")) {
promises.push(global.pve.getDeviceInfo(instance.node, config[key].split(",")[0]));
mappings.push(key);
}
}
const results = await Promise.all(promises);
results.forEach((e, i) => {
const key = mappings[i];
config[key] = e;
});
config.node = instance.node;
return config;
}
/**
* Get all configs for every instance owned by the user. Uses the expanded config data from getFullInstanceConfig.
* @param {Object} req ProxmoxAAS API request object.
* @param {Object} dbResources data about application resources, to indicate which resources are tracked.
* @returns {Object} k-v pairs of resource name and used amounts
*/
async function getAllInstanceConfigs (req, diskprefixes) {
// get the basic resources list
const resources = (await global.pve.requestPVE("/cluster/resources", "GET", { cookies: req.cookies })).data.data;
// filter resources by their type, we only want lxc and qemu
const instances = [];
for (const resource of resources) {
if (resource.type === "lxc" || resource.type === "qemu") {
instances.push(resource);
}
}
// get all instance configs, also include detailed disk and device info
const promises = [];
const mappings = [];
for (let i = 0; i < instances.length; i++) {
const instance = instances[i];
const config = getFullInstanceConfig(req, instance, diskprefixes);
promises.push(config);
mappings.push(i);
}
const configs = await Promise.all(promises);
return configs;
}
/**
* Get user resource data including used, available, and maximum resources.
* @param {Object} req ProxmoxAAS API request object.
@@ -67,7 +113,17 @@ export async function checkAuth (cookies, res, vmpath = null) {
*/
export async function getUserResources (req, user) {
const dbResources = global.config.resources;
const userResources = (await global.userManager.getUser(user, req.cookies)).resources;
const userResources = global.db.getUser(user).resources;
// setup disk prefixes object
const diskprefixes = [];
for (const resourceName of Object.keys(dbResources)) {
if (dbResources[resourceName].type === "storage") {
for (const diskPrefix of dbResources[resourceName].disks) {
diskprefixes.push(diskPrefix);
}
}
}
// setup the user resource object with used and avail for each resource and each resource pool
// also add a total counter for each resource (only used for display, not used to check requests)
@@ -121,12 +177,10 @@ export async function getUserResources (req, user) {
}
}
const configs = await global.pve.getUserResources(user, req.cookies);
const configs = await getAllInstanceConfigs(req, diskprefixes);
for (const vmid in configs) {
const config = configs[vmid];
for (const config of configs) {
const nodeName = config.node;
// count basic numeric resources
for (const resourceName of Object.keys(config)) {
// numeric resource type
if (resourceName in dbResources && dbResources[resourceName].type === "numeric") {
@@ -144,73 +198,61 @@ export async function getUserResources (req, user) {
userResources[resourceName].total.used += val;
userResources[resourceName].total.avail -= val;
}
}
// count disk resources in volumes
for (const diskid in config.volumes) {
const disk = config.volumes[diskid];
const storage = disk.storage;
const size = disk.size;
// only process disk if its storage is in the user resources to be counted
if (storage in userResources) {
// if the instance's node is restricted by this resource, add it to the instance's used value
if (nodeName in userResources[storage].nodes) {
userResources[storage].nodes[nodeName].used += size;
userResources[storage].nodes[nodeName].avail -= size;
else if (diskprefixes.some(prefix => resourceName.startsWith(prefix))) {
const diskInfo = config[resourceName];
if (diskInfo) { // only count if disk exists
const val = Number(diskInfo.size);
const storage = diskInfo.storage;
// if the instance's node is restricted by this resource, add it to the instance's used value
if (nodeName in userResources[storage].nodes) {
userResources[storage].nodes[nodeName].used += val;
userResources[storage].nodes[nodeName].avail -= val;
}
// otherwise add the resource to the global pool
else {
userResources[storage].global.used += val;
userResources[storage].global.avail -= val;
}
userResources[storage].total.used += val;
userResources[storage].total.avail -= val;
}
// otherwise add the resource to the global pool
else {
userResources[storage].global.used += size;
userResources[storage].global.avail -= size;
}
userResources[storage].total.used += size;
userResources[storage].total.avail -= size;
}
}
// count net resources in nets
for (const netid in config.nets) {
const net = config.nets[netid];
const rate = net.rate;
if (userResources.network) {
else if (resourceName.startsWith("net") && config[resourceName].includes("rate=")) { // only count net instances with a rate limit
const val = Number(config[resourceName].split("rate=")[1].split(",")[0]);
// if the instance's node is restricted by this resource, add it to the instance's used value
if (nodeName in userResources.network.nodes) {
userResources.network.nodes[nodeName].used += rate;
userResources.network.nodes[nodeName].avail -= rate;
userResources.network.nodes[nodeName].used += val;
userResources.network.nodes[nodeName].avail -= val;
}
// otherwise add the resource to the global pool
else {
userResources.network.global.used += rate;
userResources.network.global.avail -= rate;
userResources.network.global.used += val;
userResources.network.global.avail -= val;
}
userResources.network.total.used += rate;
userResources.network.total.avail -= rate;
userResources.network.total.used += val;
userResources.network.total.avail -= val;
}
}
// count pci device resources in devices
for (const deviceid in config.devices) {
const device = config.devices[deviceid];
const name = device.device_name;
// if the node has a node specific rule, add it there
if (nodeName in userResources.pci.nodes) {
const index = userResources.pci.nodes[nodeName].findIndex((availEelement) => name.includes(availEelement.match));
if (index >= 0) {
userResources.pci.nodes[nodeName][index].used++;
userResources.pci.nodes[nodeName][index].avail--;
else if (resourceName.startsWith("hostpci")) {
const deviceInfo = config[resourceName];
if (deviceInfo) { // only count if device exists
const deviceName = deviceInfo.device_name;
// if the instance's node is restricted by this resource, add it to the instance's used value
if (nodeName in userResources.pci.nodes) {
const index = userResources.pci.nodes[nodeName].findIndex((availEelement) => deviceName.includes(availEelement.match));
userResources.pci.nodes[nodeName][index].used++;
userResources.pci.nodes[nodeName][index].avail--;
}
// otherwise add the resource to the global pool
else {
const index = userResources.pci.global.findIndex((availEelement) => deviceName.includes(availEelement.match));
userResources.pci.global[index].used++;
userResources.pci.global[index].avail--;
}
const index = userResources.pci.total.findIndex((availEelement) => deviceName.includes(availEelement.match));
userResources.pci.total[index].used++;
userResources.pci.total[index].avail--;
}
}
// otherwise try to add the resource to the global pool
else {
const index = userResources.pci.global.findIndex((availEelement) => name.includes(availEelement.match));
if (index >= 0) { // device resource is in the user's global list then increment it by 1
userResources.pci.global[index].used++;
userResources.pci.global[index].avail--;
}
}
// finally, add the device to the total map
const index = userResources.pci.total.findIndex((availEelement) => name.includes(availEelement.match));
if (index >= 0) {
userResources.pci.total[index].used++;
userResources.pci.total[index].avail--;
}
}
}
@@ -289,7 +331,7 @@ export function getTimeLeft (timeout) {
/**
* Recursively import routes from target folder.
* @param {Object} router or app object.
* @param {string} baseroute base route of imported modules starting from the current path.
* @param {string} baseroute API route for each imported module.
* @param {string} target folder to import modules.
* @param {string} from source folder of calling module, optional for imports from the same base directory.
*/
@@ -320,20 +362,3 @@ export function readJSONFile (path) {
exit(1);
}
};
/**
*
* @param {*} username
* @returns {Object | null} user object containing username and realm or null if user does not exist
*/
export function getUserObjFromUsername (username) {
if (username) {
const userRealm = username.split("@").at(-1);
const userID = username.replace(`@${userRealm}`, "");
const userObj = { id: userID, realm: userRealm };
return userObj;
}
else {
return null;
}
}
+2 -11
View File
@@ -25,13 +25,13 @@
},
"memory": {
"global": {
"max": 137438953472
"max": 131072
},
"nodes": {}
},
"swap": {
"global": {
"max": 137438953472
"max": 131072
},
"nodes": {}
},
@@ -72,7 +72,6 @@
}
},
"cluster": {
"admin": false,
"nodes": {
"example-node-0": true,
"example-node-1": true,
@@ -103,20 +102,12 @@
"value": "host",
"resource": null
},
"machine": {
"value": "q35",
"resource": null
},
"net0": {
"value": "virtio,bridge=vmbr0,tag=10,rate=1000",
"resource": {
"name": "network",
"amount": 1000
}
},
"scsihw": {
"value": "virtio-scsi-single",
"resource": null
}
}
},