From cfa9acdb88396aa22cad05622d516052524648ad Mon Sep 17 00:00:00 2001 From: het Date: Fri, 21 Sep 2018 13:38:40 -0700 Subject: [PATCH] Add tests to make sure vsphere client stubs are present Add unittests for vsphere client Fixed some format issues Run the tests as part of travis ci --- .travis.yml | 5 +- .../vsphere/backuprestore/backup_schedule.py | 2 +- samples/vsphere/common/sso.py | 79 ++++++------ samples/vsphere/common/vim/file.py | 2 +- .../vsphere/logforwarding/log_forwarding.py | 1 + samples/vsphere/services/services_list.py | 13 +- .../vcenter/vm/hardware/adapter/scsi.py | 6 +- .../vcenter/vm/hardware/boot_device.py | 1 + samples/vsphere/vcenter/vm/hardware/cdrom.py | 1 + samples/vsphere/vcenter/vm/hardware/cpu.py | 1 + .../vsphere/vcenter/vm/hardware/ethernet.py | 1 + samples/vsphere/vcenter/vm/hardware/floppy.py | 1 + samples/vsphere/vcenter/vm/hardware/memory.py | 1 + setup.cfg | 2 +- test-requirements.txt | 4 +- tests/test_vsphere_client.py | 119 ++++++++++++++++++ 16 files changed, 190 insertions(+), 49 deletions(-) create mode 100644 tests/test_vsphere_client.py diff --git a/.travis.yml b/.travis.yml index 5d95a6dd..84017828 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,7 @@ python: install: - pip install -r requirements.txt --extra-index-url file://$PWD/lib --upgrade --ignore-installed six - pip install -r test-requirements.txt - - pip install pycodestyle # command to run tests -script: pycodestyle samples/*.py +script: pycodestyle samples tests +script: pytest + diff --git a/samples/vsphere/backuprestore/backup_schedule.py b/samples/vsphere/backuprestore/backup_schedule.py index 9695da10..27742181 100644 --- a/samples/vsphere/backuprestore/backup_schedule.py +++ b/samples/vsphere/backuprestore/backup_schedule.py @@ -48,7 +48,6 @@ class BackupSchedule(object): self._schedule_id = 'test_schedule' - def setup(self): parser = sample_cli.build_arg_parser() @@ -154,5 +153,6 @@ def main(): schedule.setup() schedule.run() + if __name__ == '__main__': main() diff --git a/samples/vsphere/common/sso.py b/samples/vsphere/common/sso.py index 9fbd4d5c..628735fb 100644 --- a/samples/vsphere/common/sso.py +++ b/samples/vsphere/common/sso.py @@ -15,7 +15,7 @@ __author__ = 'VMware, Inc.' __copyright__ = 'Copyright 2013, 2016, 2017 VMware, Inc. All rights reserved.' -#Standard library imports. +# Standard library imports. import six.moves.http_client import re from six import PY3 @@ -33,7 +33,7 @@ from pyVmomi import ThumbprintMismatchException from uuid import uuid4 from io import BytesIO from six.moves.urllib.parse import urlparse -#Third-party imports. +# Third-party imports. from lxml import etree from OpenSSL import crypto import ssl @@ -42,6 +42,7 @@ UTF_8 = 'utf-8' SHA256 = 'sha256' SHA512 = 'sha512' + def _extract_certificate(cert): ''' Extract DER certificate/private key from DER/base64-ed DER/PEM string. @@ -72,6 +73,7 @@ class SoapException(Exception): ''' Exception raised in case of STS request failure. ''' + def __init__(self, soap_msg, fault_code, fault_string): ''' Initializer for SoapException. @@ -104,6 +106,7 @@ class SSOHTTPSConnection(six.moves.http_client.HTTPSConnection): ''' An HTTPS class that verifies server's certificate on connect. ''' + def __init__(self, *args, **kwargs): ''' Initializer. See httplib.HTTPSConnection for other arguments @@ -154,7 +157,7 @@ class SSOHTTPSConnection(six.moves.http_client.HTTPSConnection): self.sock.close() self.sock = None raise ThumbprintMismatchException( - expected=self.server_thumbprint, actual=thumbprint) + expected=self.server_thumbprint, actual=thumbprint) def connect(self): ''' @@ -407,7 +410,8 @@ class SsoAuthenticator(object): @rtype: C{str} @return: The SAML assertion. ''' - import sspi, win32api + import sspi + import win32api spn = "sts/%s.com" % win32api.GetDomainName() sspiclient = sspi.ClientAuth("Kerberos", targetspn=spn) in_buf = None @@ -467,7 +471,8 @@ class SsoAuthenticator(object): @rtype: C{str} @return: The SAML assertion in Unicode. ''' - import kerberos, platform + import kerberos + import platform service = 'host@%s' % platform.node() _, context = kerberos.authGSSClientInit(service, 0) challenge = '' @@ -533,7 +538,7 @@ class SsoAuthenticator(object): token_duration, delegatable, renewable) else: raise Exception("Currently, not supported on this platform") - ## TODO Remove this exception once SSO supports validation of tickets + # TODO Remove this exception once SSO supports validation of tickets # generated against host machines # saml_token = self._get_bearer_saml_assertion_lin(request_duration, token_duration, delegatable) return saml_token @@ -647,13 +652,14 @@ class SsoAuthenticator(object): {'saml2': "urn:oasis:names:tc:SAML:2.0:assertion"}), pretty_print=False).decode(UTF_8) + class SecurityTokenRequest(object): ''' SecurityTokenRequest class handles the serialization of request to the STS for a SAML token. ''' - #pylint: disable=R0902 + # pylint: disable=R0902 def __init__(self, username=None, password=None, @@ -699,8 +705,7 @@ class SecurityTokenRequest(object): self._expires = time.strftime(TIME_FORMAT, time.gmtime(current + token_duration)) self._request_expires = time.strftime(TIME_FORMAT, - time.gmtime(current + - request_duration)) + time.gmtime(current + request_duration)) self._timestamp = TIMESTAMP_TEMPLATE % self.__dict__ self._username = escape(username) if username else username self._password = escape(password) if password else password @@ -714,9 +719,9 @@ class SecurityTokenRequest(object): self._binary_exchange = None self._public_key = None if gss_binary_token: - self._binary_exchange = BINARY_EXCHANGE_TEMPLATE % gss_binary_token - #The following are populated later. Set to None here to keep in-line - #with PEP8. + self._binary_exchange = BINARY_EXCHANGE_TEMPLATE % gss_binary_token + # The following are populated later. Set to None here to keep in-line + # with PEP8. self._binary_security_token = None self._hok_token = hok_token self._key_type = None @@ -730,7 +735,7 @@ class SecurityTokenRequest(object): self._xml = None self._request_digest = None - #These will only be populated if requesting an HoK token. + # These will only be populated if requesting an HoK token. if self._private_key_file: with open(self._private_key_file) as fp: self._private_key = fp.read() @@ -942,15 +947,17 @@ def _load_private_key(der_key): for key_type in ('PRIVATE KEY', 'RSA PRIVATE KEY'): try: return crypto.load_privatekey(crypto.FILETYPE_PEM, - '-----BEGIN ' + key_type + '-----\n' + - base64.encodestring(der_key).decode(UTF_8) + - '-----END ' + key_type + '-----\n', + '-----BEGIN {}-----\n{}-----END {}-----\n'.format( + key_type, + base64.encodestring(der_key).decode(UTF_8), + key_type), b'') except (crypto.Error, ValueError): pass # We could try 'ENCRYPTED PRIVATE KEY' here - but we do not know passphrase. raise + def _sign(private_key, data, digest=SHA256): ''' An internal helper method to sign the 'data' with the 'private_key'. @@ -972,6 +979,7 @@ def _sign(private_key, data, digest=SHA256): pkey = _load_private_key(_extract_certificate(private_key)) return base64.b64encode(crypto.sign(pkey, data, digest)) + def _canonicalize(xml_string): ''' Given an xml string, canonicalize the string per @@ -989,6 +997,7 @@ def _canonicalize(xml_string): tree.write_c14n(string, exclusive=True, with_comments=False) return string.getvalue().decode(UTF_8) + def _extract_element(xml, element_name, namespace): ''' An internal method provided to extract an element from the given XML. @@ -1042,9 +1051,9 @@ def _make_hash_sha512(data): TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.987Z" -#The SAML token requests usually contain an xmldsig which guarantees that the -#message hasn't been tampered with during the transport. The following -#SIGNED_INFO_TEMPLATE is used to construct the signedinfo part of the signature. +# The SAML token requests usually contain an xmldsig which guarantees that the +# message hasn't been tampered with during the transport. The following +# SIGNED_INFO_TEMPLATE is used to construct the signedinfo part of the signature. SIGNED_INFO_TEMPLATE = """\ @@ -1066,11 +1075,11 @@ SIGNED_INFO_TEMPLATE = """\ """ -#The following template is used as the container for signed info in WS-Trust -#SOAP requests signed with the SAML token. It contains the digest of the -#signed info, signed with the private key of the Solution user and contains a -#reference to the actual SAML token which contains the solution user's public -#key. +# The following template is used as the container for signed info in WS-Trust +# SOAP requests signed with the SAML token. It contains the digest of the +# signed info, signed with the private key of the Solution user and contains a +# reference to the actual SAML token which contains the solution user's public +# key. REQUEST_SIGNATURE_TEMPLATE = """\ %(_signed_info)s @@ -1084,9 +1093,9 @@ REQUEST_SIGNATURE_TEMPLATE = """\ """ -#The following template is used as a signed info container for the actual SAML -#token requests requesting a SAML token. It contains the digest of the signed -#info signed with the Service User's private key. +# The following template is used as a signed info container for the actual SAML +# token requests requesting a SAML token. It contains the digest of the signed +# info signed with the Service User's private key. SIGNATURE_TEMPLATE = """\ %(_signed_info)s @@ -1098,7 +1107,7 @@ SIGNATURE_TEMPLATE = """\ """ -#The following template is used to construct the token requests to the STS. +# The following template is used to construct the token requests to the STS. REQUEST_TEMPLATE = """\ @@ -1128,7 +1137,7 @@ REQUEST_TEMPLATE = """\ """ -#The following template is used to construct the token-by-token requests to the STS. +# The following template is used to construct the token-by-token requests to the STS. REQUEST_TEMPLATE_TOKEN_BY_TOKEN = """\ @@ -1185,8 +1194,8 @@ GSS_REQUEST_TEMPLATE = """\ """ -#Template container for the service user's public key when requesting an HoK -#token. +# Template container for the service user's public key when requesting an HoK +# token. BINARY_SECURITY_TOKEN_TEMPLATE = """\ %(_binary_security_token)s """ -#Template container for user's credentials when requesting a bearer token. +# Template container for user's credentials when requesting a bearer token. USERNAME_TOKEN_TEMPLATE = """\ %(_username)s %(_password)s """ -#Template containing the anchor to the signature. +# Template containing the anchor to the signature. USE_KEY_TEMPLATE = """\ """ -#The follwoing template is used to create a timestamp for the various messages. -#The timestamp is used to indicate the duration of the request itself. +# The follwoing template is used to create a timestamp for the various messages. +# The timestamp is used to indicate the duration of the request itself. TIMESTAMP_TEMPLATE = """\ %(_created)s%(_request_expires)s""" diff --git a/samples/vsphere/common/vim/file.py b/samples/vsphere/common/vim/file.py index 4c95398e..657f539d 100644 --- a/samples/vsphere/common/vim/file.py +++ b/samples/vsphere/common/vim/file.py @@ -19,7 +19,7 @@ from samples.vsphere.common.vim.inventory import get_datastore_mo from samples.vsphere.common.vim import datastore_file -datastore_path_regex = re.compile('\[(.+)\]\s?(.*)') +datastore_path_regex = re.compile(br'\[(.+)\]\s?(.*)') def parse_datastore_path(datastore_path): diff --git a/samples/vsphere/logforwarding/log_forwarding.py b/samples/vsphere/logforwarding/log_forwarding.py index 8dda5fa9..823f835b 100644 --- a/samples/vsphere/logforwarding/log_forwarding.py +++ b/samples/vsphere/logforwarding/log_forwarding.py @@ -128,5 +128,6 @@ def main(): log_forwarding.setup() log_forwarding.run() + if __name__ == '__main__': main() diff --git a/samples/vsphere/services/services_list.py b/samples/vsphere/services/services_list.py index 1be3cac5..74489bb8 100644 --- a/samples/vsphere/services/services_list.py +++ b/samples/vsphere/services/services_list.py @@ -25,6 +25,7 @@ from samples.vsphere.common.ssl_helper import get_unverified_session from samples.vsphere.common import sample_cli from samples.vsphere.common import sample_util + class ListServices(object): """ Demonstrates the details of vCenter Services @@ -35,8 +36,7 @@ class ListServices(object): - vCenter Server """ - - def __init__(self): + def __init__(self): # Create argument parser for standard inputs: # server, username, password and skipverification parser = sample_cli.build_arg_parser() @@ -51,10 +51,11 @@ class ListServices(object): username=args.username, password=args.password, session=session) + def run(self): services_list = self.client.vcenter.services.Service.list_details() table = [] - for key,value in services_list.items(): + for key, value in services_list.items(): row = [key, value.name_key, value.health, @@ -62,11 +63,13 @@ class ListServices(object): value.startup_type] table.append(row) headers = ["Service Name", "Service Name Key", "Service Health", "Service Status", "Service Startup Type"] - print(tabulate(table,headers)) + print(tabulate(table, headers)) + def main(): list_services = ListServices() list_services.run() + if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/samples/vsphere/vcenter/vm/hardware/adapter/scsi.py b/samples/vsphere/vcenter/vm/hardware/adapter/scsi.py index 2fe0e319..debfdcad 100644 --- a/samples/vsphere/vcenter/vm/hardware/adapter/scsi.py +++ b/samples/vsphere/vcenter/vm/hardware/adapter/scsi.py @@ -87,11 +87,11 @@ def run(): print('\n# Example: Create SCSI adapter with defaults') scsi_create_spec = Scsi.CreateSpec() - scsi = client.vcenter.vm.hardware.adapter.Scsi.create(vm, scsi_create_spec) + scsi = client.vcenter.vm.hardware.adapter.Scsi.create(vm, scsi_create_spec) print('vm.hardware.adapter.Scsi.create({}, {}) -> {}'. format(vm, scsi_create_spec, scsi)) scsis_to_delete.append(scsi) - scsi_info = client.vcenter.vm.hardware.adapter.Scsi.get(vm, scsi) + scsi_info = client.vcenter.vm.hardware.adapter.Scsi.get(vm, scsi) print('vm.hardware.adapter.Scsi.get({}, {}) -> {}'. format(vm, scsi, pp(scsi_info))) @@ -112,7 +112,7 @@ def run(): client.vcenter.vm.hardware.adapter.Scsi.update(vm, scsi, scsi_update_spec) print('vm.hardware.adapter.Scsi.update({}, {}, {})'. format(vm, scsi, scsi_create_spec)) - scsi_info = client.vcenter.vm.hardware.adapter.Scsi.get(vm, scsi) + scsi_info = client.vcenter.vm.hardware.adapter.Scsi.get(vm, scsi) print('vm.hardware.adapter.Scsi.get({}, {}) -> {}'. format(vm, scsi, pp(scsi_info))) diff --git a/samples/vsphere/vcenter/vm/hardware/boot_device.py b/samples/vsphere/vcenter/vm/hardware/boot_device.py index 9e25ccb0..f52e0bf8 100644 --- a/samples/vsphere/vcenter/vm/hardware/boot_device.py +++ b/samples/vsphere/vcenter/vm/hardware/boot_device.py @@ -62,6 +62,7 @@ def setup(context=None): password=password, session=session) + def run(): global vm vm = get_vm(client, vm_name) diff --git a/samples/vsphere/vcenter/vm/hardware/cdrom.py b/samples/vsphere/vcenter/vm/hardware/cdrom.py index 45b7677d..454c8262 100644 --- a/samples/vsphere/vcenter/vm/hardware/cdrom.py +++ b/samples/vsphere/vcenter/vm/hardware/cdrom.py @@ -63,6 +63,7 @@ def setup(context=None): password=password, session=session) + def run(): global vm vm = get_vm(client, vm_name) diff --git a/samples/vsphere/vcenter/vm/hardware/cpu.py b/samples/vsphere/vcenter/vm/hardware/cpu.py index 0b2254d9..536ef653 100644 --- a/samples/vsphere/vcenter/vm/hardware/cpu.py +++ b/samples/vsphere/vcenter/vm/hardware/cpu.py @@ -58,6 +58,7 @@ def setup(context=None): password=password, session=session) + def run(): global vm vm = get_vm(client, vm_name) diff --git a/samples/vsphere/vcenter/vm/hardware/ethernet.py b/samples/vsphere/vcenter/vm/hardware/ethernet.py index 13e918a8..89260471 100644 --- a/samples/vsphere/vcenter/vm/hardware/ethernet.py +++ b/samples/vsphere/vcenter/vm/hardware/ethernet.py @@ -59,6 +59,7 @@ def setup(context=None): password=password, session=session) + def run(): global vm vm = get_vm(client, vm_name) diff --git a/samples/vsphere/vcenter/vm/hardware/floppy.py b/samples/vsphere/vcenter/vm/hardware/floppy.py index 695edb9c..9e759e53 100644 --- a/samples/vsphere/vcenter/vm/hardware/floppy.py +++ b/samples/vsphere/vcenter/vm/hardware/floppy.py @@ -59,6 +59,7 @@ def setup(context=None): password=password, session=session) + def run(): # * Floppy images must be pre-existing. This API does not expose # a way to create new floppy images. diff --git a/samples/vsphere/vcenter/vm/hardware/memory.py b/samples/vsphere/vcenter/vm/hardware/memory.py index 71d60ee1..e206986c 100644 --- a/samples/vsphere/vcenter/vm/hardware/memory.py +++ b/samples/vsphere/vcenter/vm/hardware/memory.py @@ -58,6 +58,7 @@ def setup(context=None): password=password, session=session) + def run(): global vm vm = get_vm(client, vm_name) diff --git a/setup.cfg b/setup.cfg index e445209e..9f71b112 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,2 @@ [pycodestyle] -ignore = E402, E501, E122, E126, E127, E128, E129, E131 \ No newline at end of file +ignore = E402, E501, E122, E126, E127, E128, E129, E131, W503, W504 \ No newline at end of file diff --git a/test-requirements.txt b/test-requirements.txt index 5c55ed98..f8ae5e5c 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1 +1,3 @@ -testtools>=0.9.34 +pytest +pycodestyle + diff --git a/tests/test_vsphere_client.py b/tests/test_vsphere_client.py new file mode 100644 index 00000000..5532b5b7 --- /dev/null +++ b/tests/test_vsphere_client.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python + +""" +* ******************************************************* +* Copyright (c) VMware, Inc. 2018. All Rights Reserved. +* SPDX-License-Identifier: MIT +* ******************************************************* +* +* DISCLAIMER. THIS PROGRAM IS PROVIDED TO YOU "AS IS" WITHOUT +* WARRANTIES OR CONDITIONS OF ANY KIND, WHETHER ORAL OR WRITTEN, +* EXPRESS OR IMPLIED. THE AUTHOR SPECIFICALLY DISCLAIMS ANY IMPLIED +* WARRANTIES OR CONDITIONS OF MERCHANTABILITY, SATISFACTORY QUALITY, +* NON-INFRINGEMENT AND FITNESS FOR A PARTICULAR PURPOSE. +""" + +__author__ = 'VMware, Inc.' + + +import requests +from vmware.vapi.bindings.stub import ApiClient, StubFactoryBase +from vmware.vapi.lib.connect import get_requests_connector +from vmware.vapi.stdlib.client.factories import StubConfigurationFactory + +from vmware.vapi.vsphere.client import StubFactory + +stub_config = StubConfigurationFactory.new_std_configuration( + get_requests_connector(session=requests.session(), url='https://localhost/vapi')) +stub_factory = StubFactory(stub_config) +client = ApiClient(stub_factory) + + +def test_vcenter_client(): + assert hasattr(client, 'vcenter') + assert isinstance(client.vcenter, StubFactoryBase) + + +def test_cluster_client(): + assert hasattr(client.vcenter, 'Cluster') + + +def test_datacenter_client(): + assert hasattr(client.vcenter, 'Datacenter') + + +def test_datastore_client(): + assert hasattr(client.vcenter, 'Datastore') + + +def test_deployment_client(): + assert hasattr(client.vcenter, 'Deployment') + + +def test_configuration_client(): + assert hasattr(client.content, 'Configuration') + + +def test_appliance_client(): + assert hasattr(client, 'appliance') + assert isinstance(client.appliance, StubFactoryBase) + + +def test_content_client(): + assert hasattr(client, 'content') + assert isinstance(client.content, StubFactoryBase) + + +def test_tagging_client(): + assert hasattr(client, 'tagging') + assert isinstance(client.tagging, StubFactoryBase) + + +def test_ovf_client(): + assert hasattr(client.vcenter, 'ovf') + assert isinstance(client.vcenter.ovf, StubFactoryBase) + + +def test_hvc_client(): + assert hasattr(client.vcenter, 'hvc') + assert isinstance(client.vcenter.hvc, StubFactoryBase) + + +def test_inventory_client(): + assert hasattr(client.vcenter, 'inventory') + assert isinstance(client.vcenter.inventory, StubFactoryBase) + + +def test_iso_client(): + assert hasattr(client.vcenter, 'iso') + assert isinstance(client.vcenter.iso, StubFactoryBase) + + +def test_ovf_client(): + assert hasattr(client.vcenter, 'ovf') + assert isinstance(client.vcenter.ovf, StubFactoryBase) + + +def test_vm_template_client(): + assert hasattr(client.vcenter, 'vm_template') + assert isinstance(client.vcenter.vm_template, StubFactoryBase) + + +def test_appliance_update_client(): + assert hasattr(client.appliance, 'recovery') + assert isinstance(client.appliance.recovery, StubFactoryBase) + + +def test_appliance_vmon_client(): + assert hasattr(client.appliance, 'vmon') + assert isinstance(client.appliance.vmon, StubFactoryBase) + + +def test_compute_policy_client(): + assert hasattr(client.vcenter, 'compute') + assert isinstance(client.vcenter.compute, StubFactoryBase) + + +def test_vm_compute_policy_client(): + assert hasattr(client.vcenter.vm, 'compute') + assert isinstance(client.vcenter.vm.compute, StubFactoryBase)