diff --git a/plugins/ufm_rust_client/.gitignore b/plugins/ufm_rust_client/.gitignore new file mode 100644 index 00000000..ebe7173a --- /dev/null +++ b/plugins/ufm_rust_client/.gitignore @@ -0,0 +1,17 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + + +# Added by cargo + +/target +/Cargo.lock +/.idea/ diff --git a/plugins/ufm_rust_client/Cargo.toml b/plugins/ufm_rust_client/Cargo.toml new file mode 100644 index 00000000..0f78cbfa --- /dev/null +++ b/plugins/ufm_rust_client/Cargo.toml @@ -0,0 +1,8 @@ +[workspace] + +members = [ + "cli", + "client", +] + + diff --git a/plugins/ufm_rust_client/README.md b/plugins/ufm_rust_client/README.md new file mode 100644 index 00000000..3ded3bbd --- /dev/null +++ b/plugins/ufm_rust_client/README.md @@ -0,0 +1,71 @@ +## *UFM rust client* +The rust client of UFM + +### *Build* +To install [rust](https://www.rust-lang.org/tools/install) on the machine. + +``` +cargo build +``` + +### *Usage* + +Use the following syntax to run `ufmcli` commands from your terminal window: +``` +ufmcli [flags] [command] [parameters] +``` + +where flags, command, and parameters are: +* `flags`: Specifies optional flags. For example, you can use the `--ufm-address` or `--ufm-token` flags to specify the address and token of the UFM server. +* `command`: Specifies the operation that you want to perform on UFM, for example `view`, `list`, `delete`, `create`. +* `parameters`: Specifies the parameters for command, for example `-p` or `--pkey` to specify the pkey for the partition. + +`ufmcli` needs to know the UFM server address and authentication(username/password or token) to access UFM server. + +There are two ways to set the UFM server address and authentication for `ufmcli` command. +* Environment: Set environment `UFM_ADDRESS` for the UFM server address, for example `export UFM_ADDRESS=http://hpc-cloud-gw.mtr.labs.mlnx:4402`. Set environment `UFM_TOKEN` or `UFM_USERNAME`, `UFM_PASSWORD` for the authentication. +* Flag: Specifies the flags in the `ufmcli` command, you can use `--ufm-address` to specify the UFM address and `--ufm-token` or `--ufm-username`, `--ufm-password` to specify the authentication. + + +#### *Version* +``` +./ufmcli version +6.11.1-2 +``` +#### *Create a Partition Key* +``` +./ufmcli create --pkey 5 --mtu 2 --membership full --service-level 0 --rate-limit 2.5 --guids 0011223344560200 --guids 1070fd0300176625 --guids 0011223344560201 +``` + +#### *View a Partition Key* +``` +./ufmcli view --pkey 0x5 +Name : api_pkey_0x5 +Pkey : 0x5 +IPoIB : false +MTU : 2 +Rate Limit : 2.5 +Service Level : 0 +Ports : + GUID ParentGUID PortType SystemID LID SystemName LogState Name + 0011223344560200 1070fd0300176624 virtual 1070fd0300176624 7 hpc-cloud01 Active + 1070fd0300176625 physical 1070fd0300176624 4 hpc-cloud01 Active 1070fd0300176625_2 + 0011223344560201 65535 Unknown + +``` + +#### *List Partition Keys* +``` +./ufmcli list +Name Pkey IPoIB MTU Rate Level +api_pkey_0x5 0x5 false 2 2.5 0 +api_pkey_0x2 0x2 false 2 2.5 0 +management 0x7fff true 2 2.5 0 +api_pkey_0x1 0x1 false 2 2.5 0 +api_pkey_0x4 0x4 false 2 2.5 0 +``` + +#### *Delete a Partition Key* +``` +./ufmcli delete --pkey 0x2 +``` \ No newline at end of file diff --git a/plugins/ufm_rust_client/cli/Cargo.toml b/plugins/ufm_rust_client/cli/Cargo.toml new file mode 100644 index 00000000..7cfe6c5b --- /dev/null +++ b/plugins/ufm_rust_client/cli/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "ufmcli" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +ufmclient = { path = "../client" } +tokio = { version = "1", features = ["full"] } +clap = { version = "4.1", features = ["derive", "env"] } +env_logger = { version = "0.10" } + diff --git a/plugins/ufm_rust_client/cli/src/create.rs b/plugins/ufm_rust_client/cli/src/create.rs new file mode 100644 index 00000000..3516536c --- /dev/null +++ b/plugins/ufm_rust_client/cli/src/create.rs @@ -0,0 +1,55 @@ +/* + * Copyright © 2013-2022 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. + * + * This software product is a proprietary product of Nvidia Corporation and its affiliates + * (the "Company") and all right, title, and interest in and to the software product, + * including all associated intellectual property rights, are and shall + * remain exclusively with the Company. + * + * This software product is governed by the End User License Agreement + * provided with the software product. + */ + + +use ufmclient::{ + Partition, PartitionKey, PartitionQoS, PortConfig, PortMembership, UFMConfig, UFMError, +}; + +pub struct CreateOptions { + pub pkey: String, + pub mtu: u16, + pub ipoib: bool, + pub index0: bool, + pub membership: String, + pub service_level: u8, + pub rate_limit: f64, + pub guids: Vec, +} + +pub async fn run(conf: UFMConfig, opt: &CreateOptions) -> Result<(), UFMError> { + let ufm = ufmclient::connect(conf)?; + + let mut pbs = vec![]; + for g in &opt.guids { + pbs.push(PortConfig { + guid: g.to_string(), + index0: opt.index0, + membership: PortMembership::try_from(opt.membership.clone())?, + }) + } + + let p = Partition { + name: "".to_string(), + pkey: PartitionKey::try_from(opt.pkey.clone())?, + ipoib: opt.ipoib, + qos: PartitionQoS { + mtu_limit: opt.mtu, + service_level: opt.service_level, + rate_limit: opt.rate_limit, + }, + }; + + ufm.bind_ports(p, pbs).await?; + + Ok(()) +} diff --git a/plugins/ufm_rust_client/cli/src/delete.rs b/plugins/ufm_rust_client/cli/src/delete.rs new file mode 100644 index 00000000..11500cec --- /dev/null +++ b/plugins/ufm_rust_client/cli/src/delete.rs @@ -0,0 +1,20 @@ +/* + * Copyright © 2013-2022 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. + * + * This software product is a proprietary product of Nvidia Corporation and its affiliates + * (the "Company") and all right, title, and interest in and to the software product, + * including all associated intellectual property rights, are and shall + * remain exclusively with the Company. + * + * This software product is governed by the End User License Agreement + * provided with the software product. + */ + +use ufmclient::{UFMConfig, UFMError}; + +pub async fn run(conf: UFMConfig, pkey: &String) -> Result<(), UFMError> { + let ufm = ufmclient::connect(conf)?; + ufm.delete_partition(pkey).await?; + + Ok(()) +} diff --git a/plugins/ufm_rust_client/cli/src/list.rs b/plugins/ufm_rust_client/cli/src/list.rs new file mode 100644 index 00000000..6a8e0297 --- /dev/null +++ b/plugins/ufm_rust_client/cli/src/list.rs @@ -0,0 +1,37 @@ +/* + * Copyright © 2013-2022 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. + * + * This software product is a proprietary product of Nvidia Corporation and its affiliates + * (the "Company") and all right, title, and interest in and to the software product, + * including all associated intellectual property rights, are and shall + * remain exclusively with the Company. + * + * This software product is governed by the End User License Agreement + * provided with the software product. + */ + +use ufmclient::{UFMConfig, UFMError}; + +pub async fn run(conf: UFMConfig) -> Result<(), UFMError> { + let ufm = ufmclient::connect(conf)?; + let ps = ufm.list_partition().await?; + + println!( + "{:<15}{:<10}{:<10}{:<10}{:<10}{:<10}", + "Name", "Pkey", "IPoIB", "MTU", "Rate", "Level" + ); + + for p in ps { + println!( + "{:<15}{:<10}{:<10}{:<10}{:<10}{:<10}", + p.name, + p.pkey.to_string(), + p.ipoib, + p.qos.mtu_limit, + p.qos.rate_limit, + p.qos.service_level + ) + } + + Ok(()) +} diff --git a/plugins/ufm_rust_client/cli/src/main.rs b/plugins/ufm_rust_client/cli/src/main.rs new file mode 100644 index 00000000..0b529a2d --- /dev/null +++ b/plugins/ufm_rust_client/cli/src/main.rs @@ -0,0 +1,138 @@ +/* + * Copyright © 2013-2022 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. + * + * This software product is a proprietary product of Nvidia Corporation and its affiliates + * (the "Company") and all right, title, and interest in and to the software product, + * including all associated intellectual property rights, are and shall + * remain exclusively with the Company. + * + * This software product is governed by the End User License Agreement + * provided with the software product. + */ + +use clap::{Parser, Subcommand}; +use ufmclient::{UFMConfig, UFMError}; + +mod create; +mod delete; +mod list; +mod version; +mod view; + +#[derive(Parser)] +#[command(name = "ufm")] +#[command(version = "0.1.0")] +#[command(about = "UFM command line", long_about = None)] +struct Options { + #[clap(long, env = "UFM_ADDRESS")] + ufm_address: Option, + #[clap(long, env = "UFM_USERNAME")] + ufm_username: Option, + #[clap(long, env = "UFM_PASSWORD")] + ufm_password: Option, + #[clap(long, env = "UFM_TOKEN")] + ufm_token: Option, + #[clap(subcommand)] + command: Option, +} + +#[derive(Subcommand)] +enum Commands { + /// View the detail of the partition + View { + /// The pkey of the partition to view + #[arg(short, long)] + pkey: String, + }, + /// List all partitions + List, + /// Get the version of UFM + Version, + /// Delete the partition + Delete { + /// The pkey of the partition to delete + #[arg(short, long)] + pkey: String, + }, + /// Create a partition + Create { + /// The pkey for the new partition + #[arg(short, long)] + pkey: String, + /// The MTU of the new partition + #[arg(long, default_value_t = 2048)] + mtu: u16, + /// The IPOverIB of the new partition + #[arg(long, default_value_t = true)] + ipoib: bool, + /// The Index0 of the new partition + #[arg(long, default_value_t = true)] + index0: bool, + /// The Membership of the new partition + #[arg(short, long, default_value_t = String::from("full"))] + membership: String, + /// The ServiceLevel of the new partition + #[arg(short, long, default_value_t = 0)] + service_level: u8, + /// The RateLimit of the new partition + #[arg(short, long, default_value_t = 100.0)] + rate_limit: f64, + /// The GUIDs of the new partition + #[arg(short, long)] + guids: Vec, + }, +} + +#[tokio::main] +async fn main() -> Result<(), UFMError> { + env_logger::init(); + + let opt: Options = Options::parse(); + + let conf = load_conf(&opt); + match &opt.command { + Some(Commands::Delete { pkey }) => delete::run(conf, pkey).await?, + Some(Commands::Version) => version::run(conf).await?, + Some(Commands::List) => list::run(conf).await?, + Some(Commands::View { pkey }) => view::run(conf, pkey).await?, + Some(Commands::Create { + pkey, + mtu, + ipoib, + index0, + membership, + service_level, + rate_limit, + guids, + }) => { + let opt = create::CreateOptions { + pkey: pkey.to_string(), + mtu: *mtu, + ipoib: *ipoib, + index0: *index0, + membership: membership.to_string(), + service_level: *service_level, + rate_limit: *rate_limit, + guids: guids.to_vec(), + }; + create::run(conf, &opt).await? + } + None => {} + }; + + Ok(()) +} + +fn load_conf(opt: &Options) -> UFMConfig { + let ufm_address = match opt.ufm_address.clone() { + Some(s) => s, + None => panic!("UFM_ADDRESS environment or ufm_address parameter not found"), + }; + + UFMConfig { + address: ufm_address, + username: opt.ufm_username.clone(), + password: opt.ufm_password.clone(), + token: opt.ufm_token.clone(), + } +} diff --git a/plugins/ufm_rust_client/cli/src/version.rs b/plugins/ufm_rust_client/cli/src/version.rs new file mode 100644 index 00000000..f72acb37 --- /dev/null +++ b/plugins/ufm_rust_client/cli/src/version.rs @@ -0,0 +1,22 @@ +/* + * Copyright © 2013-2022 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. + * + * This software product is a proprietary product of Nvidia Corporation and its affiliates + * (the "Company") and all right, title, and interest in and to the software product, + * including all associated intellectual property rights, are and shall + * remain exclusively with the Company. + * + * This software product is governed by the End User License Agreement + * provided with the software product. + */ + +use ufmclient::{UFMConfig, UFMError}; + +pub async fn run(conf: UFMConfig) -> Result<(), UFMError> { + let ufm = ufmclient::connect(conf)?; + let v = ufm.version().await?; + + println!("{}", v); + + Ok(()) +} diff --git a/plugins/ufm_rust_client/cli/src/view.rs b/plugins/ufm_rust_client/cli/src/view.rs new file mode 100644 index 00000000..51569989 --- /dev/null +++ b/plugins/ufm_rust_client/cli/src/view.rs @@ -0,0 +1,37 @@ +/* + * Copyright © 2013-2022 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. + * + * This software product is a proprietary product of Nvidia Corporation and its affiliates + * (the "Company") and all right, title, and interest in and to the software product, + * including all associated intellectual property rights, are and shall + * remain exclusively with the Company. + * + * This software product is governed by the End User License Agreement + * provided with the software product. + */ + +use ufmclient::{UFMConfig, UFMError}; + +pub async fn run(conf: UFMConfig, pkey: &String) -> Result<(), UFMError> { + let ufm = ufmclient::connect(conf)?; + let p = ufm.get_partition(pkey).await?; + let ps = ufm.list_port(p.pkey).await?; + + println!("{:15}: {}", "Name", p.name); + println!("{:15}: {}", "Pkey", p.pkey.to_string()); + println!("{:15}: {}", "IPoIB", p.ipoib); + println!("{:15}: {}", "MTU", p.qos.mtu_limit); + println!("{:15}: {}", "Rate Limit", p.qos.rate_limit); + println!("{:15}: {}", "Service Level", p.qos.service_level); + println!("{:15}: ", "Ports"); + + println!( + " {:<20}{:<20}{:<10}{:<20}{:<10}{:<15}{:<10}{:<20}", + "GUID", "ParentGUID", "PortType", "SystemID", "LID", "SystemName", "LogState", "Name", + ); + for port in ps { + println!("{}", port.to_string()); + } + + Ok(()) +} diff --git a/plugins/ufm_rust_client/client/Cargo.toml b/plugins/ufm_rust_client/client/Cargo.toml new file mode 100644 index 00000000..5479b30b --- /dev/null +++ b/plugins/ufm_rust_client/client/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "ufmclient" +version = "0.1.2" +edition = "2021" +description = "The Rust client of Nvidia UFM" +license-file = "../LICENSE" +readme = "../README.md" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +hyper = { version = "0.14", features = ["full"] } +hyper-tls = { version = "0.5"} + +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + +base64 = "0.21.0" +thiserror = "1.0" +url = { version = "2", features = ["serde"] } + +log = { version = "0.4", features = ["std", "serde"] } diff --git a/plugins/ufm_rust_client/client/src/lib.rs b/plugins/ufm_rust_client/client/src/lib.rs new file mode 100644 index 00000000..b1e0849e --- /dev/null +++ b/plugins/ufm_rust_client/client/src/lib.rs @@ -0,0 +1,408 @@ +/* + * Copyright © 2013-2022 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. + * + * This software product is a proprietary product of Nvidia Corporation and its affiliates + * (the "Company") and all right, title, and interest in and to the software product, + * including all associated intellectual property rights, are and shall + * remain exclusively with the Company. + * + * This software product is governed by the End User License Agreement + * provided with the software product. + */ + +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; +use thiserror::Error; +use url::Url; + +use self::port::{PhysicalPort, Port, VirtualPort}; +use self::rest::{RestClient, RestClientConfig, RestError, RestScheme}; + +mod port; +mod rest; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct PartitionQoS { + // Default 2k; one of 2k or 4k; the MTU of the services. + pub mtu_limit: u16, + // Default is None, value can be range from 0-15 + pub service_level: u8, + // Default is None, can be one of the following: 2.5, 10, 30, 5, 20, 40, 60, 80, 120, 14, 56, 112, 168, 25, 100, 200, or 300 + pub rate_limit: f64, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "lowercase")] +pub enum PortMembership { + Limited, + Full, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct PortConfig { + /// The GUID of Port. + pub guid: String, + /// Default false; store the PKey at index 0 of the PKey table of the GUID. + pub index0: bool, + /// Default is full: + /// "full" - members with full membership can communicate with all hosts (members) within the network/partition + /// "limited" - members with limited membership cannot communicate with other members with limited membership. + /// However, communication is allowed between every other combination of membership types. + pub membership: PortMembership, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy)] +pub struct PartitionKey(i32); + +impl PartitionKey { + pub fn is_default_pkey(&self) -> bool { + self.0 == 0x7fff + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Partition { + /// The name of Partition. + pub name: String, + /// The pkey of Partition. + pub pkey: PartitionKey, + /// Default false + pub ipoib: bool, + /// The QoS of Partition. + pub qos: PartitionQoS, +} + +#[derive(Serialize, Deserialize, Debug)] +struct Pkey { + pkey: String, + ip_over_ib: bool, + membership: PortMembership, + index0: bool, + guids: Vec, +} + +impl TryFrom for PartitionKey { + type Error = UFMError; + + fn try_from(pkey: i32) -> Result { + if pkey != (pkey & 0x7fff) { + return Err(UFMError::InvalidPKey(pkey.to_string())); + } + + Ok(PartitionKey(pkey)) + } +} + +impl TryFrom for PartitionKey { + type Error = UFMError; + + fn try_from(pkey: String) -> Result { + let p = pkey.trim_start_matches("0x"); + let k = i32::from_str_radix(p, 16); + + match k { + Ok(v) => Ok(PartitionKey(v)), + Err(_e) => Err(UFMError::InvalidPKey(pkey.to_string())), + } + } +} + +impl TryFrom<&String> for PartitionKey { + type Error = UFMError; + + fn try_from(pkey: &String) -> Result { + PartitionKey::try_from(pkey.to_string()) + } +} + +impl TryFrom<&str> for PartitionKey { + type Error = UFMError; + + fn try_from(pkey: &str) -> Result { + PartitionKey::try_from(pkey.to_string()) + } +} + +impl ToString for PartitionKey { + fn to_string(&self) -> String { + format!("0x{:x}", self.0) + } +} + +impl From for i32 { + fn from(v: PartitionKey) -> i32 { + v.0 + } +} + +impl TryFrom for PortMembership { + type Error = UFMError; + + fn try_from(membership: String) -> Result { + match membership.to_lowercase().as_str() { + "full" => Ok(PortMembership::Full), + "limited" => Ok(PortMembership::Limited), + _ => Err(UFMError::InvalidConfig("invalid membership".to_string())), + } + } +} + +impl TryFrom<&str> for PortMembership { + type Error = UFMError; + + fn try_from(membership: &str) -> Result { + PortMembership::try_from(membership.to_string()) + } +} + +pub struct Ufm { + client: RestClient, +} + +#[derive(Error, Debug)] +pub enum UFMError { + #[error("{0}")] + Unknown(String), + #[error("'{0}' not found")] + NotFound(String), + #[error("invalid pkey '{0}'")] + InvalidPKey(String), + #[error("invalid configuration '{0}'")] + InvalidConfig(String), +} + +impl From for UFMError { + fn from(e: RestError) -> Self { + match e { + RestError::Unknown(msg) => UFMError::Unknown(msg), + RestError::NotFound(msg) => UFMError::NotFound(msg), + RestError::AuthFailure(msg) => UFMError::InvalidConfig(msg), + RestError::InvalidConfig(msg) => UFMError::InvalidConfig(msg), + } + } +} + +pub struct UFMConfig { + pub address: String, + pub username: Option, + pub password: Option, + pub token: Option, +} + +pub fn connect(conf: UFMConfig) -> Result { + let addr = Url::parse(&conf.address) + .map_err(|_| UFMError::InvalidConfig("invalid UFM url".to_string()))?; + let address = addr + .host_str() + .ok_or(UFMError::InvalidConfig("invalid UFM host".to_string()))?; + + let (base_path, auth_info) = match &conf.token { + None => { + let password = conf + .password + .clone() + .ok_or(UFMError::InvalidConfig("password is empty".to_string()))?; + let username = conf + .username + .clone() + .ok_or(UFMError::InvalidConfig("username is empty".to_string()))?; + + ( + "/ufmRest".to_string(), + base64::encode(format!("{}:{}", username, password)), + ) + } + Some(t) => ("/ufmRestV3".to_string(), t.to_string()), + }; + + let c = RestClient::new(&RestClientConfig { + address: address.to_string(), + port: addr.port(), + auth_info, + base_path, + scheme: RestScheme::from(addr.scheme().to_string()), + })?; + + Ok(Ufm { client: c }) +} + +impl Ufm { + pub async fn bind_ports(&self, p: Partition, ports: Vec) -> Result<(), UFMError> { + let path = String::from("/resources/pkeys"); + + let mut membership = PortMembership::Full; + let mut index0 = true; + + let mut guids = vec![]; + for pb in ports { + membership = pb.membership.clone(); + index0 = pb.index0; + guids.push(pb.guid.to_string()); + } + + let pkey = Pkey { + pkey: p.pkey.clone().to_string(), + ip_over_ib: p.ipoib, + membership, + index0, + guids, + }; + + let data = serde_json::to_string(&pkey) + .map_err(|_| UFMError::InvalidConfig("invalid partition".to_string()))?; + + self.client.post(&path, data).await?; + + Ok(()) + } + + pub async fn unbind_ports( + &self, + pkey: PartitionKey, + guids: Vec, + ) -> Result<(), UFMError> { + let path = String::from("/actions/remove_guids_from_pkey"); + + #[derive(Serialize, Deserialize, Debug)] + struct Pkey { + pkey: String, + guids: Vec, + } + + let pkey = Pkey { + pkey: pkey.clone().to_string(), + guids, + }; + + let data = serde_json::to_string(&pkey) + .map_err(|_| UFMError::InvalidConfig("invalid partition".to_string()))?; + + self.client.post(&path, data).await?; + + Ok(()) + } + + pub async fn get_partition(&self, pkey: &str) -> Result { + let pkey = PartitionKey::try_from(pkey)?; + + let path = format!("/resources/pkeys/{}?qos_conf=true", pkey.to_string()); + + #[derive(Serialize, Deserialize, Debug)] + struct Pkey { + partition: String, + ip_over_ib: bool, + qos_conf: PartitionQoS, + } + let pk: Pkey = self.client.get(&path).await?; + + Ok(Partition { + name: pk.partition, + pkey, + ipoib: pk.ip_over_ib, + qos: pk.qos_conf, + }) + } + + pub async fn list_partition(&self) -> Result, UFMError> { + #[derive(Serialize, Deserialize, Debug)] + struct Pkey { + partition: String, + ip_over_ib: bool, + qos_conf: PartitionQoS, + } + + let path = String::from("/resources/pkeys?qos_conf=true"); + let pkey_qos: HashMap = self.client.list(&path).await?; + + let mut parts = Vec::new(); + + for (k, v) in pkey_qos { + parts.push(Partition { + name: v.partition, + pkey: PartitionKey::try_from(&k)?, + ipoib: v.ip_over_ib, + qos: v.qos_conf.clone(), + }); + } + + Ok(parts) + } + + pub async fn delete_partition(&self, pkey: &str) -> Result<(), UFMError> { + let path = format!("/resources/pkeys/{}", pkey); + self.client.delete(&path).await?; + + Ok(()) + } + + pub async fn list_port(&self, pkey: PartitionKey) -> Result, UFMError> { + let mut res = Vec::new(); + // get GUIDs from pkey + #[derive(Serialize, Deserialize, Debug)] + struct PkeyWithGUIDs { + pub partition: String, + pub ip_over_ib: bool, + pub guids: Vec, + } + + let path = format!("resources/pkeys/{}?guids_data=true", pkey.to_string()); + let pkeywithguids: PkeyWithGUIDs = self.client.get(&path).await?; + + // list physical ports + let path = String::from("/resources/ports?sys_type=Computer"); + let physical_ports: Vec = self.client.list(&path).await?; + + // list virtual ports + let path = String::from("/resources/vports"); + let virtual_ports: Vec = self.client.list(&path).await?; + + let mut port_map = HashMap::new(); + for pport in physical_ports { + port_map.insert(pport.guid.clone(), Port::from(pport)); + } + for vport in virtual_ports { + port_map.insert(vport.virtual_port_guid.clone(), Port::from(vport)); + } + + if !pkey.is_default_pkey() { + for port_config in pkeywithguids.guids { + let guid = port_config.guid; + match port_map.get(&guid) { + Some(p) => { + res.push(p.clone()); + } + None => { + let p = Port { + guid, + name: None, + system_id: "".to_string(), + lid: 65535, + system_name: "".to_string(), + logical_state: "Unknown".to_string(), + parent_guid: None, + port_type: None, + }; + res.push(p); + } + } + } + } else { + // list all the ports for default pkey(0x7fff) + res = port_map.values().cloned().collect(); + } + Ok(res) + } + + pub async fn version(&self) -> Result { + #[derive(Serialize, Deserialize, Debug)] + struct Version { + ufm_release_version: String, + } + + let path = String::from("/app/ufm_version"); + let v: Version = self.client.get(&path).await?; + + Ok(v.ufm_release_version) + } +} diff --git a/plugins/ufm_rust_client/client/src/port.rs b/plugins/ufm_rust_client/client/src/port.rs new file mode 100644 index 00000000..6881fda3 --- /dev/null +++ b/plugins/ufm_rust_client/client/src/port.rs @@ -0,0 +1,116 @@ +/* + * Copyright © 2013-2022 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. + * + * This software product is a proprietary product of Nvidia Corporation and its affiliates + * (the "Company") and all right, title, and interest in and to the software product, + * including all associated intellectual property rights, are and shall + * remain exclusively with the Company. + * + * This software product is governed by the End User License Agreement + * provided with the software product. + */ + +use std::fmt::Display; + +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "lowercase")] +pub enum PortType { + Physical, + Virtual, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "lowercase")] +pub struct Port { + pub guid: String, + pub name: Option, + pub system_id: String, + pub lid: i32, + pub system_name: String, + pub logical_state: String, + pub parent_guid: Option, + pub port_type: Option, +} + +impl Display for Port { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let name = match self.name.clone() { + Some(n) => n, + None => "".to_string(), + }; + let parent_guid = match self.parent_guid.clone() { + Some(p) => p, + None => "".to_string(), + }; + let port_type = match self.port_type { + Some(PortType::Physical) => "physical".to_string(), + Some(PortType::Virtual) => "virtual".to_string(), + None => "".to_string(), + }; + write!( + f, + " {:<20}{:<20}{:<10}{:<20}{:<10}{:<15}{:<10}{:<20}", + self.guid, + parent_guid, + port_type, + self.system_id, + self.lid, + self.system_name, + self.logical_state, + name, + ) + } +} + +impl From for Port { + fn from(physicalport: PhysicalPort) -> Self { + Port { + guid: physicalport.guid.clone(), + name: Some(physicalport.name), + system_id: physicalport.system_id, + lid: physicalport.lid, + system_name: physicalport.system_name, + logical_state: physicalport.logical_state, + parent_guid: None, + port_type: Some(PortType::Physical), + } + } +} + +impl From for Port { + fn from(virtualport: VirtualPort) -> Self { + Port { + guid: virtualport.virtual_port_guid.clone(), + name: None, + system_id: virtualport.system_guid, + lid: virtualport.virtual_port_lid, + system_name: virtualport.system_name, + logical_state: virtualport.virtual_port_state, + parent_guid: Some(virtualport.port_guid), + port_type: Some(PortType::Virtual), + } + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct PhysicalPort { + pub guid: String, + pub name: String, + #[serde(rename = "systemID")] + pub system_id: String, + pub lid: i32, + pub system_name: String, + pub logical_state: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct VirtualPort { + pub virtual_port_guid: String, + pub system_guid: String, + pub virtual_port_lid: i32, + pub system_name: String, + pub virtual_port_state: String, + pub port_guid: String, +} diff --git a/plugins/ufm_rust_client/client/src/rest.rs b/plugins/ufm_rust_client/client/src/rest.rs new file mode 100644 index 00000000..74216697 --- /dev/null +++ b/plugins/ufm_rust_client/client/src/rest.rs @@ -0,0 +1,194 @@ +/* + * Copyright © 2013-2022 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. + * + * This software product is a proprietary product of Nvidia Corporation and its affiliates + * (the "Company") and all right, title, and interest in and to the software product, + * including all associated intellectual property rights, are and shall + * remain exclusively with the Company. + * + * This software product is governed by the End User License Agreement + * provided with the software product. + */ + +use std::fmt; +use std::fmt::{Display, Formatter}; + +use hyper::client::HttpConnector; +use hyper::header::{AUTHORIZATION, CONTENT_TYPE}; +use hyper::http::StatusCode; +use hyper::{Body, Client, Method, Uri}; +use hyper_tls::HttpsConnector; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum RestError { + #[error("{0}")] + Unknown(String), + #[error("'{0}' not found")] + NotFound(String), + #[error("failed to auth '{0}'")] + AuthFailure(String), + #[error("invalid configuration '{0}'")] + InvalidConfig(String), +} + +impl From for RestError { + fn from(value: hyper::Error) -> Self { + if value.is_user() { + return RestError::AuthFailure(value.message().to_string()); + } + + RestError::Unknown(value.message().to_string()) + } +} + +#[derive(Clone, Debug)] +pub enum RestScheme { + Http, + Https, +} + +impl From for RestScheme { + fn from(value: String) -> Self { + match value.to_uppercase().as_str() { + "HTTP" => RestScheme::Http, + "HTTPS" => RestScheme::Https, + _ => RestScheme::Http, + } + } +} + +impl Display for RestScheme { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + RestScheme::Http => write!(f, "http"), + RestScheme::Https => write!(f, "https"), + } + } +} + +pub struct RestClientConfig { + pub address: String, + pub port: Option, + pub scheme: RestScheme, + pub auth_info: String, + pub base_path: String, +} + +pub struct RestClient { + base_url: String, + auth_info: String, + scheme: RestScheme, + http_client: hyper::Client, + https_client: hyper::Client>, +} + +impl RestClient { + pub fn new(conf: &RestClientConfig) -> Result { + let auth_info = format!("Basic {}", conf.auth_info.clone().trim()); + + let base_url = match &conf.port { + None => format!( + "{}://{}/{}", + conf.scheme, + conf.address, + conf.base_path.trim_matches('/') + ), + Some(p) => format!( + "{}://{}:{}/{}", + conf.scheme, + conf.address, + p, + conf.base_path.trim_matches('/') + ), + }; + + let _ = base_url + .parse::() + .map_err(|_| RestError::InvalidConfig("invalid rest address".to_string()))?; + + Ok(Self { + base_url, + auth_info, + scheme: conf.scheme.clone(), + // TODO(k82cn): Add timout for the clients. + http_client: Client::new(), + https_client: Client::builder().build::<_, hyper::Body>(HttpsConnector::new()), + }) + } + + pub async fn get<'a, T: serde::de::DeserializeOwned>( + &'a self, + path: &'a str, + ) -> Result { + let resp = self.execute_request(Method::GET, path, None).await?; + if resp.eq("{}") { + return Err(RestError::NotFound("not found".to_string())); + } + + let data = serde_json::from_str(&resp) + .map_err(|_| RestError::InvalidConfig("invalid response".to_string()))?; + + Ok(data) + } + + pub async fn list<'a, T: serde::de::DeserializeOwned>( + &'a self, + path: &'a str, + ) -> Result { + let resp = self.execute_request(Method::GET, path, None).await?; + let data = serde_json::from_str(&resp) + .map_err(|_| RestError::InvalidConfig("invalid response".to_string()))?; + + Ok(data) + } + + pub async fn post(&self, path: &str, data: String) -> Result<(), RestError> { + self.execute_request(Method::POST, path, Some(data)).await?; + + Ok(()) + } + + pub async fn delete(&self, path: &str) -> Result<(), RestError> { + self.execute_request(Method::DELETE, path, None).await?; + + Ok(()) + } + + async fn execute_request( + &self, + method: Method, + path: &str, + data: Option, + ) -> Result { + let url = format!("{}/{}", self.base_url, path.trim_matches('/')); + let uri = url + .parse::() + .map_err(|_| RestError::InvalidConfig("invalid path".to_string()))?; + + let body = data.unwrap_or(String::new()); + log::debug!("Method: {method}, URL: {url}, Body: {body}"); + + let req = hyper::Request::builder() + .method(method) + .uri(uri) + .header(CONTENT_TYPE, "application/json") + .header(AUTHORIZATION, self.auth_info.to_string()) + .body(Body::from(body)) + .map_err(|_| RestError::InvalidConfig("invalid rest request".to_string()))?; + + let body = match &self.scheme { + RestScheme::Http => self.http_client.request(req).await?, + RestScheme::Https => self.https_client.request(req).await?, + }; + + let status = body.status(); + let chunk = hyper::body::to_bytes(body.into_body()).await?; + let data = String::from_utf8(chunk.to_vec()).unwrap(); + + match status { + StatusCode::OK => Ok(data), + _ => Err(RestError::Unknown(data)), + } + } +}