Introduction
This blog post demonstrates how to generate client-side Java bindings from the VI/JSON OpenAPI specification and use them in a simple application to communicate with a vCenter instance. We will create a Java project with Gradle and use the openapi-generator-gradle-plugin to generate bindings code. The sample application will then use the classes provided by the bindings code to log in to a live VC instance, create a datacenter and add a host. In addition, we will provide some sample code demonstrating how to use the PropertyCollector API to monitor the VC inventory for changes.
Using the OpenAPI generator to produce API bindings from the VI/JSON spec
The OpenAPI Tools suite uses the OpenAPI spec to generate client and server-side bindings (SDKs) for various programming languages. The tooling is controlled via numerous parameters, possibly resulting in bindings with incompatible interfaces. This example uses the Gradle plugin to generate typical bindings that will be later used in a simple Java project.
The first step is to download the VI/JSON spec – Virtual Infrastructure JSON OpenAPI Specification 8.0 U1. Then we apply the openapi-generator-gradle-plugin and use its openApiGenerate function in our project’s build.gradle file to define the binding generation configuration. Check out the sample from the build.gradle file below. Note that our project uses Gradle 8.0.2, and some details might differ if using other versions. If you are unfamiliar with Gradle, make sure to check the official documentation for more details
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
// Add the openapi-generator-gradle-plugin to classpath buildscript { repositories { mavenLocal() maven { url "https://repo1.maven.org/maven2" } } dependencies { classpath "org.openapitools:openapi-generator-gradle-plugin:6.2.0" } } // Load the openapi-generator plugin apply plugin: 'org.openapi.generator' // Binding generation configuration. Check https://openapi-generator.tech/docs/plugins/#gradle // for more details regarding the specific options. openApiGenerate { generatorName = "java" inputSpec = "$rootDir/vi-json-openapi-spec.yaml".toString() outputDir = "$buildDir/generated".toString() apiPackage = "com.vmware.vijson.bindings.client.api" modelPackage = "com.vmware.vijson.bindings.client.model" library = "resttemplate" removeOperationIdPrefix = true validateSpec = false globalProperties = [ generateApiTests: "false", generateModelTests: "false", generateApiDocumentation: "false", generateModelDocumentation: "false" ] } // Adding openApiGenerate's outputDir as a source set is needed for proper IDE integration. sourceSets { main { java { srcDir "${project.buildDir}/generated/src/main/java" srcDir "src/main/java" } } } // Add the openApiGenerate task as a dependency for the main compileJava task compileJava.dependsOn tasks.openApiGenerate |
The bindings code would get generated before project compilation.
Using OpenAPI Java bindings to interact with vCenter
This example assumes some prior knowledge of the vSphere Web Services API Programming Model. For a refresh, please refer to the vSphere Web Services SDK Programming Guide (8.0U1). The specifics around using the VI/JSON protocol are discussed here. You could also refer to the Virtual Infrastructure JSON API Reference Guide.
Add a Host to a Datacenter
Adding a Host (ESXi) to a Datacenter involves retrieving the root ServiceContent object that serves as a catalog for the various available APIs. We use it to get references to various managed objects like the SessionManager we use for login, the ViewManager, the root folder of the inventory, etc.
Initialize an API client
In order to establish connection to the Virtual Infrastructure JSON API’s endpoint we would need an instance of the com.vmware.vijson.bindings.client.ApiClient class. In the general case we would extend the ApiClient class and overwrite its buildRestTemplate() method to account for some specifics of the VI/JSON API.
The buildRestTemplate() method returns org.springframework.web.client.RestTemplate so you can refer to Spring’s documentation for more details. The highlight here is that we add a new request interceptor (ClientHttpRequestInterceptor) that stores the session id after login and appends it as a header to each subsequent request to the VI/JSON API. Since the VI/JSON API does not allow null values in the incoming json, we also need to add a message converter (MappingJackson2HttpMessageConverter) which filters all null values in the request body before passing it to the VI/JSON API endpoint.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
public class SessionAwareNullDisallowingApiClient extends ApiClient { /** * Build the RestTemplate used to make HTTP requests. * * @return RestTemplate */ @Override protected RestTemplate buildRestTemplate() { RestTemplate restTemplate = new RestTemplate(); CloseableHttpClient httpClient = HttpClients.custom().build(); HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(); requestFactory.setHttpClient(httpClient); restTemplate.setRequestFactory(new BufferingClientHttpRequestFactory(requestFactory)); restTemplate.setInterceptors( Collections.singletonList(new SessionAwareRestTemplateInterceptor())); restTemplate.getMessageConverters().add(0, createNullDisallowingConverter()); return restTemplate; } private MappingJackson2HttpMessageConverter createNullDisallowingConverter() { MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); ObjectMapper objMapper = new ObjectMapper(); objMapper.setSerializationInclusion(Include.NON_NULL); objMapper.registerModule(new JavaTimeModule()); objMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); converter.setObjectMapper(objMapper); return converter; } private static class SessionAwareRestTemplateInterceptor implements ClientHttpRequestInterceptor { private String session; @Override public ClientHttpResponse intercept(HttpRequest req, byte[] body, ClientHttpRequestExecution execution) throws IOException { if (session != null) { req.getHeaders().add("vmware-api-session-id", session); } ClientHttpResponse response = execution.execute(req, body); if (session == null) { session = response.getHeaders().getFirst("vmware-api-session-id"); } return response; } } } |
Once we setup the SessionAwareNullDisallowingApiClient instance we just need to pass the VI/JSON API endpoint address via the setBasePath method
1 2 |
ApiClient apiClient = new SessionAwareNullDisallowingApiClient(); apiClient.setBasePath(basePath); |
The API client’s base path should point to the server URL from the OpenAPI spec
1 2 |
servers: - url: https://{vcenter-host}/sdk/vim25/8.0.1.0 |
Retrieve root service content
This code snipped demonstrates how to obtain a reference to the ServiceInstance object. This is done by initializing a ServiceInstanceApi stub and calling the getContent method.
1 2 |
ServiceInstanceApi serviceInstanceApi = new ServiceInstanceApi(apiClient); ServiceContent serviceContent = serviceInstanceApi.getContent("ServiceInstance"); |
Login
This code snippet demonstrates how to obtain a reference to the SessionManager via the ServiceContent instance and use the SessionManagerAPI to log in using username and password. Additional login parameters can be passed via an instance of the LoginRequestType class.
1 2 3 4 5 6 7 8 |
SessionManagerApi sessionManagerApi = new SessionManagerApi(apiClient); LoginRequestType loginRequest = new LoginRequestType(); loginRequest.setUserName(vcUsername); loginRequest.setPassword(vcPassword); loginRequest.setLocale("en"); sessionManagerApi.login(serviceContent.getSessionManager().getValue(), loginRequest); |
Create a datacenter, add a host, and obtain the host’s resource pool reference
The following code snippet demonstrates how to use the FolderAPI to create a new Datacenter folder in the inventory. Here we use the serviceContent instance to obtain a reference to the root folder and call the createDatacenter method, passing an instance of the CreateDatacenterRequestType where we have specified the datacenter’s name.
As a next step, we use the DatacenterAPI to obtain a reference to the HostFolder within the newly created Datacenter and pass it to the addStandaloneHostTask method along with an AddHostRequestType instance to add a new Host. We use the AddHostRequestType instance to pass login credentials and additional configuration. Since adding a host is usually a long-running task, it is executed asynchronously and the addStandaloneHostTask method returns a Task reference that we can later use to monitor whether the add host action is completed successfully, handle errors if not, etc. This is done in the next step by initializing an instance of the TaskAPI class and passing it along with the Task reference to TaskInfo’s waitForTask method. We can use the resulting TaskInfo object to obtain information about the Add host task’s status.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// Step 1 - create datacenter FolderApi folderApi = new FolderApi(apiClient); CreateDatacenterRequestType createDatacenterParams = new CreateDatacenterRequestType(); createDatacenterParams.setName("datacenter"); ManagedObjectReference datacenterMoRef = folderApi.createDatacenter(serviceContent.getRootFolder().getValue(), createDatacenterParams); // Step 2 - get host folder moid DatacenterApi datacenterApi = new DatacenterApi(apiClient); ManagedObjectReference hostFolderMoRef = datacenterApi.getHostFolder(datacenterMoRef.getValue()); // Step 3 - initiate add host task AddStandaloneHostRequestType addHostRequest = new AddStandaloneHostRequestType(); HostConnectSpec hostSpec = new HostConnectSpec(); hostSpec.setUserName(esxUsername); hostSpec.setPassword(esxPassword); hostSpec.setHostName(esxAddress); hostSpec.setForce(false); addHostRequest.setAddConnected(true); addHostRequest.setSpec(hostSpec); ManagedObjectReference addHostTaskMoRef = folderApi.addStandaloneHostTask(hostFolderMoRef.getValue(), addHostRequest); // Step 6 - obtain a TaskInfo object. TaskApi taskApi = new TaskApi(apiClient); TaskInfo hostTaskInfo = waitForTask(taskApi, addHostTaskMoRef); |
Monitoring the VM inventory for changes using the PropertyCollector API
This section demonstrates how to use the PropertyCollectorAPI to monitor the state of some parts of the inventory, given filters and property specs. Please refer to this section of the programming guide if you are unfamiliar with the PropertyCollectorAPI. The PropertyCollector API is quite complex, so a thorough understanding of the underlying programming model is strongly recommended.
Create a ContainerView and an Object spec.
Creating a ContainerView is the first step. It provides information about what type of ManagedObjects we want to monitor. ViewManagerApi is used to create a ContainerView instance given a reference to the ViewManager instance and some configuration supplied via a CreateContainerViewRequestType object. A reference to the newly initialized ContainerView is passed to an ObjectSpec instance which is part of the PropertyCollector’s filter configuration. More on that later in the blog post.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// Create a Container View ViewManagerApi viewManager = new ViewManagerApi(apiClient); ManagedObjectReference viewManagerRef = serviceContent.getViewManager(); ArrayList<String> vmList = new ArrayList<>(); vmList.add("VirtualMachine"); CreateContainerViewRequestType createContainerViewRequest = new CreateContainerViewRequestType(); createContainerViewRequest.setContainer(serviceContent.getRootFolder()); createContainerViewRequest.setType(vmList); createContainerViewRequest.setRecursive(true); ManagedObjectReference containerViewRef = viewManager.createContainerView(viewManagerRef.getValue(), createContainerViewRequest); // Create an Object spec ObjectSpec objectSpec = new ObjectSpec(); objectSpec.setObj(containerViewRef); objectSpec.setSkip(true); |
Create property spec
Another essential part of PropertyCollector’s configuration is the PropertySpec. It defines the actual ManagedObject properties that we want to monitor for changes. The code snipped below shows how to set it up.
1 2 3 4 5 6 |
PropertySpec vmPropertySpec = new PropertySpec(); vmPropertySpec.setType("VirtualMachine"); List<String> vmPropsPathSet = new ArrayList<>(); vmPropsPathSet.add("name"); vmPropsPathSet.add("runtime.powerState"); vmPropertySpec.setPathSet(vmPropsPathSet); |
Instantiate a PropertyCollector object and create a persistent filter
After initializing an ObjectSpec and a PropertySpec, we are ready to set up the PropertyCollector. We start by obtaining a reference to a PropertyCollector instance for the serviceContent object. We then use an instance of the PropertyCollectorAPI to create a filter passing the necessary configuration via a CreateFilterRequestType object instance. See the code snipped below for more details.
1 2 3 4 5 6 7 8 9 10 11 12 |
PropertyCollectorApi propertyCollector = new PropertyCollectorApi(apiClient); ManagedObjectReference propertyCollectorRef = serviceContent.getPropertyCollector(); CreateFilterRequestType createFilterRequest = new CreateFilterRequestType(); PropertyFilterSpec persistantPropertyFilterSpec = new PropertyFilterSpec(); persistantPropertyFilterSpec.getObjectSet().add(objectSpec); persistantPropertyFilterSpec.getPropSet().add(vmPropertySpec); createFilterRequest.setSpec(persistantPropertyFilterSpec); createFilterRequest.setPartialUpdates(false); propertyCollector.createFilter(propertyCollectorRef.getValue(),createFilterRequest); |
Initiate monitoring
This section demonstrates how to start the monitoring task. This is usually done in a separate thread, but we skip this part for simplicity. To start monitoring for changes, we call the waitForUpdatesEx method of the propertyCollector instance we just configured. It requires a WaitOptions object that provides some configuration, like a timeout and an initial version of the inventory state. Once the waitForUpdatesEx method is called, it will return a UpdateSet object each time the state of the inventory changes.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
WaitForUpdatesExRequestType waitForUpdatesRequest = new WaitForUpdatesExRequestType(); WaitOptions waitOptions = new WaitOptions(); waitOptions.setMaxWaitSeconds(30); waitForUpdatesRequest.setOptions(waitOptions); waitForUpdatesRequest.setVersion(""); // default initial version UpdateSet updates = propertyCollector.waitForUpdatesEx(propertyCollectorRef.getValue(), waitForUpdatesRequest); // Just print the diff to the console if (updates != null) { System.out.println(updates); } |
Summary
So this is how one can use the VI/JSON OpenAPI specification to generate Java bindings and utilize the VC Automation API(s) to interact with a VC instance. Please mind the disclaimer below. Feel free to explore the vSphere Web Services SDK Programming Guide (8.0U1) for more examples with regard to API usage and programming models.
Disclaimer
The VMware-provided OpenAPI spec formally describes the officially-supported VI/JSON wire protocol. Although the spec is standard-compliant, due to VI APIs’ size and complexity, only some 3rd-party tools are capable of producing bindings with sufficient quality from it. This blog post presents Java-based examples based on the bindings produced by one of the most advanced tools: the OpenAPI Generator. The build scripts and code samples are provided as-is for convenience and are UNSUPPORTED by VMware in any way or form.
This blog post is part of a series with examples that examine the protocol in depth.
Follow us on Twitter @VMware_API_team and send us any feedback.