DEV Community

Joe Neville
Joe Neville

Posted on

Rust reqwest and Aruba AOS-CX API

AOS-CX rust reqwest client

These code snippets provide an example of API calls to the Aruba AOS-CX API using Rust reqwests.

1. Dependencies

First we need to add our dependencies to the project.
To do this, update the cargo.toml file with the following:

[dependencies]
reqwest = { version = "0.11", features = ["json", "cookies"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1.0"
Enter fullscreen mode Exit fullscreen mode

2. Login/logout

Any interaction with the AOS-CX API requires a login, and, for good house-keeping, a logout.
The API uses a username/password for authentication.

With this snippet we:

  • Send a Login API call
  • Check the request status code
  • Print an error to screen if the login is unsuccessful.
  • Send a logout call
  • Check the logout call status code and print to screen if this is not 200.
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let ip_add: &str = "your-ip-address";
    let full_url = format!("https://{}/rest/latest/", ip_add);

    let client = reqwest::Client::builder()
        .cookie_store(true)
        .danger_accept_invalid_certs(true)
        .build()?;
    let params = [("username", "your-username"), ("password", "your-password")];

    let resp = client
        .post(format!("{}login", full_url))
        .form(&params)
        .send()
        .await?;
    if resp.status() != 200 {
        println!("Login unsuccessful, code is {:#?}", resp.status());
    }
    let logout = client.post(format!("{}logout", full_url)).send().await?;
    if logout.status() != 200 {
        println!("Logout unsuccessful, code is {:#?}", logout.status());
    } else {
        println!("Logout successful");
    }

    Ok(())
}

Enter fullscreen mode Exit fullscreen mode

3. GET some data

Once we have the login/logout framework, we can insert API calls to GET, POST, PUT, and DELETE data.
Here's an example that sends a GET call to query the firmware on a device.
The response is JSON, which we can deserialize, using a predefined struct to handle the response key, value pairs.

  • First we define the struct, this is for the key 'current_version', with the value given a type of String.
  • Then we use the reqwests client to define the URL for the GET and how to handle the returned JSON.
  • Note that the name of the struct is referenced against json in the response.
use serde::Deserialize;

#[derive(Deserialize)]
struct Image {
    current_version: String,
}

let firmware = client
        .get(format!("{}firmware", full_url))
        .send()
        .await?
        .json::<Image>()
        .await?;
    println!("{:?}", firmware.current_version);
Enter fullscreen mode Exit fullscreen mode

4. Dynamically parse JSON

The example given in step 3 provides a great way to handle returned JSON, but we need to know what key,value pairs are returned from an API in order to be able to compose the struct.
When investigating an API, it is often a case of trial and error to find the data that we need.

So how can we send a GET call to receive data, without knowing what will be returned?

For this we can dynamically feed the JSON response into a hash-map:

  • First we need to bring std::collections::HashMap; into scope.
  • We then send the same GET call as the second example.
  • But for the JSON response we reference a dynamic hash-map, rather than a struct.
  • The downside is that we cannot call specific keys to provide data directly, the key,value pairs are now merely strings, but we can print out the whole response.
use std::collections::HashMap;

     let firmware = client
         .get(format!("{}firmware", full_url))
         .send()
         .await?
         .json::<HashMap<String, String>>()
         .await?;
     println!("{:#?}", firmware);
Enter fullscreen mode Exit fullscreen mode

5. Putting it all together using a struct

Here's the full code using a struct, I've added in an extra key:

use serde::Deserialize;

#[derive(Deserialize)]
struct Image {
    current_version: String,
    booted_image: String,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let ip_add: &str = "your-ip-address";
    let full_url = format!("https://{}/rest/latest/", ip_add);

    let client = reqwest::Client::builder()
        .cookie_store(true)
        .danger_accept_invalid_certs(true)
        .build()?;
    let params = [("username", "your-username"), ("password", "your-password")];

    let resp = client
        .post(format!("{}login", full_url))
        .form(&params)
        .send()
        .await?;
    if resp.status() != 200 {
        println!("Login unsuccessful, code is {:#?}", resp.status());
    }

    let firmware = client
        .get(format!("{}firmware", full_url))
        .send()
        .await?
        .json::<Image>()
        .await?;

    println!(
        "Current firmware image: {}\nBoot firmware image: {}",
        firmware.current_version, firmware.booted_image
    );
    let logout = client.post(format!("{}logout", full_url)).send().await?;
    if logout.status() != 200 {
        println!("Logout unsuccessful, code is {:#?}", logout.status());
    } else {
        println!("Logout successful");
    }

    Ok(())
}

Enter fullscreen mode Exit fullscreen mode

Here's a sample output:

➜  test1 git:(master) ✗ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.09s
     Running `target/debug/test1`
Current firmware image: FL.10.09.1020
Boot firmware image: primary
Logout successful
Enter fullscreen mode Exit fullscreen mode

6. Putting it all together using a dynamic hash-map

use std::collections::HashMap;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let ip_add: &str = "your-ip-address";
    let full_url = format!("https://{}/rest/latest/", ip_add);

    let client = reqwest::Client::builder()
        .cookie_store(true)
        .danger_accept_invalid_certs(true)
        .build()?;
    let params = [("username", "your-username"), ("password", "your-password")];

    let resp = client
        .post(format!("{}login", full_url))
        .form(&params)
        .send()
        .await?;
    if resp.status() != 200 {
        println!("Login unsuccessful, code is {:#?}", resp.status());
    }
     let firmware = client
         .get(format!("{}firmware", full_url))
         .send()
         .await?
         .json::<HashMap<String, String>>()
         .await?;
     println!("{:#?}", firmware);

    let logout = client.post(format!("{}logout", full_url)).send().await?;
    if logout.status() != 200 {
        println!("Logout unsuccessful, code is {:#?}", logout.status());
    } else {
        println!("Logout successful");
    }

    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

Sample output:

➜  test1 git:(master) ✗ cargo run
   Compiling test1 v0.1.0 (/Users/joeneville/code/test1)
    Finished dev [unoptimized + debuginfo] target(s) in 1.56s
     Running `target/debug/test1`
{
    "current_version": "FL.10.09.1020",
    "primary_version": "FL.10.09.1020",
    "secondary_version": "FL.10.09.1010",
    "default_image": "primary",
    "booted_image": "primary",
}
Logout successful
Enter fullscreen mode Exit fullscreen mode

Latest comments (0)