cml 0.1.6

A library to programmatically and safely interact with the rest API of Cisco Modeling Labs (CML), formerly VIRL2
Documentation

use std::collections::HashMap;

use crate::rest_types::LabTopology;
use crate::{CmlResult, CmlUser, rt};
use log::debug;
use regex::Regex;
use thiserror::Error;

#[derive(Debug, Clone, Copy, Error)]
pub enum NodeSearchError {
	#[error("No lab matching the desired lab descriptor")]
	NoMatchingLab,
	#[error("No node matching the desired node descriptor within the specified lab")]
	NoMatchingNode,
	#[error("Multiple labs matched by lab title. Try renaming the labs, or using a lab ID")]
	MultipleMatchingLabs,
	#[error("Multiple nodes matched by node title within the lab. Try renaming the nodes, or using a node ID")]
	MultipleMatchingNodes,
}


#[derive(Debug, Clone)]
pub struct NodeCtx {
	host: String,
	user: String,
	lab: (String, String),
	node: (String, String),
	meta: rt::NodeDescription,
}
impl NodeCtx {
	pub fn host(&self) -> &str { &self.host }
	pub fn user(&self) -> &str { &self.user }

	/// Returns a `str` tuple of (lab_id, lab_name)
	pub fn lab(&self) -> (&str, &str) { (&self.lab.0, &self.lab.1) }

	/// Returns a `str` tuple of (node_id, node_name)
	pub fn node(&self) -> (&str, &str) { (&self.node.0, &self.node.1) }
	pub fn meta(&self) -> &rt::NodeDescription { &self.meta }


	/// Searches for a single lab/device, based on full ID/names of them.
	pub async fn search(client: &CmlUser, lab: &str, device: &str) -> CmlResult<Result<NodeCtx, NodeSearchError>> {
		// get all the labs and topologies on the CML server
		let labs = client.labs(true).await?;
		let topos = client.lab_topologies(&labs, false).await?
			.into_iter()
			.map(|(s, t)| (s.to_string(), t.expect("Lab removed during searching")))
			.collect::<HashMap<_, _>>();

		// find an appropriate lab, prioritizing IDs on conflicts
		// returns early if none are found or found multiple labs with desired title
		let (lab_id, lab_topo) = match <(String, LabTopology)>::find_single(topos.into_iter(), lab) {
			Err(se) => return Ok(Err(se)),
			Ok(s) => s
		};
		let (node_id, node_data) = match <rt::labeled::Data<rt::NodeDescription>>::find_single(lab_topo.nodes, device) {
			Err(se) => return Ok(Err(se)),
			Ok(node) => (node.id, node.data)
		};

		Ok(Ok(NodeCtx {
			host: client.host().to_owned(),
			user: client.username().to_owned(),
			lab: (lab_id, lab_topo.title),
			node: (node_id, node_data.label.clone()),
			meta: node_data,
		}))
	}

	/// Searches for multiple labs/devices based on a specified regex
	///
	/// If multiple labs/nodes match, and one of the matches is by full ID (eg, `"^abc12f$"`) then only the lab/node with the ID is matched, and others are skipped.
	///
	/// In other words, in conflicts with an exact match of lab/node ID/name, the ID is favored.
	pub async fn search_pat(client: &CmlUser, lab: &Regex, node: &Regex) -> CmlResult<HashMap<String, (LabTopology, Vec<(String, ())>)>> {
		
		let labs = client.labs(true).await?;

		let mut matched_lab_id = false;
		let mut topos: Vec<(String, LabTopology)> = client.lab_topologies(&labs, false).await?
			.into_iter()
			.map(|(s, t)| {
				(s.to_string(), t.expect("Lab removed during searching"))
			})
			.filter(|(lab_id, topo)| {
				let idmatches = lab.is_match(&lab_id);
				if idmatches { matched_lab_id = true; }
				idmatches || lab.is_match(&topo.title)
			})
			.collect();

		// if a lab matched by full ID
		let labregstr = lab.as_str();
		if matched_lab_id && labregstr.starts_with('^') && labregstr.ends_with('$') {
			// filter topos to the single ID
			topos.retain(|(lab_id, _)| lab.is_match(&lab_id));
		}

		// we have matched our labs - filter nodes
		


		todo!()
	}
}


// internal helpers used for ConsoleCtx::search
trait NamedMatcher {
	fn matcher_kind() -> &'static str;
	fn none_found() -> NodeSearchError;
	fn multiple_found() -> NodeSearchError;
	fn id(&self) -> &str;
	fn label(&self) -> &str;
	fn state(&self) -> rt::State;
	
	fn matches(&self, descriptor: &str) -> bool {
		self.id() == descriptor || self.label() == descriptor
	}

	fn find_single<'a, N: NamedMatcher + Sized, I: IntoIterator<Item = N>>(s: I, desc: &'_ str) -> Result<N, NodeSearchError> {
		let mut matching: Vec<N> = s.into_iter()
			.filter(|lab_data| lab_data.matches(desc))
			.inspect(|n| debug!("found {} matching the provided description: (id, name, state) = {:?}", N::matcher_kind(), (n.id(), n.label(), n.state())))
			.collect();

		if matching.len() == 0 {
			Err(Self::none_found())
			//Err(format!("No {} found by ID/name: {:?}", N::matcher_kind(), desc))
		} else if matching.len() == 1 {
			Ok(matching.remove(0))
		} else {
			let by_id = matching.iter()
				.position(|n| n.id() == desc);

			if let Some(res_i) = by_id {
				debug!("Found multiple {}s for {} description {:?} - interpreting it as an ID", N::matcher_kind(), N::matcher_kind(), desc);
				Ok(matching.remove(res_i))
			} else {
				Err(Self::multiple_found())
			}
		}
	}
}
impl NamedMatcher for (String, LabTopology) {
	fn matcher_kind() -> &'static str { "lab" }
	fn none_found() -> NodeSearchError { NodeSearchError::NoMatchingLab }
	fn multiple_found() -> NodeSearchError { NodeSearchError::MultipleMatchingLabs }
	fn id(&self) -> &str { &self.0 }
	fn label(&self) -> &str { &self.1.title }
	fn state(&self) -> rt::State { self.1.state }
}
impl NamedMatcher for rt::labeled::Data<rt::NodeDescription> {
	fn matcher_kind() -> &'static str { "node" }
	fn none_found() -> NodeSearchError { NodeSearchError::NoMatchingNode }
	fn multiple_found() -> NodeSearchError { NodeSearchError::MultipleMatchingNodes }
	fn id(&self) -> &str { &self.id }
	fn label(&self) -> &str { &self.data.label }
	fn state(&self) -> rt::State { self.data.state }
}