From 91c95bd764b4754ba058cd24db0237b2c967515d Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Tue, 31 Dec 2024 12:18:55 +0100 Subject: [PATCH 01/16] feat(api/routes/accounting): add server_consumption module with bare handler Signed-off-by: Sandro-Alessio Gierens --- api/src/routes/accounting/mod.rs | 6 ++++- .../accounting/server_consumption/get.rs | 25 +++++++++++++++++++ .../accounting/server_consumption/mod.rs | 9 +++++++ 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 api/src/routes/accounting/server_consumption/get.rs create mode 100644 api/src/routes/accounting/server_consumption/mod.rs diff --git a/api/src/routes/accounting/mod.rs b/api/src/routes/accounting/mod.rs index 7a626f89..d5cb5911 100644 --- a/api/src/routes/accounting/mod.rs +++ b/api/src/routes/accounting/mod.rs @@ -3,7 +3,11 @@ use actix_web::Scope; mod server_state; use server_state::server_states_scope; +mod server_consumption; +use server_consumption::server_consumption_scope; pub fn accounting_scope() -> Scope { - scope("/accounting").service(server_states_scope()) + scope("/accounting") + .service(server_states_scope()) + .service(server_consumption_scope()) } diff --git a/api/src/routes/accounting/server_consumption/get.rs b/api/src/routes/accounting/server_consumption/get.rs new file mode 100644 index 00000000..b9cc0c3a --- /dev/null +++ b/api/src/routes/accounting/server_consumption/get.rs @@ -0,0 +1,25 @@ +use crate::error::OptionApiError; +use actix_web::web::{Data, ReqData}; +use actix_web::HttpResponse; +use anyhow::Context; +use lrzcc_wire::user::{Project, User}; +use sqlx::MySqlPool; + +#[tracing::instrument(name = "server_consumption")] +pub async fn server_consumption( + user: ReqData, + // TODO: not necessary? + project: ReqData, + db_pool: Data, + // TODO: is the ValidationError variant ever used? +) -> Result { + let mut _transaction = db_pool + .begin() + .await + .context("Failed to begin transaction")?; + _transaction + .commit() + .await + .context("Failed to commit transaction")?; + Ok(HttpResponse::Ok().content_type("application/json").json(())) +} diff --git a/api/src/routes/accounting/server_consumption/mod.rs b/api/src/routes/accounting/server_consumption/mod.rs new file mode 100644 index 00000000..7fa7cc4d --- /dev/null +++ b/api/src/routes/accounting/server_consumption/mod.rs @@ -0,0 +1,9 @@ +use actix_web::web::{get, scope}; +use actix_web::Scope; + +mod get; +use get::server_consumption; + +pub fn server_consumption_scope() -> Scope { + scope("/serverconsumption").route("/", get().to(server_consumption)) +} From 8066e758308a455b5c6c66b6fb8300a21903e20f Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Tue, 31 Dec 2024 12:33:26 +0100 Subject: [PATCH 02/16] feat(wire/accounting): add ServerConsumptionParams Signed-off-by: Sandro-Alessio Gierens --- wire/src/accounting/server_consumption.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/wire/src/accounting/server_consumption.rs b/wire/src/accounting/server_consumption.rs index d2b1feaf..fa889b3a 100644 --- a/wire/src/accounting/server_consumption.rs +++ b/wire/src/accounting/server_consumption.rs @@ -1,3 +1,4 @@ +use chrono::{DateTime, FixedOffset}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -22,3 +23,14 @@ pub struct ServerConsumptionAll { pub total: ServerConsumptionFlavors, pub projects: HashMap, } + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ServerConsumptionParams { + pub begin: Option>, + pub end: Option>, + pub server: Option, + pub user: Option, + pub project: Option, + pub all: bool, + pub detail: bool, +} From 9afa15a36f6224a3f228c8556d2b3c0147ef0312 Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Tue, 31 Dec 2024 12:34:05 +0100 Subject: [PATCH 03/16] refactor(lib/accounting): revise ServerConsumptionRequest to use ServerConsumptionParams Signed-off-by: Sandro-Alessio Gierens --- lib/src/accounting/server_consumption.rs | 181 +++++++++++++---------- 1 file changed, 102 insertions(+), 79 deletions(-) diff --git a/lib/src/accounting/server_consumption.rs b/lib/src/accounting/server_consumption.rs index e6e575a7..205258b6 100644 --- a/lib/src/accounting/server_consumption.rs +++ b/lib/src/accounting/server_consumption.rs @@ -3,11 +3,11 @@ use crate::error::ApiError; use anyhow::Context; use chrono::{DateTime, FixedOffset}; use lrzcc_wire::accounting::{ - ServerConsumptionAll, ServerConsumptionFlavors, ServerConsumptionProject, - ServerConsumptionServer, ServerConsumptionUser, + ServerConsumptionAll, ServerConsumptionFlavors, ServerConsumptionParams, + ServerConsumptionProject, ServerConsumptionServer, ServerConsumptionUser, }; use reqwest::blocking::Client; -use reqwest::{Method, StatusCode, Url}; +use reqwest::{Method, StatusCode}; use std::fmt::Debug; use std::rc::Rc; @@ -16,13 +16,7 @@ pub struct ServerConsumptionRequest { url: String, client: Rc, - begin: Option>, - end: Option>, - server: Option, - user: Option, - project: Option, - all: bool, - detail: bool, + params: ServerConsumptionParams, } impl ServerConsumptionRequest { @@ -31,46 +25,25 @@ impl ServerConsumptionRequest { url: url.to_string(), client: Rc::clone(client), - begin: None, - end: None, - server: None, - user: None, - project: None, - all: false, - detail: false, + params: ServerConsumptionParams { + begin: None, + end: None, + server: None, + user: None, + project: None, + all: false, + detail: false, + }, } } - fn params(&self) -> Vec<(&str, String)> { - let mut params = Vec::new(); - if let Some(begin) = self.begin { - params.push(("begin", begin.to_rfc3339().to_string())); - } - if let Some(end) = self.end { - params.push(("end", end.to_rfc3339().to_string())); - } - if let Some(server) = &self.server { - params.push(("server", server.to_string())); - } else if let Some(user) = self.user { - params.push(("user", user.to_string())); - } else if let Some(project) = self.project { - params.push(("project", project.to_string())); - } else if self.all { - params.push(("all", "1".to_string())); - } - if self.detail { - params.push(("detail", "1".to_string())); - } - params - } - pub fn begin(&mut self, begin: DateTime) -> &mut Self { - self.begin = Some(begin); + self.params.begin = Some(begin); self } pub fn end(&mut self, end: DateTime) -> &mut Self { - self.end = Some(end); + self.params.end = Some(end); self } @@ -78,10 +51,15 @@ impl ServerConsumptionRequest { &mut self, server: &str, ) -> Result { - self.server = Some(server.to_string()); - self.detail = false; - let url = Url::parse_with_params(self.url.as_str(), self.params()) - .context("Could not parse URL GET parameters.")?; + self.params.server = Some(server.to_string()); + self.params.detail = false; + let params = serde_urlencoded::to_string(&self.params) + .context("Failed to envode URL parameters")?; + let url = if params.is_empty() { + self.url.clone() + } else { + format!("{}?{}", self.url, params) + }; request( &self.client, Method::GET, @@ -95,10 +73,15 @@ impl ServerConsumptionRequest { &mut self, server: &str, ) -> Result { - self.server = Some(server.to_string()); - self.detail = true; - let url = Url::parse_with_params(self.url.as_str(), self.params()) - .context("Could not parse URL GET parameters.")?; + self.params.server = Some(server.to_string()); + self.params.detail = true; + let params = serde_urlencoded::to_string(&self.params) + .context("Failed to envode URL parameters")?; + let url = if params.is_empty() { + self.url.clone() + } else { + format!("{}?{}", self.url, params) + }; request( &self.client, Method::GET, @@ -112,10 +95,15 @@ impl ServerConsumptionRequest { &mut self, user: u32, ) -> Result { - self.user = Some(user); - self.detail = false; - let url = Url::parse_with_params(self.url.as_str(), self.params()) - .context("Could not parse URL GET parameters.")?; + self.params.user = Some(user); + self.params.detail = false; + let params = serde_urlencoded::to_string(&self.params) + .context("Failed to envode URL parameters")?; + let url = if params.is_empty() { + self.url.clone() + } else { + format!("{}?{}", self.url, params) + }; request( &self.client, Method::GET, @@ -129,10 +117,15 @@ impl ServerConsumptionRequest { &mut self, user: u32, ) -> Result { - self.user = Some(user); - self.detail = true; - let url = Url::parse_with_params(self.url.as_str(), self.params()) - .context("Could not parse URL GET parameters.")?; + self.params.user = Some(user); + self.params.detail = true; + let params = serde_urlencoded::to_string(&self.params) + .context("Failed to envode URL parameters")?; + let url = if params.is_empty() { + self.url.clone() + } else { + format!("{}?{}", self.url, params) + }; request( &self.client, Method::GET, @@ -146,10 +139,15 @@ impl ServerConsumptionRequest { &mut self, project: u32, ) -> Result { - self.project = Some(project); - self.detail = false; - let url = Url::parse_with_params(self.url.as_str(), self.params()) - .context("Could not parse URL GET parameters.")?; + self.params.project = Some(project); + self.params.detail = false; + let params = serde_urlencoded::to_string(&self.params) + .context("Failed to envode URL parameters")?; + let url = if params.is_empty() { + self.url.clone() + } else { + format!("{}?{}", self.url, params) + }; request( &self.client, Method::GET, @@ -163,10 +161,15 @@ impl ServerConsumptionRequest { &mut self, project: u32, ) -> Result { - self.project = Some(project); - self.detail = true; - let url = Url::parse_with_params(self.url.as_str(), self.params()) - .context("Could not parse URL GET parameters.")?; + self.params.project = Some(project); + self.params.detail = true; + let params = serde_urlencoded::to_string(&self.params) + .context("Failed to envode URL parameters")?; + let url = if params.is_empty() { + self.url.clone() + } else { + format!("{}?{}", self.url, params) + }; request( &self.client, Method::GET, @@ -177,10 +180,15 @@ impl ServerConsumptionRequest { } pub fn all(&mut self) -> Result { - self.all = true; - self.detail = false; - let url = Url::parse_with_params(self.url.as_str(), self.params()) - .context("Could not parse URL GET parameters.")?; + self.params.all = true; + self.params.detail = false; + let params = serde_urlencoded::to_string(&self.params) + .context("Failed to envode URL parameters")?; + let url = if params.is_empty() { + self.url.clone() + } else { + format!("{}?{}", self.url, params) + }; request( &self.client, Method::GET, @@ -191,10 +199,15 @@ impl ServerConsumptionRequest { } pub fn all_detail(&mut self) -> Result { - self.all = true; - self.detail = true; - let url = Url::parse_with_params(self.url.as_str(), self.params()) - .context("Could not parse URL GET parameters.")?; + self.params.all = true; + self.params.detail = true; + let params = serde_urlencoded::to_string(&self.params) + .context("Failed to envode URL parameters")?; + let url = if params.is_empty() { + self.url.clone() + } else { + format!("{}?{}", self.url, params) + }; request( &self.client, Method::GET, @@ -205,8 +218,13 @@ impl ServerConsumptionRequest { } pub fn mine(&mut self) -> Result { - let url = Url::parse_with_params(self.url.as_str(), self.params()) - .context("Could not parse URL GET parameters.")?; + let params = serde_urlencoded::to_string(&self.params) + .context("Failed to envode URL parameters")?; + let url = if params.is_empty() { + self.url.clone() + } else { + format!("{}?{}", self.url, params) + }; request( &self.client, Method::GET, @@ -217,9 +235,14 @@ impl ServerConsumptionRequest { } pub fn mine_detail(&mut self) -> Result { - self.detail = true; - let url = Url::parse_with_params(self.url.as_str(), self.params()) - .context("Could not parse URL GET parameters.")?; + self.params.detail = true; + let params = serde_urlencoded::to_string(&self.params) + .context("Failed to envode URL parameters")?; + let url = if params.is_empty() { + self.url.clone() + } else { + format!("{}?{}", self.url, params) + }; request( &self.client, Method::GET, From 518adaa591160654bdef4c4cd172f568c3aa612c Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Tue, 31 Dec 2024 12:43:12 +0100 Subject: [PATCH 04/16] feat(api/routes/accounting): add params to server_consumption handler Signed-off-by: Sandro-Alessio Gierens --- api/src/routes/accounting/server_consumption/get.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/src/routes/accounting/server_consumption/get.rs b/api/src/routes/accounting/server_consumption/get.rs index b9cc0c3a..2b04e3c8 100644 --- a/api/src/routes/accounting/server_consumption/get.rs +++ b/api/src/routes/accounting/server_consumption/get.rs @@ -1,7 +1,8 @@ use crate::error::OptionApiError; -use actix_web::web::{Data, ReqData}; +use actix_web::web::{Data, Query, ReqData}; use actix_web::HttpResponse; use anyhow::Context; +use lrzcc_wire::accounting::ServerConsumptionParams; use lrzcc_wire::user::{Project, User}; use sqlx::MySqlPool; @@ -11,6 +12,7 @@ pub async fn server_consumption( // TODO: not necessary? project: ReqData, db_pool: Data, + params: Query, // TODO: is the ValidationError variant ever used? ) -> Result { let mut _transaction = db_pool From 594761cc61fdc5af070036726540c4016356730c Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Tue, 31 Dec 2024 13:51:22 +0100 Subject: [PATCH 05/16] feat(api/database/accounting): select_ordered_server_states_by_server_begin_and_end_from_db Signed-off-by: Sandro-Alessio Gierens --- api/src/database/accounting/server_state.rs | 163 ++++++++++++++++++++ 1 file changed, 163 insertions(+) diff --git a/api/src/database/accounting/server_state.rs b/api/src/database/accounting/server_state.rs index e2d5f065..1d229892 100644 --- a/api/src/database/accounting/server_state.rs +++ b/api/src/database/accounting/server_state.rs @@ -532,3 +532,166 @@ pub async fn insert_server_state_into_db( } Ok(id) } + +#[tracing::instrument( + name = "select_ordered_server_states_by_server_begin_and_end_from_db", + skip(transaction) +)] +pub async fn select_ordered_server_states_by_server_begin_and_end_from_db( + transaction: &mut Transaction<'_, MySql>, + server_id: String, + begin: Option>, + end: Option>, +) -> Result, UnexpectedOnlyError> { + let result = match (begin, end) { + (None, None) => { + let query = sqlx::query!( + r#" + SELECT + s.id as id, + s.begin as begin, + s.end as end, + ss.instance_id as instance_id, + ss.instance_name as instance_name, + f.id as flavor, + f.name as flavor_name, + ss.status as status, + u.id as user, + u.name as username + FROM + accounting_state as s, + accounting_serverstate as ss, + resources_flavor as f, + user_user as u + WHERE + ss.flavor_id = f.id AND + ss.user_id = u.id AND + ss.state_ptr_id = s.id AND + ss.instance_id = ? + ORDER BY s.id + "#, + server_id + ); + transaction.fetch_all(query).await + } + (Some(begin), None) => { + let query = sqlx::query!( + r#" + SELECT + s.id as id, + s.begin as begin, + s.end as end, + ss.instance_id as instance_id, + ss.instance_name as instance_name, + f.id as flavor, + f.name as flavor_name, + ss.status as status, + u.id as user, + u.name as username + FROM + accounting_state as s, + accounting_serverstate as ss, + resources_flavor as f, + user_user as u + WHERE + ss.flavor_id = f.id AND + ss.user_id = u.id AND + ss.state_ptr_id = s.id AND + ss.instance_id = ? AND + (s.end > ? OR s.end IS NULL) + ORDER BY s.id + "#, + server_id, + begin + ); + transaction.fetch_all(query).await + } + (None, Some(end)) => { + let query = sqlx::query!( + r#" + SELECT + s.id as id, + s.begin as begin, + s.end as end, + ss.instance_id as instance_id, + ss.instance_name as instance_name, + f.id as flavor, + f.name as flavor_name, + ss.status as status, + u.id as user, + u.name as username + FROM + accounting_state as s, + accounting_serverstate as ss, + resources_flavor as f, + user_user as u + WHERE + ss.flavor_id = f.id AND + ss.user_id = u.id AND + ss.state_ptr_id = s.id AND + ss.instance_id = ? AND + s.begin < ? + ORDER BY s.id + "#, + server_id, + end + ); + transaction.fetch_all(query).await + } + (Some(begin), Some(end)) => { + let query = sqlx::query!( + r#" + SELECT + s.id as id, + s.begin as begin, + s.end as end, + ss.instance_id as instance_id, + ss.instance_name as instance_name, + f.id as flavor, + f.name as flavor_name, + ss.status as status, + u.id as user, + u.name as username + FROM + accounting_state as s, + accounting_serverstate as ss, + resources_flavor as f, + user_user as u + WHERE + ss.flavor_id = f.id AND + ss.user_id = u.id AND + ss.state_ptr_id = s.id AND + ss.instance_id = ? AND + (s.end > ? OR s.end IS NULL) AND + s.begin < ? + ORDER BY s.id + "#, + server_id, + begin, + end + ); + transaction.fetch_all(query).await + } + }; + let rows = result + .context("Failed to execute select query")? + .into_iter() + .map(|r| ServerStateRow::from_row(&r)) + .collect::, _>>() + .context("Failed to convert row to server state")? + .into_iter() + .map(|r| ServerState { + id: r.id, + begin: r.begin.fixed_offset(), + end: r.end.map(|end| end.fixed_offset()), + instance_id: r.instance_id, + instance_name: r.instance_name, + flavor: r.flavor, + flavor_name: r.flavor_name, + status: r.status, + user: r.user, + username: r.username, + }) + .collect::>(); + Ok(rows) +} From a643115e120c73344861c017def9adabd30022f1 Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Tue, 31 Dec 2024 13:51:59 +0100 Subject: [PATCH 06/16] chore(sqlx): update offline query data Signed-off-by: Sandro-Alessio Gierens --- ...57bc6292f32d93bf4910bd152ab4a61546b29.json | 114 ++++++++++++++++++ ...c2b4aeb666ddb94f8dc85fe400a5720326cb2.json | 114 ++++++++++++++++++ ...209f5b6bd0d4fe3833d5979c5cc61a2c50363.json | 114 ++++++++++++++++++ ...7c6246aa723ad89d79d4c897971837c8d46fa.json | 114 ++++++++++++++++++ 4 files changed, 456 insertions(+) create mode 100644 .sqlx/query-23e9e2dc07212447a95ac29d3f957bc6292f32d93bf4910bd152ab4a61546b29.json create mode 100644 .sqlx/query-67ae224810d7e79dbb302dca86dc2b4aeb666ddb94f8dc85fe400a5720326cb2.json create mode 100644 .sqlx/query-847854addb1b261f6645b98b7a7209f5b6bd0d4fe3833d5979c5cc61a2c50363.json create mode 100644 .sqlx/query-f6223bbb88161ed7db11def83b87c6246aa723ad89d79d4c897971837c8d46fa.json diff --git a/.sqlx/query-23e9e2dc07212447a95ac29d3f957bc6292f32d93bf4910bd152ab4a61546b29.json b/.sqlx/query-23e9e2dc07212447a95ac29d3f957bc6292f32d93bf4910bd152ab4a61546b29.json new file mode 100644 index 00000000..c9473937 --- /dev/null +++ b/.sqlx/query-23e9e2dc07212447a95ac29d3f957bc6292f32d93bf4910bd152ab4a61546b29.json @@ -0,0 +1,114 @@ +{ + "db_name": "MySQL", + "query": "\n SELECT\n s.id as id,\n s.begin as begin,\n s.end as end,\n ss.instance_id as instance_id,\n ss.instance_name as instance_name,\n f.id as flavor,\n f.name as flavor_name,\n ss.status as status,\n u.id as user,\n u.name as username\n FROM\n accounting_state as s,\n accounting_serverstate as ss,\n resources_flavor as f,\n user_user as u\n WHERE\n ss.flavor_id = f.id AND\n ss.user_id = u.id AND\n ss.state_ptr_id = s.id AND\n ss.instance_id = ? AND\n (s.end > ? OR s.end IS NULL) AND\n s.begin < ?\n ORDER BY s.id\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": { + "type": "Long", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT", + "max_size": 11 + } + }, + { + "ordinal": 1, + "name": "begin", + "type_info": { + "type": "Datetime", + "flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE", + "max_size": 26 + } + }, + { + "ordinal": 2, + "name": "end", + "type_info": { + "type": "Datetime", + "flags": "BINARY", + "max_size": 26 + } + }, + { + "ordinal": 3, + "name": "instance_id", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "max_size": 144 + } + }, + { + "ordinal": 4, + "name": "instance_name", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "max_size": 1020 + } + }, + { + "ordinal": 5, + "name": "flavor", + "type_info": { + "type": "LongLong", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT", + "max_size": 20 + } + }, + { + "ordinal": 6, + "name": "flavor_name", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE", + "max_size": 256 + } + }, + { + "ordinal": 7, + "name": "status", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "max_size": 72 + } + }, + { + "ordinal": 8, + "name": "user", + "type_info": { + "type": "Long", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT", + "max_size": 11 + } + }, + { + "ordinal": 9, + "name": "username", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE", + "max_size": 1020 + } + } + ], + "parameters": { + "Right": 3 + }, + "nullable": [ + false, + false, + true, + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "23e9e2dc07212447a95ac29d3f957bc6292f32d93bf4910bd152ab4a61546b29" +} diff --git a/.sqlx/query-67ae224810d7e79dbb302dca86dc2b4aeb666ddb94f8dc85fe400a5720326cb2.json b/.sqlx/query-67ae224810d7e79dbb302dca86dc2b4aeb666ddb94f8dc85fe400a5720326cb2.json new file mode 100644 index 00000000..930a0cef --- /dev/null +++ b/.sqlx/query-67ae224810d7e79dbb302dca86dc2b4aeb666ddb94f8dc85fe400a5720326cb2.json @@ -0,0 +1,114 @@ +{ + "db_name": "MySQL", + "query": "\n SELECT\n s.id as id,\n s.begin as begin,\n s.end as end,\n ss.instance_id as instance_id,\n ss.instance_name as instance_name,\n f.id as flavor,\n f.name as flavor_name,\n ss.status as status,\n u.id as user,\n u.name as username\n FROM\n accounting_state as s,\n accounting_serverstate as ss,\n resources_flavor as f,\n user_user as u\n WHERE\n ss.flavor_id = f.id AND\n ss.user_id = u.id AND\n ss.state_ptr_id = s.id AND\n ss.instance_id = ? AND\n s.begin < ?\n ORDER BY s.id\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": { + "type": "Long", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT", + "max_size": 11 + } + }, + { + "ordinal": 1, + "name": "begin", + "type_info": { + "type": "Datetime", + "flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE", + "max_size": 26 + } + }, + { + "ordinal": 2, + "name": "end", + "type_info": { + "type": "Datetime", + "flags": "BINARY", + "max_size": 26 + } + }, + { + "ordinal": 3, + "name": "instance_id", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "max_size": 144 + } + }, + { + "ordinal": 4, + "name": "instance_name", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "max_size": 1020 + } + }, + { + "ordinal": 5, + "name": "flavor", + "type_info": { + "type": "LongLong", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT", + "max_size": 20 + } + }, + { + "ordinal": 6, + "name": "flavor_name", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE", + "max_size": 256 + } + }, + { + "ordinal": 7, + "name": "status", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "max_size": 72 + } + }, + { + "ordinal": 8, + "name": "user", + "type_info": { + "type": "Long", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT", + "max_size": 11 + } + }, + { + "ordinal": 9, + "name": "username", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE", + "max_size": 1020 + } + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false, + false, + true, + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "67ae224810d7e79dbb302dca86dc2b4aeb666ddb94f8dc85fe400a5720326cb2" +} diff --git a/.sqlx/query-847854addb1b261f6645b98b7a7209f5b6bd0d4fe3833d5979c5cc61a2c50363.json b/.sqlx/query-847854addb1b261f6645b98b7a7209f5b6bd0d4fe3833d5979c5cc61a2c50363.json new file mode 100644 index 00000000..212d8936 --- /dev/null +++ b/.sqlx/query-847854addb1b261f6645b98b7a7209f5b6bd0d4fe3833d5979c5cc61a2c50363.json @@ -0,0 +1,114 @@ +{ + "db_name": "MySQL", + "query": "\n SELECT\n s.id as id,\n s.begin as begin,\n s.end as end,\n ss.instance_id as instance_id,\n ss.instance_name as instance_name,\n f.id as flavor,\n f.name as flavor_name,\n ss.status as status,\n u.id as user,\n u.name as username\n FROM\n accounting_state as s,\n accounting_serverstate as ss,\n resources_flavor as f,\n user_user as u\n WHERE\n ss.flavor_id = f.id AND\n ss.user_id = u.id AND\n ss.state_ptr_id = s.id AND\n ss.instance_id = ?\n ORDER BY s.id\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": { + "type": "Long", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT", + "max_size": 11 + } + }, + { + "ordinal": 1, + "name": "begin", + "type_info": { + "type": "Datetime", + "flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE", + "max_size": 26 + } + }, + { + "ordinal": 2, + "name": "end", + "type_info": { + "type": "Datetime", + "flags": "BINARY", + "max_size": 26 + } + }, + { + "ordinal": 3, + "name": "instance_id", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "max_size": 144 + } + }, + { + "ordinal": 4, + "name": "instance_name", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "max_size": 1020 + } + }, + { + "ordinal": 5, + "name": "flavor", + "type_info": { + "type": "LongLong", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT", + "max_size": 20 + } + }, + { + "ordinal": 6, + "name": "flavor_name", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE", + "max_size": 256 + } + }, + { + "ordinal": 7, + "name": "status", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "max_size": 72 + } + }, + { + "ordinal": 8, + "name": "user", + "type_info": { + "type": "Long", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT", + "max_size": 11 + } + }, + { + "ordinal": 9, + "name": "username", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE", + "max_size": 1020 + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + true, + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "847854addb1b261f6645b98b7a7209f5b6bd0d4fe3833d5979c5cc61a2c50363" +} diff --git a/.sqlx/query-f6223bbb88161ed7db11def83b87c6246aa723ad89d79d4c897971837c8d46fa.json b/.sqlx/query-f6223bbb88161ed7db11def83b87c6246aa723ad89d79d4c897971837c8d46fa.json new file mode 100644 index 00000000..b1fd002d --- /dev/null +++ b/.sqlx/query-f6223bbb88161ed7db11def83b87c6246aa723ad89d79d4c897971837c8d46fa.json @@ -0,0 +1,114 @@ +{ + "db_name": "MySQL", + "query": "\n SELECT\n s.id as id,\n s.begin as begin,\n s.end as end,\n ss.instance_id as instance_id,\n ss.instance_name as instance_name,\n f.id as flavor,\n f.name as flavor_name,\n ss.status as status,\n u.id as user,\n u.name as username\n FROM\n accounting_state as s,\n accounting_serverstate as ss,\n resources_flavor as f,\n user_user as u\n WHERE\n ss.flavor_id = f.id AND\n ss.user_id = u.id AND\n ss.state_ptr_id = s.id AND\n ss.instance_id = ? AND\n (s.end > ? OR s.end IS NULL)\n ORDER BY s.id\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": { + "type": "Long", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT", + "max_size": 11 + } + }, + { + "ordinal": 1, + "name": "begin", + "type_info": { + "type": "Datetime", + "flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE", + "max_size": 26 + } + }, + { + "ordinal": 2, + "name": "end", + "type_info": { + "type": "Datetime", + "flags": "BINARY", + "max_size": 26 + } + }, + { + "ordinal": 3, + "name": "instance_id", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "max_size": 144 + } + }, + { + "ordinal": 4, + "name": "instance_name", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "max_size": 1020 + } + }, + { + "ordinal": 5, + "name": "flavor", + "type_info": { + "type": "LongLong", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT", + "max_size": 20 + } + }, + { + "ordinal": 6, + "name": "flavor_name", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE", + "max_size": 256 + } + }, + { + "ordinal": 7, + "name": "status", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "max_size": 72 + } + }, + { + "ordinal": 8, + "name": "user", + "type_info": { + "type": "Long", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT", + "max_size": 11 + } + }, + { + "ordinal": 9, + "name": "username", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE", + "max_size": 1020 + } + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false, + false, + true, + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "f6223bbb88161ed7db11def83b87c6246aa723ad89d79d4c897971837c8d46fa" +} From 44eeaf220cd39928e53f7d4a72dfd36a715a65de Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Wed, 1 Jan 2025 11:19:46 +0100 Subject: [PATCH 07/16] feat(wire/accounting): make ServerConsumptionParams.all/detail Options Signed-off-by: Sandro-Alessio Gierens --- wire/src/accounting/server_consumption.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wire/src/accounting/server_consumption.rs b/wire/src/accounting/server_consumption.rs index fa889b3a..db7e3b8a 100644 --- a/wire/src/accounting/server_consumption.rs +++ b/wire/src/accounting/server_consumption.rs @@ -31,6 +31,6 @@ pub struct ServerConsumptionParams { pub server: Option, pub user: Option, pub project: Option, - pub all: bool, - pub detail: bool, + pub all: Option, + pub detail: Option, } From e72fda07fc0d52ee28b98fa5b2061e0c5287cf48 Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Wed, 1 Jan 2025 11:20:14 +0100 Subject: [PATCH 08/16] refactor(lib/accounting): revise for optional ServerConsumptionParams fields Signed-off-by: Sandro-Alessio Gierens --- lib/src/accounting/server_consumption.rs | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/lib/src/accounting/server_consumption.rs b/lib/src/accounting/server_consumption.rs index 205258b6..06cb4876 100644 --- a/lib/src/accounting/server_consumption.rs +++ b/lib/src/accounting/server_consumption.rs @@ -31,8 +31,8 @@ impl ServerConsumptionRequest { server: None, user: None, project: None, - all: false, - detail: false, + all: None, + detail: None, }, } } @@ -52,7 +52,6 @@ impl ServerConsumptionRequest { server: &str, ) -> Result { self.params.server = Some(server.to_string()); - self.params.detail = false; let params = serde_urlencoded::to_string(&self.params) .context("Failed to envode URL parameters")?; let url = if params.is_empty() { @@ -74,7 +73,7 @@ impl ServerConsumptionRequest { server: &str, ) -> Result { self.params.server = Some(server.to_string()); - self.params.detail = true; + self.params.detail = Some(true); let params = serde_urlencoded::to_string(&self.params) .context("Failed to envode URL parameters")?; let url = if params.is_empty() { @@ -96,7 +95,6 @@ impl ServerConsumptionRequest { user: u32, ) -> Result { self.params.user = Some(user); - self.params.detail = false; let params = serde_urlencoded::to_string(&self.params) .context("Failed to envode URL parameters")?; let url = if params.is_empty() { @@ -118,7 +116,7 @@ impl ServerConsumptionRequest { user: u32, ) -> Result { self.params.user = Some(user); - self.params.detail = true; + self.params.detail = Some(true); let params = serde_urlencoded::to_string(&self.params) .context("Failed to envode URL parameters")?; let url = if params.is_empty() { @@ -140,7 +138,6 @@ impl ServerConsumptionRequest { project: u32, ) -> Result { self.params.project = Some(project); - self.params.detail = false; let params = serde_urlencoded::to_string(&self.params) .context("Failed to envode URL parameters")?; let url = if params.is_empty() { @@ -162,7 +159,7 @@ impl ServerConsumptionRequest { project: u32, ) -> Result { self.params.project = Some(project); - self.params.detail = true; + self.params.detail = Some(true); let params = serde_urlencoded::to_string(&self.params) .context("Failed to envode URL parameters")?; let url = if params.is_empty() { @@ -180,8 +177,7 @@ impl ServerConsumptionRequest { } pub fn all(&mut self) -> Result { - self.params.all = true; - self.params.detail = false; + self.params.all = Some(true); let params = serde_urlencoded::to_string(&self.params) .context("Failed to envode URL parameters")?; let url = if params.is_empty() { @@ -199,8 +195,8 @@ impl ServerConsumptionRequest { } pub fn all_detail(&mut self) -> Result { - self.params.all = true; - self.params.detail = true; + self.params.all = Some(true); + self.params.detail = Some(true); let params = serde_urlencoded::to_string(&self.params) .context("Failed to envode URL parameters")?; let url = if params.is_empty() { @@ -235,7 +231,7 @@ impl ServerConsumptionRequest { } pub fn mine_detail(&mut self) -> Result { - self.params.detail = true; + self.params.detail = Some(true); let params = serde_urlencoded::to_string(&self.params) .context("Failed to envode URL parameters")?; let url = if params.is_empty() { From 6d2ae1db232ca6eab386581760a4074c4678a58c Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Wed, 1 Jan 2025 11:24:34 +0100 Subject: [PATCH 09/16] feat(api/accounting): implement server-consumption for server Signed-off-by: Sandro-Alessio Gierens --- .../accounting/server_consumption/get.rs | 106 +++++++++++++++++- 1 file changed, 100 insertions(+), 6 deletions(-) diff --git a/api/src/routes/accounting/server_consumption/get.rs b/api/src/routes/accounting/server_consumption/get.rs index 2b04e3c8..a1ce74df 100644 --- a/api/src/routes/accounting/server_consumption/get.rs +++ b/api/src/routes/accounting/server_consumption/get.rs @@ -1,10 +1,87 @@ -use crate::error::OptionApiError; +use crate::authorization::require_admin_user; +use crate::database::accounting::server_state::select_ordered_server_states_by_server_begin_and_end_from_db; +use crate::error::{OptionApiError, UnexpectedOnlyError}; use actix_web::web::{Data, Query, ReqData}; use actix_web::HttpResponse; use anyhow::Context; -use lrzcc_wire::accounting::ServerConsumptionParams; +use chrono::{DateTime, Datelike, TimeZone, Utc}; +use lrzcc_wire::accounting::{ + ServerConsumptionParams, ServerConsumptionServer, ServerState, +}; use lrzcc_wire::user::{Project, User}; -use sqlx::MySqlPool; +use sqlx::{MySql, MySqlPool, Transaction}; + +const CONSUMING_STATES: [&str; 15] = [ + "ACTIVE", + "BUILD", + "HARD_REBOOT", + "MIGRATING", + "PASSWORD", + "PAUSED", + "REBOOT", + "REBUILD", + "RESCUE", + "RESIZE", + "REVERT_RESIZE", + "SHUTOFF", + "SUSPENDED", + "UNKNOWN", + "VERIFY_RESIZE", +]; + +pub async fn calculate_server_consumption_for_server( + transaction: &mut Transaction<'_, MySql>, + server_uuid: &str, + begin: Option>, + end: Option>, + states: Option>, +) -> Result { + let mut states = match states { + Some(states) => states, + None => { + select_ordered_server_states_by_server_begin_and_end_from_db( + transaction, + server_uuid.to_string(), + begin, + end, + ) + .await? + } + }; + let mut consumption = ServerConsumptionServer::default(); + if states.is_empty() { + return Ok(consumption); + } + let first = states.first_mut().unwrap(); + if let Some(begin) = begin { + if begin.fixed_offset() > first.begin { + first.begin = begin.fixed_offset(); + } + } + let last = states.last_mut().unwrap(); + if last.end.is_none() { + if let Some(end) = end { + last.end = Some(end.fixed_offset()); + } + } + if let Some(end) = end { + if end.fixed_offset() < last.end.unwrap() { + last.end = Some(end.fixed_offset()); + } + } + for state in states { + if !consumption.contains_key(&state.flavor_name) { + consumption.insert(state.flavor_name.clone(), 0.0); + } + if !CONSUMING_STATES.contains(&state.status.as_str()) { + continue; + } + *consumption.get_mut(&state.flavor_name).unwrap() += + (state.end.unwrap() - state.begin).num_seconds() as f64; + } + // TODO: + Ok(consumption) +} #[tracing::instrument(name = "server_consumption")] pub async fn server_consumption( @@ -15,13 +92,30 @@ pub async fn server_consumption( params: Query, // TODO: is the ValidationError variant ever used? ) -> Result { - let mut _transaction = db_pool + require_admin_user(&user)?; + let end = params.end.unwrap_or(Utc::now().fixed_offset()); + let begin = params.begin.unwrap_or( + Utc.with_ymd_and_hms(Utc::now().year(), 1, 1, 1, 0, 0) + .unwrap() + .fixed_offset(), + ); + let mut transaction = db_pool .begin() .await .context("Failed to begin transaction")?; - _transaction + let consumption = calculate_server_consumption_for_server( + &mut transaction, + params.server.clone().unwrap().as_str(), + Some(begin.into()), + Some(end.into()), + None, + ) + .await?; + transaction .commit() .await .context("Failed to commit transaction")?; - Ok(HttpResponse::Ok().content_type("application/json").json(())) + Ok(HttpResponse::Ok() + .content_type("application/json") + .json(consumption)) } From accf9cf2aa5fff51f9a997dfed79166b55110c8c Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Wed, 1 Jan 2025 15:56:38 +0100 Subject: [PATCH 10/16] feat(wire/accounting): derive Default for ServerConsumption structs Signed-off-by: Sandro-Alessio Gierens --- wire/src/accounting/server_consumption.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wire/src/accounting/server_consumption.rs b/wire/src/accounting/server_consumption.rs index db7e3b8a..15d48672 100644 --- a/wire/src/accounting/server_consumption.rs +++ b/wire/src/accounting/server_consumption.rs @@ -6,19 +6,19 @@ pub type ServerConsumptionFlavors = HashMap; pub type ServerConsumptionServer = ServerConsumptionFlavors; -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)] pub struct ServerConsumptionUser { pub total: ServerConsumptionFlavors, pub servers: HashMap, } -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)] pub struct ServerConsumptionProject { pub total: ServerConsumptionFlavors, pub users: HashMap, } -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)] pub struct ServerConsumptionAll { pub total: ServerConsumptionFlavors, pub projects: HashMap, From 18158857959fc668b9dd9faa310373f0ef5a2c25 Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Wed, 1 Jan 2025 15:57:45 +0100 Subject: [PATCH 11/16] feat(api/database/accounting): add select_ordered_server_states_by_user_begin_and_end_from_db Signed-off-by: Sandro-Alessio Gierens --- api/src/database/accounting/server_state.rs | 163 ++++++++++++++++++++ 1 file changed, 163 insertions(+) diff --git a/api/src/database/accounting/server_state.rs b/api/src/database/accounting/server_state.rs index 1d229892..5989654e 100644 --- a/api/src/database/accounting/server_state.rs +++ b/api/src/database/accounting/server_state.rs @@ -695,3 +695,166 @@ pub async fn select_ordered_server_states_by_server_begin_and_end_from_db( .collect::>(); Ok(rows) } + +#[tracing::instrument( + name = "select_ordered_server_states_by_user_begin_and_end_from_db", + skip(transaction) +)] +pub async fn select_ordered_server_states_by_user_begin_and_end_from_db( + transaction: &mut Transaction<'_, MySql>, + user_id: u64, + begin: Option>, + end: Option>, +) -> Result, UnexpectedOnlyError> { + let result = match (begin, end) { + (None, None) => { + let query = sqlx::query!( + r#" + SELECT + s.id as id, + s.begin as begin, + s.end as end, + ss.instance_id as instance_id, + ss.instance_name as instance_name, + f.id as flavor, + f.name as flavor_name, + ss.status as status, + u.id as user, + u.name as username + FROM + accounting_state as s, + accounting_serverstate as ss, + resources_flavor as f, + user_user as u + WHERE + ss.flavor_id = f.id AND + ss.user_id = u.id AND + ss.state_ptr_id = s.id AND + ss.user_id = ? + ORDER BY s.id + "#, + user_id + ); + transaction.fetch_all(query).await + } + (Some(begin), None) => { + let query = sqlx::query!( + r#" + SELECT + s.id as id, + s.begin as begin, + s.end as end, + ss.instance_id as instance_id, + ss.instance_name as instance_name, + f.id as flavor, + f.name as flavor_name, + ss.status as status, + u.id as user, + u.name as username + FROM + accounting_state as s, + accounting_serverstate as ss, + resources_flavor as f, + user_user as u + WHERE + ss.flavor_id = f.id AND + ss.user_id = u.id AND + ss.state_ptr_id = s.id AND + ss.user_id = ? AND + (s.end > ? OR s.end IS NULL) + ORDER BY s.id + "#, + user_id, + begin + ); + transaction.fetch_all(query).await + } + (None, Some(end)) => { + let query = sqlx::query!( + r#" + SELECT + s.id as id, + s.begin as begin, + s.end as end, + ss.instance_id as instance_id, + ss.instance_name as instance_name, + f.id as flavor, + f.name as flavor_name, + ss.status as status, + u.id as user, + u.name as username + FROM + accounting_state as s, + accounting_serverstate as ss, + resources_flavor as f, + user_user as u + WHERE + ss.flavor_id = f.id AND + ss.user_id = u.id AND + ss.state_ptr_id = s.id AND + ss.user_id = ? AND + s.begin < ? + ORDER BY s.id + "#, + user_id, + end + ); + transaction.fetch_all(query).await + } + (Some(begin), Some(end)) => { + let query = sqlx::query!( + r#" + SELECT + s.id as id, + s.begin as begin, + s.end as end, + ss.instance_id as instance_id, + ss.instance_name as instance_name, + f.id as flavor, + f.name as flavor_name, + ss.status as status, + u.id as user, + u.name as username + FROM + accounting_state as s, + accounting_serverstate as ss, + resources_flavor as f, + user_user as u + WHERE + ss.flavor_id = f.id AND + ss.user_id = u.id AND + ss.state_ptr_id = s.id AND + ss.user_id = ? AND + (s.end > ? OR s.end IS NULL) AND + s.begin < ? + ORDER BY s.id + "#, + user_id, + begin, + end + ); + transaction.fetch_all(query).await + } + }; + let rows = result + .context("Failed to execute select query")? + .into_iter() + .map(|r| ServerStateRow::from_row(&r)) + .collect::, _>>() + .context("Failed to convert row to server state")? + .into_iter() + .map(|r| ServerState { + id: r.id, + begin: r.begin.fixed_offset(), + end: r.end.map(|end| end.fixed_offset()), + instance_id: r.instance_id, + instance_name: r.instance_name, + flavor: r.flavor, + flavor_name: r.flavor_name, + status: r.status, + user: r.user, + username: r.username, + }) + .collect::>(); + Ok(rows) +} From 08a13232dad53d4db70e31ee14dd282f785ed233 Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Wed, 1 Jan 2025 15:59:05 +0100 Subject: [PATCH 12/16] feat(api/accounting): implement server-consumption for user Signed-off-by: Sandro-Alessio Gierens --- .../accounting/server_consumption/get.rs | 130 ++++++++++++++++-- 1 file changed, 120 insertions(+), 10 deletions(-) diff --git a/api/src/routes/accounting/server_consumption/get.rs b/api/src/routes/accounting/server_consumption/get.rs index a1ce74df..1e0428c3 100644 --- a/api/src/routes/accounting/server_consumption/get.rs +++ b/api/src/routes/accounting/server_consumption/get.rs @@ -1,15 +1,21 @@ use crate::authorization::require_admin_user; -use crate::database::accounting::server_state::select_ordered_server_states_by_server_begin_and_end_from_db; +use crate::database::accounting::server_state::{ + select_ordered_server_states_by_server_begin_and_end_from_db, + select_ordered_server_states_by_user_begin_and_end_from_db, +}; use crate::error::{OptionApiError, UnexpectedOnlyError}; use actix_web::web::{Data, Query, ReqData}; use actix_web::HttpResponse; use anyhow::Context; use chrono::{DateTime, Datelike, TimeZone, Utc}; use lrzcc_wire::accounting::{ - ServerConsumptionParams, ServerConsumptionServer, ServerState, + ServerConsumptionFlavors, ServerConsumptionParams, ServerConsumptionServer, + ServerConsumptionUser, ServerState, }; use lrzcc_wire::user::{Project, User}; +use serde::Serialize; use sqlx::{MySql, MySqlPool, Transaction}; +use std::collections::HashMap; const CONSUMING_STATES: [&str; 15] = [ "ACTIVE", @@ -83,6 +89,80 @@ pub async fn calculate_server_consumption_for_server( Ok(consumption) } +#[derive(Serialize)] +#[serde(untagged)] +pub enum ServerConsumptionForUser { + Normal(ServerConsumptionFlavors), + Detail(ServerConsumptionUser), +} + +pub async fn calculate_server_consumption_for_user( + transaction: &mut Transaction<'_, MySql>, + user_id: u64, + begin: Option>, + end: Option>, + detail: Option, +) -> Result { + let states = select_ordered_server_states_by_user_begin_and_end_from_db( + transaction, + user_id, + begin, + end, + ) + .await?; + + let mut server_state_map: HashMap> = + HashMap::new(); + for state in states { + if !server_state_map.contains_key(state.instance_id.as_str()) { + server_state_map.insert(state.instance_id.clone(), Vec::new()); + } + server_state_map + .get_mut(state.instance_id.as_str()) + .unwrap() + .push(state); + } + + let mut consumption = ServerConsumptionUser::default(); + for (server_uuid, server_states) in server_state_map { + consumption.servers.insert( + server_uuid.clone(), + calculate_server_consumption_for_server( + transaction, + server_uuid.as_str(), + begin, + end, + Some(server_states), + ) + .await?, + ); + } + + for server_consumption in consumption.servers.values() { + for (flavor, value) in server_consumption { + if !consumption.total.contains_key(flavor.as_str()) { + consumption.total.insert(flavor.clone(), 0.0); + } + *consumption.total.get_mut(flavor.as_str()).unwrap() += value; + } + } + + Ok(if detail.is_some() && detail.unwrap() { + ServerConsumptionForUser::Detail(consumption) + } else { + ServerConsumptionForUser::Normal(consumption.total) + }) +} + +#[derive(Serialize)] +#[serde(untagged)] +pub enum ServerConsumption { + Server(ServerConsumptionServer), + User(ServerConsumptionForUser), + Project(ServerConsumptionForUser), + All(ServerConsumptionForUser), +} + #[tracing::instrument(name = "server_consumption")] pub async fn server_consumption( user: ReqData, @@ -103,14 +183,44 @@ pub async fn server_consumption( .begin() .await .context("Failed to begin transaction")?; - let consumption = calculate_server_consumption_for_server( - &mut transaction, - params.server.clone().unwrap().as_str(), - Some(begin.into()), - Some(end.into()), - None, - ) - .await?; + let consumption = if params.all.unwrap_or(false) { + todo!() + } else if let Some(_project_id) = params.project { + todo!() + } else if let Some(user_id) = params.user { + ServerConsumption::User( + calculate_server_consumption_for_user( + &mut transaction, + user_id as u64, + Some(begin.into()), + Some(end.into()), + params.detail, + ) + .await?, + ) + } else if let Some(server_id) = params.server.clone() { + ServerConsumption::Server( + calculate_server_consumption_for_server( + &mut transaction, + server_id.as_str(), + Some(begin.into()), + Some(end.into()), + None, + ) + .await?, + ) + } else { + ServerConsumption::User( + calculate_server_consumption_for_user( + &mut transaction, + user.id as u64, + Some(begin.into()), + Some(end.into()), + params.detail, + ) + .await?, + ) + }; transaction .commit() .await From 881aae45e3f23eac3f94099d98aa77b96fac62fd Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Wed, 1 Jan 2025 15:59:52 +0100 Subject: [PATCH 13/16] chore(sqlx): update offline query data Signed-off-by: Sandro-Alessio Gierens --- ...755fbe4a32e1fa19068cf5f739d9641d8e439.json | 114 ++++++++++++++++++ ...489f5410932613d0cc78b44e9693cd1259a87.json | 114 ++++++++++++++++++ ...fdd8db1e96a06f0d715ef41922f9a5a59703d.json | 114 ++++++++++++++++++ ...9b21d618bdc36134901cad44671734f9b0ccf.json | 114 ++++++++++++++++++ 4 files changed, 456 insertions(+) create mode 100644 .sqlx/query-58678030d267f31953f05167d0a755fbe4a32e1fa19068cf5f739d9641d8e439.json create mode 100644 .sqlx/query-7052dec0e9276fb57241b3ee7d3489f5410932613d0cc78b44e9693cd1259a87.json create mode 100644 .sqlx/query-a76c116d3d7254ac60f1da8027dfdd8db1e96a06f0d715ef41922f9a5a59703d.json create mode 100644 .sqlx/query-f35c249b26581043fde6be33f0b9b21d618bdc36134901cad44671734f9b0ccf.json diff --git a/.sqlx/query-58678030d267f31953f05167d0a755fbe4a32e1fa19068cf5f739d9641d8e439.json b/.sqlx/query-58678030d267f31953f05167d0a755fbe4a32e1fa19068cf5f739d9641d8e439.json new file mode 100644 index 00000000..557e60de --- /dev/null +++ b/.sqlx/query-58678030d267f31953f05167d0a755fbe4a32e1fa19068cf5f739d9641d8e439.json @@ -0,0 +1,114 @@ +{ + "db_name": "MySQL", + "query": "\n SELECT\n s.id as id,\n s.begin as begin,\n s.end as end,\n ss.instance_id as instance_id,\n ss.instance_name as instance_name,\n f.id as flavor,\n f.name as flavor_name,\n ss.status as status,\n u.id as user,\n u.name as username\n FROM\n accounting_state as s,\n accounting_serverstate as ss,\n resources_flavor as f,\n user_user as u\n WHERE\n ss.flavor_id = f.id AND\n ss.user_id = u.id AND\n ss.state_ptr_id = s.id AND\n ss.user_id = ? AND\n s.begin < ?\n ORDER BY s.id\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": { + "type": "Long", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT", + "max_size": 11 + } + }, + { + "ordinal": 1, + "name": "begin", + "type_info": { + "type": "Datetime", + "flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE", + "max_size": 26 + } + }, + { + "ordinal": 2, + "name": "end", + "type_info": { + "type": "Datetime", + "flags": "BINARY", + "max_size": 26 + } + }, + { + "ordinal": 3, + "name": "instance_id", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "max_size": 144 + } + }, + { + "ordinal": 4, + "name": "instance_name", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "max_size": 1020 + } + }, + { + "ordinal": 5, + "name": "flavor", + "type_info": { + "type": "LongLong", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT", + "max_size": 20 + } + }, + { + "ordinal": 6, + "name": "flavor_name", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE", + "max_size": 256 + } + }, + { + "ordinal": 7, + "name": "status", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "max_size": 72 + } + }, + { + "ordinal": 8, + "name": "user", + "type_info": { + "type": "Long", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT", + "max_size": 11 + } + }, + { + "ordinal": 9, + "name": "username", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE", + "max_size": 1020 + } + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false, + false, + true, + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "58678030d267f31953f05167d0a755fbe4a32e1fa19068cf5f739d9641d8e439" +} diff --git a/.sqlx/query-7052dec0e9276fb57241b3ee7d3489f5410932613d0cc78b44e9693cd1259a87.json b/.sqlx/query-7052dec0e9276fb57241b3ee7d3489f5410932613d0cc78b44e9693cd1259a87.json new file mode 100644 index 00000000..d6fd805e --- /dev/null +++ b/.sqlx/query-7052dec0e9276fb57241b3ee7d3489f5410932613d0cc78b44e9693cd1259a87.json @@ -0,0 +1,114 @@ +{ + "db_name": "MySQL", + "query": "\n SELECT\n s.id as id,\n s.begin as begin,\n s.end as end,\n ss.instance_id as instance_id,\n ss.instance_name as instance_name,\n f.id as flavor,\n f.name as flavor_name,\n ss.status as status,\n u.id as user,\n u.name as username\n FROM\n accounting_state as s,\n accounting_serverstate as ss,\n resources_flavor as f,\n user_user as u\n WHERE\n ss.flavor_id = f.id AND\n ss.user_id = u.id AND\n ss.state_ptr_id = s.id AND\n ss.user_id = ? AND\n (s.end > ? OR s.end IS NULL)\n ORDER BY s.id\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": { + "type": "Long", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT", + "max_size": 11 + } + }, + { + "ordinal": 1, + "name": "begin", + "type_info": { + "type": "Datetime", + "flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE", + "max_size": 26 + } + }, + { + "ordinal": 2, + "name": "end", + "type_info": { + "type": "Datetime", + "flags": "BINARY", + "max_size": 26 + } + }, + { + "ordinal": 3, + "name": "instance_id", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "max_size": 144 + } + }, + { + "ordinal": 4, + "name": "instance_name", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "max_size": 1020 + } + }, + { + "ordinal": 5, + "name": "flavor", + "type_info": { + "type": "LongLong", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT", + "max_size": 20 + } + }, + { + "ordinal": 6, + "name": "flavor_name", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE", + "max_size": 256 + } + }, + { + "ordinal": 7, + "name": "status", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "max_size": 72 + } + }, + { + "ordinal": 8, + "name": "user", + "type_info": { + "type": "Long", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT", + "max_size": 11 + } + }, + { + "ordinal": 9, + "name": "username", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE", + "max_size": 1020 + } + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false, + false, + true, + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "7052dec0e9276fb57241b3ee7d3489f5410932613d0cc78b44e9693cd1259a87" +} diff --git a/.sqlx/query-a76c116d3d7254ac60f1da8027dfdd8db1e96a06f0d715ef41922f9a5a59703d.json b/.sqlx/query-a76c116d3d7254ac60f1da8027dfdd8db1e96a06f0d715ef41922f9a5a59703d.json new file mode 100644 index 00000000..07763a5a --- /dev/null +++ b/.sqlx/query-a76c116d3d7254ac60f1da8027dfdd8db1e96a06f0d715ef41922f9a5a59703d.json @@ -0,0 +1,114 @@ +{ + "db_name": "MySQL", + "query": "\n SELECT\n s.id as id,\n s.begin as begin,\n s.end as end,\n ss.instance_id as instance_id,\n ss.instance_name as instance_name,\n f.id as flavor,\n f.name as flavor_name,\n ss.status as status,\n u.id as user,\n u.name as username\n FROM\n accounting_state as s,\n accounting_serverstate as ss,\n resources_flavor as f,\n user_user as u\n WHERE\n ss.flavor_id = f.id AND\n ss.user_id = u.id AND\n ss.state_ptr_id = s.id AND\n ss.user_id = ?\n ORDER BY s.id\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": { + "type": "Long", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT", + "max_size": 11 + } + }, + { + "ordinal": 1, + "name": "begin", + "type_info": { + "type": "Datetime", + "flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE", + "max_size": 26 + } + }, + { + "ordinal": 2, + "name": "end", + "type_info": { + "type": "Datetime", + "flags": "BINARY", + "max_size": 26 + } + }, + { + "ordinal": 3, + "name": "instance_id", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "max_size": 144 + } + }, + { + "ordinal": 4, + "name": "instance_name", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "max_size": 1020 + } + }, + { + "ordinal": 5, + "name": "flavor", + "type_info": { + "type": "LongLong", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT", + "max_size": 20 + } + }, + { + "ordinal": 6, + "name": "flavor_name", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE", + "max_size": 256 + } + }, + { + "ordinal": 7, + "name": "status", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "max_size": 72 + } + }, + { + "ordinal": 8, + "name": "user", + "type_info": { + "type": "Long", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT", + "max_size": 11 + } + }, + { + "ordinal": 9, + "name": "username", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE", + "max_size": 1020 + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + true, + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "a76c116d3d7254ac60f1da8027dfdd8db1e96a06f0d715ef41922f9a5a59703d" +} diff --git a/.sqlx/query-f35c249b26581043fde6be33f0b9b21d618bdc36134901cad44671734f9b0ccf.json b/.sqlx/query-f35c249b26581043fde6be33f0b9b21d618bdc36134901cad44671734f9b0ccf.json new file mode 100644 index 00000000..3236228a --- /dev/null +++ b/.sqlx/query-f35c249b26581043fde6be33f0b9b21d618bdc36134901cad44671734f9b0ccf.json @@ -0,0 +1,114 @@ +{ + "db_name": "MySQL", + "query": "\n SELECT\n s.id as id,\n s.begin as begin,\n s.end as end,\n ss.instance_id as instance_id,\n ss.instance_name as instance_name,\n f.id as flavor,\n f.name as flavor_name,\n ss.status as status,\n u.id as user,\n u.name as username\n FROM\n accounting_state as s,\n accounting_serverstate as ss,\n resources_flavor as f,\n user_user as u\n WHERE\n ss.flavor_id = f.id AND\n ss.user_id = u.id AND\n ss.state_ptr_id = s.id AND\n ss.user_id = ? AND\n (s.end > ? OR s.end IS NULL) AND\n s.begin < ?\n ORDER BY s.id\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": { + "type": "Long", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT", + "max_size": 11 + } + }, + { + "ordinal": 1, + "name": "begin", + "type_info": { + "type": "Datetime", + "flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE", + "max_size": 26 + } + }, + { + "ordinal": 2, + "name": "end", + "type_info": { + "type": "Datetime", + "flags": "BINARY", + "max_size": 26 + } + }, + { + "ordinal": 3, + "name": "instance_id", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "max_size": 144 + } + }, + { + "ordinal": 4, + "name": "instance_name", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "max_size": 1020 + } + }, + { + "ordinal": 5, + "name": "flavor", + "type_info": { + "type": "LongLong", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT", + "max_size": 20 + } + }, + { + "ordinal": 6, + "name": "flavor_name", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE", + "max_size": 256 + } + }, + { + "ordinal": 7, + "name": "status", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "max_size": 72 + } + }, + { + "ordinal": 8, + "name": "user", + "type_info": { + "type": "Long", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT", + "max_size": 11 + } + }, + { + "ordinal": 9, + "name": "username", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE", + "max_size": 1020 + } + } + ], + "parameters": { + "Right": 3 + }, + "nullable": [ + false, + false, + true, + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "f35c249b26581043fde6be33f0b9b21d618bdc36134901cad44671734f9b0ccf" +} From 1144abf63e89323d33d480467f78677d8484dc3e Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Thu, 2 Jan 2025 21:55:44 +0100 Subject: [PATCH 14/16] feat(api/accounting): add empty remaining server-consumption calc functions Signed-off-by: Sandro-Alessio Gierens --- .../accounting/server_consumption/get.rs | 81 +++++++++++++++++-- 1 file changed, 74 insertions(+), 7 deletions(-) diff --git a/api/src/routes/accounting/server_consumption/get.rs b/api/src/routes/accounting/server_consumption/get.rs index 1e0428c3..7c64e3af 100644 --- a/api/src/routes/accounting/server_consumption/get.rs +++ b/api/src/routes/accounting/server_consumption/get.rs @@ -9,8 +9,9 @@ use actix_web::HttpResponse; use anyhow::Context; use chrono::{DateTime, Datelike, TimeZone, Utc}; use lrzcc_wire::accounting::{ - ServerConsumptionFlavors, ServerConsumptionParams, ServerConsumptionServer, - ServerConsumptionUser, ServerState, + ServerConsumptionAll, ServerConsumptionFlavors, ServerConsumptionParams, + ServerConsumptionProject, ServerConsumptionServer, ServerConsumptionUser, + ServerState, }; use lrzcc_wire::user::{Project, User}; use serde::Serialize; @@ -154,13 +155,62 @@ pub async fn calculate_server_consumption_for_user( }) } +#[derive(Serialize)] +#[serde(untagged)] +pub enum ServerConsumptionForProject { + Normal(ServerConsumptionFlavors), + Detail(ServerConsumptionProject), +} + +pub async fn calculate_server_consumption_for_project( + transaction: &mut Transaction<'_, MySql>, + project_id: u64, + begin: Option>, + end: Option>, + detail: Option, +) -> Result { + let mut consumption = ServerConsumptionProject::default(); + + // TODO: + + Ok(if detail.is_some() && detail.unwrap() { + ServerConsumptionForProject::Detail(consumption) + } else { + ServerConsumptionForProject::Normal(consumption.total) + }) +} + +#[derive(Serialize)] +#[serde(untagged)] +pub enum ServerConsumptionForAll { + Normal(ServerConsumptionFlavors), + Detail(ServerConsumptionAll), +} + +pub async fn calculate_server_consumption_for_all( + transaction: &mut Transaction<'_, MySql>, + begin: Option>, + end: Option>, + detail: Option, +) -> Result { + let mut consumption = ServerConsumptionAll::default(); + + // TODO: + + Ok(if detail.is_some() && detail.unwrap() { + ServerConsumptionForAll::Detail(consumption) + } else { + ServerConsumptionForAll::Normal(consumption.total) + }) +} + #[derive(Serialize)] #[serde(untagged)] pub enum ServerConsumption { Server(ServerConsumptionServer), User(ServerConsumptionForUser), - Project(ServerConsumptionForUser), - All(ServerConsumptionForUser), + Project(ServerConsumptionForProject), + All(ServerConsumptionForAll), } #[tracing::instrument(name = "server_consumption")] @@ -184,9 +234,26 @@ pub async fn server_consumption( .await .context("Failed to begin transaction")?; let consumption = if params.all.unwrap_or(false) { - todo!() - } else if let Some(_project_id) = params.project { - todo!() + ServerConsumption::All( + calculate_server_consumption_for_all( + &mut transaction, + Some(begin.into()), + Some(end.into()), + params.detail, + ) + .await?, + ) + } else if let Some(project_id) = params.project { + ServerConsumption::Project( + calculate_server_consumption_for_project( + &mut transaction, + project_id as u64, + Some(begin.into()), + Some(end.into()), + params.detail, + ) + .await?, + ) } else if let Some(user_id) = params.user { ServerConsumption::User( calculate_server_consumption_for_user( From a2a1a3dac83cdc77d0925e54c75c169b9e677b83 Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Fri, 3 Jan 2025 00:57:16 +0100 Subject: [PATCH 15/16] feat(api/accounting): implement server-consumption for project Signed-off-by: Sandro-Alessio Gierens --- .../accounting/server_consumption/get.rs | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/api/src/routes/accounting/server_consumption/get.rs b/api/src/routes/accounting/server_consumption/get.rs index 7c64e3af..bcbf4119 100644 --- a/api/src/routes/accounting/server_consumption/get.rs +++ b/api/src/routes/accounting/server_consumption/get.rs @@ -3,6 +3,7 @@ use crate::database::accounting::server_state::{ select_ordered_server_states_by_server_begin_and_end_from_db, select_ordered_server_states_by_user_begin_and_end_from_db, }; +use crate::database::user::user::select_users_by_project_from_db; use crate::error::{OptionApiError, UnexpectedOnlyError}; use actix_web::web::{Data, Query, ReqData}; use actix_web::HttpResponse; @@ -171,7 +172,33 @@ pub async fn calculate_server_consumption_for_project( ) -> Result { let mut consumption = ServerConsumptionProject::default(); - // TODO: + let users = + select_users_by_project_from_db(transaction, project_id).await?; + for user in users { + let user_consumption = match calculate_server_consumption_for_user( + transaction, + user.id as u64, + begin, + end, + Some(true), + ) + .await? + { + ServerConsumptionForUser::Normal(_normal) => unreachable!(), + ServerConsumptionForUser::Detail(detail) => detail, + }; + + for (flavor, value) in user_consumption.total.clone() { + if !consumption.total.contains_key(flavor.as_str()) { + consumption.total.insert(flavor.clone(), 0.0); + } + *consumption.total.get_mut(flavor.as_str()).unwrap() += value; + } + + consumption + .users + .insert(user.name.clone(), user_consumption); + } Ok(if detail.is_some() && detail.unwrap() { ServerConsumptionForProject::Detail(consumption) From d4840fbb4a8f887eb130d2a0cde0dfb6ca7e7f12 Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Fri, 3 Jan 2025 01:09:00 +0100 Subject: [PATCH 16/16] feat(api/accounting): implement server-consumption for all Signed-off-by: Sandro-Alessio Gierens --- .../accounting/server_consumption/get.rs | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/api/src/routes/accounting/server_consumption/get.rs b/api/src/routes/accounting/server_consumption/get.rs index bcbf4119..a5628e1d 100644 --- a/api/src/routes/accounting/server_consumption/get.rs +++ b/api/src/routes/accounting/server_consumption/get.rs @@ -3,6 +3,7 @@ use crate::database::accounting::server_state::{ select_ordered_server_states_by_server_begin_and_end_from_db, select_ordered_server_states_by_user_begin_and_end_from_db, }; +use crate::database::user::project::select_all_projects_from_db; use crate::database::user::user::select_users_by_project_from_db; use crate::error::{OptionApiError, UnexpectedOnlyError}; use actix_web::web::{Data, Query, ReqData}; @@ -222,7 +223,33 @@ pub async fn calculate_server_consumption_for_all( ) -> Result { let mut consumption = ServerConsumptionAll::default(); - // TODO: + let projects = select_all_projects_from_db(transaction).await?; + for project in projects { + let project_consumption = + match calculate_server_consumption_for_project( + transaction, + project.id as u64, + begin, + end, + Some(true), + ) + .await? + { + ServerConsumptionForProject::Normal(_normal) => unreachable!(), + ServerConsumptionForProject::Detail(detail) => detail, + }; + + for (flavor, value) in project_consumption.total.clone() { + if !consumption.total.contains_key(flavor.as_str()) { + consumption.total.insert(flavor.clone(), 0.0); + } + *consumption.total.get_mut(flavor.as_str()).unwrap() += value; + } + + consumption + .projects + .insert(project.name.clone(), project_consumption); + } Ok(if detail.is_some() && detail.unwrap() { ServerConsumptionForAll::Detail(consumption)