Rust reqwest and Aruba AOS-CX API
Published: 2022-08-12 19:05:00

# 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"
```

## 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.

```rust
#[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(())
}

```

## 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.

```rust
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);
```

## 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.

```rust
use std::collections::HashMap;

     let firmware = client
         .get(format!("{}firmware", full_url))
         .send()
         .await?
         .json::<HashMap<String, String>>()
         .await?;
     println!("{:#?}", firmware);
```
## 5. Putting it all together using a struct

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

```rust
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(())
}

```

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
```

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

```rust
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(())
}
```

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
```