diff --git a/Cargo.lock b/Cargo.lock index a53a8b1f0e..e554460c12 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2081,6 +2081,7 @@ dependencies = [ name = "iml-snapshot" version = "0.4.0" dependencies = [ + "chrono", "futures", "futures-util", "iml-command-utils", diff --git a/iml-api/src/graphql/mod.rs b/iml-api/src/graphql/mod.rs index fa763b157d..ee55f4de60 100644 --- a/iml-api/src/graphql/mod.rs +++ b/iml-api/src/graphql/mod.rs @@ -6,11 +6,7 @@ mod filesystem; mod stratagem; mod task; -use crate::{ - command::get_command, - error::ImlApiError, - timer::{configure_snapshot_timer, remove_snapshot_timer}, -}; +use crate::{command::get_command, error::ImlApiError}; use chrono::{DateTime, Utc}; use futures::{ future::{self, join_all}, @@ -25,7 +21,7 @@ use iml_wire_types::{ graphql::{ServerProfile, ServerProfileInput}, graphql_duration::GraphQLDuration, logs::{LogResponse, Meta}, - snapshot::{ReserveUnit, Snapshot, SnapshotInterval, SnapshotRetention}, + snapshot::{Snapshot, SnapshotPolicy}, task::Task, Command, EndpointName, FsType, Job, LogMessage, LogSeverity, MessageClass, SortDir, }; @@ -441,47 +437,6 @@ impl QueryRoot { Ok(commands) } - /// List all snapshot intervals - async fn snapshot_intervals(context: &Context) -> juniper::FieldResult> { - let xs: Vec = sqlx::query!("SELECT * FROM snapshot_interval") - .fetch(&context.pg_pool) - .map_ok(|x| SnapshotInterval { - id: x.id, - filesystem_name: x.filesystem_name, - use_barrier: x.use_barrier, - interval: x.interval.into(), - last_run: x.last_run, - }) - .try_collect() - .await?; - - Ok(xs) - } - /// List all snapshot retention policies. Snapshots will automatically be deleted (starting with the oldest) - /// when free space falls below the defined reserve value and its associated unit. - async fn snapshot_retention_policies( - context: &Context, - ) -> juniper::FieldResult> { - let xs: Vec = sqlx::query_as!( - SnapshotRetention, - r#" - SELECT - id, - filesystem_name, - reserve_value, - reserve_unit as "reserve_unit:ReserveUnit", - last_run, - keep_num - FROM snapshot_retention - "# - ) - .fetch(&context.pg_pool) - .try_collect() - .await?; - - Ok(xs) - } - #[graphql(arguments( limit(description = "optional paging limit, defaults to 100",), offset(description = "Offset into items, defaults to 0"), @@ -641,27 +596,26 @@ impl QueryRoot { Ok(mount_command) } -} - -struct SnapshotIntervalName { - id: i32, - fs_name: String, - timestamp: DateTime, -} -fn parse_snapshot_name(name: &str) -> Option { - match name.trim().splitn(3, "-").collect::>().as_slice() { - [id, fs, ts] => { - let ts = ts.parse::>().ok()?; - let id = id.parse::().ok()?; - - Some(SnapshotIntervalName { - id, - fs_name: fs.to_string(), - timestamp: ts, + /// List all automatic snapshot policies. + async fn snapshot_policies(context: &Context) -> juniper::FieldResult> { + let xs = sqlx::query!(r#"SELECT * FROM snapshot_policy"#) + .fetch(&context.pg_pool) + .map_ok(|x| SnapshotPolicy { + id: x.id, + filesystem: x.filesystem, + interval: x.interval.into(), + barrier: x.barrier, + keep: x.keep, + daily: x.daily, + weekly: x.weekly, + monthly: x.monthly, + last_run: x.last_run, }) - } - _ => None, + .try_collect() + .await?; + + Ok(xs) } } @@ -699,22 +653,6 @@ impl MutationRoot { let name = name.trim(); validate_snapshot_name(name)?; - let snapshot_interval_name = parse_snapshot_name(name); - if let Some(data) = snapshot_interval_name { - sqlx::query!( - r#" - UPDATE snapshot_interval - SET last_run=$1 - WHERE id=$2 AND filesystem_name=$3 - "#, - data.timestamp, - data.id, - data.fs_name, - ) - .execute(&context.pg_pool) - .await?; - } - let active_mgs_host_fqdn = active_mgs_host_fqdn(&fsname, &context.pg_pool) .await? .ok_or_else(|| { @@ -890,117 +828,6 @@ impl MutationRoot { .await .map_err(|e| e.into()) } - #[graphql(arguments( - fsname(description = "The filesystem to create snapshots with"), - interval(description = "How often a snapshot should be taken"), - use_barrier( - description = "Set write barrier before creating snapshot. The default value is `false`" - ), - ))] - /// Creates a new snapshot interval. - /// A recurring snapshot will be taken once the given `interval` expires for the given `fsname`. - /// In order for the snapshot to be successful, the filesystem must be available. - async fn create_snapshot_interval( - context: &Context, - fsname: String, - interval: GraphQLDuration, - use_barrier: Option, - ) -> juniper::FieldResult { - let _ = fs_id_by_name(&context.pg_pool, &fsname).await?; - let maybe_id = sqlx::query!( - r#" - INSERT INTO snapshot_interval ( - filesystem_name, - use_barrier, - interval - ) - VALUES ($1, $2, $3) - ON CONFLICT (filesystem_name, interval) - DO NOTHING - RETURNING id - "#, - fsname, - use_barrier.unwrap_or_default(), - PgInterval::try_from(interval.0)?, - ) - .fetch_optional(&context.pg_pool) - .await? - .map(|x| x.id); - - if let Some(id) = maybe_id { - configure_snapshot_timer(id, fsname, interval.0, use_barrier.unwrap_or_default()) - .await?; - } - - Ok(true) - } - /// Removes an existing snapshot interval. - /// This will also cancel any outstanding intervals scheduled by this rule. - #[graphql(arguments(id(description = "The snapshot interval id"),))] - async fn remove_snapshot_interval(context: &Context, id: i32) -> juniper::FieldResult { - sqlx::query!("DELETE FROM snapshot_interval WHERE id=$1", id) - .execute(&context.pg_pool) - .await?; - - remove_snapshot_timer(id).await?; - - Ok(true) - } - #[graphql(arguments( - fsname(description = "Filesystem name"), - reserve_value( - description = "Delete the oldest snapshot when available space falls below this value" - ), - reserve_unit(description = "The unit of measurement associated with the reserve_value"), - keep_num( - description = "The minimum number of snapshots to keep. This is to avoid deleting all snapshots while pursuiting the reserve goal" - ) - ))] - /// Creates a new snapshot retention policy for the given `fsname`. - /// Snapshots will automatically be deleted (starting with the oldest) - /// when free space falls below the defined reserve value and its associated unit. - async fn create_snapshot_retention( - context: &Context, - fsname: String, - reserve_value: i32, - reserve_unit: ReserveUnit, - keep_num: Option, - ) -> juniper::FieldResult { - let _ = fs_id_by_name(&context.pg_pool, &fsname).await?; - sqlx::query!( - r#" - INSERT INTO snapshot_retention ( - filesystem_name, - reserve_value, - reserve_unit, - keep_num - ) - VALUES ($1, $2, $3, $4) - ON CONFLICT (filesystem_name) - DO UPDATE SET - reserve_value = EXCLUDED.reserve_value, - reserve_unit = EXCLUDED.reserve_unit, - keep_num = EXCLUDED.keep_num - "#, - fsname, - reserve_value, - reserve_unit as ReserveUnit, - keep_num.unwrap_or(0) - ) - .execute(&context.pg_pool) - .await?; - - Ok(true) - } - /// Remove an existing snapshot retention policy. - #[graphql(arguments(id(description = "The snapshot retention policy id")))] - async fn remove_snapshot_retention(context: &Context, id: i32) -> juniper::FieldResult { - sqlx::query!("DELETE FROM snapshot_retention WHERE id=$1", id) - .execute(&context.pg_pool) - .await?; - - Ok(true) - } /// Create a server profile. #[graphql(arguments(profile(description = "The server profile to add")))] @@ -1108,6 +935,67 @@ impl MutationRoot { Ok(true) } + #[graphql(arguments( + filesystem(description = "The filesystem to create snapshots with"), + interval(description = "How often a snapshot should be taken"), + use_barrier( + description = "Set write barrier before creating snapshot. The default value is `false`" + ), + keep(description = "Number of the most recent snapshots to keep"), + daily(description = "Then, number of days when keep the most recent snapshot of each day"), + weekly( + description = "Then, number of weeks when keep the most recent snapshot of each week" + ), + monthly( + description = "Then, number of months when keep the most recent snapshot of each month" + ), + ))] + /// Creates a new automatic snapshot policy. + async fn create_snapshot_policy( + context: &Context, + filesystem: String, + interval: GraphQLDuration, + barrier: Option, + keep: i32, + daily: Option, + weekly: Option, + monthly: Option, + ) -> juniper::FieldResult { + sqlx::query!( + r#" + INSERT INTO snapshot_policy ( + filesystem, + interval, + barrier, + keep, + daily, + weekly, + monthly + ) + VALUES ($1, $2, $3, $4, $5, $6, $7) + ON CONFLICT (filesystem) + DO UPDATE SET + interval = EXCLUDED.interval, + barrier = EXCLUDED.barrier, + keep = EXCLUDED.keep, + daily = EXCLUDED.daily, + weekly = EXCLUDED.weekly, + monthly = EXCLUDED.monthly + "#, + filesystem, + PgInterval::try_from(interval.0)?, + barrier.unwrap_or(false), + keep, + daily.unwrap_or(0), + weekly.unwrap_or(0), + monthly.unwrap_or(0) + ) + .execute(&context.pg_pool) + .await?; + + Ok(true) + } + #[graphql(arguments(profile_name(description = "Name of the profile to remove")))] async fn remove_server_profile( context: &Context, @@ -1139,6 +1027,32 @@ impl MutationRoot { transaction.commit().await?; Ok(true) } + + #[graphql(arguments( + filesystem(description = "The filesystem to remove snapshot policies for"), + id(description = "Id of the policy to remove"), + ))] + /// Removes the automatic snapshot policy. + async fn remove_snapshot_policy( + context: &Context, + filesystem: Option, + id: Option, + ) -> juniper::FieldResult { + sqlx::query!( + r#" + DELETE FROM snapshot_policy + WHERE (filesystem IS NOT DISTINCT FROM $1) + OR (id IS NOT DISTINCT FROM $2) + + "#, + filesystem, + id + ) + .execute(&context.pg_pool) + .await?; + + Ok(true) + } } #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] diff --git a/iml-api/src/main.rs b/iml-api/src/main.rs index 6e4b078361..2a9dd03bdf 100644 --- a/iml-api/src/main.rs +++ b/iml-api/src/main.rs @@ -6,7 +6,6 @@ mod action; mod command; mod error; mod graphql; -mod timer; use iml_manager_env::get_pool_limit; use iml_postgres::get_db_pool; diff --git a/iml-api/src/timer.rs b/iml-api/src/timer.rs deleted file mode 100644 index 615e6ad48a..0000000000 --- a/iml-api/src/timer.rs +++ /dev/null @@ -1,105 +0,0 @@ -use crate::error::ImlApiError; -use iml_manager_client::{delete, get_client, put}; -use iml_manager_env::{get_timer_addr, running_in_docker}; -use std::time::Duration; - -#[derive(serde::Serialize, Debug)] -pub struct TimerConfig { - config_id: String, - file_prefix: String, - timer_config: String, - service_config: String, -} - -pub async fn configure_snapshot_timer( - config_id: i32, - fsname: String, - interval: Duration, - use_barrier: bool, -) -> Result<(), ImlApiError> { - let iml_cmd = format!( - r#"/bin/bash -c "/usr/bin/date +\"%%Y-%%m-%%dT%%TZ\" | xargs -I %% /usr/bin/iml snapshot create {} -c 'automatically created by IML' {} {}-{}-%%""#, - if use_barrier { "-b" } else { "" }, - fsname, - config_id, - fsname - ); - - let timer_config = format!( - r#"# Automatically created by IML - -[Unit] -Description=Create snapshot on filesystem {} - -[Timer] -OnActiveSec={} -OnUnitActiveSec={} -AccuracySec=1us -Persistent=true - -[Install] -WantedBy=timers.target -"#, - fsname, - interval.as_secs(), - interval.as_secs() - ); - - let service_config = format!( - r#"# Automatically created by IML - -[Unit] -Description=Create snapshot on filesystem {} -{} - -[Service] -Type=oneshot -EnvironmentFile=/var/lib/chroma/iml-settings.conf -ExecStart={} -"#, - fsname, - if !running_in_docker() { - "After=iml-manager.target" - } else { - "" - }, - iml_cmd, - ); - - let config = TimerConfig { - config_id: config_id.to_string(), - file_prefix: "iml-snapshot".to_string(), - timer_config, - service_config, - }; - - let client = get_client()?; - - let url = format!("http://{}/configure/", get_timer_addr()); - tracing::debug!( - "Sending snapshot interval config to timer service: {:?} {:?}", - url, - config - ); - put(client, url.as_str(), config).await?; - - Ok(()) -} - -pub async fn remove_snapshot_timer(config_id: i32) -> Result<(), ImlApiError> { - let client = get_client()?; - - delete( - client, - format!( - "http://{}/unconfigure/iml-snapshot/{}", - get_timer_addr(), - config_id - ) - .as_str(), - serde_json::json!("{}"), - ) - .await?; - - Ok(()) -} diff --git a/iml-graphql-queries/src/snapshot.rs b/iml-graphql-queries/src/snapshot.rs index 54795d4b87..fd5a7a5d9b 100644 --- a/iml-graphql-queries/src/snapshot.rs +++ b/iml-graphql-queries/src/snapshot.rs @@ -238,205 +238,104 @@ pub mod list { } } -pub mod create_interval { - use crate::Query; - - pub static QUERY: &str = r#" - mutation CreateSnapshotInterval($fsname: String!, $interval: Duration!, $use_barrier: Boolean) { - createSnapshotInterval(fsname: $fsname, interval: $interval, useBarrier: $use_barrier) - } - "#; - - #[derive(Debug, serde::Serialize)] - pub struct Vars { - fsname: String, - interval: String, - use_barrier: Option, - } - - pub fn build( - fsname: impl ToString, - interval: String, - use_barrier: Option, - ) -> Query { - Query { - query: QUERY.to_string(), - variables: Some(Vars { - fsname: fsname.to_string(), - interval, - use_barrier, - }), - } - } - - #[derive(Debug, Clone, serde::Deserialize)] - pub struct Resp { - #[serde(rename(deserialize = "createSnapshotInterval"))] - pub create_snapshot_interval: bool, - } -} - -pub mod remove_interval { - use crate::Query; +pub mod policy { + pub mod list { + use crate::Query; + use iml_wire_types::snapshot::SnapshotPolicy; + + pub static QUERY: &str = r#" + query SnapshotPolicies { + snapshotPolicies { + id + filesystem + interval + barrier + keep + daily + weekly + monthly + last_run: lastRun + } + } + "#; - pub static QUERY: &str = r#" - mutation RemoveSnapshotInterval($id: Int!) { - removeSnapshotInterval(id: $id) + pub fn build() -> Query<()> { + Query { + query: QUERY.to_string(), + variables: None, + } } - "#; - #[derive(Debug, serde::Serialize)] - pub struct Vars { - id: i32, - } - - pub fn build(id: i32) -> Query { - Query { - query: QUERY.to_string(), - variables: Some(Vars { id }), + #[derive(Debug, Clone, serde::Deserialize)] + pub struct Resp { + #[serde(rename(deserialize = "snapshotPolicies"))] + pub snapshot_policies: Vec, } } - #[derive(Debug, Clone, serde::Deserialize)] - pub struct Resp { - #[serde(rename(deserialize = "removeSnapshotInterval"))] - pub remove_snapshot_interval: bool, - } -} - -pub mod list_intervals { - use crate::Query; - use iml_wire_types::snapshot::SnapshotInterval; + pub mod create { + use crate::Query; - pub static QUERY: &str = r#" - query SnapshotIntervals { - snapshotIntervals { - id - filesystem_name: filesystemName - use_barrier: useBarrier - interval - last_run: lastRun - } - } - "#; + pub static QUERY: &str = r#" + mutation CreateSnapshotPolicy($filesystem: String!, $interval: Duration!, $barrier: Boolean, + $keep: Int!, $daily: Int, $weekly: Int, $monthly: Int) { + createSnapshotPolicy(filesystem: $filesystem, interval: $interval, barrier: $barrier, + keep: $keep, daily: $daily, weekly: $weekly, monthly: $monthly) + } + "#; - pub fn build() -> Query<()> { - Query { - query: QUERY.to_string(), - variables: None, + #[derive(Debug, serde::Serialize, Default, Clone)] + pub struct Vars { + pub filesystem: String, + pub interval: String, + pub barrier: Option, + pub keep: i32, + pub daily: Option, + pub weekly: Option, + pub monthly: Option, } - } - #[derive(Debug, Clone, serde::Deserialize)] - pub struct Resp { - #[serde(rename(deserialize = "snapshotIntervals"))] - pub snapshot_intervals: Vec, - } -} - -/// Graphql query to create a new retention. Note that -/// Snapshots will automatically be deleted (starting with the oldest) -/// when free space falls below the defined reserve value and its associated unit. -pub mod create_retention { - use crate::Query; - use iml_wire_types::snapshot::ReserveUnit; - - pub static QUERY: &str = r#" - mutation CreateSnapshotRetention($fsname: String!, $reserve_value: Int!, $reserve_unit: ReserveUnit!, $keep_num: Int) { - createSnapshotRetention(fsname: $fsname, reserveValue: $reserve_value, reserveUnit: $reserve_unit, keepNum: $keep_num) + pub fn build(vars: Vars) -> Query { + Query { + query: QUERY.to_string(), + variables: Some(vars), + } } - "#; - - #[derive(Debug, serde::Serialize)] - pub struct Vars { - fsname: String, - reserve_value: u32, - reserve_unit: ReserveUnit, - keep_num: Option, - } - pub fn build( - fsname: impl ToString, - reserve_value: u32, - reserve_unit: ReserveUnit, - keep_num: Option, - ) -> Query { - Query { - query: QUERY.to_string(), - variables: Some(Vars { - fsname: fsname.to_string(), - reserve_value, - reserve_unit, - keep_num, - }), + #[derive(Debug, Clone, serde::Deserialize)] + pub struct Resp { + #[serde(rename(deserialize = "createSnapshotPolicy"))] + pub snapshot_policy: bool, } } - #[derive(Debug, Clone, serde::Deserialize)] - pub struct Resp { - #[serde(rename(deserialize = "createSnapshotRetention"))] - pub create_snapshot_retention: bool, - } -} - -pub mod remove_retention { - use crate::Query; - - pub static QUERY: &str = r#" - mutation RemoveSnapshotRetention($id: Int!) { - removeSnapshotRetention(id: $id) - } - "#; + pub mod remove { + use crate::Query; - #[derive(Debug, serde::Serialize)] - pub struct Vars { - id: i32, - } + pub static QUERY: &str = r#" + mutation RemoveSnapshotPolicy($filesystem: String!) { + removeSnapshotPolicy(filesystem: $filesystem) + } + "#; - pub fn build(id: i32) -> Query { - Query { - query: QUERY.to_string(), - variables: Some(Vars { id }), + #[derive(Debug, serde::Serialize, Default)] + pub struct Vars { + filesystem: String, } - } - - #[derive(Debug, Clone, serde::Deserialize)] - pub struct Resp { - #[serde(rename(deserialize = "removeSnapshotRetention"))] - pub remove_snapshot_retention: bool, - } -} - -/// Graphql query to list retentions. For each retention, snapshots will automatically -/// be deleted (starting with the oldest) when free space falls below the defined reserve -/// value and its associated unit. -pub mod list_retentions { - use crate::Query; - use iml_wire_types::snapshot::SnapshotRetention; - pub static QUERY: &str = r#" - query SnapshotRetentionPolicies { - snapshotRetentionPolicies { - id - filesystem_name: filesystemName - reserve_value: reserveValue - reserve_unit: reserveUnit - keep_num: keepNum - last_run: lastRun - } + pub fn build(filesystem: impl ToString) -> Query { + Query { + query: QUERY.to_string(), + variables: Some(Vars { + filesystem: filesystem.to_string(), + }), + } } - "#; - pub fn build() -> Query<()> { - Query { - query: QUERY.to_string(), - variables: None, + #[derive(Debug, Clone, serde::Deserialize)] + pub struct Resp { + #[serde(rename(deserialize = "removeSnapshotPolicy"))] + pub snapshot_policy: bool, } } - - #[derive(Debug, Clone, serde::Deserialize)] - pub struct Resp { - #[serde(rename(deserialize = "snapshotRetentionPolicies"))] - pub snapshot_retention_policies: Vec, - } } diff --git a/iml-gui/crate/src/lib.rs b/iml-gui/crate/src/lib.rs index 611826b78d..a9c1530477 100644 --- a/iml-gui/crate/src/lib.rs +++ b/iml-gui/crate/src/lib.rs @@ -682,11 +682,8 @@ fn handle_record_change( ArcRecord::Snapshot(x) => { model.records.snapshot.insert(x.id, Arc::clone(&x)); } - ArcRecord::SnapshotInterval(x) => { - model.records.snapshot_interval.insert(x.id, Arc::clone(&x)); - } - ArcRecord::SnapshotRetention(x) => { - model.records.snapshot_retention.insert(x.id, Arc::clone(&x)); + ArcRecord::SnapshotPolicy(x) => { + model.records.snapshot_policy.insert(x.id, Arc::clone(&x)); } ArcRecord::StratagemConfig(x) => { model.records.stratagem_config.insert(x.id, Arc::clone(&x)); diff --git a/iml-gui/crate/src/page/snapshot/add_interval.rs b/iml-gui/crate/src/page/snapshot/add_interval.rs deleted file mode 100644 index a5afcabf1c..0000000000 --- a/iml-gui/crate/src/page/snapshot/add_interval.rs +++ /dev/null @@ -1,316 +0,0 @@ -// Copyright (c) 2020 DDN. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -use crate::{ - components::{font_awesome, form, modal, Placement}, - extensions::{MergeAttrs as _, NodeExt as _}, - generated::css_classes::C, - key_codes, - page::{ - snapshot::{get_fs_names, help_indicator}, - RecordChange, - }, - GMsg, RequestExt, -}; -use iml_graphql_queries::{snapshot, Response}; -use iml_wire_types::{warp_drive::ArcCache, warp_drive::ArcRecord, warp_drive::RecordId, Filesystem}; -use seed::{prelude::*, *}; -use std::sync::Arc; - -#[derive(Debug)] -pub struct Model { - submitting: bool, - filesystems: Vec>, - fs_name: String, - barrier: bool, - interval_value: String, - interval_unit: String, - pub modal: modal::Model, -} - -impl Default for Model { - fn default() -> Self { - Model { - submitting: false, - filesystems: vec![], - fs_name: "".into(), - barrier: false, - interval_value: "".into(), - interval_unit: "d".into(), - modal: modal::Model::default(), - } - } -} - -impl RecordChange for Model { - fn update_record(&mut self, _: ArcRecord, cache: &ArcCache, orders: &mut impl Orders) { - orders.send_msg(Msg::SetFilesystems(cache.filesystem.values().cloned().collect())); - } - fn remove_record(&mut self, _: RecordId, cache: &ArcCache, orders: &mut impl Orders) { - orders.send_msg(Msg::SetFilesystems(cache.filesystem.values().cloned().collect())); - - let present = cache.filesystem.values().any(|x| x.name == self.fs_name); - - if !present { - let x = get_fs_names(cache).into_iter().next().unwrap_or_default(); - orders.send_msg(Msg::FsNameChanged(x)); - } - } - fn set_records(&mut self, cache: &ArcCache, orders: &mut impl Orders) { - orders.send_msg(Msg::SetFilesystems(cache.filesystem.values().cloned().collect())); - - let x = get_fs_names(cache).into_iter().next().unwrap_or_default(); - orders.send_msg(Msg::FsNameChanged(x)); - } -} - -#[derive(Clone, Debug)] -pub enum Msg { - Modal(modal::Msg), - Open, - Close, - SetFilesystems(Vec>), - BarrierChanged(String), - FsNameChanged(String), - IntervalValueChanged(String), - IntervalUnitChanged(String), - Submit, - SnapshotCreateIntervalResp(fetch::ResponseDataResult>), - Noop, -} - -pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { - match msg { - Msg::Modal(msg) => { - modal::update(msg, &mut model.modal, &mut orders.proxy(Msg::Modal)); - } - Msg::Open => { - model.modal.open = true; - } - Msg::Close => { - model.modal.open = false; - } - Msg::SetFilesystems(x) => { - model.filesystems = x; - } - Msg::FsNameChanged(x) => { - model.fs_name = x; - } - Msg::BarrierChanged(_) => { - model.barrier = !model.barrier; - } - Msg::IntervalValueChanged(x) => { - model.interval_value = x; - } - Msg::IntervalUnitChanged(x) => { - model.interval_unit = x; - } - Msg::Submit => { - model.submitting = true; - - let interval = format!("{}{}", model.interval_value.trim(), model.interval_unit); - - let query = snapshot::create_interval::build(&model.fs_name, interval, Some(model.barrier)); - - let req = fetch::Request::graphql_query(&query); - - orders.perform_cmd(req.fetch_json_data(|x| Msg::SnapshotCreateIntervalResp(x))); - } - Msg::SnapshotCreateIntervalResp(x) => { - model.submitting = false; - orders.send_msg(Msg::Close); - - match x { - Ok(Response::Data(_)) => { - *model = Model { - fs_name: model.fs_name.to_string(), - ..Model::default() - }; - } - Ok(Response::Errors(e)) => { - error!("An error has occurred during Snapshot Interval creation: ", e); - } - Err(e) => { - error!("An error has occurred during Snapshot Interval creation: ", e); - } - } - } - Msg::Noop => {} - }; -} - -pub fn view(model: &Model) -> Node { - let input_cls = class![ - C.appearance_none, - C.focus__outline_none, - C.focus__shadow_outline, - C.px_3, - C.py_2, - C.rounded_sm - ]; - - modal::bg_view( - model.modal.open, - Msg::Modal, - modal::content_view( - Msg::Modal, - div![ - modal::title_view(Msg::Modal, span!["Add Automated Snapshot Rule"]), - form![ - ev(Ev::Submit, move |event| { - event.prevent_default(); - Msg::Submit - }), - div![ - class![C.grid, C.grid_cols_2, C.gap_4, C.p_4, C.items_center], - label![attrs! {At::For => "interval_fs_name"}, "Filesystem Name"], - div![ - class![C.inline_block, C.relative, C.bg_gray_200], - select![ - id!["interval_fs_name"], - &input_cls, - class![ - C.block, - C.text_gray_800, - C.leading_tight, - C.bg_transparent, - C.pr_8, - C.rounded, - C.w_full - ], - model.filesystems.iter().map(|x| { - let mut opt = option![class![C.font_sans], attrs! {At::Value => x.name}, x.name]; - if x.name == model.fs_name.as_str() { - opt.add_attr(At::Selected.to_string(), "selected"); - } - - opt - }), - attrs! { - At::Required => true.as_at_value(), - }, - input_ev(Ev::Change, Msg::FsNameChanged), - ], - div![ - class![ - C.pointer_events_none, - C.absolute, - C.inset_y_0, - C.right_0, - C.flex, - C.items_center, - C.px_2, - C.text_gray_700, - ], - font_awesome(class![C.w_4, C.h_4, C.inline, C.ml_1], "chevron-down") - ], - ], - label![ - attrs! {At::For => "interval_value"}, - "Interval", - help_indicator("How often to take snapshot for selected filesystem", Placement::Right) - ], - div![ - class![C.grid, C.grid_cols_6], - input![ - &input_cls, - class![C.bg_gray_200, C.text_gray_800, C.col_span_4, C.rounded_r_none], - id!["interval_value"], - attrs! { - At::Type => "number", - At::Min => "1", - At::Placeholder => "Required", - At::Required => true.as_at_value(), - }, - input_ev(Ev::Change, Msg::IntervalValueChanged), - ], - div![ - class![C.inline_block, C.relative, C.col_span_2, C.text_white, C.bg_blue_500], - select![ - id!["interval_unit"], - &input_cls, - class![C.w_full, C.h_full C.rounded_l_none, C.bg_transparent], - option![class![C.font_sans], attrs! {At::Value => "d"}, "Days"], - option![class![C.font_sans], attrs! {At::Value => "y"}, "Years"], - option![class![C.font_sans], attrs! {At::Value => "m"}, "Minutes"], - attrs! { - At::Required => true.as_at_value(), - }, - input_ev(Ev::Change, Msg::IntervalUnitChanged), - ], - div![ - class![ - C.pointer_events_none, - C.absolute, - C.inset_y_0, - C.right_0, - C.flex, - C.items_center, - C.px_2, - C.text_white, - ], - font_awesome(class![C.w_4, C.h_4, C.inline, C.ml_1], "chevron-down") - ] - ], - ], - label![ - attrs! {At::For => "interval_barrier"}, - "Use Barrier", - help_indicator("Set write barrier before creating snapshot", Placement::Right) - ], - form::toggle() - .merge_attrs(id!["interval_barrier"]) - .merge_attrs(attrs! { - At::Checked => model.barrier.as_at_value() - }) - .with_listener(input_ev(Ev::Change, Msg::BarrierChanged)), - ], - modal::footer_view(vec![ - button![ - class![ - C.bg_blue_500, - C.duration_300, - C.flex, - C.form_invalid__bg_gray_500, - C.form_invalid__cursor_not_allowed, - C.form_invalid__pointer_events_none, - C.hover__bg_blue_400, - C.items_center - C.px_4, - C.py_2, - C.rounded_full, - C.text_white, - C.transition_colors, - ], - font_awesome(class![C.h_3, C.w_3, C.mr_1, C.inline], "plus"), - "Add Rule", - ], - button![ - class![ - C.bg_transparent, - C.duration_300, - C.hover__bg_gray_100, - C.hover__text_blue_400, - C.ml_2, - C.px_4, - C.py_2, - C.rounded_full, - C.text_blue_500, - C.transition_colors, - ], - simple_ev(Ev::Click, modal::Msg::Close), - "Cancel", - ] - .map_msg(Msg::Modal), - ]) - .merge_attrs(class![C.pt_8]) - ] - ], - ) - .with_listener(keyboard_ev(Ev::KeyDown, move |ev| match ev.key_code() { - key_codes::ESC => Msg::Modal(modal::Msg::Close), - _ => Msg::Noop, - })), - ) -} diff --git a/iml-gui/crate/src/page/snapshot/create_retention.rs b/iml-gui/crate/src/page/snapshot/create_policy.rs similarity index 54% rename from iml-gui/crate/src/page/snapshot/create_retention.rs rename to iml-gui/crate/src/page/snapshot/create_policy.rs index 4a4f8e3638..d353aaa344 100644 --- a/iml-gui/crate/src/page/snapshot/create_retention.rs +++ b/iml-gui/crate/src/page/snapshot/create_policy.rs @@ -3,7 +3,7 @@ // license that can be found in the LICENSE file. use crate::{ - components::{font_awesome, modal, Placement}, + components::{font_awesome, form, modal, Placement}, extensions::{MergeAttrs as _, NodeExt as _}, generated::css_classes::C, key_codes, @@ -14,35 +14,19 @@ use crate::{ GMsg, RequestExt, }; use iml_graphql_queries::{snapshot, Response}; -use iml_wire_types::{ - snapshot::ReserveUnit, warp_drive::ArcCache, warp_drive::ArcRecord, warp_drive::RecordId, Filesystem, -}; +use iml_wire_types::{warp_drive::ArcCache, warp_drive::ArcRecord, warp_drive::RecordId, Filesystem}; use seed::{prelude::*, *}; -use std::{str::FromStr, sync::Arc}; +use std::sync::Arc; -#[derive(Debug)] +#[derive(Debug, Default)] pub struct Model { + pub modal: modal::Model, submitting: bool, filesystems: Vec>, - fs_name: String, - reserve_value: u32, - reserve_unit: ReserveUnit, - keep_num: Option, - pub modal: modal::Model, -} -impl Default for Model { - fn default() -> Self { - Model { - submitting: false, - filesystems: vec![], - fs_name: "".into(), - reserve_value: 0, - reserve_unit: ReserveUnit::Percent, - keep_num: None, - modal: modal::Model::default(), - } - } + interval: i32, + interval_unit: String, + vars: snapshot::policy::create::Vars, } impl RecordChange for Model { @@ -52,18 +36,19 @@ impl RecordChange for Model { fn remove_record(&mut self, _: RecordId, cache: &ArcCache, orders: &mut impl Orders) { orders.send_msg(Msg::SetFilesystems(cache.filesystem.values().cloned().collect())); - let present = cache.filesystem.values().any(|x| x.name == self.fs_name); + let present = cache.filesystem.values().any(|x| x.name == self.vars.filesystem); if !present { let x = get_fs_names(cache).into_iter().next().unwrap_or_default(); - orders.send_msg(Msg::FsNameChanged(x)); + orders.send_msg(Msg::Input(Input::Filesystem(x))); } } fn set_records(&mut self, cache: &ArcCache, orders: &mut impl Orders) { orders.send_msg(Msg::SetFilesystems(cache.filesystem.values().cloned().collect())); let x = get_fs_names(cache).into_iter().next().unwrap_or_default(); - orders.send_msg(Msg::FsNameChanged(x)); + orders.send_msg(Msg::Input(Input::Filesystem(x))); + orders.send_msg(Msg::Input(Input::IntervalUnit("days".to_string()))); } } @@ -73,66 +58,56 @@ pub enum Msg { Open, Close, SetFilesystems(Vec>), - KeepNumChanged(String), - FsNameChanged(String), - ReserveValueChanged(String), - ReserveUnitChanged(String), + Input(Input), Submit, - SnapshotCreateRetentionResp(fetch::ResponseDataResult>), + CreatePolicyResp(fetch::ResponseDataResult>), Noop, } +#[derive(Clone, Debug)] +pub enum Input { + Filesystem(String), + Interval(i32), + IntervalUnit(String), + ToggleBarrier, + Keep(i32), + Daily(Option), + Monthly(Option), + Weekly(Option), +} + pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { match msg { - Msg::Modal(msg) => { - modal::update(msg, &mut model.modal, &mut orders.proxy(Msg::Modal)); - } - Msg::Open => { - model.modal.open = true; - } - Msg::Close => { - model.modal.open = false; - } Msg::SetFilesystems(x) => { model.filesystems = x; } - Msg::FsNameChanged(x) => { - model.fs_name = x; - } - Msg::KeepNumChanged(x) => { - model.keep_num = x.parse().ok(); - } - Msg::ReserveValueChanged(x) => { - model.reserve_value = x.parse().unwrap(); - } - Msg::ReserveUnitChanged(x) => { - model.reserve_unit = ReserveUnit::from_str(&x).unwrap(); - } + Msg::Input(x) => match x { + Input::Interval(i) => model.interval = i, + Input::IntervalUnit(i) => model.interval_unit = i, + + Input::Filesystem(i) => model.vars.filesystem = i, + Input::ToggleBarrier => model.vars.barrier = Some(!model.vars.barrier.unwrap_or(false)), + Input::Keep(i) => model.vars.keep = i, + Input::Daily(i) => model.vars.daily = i, + Input::Weekly(i) => model.vars.weekly = i, + Input::Monthly(i) => model.vars.monthly = i, + }, Msg::Submit => { model.submitting = true; + model.vars.interval = format!("{}{}", model.interval, model.interval_unit); - let query = snapshot::create_retention::build( - &model.fs_name, - model.reserve_value, - model.reserve_unit, - model.keep_num, - ); + let query = snapshot::policy::create::build(model.vars.clone()); let req = fetch::Request::graphql_query(&query); - orders.perform_cmd(req.fetch_json_data(|x| Msg::SnapshotCreateRetentionResp(x))); + orders.perform_cmd(req.fetch_json_data(|x| Msg::CreatePolicyResp(x))); } - Msg::SnapshotCreateRetentionResp(x) => { + Msg::CreatePolicyResp(x) => { model.submitting = false; orders.send_msg(Msg::Close); match x { - Ok(Response::Data(_)) => { - *model = Model { - fs_name: model.fs_name.to_string(), - ..Model::default() - }; - } + Ok(Response::Data(_)) => {} Ok(Response::Errors(e)) => { error!("An error has occurred during policy creation: ", e); } @@ -141,10 +116,45 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) } } } + Msg::Modal(msg) => { + modal::update(msg, &mut model.modal, &mut orders.proxy(Msg::Modal)); + } + Msg::Open => { + model.modal.open = true; + } + Msg::Close => { + model.modal.open = false; + } Msg::Noop => {} }; } +// FIXME: this function was created to help rustfmt only +fn interval_unit_options(selected: &str) -> Vec> { + vec![ + option![ + class![C.font_sans], + attrs! {At::Value => "minutes", At::Selected => (selected == "minutes").as_at_value()}, + "Minutes" + ], + option![ + class![C.font_sans], + attrs! {At::Value => "hours", At::Selected => (selected == "hours").as_at_value()}, + "Hours" + ], + option![ + class![C.font_sans], + attrs! {At::Value => "days", At::Selected => (selected == "days").as_at_value()}, + "Days" + ], + option![ + class![C.font_sans], + attrs! {At::Value => "years", At::Selected => (selected == "years").as_at_value()}, + "Years" + ], + ] +} + pub fn view(model: &Model) -> Node { let input_cls = class![ C.appearance_none, @@ -161,7 +171,7 @@ pub fn view(model: &Model) -> Node { modal::content_view( Msg::Modal, div![ - modal::title_view(Msg::Modal, span!["Create Snapshot Retention Policy"]), + modal::title_view(Msg::Modal, span!["Create Automatic Snapshot Policy"]), form![ ev(Ev::Submit, move |event| { event.prevent_default(); @@ -169,11 +179,11 @@ pub fn view(model: &Model) -> Node { }), div![ class![C.grid, C.grid_cols_2, C.gap_4, C.p_4, C.items_center], - label![attrs! {At::For => "retention_fs_name"}, "Filesystem Name"], + label![attrs! {At::For => "policy_filesystem"}, "Filesystem Name"], div![ class![C.inline_block, C.relative, C.bg_gray_200], select![ - id!["retention_fs_name"], + id!["policy_filesystem"], &input_cls, class![ C.block, @@ -186,7 +196,7 @@ pub fn view(model: &Model) -> Node { ], model.filesystems.iter().map(|x| { let mut opt = option![class![C.font_sans], attrs! {At::Value => x.name}, x.name]; - if x.name == model.fs_name.as_str() { + if x.name == model.vars.filesystem.as_str() { opt.add_attr(At::Selected.to_string(), "selected"); } @@ -195,7 +205,7 @@ pub fn view(model: &Model) -> Node { attrs! { At::Required => true.as_at_value(), }, - input_ev(Ev::Change, Msg::FsNameChanged), + input_ev(Ev::Change, |s| Msg::Input(Input::Filesystem(s))), ], div![ class![ @@ -212,10 +222,10 @@ pub fn view(model: &Model) -> Node { ], ], label![ - attrs! {At::For => "reserve_value"}, - "Reserve", + attrs! {At::For => "policy_interval"}, + "Interval", help_indicator( - "Delete the oldest snapshot when available space falls below this value.", + "How often to take a snapshot for a selected filesystem", Placement::Right ) ], @@ -224,28 +234,29 @@ pub fn view(model: &Model) -> Node { input![ &input_cls, class![C.bg_gray_200, C.text_gray_800, C.col_span_4, C.rounded_r_none], - id!["reserve_value"], + id!["policy_interval"], attrs! { At::Type => "number", - At::Min => "0", + At::Min => "1", At::Placeholder => "Required", At::Required => true.as_at_value(), }, - input_ev(Ev::Change, Msg::ReserveValueChanged), + input_ev(Ev::Change, |s| s + .parse() + .map(|i| Msg::Input(Input::Interval(i))) + .unwrap_or(Msg::Noop)), ], div![ class![C.inline_block, C.relative, C.col_span_2, C.text_white, C.bg_blue_500], select![ - id!["reserve_unit"], + id!["interval_unit"], &input_cls, class![C.w_full, C.h_full C.rounded_l_none, C.bg_transparent], - option![class![C.font_sans], attrs! {At::Value => "percent"}, "%"], - option![class![C.font_sans], attrs! {At::Value => "gibibytes"}, "GiB"], - option![class![C.font_sans], attrs! {At::Value => "tebibytes"}, "TiB"], + interval_unit_options(&model.interval_unit), attrs! { At::Required => true.as_at_value(), }, - input_ev(Ev::Change, Msg::ReserveUnitChanged), + input_ev(Ev::Change, |s| Msg::Input(Input::IntervalUnit(s))), ], div![ class![ @@ -263,22 +274,96 @@ pub fn view(model: &Model) -> Node { ], ], label![ - attrs! {At::For => "keep_num"}, - "Minimum Snapshots", - help_indicator("Minimum number of snapshots to keep", Placement::Right) + attrs! {At::For => "policy_keep"}, + "Keep Recent Snapshots", + help_indicator("Number of the most recent snapshots to keep", Placement::Right) ], input![ &input_cls, class![C.bg_gray_200, C.text_gray_800, C.rounded_r_none], - id!["keep_num"], + id!["policy_keep"], + attrs! { + At::Type => "number", + At::Min => "1", + At::Placeholder => "Required", + At::Required => true.as_at_value(), + }, + input_ev(Ev::Change, |s| s + .parse() + .map(|i| Msg::Input(Input::Keep(i))) + .unwrap_or(Msg::Noop)), + ], + label![ + attrs! {At::For => "policy_daily"}, + "Daily Snapshots", + help_indicator( + "Number of days when keep the most recent snapshot of each day", + Placement::Right + ) + ], + input![ + &input_cls, + class![C.bg_gray_200, C.text_gray_800, C.rounded_r_none], + id!["policy_daily"], + attrs! { + At::Type => "number", + At::Min => "0", + At::Placeholder => "Optional", + At::Required => false.as_at_value(), + }, + input_ev(Ev::Change, |s| Msg::Input(Input::Daily(s.parse().ok()))), + ], + label![ + attrs! {At::For => "policy_weekly"}, + "Weekly Snapshots", + help_indicator( + "Number of weeks when keep the most recent snapshot of each week", + Placement::Right + ) + ], + input![ + &input_cls, + class![C.bg_gray_200, C.text_gray_800, C.rounded_r_none], + id!["policy_weekly"], attrs! { At::Type => "number", At::Min => "0", - At::Placeholder => "Optional (default: 0)", + At::Placeholder => "Optional", At::Required => false.as_at_value(), }, - input_ev(Ev::Change, Msg::KeepNumChanged), + input_ev(Ev::Change, |s| Msg::Input(Input::Weekly(s.parse().ok()))), + ], + label![ + attrs! {At::For => "policy_monthly"}, + "Monthly Snapshots", + help_indicator( + "Number of months when keep the most recent snapshot of each months", + Placement::Right + ) + ], + input![ + &input_cls, + class![C.bg_gray_200, C.text_gray_800, C.rounded_r_none], + id!["policy_monthly"], + attrs! { + At::Type => "number", + At::Min => "0", + At::Placeholder => "Optional", + At::Required => false.as_at_value(), + }, + input_ev(Ev::Change, |s| Msg::Input(Input::Monthly(s.parse().ok()))), + ], + label![ + attrs! {At::For => "policy_barrier"}, + "Use Barrier", + help_indicator("Set write barrier before creating snapshot", Placement::Right) ], + form::toggle() + .merge_attrs(id!["policy_barrier"]) + .merge_attrs(attrs! { + At::Checked => model.vars.barrier.unwrap_or(false).as_at_value() + }) + .with_listener(input_ev(Ev::Change, |_| Msg::Input(Input::ToggleBarrier))), ], modal::footer_view(vec![ button![ diff --git a/iml-gui/crate/src/page/snapshot/list_interval.rs b/iml-gui/crate/src/page/snapshot/list_interval.rs deleted file mode 100644 index 2863781b4a..0000000000 --- a/iml-gui/crate/src/page/snapshot/list_interval.rs +++ /dev/null @@ -1,211 +0,0 @@ -// Copyright (c) 2020 DDN. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -use super::*; -use crate::{extensions::RequestExt, font_awesome}; -use chrono_humanize::{Accuracy, HumanTime, Tense}; -use iml_wire_types::snapshot::SnapshotInterval; -use std::time::Duration; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum SortField { - FilesystemName, - Interval, -} - -impl Default for SortField { - fn default() -> Self { - Self::FilesystemName - } -} - -#[derive(Clone, Debug)] -pub enum Msg { - Page(paging::Msg), - Sort, - Delete(Arc), - SnapshotDeleteIntervalResp(fetch::ResponseDataResult>), - SortBy(table::SortBy), -} - -#[derive(Default, Debug)] -pub struct Model { - pager: paging::Model, - rows: Vec>, - sort: (SortField, paging::Dir), - take: take::Model, -} - -pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { - match msg { - Msg::SortBy(table::SortBy(x)) => { - let dir = if x == model.sort.0 { - model.sort.1.next() - } else { - paging::Dir::default() - }; - - model.sort = (x, dir); - - orders.send_msg(Msg::Sort); - } - Msg::Page(msg) => { - paging::update(msg, &mut model.pager, &mut orders.proxy(Msg::Page)); - } - Msg::Sort => { - let sort_fn = match model.sort { - (SortField::FilesystemName, paging::Dir::Asc) => { - Box::new(|a: &Arc, b: &Arc| { - natord::compare(&a.filesystem_name, &b.filesystem_name) - }) as Box, &Arc) -> Ordering> - } - (SortField::FilesystemName, paging::Dir::Desc) => { - Box::new(|a: &Arc, b: &Arc| { - natord::compare(&b.filesystem_name, &a.filesystem_name) - }) - } - (SortField::Interval, paging::Dir::Asc) => { - Box::new(|a: &Arc, b: &Arc| { - a.interval.0.partial_cmp(&b.interval.0).unwrap() - }) - } - (SortField::Interval, paging::Dir::Desc) => { - Box::new(|a: &Arc, b: &Arc| { - b.interval.0.partial_cmp(&a.interval.0).unwrap() - }) - } - }; - - model.rows.sort_by(sort_fn); - } - Msg::Delete(x) => { - if let Ok(true) = window().confirm_with_message("Are you sure you want to delete this interval?") { - let query = snapshot::remove_interval::build(x.id); - - let req = fetch::Request::graphql_query(&query); - - orders.perform_cmd(req.fetch_json_data(|x| Msg::SnapshotDeleteIntervalResp(x))); - } - } - Msg::SnapshotDeleteIntervalResp(x) => match x { - Ok(Response::Data(_)) => {} - Ok(Response::Errors(e)) => { - error!("An error has occurred during Snapshot deletion: ", e); - } - Err(e) => { - error!("An error has occurred during Snapshot deletion: ", e); - } - }, - }; -} - -impl RecordChange for Model { - fn update_record(&mut self, _: ArcRecord, cache: &ArcCache, orders: &mut impl Orders) { - self.rows = cache.snapshot_interval.values().cloned().collect(); - - orders.proxy(Msg::Page).send_msg(paging::Msg::SetTotal(self.rows.len())); - - orders.send_msg(Msg::Sort); - } - fn remove_record(&mut self, _: RecordId, cache: &ArcCache, orders: &mut impl Orders) { - self.rows = cache.snapshot_interval.values().cloned().collect(); - - orders.proxy(Msg::Page).send_msg(paging::Msg::SetTotal(self.rows.len())); - - orders.send_msg(Msg::Sort); - } - fn set_records(&mut self, cache: &ArcCache, orders: &mut impl Orders) { - self.rows = cache.snapshot_interval.values().cloned().collect(); - - orders.proxy(Msg::Page).send_msg(paging::Msg::SetTotal(self.rows.len())); - - orders.send_msg(Msg::Sort); - } -} - -pub fn view(model: &Model, cache: &ArcCache, session: Option<&Session>) -> Node { - panel::view( - h3![class![C.py_4, C.font_normal, C.text_lg], "Automated Snapshot Rules"], - div![ - table::wrapper_view(vec![ - table::thead_view(vec![ - table::sort_header("FS Name", SortField::FilesystemName, model.sort.0, model.sort.1) - .map_msg(Msg::SortBy), - table::sort_header("Interval", SortField::Interval, model.sort.0, model.sort.1) - .map_msg(Msg::SortBy), - table::th_view(plain!["Use Barrier"]), - table::th_view(plain!["Last Run"]), - restrict::view(session, GroupType::FilesystemAdministrators, th![]), - ]), - tbody![model.rows[model.pager.range()].iter().map(|x| { - tr![ - td![ - table::td_cls(), - class![C.text_center], - match get_fs_by_name(cache, &x.filesystem_name) { - Some(x) => { - div![resource_links::fs_link(&x)] - } - None => { - plain![x.filesystem_name.to_string()] - } - } - ], - table::td_center(plain![display_interval(x.interval.0)]), - table::td_center(plain![match x.use_barrier { - true => { - "yes" - } - false => { - "no" - } - }]), - table::td_center(plain![x - .last_run - .map(|x| x.format("%m/%d/%Y %H:%M:%S").to_string()) - .unwrap_or_else(|| "---".to_string())]), - td![ - class![C.flex, C.justify_center, C.p_4, C.px_3], - restrict::view( - session, - GroupType::FilesystemAdministrators, - button![ - class![ - C.bg_blue_500, - C.duration_300, - C.flex, - C.hover__bg_blue_400, - C.items_center, - C.px_6, - C.py_2, - C.rounded_sm, - C.text_white, - C.transition_colors, - ], - font_awesome(class![C.w_3, C.h_3, C.inline, C.mr_1], "trash"), - "Delete Rule", - simple_ev(Ev::Click, Msg::Delete(Arc::clone(&x))) - ] - ) - ] - ] - })] - ]) - .merge_attrs(class![C.my_6]), - div![ - class![C.flex, C.justify_end, C.py_1, C.pr_3], - paging::limit_selection_view(&model.pager).map_msg(Msg::Page), - paging::page_count_view(&model.pager), - paging::next_prev_view(&model.pager).map_msg(Msg::Page) - ] - ], - ) -} - -fn display_interval(x: Duration) -> String { - chrono::Duration::from_std(x) - .map(HumanTime::from) - .map(|x| x.to_text_en(Accuracy::Precise, Tense::Present)) - .unwrap_or("---".into()) -} diff --git a/iml-gui/crate/src/page/snapshot/list_retention.rs b/iml-gui/crate/src/page/snapshot/list_policy.rs similarity index 68% rename from iml-gui/crate/src/page/snapshot/list_retention.rs rename to iml-gui/crate/src/page/snapshot/list_policy.rs index 597b01ec3f..b0c871a87d 100644 --- a/iml-gui/crate/src/page/snapshot/list_retention.rs +++ b/iml-gui/crate/src/page/snapshot/list_policy.rs @@ -4,19 +4,20 @@ use super::*; use crate::{extensions::RequestExt, font_awesome}; -use iml_wire_types::snapshot::{ReserveUnit, SnapshotRetention}; +use chrono_humanize::{Accuracy, HumanTime, Tense}; +use iml_wire_types::snapshot::SnapshotPolicy; #[derive(Clone, Debug)] pub enum Msg { Page(paging::Msg), - Delete(Arc), - DeleteRetentionResp(fetch::ResponseDataResult>), + Delete(Arc), + DeleteResp(fetch::ResponseDataResult>), } #[derive(Default, Debug)] pub struct Model { pager: paging::Model, - rows: Vec>, + rows: Vec>, take: take::Model, } @@ -26,21 +27,22 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) paging::update(msg, &mut model.pager, &mut orders.proxy(Msg::Page)); } Msg::Delete(x) => { - if let Ok(true) = window().confirm_with_message("Are you sure you want to delete this retention policy?") { - let query = snapshot::remove_retention::build(x.id); + if let Ok(true) = window().confirm_with_message(&format!("Delete snapshot policy for '{}' ?", x.filesystem)) + { + let query = snapshot::policy::remove::build(&x.filesystem); let req = fetch::Request::graphql_query(&query); - orders.perform_cmd(req.fetch_json_data(|x| Msg::DeleteRetentionResp(x))); + orders.perform_cmd(req.fetch_json_data(Msg::DeleteResp)); } } - Msg::DeleteRetentionResp(x) => match x { + Msg::DeleteResp(x) => match x { Ok(Response::Data(_)) => {} Ok(Response::Errors(e)) => { - error!("An error has occurred during Snapshot deletion: ", e); + error!("An error has occurred during deletion: ", e); } Err(e) => { - error!("An error has occurred during Snapshot deletion: ", e); + error!("An error has occurred during deletion: ", e); } }, }; @@ -48,17 +50,17 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) impl RecordChange for Model { fn update_record(&mut self, _: ArcRecord, cache: &ArcCache, orders: &mut impl Orders) { - self.rows = cache.snapshot_retention.values().cloned().collect(); + self.rows = cache.snapshot_policy.values().cloned().collect(); orders.proxy(Msg::Page).send_msg(paging::Msg::SetTotal(self.rows.len())); } fn remove_record(&mut self, _: RecordId, cache: &ArcCache, orders: &mut impl Orders) { - self.rows = cache.snapshot_retention.values().cloned().collect(); + self.rows = cache.snapshot_policy.values().cloned().collect(); orders.proxy(Msg::Page).send_msg(paging::Msg::SetTotal(self.rows.len())); } fn set_records(&mut self, cache: &ArcCache, orders: &mut impl Orders) { - self.rows = cache.snapshot_retention.values().cloned().collect(); + self.rows = cache.snapshot_policy.values().cloned().collect(); orders.proxy(Msg::Page).send_msg(paging::Msg::SetTotal(self.rows.len())); } @@ -66,13 +68,17 @@ impl RecordChange for Model { pub fn view(model: &Model, cache: &ArcCache, session: Option<&Session>) -> Node { panel::view( - h3![class![C.py_4, C.font_normal, C.text_lg], "Snapshot Retention Policies"], + h3![class![C.py_4, C.font_normal, C.text_lg], "Automatic Snapshot Policies"], div![ table::wrapper_view(vec![ table::thead_view(vec![ table::th_view(plain!["Filesystem"]), - table::th_view(plain!["Reserve"]), + table::th_view(plain!["Interval"]), table::th_view(plain!["Keep"]), + table::th_view(plain!["Daily"]), + table::th_view(plain!["Weekly"]), + table::th_view(plain!["Monthly"]), + table::th_view(plain!["Barrier"]), table::th_view(plain!["Last Run"]), restrict::view(session, GroupType::FilesystemAdministrators, th![]), ]), @@ -81,25 +87,24 @@ pub fn view(model: &Model, cache: &ArcCache, session: Option<&Session>) -> Node< td![ table::td_cls(), class![C.text_center], - match get_fs_by_name(cache, &x.filesystem_name) { + match get_fs_by_name(cache, &x.filesystem) { Some(x) => { div![resource_links::fs_link(&x)] } None => { - plain![x.filesystem_name.to_string()] + plain![x.filesystem.to_string()] } } ], - table::td_center(plain![format!( - "{} {}", - x.reserve_value, - match x.reserve_unit { - ReserveUnit::Percent => "%", - ReserveUnit::Gibibytes => "GiB", - ReserveUnit::Tebibytes => "TiB", - } - )]), - table::td_center(plain![x.keep_num.to_string()]), + table::td_center(plain![chrono::Duration::from_std(x.interval.0) + .map(HumanTime::from) + .map(|x| x.to_text_en(Accuracy::Precise, Tense::Present)) + .unwrap_or("---".into())]), + table::td_center(plain![x.keep.to_string()]), + table::td_center(plain![x.daily.to_string()]), + table::td_center(plain![x.weekly.to_string()]), + table::td_center(plain![x.monthly.to_string()]), + table::td_center(plain![x.barrier.to_string()]), table::td_center(plain![x .last_run .map(|x| x.format("%m/%d/%Y %H:%M:%S").to_string()) diff --git a/iml-gui/crate/src/page/snapshot/mod.rs b/iml-gui/crate/src/page/snapshot/mod.rs index 9b419bd509..8d74f7662e 100644 --- a/iml-gui/crate/src/page/snapshot/mod.rs +++ b/iml-gui/crate/src/page/snapshot/mod.rs @@ -2,11 +2,9 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -mod add_interval; -mod create_retention; +mod create_policy; mod list; -mod list_interval; -mod list_retention; +mod list_policy; mod take; use crate::{ @@ -31,11 +29,9 @@ use std::{cmp::Ordering, ops::Deref, sync::Arc}; #[derive(Default, Debug)] pub struct Model { take: take::Model, - list_interval: list_interval::Model, - list_retention: list_retention::Model, list: list::Model, - add_interval: add_interval::Model, - create_retention: create_retention::Model, + create_policy: create_policy::Model, + list_policy: list_policy::Model, } impl RecordChange for Model { @@ -43,64 +39,41 @@ impl RecordChange for Model { self.list .update_record(record.clone(), cache, &mut orders.proxy(Msg::List)); - self.list_interval - .update_record(record.clone(), cache, &mut orders.proxy(Msg::ListInterval)); + self.take + .update_record(record.clone(), cache, &mut orders.proxy(Msg::Take)); - self.list_retention - .update_record(record.clone(), cache, &mut orders.proxy(Msg::ListRetention)); - - self.add_interval - .update_record(record.clone(), cache, &mut orders.proxy(Msg::AddInterval)); - - self.create_retention - .update_record(record.clone(), cache, &mut orders.proxy(Msg::CreatRetention)); - - self.take.update_record(record, cache, &mut orders.proxy(Msg::Take)); + self.list_policy + .update_record(record.clone(), cache, &mut orders.proxy(Msg::ListPolicy)); + self.create_policy + .update_record(record, cache, &mut orders.proxy(Msg::CreatePolicy)); } fn remove_record(&mut self, record: RecordId, cache: &ArcCache, orders: &mut impl Orders) { self.list.remove_record(record, cache, &mut orders.proxy(Msg::List)); - self.list_interval - .remove_record(record, cache, &mut orders.proxy(Msg::ListInterval)); - - self.list_retention - .remove_record(record, cache, &mut orders.proxy(Msg::ListRetention)); - - self.add_interval - .remove_record(record, cache, &mut orders.proxy(Msg::AddInterval)); - - self.create_retention - .remove_record(record, cache, &mut orders.proxy(Msg::CreatRetention)); - self.take.remove_record(record, cache, &mut orders.proxy(Msg::Take)); + + self.list_policy + .remove_record(record, cache, &mut orders.proxy(Msg::ListPolicy)); + self.create_policy + .remove_record(record, cache, &mut orders.proxy(Msg::CreatePolicy)); } fn set_records(&mut self, cache: &ArcCache, orders: &mut impl Orders) { self.list.set_records(cache, &mut orders.proxy(Msg::List)); - self.list_interval - .set_records(cache, &mut orders.proxy(Msg::ListInterval)); - - self.list_retention - .set_records(cache, &mut orders.proxy(Msg::ListRetention)); - - self.add_interval - .set_records(cache, &mut orders.proxy(Msg::AddInterval)); - - self.create_retention - .set_records(cache, &mut orders.proxy(Msg::CreatRetention)); - self.take.set_records(cache, &mut orders.proxy(Msg::Take)); + + self.list_policy.set_records(cache, &mut orders.proxy(Msg::ListPolicy)); + self.create_policy + .set_records(cache, &mut orders.proxy(Msg::CreatePolicy)); } } #[derive(Clone, Debug)] pub enum Msg { Take(take::Msg), - ListInterval(list_interval::Msg), - ListRetention(list_retention::Msg), List(list::Msg), - AddInterval(add_interval::Msg), - CreatRetention(create_retention::Msg), + CreatePolicy(create_policy::Msg), + ListPolicy(list_policy::Msg), } pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { @@ -108,20 +81,14 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) Msg::Take(msg) => { take::update(msg, &mut model.take, &mut orders.proxy(Msg::Take)); } - Msg::ListInterval(msg) => { - list_interval::update(msg, &mut model.list_interval, &mut orders.proxy(Msg::ListInterval)); - } - Msg::ListRetention(msg) => { - list_retention::update(msg, &mut model.list_retention, &mut orders.proxy(Msg::ListRetention)); - } Msg::List(msg) => { list::update(msg, &mut model.list, &mut orders.proxy(Msg::List)); } - Msg::AddInterval(msg) => { - add_interval::update(msg, &mut model.add_interval, &mut orders.proxy(Msg::AddInterval)); + Msg::CreatePolicy(msg) => { + create_policy::update(msg, &mut model.create_policy, &mut orders.proxy(Msg::CreatePolicy)); } - Msg::CreatRetention(msg) => { - create_retention::update(msg, &mut model.create_retention, &mut orders.proxy(Msg::CreatRetention)); + Msg::ListPolicy(msg) => { + list_policy::update(msg, &mut model.list_policy, &mut orders.proxy(Msg::ListPolicy)); } } } @@ -142,60 +109,20 @@ pub fn view(model: &Model, cache: &ArcCache, session: Option<&Session>) -> impl div![ take::view(&model.take).map_msg(Msg::Take).merge_attrs(class![C.my_6]), - if cache.snapshot_interval.is_empty() { - vec![add_interval_btn(false, session)] - } else { - vec![ - list_interval::view(&model.list_interval, cache, session) - .map_msg(Msg::ListInterval) - .merge_attrs(class![C.my_6]), - add_interval_btn(true, session), - ] - }, - add_interval::view(&model.add_interval).map_msg(Msg::AddInterval), - if cache.snapshot_retention.is_empty() { + if cache.snapshot_policy.is_empty() { empty![] } else { - list_retention::view(&model.list_retention, cache, session) - .map_msg(Msg::ListRetention) + list_policy::view(&model.list_policy, cache, session) + .map_msg(Msg::ListPolicy) .merge_attrs(class![C.my_6]) }, - create_retention_btn(session), - create_retention::view(&model.create_retention).map_msg(Msg::CreatRetention), + create_policy_btn(session), + create_policy::view(&model.create_policy).map_msg(Msg::CreatePolicy), list::view(&model.list, cache).map_msg(Msg::List) ] } -fn add_interval_btn(has_intervals: bool, session: Option<&Session>) -> Node { - restrict::view( - session, - GroupType::FilesystemAdministrators, - button![ - class![ - C.bg_blue_500, - C.duration_300, - C.flex, - C.hover__bg_blue_400, - C.items_center, - C.mb_6, - C.px_6, - C.py_2, - C.rounded_sm, - C.text_white, - C.transition_colors - ], - font_awesome(class![C.h_3, C.w_3, C.mr_1, C.inline], "plus"), - if has_intervals { - "Add Another Automated Snapshot Rule" - } else { - "Add Automated Snapshot Rule" - }, - simple_ev(Ev::Click, add_interval::Msg::Open).map_msg(Msg::AddInterval) - ], - ) -} - -fn create_retention_btn(session: Option<&Session>) -> Node { +fn create_policy_btn(session: Option<&Session>) -> Node { restrict::view( session, GroupType::FilesystemAdministrators, @@ -214,8 +141,8 @@ fn create_retention_btn(session: Option<&Session>) -> Node { C.transition_colors ], font_awesome(class![C.h_3, C.w_3, C.mr_1, C.inline], "plus"), - "Create Snapshot Retention Policy", - simple_ev(Ev::Click, create_retention::Msg::Open).map_msg(Msg::CreatRetention) + "Create Automatic Snapshot Policy", + simple_ev(Ev::Click, create_policy::Msg::Open).map_msg(Msg::CreatePolicy) ], ) } diff --git a/iml-gui/crate/src/test_utils/fixture.json b/iml-gui/crate/src/test_utils/fixture.json index 38e193be67..5ed8ca4b91 100644 --- a/iml-gui/crate/src/test_utils/fixture.json +++ b/iml-gui/crate/src/test_utils/fixture.json @@ -1221,8 +1221,7 @@ "sfa_storage_system": {}, "sfa_controller": {}, "snapshot": {}, - "snapshot_interval": {}, - "snapshot_retention": {}, + "snapshot_policy": {}, "stratagem_config": {}, "target": { "19": { diff --git a/iml-manager-cli/src/display_utils.rs b/iml-manager-cli/src/display_utils.rs index 430cf6f186..5c6f7a871e 100644 --- a/iml-manager-cli/src/display_utils.rs +++ b/iml-manager-cli/src/display_utils.rs @@ -2,13 +2,12 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -use chrono_humanize::{Accuracy, HumanTime, Tense}; use console::style; use futures::{Future, FutureExt}; use iml_wire_types::{ db::TargetRecord, graphql::ServerProfile, - snapshot::{ReserveUnit, Snapshot, SnapshotInterval, SnapshotRetention}, + snapshot::{Snapshot, SnapshotPolicy}, Command, Filesystem, Host, OstPool, StratagemConfiguration, StratagemReport, }; use indicatif::ProgressBar; @@ -138,47 +137,29 @@ impl IntoTable for Vec { } } -impl IntoTable for Vec { +impl IntoTable for Vec { fn into_table(self) -> Table { generate_table( - &["Id", "Filesystem", "Interval", "Use Barrier", "Last Run"], - self.into_iter().map(|i| { - vec![ - i.id.to_string(), - i.filesystem_name, - chrono::Duration::from_std(i.interval.0) - .map(HumanTime::from) - .map(|x| x.to_text_en(Accuracy::Precise, Tense::Present)) - .unwrap_or_else(|_| "---".to_string()), - i.use_barrier.to_string(), - i.last_run - .map(|t| t.to_rfc2822()) - .unwrap_or_else(|| "---".to_string()), - ] - }), - ) - } -} - -impl IntoTable for Vec { - fn into_table(self) -> Table { - generate_table( - &["Id", "Filesystem", "Reserve", "Keep", "Last Run"], - self.into_iter().map(|r| { + &[ + "Filesystem", + "Interval", + "Keep", + "Daily", + "Weekly", + "Monthly", + "Barrier", + "Last Run", + ], + self.into_iter().map(|p| { vec![ - r.id.to_string(), - r.filesystem_name, - format!( - "{} {}", - r.reserve_value, - match r.reserve_unit { - ReserveUnit::Percent => "%", - ReserveUnit::Gibibytes => "GiB", - ReserveUnit::Tebibytes => "TiB", - } - ), - r.keep_num.to_string(), - r.last_run + p.filesystem, + p.interval.to_string(), + p.keep.to_string(), + p.daily.to_string(), + p.weekly.to_string(), + p.monthly.to_string(), + p.barrier.to_string(), + p.last_run .map(|t| t.to_rfc2822()) .unwrap_or_else(|| "---".to_string()), ] diff --git a/iml-manager-cli/src/snapshot.rs b/iml-manager-cli/src/snapshot.rs index 590d8008c1..aa7d50dd7a 100644 --- a/iml-manager-cli/src/snapshot.rs +++ b/iml-manager-cli/src/snapshot.rs @@ -14,57 +14,50 @@ use iml_wire_types::snapshot; use structopt::StructOpt; #[derive(Debug, StructOpt)] -pub enum IntervalCommand { - /// List snapshots intervals +pub enum PolicyCommand { + /// List snapshot policies (default) List { /// Display type: json, yaml, tabular #[structopt(short = "d", long = "display", default_value = "tabular")] display_type: DisplayType, }, - /// Add new snapshot interval - Add { + /// Create or update snapshot policy + Create { + /// Filesystem to create a snapshot policy for + filesystem: String, + /// Automatic snapshot interval in human form, e. g. 1hour + #[structopt()] + interval: String, /// Use barrier when creating snapshots #[structopt(short = "b", long = "barrier")] barrier: bool, - /// Filesystem to add a snapshot interval for - filesystem: String, - /// Snapshot interval in human form, e. g. 1hour - #[structopt(required = true, min_values = 1)] - interval: Vec, + /// Number of recent snapshots to keep + #[structopt(short = "k", long = "keep")] + keep: i32, + /// Number of days when keep the most recent snapshot of each day + #[structopt(short = "d", long = "daily")] + daily: Option, + /// Number of weeks when keep the most recent snapshot of each week + #[structopt(short = "w", long = "weekly")] + weekly: Option, + /// Number of months when keep the most recent snapshot of each month + #[structopt(short = "m", long = "monthly")] + monthly: Option, }, - /// Remove snapshot intervals + /// Remove snapshot policies Remove { - /// The ids of the snapshot intervals to remove + /// Filesystem names to remove policies for #[structopt(required = true, min_values = 1)] - ids: Vec, + filesystem: Vec, }, } -#[derive(Debug, StructOpt)] -pub enum RetentionCommand { - /// List snapshots retention rules - List { - /// Display type: json, yaml, tabular - #[structopt(short = "d", long = "display", default_value = "tabular")] - display_type: DisplayType, - }, - /// Create snapshot retention rule - Create { - /// Filesystem to create a snapshot retention rule for - filesystem: String, - /// Delete the oldest snapshot when available space falls below this value - reserve_value: u32, - /// The unit of measurement associated with the reserve_value (%, GiB or TiB) - reserve_unit: snapshot::ReserveUnit, - /// Minimum number of snapshots to keep (default: 0) - keep_num: Option, - }, - /// Remove snapshot retention rule - Remove { - /// The ids of the retention rules to remove - #[structopt(required = true, min_values = 1)] - ids: Vec, - }, +impl Default for PolicyCommand { + fn default() -> Self { + PolicyCommand::List { + display_type: DisplayType::Tabular, + } + } } #[derive(Debug, StructOpt)] @@ -85,99 +78,64 @@ pub enum SnapshotCommand { /// The filesystem to list snapshots for fsname: String, }, - /// Snapshot intervals operations - Interval(IntervalCommand), - /// Snapshot retention rules operations - Retention(RetentionCommand), + /// Automatic snapshot policies operations + Policy { + #[structopt(subcommand)] + command: Option, + }, } -async fn interval_cli(cmd: IntervalCommand) -> Result<(), ImlManagerCliError> { +async fn policy_cli(cmd: PolicyCommand) -> Result<(), ImlManagerCliError> { match cmd { - IntervalCommand::List { display_type } => { - let query = snapshot_queries::list_intervals::build(); + PolicyCommand::List { display_type } => { + let query = snapshot_queries::policy::list::build(); - let resp: iml_graphql_queries::Response = + let resp: iml_graphql_queries::Response = graphql(query).await?; - let intervals = Result::from(resp)?.data.snapshot_intervals; + let policies = Result::from(resp)?.data.snapshot_policies; - let x = intervals.into_display_type(display_type); + let x = policies.into_display_type(display_type); let term = Term::stdout(); term.write_line(&x).unwrap(); Ok(()) } - IntervalCommand::Add { + PolicyCommand::Create { filesystem, interval, barrier, + keep, + daily, + weekly, + monthly, } => { - let query = snapshot_queries::create_interval::build( - filesystem, - interval.join(" "), - Some(barrier), - ); - - let _resp: iml_graphql_queries::Response = + let query = + snapshot_queries::policy::create::build(snapshot_queries::policy::create::Vars { + filesystem, + interval, + barrier: Some(barrier), + keep, + daily, + weekly, + monthly, + }); + + let _resp: iml_graphql_queries::Response = graphql(query).await?; Ok(()) } - IntervalCommand::Remove { ids } => { - for id in ids { - let query = snapshot_queries::remove_interval::build(id); + PolicyCommand::Remove { filesystem } => { + for fs in filesystem { + let query = snapshot_queries::policy::remove::build(fs); - let _resp: iml_graphql_queries::Response = + let _resp: iml_graphql_queries::Response = graphql(query).await?; } - Ok(()) - } - } -} - -async fn retention_cli(cmd: RetentionCommand) -> Result<(), ImlManagerCliError> { - match cmd { - RetentionCommand::List { display_type } => { - let query = snapshot_queries::list_retentions::build(); - - let resp: iml_graphql_queries::Response = - graphql(query).await?; - let retentions = Result::from(resp)?.data.snapshot_retention_policies; - - let x = retentions.into_display_type(display_type); - - let term = Term::stdout(); - term.write_line(&x).unwrap(); Ok(()) } - RetentionCommand::Create { - filesystem, - keep_num, - reserve_value, - reserve_unit, - } => { - let query = snapshot_queries::create_retention::build( - filesystem, - reserve_value, - reserve_unit, - keep_num, - ); - - let _resp: iml_graphql_queries::Response = - graphql(query).await?; - - Ok(()) - } - RetentionCommand::Remove { ids } => { - for id in ids { - let query = snapshot_queries::remove_retention::build(id); - - let _resp: iml_graphql_queries::Response = - graphql(query).await?; - } - Ok(()) - } } } @@ -238,7 +196,6 @@ pub async fn snapshot_cli(command: SnapshotCommand) -> Result<(), ImlManagerCliE Ok(()) } - SnapshotCommand::Interval(cmd) => interval_cli(cmd).await, - SnapshotCommand::Retention(cmd) => retention_cli(cmd).await, + SnapshotCommand::Policy { command } => policy_cli(command.unwrap_or_default()).await, } } diff --git a/iml-services/iml-snapshot/Cargo.toml b/iml-services/iml-snapshot/Cargo.toml index 3bc9043c6b..636360c9ed 100644 --- a/iml-services/iml-snapshot/Cargo.toml +++ b/iml-services/iml-snapshot/Cargo.toml @@ -5,18 +5,19 @@ name = "iml-snapshot" version = "0.4.0" [dependencies] +chrono = "0.4" futures = "0.3" futures-util = "0.3" iml-command-utils = {path = "../../iml-command-utils", version = "0.4"} iml-graphql-queries = {path = "../../iml-graphql-queries", version = "0.2"} -iml-influx = {path = "../../iml-influx", version = "0.2"} +iml-influx = {path = "../../iml-influx", version = "0.2", features = ["with-db-client"]} iml-manager-client = {path = "../../iml-manager-client", version = "0.4"} iml-manager-env = {path = "../../iml-manager-env", version = "0.4"} iml-postgres = {path = "../../iml-postgres", version = "0.4"} iml-rabbit = {path = "../../iml-rabbit", version = "0.4"} iml-service-queue = {path = "../iml-service-queue", version = "0.4"} iml-tracing = {version = "0.3", path = "../../iml-tracing"} -iml-wire-types = {path = "../../iml-wire-types", version = "0.4"} +iml-wire-types = {path = "../../iml-wire-types", version = "0.4", features = ["postgres-interop"]} serde = "1.0" thiserror = "1.0" tokio = {version = "0.2", features = ["rt-threaded"]} diff --git a/iml-services/iml-snapshot/src/lib.rs b/iml-services/iml-snapshot/src/lib.rs index cbaf33d6b5..2e3a4f9845 100644 --- a/iml-services/iml-snapshot/src/lib.rs +++ b/iml-services/iml-snapshot/src/lib.rs @@ -3,7 +3,7 @@ use iml_manager_client::ImlManagerClientError; use tokio::time::Instant; pub mod client_monitor; -pub mod retention; +pub mod policy; #[derive(thiserror::Error, Debug)] pub enum Error { diff --git a/iml-services/iml-snapshot/src/main.rs b/iml-services/iml-snapshot/src/main.rs index d33c580b93..9d63f6836b 100644 --- a/iml-services/iml-snapshot/src/main.rs +++ b/iml-services/iml-snapshot/src/main.rs @@ -3,12 +3,11 @@ // license that can be found in the LICENSE file. use futures::{StreamExt, TryStreamExt}; -use iml_influx::Client as InfluxClient; -use iml_manager_client::{Client as ManagerClient, Url}; -use iml_manager_env::{get_influxdb_addr, get_influxdb_metrics_db, get_pool_limit}; +use iml_manager_client::Client as ManagerClient; +use iml_manager_env::get_pool_limit; use iml_postgres::{get_db_pool, sqlx}; use iml_service_queue::service_queue::consume_data; -use iml_snapshot::{client_monitor::tick, retention::handle_retention_rules, MonitorState}; +use iml_snapshot::{client_monitor::tick, policy, MonitorState}; use iml_tracing::tracing; use iml_wire_types::snapshot; use std::collections::HashMap; @@ -30,19 +29,12 @@ async fn main() -> Result<(), Box> { consume_data::>>(&ch, "rust_agent_snapshot_rx"); let pool = get_db_pool(get_pool_limit().unwrap_or(DEFAULT_POOL_LIMIT)).await?; - let pool_2 = pool.clone(); - let pool_3 = pool.clone(); let manager_client: ManagerClient = iml_manager_client::get_client()?; - let influx_url: String = format!("http://{}", get_influxdb_addr()); - let influx_client = InfluxClient::new( - Url::parse(&influx_url).expect("Influx URL is invalid."), - get_influxdb_metrics_db(), - ); - sqlx::migrate!("../../migrations").run(&pool).await?; + let pool_2 = pool.clone(); tokio::spawn(async move { let mut interval = interval(Duration::from_secs(60)); let mut snapshot_client_counts: HashMap = HashMap::new(); @@ -55,11 +47,7 @@ async fn main() -> Result<(), Box> { } }); - tokio::spawn(handle_retention_rules( - manager_client, - influx_client, - pool_3.clone(), - )); + tokio::spawn(policy::main(manager_client, pool.clone())); while let Some((fqdn, snap_map)) = s.try_next().await? { for (fs_name, snapshots) in snap_map { diff --git a/iml-services/iml-snapshot/src/policy.rs b/iml-services/iml-snapshot/src/policy.rs new file mode 100644 index 0000000000..b77eaef87c --- /dev/null +++ b/iml-services/iml-snapshot/src/policy.rs @@ -0,0 +1,524 @@ +// Copyright (c) 2020 DDN. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +use crate::Error; +use chrono::{DateTime, Datelike as _, Duration, NaiveDate, NaiveDateTime, Utc}; +use futures::future::{try_join_all, AbortHandle, Abortable}; +use iml_command_utils::wait_for_cmds_success; +use iml_graphql_queries::snapshot; +use iml_manager_client::{graphql, Client}; +use iml_postgres::{sqlx, PgPool}; +use iml_tracing::tracing; +use iml_wire_types::{snapshot::SnapshotPolicy, Command}; +use std::collections::{HashMap, HashSet, LinkedList}; + +pub async fn main(client: Client, pg: PgPool) -> Result<(), Error> { + let mut svcs: HashMap = HashMap::new(); + loop { + let resp: iml_graphql_queries::Response = + graphql(client.clone(), snapshot::policy::list::build()).await?; + + let new: HashSet = Result::from(resp)? + .data + .snapshot_policies + .into_iter() + .collect(); + + let old: HashSet = svcs.keys().cloned().collect(); + + for p in old.difference(&new) { + tracing::debug!("stopping obsolete {:?}", p); + let (c, d) = svcs.remove(p).unwrap(); + c.abort(); + d.abort(); + } + for p in new.difference(&old).cloned() { + tracing::debug!("starting new {:?}", p); + let ah = start(client.clone(), pg.clone(), p.clone()); + svcs.insert(p, ah); + } + + tokio::time::delay_for(tokio::time::Duration::from_secs(6)).await; + } +} + +fn start(client: Client, pg: PgPool, policy: SnapshotPolicy) -> (AbortHandle, AbortHandle) { + let (create, create_reg) = AbortHandle::new_pair(); + let (destroy, destroy_reg) = AbortHandle::new_pair(); + + let client_1 = client.clone(); + let pg_1 = pg.clone(); + let policy_1 = policy.clone(); + tokio::spawn(Abortable::new( + async move { + loop { + if let Err(e) = create_snapshot(&client_1, &pg_1, &policy_1).await { + tracing::warn!("automatic snapshot failed: {}", e); + tokio::time::delay_for(tokio::time::Duration::from_secs(10)).await; + } + } + }, + create_reg, + )); + + tokio::spawn(Abortable::new( + async move { + loop { + if let Err(e) = destroy_obsolete(&client, &pg, &policy).await { + tracing::warn!("obsolete snapshot destroying: {}", e); + tokio::time::delay_for(tokio::time::Duration::from_secs(10)).await; + } + } + }, + destroy_reg, + )); + + (create, destroy) +} + +async fn create_snapshot( + client: &Client, + pg: &PgPool, + policy: &SnapshotPolicy, +) -> Result<(), Error> { + let latest = sqlx::query!( + r#" + SELECT snapshot_name, create_time FROM snapshot + WHERE filesystem_name = $1 + AND snapshot_name LIKE '\_\_%' + ORDER BY create_time DESC + LIMIT 1 + "#, + policy.filesystem + ) + .fetch_optional(pg) + .await?; + + if let Some(x) = latest { + tracing::debug!( + "latest automatic snapshot of {}: {} at {:?}", + policy.filesystem, + x.snapshot_name, + x.create_time + ); + let pause = Duration::to_std(&(Utc::now() - x.create_time)) + .ok() + .and_then(|y| policy.interval.0.checked_sub(y)) + .unwrap_or(std::time::Duration::from_secs(0)); + + if pause.as_secs() > 0 { + tracing::debug!( + "sleeping {:?} before creating next snapshot of {}", + pause, + policy.filesystem + ); + tokio::time::delay_for(pause).await; + } + } + + let time = Utc::now(); + let name = time.format("__%s"); + tracing::info!( + "creating automatic snapshot {} of {}", + name, + policy.filesystem + ); + + let query = snapshot::create::build( + policy.filesystem.clone(), + name, + "automatic snapshot".into(), + Some(policy.barrier), + ); + let resp: iml_graphql_queries::Response = + graphql(client.clone(), query).await?; + let x = Result::from(resp)?.data.create_snapshot; + wait_for_cmds_success(&[x], None).await?; + + sqlx::query!( + "UPDATE snapshot_policy SET last_run = $1 WHERE filesystem = $2", + time, + policy.filesystem + ) + .execute(pg) + .await?; + + // XXX This is need to make sure the new snapshot is registered. + // XXX Might need to registered snapshots right after creation (not relying on polling). + let cooldown = policy.interval.0.div_f64(2.0); + tracing::debug!("cooldown sleeping {:?} for {}", cooldown, policy.filesystem); + tokio::time::delay_for(cooldown).await; + + Ok(()) +} + +async fn destroy_obsolete( + client: &Client, + pg: &PgPool, + policy: &SnapshotPolicy, +) -> Result<(), Error> { + let snapshots = sqlx::query!( + r#" + SELECT snapshot_name, create_time FROM snapshot + WHERE filesystem_name = $1 + AND snapshot_name LIKE '\_\_%' + ORDER BY create_time DESC + "#, + policy.filesystem + ) + .fetch_all(pg) + .await? + .into_iter() + .map(|x| (x.create_time, x.snapshot_name)) + .collect::>(); + + let obsolete = get_obsolete(policy, snapshots); + + if obsolete.is_empty() { + tracing::info!("no obsolete snapshots of {}", policy.filesystem); + } else { + let cmd_futs: Vec<_> = obsolete + .iter() + .map(|x| destroy_snapshot(client.clone(), policy.filesystem.clone(), x.clone())) + .collect(); + let cmds = try_join_all(cmd_futs).await?; + wait_for_cmds_success(&cmds, None).await?; + + tracing::info!("destroyed obsolete snapshots: {}", obsolete.join(",")); + + sqlx::query!( + "UPDATE snapshot_policy SET last_run = $1 WHERE filesystem = $2", + Utc::now(), + policy.filesystem + ) + .execute(pg) + .await?; + } + + let cooldown = policy.interval.0.div_f64(2.0); + tracing::debug!( + "sleeping {:?} before next search for obsolete snapshots of {}", + cooldown, + policy.filesystem + ); + tokio::time::delay_for(cooldown).await; + + Ok(()) +} + +async fn destroy_snapshot( + client: Client, + filesystem: String, + snapshot: String, +) -> Result { + let query = snapshot::destroy::build(filesystem, snapshot, true); + let resp: iml_graphql_queries::Response = + graphql(client, query).await?; + let cmd = Result::from(resp)?.data.destroy_snapshot; + Ok(cmd) +} + +fn get_obsolete(policy: &SnapshotPolicy, snapshots: Vec<(DateTime, String)>) -> Vec { + let mut tail: Vec<_> = snapshots + .iter() + .skip(policy.keep as usize) + .map(|x| x.0) + .collect(); + + tracing::debug!( + "snapshots of {} to consider for deletion after the latest {}: {:?}", + policy.filesystem, + policy.keep, + tail + ); + + let mut to_delete: Vec> = Vec::with_capacity(tail.len()); + + // Handle daily snapshots: + if let Some(x) = tail.get(0) { + let next_day = x.date().succ().and_hms(0, 0, 0); + let cut = next_day - Duration::days(policy.daily as i64); + + let (daily, new_tail): (Vec<_>, Vec<_>) = tail.into_iter().partition(|x| *x > cut); + tracing::debug!("daily snapshots to consider: {:?}", daily); + + let datetimes: Vec = daily.iter().map(|x| x.naive_utc()).collect(); + let res = partition_datetime( + &|_| Duration::days(1).num_seconds(), + next_day.naive_utc(), + &datetimes, + ); + tracing::debug!("daily partition: {:?}", res); + for x in res.iter() { + for y in x.iter().skip(1) { + to_delete.push(DateTime::from_utc(*y, Utc)); + } + } + tail = new_tail; + } + tracing::debug!( + "snapshots of {} to consider for deletion after the daily schedule: {:?}", + policy.filesystem, + tail + ); + + // Handle weekly snapshots: + if let Some(x) = tail.get(0) { + let date = x.date(); + let days_to_next_week = Duration::days((7 - date.weekday().num_days_from_monday()).into()); + let next_week = (date + days_to_next_week).and_hms(0, 0, 0); + let cut = next_week - Duration::weeks(policy.weekly as i64); + + let (weekly, new_tail): (Vec<_>, Vec<_>) = tail.into_iter().partition(|x| *x > cut); + tracing::debug!("weekly snapshots to consider: {:?}", weekly); + + let datetimes: Vec = weekly.iter().map(|x| x.naive_utc()).collect(); + let res = partition_datetime( + &|_| Duration::weeks(1).num_seconds(), + next_week.naive_utc(), + &datetimes, + ); + tracing::debug!("weekly partition: {:?}", res); + for x in res.iter() { + for y in x.iter().skip(1) { + to_delete.push(DateTime::from_utc(*y, Utc)); + } + } + tail = new_tail; + } + tracing::debug!( + "snapshots of {} to consider for deletion after the weekly schedule: {:?}", + policy.filesystem, + tail + ); + + // Handle monthly snapshots: + if let Some(x) = tail.get(0) { + let next_month = add_month(&x.naive_utc(), 1); + let cut = DateTime::::from_utc(add_month(&next_month, -policy.monthly), Utc); + let f = |n: u32| { + let n_month = add_month(&next_month, 0 - n as i32); + let n1_month = add_month(&n_month, 1); + (n1_month - n_month).num_seconds() + }; + + let (monthly, new_tail): (Vec<_>, Vec<_>) = tail.into_iter().partition(|x| *x > cut); + tracing::debug!("monthly snapshots to consider: {:?}, {:?}", cut, monthly); + + let datetimes: Vec = monthly.iter().map(|x| x.naive_utc()).collect(); + let res = partition_datetime(&f, next_month, &datetimes); + tracing::debug!("monthly partition: {:?}", res); + for x in res.iter() { + for y in x.iter().skip(1) { + to_delete.push(DateTime::from_utc(*y, Utc)); + } + } + tail = new_tail; + } + tracing::debug!( + "snapshots of {} to consider for deletion after the monthly schedule: {:?}", + policy.filesystem, + tail + ); + + to_delete.append(&mut tail); + + to_delete.sort_unstable(); + snapshots + .into_iter() + .filter(|x| to_delete.binary_search(&x.0).is_ok()) + .map(|x| x.1) + .collect() +} + +fn add_month(date: &NaiveDateTime, n: i32) -> NaiveDateTime { + let month = date.date().month() as i32; + + let x = month + n; + let new_year = date.date().year() + + if x > 12 { + x / 12 + } else if x < 0 { + x / 12 - 1 + } else { + 0 + }; + + let x = month + n % 12; + let new_month = if x > 12 { + x - 12 + } else if x <= 0 { + 12 + x + } else { + x + } as u32; + + let new_date = NaiveDate::from_ymd(new_year, new_month, 1); + + new_date.and_hms(0, 0, 0) +} + +fn partition(f: &dyn Fn(u32) -> i64, v0: i64, v: I) -> LinkedList> +where + I: IntoIterator, +{ + let mut term: LinkedList = LinkedList::new(); + let mut res: LinkedList> = LinkedList::new(); + let mut n: u32 = 1; + let mut a: i64 = v0; + + for i in v { + while i >= a + f(n) { + res.push_back(term); + term = LinkedList::new(); + a += f(n); + n += 1; + } + term.push_back(i); + } + res.push_back(term); + + res +} + +fn partition_datetime( + f: &dyn Fn(u32) -> i64, + start: NaiveDateTime, + datetimes: &[NaiveDateTime], +) -> LinkedList> { + let mut v: Vec = datetimes + .into_iter() + .map(|&d| (start - d).num_seconds()) + .collect(); + v.sort_unstable(); + v.dedup(); + + let part = partition(f, 0, v); + + part.into_iter() + .map(|l| { + l.into_iter() + .map(|d| start - Duration::seconds(d)) + .collect() + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + use chrono::Timelike; + use iml_wire_types::graphql_duration::GraphQLDuration; + + #[test] + fn test_add_month() { + for (d0, n, d) in vec![ + ("2020-11-24T12:34:56Z", 0, "2020-11-01T00:00:00Z"), + ("2020-10-24T14:35:02Z", 1, "2020-11-01T00:00:00Z"), + ("2020-11-24T14:35:02Z", 1, "2020-12-01T00:00:00Z"), + ("2020-01-11T14:34:10Z", 10, "2020-11-01T00:00:00Z"), + ("2020-12-01T06:34:12Z", 23, "2022-11-01T00:00:00Z"), + ("2020-03-11T06:34:12Z", 36, "2023-03-01T00:00:00Z"), + ("2020-10-24T14:35:02Z", -1, "2020-09-01T00:00:00Z"), + ("2020-12-01T00:00:00Z", -3, "2020-09-01T00:00:00Z"), + ("2020-10-24T14:35:02Z", -12, "2019-10-01T00:00:00Z"), + ("2020-10-14T14:35:02Z", -16, "2019-06-01T00:00:00Z"), + ("2020-09-04T14:35:02Z", -36, "2017-09-01T00:00:00Z"), + ] { + let start = DateTime::parse_from_rfc3339(d0) + .unwrap() + .with_timezone(&Utc) + .naive_utc(); + let end = DateTime::parse_from_rfc3339(d) + .unwrap() + .with_timezone(&Utc) + .naive_utc(); + + assert_eq!(add_month(&start, n), end); + } + } + + #[test] + fn test_get_obsolete() { + let policy = SnapshotPolicy { + id: 0, + filesystem: "fs".into(), + interval: GraphQLDuration(std::time::Duration::from_secs(60)), + barrier: true, + keep: 2, + daily: 4, + weekly: 3, + monthly: 3, + last_run: None, + }; + + let snapshots: Vec<(DateTime, String)> = vec![ + "2020-11-24T14:36:01Z", + "2020-11-24T14:35:02Z", + "2020-11-24T14:34:11Z", + "2020-11-24T14:33:00Z", + "2020-11-24T04:30:00Z", + "2020-11-23T04:36:12Z", + "2020-11-23T04:34:00Z", + "2020-11-23T01:30:00Z", + "2020-11-22T21:38:13Z", + "2020-11-22T16:32:00Z", + "2020-11-22T03:33:00Z", + "2020-11-21T23:22:14Z", + "2020-11-21T11:59:00Z", + "2020-11-17T00:59:21Z", + "2020-11-14T23:22:22Z", + "2020-11-14T11:59:00Z", + "2020-11-13T09:44:00Z", + "2020-11-13T08:37:00Z", + "2020-11-12T05:11:00Z", + "2020-11-06T23:11:23Z", + "2020-11-05T13:55:00Z", + "2020-11-01T13:11:31Z", + "2020-10-31T10:55:32Z", + "2020-10-31T00:55:00Z", + "2020-10-23T00:55:00Z", + "2020-10-01T00:01:00Z", + "2020-09-21T00:00:33Z", + ] + .into_iter() + .map(|t| { + ( + DateTime::parse_from_rfc3339(t).unwrap().with_timezone(&Utc), + t.into(), + ) + }) + .collect(); + + let expected_number_of_obsolete = snapshots + .iter() + .filter(|x| x.0.time().second() == 0) + .count(); + + let obsolete = get_obsolete(&policy, snapshots); + let expected_obsolete: Vec = vec![ + "2020-11-24T14:33:00Z", + "2020-11-24T04:30:00Z", + "2020-11-23T04:34:00Z", + "2020-11-23T01:30:00Z", + "2020-11-22T16:32:00Z", + "2020-11-22T03:33:00Z", + "2020-11-21T11:59:00Z", + "2020-11-14T11:59:00Z", + "2020-11-13T09:44:00Z", + "2020-11-13T08:37:00Z", + "2020-11-12T05:11:00Z", + "2020-11-05T13:55:00Z", + "2020-10-31T00:55:00Z", + "2020-10-23T00:55:00Z", + "2020-10-01T00:01:00Z", + ] + .into_iter() + .map(String::from) + .collect(); + + assert_eq!(obsolete, expected_obsolete); + assert_eq!(obsolete.len(), expected_number_of_obsolete); + } +} diff --git a/iml-services/iml-snapshot/src/retention.rs b/iml-services/iml-snapshot/src/retention.rs deleted file mode 100644 index 12c36c0151..0000000000 --- a/iml-services/iml-snapshot/src/retention.rs +++ /dev/null @@ -1,213 +0,0 @@ -use crate::{Error, FsStats}; -use iml_command_utils::wait_for_cmds_success; -use iml_influx::{Client as InfluxClient, InfluxClientExt as _}; -use iml_manager_client::{graphql, Client}; -use iml_postgres::{sqlx, PgPool}; -use iml_tracing::tracing; -use iml_wire_types::{snapshot, Command}; -use std::collections::HashMap; - -async fn get_stats_from_influx( - fs_name: &str, - client: &InfluxClient, -) -> Result, Error> { - let nodes: Option> = client - .query_into( - format!( - r#" - SELECT - SUM(bytes_total) as bytes_total, - SUM(bytes_free) as bytes_free, - SUM("bytes_avail") as bytes_avail - FROM ( - SELECT LAST("bytes_total") AS bytes_total, - LAST("bytes_free") as bytes_free, - LAST("bytes_avail") as bytes_avail - FROM "target" - WHERE "kind" = 'OST' AND "fs" = '{}' - GROUP BY target - ) - "#, - fs_name, - ) - .as_str(), - None, - ) - .await?; - - if let Some(nodes) = nodes { - if nodes.is_empty() { - return Ok(None); - } else { - let bytes_avail = nodes[0].bytes_avail; - let bytes_total = nodes[0].bytes_total; - let bytes_free = nodes[0].bytes_free; - let bytes_used = bytes_total - bytes_free; - - return Ok(Some((bytes_avail, bytes_free, bytes_used))); - } - } - - Ok(None) -} - -async fn get_snapshots( - pool: &sqlx::PgPool, - fs_name: &str, -) -> Result, Error> { - let xs = sqlx::query_as!( - snapshot::SnapshotRecord, - "SELECT * FROM snapshot WHERE filesystem_name = $1 ORDER BY create_time ASC", - fs_name - ) - .fetch_all(pool) - .await?; - - Ok(xs) -} - -async fn get_retentions(pool: &sqlx::PgPool) -> Result, Error> { - let xs = sqlx::query_as!( - snapshot::SnapshotRetention, - r#" - SELECT - id, - filesystem_name, - reserve_value, - reserve_unit as "reserve_unit:snapshot::ReserveUnit", - last_run, - keep_num - FROM snapshot_retention - "# - ) - .fetch_all(pool) - .await?; - - Ok(xs) -} - -async fn get_retention_policy( - pool: &PgPool, - fs_name: &str, -) -> Result, Error> { - let xs = get_retentions(pool).await?; - - Ok(xs.into_iter().find(|x| x.filesystem_name == fs_name)) -} - -async fn destroy_snapshot( - client: Client, - fs_name: &str, - snapshot_name: &str, -) -> Result { - let resp: iml_graphql_queries::Response = - graphql( - client, - iml_graphql_queries::snapshot::destroy::build(fs_name, snapshot_name, true), - ) - .await?; - - let cmd = Result::from(resp)?.data.destroy_snapshot; - - Ok(cmd) -} - -async fn get_retention_filesystems(pool: &PgPool) -> Result, Error> { - let xs = get_retentions(pool).await?; - - let fs_names = xs.into_iter().map(|x| x.filesystem_name).collect(); - Ok(fs_names) -} - -pub async fn process_retention( - client: &Client, - influx_client: &InfluxClient, - pool: &PgPool, - mut stats_record: HashMap, -) -> Result, Error> { - let filesystems = get_retention_filesystems(pool).await?; - - tracing::debug!("Filesystems with retentions: {:?}", filesystems); - - for fs_name in filesystems { - let stats = get_stats_from_influx(&fs_name, &influx_client).await?; - - if let Some((bytes_avail, bytes_free, bytes_used)) = stats { - tracing::debug!( - "stats values: {}, {}, {}", - bytes_avail, - bytes_free, - bytes_used - ); - let percent_used = - (bytes_used as f64 / (bytes_used as f64 + bytes_avail as f64)) as f64 * 100.0f64; - let percent_free = 100.0f64 - percent_used; - - tracing::debug!( - "stats record: {:?} - bytes free: {}", - stats_record.get(&fs_name), - bytes_free - ); - let retention = get_retention_policy(pool, &fs_name).await?; - - if let Some(retention) = retention { - let snapshots = get_snapshots(pool, &fs_name).await?; - - tracing::debug!( - "percent_left: {}, reserve value: {}", - percent_free, - retention.reserve_value - ); - let should_delete_snapshot = match retention.reserve_unit { - snapshot::ReserveUnit::Percent => percent_free < retention.reserve_value as f64, - snapshot::ReserveUnit::Gibibytes => { - let gib_free: f64 = bytes_free as f64 / 1_073_741_824_f64; - gib_free < retention.reserve_value as f64 - } - snapshot::ReserveUnit::Tebibytes => { - let teb_free: f64 = bytes_free as f64 / 1_099_511_627_776_f64; - teb_free < retention.reserve_value as f64 - } - }; - tracing::debug!("Should delete snapshot?: {}", should_delete_snapshot); - - if should_delete_snapshot - && snapshots.len() > retention.keep_num as usize - && stats_record.get(&fs_name) != Some(&bytes_used) - { - stats_record.insert(fs_name.to_string(), bytes_used); - tracing::debug!("About to delete earliest snapshot."); - let snapshot_name = snapshots[0].snapshot_name.to_string(); - tracing::debug!("Deleting {}", snapshot_name); - let cmd = - destroy_snapshot(client.clone(), &fs_name, snapshot_name.as_ref()).await?; - - wait_for_cmds_success(&vec![cmd], None).await?; - } - } - } - } - - Ok(stats_record) -} - -pub async fn handle_retention_rules( - client: Client, - influx_client: InfluxClient, - pool: PgPool, -) -> Result<(), Error> { - let mut prev_stats: HashMap = vec![].into_iter().collect::>(); - - loop { - prev_stats = - match process_retention(&client, &influx_client, &pool, prev_stats.clone()).await { - Ok(x) => x, - Err(e) => { - tracing::error!("Retention Rule processing error: {:?}", e); - prev_stats - } - }; - - tokio::time::delay_for(tokio::time::Duration::from_secs(60)).await; - } -} diff --git a/iml-warp-drive/src/cache.rs b/iml-warp-drive/src/cache.rs index 434a053f3f..49c0dfa325 100644 --- a/iml-warp-drive/src/cache.rs +++ b/iml-warp-drive/src/cache.rs @@ -18,7 +18,7 @@ use iml_wire_types::{ EnclosureType, HealthState, JobState, JobType, MemberState, SfaController, SfaDiskDrive, SfaEnclosure, SfaJob, SfaPowerSupply, SfaStorageSystem, SubTargetType, }, - snapshot::{ReserveUnit, SnapshotInterval, SnapshotRecord, SnapshotRetention}, + snapshot::{SnapshotPolicy, SnapshotRecord}, warp_drive::{Cache, Record, RecordChange, RecordId}, Alert, ApiList, EndpointName, Filesystem, FlatQuery, FsType, Host, }; @@ -190,16 +190,10 @@ pub async fn db_record_to_change_record( Ok(RecordChange::Update(Record::Snapshot(x))) } }, - DbRecord::SnapshotInterval(x) => match (msg_type, x) { - (MessageType::Delete, x) => Ok(RecordChange::Delete(RecordId::SnapshotInterval(x.id))), + DbRecord::SnapshotPolicy(x) => match (msg_type, x) { + (MessageType::Delete, x) => Ok(RecordChange::Delete(RecordId::SnapshotPolicy(x.id))), (MessageType::Insert, x) | (MessageType::Update, x) => { - Ok(RecordChange::Update(Record::SnapshotInterval(x))) - } - }, - DbRecord::SnapshotRetention(x) => match (msg_type, x) { - (MessageType::Delete, x) => Ok(RecordChange::Delete(RecordId::SnapshotRetention(x.id))), - (MessageType::Insert, x) | (MessageType::Update, x) => { - Ok(RecordChange::Update(Record::SnapshotRetention(x))) + Ok(RecordChange::Update(Record::SnapshotPolicy(x))) } }, DbRecord::LnetConfiguration(x) => match (msg_type, x) { @@ -578,16 +572,20 @@ pub async fn populate_from_db( .try_collect() .await?; - cache.snapshot_interval = sqlx::query!("SELECT * FROM snapshot_interval") + cache.snapshot_policy = sqlx::query!(r#"SELECT * FROM snapshot_policy"#) .fetch(pool) .map_ok(|x| { ( x.id, - SnapshotInterval { + SnapshotPolicy { id: x.id, - filesystem_name: x.filesystem_name, - use_barrier: x.use_barrier, + filesystem: x.filesystem, interval: x.interval.into(), + barrier: x.barrier, + keep: x.keep, + daily: x.daily, + weekly: x.weekly, + monthly: x.monthly, last_run: x.last_run, }, ) @@ -595,24 +593,6 @@ pub async fn populate_from_db( .try_collect() .await?; - cache.snapshot_retention = sqlx::query_as!( - SnapshotRetention, - r#" - SELECT - id, - filesystem_name, - reserve_value, - reserve_unit as "reserve_unit:ReserveUnit", - last_run, - keep_num - FROM snapshot_retention - "# - ) - .fetch(pool) - .map_ok(|x| (x.id, x)) - .try_collect() - .await?; - cache.stratagem_config = sqlx::query_as!( StratagemConfiguration, "select * from chroma_core_stratagemconfiguration where not_deleted = 't'" diff --git a/iml-warp-drive/src/db_record.rs b/iml-warp-drive/src/db_record.rs index d4a7c4e7ee..d53453c6ea 100644 --- a/iml-warp-drive/src/db_record.rs +++ b/iml-warp-drive/src/db_record.rs @@ -22,10 +22,7 @@ use iml_wire_types::{ SFA_CONTROLLER_TABLE_NAME, SFA_DISK_DRIVE_TABLE_NAME, SFA_ENCLOSURE_TABLE_NAME, SFA_JOB_TABLE_NAME, SFA_POWER_SUPPLY_TABLE_NAME, SFA_STORAGE_SYSTEM_TABLE_NAME, }, - snapshot::{ - SnapshotInterval, SnapshotRecord, SnapshotRetention, SNAPSHOT_INTERVAL_TABLE_NAME, - SNAPSHOT_RETENTION_TABLE_NAME, SNAPSHOT_TABLE_NAME, - }, + snapshot::{SnapshotPolicy, SnapshotRecord, SNAPSHOT_POLICY_TABLE_NAME, SNAPSHOT_TABLE_NAME}, }; use serde::de::Error; use std::convert::TryFrom; @@ -56,8 +53,7 @@ pub enum DbRecord { SfaPowerSupply(SfaPowerSupply), SfaController(SfaController), Snapshot(SnapshotRecord), - SnapshotInterval(SnapshotInterval), - SnapshotRetention(SnapshotRetention), + SnapshotPolicy(SnapshotPolicy), StratagemConfiguration(StratagemConfiguration), TargetRecord(TargetRecord), Volume(VolumeRecord), @@ -95,13 +91,8 @@ impl TryFrom<(TableName<'_>, serde_json::Value)> for DbRecord { serde_json::from_value(x).map(DbRecord::StratagemConfiguration) } SNAPSHOT_TABLE_NAME => serde_json::from_value(x).map(DbRecord::Snapshot), - SNAPSHOT_INTERVAL_TABLE_NAME => { - serde_json::from_value(x).map(DbRecord::SnapshotInterval) - } - SNAPSHOT_RETENTION_TABLE_NAME => { - serde_json::from_value(x).map(DbRecord::SnapshotRetention) - } TARGET_TABLE_NAME => serde_json::from_value(x).map(DbRecord::TargetRecord), + SNAPSHOT_POLICY_TABLE_NAME => serde_json::from_value(x).map(DbRecord::SnapshotPolicy), LNET_CONFIGURATION_TABLE_NAME => { serde_json::from_value(x).map(DbRecord::LnetConfiguration) } diff --git a/iml-wire-types/src/graphql_duration.rs b/iml-wire-types/src/graphql_duration.rs index a31bb8016e..822731ffe7 100644 --- a/iml-wire-types/src/graphql_duration.rs +++ b/iml-wire-types/src/graphql_duration.rs @@ -8,7 +8,7 @@ use sqlx::postgres::types::PgInterval; use std::convert::TryInto; use std::{convert::TryFrom, fmt, time::Duration}; -#[derive(serde::Deserialize, serde::Serialize, Clone, PartialEq, Debug)] +#[derive(serde::Deserialize, serde::Serialize, Clone, Eq, Hash, PartialEq, Debug)] #[serde(try_from = "String", into = "String")] pub struct GraphQLDuration(pub Duration); diff --git a/iml-wire-types/src/snapshot.rs b/iml-wire-types/src/snapshot.rs index 3bf2344f3c..ee3f697e1f 100644 --- a/iml-wire-types/src/snapshot.rs +++ b/iml-wire-types/src/snapshot.rs @@ -9,7 +9,6 @@ use crate::{ graphql_duration::GraphQLDuration, }; use chrono::{offset::Utc, DateTime}; -use std::str::FromStr; #[cfg(feature = "cli")] use structopt::StructOpt; @@ -59,77 +58,60 @@ impl Id for SnapshotRecord { pub const SNAPSHOT_TABLE_NAME: TableName = TableName("snapshot"); #[cfg_attr(feature = "graphql", derive(juniper::GraphQLObject))] -#[derive(serde::Deserialize, serde::Serialize, Clone, PartialEq, Debug)] -/// A Snapshot interval -pub struct SnapshotInterval { +#[derive(serde::Deserialize, serde::Serialize, Eq, Clone, Debug)] +/// Automatic snapshot policy +pub struct SnapshotPolicy { /// The configuration id pub id: i32, /// The filesystem name - pub filesystem_name: String, - /// Use a write barrier - pub use_barrier: bool, + pub filesystem: String, /// The interval configuration pub interval: GraphQLDuration, - // Last known run + /// Use a write barrier + pub barrier: bool, + /// Number of recent snapshots to keep + pub keep: i32, + /// Then, number of days to keep the most recent snapshot of each day + pub daily: i32, + /// Then, number of weeks to keep the most recent snapshot of each week + pub weekly: i32, + /// Then, number of months to keep the most recent snapshot of each months + pub monthly: i32, + /// Last known run pub last_run: Option>, } -impl Id for SnapshotInterval { - fn id(&self) -> i32 { - self.id +impl PartialEq for SnapshotPolicy { + fn eq(&self, other: &Self) -> bool { + self.filesystem == other.filesystem + && self.interval == other.interval + && self.barrier == other.barrier + && self.keep == other.keep + && self.daily == other.daily + && self.weekly == other.weekly + && self.monthly == other.monthly } } -pub const SNAPSHOT_INTERVAL_TABLE_NAME: TableName = TableName("snapshot_interval"); - -#[cfg_attr(feature = "graphql", derive(juniper::GraphQLObject))] -#[derive(serde::Deserialize, serde::Serialize, Clone, PartialEq, Debug)] -pub struct SnapshotRetention { - pub id: i32, - pub filesystem_name: String, - /// Amount or percent of free space to reserve - pub reserve_value: i32, - pub reserve_unit: ReserveUnit, - /// Minimum number of snapshots to keep - pub keep_num: i32, - pub last_run: Option>, +impl std::hash::Hash for SnapshotPolicy { + fn hash(&self, state: &mut H) { + self.filesystem.hash(state); + self.interval.hash(state); + self.barrier.hash(state); + self.keep.hash(state); + self.daily.hash(state); + self.weekly.hash(state); + self.monthly.hash(state); + } } -impl Id for SnapshotRetention { +impl Id for SnapshotPolicy { fn id(&self) -> i32 { self.id } } -pub const SNAPSHOT_RETENTION_TABLE_NAME: TableName = TableName("snapshot_retention"); - -#[cfg_attr(feature = "graphql", derive(juniper::GraphQLEnum))] -#[cfg_attr(feature = "postgres-interop", derive(sqlx::Type))] -#[cfg_attr(feature = "postgres-interop", sqlx(rename = "snapshot_reserve_unit"))] -#[cfg_attr(feature = "postgres-interop", sqlx(rename_all = "lowercase"))] -#[derive(serde::Deserialize, serde::Serialize, Clone, Copy, PartialEq, Debug)] -#[serde(rename_all = "lowercase")] -pub enum ReserveUnit { - #[cfg_attr(feature = "graphql", graphql(name = "percent"))] - Percent, - #[cfg_attr(feature = "graphql", graphql(name = "gibibytes"))] - Gibibytes, - #[cfg_attr(feature = "graphql", graphql(name = "tebibytes"))] - Tebibytes, -} - -impl FromStr for ReserveUnit { - type Err = String; - - fn from_str(s: &str) -> Result { - match s.to_lowercase().as_str() { - "%" | "percent" => Ok(Self::Percent), - "gib" | "g" | "gibibytes" => Ok(Self::Gibibytes), - "tib" | "t" | "tebibytes" => Ok(Self::Tebibytes), - x => Err(format!("Unexpected '{}'", x)), - } - } -} +pub const SNAPSHOT_POLICY_TABLE_NAME: TableName = TableName("snapshot_policy"); #[derive(serde::Deserialize, Debug)] #[cfg_attr(feature = "cli", derive(StructOpt))] diff --git a/iml-wire-types/src/warp_drive.rs b/iml-wire-types/src/warp_drive.rs index 547e911ceb..b26a798759 100644 --- a/iml-wire-types/src/warp_drive.rs +++ b/iml-wire-types/src/warp_drive.rs @@ -11,7 +11,7 @@ use crate::{ StratagemConfiguration, TargetRecord, VolumeNodeRecord, VolumeRecord, }, sfa::{SfaController, SfaDiskDrive, SfaEnclosure, SfaJob, SfaPowerSupply, SfaStorageSystem}, - snapshot::{SnapshotInterval, SnapshotRecord, SnapshotRetention}, + snapshot::{SnapshotPolicy, SnapshotRecord}, Alert, CompositeId, EndpointNameSelf, Filesystem, Host, Label, LockChange, ToCompositeId, }; use im::{HashMap, HashSet}; @@ -110,8 +110,7 @@ pub struct Cache { pub sfa_storage_system: HashMap, pub sfa_controller: HashMap, pub snapshot: HashMap, - pub snapshot_interval: HashMap, - pub snapshot_retention: HashMap, + pub snapshot_policy: HashMap, pub stratagem_config: HashMap, pub target: HashMap, pub target_record: HashMap, @@ -143,8 +142,7 @@ pub struct ArcCache { pub sfa_power_supply: HashMap>, pub sfa_controller: HashMap>, pub snapshot: HashMap>, - pub snapshot_interval: HashMap>, - pub snapshot_retention: HashMap>, + pub snapshot_policy: HashMap>, pub stratagem_config: HashMap>, pub target: HashMap>, pub target_record: HashMap>, @@ -204,14 +202,9 @@ impl Cache { .remove(&id) .map(Record::StratagemConfig), RecordId::Snapshot(id) => self.snapshot.remove(&id).map(Record::Snapshot), - RecordId::SnapshotInterval(id) => self - .snapshot_interval - .remove(&id) - .map(Record::SnapshotInterval), - RecordId::SnapshotRetention(id) => self - .snapshot_retention - .remove(&id) - .map(Record::SnapshotRetention), + RecordId::SnapshotPolicy(id) => { + self.snapshot_policy.remove(&id).map(Record::SnapshotPolicy) + } RecordId::Target(id) => self.target.remove(&id).map(Record::Target), RecordId::TargetRecord(id) => self.target_record.remove(&id).map(Record::TargetRecord), RecordId::User(id) => self.user.remove(&id).map(Record::User), @@ -280,11 +273,8 @@ impl Cache { Record::Snapshot(x) => { self.snapshot.insert(x.id, x); } - Record::SnapshotInterval(x) => { - self.snapshot_interval.insert(x.id(), x); - } - Record::SnapshotRetention(x) => { - self.snapshot_retention.insert(x.id(), x); + Record::SnapshotPolicy(x) => { + self.snapshot_policy.insert(x.id(), x); } Record::StratagemConfig(x) => { self.stratagem_config.insert(x.id(), x); @@ -348,8 +338,7 @@ impl ArcCache { RecordId::SfaPowerSupply(id) => self.sfa_power_supply.remove(&id).is_some(), RecordId::SfaController(id) => self.sfa_controller.remove(&id).is_some(), RecordId::Snapshot(id) => self.snapshot.remove(&id).is_some(), - RecordId::SnapshotInterval(id) => self.snapshot_interval.remove(&id).is_some(), - RecordId::SnapshotRetention(id) => self.snapshot_retention.remove(&id).is_some(), + RecordId::SnapshotPolicy(id) => self.snapshot_policy.remove(&id).is_some(), RecordId::StratagemConfig(id) => self.stratagem_config.remove(&id).is_some(), RecordId::Target(id) => self.target.remove(&id).is_some(), RecordId::TargetRecord(id) => self.target_record.remove(&id).is_some(), @@ -419,11 +408,8 @@ impl ArcCache { Record::Snapshot(x) => { self.snapshot.insert(x.id, Arc::new(x)); } - Record::SnapshotInterval(x) => { - self.snapshot_interval.insert(x.id, Arc::new(x)); - } - Record::SnapshotRetention(x) => { - self.snapshot_retention.insert(x.id(), Arc::new(x)); + Record::SnapshotPolicy(x) => { + self.snapshot_policy.insert(x.id(), Arc::new(x)); } Record::StratagemConfig(x) => { self.stratagem_config.insert(x.id(), Arc::new(x)); @@ -501,8 +487,7 @@ impl From<&Cache> for ArcCache { sfa_power_supply: hashmap_to_arc_hashmap(&cache.sfa_power_supply), sfa_controller: hashmap_to_arc_hashmap(&cache.sfa_controller), snapshot: hashmap_to_arc_hashmap(&cache.snapshot), - snapshot_interval: hashmap_to_arc_hashmap(&cache.snapshot_interval), - snapshot_retention: hashmap_to_arc_hashmap(&cache.snapshot_retention), + snapshot_policy: hashmap_to_arc_hashmap(&cache.snapshot_policy), stratagem_config: hashmap_to_arc_hashmap(&cache.stratagem_config), target: hashmap_to_arc_hashmap(&cache.target), target_record: hashmap_to_arc_hashmap(&cache.target_record), @@ -536,8 +521,7 @@ impl From<&ArcCache> for Cache { sfa_power_supply: arc_hashmap_to_hashmap(&cache.sfa_power_supply), sfa_controller: arc_hashmap_to_hashmap(&cache.sfa_controller), snapshot: arc_hashmap_to_hashmap(&cache.snapshot), - snapshot_interval: arc_hashmap_to_hashmap(&cache.snapshot_interval), - snapshot_retention: arc_hashmap_to_hashmap(&cache.snapshot_retention), + snapshot_policy: arc_hashmap_to_hashmap(&cache.snapshot_policy), stratagem_config: arc_hashmap_to_hashmap(&cache.stratagem_config), target: arc_hashmap_to_hashmap(&cache.target), target_record: arc_hashmap_to_hashmap(&cache.target_record), @@ -572,8 +556,7 @@ pub enum Record { SfaPowerSupply(SfaPowerSupply), SfaController(SfaController), Snapshot(SnapshotRecord), - SnapshotInterval(SnapshotInterval), - SnapshotRetention(SnapshotRetention), + SnapshotPolicy(SnapshotPolicy), StratagemConfig(StratagemConfiguration), Target(ManagedTargetRecord), TargetRecord(TargetRecord), @@ -604,8 +587,7 @@ pub enum ArcRecord { SfaPowerSupply(Arc), SfaController(Arc), Snapshot(Arc), - SnapshotInterval(Arc), - SnapshotRetention(Arc), + SnapshotPolicy(Arc), StratagemConfig(Arc), Target(Arc), TargetRecord(Arc), @@ -638,8 +620,7 @@ impl From for ArcRecord { Record::SfaController(x) => Self::SfaController(Arc::new(x)), Record::StratagemConfig(x) => Self::StratagemConfig(Arc::new(x)), Record::Snapshot(x) => Self::Snapshot(Arc::new(x)), - Record::SnapshotInterval(x) => Self::SnapshotInterval(Arc::new(x)), - Record::SnapshotRetention(x) => Self::SnapshotRetention(Arc::new(x)), + Record::SnapshotPolicy(x) => Self::SnapshotPolicy(Arc::new(x)), Record::Target(x) => Self::Target(Arc::new(x)), Record::TargetRecord(x) => Self::TargetRecord(Arc::new(x)), Record::User(x) => Self::User(Arc::new(x)), @@ -673,8 +654,7 @@ pub enum RecordId { SfaController(i32), StratagemConfig(i32), Snapshot(i32), - SnapshotInterval(i32), - SnapshotRetention(i32), + SnapshotPolicy(i32), Target(i32), TargetRecord(i32), User(i32), @@ -706,8 +686,7 @@ impl From<&Record> for RecordId { Record::SfaController(x) => RecordId::SfaController(x.id), Record::StratagemConfig(x) => RecordId::StratagemConfig(x.id), Record::Snapshot(x) => RecordId::Snapshot(x.id), - Record::SnapshotInterval(x) => RecordId::SnapshotInterval(x.id), - Record::SnapshotRetention(x) => RecordId::SnapshotRetention(x.id), + Record::SnapshotPolicy(x) => RecordId::SnapshotPolicy(x.id), Record::Target(x) => RecordId::Target(x.id), Record::TargetRecord(x) => RecordId::TargetRecord(x.id), Record::User(x) => RecordId::User(x.id), @@ -742,8 +721,7 @@ impl Deref for RecordId { | Self::SfaController(x) | Self::Snapshot(x) | Self::StratagemConfig(x) - | Self::SnapshotInterval(x) - | Self::SnapshotRetention(x) + | Self::SnapshotPolicy(x) | Self::Target(x) | Self::TargetRecord(x) | Self::User(x) diff --git a/migrations/20201208000000_snapshot_v2.sql b/migrations/20201208000000_snapshot_v2.sql new file mode 100644 index 0000000000..300c364743 --- /dev/null +++ b/migrations/20201208000000_snapshot_v2.sql @@ -0,0 +1,69 @@ +CREATE TABLE IF NOT EXISTS snapshot_policy ( + id serial PRIMARY KEY, + filesystem TEXT NOT NULL UNIQUE, + interval INTERVAL NOT NULL, + barrier BOOLEAN DEFAULT false NOT NULL, + keep INT NOT NULL CHECK (keep > 0), + daily INT DEFAULT 0 NOT NULL CHECK (daily >= 0), + weekly INT DEFAULT 0 NOT NULL CHECK (weekly >= 0), + monthly INT DEFAULT 0 NOT NULL CHECK (monthly >= 0), + last_run TIMESTAMP WITH TIME ZONE +); + + +CREATE OR REPLACE FUNCTION snapshot_policy_func() RETURNS TRIGGER AS $$ +DECLARE + r snapshot_policy; +BEGIN + IF (TG_OP = 'INSERT') OR (TG_OP = 'UPDATE' AND OLD IS DISTINCT FROM NEW) + THEN + r := NEW; + ELSEIF TG_OP = 'DELETE' + THEN + r := OLD; + ELSE + r := NULL; + END IF; + + IF r IS NOT NULL + THEN + PERFORM pg_notify( + 'table_update', + notify_row(TG_OP, TG_TABLE_NAME, + json_build_object( + 'id', r.id, + 'filesystem', r.filesystem, + 'interval', interval_to_seconds(r.interval), + 'barrier', r.barrier, + 'keep', r.keep, + 'daily', r.daily, + 'weekly', r.weekly, + 'monthly', r.monthly, + 'last_run', r.last_run + ) + ) + ); + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + + +DROP TRIGGER IF EXISTS snapshot_policy_notify_update ON snapshot_policy; +CREATE TRIGGER snapshot_policy_notify_update AFTER UPDATE ON snapshot_policy +FOR EACH ROW EXECUTE PROCEDURE snapshot_policy_func(); + +DROP TRIGGER IF EXISTS snapshot_policy_notify_insert ON snapshot_policy; +CREATE TRIGGER snapshot_policy_notify_insert AFTER INSERT ON snapshot_policy +FOR EACH ROW EXECUTE PROCEDURE snapshot_policy_func(); + +DROP TRIGGER IF EXISTS snapshot_policy_notify_delete ON snapshot_policy; +CREATE TRIGGER snapshot_policy_notify_delete AFTER DELETE ON snapshot_policy +FOR EACH ROW EXECUTE PROCEDURE snapshot_policy_func(); + + +DROP TABLE IF EXISTS snapshot_interval CASCADE; +DROP TABLE IF EXISTS snapshot_retention CASCADE; +DROP FUNCTION IF EXISTS table_update_notify_snapshot_interval() CASCADE; +DROP TYPE IF EXISTS snapshot_reserve_unit CASCADE; diff --git a/sqlx-data.json b/sqlx-data.json index 5046855f1d..59f3168fc3 100644 --- a/sqlx-data.json +++ b/sqlx-data.json @@ -513,65 +513,6 @@ "nullable": [] } }, - "17645262c426038efcc8e22bf999c0d3cee07f52c4e276a1b607e2e00b2e62bd": { - "query": "\n SELECT\n id,\n filesystem_name,\n reserve_value,\n reserve_unit as \"reserve_unit:ReserveUnit\",\n last_run,\n keep_num\n FROM snapshot_retention\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int4" - }, - { - "ordinal": 1, - "name": "filesystem_name", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "reserve_value", - "type_info": "Int4" - }, - { - "ordinal": 3, - "name": "reserve_unit:ReserveUnit", - "type_info": { - "Custom": { - "name": "snapshot_reserve_unit", - "kind": { - "Enum": [ - "percent", - "gibibytes", - "tebibytes" - ] - } - } - } - }, - { - "ordinal": 4, - "name": "last_run", - "type_info": "Timestamptz" - }, - { - "ordinal": 5, - "name": "keep_num", - "type_info": "Int4" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false, - false, - false, - false, - true, - false - ] - } - }, "17ed37ab2c915514b18cde4f0a1f3d2bf2eface63c4cb96e18155ab370fe39b6": { "query": "DELETE FROM chroma_core_serverprofile_repolist WHERE serverprofile_id = $1", "describe": { @@ -1295,28 +1236,6 @@ ] } }, - "3a099c680488372f27c087f941161200360329a21145cf0fa22c81f3ec5ee2ef": { - "query": "\n INSERT INTO snapshot_interval (\n filesystem_name,\n use_barrier,\n interval\n )\n VALUES ($1, $2, $3)\n ON CONFLICT (filesystem_name, interval)\n DO NOTHING\n RETURNING id\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int4" - } - ], - "parameters": { - "Left": [ - "Text", - "Bool", - "Interval" - ] - }, - "nullable": [ - false - ] - } - }, "3b1ddcee3dee96365325874c4ea32c832ffd74ab137399443052a0918d69f2ed": { "query": "\n SELECT\n c.id AS id,\n cancelled,\n complete,\n errored,\n created_at,\n array_agg(cj.job_id)::INT[] AS job_ids,\n message\n FROM chroma_core_command c\n JOIN chroma_core_command_jobs cj ON c.id = cj.command_id\n WHERE (c.id = ANY ($3::INT[]))\n GROUP BY c.id\n OFFSET $1 LIMIT $2\n ", "describe": { @@ -1625,107 +1544,6 @@ ] } }, - "45e66b9722e3f0ea68049efc3ce9a52ac0ccc202cd82c415578337c4053c873b": { - "query": "\n SELECT\n id,\n filesystem_name,\n reserve_value,\n reserve_unit as \"reserve_unit:snapshot::ReserveUnit\",\n last_run,\n keep_num\n FROM snapshot_retention\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int4" - }, - { - "ordinal": 1, - "name": "filesystem_name", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "reserve_value", - "type_info": "Int4" - }, - { - "ordinal": 3, - "name": "reserve_unit:snapshot::ReserveUnit", - "type_info": { - "Custom": { - "name": "snapshot_reserve_unit", - "kind": { - "Enum": [ - "percent", - "gibibytes", - "tebibytes" - ] - } - } - } - }, - { - "ordinal": 4, - "name": "last_run", - "type_info": "Timestamptz" - }, - { - "ordinal": 5, - "name": "keep_num", - "type_info": "Int4" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false, - false, - false, - false, - true, - false - ] - } - }, - "46a7815b904eddf8c5b3f77e9c9623806ba3e0a0247080ac9900adaad6b3ce11": { - "query": "SELECT * FROM snapshot_interval", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int4" - }, - { - "ordinal": 1, - "name": "filesystem_name", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "use_barrier", - "type_info": "Bool" - }, - { - "ordinal": 3, - "name": "last_run", - "type_info": "Timestamptz" - }, - { - "ordinal": 4, - "name": "interval", - "type_info": "Interval" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false, - false, - false, - true, - false - ] - } - }, "484084fcd20a682adf9354fc35b4180e0ec9190f3941e993ba44ec30e60d0d20": { "query": "\n SELECT cluster_id, name, active\n FROM corosync_resource\n WHERE resource_agent = 'ocf::ddn:Ticketer';\n ", "describe": { @@ -2476,6 +2294,24 @@ ] } }, + "71b25feb0a6f2f39d6cb50e81d252016d921aaed81798aa23e3805bffc6c0e77": { + "query": "\n INSERT INTO snapshot_policy (\n filesystem,\n interval,\n barrier,\n keep,\n daily,\n weekly,\n monthly\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n ON CONFLICT (filesystem)\n DO UPDATE SET\n interval = EXCLUDED.interval,\n barrier = EXCLUDED.barrier,\n keep = EXCLUDED.keep,\n daily = EXCLUDED.daily,\n weekly = EXCLUDED.weekly,\n monthly = EXCLUDED.monthly\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Interval", + "Bool", + "Int4", + "Int4", + "Int4", + "Int4" + ] + }, + "nullable": [] + } + }, "78b8a5d4f2a0f92377ff7921c89adb5d188bce96d9cc5fcf25a05f6213bf8be4": { "query": "\n UPDATE chroma_core_managedtarget SET\n state_modified_at = now(),\n state = 'mounted',\n immutable_state = 'f',\n name = $1,\n ha_label = $2,\n reformat = 'f',\n content_type_id = $3\n WHERE uuid = $4\n ", "describe": { @@ -2855,6 +2691,72 @@ ] } }, + "82aa67ceef3e830c8a4f4739d8e02c2fce46906173bbc19dedcc324d0183628d": { + "query": "SELECT * FROM snapshot_policy", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "filesystem", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "interval", + "type_info": "Interval" + }, + { + "ordinal": 3, + "name": "barrier", + "type_info": "Bool" + }, + { + "ordinal": 4, + "name": "keep", + "type_info": "Int4" + }, + { + "ordinal": 5, + "name": "daily", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "weekly", + "type_info": "Int4" + }, + { + "ordinal": 7, + "name": "monthly", + "type_info": "Int4" + }, + { + "ordinal": 8, + "name": "last_run", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + true + ] + } + }, "857ec5f2517d25f901399ddfb8efa1ab3f73ed8c8f899692c071172b61179d1a": { "query": "select * from chroma_core_lnetconfiguration where not_deleted = 't'", "describe": { @@ -3073,20 +2975,6 @@ ] } }, - "8ac87d0e2c016985d993de3824a253423b3aa038a1609a0eecc2562d1b4887aa": { - "query": "\n UPDATE snapshot_interval\n SET last_run=$1\n WHERE id=$2 AND filesystem_name=$3\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Timestamptz", - "Int4", - "Text" - ] - }, - "nullable": [] - } - }, "8b11d7745ec336638b333bc511166314f63619293bf8b3df15fd1df807e1ee9a": { "query": "SELECT id, message FROM chroma_core_alertstate WHERE lustre_pid = $1 ORDER BY id DESC LIMIT 1", "describe": { @@ -3266,25 +3154,39 @@ ] } }, - "922e7457db1e165807273def66a5ecc6b22be1a849f5e4b06d5149ec00b5e8aa": { - "query": "\n DELETE FROM chroma_core_logmessage\n WHERE id in ( \n SELECT id FROM chroma_core_logmessage ORDER BY id LIMIT $1\n )\n ", + "9225bc4f7185030de710bb86e0ef3a1bbbf9e8f96e08389a1d6743beffd3ee1c": { + "query": "\n SELECT snapshot_name, create_time FROM snapshot\n WHERE filesystem_name = $1\n AND snapshot_name LIKE '\\_\\_%'\n ORDER BY create_time DESC\n LIMIT 1\n ", "describe": { - "columns": [], + "columns": [ + { + "ordinal": 0, + "name": "snapshot_name", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "create_time", + "type_info": "Timestamptz" + } + ], "parameters": { "Left": [ - "Int8" + "Text" ] }, - "nullable": [] + "nullable": [ + false, + false + ] } }, - "93e2695978ceecbebff40c31f2f58bf6fd5351869d3b279adc5ad1f7436a25e9": { - "query": "DELETE FROM snapshot_interval WHERE id=$1", + "922e7457db1e165807273def66a5ecc6b22be1a849f5e4b06d5149ec00b5e8aa": { + "query": "\n DELETE FROM chroma_core_logmessage\n WHERE id in ( \n SELECT id FROM chroma_core_logmessage ORDER BY id LIMIT $1\n )\n ", "describe": { "columns": [], "parameters": { "Left": [ - "Int4" + "Int8" ] }, "nullable": [] @@ -3344,68 +3246,6 @@ ] } }, - "9bc21bd7e2f9506e3ae0cb80fd80e0fc95243470a9bedd1d728a860d7d6b408f": { - "query": "SELECT * FROM snapshot WHERE filesystem_name = $1 ORDER BY create_time ASC", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int4" - }, - { - "ordinal": 1, - "name": "filesystem_name", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "snapshot_name", - "type_info": "Text" - }, - { - "ordinal": 3, - "name": "create_time", - "type_info": "Timestamptz" - }, - { - "ordinal": 4, - "name": "modify_time", - "type_info": "Timestamptz" - }, - { - "ordinal": 5, - "name": "snapshot_fsname", - "type_info": "Text" - }, - { - "ordinal": 6, - "name": "mounted", - "type_info": "Bool" - }, - { - "ordinal": 7, - "name": "comment", - "type_info": "Text" - } - ], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - true - ] - } - }, "9d75d9c59b4e8e5e54653866451a026b2308d9d97f1a5ea65513de4a1ce3862c": { "query": "\n INSERT INTO chroma_core_managedtarget (\n state_modified_at,\n state,\n immutable_state,\n name,\n uuid,\n ha_label,\n reformat,\n not_deleted,\n content_type_id\n ) VALUES (now(), 'mounted', 'f', $1, $2, $3, 'f', 't', $4)\n RETURNING id\n ", "describe": { @@ -3521,6 +3361,19 @@ ] } }, + "a0e41b98f3319a78ac985a3e7c0d5204cdceb8fd0ceb04b69d58e50abecf16b7": { + "query": "UPDATE snapshot_policy SET last_run = $1 WHERE filesystem = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Timestamptz", + "Text" + ] + }, + "nullable": [] + } + }, "a3269a5f7c491a332facfcf86c576350f4b9e7c14638a26d0bd17daef52c0613": { "query": "SELECT\n id,\n index,\n enclosure_index,\n failed,\n slot_number,\n health_state as \"health_state: HealthState\",\n health_state_reason,\n member_index,\n member_state as \"member_state: MemberState\",\n storage_system\n FROM chroma_core_sfadiskdrive\n ", "describe": { @@ -3593,6 +3446,32 @@ ] } }, + "a4916e07e7f17d6bddb11973898bdbd1d38c0f70d80f75897e2048209cf0df6f": { + "query": "\n SELECT snapshot_name, create_time FROM snapshot\n WHERE filesystem_name = $1\n AND snapshot_name LIKE '\\_\\_%'\n ORDER BY create_time DESC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "snapshot_name", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "create_time", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false + ] + } + }, "a5e14b628a8f67d458167f1ea5d0390aacd72b92a725bb1092e6d9c104414a7b": { "query": "\n WITH updated AS (\n INSERT INTO nid\n (net_type, host_id, nid, status, interfaces)\n SELECT net_type, host_id, nid, status, string_to_array(interfaces, ',')::text[]\n FROM UNNEST($1::text[], $2::int[], $3::text[], $4::text[], $5::text[])\n AS t(net_type, host_id, nid, status, interfaces)\n ON CONFLICT (host_id, nid)\n DO\n UPDATE SET net_type = EXCLUDED.net_type,\n status = EXCLUDED.status,\n interfaces = EXCLUDED.interfaces\n RETURNING id\n )\n\n INSERT INTO lnet\n (host_id, state, nids)\n (SELECT $6, $7, array_agg(id) from updated)\n ON CONFLICT (host_id)\n DO\n UPDATE SET nids = EXCLUDED.nids,\n state = EXCLUDED.state;\n ", "describe": { @@ -4354,18 +4233,6 @@ "nullable": [] } }, - "c92c006232fff5c4ba37894b349d63142824fc0141a156f06114fa113ed36fa7": { - "query": "DELETE FROM snapshot_retention WHERE id=$1", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int4" - ] - }, - "nullable": [] - } - }, "c9f733dc3958a75b638d7c6f567e6fccf89575ac2ac3a43a14bf64978557d91c": { "query": "SELECT id, name, cluster_id, resource_agent, role, active, orphaned, managed,\n failed, failure_ignored, nodes_running_on, (active_node).id AS active_node_id,\n (active_node).name AS active_node_name, mount_point\n FROM corosync_resource", "describe": { @@ -4712,26 +4579,13 @@ ] } }, - "dc969e1cf438fc20baccd36a43cf7725bcf95cf758c6f08d5ce88bd4f378a71c": { - "query": "\n INSERT INTO snapshot_retention (\n filesystem_name,\n reserve_value,\n reserve_unit,\n keep_num\n )\n VALUES ($1, $2, $3, $4)\n ON CONFLICT (filesystem_name)\n DO UPDATE SET\n reserve_value = EXCLUDED.reserve_value,\n reserve_unit = EXCLUDED.reserve_unit,\n keep_num = EXCLUDED.keep_num\n ", + "dc27c8e53476f37d4d8b5d539be33f3b62071e35d95ebea78e15a5ce59d35550": { + "query": "\n DELETE FROM snapshot_policy\n WHERE (filesystem IS NOT DISTINCT FROM $1)\n OR (id IS NOT DISTINCT FROM $2)\n\n ", "describe": { "columns": [], "parameters": { "Left": [ "Text", - "Int4", - { - "Custom": { - "name": "snapshot_reserve_unit", - "kind": { - "Enum": [ - "percent", - "gibibytes", - "tebibytes" - ] - } - } - }, "Int4" ] }, @@ -4832,65 +4686,6 @@ ] } }, - "e211fee58a58a67ae21206e84802a76ce81d15f4a3573bcceb3ba55f32a282b1": { - "query": "\n SELECT\n id,\n filesystem_name,\n reserve_value,\n reserve_unit as \"reserve_unit:ReserveUnit\",\n last_run,\n keep_num\n FROM snapshot_retention\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int4" - }, - { - "ordinal": 1, - "name": "filesystem_name", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "reserve_value", - "type_info": "Int4" - }, - { - "ordinal": 3, - "name": "reserve_unit:ReserveUnit", - "type_info": { - "Custom": { - "name": "snapshot_reserve_unit", - "kind": { - "Enum": [ - "percent", - "gibibytes", - "tebibytes" - ] - } - } - } - }, - { - "ordinal": 4, - "name": "last_run", - "type_info": "Timestamptz" - }, - { - "ordinal": 5, - "name": "keep_num", - "type_info": "Int4" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false, - false, - false, - false, - true, - false - ] - } - }, "e556047b44f30c75388944aa4d96d4ade4f5eed4e0a401bbd766943cf9495ca0": { "query": "\n SELECT \n mt.state,\n t.name,\n t.filesystems\n FROM chroma_core_managedtarget mt\n INNER JOIN target t\n ON t.uuid = mt.uuid\n WHERE mt.not_deleted = 't'\n AND $1::text[] @> t.filesystems;\n ", "describe": {