diff --git a/Cargo.lock b/Cargo.lock index ee1f14658..fb10ade55 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -578,15 +578,6 @@ dependencies = [ "windows-sys 0.61.0", ] -[[package]] -name = "convert_case" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "core-foundation" version = "0.9.4" @@ -635,9 +626,11 @@ checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ "bitflags 2.9.4", "crossterm_winapi", + "futures-core", "mio", "parking_lot", "rustix 0.38.44", + "serde", "signal-hook", "signal-hook-mio", "winapi", @@ -651,15 +644,9 @@ checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ "bitflags 2.9.4", "crossterm_winapi", - "derive_more", "document-features", - "futures-core", - "mio", "parking_lot", "rustix 1.1.2", - "serde", - "signal-hook", - "signal-hook-mio", "winapi", ] @@ -823,27 +810,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "derive_more" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "devd-rs" version = "0.3.6" @@ -936,6 +902,16 @@ dependencies = [ "litrs", ] +[[package]] +name = "edit" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f364860e764787163c8c8f58231003839be31276e821e2ad2092ddf496b1aa09" +dependencies = [ + "tempfile", + "which", +] + [[package]] name = "either" version = "1.15.0" @@ -1346,6 +1322,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "http" version = "1.3.1" @@ -2326,10 +2311,11 @@ dependencies = [ "clap", "color-eyre", "config", - "crossterm 0.29.0", + "crossterm 0.28.1", "derive_builder", "derive_deref", "dirs", + "edit", "eyre", "futures", "itertools 0.14.0", @@ -4196,6 +4182,18 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/deny.toml b/deny.toml index 33865f252..3f713cf25 100644 --- a/deny.toml +++ b/deny.toml @@ -94,6 +94,7 @@ ignore = [ allow = [ "MIT", "Apache-2.0", + "CC0-1.0", "CDLA-Permissive-2.0", "Unicode-DFS-2016", "BSD-3-Clause", diff --git a/openstack_sdk/src/api/network/v2/security_group_rule/create.rs b/openstack_sdk/src/api/network/v2/security_group_rule/create.rs index f70aa8744..301ffe0af 100644 --- a/openstack_sdk/src/api/network/v2/security_group_rule/create.rs +++ b/openstack_sdk/src/api/network/v2/security_group_rule/create.rs @@ -30,7 +30,7 @@ use serde::Deserialize; use serde::Serialize; use std::borrow::Cow; -#[derive(Debug, Deserialize, Clone, Serialize)] +#[derive(Debug, Deserialize, Clone, Eq, PartialEq, Serialize)] pub enum Direction { #[serde(rename = "egress")] Egress, @@ -38,7 +38,7 @@ pub enum Direction { Ingress, } -#[derive(Debug, Deserialize, Clone, Serialize)] +#[derive(Debug, Deserialize, Clone, Eq, PartialEq, Serialize)] pub enum Ethertype { #[serde(rename = "IPv4")] Ipv4, diff --git a/openstack_tui/.config/config.yaml b/openstack_tui/.config/config.yaml index 3695f1046..2a42f6bef 100644 --- a/openstack_tui/.config/config.yaml +++ b/openstack_tui/.config/config.yaml @@ -240,6 +240,9 @@ mode_keybindings: SetNetworkSecurityGroupRuleListFilters: {} description: All type: Filter + "ctrl-n": + action: CreateNetworkSecurityGroupRule + description: Create "ctrl-d": action: DeleteNetworkSecurityGroupRule description: Delete diff --git a/openstack_tui/Cargo.toml b/openstack_tui/Cargo.toml index 3404cb297..fef5ebf6f 100644 --- a/openstack_tui/Cargo.toml +++ b/openstack_tui/Cargo.toml @@ -24,10 +24,13 @@ chrono = { workspace= true } clap = { workspace = true, features = ["cargo", "derive", "env", "wrap_help", "unicode", "string", "unstable-styles"] } color-eyre = { workspace = true } config = { workspace = true, features = ["json", "json5", "yaml"] } -crossterm = { version = "^0.29", features = ["serde", "event-stream"] } +# edtui MUST use the same version as ratatui otherwise the Into does not work +crossterm = { version = "0.28.1", features = ["serde", "event-stream"] } derive_builder = { workspace = true } derive_deref = "^1.1" dirs = { workspace = true } +edit = "0.1.5" +#edtui = { version = "0.9" } eyre = { workspace = true } futures = { workspace = true } itertools = { workspace = true } @@ -37,7 +40,7 @@ open.workspace = true openstack_sdk = { path = "../openstack_sdk", version = "^0.22", default-features = false, features = ["async", "block_storage", "compute", "dns", "identity", "image", "load_balancer", "network"] } openstack_types = { path = "../openstack_types", version = "^0.22" } pretty_assertions = "^1.4" -ratatui = { version = "^0.29", features = ["serde", "macros"] } +ratatui = { version = "^0.29", features = ["serde", "macros", "crossterm"] } secrecy = "0.10.3" serde = { workspace = true } serde_json = { workspace = true } diff --git a/openstack_tui/src/action.rs b/openstack_tui/src/action.rs index 39ddb775f..a6dc07fa2 100644 --- a/openstack_tui/src/action.rs +++ b/openstack_tui/src/action.rs @@ -92,6 +92,16 @@ pub enum Action { /// Close confirmation prompt ConfirmAccepted(cloud_types::ApiRequest), + /// Edit. Open the default editor to get the user input for the operation. + Edit { + template: String, + original_action: Box, + }, + EditResult { + result: serde_json::Value, + original_action: Box, + }, + // Block Storage (Cinder) /// Delete volume DeleteBlockStorageVolume, @@ -183,6 +193,9 @@ pub enum Action { SetNetworkSecurityGroupListFilters(cloud_types::NetworkSecurityGroupList), /// Switch to NetworkSecurityGroupRules ShowNetworkSecurityGroupRules, + /// Create the security group rule. + CreateNetworkSecurityGroupRule, + //CreateNetworkSecurityGroupRuleData(serde_json::Value), /// Delete the security group rule. DeleteNetworkSecurityGroupRule, /// Switch to routers view diff --git a/openstack_tui/src/app.rs b/openstack_tui/src/app.rs index ff66fbb2c..febff4d6c 100644 --- a/openstack_tui/src/app.rs +++ b/openstack_tui/src/app.rs @@ -55,9 +55,12 @@ use crate::{ pools::LoadBalancerPools, }, network::{ - networks::NetworkNetworks, routers::NetworkRouters, + networks::NetworkNetworks, + routers::NetworkRouters, + //security_group_rule_create::CreateSecurityGroupRuleComponent, security_group_rules::NetworkSecurityGroupRules, - security_groups::NetworkSecurityGroups, subnets::NetworkSubnets, + security_groups::NetworkSecurityGroups, + subnets::NetworkSubnets, }, project_select_popup::ProjectSelect, resource_select_popup::ApiRequestSelect, @@ -76,6 +79,7 @@ enum Popup { SelectApiRequest, SwitchCloud, SwitchProject, + //CreateNetworkSecurityGroupRule, Confirm, } @@ -439,6 +443,21 @@ impl App { self.active_popup = Some(Popup::AuthHelper); self.render(tui)?; } + Action::Edit { + ref template, + ref original_action, + } => { + tui.exit()?; + let edited = edit::edit(template)?; + tracing::debug!("after editing: '{}'", edited); + tui.enter()?; + tui.terminal.clear()?; + self.action_tx.send(Action::EditResult { + result: serde_json::from_value(serde_yaml::from_str(&edited)?)?, + original_action: original_action.clone(), + })?; + self.render(tui)?; + } Action::AuthHelperCompleted => { self.active_popup = None; self.render(tui)?; diff --git a/openstack_tui/src/cloud_worker/network/v2/security_group_rule/list.rs b/openstack_tui/src/cloud_worker/network/v2/security_group_rule/list.rs index 4118ca63e..910da45cf 100644 --- a/openstack_tui/src/cloud_worker/network/v2/security_group_rule/list.rs +++ b/openstack_tui/src/cloud_worker/network/v2/security_group_rule/list.rs @@ -144,66 +144,66 @@ impl TryFrom<&NetworkSecurityGroupRuleList> for RequestBuilder<'_> { type Error = Report; fn try_from(value: &NetworkSecurityGroupRuleList) -> Result { let mut ep_builder = Self::default(); - if let Some(val) = &value.limit { - ep_builder.limit(*val); - } - if let Some(val) = &value.marker { - ep_builder.marker(val.clone()); - } - if let Some(val) = &value.page_reverse { - ep_builder.page_reverse(*val); + if let Some(val) = &value.id { + ep_builder.id(val.clone()); } - if let Some(val) = &value.belongs_to_default_sg { - ep_builder.belongs_to_default_sg(*val); + if let Some(val) = &value.security_group_id { + ep_builder.security_group_id(val.clone()); } - if let Some(val) = &value.description { - ep_builder.description(val.clone()); + if let Some(val) = &value.remote_group_id { + ep_builder.remote_group_id(val.clone()); } if let Some(val) = &value.direction { ep_builder.direction(val.clone()); } - if let Some(val) = &value.ethertype { - ep_builder.ethertype(val.clone()); - } - if let Some(val) = &value.id { - ep_builder.id(val.clone()); - } - if let Some(val) = &value.normalized_cidr { - ep_builder.normalized_cidr(val.clone()); - } - if let Some(val) = &value.port_range_max { - ep_builder.port_range_max(*val); + if let Some(val) = &value.protocol { + ep_builder.protocol(val.clone()); } if let Some(val) = &value.port_range_min { ep_builder.port_range_min(*val); } - if let Some(val) = &value.protocol { - ep_builder.protocol(val.clone()); - } - if let Some(val) = &value.remote_address_group_id { - ep_builder.remote_address_group_id(val.clone()); + if let Some(val) = &value.port_range_max { + ep_builder.port_range_max(*val); } - if let Some(val) = &value.remote_group_id { - ep_builder.remote_group_id(val.clone()); + if let Some(val) = &value.ethertype { + ep_builder.ethertype(val.clone()); } if let Some(val) = &value.remote_ip_prefix { ep_builder.remote_ip_prefix(val.clone()); } + if let Some(val) = &value.tenant_id { + ep_builder.tenant_id(val.clone()); + } if let Some(val) = &value.revision_number { ep_builder.revision_number(val.clone()); } - if let Some(val) = &value.security_group_id { - ep_builder.security_group_id(val.clone()); + if let Some(val) = &value.description { + ep_builder.description(val.clone()); } - if let Some(val) = &value.tenant_id { - ep_builder.tenant_id(val.clone()); + if let Some(val) = &value.normalized_cidr { + ep_builder.normalized_cidr(val.clone()); } - if let Some(val) = &value.sort_dir { - ep_builder.sort_dir(val.iter().cloned()); + if let Some(val) = &value.remote_address_group_id { + ep_builder.remote_address_group_id(val.clone()); + } + if let Some(val) = &value.belongs_to_default_sg { + ep_builder.belongs_to_default_sg(*val); } if let Some(val) = &value.sort_key { ep_builder.sort_key(val.iter().cloned()); } + if let Some(val) = &value.sort_dir { + ep_builder.sort_dir(val.iter().cloned()); + } + if let Some(val) = &value.limit { + ep_builder.limit(*val); + } + if let Some(val) = &value.marker { + ep_builder.marker(val.clone()); + } + if let Some(val) = &value.page_reverse { + ep_builder.page_reverse(*val); + } Ok(ep_builder) } diff --git a/openstack_tui/src/components/network/security_group_rules.rs b/openstack_tui/src/components/network/security_group_rules.rs index 7f31f8254..ab3d89285 100644 --- a/openstack_tui/src/components/network/security_group_rules.rs +++ b/openstack_tui/src/components/network/security_group_rules.rs @@ -136,11 +136,13 @@ impl Component for NetworkSecurityGroupRules<'_> { } => { if let NetworkSecurityGroupRuleApiRequest::List(_) = *req { self.set_data(data)?; + } else if let NetworkSecurityGroupRuleApiRequest::Create(_) = *req { + self.set_data(data)?; } } Action::ApiResponseData { request: ApiRequest::Network(NetworkApiRequest::SecurityGroupRule(req)), - .. + data, } => { if let NetworkSecurityGroupRuleApiRequest::Delete(del) = *req { let NetworkSecurityGroupRuleDelete { ref id, .. } = *del; @@ -149,6 +151,9 @@ impl Component for NetworkSecurityGroupRules<'_> { } self.sync_table_data()?; self.set_loading(false); + } else if let NetworkSecurityGroupRuleApiRequest::Create(_) = *req { + self.append_new_row(data)?; + self.set_loading(false); } } Action::DeleteNetworkSecurityGroupRule => { @@ -169,6 +174,77 @@ impl Component for NetworkSecurityGroupRules<'_> { } } } + Action::CreateNetworkSecurityGroupRule => { + if let Some(command_tx) = self.get_command_tx() { + command_tx.send(Action::Edit { + template: format!( + r#"# Please provide the SecurityGroupRule data as YAML +security_group_rule: + # /// The security group ID to associate with this security group rule. + security_group_id: "{}" + + # /// The minimum port number in the range that is matched by the security + # /// group rule. If the protocol is TCP, UDP, DCCP, SCTP or UDP-Lite this + # /// value must be less than or equal to the `port_range_max` attribute + # /// value. If the protocol is ICMP, this value must be an ICMP type. + # port_range_min: + + # /// The maximum port number in the range that is matched by the security + # /// group rule. If the protocol is TCP, UDP, DCCP, SCTP or UDP-Lite this + # /// value must be greater than or equal to the `port_range_min` attribute + # /// value. If the protocol is ICMP, this value must be an ICMP code. + # port_range_max: + + # /// The IP protocol can be represented by a string, an integer, or `null`. + # /// Valid string or integer values are `any` or `0`, `ah` or `51`, `dccp` + # /// or `33`, `egp` or `8`, `esp` or `50`, `gre` or `47`, `icmp` or `1`, + # /// `icmpv6` or `58`, `igmp` or `2`, `ipip` or `4`, `ipv6-encap` or `41`, + # /// `ipv6-frag` or `44`, `ipv6-icmp` or `58`, `ipv6-nonxt` or `59`, + # /// `ipv6-opts` or `60`, `ipv6-route` or `43`, `ospf` or `89`, `pgm` or + # /// `113`, `rsvp` or `46`, `sctp` or `132`, `tcp` or `6`, `udp` or `17`, + # /// `udplite` or `136`, `vrrp` or `112`. Additionally, any integer value + # /// between [0-255] is also valid. The string `any` (or integer `0`) means + # /// `all` IP protocols. See the constants in `neutron_lib.constants` for + # /// the most up-to-date list of supported strings. + + # protocol: + # /// Must be IPv4 or IPv6, and addresses represented in CIDR must match the + # /// ingress or egress rules. + + # ethertype: + + # /// Ingress or egress, which is the direction in which the security group + # /// rule is applied. + # direction: + + # /// A human-readable description for the resource. Default is an empty + # /// string. + # description: +"#, + self.get_filters() + .security_group_id + .clone() + .unwrap_or("".to_string()) + ), + original_action: Box::new(Action::CreateNetworkSecurityGroupRule), + })?; + } + } + Action::EditResult { + result, + original_action, + } => { + if let Action::CreateNetworkSecurityGroupRule = *original_action { + tracing::debug!("Would be creating sgr with {:?}", result); + self.set_loading(true); + if let Some(command_tx) = self.get_command_tx() { + let data: crate::cloud_worker::network::v2::security_group_rule::NetworkSecurityGroupRuleCreate = serde_json::from_value(result)?; + command_tx.send(Action::Confirm(ApiRequest::from( + NetworkSecurityGroupRuleApiRequest::Create(Box::new(data)), + )))?; + } + } + } _ => {} }; Ok(None) diff --git a/openstack_tui/src/components/table_view.rs b/openstack_tui/src/components/table_view.rs index b0189769d..f910d9d04 100644 --- a/openstack_tui/src/components/table_view.rs +++ b/openstack_tui/src/components/table_view.rs @@ -711,4 +711,15 @@ where } Ok(()) } + + /// append new row. + #[instrument(level = "debug", skip(self))] + pub fn append_new_row(&mut self, data: Value) -> Result<(), TuiError> { + let item = serde_json::from_value::(data.clone())?; + self.items.push(item); + self.raw_items.push(data.clone()); + self.sync_table_data()?; + self.set_loading(false); + Ok(()) + } }