Support for IPv6 and Non-standard ports

IPv6 urls are formatted with '[' and ']' around the host and
the port actually shows up after the trailing ']' as well. So
we need to allow for better support for both ipv6 and non
standard urls when dealing with both SOAP and WSDL urls.

Examples:
   https://[fd2e:1201:1d9f:0:8886:78e0:38cd:cb03]/sdk
   https://[::1]:9443/sdk/

Closes-Bug: #1287292

Change-Id: I84ab38087ea77a30f6082d8c3fae0341e3371a78
This commit is contained in:
Davanum Srinivas 2014-05-01 10:05:41 -04:00
parent c4d2d95eb3
commit a76bd253fb
11 changed files with 150 additions and 47 deletions

View File

@ -125,18 +125,20 @@ class VMwareAPISession(object):
"""Setup a session with the server and handles all calls made to it.
Example:
api_session = VMwareAPISession('10.1.2.3', 'administrator', 'password',
10, 0.1, create_session=False)
api_session = VMwareAPISession('10.1.2.3', 443, 'administrator',
'password', 10, 0.1,
create_session=False)
result = api_session.invoke_api(vim_util, 'get_objects',
api_session.vim, 'HostSystem', 100)
"""
def __init__(self, host, server_username, server_password,
def __init__(self, host, port, server_username, server_password,
api_retry_count, task_poll_interval, scheme='https',
create_session=True, wsdl_loc=None, pbm_wsdl_loc=None):
"""Initializes the API session with given parameters.
:param host: ESX/VC server IP address[:port] or host name[:port]
:param host: ESX/VC server IP address or host name
:param port: port for connection
:param server_username: username of ESX/VC server admin user
:param server_password: password for param server_username
:param api_retry_count: number of times an API must be retried upon
@ -152,6 +154,7 @@ class VMwareAPISession(object):
VimSessionOverLoadException
"""
self._host = host
self._port = port
self._server_username = server_username
self._server_password = server_password
self._api_retry_count = api_retry_count
@ -171,6 +174,7 @@ class VMwareAPISession(object):
if not self._vim:
self._vim = vim.Vim(protocol=self._scheme,
host=self._host,
port=self._port,
wsdl_loc=self._vim_wsdl_loc)
return self._vim
@ -179,7 +183,8 @@ class VMwareAPISession(object):
if not self._pbm and self._pbm_wsdl_loc:
self._pbm = pbm.PBMClient(self._pbm_wsdl_loc,
protocol=self._scheme,
host=self._host)
host=self._host,
port=self._port)
if self._session_id:
# To handle the case where pbm property is accessed after
# session creation. If pbm property is accessed before session

View File

@ -416,6 +416,7 @@ def download_flat_image(context, timeout_secs, image_service, image_id,
read_handle = rw_handles.ImageReadHandle(read_iter)
file_size = int(kwargs.get('image_size'))
write_handle = rw_handles.FileWriteHandle(kwargs.get('host'),
kwargs.get('port'),
kwargs.get('data_center_name'),
kwargs.get('datastore_name'),
kwargs.get('cookies'),
@ -448,6 +449,7 @@ def download_stream_optimized_data(context, timeout_secs, read_handle,
file_size = int(kwargs.get('image_size'))
write_handle = rw_handles.VmdkWriteHandle(kwargs.get('session'),
kwargs.get('host'),
kwargs.get('port'),
kwargs.get('resource_pool'),
kwargs.get('vm_folder'),
kwargs.get('vm_import_spec'),
@ -511,6 +513,7 @@ def upload_image(context, timeout_secs, image_service, image_id, owner_id,
file_size = kwargs.get('vmdk_size')
read_handle = rw_handles.VmdkReadHandle(kwargs.get('session'),
kwargs.get('host'),
kwargs.get('port'),
kwargs.get('vm'),
kwargs.get('vmdk_file_path'),
file_size)

View File

@ -37,14 +37,16 @@ LOG = logging.getLogger(__name__)
class PBMClient(vim.Vim):
"""SOAP based PBM client."""
def __init__(self, pbm_wsdl_loc, protocol='https', host='localhost'):
def __init__(self, pbm_wsdl_loc, protocol='https', host='localhost',
port=443):
"""Constructs a PBM client object.
:param pbm_wsdl_loc: PBM WSDL file location
:param protocol: http or https
:param host: server IP address[:port] or host name[:port]
:param host: server IP address or host name
:param port: port for connection
"""
self._url = vim_util.get_soap_url(protocol, host, 'pbm')
self._url = vim_util.get_soap_url(protocol, host, port, 'pbm')
self._pbm_client = suds.client.Client(pbm_wsdl_loc, location=self._url)
self._pbm_service_content = None

View File

@ -102,13 +102,13 @@ class FileHandle(object):
except Exception:
return False
def _get_soap_url(self, scheme, host):
def _get_soap_url(self, scheme, host, port):
"""Returns the IPv4/v6 compatible SOAP URL for the given host."""
if self._is_valid_ipv6(host):
return '%s://[%s]' % (scheme, host)
return '%s://%s' % (scheme, host)
return '%s://[%s]:%d' % (scheme, host, port)
return '%s://%s:%d' % (scheme, host, port)
def _fix_esx_url(self, url, host):
def _fix_esx_url(self, url, host, port):
"""Fix netloc in the case of an ESX host.
In the case of an ESX host, the netloc is set to '*' in the URL
@ -118,21 +118,25 @@ class FileHandle(object):
urlp = urlparse.urlparse(url)
if urlp.netloc == '*':
scheme, netloc, path, params, query, fragment = urlp
if netaddr.valid_ipv6(host):
netloc = '[%s]:%d' % (host, port)
else:
netloc = "%s:%d" % (host, port)
url = urlparse.urlunparse((scheme,
host,
netloc,
path,
params,
query,
fragment))
return url
def _find_vmdk_url(self, lease_info, host):
def _find_vmdk_url(self, lease_info, host, port):
"""Find the URL corresponding to a VMDK file in lease info."""
LOG.debug("Finding VMDK URL from lease info.")
url = None
for deviceUrl in lease_info.deviceUrl:
if deviceUrl.disk:
url = self._fix_esx_url(deviceUrl.url, host)
url = self._fix_esx_url(deviceUrl.url, host, port)
break
if not url:
excep_msg = _("Could not retrieve VMDK URL from lease info.")
@ -145,11 +149,12 @@ class FileHandle(object):
class FileWriteHandle(FileHandle):
"""Write handle for a file in VMware server."""
def __init__(self, host, data_center_name, datastore_name, cookies,
def __init__(self, host, port, data_center_name, datastore_name, cookies,
file_path, file_size, scheme='https'):
"""Initializes the write handle with given parameters.
:param host: ESX/VC server IP address[:port] or host name[:port]
:param host: ESX/VC server IP address or host name
:param port: port for connection
:param data_center_name: name of the data center in the case of a VC
server
:param datastore_name: name of the datastore where the file is stored
@ -159,7 +164,7 @@ class FileWriteHandle(FileHandle):
:param scheme: protocol-- http or https
:raises: VimConnectionException, ValueError
"""
soap_url = self._get_soap_url(scheme, host)
soap_url = self._get_soap_url(scheme, host, port)
param_list = {'dcPath': data_center_name, 'dsName': datastore_name}
self._url = '%s/folder/%s' % (soap_url, file_path)
self._url = self._url + '?' + urllib.urlencode(param_list)
@ -248,12 +253,13 @@ class VmdkWriteHandle(FileHandle):
virtual disk contents.
"""
def __init__(self, session, host, rp_ref, vm_folder_ref, import_spec,
def __init__(self, session, host, port, rp_ref, vm_folder_ref, import_spec,
vmdk_size):
"""Initializes the VMDK write handle with input parameters.
:param session: valid API session to ESX/VC server
:param host: ESX/VC server IP address[:port] or host name[:port]
:param host: ESX/VC server IP address or host name
:param port: port for connection
:param rp_ref: resource pool into which the backing VM is imported
:param vm_folder_ref: VM folder in ESX/VC inventory to use as parent
of backing VM
@ -281,7 +287,7 @@ class VmdkWriteHandle(FileHandle):
'info')
# Find VMDK URL where data is to be written
self._url = self._find_vmdk_url(lease_info, host)
self._url = self._find_vmdk_url(lease_info, host, port)
self._vm_ref = lease_info.entity
# Create HTTP connection to write to VMDK URL
@ -449,7 +455,7 @@ class VmdkWriteHandle(FileHandle):
class VmdkReadHandle(FileHandle):
"""VMDK read handle based on HttpNfcLease."""
def __init__(self, session, host, vm_ref, vmdk_path, vmdk_size):
def __init__(self, session, host, port, vm_ref, vmdk_path, vmdk_size):
"""Initializes the VMDK read handle with the given parameters.
During the read (export) operation, the VMDK file is converted to a
@ -457,7 +463,8 @@ class VmdkReadHandle(FileHandle):
file read may be smaller than the actual VMDK size.
:param session: valid api session to ESX/VC server
:param host: ESX/VC server IP address[:port] or host name[:port]
:param host: ESX/VC server IP address or host name
:param port: port for connection
:param vm_ref: managed object reference of the backing VM whose VMDK
is to be exported
:param vmdk_path: path of the VMDK file to be exported
@ -480,7 +487,7 @@ class VmdkReadHandle(FileHandle):
'info')
# find URL of the VMDK file to be read and open connection
self._url = self._find_vmdk_url(lease_info, host)
self._url = self._find_vmdk_url(lease_info, host, port)
self._conn = self._create_connection(session, self._url)
FileHandle.__init__(self, self._conn)

View File

@ -68,32 +68,44 @@ class VimMessagePlugin(suds.plugin.MessagePlugin):
class Vim(object):
"""VIM API Client."""
def __init__(self, protocol='https', host='localhost', wsdl_loc=None):
def __init__(self, protocol='https', host='localhost', port=None,
wsdl_loc=None):
"""Create communication interfaces for initiating SOAP transactions.
:param protocol: http or https
:param host: server IP address[:port] or host name[:port]
:param host: server IP address or host name
:param port: port for connection
:param wsdl_loc: WSDL file location
:raises: VimException, VimFaultException, VimAttributeException,
VimSessionOverLoadException, VimConnectionException
"""
if not wsdl_loc:
wsdl_loc = Vim._get_wsdl_loc(protocol, host)
soap_url = vim_util.get_soap_url(protocol, host)
wsdl_loc = Vim._get_wsdl_loc(protocol, host, port)
self._wsdl_loc = wsdl_loc
self._soap_url = vim_util.get_soap_url(protocol, host, port)
self._client = suds.client.Client(wsdl_loc,
location=soap_url,
location=self._soap_url,
plugins=[VimMessagePlugin()])
self._service_content = self.RetrieveServiceContent('ServiceInstance')
@staticmethod
def _get_wsdl_loc(protocol, host):
def _get_wsdl_loc(protocol, host, port):
"""Get the default WSDL file location hosted at the server.
:param protocol: http or https
:param host: server IP address[:port] or host name[:port]
:param host: server IP address or host name
:param port: port for connection
:returns: default WSDL file location hosted at the server
"""
return '%s://%s/sdk/vimService.wsdl' % (protocol, host)
return vim_util.get_wsdl_url(protocol, host, port)
@property
def wsdl_url(self):
return self._wsdl_loc
@property
def soap_url(self):
return self._soap_url
@property
def service_content(self):

View File

@ -368,14 +368,32 @@ def get_object_property(vim, moref, property_name):
return prop_val
def get_soap_url(protocol, host, path='sdk'):
def get_wsdl_url(protocol, host, port=None):
"""Get the default WSDL file location hosted at the server.
:param protocol: http or https
:param host: server IP address or host name
:param port: port for connection
:returns: default WSDL file location hosted at the server
"""
return get_soap_url(protocol, host, port) + "/vimService.wsdl"
def get_soap_url(protocol, host, port=None, path='sdk'):
"""Return ESX/VC server's SOAP service URL.
:param protocol: https or http
:param host: server IP address[:port] or host name[:port]
:param host: server IP address or host name
:param port: port for connection
:param path: path part of the SOAP URL
:returns: SOAP service URL
"""
if netaddr.valid_ipv6(host):
if port is None:
return '%s://[%s]/%s' % (protocol, host, path)
else:
return '%s://[%s]:%d/%s' % (protocol, host, port, path)
if port is None:
return '%s://%s/%s' % (protocol, host, path)
else:
return '%s://%s:%d/%s' % (protocol, host, port, path)

View File

@ -96,6 +96,7 @@ class VMwareAPISessionTest(base.TestCase):
"""Tests for VMwareAPISession."""
SERVER_IP = '10.1.2.3'
PORT = 443
USERNAME = 'admin'
PASSWORD = 'password'
@ -109,6 +110,7 @@ class VMwareAPISessionTest(base.TestCase):
def _create_api_session(self, _create_session, retry_count=10,
task_poll_interval=1):
return api.VMwareAPISession(VMwareAPISessionTest.SERVER_IP,
VMwareAPISessionTest.PORT,
VMwareAPISessionTest.USERNAME,
VMwareAPISessionTest.PASSWORD,
retry_count,
@ -121,6 +123,7 @@ class VMwareAPISessionTest(base.TestCase):
api_session.vim
self.VimMock.assert_called_with(protocol=api_session._scheme,
host=VMwareAPISessionTest.SERVER_IP,
port=VMwareAPISessionTest.PORT,
wsdl_loc=api_session._vim_wsdl_loc)
@mock.patch.object(pbm, 'PBMClient')

View File

@ -307,6 +307,7 @@ class ImageTransferUtilityTest(base.TestCase):
timeout_secs = 10
image_size = 1000
host = '127.0.0.1'
port = 443
dc_path = 'dc1'
ds_name = 'ds1'
file_path = '/fake_path'
@ -321,6 +322,7 @@ class ImageTransferUtilityTest(base.TestCase):
image_id,
image_size=image_size,
host=host,
port=port,
data_center_name=dc_path,
datastore_name=ds_name,
cookies=cookies,
@ -332,6 +334,7 @@ class ImageTransferUtilityTest(base.TestCase):
fake_rw_handles_FileWriteHandle.assert_called_once_with(
host,
port,
dc_path,
ds_name,
cookies,
@ -356,6 +359,7 @@ class ImageTransferUtilityTest(base.TestCase):
timeout_secs = 10
image_size = 1000
host = '127.0.0.1'
port = 443
resource_pool = 'rp-1'
vm_folder = 'folder-1'
vm_import_spec = None
@ -370,6 +374,7 @@ class ImageTransferUtilityTest(base.TestCase):
read_handle,
session=session,
host=host,
port=port,
resource_pool=resource_pool,
vm_folder=vm_folder,
vm_import_spec=vm_import_spec,
@ -378,6 +383,7 @@ class ImageTransferUtilityTest(base.TestCase):
fake_rw_handles_VmdkWriteHandle.assert_called_once_with(
session,
host,
port,
resource_pool,
vm_folder,
vm_import_spec,
@ -404,6 +410,7 @@ class ImageTransferUtilityTest(base.TestCase):
timeout_secs = 10
image_size = 1000
host = '127.0.0.1'
port = 443
resource_pool = 'rp-1'
vm_folder = 'folder-1'
vm_import_spec = None
@ -423,6 +430,7 @@ class ImageTransferUtilityTest(base.TestCase):
image_id,
session=session,
host=host,
port=port,
resource_pool=resource_pool,
vm_folder=vm_folder,
vm_import_spec=vm_import_spec,
@ -438,6 +446,7 @@ class ImageTransferUtilityTest(base.TestCase):
fake_ImageReadHandle,
session=session,
host=host,
port=port,
resource_pool=resource_pool,
vm_folder=vm_folder,
vm_import_spec=vm_import_spec,
@ -457,6 +466,7 @@ class ImageTransferUtilityTest(base.TestCase):
timeout_secs = 10
image_size = 1000
host = '127.0.0.1'
port = 443
file_path = '/fake_path'
is_public = False
image_name = 'fake_image'
@ -472,6 +482,7 @@ class ImageTransferUtilityTest(base.TestCase):
owner_id,
session=session,
host=host,
port=port,
vm=vm,
vmdk_file_path=file_path,
vmdk_size=image_size,
@ -481,6 +492,7 @@ class ImageTransferUtilityTest(base.TestCase):
fake_rw_handles_VmdkReadHandle.assert_called_once_with(session,
host,
port,
vm,
file_path,
image_size)

View File

@ -43,10 +43,12 @@ class FileHandleTest(base.TestCase):
lease_info = mock.Mock()
lease_info.deviceUrl = [device_url_0, device_url_1]
host = '10.1.2.3'
exp_url = 'https://%s/ds1/vm1.vmdk' % host
port = 443
exp_url = 'https://%s:%d/ds1/vm1.vmdk' % (host, port)
vmw_http_file = rw_handles.FileHandle(None)
self.assertEqual(exp_url, vmw_http_file._find_vmdk_url(lease_info,
host))
host,
port))
class FileWriteHandleTest(base.TestCase):
@ -66,7 +68,8 @@ class FileWriteHandleTest(base.TestCase):
HTTPConnectionMock.return_value = self._conn
self.vmw_http_write_file = rw_handles.FileWriteHandle(
'10.1.2.3', 'dc-0', 'ds-0', [vim_cookie], '1.vmdk', 100, 'http')
'10.1.2.3', 443, 'dc-0', 'ds-0', [vim_cookie], '1.vmdk', 100,
'http')
def test_write(self):
self.vmw_http_write_file.write(None)
@ -118,6 +121,7 @@ class VmdkWriteHandleTest(base.TestCase):
self.assertRaises(exceptions.VimException,
lambda: rw_handles.VmdkWriteHandle(session,
'10.1.2.3',
443,
'rp-1',
'folder-1',
None,
@ -125,7 +129,7 @@ class VmdkWriteHandleTest(base.TestCase):
def test_write(self):
session = self._create_mock_session()
handle = rw_handles.VmdkWriteHandle(session, '10.1.2.3',
handle = rw_handles.VmdkWriteHandle(session, '10.1.2.3', 443,
'rp-1', 'folder-1', None,
100)
data = [1] * 10
@ -137,7 +141,7 @@ class VmdkWriteHandleTest(base.TestCase):
vmdk_size = 100
data_size = 10
session = self._create_mock_session(True, 10)
handle = rw_handles.VmdkWriteHandle(session, '10.1.2.3',
handle = rw_handles.VmdkWriteHandle(session, '10.1.2.3', 443,
'rp-1', 'folder-1', None,
vmdk_size)
handle.write([1] * data_size)
@ -145,7 +149,7 @@ class VmdkWriteHandleTest(base.TestCase):
def test_update_progress_with_error(self):
session = self._create_mock_session(True, 10)
handle = rw_handles.VmdkWriteHandle(session, '10.1.2.3',
handle = rw_handles.VmdkWriteHandle(session, '10.1.2.3', 443,
'rp-1', 'folder-1', None,
100)
session.invoke_api.side_effect = exceptions.VimException(None)
@ -153,7 +157,7 @@ class VmdkWriteHandleTest(base.TestCase):
def test_close(self):
session = self._create_mock_session()
handle = rw_handles.VmdkWriteHandle(session, '10.1.2.3',
handle = rw_handles.VmdkWriteHandle(session, '10.1.2.3', 443,
'rp-1', 'folder-1', None,
100)
@ -215,6 +219,7 @@ class VmdkReadHandleTest(base.TestCase):
self.assertRaises(exceptions.VimException,
lambda: rw_handles.VmdkReadHandle(session,
'10.1.2.3',
443,
'vm-1',
'[ds] disk1.vmdk',
100))
@ -223,7 +228,7 @@ class VmdkReadHandleTest(base.TestCase):
chunk_size = rw_handles.READ_CHUNKSIZE
session = self._create_mock_session()
self._conn.read.return_value = [1] * chunk_size
handle = rw_handles.VmdkReadHandle(session, '10.1.2.3',
handle = rw_handles.VmdkReadHandle(session, '10.1.2.3', 443,
'vm-1', '[ds] disk1.vmdk',
chunk_size * 10)
handle.read(chunk_size)
@ -235,7 +240,7 @@ class VmdkReadHandleTest(base.TestCase):
vmdk_size = chunk_size * 10
session = self._create_mock_session(True, 10)
self._conn.read.return_value = [1] * chunk_size
handle = rw_handles.VmdkReadHandle(session, '10.1.2.3',
handle = rw_handles.VmdkReadHandle(session, '10.1.2.3', 443,
'vm-1', '[ds] disk1.vmdk',
vmdk_size)
handle.read(chunk_size)
@ -243,7 +248,7 @@ class VmdkReadHandleTest(base.TestCase):
def test_update_progress_with_error(self):
session = self._create_mock_session(True, 10)
handle = rw_handles.VmdkReadHandle(session, '10.1.2.3',
handle = rw_handles.VmdkReadHandle(session, '10.1.2.3', 443,
'vm-1', '[ds] disk1.vmdk',
100)
session.invoke_api.side_effect = exceptions.VimException(None)
@ -251,7 +256,7 @@ class VmdkReadHandleTest(base.TestCase):
def test_close(self):
session = self._create_mock_session()
handle = rw_handles.VmdkReadHandle(session, '10.1.2.3',
handle = rw_handles.VmdkReadHandle(session, '10.1.2.3', 443,
'vm-1', '[ds] disk1.vmdk',
100)

View File

@ -312,3 +312,24 @@ class VimTest(base.TestCase):
"Faults: [ValueError('example',)]\n"
"Details: {'foo': 'bar'}",
string)
def test_configure_non_default_host_port(self):
vim_obj = vim.Vim('https', 'www.test.com', 12345)
self.assertEqual('https://www.test.com:12345/sdk/vimService.wsdl',
vim_obj.wsdl_url)
self.assertEqual('https://www.test.com:12345/sdk',
vim_obj.soap_url)
def test_configure_ipv6(self):
vim_obj = vim.Vim('https', '::1')
self.assertEqual('https://[::1]/sdk/vimService.wsdl',
vim_obj.wsdl_url)
self.assertEqual('https://[::1]/sdk',
vim_obj.soap_url)
def test_configure_ipv6_and_non_default_host_port(self):
vim_obj = vim.Vim('https', '::1', 12345)
self.assertEqual('https://[::1]:12345/sdk/vimService.wsdl',
vim_obj.wsdl_url)
self.assertEqual('https://[::1]:12345/sdk',
vim_obj.soap_url)

View File

@ -288,3 +288,18 @@ class VimUtilTest(base.TestCase):
self.assertEqual(prop.val, val)
get_object_properties.assert_called_once_with(
vim, moref, [property_name])
def test_configure_without_wsdl_loc_override(self):
wsdl_url = vim_util.get_wsdl_url("https", "www.example.com")
url = vim_util.get_soap_url("https", "www.example.com")
self.assertEqual("https://www.example.com/sdk/vimService.wsdl",
wsdl_url)
self.assertEqual("https://www.example.com/sdk", url)
def test_configure_without_wsdl_loc_override_using_ipv6(self):
# Same as above but with ipv6 based host ip
wsdl_url = vim_util.get_wsdl_url("https", "::1")
url = vim_util.get_soap_url("https", "::1")
self.assertEqual("https://[::1]/sdk/vimService.wsdl",
wsdl_url)
self.assertEqual("https://[::1]/sdk", url)