netconf/message/rpc/operation/
mod.rs1use std::{
2 fmt::{self, Debug, Display},
3 io::Write,
4 ops::Deref,
5 sync::Arc,
6 time::Duration,
7};
8
9use iri_string::types::UriStr;
10use quick_xml::{
11 events::{BytesStart, BytesText},
12 NsReader, Writer,
13};
14use uuid::Uuid;
15
16use crate::{
17 capabilities::{Capability, Requirements},
18 message::{ReadError, ReadXml, WriteError, WriteXml},
19 session::Context,
20 Error,
21};
22
23use super::{DataReply, EmptyReply, IntoResult};
24
25pub trait Operation: Debug + WriteXml + Send + Sync + Sized {
26 const NAME: &'static str;
27 const REQUIRED_CAPABILITIES: Requirements;
28
29 type Builder<'a>: Builder<'a, Self>;
30 type Reply: Debug + ReadXml + IntoResult;
31
32 fn new<'a, F>(ctx: &'a Context, build_fn: F) -> Result<Self, Error>
33 where
34 F: FnOnce(Self::Builder<'a>) -> Result<Self, Error>,
35 {
36 Self::REQUIRED_CAPABILITIES
37 .check(ctx.server_capabilities())
38 .then(|| Self::Builder::new(ctx).build(build_fn))
39 .ok_or(Error::UnsupportedOperation {
40 operation_name: Self::NAME,
41 required_capabilities: Self::REQUIRED_CAPABILITIES,
42 })?
43 }
44}
45
46pub trait Builder<'a, O: Operation>: Debug + Sized {
47 fn new(ctx: &'a Context) -> Self;
48
49 fn finish(self) -> Result<O, Error>;
50
51 fn build<F>(self, build_fn: F) -> Result<O, Error>
52 where
53 F: FnOnce(Self) -> Result<O, Error>,
54 {
55 build_fn(self)
56 }
57}
58
59mod params;
60
61pub mod get;
62#[doc(inline)]
63pub use self::get::Get;
64
65pub mod get_config;
66#[doc(inline)]
67pub use self::get_config::GetConfig;
68
69pub mod edit_config;
70#[doc(inline)]
71pub use self::edit_config::EditConfig;
72
73pub mod copy_config;
74#[doc(inline)]
75pub use self::copy_config::CopyConfig;
76
77pub mod delete_config;
78#[doc(inline)]
79pub use self::delete_config::DeleteConfig;
80
81pub mod lock;
82#[doc(inline)]
83pub use self::lock::{Lock, Unlock};
84
85pub mod kill_session;
86#[doc(inline)]
87pub use self::kill_session::KillSession;
88
89pub mod commit;
90#[doc(inline)]
91pub use self::commit::Commit;
92
93pub mod cancel_commit;
94#[doc(inline)]
95pub use self::cancel_commit::CancelCommit;
96
97pub mod discard_changes;
98#[doc(inline)]
99pub use self::discard_changes::DiscardChanges;
100
101pub mod validate;
102#[doc(inline)]
103pub use self::validate::Validate;
104
105pub(crate) mod close_session;
106pub(crate) use self::close_session::CloseSession;
107
108#[cfg(feature = "junos")]
109pub mod junos;
110
111#[derive(Debug, Default, Copy, Clone)]
112pub enum Datastore {
113 #[default]
114 Running,
115 Candidate,
116 Startup,
117}
118
119impl Datastore {
120 fn try_as_source(self, ctx: &Context) -> Result<Self, Error> {
121 let required_capabilities = match self {
122 Self::Running => Requirements::None,
123 Self::Candidate => Requirements::One(Capability::Candidate),
124 Self::Startup => Requirements::One(Capability::Startup),
125 };
126 if required_capabilities.check(ctx.server_capabilities()) {
127 Ok(self)
128 } else {
129 Err(Error::UnsupportedSource {
130 datastore: self,
131 required_capabilities,
132 })
133 }
134 }
135
136 fn try_as_target(self, ctx: &Context) -> Result<Self, Error> {
137 let required_capabilities = match self {
138 Self::Running => Requirements::One(Capability::WritableRunning),
139 Self::Candidate => Requirements::One(Capability::Candidate),
140 Self::Startup => Requirements::One(Capability::Startup),
141 };
142 if required_capabilities.check(ctx.server_capabilities()) {
143 Ok(self)
144 } else {
145 Err(Error::UnsupportedTarget {
146 datastore: self,
147 required_capabilities,
148 })
149 }
150 }
151
152 fn try_as_lock_target(self, ctx: &Context) -> Result<Self, Error> {
153 let required_capabilities = match self {
154 Self::Running => Requirements::None,
155 Self::Candidate => Requirements::One(Capability::Candidate),
156 Self::Startup => Requirements::One(Capability::Startup),
157 };
158 if required_capabilities.check(ctx.server_capabilities()) {
159 Ok(self)
160 } else {
161 Err(Error::UnsupportedLockTarget {
162 datastore: self,
163 required_capabilities,
164 })
165 }
166 }
167
168 const fn as_str(self) -> &'static str {
169 match self {
170 Self::Running => "running",
171 Self::Candidate => "candidate",
172 Self::Startup => "startup",
173 }
174 }
175}
176
177impl WriteXml for Datastore {
178 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<(), WriteError> {
179 _ = writer.create_element(self.as_str()).write_empty()?;
180 Ok(())
181 }
182}
183
184#[derive(Debug, Clone)]
185pub enum Source {
186 Datastore(Datastore),
187 Config(String),
188 Url(Url),
189}
190
191impl WriteXml for Source {
192 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<(), WriteError> {
193 match self {
194 Self::Datastore(datastore) => datastore.write_xml(writer)?,
195 Self::Config(config) => {
196 _ = writer
197 .create_element("config")
198 .write_inner_content(|writer| {
199 writer
200 .get_mut()
201 .write_all(config.as_bytes())
202 .map_err(|err| WriteError::Other(err.into()))
203 })?;
204 }
205 Self::Url(url) => url.write_xml(writer)?,
206 };
207 Ok(())
208 }
209}
210
211#[derive(Debug, Clone)]
212pub enum Filter {
213 Subtree(String),
214 XPath(String),
215}
216
217impl Filter {
218 const fn as_str(&self) -> &'static str {
219 match self {
220 Self::Subtree(_) => "subtree",
221 Self::XPath(_) => "xpath",
222 }
223 }
224
225 fn try_use(self, ctx: &Context) -> Result<Self, Error> {
226 let required_capabilities = match self {
227 Self::Subtree(_) => Requirements::None,
228 Self::XPath(_) => Requirements::One(Capability::XPath),
229 };
230 if required_capabilities.check(ctx.server_capabilities()) {
231 Ok(self)
232 } else {
233 Err(Error::UnsupportedFilterType {
234 filter: self.as_str(),
235 required_capabilities,
236 })
237 }
238 }
239}
240
241impl WriteXml for Filter {
242 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<(), WriteError> {
243 let elem = writer
244 .create_element("filter")
245 .with_attribute(("type", self.as_str()));
246 _ = match self {
247 Self::Subtree(filter) => elem.write_inner_content(|writer| {
248 writer
249 .get_mut()
250 .write_all(filter.as_bytes())
251 .map_err(|err| WriteError::Other(err.into()))
252 })?,
253 Self::XPath(select) => elem
254 .with_attribute(("select", select.as_str()))
255 .write_empty()?,
256 };
257 Ok(())
258 }
259}
260
261#[derive(Debug, Clone, PartialEq, Eq)]
262pub struct Opaque {
263 inner: Arc<str>,
264}
265
266impl<A: AsRef<str>> From<A> for Opaque {
267 fn from(value: A) -> Self {
268 let inner = value.as_ref().into();
269 Self { inner }
270 }
271}
272
273impl ReadXml for Opaque {
274 #[tracing::instrument(skip_all, fields(tag = ?start.local_name()), level = "debug")]
275 fn read_xml(reader: &mut NsReader<&[u8]>, start: &BytesStart<'_>) -> Result<Self, ReadError> {
276 let end = start.to_end();
277 let inner = reader.read_text(end.name())?.into();
278 Ok(Self { inner })
279 }
280}
281
282impl WriteXml for Opaque {
283 #[tracing::instrument(skip(writer))]
284 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<(), WriteError> {
285 writer
286 .get_mut()
287 .write_all(self.as_bytes())
288 .map_err(|err| WriteError::Other(err.into()))
289 }
290}
291
292impl Display for Opaque {
293 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
294 Display::fmt(&self.inner, f)
295 }
296}
297
298impl Deref for Opaque {
299 type Target = str;
300
301 fn deref(&self) -> &Self::Target {
302 &self.inner
303 }
304}
305#[derive(Debug, Clone)]
312pub struct Url {
313 inner: Arc<UriStr>,
314}
315
316impl Url {
317 fn try_new<S: AsRef<str>>(s: S, ctx: &Context) -> Result<Self, Error> {
318 let url = UriStr::new(s.as_ref())?;
319 ctx.server_capabilities()
320 .iter()
321 .filter_map(|capability| {
322 if let Capability::Url(schemes) = capability {
323 Some(schemes.iter())
324 } else {
325 None
326 }
327 })
328 .flatten()
329 .find(|&scheme| url.scheme_str() == scheme.as_ref())
330 .ok_or_else(|| Error::UnsupportedUrlScheme { url: url.into() })
331 .map(|_| Self { inner: url.into() })
332 }
333}
334
335impl AsRef<str> for Url {
336 fn as_ref(&self) -> &str {
337 self.inner.as_str()
338 }
339}
340
341impl Display for Url {
342 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
343 Display::fmt(&self.inner.as_ref(), f)
344 }
345}
346
347impl WriteXml for Url {
348 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<(), WriteError> {
349 _ = writer
350 .create_element("url")
351 .write_text_content(BytesText::new(self.inner.as_str()))?;
352 Ok(())
353 }
354}
355
356#[derive(Debug, Clone, PartialEq, Eq)]
357pub struct Token {
358 inner: Arc<str>,
359}
360
361impl Token {
362 pub fn new<S: AsRef<str>>(token: S) -> Self {
363 let inner = token.as_ref().into();
364 Self { inner }
365 }
366
367 #[must_use]
368 pub fn generate() -> Self {
369 let inner = Arc::from(
370 &*Uuid::new_v4()
371 .urn()
372 .encode_lower(&mut Uuid::encode_buffer()),
373 );
374 Self { inner }
375 }
376}
377
378impl Display for Token {
379 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
380 Display::fmt(&self.inner, f)
381 }
382}
383
384#[derive(Debug, Clone, Copy, PartialEq, Eq)]
385struct Timeout(Duration);
386
387impl Timeout {
388 fn seconds(&self) -> BytesText<'static> {
389 BytesText::new(&self.0.as_secs().to_string()).into_owned()
390 }
391
392 #[cfg(feature = "junos")]
393 fn minutes(&self) -> BytesText<'static> {
394 BytesText::new(&self.0.as_secs().div_ceil(60).to_string()).into_owned()
395 }
396}
397
398impl Default for Timeout {
399 fn default() -> Self {
400 Self(Duration::from_secs(600))
401 }
402}
403
404#[cfg(test)]
405mod tests {
406 use super::*;
407
408 use quick_xml::events::Event;
409
410 #[test]
411 fn reply_from_xml() {
412 let reply = "<configuration><top/></configuration>";
413 let expect = Opaque {
414 inner: reply.into(),
415 };
416 let msg = format!("<data>{reply}</data>");
417 let mut reader = NsReader::from_str(msg.as_str());
418 _ = reader.trim_text(true);
419 if let Event::Start(start) = reader.read_event().unwrap() {
420 assert_eq!(Opaque::read_xml(&mut reader, &start).unwrap(), expect);
421 } else {
422 panic!("missing <data> tag")
423 }
424 }
425}