use crate::folders::{FolderClient, FolderPath};
use crate::interfaces::InterfaceType;
use crate::networks::{NetworkClient, NetworksClient};
use crate::nodes::NodeStatus;
use crate::nodes::{NodeClient, NodesClient};
use crate::system::SystemClient;
use crate::utils::{empty_string_is_none, map_or_seq, number_from_string, validate_name};
use crate::{Client, Error, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Debug, Serialize, Deserialize)]
pub struct Lab {
pub filename: String,
pub id: String,
pub lock: u8,
pub name: String,
pub scripttimeout: u32,
#[serde(deserialize_with = "number_from_string")]
pub version: u32,
#[serde(
deserialize_with = "empty_string_is_none",
skip_serializing_if = "Option::is_none"
)]
pub author: Option<String>,
#[serde(
deserialize_with = "empty_string_is_none",
skip_serializing_if = "Option::is_none"
)]
pub body: Option<String>,
#[serde(
deserialize_with = "empty_string_is_none",
skip_serializing_if = "Option::is_none"
)]
pub description: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct TopologyEntry {
pub destination: String,
pub destination_label: String,
pub destination_type: String,
pub source: String,
pub source_label: String,
pub source_type: String,
#[serde(rename = "type")]
pub connection_type: InterfaceType,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Links {
#[serde(deserialize_with = "map_or_seq")]
pub ethernet: HashMap<i32, String>,
#[serde(deserialize_with = "map_or_seq")]
pub serial: HashMap<i32, HashMap<i32, String>>,
}
pub struct LabsClient {
client: Client,
path: FolderPath,
}
impl LabsClient {
pub(crate) fn new(client: Client, path: FolderPath) -> Self {
Self { client, path }
}
pub async fn add(&self, params: AddLabRequest) -> Result<LabClient> {
let params = params.path(self.path.as_str());
self.client
.post::<(), AddLabRequest>("labs", ¶ms)
.await?;
let new_path = LabPath::from_validated(self.path.clone(), ¶ms.name);
Ok(LabClient::from_path(self.client.clone(), new_path))
}
pub async fn current(&self) -> Result<Option<LabClient>> {
let Some(current_lab) = SystemClient::new(self.client.clone())
.auth_status()
.await?
.lab
else {
return Ok(None);
};
let path = LabPath::from_str(¤t_lab);
Ok(Some(LabClient::from_path(self.client.clone(), path)))
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct LabPath(Arc<str>);
impl LabPath {
pub(crate) fn new(path: FolderPath, name: impl AsRef<str>) -> Result<Self> {
let name = name.as_ref();
Self::validate(name)?;
Ok(Self(Arc::from(Self::join(path, name))))
}
fn validate(name: &str) -> Result<()> {
if name.is_empty() {
return Err(Error::Client("Lab name cannot be empty".to_string()));
}
if !validate_name(name, &['-', '_', ' ']) {
return Err(Error::Client(format!(
"Invalid lab name '{}', must only contain letters, digits, spaces and '-'/'_'",
name
)));
}
Ok(())
}
pub(crate) fn from_str(path: &str) -> Self {
Self(Arc::from(path))
}
pub(crate) fn from_validated(path: FolderPath, name: &str) -> Self {
Self::from_str(&Self::join(path, name))
}
pub(crate) fn as_str(&self) -> &str {
&self.0
}
pub(crate) fn folder(&self) -> FolderPath {
FolderPath::from_str(
self.0
.rsplit_once("/")
.map(|(p, _)| p)
.filter(|p| !p.is_empty())
.unwrap_or("/"),
)
}
pub(crate) fn lab_file(&self) -> &str {
self.0.rsplit("/").next().unwrap()
}
pub(crate) fn lab_name(&self) -> &str {
self.lab_file()
.split_once(".")
.map(|(name, _)| name)
.unwrap()
}
fn join(folder: FolderPath, lab: &str) -> String {
format!("{}/{}.unl", folder.as_str().trim_end_matches("/"), lab)
}
}
impl std::fmt::Display for LabPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
pub struct LabClient {
client: Client,
path: LabPath,
}
impl LabClient {
pub(crate) fn new(client: Client, path: FolderPath, name: impl AsRef<str>) -> Result<Self> {
Ok(Self {
client,
path: LabPath::new(path, name)?,
})
}
pub(crate) fn from_path(client: Client, path: LabPath) -> Self {
Self { client, path }
}
fn labs(&self) -> LabsClient {
LabsClient::new(self.client.clone(), self.path.folder())
}
pub async fn get(&self) -> Result<Lab> {
self.client
.get(&format!("labs{}", self.path))
.await?
.into_data()
}
pub async fn edit(&self, params: EditLabRequest) -> Result<()> {
self.client
.put::<(), EditLabRequest>(&format!("labs{}", self.path), ¶ms)
.await?;
Ok(())
}
pub async fn rename(self, name: impl AsRef<str>) -> Result<Self> {
let new_path = LabPath::new(self.path.folder(), name)?;
let params = serde_json::json!({"name": new_path.lab_name()});
self.client
.put::<(), serde_json::Value>(&format!("labs{}", self.path), ¶ms)
.await?;
Ok(Self::from_path(self.client.clone(), new_path))
}
pub async fn move_to(self, folder: &FolderClient) -> Result<Self> {
let path = &folder.path;
let params = serde_json::json!({"path": path.as_str()});
self.client
.put::<(), serde_json::Value>(&format!("labs{}/move", self.path), ¶ms)
.await?;
let new_path = LabPath::from_validated(path.clone(), self.path.lab_name());
Ok(Self::from_path(self.client.clone(), new_path))
}
pub async fn delete(self) -> Result<()> {
self.client
.delete::<()>(&format!("labs{}", self.path))
.await?;
Ok(())
}
pub async fn lock(&self) -> Result<()> {
self.client
.put::<(), ()>(&format!("labs{}/Lock", self.path), &())
.await?;
Ok(())
}
pub async fn unlock(&self) -> Result<()> {
self.client
.put::<(), ()>(&format!("labs{}/Unlock", self.path), &())
.await?;
Ok(())
}
pub async fn topology(&self) -> Result<Vec<TopologyEntry>> {
let current_lab = self.labs().current().await?;
let topology = self.topology_inner().await;
if let Some(lab) = current_lab {
if lab.path != self.path {
lab.topology_inner().await?;
}
} else {
self.close_inner().await?;
}
topology
}
async fn topology_inner(&self) -> Result<Vec<TopologyEntry>> {
self.client
.get(&format!("labs{}/topology", self.path))
.await?
.into_data()
}
pub async fn links(&self) -> Result<Links> {
self.client
.get(&format!("labs{}/links", self.path))
.await?
.into_data()
}
pub(crate) async fn open(&self) -> Result<()> {
match self.labs().current().await? {
Some(lab) if lab.path == self.path => return Ok(()),
Some(lab) => {
return Err(Error::Client(format!(
"Cannot open lab '{}' because '{}' is currently open.",
self.path, lab.path
)));
}
None => {}
}
self.topology_inner().await?;
Ok(())
}
pub async fn close(&self) -> Result<()> {
match self.labs().current().await? {
Some(lab) if lab.path == self.path => {}
_ => return Ok(()),
}
self.close_inner().await
}
async fn close_inner(&self) -> Result<()> {
let has_running_nodes = self
.nodes()
.list()
.await?
.iter()
.any(|(_, v)| !matches!(v.status, NodeStatus::Stopped));
if has_running_nodes {
return Err(Error::Client(format!(
"Lab '{}' cannot be closed as it has running nodes.",
self.path
)));
}
self.client.delete::<()>("labs/close").await?;
Ok(())
}
pub fn nodes(&self) -> NodesClient {
NodesClient::new(self.client.clone(), self.path.clone())
}
pub fn node(&self, id: u32) -> NodeClient {
NodeClient::new(self.client.clone(), self.path.clone(), id)
}
pub fn networks(&self) -> NetworksClient {
NetworksClient::new(self.client.clone(), self.path.clone())
}
pub fn network(&self, id: u32) -> NetworkClient {
NetworkClient::new(self.client.clone(), self.path.clone(), id)
}
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct AddLabRequest {
name: String,
path: String,
scripttimeout: u32,
version: u32,
#[serde(skip_serializing_if = "Option::is_none")]
author: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
body: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
}
impl AddLabRequest {
pub fn new(name: impl Into<String>) -> Result<Self> {
let name = name.into();
LabPath::validate(&name)?;
Ok(Self {
name,
scripttimeout: 300,
version: 1,
..Default::default()
})
}
pub fn author(mut self, author: impl Into<String>) -> Self {
self.author = Some(author.into());
self
}
pub fn body(mut self, body: impl Into<String>) -> Self {
self.body = Some(body.into());
self
}
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn version(mut self, version: u32) -> Self {
self.version = version;
self
}
pub fn scripttimeout(mut self, scripttimeout: u32) -> Result<Self> {
if scripttimeout < 300 {
return Err(Error::Client(
"Minimum script timeout is 300 seconds".to_string(),
));
}
self.scripttimeout = scripttimeout;
Ok(self)
}
pub(crate) fn path(mut self, path: impl Into<String>) -> Self {
self.path = path.into();
self
}
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct EditLabRequest {
#[serde(skip_serializing_if = "Option::is_none")]
author: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
body: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
scripttimeout: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
version: Option<u32>,
}
impl EditLabRequest {
pub fn new() -> Self {
Self::default()
}
pub fn author(mut self, author: impl Into<String>) -> Self {
self.author = Some(author.into());
self
}
pub fn clear_author(mut self) -> Self {
self.author = Some(String::new());
self
}
pub fn body(mut self, body: impl Into<String>) -> Self {
self.body = Some(body.into());
self
}
pub fn clear_body(mut self) -> Self {
self.body = Some(String::new());
self
}
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn clear_description(mut self) -> Self {
self.description = Some(String::new());
self
}
pub fn scripttimeout(mut self, scripttimeout: u32) -> Result<Self> {
if scripttimeout < 300 {
return Err(Error::Client(
"Minimum script timeout is 300 seconds".to_string(),
));
}
self.scripttimeout = Some(scripttimeout);
Ok(self)
}
pub fn version(mut self, version: u32) -> Self {
self.version = Some(version);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
fn root() -> FolderPath {
FolderPath::new("/").unwrap()
}
fn test_folder() -> FolderPath {
FolderPath::new("/Test Folder").unwrap()
}
fn new_folder() -> FolderPath {
FolderPath::new("/New Folder").unwrap()
}
#[test]
fn valid_lab_path() {
let path = LabPath::new(test_folder(), "Test").unwrap();
assert_eq!(path.as_str(), "/Test Folder/Test.unl");
assert_eq!(path.folder().as_str(), "/Test Folder");
assert_eq!(path.lab_file(), "Test.unl");
}
#[test]
fn invalid_lab_path() {
let path = LabPath::new(test_folder(), "Lab: Test");
assert!(path.is_err());
}
#[test]
fn lab_rename() {
let path = LabPath::new(root(), "Test").unwrap();
let new_path = LabPath::new(path.folder(), "Test1").unwrap();
assert_eq!(new_path.as_str(), "/Test1.unl");
assert_eq!(new_path.folder().as_str(), "/");
assert_eq!(new_path.lab_file(), "Test1.unl");
assert_eq!(new_path.lab_name(), "Test1");
}
#[test]
fn lab_move() {
let path = LabPath::new(root(), "Test").unwrap();
let new_path = LabPath::new(new_folder(), path.lab_name()).unwrap();
assert_eq!(new_path.as_str(), "/New Folder/Test.unl");
assert_eq!(new_path.folder().as_str(), "/New Folder");
assert_eq!(new_path.lab_file(), "Test.unl");
}
#[test]
fn add_lab_defaults() {
let req = AddLabRequest::new("Test").unwrap();
assert_eq!(req.author, None);
assert_eq!(req.body, None);
assert_eq!(req.description, None);
assert_eq!(req.name, "Test".to_string());
assert_eq!(req.path, String::new());
assert_eq!(req.scripttimeout, 300);
assert_eq!(req.version, 1);
}
#[test]
fn add_lab_invalid() {
let req = AddLabRequest::new("Test").unwrap().scripttimeout(0);
assert!(req.is_err());
}
#[test]
fn add_lab_setters() {
let req = AddLabRequest::new("Test")
.unwrap()
.author("Test User")
.body("This lab is created for test purposes.")
.description("A test lab")
.scripttimeout(600)
.unwrap()
.version(2);
assert_eq!(req.scripttimeout, 600);
assert_eq!(req.version, 2);
assert_eq!(req.author, Some("Test User".to_string()));
assert_eq!(
req.body,
Some("This lab is created for test purposes.".to_string())
);
assert_eq!(req.description, Some("A test lab".to_string()));
}
#[test]
fn edit_lab_defaults() {
let req = EditLabRequest::new();
assert_eq!(req.scripttimeout, None);
assert_eq!(req.version, None);
assert_eq!(req.author, None);
assert_eq!(req.body, None);
assert_eq!(req.description, None);
}
#[test]
fn edit_lab_invalid() {
let req = EditLabRequest::new().scripttimeout(0);
assert!(req.is_err());
}
#[test]
fn edit_lab_setters() {
let req = EditLabRequest::new()
.author("Test User")
.body("This lab is created for test purposes.")
.description("A test lab")
.scripttimeout(600)
.unwrap()
.version(2);
assert_eq!(req.scripttimeout, Some(600));
assert_eq!(req.version, Some(2));
assert_eq!(req.author, Some("Test User".to_string()));
assert_eq!(
req.body,
Some("This lab is created for test purposes.".to_string())
);
assert_eq!(req.description, Some("A test lab".to_string()));
}
}