Skip to content

Commit

Permalink
implement :url capability
Browse files Browse the repository at this point in the history
  • Loading branch information
benmaddison committed Dec 22, 2023
1 parent e62d8de commit 8d38af1
Show file tree
Hide file tree
Showing 8 changed files with 292 additions and 185 deletions.
183 changes: 92 additions & 91 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions netconf/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ tokio.workspace = true
thiserror.workspace = true
async-trait = "^0.1"
bytes = "^1.0"
iri-string = "^0.7"
memchr = "^2.0"
quick-xml = "^0.31"
russh = "^0.39"
Expand Down
95 changes: 69 additions & 26 deletions netconf/src/capabilities.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
use std::{
borrow::Borrow,
borrow::Cow,
collections::{BTreeSet, HashSet},
io::Write,
str::FromStr,
sync::Arc,
};

use iri_string::types::UriStr;
use quick_xml::{
events::{BytesStart, BytesText, Event},
name::ResolveResult,
Expand Down Expand Up @@ -61,7 +64,7 @@ impl ReadXml for Capabilities {
if ns == xmlns::BASE && tag.local_name().as_ref() == b"capability" =>
{
let span = reader.read_text(tag.to_end().name())?;
_ = inner.insert(Capability::from_uri(span.borrow())?);
_ = inner.insert(span.parse()?);
}
(_, Event::End(tag)) if tag == end => break,
(ns, event) => {
Expand Down Expand Up @@ -112,6 +115,7 @@ mod uri {
"urn:ietf:params:netconf:capability:rollback-on-error:1.0";
pub(super) const VALIDATE_V1_0: &str = "urn:ietf:params:netconf:capability:validate:1.0";
pub(super) const STARTUP_V1_0: &str = "urn:ietf:params:netconf:capability:startup:1.0";
pub(super) const URL_V1_0: &str = "urn:ietf:params:netconf:capability:url:1.0";
}

#[allow(variant_size_differences)]
Expand All @@ -124,36 +128,75 @@ pub enum Capability {
RollbackOnError,
Validate,
Startup,
Unknown(String),
Url(Vec<Box<str>>),
Unknown(Arc<UriStr>),
}

impl Capability {
#[tracing::instrument(level = "debug")]
pub fn from_uri(uri: &str) -> Result<Self, Error> {
match uri {
uri::BASE_V1_0 => Ok(Self::Base(Base::V1_0)),
uri::BASE_V1_1 => Ok(Self::Base(Base::V1_1)),
uri::WRITABLE_RUNNING_V1_0 => Ok(Self::WritableRunning),
uri::CANDIDATE_V1_0 => Ok(Self::Candidate),
uri::CONFIRMED_COMMIT_V1_0 => Ok(Self::ConfirmedCommit),
uri::ROLLBACK_ON_ERROR_V1_0 => Ok(Self::RollbackOnError),
uri::VALIDATE_V1_0 => Ok(Self::Validate),
uri::STARTUP_V1_0 => Ok(Self::Startup),
_ => Ok(Self::Unknown(uri.to_string())),
impl FromStr for Capability {
type Err = Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let uri = UriStr::new(s)?;
match (
uri.scheme_str(),
uri.authority_str(),
uri.path_str(),
uri.query_str(),
uri.fragment(),
) {
("urn", None, "ietf:params:netconf:base:1.0", None, None) => Ok(Self::Base(Base::V1_0)),
("urn", None, "ietf:params:netconf:base:1.1", None, None) => Ok(Self::Base(Base::V1_1)),
("urn", None, "ietf:params:netconf:capability:writable-running:1.0", None, None) => {
Ok(Self::WritableRunning)
}
("urn", None, "ietf:params:netconf:capability:candidate:1.0", None, None) => {
Ok(Self::Candidate)
}
("urn", None, "ietf:params:netconf:capability:confirmed-commit:1.0", None, None) => {
Ok(Self::ConfirmedCommit)
}
("urn", None, "ietf:params:netconf:capability:rollback-on-error:1.0", None, None) => {
Ok(Self::RollbackOnError)
}
("urn", None, "ietf:params:netconf:capability:validate:1.0", None, None) => {
Ok(Self::Validate)
}
("urn", None, "ietf:params:netconf:capability:startup:1.0", None, None) => {
Ok(Self::Startup)
}
("urn", None, "ietf:params:netconf:capability:url:1.0", Some(query), None) => {
let schemes = query
.split('&')
.filter_map(|pair| match pair.split_once('=') {
Some(("scheme", values)) => Some(values.split(',')),
_ => None,
})
.flatten()
.map(Box::from)
.collect();
Ok(Self::Url(schemes))
}
_ => Ok(Self::Unknown(uri.into())),
}
}
}

impl Capability {
#[must_use]
pub fn uri(&self) -> &str {
pub fn uri(&self) -> Cow<'_, str> {
match self {
Self::Base(base) => base.uri(),
Self::WritableRunning => uri::WRITABLE_RUNNING_V1_0,
Self::Candidate => uri::CANDIDATE_V1_0,
Self::ConfirmedCommit => uri::CONFIRMED_COMMIT_V1_0,
Self::RollbackOnError => uri::ROLLBACK_ON_ERROR_V1_0,
Self::Validate => uri::VALIDATE_V1_0,
Self::Startup => uri::STARTUP_V1_0,
Self::Unknown(uri) => uri.as_str(),
Self::Base(base) => Cow::Borrowed(base.uri()),
Self::WritableRunning => Cow::Borrowed(uri::WRITABLE_RUNNING_V1_0),
Self::Candidate => Cow::Borrowed(uri::CANDIDATE_V1_0),
Self::ConfirmedCommit => Cow::Borrowed(uri::CONFIRMED_COMMIT_V1_0),
Self::RollbackOnError => Cow::Borrowed(uri::ROLLBACK_ON_ERROR_V1_0),
Self::Validate => Cow::Borrowed(uri::VALIDATE_V1_0),
Self::Startup => Cow::Borrowed(uri::STARTUP_V1_0),
// TODO
Self::Url(schemes) => {
Cow::Owned(format!("{}?scheme={}", uri::URL_V1_0, schemes.join(",")))
}
Self::Unknown(uri) => Cow::Borrowed(uri.as_str()),
}
}
}
Expand All @@ -164,7 +207,7 @@ impl WriteXml for Capability {
fn write_xml<W: Write>(&self, writer: &mut W) -> Result<(), Self::Error> {
_ = Writer::new(writer)
.create_element("capability")
.write_text_content(BytesText::new(self.uri()))?;
.write_text_content(BytesText::new(&self.uri()))?;
Ok(())
}
}
Expand Down
7 changes: 7 additions & 0 deletions netconf/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use bytes::Bytes;
use iri_string::types::UriStr;
use tokio::sync::mpsc;

use crate::{
Expand Down Expand Up @@ -60,6 +61,9 @@ pub enum Error {
#[error("failed to parse message-id")]
MessageIdParse(#[from] std::num::ParseIntError),

#[error("failed to parse capability URI")]
ParseCapability(#[from] iri_string::validate::Error),

#[error("missing common mandatory capabilities")]
BaseCapability,

Expand Down Expand Up @@ -125,4 +129,7 @@ pub enum Error {

#[error("unsupported lock target datastore '{0:?}' (requires capability '{1:?}')")]
UnsupportedLockTarget(Datastore, Capability),

#[error("unsupported scheme in url '{0}' (requires ':url:1.0' capability with corresponding 'scheme' parameter)")]
UnsupportedUrlScheme(Box<UriStr>),
}
99 changes: 44 additions & 55 deletions netconf/src/message/hello.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ impl ClientMsg for ClientHello {}

#[cfg(test)]
mod tests {
use iri_string::types::UriStr;

use crate::capabilities::Base;

use super::*;

#[test]
Expand All @@ -156,32 +160,24 @@ mod tests {
"#;
let expect = ServerHello {
capabilities: [
Capability::from_uri("urn:ietf:params:netconf:base:1.0").unwrap(),
Capability::from_uri("urn:ietf:params:netconf:capability:candidate:1.0").unwrap(),
Capability::from_uri("urn:ietf:params:netconf:capability:confirmed-commit:1.0")
.unwrap(),
Capability::from_uri("urn:ietf:params:netconf:capability:validate:1.0").unwrap(),
Capability::from_uri(
"urn:ietf:params:netconf:capability:url:1.0?scheme=http,ftp,file",
)
.unwrap(),
Capability::from_uri("urn:ietf:params:xml:ns:netconf:base:1.0").unwrap(),
Capability::from_uri("urn:ietf:params:xml:ns:netconf:capability:candidate:1.0")
.unwrap(),
Capability::from_uri(
"urn:ietf:params:xml:ns:netconf:capability:confirmed-commit:1.0",
)
.unwrap(),
Capability::from_uri("urn:ietf:params:xml:ns:netconf:capability:validate:1.0")
.unwrap(),
Capability::from_uri(
"urn:ietf:params:xml:ns:netconf:capability:url:1.0?scheme=http,ftp,file",
)
.unwrap(),
Capability::from_uri("urn:ietf:params:xml:ns:yang:ietf-netconf-monitoring")
.unwrap(),
Capability::from_uri("http://xml.juniper.net/netconf/junos/1.0").unwrap(),
Capability::from_uri("http://xml.juniper.net/dmi/system/1.0").unwrap(),
Capability::Base(Base::V1_0),
Capability::Candidate,
Capability::ConfirmedCommit,
Capability::Validate,
Capability::Url(vec!["http".into(), "ftp".into(), "file".into()]),
Capability::ConfirmedCommit,
Capability::Unknown(UriStr::new("urn:ietf:params:xml:ns:netconf:base:1.0").unwrap().into()),
Capability::Unknown(UriStr::new("urn:ietf:params:xml:ns:netconf:capability:candidate:1.0").unwrap().into()),
Capability::Unknown(
UriStr::new("urn:ietf:params:xml:ns:netconf:capability:confirmed-commit:1.0").unwrap().into(),
),
Capability::Unknown(UriStr::new("urn:ietf:params:xml:ns:netconf:capability:validate:1.0").unwrap().into()),
Capability::Unknown(
UriStr::new("urn:ietf:params:xml:ns:netconf:capability:url:1.0?scheme=http,ftp,file").unwrap().into(),
),
Capability::Unknown(UriStr::new("urn:ietf:params:xml:ns:yang:ietf-netconf-monitoring").unwrap().into()),
Capability::Unknown(UriStr::new("http://xml.juniper.net/netconf/junos/1.0").unwrap().into()),
Capability::Unknown(UriStr::new("http://xml.juniper.net/dmi/system/1.0").unwrap().into()),
]
.into_iter()
.collect(),
Expand Down Expand Up @@ -219,34 +215,27 @@ mod tests {
let expect = ServerHello {
capabilities:
[
Capability::from_uri("urn:ietf:params:netconf:base:1.0").unwrap(),
Capability::from_uri("urn:ietf:params:netconf:capability:candidate:1.0").unwrap(),
Capability::from_uri(
"urn:ietf:params:netconf:capability:confirmed-commit:1.0",
).unwrap(),
Capability::from_uri("urn:ietf:params:netconf:capability:validate:1.0").unwrap(),
Capability::from_uri(
"urn:ietf:params:netconf:capability:url:1.0?scheme=http,ftp,file",
).unwrap(),
Capability::from_uri("urn:ietf:params:xml:ns:netconf:base:1.0?module=ietf-netconf&amp;revision=2011-06-01").unwrap(),
Capability::from_uri(
"urn:ietf:params:xml:ns:netconf:capability:candidate:1.0",
).unwrap(),
Capability::from_uri(
"urn:ietf:params:xml:ns:netconf:capability:confirmed-commit:1.0",
).unwrap(),
Capability::from_uri(
"urn:ietf:params:xml:ns:netconf:capability:validate:1.0",
).unwrap(),
Capability::from_uri(
"urn:ietf:params:xml:ns:netconf:capability:url:1.0?scheme=http,ftp,file",
).unwrap(),
Capability::from_uri("urn:ietf:params:xml:ns:yang:ietf-inet-types?module=ietf-inet-types&amp;revision=2013-07-15").unwrap(),
Capability::from_uri("urn:ietf:params:xml:ns:yang:ietf-yang-metadata?module=ietf-yang-metadata&amp;revision=2016-08-05").unwrap(),
Capability::from_uri("urn:ietf:params:xml:ns:yang:ietf-netconf-monitoring").unwrap(),
Capability::from_uri("http://xml.juniper.net/netconf/junos/1.0").unwrap(),
Capability::from_uri("http://xml.juniper.net/dmi/system/1.0").unwrap(),
Capability::from_uri("http://yang.juniper.net/junos/jcmd?module=junos-configuration-metadata&amp;revision=2021-09-01").unwrap()
Capability::Base(Base::V1_0),
Capability::Candidate,
Capability::ConfirmedCommit,
Capability::Validate,
Capability::Url(vec!["http".into(), "ftp".into(), "file".into()]),
Capability::ConfirmedCommit,
Capability::Unknown(UriStr::new("urn:ietf:params:xml:ns:netconf:base:1.0?module=ietf-netconf&amp;revision=2011-06-01").unwrap().into()),
Capability::Unknown(UriStr::new("urn:ietf:params:xml:ns:netconf:capability:candidate:1.0").unwrap().into()),
Capability::Unknown(
UriStr::new("urn:ietf:params:xml:ns:netconf:capability:confirmed-commit:1.0").unwrap().into(),
),
Capability::Unknown(UriStr::new("urn:ietf:params:xml:ns:netconf:capability:validate:1.0").unwrap().into()),
Capability::Unknown(
UriStr::new("urn:ietf:params:xml:ns:netconf:capability:url:1.0?scheme=http,ftp,file").unwrap().into(),
),
Capability::Unknown(UriStr::new("urn:ietf:params:xml:ns:yang:ietf-inet-types?module=ietf-inet-types&amp;revision=2013-07-15").unwrap().into()),
Capability::Unknown(UriStr::new("urn:ietf:params:xml:ns:yang:ietf-yang-metadata?module=ietf-yang-metadata&amp;revision=2016-08-05").unwrap().into()),
Capability::Unknown(UriStr::new("urn:ietf:params:xml:ns:yang:ietf-netconf-monitoring").unwrap().into()),
Capability::Unknown(UriStr::new("http://xml.juniper.net/netconf/junos/1.0").unwrap().into()),
Capability::Unknown(UriStr::new("http://xml.juniper.net/dmi/system/1.0").unwrap().into()),
Capability::Unknown(UriStr::new("http://yang.juniper.net/junos/jcmd?module=junos-configuration-metadata&amp;revision=2021-09-01").unwrap().into()),
]
.into_iter().collect(),
session_id: SessionId::new(43129).unwrap(),
Expand All @@ -258,7 +247,7 @@ mod tests {
fn client_hello_to_xml() {
let req = ClientHello {
capabilities:
std::iter::once(Capability::from_uri("urn:ietf:params:netconf:base:1.0").unwrap()).collect(),
std::iter::once(Capability::Base(Base::V1_0)).collect(),
};
let expect = "<hello><capabilities><capability>urn:ietf:params:netconf:base:1.0</capability></capabilities></hello>]]>]]>";
assert_eq!(req.to_xml().unwrap(), expect);
Expand Down
11 changes: 10 additions & 1 deletion netconf/src/message/rpc/operation/delete_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use quick_xml::Writer;

use crate::{message::rpc::Empty, session::Context, Error};

use super::{Datastore, Operation, WriteXml};
use super::{Datastore, Operation, Url, WriteXml};

#[derive(Debug, Clone)]
pub struct DeleteConfig {
Expand Down Expand Up @@ -49,6 +49,13 @@ impl Builder<'_> {
self
})
}

pub fn url<S: AsRef<str>>(mut self, url: S) -> Result<Self, Error> {
Url::try_new(url, self.ctx).map(|url| {
self.target = Some(Target::Url(url));
self
})
}
}

impl<'a> super::Builder<'a, DeleteConfig> for Builder<'a> {
Expand All @@ -67,6 +74,7 @@ impl<'a> super::Builder<'a, DeleteConfig> for Builder<'a> {
#[derive(Debug, Clone)]
enum Target {
Datastore(Datastore),
Url(Url),
}

impl WriteXml for Target {
Expand All @@ -75,6 +83,7 @@ impl WriteXml for Target {
fn write_xml<W: Write>(&self, writer: &mut W) -> Result<(), Self::Error> {
match self {
Self::Datastore(datastore) => datastore.write_xml(writer),
Self::Url(url) => url.write_xml(writer),
}
}
}
Expand Down
28 changes: 17 additions & 11 deletions netconf/src/message/rpc/operation/edit_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use quick_xml::Writer;

use crate::{capabilities::Capability, message::rpc::Empty, session::Context, Error};

use super::{Datastore, Operation, WriteXml};
use super::{Datastore, Operation, Url, WriteXml};

#[derive(Debug, Clone)]
pub struct EditConfig {
Expand Down Expand Up @@ -85,6 +85,13 @@ impl Builder<'_> {
self
}

pub fn url<S: AsRef<str>>(mut self, url: S) -> Result<Self, Error> {
Url::try_new(url, self.ctx).map(|url| {
self.source = Some(Source::Url(url));
self
})
}

pub const fn default_operation(mut self, default_operation: DefaultOperation) -> Self {
self.default_operation = default_operation;
self
Expand Down Expand Up @@ -137,26 +144,25 @@ impl<'a> super::Builder<'a, EditConfig> for Builder<'a> {
#[derive(Debug, Clone)]
pub enum Source {
Config(String),
Url(Url),
}

impl WriteXml for Source {
type Error = Error;

fn write_xml<W: Write>(&self, writer: &mut W) -> Result<(), Self::Error> {
let mut writer = Writer::new(writer);
_ = match self {
match self {
Self::Config(config) => {
writer
_ = Writer::new(writer)
.create_element("config")
.write_inner_content(|writer| {
writer
.get_mut()
.write_all(config.as_bytes())
.map_err(|err| Error::RpcRequestSerialization(err.into()))
})?
write!(writer.get_mut(), "{config}")?;
Ok::<_, Error>(())
})?;
Ok(())
}
};
Ok(())
Self::Url(url) => url.write_xml(writer),
}
}
}

Expand Down
Loading

0 comments on commit 8d38af1

Please sign in to comment.