diff --git a/Cargo.lock b/Cargo.lock index 704d14223..c1ab118fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -319,6 +319,7 @@ dependencies = [ "lazy_static", "netif", "path-absolutize", + "path-clean", "reed-solomon-erasure", "regex", "rmp-serde", @@ -882,6 +883,12 @@ dependencies = [ "path-dedot", ] +[[package]] +name = "path-clean" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17359afc20d7ab31fdb42bb844c8b3bb1dabd7dcf7e68428492da7f16966fcef" + [[package]] name = "path-dedot" version = "3.1.1" diff --git a/ecstore/Cargo.toml b/ecstore/Cargo.toml index 8b7a5d17d..de95f7a6f 100644 --- a/ecstore/Cargo.toml +++ b/ecstore/Cargo.toml @@ -17,6 +17,8 @@ async-trait.workspace = true tracing.workspace = true serde.workspace = true anyhow.workspace = true +time.workspace = true +serde_json.workspace = true url = "2.5.2" uuid = { version = "1.8.0", features = ["v4", "fast-rng", "serde"] } reed-solomon-erasure = "6.0.0" @@ -25,15 +27,13 @@ lazy_static = "1.5.0" regex = "1.10.5" netif = "0.1.6" tracing-error = "0.2.0" -serde_json.workspace = true path-absolutize = "3.1.1" -time.workspace = true rmp-serde = "1.3.0" tokio-util = { version = "0.7.11", features = ["io"] } s3s = "0.10.0" crc32fast = "1.4.2" siphasher = "1.0.1" - +path-clean = "1.0.1" [dev-dependencies] tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } diff --git a/ecstore/src/disks_layout.rs b/ecstore/src/disks_layout.rs index 261a239c3..88364c009 100644 --- a/ecstore/src/disks_layout.rs +++ b/ecstore/src/disks_layout.rs @@ -37,7 +37,7 @@ impl PoolDisksLayout { self.layout.len() } - pub fn as_cmd_line(&self) -> &str { + pub fn get_cmd_line(&self) -> &str { &self.cmd_line } } @@ -283,11 +283,11 @@ fn possible_set_counts_with_symmetry(set_counts: &[usize], arg_patterns: &[ArgPa for &ss in set_counts { let mut symmetry = false; for arg_pattern in arg_patterns { - for p in arg_pattern.inner.iter() { - if p.seq.len() > ss { - symmetry = (p.seq.len() % ss) == 0; + for p in arg_pattern.as_ref().iter() { + if p.len() > ss { + symmetry = (p.len() % ss) == 0; } else { - symmetry = (ss % p.seq.len()) == 0; + symmetry = (ss % p.len()) == 0; } } } @@ -520,6 +520,13 @@ mod test { ]], success: true, }, + TestCase { + num: 15, + args: vec!["https://node{1...3}.example.net/mnt/drive{1...8}"], + total_sizes: vec![24], + indexes: vec![vec![12, 12]], + success: true, + }, ]; for test_case in test_cases { diff --git a/ecstore/src/ellipses.rs b/ecstore/src/ellipses.rs index 170ec0d7f..fdb647eaa 100644 --- a/ecstore/src/ellipses.rs +++ b/ecstore/src/ellipses.rs @@ -16,9 +16,9 @@ const ELLIPSES: &str = "..."; /// associated prefix and suffixes. #[derive(Debug, Default, PartialEq, Eq)] pub struct Pattern { - pub prefix: String, - pub suffix: String, - pub seq: Vec, + pub(crate) prefix: String, + pub(crate) suffix: String, + pub(crate) seq: Vec, } impl Pattern { @@ -37,12 +37,28 @@ impl Pattern { ret } + + pub fn len(&self) -> usize { + self.seq.len() + } } /// contains a list of patterns provided in the input. #[derive(Debug, PartialEq, Eq)] pub struct ArgPattern { - pub inner: Vec, + inner: Vec, +} + +impl AsRef> for ArgPattern { + fn as_ref(&self) -> &Vec { + &self.inner + } +} + +impl AsMut> for ArgPattern { + fn as_mut(&mut self) -> &mut Vec { + &mut self.inner + } } impl ArgPattern { diff --git a/ecstore/src/endpoint.rs b/ecstore/src/endpoint.rs index 5ee20f3cf..7b6cef1c4 100644 --- a/ecstore/src/endpoint.rs +++ b/ecstore/src/endpoint.rs @@ -2,13 +2,19 @@ use super::disks_layout::DisksLayout; use super::error::{Error, Result}; use super::utils::net; use path_absolutize::Absolutize; +use path_clean::PathClean; use std::collections::HashSet; use std::fmt::Display; -use std::{collections::HashMap, path::Path, usize}; +use std::net::IpAddr; +use std::{ + collections::{hash_map::Entry, HashMap}, + path::Path, + usize, +}; use url::{ParseError, Url}; /// enum for endpoint type. -#[derive(PartialEq, Eq)] +#[derive(PartialEq, Eq, Debug)] pub enum EndpointType { /// path style endpoint type enum. Path, @@ -18,8 +24,11 @@ pub enum EndpointType { } /// enum for setup type. -#[derive(Debug)] +#[derive(PartialEq, Eq, Debug)] pub enum SetupType { + /// starts with unknown setup type. + Unknown, + /// FS setup type enum. FS, @@ -43,7 +52,7 @@ pub struct Node { } /// any type of endpoint. -#[derive(Debug, Clone)] +#[derive(Debug, PartialEq, Eq, Clone)] pub struct Endpoint { pub url: url::Url, pub is_local: bool, @@ -70,8 +79,8 @@ impl TryFrom<&str> for Endpoint { /// Performs the conversion. fn try_from(value: &str) -> Result { /// check whether given path is not empty. - fn is_empty_path(path: &str) -> bool { - ["", "/", "\\"].iter().any(|&v| v.eq(path)) + fn is_empty_path(path: impl AsRef) -> bool { + ["", "/", "\\"].iter().any(|&v| Path::new(v).eq(path.as_ref())) } if is_empty_path(value) { @@ -80,7 +89,8 @@ impl TryFrom<&str> for Endpoint { let mut is_local = false; let url = match Url::parse(value) { - Ok(mut url) => { + #[allow(unused_mut)] + Ok(mut url) if url.has_host() => { // URL style of endpoint. // Valid URL style endpoint is // - Scheme field must contain "http" or "https" @@ -93,8 +103,13 @@ impl TryFrom<&str> for Endpoint { return Err(Error::from_string("invalid URL endpoint format")); } - if is_empty_path(url.path()) { - return Err(Error::from_string("empty or root endpoint is not supported")); + let path = Path::new(url.path()).clean(); + if is_empty_path(&path) { + return Err(Error::from_string("empty or root path is not supported in URL endpoint")); + } + + if let Some(v) = path.to_str() { + url.set_path(v) } // On windows having a preceding SlashSeparator will cause problems, if the @@ -114,38 +129,27 @@ impl TryFrom<&str> for Endpoint { #[cfg(windows)] { let path = url.path().to_owned(); - if Path::new(&path[1..]).has_root() { + if Path::new(&path[1..]).is_absolute() { url.set_path(&path[1..]); } } url } + Ok(_) => { + // like d:/foo + is_local = true; + url_parse_from_file_path(value)? + } Err(e) => match e { ParseError::InvalidPort => { return Err(Error::from_string("invalid URL endpoint format: port number must be between 1 to 65535")) } ParseError::EmptyHost => return Err(Error::from_string("invalid URL endpoint format: empty host name")), ParseError::RelativeUrlWithoutBase => { - // Only check if the arg is an ip address and ask for scheme since its absent. - // localhost, example.com, any FQDN cannot be disambiguated from a regular file path such as - // /mnt/export1. So we go ahead and start the rustfs server in FS modes in these cases. - if net::is_socket_addr(value) { - return Err(Error::from_string("invalid URL endpoint format: missing scheme http or https")); - } - - let file_path = match Path::new(value).absolutize() { - Ok(path) => path, - Err(err) => return Err(Error::from_string(format!("absolute path failed: {}", err))), - }; - - match Url::from_file_path(file_path) { - Ok(url) => { - is_local = true; - url - } - Err(_) => return Err(Error::from_string("Convert a file path into an URL failed")), - } + // like /foo + is_local = true; + url_parse_from_file_path(value)? } _ => return Err(Error::from_string(format!("invalid URL endpoint format: {}", e))), }, @@ -206,10 +210,18 @@ impl Endpoint { _ => String::new(), } } + + fn host_port(&self) -> String { + match (self.url.host(), self.url.port()) { + (Some(host), Some(port)) => format!("{}:{}", host, port), + (Some(host), None) => format!("{}", host), + _ => String::new(), + } + } } /// list of same type of endpoint. -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] pub struct Endpoints(Vec); impl AsRef> for Endpoints { @@ -235,8 +247,8 @@ impl TryFrom<&[String]> for Endpoints { /// returns new endpoint list based on input args. fn try_from(args: &[String]) -> Result { - let mut endpoint_type; - let mut schema; + let mut endpoint_type = None; + let mut schema = None; let mut endpoints = Vec::with_capacity(args.len()); let mut uniq_set = HashSet::with_capacity(args.len()); @@ -249,11 +261,11 @@ impl TryFrom<&[String]> for Endpoints { // All endpoints have to be same type and scheme if applicable. if i == 0 { - endpoint_type = endpoint.get_type(); - schema = endpoint.url.scheme(); - } else if endpoint.get_type() != endpoint_type { + endpoint_type = Some(endpoint.get_type()); + schema = Some(endpoint.url.scheme().to_owned()); + } else if Some(endpoint.get_type()) != endpoint_type { return Err(Error::from_string("mixed style endpoints are not supported")); - } else if endpoint.url.scheme() != schema { + } else if Some(endpoint.url.scheme()) != schema.as_deref() { return Err(Error::from_string("mixed scheme is not supported")); } @@ -304,6 +316,10 @@ impl PoolEndpointList { let mut endpoint = Endpoint::try_from(disks_layout.get_single_drive_layout())?; endpoint.update_is_local(server_addr.port())?; + if endpoint.get_type() != EndpointType::Path { + return Err(Error::from_string("use path style endpoint for single node setup")); + } + endpoint.set_pool_index(0); endpoint.set_set_index(0); endpoint.set_disk_index(0); @@ -343,17 +359,108 @@ impl PoolEndpointList { // setup type let mut unique_args = HashSet::new(); - for pool in pool_endpoints.iter() { - for ep in pool.as_ref() { - if let Some(host) = ep.url.host_str() { - unique_args.insert(host); - } else { - unique_args.insert(format!("localhost:{}", server_addr.port()).as_str()); + let mut pool_endpoint_list = Self { + inner: pool_endpoints, + setup_type: SetupType::Unknown, + }; + + pool_endpoint_list.update_is_local(server_addr.port())?; + + for endpoints in pool_endpoint_list.inner.iter_mut() { + // Check whether same path is not used in endpoints of a host on different port. + let mut path_ip_map: HashMap<&str, HashSet> = HashMap::new(); + let mut host_ip_cache = HashMap::new(); + for ep in endpoints.as_ref() { + if !ep.url.has_host() { + continue; } + + let host = ep.url.host().unwrap(); + let host_ip_set = host_ip_cache.entry(host.clone()).or_insert({ + net::get_host_ip(host.clone()) + .map_err(|e| Error::from_string(format!("host '{}' cannot resolve: {}", host, e)))? + }); + + let path = ep.url.path(); + match path_ip_map.entry(path) { + Entry::Occupied(mut e) => { + if e.get().intersection(host_ip_set).count() > 0 { + return Err(Error::from_string(format!( + "same path '{}' can not be served by different port on same address", + path + ))); + } + e.get_mut().extend(host_ip_set.iter()); + } + Entry::Vacant(e) => { + e.insert(host_ip_set.clone()); + } + } + } + + // Check whether same path is used for more than 1 local endpoints. + let mut local_path_set = HashSet::new(); + for ep in endpoints.as_ref() { + if !ep.is_local { + continue; + } + + let path = ep.url.path(); + if local_path_set.contains(path) { + return Err(Error::from_string(format!( + "path '{}' cannot be served by different address on same server", + path + ))); + } + local_path_set.insert(path); + } + + // Here all endpoints are URL style. + let mut ep_path_set = HashSet::new(); + let mut local_server_host_set = HashSet::new(); + let mut local_port_set = HashSet::new(); + let mut local_endpoint_count = 0; + + for ep in endpoints.as_ref() { + ep_path_set.insert(ep.url.path()); + if ep.is_local && ep.url.has_host() { + local_server_host_set.insert(ep.url.host()); + local_port_set.insert(ep.url.port()); + local_endpoint_count += 1; + } + } + + // All endpoints are pointing to local host + if endpoints.as_ref().len() == local_endpoint_count { + // If all endpoints have same port number, Just treat it as local erasure setup + // using URL style endpoints. + if local_port_set.len() == 1 && local_server_host_set.len() > 1 { + return Err(Error::from_string("all local endpoints should not have different hostnames/ips")); + } + } + + // Add missing port in all endpoints. + for ep in endpoints.as_mut() { + if !ep.url.has_host() { + unique_args.insert(format!("localhost:{}", server_addr.port())); + continue; + } + match ep.url.port() { + None => { + let _ = ep.url.set_port(Some(server_addr.port())); + } + Some(port) => { + // If endpoint is local, but port is different than serverAddrPort, then make it as remote. + if ep.is_local && server_addr.port() != port { + ep.is_local = false; + } + } + } + unique_args.insert(ep.host_port()); } } - let setup_type = match pool_endpoints[0].as_ref()[0].get_type() { + let setup_type = match pool_endpoint_list.as_ref()[0].as_ref()[0].get_type() { EndpointType::Path => SetupType::Erasure, EndpointType::Url => match unique_args.len() { 1 => SetupType::Erasure, @@ -361,24 +468,85 @@ impl PoolEndpointList { }, }; - let mut pool_endpoint_list = Self { - inner: pool_endpoints, - setup_type, - }; - - pool_endpoint_list.update_is_local()?; + pool_endpoint_list.setup_type = setup_type; Ok(pool_endpoint_list) } - fn update_is_local(&mut self) -> Result<()> { + /// resolves all hosts and discovers which are local + fn update_is_local(&mut self, local_port: u16) -> Result<()> { + for endpoints in self.inner.iter_mut() { + for ep in endpoints.as_mut() { + match ep.url.host() { + None => { + ep.is_local = true; + } + Some(host) => { + ep.is_local = net::is_local_host(host, ep.url.port().unwrap_or_default(), local_port)?; + } + } + } + } + + Ok(()) + } + + /// resolves all hosts and discovers which are local + fn _update_is_local(&mut self, local_port: u16) -> Result<()> { + let mut eps_resolved = 0; + let mut found_local = false; + let mut resolved_set: HashSet<(usize, usize)> = HashSet::new(); + let ep_count: usize = self.inner.iter().map(|v| v.as_ref().len()).sum(); + + loop { + // Break if the local endpoint is found already Or all the endpoints are resolved. + if found_local || eps_resolved == ep_count { + break; + } + + for (i, endpoints) in self.inner.iter_mut().enumerate() { + for (j, ep) in endpoints.as_mut().iter_mut().enumerate() { + if resolved_set.contains(&(i, j)) { + // Continue if host is already resolved. + continue; + } + + match ep.url.host() { + None => { + if !found_local { + found_local = true; + } + ep.is_local = true; + eps_resolved += 1; + resolved_set.insert((i, j)); + continue; + } + Some(host) => match net::is_local_host(host, ep.url.port().unwrap_or_default(), local_port) { + Ok(is_local) => { + if !found_local { + found_local = is_local; + } + ep.is_local = is_local; + eps_resolved += 1; + resolved_set.insert((i, j)); + } + Err(err) => { + // TODO Retry infinitely on Kubernetes and Docker swarm? + return Err(err); + } + }, + } + } + } + } + unimplemented!() } } /// represent endpoints in a given pool /// along with its setCount and setDriveCount. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct PoolEndpoints { // indicates if endpoints are provided in non-ellipses style pub legacy: bool, @@ -390,7 +558,7 @@ pub struct PoolEndpoints { } /// list of list of endpoints -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct EndpointServerPools(Vec); impl From> for EndpointServerPools { @@ -419,25 +587,25 @@ impl EndpointServerPools { return Err(Error::from_string("Invalid arguments specified")); } - let mut pool_eps = PoolEndpointList::create_pool_endpoints(server_addr, disks_layout)?; + let pool_eps = PoolEndpointList::create_pool_endpoints(server_addr, disks_layout)?; let mut ret: EndpointServerPools = Vec::with_capacity(pool_eps.as_ref().len()).into(); - for (i, eps) in pool_eps.as_mut().into_iter().enumerate() { + for (i, eps) in pool_eps.inner.into_iter().enumerate() { let layout = disks_layout.get_layout(i); let set_count = layout.map_or(0, |v| v.count()); - let drives_per_set = layout.map_or(0, |v| v.as_ref().get(0).map_or(0, |v| v.len())); - let cmd_line = layout.map_or(String::new(), |v| v.as_cmd_line().to_owned()); + let drives_per_set = layout.map_or(0, |v| v.as_ref().first().map_or(0, |v| v.len())); + let cmd_line = layout.map_or(String::new(), |v| v.get_cmd_line().to_owned()); let ep = PoolEndpoints { legacy: disks_layout.legacy, set_count, drives_per_set, - endpoints: *eps, + endpoints: eps, cmd_line, - platform: String::new(), + platform: format!("OS: {} | Arch: {}", std::env::consts::OS, std::env::consts::ARCH), }; - ret.add(ep); + ret.add(ep)?; } Ok((ret, pool_eps.setup_type)) @@ -463,18 +631,25 @@ impl EndpointServerPools { Ok(()) } + /// returns true if the first endpoint is local. + pub fn first_local(&self) -> bool { + self.0 + .first() + .and_then(|v| v.endpoints.as_ref().first()) + .map_or(false, |v| v.is_local) + } + /// returns a sorted list of nodes in this cluster pub fn get_nodes(&self) -> Vec { let mut node_map = HashMap::new(); for pool in self.0.iter() { for ep in pool.endpoints.as_ref() { - let host = ep.grid_host(); - let n = node_map.entry(host).or_insert(Node { + let n = node_map.entry(ep.host_port()).or_insert(Node { url: ep.url.clone(), pools: vec![], is_local: ep.is_local, - grid_host: host, + grid_host: ep.grid_host(), }); if let Some(pool_idx) = ep.pool_idx { @@ -485,10 +660,962 @@ impl EndpointServerPools { } } - let mut nodes: Vec = node_map.into_iter().map(|(_, n)| n).collect(); + let mut nodes: Vec = node_map.into_values().collect(); nodes.sort_by(|a, b| a.grid_host.cmp(&b.grid_host)); nodes } } + +/// parse a file path into an URL. +fn url_parse_from_file_path(value: &str) -> Result { + // Only check if the arg is an ip address and ask for scheme since its absent. + // localhost, example.com, any FQDN cannot be disambiguated from a regular file path such as + // /mnt/export1. So we go ahead and start the rustfs server in FS modes in these cases. + let addr: Vec<&str> = value.splitn(2, '/').collect(); + if net::is_socket_addr(addr[0]) { + return Err(Error::from_string("invalid URL endpoint format: missing scheme http or https")); + } + + let file_path = match Path::new(value).absolutize() { + Ok(path) => path, + Err(err) => return Err(Error::from_string(format!("absolute path failed: {}", err))), + }; + + match Url::from_file_path(file_path) { + Ok(url) => Ok(url), + Err(_) => Err(Error::from_string("Convert a file path into an URL failed")), + } +} + +#[cfg(test)] +mod test { + + use super::*; + + #[test] + fn test_new_endpoint() { + #[derive(Default)] + struct TestCase<'a> { + arg: &'a str, + expected_endpoint: Option, + expected_type: Option, + expected_err: Option, + } + + let u2 = url::Url::parse("https://example.org/path").unwrap(); + let u4 = url::Url::parse("http://192.168.253.200/path").unwrap(); + let u6 = url::Url::parse("http://server:/path").unwrap(); + let root_slash_foo = url::Url::from_file_path("/foo").unwrap(); + + let test_cases = [ + TestCase { + arg: "/foo", + expected_endpoint: Some(Endpoint { + url: root_slash_foo, + is_local: true, + pool_idx: None, + set_idx: None, + disk_idx: None, + }), + expected_type: Some(EndpointType::Path), + expected_err: None, + }, + TestCase { + arg: "https://example.org/path", + expected_endpoint: Some(Endpoint { + url: u2, + is_local: false, + pool_idx: None, + set_idx: None, + disk_idx: None, + }), + expected_type: Some(EndpointType::Url), + expected_err: None, + }, + TestCase { + arg: "http://192.168.253.200/path", + expected_endpoint: Some(Endpoint { + url: u4, + is_local: false, + pool_idx: None, + set_idx: None, + disk_idx: None, + }), + expected_type: Some(EndpointType::Url), + expected_err: None, + }, + TestCase { + arg: "", + expected_endpoint: None, + expected_type: None, + expected_err: Some(Error::from_string("empty or root endpoint is not supported")), + }, + TestCase { + arg: "/", + expected_endpoint: None, + expected_type: None, + expected_err: Some(Error::from_string("empty or root endpoint is not supported")), + }, + TestCase { + arg: "\\", + expected_endpoint: None, + expected_type: None, + expected_err: Some(Error::from_string("empty or root endpoint is not supported")), + }, + TestCase { + arg: "c://foo", + expected_endpoint: None, + expected_type: None, + expected_err: Some(Error::from_string("invalid URL endpoint format")), + }, + TestCase { + arg: "ftp://foo", + expected_endpoint: None, + expected_type: None, + expected_err: Some(Error::from_string("invalid URL endpoint format")), + }, + TestCase { + arg: "http://server/path?location", + expected_endpoint: None, + expected_type: None, + expected_err: Some(Error::from_string("invalid URL endpoint format")), + }, + TestCase { + arg: "http://:/path", + expected_endpoint: None, + expected_type: None, + expected_err: Some(Error::from_string("invalid URL endpoint format: empty host name")), + }, + TestCase { + arg: "http://:8080/path", + expected_endpoint: None, + expected_type: None, + expected_err: Some(Error::from_string("invalid URL endpoint format: empty host name")), + }, + TestCase { + arg: "http://server:/path", + expected_endpoint: Some(Endpoint { + url: u6, + is_local: false, + pool_idx: None, + set_idx: None, + disk_idx: None, + }), + expected_type: Some(EndpointType::Url), + expected_err: None, + }, + TestCase { + arg: "https://93.184.216.34:808080/path", + expected_endpoint: None, + expected_type: None, + expected_err: Some(Error::from_string("invalid URL endpoint format: port number must be between 1 to 65535")), + }, + TestCase { + arg: "http://server:8080//", + expected_endpoint: None, + expected_type: None, + expected_err: Some(Error::from_string("empty or root path is not supported in URL endpoint")), + }, + TestCase { + arg: "http://server:8080/", + expected_endpoint: None, + expected_type: None, + expected_err: Some(Error::from_string("empty or root path is not supported in URL endpoint")), + }, + TestCase { + arg: "192.168.1.210:9000", + expected_endpoint: None, + expected_type: None, + expected_err: Some(Error::from_string("invalid URL endpoint format: missing scheme http or https")), + }, + ]; + + for test_case in test_cases { + let ret = Endpoint::try_from(test_case.arg); + if test_case.expected_err.is_none() && ret.is_err() { + panic!("{}: error: expected = , got = {:?}", test_case.arg, ret); + } + if test_case.expected_err.is_some() && ret.is_ok() { + panic!("{}: error: expected = {:?}, got = ", test_case.arg, test_case.expected_err); + } + match (test_case.expected_err, ret) { + (None, Err(e)) => panic!("{}: error: expected = , got = {}", test_case.arg, e), + (None, Ok(mut ep)) => { + let _ = ep.update_is_local(9000); + if test_case.expected_type != Some(ep.get_type()) { + panic!( + "{}: type: expected = {:?}, got = {:?}", + test_case.arg, + test_case.expected_type, + ep.get_type() + ); + } + + assert_eq!(test_case.expected_endpoint, Some(ep), "{}: endpoint", test_case.arg); + } + (Some(e), Ok(_)) => panic!("{}: error: expected = {}, got = ", test_case.arg, e), + (Some(e), Err(e2)) => { + assert_eq!(e.to_string(), e2.to_string(), "{}: error: expected = {}, got = {}", test_case.arg, e, e2) + } + } + } + } + + #[test] + fn test_new_endpoints() { + let test_cases = [ + (vec!["/d1", "/d2", "/d3", "/d4"], None, 1), + ( + vec![ + "http://localhost/d1", + "http://localhost/d2", + "http://localhost/d3", + "http://localhost/d4", + ], + None, + 2, + ), + ( + vec![ + "http://example.org/d1", + "http://example.com/d1", + "http://example.net/d1", + "http://example.edu/d1", + ], + None, + 3, + ), + ( + vec![ + "http://localhost/d1", + "http://localhost/d2", + "http://example.org/d1", + "http://example.org/d2", + ], + None, + 4, + ), + ( + vec![ + "https://localhost:9000/d1", + "https://localhost:9001/d2", + "https://localhost:9002/d3", + "https://localhost:9003/d4", + ], + None, + 5, + ), + // It is valid WRT endpoint list that same path is expected with different port on same server. + ( + vec![ + "https://127.0.0.1:9000/d1", + "https://127.0.0.1:9001/d1", + "https://127.0.0.1:9002/d1", + "https://127.0.0.1:9003/d1", + ], + None, + 6, + ), + (vec!["d1", "d2", "d3", "d1"], Some(Error::from_string("duplicate endpoints found")), 7), + (vec!["d1", "d2", "d3", "./d1"], Some(Error::from_string("duplicate endpoints found")), 8), + ( + vec![ + "http://localhost/d1", + "http://localhost/d2", + "http://localhost/d1", + "http://localhost/d4", + ], + Some(Error::from_string("duplicate endpoints found")), + 9, + ), + ( + vec!["ftp://server/d1", "http://server/d2", "http://server/d3", "http://server/d4"], + Some(Error::from_string("'ftp://server/d1': invalid URL endpoint format")), + 10, + ), + ( + vec!["d1", "http://localhost/d2", "d3", "d4"], + Some(Error::from_string("mixed style endpoints are not supported")), + 11, + ), + ( + vec![ + "http://example.org/d1", + "https://example.com/d1", + "http://example.net/d1", + "https://example.edut/d1", + ], + Some(Error::from_string("mixed scheme is not supported")), + 12, + ), + ( + vec![ + "192.168.1.210:9000/tmp/dir0", + "192.168.1.210:9000/tmp/dir1", + "192.168.1.210:9000/tmp/dir2", + "192.168.110:9000/tmp/dir3", + ], + Some(Error::from_string( + "'192.168.1.210:9000/tmp/dir0': invalid URL endpoint format: missing scheme http or https", + )), + 13, + ), + ]; + + for test_case in test_cases { + let args: Vec = test_case.0.iter().map(|v| v.to_string()).collect(); + let ret = Endpoints::try_from(args.as_slice()); + + match (test_case.1, ret) { + (None, Err(e)) => panic!("{}: error: expected = , got = {}", test_case.2, e), + (None, Ok(_)) => {} + (Some(e), Ok(_)) => panic!("{}: error: expected = {}, got = ", test_case.2, e), + (Some(e), Err(e2)) => { + assert_eq!(e.to_string(), e2.to_string(), "{}: error: expected = {}, got = {}", test_case.2, e, e2) + } + } + } + } + + #[test] + fn test_create_pool_endpoints() { + #[derive(Default)] + struct TestCase<'a> { + num: usize, + server_addr: &'a str, + args: Vec<&'a str>, + expected_endpoints: Option, + expected_setup_type: Option, + expected_err: Option, + } + + // Filter ipList by IPs those do not start with '127.'. + let non_loop_back_i_ps = + net::must_get_local_ips().map_or(vec![], |v| v.into_iter().filter(|ip| ip.is_ipv4() && ip.is_loopback()).collect()); + if non_loop_back_i_ps.is_empty() { + panic!("No non-loop back IP address found for this host"); + } + let non_loop_back_ip = non_loop_back_i_ps[0]; + + let case1_endpoint1 = format!("http://{}/d1", non_loop_back_ip); + let case1_endpoint2 = format!("http://{}/d2", non_loop_back_ip); + let args = vec![ + format!("http://{}:10000/d1", non_loop_back_ip), + format!("http://{}:10000/d2", non_loop_back_ip), + "http://example.org:10000/d3".to_string(), + "http://example.com:10000/d4".to_string(), + ]; + let (case1_ur_ls, case1_local_flags) = get_expected_endpoints(args, format!("http://{}:10000/", non_loop_back_ip)); + + let case2_endpoint1 = format!("http://{}/d1", non_loop_back_ip); + let case2_endpoint2 = format!("http://{}:9000/d2", non_loop_back_ip); + let args = vec![ + format!("http://{}:10000/d1", non_loop_back_ip), + format!("http://{}:9000/d2", non_loop_back_ip), + "http://example.org:10000/d3".to_string(), + "http://example.com:10000/d4".to_string(), + ]; + let (case2_ur_ls, case2_local_flags) = get_expected_endpoints(args, format!("http://{}:10000/", non_loop_back_ip)); + + let case3_endpoint1 = format!("http://{}/d1", non_loop_back_ip); + let args = vec![ + format!("http://{}:80/d1", non_loop_back_ip), + "http://example.org:9000/d2".to_string(), + "http://example.com:80/d3".to_string(), + "http://example.net:80/d4".to_string(), + ]; + let (case3_ur_ls, case3_local_flags) = get_expected_endpoints(args, format!("http://{}:80/", non_loop_back_ip)); + + let case4_endpoint1 = format!("http://{}/d1", non_loop_back_ip); + let args = vec![ + format!("http://{}:9000/d1", non_loop_back_ip), + "http://example.org:9000/d2".to_string(), + "http://example.com:9000/d3".to_string(), + "http://example.net:9000/d4".to_string(), + ]; + let (case4_ur_ls, case4_local_flags) = get_expected_endpoints(args, format!("http://{}:9000/", non_loop_back_ip)); + + let case5_endpoint1 = format!("http://{}:9000/d1", non_loop_back_ip); + let case5_endpoint2 = format!("http://{}:9001/d2", non_loop_back_ip); + let case5_endpoint3 = format!("http://{}:9002/d3", non_loop_back_ip); + let case5_endpoint4 = format!("http://{}:9003/d4", non_loop_back_ip); + let args = vec![ + case5_endpoint1.clone(), + case5_endpoint2.clone(), + case5_endpoint3.clone(), + case5_endpoint4.clone(), + ]; + let (case5_ur_ls, case5_local_flags) = get_expected_endpoints(args, format!("http://{}:9000/", non_loop_back_ip)); + + let case6_endpoint1 = format!("http://{}:9003/d4", non_loop_back_ip); + let args = vec![ + "http://localhost:9000/d1".to_string(), + "http://localhost:9001/d2".to_string(), + "http://127.0.0.1:9002/d3".to_string(), + case6_endpoint1.clone(), + ]; + let (case6_ur_ls, case6_local_flags) = get_expected_endpoints(args, format!("http://{}:9003/", non_loop_back_ip)); + + let case7_endpoint1 = format!("http://{}:9001/export", non_loop_back_ip); + let case7_endpoint2 = format!("http://{}:9000/export", non_loop_back_ip); + + let test_cases = [ + TestCase { + num: 1, + server_addr: "localhost", + expected_err: Some(Error::from_string("address localhost: missing port in address")), + ..Default::default() + }, + // Erasure Single Drive + TestCase { + num: 2, + server_addr: "localhost:9000", + args: vec!["http://localhost/d1"], + expected_err: Some(Error::from_string("use path style endpoint for single node setup")), + ..Default::default() + }, + TestCase { + num: 3, + server_addr: "0.0.0.0:443", + args: vec!["/d1"], + expected_endpoints: Some(Endpoints(vec![Endpoint { + url: must_file_path("/d1"), + is_local: true, + pool_idx: None, + set_idx: None, + disk_idx: None, + }])), + expected_setup_type: Some(SetupType::ErasureSD), + ..Default::default() + }, + TestCase { + num: 4, + server_addr: "localhost:10000", + args: vec!["/d1"], + expected_endpoints: Some(Endpoints(vec![Endpoint { + url: must_file_path("/d1"), + is_local: true, + pool_idx: None, + set_idx: None, + disk_idx: None, + }])), + expected_setup_type: Some(SetupType::ErasureSD), + ..Default::default() + }, + TestCase { + num: 5, + server_addr: "localhost:9000", + args: vec![ + "https://127.0.0.1:9000/d1", + "https://localhost:9001/d1", + "https://example.com/d1", + "https://example.com/d2", + ], + expected_err: Some(Error::from_string("same path '/d1' can not be served by different port on same address")), + ..Default::default() + }, + // Erasure Setup with PathEndpointType + TestCase { + num: 6, + server_addr: "0.0.0.0:1234", + args: vec!["/d1", "/d2", "/d3", "/d4"], + expected_endpoints: Some(Endpoints(vec![ + Endpoint { + url: must_file_path("/d1"), + is_local: true, + pool_idx: None, + set_idx: None, + disk_idx: None, + }, + Endpoint { + url: must_file_path("/d2"), + is_local: true, + pool_idx: None, + set_idx: None, + disk_idx: None, + }, + Endpoint { + url: must_file_path("/d3"), + is_local: true, + pool_idx: None, + set_idx: None, + disk_idx: None, + }, + Endpoint { + url: must_file_path("/d4"), + is_local: true, + pool_idx: None, + set_idx: None, + disk_idx: None, + }, + ])), + expected_setup_type: Some(SetupType::Erasure), + ..Default::default() + }, + // DistErasure Setup with URLEndpointType + TestCase { + num: 7, + server_addr: "0.0.0.0:9000", + args: vec![ + "http://localhost/d1", + "http://localhost/d2", + "http://localhost/d3", + "http://localhost/d4", + ], + expected_endpoints: Some(Endpoints(vec![ + Endpoint { + url: must_url("http://localhost:9000/d1"), + is_local: true, + pool_idx: None, + set_idx: None, + disk_idx: None, + }, + Endpoint { + url: must_url("http://localhost:9000/d2"), + is_local: true, + pool_idx: None, + set_idx: None, + disk_idx: None, + }, + Endpoint { + url: must_url("http://localhost:9000/d3"), + is_local: true, + pool_idx: None, + set_idx: None, + disk_idx: None, + }, + Endpoint { + url: must_url("http://localhost:9000/d4"), + is_local: true, + pool_idx: None, + set_idx: None, + disk_idx: None, + }, + ])), + expected_setup_type: Some(SetupType::Erasure), + ..Default::default() + }, + // DistErasure Setup with URLEndpointType having mixed naming to local host. + TestCase { + num: 8, + server_addr: "127.0.0.1:10000", + args: vec![ + "http://localhost/d1", + "http://localhost/d2", + "http://127.0.0.1/d3", + "http://127.0.0.1/d4", + ], + expected_err: Some(Error::from_string("all local endpoints should not have different hostnames/ips")), + ..Default::default() + }, + TestCase { + num: 9, + server_addr: "0.0.0.0:9001", + args: vec![ + "http://10.0.0.1:9000/export", + "http://10.0.0.2:9000/export", + case7_endpoint1.as_str(), + "http://10.0.0.2:9001/export", + ], + expected_err: Some(Error::from_string( + "same path '/export' can not be served by different port on same address", + )), + ..Default::default() + }, + TestCase { + num: 10, + server_addr: "0.0.0.0:9000", + args: vec![ + "http://127.0.0.1:9000/export", + case7_endpoint2.as_str(), + "http://10.0.0.1:9000/export", + "http://10.0.0.2:9000/export", + ], + expected_err: Some(Error::from_string("path '/export' cannot be served by different address on same server")), + ..Default::default() + }, + // DistErasure type + TestCase { + num: 11, + server_addr: "127.0.0.1:10000", + args: vec![ + case1_endpoint1.as_str(), + case1_endpoint2.as_str(), + "http://example.org/d3", + "http://example.com/d4", + ], + expected_endpoints: Some(Endpoints(vec![ + Endpoint { + url: case1_ur_ls[0].clone(), + is_local: case1_local_flags[0], + pool_idx: None, + set_idx: None, + disk_idx: None, + }, + Endpoint { + url: case1_ur_ls[1].clone(), + is_local: case1_local_flags[1], + pool_idx: None, + set_idx: None, + disk_idx: None, + }, + Endpoint { + url: case1_ur_ls[2].clone(), + is_local: case1_local_flags[2], + pool_idx: None, + set_idx: None, + disk_idx: None, + }, + Endpoint { + url: case1_ur_ls[3].clone(), + is_local: case1_local_flags[3], + pool_idx: None, + set_idx: None, + disk_idx: None, + }, + ])), + expected_setup_type: Some(SetupType::DistErasure), + ..Default::default() + }, + TestCase { + num: 12, + server_addr: "127.0.0.1:10000", + args: vec![ + case2_endpoint1.as_str(), + case2_endpoint2.as_str(), + "http://example.org/d3", + "http://example.com/d4", + ], + expected_endpoints: Some(Endpoints(vec![ + Endpoint { + url: case2_ur_ls[0].clone(), + is_local: case2_local_flags[0], + pool_idx: None, + set_idx: None, + disk_idx: None, + }, + Endpoint { + url: case2_ur_ls[1].clone(), + is_local: case2_local_flags[1], + pool_idx: None, + set_idx: None, + disk_idx: None, + }, + Endpoint { + url: case2_ur_ls[2].clone(), + is_local: case2_local_flags[2], + pool_idx: None, + set_idx: None, + disk_idx: None, + }, + Endpoint { + url: case2_ur_ls[3].clone(), + is_local: case2_local_flags[3], + pool_idx: None, + set_idx: None, + disk_idx: None, + }, + ])), + expected_setup_type: Some(SetupType::DistErasure), + ..Default::default() + }, + TestCase { + num: 13, + server_addr: "0.0.0.0:80", + args: vec![ + case3_endpoint1.as_str(), + "http://example.org:9000/d2", + "http://example.com/d3", + "http://example.net/d4", + ], + expected_endpoints: Some(Endpoints(vec![ + Endpoint { + url: case3_ur_ls[0].clone(), + is_local: case3_local_flags[0], + pool_idx: None, + set_idx: None, + disk_idx: None, + }, + Endpoint { + url: case3_ur_ls[1].clone(), + is_local: case3_local_flags[1], + pool_idx: None, + set_idx: None, + disk_idx: None, + }, + Endpoint { + url: case3_ur_ls[2].clone(), + is_local: case3_local_flags[2], + pool_idx: None, + set_idx: None, + disk_idx: None, + }, + Endpoint { + url: case3_ur_ls[3].clone(), + is_local: case3_local_flags[3], + pool_idx: None, + set_idx: None, + disk_idx: None, + }, + ])), + expected_setup_type: Some(SetupType::DistErasure), + ..Default::default() + }, + TestCase { + num: 14, + server_addr: "0.0.0.0:9000", + args: vec![ + case4_endpoint1.as_str(), + "http://example.org/d2", + "http://example.com/d3", + "http://example.net/d4", + ], + expected_endpoints: Some(Endpoints(vec![ + Endpoint { + url: case4_ur_ls[0].clone(), + is_local: case4_local_flags[0], + pool_idx: None, + set_idx: None, + disk_idx: None, + }, + Endpoint { + url: case4_ur_ls[1].clone(), + is_local: case4_local_flags[1], + pool_idx: None, + set_idx: None, + disk_idx: None, + }, + Endpoint { + url: case4_ur_ls[2].clone(), + is_local: case4_local_flags[2], + pool_idx: None, + set_idx: None, + disk_idx: None, + }, + Endpoint { + url: case4_ur_ls[3].clone(), + is_local: case4_local_flags[3], + pool_idx: None, + set_idx: None, + disk_idx: None, + }, + ])), + expected_setup_type: Some(SetupType::DistErasure), + ..Default::default() + }, + TestCase { + num: 15, + server_addr: "0.0.0.0:9000", + args: vec![ + case5_endpoint1.as_str(), + case5_endpoint2.as_str(), + case5_endpoint3.as_str(), + case5_endpoint4.as_str(), + ], + expected_endpoints: Some(Endpoints(vec![ + Endpoint { + url: case5_ur_ls[0].clone(), + is_local: case5_local_flags[0], + pool_idx: None, + set_idx: None, + disk_idx: None, + }, + Endpoint { + url: case5_ur_ls[1].clone(), + is_local: case5_local_flags[1], + pool_idx: None, + set_idx: None, + disk_idx: None, + }, + Endpoint { + url: case5_ur_ls[2].clone(), + is_local: case5_local_flags[2], + pool_idx: None, + set_idx: None, + disk_idx: None, + }, + Endpoint { + url: case5_ur_ls[3].clone(), + is_local: case5_local_flags[3], + pool_idx: None, + set_idx: None, + disk_idx: None, + }, + ])), + expected_setup_type: Some(SetupType::DistErasure), + ..Default::default() + }, + TestCase { + num: 16, + server_addr: "0.0.0.0:9003", + args: vec![ + "http://localhost:9000/d1", + "http://localhost:9001/d2", + "http://127.0.0.1:9002/d3", + case6_endpoint1.as_str(), + ], + expected_endpoints: Some(Endpoints(vec![ + Endpoint { + url: case6_ur_ls[0].clone(), + is_local: case6_local_flags[0], + pool_idx: None, + set_idx: None, + disk_idx: None, + }, + Endpoint { + url: case6_ur_ls[1].clone(), + is_local: case6_local_flags[1], + pool_idx: None, + set_idx: None, + disk_idx: None, + }, + Endpoint { + url: case6_ur_ls[2].clone(), + is_local: case6_local_flags[2], + pool_idx: None, + set_idx: None, + disk_idx: None, + }, + Endpoint { + url: case6_ur_ls[3].clone(), + is_local: case6_local_flags[3], + pool_idx: None, + set_idx: None, + disk_idx: None, + }, + ])), + expected_setup_type: Some(SetupType::DistErasure), + ..Default::default() + }, + ]; + + for test_case in test_cases { + let disks_layout = match DisksLayout::try_from(test_case.args.as_slice()) { + Ok(v) => v, + Err(e) => { + if test_case.expected_err.is_none() { + panic!("Test {}: unexpected error: {}", test_case.num, e); + } + continue; + } + }; + + match ( + test_case.expected_err, + PoolEndpointList::create_pool_endpoints(test_case.server_addr, &disks_layout), + ) { + (None, Err(err)) => panic!("Test {}: error: expected = , got = {}", test_case.num, err), + (Some(err), Ok(_)) => panic!("Test {}: error: expected = {}, got = ", test_case.num, err), + (Some(e), Err(e2)) => { + assert_eq!( + e.to_string(), + e2.to_string(), + "Test {}: error: expected = {}, got = {}", + test_case.num, + e, + e2 + ) + } + (None, Ok(pools)) => { + if Some(&pools.setup_type) != test_case.expected_setup_type.as_ref() { + panic!( + "Test {}: setupType: expected = {:?}, got = {:?}", + test_case.num, test_case.expected_setup_type, &pools.setup_type + ) + } + + let left_len = test_case.expected_endpoints.as_ref().map(|v| v.as_ref().len()); + let right_len = pools.as_ref().first().map(|v| v.as_ref().len()); + + if left_len != right_len { + panic!("Test {}: endpoints len: expected = {:?}, got = {:?}", test_case.num, left_len, right_len); + } + + for (i, ep) in pools.as_ref()[0].as_ref().iter().enumerate() { + assert_eq!( + ep.to_string(), + test_case.expected_endpoints.as_ref().unwrap().as_ref()[i].to_string(), + "Test {}: endpoints: expected = {}, got = {}", + test_case.num, + test_case.expected_endpoints.as_ref().unwrap().as_ref()[i], + ep + ) + } + } + } + } + } + + fn must_file_path(s: impl AsRef) -> url::Url { + let url = url::Url::from_file_path(s.as_ref()); + + assert!(url.is_ok(), "failed to convert path to URL: {}", s.as_ref().display()); + + url.unwrap() + } + + fn must_url(s: &str) -> url::Url { + url::Url::parse(s).unwrap() + } + + fn get_expected_endpoints(args: Vec, prefix: String) -> (Vec, Vec) { + let mut urls = vec![]; + let mut local_flags = vec![]; + for arg in args { + urls.push(url::Url::parse(&arg).unwrap()); + local_flags.push(arg.starts_with(&prefix)); + } + + (urls, local_flags) + } + + #[test] + fn test_create_server_endpoints() { + let test_cases = [ + // Invalid input. + ("", vec![], false), + // Range cannot be negative. + ("0.0.0.0:9000", vec!["/export1{-1...1}"], false), + // Range cannot start bigger than end. + ("0.0.0.0:9000", vec!["/export1{64...1}"], false), + // Range can only be numeric. + ("0.0.0.0:9000", vec!["/export1{a...z}"], false), + // Duplicate disks not allowed. + ("0.0.0.0:9000", vec!["/export1{1...32}", "/export1{1...32}"], false), + // Same host cannot export same disk on two ports - special case localhost. + ("0.0.0.0:9001", vec!["http://localhost:900{1...2}/export{1...64}"], false), + // Valid inputs. + ("0.0.0.0:9000", vec!["/export1"], true), + ("0.0.0.0:9000", vec!["/export1", "/export2", "/export3", "/export4"], true), + ("0.0.0.0:9000", vec!["/export1{1...64}"], true), + ("0.0.0.0:9000", vec!["/export1{01...64}"], true), + ("0.0.0.0:9000", vec!["/export1{1...32}", "/export1{33...64}"], true), + ("0.0.0.0:9001", vec!["http://localhost:9001/export{1...64}"], true), + ("0.0.0.0:9001", vec!["http://localhost:9001/export{01...64}"], true), + ]; + + for (i, test_case) in test_cases.iter().enumerate() { + let disks_layout = match DisksLayout::try_from(test_case.1.as_slice()) { + Ok(v) => v, + Err(e) => { + if test_case.2 { + panic!("Test {}: unexpected error: {}", i + 1, e); + } + continue; + } + }; + + let ret = EndpointServerPools::create_server_endpoints(test_case.0, &disks_layout); + + if let Err(err) = ret { + if test_case.2 { + panic!("Test {}: Expected success but failed instead {}", i + 1, err) + } + } else if !test_case.2 { + panic!("Test {}: expected failure but passed instead", i + 1); + } + } + } +} diff --git a/ecstore/src/peer.rs b/ecstore/src/peer.rs index 7e61af877..cf23ffcc5 100644 --- a/ecstore/src/peer.rs +++ b/ecstore/src/peer.rs @@ -14,7 +14,7 @@ type Client = Arc>; #[async_trait] pub trait PeerS3Client: Debug + Sync + Send + 'static { async fn make_bucket(&self, bucket: &str, opts: &MakeBucketOptions) -> Result<()>; - fn get_pools(&self) -> Vec; + fn get_pools(&self) -> Vec; } #[derive(Debug)] @@ -53,7 +53,7 @@ impl S3PeerSys { #[async_trait] impl PeerS3Client for S3PeerSys { - fn get_pools(&self) -> Vec { + fn get_pools(&self) -> Vec { unimplemented!() } async fn make_bucket(&self, bucket: &str, opts: &MakeBucketOptions) -> Result<()> { @@ -80,7 +80,7 @@ impl PeerS3Client for S3PeerSys { let mut per_pool_errs = Vec::with_capacity(self.clients.len()); for (j, cli) in self.clients.iter().enumerate() { let pools = cli.get_pools(); - let idx = i as i32; + let idx = i; if pools.contains(&idx) { per_pool_errs.push(errors[j].as_ref()); } @@ -99,11 +99,11 @@ impl PeerS3Client for S3PeerSys { pub struct LocalPeerS3Client { pub local_disks: Vec, pub node: Node, - pub pools: Vec, + pub pools: Vec, } impl LocalPeerS3Client { - fn new(local_disks: Vec, node: Node, pools: Vec) -> Self { + fn new(local_disks: Vec, node: Node, pools: Vec) -> Self { Self { local_disks, node, @@ -114,7 +114,7 @@ impl LocalPeerS3Client { #[async_trait] impl PeerS3Client for LocalPeerS3Client { - fn get_pools(&self) -> Vec { + fn get_pools(&self) -> Vec { self.pools.clone() } async fn make_bucket(&self, bucket: &str, opts: &MakeBucketOptions) -> Result<()> { @@ -154,18 +154,18 @@ impl PeerS3Client for LocalPeerS3Client { #[derive(Debug)] pub struct RemotePeerS3Client { pub node: Node, - pub pools: Vec, + pub pools: Vec, } impl RemotePeerS3Client { - fn new(node: Node, pools: Vec) -> Self { + fn new(node: Node, pools: Vec) -> Self { Self { node, pools } } } #[async_trait] impl PeerS3Client for RemotePeerS3Client { - fn get_pools(&self) -> Vec { + fn get_pools(&self) -> Vec { unimplemented!() } async fn make_bucket(&self, _bucket: &str, _opts: &MakeBucketOptions) -> Result<()> { diff --git a/ecstore/src/store.rs b/ecstore/src/store.rs index dfdeaa0f5..bf3145d3c 100644 --- a/ecstore/src/store.rs +++ b/ecstore/src/store.rs @@ -29,20 +29,21 @@ pub struct ECStore { impl ECStore { pub async fn new(address: String, endpoints: Vec) -> Result { - let layouts = DisksLayout::try_from(endpoints.as_slice())?; + let layouts = DisksLayout::try_from(endpoints.as_slice()).map_err(|v| Error::msg(v))?; let mut deployment_id = None; - let (endpoint_pools, _) = EndpointServerPools::create_server_endpoints(address.as_str(), &layouts)?; + let (endpoint_pools, _) = + EndpointServerPools::create_server_endpoints(address.as_str(), &layouts).map_err(|v| Error::msg(v))?; - let mut pools = Vec::with_capacity(endpoint_pools.len()); - let mut disk_map = HashMap::with_capacity(endpoint_pools.len()); + let mut pools = Vec::with_capacity(endpoint_pools.as_ref().len()); + let mut disk_map = HashMap::with_capacity(endpoint_pools.as_ref().len()); - let first_is_local = endpoint_pools.first_is_local(); + let first_is_local = endpoint_pools.first_local(); let mut local_disks = Vec::new(); - for (i, pool_eps) in endpoint_pools.iter().enumerate() { + for (i, pool_eps) in endpoint_pools.as_ref().iter().enumerate() { // TODO: read from config parseStorageClass let partiy_count = store_init::default_partiy_count(pool_eps.drives_per_set); diff --git a/ecstore/src/utils/net.rs b/ecstore/src/utils/net.rs index 81441512e..91f453e86 100644 --- a/ecstore/src/utils/net.rs +++ b/ecstore/src/utils/net.rs @@ -1,13 +1,20 @@ use crate::error::{Error, Result}; +use lazy_static::lazy_static; use std::{ collections::HashSet, net::{IpAddr, SocketAddr, ToSocketAddrs}, }; use url::Host; +lazy_static! { + static ref LOCAL_IPS: Vec = must_get_local_ips().unwrap(); +} + /// helper for validating if the provided arg is an ip address. -pub fn is_socket_addr(host: &str) -> bool { - host.parse::().is_ok() || host.parse::().is_ok() +pub fn is_socket_addr(addr: &str) -> bool { + // TODO IPv6 zone information? + + addr.parse::().is_ok() || addr.parse::().is_ok() } /// checks if server_addr is valid and local host. @@ -30,30 +37,18 @@ pub fn check_local_server_addr(server_addr: &str) -> Result { SocketAddr::V6(a) => Host::Ipv6(*a.ip()), }; - if is_local_host(host, 0, 0).is_ok() { + if is_local_host(host, 0, 0)? { return Ok(a); } } - return Err(Error::from_string("host in server address should be this server")); -} - -pub fn split_host_port(s: &str) -> Result<(String, u16)> { - let parts: Vec<&str> = s.split(':').collect(); - if parts.len() == 2 { - if let Ok(port) = parts[1].parse::() { - return Ok((parts[0].to_string(), port)); - } - } - Err(Error::from_string("Invalid address format or port number")) + Err(Error::from_string("host in server address should be this server")) } /// checks if the given parameter correspond to one of /// the local IP of the current machine pub fn is_local_host(host: Host<&str>, port: u16, local_port: u16) -> Result { - let local_ips = must_get_local_ips(); - - let local_set: HashSet = local_ips.into_iter().collect(); + let local_set: HashSet = LOCAL_IPS.iter().copied().collect(); let is_local_host = match host { Host::Domain(domain) => { let ips = match (domain, 0).to_socket_addrs().map(|v| v.map(|v| v.ip()).collect::>()) { @@ -74,11 +69,34 @@ pub fn is_local_host(host: Host<&str>, port: u16, local_port: u16) -> Result) -> Result> { + match host { + Host::Domain(domain) => match (domain, 0) + .to_socket_addrs() + .map(|v| v.map(|v| v.ip()).collect::>()) + { + Ok(ips) => Ok(ips), + Err(err) => Err(Error::new(Box::new(err))), + }, + Host::Ipv4(ip) => { + let mut set = HashSet::with_capacity(1); + set.insert(IpAddr::V4(ip)); + Ok(set) + } + Host::Ipv6(ip) => { + let mut set = HashSet::with_capacity(1); + set.insert(IpAddr::V6(ip)); + Ok(set) + } + } +} + /// returns IPs of local interface -fn must_get_local_ips() -> Vec { +pub(crate) fn must_get_local_ips() -> Result> { match netif::up() { - Ok(up) => up.map(|x| x.address().to_owned()).collect(), - Err(_) => vec![], + Ok(up) => Ok(up.map(|x| x.address().to_owned()).collect()), + Err(err) => Err(Error::from_string(format!("Unable to get IP addresses of this host: {}", err))), } } @@ -106,9 +124,36 @@ mod test { } } + #[test] + fn test_check_local_server_addr() { + let test_cases = [ + (":54321", Ok(())), + ("localhost:54321", Ok(())), + ("0.0.0.0:9000", Ok(())), + (":0", Ok(())), + ("localhost", Err(Error::from_string("invalid socket address"))), + ("", Err(Error::from_string("invalid socket address"))), + ( + "example.org:54321", + Err(Error::from_string("host in server address should be this server")), + ), + (":-10", Err(Error::from_string("invalid port value"))), + ]; + + for test_case in test_cases { + let ret = check_local_server_addr(test_case.0); + if test_case.1.is_ok() && ret.is_err() { + panic!("{}: error: expected = , got = {:?}", test_case.0, ret); + } + if test_case.1.is_err() && ret.is_ok() { + panic!("{}: error: expected = {:?}, got = ", test_case.0, test_case.1); + } + } + } + #[test] fn test_must_get_local_ips() { - let local_ips = must_get_local_ips(); + let local_ips = must_get_local_ips().unwrap(); let local_set: HashSet = local_ips.into_iter().collect(); assert!(local_set.contains(&IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))));