Hello community,
here is the log from the commit of package python3-img-proof for
openSUSE:Factory checked in at 2020-04-23 18:32:33
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python3-img-proof (Old)
and /work/SRC/openSUSE:Factory/.python3-img-proof.new.2738 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python3-img-proof"
Thu Apr 23 18:32:33 2020 rev:8 rq:796349 version:5.0.0
Changes:
--------
--- /work/SRC/openSUSE:Factory/python3-img-proof/python3-img-proof.changes
2020-03-11 18:54:34.331654112 +0100
+++
/work/SRC/openSUSE:Factory/.python3-img-proof.new.2738/python3-img-proof.changes
2020-04-23 18:32:36.828337529 +0200
@@ -1,0 +2,6 @@
+Tue Apr 21 22:00:30 UTC 2020 - Sean Marlow <[email protected]>
+
+- Update to v5.0.0 (2020-04-21)
+ + Migrate GCE to Google API.
+
+-------------------------------------------------------------------
Old:
----
img-proof-4.8.1.tar.gz
New:
----
img-proof-5.0.0.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python3-img-proof.spec ++++++
--- /var/tmp/diff_new_pack.9rtjY6/_old 2020-04-23 18:32:37.420338661 +0200
+++ /var/tmp/diff_new_pack.9rtjY6/_new 2020-04-23 18:32:37.424338669 +0200
@@ -18,7 +18,7 @@
%bcond_without test
Name: python3-img-proof
-Version: 4.8.1
+Version: 5.0.0
Release: 0
Summary: Command line and API for testing custom images
License: GPL-3.0-or-later
@@ -30,7 +30,6 @@
BuildRequires: python3-devel
BuildRequires: python3-setuptools
Requires: python3-PyYAML
-Requires: python3-apache-libcloud
Requires: python3-azure-common
Requires: python3-azure-mgmt-compute
Requires: python3-azure-mgmt-network
@@ -39,16 +38,16 @@
Requires: python3-certifi
Requires: python3-click
Requires: python3-cryptography
+Requires: python3-google-api-python-client
+Requires: python3-google-auth
Requires: python3-oci-sdk
Requires: python3-paramiko
Requires: python3-pycryptodome
Requires: python3-pytest
Requires: python3-testinfra
-Obsoletes: python3-ipa < 4.8.1
BuildArch: noarch
%if %{with test}
BuildRequires: python3-PyYAML
-BuildRequires: python3-apache-libcloud
BuildRequires: python3-azure-common
BuildRequires: python3-azure-mgmt-compute
BuildRequires: python3-azure-mgmt-network
@@ -57,6 +56,8 @@
BuildRequires: python3-certifi
BuildRequires: python3-coverage
BuildRequires: python3-cryptography
+BuildRequires: python3-google-api-python-client
+BuildRequires: python3-google-auth
BuildRequires: python3-oci-sdk
BuildRequires: python3-paramiko
BuildRequires: python3-pycryptodome
@@ -64,6 +65,7 @@
BuildRequires: python3-pytest-cov
BuildRequires: python3-testinfra
%endif
+Obsoletes: python3-ipa < 5.0.0
%description
img-proof provides a command line utility to test images in
@@ -74,7 +76,7 @@
Group: Development/Languages/Python
Requires: python3-susepubliccloudinfo
PreReq: python3-img-proof = %{version}
-Obsoletes: python3-ipa-tests < 4.8.1
+Obsoletes: python3-ipa-tests < 5.0.0
%description tests
Directory of infrastructure tests for testing images.
++++++ img-proof-4.8.1.tar.gz -> img-proof-5.0.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/img-proof-4.8.1/CHANGES.md
new/img-proof-5.0.0/CHANGES.md
--- old/img-proof-4.8.1/CHANGES.md 2020-03-10 22:30:44.000000000 +0100
+++ new/img-proof-5.0.0/CHANGES.md 2020-04-21 23:50:16.000000000 +0200
@@ -1,3 +1,9 @@
+v5.0.0 (2020-04-21)
+===================
+
+- Migrate GCE to Google API.
+ [\#242](https://github.com/SUSE-Enceladus/ipa/pull/242)
+
v4.8.1 (2020-03-10)
===================
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/img-proof-4.8.1/PKG-INFO new/img-proof-5.0.0/PKG-INFO
--- old/img-proof-4.8.1/PKG-INFO 2020-03-10 22:31:42.000000000 +0100
+++ new/img-proof-5.0.0/PKG-INFO 2020-04-21 23:51:07.000000000 +0200
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: img-proof
-Version: 4.8.1
+Version: 5.0.0
Summary: Package for automated testing of cloud images.
Home-page: https://github.com/SUSE-Enceladus/img-proof
Author: SUSE
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/img-proof-4.8.1/img_proof/__init__.py
new/img-proof-5.0.0/img_proof/__init__.py
--- old/img-proof-4.8.1/img_proof/__init__.py 2020-03-10 22:30:44.000000000
+0100
+++ new/img-proof-5.0.0/img_proof/__init__.py 2020-04-21 23:50:16.000000000
+0200
@@ -22,4 +22,4 @@
__author__ = """SUSE"""
__email__ = '[email protected]'
-__version__ = '4.8.1'
+__version__ = '5.0.0'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/img-proof-4.8.1/img_proof/ipa_azure.py
new/img-proof-5.0.0/img_proof/ipa_azure.py
--- old/img-proof-4.8.1/img_proof/ipa_azure.py 2020-03-10 22:30:44.000000000
+0100
+++ new/img-proof-5.0.0/img_proof/ipa_azure.py 2020-04-21 23:50:16.000000000
+0200
@@ -63,7 +63,9 @@
timeout=None,
vnet_name=None,
vnet_resource_group=None,
- collect_vm_info=None
+ collect_vm_info=None,
+ enable_secure_boot=None,
+ enable_uefi=None
):
"""Initialize Azure Cloud class."""
super(AzureCloud, self).__init__(
@@ -89,7 +91,9 @@
collect_vm_info,
ssh_private_key_file,
ssh_user,
- subnet_id
+ subnet_id,
+ enable_secure_boot,
+ enable_uefi
)
self.vnet_name = vnet_name or self.ipa_config['vnet_name']
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/img-proof-4.8.1/img_proof/ipa_cloud.py
new/img-proof-5.0.0/img_proof/ipa_cloud.py
--- old/img-proof-4.8.1/img_proof/ipa_cloud.py 2020-03-10 22:30:44.000000000
+0100
+++ new/img-proof-5.0.0/img_proof/ipa_cloud.py 2020-04-21 23:50:16.000000000
+0200
@@ -45,9 +45,7 @@
from img_proof.ipa_exceptions import (
IpaException,
IpaCloudException,
- IpaSSHException,
- IpaRetryableError,
- GCECloudRetryableError
+ IpaSSHException
)
from img_proof.results_plugin import Report
@@ -95,7 +93,9 @@
collect_vm_info=None,
ssh_private_key_file=None,
ssh_user=None,
- subnet_id=None
+ subnet_id=None,
+ enable_secure_boot=None,
+ enable_uefi=None
):
"""Initialize base cloud framework class."""
super(IpaCloud, self).__init__()
@@ -157,6 +157,11 @@
self.ssh_private_key_file = self.ipa_config['ssh_private_key_file']
self.ssh_user = self.ipa_config['ssh_user']
self.subnet_id = self.ipa_config['subnet_id']
+ self.enable_secure_boot = self.ipa_config['enable_secure_boot']
+ self.enable_uefi = self.ipa_config['enable_uefi']
+
+ if self.enable_secure_boot and not self.enable_uefi:
+ self.enable_uefi = True
if self.cloud_config:
self.cloud_config = os.path.expanduser(self.cloud_config)
@@ -668,19 +673,11 @@
self.logger.info('Launching new instance')
try:
self._launch_instance()
- except GCECloudRetryableError as error:
- with ipa_utils.ignored(Exception):
- self._cleanup_instance(1)
-
- msg = 'Unable to connect to instance: %s' % error
- self.logger.error(msg)
- raise IpaRetryableError(msg)
except Exception as error:
with ipa_utils.ignored(Exception):
self._cleanup_instance(1)
- msg = 'Unable to connect to instance: %s' % error
- self.logger.error(msg)
+ self.logger.error(error)
raise
if not self.instance_ip:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/img-proof-4.8.1/img_proof/ipa_controller.py
new/img-proof-5.0.0/img_proof/ipa_controller.py
--- old/img-proof-4.8.1/img_proof/ipa_controller.py 2020-03-10
22:30:44.000000000 +0100
+++ new/img-proof-5.0.0/img_proof/ipa_controller.py 2020-04-21
23:50:16.000000000 +0200
@@ -48,6 +48,7 @@
early_exit=None,
history_log=None,
image_id=None,
+ image_project=None,
inject=None,
instance_type=None,
ip_address=None,
@@ -75,7 +76,9 @@
signing_key_fingerprint=None,
signing_key_file=None,
tenancy=None,
- oci_user_id=None
+ oci_user_id=None,
+ enable_secure_boot=None,
+ enable_uefi=None
):
"""Creates a cloud framework instance and initiates testing."""
kwargs = {
@@ -100,7 +103,9 @@
'test_dirs': test_dirs,
'test_files': tests,
'timeout': timeout,
- 'collect_vm_info': collect_vm_info
+ 'collect_vm_info': collect_vm_info,
+ 'enable_secure_boot': enable_secure_boot,
+ 'enable_uefi': enable_uefi
}
cloud_name = cloud_name.lower()
@@ -124,6 +129,7 @@
elif cloud_name == 'gce':
cloud = GCECloud(
service_account_file=service_account_file,
+ image_project=image_project,
**kwargs
)
elif cloud_name == 'ssh':
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/img-proof-4.8.1/img_proof/ipa_ec2.py
new/img-proof-5.0.0/img_proof/ipa_ec2.py
--- old/img-proof-4.8.1/img_proof/ipa_ec2.py 2020-03-10 22:30:44.000000000
+0100
+++ new/img-proof-5.0.0/img_proof/ipa_ec2.py 2020-04-21 23:50:16.000000000
+0200
@@ -67,7 +67,9 @@
test_dirs=None,
test_files=None,
timeout=None,
- collect_vm_info=None
+ collect_vm_info=None,
+ enable_secure_boot=None,
+ enable_uefi=None
):
"""Initialize EC2 cloud framework class."""
super(EC2Cloud, self).__init__(
@@ -93,7 +95,9 @@
collect_vm_info,
ssh_private_key_file,
ssh_user,
- subnet_id
+ subnet_id,
+ enable_secure_boot,
+ enable_uefi
)
# Get command line values that are not None
cmd_line_values = self._get_non_null_values(locals())
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/img-proof-4.8.1/img_proof/ipa_exceptions.py
new/img-proof-5.0.0/img_proof/ipa_exceptions.py
--- old/img-proof-4.8.1/img_proof/ipa_exceptions.py 2020-03-10
22:30:44.000000000 +0100
+++ new/img-proof-5.0.0/img_proof/ipa_exceptions.py 2020-04-21
23:50:16.000000000 +0200
@@ -41,10 +41,6 @@
"""Generic GCE exception."""
-class GCECloudRetryableError(GCECloudException):
- """GCE retryable error exception."""
-
-
class OCICloudException(IpaCloudException):
"""Generic OCI exception."""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/img-proof-4.8.1/img_proof/ipa_gce.py
new/img-proof-5.0.0/img_proof/ipa_gce.py
--- old/img-proof-4.8.1/img_proof/ipa_gce.py 2020-03-10 22:30:44.000000000
+0100
+++ new/img-proof-5.0.0/img_proof/ipa_gce.py 2020-04-21 23:50:16.000000000
+0200
@@ -2,7 +2,7 @@
"""Cloud framework module for testing Google Compute Engine (GCE) images."""
-# Copyright (c) 2019 SUSE LLC. All rights reserved.
+# Copyright (c) 2020 SUSE LLC. All rights reserved.
#
# This file is part of img_proof. img_proof provides an api and command line
# utilities for testing images in the Public Cloud.
@@ -22,21 +22,59 @@
import json
import os
+import time
+
+from contextlib import contextmanager, suppress
from img_proof import ipa_utils
from img_proof.ipa_constants import (
GCE_DEFAULT_TYPE,
GCE_DEFAULT_USER
)
-from img_proof.ipa_exceptions import GCECloudException
-from img_proof.ipa_exceptions import GCECloudRetryableError
+from img_proof.ipa_exceptions import GCECloudException, IpaRetryableError
from img_proof.ipa_cloud import IpaCloud
-from libcloud.common.google import ResourceNotFoundError
-from libcloud.common.google import GoogleBaseError
-from libcloud.common.google import QuotaExceededError
-from libcloud.compute.types import Provider
-from libcloud.compute.providers import get_driver
+from google.oauth2 import service_account
+from googleapiclient import discovery
+
+
+def get_message_from_http_error(error, resource_name):
+ """
+ Attempt to parse error message from json.
+
+ If there is an error getting the message content
+ use the default of `resource not found`.
+ """
+ with suppress(AttributeError):
+ # In python 3.5 content is bytes
+ error.content = error.content.decode()
+
+ try:
+ message = json.loads(error.content)['error']['message']
+ except (AttributeError, KeyError):
+ message = 'Resource {resource_name} not found.'.format(
+ resource_name=resource_name
+ )
+
+ return message
+
+
+@contextmanager
+def handle_gce_http_errors(type_name, resource_name):
+ """
+ Context manager to handle GCE HTTP Errors.
+ """
+ try:
+ yield
+ except Exception as error:
+ message = get_message_from_http_error(error, resource_name)
+
+ raise GCECloudException(
+ 'Unable to retrieve {type_name}: {error}'.format(
+ type_name=type_name,
+ error=message
+ )
+ ) from error
class GCECloud(IpaCloud):
@@ -68,7 +106,10 @@
test_dirs=None,
test_files=None,
timeout=None,
- collect_vm_info=None
+ collect_vm_info=None,
+ image_project=None,
+ enable_secure_boot=None,
+ enable_uefi=None
):
super(GCECloud, self).__init__(
'gce',
@@ -93,7 +134,9 @@
collect_vm_info,
ssh_private_key_file,
ssh_user,
- subnet_id
+ subnet_id,
+ enable_secure_boot,
+ enable_uefi
)
self.service_account_file = (
@@ -116,14 +159,15 @@
self.ssh_user = self.ssh_user or GCE_DEFAULT_USER
self.ssh_public_key = self._get_ssh_public_key()
+ self.image_project = image_project
- self._get_service_account_info()
+ self.credentials = self._get_credentials()
self.compute_driver = self._get_driver()
self._validate_region()
- def _get_service_account_info(self):
- """Retrieve json dict from service account file."""
+ def _get_credentials(self):
+ """Retrieve credentials object using service account file."""
with open(self.service_account_file, 'r') as f:
info = json.load(f)
@@ -143,28 +187,27 @@
'docs for information on GCE configuration.'
)
+ return service_account.Credentials.from_service_account_file(
+ self.service_account_file
+ )
+
def _get_driver(self):
"""Get authenticated GCE driver."""
- ComputeEngine = get_driver(Provider.GCE)
- return ComputeEngine(
- self.service_account_email,
- self.service_account_file,
- project=self.service_account_project
+ return discovery.build(
+ 'compute',
+ 'v1',
+ credentials=self.credentials,
+ cache_discovery=False
)
def _get_instance(self):
"""Retrieve instance matching instance_id."""
- try:
- instance = self.compute_driver.ex_get_node(
- self.running_instance_id,
- zone=self.region
- )
- except ResourceNotFoundError as e:
- raise GCECloudException(
- 'Instance with id: {id} cannot be found: {error}'.format(
- id=self.running_instance_id, error=e
- )
- )
+ with handle_gce_http_errors('instance', self.running_instance_id):
+ instance = self.compute_driver.instances().get(
+ project=self.service_account_project,
+ zone=self.region,
+ instance=self.running_instance_id
+ ).execute()
return instance
@@ -176,92 +219,258 @@
key=key.decode()
)
+ def _get_network(self, network_id):
+ """
+ Return the network by network id (name).
+
+ If network not found GCE will raise a 404 error.
+ """
+ with handle_gce_http_errors('network', network_id):
+ network = self.compute_driver.networks().get(
+ project=self.service_account_project,
+ network=network_id
+ ).execute()
+
+ return network
+
def _get_subnet(self, subnet_id):
- subnet = None
- try:
+ """
+ Return the subnet by subnet id (name).
+
+ If subnet not found GCE will raise a 404 error.
+ """
+ with handle_gce_http_errors('subnet', subnet_id):
# Subnet lives in a region whereas self.region
# is a specific zone (us-west1-a).
region = '-'.join(self.region.split('-')[:-1])
- subnet = self.compute_driver.ex_get_subnetwork(
- subnet_id, region=region
- )
- except Exception:
- raise GCECloudException(
- 'GCE subnet: {subnet_id} not found.'.format(
- subnet_id=subnet_id
- )
- )
+ subnet = self.compute_driver.subnetworks().get(
+ project=self.service_account_project,
+ region=region,
+ subnetwork=subnet_id
+ ).execute()
return subnet
+ def _get_instance_type(self, type_name):
+ """
+ Return the instance type by name.
+
+ If type not found GCE will raise a 404 error.
+ """
+ with handle_gce_http_errors('instance type', type_name):
+ machine_type = self.compute_driver.machineTypes().get(
+ project=self.service_account_project,
+ zone=self.region,
+ machineType=type_name
+ ).execute()
+
+ return machine_type
+
+ def _get_image(self, image_name):
+ """
+ Return the image by image name.
+
+ If image is not found GCE will raise a 404 error.
+ """
+ with handle_gce_http_errors('image', image_name):
+ image = self.compute_driver.images().get(
+ project=self.image_project or self.service_account_project,
+ image=image_name
+ ).execute()
+
+ return image
+
+ def _get_disk(self, disk_name):
+ """
+ Return the disk by name.
+
+ If disk is not found GCE will raise a 404 error.
+ """
+ with handle_gce_http_errors('disk', disk_name):
+ disk = self.compute_driver.disks().get(
+ project=self.service_account_project,
+ zone=self.region,
+ disk=disk_name
+ ).execute()
+
+ return disk
+
+ def _get_network_config(self, subnet_id):
+ """
+ Return the network config.
+
+ If a subnet_id is provided use the subnet and
+ network. Otherwise use the default network.
+ """
+ interface = {
+ 'accessConfigs': [{
+ 'name': 'External NAT',
+ 'type': 'ONE_TO_ONE_NAT'
+ }]
+ }
+
+ if subnet_id:
+ subnet = self._get_subnet(subnet_id)
+ interface['subnetwork'] = subnet['selfLink']
+ interface['network'] = subnet['network']
+ else:
+ interface['network'] = self._get_network('default')['selfLink']
+
+ return interface
+
+ @staticmethod
+ def get_shielded_instance_config(
+ enable_secure_boot=False,
+ enable_vtpm=True,
+ enable_integrity_monitoring=True
+ ):
+ """
+ Return shielded instance config object.
+
+ Return with default values unless overridden by args.
+ """
+ shielded_instance_config = {
+ 'enableSecureBoot': enable_secure_boot,
+ 'enableVtpm': enable_vtpm,
+ 'enableIntegrityMonitoring': enable_integrity_monitoring
+ }
+
+ return shielded_instance_config
+
+ @staticmethod
+ def get_instance_config(
+ instance_name,
+ machine_type,
+ network_interfaces,
+ service_account_email,
+ source_image,
+ ssh_key,
+ auto_delete=True,
+ boot_disk=True,
+ disk_type='PERSISTENT',
+ disk_mode='READ_WRITE',
+ shielded_instance_config=None,
+ ):
+ """Return an instance config for launching a new instance."""
+ config = {
+ 'metadata': {
+ 'items': [{'key': 'ssh-keys', 'value': ssh_key}]
+ },
+ 'service_accounts': [{
+ 'email': service_account_email,
+ 'scopes': ['storage-ro']
+ }],
+ 'machineType': machine_type,
+ 'disks': [{
+ 'autoDelete': auto_delete,
+ 'boot': boot_disk,
+ 'type': disk_type,
+ 'mode': disk_mode,
+ 'deviceName': instance_name,
+ 'initializeParams': {
+ 'diskName': instance_name,
+ 'sourceImage': source_image
+ }
+ }],
+ 'networkInterfaces': network_interfaces,
+ 'name': instance_name
+ }
+
+ if shielded_instance_config:
+ config['shieldedInstanceConfig'] = shielded_instance_config
+ config['disks'][0]['guestOsFeatures'] = [{
+ 'type': 'UEFI_COMPATIBLE'
+ }]
+
+ return config
+
def _launch_instance(self):
"""Launch an instance of the given image."""
- metadata = {'key': 'ssh-keys', 'value': self.ssh_public_key}
self.running_instance_id = ipa_utils.generate_instance_name(
'gce-img-proof-test'
)
self.logger.debug('ID of instance: %s' % self.running_instance_id)
+ machine_type = self._get_instance_type(
+ self.instance_type or GCE_DEFAULT_TYPE
+ )['selfLink']
+ source_image = self._get_image(self.image_id)['selfLink']
+ network_interfaces = [self._get_network_config(self.subnet_id)]
+
kwargs = {
- 'location': self.region,
- 'ex_metadata': metadata,
- 'ex_service_accounts': [{
- 'email': self.service_account_email,
- 'scopes': ['storage-ro']
- }]
+ 'instance_name': self.running_instance_id,
+ 'machine_type': machine_type,
+ 'service_account_email': self.service_account_email,
+ 'source_image': source_image,
+ 'ssh_key': self.ssh_public_key,
+ 'network_interfaces': network_interfaces
}
- if self.subnet_id:
- kwargs['ex_subnetwork'] = self._get_subnet(self.subnet_id)
- kwargs['ex_network'] = kwargs['ex_subnetwork'].network
+ if self.enable_uefi:
+ kwargs['shielded_instance_config'] = \
+ self.get_shielded_instance_config(
+ enable_secure_boot=self.enable_secure_boot
+ )
try:
- instance = self.compute_driver.create_node(
- self.running_instance_id,
- self.instance_type or GCE_DEFAULT_TYPE,
- self.image_id,
- **kwargs
- )
- except ResourceNotFoundError as error:
+ response = self.compute_driver.instances().insert(
+ project=self.service_account_project,
+ zone=self.region,
+ body=self.get_instance_config(**kwargs)
+ ).execute()
+ except Exception as error:
+ with suppress(AttributeError):
+ # In python 3.5 content is bytes
+ error.content = error.content.decode()
+
+ error_obj = json.loads(error.content)['error']
+
try:
- message = error.value['message']
- except TypeError:
- message = error
+ message = error_obj['message']
+ except (AttributeError, KeyError):
+ message = 'Unknown exception.'
+
+ if error_obj['code'] == 412:
+ # 412 is conditionNotmet
+ error_class = IpaRetryableError
+ else:
+ error_class = GCECloudException
- raise GCECloudException(
- 'An error occurred launching instance: {message}.'.format(
+ raise error_class(
+ 'Failed to launch instance: {message}'.format(
message=message
)
- )
- except QuotaExceededError as error:
- raise GCECloudRetryableError(
- 'An error occurred launching instance: {message}.'.format(
- message=error.value['message']
- )
- )
- except GoogleBaseError as error:
- if error.value['reason'] in ['quotaExceeded', 'conditionNotMet']:
- raise GCECloudRetryableError(
- 'An error occurred launching instance: {message}.'.format(
- message=error.value['message']
- )
- )
+ ) from error
+
+ operation = self._wait_on_operation(response['name'])
+
+ if 'error' in operation and operation['error'].get('errors'):
+ error = operation['error']['errors'][0]
+
+ if error['code'] in ('QUOTA_EXCEEDED', 'PRECONDITION_FAILED'):
+ error_class = IpaRetryableError
else:
- raise GCECloudException(
- 'An error occurred launching instance: {message}.'.format(
- message=error.value['message']
- )
+ error_class = GCECloudException
+
+ raise error_class(
+ 'Failed to launch instance: {message}'.format(
+ message=error['message']
)
+ )
- self.compute_driver.wait_until_running(
- [instance],
+ self._wait_on_instance(
+ 'RUNNING',
timeout=self.timeout
)
def _set_image_id(self):
- """If existing image used get image id."""
+ """Set the image_id instance variable based on boot disk."""
instance = self._get_instance()
- self.image_id = instance.image
+ disk = self._get_disk(instance['disks'][0]['deviceName'])
+
+ # Example sourceImage format:
+ # projects/debian-cloud/global/images/opensuse-leap-15.0-YYYYMMDD
+ self.image_id = disk['sourceImage'].rsplit('/', maxsplit=1)[-1]
def _validate_region(self):
"""Validate region was passed in and is a valid GCE zone."""
@@ -272,7 +481,10 @@
)
try:
- zone = self.compute_driver.ex_get_zone(self.region)
+ zone = self.compute_driver.zones().get(
+ project=self.service_account_project,
+ zone=self.region
+ ).execute()
except Exception:
zone = None
@@ -287,50 +499,87 @@
def _get_instance_state(self):
"""Attempt to retrieve the state of the instance."""
instance = self._get_instance()
- return instance.state
+ return instance['status']
def _is_instance_running(self):
"""Return True if instance is in running state."""
- return self._get_instance_state() == 'running'
+ return self._get_instance_state() == 'RUNNING'
def _set_instance_ip(self):
"""Retrieve and set the instance ip address."""
instance = self._get_instance()
- if instance.public_ips:
- self.instance_ip = instance.public_ips[0]
- elif instance.private_ips:
- self.instance_ip = instance.private_ips[0]
- else:
- raise GCECloudException(
- 'IP address for instance: %s cannot be found.'
- % self.running_instance_id
- )
+ interface = instance['networkInterfaces'][0]
+ try:
+ self.instance_ip = interface['accessConfigs'][0]['natIP']
+ except (KeyError, IndexError):
+ try:
+ self.instance_ip = interface['networkIP']
+ except KeyError:
+ raise GCECloudException(
+ 'IP address for instance: %s cannot be found.'
+ % self.running_instance_id
+ )
def _start_instance(self):
"""Start the instance."""
- instance = self._get_instance()
- self.compute_driver.ex_start_node(instance)
- self.compute_driver.wait_until_running(
- [instance],
+ self.compute_driver.instances().start(
+ project=self.service_account_project,
+ zone=self.region,
+ instance=self.running_instance_id
+ ).execute()
+
+ self._wait_on_instance(
+ 'RUNNING',
timeout=self.timeout
)
def _stop_instance(self):
"""Stop the instance."""
- instance = self._get_instance()
- self.compute_driver.ex_stop_node(instance)
- self._wait_on_instance('stopped', timeout=self.timeout)
+ self.compute_driver.instances().stop(
+ project=self.service_account_project,
+ zone=self.region,
+ instance=self.running_instance_id
+ ).execute()
+
+ # In GCE an instance that is stopped has a state of TERMINATED:
+ # https://cloud.google.com/compute/docs/instances/instance-life-cycle
+ self._wait_on_instance(
+ 'TERMINATED',
+ timeout=self.timeout
+ )
def _terminate_instance(self):
"""Terminate the instance."""
- instance = self._get_instance()
- instance.destroy()
+ self.compute_driver.instances().delete(
+ project=self.service_account_project,
+ zone=self.region,
+ instance=self.running_instance_id
+ ).execute()
def get_console_log(self):
"""
Return console log output if it is available.
"""
- instance = self._get_instance()
- output = self.compute_driver.ex_get_serial_output(instance)
- return output
+ output = self.compute_driver.instances().getSerialPortOutput(
+ project=self.service_account_project,
+ zone=self.region,
+ instance=self.running_instance_id
+ ).execute()
+ return output.get('contents', '')
+
+ def _wait_on_operation(self, operation_name, timeout=600, wait_period=10):
+ start = time.time()
+ end = start + timeout
+
+ while time.time() < end:
+ time.sleep(wait_period)
+
+ operation = self.compute_driver.zoneOperations().get(
+ project=self.service_account_project,
+ zone=self.region,
+ operation=operation_name
+ ).execute()
+
+ if operation['status'] == 'DONE':
+ return operation
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/img-proof-4.8.1/img_proof/ipa_oci.py
new/img-proof-5.0.0/img_proof/ipa_oci.py
--- old/img-proof-4.8.1/img_proof/ipa_oci.py 2020-03-10 22:30:44.000000000
+0100
+++ new/img-proof-5.0.0/img_proof/ipa_oci.py 2020-04-21 23:50:16.000000000
+0200
@@ -63,7 +63,9 @@
signing_key_fingerprint=None,
signing_key_file=None,
tenancy=None,
- oci_user_id=None
+ oci_user_id=None,
+ enable_secure_boot=None,
+ enable_uefi=None
):
"""Initialize OCI cloud framework class."""
super(OCICloud, self).__init__(
@@ -89,7 +91,9 @@
collect_vm_info,
ssh_private_key_file,
ssh_user,
- subnet_id
+ subnet_id,
+ enable_secure_boot,
+ enable_uefi
)
self.availability_domain = (
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/img-proof-4.8.1/img_proof/scripts/cli.py
new/img-proof-5.0.0/img_proof/scripts/cli.py
--- old/img-proof-4.8.1/img_proof/scripts/cli.py 2020-03-10
22:30:44.000000000 +0100
+++ new/img-proof-5.0.0/img_proof/scripts/cli.py 2020-04-21
23:50:16.000000000 +0200
@@ -137,6 +137,12 @@
help='The ID of the image used for instance.'
)
@click.option(
+ '--image-project',
+ help='The image project where the image exists. This is required if '
+ 'testing an image in a project different than the service account '
+ 'project.'
+)
[email protected](
'--inject',
help='Path to an injection yaml config file.',
type=click.Path(exists=True)
@@ -273,6 +279,18 @@
'--oci-user-id',
help='The ID for the OCI user.'
)
[email protected](
+ '--enable-secure-boot',
+ is_flag=True,
+ help='Enable secure boot for the instance. Secure boot requires '
+ 'UEFI boot firmware.'
+)
[email protected](
+ '--enable-uefi',
+ is_flag=True,
+ help='Enable boot firmware for the instance. By default secure boot '
+ 'is disabled.'
+)
@click.argument('tests', nargs=-1)
@click.pass_context
def test(context,
@@ -286,6 +304,7 @@
early_exit,
history_log,
image_id,
+ image_project,
inject,
instance_type,
ip_address,
@@ -314,6 +333,8 @@
signing_key_file,
tenancy,
oci_user_id,
+ enable_secure_boot,
+ enable_uefi,
tests):
"""Test image in the given framework using the supplied test files."""
no_color = context.obj['no_color']
@@ -330,6 +351,7 @@
early_exit,
history_log,
image_id,
+ image_project,
inject,
instance_type,
ip_address,
@@ -358,6 +380,8 @@
signing_key_file,
tenancy,
oci_user_id,
+ enable_secure_boot,
+ enable_uefi
)
echo_results(results, no_color)
sys.exit(status)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/img-proof-4.8.1/img_proof.egg-info/PKG-INFO
new/img-proof-5.0.0/img_proof.egg-info/PKG-INFO
--- old/img-proof-4.8.1/img_proof.egg-info/PKG-INFO 2020-03-10
22:31:42.000000000 +0100
+++ new/img-proof-5.0.0/img_proof.egg-info/PKG-INFO 2020-04-21
23:51:07.000000000 +0200
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: img-proof
-Version: 4.8.1
+Version: 5.0.0
Summary: Package for automated testing of cloud images.
Home-page: https://github.com/SUSE-Enceladus/img-proof
Author: SUSE
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/img-proof-4.8.1/img_proof.egg-info/requires.txt
new/img-proof-5.0.0/img_proof.egg-info/requires.txt
--- old/img-proof-4.8.1/img_proof.egg-info/requires.txt 2020-03-10
22:31:42.000000000 +0100
+++ new/img-proof-5.0.0/img_proof.egg-info/requires.txt 2020-04-21
23:51:07.000000000 +0200
@@ -1,5 +1,4 @@
boto3
-apache-libcloud
azure-common
azure-mgmt-compute
azure-mgmt-network
@@ -13,6 +12,8 @@
PyYAML
testinfra
oci
+google-auth
+google-api-python-client
[dev]
coverage
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/img-proof-4.8.1/package/python3-img-proof.spec
new/img-proof-5.0.0/package/python3-img-proof.spec
--- old/img-proof-4.8.1/package/python3-img-proof.spec 2020-03-10
22:30:44.000000000 +0100
+++ new/img-proof-5.0.0/package/python3-img-proof.spec 2020-04-21
23:50:16.000000000 +0200
@@ -18,7 +18,7 @@
%bcond_without test
Name: python3-img-proof
-Version: 4.8.1
+Version: 5.0.0
Release: 0
Summary: Command line and API for testing custom images
License: GPL-3.0-or-later
@@ -31,7 +31,8 @@
BuildRequires: python3-click
Requires: python3-PyYAML
Requires: python3-boto3
-Requires: python3-apache-libcloud
+Requires: python3-google-auth
+Requires: python3-google-api-python-client
Requires: python3-azure-common
Requires: python3-azure-mgmt-compute
Requires: python3-azure-mgmt-network
@@ -48,7 +49,8 @@
%if %{with test}
BuildRequires: python3-PyYAML
BuildRequires: python3-boto3
-BuildRequires: python3-apache-libcloud
+BuildRequires: python3-google-auth
+BuildRequires: python3-google-api-python-client
BuildRequires: python3-azure-common
BuildRequires: python3-azure-mgmt-compute
BuildRequires: python3-azure-mgmt-network
@@ -63,7 +65,7 @@
BuildRequires: python3-testinfra
BuildRequires: python3-oci-sdk
%endif
-Obsoletes: python3-ipa < 4.8.1
+Obsoletes: python3-ipa < 5.0.0
%description
img-proof provides a command line utility to test images in
@@ -74,7 +76,7 @@
Group: Development/Languages/Python
Requires: python3-susepubliccloudinfo
PreReq: python3-img-proof = %{version}
-Obsoletes: python3-ipa-tests < 4.8.1
+Obsoletes: python3-ipa-tests < 5.0.0
%description tests
Directory of infrastructure tests for testing images.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/img-proof-4.8.1/requirements.txt
new/img-proof-5.0.0/requirements.txt
--- old/img-proof-4.8.1/requirements.txt 2020-03-10 22:30:44.000000000
+0100
+++ new/img-proof-5.0.0/requirements.txt 2020-04-21 23:50:16.000000000
+0200
@@ -1,5 +1,4 @@
boto3
-apache-libcloud
azure-common
azure-mgmt-compute
azure-mgmt-network
@@ -13,3 +12,5 @@
PyYAML
testinfra
oci
+google-auth
+google-api-python-client
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/img-proof-4.8.1/setup.cfg
new/img-proof-5.0.0/setup.cfg
--- old/img-proof-4.8.1/setup.cfg 2020-03-10 22:31:42.000000000 +0100
+++ new/img-proof-5.0.0/setup.cfg 2020-04-21 23:51:07.000000000 +0200
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 4.8.1
+current_version = 5.0.0
commit = True
tag = False
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/img-proof-4.8.1/setup.py new/img-proof-5.0.0/setup.py
--- old/img-proof-4.8.1/setup.py 2020-03-10 22:30:44.000000000 +0100
+++ new/img-proof-5.0.0/setup.py 2020-04-21 23:50:16.000000000 +0200
@@ -43,7 +43,7 @@
setup(
name='img-proof',
- version='4.8.1',
+ version='5.0.0',
description="Package for automated testing of cloud images.",
long_description=readme,
long_description_content_type="text/markdown",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/img-proof-4.8.1/tests/test_ipa_gce.py
new/img-proof-5.0.0/tests/test_ipa_gce.py
--- old/img-proof-4.8.1/tests/test_ipa_gce.py 2020-03-10 22:30:44.000000000
+0100
+++ new/img-proof-5.0.0/tests/test_ipa_gce.py 2020-04-21 23:50:16.000000000
+0200
@@ -21,25 +21,42 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
+import json
import pytest
from img_proof.ipa_gce import GCECloud
-from img_proof.ipa_exceptions import GCECloudException
+from img_proof.ipa_exceptions import GCECloudException, IpaRetryableError
from unittest.mock import MagicMock, patch
-from libcloud.common.google import ResourceNotFoundError
+from googleapiclient.errors import HttpError
+
+
+def get_http_error(msg, status='404'):
+ resp = MagicMock()
+ resp.status = status
+
+ content = {
+ 'error': {
+ 'code': int(status),
+ 'message': msg
+ }
+ }
+
+ return HttpError(resp, json.dumps(content).encode())
class TestGCECloud(object):
"""Test GCE cloud class."""
+ @patch('img_proof.ipa_gce.service_account')
@patch.object(GCECloud, '_validate_region')
- @patch('libcloud.compute.drivers.gce.GCENodeDriver')
+ @patch('img_proof.ipa_gce.discovery')
def setup(
self,
- mock_node_driver,
- mock_validate_region
+ mock_discovery,
+ mock_validate_region,
+ mock_service_account
):
"""Set up kwargs dict."""
self.kwargs = {
@@ -53,7 +70,11 @@
}
driver = MagicMock()
- mock_node_driver.return_value = driver
+ mock_discovery.build.return_value = driver
+
+ service_account = MagicMock()
+ mock_service_account.Credentials.\
+ from_service_account_file.return_value = service_account
self.cloud = GCECloud(**self.kwargs)
@@ -80,21 +101,14 @@
self.kwargs['ssh_private_key_file'] = 'tests/data/ida_test'
- def test_gce_get_service_account_info(self):
- """Test get service account info method."""
- self.cloud._get_service_account_info()
-
- assert self.cloud.service_account_email == \
- '[email protected]'
- assert self.cloud.service_account_project == 'test'
-
- def test_gce_get_service_account_info_invalid(self):
- """Test get service account info method."""
+ @patch('img_proof.ipa_gce.service_account')
+ def test_gce_get_service_account_info_invalid(self, mock_service_account):
+ """Test get credentials method with invalid service account."""
self.cloud.service_account_file = \
'tests/gce/invalid-service-account.json'
with pytest.raises(GCECloudException) as error:
- self.cloud._get_service_account_info()
+ self.cloud._get_credentials()
msg = 'Service account JSON file is invalid for GCE. ' \
'client_email key is expected. See getting started ' \
@@ -104,143 +118,312 @@
def test_gce_get_instance(self):
"""Test gce get instance method."""
instance = MagicMock()
- self.cloud.compute_driver.ex_get_node.return_value = instance
+ instances_obj = MagicMock()
+ operation = MagicMock()
+ operation.execute.return_value = instance
+ instances_obj.get.return_value = operation
+ self.cloud.compute_driver.instances.return_value = instances_obj
val = self.cloud._get_instance()
assert val == instance
self.cloud.running_instance_id = 'test-instance'
- self.cloud.compute_driver.ex_get_node.side_effect = \
- ResourceNotFoundError(
- 'Broken',
- 'test',
- 'test'
- )
+ instances_obj.get.side_effect = get_http_error(
+ 'test-instance cannot be found.'
+ )
with pytest.raises(GCECloudException) as error:
self.cloud._get_instance()
- assert str(error.value) == "Instance with id: test-instance cannot" \
- " be found: 'Broken'"
+ exc = "Unable to retrieve instance: test-instance cannot be found."
+ assert str(error.value) == exc
+
+ def test_gce_get_network(self):
+ """Test GCE get network method."""
+ network = MagicMock()
+ networks_obj = MagicMock()
+ operation = MagicMock()
+ operation.execute.return_value = network
+ networks_obj.get.return_value = operation
+ self.cloud.compute_driver.networks.return_value = networks_obj
+
+ result = self.cloud._get_network('test-network')
+
+ assert result == network
+
+ networks_obj.get.side_effect = get_http_error(
+ 'Resource test-network not found.'
+ )
+
+ msg = 'Unable to retrieve network: Resource test-network not found.'
+ with pytest.raises(GCECloudException) as error:
+ self.cloud._get_network('test-network')
+
+ assert msg == str(error.value)
def test_gce_get_subnet(self):
"""Test GCE get subnetwork method."""
subnetwork = MagicMock()
- self.cloud.compute_driver.ex_get_subnetwork.return_value = subnetwork
+ subnet_obj = MagicMock()
+ operation = MagicMock()
+ operation.execute.return_value = subnetwork
+ subnet_obj.get.return_value = operation
+ self.cloud.compute_driver.subnetworks.return_value = subnet_obj
self.cloud.region = 'us-west-1a'
result = self.cloud._get_subnet('test-subnet')
assert result == subnetwork
- def test_gce_get_subnet_exception(self):
- """Test GCE get subnetwork method."""
- self.cloud.compute_driver.ex_get_subnetwork.side_effect = Exception(
- 'Cannot find subnet!'
+ subnet_obj.get.side_effect = get_http_error(
+ 'Resource test-subnet not found.'
)
- self.cloud.region = 'us-west-1a'
-
- msg = 'GCE subnet: test-subnet not found.'
+ msg = 'Unable to retrieve subnet: Resource test-subnet not found.'
with pytest.raises(GCECloudException) as error:
self.cloud._get_subnet('test-subnet')
assert msg == str(error.value)
+ def test_gce_get_instance_type(self):
+ """Test GCE get instance type method."""
+ machine_type = MagicMock()
+ machine_type_obj = MagicMock()
+ operation = MagicMock()
+ operation.execute.return_value = machine_type
+ machine_type_obj.get.return_value = operation
+ self.cloud.compute_driver.machineTypes.return_value = machine_type_obj
+
+ result = self.cloud._get_instance_type('n1-standard-1')
+ assert result == machine_type
+
+ machine_type_obj.get.side_effect = get_http_error(
+ 'Resource n1-standard-1 not found.'
+ )
+
+ msg = 'Unable to retrieve instance type: ' \
+ 'Resource n1-standard-1 not found.'
+ with pytest.raises(GCECloudException) as error:
+ self.cloud._get_instance_type('n1-standard-1')
+
+ assert msg == str(error.value)
+
+ def test_gce_get_image(self):
+ """Test GCE get image method."""
+ image = MagicMock()
+ image_obj = MagicMock()
+ operation = MagicMock()
+ operation.execute.return_value = image
+ image_obj.get.return_value = operation
+ self.cloud.compute_driver.images.return_value = image_obj
+
+ result = self.cloud._get_image('fake-image-20200202')
+ assert result == image
+
+ image_obj.get.side_effect = get_http_error(
+ 'Resource fake-image-20200202 not found.'
+ )
+
+ msg = 'Unable to retrieve image: ' \
+ 'Resource fake-image-20200202 not found.'
+ with pytest.raises(GCECloudException) as error:
+ self.cloud._get_image('fake-image-20200202')
+
+ assert msg == str(error.value)
+
+ def test_gce_get_disk(self):
+ """Test GCE get image method."""
+ disk = MagicMock()
+ disk_obj = MagicMock()
+ operation = MagicMock()
+ operation.execute.return_value = disk
+ disk_obj.get.return_value = operation
+ self.cloud.compute_driver.disks.return_value = disk_obj
+
+ result = self.cloud._get_disk('disk12')
+ assert result == disk
+
+ disk_obj.get.side_effect = get_http_error(
+ 'Resource disk12 not found.'
+ )
+
+ msg = 'Unable to retrieve disk: ' \
+ 'Resource disk12 not found.'
+ with pytest.raises(GCECloudException) as error:
+ self.cloud._get_disk('disk12')
+
+ assert msg == str(error.value)
+
@patch.object(GCECloud, '_get_subnet')
+ def test_get_network_config(self, mock_get_subnet):
+ subnet = 'projects/test/regions/us-west1/subnetworks/sub-123'
+ net = 'projects/test/global/networks/network'
+
+ mock_get_subnet.return_value = {
+ 'selfLink': subnet,
+ 'network': net
+ }
+
+ subnet_config = self.cloud._get_network_config('sub-123')
+
+ assert subnet_config['network'] == net
+ assert subnet_config['subnetwork'] == subnet
+
+ def test_get_shielded_instance_config(self):
+ si_config = self.cloud.get_shielded_instance_config()
+
+ assert si_config['enableSecureBoot'] is False
+ assert si_config['enableVtpm']
+ assert si_config['enableIntegrityMonitoring']
+
+ def test_get_instance_config(self):
+ config = self.cloud.get_instance_config(
+ 'instance123',
+ 'n1-standard-1',
+ [{}],
+ '[email protected]',
+ 'image123',
+ 'secretkey',
+ shielded_instance_config={'shielded': 'config'}
+ )
+
+ assert 'metadata' in config
+ assert 'service_accounts' in config
+ assert 'machineType' in config
+ assert 'disks' in config
+ assert 'networkInterfaces' in config
+ assert 'name' in config
+ assert 'shieldedInstanceConfig' in config
+
+ @patch.object(GCECloud, '_wait_on_instance')
+ @patch.object(GCECloud, '_wait_on_operation')
+ @patch.object(GCECloud, '_get_network')
+ @patch.object(GCECloud, '_get_image')
+ @patch.object(GCECloud, '_get_instance_type')
@patch('img_proof.ipa_utils.generate_instance_name')
def test_gce_launch_instance(
self,
mock_generate_instance_name,
- mock_get_subnet
+ mock_get_instance_type,
+ mock_get_image,
+ mock_get_network,
+ mock_wait_on_operation,
+ mock_wait_on_instance
):
"""Test GCE launch instance method."""
- instance = MagicMock()
- self.cloud.compute_driver.create_node.return_value = instance
- self.cloud.compute_driver.wait_until_running.return_value = None
mock_generate_instance_name.return_value = 'test-instance'
+ mock_get_network.return_value = {
+ 'selfLink': 'projects/test/global/networks/net1'
+ }
+ mock_get_image.return_value = {
+ 'selfLink': 'projects/test/global/images/img-123'
+ }
+ mock_get_instance_type.return_value = {
+ 'selfLink': 'zones/us-west1-a/machineTypes/n1-standard-1'
+ }
+ mock_wait_on_operation.return_value = {}
- self.cloud.region = 'us-west1-a'
- self.cloud.subnet_id = 'test-subnet'
+ instances_obj = MagicMock()
+ operation = MagicMock()
+ operation.execute.return_value = {'name': 'operation123'}
+ instances_obj.insert.return_value = operation
+ self.cloud.compute_driver.instances.return_value = instances_obj
- subnet = MagicMock()
- network = MagicMock()
- subnet.network = network
- mock_get_subnet.return_value = subnet
+ self.cloud.region = 'us-west1-a'
self.cloud._launch_instance()
assert self.cloud.running_instance_id == 'test-instance'
+ assert mock_wait_on_instance.call_count == 1
+
+ # Exception on operation
+
+ mock_wait_on_operation.return_value = {
+ 'error': {
+ 'errors': [{
+ 'code': 'QUOTA_EXCEEDED',
+ 'message': 'Too many cpus.'
+ }]
+ }
+ }
+
+ with pytest.raises(IpaRetryableError) as error:
+ self.cloud._launch_instance()
+
+ assert 'Failed to launch instance: Too many cpus.' == str(error.value)
+
+ # Exception on API call
+ mock_wait_on_operation.return_value = {}
+ instances_obj.insert.side_effect = get_http_error(
+ 'Invalid instance type.',
+ '412'
+ )
+
+ with pytest.raises(IpaRetryableError) as error:
+ self.cloud._launch_instance()
+
+ msg = 'Failed to launch instance: Invalid instance type.'
+ assert msg == str(error.value)
+
+ @patch.object(GCECloud, '_get_disk')
@patch.object(GCECloud, '_get_instance')
- def test_gce_set_image_id(self, mock_get_instance):
+ def test_gce_set_image_id(self, mock_get_instance, mock_get_disk):
"""Test gce cloud set image id method."""
- instance = MagicMock()
- instance.image = 'test-image'
+ instance = {
+ 'disks': [{'deviceName': 'disk123'}]
+ }
+ disk = {
+ 'sourceImage': 'projects/suse/global/images/opensuse-leap-15.0'
+ }
mock_get_instance.return_value = instance
+ mock_get_disk.return_value = disk
self.cloud._set_image_id()
- assert self.cloud.image_id == instance.image
+ assert self.cloud.image_id == 'opensuse-leap-15.0'
assert mock_get_instance.call_count == 1
+ assert mock_get_disk.call_count == 1
- @patch.object(GCECloud, '_get_driver')
- def test_gce_validate_region(self, mock_get_driver):
+ def test_gce_validate_region(self):
"""Test gce cloud set image id method."""
- driver = MagicMock()
- driver.ex_get_zone.return_value = None
- mock_get_driver.return_value = driver
+ zones_obj = MagicMock()
+ operation = MagicMock()
+ operation.execute.return_value = None
+ zones_obj.get.return_value = operation
+ self.cloud.compute_driver.zones.return_value = zones_obj
with pytest.raises(GCECloudException) as error:
- GCECloud(**self.kwargs)
+ self.cloud._validate_region()
assert str(error.value) == \
'Zone is required for GCE cloud framework: Example: us-west1-a'
- self.kwargs['region'] = 'fake'
+ self.cloud.region = 'fake'
with pytest.raises(GCECloudException) as error:
- GCECloud(**self.kwargs)
-
- driver.ex_get_zone.assert_called_once_with('fake')
+ self.cloud._validate_region()
assert str(error.value) == \
'fake is not a valid GCE zone. Example: us-west1-a'
@patch.object(GCECloud, '_get_instance')
- def test_gce_get_instance_state(self, mock_get_instance):
- """Test gce get instance method."""
- instance = MagicMock()
- instance.state = 'running'
- mock_get_instance.return_value = instance
-
- val = self.cloud._get_instance_state()
-
- assert val == 'running'
- assert mock_get_instance.call_count == 1
-
- @patch.object(GCECloud, '_get_instance_state')
- def test_gce_is_instance_running(self, mock_get_instance_state):
+ def test_gce_is_instance_running(self, mock_get_instance):
"""Test gce cloud is instance runnning method."""
- mock_get_instance_state.return_value = 'running'
-
+ mock_get_instance.return_value = {'status': 'RUNNING'}
assert self.cloud._is_instance_running()
- assert mock_get_instance_state.call_count == 1
-
- mock_get_instance_state.return_value = 'stopped'
- mock_get_instance_state.reset_mock()
+ assert mock_get_instance.call_count == 1
+ mock_get_instance.return_value = {'status': 'TERMINATED'}
assert not self.cloud._is_instance_running()
- assert mock_get_instance_state.call_count == 1
@patch.object(GCECloud, '_get_instance')
def test_gce_set_instance_ip(self, mock_get_instance):
"""Test gce cloud set instance ip method."""
- instance = MagicMock()
- instance.public_ips = []
- instance.private_ips = []
- mock_get_instance.return_value = instance
+ mock_get_instance.return_value = {
+ 'networkInterfaces': [{'some': 'data'}]
+ }
self.cloud.running_instance_id = 'test'
@@ -251,63 +434,79 @@
'IP address for instance: test cannot be found.'
assert mock_get_instance.call_count == 1
- mock_get_instance.reset_mock()
-
- instance.public_ips = ['127.0.0.1']
+ mock_get_instance.return_value = {
+ 'networkInterfaces': [{'networkIP': '10.0.0.0'}]
+ }
self.cloud._set_instance_ip()
- assert self.cloud.instance_ip == '127.0.0.1'
- assert mock_get_instance.call_count == 1
+ assert self.cloud.instance_ip == '10.0.0.0'
- @patch.object(GCECloud, '_get_instance')
- def test_gce_start_instance(self, mock_get_instance):
+ @patch.object(GCECloud, '_wait_on_instance')
+ def test_gce_start_instance(self, mock_wait_on_instance):
"""Test gce start instance method."""
- instance = MagicMock()
- mock_get_instance.return_value = instance
- self.cloud.compute_driver.ex_start_node.return_value = None
- self.cloud.compute_driver.wait_until_running.return_value = None
+ mock_wait_on_instance.return_value = None
+
+ instances_obj = MagicMock()
+ operation = MagicMock()
+ operation.execute.return_value = None
+ instances_obj.start.return_value = operation
+ self.cloud.compute_driver.instances.return_value = instances_obj
self.cloud._start_instance()
- assert mock_get_instance.call_count == 1
- assert self.cloud.compute_driver.ex_start_node.call_count == 1
- assert self.cloud.compute_driver.wait_until_running.call_count == 1
+ assert instances_obj.start.call_count == 1
@patch.object(GCECloud, '_wait_on_instance')
- @patch.object(GCECloud, '_get_instance')
- def test_gce_stop_instance(
- self,
- mock_get_instance,
- mock_wait_on_instance
- ):
+ def test_gce_stop_instance(self, mock_wait_on_instance):
"""Test gce stop instance method."""
- instance = MagicMock()
- mock_get_instance.return_value = instance
mock_wait_on_instance.return_value = None
- self.cloud.compute_driver.ex_stop_node.return_value = None
+
+ instances_obj = MagicMock()
+ operation = MagicMock()
+ operation.execute.return_value = None
+ instances_obj.stop.return_value = operation
+ self.cloud.compute_driver.instances.return_value = instances_obj
self.cloud._stop_instance()
- assert mock_get_instance.call_count == 1
- assert self.cloud.compute_driver.ex_stop_node.call_count == 1
+ assert instances_obj.stop.call_count == 1
- @patch.object(GCECloud, '_get_instance')
- def test_gce_terminate_instance(self, mock_get_instance):
+ def test_gce_terminate_instance(self):
"""Test gce terminate instance method."""
- instance = MagicMock()
- instance.destroy.return_value = None
- mock_get_instance.return_value = instance
+ instances_obj = MagicMock()
+ operation = MagicMock()
+ operation.execute.return_value = None
+ instances_obj.delete.return_value = operation
+ self.cloud.compute_driver.instances.return_value = instances_obj
self.cloud._terminate_instance()
- assert instance.destroy.call_count == 1
+ assert instances_obj.delete.call_count == 1
- @patch.object(GCECloud, '_get_instance')
- def test_gce_get_console_log(self, mock_get_instance):
+ def test_gce_get_console_log(self):
"""Test gce get console log method."""
- instance = MagicMock()
- mock_get_instance.return_value = instance
- self.cloud.compute_driver.ex_get_serial_output.return_value = 'output'
+ instances_obj = MagicMock()
+ operation = MagicMock()
+ operation.execute.return_value = {'content': 'some output'}
+ instances_obj.getSerialPortOutput.return_value = operation
+ self.cloud.compute_driver.instances.return_value = instances_obj
+
+ self.cloud.get_console_log()
+ assert instances_obj.getSerialPortOutput.call_count == 1
+
+ @patch('img_proof.ipa_gce.time')
+ def test_wait_on_operation(self, mock_time):
+ self.cloud.service_account_project = 'test_project'
+ self.cloud.region = 'us-west1-a'
+
+ mock_time.sleep.return_value = None
+ mock_time.time.return_value = 10
- output = self.cloud.get_console_log()
- assert output == 'output'
- assert self.cloud.compute_driver.ex_get_serial_output.call_count == 1
+ zone_ops_obj = MagicMock()
+ operation = MagicMock()
+ operation.execute.return_value = {'status': 'DONE'}
+ zone_ops_obj.get.return_value = operation
+ self.cloud.compute_driver.zoneOperations.return_value = zone_ops_obj
+
+ result = self.cloud._wait_on_operation('operation213')
+ assert result['status'] == 'DONE'
+ assert zone_ops_obj.get.call_count == 1