2587 lines
92 KiB
Python
2587 lines
92 KiB
Python
# Licensed to the Apache Software Foundation (ASF) under one or more
|
|
# contributor license agreements. See the NOTICE file distributed with
|
|
# this work for additional information regarding copyright ownership.
|
|
# The ASF licenses this file to You 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.
|
|
"""
|
|
VMware vCloud driver.
|
|
"""
|
|
import copy
|
|
import datetime
|
|
import re
|
|
import base64
|
|
import os
|
|
from libcloud.utils.iso8601 import parse_date
|
|
from libcloud.utils.py3 import httplib
|
|
from libcloud.utils.py3 import urlencode
|
|
from libcloud.utils.py3 import urlparse
|
|
from libcloud.utils.py3 import b
|
|
from libcloud.utils.py3 import next
|
|
from libcloud.utils.py3 import ET
|
|
|
|
urlparse = urlparse.urlparse
|
|
|
|
import time
|
|
|
|
from xml.parsers.expat import ExpatError
|
|
|
|
from libcloud.common.base import XmlResponse, ConnectionUserAndKey
|
|
from libcloud.common.types import InvalidCredsError, LibcloudError
|
|
from libcloud.compute.providers import Provider
|
|
from libcloud.compute.types import NodeState
|
|
from libcloud.compute.base import Node, NodeDriver, NodeLocation
|
|
from libcloud.compute.base import NodeSize, NodeImage
|
|
|
|
"""
|
|
From vcloud api "The VirtualQuantity element defines the number of MB
|
|
of memory. This should be either 512 or a multiple of 1024 (1 GB)."
|
|
"""
|
|
VIRTUAL_MEMORY_VALS = [512] + [1024 * i for i in range(1, 9)]
|
|
|
|
# Default timeout (in seconds) for long running tasks
|
|
DEFAULT_TASK_COMPLETION_TIMEOUT = 600
|
|
|
|
DEFAULT_API_VERSION = '0.8'
|
|
|
|
"""
|
|
Valid vCloud API v1.5 input values.
|
|
"""
|
|
VIRTUAL_CPU_VALS_1_5 = [i for i in range(1, 9)]
|
|
FENCE_MODE_VALS_1_5 = ['bridged', 'isolated', 'natRouted']
|
|
IP_MODE_VALS_1_5 = ['POOL', 'DHCP', 'MANUAL', 'NONE']
|
|
|
|
|
|
def fixxpath(root, xpath):
|
|
"""ElementTree wants namespaces in its xpaths, so here we add them."""
|
|
namespace, root_tag = root.tag[1:].split("}", 1)
|
|
fixed_xpath = "/".join(["{%s}%s" % (namespace, e)
|
|
for e in xpath.split("/")])
|
|
return fixed_xpath
|
|
|
|
|
|
def get_url_path(url):
|
|
return urlparse(url.strip()).path
|
|
|
|
|
|
class Vdc(object):
|
|
"""
|
|
Virtual datacenter (vDC) representation
|
|
"""
|
|
def __init__(self, id, name, driver, allocation_model=None, cpu=None,
|
|
memory=None, storage=None):
|
|
self.id = id
|
|
self.name = name
|
|
self.driver = driver
|
|
self.allocation_model = allocation_model
|
|
self.cpu = cpu
|
|
self.memory = memory
|
|
self.storage = storage
|
|
|
|
def __repr__(self):
|
|
return ('<Vdc: id=%s, name=%s, driver=%s ...>'
|
|
% (self.id, self.name, self.driver.name))
|
|
|
|
|
|
class Capacity(object):
|
|
"""
|
|
Represents CPU, Memory or Storage capacity of vDC.
|
|
"""
|
|
def __init__(self, limit, used, units):
|
|
self.limit = limit
|
|
self.used = used
|
|
self.units = units
|
|
|
|
def __repr__(self):
|
|
return ('<Capacity: limit=%s, used=%s, units=%s>'
|
|
% (self.limit, self.used, self.units))
|
|
|
|
|
|
class ControlAccess(object):
|
|
"""
|
|
Represents control access settings of a node
|
|
"""
|
|
class AccessLevel(object):
|
|
READ_ONLY = 'ReadOnly'
|
|
CHANGE = 'Change'
|
|
FULL_CONTROL = 'FullControl'
|
|
|
|
def __init__(self, node, everyone_access_level, subjects=None):
|
|
self.node = node
|
|
self.everyone_access_level = everyone_access_level
|
|
if not subjects:
|
|
subjects = []
|
|
self.subjects = subjects
|
|
|
|
def __repr__(self):
|
|
return ('<ControlAccess: node=%s, everyone_access_level=%s, '
|
|
'subjects=%s>'
|
|
% (self.node, self.everyone_access_level, self.subjects))
|
|
|
|
|
|
class Subject(object):
|
|
"""
|
|
User or group subject
|
|
"""
|
|
def __init__(self, type, name, access_level, id=None):
|
|
self.type = type
|
|
self.name = name
|
|
self.access_level = access_level
|
|
self.id = id
|
|
|
|
def __repr__(self):
|
|
return ('<Subject: type=%s, name=%s, access_level=%s>'
|
|
% (self.type, self.name, self.access_level))
|
|
|
|
|
|
class Lease(object):
|
|
"""
|
|
Lease information for vApps.
|
|
|
|
More info at: 'https://www.vmware.com/support/vcd/doc/
|
|
rest-api-doc-1.5-html/types/LeaseSettingsSectionType.html'
|
|
"""
|
|
|
|
def __init__(self, lease_id, deployment_lease=None, storage_lease=None,
|
|
deployment_lease_expiration=None,
|
|
storage_lease_expiration=None):
|
|
"""
|
|
:param lease_id: ID (link) to the lease settings section of a vApp.
|
|
:type lease_id: ``str``
|
|
|
|
:param deployment_lease: Deployment lease time in seconds
|
|
:type deployment_lease: ``int`` or ``None``
|
|
|
|
:param storage_lease: Storage lease time in seconds
|
|
:type storage_lease: ``int`` or ``None``
|
|
|
|
:param deployment_lease_expiration: Deployment lease expiration time
|
|
:type deployment_lease_expiration: ``datetime.datetime`` or ``None``
|
|
|
|
:param storage_lease_expiration: Storage lease expiration time
|
|
:type storage_lease_expiration: ``datetime.datetime`` or ``None``
|
|
"""
|
|
self.lease_id = lease_id
|
|
self.deployment_lease = deployment_lease
|
|
self.storage_lease = storage_lease
|
|
self.deployment_lease_expiration = deployment_lease_expiration
|
|
self.storage_lease_expiration = storage_lease_expiration
|
|
|
|
@classmethod
|
|
def to_lease(cls, lease_element):
|
|
"""
|
|
Convert lease settings element to lease instance.
|
|
|
|
:param lease_element: "LeaseSettingsSection" XML element
|
|
:type lease_element: ``ET.Element``
|
|
|
|
:return: Lease instance
|
|
:rtype: :class:`Lease`
|
|
"""
|
|
lease_id = lease_element.get('href')
|
|
deployment_lease = lease_element.find(
|
|
fixxpath(lease_element, 'DeploymentLeaseInSeconds')
|
|
)
|
|
storage_lease = lease_element.find(
|
|
fixxpath(lease_element, 'StorageLeaseInSeconds')
|
|
)
|
|
deployment_lease_expiration = lease_element.find(
|
|
fixxpath(lease_element, 'DeploymentLeaseExpiration')
|
|
)
|
|
storage_lease_expiration = lease_element.find(
|
|
fixxpath(lease_element, 'StorageLeaseExpiration')
|
|
)
|
|
|
|
def apply_if_elem_not_none(elem, function):
|
|
return function(elem.text) if elem is not None else None
|
|
|
|
return cls(
|
|
lease_id=lease_id,
|
|
deployment_lease=apply_if_elem_not_none(deployment_lease, int),
|
|
storage_lease=apply_if_elem_not_none(storage_lease, int),
|
|
deployment_lease_expiration=apply_if_elem_not_none(
|
|
deployment_lease_expiration,
|
|
parse_date
|
|
),
|
|
storage_lease_expiration=apply_if_elem_not_none(
|
|
storage_lease_expiration,
|
|
parse_date
|
|
)
|
|
)
|
|
|
|
def get_deployment_time(self):
|
|
"""
|
|
Gets the date and time a vApp was deployed. Time is inferred from the
|
|
deployment lease and expiration or the storage lease and expiration.
|
|
|
|
:return: Date and time the vApp was deployed or None if unable to
|
|
calculate
|
|
:rtype: ``datetime.datetime`` or ``None``
|
|
"""
|
|
if (self.deployment_lease is not None
|
|
and self.deployment_lease_expiration is not None):
|
|
return self.deployment_lease_expiration - datetime.timedelta(
|
|
seconds=self.deployment_lease
|
|
)
|
|
|
|
if (self.storage_lease is not None
|
|
and self.storage_lease_expiration is not None):
|
|
return self.storage_lease_expiration - datetime.timedelta(
|
|
seconds=self.storage_lease
|
|
)
|
|
|
|
raise Exception('Cannot get time deployed. '
|
|
'Missing complete lease and expiration information.')
|
|
|
|
def __repr__(self):
|
|
return (
|
|
'<Lease: id={lease_id}, deployment_lease={deployment_lease}, '
|
|
'storage_lease={storage_lease}, '
|
|
'deployment_lease_expiration={deployment_lease_expiration}, '
|
|
'storage_lease_expiration={storage_lease_expiration}>'.format(
|
|
lease_id=self.lease_id,
|
|
deployment_lease=self.deployment_lease,
|
|
storage_lease=self.storage_lease,
|
|
deployment_lease_expiration=self.deployment_lease_expiration,
|
|
storage_lease_expiration=self.storage_lease_expiration
|
|
)
|
|
)
|
|
|
|
def __eq__(self, other):
|
|
if not isinstance(other, Lease):
|
|
return False
|
|
|
|
return (
|
|
self.lease_id == other.lease_id
|
|
and self.deployment_lease == other.deployment_lease
|
|
and self.storage_lease == other.storage_lease
|
|
and (self.deployment_lease_expiration
|
|
== other.deployment_lease_expiration)
|
|
and self.storage_lease_expiration == other.storage_lease_expiration
|
|
)
|
|
|
|
def __ne__(self, other):
|
|
return not self == other
|
|
|
|
|
|
class InstantiateVAppXML(object):
|
|
|
|
def __init__(self, name, template, net_href, cpus, memory,
|
|
password=None, row=None, group=None):
|
|
self.name = name
|
|
self.template = template
|
|
self.net_href = net_href
|
|
self.cpus = cpus
|
|
self.memory = memory
|
|
self.password = password
|
|
self.row = row
|
|
self.group = group
|
|
|
|
self._build_xmltree()
|
|
|
|
def tostring(self):
|
|
return ET.tostring(self.root)
|
|
|
|
def _build_xmltree(self):
|
|
self.root = self._make_instantiation_root()
|
|
|
|
self._add_vapp_template(self.root)
|
|
instantiation_params = ET.SubElement(self.root,
|
|
"InstantiationParams")
|
|
|
|
# product and virtual hardware
|
|
self._make_product_section(instantiation_params)
|
|
self._make_virtual_hardware(instantiation_params)
|
|
|
|
network_config_section = ET.SubElement(instantiation_params,
|
|
"NetworkConfigSection")
|
|
|
|
network_config = ET.SubElement(network_config_section,
|
|
"NetworkConfig")
|
|
self._add_network_association(network_config)
|
|
|
|
def _make_instantiation_root(self):
|
|
return ET.Element(
|
|
"InstantiateVAppTemplateParams",
|
|
{'name': self.name,
|
|
'xml:lang': 'en',
|
|
'xmlns': "http://www.vmware.com/vcloud/v0.8",
|
|
'xmlns:xsi': "http://www.w3.org/2001/XMLSchema-instance"}
|
|
)
|
|
|
|
def _add_vapp_template(self, parent):
|
|
return ET.SubElement(
|
|
parent,
|
|
"VAppTemplate",
|
|
{'href': self.template}
|
|
)
|
|
|
|
def _make_product_section(self, parent):
|
|
prod_section = ET.SubElement(
|
|
parent,
|
|
"ProductSection",
|
|
{'xmlns:q1': "http://www.vmware.com/vcloud/v0.8",
|
|
'xmlns:ovf': "http://schemas.dmtf.org/ovf/envelope/1"}
|
|
)
|
|
|
|
if self.password:
|
|
self._add_property(prod_section, 'password', self.password)
|
|
|
|
if self.row:
|
|
self._add_property(prod_section, 'row', self.row)
|
|
|
|
if self.group:
|
|
self._add_property(prod_section, 'group', self.group)
|
|
|
|
return prod_section
|
|
|
|
def _add_property(self, parent, ovfkey, ovfvalue):
|
|
return ET.SubElement(
|
|
parent,
|
|
"Property",
|
|
{'xmlns': 'http://schemas.dmtf.org/ovf/envelope/1',
|
|
'ovf:key': ovfkey,
|
|
'ovf:value': ovfvalue}
|
|
)
|
|
|
|
def _make_virtual_hardware(self, parent):
|
|
vh = ET.SubElement(
|
|
parent,
|
|
"VirtualHardwareSection",
|
|
{'xmlns:q1': "http://www.vmware.com/vcloud/v0.8"}
|
|
)
|
|
|
|
self._add_cpu(vh)
|
|
self._add_memory(vh)
|
|
|
|
return vh
|
|
|
|
def _add_cpu(self, parent):
|
|
cpu_item = ET.SubElement(
|
|
parent,
|
|
"Item",
|
|
{'xmlns': "http://schemas.dmtf.org/ovf/envelope/1"}
|
|
)
|
|
self._add_instance_id(cpu_item, '1')
|
|
self._add_resource_type(cpu_item, '3')
|
|
self._add_virtual_quantity(cpu_item, self.cpus)
|
|
|
|
return cpu_item
|
|
|
|
def _add_memory(self, parent):
|
|
mem_item = ET.SubElement(
|
|
parent,
|
|
'Item',
|
|
{'xmlns': "http://schemas.dmtf.org/ovf/envelope/1"}
|
|
)
|
|
self._add_instance_id(mem_item, '2')
|
|
self._add_resource_type(mem_item, '4')
|
|
self._add_virtual_quantity(mem_item, self.memory)
|
|
|
|
return mem_item
|
|
|
|
def _add_instance_id(self, parent, id):
|
|
elm = ET.SubElement(
|
|
parent,
|
|
'InstanceID',
|
|
{'xmlns': 'http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/'
|
|
'CIM_ResourceAllocationSettingData'}
|
|
)
|
|
elm.text = id
|
|
return elm
|
|
|
|
def _add_resource_type(self, parent, type):
|
|
elm = ET.SubElement(
|
|
parent,
|
|
'ResourceType',
|
|
{'xmlns': 'http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/'
|
|
'CIM_ResourceAllocationSettingData'}
|
|
)
|
|
elm.text = type
|
|
return elm
|
|
|
|
def _add_virtual_quantity(self, parent, amount):
|
|
elm = ET.SubElement(
|
|
parent,
|
|
'VirtualQuantity',
|
|
{'xmlns': 'http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/'
|
|
'CIM_ResourceAllocationSettingData'}
|
|
)
|
|
elm.text = amount
|
|
return elm
|
|
|
|
def _add_network_association(self, parent):
|
|
return ET.SubElement(
|
|
parent,
|
|
'NetworkAssociation',
|
|
{'href': self.net_href}
|
|
)
|
|
|
|
|
|
class VCloudResponse(XmlResponse):
|
|
|
|
def success(self):
|
|
return self.status in (httplib.OK, httplib.CREATED,
|
|
httplib.NO_CONTENT, httplib.ACCEPTED)
|
|
|
|
|
|
class VCloudConnection(ConnectionUserAndKey):
|
|
|
|
"""
|
|
Connection class for the vCloud driver
|
|
"""
|
|
|
|
responseCls = VCloudResponse
|
|
token = None
|
|
host = None
|
|
|
|
def request(self, *args, **kwargs):
|
|
self._get_auth_token()
|
|
return super(VCloudConnection, self).request(*args, **kwargs)
|
|
|
|
def check_org(self):
|
|
# the only way to get our org is by logging in.
|
|
self._get_auth_token()
|
|
|
|
def _get_auth_headers(self):
|
|
"""Some providers need different headers than others"""
|
|
return {
|
|
'Authorization': "Basic %s" % base64.b64encode(
|
|
b('%s:%s' % (self.user_id, self.key))).decode('utf-8'),
|
|
'Content-Length': '0',
|
|
'Accept': 'application/*+xml'
|
|
}
|
|
|
|
def _get_auth_token(self):
|
|
if not self.token:
|
|
self.connection.request(method='POST', url='/api/v0.8/login',
|
|
headers=self._get_auth_headers())
|
|
|
|
resp = self.connection.getresponse()
|
|
headers = resp.headers
|
|
body = ET.XML(resp.text)
|
|
|
|
try:
|
|
self.token = headers['set-cookie']
|
|
except KeyError:
|
|
raise InvalidCredsError()
|
|
|
|
self.driver.org = get_url_path(
|
|
body.find(fixxpath(body, 'Org')).get('href')
|
|
)
|
|
|
|
def add_default_headers(self, headers):
|
|
headers['Cookie'] = self.token
|
|
headers['Accept'] = 'application/*+xml'
|
|
return headers
|
|
|
|
|
|
class VCloudNodeDriver(NodeDriver):
|
|
|
|
"""
|
|
vCloud node driver
|
|
"""
|
|
|
|
type = Provider.VCLOUD
|
|
name = 'vCloud'
|
|
website = 'http://www.vmware.com/products/vcloud/'
|
|
connectionCls = VCloudConnection
|
|
org = None
|
|
_vdcs = None
|
|
|
|
NODE_STATE_MAP = {'0': NodeState.PENDING,
|
|
'1': NodeState.PENDING,
|
|
'2': NodeState.PENDING,
|
|
'3': NodeState.PENDING,
|
|
'4': NodeState.RUNNING}
|
|
|
|
features = {'create_node': ['password']}
|
|
|
|
def __new__(cls, key, secret=None, secure=True, host=None, port=None,
|
|
api_version=DEFAULT_API_VERSION, **kwargs):
|
|
if cls is VCloudNodeDriver:
|
|
if api_version == '0.8':
|
|
cls = VCloudNodeDriver
|
|
elif api_version == '1.5':
|
|
cls = VCloud_1_5_NodeDriver
|
|
elif api_version == '5.1':
|
|
cls = VCloud_5_1_NodeDriver
|
|
elif api_version == '5.5':
|
|
cls = VCloud_5_5_NodeDriver
|
|
else:
|
|
raise NotImplementedError(
|
|
"No VCloudNodeDriver found for API version %s" %
|
|
(api_version))
|
|
return super(VCloudNodeDriver, cls).__new__(cls)
|
|
|
|
@property
|
|
def vdcs(self):
|
|
"""
|
|
vCloud virtual data centers (vDCs).
|
|
|
|
:return: list of vDC objects
|
|
:rtype: ``list`` of :class:`Vdc`
|
|
"""
|
|
if not self._vdcs:
|
|
self.connection.check_org() # make sure the org is set.
|
|
res = self.connection.request(self.org)
|
|
self._vdcs = [
|
|
self._to_vdc(
|
|
self.connection.request(get_url_path(i.get('href'))).object
|
|
)
|
|
for i in res.object.findall(fixxpath(res.object, "Link"))
|
|
if i.get('type') == 'application/vnd.vmware.vcloud.vdc+xml'
|
|
]
|
|
return self._vdcs
|
|
|
|
def _to_vdc(self, vdc_elm):
|
|
return Vdc(vdc_elm.get('href'), vdc_elm.get('name'), self)
|
|
|
|
def _get_vdc(self, vdc_name):
|
|
vdc = None
|
|
if not vdc_name:
|
|
# Return the first organisation VDC found
|
|
vdc = self.vdcs[0]
|
|
else:
|
|
for v in self.vdcs:
|
|
if v.name == vdc_name or v.id == vdc_name:
|
|
vdc = v
|
|
if vdc is None:
|
|
raise ValueError('%s virtual data centre could not be found' %
|
|
(vdc_name))
|
|
return vdc
|
|
|
|
@property
|
|
def networks(self):
|
|
networks = []
|
|
for vdc in self.vdcs:
|
|
res = self.connection.request(get_url_path(vdc.id)).object
|
|
networks.extend(
|
|
[network
|
|
for network in res.findall(
|
|
fixxpath(res, 'AvailableNetworks/Network')
|
|
|
|
)]
|
|
)
|
|
|
|
return networks
|
|
|
|
def _to_image(self, image):
|
|
image = NodeImage(id=image.get('href'),
|
|
name=image.get('name'),
|
|
driver=self.connection.driver)
|
|
return image
|
|
|
|
def _to_node(self, elm):
|
|
state = self.NODE_STATE_MAP[elm.get('status')]
|
|
name = elm.get('name')
|
|
public_ips = []
|
|
private_ips = []
|
|
|
|
# Following code to find private IPs works for Terremark
|
|
connections = elm.findall('%s/%s' % (
|
|
'{http://schemas.dmtf.org/ovf/envelope/1}NetworkConnectionSection',
|
|
fixxpath(elm, 'NetworkConnection'))
|
|
)
|
|
if not connections:
|
|
connections = elm.findall(
|
|
fixxpath(
|
|
elm,
|
|
'Children/Vm/NetworkConnectionSection/NetworkConnection'))
|
|
|
|
for connection in connections:
|
|
ips = [ip.text
|
|
for ip
|
|
in connection.findall(fixxpath(elm, "IpAddress"))]
|
|
if connection.get('Network') == 'Internal':
|
|
private_ips.extend(ips)
|
|
else:
|
|
public_ips.extend(ips)
|
|
|
|
node = Node(id=elm.get('href'),
|
|
name=name,
|
|
state=state,
|
|
public_ips=public_ips,
|
|
private_ips=private_ips,
|
|
driver=self.connection.driver)
|
|
|
|
return node
|
|
|
|
def _get_catalog_hrefs(self):
|
|
res = self.connection.request(self.org)
|
|
catalogs = [
|
|
i.get('href')
|
|
for i in res.object.findall(fixxpath(res.object, "Link"))
|
|
if i.get('type') == 'application/vnd.vmware.vcloud.catalog+xml'
|
|
]
|
|
|
|
return catalogs
|
|
|
|
def _wait_for_task_completion(self, task_href,
|
|
timeout=DEFAULT_TASK_COMPLETION_TIMEOUT):
|
|
start_time = time.time()
|
|
res = self.connection.request(get_url_path(task_href))
|
|
status = res.object.get('status')
|
|
while status != 'success':
|
|
if status == 'error':
|
|
# Get error reason from the response body
|
|
error_elem = res.object.find(fixxpath(res.object, 'Error'))
|
|
error_msg = "Unknown error"
|
|
if error_elem is not None:
|
|
error_msg = error_elem.get('message')
|
|
raise Exception("Error status returned by task %s.: %s"
|
|
% (task_href, error_msg))
|
|
if status == 'canceled':
|
|
raise Exception("Canceled status returned by task %s."
|
|
% task_href)
|
|
if (time.time() - start_time >= timeout):
|
|
raise Exception("Timeout (%s sec) while waiting for task %s."
|
|
% (timeout, task_href))
|
|
time.sleep(5)
|
|
res = self.connection.request(get_url_path(task_href))
|
|
status = res.object.get('status')
|
|
|
|
def destroy_node(self, node):
|
|
node_path = get_url_path(node.id)
|
|
# blindly poweroff node, it will throw an exception if already off
|
|
try:
|
|
res = self.connection.request('%s/power/action/poweroff'
|
|
% node_path,
|
|
method='POST')
|
|
self._wait_for_task_completion(res.object.get('href'))
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
res = self.connection.request('%s/action/undeploy' % node_path,
|
|
method='POST')
|
|
self._wait_for_task_completion(res.object.get('href'))
|
|
except ExpatError:
|
|
# The undeploy response is malformed XML atm.
|
|
# We can remove this whent he providers fix the problem.
|
|
pass
|
|
except Exception:
|
|
# Some vendors don't implement undeploy at all yet,
|
|
# so catch this and move on.
|
|
pass
|
|
|
|
res = self.connection.request(node_path, method='DELETE')
|
|
return res.status == httplib.ACCEPTED
|
|
|
|
def reboot_node(self, node):
|
|
res = self.connection.request('%s/power/action/reset'
|
|
% get_url_path(node.id),
|
|
method='POST')
|
|
return res.status in [httplib.ACCEPTED, httplib.NO_CONTENT]
|
|
|
|
def list_nodes(self):
|
|
return self.ex_list_nodes()
|
|
|
|
def ex_list_nodes(self, vdcs=None):
|
|
"""
|
|
List all nodes across all vDCs. Using 'vdcs' you can specify which vDCs
|
|
should be queried.
|
|
|
|
:param vdcs: None, vDC or a list of vDCs to query. If None all vDCs
|
|
will be queried.
|
|
:type vdcs: :class:`Vdc`
|
|
|
|
:rtype: ``list`` of :class:`Node`
|
|
"""
|
|
if not vdcs:
|
|
vdcs = self.vdcs
|
|
if not isinstance(vdcs, (list, tuple)):
|
|
vdcs = [vdcs]
|
|
nodes = []
|
|
for vdc in vdcs:
|
|
res = self.connection.request(get_url_path(vdc.id))
|
|
elms = res.object.findall(fixxpath(
|
|
res.object, "ResourceEntities/ResourceEntity")
|
|
)
|
|
vapps = [
|
|
(i.get('name'), i.get('href'))
|
|
for i in elms if
|
|
i.get('type') == 'application/vnd.vmware.vcloud.vApp+xml' and
|
|
i.get('name')
|
|
]
|
|
|
|
for vapp_name, vapp_href in vapps:
|
|
try:
|
|
res = self.connection.request(
|
|
get_url_path(vapp_href),
|
|
headers={'Content-Type':
|
|
'application/vnd.vmware.vcloud.vApp+xml'}
|
|
)
|
|
nodes.append(self._to_node(res.object))
|
|
except Exception as e:
|
|
# The vApp was probably removed since the previous vDC
|
|
# query, ignore
|
|
# pylint: disable=no-member
|
|
if not (e.args[0].tag.endswith('Error') and
|
|
e.args[0].get('minorErrorCode') ==
|
|
'ACCESS_TO_RESOURCE_IS_FORBIDDEN'):
|
|
raise
|
|
|
|
return nodes
|
|
|
|
def _to_size(self, ram):
|
|
ns = NodeSize(
|
|
id=None,
|
|
name="%s Ram" % ram,
|
|
ram=ram,
|
|
disk=None,
|
|
bandwidth=None,
|
|
price=None,
|
|
driver=self.connection.driver
|
|
)
|
|
return ns
|
|
|
|
def list_sizes(self, location=None):
|
|
sizes = [self._to_size(i) for i in VIRTUAL_MEMORY_VALS]
|
|
return sizes
|
|
|
|
def _get_catalogitems_hrefs(self, catalog):
|
|
"""Given a catalog href returns contained catalog item hrefs"""
|
|
res = self.connection.request(
|
|
get_url_path(catalog),
|
|
headers={
|
|
'Content-Type': 'application/vnd.vmware.vcloud.catalog+xml'
|
|
}
|
|
).object
|
|
|
|
cat_items = res.findall(fixxpath(res, "CatalogItems/CatalogItem"))
|
|
cat_item_hrefs = [i.get('href')
|
|
for i in cat_items
|
|
if i.get('type') ==
|
|
'application/vnd.vmware.vcloud.catalogItem+xml']
|
|
|
|
return cat_item_hrefs
|
|
|
|
def _get_catalogitem(self, catalog_item):
|
|
"""Given a catalog item href returns elementree"""
|
|
res = self.connection.request(
|
|
get_url_path(catalog_item),
|
|
headers={
|
|
'Content-Type': 'application/vnd.vmware.vcloud.catalogItem+xml'
|
|
}
|
|
).object
|
|
|
|
return res
|
|
|
|
def list_images(self, location=None):
|
|
images = []
|
|
for vdc in self.vdcs:
|
|
res = self.connection.request(get_url_path(vdc.id)).object
|
|
res_ents = res.findall(fixxpath(
|
|
res, "ResourceEntities/ResourceEntity")
|
|
)
|
|
images += [
|
|
self._to_image(i)
|
|
for i in res_ents
|
|
if i.get('type') ==
|
|
'application/vnd.vmware.vcloud.vAppTemplate+xml'
|
|
]
|
|
|
|
for catalog in self._get_catalog_hrefs():
|
|
for cat_item in self._get_catalogitems_hrefs(catalog):
|
|
res = self._get_catalogitem(cat_item)
|
|
res_ents = res.findall(fixxpath(res, 'Entity'))
|
|
images += [
|
|
self._to_image(i)
|
|
for i in res_ents
|
|
if i.get('type') ==
|
|
'application/vnd.vmware.vcloud.vAppTemplate+xml'
|
|
]
|
|
|
|
def idfun(image):
|
|
return image.id
|
|
|
|
return self._uniquer(images, idfun)
|
|
|
|
def _uniquer(self, seq, idfun=None):
|
|
if idfun is None:
|
|
def idfun(x): # pylint: disable=function-redefined
|
|
return x
|
|
seen = {}
|
|
result = []
|
|
for item in seq:
|
|
marker = idfun(item)
|
|
if marker in seen:
|
|
continue
|
|
seen[marker] = 1
|
|
result.append(item)
|
|
return result
|
|
|
|
def create_node(self, name, size, image, auth=None, ex_network=None,
|
|
ex_vdc=None, ex_cpus=1, ex_row=None, ex_group=None):
|
|
"""
|
|
Creates and returns node.
|
|
|
|
:keyword ex_network: link to a "Network" e.g.,
|
|
``https://services.vcloudexpress...``
|
|
:type ex_network: ``str``
|
|
|
|
:keyword ex_vdc: Name of organisation's virtual data
|
|
center where vApp VMs will be deployed.
|
|
:type ex_vdc: ``str``
|
|
|
|
:keyword ex_cpus: number of virtual cpus (limit depends on provider)
|
|
:type ex_cpus: ``int``
|
|
|
|
:type ex_row: ``str``
|
|
|
|
:type ex_group: ``str``
|
|
"""
|
|
# Some providers don't require a network link
|
|
try:
|
|
network = ex_network or self.networks[0].get('href')
|
|
except IndexError:
|
|
network = ''
|
|
|
|
password = None
|
|
auth = self._get_and_check_auth(auth)
|
|
password = auth.password
|
|
|
|
instantiate_xml = InstantiateVAppXML(
|
|
name=name,
|
|
template=image.id,
|
|
net_href=network,
|
|
cpus=str(ex_cpus),
|
|
memory=str(size.ram),
|
|
password=password,
|
|
row=ex_row,
|
|
group=ex_group
|
|
)
|
|
|
|
vdc = self._get_vdc(ex_vdc)
|
|
|
|
# Instantiate VM and get identifier.
|
|
content_type = \
|
|
'application/vnd.vmware.vcloud.instantiateVAppTemplateParams+xml'
|
|
res = self.connection.request(
|
|
'%s/action/instantiateVAppTemplate' % get_url_path(vdc.id),
|
|
data=instantiate_xml.tostring(),
|
|
method='POST',
|
|
headers={'Content-Type': content_type}
|
|
)
|
|
vapp_path = get_url_path(res.object.get('href'))
|
|
|
|
# Deploy the VM from the identifier.
|
|
res = self.connection.request('%s/action/deploy' % vapp_path,
|
|
method='POST')
|
|
|
|
self._wait_for_task_completion(res.object.get('href'))
|
|
|
|
# Power on the VM.
|
|
res = self.connection.request('%s/power/action/powerOn' % vapp_path,
|
|
method='POST')
|
|
|
|
res = self.connection.request(vapp_path)
|
|
node = self._to_node(res.object)
|
|
|
|
if getattr(auth, "generated", False):
|
|
node.extra['password'] = auth.password
|
|
|
|
return node
|
|
|
|
|
|
class HostingComConnection(VCloudConnection):
|
|
|
|
"""
|
|
vCloud connection subclass for Hosting.com
|
|
"""
|
|
|
|
host = "vcloud.safesecureweb.com"
|
|
|
|
def _get_auth_headers(self):
|
|
"""hosting.com doesn't follow the standard vCloud authentication API"""
|
|
return {
|
|
'Authentication': base64.b64encode(b('%s:%s' % (self.user_id,
|
|
self.key))),
|
|
'Content-Length': '0'
|
|
}
|
|
|
|
|
|
class HostingComDriver(VCloudNodeDriver):
|
|
|
|
"""
|
|
vCloud node driver for Hosting.com
|
|
"""
|
|
connectionCls = HostingComConnection
|
|
|
|
|
|
class TerremarkConnection(VCloudConnection):
|
|
|
|
"""
|
|
vCloud connection subclass for Terremark
|
|
"""
|
|
|
|
host = "services.vcloudexpress.terremark.com"
|
|
|
|
|
|
class TerremarkDriver(VCloudNodeDriver):
|
|
|
|
"""
|
|
vCloud node driver for Terremark
|
|
"""
|
|
|
|
connectionCls = TerremarkConnection
|
|
|
|
def list_locations(self):
|
|
return [NodeLocation(0, "Terremark Texas", 'US', self)]
|
|
|
|
|
|
class VCloud_1_5_Connection(VCloudConnection):
|
|
|
|
def _get_auth_headers(self):
|
|
"""Compatibility for using v1.5 API under vCloud Director 5.1"""
|
|
return {
|
|
'Authorization': "Basic %s" % base64.b64encode(
|
|
b('%s:%s' % (self.user_id, self.key))).decode('utf-8'),
|
|
'Content-Length': '0',
|
|
'Accept': 'application/*+xml;version=1.5'
|
|
}
|
|
|
|
def _get_auth_token(self):
|
|
if not self.token:
|
|
# Log In
|
|
self.connection.request(method='POST', url='/api/sessions',
|
|
headers=self._get_auth_headers())
|
|
|
|
resp = self.connection.getresponse()
|
|
headers = resp.headers
|
|
|
|
# Set authorization token
|
|
try:
|
|
self.token = headers['x-vcloud-authorization']
|
|
except KeyError:
|
|
raise InvalidCredsError()
|
|
|
|
# Get the URL of the Organization
|
|
body = ET.XML(resp.text)
|
|
self.org_name = body.get('org')
|
|
|
|
# pylint: disable=no-member
|
|
org_list_url = get_url_path(
|
|
next((link for link in body.findall(fixxpath(body, 'Link'))
|
|
if link.get('type') ==
|
|
'application/vnd.vmware.vcloud.orgList+xml')).get('href')
|
|
)
|
|
|
|
if self.proxy_url is not None:
|
|
self.connection.set_http_proxy(self.proxy_url)
|
|
self.connection.request(method='GET', url=org_list_url,
|
|
headers=self.add_default_headers({}))
|
|
body = ET.XML(self.connection.getresponse().text)
|
|
|
|
# pylint: disable=no-member
|
|
self.driver.org = get_url_path(
|
|
next((org for org in body.findall(fixxpath(body, 'Org'))
|
|
if org.get('name') == self.org_name)).get('href')
|
|
)
|
|
|
|
def add_default_headers(self, headers):
|
|
headers['Accept'] = 'application/*+xml;version=1.5'
|
|
headers['x-vcloud-authorization'] = self.token
|
|
return headers
|
|
|
|
|
|
class VCloud_5_5_Connection(VCloud_1_5_Connection):
|
|
|
|
def _get_auth_headers(self):
|
|
"""Compatibility for using v5.5 of the API"""
|
|
auth_headers = super(VCloud_5_5_Connection, self)._get_auth_headers()
|
|
auth_headers['Accept'] = 'application/*+xml;version=5.5'
|
|
return auth_headers
|
|
|
|
def add_default_headers(self, headers):
|
|
headers['Accept'] = 'application/*+xml;version=5.5'
|
|
headers['x-vcloud-authorization'] = self.token
|
|
return headers
|
|
|
|
|
|
class Instantiate_1_5_VAppXML(object):
|
|
|
|
def __init__(self, name, template, network, vm_network=None,
|
|
vm_fence=None, description=None):
|
|
self.name = name
|
|
self.template = template
|
|
self.network = network
|
|
self.vm_network = vm_network
|
|
self.vm_fence = vm_fence
|
|
self.description = description
|
|
self._build_xmltree()
|
|
|
|
def tostring(self):
|
|
return ET.tostring(self.root)
|
|
|
|
def _build_xmltree(self):
|
|
self.root = self._make_instantiation_root()
|
|
|
|
if self.network is not None:
|
|
instantiation_params = ET.SubElement(self.root,
|
|
'InstantiationParams')
|
|
network_config_section = ET.SubElement(instantiation_params,
|
|
'NetworkConfigSection')
|
|
ET.SubElement(
|
|
network_config_section,
|
|
'Info',
|
|
{'xmlns': 'http://schemas.dmtf.org/ovf/envelope/1'}
|
|
)
|
|
network_config = ET.SubElement(network_config_section,
|
|
'NetworkConfig')
|
|
self._add_network_association(network_config)
|
|
|
|
if self.description is not None:
|
|
ET.SubElement(self.root, 'Description').text = self.description
|
|
|
|
self._add_vapp_template(self.root)
|
|
|
|
def _make_instantiation_root(self):
|
|
return ET.Element(
|
|
'InstantiateVAppTemplateParams',
|
|
{'name': self.name,
|
|
'deploy': 'false',
|
|
'powerOn': 'false',
|
|
'xml:lang': 'en',
|
|
'xmlns': 'http://www.vmware.com/vcloud/v1.5',
|
|
'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance'}
|
|
)
|
|
|
|
def _add_vapp_template(self, parent):
|
|
return ET.SubElement(
|
|
parent,
|
|
'Source',
|
|
{'href': self.template}
|
|
)
|
|
|
|
def _add_network_association(self, parent):
|
|
if self.vm_network is None:
|
|
# Don't set a custom vApp VM network name
|
|
parent.set('networkName', self.network.get('name'))
|
|
else:
|
|
# Set a custom vApp VM network name
|
|
parent.set('networkName', self.vm_network)
|
|
configuration = ET.SubElement(parent, 'Configuration')
|
|
ET.SubElement(configuration, 'ParentNetwork',
|
|
{'href': self.network.get('href')})
|
|
|
|
if self.vm_fence is None:
|
|
fencemode = self.network.find(fixxpath(self.network,
|
|
'Configuration/FenceMode')).text
|
|
else:
|
|
fencemode = self.vm_fence
|
|
ET.SubElement(configuration, 'FenceMode').text = fencemode
|
|
|
|
|
|
class VCloud_1_5_NodeDriver(VCloudNodeDriver):
|
|
connectionCls = VCloud_1_5_Connection
|
|
|
|
# Based on
|
|
# http://pubs.vmware.com/vcloud-api-1-5/api_prog/
|
|
# GUID-843BE3AD-5EF6-4442-B864-BCAE44A51867.html
|
|
NODE_STATE_MAP = {'-1': NodeState.UNKNOWN,
|
|
'0': NodeState.PENDING,
|
|
'1': NodeState.PENDING,
|
|
'2': NodeState.PENDING,
|
|
'3': NodeState.PENDING,
|
|
'4': NodeState.RUNNING,
|
|
'5': NodeState.RUNNING,
|
|
'6': NodeState.UNKNOWN,
|
|
'7': NodeState.UNKNOWN,
|
|
'8': NodeState.STOPPED,
|
|
'9': NodeState.UNKNOWN,
|
|
'10': NodeState.UNKNOWN}
|
|
|
|
def list_locations(self):
|
|
return [NodeLocation(id=self.connection.host,
|
|
name=self.connection.host, country="N/A", driver=self)]
|
|
|
|
def ex_find_node(self, node_name, vdcs=None):
|
|
"""
|
|
Searches for node across specified vDCs. This is more effective than
|
|
querying all nodes to get a single instance.
|
|
|
|
:param node_name: The name of the node to search for
|
|
:type node_name: ``str``
|
|
|
|
:param vdcs: None, vDC or a list of vDCs to search in. If None all vDCs
|
|
will be searched.
|
|
:type vdcs: :class:`Vdc`
|
|
|
|
:return: node instance or None if not found
|
|
:rtype: :class:`Node` or ``None``
|
|
"""
|
|
if not vdcs:
|
|
vdcs = self.vdcs
|
|
if not getattr(vdcs, '__iter__', False):
|
|
vdcs = [vdcs]
|
|
for vdc in vdcs:
|
|
res = self.connection.request(get_url_path(vdc.id))
|
|
xpath = fixxpath(res.object, "ResourceEntities/ResourceEntity")
|
|
entity_elems = res.object.findall(xpath)
|
|
for entity_elem in entity_elems:
|
|
if entity_elem.get('type') == \
|
|
'application/vnd.vmware.vcloud.vApp+xml' and \
|
|
entity_elem.get('name') == node_name:
|
|
path = entity_elem.get('href')
|
|
return self._ex_get_node(path)
|
|
return None
|
|
|
|
def ex_find_vm_nodes(self, vm_name, max_results=50):
|
|
"""
|
|
Finds nodes that contain a VM with the specified name.
|
|
|
|
:param vm_name: The VM name to find nodes for
|
|
:type vm_name: ``str``
|
|
|
|
:param max_results: Maximum number of results up to 128
|
|
:type max_results: ``int``
|
|
|
|
:return: List of node instances
|
|
:rtype: `list` of :class:`Node`
|
|
"""
|
|
vms = self.ex_query(
|
|
'vm',
|
|
filter='name=={vm_name}'.format(vm_name=vm_name),
|
|
page=1,
|
|
page_size=max_results
|
|
)
|
|
return [self._ex_get_node(vm['container']) for vm in vms]
|
|
|
|
def destroy_node(self, node, shutdown=True):
|
|
try:
|
|
self.ex_undeploy_node(node, shutdown=shutdown)
|
|
except Exception:
|
|
# Some vendors don't implement undeploy at all yet,
|
|
# so catch this and move on.
|
|
pass
|
|
|
|
res = self.connection.request(get_url_path(node.id), method='DELETE')
|
|
return res.status == httplib.ACCEPTED
|
|
|
|
def reboot_node(self, node):
|
|
res = self.connection.request('%s/power/action/reset'
|
|
% get_url_path(node.id),
|
|
method='POST')
|
|
if res.status in [httplib.ACCEPTED, httplib.NO_CONTENT]:
|
|
self._wait_for_task_completion(res.object.get('href'))
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def ex_deploy_node(self, node, ex_force_customization=False):
|
|
"""
|
|
Deploys existing node. Equal to vApp "start" operation.
|
|
|
|
:param node: The node to be deployed
|
|
:type node: :class:`Node`
|
|
|
|
:param ex_force_customization: Used to specify whether to force
|
|
customization on deployment,
|
|
if not set default value is False.
|
|
:type ex_force_customization: ``bool``
|
|
|
|
:rtype: :class:`Node`
|
|
"""
|
|
if ex_force_customization:
|
|
vms = self._get_vm_elements(node.id)
|
|
for vm in vms:
|
|
self._ex_deploy_node_or_vm(vm.get('href'),
|
|
ex_force_customization=True)
|
|
else:
|
|
self._ex_deploy_node_or_vm(node.id)
|
|
|
|
res = self.connection.request(get_url_path(node.id))
|
|
return self._to_node(res.object)
|
|
|
|
def _ex_deploy_node_or_vm(self, vapp_or_vm_path,
|
|
ex_force_customization=False):
|
|
data = {'powerOn': 'true',
|
|
'forceCustomization': str(ex_force_customization).lower(),
|
|
'xmlns': 'http://www.vmware.com/vcloud/v1.5'}
|
|
deploy_xml = ET.Element('DeployVAppParams', data)
|
|
path = get_url_path(vapp_or_vm_path)
|
|
headers = {
|
|
'Content-Type':
|
|
'application/vnd.vmware.vcloud.deployVAppParams+xml'
|
|
}
|
|
res = self.connection.request('%s/action/deploy' % path,
|
|
data=ET.tostring(deploy_xml),
|
|
method='POST',
|
|
headers=headers)
|
|
self._wait_for_task_completion(res.object.get('href'))
|
|
|
|
def ex_undeploy_node(self, node, shutdown=True):
|
|
"""
|
|
Undeploys existing node. Equal to vApp "stop" operation.
|
|
|
|
:param node: The node to be deployed
|
|
:type node: :class:`Node`
|
|
|
|
:param shutdown: Whether to shutdown or power off the guest when
|
|
undeploying
|
|
:type shutdown: ``bool``
|
|
|
|
:rtype: :class:`Node`
|
|
"""
|
|
data = {'xmlns': 'http://www.vmware.com/vcloud/v1.5'}
|
|
undeploy_xml = ET.Element('UndeployVAppParams', data)
|
|
undeploy_power_action_xml = ET.SubElement(undeploy_xml,
|
|
'UndeployPowerAction')
|
|
|
|
headers = {
|
|
'Content-Type':
|
|
'application/vnd.vmware.vcloud.undeployVAppParams+xml'
|
|
}
|
|
|
|
def undeploy(action):
|
|
undeploy_power_action_xml.text = action
|
|
undeploy_res = self.connection.request(
|
|
'%s/action/undeploy' % get_url_path(node.id),
|
|
data=ET.tostring(undeploy_xml),
|
|
method='POST',
|
|
headers=headers)
|
|
self._wait_for_task_completion(undeploy_res.object.get('href'))
|
|
|
|
if shutdown:
|
|
try:
|
|
undeploy('shutdown')
|
|
except Exception:
|
|
undeploy('powerOff')
|
|
else:
|
|
undeploy('powerOff')
|
|
|
|
res = self.connection.request(get_url_path(node.id))
|
|
return self._to_node(res.object)
|
|
|
|
def ex_power_off_node(self, node):
|
|
"""
|
|
Powers on all VMs under specified node. VMs need to be This operation
|
|
is allowed only when the vApp/VM is powered on.
|
|
|
|
:param node: The node to be powered off
|
|
:type node: :class:`Node`
|
|
|
|
:rtype: :class:`Node`
|
|
"""
|
|
return self._perform_power_operation(node, 'powerOff')
|
|
|
|
def ex_power_on_node(self, node):
|
|
"""
|
|
Powers on all VMs under specified node. This operation is allowed
|
|
only when the vApp/VM is powered off or suspended.
|
|
|
|
:param node: The node to be powered on
|
|
:type node: :class:`Node`
|
|
|
|
:rtype: :class:`Node`
|
|
"""
|
|
return self._perform_power_operation(node, 'powerOn')
|
|
|
|
def ex_shutdown_node(self, node):
|
|
"""
|
|
Shutdowns all VMs under specified node. This operation is allowed only
|
|
when the vApp/VM is powered on.
|
|
|
|
:param node: The node to be shut down
|
|
:type node: :class:`Node`
|
|
|
|
:rtype: :class:`Node`
|
|
"""
|
|
return self._perform_power_operation(node, 'shutdown')
|
|
|
|
def ex_suspend_node(self, node):
|
|
"""
|
|
Suspends all VMs under specified node. This operation is allowed only
|
|
when the vApp/VM is powered on.
|
|
|
|
:param node: The node to be suspended
|
|
:type node: :class:`Node`
|
|
|
|
:rtype: :class:`Node`
|
|
"""
|
|
return self._perform_power_operation(node, 'suspend')
|
|
|
|
def _perform_power_operation(self, node, operation):
|
|
res = self.connection.request(
|
|
'%s/power/action/%s' % (get_url_path(node.id), operation),
|
|
method='POST')
|
|
self._wait_for_task_completion(res.object.get('href'))
|
|
res = self.connection.request(get_url_path(node.id))
|
|
return self._to_node(res.object)
|
|
|
|
def ex_get_control_access(self, node):
|
|
"""
|
|
Returns the control access settings for specified node.
|
|
|
|
:param node: node to get the control access for
|
|
:type node: :class:`Node`
|
|
|
|
:rtype: :class:`ControlAccess`
|
|
"""
|
|
res = self.connection.request(
|
|
'%s/controlAccess' % get_url_path(node.id))
|
|
everyone_access_level = None
|
|
is_shared_elem = res.object.find(
|
|
fixxpath(res.object, "IsSharedToEveryone"))
|
|
if is_shared_elem is not None and is_shared_elem.text == 'true':
|
|
everyone_access_level = res.object.find(
|
|
fixxpath(res.object, "EveryoneAccessLevel")).text
|
|
|
|
# Parse all subjects
|
|
subjects = []
|
|
xpath = fixxpath(res.object, "AccessSettings/AccessSetting")
|
|
for elem in res.object.findall(xpath):
|
|
access_level = elem.find(fixxpath(res.object, "AccessLevel")).text
|
|
subject_elem = elem.find(fixxpath(res.object, "Subject"))
|
|
if subject_elem.get('type') == \
|
|
'application/vnd.vmware.admin.group+xml':
|
|
subj_type = 'group'
|
|
else:
|
|
subj_type = 'user'
|
|
|
|
path = get_url_path(subject_elem.get('href'))
|
|
res = self.connection.request(path)
|
|
name = res.object.get('name')
|
|
subject = Subject(type=subj_type,
|
|
name=name,
|
|
access_level=access_level,
|
|
id=subject_elem.get('href'))
|
|
subjects.append(subject)
|
|
|
|
return ControlAccess(node, everyone_access_level, subjects)
|
|
|
|
def ex_set_control_access(self, node, control_access):
|
|
"""
|
|
Sets control access for the specified node.
|
|
|
|
:param node: node
|
|
:type node: :class:`Node`
|
|
|
|
:param control_access: control access settings
|
|
:type control_access: :class:`ControlAccess`
|
|
|
|
:rtype: ``None``
|
|
"""
|
|
xml = ET.Element('ControlAccessParams',
|
|
{'xmlns': 'http://www.vmware.com/vcloud/v1.5'})
|
|
shared_to_everyone = ET.SubElement(xml, 'IsSharedToEveryone')
|
|
if control_access.everyone_access_level:
|
|
shared_to_everyone.text = 'true'
|
|
everyone_access_level = ET.SubElement(xml, 'EveryoneAccessLevel')
|
|
everyone_access_level.text = control_access.everyone_access_level
|
|
else:
|
|
shared_to_everyone.text = 'false'
|
|
|
|
# Set subjects
|
|
if control_access.subjects:
|
|
access_settings_elem = ET.SubElement(xml, 'AccessSettings')
|
|
for subject in control_access.subjects:
|
|
setting = ET.SubElement(access_settings_elem, 'AccessSetting')
|
|
if subject.id:
|
|
href = subject.id
|
|
else:
|
|
res = self.ex_query(type=subject.type, filter='name==' +
|
|
subject.name)
|
|
if not res:
|
|
raise LibcloudError('Specified subject "%s %s" not found '
|
|
% (subject.type, subject.name))
|
|
href = res[0]['href']
|
|
ET.SubElement(setting, 'Subject', {'href': href})
|
|
ET.SubElement(setting, 'AccessLevel').text = subject.access_level
|
|
|
|
headers = {
|
|
'Content-Type': 'application/vnd.vmware.vcloud.controlAccess+xml'
|
|
}
|
|
self.connection.request(
|
|
'%s/action/controlAccess' % get_url_path(node.id),
|
|
data=ET.tostring(xml),
|
|
headers=headers,
|
|
method='POST')
|
|
|
|
def ex_get_metadata(self, node):
|
|
"""
|
|
:param node: node
|
|
:type node: :class:`Node`
|
|
|
|
:return: dictionary mapping metadata keys to metadata values
|
|
:rtype: dictionary mapping ``str`` to ``str``
|
|
"""
|
|
res = self.connection.request('%s/metadata' % (get_url_path(node.id)))
|
|
xpath = fixxpath(res.object, 'MetadataEntry')
|
|
metadata_entries = res.object.findall(xpath)
|
|
res_dict = {}
|
|
|
|
for entry in metadata_entries:
|
|
key = entry.findtext(fixxpath(res.object, 'Key'))
|
|
value = entry.findtext(fixxpath(res.object, 'Value'))
|
|
res_dict[key] = value
|
|
|
|
return res_dict
|
|
|
|
def ex_set_metadata_entry(self, node, key, value):
|
|
"""
|
|
:param node: node
|
|
:type node: :class:`Node`
|
|
|
|
:param key: metadata key to be set
|
|
:type key: ``str``
|
|
|
|
:param value: metadata value to be set
|
|
:type value: ``str``
|
|
|
|
:rtype: ``None``
|
|
"""
|
|
metadata_elem = ET.Element(
|
|
'Metadata',
|
|
{'xmlns': "http://www.vmware.com/vcloud/v1.5",
|
|
'xmlns:xsi': "http://www.w3.org/2001/XMLSchema-instance"}
|
|
)
|
|
entry = ET.SubElement(metadata_elem, 'MetadataEntry')
|
|
key_elem = ET.SubElement(entry, 'Key')
|
|
key_elem.text = key
|
|
value_elem = ET.SubElement(entry, 'Value')
|
|
value_elem.text = value
|
|
|
|
# send it back to the server
|
|
res = self.connection.request(
|
|
'%s/metadata' % get_url_path(node.id),
|
|
data=ET.tostring(metadata_elem),
|
|
headers={
|
|
'Content-Type': 'application/vnd.vmware.vcloud.metadata+xml'
|
|
},
|
|
method='POST')
|
|
self._wait_for_task_completion(res.object.get('href'))
|
|
|
|
def ex_query(self, type, filter=None, page=1, page_size=100, sort_asc=None,
|
|
sort_desc=None):
|
|
"""
|
|
Queries vCloud for specified type. See
|
|
http://www.vmware.com/pdf/vcd_15_api_guide.pdf for details. Each
|
|
element of the returned list is a dictionary with all attributes from
|
|
the record.
|
|
|
|
:param type: type to query (r.g. user, group, vApp etc.)
|
|
:type type: ``str``
|
|
|
|
:param filter: filter expression (see documentation for syntax)
|
|
:type filter: ``str``
|
|
|
|
:param page: page number
|
|
:type page: ``int``
|
|
|
|
:param page_size: page size
|
|
:type page_size: ``int``
|
|
|
|
:param sort_asc: sort in ascending order by specified field
|
|
:type sort_asc: ``str``
|
|
|
|
:param sort_desc: sort in descending order by specified field
|
|
:type sort_desc: ``str``
|
|
|
|
:rtype: ``list`` of dict
|
|
"""
|
|
# This is a workaround for filter parameter encoding
|
|
# the urllib encodes (name==Developers%20Only) into
|
|
# %28name%3D%3DDevelopers%20Only%29) which is not accepted by vCloud
|
|
params = {
|
|
'type': type,
|
|
'pageSize': page_size,
|
|
'page': page,
|
|
}
|
|
if sort_asc:
|
|
params['sortAsc'] = sort_asc
|
|
if sort_desc:
|
|
params['sortDesc'] = sort_desc
|
|
|
|
url = '/api/query?' + urlencode(params)
|
|
if filter:
|
|
if not filter.startswith('('):
|
|
filter = '(' + filter + ')'
|
|
url += '&filter=' + filter.replace(' ', '+')
|
|
|
|
results = []
|
|
res = self.connection.request(url)
|
|
for elem in res.object:
|
|
if not elem.tag.endswith('Link'):
|
|
result = elem.attrib
|
|
result['type'] = elem.tag.split('}')[1]
|
|
results.append(result)
|
|
return results
|
|
|
|
def create_node(self, **kwargs):
|
|
"""
|
|
Creates and returns node. If the source image is:
|
|
- vApp template - a new vApp is instantiated from template
|
|
- existing vApp - a new vApp is cloned from the source vApp. Can
|
|
not clone more vApps is parallel otherwise
|
|
resource busy error is raised.
|
|
|
|
|
|
@inherits: :class:`NodeDriver.create_node`
|
|
|
|
:keyword image: OS Image to boot on node. (required). Can be a
|
|
NodeImage or existing Node that will be cloned.
|
|
:type image: :class:`NodeImage` or :class:`Node`
|
|
|
|
:keyword ex_network: Organisation's network name for attaching vApp
|
|
VMs to.
|
|
:type ex_network: ``str``
|
|
|
|
:keyword ex_vdc: Name of organisation's virtual data center where
|
|
vApp VMs will be deployed.
|
|
:type ex_vdc: ``str``
|
|
|
|
:keyword ex_vm_names: list of names to be used as a VM and computer
|
|
name. The name must be max. 15 characters
|
|
long and follow the host name requirements.
|
|
:type ex_vm_names: ``list`` of ``str``
|
|
|
|
:keyword ex_vm_cpu: number of virtual CPUs/cores to allocate for
|
|
each vApp VM.
|
|
:type ex_vm_cpu: ``int``
|
|
|
|
:keyword ex_vm_memory: amount of memory in MB to allocate for each
|
|
vApp VM.
|
|
:type ex_vm_memory: ``int``
|
|
|
|
:keyword ex_vm_script: full path to file containing guest
|
|
customisation script for each vApp VM.
|
|
Useful for creating users & pushing out
|
|
public SSH keys etc.
|
|
:type ex_vm_script: ``str``
|
|
|
|
:keyword ex_vm_script_text: content of guest customisation script
|
|
for each vApp VM. Overrides ex_vm_script
|
|
parameter.
|
|
:type ex_vm_script_text: ``str``
|
|
|
|
:keyword ex_vm_network: Override default vApp VM network name.
|
|
Useful for when you've imported an OVF
|
|
originating from outside of the vCloud.
|
|
:type ex_vm_network: ``str``
|
|
|
|
:keyword ex_vm_fence: Fence mode for connecting the vApp VM network
|
|
(ex_vm_network) to the parent
|
|
organisation network (ex_network).
|
|
:type ex_vm_fence: ``str``
|
|
|
|
:keyword ex_vm_ipmode: IP address allocation mode for all vApp VM
|
|
network connections.
|
|
:type ex_vm_ipmode: ``str``
|
|
|
|
:keyword ex_deploy: set to False if the node shouldn't be deployed
|
|
(started) after creation
|
|
:type ex_deploy: ``bool``
|
|
|
|
:keyword ex_force_customization: Used to specify whether to force
|
|
customization on deployment,
|
|
if not set default value is False.
|
|
:type ex_force_customization: ``bool``
|
|
|
|
:keyword ex_clone_timeout: timeout in seconds for clone/instantiate
|
|
VM operation.
|
|
Cloning might be a time consuming
|
|
operation especially when linked clones
|
|
are disabled or VMs are created on
|
|
different datastores.
|
|
Overrides the default task completion
|
|
value.
|
|
:type ex_clone_timeout: ``int``
|
|
|
|
:keyword ex_admin_password: set the node admin password explicitly.
|
|
:type ex_admin_password: ``str``
|
|
|
|
:keyword ex_description: Set a description for the vApp.
|
|
:type ex_description: ``str``
|
|
"""
|
|
name = kwargs['name']
|
|
image = kwargs['image']
|
|
ex_vm_names = kwargs.get('ex_vm_names')
|
|
ex_vm_cpu = kwargs.get('ex_vm_cpu')
|
|
ex_vm_memory = kwargs.get('ex_vm_memory')
|
|
ex_vm_script = kwargs.get('ex_vm_script')
|
|
ex_vm_script_text = kwargs.get('ex_vm_script_text', None)
|
|
ex_vm_fence = kwargs.get('ex_vm_fence', None)
|
|
ex_network = kwargs.get('ex_network', None)
|
|
ex_vm_network = kwargs.get('ex_vm_network', None)
|
|
ex_vm_ipmode = kwargs.get('ex_vm_ipmode', None)
|
|
ex_deploy = kwargs.get('ex_deploy', True)
|
|
ex_force_customization = kwargs.get('ex_force_customization', False)
|
|
ex_vdc = kwargs.get('ex_vdc', None)
|
|
ex_clone_timeout = kwargs.get('ex_clone_timeout',
|
|
DEFAULT_TASK_COMPLETION_TIMEOUT)
|
|
ex_admin_password = kwargs.get('ex_admin_password', None)
|
|
ex_description = kwargs.get('ex_description', None)
|
|
|
|
self._validate_vm_names(ex_vm_names)
|
|
self._validate_vm_cpu(ex_vm_cpu)
|
|
self._validate_vm_memory(ex_vm_memory)
|
|
self._validate_vm_fence(ex_vm_fence)
|
|
self._validate_vm_ipmode(ex_vm_ipmode)
|
|
ex_vm_script = self._validate_vm_script(ex_vm_script)
|
|
|
|
# Some providers don't require a network link
|
|
if ex_network:
|
|
network_href = self._get_network_href(ex_network)
|
|
network_elem = self.connection.request(
|
|
get_url_path(network_href)).object
|
|
else:
|
|
network_elem = None
|
|
|
|
vdc = self._get_vdc(ex_vdc)
|
|
|
|
if self._is_node(image):
|
|
vapp_name, vapp_href = self._clone_node(
|
|
name,
|
|
image,
|
|
vdc,
|
|
ex_clone_timeout
|
|
)
|
|
else:
|
|
vapp_name, vapp_href = self._instantiate_node(
|
|
name,
|
|
image,
|
|
network_elem,
|
|
vdc,
|
|
ex_vm_network,
|
|
ex_vm_fence,
|
|
ex_clone_timeout,
|
|
description=ex_description
|
|
)
|
|
|
|
self._change_vm_names(vapp_href, ex_vm_names)
|
|
self._change_vm_cpu(vapp_href, ex_vm_cpu)
|
|
self._change_vm_memory(vapp_href, ex_vm_memory)
|
|
self._change_vm_script(vapp_href, ex_vm_script, ex_vm_script_text)
|
|
self._change_vm_ipmode(vapp_href, ex_vm_ipmode)
|
|
|
|
if ex_admin_password is not None:
|
|
self.ex_change_vm_admin_password(vapp_href, ex_admin_password)
|
|
|
|
# Power on the VM.
|
|
if ex_deploy:
|
|
res = self.connection.request(get_url_path(vapp_href))
|
|
node = self._to_node(res.object)
|
|
# Retry 3 times: when instantiating large number of VMs at the same
|
|
# time some may fail on resource allocation
|
|
retry = 3
|
|
while True:
|
|
try:
|
|
self.ex_deploy_node(node, ex_force_customization)
|
|
break
|
|
except Exception:
|
|
if retry <= 0:
|
|
raise
|
|
retry -= 1
|
|
time.sleep(10)
|
|
|
|
res = self.connection.request(get_url_path(vapp_href))
|
|
node = self._to_node(res.object)
|
|
return node
|
|
|
|
def _instantiate_node(self, name, image, network_elem, vdc, vm_network,
|
|
vm_fence, instantiate_timeout, description=None):
|
|
instantiate_xml = Instantiate_1_5_VAppXML(
|
|
name=name,
|
|
template=image.id,
|
|
network=network_elem,
|
|
vm_network=vm_network,
|
|
vm_fence=vm_fence,
|
|
description=description
|
|
)
|
|
|
|
# Instantiate VM and get identifier.
|
|
headers = {
|
|
'Content-Type':
|
|
'application/vnd.vmware.vcloud.instantiateVAppTemplateParams+xml'
|
|
}
|
|
res = self.connection.request(
|
|
'%s/action/instantiateVAppTemplate' % get_url_path(vdc.id),
|
|
data=instantiate_xml.tostring(),
|
|
method='POST',
|
|
headers=headers
|
|
)
|
|
vapp_name = res.object.get('name')
|
|
vapp_href = res.object.get('href')
|
|
|
|
task_href = res.object.find(fixxpath(res.object, "Tasks/Task")).get(
|
|
'href')
|
|
self._wait_for_task_completion(task_href, instantiate_timeout)
|
|
return vapp_name, vapp_href
|
|
|
|
def _clone_node(self, name, sourceNode, vdc, clone_timeout):
|
|
clone_xml = ET.Element(
|
|
"CloneVAppParams",
|
|
{'name': name, 'deploy': 'false', 'powerOn': 'false',
|
|
'xmlns': "http://www.vmware.com/vcloud/v1.5",
|
|
'xmlns:xsi': "http://www.w3.org/2001/XMLSchema-instance"}
|
|
)
|
|
ET.SubElement(clone_xml,
|
|
'Description').text = 'Clone of ' + sourceNode.name
|
|
ET.SubElement(clone_xml, 'Source', {'href': sourceNode.id})
|
|
|
|
headers = {
|
|
'Content-Type': 'application/vnd.vmware.vcloud.cloneVAppParams+xml'
|
|
}
|
|
res = self.connection.request(
|
|
'%s/action/cloneVApp' % get_url_path(vdc.id),
|
|
data=ET.tostring(clone_xml),
|
|
method='POST',
|
|
headers=headers
|
|
)
|
|
vapp_name = res.object.get('name')
|
|
vapp_href = res.object.get('href')
|
|
|
|
task_href = res.object.find(
|
|
fixxpath(res.object, "Tasks/Task")).get('href')
|
|
self._wait_for_task_completion(task_href, clone_timeout)
|
|
|
|
res = self.connection.request(get_url_path(vapp_href))
|
|
|
|
vms = res.object.findall(fixxpath(res.object, "Children/Vm"))
|
|
|
|
# Fix the networking for VMs
|
|
for i, vm in enumerate(vms):
|
|
# Remove network
|
|
network_xml = ET.Element("NetworkConnectionSection", {
|
|
'ovf:required': 'false',
|
|
'xmlns': "http://www.vmware.com/vcloud/v1.5",
|
|
'xmlns:ovf': 'http://schemas.dmtf.org/ovf/envelope/1'})
|
|
ET.SubElement(network_xml, "ovf:Info").text = \
|
|
'Specifies the available VM network connections'
|
|
|
|
headers = {
|
|
'Content-Type':
|
|
'application/vnd.vmware.vcloud.networkConnectionSection+xml'
|
|
}
|
|
res = self.connection.request(
|
|
'%s/networkConnectionSection' % get_url_path(vm.get('href')),
|
|
data=ET.tostring(network_xml),
|
|
method='PUT',
|
|
headers=headers
|
|
)
|
|
self._wait_for_task_completion(res.object.get('href'))
|
|
|
|
# Re-add network
|
|
network_xml = vm.find(fixxpath(vm, 'NetworkConnectionSection'))
|
|
network_conn_xml = network_xml.find(
|
|
fixxpath(network_xml, 'NetworkConnection'))
|
|
network_conn_xml.set('needsCustomization', 'true')
|
|
network_conn_xml.remove(
|
|
network_conn_xml.find(fixxpath(network_xml, 'IpAddress')))
|
|
network_conn_xml.remove(
|
|
network_conn_xml.find(fixxpath(network_xml, 'MACAddress')))
|
|
|
|
headers = {
|
|
'Content-Type':
|
|
'application/vnd.vmware.vcloud.networkConnectionSection+xml'
|
|
}
|
|
res = self.connection.request(
|
|
'%s/networkConnectionSection' % get_url_path(vm.get('href')),
|
|
data=ET.tostring(network_xml),
|
|
method='PUT',
|
|
headers=headers
|
|
)
|
|
self._wait_for_task_completion(res.object.get('href'))
|
|
|
|
return vapp_name, vapp_href
|
|
|
|
def ex_set_vm_cpu(self, vapp_or_vm_id, vm_cpu):
|
|
"""
|
|
Sets the number of virtual CPUs for the specified VM or VMs under
|
|
the vApp. If the vapp_or_vm_id param represents a link to an vApp
|
|
all VMs that are attached to this vApp will be modified.
|
|
|
|
Please ensure that hot-adding a virtual CPU is enabled for the
|
|
powered on virtual machines. Otherwise use this method on undeployed
|
|
vApp.
|
|
|
|
:keyword vapp_or_vm_id: vApp or VM ID that will be modified. If
|
|
a vApp ID is used here all attached VMs
|
|
will be modified
|
|
:type vapp_or_vm_id: ``str``
|
|
|
|
:keyword vm_cpu: number of virtual CPUs/cores to allocate for
|
|
specified VMs
|
|
:type vm_cpu: ``int``
|
|
|
|
:rtype: ``None``
|
|
"""
|
|
self._validate_vm_cpu(vm_cpu)
|
|
self._change_vm_cpu(vapp_or_vm_id, vm_cpu)
|
|
|
|
def ex_set_vm_memory(self, vapp_or_vm_id, vm_memory):
|
|
"""
|
|
Sets the virtual memory in MB to allocate for the specified VM or
|
|
VMs under the vApp. If the vapp_or_vm_id param represents a link
|
|
to an vApp all VMs that are attached to this vApp will be modified.
|
|
|
|
Please ensure that hot-change of virtual memory is enabled for the
|
|
powered on virtual machines. Otherwise use this method on undeployed
|
|
vApp.
|
|
|
|
:keyword vapp_or_vm_id: vApp or VM ID that will be modified. If
|
|
a vApp ID is used here all attached VMs
|
|
will be modified
|
|
:type vapp_or_vm_id: ``str``
|
|
|
|
:keyword vm_memory: virtual memory in MB to allocate for the
|
|
specified VM or VMs
|
|
:type vm_memory: ``int``
|
|
|
|
:rtype: ``None``
|
|
"""
|
|
self._validate_vm_memory(vm_memory)
|
|
self._change_vm_memory(vapp_or_vm_id, vm_memory)
|
|
|
|
def ex_add_vm_disk(self, vapp_or_vm_id, vm_disk_size):
|
|
"""
|
|
Adds a virtual disk to the specified VM or VMs under the vApp. If the
|
|
vapp_or_vm_id param represents a link to an vApp all VMs that are
|
|
attached to this vApp will be modified.
|
|
|
|
:keyword vapp_or_vm_id: vApp or VM ID that will be modified. If a
|
|
vApp ID is used here all attached VMs
|
|
will be modified
|
|
:type vapp_or_vm_id: ``str``
|
|
|
|
:keyword vm_disk_size: the disk capacity in GB that will be added
|
|
to the specified VM or VMs
|
|
:type vm_disk_size: ``int``
|
|
|
|
:rtype: ``None``
|
|
"""
|
|
self._validate_vm_disk_size(vm_disk_size)
|
|
self._add_vm_disk(vapp_or_vm_id, vm_disk_size)
|
|
|
|
@staticmethod
|
|
def _validate_vm_names(names):
|
|
if names is None:
|
|
return
|
|
hname_re = re.compile(
|
|
r'^(([a-zA-Z]|[a-zA-Z][a-zA-Z0-9]*)[\-])*([A-Za-z]|[A-Za-z][A-Za-z0-9]*[A-Za-z0-9])$') # NOQA
|
|
for name in names:
|
|
if len(name) > 15:
|
|
raise ValueError(
|
|
'The VM name "' + name + '" is too long for the computer '
|
|
'name (max 15 chars allowed).')
|
|
if not hname_re.match(name):
|
|
raise ValueError('The VM name "' + name + '" can not be '
|
|
'used. "' + name + '" is not a valid '
|
|
'computer name for the VM.')
|
|
|
|
@staticmethod
|
|
def _validate_vm_memory(vm_memory):
|
|
if vm_memory is None:
|
|
return
|
|
elif vm_memory not in VIRTUAL_MEMORY_VALS:
|
|
raise ValueError(
|
|
'%s is not a valid vApp VM memory value' % vm_memory)
|
|
|
|
@staticmethod
|
|
def _validate_vm_cpu(vm_cpu):
|
|
if vm_cpu is None:
|
|
return
|
|
elif vm_cpu not in VIRTUAL_CPU_VALS_1_5:
|
|
raise ValueError('%s is not a valid vApp VM CPU value' % vm_cpu)
|
|
|
|
@staticmethod
|
|
def _validate_vm_disk_size(vm_disk):
|
|
if vm_disk is None:
|
|
return
|
|
elif int(vm_disk) < 0:
|
|
raise ValueError('%s is not a valid vApp VM disk space value',
|
|
vm_disk)
|
|
|
|
@staticmethod
|
|
def _validate_vm_script(vm_script):
|
|
if vm_script is None:
|
|
return
|
|
# Try to locate the script file
|
|
if not os.path.isabs(vm_script):
|
|
vm_script = os.path.expanduser(vm_script)
|
|
vm_script = os.path.abspath(vm_script)
|
|
if not os.path.isfile(vm_script):
|
|
raise LibcloudError(
|
|
"%s the VM script file does not exist" % vm_script)
|
|
try:
|
|
open(vm_script).read()
|
|
except Exception:
|
|
raise
|
|
return vm_script
|
|
|
|
@staticmethod
|
|
def _validate_vm_fence(vm_fence):
|
|
if vm_fence is None:
|
|
return
|
|
elif vm_fence not in FENCE_MODE_VALS_1_5:
|
|
raise ValueError('%s is not a valid fencing mode value' % vm_fence)
|
|
|
|
@staticmethod
|
|
def _validate_vm_ipmode(vm_ipmode):
|
|
if vm_ipmode is None:
|
|
return
|
|
elif vm_ipmode == 'MANUAL':
|
|
raise NotImplementedError(
|
|
'MANUAL IP mode: The interface for supplying '
|
|
'IPAddress does not exist yet')
|
|
elif vm_ipmode not in IP_MODE_VALS_1_5:
|
|
raise ValueError(
|
|
'%s is not a valid IP address allocation mode value'
|
|
% vm_ipmode)
|
|
|
|
def _change_vm_names(self, vapp_or_vm_id, vm_names):
|
|
if vm_names is None:
|
|
return
|
|
|
|
vms = self._get_vm_elements(vapp_or_vm_id)
|
|
for i, vm in enumerate(vms):
|
|
if len(vm_names) <= i:
|
|
return
|
|
|
|
# Get GuestCustomizationSection
|
|
res = self.connection.request(
|
|
'%s/guestCustomizationSection' % get_url_path(vm.get('href')))
|
|
|
|
# Update GuestCustomizationSection
|
|
res.object.find(
|
|
fixxpath(res.object, 'ComputerName')).text = vm_names[i]
|
|
# Remove AdminPassword from customization section if it would be
|
|
# invalid to include it
|
|
self._remove_admin_password(res.object)
|
|
|
|
headers = {
|
|
'Content-Type':
|
|
'application/vnd.vmware.vcloud.guestCustomizationSection+xml'
|
|
}
|
|
res = self.connection.request(
|
|
'%s/guestCustomizationSection' % get_url_path(vm.get('href')),
|
|
data=ET.tostring(res.object),
|
|
method='PUT',
|
|
headers=headers
|
|
)
|
|
self._wait_for_task_completion(res.object.get('href'))
|
|
|
|
# Update Vm name
|
|
req_xml = ET.Element("Vm", {
|
|
'name': vm_names[i],
|
|
'xmlns': "http://www.vmware.com/vcloud/v1.5"})
|
|
res = self.connection.request(
|
|
get_url_path(vm.get('href')),
|
|
data=ET.tostring(req_xml),
|
|
method='PUT',
|
|
headers={
|
|
'Content-Type': 'application/vnd.vmware.vcloud.vm+xml'}
|
|
)
|
|
self._wait_for_task_completion(res.object.get('href'))
|
|
|
|
def _change_vm_cpu(self, vapp_or_vm_id, vm_cpu):
|
|
if vm_cpu is None:
|
|
return
|
|
|
|
vms = self._get_vm_elements(vapp_or_vm_id)
|
|
for vm in vms:
|
|
# Get virtualHardwareSection/cpu section
|
|
res = self.connection.request(
|
|
'%s/virtualHardwareSection/cpu' % get_url_path(vm.get('href')))
|
|
|
|
# Update VirtualQuantity field
|
|
xpath = ('{http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/'
|
|
'CIM_ResourceAllocationSettingData}VirtualQuantity')
|
|
res.object.find(xpath).text = str(vm_cpu)
|
|
|
|
headers = {
|
|
'Content-Type': 'application/vnd.vmware.vcloud.rasdItem+xml'
|
|
}
|
|
res = self.connection.request(
|
|
'%s/virtualHardwareSection/cpu' % get_url_path(vm.get('href')),
|
|
data=ET.tostring(res.object),
|
|
method='PUT',
|
|
headers=headers
|
|
)
|
|
self._wait_for_task_completion(res.object.get('href'))
|
|
|
|
def _change_vm_memory(self, vapp_or_vm_id, vm_memory):
|
|
if vm_memory is None:
|
|
return
|
|
|
|
vms = self._get_vm_elements(vapp_or_vm_id)
|
|
for vm in vms:
|
|
# Get virtualHardwareSection/memory section
|
|
res = self.connection.request(
|
|
'%s/virtualHardwareSection/memory' %
|
|
get_url_path(vm.get('href')))
|
|
|
|
# Update VirtualQuantity field
|
|
xpath = ('{http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/'
|
|
'CIM_ResourceAllocationSettingData}VirtualQuantity')
|
|
res.object.find(xpath).text = str(vm_memory)
|
|
|
|
headers = {
|
|
'Content-Type': 'application/vnd.vmware.vcloud.rasdItem+xml'
|
|
}
|
|
res = self.connection.request(
|
|
'%s/virtualHardwareSection/memory' % get_url_path(
|
|
vm.get('href')),
|
|
data=ET.tostring(res.object),
|
|
method='PUT',
|
|
headers=headers
|
|
)
|
|
self._wait_for_task_completion(res.object.get('href'))
|
|
|
|
def _add_vm_disk(self, vapp_or_vm_id, vm_disk):
|
|
if vm_disk is None:
|
|
return
|
|
|
|
rasd_ns = ('{http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/'
|
|
'CIM_ResourceAllocationSettingData}')
|
|
|
|
vms = self._get_vm_elements(vapp_or_vm_id)
|
|
for vm in vms:
|
|
# Get virtualHardwareSection/disks section
|
|
res = self.connection.request(
|
|
'%s/virtualHardwareSection/disks' %
|
|
get_url_path(vm.get('href')))
|
|
|
|
existing_ids = []
|
|
new_disk = None
|
|
for item in res.object.findall(fixxpath(res.object, 'Item')):
|
|
# Clean Items from unnecessary stuff
|
|
for elem in item:
|
|
if elem.tag == '%sInstanceID' % rasd_ns:
|
|
existing_ids.append(int(elem.text))
|
|
if elem.tag in ['%sAddressOnParent' % rasd_ns,
|
|
'%sParent' % rasd_ns]:
|
|
item.remove(elem)
|
|
if item.find('%sHostResource' % rasd_ns) is not None:
|
|
new_disk = item
|
|
|
|
new_disk = copy.deepcopy(new_disk)
|
|
disk_id = max(existing_ids) + 1
|
|
new_disk.find('%sInstanceID' % rasd_ns).text = str(disk_id)
|
|
new_disk.find('%sElementName' %
|
|
rasd_ns).text = 'Hard Disk ' + str(disk_id)
|
|
new_disk.find('%sHostResource' % rasd_ns).set(
|
|
fixxpath(new_disk, 'capacity'), str(int(vm_disk) * 1024))
|
|
res.object.append(new_disk)
|
|
|
|
headers = {
|
|
'Content-Type':
|
|
'application/vnd.vmware.vcloud.rasditemslist+xml'
|
|
}
|
|
res = self.connection.request(
|
|
'%s/virtualHardwareSection/disks' % get_url_path(
|
|
vm.get('href')),
|
|
data=ET.tostring(res.object),
|
|
method='PUT',
|
|
headers=headers
|
|
)
|
|
self._wait_for_task_completion(res.object.get('href'))
|
|
|
|
def _change_vm_script(self, vapp_or_vm_id, vm_script, vm_script_text=None):
|
|
if vm_script is None and vm_script_text is None:
|
|
return
|
|
|
|
if vm_script_text is not None:
|
|
script = vm_script_text
|
|
else:
|
|
try:
|
|
with open(vm_script, 'r') as fp:
|
|
script = fp.read()
|
|
except Exception:
|
|
return
|
|
|
|
vms = self._get_vm_elements(vapp_or_vm_id)
|
|
|
|
# ElementTree escapes script characters automatically. Escape
|
|
# requirements:
|
|
# http://www.vmware.com/support/vcd/doc/rest-api-doc-1.5-html/types/
|
|
# GuestCustomizationSectionType.html
|
|
for vm in vms:
|
|
# Get GuestCustomizationSection
|
|
res = self.connection.request(
|
|
'%s/guestCustomizationSection' % get_url_path(vm.get('href')))
|
|
|
|
# Attempt to update any existing CustomizationScript element
|
|
try:
|
|
res.object.find(
|
|
fixxpath(res.object, 'CustomizationScript')).text = script
|
|
except Exception:
|
|
# CustomizationScript section does not exist, insert it just
|
|
# before ComputerName
|
|
for i, e in enumerate(res.object):
|
|
if e.tag == \
|
|
'{http://www.vmware.com/vcloud/v1.5}ComputerName':
|
|
break
|
|
e = ET.Element(
|
|
'{http://www.vmware.com/vcloud/v1.5}CustomizationScript')
|
|
e.text = script
|
|
res.object.insert(i, e)
|
|
|
|
# Remove AdminPassword from customization section if it would be
|
|
# invalid to include it
|
|
self._remove_admin_password(res.object)
|
|
|
|
# Update VM's GuestCustomizationSection
|
|
headers = {
|
|
'Content-Type':
|
|
'application/vnd.vmware.vcloud.guestCustomizationSection+xml'
|
|
}
|
|
res = self.connection.request(
|
|
'%s/guestCustomizationSection' % get_url_path(vm.get('href')),
|
|
data=ET.tostring(res.object),
|
|
method='PUT',
|
|
headers=headers
|
|
)
|
|
self._wait_for_task_completion(res.object.get('href'))
|
|
|
|
def _change_vm_ipmode(self, vapp_or_vm_id, vm_ipmode):
|
|
if vm_ipmode is None:
|
|
return
|
|
|
|
vms = self._get_vm_elements(vapp_or_vm_id)
|
|
|
|
for vm in vms:
|
|
res = self.connection.request(
|
|
'%s/networkConnectionSection' % get_url_path(vm.get('href')))
|
|
net_conns = res.object.findall(
|
|
fixxpath(res.object, 'NetworkConnection'))
|
|
for c in net_conns:
|
|
c.find(fixxpath(c, 'IpAddressAllocationMode')).text = vm_ipmode
|
|
|
|
headers = {
|
|
'Content-Type':
|
|
'application/vnd.vmware.vcloud.networkConnectionSection+xml'
|
|
}
|
|
|
|
res = self.connection.request(
|
|
'%s/networkConnectionSection' % get_url_path(vm.get('href')),
|
|
data=ET.tostring(res.object),
|
|
method='PUT',
|
|
headers=headers
|
|
)
|
|
self._wait_for_task_completion(res.object.get('href'))
|
|
|
|
@staticmethod
|
|
def _remove_admin_password(guest_customization_section):
|
|
"""
|
|
Remove AdminPassword element from GuestCustomizationSection if it
|
|
would be invalid to include it.
|
|
|
|
This was originally done unconditionally due to an "API quirk" of
|
|
unknown origin or effect. When AdminPasswordEnabled is set to true
|
|
and AdminPasswordAuto is false, the admin password must be set or
|
|
an error will ensue, and vice versa.
|
|
:param guest_customization_section: GuestCustomizationSection element
|
|
to remove password from (if valid
|
|
to do so)
|
|
:type guest_customization_section: ``ET.Element``
|
|
"""
|
|
admin_pass_enabled = guest_customization_section.find(
|
|
fixxpath(guest_customization_section, 'AdminPasswordEnabled')
|
|
)
|
|
admin_pass_auto = guest_customization_section.find(
|
|
fixxpath(guest_customization_section, 'AdminPasswordAuto')
|
|
)
|
|
admin_pass = guest_customization_section.find(
|
|
fixxpath(guest_customization_section, 'AdminPassword')
|
|
)
|
|
if (
|
|
admin_pass is not None
|
|
and (
|
|
admin_pass_enabled is None or admin_pass_enabled.text != 'true'
|
|
or admin_pass_auto is None or admin_pass_auto.text != 'false'
|
|
)
|
|
):
|
|
guest_customization_section.remove(admin_pass)
|
|
|
|
def _update_or_insert_section(self, res, section, prev_section, text):
|
|
try:
|
|
res.object.find(
|
|
fixxpath(res.object, section)).text = text
|
|
except Exception:
|
|
# "section" section does not exist, insert it just
|
|
# before "prev_section"
|
|
for i, e in enumerate(res.object):
|
|
tag = '{http://www.vmware.com/vcloud/v1.5}%s' % prev_section
|
|
if e.tag == tag:
|
|
break
|
|
e = ET.Element(
|
|
'{http://www.vmware.com/vcloud/v1.5}%s' % section)
|
|
e.text = text
|
|
res.object.insert(i, e)
|
|
return res
|
|
|
|
def ex_change_vm_admin_password(self, vapp_or_vm_id, ex_admin_password):
|
|
"""
|
|
Changes the admin (or root) password of VM or VMs under the vApp. If
|
|
the vapp_or_vm_id param represents a link to an vApp all VMs that
|
|
are attached to this vApp will be modified.
|
|
|
|
:keyword vapp_or_vm_id: vApp or VM ID that will be modified. If a
|
|
vApp ID is used here all attached VMs
|
|
will be modified
|
|
:type vapp_or_vm_id: ``str``
|
|
|
|
:keyword ex_admin_password: admin password to be used.
|
|
:type ex_admin_password: ``str``
|
|
|
|
:rtype: ``None``
|
|
"""
|
|
if ex_admin_password is None:
|
|
return
|
|
|
|
vms = self._get_vm_elements(vapp_or_vm_id)
|
|
for vm in vms:
|
|
# Get GuestCustomizationSection
|
|
res = self.connection.request(
|
|
'%s/guestCustomizationSection' % get_url_path(vm.get('href')))
|
|
|
|
headers = {
|
|
'Content-Type':
|
|
'application/vnd.vmware.vcloud.guestCustomizationSection+xml'
|
|
}
|
|
|
|
# Fix API quirk.
|
|
# If AdminAutoLogonEnabled==False the guestCustomizationSection
|
|
# must have AdminAutoLogonCount==0, even though
|
|
# it might have AdminAutoLogonCount==1 when requesting it for
|
|
# the first time.
|
|
auto_logon = res.object.find(
|
|
fixxpath(res.object, "AdminAutoLogonEnabled"))
|
|
if auto_logon is not None and auto_logon.text == 'false':
|
|
self._update_or_insert_section(res,
|
|
"AdminAutoLogonCount",
|
|
"ResetPasswordRequired",
|
|
'0')
|
|
|
|
# If we are establishing a password we do not want it
|
|
# to be automatically chosen.
|
|
self._update_or_insert_section(res,
|
|
'AdminPasswordAuto',
|
|
'AdminPassword',
|
|
'false')
|
|
|
|
# API does not allow to set AdminPassword if
|
|
# AdminPasswordEnabled is not enabled.
|
|
self._update_or_insert_section(res,
|
|
'AdminPasswordEnabled',
|
|
'AdminPasswordAuto',
|
|
'true')
|
|
|
|
self._update_or_insert_section(res,
|
|
'AdminPassword',
|
|
'AdminAutoLogonEnabled',
|
|
ex_admin_password)
|
|
|
|
res = self.connection.request(
|
|
'%s/guestCustomizationSection' % get_url_path(vm.get('href')),
|
|
data=ET.tostring(res.object),
|
|
method='PUT',
|
|
headers=headers
|
|
)
|
|
self._wait_for_task_completion(res.object.get('href'))
|
|
|
|
def _get_network_href(self, network_name):
|
|
network_href = None
|
|
|
|
# Find the organisation's network href
|
|
res = self.connection.request(self.org)
|
|
links = res.object.findall(fixxpath(res.object, 'Link'))
|
|
for link in links:
|
|
if link.attrib['type'] == \
|
|
'application/vnd.vmware.vcloud.orgNetwork+xml' \
|
|
and link.attrib['name'] == network_name:
|
|
network_href = link.attrib['href']
|
|
|
|
if network_href is None:
|
|
raise ValueError(
|
|
'%s is not a valid organisation network name' % network_name)
|
|
else:
|
|
return network_href
|
|
|
|
def _ex_get_node(self, node_id):
|
|
"""
|
|
Get a node instance from a node ID.
|
|
|
|
:param node_id: ID of the node
|
|
:type node_id: ``str``
|
|
|
|
:return: node instance or None if not found
|
|
:rtype: :class:`Node` or ``None``
|
|
"""
|
|
res = self.connection.request(
|
|
get_url_path(node_id),
|
|
headers={'Content-Type': 'application/vnd.vmware.vcloud.vApp+xml'}
|
|
)
|
|
return self._to_node(res.object)
|
|
|
|
def _get_vm_elements(self, vapp_or_vm_id):
|
|
res = self.connection.request(get_url_path(vapp_or_vm_id))
|
|
if res.object.tag.endswith('VApp'):
|
|
vms = res.object.findall(fixxpath(res.object, 'Children/Vm'))
|
|
elif res.object.tag.endswith('Vm'):
|
|
vms = [res.object]
|
|
else:
|
|
raise ValueError(
|
|
'Specified ID value is not a valid VApp or Vm identifier.')
|
|
return vms
|
|
|
|
def _is_node(self, node_or_image):
|
|
return isinstance(node_or_image, Node)
|
|
|
|
def _to_node(self, node_elm):
|
|
# Parse snapshots and VMs as extra
|
|
if node_elm.find(fixxpath(node_elm, "SnapshotSection")) is None:
|
|
snapshots = None
|
|
else:
|
|
snapshots = []
|
|
for snapshot_elem in node_elm.findall(
|
|
fixxpath(node_elm, 'SnapshotSection/Snapshot')):
|
|
snapshots.append({
|
|
"created": snapshot_elem.get("created"),
|
|
"poweredOn": snapshot_elem.get("poweredOn"),
|
|
"size": snapshot_elem.get("size"),
|
|
})
|
|
|
|
vms = []
|
|
for vm_elem in node_elm.findall(fixxpath(node_elm, 'Children/Vm')):
|
|
public_ips = []
|
|
private_ips = []
|
|
|
|
xpath = fixxpath(vm_elem,
|
|
'NetworkConnectionSection/NetworkConnection')
|
|
for connection in vm_elem.findall(xpath):
|
|
ip = connection.find(fixxpath(connection, "IpAddress"))
|
|
if ip is not None:
|
|
private_ips.append(ip.text)
|
|
external_ip = connection.find(
|
|
fixxpath(connection, "ExternalIpAddress"))
|
|
if external_ip is not None:
|
|
public_ips.append(external_ip.text)
|
|
elif ip is not None:
|
|
public_ips.append(ip.text)
|
|
|
|
xpath = ('{http://schemas.dmtf.org/ovf/envelope/1}'
|
|
'OperatingSystemSection')
|
|
os_type_elem = vm_elem.find(xpath)
|
|
if os_type_elem is not None:
|
|
os_type = os_type_elem.get(
|
|
'{http://www.vmware.com/schema/ovf}osType')
|
|
else:
|
|
os_type = None
|
|
vm = {
|
|
'id': vm_elem.get('href'),
|
|
'name': vm_elem.get('name'),
|
|
'state': self.NODE_STATE_MAP[vm_elem.get('status')],
|
|
'public_ips': public_ips,
|
|
'private_ips': private_ips,
|
|
'os_type': os_type
|
|
}
|
|
vms.append(vm)
|
|
|
|
# Take the node IP addresses from all VMs
|
|
public_ips = []
|
|
private_ips = []
|
|
for vm in vms:
|
|
public_ips.extend(vm['public_ips'])
|
|
private_ips.extend(vm['private_ips'])
|
|
|
|
# Find vDC
|
|
vdc_id = next(link.get('href') for link
|
|
in node_elm.findall(fixxpath(node_elm, 'Link'))
|
|
if link.get('type') ==
|
|
'application/vnd.vmware.vcloud.vdc+xml'
|
|
) # pylint: disable=no-member
|
|
vdc = next(vdc for vdc in self.vdcs if vdc.id == vdc_id)
|
|
|
|
extra = {'vdc': vdc.name, 'vms': vms}
|
|
|
|
description = node_elm.find(fixxpath(node_elm, 'Description'))
|
|
if description is not None:
|
|
extra['description'] = description.text
|
|
else:
|
|
extra['description'] = ''
|
|
|
|
lease_settings = node_elm.find(
|
|
fixxpath(node_elm, 'LeaseSettingsSection')
|
|
)
|
|
if lease_settings is not None:
|
|
extra['lease_settings'] = Lease.to_lease(lease_settings)
|
|
else:
|
|
extra['lease_settings'] = None
|
|
|
|
if snapshots is not None:
|
|
extra['snapshots'] = snapshots
|
|
|
|
node = Node(id=node_elm.get('href'),
|
|
name=node_elm.get('name'),
|
|
state=self.NODE_STATE_MAP[node_elm.get('status')],
|
|
public_ips=public_ips,
|
|
private_ips=private_ips,
|
|
driver=self.connection.driver,
|
|
extra=extra)
|
|
return node
|
|
|
|
def _to_vdc(self, vdc_elm):
|
|
|
|
def get_capacity_values(capacity_elm):
|
|
if capacity_elm is None:
|
|
return None
|
|
limit = int(capacity_elm.findtext(fixxpath(capacity_elm, 'Limit')))
|
|
used = int(capacity_elm.findtext(fixxpath(capacity_elm, 'Used')))
|
|
units = capacity_elm.findtext(fixxpath(capacity_elm, 'Units'))
|
|
return Capacity(limit, used, units)
|
|
|
|
cpu = get_capacity_values(
|
|
vdc_elm.find(fixxpath(vdc_elm, 'ComputeCapacity/Cpu')))
|
|
memory = get_capacity_values(
|
|
vdc_elm.find(fixxpath(vdc_elm, 'ComputeCapacity/Memory')))
|
|
storage = get_capacity_values(
|
|
vdc_elm.find(fixxpath(vdc_elm, 'StorageCapacity')))
|
|
|
|
return Vdc(id=vdc_elm.get('href'),
|
|
name=vdc_elm.get('name'),
|
|
driver=self,
|
|
allocation_model=vdc_elm.findtext(
|
|
fixxpath(vdc_elm, 'AllocationModel')),
|
|
cpu=cpu,
|
|
memory=memory,
|
|
storage=storage)
|
|
|
|
|
|
class VCloud_5_1_NodeDriver(VCloud_1_5_NodeDriver):
|
|
|
|
@staticmethod
|
|
def _validate_vm_memory(vm_memory):
|
|
if vm_memory is None:
|
|
return None
|
|
elif (vm_memory % 4) != 0:
|
|
# The vcd 5.1 virtual machine memory size must be a multiple of 4
|
|
# MB
|
|
raise ValueError(
|
|
'%s is not a valid vApp VM memory value' % (vm_memory))
|
|
|
|
|
|
class VCloud_5_5_NodeDriver(VCloud_5_1_NodeDriver):
|
|
"""Use 5.5 Connection class to explicitly set 5.5 for the version in
|
|
Accept headers
|
|
"""
|
|
connectionCls = VCloud_5_5_Connection
|
|
|
|
def ex_create_snapshot(self, node):
|
|
"""
|
|
Creates new snapshot of a virtual machine or of all
|
|
the virtual machines in a vApp. Prior to creation of the new
|
|
snapshots, any existing user created snapshots associated
|
|
with the virtual machines are removed.
|
|
|
|
:param node: node
|
|
:type node: :class:`Node`
|
|
|
|
:rtype: :class:`Node`
|
|
"""
|
|
snapshot_xml = ET.Element(
|
|
"CreateSnapshotParams",
|
|
{'memory': 'true',
|
|
'name': 'name',
|
|
'quiesce': 'true',
|
|
'xmlns': "http://www.vmware.com/vcloud/v1.5",
|
|
'xmlns:xsi': "http://www.w3.org/2001/XMLSchema-instance"}
|
|
)
|
|
ET.SubElement(snapshot_xml, 'Description').text = 'Description'
|
|
content_type = 'application/vnd.vmware.vcloud.createSnapshotParams+xml'
|
|
headers = {
|
|
'Content-Type': content_type
|
|
}
|
|
return self._perform_snapshot_operation(node,
|
|
"createSnapshot",
|
|
snapshot_xml,
|
|
headers)
|
|
|
|
def ex_remove_snapshots(self, node):
|
|
"""
|
|
Removes all user created snapshots for a vApp or virtual machine.
|
|
|
|
:param node: node
|
|
:type node: :class:`Node`
|
|
|
|
:rtype: :class:`Node`
|
|
"""
|
|
return self._perform_snapshot_operation(node,
|
|
"removeAllSnapshots",
|
|
None,
|
|
None)
|
|
|
|
def ex_revert_to_snapshot(self, node):
|
|
"""
|
|
Reverts a vApp or virtual machine to the current snapshot, if any.
|
|
|
|
:param node: node
|
|
:type node: :class:`Node`
|
|
|
|
:rtype: :class:`Node`
|
|
"""
|
|
return self._perform_snapshot_operation(node,
|
|
"revertToCurrentSnapshot",
|
|
None,
|
|
None)
|
|
|
|
def _perform_snapshot_operation(self, node, operation, xml_data, headers):
|
|
res = self.connection.request(
|
|
'%s/action/%s' % (get_url_path(node.id), operation),
|
|
data=ET.tostring(xml_data) if xml_data is not None else None,
|
|
method='POST',
|
|
headers=headers)
|
|
self._wait_for_task_completion(res.object.get('href'))
|
|
res = self.connection.request(get_url_path(node.id))
|
|
return self._to_node(res.object)
|
|
|
|
def ex_acquire_mks_ticket(self, vapp_or_vm_id, vm_num=0):
|
|
"""
|
|
Retrieve a mks ticket that you can use to gain access to the console
|
|
of a running VM. If successful, returns a dict with the following
|
|
keys:
|
|
|
|
- host: host (or proxy) through which the console connection
|
|
is made
|
|
- vmx: a reference to the VMX file of the VM for which this
|
|
ticket was issued
|
|
- ticket: screen ticket to use to authenticate the client
|
|
- port: host port to be used for console access
|
|
|
|
:param vapp_or_vm_id: vApp or VM ID you want to connect to.
|
|
:type vapp_or_vm_id: ``str``
|
|
|
|
:param vm_num: If a vApp ID is provided, vm_num is position in the
|
|
vApp VM list of the VM you want to get a screen ticket.
|
|
Default is 0.
|
|
:type vm_num: ``int``
|
|
|
|
:rtype: ``dict``
|
|
"""
|
|
vm = self._get_vm_elements(vapp_or_vm_id)[vm_num]
|
|
try:
|
|
res = self.connection.request('%s/screen/action/acquireMksTicket' %
|
|
(get_url_path(vm.get('href'))),
|
|
method='POST')
|
|
output = {
|
|
"host": res.object.find(fixxpath(res.object, 'Host')).text,
|
|
"vmx": res.object.find(fixxpath(res.object, 'Vmx')).text,
|
|
"ticket": res.object.find(fixxpath(res.object, 'Ticket')).text,
|
|
"port": res.object.find(fixxpath(res.object, 'Port')).text,
|
|
}
|
|
return output
|
|
except Exception:
|
|
return None
|