Skip to content

Commit

Permalink
Merge pull request #6 from Lachstec/dev
Browse files Browse the repository at this point in the history
merge dev into main
  • Loading branch information
Lachstec authored Feb 14, 2024
2 parents e03053e + b725443 commit 43de549
Show file tree
Hide file tree
Showing 9 changed files with 174 additions and 95 deletions.
50 changes: 36 additions & 14 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
@@ -1,24 +1,46 @@
name: Rust
name: CI

on:
push:
branches: [ "main" ]
branches: [ "main", "dev" ]
pull_request:
branches: [ "main" ]

env:
CARGO_TERM_COLOR: always
branches: [ "main", "dev" ]

jobs:
build:
test:

name: Test `cargo check/test/build` on ${{ matrix.os }}
runs-on: ubuntu-latest

env:
CARGO_TERM_COLOR: always

steps:
- uses: actions/checkout@v3
- name: Install Protoc
uses: arduino/setup-protoc@v2
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
- name: Checkout
uses: actions/checkout@v3

- name: Install Rust
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
components: rustfmt, clippy

- name: Setup Cargo Cache
uses: actions/cache@v3
continue-on-error: false
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-

- name: Setup Protobuf Compiler
uses: arduino/setup-protoc@v2

- name: Run Unit Tests
run: cargo test
6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description = """
An asynchrounous gNMI client to interact with and manage network devices.
"""
name = "ginmi"
version = "0.1.0"
version = "0.1.1"
edition = "2021"
keywords = ["grpc", "async", "gnmi", "network-automation"]
license = "MIT OR Apache-2.0"
Expand All @@ -22,6 +22,10 @@ tokio = { version = "1.35.1", features = ["rt-multi-thread", "macros"] }
prost = "0.12.3"
tonic = { version = "0.11.0", features = ["transport", "tls", "tls-roots"] }
thiserror = "1.0.56"
tower-service = "0.3.2"
# Needs to match tonics version of http, else implementations of the Service trait break.
http = "0.2.0"
tower = "0.4.13"

[build-dependencies]
tonic-build = "0.11.0"
17 changes: 10 additions & 7 deletions build.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
fn main() -> Result<(), Box<dyn std::error::Error>> {
fn main() {
let proto_dir = "proto";
println!("cargo:rerun-if-changed={}", proto_dir);

tonic_build::configure()
.build_server(false)
.compile_well_known_types(true)
.compile(
&["proto/gnmi/gnmi.proto",
&[
"proto/gnmi/gnmi.proto",
"proto/gnmi_ext/gnmi_ext.proto",
"proto/target/target.proto",
"proto/collector/collector.proto",
"proto/google.proto"
"proto/google.proto",
],
&["proto"]
)?;
Ok(())
}
&[proto_dir],
).expect("Failed to compile protobuf files");
}
64 changes: 64 additions & 0 deletions src/auth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
use http::{HeaderValue, Request};
use std::error::Error;
use std::sync::Arc;
use std::task::{Context, Poll};
use tonic::codegen::Body;
use tower_service::Service;

/// Service that injects username and password into the request metadata
#[derive(Debug, Clone)]
pub struct AuthService<S> {
inner: S,
username: Option<Arc<HeaderValue>>,
password: Option<Arc<HeaderValue>>,
}

impl<S> AuthService<S> {
#[inline]
pub fn new(
inner: S,
username: Option<Arc<HeaderValue>>,
password: Option<Arc<HeaderValue>>,
) -> Self {
Self {
inner,
username,
password,
}
}
}

/// Implementation of Service so that it plays nicely with tonic.
/// Trait bounds have to match those specified on [`tonic::client::GrpcService`]
impl<S, ReqBody, ResBody> Service<Request<ReqBody>> for AuthService<S>
where
S: Service<Request<ReqBody>, Response = ResBody>,
S::Error:,
ResBody: Body,
<ResBody as Body>::Error: Into<Box<dyn Error + Send + Sync>>,
{
type Response = S::Response;
type Error = S::Error;
type Future = S::Future;

#[inline]
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}

#[inline]
fn call(&mut self, mut request: Request<ReqBody>) -> Self::Future {
if let Some(user) = &self.username {
if let Some(pass) = &self.password {
request
.headers_mut()
.insert("username", user.as_ref().clone());
request
.headers_mut()
.insert("password", pass.as_ref().clone());
}
}

self.inner.call(request)
}
}
78 changes: 52 additions & 26 deletions src/client/client.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
use std::str::FromStr;
use tonic::transport::{Uri, ClientTlsConfig, Certificate, Channel};
use crate::auth::AuthService;
use crate::error::GinmiError;
use crate::gen::gnmi::{CapabilityRequest, CapabilityResponse};
use crate::gen::gnmi::g_nmi_client::GNmiClient;

type ClientConn = GNmiClient<Channel>;
use crate::gen::gnmi::{CapabilityRequest, CapabilityResponse};
use http::HeaderValue;
use std::str::FromStr;
use std::sync::Arc;
use tonic::transport::{Certificate, Channel, ClientTlsConfig, Uri};

/// Provides the main functionality of connection to a target device
/// and manipulating configuration or querying telemetry.
#[derive(Debug, Clone)]
pub struct Client(ClientConn);
pub struct Client {
inner: GNmiClient<AuthService<Channel>>,
}

impl<'a> Client {
/// Create a [`ClientBuilder`] that can create [`Client`]s.
Expand All @@ -35,17 +38,15 @@ impl<'a> Client {
/// ```
pub async fn capabilities(&mut self) -> CapabilityResponse {
let req = CapabilityRequest::default();
match self.0.capabilities(req).await {
Ok(val) => {
val.into_inner()
},
Err(e) => panic!("Error getting capabilities: {:?}", e)
match self.inner.capabilities(req).await {
Ok(val) => val.into_inner(),
Err(e) => panic!("Error getting capabilities: {:?}", e),
}
}
}

#[derive(Debug, Copy, Clone)]
struct Credentials<'a> {
pub struct Credentials<'a> {
username: &'a str,
password: &'a str,
}
Expand All @@ -71,10 +72,7 @@ impl<'a> ClientBuilder<'a> {

/// Configure credentials to use for connecting to the target device.
pub fn credentials(mut self, username: &'a str, password: &'a str) -> Self {
self.creds = Some(Credentials {
username,
password
});
self.creds = Some(Credentials { username, password });
self
}

Expand All @@ -95,12 +93,10 @@ impl<'a> ClientBuilder<'a> {
/// - Returns [`GinmiError::TransportError`] if the TLS-Settings are invalid.
/// - Returns [`GinmiError::TransportError`] if a connection to the target could not be
/// established.
pub async fn build(self) -> Result<Client, GinmiError>{
pub async fn build(self) -> Result<Client, GinmiError> {
let uri = match Uri::from_str(self.target) {
Ok(u) => u,
Err(e) => {
return Err(GinmiError::InvalidUriError(e.to_string()))
}
Err(e) => return Err(GinmiError::InvalidUriError(e.to_string())),
};

let mut endpoint = Channel::builder(uri);
Expand All @@ -109,12 +105,42 @@ impl<'a> ClientBuilder<'a> {
endpoint = endpoint.tls_config(self.tls_settings.unwrap())?;
}

if self.creds.is_some() {
todo!("passing credentials is currently not implemented")
}

let channel = endpoint.connect().await?;

Ok(Client(GNmiClient::new(channel)))
return if let Some(creds) = self.creds {
let user_header = HeaderValue::from_str(creds.username)?;
let pass_header = HeaderValue::from_str(creds.password)?;
Ok(Client {
inner: GNmiClient::new(AuthService::new(
channel,
Some(Arc::new(user_header)),
Some(Arc::new(pass_header)),
)),
})
} else {
Ok(Client {
inner: GNmiClient::new(AuthService::new(channel, None, None)),
})
};
}
}

#[cfg(test)]
mod tests {
use super::*;

#[tokio::test]
async fn invalid_uri() {
let client = Client::builder("$$$$").build().await;
assert!(client.is_err());
}

#[tokio::test]
async fn invalid_tls_settings() {
let client = Client::builder("https://test:57400")
.tls("invalid cert", "invalid domain")
.build()
.await;
assert!(client.is_err());
}
}
}
5 changes: 1 addition & 4 deletions src/client/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
mod client;

pub use client::{
Client,
ClientBuilder
};
pub use client::{Client, ClientBuilder};
4 changes: 3 additions & 1 deletion src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ pub enum GinmiError {
TransportError(#[from] tonic::transport::Error),
#[error("invalid uri passed as target: {}", .0)]
InvalidUriError(String),
}
#[error("invalid header in grpc request: {}", .0)]
InvalidHeaderValue(#[from] http::header::InvalidHeaderValue),
}
8 changes: 3 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@
//!
//! Provides a Client to modify and retrieve configuration from target network devices,
//! as well as various telemetry data.
mod auth;
mod client;
mod error;

pub use client::{
Client,
ClientBuilder,
};
pub use client::{Client, ClientBuilder};

pub use error::GinmiError;

Expand All @@ -30,4 +28,4 @@ pub(crate) mod gen {
tonic::include_proto!("google.protobuf");
}
}
}
}
37 changes: 0 additions & 37 deletions tests/integration_test.rs

This file was deleted.

0 comments on commit 43de549

Please sign in to comment.