Use SSL thumbprints for NFC transfer

There are cases when clients cannot build a trust chain in order to
verify ESX certificate. The SSL thumbprint is always available and it is
the preferred way for verifying the connection when doing NFC transfer.
This patch replace the usage of truststore with SSL thumbprints in
VmdkReadHandle and VmdkWriteHandle.

Change-Id: I9573bada15aef2bf8d25fa824bbbc2b421586733
This commit is contained in:
Radoslav Gerganov 2015-08-26 14:18:49 +03:00
parent 0a0e7e8bae
commit c47cfdcf6a
2 changed files with 75 additions and 62 deletions

View File

@ -62,16 +62,47 @@ class FileHandle(object):
self._last_logged_progress = 0
self._last_progress_udpate = 0
def _create_read_connection(self, url, cookies=None, cacerts=False):
def _create_connection(self, url, method, cacerts=False,
ssl_thumbprint=None):
_urlparse = urlparse.urlparse(url)
scheme, netloc, path, params, query, fragment = _urlparse
if scheme == 'http':
conn = httplib.HTTPConnection(netloc)
elif scheme == 'https':
conn = httplib.HTTPSConnection(netloc)
cert_reqs = None
# cacerts can be either True or False or contain
# actual certificates. If it is a boolean, then
# we need to set cert_reqs and clear the cacerts
if isinstance(cacerts, bool):
if cacerts:
cert_reqs = ssl.CERT_REQUIRED
else:
cert_reqs = ssl.CERT_NONE
cacerts = None
conn.set_cert(ca_certs=cacerts, cert_reqs=cert_reqs,
assert_fingerprint=ssl_thumbprint)
else:
excep_msg = _("Invalid scheme: %s.") % scheme
LOG.error(excep_msg)
raise ValueError(excep_msg)
if query:
path = path + '?' + query
conn.putrequest(method, path)
return conn
def _create_read_connection(self, url, cookies=None, cacerts=False,
ssl_thumbprint=None):
LOG.debug("Opening URL: %s for reading.", url)
try:
headers = {'User-Agent': USER_AGENT}
if cookies:
headers.update({'Cookie':
self._build_vim_cookie_header(cookies)})
response = requests.get(url, headers=headers, stream=True,
verify=cacerts)
return response.raw
conn = self._create_connection(url, 'GET', cacerts, ssl_thumbprint)
vim_cookie = self._build_vim_cookie_header(cookies)
conn.putheader('User-Agent', USER_AGENT)
conn.putheader('Cookie', vim_cookie)
conn.endheaders()
return conn.getresponse()
except Exception as excep:
# TODO(vbala) We need to catch and raise specific exceptions
# related to connection problems, invalid request and invalid
@ -86,41 +117,15 @@ class FileHandle(object):
cookies=None,
overwrite=None,
content_type=None,
cacerts=False):
cacerts=False,
ssl_thumbprint=None):
"""Create HTTP connection to write to VMDK file."""
LOG.debug("Creating HTTP connection to write to file with "
"size = %(file_size)d and URL = %(url)s.",
{'file_size': file_size,
'url': url})
_urlparse = urlparse.urlparse(url)
scheme, netloc, path, params, query, fragment = _urlparse
try:
if scheme == 'http':
conn = httplib.HTTPConnection(netloc)
elif scheme == 'https':
conn = httplib.HTTPSConnection(netloc)
cert_reqs = None
# cacerts can be either True or False or contain
# actual certificates. If it is a boolean, then
# we need to set cert_reqs and clear the cacerts
if isinstance(cacerts, bool):
if cacerts:
cert_reqs = ssl.CERT_REQUIRED
else:
cert_reqs = ssl.CERT_NONE
cacerts = None
conn.set_cert(ca_certs=cacerts, cert_reqs=cert_reqs)
else:
excep_msg = _("Invalid scheme: %s.") % scheme
LOG.error(excep_msg)
raise ValueError(excep_msg)
if query:
path = path + '?' + query
conn = self._create_connection(url, 'PUT', cacerts, ssl_thumbprint)
headers = {'User-Agent': USER_AGENT}
if file_size:
headers.update({'Content-Length': str(file_size)})
@ -131,8 +136,6 @@ class FileHandle(object):
self._build_vim_cookie_header(cookies)})
if content_type:
headers.update({'Content-Type': content_type})
conn.putrequest('PUT', path)
for key, value in six.iteritems(headers):
conn.putheader(key, value)
conn.endheaders()
@ -213,16 +216,18 @@ class FileHandle(object):
def _find_vmdk_url(self, lease_info, host, port):
"""Find the URL corresponding to a VMDK file in lease info."""
url = None
ssl_thumbprint = None
for deviceUrl in lease_info.deviceUrl:
if deviceUrl.disk:
url = self._fix_esx_url(deviceUrl.url, host, port)
ssl_thumbprint = deviceUrl.sslThumbprint
break
if not url:
excep_msg = _("Could not retrieve VMDK URL from lease info.")
LOG.error(excep_msg)
raise exceptions.VimException(excep_msg)
LOG.debug("Found VMDK URL: %s from lease info.", url)
return url
return url, ssl_thumbprint
def _log_progress(self, progress):
"""Log data transfer progress."""
@ -338,7 +343,7 @@ class VmdkWriteHandle(FileHandle):
'info')
# Find VMDK URL where data is to be written
self._url = self._find_vmdk_url(lease_info, host, port)
self._url, thumbprint = self._find_vmdk_url(lease_info, host, port)
self._vm_ref = lease_info.entity
cookies = session.vim.client.options.transport.cookiejar
@ -349,7 +354,7 @@ class VmdkWriteHandle(FileHandle):
cookies=cookies,
overwrite='t',
content_type=octet_stream,
cacerts=session._cacert)
ssl_thumbprint=thumbprint)
FileHandle.__init__(self, self._conn)
def get_imported_vm(self):
@ -499,12 +504,11 @@ 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, port)
self._url, thumbprint = self._find_vmdk_url(lease_info, host, port)
cookies = session.vim.client.options.transport.cookiejar
cacerts = session.vim.client.options.transport.verify
self._conn = self._create_read_connection(self._url,
cookies=cookies,
cacerts=cacerts)
ssl_thumbprint=thumbprint)
FileHandle.__init__(self, self._conn)
def _create_and_wait_for_lease(self, session, vm_ref):

View File

@ -24,6 +24,7 @@ from oslo_vmware import exceptions
from oslo_vmware import rw_handles
from oslo_vmware.tests import base
from oslo_vmware import vim_util
from urllib3 import connection as httplib
class FileHandleTest(base.TestCase):
@ -41,15 +42,23 @@ class FileHandleTest(base.TestCase):
device_url_1 = mock.Mock()
device_url_1.disk = True
device_url_1.url = 'https://*/ds1/vm1.vmdk'
device_url_1.sslThumbprint = '11:22:33:44:55'
lease_info = mock.Mock()
lease_info.deviceUrl = [device_url_0, device_url_1]
host = '10.1.2.3'
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,
port))
url, thumbprint = vmw_http_file._find_vmdk_url(lease_info, host, port)
self.assertEqual(exp_url, url)
self.assertEqual('11:22:33:44:55', thumbprint)
def test_create_connection(self):
handle = rw_handles.FileHandle(None)
conn = handle._create_connection('http://fira', 'GET')
self.assertIsInstance(conn, httplib.HTTPConnection)
conn = handle._create_connection('https://fira', 'GET')
self.assertIsInstance(conn, httplib.HTTPSConnection)
class FileWriteHandleTest(base.TestCase):
@ -182,12 +191,15 @@ class VmdkReadHandleTest(base.TestCase):
def setUp(self):
super(VmdkReadHandleTest, self).setUp()
send_patcher = mock.patch('requests.sessions.Session.send')
self.addCleanup(send_patcher.stop)
send_mock = send_patcher.start()
self._response = mock.Mock()
send_mock.return_value = self._response
self._resp = mock.Mock()
self._resp.read.return_value = 'fake-data'
self._conn = mock.Mock()
self._conn.getresponse.return_value = self._resp
patcher = mock.patch(
'urllib3.connection.HTTPConnection')
self.addCleanup(patcher.stop)
HTTPConnectionMock = patcher.start()
HTTPConnectionMock.return_value = self._conn
def _create_mock_session(self, disk=True, progress=-1):
device_url = mock.Mock()
@ -227,25 +239,22 @@ class VmdkReadHandleTest(base.TestCase):
def test_read(self):
chunk_size = rw_handles.READ_CHUNKSIZE
session = self._create_mock_session()
self._response.raw.read.return_value = [1] * chunk_size
handle = rw_handles.VmdkReadHandle(session, '10.1.2.3', 443,
'vm-1', '[ds] disk1.vmdk',
chunk_size * 10)
handle.read(chunk_size)
self.assertEqual(chunk_size, handle._bytes_read)
self._response.raw.read.assert_called_once_with(chunk_size)
data = handle.read(chunk_size)
self.assertEqual('fake-data', data)
def test_update_progress(self):
chunk_size = rw_handles.READ_CHUNKSIZE
chunk_size = len('fake-data')
vmdk_size = chunk_size * 10
session = self._create_mock_session(True, 10)
self._response.raw.read.return_value = [1] * chunk_size
handle = rw_handles.VmdkReadHandle(session, '10.1.2.3', 443,
'vm-1', '[ds] disk1.vmdk',
vmdk_size)
handle.read(chunk_size)
data = handle.read(chunk_size)
handle.update_progress()
self._response.raw.read.assert_called_once_with(chunk_size)
self.assertEqual('fake-data', data)
def test_update_progress_with_error(self):
session = self._create_mock_session(True, 10)