Expand description
§VMware vSphere API Client for Rust
This crate provides a Rust interface to the VMware vSphere Virtual Infrastructure JSON API, allowing you to manage VMware infrastructure programmatically.
§Connecting to vCenter
To set up a connection, use a statement like the following:
use vsphere::ClientBuilder;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create a client with username and password
let client = ClientBuilder::new("https://vcenter.example.com")
.basic_authn("administrator@vsphere.local", "password")
.app_details(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"))
.insecure(true) // For self-signed certs
.build()
.await?;
// Now you can use the client for API calls
Ok(())
}
.app_details
describes your application as to be able to identify and troubleshoot sessions from the vCenter UI or SessionManager API.
You can add insecure()
to your builder configuration to bypass TLS checks for both hostname and certificate.
One can set the reqwest
preconfigured client through the builder’s http_client
method to reuse the reqwest
connection and connection settings. The vim_rs
client abstraction is cheap, but the reqwest
HTTP client is not.
The client
above is an Arc
around the actual client object. Use .clone()
to pass it around.
If the above goes well, you have a connection to the vCenter server with an initialized session and retrieved service content.
§Obtaining Stub for the APIs
The VIM API is a remote object-oriented API. The functionality is organized in methods of managed objects.
To set up a remote stub to a management object, one needs a connection, an object type, and an object identifier.
For example, to get a handle to stub for the default PropertyCollector
, the following code does the trick:
let content = client.service_content();
let property_collector = PropertyCollector::new(client.clone(), &content.property_collector.value);
The service_content
is a structure that contains references to the root managed objects in the vCenter server. Note that the PropertyCollector
is always present in the service content. Other objects may be optional, and a check is to be made if the reference is set.
§Invoking APIs
This is simple and intuitive once you have a remote stub from the above step.
The VIM API has properties and methods. Both are exposed in the stubs. Properties are essentially remote methods that receive no parameters.
//! Invoke a method
let events = collector.read_next_events(10).await?;
//! Fetch a property value
let vms = view.view().await?;
In the examples above, collector
is an instance of PropertyCollector
, and view
is an instance of a View
like ContainerView
.
§Working with Polymorphic Types
The VIM API is conceptualized as a classic object-oriented API, much like the Java or C++ standard libraries. It has a root Any
object from which all other objects descend. There is DataObject
that is the root for all data structures. There is also MethodFault
that is the root for all error types.
This object-oriented design is not native to Rust. There are two principal approaches in Rust to dealing with such situations - traits and enums. Enums are easy to deal with in Rust and are extremely powerful and very safe. Unfortunately, expressing the VIM API solely in enums produces very hard-to-use abstractions of many deeply nested enum definitions that are hard to work with. Traits solve some of the usability challenges while dramatically increasing the work for the Rust compiler, which is not famous for fast performance. So the vim_rs
library takes a hybrid approach. The often deep and complex hierarchy of the DataObject
and MethodFault
are represented with traits. The shallow and big inventory of boxed arrays and primitive data types used with the property collector and other dynamic APIs leverage enums with the synthetic ValueElements
types. The VIM Any
type is renamed to VimAny
and is also represented as an enum
.
Working with the trait system is a bit more complex.
Let’s look into the details.
§Data Structs
Firstly, for every structure type from the VIM API, we have a corresponding Rust struct type. For example, a network card could be described with the VirtualE1000
structure. It looks roughly as follows:
pub struct VirtualE1000 {
pub key: i32,
pub device_info: Option<Box<dyn super::traits::DescriptionTrait>>,
pub backing: Option<Box<dyn super::traits::VirtualDeviceBackingInfoTrait>>,
pub connectable: Option<VirtualDeviceConnectInfo>,
pub slot_info: Option<Box<dyn super::traits::VirtualDeviceBusSlotInfoTrait>>,
pub controller_key: Option<i32>,
pub unit_number: Option<i32>,
pub numa_node: Option<i32>,
pub device_group_info: Option<VirtualDeviceDeviceGroupInfo>,
pub dynamic_property: Option<Vec<DynamicProperty>>,
pub address_type: Option<String>,
pub mac_address: Option<String>,
pub wake_on_lan_enabled: Option<bool>,
pub resource_allocation: Option<VirtualEthernetCardResourceAllocation>,
pub external_id: Option<String>,
pub upt_compatibility_enabled: Option<bool>,
}
Let’s look at some details. First, we see that fields optional to the API use Rust Option
(e.g., Option<i32>
) while required fields require a valid value (e.g., i32
). Next, we see that arrays of elements are expressed as Rust Vec
. For fields that have children or can form a cycle, Box
indirection is used. For fields of polymorphic types, i.e., those that have children, a dyn *Trait
type is used, which refers to a trait type implemented by all alternative structures (Option<Box<dyn DescriptionTrait>>
).
Structure types support serde
JSON serialization and deserialization as well as debug print.
§Traits
Traits are generated for VIM structure types that have children types. The traits for a given type are implemented by all of its descendants. This allows the API to take in and return all possible types in a given field, i.e., casting an object to a trait its type implements is trivial in Rust.
In the Rust language unlike Java, Go, and C++ there is no straightforward mechanism to upcast or downcast trait objects into other trait objects or concrete structure types. To make these possible, the vim_rs
crate provides utilities.
For casting to concrete structure types, all traits in the VIM API have AsAny
trait bound. AsAny
allows conversion of a reference or Box
to a reference to &dyn Any
or &Box<dyn Any>
. Further, a developer can use Any
or Box
methods to attempt fallible conversion to the target type. For example, converting a VirtualDeviceTrait
reference to a VirtualE1000
structure is done as follows (unwrap should be replaced with appropriate handling):
let e1000 = vd[0].as_any_ref().downcast_ref::<VirtualE1000>().unwrap();
Sometimes we want to convert from one trait to another. For example, if we want to read the MAC address of any network card device in a VM, we need to convert VirtualDeviceTrait
into VirtualEthernetCardTrait
. There are 2 options provided with the CastInto
trait. One option is to convert to Box
, and the other is to convert a borrowed reference.
In the examples below, we see how to convert Box<dyn VirtualDevice>
into a reference and Box
respectively:
let eth: &dyn VirtualEthernetCardTrait = vd.as_ref().into_ref().unwrap();
let eth: Box<dyn VirtualEthernetCardTrait> = vd.into_box().unwrap();
As Rust TryInto
mirrors the TryFrom
trait, CastInto
has a mirror CastFrom
trait.
Last but not least, the VIM trait provides read-only accessors to the fields of the type they represent. For example, the VirtualDeviceTrait
looks as follows:
pub trait VirtualDeviceTrait : super::traits::DataObjectTrait {
fn get_key(&self) -> i32;
fn get_device_info(&self) -> &Option<Box<dyn super::traits::DescriptionTrait>>;
fn get_backing(&self) -> &Option<Box<dyn super::traits::VirtualDeviceBackingInfoTrait>>;
fn get_connectable(&self) -> &Option<VirtualDeviceConnectInfo>;
fn get_slot_info(&self) -> &Option<Box<dyn super::traits::VirtualDeviceBusSlotInfoTrait>>;
fn get_controller_key(&self) -> Option<i32>;
fn get_unit_number(&self) -> Option<i32>;
fn get_numa_node(&self) -> Option<i32>;
fn get_device_group_info(&self) -> &Option<VirtualDeviceDeviceGroupInfo>;
}
As we see, the same types are used as in the struct types. The get_*
methods return borrowed references to complex types.
For more details on design decisions and performance considerations, please see the FAQ section below.
§Pruned Types
As discussed, the VIM API is big and has a deep inheritance hierarchy. To limit the size of the library, a number of optimizations and compromises are made. One specific optimization has a direct impact on the programming model. The descendant data types of MethodFault
and Event
types are not generated (See PRUNED_TYPES). This reduces the generated code and compilation times significantly at the cost of some utility.
The MethodFault
type represents errors that can occur when invoking VIM API methods, and the Event
type represents events that occur in the vCenter server.
The MethodFault
and Event
types do not have traits, and no descendant types are generated. Instead, both types receive 2 additional members:
type_: Option<StructType>
- holding the discriminator value, e.g.,EventEx
,NotFound
, etc.extra_fields_: HashMap<String, serde_json::Value>
- holding any data fields that are not present in the base type, e.g.,eventTypeId
.
Note that extra_fields_
content uses the API native names in camelCase convention instead of the Rust-friendly names used throughout vim_rs
.
Below is a snippet on how to decode the semantic event type using type_name_
and extra_fields_
:
fn get_event_type_id(event: &Event) -> String {
let Some(type_) = event.type_ else {
return "Event".to_string();
};
if type_.child_of(StructType::EventEx) || type_.child_of(StructType::ExtendedEvent) {
if let Some(event_type_id) = event.extra_fields_["eventTypeId"].as_str() {
return event_type_id.to_string();
}
}
let s: &'static str = type_.into();
s.to_string()
}
Note that StructType
implements the child_of
method, allowing to check if a type is the same or a descendant of another.
In our example above, we check if the event is EventEx
or ExtendedEvent
to access the eventTypeId
field.
Sometimes one will want to convert part of the dynamic-like objects into proper binding. For example, the managedObject
in the ExtendedEvent
can be read into ManagedObjectReference
as follows:
let value = event.extra_fields_["managedObject"].clone();
let managed_object: ManagedObjectReference = serde_json::from_value(value)?;
§Property Retrieval with Macros
The library provides two powerful macros to simplify property retrieval and monitoring:
§One-time Property Retrieval with vim_retrievable
The vim_retrievable
macro creates structs for efficient, one-time property retrieval:
use vim_macros::vim_retrievable;
use vim_rs::core::pc_retrieve::ObjectRetriever;
// Define a struct mapping to HostSystem properties
vim_retrievable!(
struct Host: HostSystem {
name = "name",
power_state = "runtime.power_state",
connected = "runtime.connection_state",
cpu_usage = "summary.quick_stats.overall_cpu_usage",
memory_usage = "summary.quick_stats.overall_memory_usage",
uptime = "summary.quick_stats.uptime",
}
);
async fn print_hosts(client: &Client) -> Result<()> {
// Create a retriever using the client
let retriever = ObjectRetriever::new(client.clone())?;
// Retrieve all hosts with their properties in a single API call
let hosts: Vec<HostInfo> = retriever
.retrieve_objects_from_container(&client.service_content().root_folder)
.await?;
// Work with strongly-typed host objects
for host in hosts {
println!("Host {} is {:?}", host.name, host.power_state);
}
Ok(())
}
§Continuous Property Monitoring with vim_updatable
The vim_updatable
macro creates structs for continuous property monitoring:
vim_updatable!(
struct VmDetails: VirtualMachine {
name = "name",
power_state = "runtime.power_state",
}
);
impl Display for VmDetails {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"VM ({}): {} with power state: {:?}", self.id.value, self.name, self.power_state
)
}
}
struct ChangeListener {}
impl ObjectCacheListener<VmDetails> for ChangeListener {
fn on_new(&mut self, obj: &VmDetails) {
info!("New VM: {}", obj);
}
fn on_update(&mut self, obj: &VmDetails) {
info!("VM updated: {}", obj);
}
fn on_remove(&mut self, obj: VmDetails) {
info!("VM removed: {}", obj);
}
}
async fn monitor_vms(client: &Arc<Client>) -> Result<(), Error> {
let cache = Box::new(ObjectCache::new_with_listener(Box::new(ChangeListener {})));
let mut manager = CacheManager::new(client.clone())?;
let mut monitor = manager.create_monitor()?;
manager.add_container_cache(cache, &client.service_content().root_folder).await?;
let start = Instant::now();
loop {
let updates = monitor.wait_updates(10).await?;
if let Some(updates) = updates {
manager.apply_updates(updates)?;
}
if start.elapsed().as_secs() > 60 {
break;
}
}
manager.destroy().await?;
Ok(())
}
§How the Macros Work
Both macros:
- Generate a struct based on the data structure defined in the macro, corresponding to a vSphere managed object type (VirtualMachine, HostSystem, etc.)
- Elicit the types of struct fields from the property paths in the vSphere API
- Handle type conversion between vSphere types and Rust types
The vim_rs::core::pc_retrieve
module supports one-time property retrieval,
while vim_rs::core::pc_cache
provides infrastructure for continuous property monitoring.
These macros significantly reduce boilerplate code when working with vSphere properties and provide type-safe access to vSphere inventory objects.