/***************************************************************************
* Copyright (c) 2013 VMware, Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
***************************************************************************/
/***************************************************************************
* Copyright (c) 2012 VMware, Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
***************************************************************************/
package com.vmware.vhadoop.adaptor.vc;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.datatype.XMLGregorianCalendar;
import javax.xml.ws.BindingProvider;
import javax.xml.ws.handler.MessageContext;
import javax.xml.ws.soap.SOAPFaultException;
import com.vmware.vim25.DynamicProperty;
import com.vmware.vim25.InvalidLocaleFaultMsg;
import com.vmware.vim25.InvalidLoginFaultMsg;
import com.vmware.vim25.InvalidPropertyFaultMsg;
import com.vmware.vim25.ManagedObjectReference;
import com.vmware.vim25.ObjectContent;
import com.vmware.vim25.ObjectSpec;
import com.vmware.vim25.ObjectUpdate;
import com.vmware.vim25.ObjectUpdateKind;
import com.vmware.vim25.PropertyChange;
import com.vmware.vim25.PropertyFilterSpec;
import com.vmware.vim25.PropertyFilterUpdate;
import com.vmware.vim25.PropertySpec;
import com.vmware.vim25.RuntimeFaultFaultMsg;
import com.vmware.vim25.ServiceContent;
import com.vmware.vim25.TraversalSpec;
import com.vmware.vim25.UpdateSet;
import com.vmware.vim25.VimPortType;
import com.vmware.vim25.VimService;
import com.vmware.vim25.WaitOptions;
/**
* Designed to encapsulate a lot of the low level detail of interacting with the JAX-WS VC API
* Any particularly verbose utility code can go in VCUtils to keep this tidy
*
*/
public class VCConnection {
private static final Logger _log = Logger.getLogger(VCConnection.class.getName());
interface VCCredentials {
public String getHostName();
public String getExtensionKey();
public String getUserName();
public String getPassword();
}
private static final int propertyCollectorTimeout = 300;
/**
* Represents a property that we're interesting in seeing a change in.
* Params: The property, the particular attribute of the property we're expecting to change
* and the expected attribute values that define a change we're looking for.
*
*/
protected class WaitProperty {
String _propToFilter;
String _attributeToWaitFor;
Object[] _expectedAttValues;
public WaitProperty(String propToQuery, String attributeToWaitFor, Object[] expectedAttValues) {
_propToFilter = propToQuery;
_attributeToWaitFor = attributeToWaitFor;
_expectedAttValues = expectedAttValues;
}
}
/**
* Once a property has changed, the result of that is encapsulated as a PropertyChangeResult.
* We pass back the original definition of the property we were looking for a change in and
* then the actual value that it changed to.
*
*/
protected class PropertyChangeResult {
WaitProperty _property;
Object _foundResult;
public PropertyChangeResult(WaitProperty property, Object foundResult) {
_property = property;
_foundResult = foundResult;
}
}
/**
* Simple encapsulation of a ManagedObjectReference and a set of properties which the caller uses to build a DTO.
* Nothing in this class knows about the DTOs as the code should be as generic as possible
*
*/
class MoRefAndProps {
String _name;
ManagedObjectReference _moref;
Map<String, Object> _properties;
public MoRefAndProps(ManagedObjectReference moref, String name) {
_moref = moref;
_name = name;
}
public void addProperty(String key, Object value) {
if (_properties == null) {
_properties = new HashMap<String, Object>();
}
_properties.put(key, value);
}
}
/* TODO: Global state is acceptable while this class is being used by a single thread, but
* in the longer term it would be good to cache some of this state in thread local storage.
* Before we do this, we should look at which calls are the most expensive and are safe to cache.
* Ideal scenario: cached connection state is initialized when VHM method is entered, passed around
* and reused by VCConnection methods and then cleaned up when the VHM method exits.
*/
private VimService _vimService;
private VimPortType _vimPort;
private ServiceContent _serviceContent;
private VCCredentials _credentials;
private boolean _connected;
public VCConnection(VCCredentials credentials) {
_credentials = credentials;
}
@SuppressWarnings("finally")
private boolean testConnection() {
if (!_connected) {
return false;
}
/* Test the operation of the current connection using the standard simple call for this purpose.
* Note: Don't use getVimPort() which would recursively try to test the connection.
* */
XMLGregorianCalendar vcTime = null;
try {
ManagedObjectReference svcInstRef = new ManagedObjectReference();
svcInstRef.setType("ServiceInstance");
svcInstRef.setValue("ServiceInstance");
vcTime = _vimPort.currentTime(svcInstRef);
} finally {
if (vcTime == null) {
_log.log(Level.SEVERE, "testConnection found VC connection dropped; caller will reconnect");
}
return vcTime != null;
}
}
private String getWsURL() {
/*
* For login by certificate we have to use proxy and connect to sdkTunnel
*/
if (_credentials.getExtensionKey() == null) {
return "https://"+_credentials.getHostName()+":443/sdk";
} else {
return "https://sdkTunnel:8089/sdk/vimService";
}
}
/*
* Login to vCenter. If extension key set, use certificate login; else use user/password login.
*/
private void login() {
if (_credentials.getExtensionKey() == null) {
try {
_vimPort.login(_serviceContent.getSessionManager(),
_credentials.getUserName(),
_credentials.getPassword(), null/*locale*/);
} catch (Exception e) {
_log.log(Level.SEVERE, "Unexpected exception when trying user/password login to vCenter: "+e);
}
return;
}
/*
* Because we're going through the proxy, we need to manually extract the cookie
* that contains the session ID from the response message and set it in HTTP header
* for future requests.
*/
Map headers =
(Map) ((BindingProvider) _vimPort).getResponseContext().get(
MessageContext.HTTP_RESPONSE_HEADERS);
List cookies = (List) headers.get("Set-cookie");
String cookieValue = (String) cookies.get(0);
StringTokenizer tokenizer = new StringTokenizer(cookieValue, ";");
cookieValue = tokenizer.nextToken();
String path = "$" + tokenizer.nextToken();
String cookie = "$Version=\"1\"; " + cookieValue + "; " + path;
Map map = new HashMap();
map.put("Cookie", Collections.singletonList(cookie));
((BindingProvider) _vimPort).getRequestContext().put(
MessageContext.HTTP_REQUEST_HEADERS, map);
ManagedObjectReference sessionManager = _serviceContent.getSessionManager();
try {
_vimPort.loginExtensionByCertificate(sessionManager, _credentials.getExtensionKey(), null);
} catch (Exception e) {
_log.log(Level.SEVERE, "Unexpected exception when trying certificate login to vCenter: "+e);
}
}
/**
* Initialize the connection to VC using the credentials provided
*
* @return true if a successful connection is created, false otherwise
*/
public boolean connect() {
if (_connected) {
if (testConnection()) {
return true;
} else {
/* Refresh */
_connected = false;
}
}
if (_credentials.getExtensionKey() != null) {
// For login by certificate we have to use proxy and connect to sdkTunnel
System.setProperty("https.proxyHost", _credentials.getHostName());
System.setProperty("https.proxyPort", "80");
}
ManagedObjectReference svcInstRef = new ManagedObjectReference();
svcInstRef.setType("ServiceInstance");
svcInstRef.setValue("ServiceInstance");
_vimService = new VimService();
_vimPort = _vimService.getVimPort();
Map<String, Object> ctxt = ((BindingProvider) _vimPort).getRequestContext();
ctxt.put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, getWsURL());
ctxt.put(BindingProvider.SESSION_MAINTAIN_PROPERTY, true);
try {
_serviceContent = _vimPort.retrieveServiceContent(svcInstRef);
login();
_connected = true;
} catch (Exception e) {
_log.log(Level.SEVERE, "Unexpected exception when trying to connect to vCenter: "+e);
return false;
}
return testConnection();
}
/* Retry is encapsulated here. We always hand back a valid service content or null */
protected ServiceContent getServiceContent() {
if (connect()) {
return _serviceContent;
}
return null;
}
/* Retry is encapsulated here. We always hand back a valid vim port or null */
protected VimPortType getVimPort() {
if (connect()) {
return _vimPort;
}
return null;
}
public void disconnect(boolean testAsyncDrop) throws SOAPFaultException {
if (_connected) {
try {
getVimPort().logout(getServiceContent().getSessionManager());
if (!testAsyncDrop) { // for testing connection drop asynchronous to VHM operation.
_connected = false;
}
} catch (RuntimeFaultFaultMsg e) {
throw new RuntimeException("Unexpected Disconnect Exception", e);
}
}
}
/**
* For the object represented by the moref, wait until ANY of the wait properties in the array have changed
*
* @param forObject the object to wait for a property change on
* @param waitProperties the properties to look for changes in
* @return a PropertyChangeResult of the first property to change or null if none changed
*/
/* TODO: It's not clear enough whether this method blocks or just does a query and returns. Assuming blocking */
public PropertyChangeResult waitForPropertyChange(ManagedObjectReference forObject, WaitProperty[] waitProperties) {
PropertyChangeResult result = null;
String version = "";
PropertyFilter propFilter = new PropertyFilter(forObject);
for (WaitProperty waitProperty : waitProperties) {
propFilter.addPropToFilter(waitProperty._propToFilter);
}
try {
ManagedObjectReference propCollector = propFilter.getPropertyCollector();
WaitOptions waitOptions = new WaitOptions();
waitOptions.setMaxWaitSeconds(propertyCollectorTimeout);
propSearch :
while (true) {
UpdateSet updateSet = _vimPort.waitForUpdatesEx(propCollector, version, waitOptions);
if (updateSet != null) {
version = updateSet.getVersion();
List<PropertyFilterUpdate> filterSet = updateSet.getFilterSet();
if (filterSet != null) {
for (PropertyFilterUpdate pfu : filterSet) {
List<ObjectUpdate> objectSet = pfu.getObjectSet();
for (ObjectUpdate obj : objectSet) {
result = findExpectedPropertyValue(waitProperties, obj);
if (result != null) {
break propSearch;
}
}
}
}
}
}
propFilter.cleanup();
} catch (Exception e) {
_log.log(Level.SEVERE, "Unexpected exception waiting for VC property change", e);
}
return result;
}
/* Within a Property change set of an ObjectUpdate, look to see if any of the waitProperties
* in the array are a match. If so, return a PropertyChangeResult. False otherwise. */
private PropertyChangeResult findExpectedPropertyValue(WaitProperty[] waitProperties, ObjectUpdate obj) {
ObjectUpdateKind kind = obj.getKind();
if (kind == ObjectUpdateKind.MODIFY || kind == ObjectUpdateKind.ENTER || kind == ObjectUpdateKind.LEAVE) {
for (PropertyChange pc : obj.getChangeSet()) {
String pcName = pc.getName();
Object pcValue = pc.getVal();
for (WaitProperty wp : waitProperties) {
if (pcName.lastIndexOf(wp._attributeToWaitFor) >= 0) {
for (Object expected : wp._expectedAttValues) {
if (expected.equals(pcValue)) {
return new PropertyChangeResult(wp, pcValue);
}
}
}
}
}
}
return null;
}
/**
* Query VC for the current value of the property key specified for the object moref.
* If the property is found, the current value is returned. Null otherwise.
*
* @param forObject the object to query the property on
* @param statePropToRefresh the key of the property
* @return the value of the property or null
*/
public Object refreshStateProperty(ManagedObjectReference forObject, String statePropToRefresh) {
try {
PropertyFilter propertyFilter = new PropertyFilter(forObject);
propertyFilter.addPropToFilter(statePropToRefresh);
List<ObjectContent> oca = propertyFilter.retrieveProperties();
if (oca != null) {
for (ObjectContent oc : oca) {
List<DynamicProperty> dps = oc.getPropSet();
if (dps != null) {
for (DynamicProperty dp : dps) {
if (dp.getName().equals(statePropToRefresh)) {
return dp.getVal();
}
}
}
}
}
} catch (Exception e) {
return null;
}
return null;
}
/* Look for VC entities in a particular root moref (typically a folder)
* The state properties to retrieve define state on the entity that we're interested in caching
* The values of the state properties are passed back and are built into a DTO object
*/
private List<MoRefAndProps> findObjectsFromRoot(
ManagedObjectReference rootObject, String type, String restrictToName, String[] statePropsToGet) {
List<MoRefAndProps> result = new ArrayList<MoRefAndProps>();
try {
ManagedObjectReference containerView = _vimPort.createContainerView(_serviceContent.getViewManager(),
rootObject, Arrays.asList(type), true);
PropertyFilter propFilter = new PropertyFilter(containerView, type);
propFilter.addPropToFilter("name");
if (statePropsToGet != null) {
for (String prop : statePropsToGet) {
propFilter.addPropToFilter(prop);
}
}
List<ObjectContent> oca = propFilter.retrieveProperties();
if (oca != null) {
for (ObjectContent oc : oca) {
List<DynamicProperty> dps = oc.getPropSet();
if (dps != null) {
MoRefAndProps moRefAndProps = null;
Map<String, Object> cachedDps = new HashMap<String, Object>();
for (DynamicProperty dp : dps) {
cachedDps.put(dp.getName(), dp.getVal());
if (dp.getName().equalsIgnoreCase("name")) {
String objectName = (String) dp.getVal();
if ((restrictToName == null) || restrictToName.equals(objectName)) {
moRefAndProps = new MoRefAndProps(oc.getObj(), objectName);
result.add(moRefAndProps);
}
}
}
if ((moRefAndProps != null) && (statePropsToGet != null)) {
for (String prop : statePropsToGet) {
moRefAndProps.addProperty(prop, cachedDps.get(prop));
}
}
}
}
}
propFilter.cleanup();
getVimPort().destroyView(containerView);
} catch (Exception e) {
if (restrictToName != null) {
_log.log(Level.SEVERE, "Unexpected exception looking for object of name: "+restrictToName, e);
} else {
_log.log(Level.SEVERE, "Unexpected exception looking for objects of type: "+type, e);
}
}
return result;
}
/**
* Find all VC objects of a particular type within the scope of a root object
*
* @param rootObject The root object moref, typically a folder
* @param type The object type
* @param props The object properties to query
* @return Object morefs and property key/values for each one, or null
*/
public MoRefAndProps[] findObjectsFromRoot(ManagedObjectReference rootObject, String type, String[] props) {
List<MoRefAndProps> result = findObjectsFromRoot(rootObject, type, null, props);
if (result.size() > 0) {
return result.toArray(new MoRefAndProps[0]);
}
return null;
}
/**
* Find a specific VC object by name of a particular type within the scope of a root object
*
* @param rootObject The root object moref, typically a folder
* @param type The object type
* @param targetName The object name
* @param props The object properties to query
* @return Object moref and the property key/values for it, or null
*/
public MoRefAndProps findObjectFromRoot(ManagedObjectReference rootObject, String type, String targetName, String[] props) {
List<MoRefAndProps> result = findObjectsFromRoot(rootObject, type, targetName, props);
if (result.size() > 0) {
return result.get(0);
}
return null;
}
/**
* Property filters are used a lot when querying the JAX-WS API for information about VC entities
* The code is pretty ugly, so it makes sense to encapsulate it in a utility class.
* The class is dual-purpose - it can be created with either constructor depending on the need.
* Properties can then be added to the filter and once that's completed,
* retrieveProperties() or getPropertyCollector() can be called, depending on the requirement
*
*/
private class PropertyFilter {
ManagedObjectReference _propertyCollector;
ManagedObjectReference _filterRef;
PropertySpec _propertySpec;
PropertyFilterSpec _propertyFilterSpec;
boolean _initialized = false;
/* TODO: Make sense of these two constructors. Can they be consolidated? */
public PropertyFilter(ManagedObjectReference containerView, String type) {
TraversalSpec tSpec = getTraversalSpecFromView();
ObjectSpec objectSpec = new ObjectSpec();
objectSpec.setObj(containerView);
objectSpec.getSelectSet().add(tSpec);
_propertyFilterSpec = new PropertyFilterSpec();
_propertyFilterSpec.getObjectSet().add(objectSpec);
_propertySpec = new PropertySpec();
_propertySpec.setAll(Boolean.FALSE);
_propertySpec.setType(type);
}
public PropertyFilter(ManagedObjectReference moref) {
ObjectSpec objectSpec = new ObjectSpec();
objectSpec.setObj(moref);
objectSpec.setSkip(Boolean.FALSE);
_propertyFilterSpec = new PropertyFilterSpec();
_propertyFilterSpec.getObjectSet().add(objectSpec);
_propertySpec = new PropertySpec();
_propertySpec.setType(moref.getType());
}
/* TODO: Could this be called post-init? */
public void addPropToFilter(String property) {
_propertySpec.getPathSet().add(property);
}
private void init() throws RuntimeFaultFaultMsg, InvalidPropertyFaultMsg {
if (!_initialized) {
_propertyFilterSpec.getPropSet().add(_propertySpec);
_propertyCollector = getServiceContent().getPropertyCollector();
}
_initialized = true;
}
public ManagedObjectReference getPropertyCollector() throws RuntimeFaultFaultMsg, InvalidPropertyFaultMsg {
init();
if (_filterRef == null) {
_filterRef = getVimPort().createFilter(_propertyCollector, _propertyFilterSpec, true);
}
return _propertyCollector;
}
public List<ObjectContent> retrieveProperties() throws RuntimeFaultFaultMsg, InvalidPropertyFaultMsg {
init();
return getVimPort().retrieveProperties(_propertyCollector, Arrays.asList(_propertyFilterSpec));
}
public void cleanup() throws RuntimeFaultFaultMsg {
if (_filterRef != null) {
getVimPort().destroyPropertyFilter(_filterRef);
}
}
private TraversalSpec getTraversalSpecFromView() {
// Create a traversal spec that starts from the ListView object
// and traverses to its "view" property containing the managed object references.
TraversalSpec viewToObject = new TraversalSpec();
viewToObject.setName("viewToObject");
viewToObject.setType("ContainerView");
viewToObject.setPath("view");
viewToObject.setSkip(false);
return viewToObject;
}
}
}