summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/cloud_tests/__init__.py3
-rw-r--r--tests/cloud_tests/config.py2
-rw-r--r--tests/cloud_tests/platforms.yaml7
-rw-r--r--tests/cloud_tests/platforms/__init__.py2
-rw-r--r--tests/cloud_tests/platforms/azurecloud/__init__.py0
-rw-r--r--tests/cloud_tests/platforms/azurecloud/image.py116
-rw-r--r--tests/cloud_tests/platforms/azurecloud/instance.py248
-rw-r--r--tests/cloud_tests/platforms/azurecloud/platform.py233
-rw-r--r--tests/cloud_tests/platforms/azurecloud/regions.json42
-rw-r--r--tests/cloud_tests/platforms/azurecloud/snapshot.py58
-rw-r--r--tests/cloud_tests/platforms/ec2/image.py1
-rw-r--r--tests/cloud_tests/platforms/ec2/platform.py3
-rw-r--r--tests/cloud_tests/platforms/lxd/instance.py14
-rw-r--r--tests/cloud_tests/platforms/nocloudkvm/instance.py13
-rw-r--r--tests/cloud_tests/platforms/nocloudkvm/platform.py10
-rw-r--r--tests/cloud_tests/platforms/platforms.py2
-rw-r--r--tests/cloud_tests/releases.yaml18
-rw-r--r--tests/cloud_tests/setup_image.py5
-rw-r--r--tests/cloud_tests/testcases/modules/TODO.md7
-rw-r--r--tests/cloud_tests/testcases/modules/apt_pipelining_disable.yaml3
-rw-r--r--tests/cloud_tests/testcases/modules/apt_pipelining_os.py6
-rw-r--r--tests/cloud_tests/testcases/modules/apt_pipelining_os.yaml9
-rw-r--r--tests/cloud_tests/testcases/modules/snappy.py17
-rw-r--r--tests/cloud_tests/testcases/modules/snappy.yaml18
-rw-r--r--tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.py2
-rw-r--r--tests/cloud_tests/verify.py9
-rw-r--r--tests/data/azure/non_unicode_random_string1
-rw-r--r--tests/data/azure/parse_certificates_fingerprints4
-rw-r--r--tests/data/azure/parse_certificates_pem152
-rw-r--r--tests/data/azure/pubkey_extract_cert13
-rw-r--r--tests/data/azure/pubkey_extract_ssh_key1
-rw-r--r--tests/data/netinfo/freebsd-ifconfig-output39
-rw-r--r--tests/data/netinfo/freebsd-netdev-formatted-output12
-rw-r--r--tests/unittests/test_cli.py16
-rw-r--r--tests/unittests/test_data.py52
-rw-r--r--tests/unittests/test_datasource/test_aliyun.py2
-rw-r--r--tests/unittests/test_datasource/test_azure.py362
-rw-r--r--tests/unittests/test_datasource/test_azure_helper.py117
-rw-r--r--tests/unittests/test_datasource/test_cloudsigma.py20
-rw-r--r--tests/unittests/test_datasource/test_cloudstack.py21
-rw-r--r--tests/unittests/test_datasource/test_common.py24
-rw-r--r--tests/unittests/test_datasource/test_configdrive.py98
-rw-r--r--tests/unittests/test_datasource/test_ec2.py93
-rw-r--r--tests/unittests/test_datasource/test_exoscale.py211
-rw-r--r--tests/unittests/test_datasource/test_gce.py22
-rw-r--r--tests/unittests/test_datasource/test_maas.py2
-rw-r--r--tests/unittests/test_datasource/test_nocloud.py60
-rw-r--r--tests/unittests/test_datasource/test_openstack.py8
-rw-r--r--tests/unittests/test_datasource/test_ovf.py55
-rw-r--r--tests/unittests/test_datasource/test_rbx.py208
-rw-r--r--tests/unittests/test_datasource/test_scaleway.py125
-rw-r--r--tests/unittests/test_datasource/test_smartos.py13
-rw-r--r--tests/unittests/test_distros/test_create_users.py30
-rw-r--r--tests/unittests/test_distros/test_freebsd.py45
-rw-r--r--tests/unittests/test_distros/test_generic.py24
-rw-r--r--tests/unittests/test_distros/test_netconfig.py403
-rw-r--r--tests/unittests/test_distros/test_user_data_normalize.py3
-rw-r--r--tests/unittests/test_ds_identify.py148
-rw-r--r--tests/unittests/test_filters/test_launch_index.py3
-rw-r--r--tests/unittests/test_handler/test_handler_apt_configure_sources_list_v1.py8
-rw-r--r--tests/unittests/test_handler/test_handler_apt_configure_sources_list_v3.py10
-rw-r--r--tests/unittests/test_handler/test_handler_apt_source_v1.py10
-rw-r--r--tests/unittests/test_handler/test_handler_apt_source_v3.py45
-rw-r--r--tests/unittests/test_handler/test_handler_ca_certs.py5
-rw-r--r--tests/unittests/test_handler/test_handler_chef.py6
-rw-r--r--tests/unittests/test_handler/test_handler_disk_setup.py18
-rw-r--r--tests/unittests/test_handler/test_handler_growpart.py34
-rw-r--r--tests/unittests/test_handler/test_handler_locale.py5
-rw-r--r--tests/unittests/test_handler/test_handler_lxd.py7
-rw-r--r--tests/unittests/test_handler/test_handler_mcollective.py2
-rw-r--r--tests/unittests/test_handler/test_handler_mounts.py48
-rw-r--r--tests/unittests/test_handler/test_handler_ntp.py25
-rw-r--r--tests/unittests/test_handler/test_handler_power_state.py2
-rw-r--r--tests/unittests/test_handler/test_handler_puppet.py34
-rw-r--r--tests/unittests/test_handler/test_handler_resizefs.py2
-rw-r--r--tests/unittests/test_handler/test_handler_seed_random.py3
-rw-r--r--tests/unittests/test_handler/test_handler_set_hostname.py2
-rw-r--r--tests/unittests/test_handler/test_handler_snappy.py601
-rw-r--r--tests/unittests/test_handler/test_handler_spacewalk.py6
-rw-r--r--tests/unittests/test_handler/test_handler_timezone.py2
-rw-r--r--tests/unittests/test_handler/test_handler_write_files.py15
-rw-r--r--tests/unittests/test_handler/test_handler_yum_add_repo.py2
-rw-r--r--tests/unittests/test_handler/test_handler_zypper_add_repo.py2
-rw-r--r--tests/unittests/test_handler/test_schema.py3
-rw-r--r--tests/unittests/test_log.py11
-rw-r--r--tests/unittests/test_merging.py6
-rw-r--r--tests/unittests/test_net.py2087
-rw-r--r--tests/unittests/test_net_freebsd.py19
-rw-r--r--tests/unittests/test_reporting.py4
-rw-r--r--tests/unittests/test_reporting_hyperv.py187
-rw-r--r--tests/unittests/test_runs/test_merge_run.py3
-rw-r--r--tests/unittests/test_runs/test_simple_run.py7
-rw-r--r--tests/unittests/test_sshutil.py85
-rw-r--r--tests/unittests/test_util.py29
-rw-r--r--tests/unittests/test_vmware/test_custom_script.py116
-rw-r--r--tests/unittests/test_vmware/test_guestcust_util.py72
-rw-r--r--tests/unittests/test_vmware_config_file.py6
97 files changed, 5325 insertions, 1439 deletions
diff --git a/tests/cloud_tests/__init__.py b/tests/cloud_tests/__init__.py
index dd436989..6c632f99 100644
--- a/tests/cloud_tests/__init__.py
+++ b/tests/cloud_tests/__init__.py
@@ -22,7 +22,8 @@ def _initialize_logging():
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
formatter = logging.Formatter(
- '%(asctime)s - %(name)s - %(levelname)s - %(message)s')
+ '%(asctime)s - %(pathname)s:%(funcName)s:%(lineno)s '
+ '[%(levelname)s]: %(message)s')
console = logging.StreamHandler()
console.setLevel(logging.DEBUG)
diff --git a/tests/cloud_tests/config.py b/tests/cloud_tests/config.py
index 8bd569fd..06536edc 100644
--- a/tests/cloud_tests/config.py
+++ b/tests/cloud_tests/config.py
@@ -114,7 +114,7 @@ def load_os_config(platform_name, os_name, require_enabled=False,
feature_conf = main_conf['features']
feature_groups = conf.get('feature_groups', [])
overrides = merge_config(get(conf, 'features'), feature_overrides)
- conf['arch'] = c_util.get_architecture()
+ conf['arch'] = c_util.get_dpkg_architecture()
conf['features'] = merge_feature_groups(
feature_conf, feature_groups, overrides)
diff --git a/tests/cloud_tests/platforms.yaml b/tests/cloud_tests/platforms.yaml
index 448aa98d..eaaa0a71 100644
--- a/tests/cloud_tests/platforms.yaml
+++ b/tests/cloud_tests/platforms.yaml
@@ -66,5 +66,12 @@ platforms:
{{ config_get("user.vendor-data", properties.default) }}
nocloud-kvm:
enabled: true
+ cache_mode: cache=none,aio=native
+ azurecloud:
+ enabled: true
+ region: West US 2
+ vm_size: Standard_DS1_v2
+ storage_sku: standard_lrs
+ tag: ci
# vi: ts=4 expandtab
diff --git a/tests/cloud_tests/platforms/__init__.py b/tests/cloud_tests/platforms/__init__.py
index a01e51ac..6a410b84 100644
--- a/tests/cloud_tests/platforms/__init__.py
+++ b/tests/cloud_tests/platforms/__init__.py
@@ -5,11 +5,13 @@
from .ec2 import platform as ec2
from .lxd import platform as lxd
from .nocloudkvm import platform as nocloudkvm
+from .azurecloud import platform as azurecloud
PLATFORMS = {
'ec2': ec2.EC2Platform,
'nocloud-kvm': nocloudkvm.NoCloudKVMPlatform,
'lxd': lxd.LXDPlatform,
+ 'azurecloud': azurecloud.AzureCloudPlatform,
}
diff --git a/tests/cloud_tests/platforms/azurecloud/__init__.py b/tests/cloud_tests/platforms/azurecloud/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/tests/cloud_tests/platforms/azurecloud/__init__.py
diff --git a/tests/cloud_tests/platforms/azurecloud/image.py b/tests/cloud_tests/platforms/azurecloud/image.py
new file mode 100644
index 00000000..aad2bca1
--- /dev/null
+++ b/tests/cloud_tests/platforms/azurecloud/image.py
@@ -0,0 +1,116 @@
+# This file is part of cloud-init. See LICENSE file for license information.
+
+"""Azure Cloud image Base class."""
+
+from tests.cloud_tests import LOG
+
+from ..images import Image
+from .snapshot import AzureCloudSnapshot
+
+
+class AzureCloudImage(Image):
+ """Azure Cloud backed image."""
+
+ platform_name = 'azurecloud'
+
+ def __init__(self, platform, config, image_id):
+ """Set up image.
+
+ @param platform: platform object
+ @param config: image configuration
+ @param image_id: image id used to boot instance
+ """
+ super(AzureCloudImage, self).__init__(platform, config)
+ self._img_instance = None
+ self.image_id = image_id
+
+ @property
+ def _instance(self):
+ """Internal use only, returns a running instance"""
+ if not self._img_instance:
+ self._img_instance = self.platform.create_instance(
+ self.properties, self.config, self.features,
+ self.image_id, user_data=None)
+ self._img_instance.start(wait=True, wait_for_cloud_init=True)
+ return self._img_instance
+
+ def destroy(self):
+ """Delete the instance used to create a custom image."""
+ if self._img_instance:
+ LOG.debug('Deleting backing instance %s',
+ self._img_instance.vm_name)
+ delete_vm = self.platform.compute_client.virtual_machines.delete(
+ self.platform.resource_group.name, self._img_instance.vm_name)
+ delete_vm.wait()
+
+ super(AzureCloudImage, self).destroy()
+
+ def _execute(self, *args, **kwargs):
+ """Execute command in image, modifying image."""
+ LOG.debug('executing commands on image')
+ self._instance.start(wait=True)
+ return self._instance._execute(*args, **kwargs)
+
+ def push_file(self, local_path, remote_path):
+ """Copy file at 'local_path' to instance at 'remote_path'."""
+ LOG.debug('pushing file to image')
+ return self._instance.push_file(local_path, remote_path)
+
+ def run_script(self, *args, **kwargs):
+ """Run script in image, modifying image.
+
+ @return_value: script output
+ """
+ LOG.debug('running script on image')
+ self._instance.start()
+ return self._instance.run_script(*args, **kwargs)
+
+ def snapshot(self):
+ """ Create snapshot (image) of instance, wait until done.
+
+ If no instance has been booted, base image is returned.
+ Otherwise runs the clean script, deallocates, generalizes
+ and creates custom image from instance.
+ """
+ LOG.debug('creating snapshot of image')
+ if not self._img_instance:
+ LOG.debug('No existing image, snapshotting base image')
+ return AzureCloudSnapshot(self.platform, self.properties,
+ self.config, self.features,
+ self._instance.vm_name,
+ delete_on_destroy=False)
+
+ LOG.debug('creating snapshot from instance: %s', self._img_instance)
+ if self.config.get('boot_clean_script'):
+ self._img_instance.run_script(self.config.get('boot_clean_script'))
+
+ LOG.debug('deallocating instance %s', self._instance.vm_name)
+ deallocate = self.platform.compute_client.virtual_machines.deallocate(
+ self.platform.resource_group.name, self._instance.vm_name)
+ deallocate.wait()
+
+ LOG.debug('generalizing instance %s', self._instance.vm_name)
+ self.platform.compute_client.virtual_machines.generalize(
+ self.platform.resource_group.name, self._instance.vm_name)
+
+ image_params = {
+ "location": self.platform.location,
+ "properties": {
+ "sourceVirtualMachine": {
+ "id": self._img_instance.instance.id
+ }
+ }
+ }
+ LOG.debug('updating resource group image %s', self._instance.vm_name)
+ self.platform.compute_client.images.create_or_update(
+ self.platform.resource_group.name, self._instance.vm_name,
+ image_params)
+
+ LOG.debug('destroying self')
+ self.destroy()
+
+ LOG.debug('snapshot complete')
+ return AzureCloudSnapshot(self.platform, self.properties, self.config,
+ self.features, self._instance.vm_name)
+
+# vi: ts=4 expandtab
diff --git a/tests/cloud_tests/platforms/azurecloud/instance.py b/tests/cloud_tests/platforms/azurecloud/instance.py
new file mode 100644
index 00000000..f1e28a96
--- /dev/null
+++ b/tests/cloud_tests/platforms/azurecloud/instance.py
@@ -0,0 +1,248 @@
+# This file is part of cloud-init. See LICENSE file for license information.
+
+"""Base Azure Cloud instance."""
+
+from datetime import datetime, timedelta
+from urllib.parse import urlparse
+from time import sleep
+import traceback
+import os
+
+
+# pylint: disable=no-name-in-module
+from azure.storage.blob import BlockBlobService, BlobPermissions
+from msrestazure.azure_exceptions import CloudError
+
+from tests.cloud_tests import LOG
+
+from ..instances import Instance
+
+
+class AzureCloudInstance(Instance):
+ """Azure Cloud backed instance."""
+
+ platform_name = 'azurecloud'
+
+ def __init__(self, platform, properties, config,
+ features, image_id, user_data=None):
+ """Set up instance.
+
+ @param platform: platform object
+ @param properties: dictionary of properties
+ @param config: dictionary of configuration values
+ @param features: dictionary of supported feature flags
+ @param image_id: image to find and/or use
+ @param user_data: test user-data to pass to instance
+ """
+ super(AzureCloudInstance, self).__init__(
+ platform, image_id, properties, config, features)
+
+ self.ssh_port = 22
+ self.ssh_ip = None
+ self.instance = None
+ self.image_id = image_id
+ self.vm_name = 'ci-azure-i-%s' % self.platform.tag
+ self.user_data = user_data
+ self.ssh_key_file = os.path.join(
+ platform.config['data_dir'], platform.config['private_key'])
+ self.ssh_pubkey_file = os.path.join(
+ platform.config['data_dir'], platform.config['public_key'])
+ self.blob_client, self.container, self.blob = None, None, None
+
+ def start(self, wait=True, wait_for_cloud_init=False):
+ """Start instance with the platforms NIC."""
+ if self.instance:
+ return
+ data = self.image_id.split('-')
+ release, support = data[2].replace('_', '.'), data[3]
+ sku = '%s-%s' % (release, support) if support == 'LTS' else release
+ image_resource_id = '/subscriptions/%s' \
+ '/resourceGroups/%s' \
+ '/providers/Microsoft.Compute/images/%s' % (
+ self.platform.subscription_id,
+ self.platform.resource_group.name,
+ self.image_id)
+ storage_uri = "http://%s.blob.core.windows.net" \
+ % self.platform.storage.name
+ with open(self.ssh_pubkey_file, 'r') as key:
+ ssh_pub_keydata = key.read()
+
+ image_exists = False
+ try:
+ LOG.debug('finding image in resource group using image_id')
+ self.platform.compute_client.images.get(
+ self.platform.resource_group.name,
+ self.image_id
+ )
+ image_exists = True
+ LOG.debug('image found, launching instance, image_id=%s',
+ self.image_id)
+ except CloudError:
+ LOG.debug(('image not found, launching instance with base image, '
+ 'image_id=%s'), self.image_id)
+ pass
+
+ vm_params = {
+ 'name': self.vm_name,
+ 'location': self.platform.location,
+ 'os_profile': {
+ 'computer_name': 'CI-%s' % self.platform.tag,
+ 'admin_username': self.ssh_username,
+ "customData": self.user_data,
+ "linuxConfiguration": {
+ "disable_password_authentication": True,
+ "ssh": {
+ "public_keys": [{
+ "path": "/home/%s/.ssh/authorized_keys" %
+ self.ssh_username,
+ "keyData": ssh_pub_keydata
+ }]
+ }
+ }
+ },
+ "diagnosticsProfile": {
+ "bootDiagnostics": {
+ "storageUri": storage_uri,
+ "enabled": True
+ }
+ },
+ 'hardware_profile': {
+ 'vm_size': self.platform.vm_size
+ },
+ 'storage_profile': {
+ 'image_reference': {
+ 'id': image_resource_id
+ } if image_exists else {
+ 'publisher': 'Canonical',
+ 'offer': 'UbuntuServer',
+ 'sku': sku,
+ 'version': 'latest'
+ }
+ },
+ 'network_profile': {
+ 'network_interfaces': [{
+ 'id': self.platform.nic.id
+ }]
+ },
+ 'tags': {
+ 'Name': self.platform.tag,
+ }
+ }
+
+ try:
+ self.instance = self.platform.compute_client.virtual_machines.\
+ create_or_update(self.platform.resource_group.name,
+ self.vm_name, vm_params)
+ LOG.debug('creating instance %s from image_id=%s', self.vm_name,
+ self.image_id)
+ except CloudError:
+ raise RuntimeError('failed creating instance:\n{}'.format(
+ traceback.format_exc()))
+
+ if wait:
+ self.instance.wait()
+ self.ssh_ip = self.platform.network_client.\
+ public_ip_addresses.get(
+ self.platform.resource_group.name,
+ self.platform.public_ip.name
+ ).ip_address
+ self._wait_for_system(wait_for_cloud_init)
+
+ self.instance = self.instance.result()
+ self.blob_client, self.container, self.blob =\
+ self._get_blob_client()
+
+ def shutdown(self, wait=True):
+ """Finds console log then stopping/deallocates VM"""
+ LOG.debug('waiting on console log before stopping')
+ attempts, exists = 5, False
+ while not exists and attempts:
+ try:
+ attempts -= 1
+ exists = self.blob_client.get_blob_to_bytes(
+ self.container, self.blob)
+ LOG.debug('found console log')
+ except Exception as e:
+ if attempts:
+ LOG.debug('Unable to find console log, '
+ '%s attempts remaining', attempts)
+ sleep(15)
+ else:
+ LOG.warning('Could not find console log: %s', e)
+ pass
+
+ LOG.debug('stopping instance %s', self.image_id)
+ vm_deallocate = \
+ self.platform.compute_client.virtual_machines.deallocate(
+ self.platform.resource_group.name, self.image_id)
+ if wait:
+ vm_deallocate.wait()
+
+ def destroy(self):
+ """Delete VM and close all connections"""
+ if self.instance:
+ LOG.debug('destroying instance: %s', self.image_id)
+ vm_delete = self.platform.compute_client.virtual_machines.delete(
+ self.platform.resource_group.name, self.image_id)
+ vm_delete.wait()
+
+ self._ssh_close()
+
+ super(AzureCloudInstance, self).destroy()
+
+ def _execute(self, command, stdin=None, env=None):
+ """Execute command on instance."""
+ env_args = []
+ if env:
+ env_args = ['env'] + ["%s=%s" for k, v in env.items()]
+
+ return self._ssh(['sudo'] + env_args + list(command), stdin=stdin)
+
+ def _get_blob_client(self):
+ """
+ Use VM details to retrieve container and blob name.
+ Then Create blob service client for sas token to
+ retrieve console log.
+
+ :return: blob service, container name, blob name
+ """
+ LOG.debug('creating blob service for console log')
+ storage = self.platform.storage_client.storage_accounts.get_properties(
+ self.platform.resource_group.name, self.platform.storage.name)
+
+ keys = self.platform.storage_client.storage_accounts.list_keys(
+ self.platform.resource_group.name, self.platform.storage.name
+ ).keys[0].value
+
+ virtual_machine = self.platform.compute_client.virtual_machines.get(
+ self.platform.resource_group.name, self.instance.name,
+ expand='instanceView')
+
+ blob_uri = virtual_machine.instance_view.boot_diagnostics.\
+ serial_console_log_blob_uri
+
+ container, blob = urlparse(blob_uri).path.split('/')[-2:]
+
+ blob_client = BlockBlobService(
+ account_name=storage.name,
+ account_key=keys)
+
+ sas = blob_client.generate_blob_shared_access_signature(
+ container_name=container, blob_name=blob, protocol='https',
+ expiry=datetime.utcnow() + timedelta(hours=1),
+ permission=BlobPermissions.READ)
+
+ blob_client = BlockBlobService(
+ account_name=storage.name,
+ sas_token=sas)
+
+ return blob_client, container, blob
+
+ def console_log(self):
+ """Instance console.
+
+ @return_value: bytes of this instance’s console
+ """
+ boot_diagnostics = self.blob_client.get_blob_to_bytes(
+ self.container, self.blob)
+ return boot_diagnostics.content
diff --git a/tests/cloud_tests/platforms/azurecloud/platform.py b/tests/cloud_tests/platforms/azurecloud/platform.py
new file mode 100644
index 00000000..cb62a74b
--- /dev/null
+++ b/tests/cloud_tests/platforms/azurecloud/platform.py
@@ -0,0 +1,233 @@
+# This file is part of cloud-init. See LICENSE file for license information.
+
+"""Base Azure Cloud class."""
+
+import os
+import base64
+import traceback
+from datetime import datetime
+from tests.cloud_tests import LOG
+
+# pylint: disable=no-name-in-module
+from azure.common.credentials import ServicePrincipalCredentials
+# pylint: disable=no-name-in-module
+from azure.mgmt.resource import ResourceManagementClient
+# pylint: disable=no-name-in-module
+from azure.mgmt.network import NetworkManagementClient
+# pylint: disable=no-name-in-module
+from azure.mgmt.compute import ComputeManagementClient
+# pylint: disable=no-name-in-module
+from azure.mgmt.storage import StorageManagementClient
+from msrestazure.azure_exceptions import CloudError
+
+from .image import AzureCloudImage
+from .instance import AzureCloudInstance
+from ..platforms import Platform
+
+from cloudinit import util as c_util
+
+
+class AzureCloudPlatform(Platform):
+ """Azure Cloud test platforms."""
+
+ platform_name = 'azurecloud'
+
+ def __init__(self, config):
+ """Set up platform."""
+ super(AzureCloudPlatform, self).__init__(config)
+ self.tag = '%s-%s' % (
+ config['tag'], datetime.now().strftime('%Y%m%d%H%M%S'))
+ self.storage_sku = config['storage_sku']
+ self.vm_size = config['vm_size']
+ self.location = config['region']
+
+ try:
+ self.credentials, self.subscription_id = self._get_credentials()
+
+ self.resource_client = ResourceManagementClient(
+ self.credentials, self.subscription_id)
+ self.compute_client = ComputeManagementClient(
+ self.credentials, self.subscription_id)
+ self.network_client = NetworkManagementClient(
+ self.credentials, self.subscription_id)
+ self.storage_client = StorageManagementClient(
+ self.credentials, self.subscription_id)
+
+ self.resource_group = self._create_resource_group()
+ self.public_ip = self._create_public_ip_address()
+ self.storage = self._create_storage_account(config)
+ self.vnet = self._create_vnet()
+ self.subnet = self._create_subnet()
+ self.nic = self._create_nic()
+ except CloudError:
+ raise RuntimeError('failed creating a resource:\n{}'.format(
+ traceback.format_exc()))
+
+ def create_instance(self, properties, config, features,
+ image_id, user_data=None):
+ """Create an instance
+
+ @param properties: image properties
+ @param config: image configuration
+ @param features: image features
+ @param image_id: string of image id
+ @param user_data: test user-data to pass to instance
+ @return_value: cloud_tests.instances instance
+ """
+ if user_data is not None:
+ user_data = str(base64.b64encode(
+ user_data.encode('utf-8')), 'utf-8')
+
+ return AzureCloudInstance(self, properties, config, features,
+ image_id, user_data)
+
+ def get_image(self, img_conf):
+ """Get image using specified image configuration.
+
+ @param img_conf: configuration for image
+ @return_value: cloud_tests.images instance
+ """
+ ss_region = self.azure_location_to_simplestreams_region()
+
+ filters = [
+ 'arch=%s' % 'amd64',
+ 'endpoint=https://management.core.windows.net/',
+ 'region=%s' % ss_region,
+ 'release=%s' % img_conf['release']
+ ]
+
+ LOG.debug('finding image using streams')
+ image = self._query_streams(img_conf, filters)
+
+ try:
+ image_id = image['id']
+ LOG.debug('found image: %s', image_id)
+ if image_id.find('__') > 0:
+ image_id = image_id.split('__')[1]
+ LOG.debug('image_id shortened to %s', image_id)
+ except KeyError:
+ raise RuntimeError('no images found for %s' % img_conf['release'])
+
+ return AzureCloudImage(self, img_conf, image_id)
+
+ def destroy(self):
+ """Delete all resources in resource group."""
+ LOG.debug("Deleting resource group: %s", self.resource_group.name)
+ delete = self.resource_client.resource_groups.delete(
+ self.resource_group.name)
+ delete.wait()
+
+ def azure_location_to_simplestreams_region(self):
+ """Convert location to simplestreams region"""
+ location = self.location.lower().replace(' ', '')
+ LOG.debug('finding location %s using simple streams', location)
+ regions_file = os.path.join(
+ os.path.dirname(os.path.abspath(__file__)), 'regions.json')
+ region_simplestreams_map = c_util.load_json(
+ c_util.load_file(regions_file))
+ return region_simplestreams_map.get(location, location)
+
+ def _get_credentials(self):
+ """Get credentials from environment"""
+ LOG.debug('getting credentials from environment')
+ cred_file = os.path.expanduser('~/.azure/credentials.json')
+ try:
+ azure_creds = c_util.load_json(
+ c_util.load_file(cred_file))
+ subscription_id = azure_creds['subscriptionId']
+ credentials = ServicePrincipalCredentials(
+ client_id=azure_creds['clientId'],
+ secret=azure_creds['clientSecret'],
+ tenant=azure_creds['tenantId'])
+ return credentials, subscription_id
+ except KeyError:
+ raise RuntimeError('Please configure Azure service principal'
+ ' credentials in %s' % cred_file)
+
+ def _create_resource_group(self):
+ """Create resource group"""
+ LOG.debug('creating resource group')
+ resource_group_name = self.tag
+ resource_group_params = {
+ 'location': self.location
+ }
+ resource_group = self.resource_client.resource_groups.create_or_update(
+ resource_group_name, resource_group_params)
+ return resource_group
+
+ def _create_storage_account(self, config):
+ LOG.debug('creating storage account')
+ storage_account_name = 'storage%s' % datetime.now().\
+ strftime('%Y%m%d%H%M%S')
+ storage_params = {
+ 'sku': {
+ 'name': config['storage_sku']
+ },
+ 'kind': "Storage",
+ 'location': self.location
+ }
+ storage_account = self.storage_client.storage_accounts.create(
+ self.resource_group.name, storage_account_name, storage_params)
+ return storage_account.result()
+
+ def _create_public_ip_address(self):
+ """Create public ip address"""
+ LOG.debug('creating public ip address')
+ public_ip_name = '%s-ip' % self.resource_group.name
+ public_ip_params = {
+ 'location': self.location,
+ 'public_ip_allocation_method': 'Dynamic'
+ }
+ ip = self.network_client.public_ip_addresses.create_or_update(
+ self.resource_group.name, public_ip_name, public_ip_params)
+ return ip.result()
+
+ def _create_vnet(self):
+ """create virtual network"""
+ LOG.debug('creating vnet')
+ vnet_name = '%s-vnet' % self.resource_group.name
+ vnet_params = {
+ 'location': self.location,
+ 'address_space': {
+ 'address_prefixes': ['10.0.0.0/16']
+ }
+ }
+ vnet = self.network_client.virtual_networks.create_or_update(
+ self.resource_group.name, vnet_name, vnet_params)
+ return vnet.result()
+
+ def _create_subnet(self):
+ """create sub-network"""
+ LOG.debug('creating subnet')
+ subnet_name = '%s-subnet' % self.resource_group.name
+ subnet_params = {
+ 'address_prefix': '10.0.0.0/24'
+ }
+ subnet = self.network_client.subnets.create_or_update(
+ self.resource_group.name, self.vnet.name,
+ subnet_name, subnet_params)
+ return subnet.result()
+
+ def _create_nic(self):
+ """Create network interface controller"""
+ LOG.debug('creating nic')
+ nic_name = '%s-nic' % self.resource_group.name
+ nic_params = {
+ 'location': self.location,
+ 'ip_configurations': [{
+ 'name': 'ipconfig',
+ 'subnet': {
+ 'id': self.subnet.id
+ },
+ 'publicIpAddress': {
+ 'id': "/subscriptions/%s"
+ "/resourceGroups/%s/providers/Microsoft.Network"
+ "/publicIPAddresses/%s" % (
+ self.subscription_id, self.resource_group.name,
+ self.public_ip.name),
+ }
+ }]
+ }
+ nic = self.network_client.network_interfaces.create_or_update(
+ self.resource_group.name, nic_name, nic_params)
+ return nic.result()
diff --git a/tests/cloud_tests/platforms/azurecloud/regions.json b/tests/cloud_tests/platforms/azurecloud/regions.json
new file mode 100644
index 00000000..c1b4da20
--- /dev/null
+++ b/tests/cloud_tests/platforms/azurecloud/regions.json
@@ -0,0 +1,42 @@
+{
+ "eastasia": "East Asia",
+ "southeastasia": "Southeast Asia",
+ "centralus": "Central US",
+ "eastus": "East US",
+ "eastus2": "East US 2",
+ "westus": "West US",
+ "northcentralus": "North Central US",
+ "southcentralus": "South Central US",
+ "northeurope": "North Europe",
+ "westeurope": "West Europe",
+ "japanwest": "Japan West",
+ "japaneast": "Japan East",
+ "brazilsouth": "Brazil South",
+ "australiaeast": "Australia East",
+ "australiasoutheast": "Australia Southeast",
+ "southindia": "South India",
+ "centralindia": "Central India",
+ "westindia": "West India",
+ "canadacentral": "Canada Central",
+ "canadaeast": "Canada East",
+ "uksouth": "UK South",
+ "ukwest": "UK West",
+ "westcentralus": "West Central US",
+ "westus2": "West US 2",
+ "koreacentral": "Korea Central",
+ "koreasouth": "Korea South",
+ "francecentral": "France Central",
+ "francesouth": "France South",
+ "australiacentral": "Australia Central",
+ "australiacentral2": "Australia Central 2",
+ "uaecentral": "UAE Central",
+ "uaenorth": "UAE North",
+ "southafricanorth": "South Africa North",
+ "southafricawest": "South Africa West",
+ "switzerlandnorth": "Switzerland North",
+ "switzerlandwest": "Switzerland West",
+ "germanynorth": "Germany North",
+ "germanywestcentral": "Germany West Central",
+ "norwaywest": "Norway West",
+ "norwayeast": "Norway East"
+}
diff --git a/tests/cloud_tests/platforms/azurecloud/snapshot.py b/tests/cloud_tests/platforms/azurecloud/snapshot.py
new file mode 100644
index 00000000..580cc596
--- /dev/null
+++ b/tests/cloud_tests/platforms/azurecloud/snapshot.py
@@ -0,0 +1,58 @@
+# This file is part of cloud-init. See LICENSE file for license information.
+
+"""Base Azure Cloud snapshot."""
+
+from ..snapshots import Snapshot
+
+from tests.cloud_tests import LOG
+
+
+class AzureCloudSnapshot(Snapshot):
+ """Azure Cloud image copy backed snapshot."""
+
+ platform_name = 'azurecloud'
+
+ def __init__(self, platform, properties, config, features, image_id,
+ delete_on_destroy=True):
+ """Set up snapshot.
+
+ @param platform: platform object
+ @param properties: image properties
+ @param config: image config
+ @param features: supported feature flags
+ """
+ super(AzureCloudSnapshot, self).__init__(
+ platform, properties, config, features)
+
+ self.image_id = image_id
+ self.delete_on_destroy = delete_on_destroy
+
+ def launch(self, user_data, meta_data=None, block=True, start=True,
+ use_desc=None):
+ """Launch instance.
+
+ @param user_data: user-data for the instance
+ @param meta_data: meta_data for the instance
+ @param block: wait until instance is created
+ @param start: start instance and wait until fully started
+ @param use_desc: description of snapshot instance use
+ @return_value: an Instance
+ """
+ if meta_data is not None:
+ raise ValueError("metadata not supported on Azure Cloud tests")
+
+ instance = self.platform.create_instance(
+ self.properties, self.config, self.features,
+ self.image_id, user_data)
+
+ return instance
+
+ def destroy(self):
+ """Clean up snapshot data."""
+ LOG.debug('destroying image %s', self.image_id)
+ if self.delete_on_destroy:
+ self.platform.compute_client.images.delete(
+ self.platform.resource_group.name,
+ self.image_id)
+
+# vi: ts=4 expandtab
diff --git a/tests/cloud_tests/platforms/ec2/image.py b/tests/cloud_tests/platforms/ec2/image.py
index 7bedf59d..d7b2c908 100644
--- a/tests/cloud_tests/platforms/ec2/image.py
+++ b/tests/cloud_tests/platforms/ec2/image.py
@@ -4,6 +4,7 @@
from ..images import Image
from .snapshot import EC2Snapshot
+
from tests.cloud_tests import LOG
diff --git a/tests/cloud_tests/platforms/ec2/platform.py b/tests/cloud_tests/platforms/ec2/platform.py
index f188c27b..7a3d0fe0 100644
--- a/tests/cloud_tests/platforms/ec2/platform.py
+++ b/tests/cloud_tests/platforms/ec2/platform.py
@@ -135,6 +135,7 @@ class EC2Platform(Platform):
def _create_internet_gateway(self):
"""Create Internet Gateway and assign to VPC."""
LOG.debug('creating internet gateway')
+ # pylint: disable=no-member
internet_gateway = self.ec2_resource.create_internet_gateway()
internet_gateway.attach_to_vpc(VpcId=self.vpc.id)
self._tag_resource(internet_gateway)
@@ -190,7 +191,7 @@ class EC2Platform(Platform):
"""Setup AWS EC2 VPC or return existing VPC."""
LOG.debug('creating new vpc')
try:
- vpc = self.ec2_resource.create_vpc(
+ vpc = self.ec2_resource.create_vpc( # pylint: disable=no-member
CidrBlock=self.ipv4_cidr,
AmazonProvidedIpv6CidrBlock=True)
except botocore.exceptions.ClientError as e:
diff --git a/tests/cloud_tests/platforms/lxd/instance.py b/tests/cloud_tests/platforms/lxd/instance.py
index 83c97ab4..2b804a62 100644
--- a/tests/cloud_tests/platforms/lxd/instance.py
+++ b/tests/cloud_tests/platforms/lxd/instance.py
@@ -4,6 +4,7 @@
import os
import shutil
+import time
from tempfile import mkdtemp
from cloudinit.util import load_yaml, subp, ProcessExecutionError, which
@@ -224,7 +225,18 @@ class LXDInstance(Instance):
LOG.debug("%s: deleting container.", self)
self.unfreeze()
self.shutdown()
- self.pylxd_container.delete(wait=True)
+ retries = [1] * 5
+ for attempt, wait in enumerate(retries):
+ try:
+ self.pylxd_container.delete(wait=True)
+ break
+ except Exception:
+ if attempt + 1 >= len(retries):
+ raise
+ LOG.debug('Failed to delete container %s (%s/%s) retrying...',
+ self, attempt + 1, len(retries))
+ time.sleep(wait)
+
self._pylxd_container = None
if self.platform.container_exists(self.name):
diff --git a/tests/cloud_tests/platforms/nocloudkvm/instance.py b/tests/cloud_tests/platforms/nocloudkvm/instance.py
index 33ff3f24..96185b75 100644
--- a/tests/cloud_tests/platforms/nocloudkvm/instance.py
+++ b/tests/cloud_tests/platforms/nocloudkvm/instance.py
@@ -74,6 +74,8 @@ class NoCloudKVMInstance(Instance):
self.pid_file = None
self.console_file = None
self.disk = image_path
+ self.cache_mode = platform.config.get('cache_mode',
+ 'cache=none,aio=native')
self.meta_data = meta_data
def shutdown(self, wait=True):
@@ -113,7 +115,10 @@ class NoCloudKVMInstance(Instance):
pass
if self.pid_file:
- os.remove(self.pid_file)
+ try:
+ os.remove(self.pid_file)
+ except Exception:
+ pass
self.pid = None
self._ssh_close()
@@ -160,13 +165,13 @@ class NoCloudKVMInstance(Instance):
self.ssh_port = self.get_free_port()
cmd = ['./tools/xkvm',
- '--disk', '%s,cache=unsafe' % self.disk,
- '--disk', '%s,cache=unsafe' % seed,
+ '--disk', '%s,%s' % (self.disk, self.cache_mode),
+ '--disk', '%s' % seed,
'--netdev', ','.join(['user',
'hostfwd=tcp::%s-:22' % self.ssh_port,
'dnssearch=%s' % CI_DOMAIN]),
'--', '-pidfile', self.pid_file, '-vnc', 'none',
- '-m', '2G', '-smp', '2', '-nographic',
+ '-m', '2G', '-smp', '2', '-nographic', '-name', self.name,
'-serial', 'file:' + self.console_file]
subprocess.Popen(cmd,
close_fds=True,
diff --git a/tests/cloud_tests/platforms/nocloudkvm/platform.py b/tests/cloud_tests/platforms/nocloudkvm/platform.py
index 85933463..2d1480f5 100644
--- a/tests/cloud_tests/platforms/nocloudkvm/platform.py
+++ b/tests/cloud_tests/platforms/nocloudkvm/platform.py
@@ -29,9 +29,13 @@ class NoCloudKVMPlatform(Platform):
"""
(url, path) = s_util.path_from_mirror_url(img_conf['mirror_url'], None)
- filter = filters.get_filters(['arch=%s' % c_util.get_architecture(),
- 'release=%s' % img_conf['release'],
- 'ftype=disk1.img'])
+ filter = filters.get_filters(
+ [
+ 'arch=%s' % c_util.get_dpkg_architecture(),
+ 'release=%s' % img_conf['release'],
+ 'ftype=disk1.img',
+ ]
+ )
mirror_config = {'filters': filter,
'keep_items': False,
'max_items': 1,
diff --git a/tests/cloud_tests/platforms/platforms.py b/tests/cloud_tests/platforms/platforms.py
index abbfebba..bebdf1c6 100644
--- a/tests/cloud_tests/platforms/platforms.py
+++ b/tests/cloud_tests/platforms/platforms.py
@@ -48,7 +48,7 @@ class Platform(object):
if os.path.exists(filename):
c_util.del_file(filename)
- c_util.subp(['ssh-keygen', '-t', 'rsa', '-b', '4096',
+ c_util.subp(['ssh-keygen', '-m', 'PEM', '-t', 'rsa', '-b', '4096',
'-f', filename, '-P', '',
'-C', 'ubuntu@cloud_test'],
capture=True)
diff --git a/tests/cloud_tests/releases.yaml b/tests/cloud_tests/releases.yaml
index ec5da724..7ddc5b85 100644
--- a/tests/cloud_tests/releases.yaml
+++ b/tests/cloud_tests/releases.yaml
@@ -55,6 +55,8 @@ default_release_config:
# cloud-init, so must pull cloud-init in from repo using
# setup_image.upgrade
upgrade: true
+ azurecloud:
+ boot_timeout: 300
features:
# all currently supported feature flags
@@ -129,6 +131,22 @@ features:
releases:
# UBUNTU =================================================================
+ eoan:
+ # EOL: Jul 2020
+ default:
+ enabled: true
+ release: eoan
+ version: 19.10
+ os: ubuntu
+ feature_groups:
+ - base
+ - debian_base
+ - ubuntu_specific
+ lxd:
+ sstreams_server: https://cloud-images.ubuntu.com/daily
+ alias: eoan
+ setup_overrides: null
+ override_templates: false
disco:
# EOL: Jan 2020
default:
diff --git a/tests/cloud_tests/setup_image.py b/tests/cloud_tests/setup_image.py
index 39f4517f..69e66e3f 100644
--- a/tests/cloud_tests/setup_image.py
+++ b/tests/cloud_tests/setup_image.py
@@ -222,13 +222,14 @@ def setup_image(args, image):
for name, func, desc in handlers if getattr(args, name, None)]
try:
- data = yaml.load(image.read_data("/etc/cloud/build.info", decode=True))
+ data = yaml.safe_load(
+ image.read_data("/etc/cloud/build.info", decode=True))
info = ' '.join(["%s=%s" % (k, data.get(k))
for k in ("build_name", "serial") if k in data])
except Exception as e:
info = "N/A (%s)" % e
- LOG.info('setting up %s (%s)', image, info)
+ LOG.info('setting up image %s (info %s)', image, info)
res = stage.run_stage(
'set up for {}'.format(image), calls, continue_after_error=False)
return res
diff --git a/tests/cloud_tests/testcases/modules/TODO.md b/tests/cloud_tests/testcases/modules/TODO.md
index 0b933b3b..9513cb2d 100644
--- a/tests/cloud_tests/testcases/modules/TODO.md
+++ b/tests/cloud_tests/testcases/modules/TODO.md
@@ -78,11 +78,8 @@ Not applicable to write a test for this as it specifies when something should be
## scripts vendor
Not applicable to write a test for this as it specifies when something should be run.
-## snappy
-2016-11-17: Need test to install snaps from store
-
-## snap-config
-2016-11-17: Need to investigate
+## snap
+2019-12-19: Need to investigate
## spacewalk
diff --git a/tests/cloud_tests/testcases/modules/apt_pipelining_disable.yaml b/tests/cloud_tests/testcases/modules/apt_pipelining_disable.yaml
index bd9b5d08..22a31dc4 100644
--- a/tests/cloud_tests/testcases/modules/apt_pipelining_disable.yaml
+++ b/tests/cloud_tests/testcases/modules/apt_pipelining_disable.yaml
@@ -5,8 +5,7 @@ required_features:
- apt
cloud_config: |
#cloud-config
- apt:
- apt_pipelining: false
+ apt_pipelining: false
collect_scripts:
90cloud-init-pipelining: |
#!/bin/bash
diff --git a/tests/cloud_tests/testcases/modules/apt_pipelining_os.py b/tests/cloud_tests/testcases/modules/apt_pipelining_os.py
index 740dc7c0..2b940a66 100644
--- a/tests/cloud_tests/testcases/modules/apt_pipelining_os.py
+++ b/tests/cloud_tests/testcases/modules/apt_pipelining_os.py
@@ -8,8 +8,8 @@ class TestAptPipeliningOS(base.CloudTestCase):
"""Test apt-pipelining module."""
def test_os_pipelining(self):
- """Test pipelining set to os."""
- out = self.get_data_file('90cloud-init-pipelining')
- self.assertIn('Acquire::http::Pipeline-Depth "0";', out)
+ """test 'os' settings does not write apt config file."""
+ out = self.get_data_file('90cloud-init-pipelining_not_written')
+ self.assertEqual(0, int(out))
# vi: ts=4 expandtab
diff --git a/tests/cloud_tests/testcases/modules/apt_pipelining_os.yaml b/tests/cloud_tests/testcases/modules/apt_pipelining_os.yaml
index cbed3ba3..86d5220b 100644
--- a/tests/cloud_tests/testcases/modules/apt_pipelining_os.yaml
+++ b/tests/cloud_tests/testcases/modules/apt_pipelining_os.yaml
@@ -1,15 +1,14 @@
#
-# Set apt pipelining value to OS
+# Set apt pipelining value to OS, no conf written
#
required_features:
- apt
cloud_config: |
#cloud-config
- apt:
- apt_pipelining: os
+ apt_pipelining: os
collect_scripts:
- 90cloud-init-pipelining: |
+ 90cloud-init-pipelining_not_written: |
#!/bin/bash
- cat /etc/apt/apt.conf.d/90cloud-init-pipelining
+ ls /etc/apt/apt.conf.d/90cloud-init-pipelining | wc -l
# vi: ts=4 expandtab
diff --git a/tests/cloud_tests/testcases/modules/snappy.py b/tests/cloud_tests/testcases/modules/snappy.py
deleted file mode 100644
index 7d17fc5b..00000000
--- a/tests/cloud_tests/testcases/modules/snappy.py
+++ /dev/null
@@ -1,17 +0,0 @@
-# This file is part of cloud-init. See LICENSE file for license information.
-
-"""cloud-init Integration Test Verify Script"""
-from tests.cloud_tests.testcases import base
-
-
-class TestSnappy(base.CloudTestCase):
- """Test snappy module"""
-
- expected_warnings = ('DEPRECATION',)
-
- def test_snappy_version(self):
- """Test snappy version output"""
- out = self.get_data_file('snapd')
- self.assertIn('Status: install ok installed', out)
-
-# vi: ts=4 expandtab
diff --git a/tests/cloud_tests/testcases/modules/snappy.yaml b/tests/cloud_tests/testcases/modules/snappy.yaml
deleted file mode 100644
index 8ac322ae..00000000
--- a/tests/cloud_tests/testcases/modules/snappy.yaml
+++ /dev/null
@@ -1,18 +0,0 @@
-#
-# Install snappy
-#
-# Aug 17, 2018: Disabled due to requiring a proxy for testing
-# tests do not handle the proxy well at this time.
-enabled: False
-required_features:
- - snap
-cloud_config: |
- #cloud-config
- snappy:
- system_snappy: auto
-collect_scripts:
- snapd: |
- #!/bin/bash
- dpkg -s snapd
-
-# vi: ts=4 expandtab
diff --git a/tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.py b/tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.py
index e7329d48..02935447 100644
--- a/tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.py
+++ b/tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.py
@@ -11,6 +11,6 @@ class TestSshKeyFingerprintsDisable(base.CloudTestCase):
"""Verify disabled."""
out = self.get_data_file('cloud-init.log')
self.assertIn('Skipping module named ssh-authkey-fingerprints, '
- 'logging of ssh fingerprints disabled', out)
+ 'logging of SSH fingerprints disabled', out)
# vi: ts=4 expandtab
diff --git a/tests/cloud_tests/verify.py b/tests/cloud_tests/verify.py
index 9911ecf2..7018f4d5 100644
--- a/tests/cloud_tests/verify.py
+++ b/tests/cloud_tests/verify.py
@@ -61,12 +61,17 @@ def format_test_failures(test_result):
if not test_result['failures']:
return ''
failure_hdr = ' test failures:'
- failure_fmt = ' * {module}.{class}.{function}\n {error}'
+ failure_fmt = ' * {module}.{class}.{function}\n '
output = []
for failure in test_result['failures']:
if not output:
output = [failure_hdr]
- output.append(failure_fmt.format(**failure))
+ msg = failure_fmt.format(**failure)
+ if failure.get('error'):
+ msg += failure['error']
+ else:
+ msg += failure.get('traceback', '')
+ output.append(msg)
return '\n'.join(output)
diff --git a/tests/data/azure/non_unicode_random_string b/tests/data/azure/non_unicode_random_string
new file mode 100644
index 00000000..b9ecefb9
--- /dev/null
+++ b/tests/data/azure/non_unicode_random_string
@@ -0,0 +1 @@
+OEM0d\x00\x00\x00\x01\x80VRTUALMICROSFT\x02\x17\x00\x06MSFT\x97\x00\x00\x00C\xb4{V\xf4X%\x061x\x90\x1c\xfen\x86\xbf~\xf5\x8c\x94&\x88\xed\x84\xf9B\xbd\xd3\xf1\xdb\xee:\xd9\x0fc\x0e\x83(\xbd\xe3'\xfc\x85,\xdf\xf4\x13\x99N\xc5\xf3Y\x1e\xe3\x0b\xa4H\x08J\xb9\xdcdb$ \ No newline at end of file
diff --git a/tests/data/azure/parse_certificates_fingerprints b/tests/data/azure/parse_certificates_fingerprints
new file mode 100644
index 00000000..f7293c56
--- /dev/null
+++ b/tests/data/azure/parse_certificates_fingerprints
@@ -0,0 +1,4 @@
+ECEDEB3B8488D31AF3BC4CCED493F64B7D27D7B1
+073E19D14D1C799224C6A0FD8DDAB6A8BF27D473
+4C16E7FAD6297D74A9B25EB8F0A12808CEBE293E
+929130695289B450FE45DCD5F6EF0CDE69865867
diff --git a/tests/data/azure/parse_certificates_pem b/tests/data/azure/parse_certificates_pem
new file mode 100644
index 00000000..3521ea3a
--- /dev/null
+++ b/tests/data/azure/parse_certificates_pem
@@ -0,0 +1,152 @@
+Bag Attributes
+ localKeyID: 01 00 00 00
+ Microsoft CSP Name: Microsoft Enhanced Cryptographic Provider v1.0
+Key Attributes
+ X509v3 Key Usage: 10
+-----BEGIN PRIVATE KEY-----
+MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDlEe5fUqwdrQTP
+W2oVlGK2f31q/8ULT8KmOTyUvL0RPdJQ69vvHOc5Q2CKg2eviHC2LWhF8WmpnZj6
+61RL0GeFGizwvU8Moebw5p3oqdcgoGpHVtxf+mr4QcWF58/Fwez0dA4hcsimVNBz
+eNpBBUIKNBMTBG+4d6hcQBUAGKUdGRcCGEyTqXLU0MgHjxC9JgVqWJl+X2LcAGj5
+7J+tGYGTLzKJmeCeGVNN5ZtJ0T85MYHCKQk1/FElK+Kq5akovXffQHjlnCPcx0NJ
+47NBjlPaFp2gjnAChn79bT4iCjOFZ9avWpqRpeU517UCnY7djOr3fuod/MSQyh3L
+Wuem1tWBAgMBAAECggEBAM4ZXQRs6Kjmo95BHGiAEnSqrlgX+dycjcBq3QPh8KZT
+nifqnf48XhnackENy7tWIjr3DctoUq4mOp8AHt77ijhqfaa4XSg7fwKeK9NLBGC5
+lAXNtAey0o2894/sKrd+LMkgphoYIUnuI4LRaGV56potkj/ZDP/GwTcG/R4SDnTn
+C1Nb05PNTAPQtPZrgPo7TdM6gGsTnFbVrYHQLyg2Sq/osHfF15YohB01esRLCAwb
+EF8JkRC4hWIZoV7BsyQ39232zAJQGGla7+wKFs3kObwh3VnFkQpT94KZnNiZuEfG
+x5pW4Pn3gXgNsftscXsaNe/M9mYZqo//Qw7NvUIvAvECgYEA9AVveyK0HOA06fhh
++3hUWdvw7Pbrl+e06jO9+bT1RjQMbHKyI60DZyVGuAySN86iChJRoJr5c6xj+iXU
+cR6BVJDjGH5t1tyiK2aYf6hEpK9/j8Z54UiVQ486zPP0PGfT2TO4lBLK+8AUmoaH
+gk21ul8QeVCeCJa/o+xEoRFvzcUCgYEA8FCbbvInrUtNY+9eKaUYoNodsgBVjm5X
+I0YPUL9D4d+1nvupHSV2NVmQl0w1RaJwrNTafrl5LkqjhQbmuWNta6QgfZzSA3LB
+lWXo1Mm0azKdcD3qMGbvn0Q3zU+yGNEgmB/Yju3/NtgYRG6tc+FCWRbPbiCnZWT8
+v3C2Y0XggI0CgYEA2/jCZBgGkTkzue5kNVJlh5OS/aog+pCvL6hxCtarfBuTT3ed
+Sje+p46cz3DVpmUpATc+Si8py7KNdYQAm/BJ2be6X+woi9Xcgo87zWgcaPCjZzId
+0I2jsIE/Gl6XvpRCDrxnGWRPgt3GNP4szbPLrDPiH9oie8+Y9eYYf7G+PZkCgYEA
+nRSzZOPYV4f/QDF4pVQLMykfe/iH9B/fyWjEHg3He19VQmRReIHCMMEoqBziPXAe
+onpHj8oAkeer1wpZyhhZr6CKtFDLXgGm09bXSC/IRMHC81klORovyzU2HHfZfCtG
+WOmIDnU2+0xpIGIP8sztJ3qnf97MTJSkOSadsWo9gwkCgYEAh5AQmJQmck88Dff2
+qIfJIX8d+BDw47BFJ89OmMFjGV8TNB+JO+AV4Vkodg4hxKpLqTFZTTUFgoYfy5u1
+1/BhAjpmCDCrzubCFhx+8VEoM2+2+MmnuQoMAm9+/mD/IidwRaARgXgvEmp7sfdt
+RyWd+p2lYvFkC/jORQtDMY4uW1o=
+-----END PRIVATE KEY-----
+Bag Attributes
+ localKeyID: 02 00 00 00
+ Microsoft CSP Name: Microsoft Strong Cryptographic Provider
+Key Attributes
+ X509v3 Key Usage: 10
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDlQhPrZwVQYFV4
+FBc0H1iTXYaznMpwZvEITKtXWACzTdguUderEVOkXW3HTi5HvC2rMayt0nqo3zcd
+x1eGiqdjpZQ/wMrkz9wNEM/nNMsXntEwxk0jCVNKB/jz6vf+BOtrSI01SritAGZW
+dpKoTUyztT8C2mA3X6D8g3m4Dd07ltnzxaDqAQIU5jBHh3f/Q14tlPNZWUIiqVTC
+gDxgAe7MDmfs9h3CInTBX1XM5J4UsLTL23/padgeSvP5YF5qr1+0c7Tdftxr2lwA
+N3rLkisf5EiLAToVyJJlgP/exo2I8DaIKe7DZzD3Y1CrurOpkcMKYu5kM1Htlbua
+tDkAa2oDAgMBAAECggEAOvdueS9DyiMlCKAeQb1IQosdQOh0l0ma+FgEABC2CWhd
+0LgjQTBRM6cGO+urcq7/jhdWQ1UuUG4tVn71z7itCi/F/Enhxc2C22d2GhFVpWsn
+giSXJYpZ/mIjkdVfWNo6FRuRmmHwMys1p0qTOS+8qUJWhSzW75csqJZGgeUrAI61
+LBV5F0SGR7dR2xZfy7PeDs9xpD0QivDt5DpsZWPaPvw4QlhdLgw6/YU1h9vtm6ci
+xLjnPRLZ7JMpcQHO8dUDl6FiEI7yQ11BDm253VQAVMddYRPQABn7SpEF8kD/aZVh
+2Clvz61Rz80SKjPUthMPLWMCRp7zB0xDMzt3/1i+tQKBgQD6Ar1/oD3eFnRnpi4u
+n/hdHJtMuXWNfUA4dspNjP6WGOid9sgIeUUdif1XyVJ+afITzvgpWc7nUWIqG2bQ
+WxJ/4q2rjUdvjNXTy1voVungR2jD5WLQ9DKeaTR0yCliWlx4JgdPG7qGI5MMwsr+
+R/PUoUUhGeEX+o/sCSieO3iUrQKBgQDqwBEMvIdhAv/CK2sG3fsKYX8rFT55ZNX3
+Tix9DbUGY3wQColNuI8U1nDlxE9U6VOfT9RPqKelBLCgbzB23kdEJnjSlnqlTxrx
+E+Hkndyf2ckdJAR3XNxoQ6SRLJNBsgoBj/z5tlfZE9/Jc+uh0mYy3e6g6XCVPBcz
+MgoIc+ofbwKBgQCGQhZ1hR30N+bHCozeaPW9OvGDIE0qcEqeh9xYDRFilXnF6pK9
+SjJ9jG7KR8jPLiHb1VebDSl5O1EV/6UU2vNyTc6pw7LLCryBgkGW4aWy1WZDXNnW
+EG1meGS9GghvUss5kmJ2bxOZmV0Mi0brisQ8OWagQf+JGvtS7BAt+Q3l+QKBgAb9
+8YQPmXiqPjPqVyW9Ntz4SnFeEJ5NApJ7IZgX8GxgSjGwHqbR+HEGchZl4ncE/Bii
+qBA3Vcb0fM5KgYcI19aPzsl28fA6ivLjRLcqfIfGVNcpW3iyq13vpdctHLW4N9QU
+FdTaOYOds+ysJziKq8CYG6NvUIshXw+HTgUybqbBAoGBAIIOqcmmtgOClAwipA17
+dAHsI9Sjk+J0+d4JU6o+5TsmhUfUKIjXf5+xqJkJcQZMEe5GhxcCuYkgFicvh4Hz
+kv2H/EU35LcJTqC6KTKZOWIbGcn1cqsvwm3GQJffYDiO8fRZSwCaif2J3F2lfH4Y
+R/fA67HXFSTT+OncdRpY1NOn
+-----END PRIVATE KEY-----
+Bag Attributes: <Empty Attributes>
+subject=/CN=CRP/OU=AzureRT/O=Microsoft Corporation/L=Redmond/ST=WA/C=US
+issuer=/CN=Root Agency
+-----BEGIN CERTIFICATE-----
+MIIB+TCCAeOgAwIBAgIBATANBgkqhkiG9w0BAQUFADAWMRQwEgYDVQQDDAtSb290
+IEFnZW5jeTAeFw0xOTAyMTUxOTA0MDRaFw0yOTAyMTUxOTE0MDRaMGwxDDAKBgNV
+BAMMA0NSUDEQMA4GA1UECwwHQXp1cmVSVDEeMBwGA1UECgwVTWljcm9zb2Z0IENv
+cnBvcmF0aW9uMRAwDgYDVQQHDAdSZWRtb25kMQswCQYDVQQIDAJXQTELMAkGA1UE
+BhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDIlPjJXzrRih4C
+k/XsoI01oqo7IUxH3dA2F7vHGXQoIpKCp8Qe6Z6cFfdD8Uj+s+B1BX6hngwzIwjN
+jE/23X3SALVzJVWzX4Y/IEjbgsuao6sOyNyB18wIU9YzZkVGj68fmMlUw3LnhPbe
+eWkufZaJCaLyhQOwlRMbOcn48D6Ys8fccOyXNzpq3rH1OzeQpxS2M8zaJYP4/VZ/
+sf6KRpI7bP+QwyFvNKfhcaO9/gj4kMo9lVGjvDU20FW6g8UVNJCV9N4GO6mOcyqo
+OhuhVfjCNGgW7N1qi0TIVn0/MQM4l4dcT2R7Z/bV9fhMJLjGsy5A4TLAdRrhKUHT
+bzi9HyDvAgMBAAEwDQYJKoZIhvcNAQEFBQADAQA=
+-----END CERTIFICATE-----
+Bag Attributes
+ localKeyID: 01 00 00 00
+subject=/C=US/ST=WASHINGTON/L=Seattle/O=Microsoft/OU=Azure/CN=AnhVo/emailAddress=redacted@microsoft.com
+issuer=/C=US/ST=WASHINGTON/L=Seattle/O=Microsoft/OU=Azure/CN=AnhVo/emailAddress=redacted@microsoft.com
+-----BEGIN CERTIFICATE-----
+MIID7TCCAtWgAwIBAgIJALQS3yMg3R41MA0GCSqGSIb3DQEBCwUAMIGMMQswCQYD
+VQQGEwJVUzETMBEGA1UECAwKV0FTSElOR1RPTjEQMA4GA1UEBwwHU2VhdHRsZTES
+MBAGA1UECgwJTWljcm9zb2Z0MQ4wDAYDVQQLDAVBenVyZTEOMAwGA1UEAwwFQW5o
+Vm8xIjAgBgkqhkiG9w0BCQEWE2FuaHZvQG1pY3Jvc29mdC5jb20wHhcNMTkwMjE0
+MjMxMjQwWhcNMjExMTEwMjMxMjQwWjCBjDELMAkGA1UEBhMCVVMxEzARBgNVBAgM
+CldBU0hJTkdUT04xEDAOBgNVBAcMB1NlYXR0bGUxEjAQBgNVBAoMCU1pY3Jvc29m
+dDEOMAwGA1UECwwFQXp1cmUxDjAMBgNVBAMMBUFuaFZvMSIwIAYJKoZIhvcNAQkB
+FhNhbmh2b0BtaWNyb3NvZnQuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+CgKCAQEA5RHuX1KsHa0Ez1tqFZRitn99av/FC0/Cpjk8lLy9ET3SUOvb7xznOUNg
+ioNnr4hwti1oRfFpqZ2Y+utUS9BnhRos8L1PDKHm8Oad6KnXIKBqR1bcX/pq+EHF
+hefPxcHs9HQOIXLIplTQc3jaQQVCCjQTEwRvuHeoXEAVABilHRkXAhhMk6ly1NDI
+B48QvSYFaliZfl9i3ABo+eyfrRmBky8yiZngnhlTTeWbSdE/OTGBwikJNfxRJSvi
+quWpKL1330B45Zwj3MdDSeOzQY5T2hadoI5wAoZ+/W0+IgozhWfWr1qakaXlOde1
+Ap2O3Yzq937qHfzEkMody1rnptbVgQIDAQABo1AwTjAdBgNVHQ4EFgQUPvdgLiv3
+pAk4r0QTPZU3PFOZJvgwHwYDVR0jBBgwFoAUPvdgLiv3pAk4r0QTPZU3PFOZJvgw
+DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAVUHZT+h9+uCPLTEl5IDg
+kqd9WpzXA7PJd/V+7DeDDTkEd06FIKTWZLfxLVVDjQJnQqubQb//e0zGu1qKbXnX
+R7xqWabGU4eyPeUFWddmt1OHhxKLU3HbJNJJdL6XKiQtpGGUQt/mqNQ/DEr6hhNF
+im5I79iA8H/dXA2gyZrj5Rxea4mtsaYO0mfp1NrFtJpAh2Djy4B1lBXBIv4DWG9e
+mMEwzcLCOZj2cOMA6+mdLMUjYCvIRtnn5MKUHyZX5EmX79wsqMTvVpddlVLB9Kgz
+Qnvft9+SBWh9+F3ip7BsL6Q4Q9v8eHRbnP0ya7ddlgh64uwf9VOfZZdKCnwqudJP
+3g==
+-----END CERTIFICATE-----
+Bag Attributes
+ localKeyID: 02 00 00 00
+subject=/CN=/subscriptions/redacted/resourcegroups/redacted/providers/Microsoft.Compute/virtualMachines/redacted
+issuer=/CN=Microsoft.ManagedIdentity
+-----BEGIN CERTIFICATE-----
+MIIDnTCCAoWgAwIBAgIUB2lauSRccvFkoJybUfIwOUqBN7MwDQYJKoZIhvcNAQEL
+BQAwJDEiMCAGA1UEAxMZTWljcm9zb2Z0Lk1hbmFnZWRJZGVudGl0eTAeFw0xOTAy
+MTUxOTA5MDBaFw0xOTA4MTQxOTA5MDBaMIGUMYGRMIGOBgNVBAMTgYYvc3Vic2Ny
+aXB0aW9ucy8yN2I3NTBjZC1lZDQzLTQyZmQtOTA0NC04ZDc1ZTEyNGFlNTUvcmVz
+b3VyY2Vncm91cHMvYW5oZXh0cmFzc2gvcHJvdmlkZXJzL01pY3Jvc29mdC5Db21w
+dXRlL3ZpcnR1YWxNYWNoaW5lcy9hbmh0ZXN0Y2VydDCCASIwDQYJKoZIhvcNAQEB
+BQADggEPADCCAQoCggEBAOVCE+tnBVBgVXgUFzQfWJNdhrOcynBm8QhMq1dYALNN
+2C5R16sRU6RdbcdOLke8LasxrK3SeqjfNx3HV4aKp2OllD/AyuTP3A0Qz+c0yxee
+0TDGTSMJU0oH+PPq9/4E62tIjTVKuK0AZlZ2kqhNTLO1PwLaYDdfoPyDebgN3TuW
+2fPFoOoBAhTmMEeHd/9DXi2U81lZQiKpVMKAPGAB7swOZ+z2HcIidMFfVczknhSw
+tMvbf+lp2B5K8/lgXmqvX7RztN1+3GvaXAA3esuSKx/kSIsBOhXIkmWA/97GjYjw
+Nogp7sNnMPdjUKu6s6mRwwpi7mQzUe2Vu5q0OQBragMCAwEAAaNWMFQwDgYDVR0P
+AQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwHwYD
+VR0jBBgwFoAUOJvzEsriQWdJBndPrK+Me1bCPjYwDQYJKoZIhvcNAQELBQADggEB
+AFGP/g8o7Hv/to11M0UqfzJuW/AyH9RZtSRcNQFLZUndwweQ6fap8lFsA4REUdqe
+7Quqp5JNNY1XzKLWXMPoheIDH1A8FFXdsAroArzlNs9tO3TlIHE8A7HxEVZEmR4b
+7ZiixmkQPS2RkjEoV/GM6fheBrzuFn7X5kVZyE6cC5sfcebn8xhk3ZcXI0VmpdT0
+jFBsf5IvFCIXXLLhJI4KXc8VMoKFU1jT9na/jyaoGmfwovKj4ib8s2aiXGAp7Y38
+UCmY+bJapWom6Piy5Jzi/p/kzMVdJcSa+GqpuFxBoQYEVs2XYVl7cGu/wPM+NToC
+pkSoWwF1QAnHn0eokR9E1rU=
+-----END CERTIFICATE-----
+Bag Attributes: <Empty Attributes>
+subject=/CN=CRP/OU=AzureRT/O=Microsoft Corporation/L=Redmond/ST=WA/C=US
+issuer=/CN=Root Agency
+-----BEGIN CERTIFICATE-----
+MIIB+TCCAeOgAwIBAgIBATANBgkqhkiG9w0BAQUFADAWMRQwEgYDVQQDDAtSb290
+IEFnZW5jeTAeFw0xOTAyMTUxOTA0MDRaFw0yOTAyMTUxOTE0MDRaMGwxDDAKBgNV
+BAMMA0NSUDEQMA4GA1UECwwHQXp1cmVSVDEeMBwGA1UECgwVTWljcm9zb2Z0IENv
+cnBvcmF0aW9uMRAwDgYDVQQHDAdSZWRtb25kMQswCQYDVQQIDAJXQTELMAkGA1UE
+BhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDHU9IDclbKVYVb
+Yuv0+zViX+wTwlKspslmy/uf3hkWLh7pyzyrq70S7qtSW2EGixUPxZS/R8pOLHoi
+nlKF9ILgj0gVTCJsSwnWpXRg3rhZwIVoYMHN50BHS1SqVD0lsWNMXmo76LoJcjmW
+vwIznvj5C/gnhU+K7+c3m7AlCyU2wjwpBAEYj7PQs6l/wTqpEiaqC5NytNBd7qp+
+lYYysVrpa1PFL0Nj4MMZARIfjkiJtL9qDhy9YZeJRQ6q/Fhz0kjvkZnfxixfKF4y
+WzOfhBrAtpF6oOnuYKk3hxjh9KjTTX4/U8zdLojalX09iyHyEjwJKGlGEpzh1aY7
+t5btUyvpAgMBAAEwDQYJKoZIhvcNAQEFBQADAQA=
+-----END CERTIFICATE-----
diff --git a/tests/data/azure/pubkey_extract_cert b/tests/data/azure/pubkey_extract_cert
new file mode 100644
index 00000000..ce9b852d
--- /dev/null
+++ b/tests/data/azure/pubkey_extract_cert
@@ -0,0 +1,13 @@
+-----BEGIN CERTIFICATE-----
+MIIB+TCCAeOgAwIBAgIBATANBgkqhkiG9w0BAQUFADAWMRQwEgYDVQQDDAtSb290
+IEFnZW5jeTAeFw0xOTAyMTUxOTA0MDRaFw0yOTAyMTUxOTE0MDRaMGwxDDAKBgNV
+BAMMA0NSUDEQMA4GA1UECwwHQXp1cmVSVDEeMBwGA1UECgwVTWljcm9zb2Z0IENv
+cnBvcmF0aW9uMRAwDgYDVQQHDAdSZWRtb25kMQswCQYDVQQIDAJXQTELMAkGA1UE
+BhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDHU9IDclbKVYVb
+Yuv0+zViX+wTwlKspslmy/uf3hkWLh7pyzyrq70S7qtSW2EGixUPxZS/R8pOLHoi
+nlKF9ILgj0gVTCJsSwnWpXRg3rhZwIVoYMHN50BHS1SqVD0lsWNMXmo76LoJcjmW
+vwIznvj5C/gnhU+K7+c3m7AlCyU2wjwpBAEYj7PQs6l/wTqpEiaqC5NytNBd7qp+
+lYYysVrpa1PFL0Nj4MMZARIfjkiJtL9qDhy9YZeJRQ6q/Fhz0kjvkZnfxixfKF4y
+WzOfhBrAtpF6oOnuYKk3hxjh9KjTTX4/U8zdLojalX09iyHyEjwJKGlGEpzh1aY7
+t5btUyvpAgMBAAEwDQYJKoZIhvcNAQEFBQADAQA=
+-----END CERTIFICATE-----
diff --git a/tests/data/azure/pubkey_extract_ssh_key b/tests/data/azure/pubkey_extract_ssh_key
new file mode 100644
index 00000000..54d749ed
--- /dev/null
+++ b/tests/data/azure/pubkey_extract_ssh_key
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDHU9IDclbKVYVbYuv0+zViX+wTwlKspslmy/uf3hkWLh7pyzyrq70S7qtSW2EGixUPxZS/R8pOLHoinlKF9ILgj0gVTCJsSwnWpXRg3rhZwIVoYMHN50BHS1SqVD0lsWNMXmo76LoJcjmWvwIznvj5C/gnhU+K7+c3m7AlCyU2wjwpBAEYj7PQs6l/wTqpEiaqC5NytNBd7qp+lYYysVrpa1PFL0Nj4MMZARIfjkiJtL9qDhy9YZeJRQ6q/Fhz0kjvkZnfxixfKF4yWzOfhBrAtpF6oOnuYKk3hxjh9KjTTX4/U8zdLojalX09iyHyEjwJKGlGEpzh1aY7t5btUyvp
diff --git a/tests/data/netinfo/freebsd-ifconfig-output b/tests/data/netinfo/freebsd-ifconfig-output
new file mode 100644
index 00000000..f64c2f60
--- /dev/null
+++ b/tests/data/netinfo/freebsd-ifconfig-output
@@ -0,0 +1,39 @@
+vtnet0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500
+ options=6c07bb<RXCSUM,TXCSUM,VLAN_MTU,VLAN_HWTAGGING,JUMBO_MTU,VLAN_HWCSUM,TSO4,TSO6,LRO,VLAN_HWTSO,LINKSTATE,RXCSUM_IPV6,TXCSUM_IPV6>
+ ether 52:54:00:50:b7:0d
+re0.33: flags=8943<UP,BROADCAST,RUNNING,PROMISC,SIMPLEX,MULTICAST> metric 0 mtu 1500
+ options=80003<RXCSUM,TXCSUM,LINKSTATE>
+ ether 80:00:73:63:5c:48
+ groups: vlan
+ vlan: 33 vlanpcp: 0 parent interface: re0
+ media: Ethernet autoselect (1000baseT <full-duplex,master>)
+ status: active
+ nd6 options=21<PERFORMNUD,AUTO_LINKLOCAL>
+bridge0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500
+ ether 02:14:39:0e:25:00
+ inet 192.168.1.1 netmask 0xffffff00 broadcast 192.168.1.255
+ id 00:00:00:00:00:00 priority 32768 hellotime 2 fwddelay 15
+ maxage 20 holdcnt 6 proto rstp maxaddr 2000 timeout 1200
+ root id 00:00:00:00:00:00 priority 32768 ifcost 0 port 0
+ member: vnet0:11 flags=143<LEARNING,DISCOVER,AUTOEDGE,AUTOPTP>
+ ifmaxaddr 0 port 5 priority 128 path cost 2000
+ member: vnet0:1 flags=143<LEARNING,DISCOVER,AUTOEDGE,AUTOPTP>
+ ifmaxaddr 0 port 4 priority 128 path cost 2000
+ groups: bridge
+ nd6 options=9<PERFORMNUD,IFDISABLED>
+vnet0:11: flags=8943<UP,BROADCAST,RUNNING,PROMISC,SIMPLEX,MULTICAST> metric 0 mtu 1500
+ description: 'associated with jail: webirc'
+ options=8<VLAN_MTU>
+ ether 02:ff:60:8c:f3:72
+ hwaddr 02:2b:bb:64:3f:0a
+ inet6 fe80::2b:bbff:fe64:3f0a%vnet0:11 prefixlen 64 tentative scopeid 0x5
+ groups: epair
+ media: Ethernet 10Gbase-T (10Gbase-T <full-duplex>)
+ status: active
+ nd6 options=29<PERFORMNUD,IFDISABLED,AUTO_LINKLOCAL>
+lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> metric 0 mtu 16384
+ options=600003<RXCSUM,TXCSUM,RXCSUM_IPV6,TXCSUM_IPV6>
+ inet6 ::1 prefixlen 128
+ inet6 fe80::1%lo0 prefixlen 64 scopeid 0x2
+ inet 127.0.0.1 netmask 0xff000000
+ nd6 options=21<PERFORMNUD,AUTO_LINKLOCAL>
diff --git a/tests/data/netinfo/freebsd-netdev-formatted-output b/tests/data/netinfo/freebsd-netdev-formatted-output
new file mode 100644
index 00000000..a0d937b3
--- /dev/null
+++ b/tests/data/netinfo/freebsd-netdev-formatted-output
@@ -0,0 +1,12 @@
++++++++++++++++++++++++++++++++++++++++++Net device info++++++++++++++++++++++++++++++++++++++++++
++----------+------+-------------------------------------+------------+-------+-------------------+
+| Device | Up | Address | Mask | Scope | Hw-Address |
++----------+------+-------------------------------------+------------+-------+-------------------+
+| bridge0 | True | 192.168.1.1 | 0xffffff00 | . | 02:14:39:0e:25:00 |
+| lo0 | True | 127.0.0.1 | 0xff000000 | . | . |
+| lo0 | True | ::1/128 | . | . | . |
+| lo0 | True | fe80::1%lo0/64 | . | 0x2 | . |
+| re0.33 | True | . | . | . | 80:00:73:63:5c:48 |
+| vnet0:11 | True | fe80::2b:bbff:fe64:3f0a%vnet0:11/64 | . | 0x5 | 02:2b:bb:64:3f:0a |
+| vtnet0 | True | . | . | . | 52:54:00:50:b7:0d |
++----------+------+-------------------------------------+------------+-------+-------------------+
diff --git a/tests/unittests/test_cli.py b/tests/unittests/test_cli.py
index d283f136..e57c15d1 100644
--- a/tests/unittests/test_cli.py
+++ b/tests/unittests/test_cli.py
@@ -1,8 +1,8 @@
# This file is part of cloud-init. See LICENSE file for license information.
-from collections import namedtuple
import os
-import six
+import io
+from collections import namedtuple
from cloudinit.cmd import main as cli
from cloudinit.tests import helpers as test_helpers
@@ -18,7 +18,7 @@ class TestCLI(test_helpers.FilesystemMockingTestCase):
def setUp(self):
super(TestCLI, self).setUp()
- self.stderr = six.StringIO()
+ self.stderr = io.StringIO()
self.patchStdoutAndStderr(stderr=self.stderr)
def _call_main(self, sysv_args=None):
@@ -147,7 +147,7 @@ class TestCLI(test_helpers.FilesystemMockingTestCase):
def test_conditional_subcommands_from_entry_point_sys_argv(self):
"""Subcommands from entry-point are properly parsed from sys.argv."""
- stdout = six.StringIO()
+ stdout = io.StringIO()
self.patchStdoutAndStderr(stdout=stdout)
expected_errors = [
@@ -178,7 +178,7 @@ class TestCLI(test_helpers.FilesystemMockingTestCase):
def test_collect_logs_subcommand_parser(self):
"""The subcommand cloud-init collect-logs calls the subparser."""
# Provide -h param to collect-logs to avoid having to mock behavior.
- stdout = six.StringIO()
+ stdout = io.StringIO()
self.patchStdoutAndStderr(stdout=stdout)
self._call_main(['cloud-init', 'collect-logs', '-h'])
self.assertIn('usage: cloud-init collect-log', stdout.getvalue())
@@ -186,7 +186,7 @@ class TestCLI(test_helpers.FilesystemMockingTestCase):
def test_clean_subcommand_parser(self):
"""The subcommand cloud-init clean calls the subparser."""
# Provide -h param to clean to avoid having to mock behavior.
- stdout = six.StringIO()
+ stdout = io.StringIO()
self.patchStdoutAndStderr(stdout=stdout)
self._call_main(['cloud-init', 'clean', '-h'])
self.assertIn('usage: cloud-init clean', stdout.getvalue())
@@ -194,7 +194,7 @@ class TestCLI(test_helpers.FilesystemMockingTestCase):
def test_status_subcommand_parser(self):
"""The subcommand cloud-init status calls the subparser."""
# Provide -h param to clean to avoid having to mock behavior.
- stdout = six.StringIO()
+ stdout = io.StringIO()
self.patchStdoutAndStderr(stdout=stdout)
self._call_main(['cloud-init', 'status', '-h'])
self.assertIn('usage: cloud-init status', stdout.getvalue())
@@ -219,7 +219,7 @@ class TestCLI(test_helpers.FilesystemMockingTestCase):
def test_wb_devel_schema_subcommand_doc_content(self):
"""Validate that doc content is sane from known examples."""
- stdout = six.StringIO()
+ stdout = io.StringIO()
self.patchStdoutAndStderr(stdout=stdout)
self._call_main(['cloud-init', 'devel', 'schema', '--doc'])
expected_doc_sections = [
diff --git a/tests/unittests/test_data.py b/tests/unittests/test_data.py
index 3efe7adf..74cc26ec 100644
--- a/tests/unittests/test_data.py
+++ b/tests/unittests/test_data.py
@@ -5,13 +5,8 @@
import gzip
import logging
import os
-
-try:
- from unittest import mock
-except ImportError:
- import mock
-
-from six import BytesIO, StringIO
+from io import BytesIO, StringIO
+from unittest import mock
from email import encoders
from email.mime.application import MIMEApplication
@@ -27,6 +22,7 @@ from cloudinit.settings import (PER_INSTANCE)
from cloudinit import sources
from cloudinit import stages
from cloudinit import user_data as ud
+from cloudinit import safeyaml
from cloudinit import util
from cloudinit.tests import helpers
@@ -502,7 +498,7 @@ c: 4
data = [{'content': '#cloud-config\npassword: gocubs\n'},
{'content': '#cloud-config\nlocale: chicago\n'},
{'content': non_decodable}]
- message = b'#cloud-config-archive\n' + util.yaml_dumps(data).encode()
+ message = b'#cloud-config-archive\n' + safeyaml.dumps(data).encode()
self.reRoot()
ci = stages.Init()
@@ -524,6 +520,46 @@ c: 4
self.assertEqual(cfg.get('password'), 'gocubs')
self.assertEqual(cfg.get('locale'), 'chicago')
+ @mock.patch('cloudinit.util.read_conf_with_confd')
+ def test_dont_allow_user_data(self, mock_cfg):
+ mock_cfg.return_value = {"allow_userdata": False}
+
+ # test that user-data is ignored but vendor-data is kept
+ user_blob = '''
+#cloud-config-jsonp
+[
+ { "op": "add", "path": "/baz", "value": "qux" },
+ { "op": "add", "path": "/bar", "value": "qux2" }
+]
+'''
+ vendor_blob = '''
+#cloud-config-jsonp
+[
+ { "op": "add", "path": "/baz", "value": "quxA" },
+ { "op": "add", "path": "/bar", "value": "quxB" },
+ { "op": "add", "path": "/foo", "value": "quxC" }
+]
+'''
+ self.reRoot()
+ initer = stages.Init()
+ initer.datasource = FakeDataSource(user_blob, vendordata=vendor_blob)
+ initer.read_cfg()
+ initer.initialize()
+ initer.fetch()
+ initer.instancify()
+ initer.update()
+ initer.cloudify().run('consume_data',
+ initer.consume_data,
+ args=[PER_INSTANCE],
+ freq=PER_INSTANCE)
+ mods = stages.Modules(initer)
+ (_which_ran, _failures) = mods.run_section('cloud_init_modules')
+ cfg = mods.cfg
+ self.assertIn('vendor_data', cfg)
+ self.assertEqual('quxA', cfg['baz'])
+ self.assertEqual('quxB', cfg['bar'])
+ self.assertEqual('quxC', cfg['foo'])
+
class TestConsumeUserDataHttp(TestConsumeUserData, helpers.HttprettyTestCase):
diff --git a/tests/unittests/test_datasource/test_aliyun.py b/tests/unittests/test_datasource/test_aliyun.py
index e9213ca1..1e66fcdb 100644
--- a/tests/unittests/test_datasource/test_aliyun.py
+++ b/tests/unittests/test_datasource/test_aliyun.py
@@ -2,8 +2,8 @@
import functools
import httpretty
-import mock
import os
+from unittest import mock
from cloudinit import helpers
from cloudinit.sources import DataSourceAliYun as ay
diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py
index 417d86a9..a809fd87 100644
--- a/tests/unittests/test_datasource/test_azure.py
+++ b/tests/unittests/test_datasource/test_azure.py
@@ -6,13 +6,13 @@ from cloudinit import url_helper
from cloudinit.sources import (
UNSET, DataSourceAzure as dsaz, InvalidMetaDataException)
from cloudinit.util import (b64e, decode_binary, load_file, write_file,
- find_freebsd_part, get_path_dev_freebsd,
- MountFailedError)
+ MountFailedError, json_dumps, load_json)
from cloudinit.version import version_string as vs
from cloudinit.tests.helpers import (
HttprettyTestCase, CiTestCase, populate_dir, mock, wrap_and_call,
- ExitStack, PY26, SkipTest)
+ ExitStack, resourceLocation)
+import copy
import crypt
import httpretty
import json
@@ -85,6 +85,25 @@ def construct_valid_ovf_env(data=None, pubkeys=None,
NETWORK_METADATA = {
+ "compute": {
+ "location": "eastus2",
+ "name": "my-hostname",
+ "offer": "UbuntuServer",
+ "osType": "Linux",
+ "placementGroupId": "",
+ "platformFaultDomain": "0",
+ "platformUpdateDomain": "0",
+ "publisher": "Canonical",
+ "resourceGroupName": "srugroup1",
+ "sku": "19.04-DAILY",
+ "subscriptionId": "12aad61c-6de4-4e53-a6c6-5aff52a83777",
+ "tags": "",
+ "version": "19.04.201906190",
+ "vmId": "ff702a6b-cb6a-4fcd-ad68-b4ce38227642",
+ "vmScaleSetName": "",
+ "vmSize": "Standard_DS1_v2",
+ "zone": ""
+ },
"network": {
"interface": [
{
@@ -111,9 +130,155 @@ NETWORK_METADATA = {
}
}
+SECONDARY_INTERFACE = {
+ "macAddress": "220D3A047598",
+ "ipv6": {
+ "ipAddress": []
+ },
+ "ipv4": {
+ "subnet": [
+ {
+ "prefix": "24",
+ "address": "10.0.1.0"
+ }
+ ],
+ "ipAddress": [
+ {
+ "privateIpAddress": "10.0.1.5",
+ }
+ ]
+ }
+}
+
MOCKPATH = 'cloudinit.sources.DataSourceAzure.'
+class TestParseNetworkConfig(CiTestCase):
+
+ maxDiff = None
+
+ def test_single_ipv4_nic_configuration(self):
+ """parse_network_config emits dhcp on single nic with ipv4"""
+ expected = {'ethernets': {
+ 'eth0': {'dhcp4': True,
+ 'dhcp4-overrides': {'route-metric': 100},
+ 'dhcp6': False,
+ 'match': {'macaddress': '00:0d:3a:04:75:98'},
+ 'set-name': 'eth0'}}, 'version': 2}
+ self.assertEqual(expected, dsaz.parse_network_config(NETWORK_METADATA))
+
+ def test_increases_route_metric_for_non_primary_nics(self):
+ """parse_network_config increases route-metric for each nic"""
+ expected = {'ethernets': {
+ 'eth0': {'dhcp4': True,
+ 'dhcp4-overrides': {'route-metric': 100},
+ 'dhcp6': False,
+ 'match': {'macaddress': '00:0d:3a:04:75:98'},
+ 'set-name': 'eth0'},
+ 'eth1': {'set-name': 'eth1',
+ 'match': {'macaddress': '22:0d:3a:04:75:98'},
+ 'dhcp6': False,
+ 'dhcp4': True,
+ 'dhcp4-overrides': {'route-metric': 200}},
+ 'eth2': {'set-name': 'eth2',
+ 'match': {'macaddress': '33:0d:3a:04:75:98'},
+ 'dhcp6': False,
+ 'dhcp4': True,
+ 'dhcp4-overrides': {'route-metric': 300}}}, 'version': 2}
+ imds_data = copy.deepcopy(NETWORK_METADATA)
+ imds_data['network']['interface'].append(SECONDARY_INTERFACE)
+ third_intf = copy.deepcopy(SECONDARY_INTERFACE)
+ third_intf['macAddress'] = third_intf['macAddress'].replace('22', '33')
+ third_intf['ipv4']['subnet'][0]['address'] = '10.0.2.0'
+ third_intf['ipv4']['ipAddress'][0]['privateIpAddress'] = '10.0.2.6'
+ imds_data['network']['interface'].append(third_intf)
+ self.assertEqual(expected, dsaz.parse_network_config(imds_data))
+
+ def test_ipv4_and_ipv6_route_metrics_match_for_nics(self):
+ """parse_network_config emits matching ipv4 and ipv6 route-metrics."""
+ expected = {'ethernets': {
+ 'eth0': {'addresses': ['10.0.0.5/24', '2001:dead:beef::2/128'],
+ 'dhcp4': True,
+ 'dhcp4-overrides': {'route-metric': 100},
+ 'dhcp6': True,
+ 'dhcp6-overrides': {'route-metric': 100},
+ 'match': {'macaddress': '00:0d:3a:04:75:98'},
+ 'set-name': 'eth0'},
+ 'eth1': {'set-name': 'eth1',
+ 'match': {'macaddress': '22:0d:3a:04:75:98'},
+ 'dhcp4': True,
+ 'dhcp6': False,
+ 'dhcp4-overrides': {'route-metric': 200}},
+ 'eth2': {'set-name': 'eth2',
+ 'match': {'macaddress': '33:0d:3a:04:75:98'},
+ 'dhcp4': True,
+ 'dhcp4-overrides': {'route-metric': 300},
+ 'dhcp6': True,
+ 'dhcp6-overrides': {'route-metric': 300}}}, 'version': 2}
+ imds_data = copy.deepcopy(NETWORK_METADATA)
+ nic1 = imds_data['network']['interface'][0]
+ nic1['ipv4']['ipAddress'].append({'privateIpAddress': '10.0.0.5'})
+
+ nic1['ipv6'] = {
+ "subnet": [{"address": "2001:dead:beef::16"}],
+ "ipAddress": [{"privateIpAddress": "2001:dead:beef::1"},
+ {"privateIpAddress": "2001:dead:beef::2"}]
+ }
+ imds_data['network']['interface'].append(SECONDARY_INTERFACE)
+ third_intf = copy.deepcopy(SECONDARY_INTERFACE)
+ third_intf['macAddress'] = third_intf['macAddress'].replace('22', '33')
+ third_intf['ipv4']['subnet'][0]['address'] = '10.0.2.0'
+ third_intf['ipv4']['ipAddress'][0]['privateIpAddress'] = '10.0.2.6'
+ third_intf['ipv6'] = {
+ "subnet": [{"prefix": "64", "address": "2001:dead:beef::2"}],
+ "ipAddress": [{"privateIpAddress": "2001:dead:beef::1"}]
+ }
+ imds_data['network']['interface'].append(third_intf)
+ self.assertEqual(expected, dsaz.parse_network_config(imds_data))
+
+ def test_ipv4_secondary_ips_will_be_static_addrs(self):
+ """parse_network_config emits primary ipv4 as dhcp others are static"""
+ expected = {'ethernets': {
+ 'eth0': {'addresses': ['10.0.0.5/24'],
+ 'dhcp4': True,
+ 'dhcp4-overrides': {'route-metric': 100},
+ 'dhcp6': True,
+ 'dhcp6-overrides': {'route-metric': 100},
+ 'match': {'macaddress': '00:0d:3a:04:75:98'},
+ 'set-name': 'eth0'}}, 'version': 2}
+ imds_data = copy.deepcopy(NETWORK_METADATA)
+ nic1 = imds_data['network']['interface'][0]
+ nic1['ipv4']['ipAddress'].append({'privateIpAddress': '10.0.0.5'})
+
+ nic1['ipv6'] = {
+ "subnet": [{"prefix": "10", "address": "2001:dead:beef::16"}],
+ "ipAddress": [{"privateIpAddress": "2001:dead:beef::1"}]
+ }
+ self.assertEqual(expected, dsaz.parse_network_config(imds_data))
+
+ def test_ipv6_secondary_ips_will_be_static_cidrs(self):
+ """parse_network_config emits primary ipv6 as dhcp others are static"""
+ expected = {'ethernets': {
+ 'eth0': {'addresses': ['10.0.0.5/24', '2001:dead:beef::2/10'],
+ 'dhcp4': True,
+ 'dhcp4-overrides': {'route-metric': 100},
+ 'dhcp6': True,
+ 'dhcp6-overrides': {'route-metric': 100},
+ 'match': {'macaddress': '00:0d:3a:04:75:98'},
+ 'set-name': 'eth0'}}, 'version': 2}
+ imds_data = copy.deepcopy(NETWORK_METADATA)
+ nic1 = imds_data['network']['interface'][0]
+ nic1['ipv4']['ipAddress'].append({'privateIpAddress': '10.0.0.5'})
+
+ # Secondary ipv6 addresses currently ignored/unconfigured
+ nic1['ipv6'] = {
+ "subnet": [{"prefix": "10", "address": "2001:dead:beef::16"}],
+ "ipAddress": [{"privateIpAddress": "2001:dead:beef::1"},
+ {"privateIpAddress": "2001:dead:beef::2"}]
+ }
+ self.assertEqual(expected, dsaz.parse_network_config(imds_data))
+
+
class TestGetMetadataFromIMDS(HttprettyTestCase):
with_logs = True
@@ -142,7 +307,7 @@ class TestGetMetadataFromIMDS(HttprettyTestCase):
self.logs.getvalue())
@mock.patch(MOCKPATH + 'readurl')
- @mock.patch(MOCKPATH + 'EphemeralDHCPv4')
+ @mock.patch(MOCKPATH + 'EphemeralDHCPv4WithReporting')
@mock.patch(MOCKPATH + 'net.is_up')
def test_get_metadata_performs_dhcp_when_network_is_down(
self, m_net_is_up, m_dhcp, m_readurl):
@@ -156,14 +321,15 @@ class TestGetMetadataFromIMDS(HttprettyTestCase):
dsaz.get_metadata_from_imds('eth9', retries=2))
m_net_is_up.assert_called_with('eth9')
- m_dhcp.assert_called_with('eth9')
+ m_dhcp.assert_called_with(mock.ANY, 'eth9')
self.assertIn(
"Crawl of Azure Instance Metadata Service (IMDS) took", # log_time
self.logs.getvalue())
m_readurl.assert_called_with(
self.network_md_url, exception_cb=mock.ANY,
- headers={'Metadata': 'true'}, retries=2, timeout=1)
+ headers={'Metadata': 'true'}, retries=2,
+ timeout=dsaz.IMDS_TIMEOUT_IN_SECONDS)
@mock.patch('cloudinit.url_helper.time.sleep')
@mock.patch(MOCKPATH + 'net.is_up')
@@ -221,8 +387,6 @@ class TestAzureDataSource(CiTestCase):
def setUp(self):
super(TestAzureDataSource, self).setUp()
- if PY26:
- raise SkipTest("Does not work on python 2.6")
self.tmp = self.tmp_dir()
# patch cloud_dir, so our 'seed_dir' is guaranteed empty
@@ -313,7 +477,7 @@ scbus-1 on xpt0 bus 0
'public-keys': [],
})
- self.instance_id = 'test-instance-id'
+ self.instance_id = 'D0DF4C54-4ECB-4A4B-9954-5BDF3ED5C3B8'
def _dmi_mocks(key):
if key == 'system-uuid':
@@ -392,29 +556,6 @@ scbus-1 on xpt0 bus 0
dev = ds.get_resource_disk_on_freebsd(1)
self.assertEqual("da1", dev)
- @mock.patch('cloudinit.util.subp')
- def test_find_freebsd_part_on_Azure(self, mock_subp):
- glabel_out = '''
-gptid/fa52d426-c337-11e6-8911-00155d4c5e47 N/A da0p1
- label/rootfs N/A da0p2
- label/swap N/A da0p3
-'''
- mock_subp.return_value = (glabel_out, "")
- res = find_freebsd_part("/dev/label/rootfs")
- self.assertEqual("da0p2", res)
-
- def test_get_path_dev_freebsd_on_Azure(self):
- mnt_list = '''
-/dev/label/rootfs / ufs rw 1 1
-devfs /dev devfs rw,multilabel 0 0
-fdescfs /dev/fd fdescfs rw 0 0
-/dev/da1s1 /mnt/resource ufs rw 2 2
-'''
- with mock.patch.object(os.path, 'exists',
- return_value=True):
- res = get_path_dev_freebsd('/etc', mnt_list)
- self.assertIsNotNone(res)
-
@mock.patch(MOCKPATH + '_is_platform_viable')
def test_call_is_platform_viable_seed(self, m_is_platform_viable):
"""Check seed_dir using _is_platform_viable and return False."""
@@ -503,14 +644,8 @@ fdescfs /dev/fd fdescfs rw 0 0
expected_metadata = {
'azure_data': {
'configurationsettype': 'LinuxProvisioningConfiguration'},
- 'imds': {'network': {'interface': [{
- 'ipv4': {'ipAddress': [
- {'privateIpAddress': '10.0.0.4',
- 'publicIpAddress': '104.46.124.81'}],
- 'subnet': [{'address': '10.0.0.0', 'prefix': '24'}]},
- 'ipv6': {'ipAddress': []},
- 'macAddress': '000D3A047598'}]}},
- 'instance-id': 'test-instance-id',
+ 'imds': NETWORK_METADATA,
+ 'instance-id': 'D0DF4C54-4ECB-4A4B-9954-5BDF3ED5C3B8',
'local-hostname': u'myhost',
'random_seed': 'wild'}
@@ -543,7 +678,8 @@ fdescfs /dev/fd fdescfs rw 0 0
dsrc.crawl_metadata()
self.assertEqual(str(cm.exception), error_msg)
- @mock.patch('cloudinit.sources.DataSourceAzure.EphemeralDHCPv4')
+ @mock.patch(
+ 'cloudinit.sources.DataSourceAzure.EphemeralDHCPv4WithReporting')
@mock.patch('cloudinit.sources.DataSourceAzure.util.write_file')
@mock.patch(
'cloudinit.sources.DataSourceAzure.DataSourceAzure._report_ready')
@@ -631,12 +767,71 @@ fdescfs /dev/fd fdescfs rw 0 0
'ethernets': {
'eth0': {'set-name': 'eth0',
'match': {'macaddress': '00:0d:3a:04:75:98'},
- 'dhcp4': True}},
+ 'dhcp6': False,
+ 'dhcp4': True,
+ 'dhcp4-overrides': {'route-metric': 100}}},
'version': 2}
dsrc = self._get_ds(data)
dsrc.get_data()
self.assertEqual(expected_network_config, dsrc.network_config)
+ def test_network_config_set_from_imds_route_metric_for_secondary_nic(self):
+ """Datasource.network_config adds route-metric to secondary nics."""
+ sys_cfg = {'datasource': {'Azure': {'apply_network_config': True}}}
+ odata = {}
+ data = {'ovfcontent': construct_valid_ovf_env(data=odata),
+ 'sys_cfg': sys_cfg}
+ expected_network_config = {
+ 'ethernets': {
+ 'eth0': {'set-name': 'eth0',
+ 'match': {'macaddress': '00:0d:3a:04:75:98'},
+ 'dhcp6': False,
+ 'dhcp4': True,
+ 'dhcp4-overrides': {'route-metric': 100}},
+ 'eth1': {'set-name': 'eth1',
+ 'match': {'macaddress': '22:0d:3a:04:75:98'},
+ 'dhcp6': False,
+ 'dhcp4': True,
+ 'dhcp4-overrides': {'route-metric': 200}},
+ 'eth2': {'set-name': 'eth2',
+ 'match': {'macaddress': '33:0d:3a:04:75:98'},
+ 'dhcp6': False,
+ 'dhcp4': True,
+ 'dhcp4-overrides': {'route-metric': 300}}},
+ 'version': 2}
+ imds_data = copy.deepcopy(NETWORK_METADATA)
+ imds_data['network']['interface'].append(SECONDARY_INTERFACE)
+ third_intf = copy.deepcopy(SECONDARY_INTERFACE)
+ third_intf['macAddress'] = third_intf['macAddress'].replace('22', '33')
+ third_intf['ipv4']['subnet'][0]['address'] = '10.0.2.0'
+ third_intf['ipv4']['ipAddress'][0]['privateIpAddress'] = '10.0.2.6'
+ imds_data['network']['interface'].append(third_intf)
+
+ self.m_get_metadata_from_imds.return_value = imds_data
+ dsrc = self._get_ds(data)
+ dsrc.get_data()
+ self.assertEqual(expected_network_config, dsrc.network_config)
+
+ def test_availability_zone_set_from_imds(self):
+ """Datasource.availability returns IMDS platformFaultDomain."""
+ sys_cfg = {'datasource': {'Azure': {'apply_network_config': True}}}
+ odata = {}
+ data = {'ovfcontent': construct_valid_ovf_env(data=odata),
+ 'sys_cfg': sys_cfg}
+ dsrc = self._get_ds(data)
+ dsrc.get_data()
+ self.assertEqual('0', dsrc.availability_zone)
+
+ def test_region_set_from_imds(self):
+ """Datasource.region returns IMDS region location."""
+ sys_cfg = {'datasource': {'Azure': {'apply_network_config': True}}}
+ odata = {}
+ data = {'ovfcontent': construct_valid_ovf_env(data=odata),
+ 'sys_cfg': sys_cfg}
+ dsrc = self._get_ds(data)
+ dsrc.get_data()
+ self.assertEqual('eastus2', dsrc.region)
+
def test_user_cfg_set_agent_command(self):
# set dscfg in via base64 encoded yaml
cfg = {'agent_command': "my_command"}
@@ -704,6 +899,22 @@ fdescfs /dev/fd fdescfs rw 0 0
crypt.crypt(odata['UserPassword'],
defuser['passwd'][0:pos]))
+ def test_user_not_locked_if_password_redacted(self):
+ odata = {'HostName': "myhost", 'UserName': "myuser",
+ 'UserPassword': dsaz.DEF_PASSWD_REDACTION}
+ data = {'ovfcontent': construct_valid_ovf_env(data=odata)}
+
+ dsrc = self._get_ds(data)
+ ret = dsrc.get_data()
+ self.assertTrue(ret)
+ self.assertTrue('default_user' in dsrc.cfg['system_info'])
+ defuser = dsrc.cfg['system_info']['default_user']
+
+ # default user should be updated username and should not be locked.
+ self.assertEqual(defuser['name'], odata['UserName'])
+ self.assertIn('lock_passwd', defuser)
+ self.assertFalse(defuser['lock_passwd'])
+
def test_userdata_plain(self):
mydata = "FOOBAR"
odata = {'UserData': {'text': mydata, 'encoding': 'plain'}}
@@ -880,6 +1091,24 @@ fdescfs /dev/fd fdescfs rw 0 0
self.assertTrue(ret)
self.assertEqual('value', dsrc.metadata['test'])
+ def test_instance_id_endianness(self):
+ """Return the previous iid when dmi uuid is the byteswapped iid."""
+ ds = self._get_ds({'ovfcontent': construct_valid_ovf_env()})
+ # byte-swapped previous
+ write_file(
+ os.path.join(self.paths.cloud_dir, 'data', 'instance-id'),
+ '544CDFD0-CB4E-4B4A-9954-5BDF3ED5C3B8')
+ ds.get_data()
+ self.assertEqual(
+ '544CDFD0-CB4E-4B4A-9954-5BDF3ED5C3B8', ds.metadata['instance-id'])
+ # not byte-swapped previous
+ write_file(
+ os.path.join(self.paths.cloud_dir, 'data', 'instance-id'),
+ '644CDFD0-CB4E-4B4A-9954-5BDF3ED5C3B8')
+ ds.get_data()
+ self.assertEqual(
+ 'D0DF4C54-4ECB-4A4B-9954-5BDF3ED5C3B8', ds.metadata['instance-id'])
+
def test_instance_id_from_dmidecode_used(self):
ds = self._get_ds({'ovfcontent': construct_valid_ovf_env()})
ds.get_data()
@@ -917,6 +1146,8 @@ fdescfs /dev/fd fdescfs rw 0 0
expected_cfg = {
'ethernets': {
'eth0': {'dhcp4': True,
+ 'dhcp4-overrides': {'route-metric': 100},
+ 'dhcp6': False,
'match': {'macaddress': '00:0d:3a:04:75:98'},
'set-name': 'eth0'}},
'version': 2}
@@ -1079,7 +1310,7 @@ class TestAzureBounce(CiTestCase):
def _dmi_mocks(key):
if key == 'system-uuid':
- return 'test-instance-id'
+ return 'D0DF4C54-4ECB-4A4B-9954-5BDF3ED5C3B8'
elif key == 'chassis-asset-tag':
return '7783-7084-3265-9085-8269-3286-77'
raise RuntimeError('should not get here')
@@ -1243,7 +1474,9 @@ class TestAzureBounce(CiTestCase):
self.assertEqual(initial_host_name,
self.set_hostname.call_args_list[-1][0][0])
- def test_environment_correct_for_bounce_command(self):
+ @mock.patch.object(dsaz, 'get_boot_telemetry')
+ def test_environment_correct_for_bounce_command(
+ self, mock_get_boot_telemetry):
interface = 'int0'
hostname = 'my-new-host'
old_hostname = 'my-old-host'
@@ -1259,7 +1492,9 @@ class TestAzureBounce(CiTestCase):
self.assertEqual(hostname, bounce_env['hostname'])
self.assertEqual(old_hostname, bounce_env['old_hostname'])
- def test_default_bounce_command_ifup_used_by_default(self):
+ @mock.patch.object(dsaz, 'get_boot_telemetry')
+ def test_default_bounce_command_ifup_used_by_default(
+ self, mock_get_boot_telemetry):
cfg = {'hostname_bounce': {'policy': 'force'}}
data = self.get_ovf_env_with_dscfg('some-hostname', cfg)
dsrc = self._get_ds(data, agent_command=['not', '__builtin__'])
@@ -1377,12 +1612,15 @@ class TestCanDevBeReformatted(CiTestCase):
self._domock(p + "util.mount_cb", 'm_mount_cb')
self._domock(p + "os.path.realpath", 'm_realpath')
self._domock(p + "os.path.exists", 'm_exists')
+ self._domock(p + "util.SeLinuxGuard", 'm_selguard')
self.m_exists.side_effect = lambda p: p in bypath
self.m_realpath.side_effect = realpath
self.m_has_ntfs_filesystem.side_effect = has_ntfs_fs
self.m_mount_cb.side_effect = mount_cb
self.m_partitions_on_device.side_effect = partitions_on_device
+ self.m_selguard.__enter__ = mock.Mock(return_value=False)
+ self.m_selguard.__exit__ = mock.Mock()
def test_three_partitions_is_false(self):
"""A disk with 3 partitions can not be formatted."""
@@ -1692,6 +1930,7 @@ class TestPreprovisioningPollIMDS(CiTestCase):
self.paths = helpers.Paths({'cloud_dir': self.tmp})
dsaz.BUILTIN_DS_CONFIG['data_dir'] = self.waagent_d
+ @mock.patch('time.sleep', mock.MagicMock())
@mock.patch(MOCKPATH + 'EphemeralDHCPv4')
def test_poll_imds_re_dhcp_on_timeout(self, m_dhcpv4, report_ready_func,
fake_resp, m_media_switch, m_dhcp,
@@ -1789,12 +2028,14 @@ class TestAzureDataSourcePreprovisioning(CiTestCase):
headers={'Metadata': 'true',
'User-Agent':
'Cloud-Init/%s' % vs()
- }, method='GET', timeout=1,
+ }, method='GET',
+ timeout=dsaz.IMDS_TIMEOUT_IN_SECONDS,
url=full_url)])
self.assertEqual(m_dhcp.call_count, 2)
m_net.assert_any_call(
broadcast='192.168.2.255', interface='eth9', ip='192.168.2.9',
- prefix_or_mask='255.255.255.0', router='192.168.2.1')
+ prefix_or_mask='255.255.255.0', router='192.168.2.1',
+ static_routes=None)
self.assertEqual(m_net.call_count, 2)
def test__reprovision_calls__poll_imds(self, fake_resp,
@@ -1826,11 +2067,14 @@ class TestAzureDataSourcePreprovisioning(CiTestCase):
headers={'Metadata': 'true',
'User-Agent':
'Cloud-Init/%s' % vs()},
- method='GET', timeout=1, url=full_url)])
+ method='GET',
+ timeout=dsaz.IMDS_TIMEOUT_IN_SECONDS,
+ url=full_url)])
self.assertEqual(m_dhcp.call_count, 2)
m_net.assert_any_call(
broadcast='192.168.2.255', interface='eth9', ip='192.168.2.9',
- prefix_or_mask='255.255.255.0', router='192.168.2.1')
+ prefix_or_mask='255.255.255.0', router='192.168.2.1',
+ static_routes=None)
self.assertEqual(m_net.call_count, 2)
@@ -1924,4 +2168,24 @@ class TestWBIsPlatformViable(CiTestCase):
self.logs.getvalue())
+class TestRandomSeed(CiTestCase):
+ """Test proper handling of random_seed"""
+
+ def test_non_ascii_seed_is_serializable(self):
+ """Pass if a random string from the Azure infrastructure which
+ contains at least one non-Unicode character can be converted to/from
+ JSON without alteration and without throwing an exception.
+ """
+ path = resourceLocation("azure/non_unicode_random_string")
+ result = dsaz._get_random_seed(path)
+
+ obj = {'seed': result}
+ try:
+ serialized = json_dumps(obj)
+ deserialized = load_json(serialized)
+ except UnicodeDecodeError:
+ self.fail("Non-serializable random seed returned")
+
+ self.assertEqual(deserialized['seed'], result)
+
# vi: ts=4 expandtab
diff --git a/tests/unittests/test_datasource/test_azure_helper.py b/tests/unittests/test_datasource/test_azure_helper.py
index 26b2b93d..007df09f 100644
--- a/tests/unittests/test_datasource/test_azure_helper.py
+++ b/tests/unittests/test_datasource/test_azure_helper.py
@@ -1,11 +1,13 @@
# This file is part of cloud-init. See LICENSE file for license information.
import os
+import unittest2
from textwrap import dedent
from cloudinit.sources.helpers import azure as azure_helper
from cloudinit.tests.helpers import CiTestCase, ExitStack, mock, populate_dir
+from cloudinit.util import load_file
from cloudinit.sources.helpers.azure import WALinuxAgentShim as wa_shim
GOAL_STATE_TEMPLATE = """\
@@ -65,12 +67,17 @@ class TestFindEndpoint(CiTestCase):
self.networkd_leases.return_value = None
def test_missing_file(self):
- self.assertRaises(ValueError, wa_shim.find_endpoint)
+ """wa_shim find_endpoint uses default endpoint if leasefile not found
+ """
+ self.assertEqual(wa_shim.find_endpoint(), "168.63.129.16")
def test_missing_special_azure_line(self):
+ """wa_shim find_endpoint uses default endpoint if leasefile is found
+ but does not contain DHCP Option 245 (whose value is the endpoint)
+ """
self.load_file.return_value = ''
self.dhcp_options.return_value = {'eth0': {'key': 'value'}}
- self.assertRaises(ValueError, wa_shim.find_endpoint)
+ self.assertEqual(wa_shim.find_endpoint(), "168.63.129.16")
@staticmethod
def _build_lease_content(encoded_address):
@@ -163,6 +170,25 @@ class TestGoalStateParsing(CiTestCase):
goal_state = self._get_goal_state(instance_id=instance_id)
self.assertEqual(instance_id, goal_state.instance_id)
+ def test_instance_id_byte_swap(self):
+ """Return true when previous_iid is byteswapped current_iid"""
+ previous_iid = "D0DF4C54-4ECB-4A4B-9954-5BDF3ED5C3B8"
+ current_iid = "544CDFD0-CB4E-4B4A-9954-5BDF3ED5C3B8"
+ self.assertTrue(
+ azure_helper.is_byte_swapped(previous_iid, current_iid))
+
+ def test_instance_id_no_byte_swap_same_instance_id(self):
+ previous_iid = "D0DF4C54-4ECB-4A4B-9954-5BDF3ED5C3B8"
+ current_iid = "D0DF4C54-4ECB-4A4B-9954-5BDF3ED5C3B8"
+ self.assertFalse(
+ azure_helper.is_byte_swapped(previous_iid, current_iid))
+
+ def test_instance_id_no_byte_swap_diff_instance_id(self):
+ previous_iid = "D0DF4C54-4ECB-4A4B-9954-5BDF3ED5C3B8"
+ current_iid = "G0DF4C54-4ECB-4A4B-9954-5BDF3ED5C3B8"
+ self.assertFalse(
+ azure_helper.is_byte_swapped(previous_iid, current_iid))
+
def test_certificates_xml_parsed_and_fetched_correctly(self):
http_client = mock.MagicMock()
certificates_url = 'TestCertificatesUrl'
@@ -205,8 +231,10 @@ class TestAzureEndpointHttpClient(CiTestCase):
response = client.get(url, secure=False)
self.assertEqual(1, self.read_file_or_url.call_count)
self.assertEqual(self.read_file_or_url.return_value, response)
- self.assertEqual(mock.call(url, headers=self.regular_headers),
- self.read_file_or_url.call_args)
+ self.assertEqual(
+ mock.call(url, headers=self.regular_headers, retries=10,
+ timeout=5),
+ self.read_file_or_url.call_args)
def test_secure_get(self):
url = 'MyTestUrl'
@@ -220,8 +248,10 @@ class TestAzureEndpointHttpClient(CiTestCase):
response = client.get(url, secure=True)
self.assertEqual(1, self.read_file_or_url.call_count)
self.assertEqual(self.read_file_or_url.return_value, response)
- self.assertEqual(mock.call(url, headers=expected_headers),
- self.read_file_or_url.call_args)
+ self.assertEqual(
+ mock.call(url, headers=expected_headers, retries=10,
+ timeout=5),
+ self.read_file_or_url.call_args)
def test_post(self):
data = mock.MagicMock()
@@ -231,7 +261,8 @@ class TestAzureEndpointHttpClient(CiTestCase):
self.assertEqual(1, self.read_file_or_url.call_count)
self.assertEqual(self.read_file_or_url.return_value, response)
self.assertEqual(
- mock.call(url, data=data, headers=self.regular_headers),
+ mock.call(url, data=data, headers=self.regular_headers, retries=10,
+ timeout=5),
self.read_file_or_url.call_args)
def test_post_with_extra_headers(self):
@@ -243,7 +274,8 @@ class TestAzureEndpointHttpClient(CiTestCase):
expected_headers = self.regular_headers.copy()
expected_headers.update(extra_headers)
self.assertEqual(
- mock.call(mock.ANY, data=mock.ANY, headers=expected_headers),
+ mock.call(mock.ANY, data=mock.ANY, headers=expected_headers,
+ retries=10, timeout=5),
self.read_file_or_url.call_args)
@@ -289,6 +321,50 @@ class TestOpenSSLManager(CiTestCase):
self.assertEqual([mock.call(manager.tmpdir)], del_dir.call_args_list)
+class TestOpenSSLManagerActions(CiTestCase):
+
+ def setUp(self):
+ super(TestOpenSSLManagerActions, self).setUp()
+
+ self.allowed_subp = True
+
+ def _data_file(self, name):
+ path = 'tests/data/azure'
+ return os.path.join(path, name)
+
+ @unittest2.skip("todo move to cloud_test")
+ def test_pubkey_extract(self):
+ cert = load_file(self._data_file('pubkey_extract_cert'))
+ good_key = load_file(self._data_file('pubkey_extract_ssh_key'))
+ sslmgr = azure_helper.OpenSSLManager()
+ key = sslmgr._get_ssh_key_from_cert(cert)
+ self.assertEqual(good_key, key)
+
+ good_fingerprint = '073E19D14D1C799224C6A0FD8DDAB6A8BF27D473'
+ fingerprint = sslmgr._get_fingerprint_from_cert(cert)
+ self.assertEqual(good_fingerprint, fingerprint)
+
+ @unittest2.skip("todo move to cloud_test")
+ @mock.patch.object(azure_helper.OpenSSLManager, '_decrypt_certs_from_xml')
+ def test_parse_certificates(self, mock_decrypt_certs):
+ """Azure control plane puts private keys as well as certificates
+ into the Certificates XML object. Make sure only the public keys
+ from certs are extracted and that fingerprints are converted to
+ the form specified in the ovf-env.xml file.
+ """
+ cert_contents = load_file(self._data_file('parse_certificates_pem'))
+ fingerprints = load_file(self._data_file(
+ 'parse_certificates_fingerprints')
+ ).splitlines()
+ mock_decrypt_certs.return_value = cert_contents
+ sslmgr = azure_helper.OpenSSLManager()
+ keys_by_fp = sslmgr.parse_certificates('')
+ for fp in keys_by_fp.keys():
+ self.assertIn(fp, fingerprints)
+ for fp in fingerprints:
+ self.assertIn(fp, keys_by_fp)
+
+
class TestWALinuxAgentShim(CiTestCase):
def setUp(self):
@@ -329,18 +405,31 @@ class TestWALinuxAgentShim(CiTestCase):
def test_certificates_used_to_determine_public_keys(self):
shim = wa_shim()
- data = shim.register_with_azure_and_fetch_data()
+ """if register_with_azure_and_fetch_data() isn't passed some info about
+ the user's public keys, there's no point in even trying to parse
+ the certificates
+ """
+ mypk = [{'fingerprint': 'fp1', 'path': 'path1'},
+ {'fingerprint': 'fp3', 'path': 'path3', 'value': ''}]
+ certs = {'fp1': 'expected-key',
+ 'fp2': 'should-not-be-found',
+ 'fp3': 'expected-no-value-key',
+ }
+ sslmgr = self.OpenSSLManager.return_value
+ sslmgr.parse_certificates.return_value = certs
+ data = shim.register_with_azure_and_fetch_data(pubkey_info=mypk)
self.assertEqual(
[mock.call(self.GoalState.return_value.certificates_xml)],
- self.OpenSSLManager.return_value.parse_certificates.call_args_list)
- self.assertEqual(
- self.OpenSSLManager.return_value.parse_certificates.return_value,
- data['public-keys'])
+ sslmgr.parse_certificates.call_args_list)
+ self.assertIn('expected-key', data['public-keys'])
+ self.assertIn('expected-no-value-key', data['public-keys'])
+ self.assertNotIn('should-not-be-found', data['public-keys'])
def test_absent_certificates_produces_empty_public_keys(self):
+ mypk = [{'fingerprint': 'fp1', 'path': 'path1'}]
self.GoalState.return_value.certificates_xml = None
shim = wa_shim()
- data = shim.register_with_azure_and_fetch_data()
+ data = shim.register_with_azure_and_fetch_data(pubkey_info=mypk)
self.assertEqual([], data['public-keys'])
def test_correct_url_used_for_report_ready(self):
diff --git a/tests/unittests/test_datasource/test_cloudsigma.py b/tests/unittests/test_datasource/test_cloudsigma.py
index 3bf52e69..d62d542b 100644
--- a/tests/unittests/test_datasource/test_cloudsigma.py
+++ b/tests/unittests/test_datasource/test_cloudsigma.py
@@ -30,6 +30,8 @@ SERVER_CONTEXT = {
}
}
+DS_PATH = 'cloudinit.sources.DataSourceCloudSigma.DataSourceCloudSigma'
+
class CepkoMock(Cepko):
def __init__(self, mocked_context):
@@ -42,17 +44,15 @@ class CepkoMock(Cepko):
class DataSourceCloudSigmaTest(test_helpers.CiTestCase):
def setUp(self):
super(DataSourceCloudSigmaTest, self).setUp()
- self.add_patch(
- "cloudinit.sources.DataSourceCloudSigma.util.is_container",
- "m_is_container", return_value=False)
self.paths = helpers.Paths({'run_dir': self.tmp_dir()})
+ self.add_patch(DS_PATH + '.is_running_in_cloudsigma',
+ "m_is_container", return_value=True)
self.datasource = DataSourceCloudSigma.DataSourceCloudSigma(
"", "", paths=self.paths)
- self.datasource.is_running_in_cloudsigma = lambda: True
self.datasource.cepko = CepkoMock(SERVER_CONTEXT)
- self.datasource.get_data()
def test_get_hostname(self):
+ self.datasource.get_data()
self.assertEqual("test_server", self.datasource.get_hostname())
self.datasource.metadata['name'] = ''
self.assertEqual("65b2fb23", self.datasource.get_hostname())
@@ -61,23 +61,28 @@ class DataSourceCloudSigmaTest(test_helpers.CiTestCase):
self.assertEqual("65b2fb23", self.datasource.get_hostname())
def test_get_public_ssh_keys(self):
+ self.datasource.get_data()
self.assertEqual([SERVER_CONTEXT['meta']['ssh_public_key']],
self.datasource.get_public_ssh_keys())
def test_get_instance_id(self):
+ self.datasource.get_data()
self.assertEqual(SERVER_CONTEXT['uuid'],
self.datasource.get_instance_id())
def test_platform(self):
"""All platform-related attributes are set."""
+ self.datasource.get_data()
self.assertEqual(self.datasource.cloud_name, 'cloudsigma')
self.assertEqual(self.datasource.platform_type, 'cloudsigma')
self.assertEqual(self.datasource.subplatform, 'cepko (/dev/ttyS1)')
def test_metadata(self):
+ self.datasource.get_data()
self.assertEqual(self.datasource.metadata, SERVER_CONTEXT)
def test_user_data(self):
+ self.datasource.get_data()
self.assertEqual(self.datasource.userdata_raw,
SERVER_CONTEXT['meta']['cloudinit-user-data'])
@@ -91,14 +96,13 @@ class DataSourceCloudSigmaTest(test_helpers.CiTestCase):
self.assertEqual(self.datasource.userdata_raw, b'hi world\n')
def test_vendor_data(self):
+ self.datasource.get_data()
self.assertEqual(self.datasource.vendordata_raw,
SERVER_CONTEXT['vendor_data']['cloudinit'])
def test_lack_of_vendor_data(self):
stripped_context = copy.deepcopy(SERVER_CONTEXT)
del stripped_context["vendor_data"]
- self.datasource = DataSourceCloudSigma.DataSourceCloudSigma(
- "", "", paths=self.paths)
self.datasource.cepko = CepkoMock(stripped_context)
self.datasource.get_data()
@@ -107,8 +111,6 @@ class DataSourceCloudSigmaTest(test_helpers.CiTestCase):
def test_lack_of_cloudinit_key_in_vendor_data(self):
stripped_context = copy.deepcopy(SERVER_CONTEXT)
del stripped_context["vendor_data"]["cloudinit"]
- self.datasource = DataSourceCloudSigma.DataSourceCloudSigma(
- "", "", paths=self.paths)
self.datasource.cepko = CepkoMock(stripped_context)
self.datasource.get_data()
diff --git a/tests/unittests/test_datasource/test_cloudstack.py b/tests/unittests/test_datasource/test_cloudstack.py
index d6d2d6b2..83c2f753 100644
--- a/tests/unittests/test_datasource/test_cloudstack.py
+++ b/tests/unittests/test_datasource/test_cloudstack.py
@@ -10,6 +10,9 @@ from cloudinit.tests.helpers import CiTestCase, ExitStack, mock
import os
import time
+MOD_PATH = 'cloudinit.sources.DataSourceCloudStack'
+DS_PATH = MOD_PATH + '.DataSourceCloudStack'
+
class TestCloudStackPasswordFetching(CiTestCase):
@@ -17,7 +20,7 @@ class TestCloudStackPasswordFetching(CiTestCase):
super(TestCloudStackPasswordFetching, self).setUp()
self.patches = ExitStack()
self.addCleanup(self.patches.close)
- mod_name = 'cloudinit.sources.DataSourceCloudStack'
+ mod_name = MOD_PATH
self.patches.enter_context(mock.patch('{0}.ec2'.format(mod_name)))
self.patches.enter_context(mock.patch('{0}.uhelp'.format(mod_name)))
default_gw = "192.201.20.0"
@@ -56,7 +59,9 @@ class TestCloudStackPasswordFetching(CiTestCase):
ds.get_data()
self.assertEqual({}, ds.get_config_obj())
- def test_password_sets_password(self):
+ @mock.patch(DS_PATH + '.wait_for_metadata_service')
+ def test_password_sets_password(self, m_wait):
+ m_wait.return_value = True
password = 'SekritSquirrel'
self._set_password_server_response(password)
ds = DataSourceCloudStack(
@@ -64,7 +69,9 @@ class TestCloudStackPasswordFetching(CiTestCase):
ds.get_data()
self.assertEqual(password, ds.get_config_obj()['password'])
- def test_bad_request_doesnt_stop_ds_from_working(self):
+ @mock.patch(DS_PATH + '.wait_for_metadata_service')
+ def test_bad_request_doesnt_stop_ds_from_working(self, m_wait):
+ m_wait.return_value = True
self._set_password_server_response('bad_request')
ds = DataSourceCloudStack(
{}, None, helpers.Paths({'run_dir': self.tmp}))
@@ -79,7 +86,9 @@ class TestCloudStackPasswordFetching(CiTestCase):
request_types.append(arg.split()[1])
self.assertEqual(expected_request_types, request_types)
- def test_valid_response_means_password_marked_as_saved(self):
+ @mock.patch(DS_PATH + '.wait_for_metadata_service')
+ def test_valid_response_means_password_marked_as_saved(self, m_wait):
+ m_wait.return_value = True
password = 'SekritSquirrel'
subp = self._set_password_server_response(password)
ds = DataSourceCloudStack(
@@ -92,7 +101,9 @@ class TestCloudStackPasswordFetching(CiTestCase):
subp = self._set_password_server_response(response_string)
ds = DataSourceCloudStack(
{}, None, helpers.Paths({'run_dir': self.tmp}))
- ds.get_data()
+ with mock.patch(DS_PATH + '.wait_for_metadata_service') as m_wait:
+ m_wait.return_value = True
+ ds.get_data()
self.assertRequestTypesSent(subp, ['send_my_password'])
def test_password_not_saved_if_empty(self):
diff --git a/tests/unittests/test_datasource/test_common.py b/tests/unittests/test_datasource/test_common.py
index 6b01a4ea..4ab5d471 100644
--- a/tests/unittests/test_datasource/test_common.py
+++ b/tests/unittests/test_datasource/test_common.py
@@ -4,6 +4,7 @@ from cloudinit import settings
from cloudinit import sources
from cloudinit import type_utils
from cloudinit.sources import (
+ DataSource,
DataSourceAliYun as AliYun,
DataSourceAltCloud as AltCloud,
DataSourceAzure as Azure,
@@ -13,6 +14,7 @@ from cloudinit.sources import (
DataSourceConfigDrive as ConfigDrive,
DataSourceDigitalOcean as DigitalOcean,
DataSourceEc2 as Ec2,
+ DataSourceExoscale as Exoscale,
DataSourceGCE as GCE,
DataSourceHetzner as Hetzner,
DataSourceIBMCloud as IBMCloud,
@@ -22,6 +24,7 @@ from cloudinit.sources import (
DataSourceOpenStack as OpenStack,
DataSourceOracle as Oracle,
DataSourceOVF as OVF,
+ DataSourceRbxCloud as RbxCloud,
DataSourceScaleway as Scaleway,
DataSourceSmartOS as SmartOS,
)
@@ -43,6 +46,7 @@ DEFAULT_LOCAL = [
SmartOS.DataSourceSmartOS,
Ec2.DataSourceEc2Local,
OpenStack.DataSourceOpenStackLocal,
+ RbxCloud.DataSourceRbxCloud,
Scaleway.DataSourceScaleway,
]
@@ -53,6 +57,7 @@ DEFAULT_NETWORK = [
CloudStack.DataSourceCloudStack,
DSNone.DataSourceNone,
Ec2.DataSourceEc2,
+ Exoscale.DataSourceExoscale,
GCE.DataSourceGCE,
MAAS.DataSourceMAAS,
NoCloud.DataSourceNoCloudNet,
@@ -83,4 +88,23 @@ class ExpectedDataSources(test_helpers.TestCase):
self.assertEqual(set([AliYun.DataSourceAliYun]), set(found))
+class TestDataSourceInvariants(test_helpers.TestCase):
+ def test_data_sources_have_valid_network_config_sources(self):
+ for ds in DEFAULT_LOCAL + DEFAULT_NETWORK:
+ for cfg_src in ds.network_config_sources:
+ fail_msg = ('{} has an invalid network_config_sources entry:'
+ ' {}'.format(str(ds), cfg_src))
+ self.assertTrue(hasattr(sources.NetworkConfigSource, cfg_src),
+ fail_msg)
+
+ def test_expected_dsname_defined(self):
+ for ds in DEFAULT_LOCAL + DEFAULT_NETWORK:
+ fail_msg = (
+ '{} has an invalid / missing dsname property: {}'.format(
+ str(ds), str(ds.dsname)
+ )
+ )
+ self.assertNotEqual(ds.dsname, DataSource.dsname, fail_msg)
+ self.assertIsNotNone(ds.dsname)
+
# vi: ts=4 expandtab
diff --git a/tests/unittests/test_datasource/test_configdrive.py b/tests/unittests/test_datasource/test_configdrive.py
index dcdabea5..6f830cc6 100644
--- a/tests/unittests/test_datasource/test_configdrive.py
+++ b/tests/unittests/test_datasource/test_configdrive.py
@@ -220,13 +220,15 @@ CFG_DRIVE_FILES_V2 = {
'openstack/2015-10-15/user_data': USER_DATA,
'openstack/2015-10-15/network_data.json': json.dumps(NETWORK_DATA)}
+M_PATH = "cloudinit.sources.DataSourceConfigDrive."
+
class TestConfigDriveDataSource(CiTestCase):
def setUp(self):
super(TestConfigDriveDataSource, self).setUp()
self.add_patch(
- "cloudinit.sources.DataSourceConfigDrive.util.find_devs_with",
+ M_PATH + "util.find_devs_with",
"m_find_devs_with", return_value=[])
self.tmp = self.tmp_dir()
@@ -268,8 +270,7 @@ class TestConfigDriveDataSource(CiTestCase):
exists_mock = mocks.enter_context(
mock.patch.object(os.path, 'exists',
side_effect=exists_side_effect()))
- device = cfg_ds.device_name_to_device(name)
- self.assertEqual(dev_name, device)
+ self.assertEqual(dev_name, cfg_ds.device_name_to_device(name))
find_mock.assert_called_once_with(mock.ANY)
self.assertEqual(exists_mock.call_count, 2)
@@ -296,8 +297,7 @@ class TestConfigDriveDataSource(CiTestCase):
exists_mock = mocks.enter_context(
mock.patch.object(os.path, 'exists',
return_value=True))
- device = cfg_ds.device_name_to_device(name)
- self.assertEqual(dev_name, device)
+ self.assertEqual(dev_name, cfg_ds.device_name_to_device(name))
find_mock.assert_called_once_with(mock.ANY)
exists_mock.assert_called_once_with(mock.ANY)
@@ -331,8 +331,7 @@ class TestConfigDriveDataSource(CiTestCase):
yield True
with mock.patch.object(os.path, 'exists',
side_effect=exists_side_effect()):
- device = cfg_ds.device_name_to_device(name)
- self.assertEqual(dev_name, device)
+ self.assertEqual(dev_name, cfg_ds.device_name_to_device(name))
# We don't assert the call count for os.path.exists() because
# not all of the entries in name_tests results in two calls to
# that function. Specifically, 'root2k' doesn't seem to call
@@ -359,8 +358,7 @@ class TestConfigDriveDataSource(CiTestCase):
}
for name, dev_name in name_tests.items():
with mock.patch.object(os.path, 'exists', return_value=True):
- device = cfg_ds.device_name_to_device(name)
- self.assertEqual(dev_name, device)
+ self.assertEqual(dev_name, cfg_ds.device_name_to_device(name))
def test_dir_valid(self):
"""Verify a dir is read as such."""
@@ -472,7 +470,7 @@ class TestConfigDriveDataSource(CiTestCase):
util.find_devs_with = orig_find_devs_with
util.is_partition = orig_is_partition
- @mock.patch('cloudinit.sources.DataSourceConfigDrive.on_first_boot')
+ @mock.patch(M_PATH + 'on_first_boot')
def test_pubkeys_v2(self, on_first_boot):
"""Verify that public-keys work in config-drive-v2."""
myds = cfg_ds_from_dir(self.tmp, files=CFG_DRIVE_FILES_V2)
@@ -482,6 +480,19 @@ class TestConfigDriveDataSource(CiTestCase):
self.assertEqual('openstack', myds.platform)
self.assertEqual('seed-dir (%s/seed)' % self.tmp, myds.subplatform)
+ def test_subplatform_config_drive_when_starts_with_dev(self):
+ """subplatform reports config-drive when source starts with /dev/."""
+ cfg_ds = ds.DataSourceConfigDrive(settings.CFG_BUILTIN,
+ None,
+ helpers.Paths({}))
+ with mock.patch(M_PATH + 'find_candidate_devs') as m_find_devs:
+ with mock.patch(M_PATH + 'util.is_FreeBSD', return_value=False):
+ with mock.patch(M_PATH + 'util.mount_cb'):
+ with mock.patch(M_PATH + 'on_first_boot'):
+ m_find_devs.return_value = ['/dev/anything']
+ self.assertEqual(True, cfg_ds.get_data())
+ self.assertEqual('config-disk (/dev/anything)', cfg_ds.subplatform)
+
class TestNetJson(CiTestCase):
def setUp(self):
@@ -489,13 +500,13 @@ class TestNetJson(CiTestCase):
self.tmp = self.tmp_dir()
self.maxDiff = None
- @mock.patch('cloudinit.sources.DataSourceConfigDrive.on_first_boot')
+ @mock.patch(M_PATH + 'on_first_boot')
def test_network_data_is_found(self, on_first_boot):
"""Verify that network_data is present in ds in config-drive-v2."""
myds = cfg_ds_from_dir(self.tmp, files=CFG_DRIVE_FILES_V2)
self.assertIsNotNone(myds.network_json)
- @mock.patch('cloudinit.sources.DataSourceConfigDrive.on_first_boot')
+ @mock.patch(M_PATH + 'on_first_boot')
def test_network_config_is_converted(self, on_first_boot):
"""Verify that network_data is converted and present on ds object."""
myds = cfg_ds_from_dir(self.tmp, files=CFG_DRIVE_FILES_V2)
@@ -503,6 +514,46 @@ class TestNetJson(CiTestCase):
known_macs=KNOWN_MACS)
self.assertEqual(myds.network_config, network_config)
+ def test_network_config_conversion_dhcp6(self):
+ """Test some ipv6 input network json and check the expected
+ conversions."""
+ in_data = {
+ 'links': [
+ {'vif_id': '2ecc7709-b3f7-4448-9580-e1ec32d75bbd',
+ 'ethernet_mac_address': 'fa:16:3e:69:b0:58',
+ 'type': 'ovs', 'mtu': None, 'id': 'tap2ecc7709-b3'},
+ {'vif_id': '2f88d109-5b57-40e6-af32-2472df09dc33',
+ 'ethernet_mac_address': 'fa:16:3e:d4:57:ad',
+ 'type': 'ovs', 'mtu': None, 'id': 'tap2f88d109-5b'},
+ ],
+ 'networks': [
+ {'link': 'tap2ecc7709-b3', 'type': 'ipv6_dhcpv6-stateless',
+ 'network_id': '6d6357ac-0f70-4afa-8bd7-c274cc4ea235',
+ 'id': 'network0'},
+ {'link': 'tap2f88d109-5b', 'type': 'ipv6_dhcpv6-stateful',
+ 'network_id': 'd227a9b3-6960-4d94-8976-ee5788b44f54',
+ 'id': 'network1'},
+ ]
+ }
+ out_data = {
+ 'version': 1,
+ 'config': [
+ {'mac_address': 'fa:16:3e:69:b0:58',
+ 'mtu': None,
+ 'name': 'enp0s1',
+ 'subnets': [{'type': 'ipv6_dhcpv6-stateless'}],
+ 'type': 'physical'},
+ {'mac_address': 'fa:16:3e:d4:57:ad',
+ 'mtu': None,
+ 'name': 'enp0s2',
+ 'subnets': [{'type': 'ipv6_dhcpv6-stateful'}],
+ 'type': 'physical',
+ 'accept-ra': True}
+ ],
+ }
+ conv_data = openstack.convert_net_json(in_data, known_macs=KNOWN_MACS)
+ self.assertEqual(out_data, conv_data)
+
def test_network_config_conversions(self):
"""Tests a bunch of input network json and checks the
expected conversions."""
@@ -604,6 +655,9 @@ class TestNetJson(CiTestCase):
class TestConvertNetworkData(CiTestCase):
+
+ with_logs = True
+
def setUp(self):
super(TestConvertNetworkData, self).setUp()
self.tmp = self.tmp_dir()
@@ -730,6 +784,26 @@ class TestConvertNetworkData(CiTestCase):
'enp0s2': 'fa:16:3e:d4:57:ad'}
self.assertEqual(expected, config_name2mac)
+ def test_unknown_device_types_accepted(self):
+ # If we don't recognise a link, we should treat it as physical for a
+ # best-effort boot
+ my_netdata = deepcopy(NETWORK_DATA)
+ my_netdata['links'][0]['type'] = 'my-special-link-type'
+
+ ncfg = openstack.convert_net_json(my_netdata, known_macs=KNOWN_MACS)
+ config_name2mac = {}
+ for n in ncfg['config']:
+ if n['type'] == 'physical':
+ config_name2mac[n['name']] = n['mac_address']
+
+ expected = {'nic0': 'fa:16:3e:05:30:fe', 'enp0s1': 'fa:16:3e:69:b0:58',
+ 'enp0s2': 'fa:16:3e:d4:57:ad'}
+ self.assertEqual(expected, config_name2mac)
+
+ # We should, however, warn the user that we don't recognise the type
+ self.assertIn('Unknown network_data link type (my-special-link-type)',
+ self.logs.getvalue())
+
def cfg_ds_from_dir(base_d, files=None):
run = os.path.join(base_d, "run")
diff --git a/tests/unittests/test_datasource/test_ec2.py b/tests/unittests/test_datasource/test_ec2.py
index 1a5956d9..2a96122f 100644
--- a/tests/unittests/test_datasource/test_ec2.py
+++ b/tests/unittests/test_datasource/test_ec2.py
@@ -3,7 +3,7 @@
import copy
import httpretty
import json
-import mock
+from unittest import mock
from cloudinit import helpers
from cloudinit.sources import DataSourceEc2 as ec2
@@ -191,7 +191,9 @@ def register_mock_metaserver(base_url, data):
register(base_url, 'not found', status=404)
def myreg(*argc, **kwargs):
- return httpretty.register_uri(httpretty.GET, *argc, **kwargs)
+ url = argc[0]
+ method = httpretty.PUT if ec2.API_TOKEN_ROUTE in url else httpretty.GET
+ return httpretty.register_uri(method, *argc, **kwargs)
register_helper(myreg, base_url, data)
@@ -237,6 +239,8 @@ class TestEc2(test_helpers.HttprettyTestCase):
if md:
all_versions = (
[ds.min_metadata_version] + ds.extended_metadata_versions)
+ token_url = self.data_url('latest', data_item='api/token')
+ register_mock_metaserver(token_url, 'API-TOKEN')
for version in all_versions:
metadata_url = self.data_url(version) + '/'
if version == md_version:
@@ -401,6 +405,47 @@ class TestEc2(test_helpers.HttprettyTestCase):
ds.metadata = DEFAULT_METADATA
self.assertEqual('my-identity-id', ds.get_instance_id())
+ def test_classic_instance_true(self):
+ """If no vpc-id in metadata, is_classic_instance must return true."""
+ md_copy = copy.deepcopy(DEFAULT_METADATA)
+ ifaces_md = md_copy.get('network', {}).get('interfaces', {})
+ for _mac, mac_data in ifaces_md.get('macs', {}).items():
+ if 'vpc-id' in mac_data:
+ del mac_data['vpc-id']
+
+ ds = self._setup_ds(
+ platform_data=self.valid_platform_data,
+ sys_cfg={'datasource': {'Ec2': {'strict_id': False}}},
+ md={'md': md_copy})
+ self.assertTrue(ds.get_data())
+ self.assertTrue(ds.is_classic_instance())
+
+ def test_classic_instance_false(self):
+ """If vpc-id in metadata, is_classic_instance must return false."""
+ ds = self._setup_ds(
+ platform_data=self.valid_platform_data,
+ sys_cfg={'datasource': {'Ec2': {'strict_id': False}}},
+ md={'md': DEFAULT_METADATA})
+ self.assertTrue(ds.get_data())
+ self.assertFalse(ds.is_classic_instance())
+
+ def test_aws_token_redacted(self):
+ """Verify that aws tokens are redacted when logged."""
+ ds = self._setup_ds(
+ platform_data=self.valid_platform_data,
+ sys_cfg={'datasource': {'Ec2': {'strict_id': False}}},
+ md={'md': DEFAULT_METADATA})
+ self.assertTrue(ds.get_data())
+ all_logs = self.logs.getvalue().splitlines()
+ REDACT_TTL = "'X-aws-ec2-metadata-token-ttl-seconds': 'REDACTED'"
+ REDACT_TOK = "'X-aws-ec2-metadata-token': 'REDACTED'"
+ logs_with_redacted_ttl = [log for log in all_logs if REDACT_TTL in log]
+ logs_with_redacted = [log for log in all_logs if REDACT_TOK in log]
+ logs_with_token = [log for log in all_logs if 'API-TOKEN' in log]
+ self.assertEqual(1, len(logs_with_redacted_ttl))
+ self.assertEqual(79, len(logs_with_redacted))
+ self.assertEqual(0, len(logs_with_token))
+
@mock.patch('cloudinit.net.dhcp.maybe_perform_dhcp_discovery')
def test_valid_platform_with_strict_true(self, m_dhcp):
"""Valid platform data should return true with strict_id true."""
@@ -514,7 +559,8 @@ class TestEc2(test_helpers.HttprettyTestCase):
m_dhcp.assert_called_once_with('eth9')
m_net.assert_called_once_with(
broadcast='192.168.2.255', interface='eth9', ip='192.168.2.9',
- prefix_or_mask='255.255.255.0', router='192.168.2.1')
+ prefix_or_mask='255.255.255.0', router='192.168.2.1',
+ static_routes=None)
self.assertIn('Crawl of metadata service took', self.logs.getvalue())
@@ -637,4 +683,45 @@ class TestConvertEc2MetadataNetworkConfig(test_helpers.CiTestCase):
expected,
ec2.convert_ec2_metadata_network_config(self.network_metadata))
+
+class TesIdentifyPlatform(test_helpers.CiTestCase):
+
+ def collmock(self, **kwargs):
+ """return non-special _collect_platform_data updated with changes."""
+ unspecial = {
+ 'asset_tag': '3857-0037-2746-7462-1818-3997-77',
+ 'serial': 'H23-C4J3JV-R6',
+ 'uuid': '81c7e555-6471-4833-9551-1ab366c4cfd2',
+ 'uuid_source': 'dmi',
+ 'vendor': 'tothecloud',
+ }
+ unspecial.update(**kwargs)
+ return unspecial
+
+ @mock.patch('cloudinit.sources.DataSourceEc2._collect_platform_data')
+ def test_identify_zstack(self, m_collect):
+ """zstack should be identified if chassis-asset-tag ends in .zstack.io
+ """
+ m_collect.return_value = self.collmock(asset_tag='123456.zstack.io')
+ self.assertEqual(ec2.CloudNames.ZSTACK, ec2.identify_platform())
+
+ @mock.patch('cloudinit.sources.DataSourceEc2._collect_platform_data')
+ def test_identify_zstack_full_domain_only(self, m_collect):
+ """zstack asset-tag matching should match only on full domain boundary.
+ """
+ m_collect.return_value = self.collmock(asset_tag='123456.buzzstack.io')
+ self.assertEqual(ec2.CloudNames.UNKNOWN, ec2.identify_platform())
+
+ @mock.patch('cloudinit.sources.DataSourceEc2._collect_platform_data')
+ def test_identify_e24cloud(self, m_collect):
+ """e24cloud identified if vendor is e24cloud"""
+ m_collect.return_value = self.collmock(vendor='e24cloud')
+ self.assertEqual(ec2.CloudNames.E24CLOUD, ec2.identify_platform())
+
+ @mock.patch('cloudinit.sources.DataSourceEc2._collect_platform_data')
+ def test_identify_e24cloud_negative(self, m_collect):
+ """e24cloud identified if vendor is e24cloud"""
+ m_collect.return_value = self.collmock(vendor='e24cloudyday')
+ self.assertEqual(ec2.CloudNames.UNKNOWN, ec2.identify_platform())
+
# vi: ts=4 expandtab
diff --git a/tests/unittests/test_datasource/test_exoscale.py b/tests/unittests/test_datasource/test_exoscale.py
new file mode 100644
index 00000000..f0061199
--- /dev/null
+++ b/tests/unittests/test_datasource/test_exoscale.py
@@ -0,0 +1,211 @@
+# Author: Mathieu Corbin <mathieu.corbin@exoscale.com>
+# Author: Christopher Glass <christopher.glass@exoscale.com>
+#
+# This file is part of cloud-init. See LICENSE file for license information.
+from cloudinit import helpers
+from cloudinit.sources.DataSourceExoscale import (
+ API_VERSION,
+ DataSourceExoscale,
+ METADATA_URL,
+ get_password,
+ PASSWORD_SERVER_PORT,
+ read_metadata)
+from cloudinit.tests.helpers import HttprettyTestCase, mock
+from cloudinit import util
+
+import httpretty
+import os
+import requests
+
+
+TEST_PASSWORD_URL = "{}:{}/{}/".format(METADATA_URL,
+ PASSWORD_SERVER_PORT,
+ API_VERSION)
+
+TEST_METADATA_URL = "{}/{}/meta-data/".format(METADATA_URL,
+ API_VERSION)
+
+TEST_USERDATA_URL = "{}/{}/user-data".format(METADATA_URL,
+ API_VERSION)
+
+
+@httpretty.activate
+class TestDatasourceExoscale(HttprettyTestCase):
+
+ def setUp(self):
+ super(TestDatasourceExoscale, self).setUp()
+ self.tmp = self.tmp_dir()
+ self.password_url = TEST_PASSWORD_URL
+ self.metadata_url = TEST_METADATA_URL
+ self.userdata_url = TEST_USERDATA_URL
+
+ def test_password_saved(self):
+ """The password is not set when it is not found
+ in the metadata service."""
+ httpretty.register_uri(httpretty.GET,
+ self.password_url,
+ body="saved_password")
+ self.assertFalse(get_password())
+
+ def test_password_empty(self):
+ """No password is set if the metadata service returns
+ an empty string."""
+ httpretty.register_uri(httpretty.GET,
+ self.password_url,
+ body="")
+ self.assertFalse(get_password())
+
+ def test_password(self):
+ """The password is set to what is found in the metadata
+ service."""
+ expected_password = "p@ssw0rd"
+ httpretty.register_uri(httpretty.GET,
+ self.password_url,
+ body=expected_password)
+ password = get_password()
+ self.assertEqual(expected_password, password)
+
+ def test_activate_removes_set_passwords_semaphore(self):
+ """Allow set_passwords to run every boot by removing the semaphore."""
+ path = helpers.Paths({'cloud_dir': self.tmp})
+ sem_dir = self.tmp_path('instance/sem', dir=self.tmp)
+ util.ensure_dir(sem_dir)
+ sem_file = os.path.join(sem_dir, 'config_set_passwords')
+ with open(sem_file, 'w') as stream:
+ stream.write('')
+ ds = DataSourceExoscale({}, None, path)
+ ds.activate(None, None)
+ self.assertFalse(os.path.exists(sem_file))
+
+ def test_get_data(self):
+ """The datasource conforms to expected behavior when supplied
+ full test data."""
+ path = helpers.Paths({'run_dir': self.tmp})
+ ds = DataSourceExoscale({}, None, path)
+ ds._is_platform_viable = lambda: True
+ expected_password = "p@ssw0rd"
+ expected_id = "12345"
+ expected_hostname = "myname"
+ expected_userdata = "#cloud-config"
+ httpretty.register_uri(httpretty.GET,
+ self.userdata_url,
+ body=expected_userdata)
+ httpretty.register_uri(httpretty.GET,
+ self.password_url,
+ body=expected_password)
+ httpretty.register_uri(httpretty.GET,
+ self.metadata_url,
+ body="instance-id\nlocal-hostname")
+ httpretty.register_uri(httpretty.GET,
+ "{}local-hostname".format(self.metadata_url),
+ body=expected_hostname)
+ httpretty.register_uri(httpretty.GET,
+ "{}instance-id".format(self.metadata_url),
+ body=expected_id)
+ self.assertTrue(ds._get_data())
+ self.assertEqual(ds.userdata_raw.decode("utf-8"), "#cloud-config")
+ self.assertEqual(ds.metadata, {"instance-id": expected_id,
+ "local-hostname": expected_hostname})
+ self.assertEqual(ds.get_config_obj(),
+ {'ssh_pwauth': True,
+ 'password': expected_password,
+ 'chpasswd': {
+ 'expire': False,
+ }})
+
+ def test_get_data_saved_password(self):
+ """The datasource conforms to expected behavior when saved_password is
+ returned by the password server."""
+ path = helpers.Paths({'run_dir': self.tmp})
+ ds = DataSourceExoscale({}, None, path)
+ ds._is_platform_viable = lambda: True
+ expected_answer = "saved_password"
+ expected_id = "12345"
+ expected_hostname = "myname"
+ expected_userdata = "#cloud-config"
+ httpretty.register_uri(httpretty.GET,
+ self.userdata_url,
+ body=expected_userdata)
+ httpretty.register_uri(httpretty.GET,
+ self.password_url,
+ body=expected_answer)
+ httpretty.register_uri(httpretty.GET,
+ self.metadata_url,
+ body="instance-id\nlocal-hostname")
+ httpretty.register_uri(httpretty.GET,
+ "{}local-hostname".format(self.metadata_url),
+ body=expected_hostname)
+ httpretty.register_uri(httpretty.GET,
+ "{}instance-id".format(self.metadata_url),
+ body=expected_id)
+ self.assertTrue(ds._get_data())
+ self.assertEqual(ds.userdata_raw.decode("utf-8"), "#cloud-config")
+ self.assertEqual(ds.metadata, {"instance-id": expected_id,
+ "local-hostname": expected_hostname})
+ self.assertEqual(ds.get_config_obj(), {})
+
+ def test_get_data_no_password(self):
+ """The datasource conforms to expected behavior when no password is
+ returned by the password server."""
+ path = helpers.Paths({'run_dir': self.tmp})
+ ds = DataSourceExoscale({}, None, path)
+ ds._is_platform_viable = lambda: True
+ expected_answer = ""
+ expected_id = "12345"
+ expected_hostname = "myname"
+ expected_userdata = "#cloud-config"
+ httpretty.register_uri(httpretty.GET,
+ self.userdata_url,
+ body=expected_userdata)
+ httpretty.register_uri(httpretty.GET,
+ self.password_url,
+ body=expected_answer)
+ httpretty.register_uri(httpretty.GET,
+ self.metadata_url,
+ body="instance-id\nlocal-hostname")
+ httpretty.register_uri(httpretty.GET,
+ "{}local-hostname".format(self.metadata_url),
+ body=expected_hostname)
+ httpretty.register_uri(httpretty.GET,
+ "{}instance-id".format(self.metadata_url),
+ body=expected_id)
+ self.assertTrue(ds._get_data())
+ self.assertEqual(ds.userdata_raw.decode("utf-8"), "#cloud-config")
+ self.assertEqual(ds.metadata, {"instance-id": expected_id,
+ "local-hostname": expected_hostname})
+ self.assertEqual(ds.get_config_obj(), {})
+
+ @mock.patch('cloudinit.sources.DataSourceExoscale.get_password')
+ def test_read_metadata_when_password_server_unreachable(self, m_password):
+ """The read_metadata function returns partial results in case the
+ password server (only) is unreachable."""
+ expected_id = "12345"
+ expected_hostname = "myname"
+ expected_userdata = "#cloud-config"
+
+ m_password.side_effect = requests.Timeout('Fake Connection Timeout')
+ httpretty.register_uri(httpretty.GET,
+ self.userdata_url,
+ body=expected_userdata)
+ httpretty.register_uri(httpretty.GET,
+ self.metadata_url,
+ body="instance-id\nlocal-hostname")
+ httpretty.register_uri(httpretty.GET,
+ "{}local-hostname".format(self.metadata_url),
+ body=expected_hostname)
+ httpretty.register_uri(httpretty.GET,
+ "{}instance-id".format(self.metadata_url),
+ body=expected_id)
+
+ result = read_metadata()
+
+ self.assertIsNone(result.get("password"))
+ self.assertEqual(result.get("user-data").decode("utf-8"),
+ expected_userdata)
+
+ def test_non_viable_platform(self):
+ """The datasource fails fast when the platform is not viable."""
+ path = helpers.Paths({'run_dir': self.tmp})
+ ds = DataSourceExoscale({}, None, path)
+ ds._is_platform_viable = lambda: False
+ self.assertFalse(ds._get_data())
diff --git a/tests/unittests/test_datasource/test_gce.py b/tests/unittests/test_datasource/test_gce.py
index 41176c6a..4afbccff 100644
--- a/tests/unittests/test_datasource/test_gce.py
+++ b/tests/unittests/test_datasource/test_gce.py
@@ -7,11 +7,11 @@
import datetime
import httpretty
import json
-import mock
import re
+from unittest import mock
+from urllib.parse import urlparse
from base64 import b64encode, b64decode
-from six.moves.urllib_parse import urlparse
from cloudinit import distros
from cloudinit import helpers
@@ -55,6 +55,8 @@ GCE_USER_DATA_TEXT = {
HEADERS = {'Metadata-Flavor': 'Google'}
MD_URL_RE = re.compile(
r'http://metadata.google.internal/computeMetadata/v1/.*')
+GUEST_ATTRIBUTES_URL = ('http://metadata.google.internal/computeMetadata/'
+ 'v1/instance/guest-attributes/hostkeys/')
def _set_mock_metadata(gce_meta=None):
@@ -341,4 +343,20 @@ class TestDataSourceGCE(test_helpers.HttprettyTestCase):
public_key_data, default_user='default')
self.assertEqual(sorted(found), sorted(expected))
+ @mock.patch("cloudinit.url_helper.readurl")
+ def test_publish_host_keys(self, m_readurl):
+ hostkeys = [('ssh-rsa', 'asdfasdf'),
+ ('ssh-ed25519', 'qwerqwer')]
+ readurl_expected_calls = [
+ mock.call(check_status=False, data=b'asdfasdf', headers=HEADERS,
+ request_method='PUT',
+ url='%s%s' % (GUEST_ATTRIBUTES_URL, 'ssh-rsa')),
+ mock.call(check_status=False, data=b'qwerqwer', headers=HEADERS,
+ request_method='PUT',
+ url='%s%s' % (GUEST_ATTRIBUTES_URL, 'ssh-ed25519')),
+ ]
+ self.ds.publish_host_keys(hostkeys)
+ m_readurl.assert_has_calls(readurl_expected_calls, any_order=True)
+
+
# vi: ts=4 expandtab
diff --git a/tests/unittests/test_datasource/test_maas.py b/tests/unittests/test_datasource/test_maas.py
index c84d067e..2a81d3f5 100644
--- a/tests/unittests/test_datasource/test_maas.py
+++ b/tests/unittests/test_datasource/test_maas.py
@@ -1,11 +1,11 @@
# This file is part of cloud-init. See LICENSE file for license information.
from copy import copy
-import mock
import os
import shutil
import tempfile
import yaml
+from unittest import mock
from cloudinit.sources import DataSourceMAAS
from cloudinit import url_helper
diff --git a/tests/unittests/test_datasource/test_nocloud.py b/tests/unittests/test_datasource/test_nocloud.py
index 3429272c..18bea0b9 100644
--- a/tests/unittests/test_datasource/test_nocloud.py
+++ b/tests/unittests/test_datasource/test_nocloud.py
@@ -32,6 +32,36 @@ class TestNoCloudDataSource(CiTestCase):
self.mocks.enter_context(
mock.patch.object(util, 'read_dmi_data', return_value=None))
+ def _test_fs_config_is_read(self, fs_label, fs_label_to_search):
+ vfat_device = 'device-1'
+
+ def m_mount_cb(device, callback, mtype):
+ if (device == vfat_device):
+ return {'meta-data': yaml.dump({'instance-id': 'IID'})}
+ else:
+ return {}
+
+ def m_find_devs_with(query='', path=''):
+ if 'TYPE=vfat' == query:
+ return [vfat_device]
+ elif 'LABEL={}'.format(fs_label) == query:
+ return [vfat_device]
+ else:
+ return []
+
+ self.mocks.enter_context(
+ mock.patch.object(util, 'find_devs_with',
+ side_effect=m_find_devs_with))
+ self.mocks.enter_context(
+ mock.patch.object(util, 'mount_cb',
+ side_effect=m_mount_cb))
+ sys_cfg = {'datasource': {'NoCloud': {'fs_label': fs_label_to_search}}}
+ dsrc = dsNoCloud(sys_cfg=sys_cfg, distro=None, paths=self.paths)
+ ret = dsrc.get_data()
+
+ self.assertEqual(dsrc.metadata.get('instance-id'), 'IID')
+ self.assertTrue(ret)
+
def test_nocloud_seed_dir_on_lxd(self, m_is_lxd):
md = {'instance-id': 'IID', 'dsmode': 'local'}
ud = b"USER_DATA_HERE"
@@ -90,6 +120,18 @@ class TestNoCloudDataSource(CiTestCase):
ret = dsrc.get_data()
self.assertFalse(ret)
+ def test_fs_config_lowercase_label(self, m_is_lxd):
+ self._test_fs_config_is_read('cidata', 'cidata')
+
+ def test_fs_config_uppercase_label(self, m_is_lxd):
+ self._test_fs_config_is_read('CIDATA', 'cidata')
+
+ def test_fs_config_lowercase_label_search_uppercase(self, m_is_lxd):
+ self._test_fs_config_is_read('cidata', 'CIDATA')
+
+ def test_fs_config_uppercase_label_search_uppercase(self, m_is_lxd):
+ self._test_fs_config_is_read('CIDATA', 'CIDATA')
+
def test_no_datasource_expected(self, m_is_lxd):
# no source should be found if no cmdline, config, and fs_label=None
sys_cfg = {'datasource': {'NoCloud': {'fs_label': None}}}
@@ -236,6 +278,24 @@ class TestNoCloudDataSource(CiTestCase):
self.assertEqual(netconf, dsrc.network_config)
self.assertNotIn(gateway, str(dsrc.network_config))
+ @mock.patch("cloudinit.util.blkid")
+ def test_nocloud_get_devices_freebsd(self, m_is_lxd, fake_blkid):
+ populate_dir(os.path.join(self.paths.seed_dir, "nocloud"),
+ {'user-data': b"ud", 'meta-data': "instance-id: IID\n"})
+
+ sys_cfg = {'datasource': {'NoCloud': {'fs_label': None}}}
+
+ self.mocks.enter_context(
+ mock.patch.object(util, 'is_FreeBSD', return_value=True))
+
+ self.mocks.enter_context(
+ mock.patch.object(os.path, 'exists', return_value=True))
+
+ dsrc = dsNoCloud(sys_cfg=sys_cfg, distro=None, paths=self.paths)
+ ret = dsrc._get_devices('foo')
+ self.assertEqual(['/dev/msdosfs/foo', '/dev/iso9660/foo'], ret)
+ fake_blkid.assert_not_called()
+
class TestParseCommandLineData(CiTestCase):
diff --git a/tests/unittests/test_datasource/test_openstack.py b/tests/unittests/test_datasource/test_openstack.py
index a731f1ed..f754556f 100644
--- a/tests/unittests/test_datasource/test_openstack.py
+++ b/tests/unittests/test_datasource/test_openstack.py
@@ -8,12 +8,11 @@ import copy
import httpretty as hp
import json
import re
+from io import StringIO
+from urllib.parse import urlparse
from cloudinit.tests import helpers as test_helpers
-from six.moves.urllib.parse import urlparse
-from six import StringIO, text_type
-
from cloudinit import helpers
from cloudinit import settings
from cloudinit.sources import BrokenMetadata, convert_vendordata, UNSET
@@ -569,8 +568,7 @@ class TestMetadataReader(test_helpers.HttprettyTestCase):
'uuid': 'b0fa911b-69d4-4476-bbe2-1c92bff6535c'}
def register(self, path, body=None, status=200):
- content = (body if not isinstance(body, text_type)
- else body.encode('utf-8'))
+ content = body if not isinstance(body, str) else body.encode('utf-8')
hp.register_uri(
hp.GET, self.burl + "openstack" + path, status=status,
body=content)
diff --git a/tests/unittests/test_datasource/test_ovf.py b/tests/unittests/test_datasource/test_ovf.py
index 349d54cc..a19c35c8 100644
--- a/tests/unittests/test_datasource/test_ovf.py
+++ b/tests/unittests/test_datasource/test_ovf.py
@@ -169,19 +169,56 @@ class TestDatasourceOVF(CiTestCase):
MARKER-ID = 12345345
""")
util.write_file(conf_file, conf_content)
- with self.assertRaises(CustomScriptNotFound) as context:
- wrap_and_call(
- 'cloudinit.sources.DataSourceOVF',
- {'util.read_dmi_data': 'vmware',
- 'util.del_dir': True,
- 'search_file': self.tdir,
- 'wait_for_imc_cfg_file': conf_file,
- 'get_nics_to_enable': ''},
- ds.get_data)
+ with mock.patch(MPATH + 'get_tools_config', return_value='true'):
+ with self.assertRaises(CustomScriptNotFound) as context:
+ wrap_and_call(
+ 'cloudinit.sources.DataSourceOVF',
+ {'util.read_dmi_data': 'vmware',
+ 'util.del_dir': True,
+ 'search_file': self.tdir,
+ 'wait_for_imc_cfg_file': conf_file,
+ 'get_nics_to_enable': ''},
+ ds.get_data)
customscript = self.tmp_path('test-script', self.tdir)
self.assertIn('Script %s not found!!' % customscript,
str(context.exception))
+ def test_get_data_cust_script_disabled(self):
+ """If custom script is disabled by VMware tools configuration,
+ raise a RuntimeError.
+ """
+ paths = Paths({'cloud_dir': self.tdir})
+ ds = self.datasource(
+ sys_cfg={'disable_vmware_customization': False}, distro={},
+ paths=paths)
+ # Prepare the conf file
+ conf_file = self.tmp_path('test-cust', self.tdir)
+ conf_content = dedent("""\
+ [CUSTOM-SCRIPT]
+ SCRIPT-NAME = test-script
+ [MISC]
+ MARKER-ID = 12345346
+ """)
+ util.write_file(conf_file, conf_content)
+ # Prepare the custom sript
+ customscript = self.tmp_path('test-script', self.tdir)
+ util.write_file(customscript, "This is the post cust script")
+
+ with mock.patch(MPATH + 'get_tools_config', return_value='invalid'):
+ with mock.patch(MPATH + 'set_customization_status',
+ return_value=('msg', b'')):
+ with self.assertRaises(RuntimeError) as context:
+ wrap_and_call(
+ 'cloudinit.sources.DataSourceOVF',
+ {'util.read_dmi_data': 'vmware',
+ 'util.del_dir': True,
+ 'search_file': self.tdir,
+ 'wait_for_imc_cfg_file': conf_file,
+ 'get_nics_to_enable': ''},
+ ds.get_data)
+ self.assertIn('Custom script is disabled by VM Administrator',
+ str(context.exception))
+
def test_get_data_non_vmware_seed_platform_info(self):
"""Platform info properly reports when on non-vmware platforms."""
paths = Paths({'cloud_dir': self.tdir, 'run_dir': self.tdir})
diff --git a/tests/unittests/test_datasource/test_rbx.py b/tests/unittests/test_datasource/test_rbx.py
new file mode 100644
index 00000000..aabf1f18
--- /dev/null
+++ b/tests/unittests/test_datasource/test_rbx.py
@@ -0,0 +1,208 @@
+import json
+
+from cloudinit import helpers
+from cloudinit import distros
+from cloudinit.sources import DataSourceRbxCloud as ds
+from cloudinit.tests.helpers import mock, CiTestCase, populate_dir
+
+DS_PATH = "cloudinit.sources.DataSourceRbxCloud"
+
+CRYPTO_PASS = "$6$uktth46t$FvpDzFD2iL9YNZIG1Epz7957hJqbH0f" \
+ "QKhnzcfBcUhEodGAWRqTy7tYG4nEW7SUOYBjxOSFIQW5" \
+ "tToyGP41.s1"
+
+CLOUD_METADATA = {
+ "vm": {
+ "memory": 4,
+ "cpu": 2,
+ "name": "vm-image-builder",
+ "_id": "5beab44f680cffd11f0e60fc"
+ },
+ "additionalMetadata": {
+ "username": "guru",
+ "sshKeys": ["ssh-rsa ..."],
+ "password": {
+ "sha512": CRYPTO_PASS
+ }
+ },
+ "disk": [
+ {"size": 10, "type": "ssd",
+ "name": "vm-image-builder-os",
+ "_id": "5beab450680cffd11f0e60fe"},
+ {"size": 2, "type": "ssd",
+ "name": "ubuntu-1804-bionic",
+ "_id": "5bef002c680cffd11f107590"}
+ ],
+ "netadp": [
+ {
+ "ip": [{"address": "62.181.8.174"}],
+ "network": {
+ "dns": {"nameservers": ["8.8.8.8", "8.8.4.4"]},
+ "routing": [],
+ "gateway": "62.181.8.1",
+ "netmask": "255.255.248.0",
+ "name": "public",
+ "type": "public",
+ "_id": "5784e97be2627505227b578c"
+ },
+ "speed": 1000,
+ "type": "hv",
+ "macaddress": "00:15:5D:FF:0F:03",
+ "_id": "5beab450680cffd11f0e6102"
+ },
+ {
+ "ip": [{"address": "10.209.78.11"}],
+ "network": {
+ "dns": {"nameservers": ["9.9.9.9", "8.8.8.8"]},
+ "routing": [],
+ "gateway": "10.209.78.1",
+ "netmask": "255.255.255.0",
+ "name": "network-determined-bardeen",
+ "type": "private",
+ "_id": "5beaec64680cffd11f0e7c31"
+ },
+ "speed": 1000,
+ "type": "hv",
+ "macaddress": "00:15:5D:FF:0F:24",
+ "_id": "5bec18c6680cffd11f0f0d8b"
+ }
+ ],
+ "dvddrive": [{"iso": {}}]
+}
+
+
+class TestRbxDataSource(CiTestCase):
+ parsed_user = None
+ allowed_subp = ['bash']
+
+ def _fetch_distro(self, kind):
+ cls = distros.fetch(kind)
+ paths = helpers.Paths({})
+ return cls(kind, {}, paths)
+
+ def setUp(self):
+ super(TestRbxDataSource, self).setUp()
+ self.tmp = self.tmp_dir()
+ self.paths = helpers.Paths(
+ {'cloud_dir': self.tmp, 'run_dir': self.tmp}
+ )
+
+ # defaults for few tests
+ self.ds = ds.DataSourceRbxCloud
+ self.seed_dir = self.paths.seed_dir
+ self.sys_cfg = {'datasource': {'RbxCloud': {'dsmode': 'local'}}}
+
+ def test_seed_read_user_data_callback_empty_file(self):
+ populate_user_metadata(self.seed_dir, '')
+ populate_cloud_metadata(self.seed_dir, {})
+ results = ds.read_user_data_callback(self.seed_dir)
+
+ self.assertIsNone(results)
+
+ def test_seed_read_user_data_callback_valid_disk(self):
+ populate_user_metadata(self.seed_dir, '')
+ populate_cloud_metadata(self.seed_dir, CLOUD_METADATA)
+ results = ds.read_user_data_callback(self.seed_dir)
+
+ self.assertNotEqual(results, None)
+ self.assertTrue('userdata' in results)
+ self.assertTrue('metadata' in results)
+ self.assertTrue('cfg' in results)
+
+ def test_seed_read_user_data_callback_userdata(self):
+ userdata = "#!/bin/sh\nexit 1"
+ populate_user_metadata(self.seed_dir, userdata)
+ populate_cloud_metadata(self.seed_dir, CLOUD_METADATA)
+
+ results = ds.read_user_data_callback(self.seed_dir)
+
+ self.assertNotEqual(results, None)
+ self.assertTrue('userdata' in results)
+ self.assertEqual(results['userdata'], userdata)
+
+ def test_generate_network_config(self):
+ expected = {
+ 'version': 1,
+ 'config': [
+ {
+ 'subnets': [
+ {'control': 'auto',
+ 'dns_nameservers': ['8.8.8.8', '8.8.4.4'],
+ 'netmask': '255.255.248.0',
+ 'address': '62.181.8.174',
+ 'type': 'static', 'gateway': '62.181.8.1'}
+ ],
+ 'type': 'physical',
+ 'name': 'eth0',
+ 'mac_address': '00:15:5d:ff:0f:03'
+ },
+ {
+ 'subnets': [
+ {'control': 'auto',
+ 'dns_nameservers': ['9.9.9.9', '8.8.8.8'],
+ 'netmask': '255.255.255.0',
+ 'address': '10.209.78.11',
+ 'type': 'static',
+ 'gateway': '10.209.78.1'}
+ ],
+ 'type': 'physical',
+ 'name': 'eth1',
+ 'mac_address': '00:15:5d:ff:0f:24'
+ }
+ ]
+ }
+ self.assertTrue(
+ ds.generate_network_config(CLOUD_METADATA['netadp']),
+ expected
+ )
+
+ @mock.patch(DS_PATH + '.util.subp')
+ def test_gratuitous_arp_run_standard_arping(self, m_subp):
+ """Test handle run arping & parameters."""
+ items = [
+ {
+ 'destination': '172.17.0.2',
+ 'source': '172.16.6.104'
+ },
+ {
+ 'destination': '172.17.0.2',
+ 'source': '172.16.6.104',
+ },
+ ]
+ ds.gratuitous_arp(items, self._fetch_distro('ubuntu'))
+ self.assertEqual([
+ mock.call([
+ 'arping', '-c', '2', '-S',
+ '172.16.6.104', '172.17.0.2'
+ ]),
+ mock.call([
+ 'arping', '-c', '2', '-S',
+ '172.16.6.104', '172.17.0.2'
+ ])
+ ], m_subp.call_args_list
+ )
+
+ @mock.patch(DS_PATH + '.util.subp')
+ def test_handle_rhel_like_arping(self, m_subp):
+ """Test handle on RHEL-like distros."""
+ items = [
+ {
+ 'source': '172.16.6.104',
+ 'destination': '172.17.0.2',
+ }
+ ]
+ ds.gratuitous_arp(items, self._fetch_distro('fedora'))
+ self.assertEqual([
+ mock.call(
+ ['arping', '-c', '2', '-s', '172.16.6.104', '172.17.0.2']
+ )],
+ m_subp.call_args_list
+ )
+
+
+def populate_cloud_metadata(path, data):
+ populate_dir(path, {'cloud.json': json.dumps(data)})
+
+
+def populate_user_metadata(path, data):
+ populate_dir(path, {'user.data': data})
diff --git a/tests/unittests/test_datasource/test_scaleway.py b/tests/unittests/test_datasource/test_scaleway.py
index c2bc7a00..1b4dd0ad 100644
--- a/tests/unittests/test_datasource/test_scaleway.py
+++ b/tests/unittests/test_datasource/test_scaleway.py
@@ -7,6 +7,7 @@ import requests
from cloudinit import helpers
from cloudinit import settings
+from cloudinit import sources
from cloudinit.sources import DataSourceScaleway
from cloudinit.tests.helpers import mock, HttprettyTestCase, CiTestCase
@@ -49,6 +50,9 @@ class MetadataResponses(object):
FAKE_METADATA = {
'id': '00000000-0000-0000-0000-000000000000',
'hostname': 'scaleway.host',
+ 'tags': [
+ "AUTHORIZED_KEY=ssh-rsa_AAAAB3NzaC1yc2EAAAADAQABDDDDD",
+ ],
'ssh_public_keys': [{
'key': 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABA',
'fingerprint': '2048 06:ae:... login (RSA)'
@@ -204,10 +208,11 @@ class TestDataSourceScaleway(HttprettyTestCase):
self.assertEqual(self.datasource.get_instance_id(),
MetadataResponses.FAKE_METADATA['id'])
- self.assertEqual(self.datasource.get_public_ssh_keys(), [
- elem['key'] for elem in
- MetadataResponses.FAKE_METADATA['ssh_public_keys']
- ])
+ self.assertEqual(self.datasource.get_public_ssh_keys().sort(), [
+ u'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABCCCCC',
+ u'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABDDDDD',
+ u'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABA',
+ ].sort())
self.assertEqual(self.datasource.get_hostname(),
MetadataResponses.FAKE_METADATA['hostname'])
self.assertEqual(self.datasource.get_userdata_raw(),
@@ -218,6 +223,70 @@ class TestDataSourceScaleway(HttprettyTestCase):
self.assertIsNone(self.datasource.region)
self.assertEqual(sleep.call_count, 0)
+ def test_ssh_keys_empty(self):
+ """
+ get_public_ssh_keys() should return empty list if no ssh key are
+ available
+ """
+ self.datasource.metadata['tags'] = []
+ self.datasource.metadata['ssh_public_keys'] = []
+ self.assertEqual(self.datasource.get_public_ssh_keys(), [])
+
+ def test_ssh_keys_only_tags(self):
+ """
+ get_public_ssh_keys() should return list of keys available in tags
+ """
+ self.datasource.metadata['tags'] = [
+ "AUTHORIZED_KEY=ssh-rsa_AAAAB3NzaC1yc2EAAAADAQABDDDDD",
+ "AUTHORIZED_KEY=ssh-rsa_AAAAB3NzaC1yc2EAAAADAQABCCCCC",
+ ]
+ self.datasource.metadata['ssh_public_keys'] = []
+ self.assertEqual(self.datasource.get_public_ssh_keys().sort(), [
+ u'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABDDDDD',
+ u'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABCCCCC',
+ ].sort())
+
+ def test_ssh_keys_only_conf(self):
+ """
+ get_public_ssh_keys() should return list of keys available in
+ ssh_public_keys field
+ """
+ self.datasource.metadata['tags'] = []
+ self.datasource.metadata['ssh_public_keys'] = [{
+ 'key': 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABA',
+ 'fingerprint': '2048 06:ae:... login (RSA)'
+ }, {
+ 'key': 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABCCCCC',
+ 'fingerprint': '2048 06:ff:... login2 (RSA)'
+ }]
+ self.assertEqual(self.datasource.get_public_ssh_keys().sort(), [
+ u'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABCCCCC',
+ u'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABDDDDD',
+ u'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABA',
+ ].sort())
+
+ def test_ssh_keys_both(self):
+ """
+ get_public_ssh_keys() should return a merge of keys available
+ in ssh_public_keys and tags
+ """
+ self.datasource.metadata['tags'] = [
+ "AUTHORIZED_KEY=ssh-rsa_AAAAB3NzaC1yc2EAAAADAQABDDDDD",
+ ]
+
+ self.datasource.metadata['ssh_public_keys'] = [{
+ 'key': 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABA',
+ 'fingerprint': '2048 06:ae:... login (RSA)'
+ }, {
+ 'key': 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABCCCCC',
+ 'fingerprint': '2048 06:ff:... login2 (RSA)'
+ }]
+ self.assertEqual(self.datasource.get_public_ssh_keys().sort(), [
+ u'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABCCCCC',
+ u'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABDDDDD',
+ u'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABA',
+ ].sort())
+
@mock.patch('cloudinit.sources.DataSourceScaleway.EphemeralDHCPv4')
@mock.patch('cloudinit.sources.DataSourceScaleway.SourceAddressAdapter',
get_source_address_adapter)
@@ -335,3 +404,51 @@ class TestDataSourceScaleway(HttprettyTestCase):
netcfg = self.datasource.network_config
self.assertEqual(netcfg, '0xdeadbeef')
+
+ @mock.patch('cloudinit.sources.DataSourceScaleway.net.find_fallback_nic')
+ @mock.patch('cloudinit.util.get_cmdline')
+ def test_network_config_unset(self, m_get_cmdline, fallback_nic):
+ """
+ _network_config will be set to sources.UNSET after the first boot.
+ Make sure it behave correctly.
+ """
+ m_get_cmdline.return_value = 'scaleway'
+ fallback_nic.return_value = 'ens2'
+ self.datasource.metadata['ipv6'] = None
+ self.datasource._network_config = sources.UNSET
+
+ resp = {'version': 1,
+ 'config': [{
+ 'type': 'physical',
+ 'name': 'ens2',
+ 'subnets': [{'type': 'dhcp4'}]}]
+ }
+
+ netcfg = self.datasource.network_config
+ self.assertEqual(netcfg, resp)
+
+ @mock.patch('cloudinit.sources.DataSourceScaleway.LOG.warning')
+ @mock.patch('cloudinit.sources.DataSourceScaleway.net.find_fallback_nic')
+ @mock.patch('cloudinit.util.get_cmdline')
+ def test_network_config_cached_none(self, m_get_cmdline, fallback_nic,
+ logwarning):
+ """
+ network_config() should return config data if cached data is None
+ rather than sources.UNSET
+ """
+ m_get_cmdline.return_value = 'scaleway'
+ fallback_nic.return_value = 'ens2'
+ self.datasource.metadata['ipv6'] = None
+ self.datasource._network_config = None
+
+ resp = {'version': 1,
+ 'config': [{
+ 'type': 'physical',
+ 'name': 'ens2',
+ 'subnets': [{'type': 'dhcp4'}]}]
+ }
+
+ netcfg = self.datasource.network_config
+ self.assertEqual(netcfg, resp)
+ logwarning.assert_called_with('Found None as cached _network_config. '
+ 'Resetting to %s', sources.UNSET)
diff --git a/tests/unittests/test_datasource/test_smartos.py b/tests/unittests/test_datasource/test_smartos.py
index 42ac6971..62084de5 100644
--- a/tests/unittests/test_datasource/test_smartos.py
+++ b/tests/unittests/test_datasource/test_smartos.py
@@ -1,5 +1,5 @@
# Copyright (C) 2013 Canonical Ltd.
-# Copyright (c) 2018, Joyent, Inc.
+# Copyright 2019 Joyent, Inc.
#
# Author: Ben Howard <ben.howard@canonical.com>
#
@@ -31,8 +31,7 @@ from cloudinit.sources.DataSourceSmartOS import (
convert_smartos_network_data as convert_net,
SMARTOS_ENV_KVM, SERIAL_DEVICE, get_smartos_environ,
identify_file)
-
-import six
+from cloudinit.event import EventType
from cloudinit import helpers as c_helpers
from cloudinit.util import (
@@ -653,6 +652,12 @@ class TestSmartOSDataSource(FilesystemMockingTestCase):
self.assertEqual(dsrc.device_name_to_device('FOO'),
mydscfg['disk_aliases']['FOO'])
+ def test_reconfig_network_on_boot(self):
+ # Test to ensure that network is configured from metadata on each boot
+ dsrc = self._get_ds(mockdata=MOCK_RETURNS)
+ self.assertSetEqual(set([EventType.BOOT_NEW_INSTANCE, EventType.BOOT]),
+ dsrc.update_events['network'])
+
class TestIdentifyFile(CiTestCase):
"""Test the 'identify_file' utility."""
@@ -791,7 +796,7 @@ class TestJoyentMetadataClient(FilesystemMockingTestCase):
return self.serial.write.call_args[0][0]
def test_get_metadata_writes_bytes(self):
- self.assertIsInstance(self._get_written_line(), six.binary_type)
+ self.assertIsInstance(self._get_written_line(), bytes)
def test_get_metadata_line_starts_with_v2(self):
foo = self._get_written_line()
diff --git a/tests/unittests/test_distros/test_create_users.py b/tests/unittests/test_distros/test_create_users.py
index c3f258d5..ef11784d 100644
--- a/tests/unittests/test_distros/test_create_users.py
+++ b/tests/unittests/test_distros/test_create_users.py
@@ -206,7 +206,7 @@ class TestCreateUser(CiTestCase):
user = 'foouser'
self.dist.create_user(user, ssh_redirect_user='someuser')
self.assertIn(
- 'WARNING: Unable to disable ssh logins for foouser given '
+ 'WARNING: Unable to disable SSH logins for foouser given '
'ssh_redirect_user: someuser. No cloud public-keys present.\n',
self.logs.getvalue())
m_setup_user_keys.assert_not_called()
@@ -240,4 +240,32 @@ class TestCreateUser(CiTestCase):
[mock.call(set(['auth1']), user), # not disabled
mock.call(set(['key1']), 'foouser', options=disable_prefix)])
+ @mock.patch("cloudinit.distros.util.which")
+ def test_lock_with_usermod_if_no_passwd(self, m_which, m_subp,
+ m_is_snappy):
+ """Lock uses usermod --lock if no 'passwd' cmd available."""
+ m_which.side_effect = lambda m: m in ('usermod',)
+ self.dist.lock_passwd("bob")
+ self.assertEqual(
+ [mock.call(['usermod', '--lock', 'bob'])],
+ m_subp.call_args_list)
+
+ @mock.patch("cloudinit.distros.util.which")
+ def test_lock_with_passwd_if_available(self, m_which, m_subp,
+ m_is_snappy):
+ """Lock with only passwd will use passwd."""
+ m_which.side_effect = lambda m: m in ('passwd',)
+ self.dist.lock_passwd("bob")
+ self.assertEqual(
+ [mock.call(['passwd', '-l', 'bob'])],
+ m_subp.call_args_list)
+
+ @mock.patch("cloudinit.distros.util.which")
+ def test_lock_raises_runtime_if_no_commands(self, m_which, m_subp,
+ m_is_snappy):
+ """Lock with no commands available raises RuntimeError."""
+ m_which.return_value = None
+ with self.assertRaises(RuntimeError):
+ self.dist.lock_passwd("bob")
+
# vi: ts=4 expandtab
diff --git a/tests/unittests/test_distros/test_freebsd.py b/tests/unittests/test_distros/test_freebsd.py
new file mode 100644
index 00000000..8af253a2
--- /dev/null
+++ b/tests/unittests/test_distros/test_freebsd.py
@@ -0,0 +1,45 @@
+# This file is part of cloud-init. See LICENSE file for license information.
+
+from cloudinit.util import (find_freebsd_part, get_path_dev_freebsd)
+from cloudinit.tests.helpers import (CiTestCase, mock)
+
+import os
+
+
+class TestDeviceLookUp(CiTestCase):
+
+ @mock.patch('cloudinit.util.subp')
+ def test_find_freebsd_part_label(self, mock_subp):
+ glabel_out = '''
+gptid/fa52d426-c337-11e6-8911-00155d4c5e47 N/A da0p1
+ label/rootfs N/A da0p2
+ label/swap N/A da0p3
+'''
+ mock_subp.return_value = (glabel_out, "")
+ res = find_freebsd_part("/dev/label/rootfs")
+ self.assertEqual("da0p2", res)
+
+ @mock.patch('cloudinit.util.subp')
+ def test_find_freebsd_part_gpt(self, mock_subp):
+ glabel_out = '''
+ gpt/bootfs N/A vtbd0p1
+gptid/3f4cbe26-75da-11e8-a8f2-002590ec6166 N/A vtbd0p1
+ gpt/swapfs N/A vtbd0p2
+ gpt/rootfs N/A vtbd0p3
+ iso9660/cidata N/A vtbd2
+'''
+ mock_subp.return_value = (glabel_out, "")
+ res = find_freebsd_part("/dev/gpt/rootfs")
+ self.assertEqual("vtbd0p3", res)
+
+ def test_get_path_dev_freebsd_label(self):
+ mnt_list = '''
+/dev/label/rootfs / ufs rw 1 1
+devfs /dev devfs rw,multilabel 0 0
+fdescfs /dev/fd fdescfs rw 0 0
+/dev/da1s1 /mnt/resource ufs rw 2 2
+'''
+ with mock.patch.object(os.path, 'exists',
+ return_value=True):
+ res = get_path_dev_freebsd('/etc', mnt_list)
+ self.assertIsNotNone(res)
diff --git a/tests/unittests/test_distros/test_generic.py b/tests/unittests/test_distros/test_generic.py
index 791fe612..02b334e3 100644
--- a/tests/unittests/test_distros/test_generic.py
+++ b/tests/unittests/test_distros/test_generic.py
@@ -8,11 +8,7 @@ from cloudinit.tests import helpers
import os
import shutil
import tempfile
-
-try:
- from unittest import mock
-except ImportError:
- import mock
+from unittest import mock
unknown_arch_info = {
'arches': ['default'],
@@ -244,5 +240,23 @@ class TestGenericDistro(helpers.FilesystemMockingTestCase):
with self.assertRaises(NotImplementedError):
d.get_locale()
+ def test_expire_passwd_uses_chpasswd(self):
+ """Test ubuntu.expire_passwd uses the passwd command."""
+ for d_name in ("ubuntu", "rhel"):
+ cls = distros.fetch(d_name)
+ d = cls(d_name, {}, None)
+ with mock.patch("cloudinit.util.subp") as m_subp:
+ d.expire_passwd("myuser")
+ m_subp.assert_called_once_with(["passwd", "--expire", "myuser"])
+
+ def test_expire_passwd_freebsd_uses_pw_command(self):
+ """Test FreeBSD.expire_passwd uses the pw command."""
+ cls = distros.fetch("freebsd")
+ d = cls("freebsd", {}, None)
+ with mock.patch("cloudinit.util.subp") as m_subp:
+ d.expire_passwd("myuser")
+ m_subp.assert_called_once_with(
+ ["pw", "usermod", "myuser", "-p", "01-Jan-1970"])
+
# vi: ts=4 expandtab
diff --git a/tests/unittests/test_distros/test_netconfig.py b/tests/unittests/test_distros/test_netconfig.py
index 6e339355..ccf66161 100644
--- a/tests/unittests/test_distros/test_netconfig.py
+++ b/tests/unittests/test_distros/test_netconfig.py
@@ -1,20 +1,17 @@
# This file is part of cloud-init. See LICENSE file for license information.
+import copy
import os
-from six import StringIO
+from io import StringIO
from textwrap import dedent
-
-try:
- from unittest import mock
-except ImportError:
- import mock
+from unittest import mock
from cloudinit import distros
from cloudinit.distros.parsers.sys_conf import SysConf
from cloudinit import helpers
from cloudinit import settings
from cloudinit.tests.helpers import (
- FilesystemMockingTestCase, dir2dict, populate_dir)
+ FilesystemMockingTestCase, dir2dict)
from cloudinit import util
@@ -91,9 +88,9 @@ V1_NET_CFG = {'config': [{'name': 'eth0',
'version': 1}
V1_NET_CFG_OUTPUT = """\
-# This file is generated from information provided by
-# the datasource. Changes to it will not persist across an instance.
-# To disable cloud-init's network configuration capabilities, write a file
+# This file is generated from information provided by the datasource. Changes
+# to it will not persist across an instance reboot. To disable cloud-init's
+# network configuration capabilities, write a file
# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:
# network: {config: disabled}
auto lo
@@ -109,13 +106,31 @@ auto eth1
iface eth1 inet dhcp
"""
+V1_NET_CFG_IPV6_OUTPUT = """\
+# This file is generated from information provided by the datasource. Changes
+# to it will not persist across an instance reboot. To disable cloud-init's
+# network configuration capabilities, write a file
+# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:
+# network: {config: disabled}
+auto lo
+iface lo inet loopback
+
+auto eth0
+iface eth0 inet6 static
+ address 2607:f0d0:1002:0011::2/64
+ gateway 2607:f0d0:1002:0011::1
+
+auto eth1
+iface eth1 inet dhcp
+"""
+
V1_NET_CFG_IPV6 = {'config': [{'name': 'eth0',
'subnets': [{'address':
'2607:f0d0:1002:0011::2',
'gateway':
'2607:f0d0:1002:0011::1',
'netmask': '64',
- 'type': 'static'}],
+ 'type': 'static6'}],
'type': 'physical'},
{'name': 'eth1',
'subnets': [{'control': 'auto',
@@ -125,9 +140,9 @@ V1_NET_CFG_IPV6 = {'config': [{'name': 'eth0',
V1_TO_V2_NET_CFG_OUTPUT = """\
-# This file is generated from information provided by
-# the datasource. Changes to it will not persist across an instance.
-# To disable cloud-init's network configuration capabilities, write a file
+# This file is generated from information provided by the datasource. Changes
+# to it will not persist across an instance reboot. To disable cloud-init's
+# network configuration capabilities, write a file
# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:
# network: {config: disabled}
network:
@@ -141,6 +156,23 @@ network:
dhcp4: true
"""
+V1_TO_V2_NET_CFG_IPV6_OUTPUT = """\
+# This file is generated from information provided by the datasource. Changes
+# to it will not persist across an instance reboot. To disable cloud-init's
+# network configuration capabilities, write a file
+# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:
+# network: {config: disabled}
+network:
+ version: 2
+ ethernets:
+ eth0:
+ addresses:
+ - 2607:f0d0:1002:0011::2/64
+ gateway6: 2607:f0d0:1002:0011::1
+ eth1:
+ dhcp4: true
+"""
+
V2_NET_CFG = {
'ethernets': {
'eth7': {
@@ -154,9 +186,9 @@ V2_NET_CFG = {
V2_TO_V2_NET_CFG_OUTPUT = """\
-# This file is generated from information provided by
-# the datasource. Changes to it will not persist across an instance.
-# To disable cloud-init's network configuration capabilities, write a file
+# This file is generated from information provided by the datasource. Changes
+# to it will not persist across an instance reboot. To disable cloud-init's
+# network configuration capabilities, write a file
# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:
# network: {config: disabled}
network:
@@ -213,128 +245,95 @@ class TestNetCfgDistroBase(FilesystemMockingTestCase):
self.assertEqual(v, b2[k])
-class TestNetCfgDistroFreebsd(TestNetCfgDistroBase):
+class TestNetCfgDistroFreeBSD(TestNetCfgDistroBase):
+
+ def setUp(self):
+ super(TestNetCfgDistroFreeBSD, self).setUp()
+ self.distro = self._get_distro('freebsd', renderers=['freebsd'])
- frbsd_ifout = """\
-hn0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500
- options=51b<RXCSUM,TXCSUM,VLAN_MTU,VLAN_HWTAGGING,TSO4,LRO>
- ether 00:15:5d:4c:73:00
- inet6 fe80::215:5dff:fe4c:7300%hn0 prefixlen 64 scopeid 0x2
- inet 10.156.76.127 netmask 0xfffffc00 broadcast 10.156.79.255
- nd6 options=23<PERFORMNUD,ACCEPT_RTADV,AUTO_LINKLOCAL>
- media: Ethernet autoselect (10Gbase-T <full-duplex>)
- status: active
+ def _apply_and_verify_freebsd(self, apply_fn, config, expected_cfgs=None,
+ bringup=False):
+ if not expected_cfgs:
+ raise ValueError('expected_cfg must not be None')
+
+ tmpd = None
+ with mock.patch('cloudinit.net.freebsd.available') as m_avail:
+ m_avail.return_value = True
+ with self.reRooted(tmpd) as tmpd:
+ util.ensure_dir('/etc')
+ util.ensure_file('/etc/rc.conf')
+ util.ensure_file('/etc/resolv.conf')
+ apply_fn(config, bringup)
+
+ results = dir2dict(tmpd)
+ for cfgpath, expected in expected_cfgs.items():
+ print("----------")
+ print(expected)
+ print("^^^^ expected | rendered VVVVVVV")
+ print(results[cfgpath])
+ print("----------")
+ self.assertEqual(
+ set(expected.split('\n')),
+ set(results[cfgpath].split('\n')))
+ self.assertEqual(0o644, get_mode(cfgpath, tmpd))
+
+ @mock.patch('cloudinit.net.get_interfaces_by_mac')
+ def test_apply_network_config_freebsd_standard(self, ifaces_mac):
+ ifaces_mac.return_value = {
+ '00:15:5d:4c:73:00': 'eth0',
+ }
+ rc_conf_expected = """\
+defaultrouter=192.168.1.254
+ifconfig_eth0='192.168.1.5 netmask 255.255.255.0'
+ifconfig_eth1=DHCP
"""
- @mock.patch('cloudinit.distros.freebsd.Distro.get_ifconfig_list')
- @mock.patch('cloudinit.distros.freebsd.Distro.get_ifconfig_ifname_out')
- def test_get_ip_nic_freebsd(self, ifname_out, iflist):
- frbsd_distro = self._get_distro('freebsd')
- iflist.return_value = "lo0 hn0"
- ifname_out.return_value = self.frbsd_ifout
- res = frbsd_distro.get_ipv4()
- self.assertEqual(res, ['lo0', 'hn0'])
- res = frbsd_distro.get_ipv6()
- self.assertEqual(res, [])
-
- @mock.patch('cloudinit.distros.freebsd.Distro.get_ifconfig_ether')
- @mock.patch('cloudinit.distros.freebsd.Distro.get_ifconfig_ifname_out')
- @mock.patch('cloudinit.distros.freebsd.Distro.get_interface_mac')
- def test_generate_fallback_config_freebsd(self, mac, ifname_out, if_ether):
- frbsd_distro = self._get_distro('freebsd')
-
- if_ether.return_value = 'hn0'
- ifname_out.return_value = self.frbsd_ifout
- mac.return_value = '00:15:5d:4c:73:00'
- res = frbsd_distro.generate_fallback_config()
- self.assertIsNotNone(res)
-
- def test_simple_write_freebsd(self):
- fbsd_distro = self._get_distro('freebsd')
-
- rc_conf = '/etc/rc.conf'
- read_bufs = {
- rc_conf: 'initial-rc-conf-not-validated',
- '/etc/resolv.conf': 'initial-resolv-conf-not-validated',
+ expected_cfgs = {
+ '/etc/rc.conf': rc_conf_expected,
+ '/etc/resolv.conf': ''
}
+ self._apply_and_verify_freebsd(self.distro.apply_network_config,
+ V1_NET_CFG,
+ expected_cfgs=expected_cfgs.copy())
- tmpd = self.tmp_dir()
- populate_dir(tmpd, read_bufs)
- with self.reRooted(tmpd):
- with mock.patch("cloudinit.distros.freebsd.util.subp",
- return_value=('vtnet0', '')):
- fbsd_distro.apply_network(BASE_NET_CFG, False)
- results = dir2dict(tmpd)
-
- self.assertIn(rc_conf, results)
- self.assertCfgEquals(
- dedent('''\
- ifconfig_vtnet0="192.168.1.5 netmask 255.255.255.0"
- ifconfig_vtnet1="DHCP"
- defaultrouter="192.168.1.254"
- '''), results[rc_conf])
- self.assertEqual(0o644, get_mode(rc_conf, tmpd))
-
- def test_simple_write_freebsd_from_v2eni(self):
- fbsd_distro = self._get_distro('freebsd')
-
- rc_conf = '/etc/rc.conf'
- read_bufs = {
- rc_conf: 'initial-rc-conf-not-validated',
- '/etc/resolv.conf': 'initial-resolv-conf-not-validated',
+ @mock.patch('cloudinit.net.get_interfaces_by_mac')
+ def test_apply_network_config_freebsd_ifrename(self, ifaces_mac):
+ ifaces_mac.return_value = {
+ '00:15:5d:4c:73:00': 'vtnet0',
}
+ rc_conf_expected = """\
+ifconfig_vtnet0_name=eth0
+defaultrouter=192.168.1.254
+ifconfig_eth0='192.168.1.5 netmask 255.255.255.0'
+ifconfig_eth1=DHCP
+"""
- tmpd = self.tmp_dir()
- populate_dir(tmpd, read_bufs)
- with self.reRooted(tmpd):
- with mock.patch("cloudinit.distros.freebsd.util.subp",
- return_value=('vtnet0', '')):
- fbsd_distro.apply_network(BASE_NET_CFG_FROM_V2, False)
- results = dir2dict(tmpd)
-
- self.assertIn(rc_conf, results)
- self.assertCfgEquals(
- dedent('''\
- ifconfig_vtnet0="192.168.1.5 netmask 255.255.255.0"
- ifconfig_vtnet1="DHCP"
- defaultrouter="192.168.1.254"
- '''), results[rc_conf])
- self.assertEqual(0o644, get_mode(rc_conf, tmpd))
-
- def test_apply_network_config_fallback_freebsd(self):
- fbsd_distro = self._get_distro('freebsd')
-
- # a weak attempt to verify that we don't have an implementation
- # of _write_network_config or apply_network_config in fbsd now,
- # which would make this test not actually test the fallback.
- self.assertRaises(
- NotImplementedError, fbsd_distro._write_network_config,
- BASE_NET_CFG)
-
- # now run
- mynetcfg = {
- 'config': [{"type": "physical", "name": "eth0",
- "mac_address": "c0:d6:9f:2c:e8:80",
- "subnets": [{"type": "dhcp"}]}],
- 'version': 1}
-
- rc_conf = '/etc/rc.conf'
- read_bufs = {
- rc_conf: 'initial-rc-conf-not-validated',
- '/etc/resolv.conf': 'initial-resolv-conf-not-validated',
+ V1_NET_CFG_RENAME = copy.deepcopy(V1_NET_CFG)
+ V1_NET_CFG_RENAME['config'][0]['mac_address'] = '00:15:5d:4c:73:00'
+
+ expected_cfgs = {
+ '/etc/rc.conf': rc_conf_expected,
+ '/etc/resolv.conf': ''
}
+ self._apply_and_verify_freebsd(self.distro.apply_network_config,
+ V1_NET_CFG_RENAME,
+ expected_cfgs=expected_cfgs.copy())
- tmpd = self.tmp_dir()
- populate_dir(tmpd, read_bufs)
- with self.reRooted(tmpd):
- with mock.patch("cloudinit.distros.freebsd.util.subp",
- return_value=('vtnet0', '')):
- fbsd_distro.apply_network_config(mynetcfg, bring_up=False)
- results = dir2dict(tmpd)
+ @mock.patch('cloudinit.net.get_interfaces_by_mac')
+ def test_apply_network_config_freebsd_nameserver(self, ifaces_mac):
+ ifaces_mac.return_value = {
+ '00:15:5d:4c:73:00': 'eth0',
+ }
- self.assertIn(rc_conf, results)
- self.assertCfgEquals('ifconfig_vtnet0="DHCP"', results[rc_conf])
- self.assertEqual(0o644, get_mode(rc_conf, tmpd))
+ V1_NET_CFG_DNS = copy.deepcopy(V1_NET_CFG)
+ ns = ['1.2.3.4']
+ V1_NET_CFG_DNS['config'][0]['subnets'][0]['dns_nameservers'] = ns
+ expected_cfgs = {
+ '/etc/resolv.conf': 'nameserver 1.2.3.4\n'
+ }
+ self._apply_and_verify_freebsd(self.distro.apply_network_config,
+ V1_NET_CFG_DNS,
+ expected_cfgs=expected_cfgs.copy())
class TestNetCfgDistroUbuntuEni(TestNetCfgDistroBase):
@@ -376,6 +375,14 @@ class TestNetCfgDistroUbuntuEni(TestNetCfgDistroBase):
V1_NET_CFG,
expected_cfgs=expected_cfgs.copy())
+ def test_apply_network_config_ipv6_ub(self):
+ expected_cfgs = {
+ self.eni_path(): V1_NET_CFG_IPV6_OUTPUT
+ }
+ self._apply_and_verify_eni(self.distro.apply_network_config,
+ V1_NET_CFG_IPV6,
+ expected_cfgs=expected_cfgs.copy())
+
class TestNetCfgDistroUbuntuNetplan(TestNetCfgDistroBase):
def setUp(self):
@@ -407,7 +414,7 @@ class TestNetCfgDistroUbuntuNetplan(TestNetCfgDistroBase):
self.assertEqual(0o644, get_mode(cfgpath, tmpd))
def netplan_path(self):
- return '/etc/netplan/50-cloud-init.yaml'
+ return '/etc/netplan/50-cloud-init.yaml'
def test_apply_network_config_v1_to_netplan_ub(self):
expected_cfgs = {
@@ -419,6 +426,16 @@ class TestNetCfgDistroUbuntuNetplan(TestNetCfgDistroBase):
V1_NET_CFG,
expected_cfgs=expected_cfgs.copy())
+ def test_apply_network_config_v1_ipv6_to_netplan_ub(self):
+ expected_cfgs = {
+ self.netplan_path(): V1_TO_V2_NET_CFG_IPV6_OUTPUT,
+ }
+
+ # ub_distro.apply_network_config(V1_NET_CFG_IPV6, False)
+ self._apply_and_verify_netplan(self.distro.apply_network_config,
+ V1_NET_CFG_IPV6,
+ expected_cfgs=expected_cfgs.copy())
+
def test_apply_network_config_v2_passthrough_ub(self):
expected_cfgs = {
self.netplan_path(): V2_TO_V2_NET_CFG_OUTPUT,
@@ -551,24 +568,14 @@ class TestNetCfgDistroOpensuse(TestNetCfgDistroBase):
"""Opensuse uses apply_network_config and renders sysconfig"""
expected_cfgs = {
self.ifcfg_path('eth0'): dedent("""\
- BOOTPROTO=none
- DEFROUTE=yes
- DEVICE=eth0
- GATEWAY=192.168.1.254
+ BOOTPROTO=static
IPADDR=192.168.1.5
NETMASK=255.255.255.0
- NM_CONTROLLED=no
- ONBOOT=yes
- TYPE=Ethernet
- USERCTL=no
+ STARTMODE=auto
"""),
self.ifcfg_path('eth1'): dedent("""\
- BOOTPROTO=dhcp
- DEVICE=eth1
- NM_CONTROLLED=no
- ONBOOT=yes
- TYPE=Ethernet
- USERCTL=no
+ BOOTPROTO=dhcp4
+ STARTMODE=auto
"""),
}
self._apply_and_verify(self.distro.apply_network_config,
@@ -579,24 +586,13 @@ class TestNetCfgDistroOpensuse(TestNetCfgDistroBase):
"""Opensuse uses apply_network_config and renders sysconfig w/ipv6"""
expected_cfgs = {
self.ifcfg_path('eth0'): dedent("""\
- BOOTPROTO=none
- DEFROUTE=yes
- DEVICE=eth0
- IPV6ADDR=2607:f0d0:1002:0011::2/64
- IPV6INIT=yes
- IPV6_DEFAULTGW=2607:f0d0:1002:0011::1
- NM_CONTROLLED=no
- ONBOOT=yes
- TYPE=Ethernet
- USERCTL=no
+ BOOTPROTO=static
+ IPADDR6=2607:f0d0:1002:0011::2/64
+ STARTMODE=auto
"""),
self.ifcfg_path('eth1'): dedent("""\
- BOOTPROTO=dhcp
- DEVICE=eth1
- NM_CONTROLLED=no
- ONBOOT=yes
- TYPE=Ethernet
- USERCTL=no
+ BOOTPROTO=dhcp4
+ STARTMODE=auto
"""),
}
self._apply_and_verify(self.distro.apply_network_config,
@@ -604,6 +600,93 @@ class TestNetCfgDistroOpensuse(TestNetCfgDistroBase):
expected_cfgs=expected_cfgs.copy())
+class TestNetCfgDistroArch(TestNetCfgDistroBase):
+ def setUp(self):
+ super(TestNetCfgDistroArch, self).setUp()
+ self.distro = self._get_distro('arch', renderers=['netplan'])
+
+ def _apply_and_verify(self, apply_fn, config, expected_cfgs=None,
+ bringup=False, with_netplan=False):
+ if not expected_cfgs:
+ raise ValueError('expected_cfg must not be None')
+
+ tmpd = None
+ with mock.patch('cloudinit.net.netplan.available',
+ return_value=with_netplan):
+ with self.reRooted(tmpd) as tmpd:
+ apply_fn(config, bringup)
+
+ results = dir2dict(tmpd)
+ for cfgpath, expected in expected_cfgs.items():
+ print("----------")
+ print(expected)
+ print("^^^^ expected | rendered VVVVVVV")
+ print(results[cfgpath])
+ print("----------")
+ self.assertEqual(expected, results[cfgpath])
+ self.assertEqual(0o644, get_mode(cfgpath, tmpd))
+
+ def netctl_path(self, iface):
+ return '/etc/netctl/%s' % iface
+
+ def netplan_path(self):
+ return '/etc/netplan/50-cloud-init.yaml'
+
+ def test_apply_network_config_v1_without_netplan(self):
+ # Note that this is in fact an invalid netctl config:
+ # "Address=None/None"
+ # But this is what the renderer has been writing out for a long time,
+ # and the test's purpose is to assert that the netctl renderer is
+ # still being used in absence of netplan, not the correctness of the
+ # rendered netctl config.
+ expected_cfgs = {
+ self.netctl_path('eth0'): dedent("""\
+ Address=192.168.1.5/255.255.255.0
+ Connection=ethernet
+ DNS=()
+ Gateway=192.168.1.254
+ IP=static
+ Interface=eth0
+ """),
+ self.netctl_path('eth1'): dedent("""\
+ Address=None/None
+ Connection=ethernet
+ DNS=()
+ Gateway=
+ IP=dhcp
+ Interface=eth1
+ """),
+ }
+
+ # ub_distro.apply_network_config(V1_NET_CFG, False)
+ self._apply_and_verify(self.distro.apply_network_config,
+ V1_NET_CFG,
+ expected_cfgs=expected_cfgs.copy(),
+ with_netplan=False)
+
+ def test_apply_network_config_v1_with_netplan(self):
+ expected_cfgs = {
+ self.netplan_path(): dedent("""\
+ # generated by cloud-init
+ network:
+ version: 2
+ ethernets:
+ eth0:
+ addresses:
+ - 192.168.1.5/24
+ gateway4: 192.168.1.254
+ eth1:
+ dhcp4: true
+ """),
+ }
+
+ with mock.patch('cloudinit.util.is_FreeBSD', return_value=False):
+ self._apply_and_verify(self.distro.apply_network_config,
+ V1_NET_CFG,
+ expected_cfgs=expected_cfgs.copy(),
+ with_netplan=True)
+
+
def get_mode(path, target=None):
return os.stat(util.target_path(target, path)).st_mode & 0o777
diff --git a/tests/unittests/test_distros/test_user_data_normalize.py b/tests/unittests/test_distros/test_user_data_normalize.py
index fa4b6cfe..a6faf0ef 100644
--- a/tests/unittests/test_distros/test_user_data_normalize.py
+++ b/tests/unittests/test_distros/test_user_data_normalize.py
@@ -1,12 +1,13 @@
# This file is part of cloud-init. See LICENSE file for license information.
+from unittest import mock
+
from cloudinit import distros
from cloudinit.distros import ug_util
from cloudinit import helpers
from cloudinit import settings
from cloudinit.tests.helpers import TestCase
-import mock
bcfg = {
diff --git a/tests/unittests/test_ds_identify.py b/tests/unittests/test_ds_identify.py
index 756b4fb4..36d7fbbf 100644
--- a/tests/unittests/test_ds_identify.py
+++ b/tests/unittests/test_ds_identify.py
@@ -140,7 +140,8 @@ class DsIdentifyBase(CiTestCase):
{'name': 'blkid', 'out': BLKID_EFI_ROOT},
{'name': 'ovf_vmware_transport_guestinfo',
'out': 'No value found', 'ret': 1},
-
+ {'name': 'dmi_decode', 'ret': 1,
+ 'err': 'No dmidecode program. ERROR.'},
]
written = [d['name'] for d in mocks]
@@ -195,6 +196,10 @@ class DsIdentifyBase(CiTestCase):
return self._check_via_dict(
data, RC_FOUND, dslist=[data.get('ds'), DS_NONE])
+ def _test_ds_not_found(self, name):
+ data = copy.deepcopy(VALID_CFG[name])
+ return self._check_via_dict(data, RC_NOT_FOUND)
+
def _check_via_dict(self, data, rc, dslist=None, **kwargs):
ret = self._call_via_dict(data, **kwargs)
good = False
@@ -244,9 +249,13 @@ class TestDsIdentify(DsIdentifyBase):
self._test_ds_found('Ec2-xen')
def test_brightbox_is_ec2(self):
- """EC2: product_serial ends with 'brightbox.com'"""
+ """EC2: product_serial ends with '.brightbox.com'"""
self._test_ds_found('Ec2-brightbox')
+ def test_bobrightbox_is_not_brightbox(self):
+ """EC2: bobrightbox.com in product_serial is not brightbox'"""
+ self._test_ds_not_found('Ec2-brightbox-negative')
+
def test_gce_by_product_name(self):
"""GCE identifies itself with product_name."""
self._test_ds_found('GCE')
@@ -259,10 +268,13 @@ class TestDsIdentify(DsIdentifyBase):
"""ConfigDrive datasource has a disk with LABEL=config-2."""
self._test_ds_found('ConfigDrive')
+ def test_rbx_cloud(self):
+ """Rbx datasource has a disk with LABEL=CLOUDMD."""
+ self._test_ds_found('RbxCloud')
+
def test_config_drive_upper(self):
"""ConfigDrive datasource has a disk with LABEL=CONFIG-2."""
self._test_ds_found('ConfigDriveUpper')
- return
def test_config_drive_seed(self):
"""Config Drive seed directory."""
@@ -435,13 +447,21 @@ class TestDsIdentify(DsIdentifyBase):
"""Open Telecom identification."""
self._test_ds_found('OpenStack-OpenTelekom')
+ def test_openstack_asset_tag_nova(self):
+ """OpenStack identification via asset tag OpenStack Nova."""
+ self._test_ds_found('OpenStack-AssetTag-Nova')
+
+ def test_openstack_asset_tag_copute(self):
+ """OpenStack identification via asset tag OpenStack Compute."""
+ self._test_ds_found('OpenStack-AssetTag-Compute')
+
def test_openstack_on_non_intel_is_maybe(self):
"""On non-Intel, openstack without dmi info is maybe.
nova does not identify itself on platforms other than intel.
https://bugs.launchpad.net/cloud-init/+bugs?field.tag=dsid-nova"""
- data = VALID_CFG['OpenStack'].copy()
+ data = copy.deepcopy(VALID_CFG['OpenStack'])
del data['files'][P_PRODUCT_NAME]
data.update({'policy_dmi': POLICY_FOUND_OR_MAYBE,
'policy_no_dmi': POLICY_FOUND_OR_MAYBE})
@@ -516,10 +536,38 @@ class TestDsIdentify(DsIdentifyBase):
self._check_via_dict(
ovf_cdrom_by_label, rc=RC_FOUND, dslist=['OVF', DS_NONE])
+ def test_ovf_on_vmware_iso_found_by_cdrom_with_different_size(self):
+ """OVF is identified by well-known iso9660 labels."""
+ ovf_cdrom_with_size = copy.deepcopy(VALID_CFG['OVF'])
+
+ # Set cdrom size to 20480 (10MB in 512 byte units)
+ ovf_cdrom_with_size['files']['sys/class/block/sr0/size'] = '20480\n'
+ self._check_via_dict(
+ ovf_cdrom_with_size, rc=RC_NOT_FOUND, policy_dmi="disabled")
+
+ # Set cdrom size to 204800 (100MB in 512 byte units)
+ ovf_cdrom_with_size['files']['sys/class/block/sr0/size'] = '204800\n'
+ self._check_via_dict(
+ ovf_cdrom_with_size, rc=RC_NOT_FOUND, policy_dmi="disabled")
+
+ # Set cdrom size to 18432 (9MB in 512 byte units)
+ ovf_cdrom_with_size['files']['sys/class/block/sr0/size'] = '18432\n'
+ self._check_via_dict(
+ ovf_cdrom_with_size, rc=RC_FOUND, dslist=['OVF', DS_NONE])
+
+ # Set cdrom size to 2048 (1MB in 512 byte units)
+ ovf_cdrom_with_size['files']['sys/class/block/sr0/size'] = '2048\n'
+ self._check_via_dict(
+ ovf_cdrom_with_size, rc=RC_FOUND, dslist=['OVF', DS_NONE])
+
def test_default_nocloud_as_vdb_iso9660(self):
"""NoCloud is found with iso9660 filesystem on non-cdrom disk."""
self._test_ds_found('NoCloud')
+ def test_nocloud_upper(self):
+ """NoCloud is found with uppercase filesystem label."""
+ self._test_ds_found('NoCloudUpper')
+
def test_nocloud_seed(self):
"""Nocloud seed directory."""
self._test_ds_found('NoCloud-seed')
@@ -565,6 +613,33 @@ class TestDsIdentify(DsIdentifyBase):
self.assertEqual(expected, [p for p in expected if p in toks],
"path did not have expected tokens")
+ def test_zstack_is_ec2(self):
+ """EC2: chassis asset tag ends with 'zstack.io'"""
+ self._test_ds_found('Ec2-ZStack')
+
+ def test_e24cloud_is_ec2(self):
+ """EC2: e24cloud identified by sys_vendor"""
+ self._test_ds_found('Ec2-E24Cloud')
+
+ def test_e24cloud_not_active(self):
+ """EC2: bobrightbox.com in product_serial is not brightbox'"""
+ self._test_ds_not_found('Ec2-E24Cloud-negative')
+
+
+class TestBSDNoSys(DsIdentifyBase):
+ """Test *BSD code paths
+
+ FreeBSD doesn't have /sys so we use dmidecode(8) here
+ It also doesn't have systemd-detect-virt(8), so we use sysctl(8) to query
+ kern.vm_guest, and optionally map it"""
+
+ def test_dmi_decode(self):
+ """Test that dmidecode(8) works on systems which don't have /sys
+
+ This will be used on *BSD systems.
+ """
+ self._test_ds_found('Hetzner-dmidecode')
+
class TestIsIBMProvisioning(DsIdentifyBase):
"""Test the is_ibm_provisioning method in ds-identify."""
@@ -688,7 +763,11 @@ VALID_CFG = {
},
'Ec2-brightbox': {
'ds': 'Ec2',
- 'files': {P_PRODUCT_SERIAL: 'facc6e2f.brightbox.com\n'},
+ 'files': {P_PRODUCT_SERIAL: 'srv-otuxg.gb1.brightbox.com\n'},
+ },
+ 'Ec2-brightbox-negative': {
+ 'ds': 'Ec2',
+ 'files': {P_PRODUCT_SERIAL: 'tricky-host.bobrightbox.com\n'},
},
'GCE': {
'ds': 'GCE',
@@ -713,6 +792,19 @@ VALID_CFG = {
'dev/vdb': 'pretend iso content for cidata\n',
}
},
+ 'NoCloudUpper': {
+ 'ds': 'NoCloud',
+ 'mocks': [
+ MOCK_VIRT_IS_KVM,
+ {'name': 'blkid', 'ret': 0,
+ 'out': blkid_out(
+ BLKID_UEFI_UBUNTU +
+ [{'DEVNAME': 'vdb', 'TYPE': 'iso9660', 'LABEL': 'CIDATA'}])},
+ ],
+ 'files': {
+ 'dev/vdb': 'pretend iso content for cidata\n',
+ }
+ },
'NoCloud-seed': {
'ds': 'NoCloud',
'files': {
@@ -742,6 +834,18 @@ VALID_CFG = {
'files': {P_CHASSIS_ASSET_TAG: 'OpenTelekomCloud\n'},
'mocks': [MOCK_VIRT_IS_XEN],
},
+ 'OpenStack-AssetTag-Nova': {
+ # VMware vSphere can't modify product-name, LP: #1669875
+ 'ds': 'OpenStack',
+ 'files': {P_CHASSIS_ASSET_TAG: 'OpenStack Nova\n'},
+ 'mocks': [MOCK_VIRT_IS_XEN],
+ },
+ 'OpenStack-AssetTag-Compute': {
+ # VMware vSphere can't modify product-name, LP: #1669875
+ 'ds': 'OpenStack',
+ 'files': {P_CHASSIS_ASSET_TAG: 'OpenStack Compute\n'},
+ 'mocks': [MOCK_VIRT_IS_XEN],
+ },
'OVF-seed': {
'ds': 'OVF',
'files': {
@@ -778,6 +882,7 @@ VALID_CFG = {
],
'files': {
'dev/sr0': 'pretend ovf iso has ' + OVF_MATCH_STRING + '\n',
+ 'sys/class/block/sr0/size': '2048\n',
}
},
'OVF-guestinfo': {
@@ -818,10 +923,28 @@ VALID_CFG = {
os.path.join(P_SEED_DIR, 'config_drive', 'openstack',
'latest', 'meta_data.json'): 'md\n'},
},
+ 'RbxCloud': {
+ 'ds': 'RbxCloud',
+ 'mocks': [
+ {'name': 'blkid', 'ret': 0,
+ 'out': blkid_out(
+ [{'DEVNAME': 'vda1', 'TYPE': 'vfat', 'PARTUUID': uuid4()},
+ {'DEVNAME': 'vda2', 'TYPE': 'ext4',
+ 'LABEL': 'cloudimg-rootfs', 'PARTUUID': uuid4()},
+ {'DEVNAME': 'vdb', 'TYPE': 'vfat', 'LABEL': 'CLOUDMD'}]
+ )},
+ ],
+ },
'Hetzner': {
'ds': 'Hetzner',
'files': {P_SYS_VENDOR: 'Hetzner\n'},
},
+ 'Hetzner-dmidecode': {
+ 'ds': 'Hetzner',
+ 'mocks': [
+ {'name': 'dmi_decode', 'ret': 0, 'RET': 'Hetzner'}
+ ],
+ },
'IBMCloud-metadata': {
'ds': 'IBMCloud',
'mocks': [
@@ -897,8 +1020,19 @@ VALID_CFG = {
{'name': 'blkid', 'ret': 2, 'out': ''},
],
'files': {ds_smartos.METADATA_SOCKFILE: 'would be a socket\n'},
- }
-
+ },
+ 'Ec2-ZStack': {
+ 'ds': 'Ec2',
+ 'files': {P_CHASSIS_ASSET_TAG: '123456.zstack.io\n'},
+ },
+ 'Ec2-E24Cloud': {
+ 'ds': 'Ec2',
+ 'files': {P_SYS_VENDOR: 'e24cloud\n'},
+ },
+ 'Ec2-E24Cloud-negative': {
+ 'ds': 'Ec2',
+ 'files': {P_SYS_VENDOR: 'e24cloudyday\n'},
+ }
}
# vi: ts=4 expandtab
diff --git a/tests/unittests/test_filters/test_launch_index.py b/tests/unittests/test_filters/test_launch_index.py
index e1a5d2c8..1492361e 100644
--- a/tests/unittests/test_filters/test_launch_index.py
+++ b/tests/unittests/test_filters/test_launch_index.py
@@ -1,11 +1,10 @@
# This file is part of cloud-init. See LICENSE file for license information.
import copy
+from itertools import filterfalse
from cloudinit.tests import helpers
-from six.moves import filterfalse
-
from cloudinit.filters import launch_index
from cloudinit import user_data as ud
from cloudinit import util
diff --git a/tests/unittests/test_handler/test_handler_apt_configure_sources_list_v1.py b/tests/unittests/test_handler/test_handler_apt_configure_sources_list_v1.py
index 23bd6e10..69009a44 100644
--- a/tests/unittests/test_handler/test_handler_apt_configure_sources_list_v1.py
+++ b/tests/unittests/test_handler/test_handler_apt_configure_sources_list_v1.py
@@ -7,11 +7,7 @@ import logging
import os
import shutil
import tempfile
-
-try:
- from unittest import mock
-except ImportError:
- import mock
+from unittest import mock
from cloudinit import cloud
from cloudinit import distros
@@ -78,7 +74,7 @@ class TestAptSourceConfigSourceList(t_help.FilesystemMockingTestCase):
get_rel = rpatcher.start()
get_rel.return_value = {'codename': "fakerelease"}
self.addCleanup(rpatcher.stop)
- apatcher = mock.patch("cloudinit.util.get_architecture")
+ apatcher = mock.patch("cloudinit.util.get_dpkg_architecture")
get_arch = apatcher.start()
get_arch.return_value = 'amd64'
self.addCleanup(apatcher.stop)
diff --git a/tests/unittests/test_handler/test_handler_apt_configure_sources_list_v3.py b/tests/unittests/test_handler/test_handler_apt_configure_sources_list_v3.py
index f7608c28..0aa3d51a 100644
--- a/tests/unittests/test_handler/test_handler_apt_configure_sources_list_v3.py
+++ b/tests/unittests/test_handler/test_handler_apt_configure_sources_list_v3.py
@@ -7,12 +7,8 @@ import logging
import os
import shutil
import tempfile
-
-try:
- from unittest import mock
-except ImportError:
- import mock
-from mock import call
+from unittest import mock
+from unittest.mock import call
from cloudinit import cloud
from cloudinit import distros
@@ -106,7 +102,7 @@ class TestAptSourceConfigSourceList(t_help.FilesystemMockingTestCase):
get_rel = rpatcher.start()
get_rel.return_value = {'codename': "fakerel"}
self.addCleanup(rpatcher.stop)
- apatcher = mock.patch("cloudinit.util.get_architecture")
+ apatcher = mock.patch("cloudinit.util.get_dpkg_architecture")
get_arch = apatcher.start()
get_arch.return_value = 'amd64'
self.addCleanup(apatcher.stop)
diff --git a/tests/unittests/test_handler/test_handler_apt_source_v1.py b/tests/unittests/test_handler/test_handler_apt_source_v1.py
index a3132fbd..866752ef 100644
--- a/tests/unittests/test_handler/test_handler_apt_source_v1.py
+++ b/tests/unittests/test_handler/test_handler_apt_source_v1.py
@@ -9,12 +9,8 @@ import os
import re
import shutil
import tempfile
-
-try:
- from unittest import mock
-except ImportError:
- import mock
-from mock import call
+from unittest import mock
+from unittest.mock import call
from cloudinit.config import cc_apt_configure
from cloudinit import gpg
@@ -77,7 +73,7 @@ class TestAptSourceConfig(TestCase):
get_rel = rpatcher.start()
get_rel.return_value = {'codename': self.release}
self.addCleanup(rpatcher.stop)
- apatcher = mock.patch("cloudinit.util.get_architecture")
+ apatcher = mock.patch("cloudinit.util.get_dpkg_architecture")
get_arch = apatcher.start()
get_arch.return_value = 'amd64'
self.addCleanup(apatcher.stop)
diff --git a/tests/unittests/test_handler/test_handler_apt_source_v3.py b/tests/unittests/test_handler/test_handler_apt_source_v3.py
index 90fe6eed..90949b6d 100644
--- a/tests/unittests/test_handler/test_handler_apt_source_v3.py
+++ b/tests/unittests/test_handler/test_handler_apt_source_v3.py
@@ -11,13 +11,8 @@ import shutil
import socket
import tempfile
-from unittest import TestCase
-
-try:
- from unittest import mock
-except ImportError:
- import mock
-from mock import call
+from unittest import TestCase, mock
+from unittest.mock import call
from cloudinit import cloud
from cloudinit import distros
@@ -453,14 +448,14 @@ class TestAptSourceConfig(t_help.FilesystemMockingTestCase):
self.assertFalse(os.path.isfile(self.aptlistfile2))
self.assertFalse(os.path.isfile(self.aptlistfile3))
- @mock.patch("cloudinit.config.cc_apt_configure.util.get_architecture")
- def test_apt_v3_list_rename(self, m_get_architecture):
+ @mock.patch("cloudinit.config.cc_apt_configure.util.get_dpkg_architecture")
+ def test_apt_v3_list_rename(self, m_get_dpkg_architecture):
"""test_apt_v3_list_rename - Test find mirror and apt list renaming"""
pre = "/var/lib/apt/lists"
# filenames are archive dependent
arch = 's390x'
- m_get_architecture.return_value = arch
+ m_get_dpkg_architecture.return_value = arch
component = "ubuntu-ports"
archive = "ports.ubuntu.com"
@@ -487,16 +482,17 @@ class TestAptSourceConfig(t_help.FilesystemMockingTestCase):
with mock.patch.object(os, 'rename') as mockren:
with mock.patch.object(glob, 'glob',
return_value=[fromfn]):
- cc_apt_configure.rename_apt_lists(mirrors, TARGET)
+ cc_apt_configure.rename_apt_lists(mirrors, TARGET, arch)
mockren.assert_any_call(fromfn, tofn)
- @mock.patch("cloudinit.config.cc_apt_configure.util.get_architecture")
- def test_apt_v3_list_rename_non_slash(self, m_get_architecture):
+ @mock.patch("cloudinit.config.cc_apt_configure.util.get_dpkg_architecture")
+ def test_apt_v3_list_rename_non_slash(self, m_get_dpkg_architecture):
target = os.path.join(self.tmp, "rename_non_slash")
apt_lists_d = os.path.join(target, "./" + cc_apt_configure.APT_LISTS)
- m_get_architecture.return_value = 'amd64'
+ arch = 'amd64'
+ m_get_dpkg_architecture.return_value = arch
mirror_path = "some/random/path/"
primary = "http://test.ubuntu.com/" + mirror_path
@@ -532,7 +528,7 @@ class TestAptSourceConfig(t_help.FilesystemMockingTestCase):
fpath = os.path.join(apt_lists_d, opre + suff)
util.write_file(fpath, content=fpath)
- cc_apt_configure.rename_apt_lists(mirrors, target)
+ cc_apt_configure.rename_apt_lists(mirrors, target, arch)
found = sorted(os.listdir(apt_lists_d))
self.assertEqual(expected, found)
@@ -625,10 +621,12 @@ class TestAptSourceConfig(t_help.FilesystemMockingTestCase):
self.assertEqual(mirrors['SECURITY'],
smir)
- @mock.patch("cloudinit.config.cc_apt_configure.util.get_architecture")
- def test_apt_v3_get_def_mir_non_intel_no_arch(self, m_get_architecture):
+ @mock.patch("cloudinit.config.cc_apt_configure.util.get_dpkg_architecture")
+ def test_apt_v3_get_def_mir_non_intel_no_arch(
+ self, m_get_dpkg_architecture
+ ):
arch = 'ppc64el'
- m_get_architecture.return_value = arch
+ m_get_dpkg_architecture.return_value = arch
expected = {'PRIMARY': 'http://ports.ubuntu.com/ubuntu-ports',
'SECURITY': 'http://ports.ubuntu.com/ubuntu-ports'}
self.assertEqual(expected, cc_apt_configure.get_default_mirrors())
@@ -998,6 +996,17 @@ deb http://ubuntu.com/ubuntu/ xenial-proposed main""")
class TestDebconfSelections(TestCase):
+ @mock.patch("cloudinit.config.cc_apt_configure.util.subp")
+ def test_set_sel_appends_newline_if_absent(self, m_subp):
+ """Automatically append a newline to debconf-set-selections config."""
+ selections = b'some/setting boolean true'
+ cc_apt_configure.debconf_set_selections(selections=selections)
+ cc_apt_configure.debconf_set_selections(selections=selections + b'\n')
+ m_call = mock.call(
+ ['debconf-set-selections'], data=selections + b'\n', capture=True,
+ target=None)
+ self.assertEqual([m_call, m_call], m_subp.call_args_list)
+
@mock.patch("cloudinit.config.cc_apt_configure.debconf_set_selections")
def test_no_set_sel_if_none_to_set(self, m_set_sel):
cc_apt_configure.apply_debconf_selections({'foo': 'bar'})
diff --git a/tests/unittests/test_handler/test_handler_ca_certs.py b/tests/unittests/test_handler/test_handler_ca_certs.py
index 06e14db0..5b4105dd 100644
--- a/tests/unittests/test_handler/test_handler_ca_certs.py
+++ b/tests/unittests/test_handler/test_handler_ca_certs.py
@@ -11,12 +11,9 @@ import logging
import shutil
import tempfile
import unittest
+from unittest import mock
try:
- from unittest import mock
-except ImportError:
- import mock
-try:
from contextlib import ExitStack
except ImportError:
from contextlib2 import ExitStack
diff --git a/tests/unittests/test_handler/test_handler_chef.py b/tests/unittests/test_handler/test_handler_chef.py
index b16532ea..2dab3a54 100644
--- a/tests/unittests/test_handler/test_handler_chef.py
+++ b/tests/unittests/test_handler/test_handler_chef.py
@@ -4,7 +4,6 @@ import httpretty
import json
import logging
import os
-import six
from cloudinit import cloud
from cloudinit.config import cc_chef
@@ -145,6 +144,7 @@ class TestChef(FilesystemMockingTestCase):
file_backup_path "/var/backups/chef"
pid_file "/var/run/chef/client.pid"
Chef::Log::Formatter.show_time = true
+ encrypted_data_bag_secret "/etc/chef/encrypted_data_bag_secret"
"""
tpl_file = util.load_file('templates/chef_client.rb.tmpl')
self.patchUtils(self.tmp)
@@ -157,6 +157,8 @@ class TestChef(FilesystemMockingTestCase):
'validation_name': 'bob',
'validation_key': "/etc/chef/vkey.pem",
'validation_cert': "this is my cert",
+ 'encrypted_data_bag_secret':
+ '/etc/chef/encrypted_data_bag_secret'
},
}
cc_chef.handle('chef', cfg, self.fetch_cloud('ubuntu'), LOG, [])
@@ -175,7 +177,7 @@ class TestChef(FilesystemMockingTestCase):
continue
# the value from the cfg overrides that in the default
val = cfg['chef'].get(k, v)
- if isinstance(val, six.string_types):
+ if isinstance(val, str):
self.assertIn(val, c)
c = util.load_file(cc_chef.CHEF_FB_PATH)
self.assertEqual({}, json.loads(c))
diff --git a/tests/unittests/test_handler/test_handler_disk_setup.py b/tests/unittests/test_handler/test_handler_disk_setup.py
index 5afcacaf..0e51f17a 100644
--- a/tests/unittests/test_handler/test_handler_disk_setup.py
+++ b/tests/unittests/test_handler/test_handler_disk_setup.py
@@ -222,4 +222,22 @@ class TestMkfsCommandHandling(CiTestCase):
'-L', 'without_cmd', '-F', 'are', 'added'],
shell=False)
+ @mock.patch('cloudinit.config.cc_disk_setup.util.which')
+ def test_mkswap(self, m_which, subp, *args):
+ """mkfs observes extra_opts and overwrite settings when cmd is not
+ present."""
+ m_which.side_effect = iter([None, '/sbin/mkswap'])
+ cc_disk_setup.mkfs({
+ 'filesystem': 'swap',
+ 'device': '/dev/xdb1',
+ 'label': 'swap',
+ 'overwrite': True,
+ })
+
+ self.assertEqual([mock.call('mkfs.swap'), mock.call('mkswap')],
+ m_which.call_args_list)
+ subp.assert_called_once_with(
+ ['/sbin/mkswap', '/dev/xdb1', '-L', 'swap', '-f'], shell=False)
+
+#
# vi: ts=4 expandtab
diff --git a/tests/unittests/test_handler/test_handler_growpart.py b/tests/unittests/test_handler/test_handler_growpart.py
index a3e46351..43b53745 100644
--- a/tests/unittests/test_handler/test_handler_growpart.py
+++ b/tests/unittests/test_handler/test_handler_growpart.py
@@ -11,12 +11,9 @@ import logging
import os
import re
import unittest
+from unittest import mock
try:
- from unittest import mock
-except ImportError:
- import mock
-try:
from contextlib import ExitStack
except ImportError:
from contextlib2 import ExitStack
@@ -52,6 +49,18 @@ growpart disk partition
Resize partition 1 on /dev/sda
"""
+HELP_GPART = """
+usage: gpart add -t type [-a alignment] [-b start] <SNIP> geom
+ gpart backup geom
+ gpart bootcode [-b bootcode] [-p partcode -i index] [-f flags] geom
+<SNIP>
+ gpart resize -i index [-a alignment] [-s size] [-f flags] geom
+ gpart restore [-lF] [-f flags] provider [...]
+ gpart recover [-f flags] geom
+ gpart help
+<SNIP>
+"""
+
class TestDisabled(unittest.TestCase):
def setUp(self):
@@ -97,8 +106,9 @@ class TestConfig(TestCase):
self.handle(self.name, config, self.cloud_init, self.log,
self.args)
- mockobj.assert_called_once_with(
- ['growpart', '--help'], env={'LANG': 'C'})
+ mockobj.assert_has_calls([
+ mock.call(['growpart', '--help'], env={'LANG': 'C'}),
+ mock.call(['gpart', 'help'], env={'LANG': 'C'}, rcs=[0, 1])])
@mock.patch.dict("os.environ", clear=True)
def test_no_resizers_mode_growpart_is_exception(self):
@@ -124,6 +134,18 @@ class TestConfig(TestCase):
mockobj.assert_called_once_with(
['growpart', '--help'], env={'LANG': 'C'})
+ @mock.patch.dict("os.environ", clear=True)
+ def test_mode_auto_falls_back_to_gpart(self):
+ with mock.patch.object(
+ util, 'subp',
+ return_value=("", HELP_GPART)) as mockobj:
+ ret = cc_growpart.resizer_factory(mode="auto")
+ self.assertIsInstance(ret, cc_growpart.ResizeGpart)
+
+ mockobj.assert_has_calls([
+ mock.call(['growpart', '--help'], env={'LANG': 'C'}),
+ mock.call(['gpart', 'help'], env={'LANG': 'C'}, rcs=[0, 1])])
+
def test_handle_with_no_growpart_entry(self):
# if no 'growpart' entry in config, then mode=auto should be used
diff --git a/tests/unittests/test_handler/test_handler_locale.py b/tests/unittests/test_handler/test_handler_locale.py
index e29a06f9..2b22559f 100644
--- a/tests/unittests/test_handler/test_handler_locale.py
+++ b/tests/unittests/test_handler/test_handler_locale.py
@@ -17,13 +17,12 @@ from cloudinit.tests import helpers as t_help
from configobj import ConfigObj
-from six import BytesIO
-
import logging
-import mock
import os
import shutil
import tempfile
+from io import BytesIO
+from unittest import mock
LOG = logging.getLogger(__name__)
diff --git a/tests/unittests/test_handler/test_handler_lxd.py b/tests/unittests/test_handler/test_handler_lxd.py
index 2478ebc4..40b521e5 100644
--- a/tests/unittests/test_handler/test_handler_lxd.py
+++ b/tests/unittests/test_handler/test_handler_lxd.py
@@ -5,10 +5,7 @@ from cloudinit.sources import DataSourceNoCloud
from cloudinit import (distros, helpers, cloud)
from cloudinit.tests import helpers as t_help
-try:
- from unittest import mock
-except ImportError:
- import mock
+from unittest import mock
class TestLxd(t_help.CiTestCase):
@@ -62,7 +59,7 @@ class TestLxd(t_help.CiTestCase):
cc_lxd.handle('cc_lxd', self.lxd_cfg, cc, self.logger, [])
self.assertFalse(m_maybe_clean.called)
install_pkg = cc.distro.install_packages.call_args_list[0][0][0]
- self.assertEqual(sorted(install_pkg), ['lxd', 'zfs'])
+ self.assertEqual(sorted(install_pkg), ['lxd', 'zfsutils-linux'])
@mock.patch("cloudinit.config.cc_lxd.maybe_cleanup_default")
@mock.patch("cloudinit.config.cc_lxd.util")
diff --git a/tests/unittests/test_handler/test_handler_mcollective.py b/tests/unittests/test_handler/test_handler_mcollective.py
index 7eec7352..c013a538 100644
--- a/tests/unittests/test_handler/test_handler_mcollective.py
+++ b/tests/unittests/test_handler/test_handler_mcollective.py
@@ -10,8 +10,8 @@ import configobj
import logging
import os
import shutil
-from six import BytesIO
import tempfile
+from io import BytesIO
LOG = logging.getLogger(__name__)
diff --git a/tests/unittests/test_handler/test_handler_mounts.py b/tests/unittests/test_handler/test_handler_mounts.py
index 8fea6c2a..05ac183e 100644
--- a/tests/unittests/test_handler/test_handler_mounts.py
+++ b/tests/unittests/test_handler/test_handler_mounts.py
@@ -1,16 +1,12 @@
# This file is part of cloud-init. See LICENSE file for license information.
import os.path
+from unittest import mock
from cloudinit.config import cc_mounts
from cloudinit.tests import helpers as test_helpers
-try:
- from unittest import mock
-except ImportError:
- import mock
-
class TestSanitizeDevname(test_helpers.FilesystemMockingTestCase):
@@ -154,7 +150,15 @@ class TestFstabHandling(test_helpers.FilesystemMockingTestCase):
return_value=True)
self.add_patch('cloudinit.config.cc_mounts.util.subp',
- 'mock_util_subp')
+ 'm_util_subp')
+
+ self.add_patch('cloudinit.config.cc_mounts.util.mounts',
+ 'mock_util_mounts',
+ return_value={
+ '/dev/sda1': {'fstype': 'ext4',
+ 'mountpoint': '/',
+ 'opts': 'rw,relatime,discard'
+ }})
self.mock_cloud = mock.Mock()
self.mock_log = mock.Mock()
@@ -173,6 +177,18 @@ class TestFstabHandling(test_helpers.FilesystemMockingTestCase):
return dev
+ def test_swap_integrity(self):
+ '''Ensure that the swap file is correctly created and can
+ swapon successfully. Fixing the corner case of:
+ kernel: swapon: swapfile has holes'''
+
+ fstab = '/swap.img swap swap defaults 0 0\n'
+
+ with open(cc_mounts.FSTAB_PATH, 'w') as fd:
+ fd.write(fstab)
+ cc = {'swap': ['filename: /swap.img', 'size: 512', 'maxsize: 512']}
+ cc_mounts.handle(None, cc, self.mock_cloud, self.mock_log, [])
+
def test_fstab_no_swap_device(self):
'''Ensure that cloud-init adds a discovered swap partition
to /etc/fstab.'''
@@ -230,4 +246,24 @@ class TestFstabHandling(test_helpers.FilesystemMockingTestCase):
fstab_new_content = fd.read()
self.assertEqual(fstab_expected_content, fstab_new_content)
+ def test_no_change_fstab_sets_needs_mount_all(self):
+ '''verify unchanged fstab entries are mounted if not call mount -a'''
+ fstab_original_content = (
+ 'LABEL=cloudimg-rootfs / ext4 defaults 0 0\n'
+ 'LABEL=UEFI /boot/efi vfat defaults 0 0\n'
+ '/dev/vdb /mnt auto defaults,noexec,comment=cloudconfig 0 2\n'
+ )
+ fstab_expected_content = fstab_original_content
+ cc = {'mounts': [
+ ['/dev/vdb', '/mnt', 'auto', 'defaults,noexec']]}
+ with open(cc_mounts.FSTAB_PATH, 'w') as fd:
+ fd.write(fstab_original_content)
+ with open(cc_mounts.FSTAB_PATH, 'r') as fd:
+ fstab_new_content = fd.read()
+ self.assertEqual(fstab_expected_content, fstab_new_content)
+ cc_mounts.handle(None, cc, self.mock_cloud, self.mock_log, [])
+ self.m_util_subp.assert_has_calls([
+ mock.call(['mount', '-a']),
+ mock.call(['systemctl', 'daemon-reload'])])
+
# vi: ts=4 expandtab
diff --git a/tests/unittests/test_handler/test_handler_ntp.py b/tests/unittests/test_handler/test_handler_ntp.py
index 0f22e579..463d892a 100644
--- a/tests/unittests/test_handler/test_handler_ntp.py
+++ b/tests/unittests/test_handler/test_handler_ntp.py
@@ -268,17 +268,22 @@ class TestNtp(FilesystemMockingTestCase):
template_fn=template_fn)
content = util.load_file(confpath)
if client in ['ntp', 'chrony']:
- expected_servers = '\n'.join([
- 'server {0} iburst'.format(srv) for srv in servers])
+ content_lines = content.splitlines()
+ expected_servers = [
+ 'server {0} iburst'.format(srv) for srv in servers]
print('distro=%s client=%s' % (distro, client))
- self.assertIn(expected_servers, content,
- ('failed to render {0} conf'
- ' for distro:{1}'.format(client, distro)))
- expected_pools = '\n'.join([
- 'pool {0} iburst'.format(pool) for pool in pools])
- self.assertIn(expected_pools, content,
- ('failed to render {0} conf'
- ' for distro:{1}'.format(client, distro)))
+ for sline in expected_servers:
+ self.assertIn(sline, content_lines,
+ ('failed to render {0} conf'
+ ' for distro:{1}'.format(client,
+ distro)))
+ expected_pools = [
+ 'pool {0} iburst'.format(pool) for pool in pools]
+ for pline in expected_pools:
+ self.assertIn(pline, content_lines,
+ ('failed to render {0} conf'
+ ' for distro:{1}'.format(client,
+ distro)))
elif client == 'systemd-timesyncd':
expected_content = (
"# cloud-init generated file\n" +
diff --git a/tests/unittests/test_handler/test_handler_power_state.py b/tests/unittests/test_handler/test_handler_power_state.py
index 3c726422..0d8d17b9 100644
--- a/tests/unittests/test_handler/test_handler_power_state.py
+++ b/tests/unittests/test_handler/test_handler_power_state.py
@@ -90,7 +90,7 @@ class TestCheckCondition(t_help.TestCase):
mocklog = mock.Mock()
self.assertEqual(
psc.check_condition(self.cmd_with_exit(2), mocklog), False)
- self.assertEqual(mocklog.warn.call_count, 1)
+ self.assertEqual(mocklog.warning.call_count, 1)
def check_lps_ret(psc_return, mode=None):
diff --git a/tests/unittests/test_handler/test_handler_puppet.py b/tests/unittests/test_handler/test_handler_puppet.py
index 0b6e3b58..1494177d 100644
--- a/tests/unittests/test_handler/test_handler_puppet.py
+++ b/tests/unittests/test_handler/test_handler_puppet.py
@@ -6,6 +6,7 @@ from cloudinit import (distros, helpers, cloud, util)
from cloudinit.tests.helpers import CiTestCase, mock
import logging
+import textwrap
LOG = logging.getLogger(__name__)
@@ -64,6 +65,7 @@ class TestPuppetHandle(CiTestCase):
super(TestPuppetHandle, self).setUp()
self.new_root = self.tmp_dir()
self.conf = self.tmp_path('puppet.conf')
+ self.csr_attributes_path = self.tmp_path('csr_attributes.yaml')
def _get_cloud(self, distro):
paths = helpers.Paths({'templates_dir': self.new_root})
@@ -140,3 +142,35 @@ class TestPuppetHandle(CiTestCase):
content = util.load_file(self.conf)
expected = '[agent]\nserver = puppetmaster.example.org\nother = 3\n\n'
self.assertEqual(expected, content)
+
+ @mock.patch('cloudinit.config.cc_puppet.util.subp')
+ def test_handler_puppet_writes_csr_attributes_file(self, m_subp, m_auto):
+ """When csr_attributes is provided
+ creates file in PUPPET_CSR_ATTRIBUTES_PATH."""
+ mycloud = self._get_cloud('ubuntu')
+ mycloud.distro = mock.MagicMock()
+ cfg = {
+ 'puppet': {
+ 'csr_attributes': {
+ 'custom_attributes': {
+ '1.2.840.113549.1.9.7': '342thbjkt82094y0ut'
+ 'hhor289jnqthpc2290'},
+ 'extension_requests': {
+ 'pp_uuid': 'ED803750-E3C7-44F5-BB08-41A04433FE2E',
+ 'pp_image_name': 'my_ami_image',
+ 'pp_preshared_key': '342thbjkt82094y0uthhor289jnqthpc2290'}
+ }}}
+ csr_attributes = 'cloudinit.config.cc_puppet.' \
+ 'PUPPET_CSR_ATTRIBUTES_PATH'
+ with mock.patch(csr_attributes, self.csr_attributes_path):
+ cc_puppet.handle('notimportant', cfg, mycloud, LOG, None)
+ content = util.load_file(self.csr_attributes_path)
+ expected = textwrap.dedent("""\
+ custom_attributes:
+ 1.2.840.113549.1.9.7: 342thbjkt82094y0uthhor289jnqthpc2290
+ extension_requests:
+ pp_image_name: my_ami_image
+ pp_preshared_key: 342thbjkt82094y0uthhor289jnqthpc2290
+ pp_uuid: ED803750-E3C7-44F5-BB08-41A04433FE2E
+ """)
+ self.assertEqual(expected, content)
diff --git a/tests/unittests/test_handler/test_handler_resizefs.py b/tests/unittests/test_handler/test_handler_resizefs.py
index 35187847..db9a0414 100644
--- a/tests/unittests/test_handler/test_handler_resizefs.py
+++ b/tests/unittests/test_handler/test_handler_resizefs.py
@@ -147,7 +147,7 @@ class TestResizefs(CiTestCase):
def test_resize_ufs_cmd_return(self):
mount_point = '/'
devpth = '/dev/sda2'
- self.assertEqual(('growfs', '-y', devpth),
+ self.assertEqual(('growfs', '-y', mount_point),
_resize_ufs(mount_point, devpth))
@mock.patch('cloudinit.util.is_container', return_value=False)
diff --git a/tests/unittests/test_handler/test_handler_seed_random.py b/tests/unittests/test_handler/test_handler_seed_random.py
index f60dedc2..abecc53b 100644
--- a/tests/unittests/test_handler/test_handler_seed_random.py
+++ b/tests/unittests/test_handler/test_handler_seed_random.py
@@ -12,8 +12,7 @@ from cloudinit.config import cc_seed_random
import gzip
import tempfile
-
-from six import BytesIO
+from io import BytesIO
from cloudinit import cloud
from cloudinit import distros
diff --git a/tests/unittests/test_handler/test_handler_set_hostname.py b/tests/unittests/test_handler/test_handler_set_hostname.py
index d09ec23a..58abf51a 100644
--- a/tests/unittests/test_handler/test_handler_set_hostname.py
+++ b/tests/unittests/test_handler/test_handler_set_hostname.py
@@ -13,8 +13,8 @@ from configobj import ConfigObj
import logging
import os
import shutil
-from six import BytesIO
import tempfile
+from io import BytesIO
LOG = logging.getLogger(__name__)
diff --git a/tests/unittests/test_handler/test_handler_snappy.py b/tests/unittests/test_handler/test_handler_snappy.py
deleted file mode 100644
index 76b79c29..00000000
--- a/tests/unittests/test_handler/test_handler_snappy.py
+++ /dev/null
@@ -1,601 +0,0 @@
-# This file is part of cloud-init. See LICENSE file for license information.
-
-from cloudinit.config.cc_snappy import (
- makeop, get_package_ops, render_snap_op)
-from cloudinit.config.cc_snap_config import (
- add_assertions, add_snap_user, ASSERTIONS_FILE)
-from cloudinit import (distros, helpers, cloud, util)
-from cloudinit.config.cc_snap_config import handle as snap_handle
-from cloudinit.sources import DataSourceNone
-from cloudinit.tests.helpers import FilesystemMockingTestCase, mock
-
-from cloudinit.tests import helpers as t_help
-
-import logging
-import os
-import shutil
-import tempfile
-import textwrap
-import yaml
-
-LOG = logging.getLogger(__name__)
-ALLOWED = (dict, list, int, str)
-
-
-class TestInstallPackages(t_help.TestCase):
- def setUp(self):
- super(TestInstallPackages, self).setUp()
- self.unapply = []
-
- # by default 'which' has nothing in its path
- self.apply_patches([(util, 'subp', self._subp)])
- self.subp_called = []
- self.snapcmds = []
- self.tmp = tempfile.mkdtemp(prefix="TestInstallPackages")
-
- def tearDown(self):
- apply_patches([i for i in reversed(self.unapply)])
- shutil.rmtree(self.tmp)
-
- def apply_patches(self, patches):
- ret = apply_patches(patches)
- self.unapply += ret
-
- def populate_tmp(self, files):
- return t_help.populate_dir(self.tmp, files)
-
- def _subp(self, *args, **kwargs):
- # supports subp calling with cmd as args or kwargs
- if 'args' not in kwargs:
- kwargs['args'] = args[0]
- self.subp_called.append(kwargs)
- args = kwargs['args']
- # here we basically parse the snappy command invoked
- # and append to snapcmds a list of (mode, pkg, config)
- if args[0:2] == ['snappy', 'config']:
- if args[3] == "-":
- config = kwargs.get('data', '')
- else:
- with open(args[3], "rb") as fp:
- config = yaml.safe_load(fp.read())
- self.snapcmds.append(['config', args[2], config])
- elif args[0:2] == ['snappy', 'install']:
- config = None
- pkg = None
- for arg in args[2:]:
- if arg.startswith("-"):
- continue
- if not pkg:
- pkg = arg
- elif not config:
- cfgfile = arg
- if cfgfile == "-":
- config = kwargs.get('data', '')
- elif cfgfile:
- with open(cfgfile, "rb") as fp:
- config = yaml.safe_load(fp.read())
- self.snapcmds.append(['install', pkg, config])
-
- def test_package_ops_1(self):
- ret = get_package_ops(
- packages=['pkg1', 'pkg2', 'pkg3'],
- configs={'pkg2': b'mycfg2'}, installed=[])
- self.assertEqual(
- ret, [makeop('install', 'pkg1', None, None),
- makeop('install', 'pkg2', b'mycfg2', None),
- makeop('install', 'pkg3', None, None)])
-
- def test_package_ops_config_only(self):
- ret = get_package_ops(
- packages=None,
- configs={'pkg2': b'mycfg2'}, installed=['pkg1', 'pkg2'])
- self.assertEqual(
- ret, [makeop('config', 'pkg2', b'mycfg2')])
-
- def test_package_ops_install_and_config(self):
- ret = get_package_ops(
- packages=['pkg3', 'pkg2'],
- configs={'pkg2': b'mycfg2', 'xinstalled': b'xcfg'},
- installed=['xinstalled'])
- self.assertEqual(
- ret, [makeop('install', 'pkg3'),
- makeop('install', 'pkg2', b'mycfg2'),
- makeop('config', 'xinstalled', b'xcfg')])
-
- def test_package_ops_install_long_config_short(self):
- # a package can be installed by full name, but have config by short
- cfg = {'k1': 'k2'}
- ret = get_package_ops(
- packages=['config-example.canonical'],
- configs={'config-example': cfg}, installed=[])
- self.assertEqual(
- ret, [makeop('install', 'config-example.canonical', cfg)])
-
- def test_package_ops_with_file(self):
- self.populate_tmp(
- {"snapf1.snap": b"foo1", "snapf1.config": b"snapf1cfg",
- "snapf2.snap": b"foo2", "foo.bar": "ignored"})
- ret = get_package_ops(
- packages=['pkg1'], configs={}, installed=[], fspath=self.tmp)
- self.assertEqual(
- ret,
- [makeop_tmpd(self.tmp, 'install', 'snapf1', path="snapf1.snap",
- cfgfile="snapf1.config"),
- makeop_tmpd(self.tmp, 'install', 'snapf2', path="snapf2.snap"),
- makeop('install', 'pkg1')])
-
- def test_package_ops_common_filename(self):
- # fish package name from filename
- # package names likely look like: pkgname.namespace_version_arch.snap
-
- # find filenames
- self.populate_tmp(
- {"pkg-ws.smoser_0.3.4_all.snap": "pkg-ws-snapdata",
- "pkg-ws.config": "pkg-ws-config",
- "pkg1.smoser_1.2.3_all.snap": "pkg1.snapdata",
- "pkg1.smoser.config": "pkg1.smoser.config-data",
- "pkg1.config": "pkg1.config-data",
- "pkg2.smoser_0.0_amd64.snap": "pkg2-snapdata",
- "pkg2.smoser_0.0_amd64.config": "pkg2.config"})
-
- ret = get_package_ops(
- packages=[], configs={}, installed=[], fspath=self.tmp)
- self.assertEqual(
- ret,
- [makeop_tmpd(self.tmp, 'install', 'pkg-ws.smoser',
- path="pkg-ws.smoser_0.3.4_all.snap",
- cfgfile="pkg-ws.config"),
- makeop_tmpd(self.tmp, 'install', 'pkg1.smoser',
- path="pkg1.smoser_1.2.3_all.snap",
- cfgfile="pkg1.smoser.config"),
- makeop_tmpd(self.tmp, 'install', 'pkg2.smoser',
- path="pkg2.smoser_0.0_amd64.snap",
- cfgfile="pkg2.smoser_0.0_amd64.config"),
- ])
-
- def test_package_ops_config_overrides_file(self):
- # config data overrides local file .config
- self.populate_tmp(
- {"snapf1.snap": b"foo1", "snapf1.config": b"snapf1cfg"})
- ret = get_package_ops(
- packages=[], configs={'snapf1': 'snapf1cfg-config'},
- installed=[], fspath=self.tmp)
- self.assertEqual(
- ret, [makeop_tmpd(self.tmp, 'install', 'snapf1',
- path="snapf1.snap", config="snapf1cfg-config")])
-
- def test_package_ops_namespacing(self):
- cfgs = {
- 'config-example': {'k1': 'v1'},
- 'pkg1': {'p1': 'p2'},
- 'ubuntu-core': {'c1': 'c2'},
- 'notinstalled.smoser': {'s1': 's2'},
- }
- ret = get_package_ops(
- packages=['config-example.canonical'], configs=cfgs,
- installed=['config-example.smoser', 'pkg1.canonical',
- 'ubuntu-core'])
-
- expected_configs = [
- makeop('config', 'pkg1', config=cfgs['pkg1']),
- makeop('config', 'ubuntu-core', config=cfgs['ubuntu-core'])]
- expected_installs = [
- makeop('install', 'config-example.canonical',
- config=cfgs['config-example'])]
-
- installs = [i for i in ret if i['op'] == 'install']
- configs = [c for c in ret if c['op'] == 'config']
-
- self.assertEqual(installs, expected_installs)
- # configs are not ordered
- self.assertEqual(len(configs), len(expected_configs))
- self.assertTrue(all(found in expected_configs for found in configs))
-
- def test_render_op_localsnap(self):
- self.populate_tmp({"snapf1.snap": b"foo1"})
- op = makeop_tmpd(self.tmp, 'install', 'snapf1',
- path='snapf1.snap')
- render_snap_op(**op)
- self.assertEqual(
- self.snapcmds, [['install', op['path'], None]])
-
- def test_render_op_localsnap_localconfig(self):
- self.populate_tmp(
- {"snapf1.snap": b"foo1", 'snapf1.config': b'snapf1cfg'})
- op = makeop_tmpd(self.tmp, 'install', 'snapf1',
- path='snapf1.snap', cfgfile='snapf1.config')
- render_snap_op(**op)
- self.assertEqual(
- self.snapcmds, [['install', op['path'], 'snapf1cfg']])
-
- def test_render_op_snap(self):
- op = makeop('install', 'snapf1')
- render_snap_op(**op)
- self.assertEqual(
- self.snapcmds, [['install', 'snapf1', None]])
-
- def test_render_op_snap_config(self):
- mycfg = {'key1': 'value1'}
- name = "snapf1"
- op = makeop('install', name, config=mycfg)
- render_snap_op(**op)
- self.assertEqual(
- self.snapcmds, [['install', name, {'config': {name: mycfg}}]])
-
- def test_render_op_config_bytes(self):
- name = "snapf1"
- mycfg = b'myconfig'
- op = makeop('config', name, config=mycfg)
- render_snap_op(**op)
- self.assertEqual(
- self.snapcmds, [['config', 'snapf1', {'config': {name: mycfg}}]])
-
- def test_render_op_config_string(self):
- name = 'snapf1'
- mycfg = 'myconfig: foo\nhisconfig: bar\n'
- op = makeop('config', name, config=mycfg)
- render_snap_op(**op)
- self.assertEqual(
- self.snapcmds, [['config', 'snapf1', {'config': {name: mycfg}}]])
-
- def test_render_op_config_dict(self):
- # config entry for package can be a dict, not a string blob
- mycfg = {'foo': 'bar'}
- name = 'snapf1'
- op = makeop('config', name, config=mycfg)
- render_snap_op(**op)
- # snapcmds is a list of 3-entry lists. data_found will be the
- # blob of data in the file in 'snappy install --config=<file>'
- data_found = self.snapcmds[0][2]
- self.assertEqual(mycfg, data_found['config'][name])
-
- def test_render_op_config_list(self):
- # config entry for package can be a list, not a string blob
- mycfg = ['foo', 'bar', 'wark', {'f1': 'b1'}]
- name = "snapf1"
- op = makeop('config', name, config=mycfg)
- render_snap_op(**op)
- data_found = self.snapcmds[0][2]
- self.assertEqual(mycfg, data_found['config'][name])
-
- def test_render_op_config_int(self):
- # config entry for package can be a list, not a string blob
- mycfg = 1
- name = 'snapf1'
- op = makeop('config', name, config=mycfg)
- render_snap_op(**op)
- data_found = self.snapcmds[0][2]
- self.assertEqual(mycfg, data_found['config'][name])
-
- def test_render_long_configs_short(self):
- # install a namespaced package should have un-namespaced config
- mycfg = {'k1': 'k2'}
- name = 'snapf1'
- op = makeop('install', name + ".smoser", config=mycfg)
- render_snap_op(**op)
- data_found = self.snapcmds[0][2]
- self.assertEqual(mycfg, data_found['config'][name])
-
- def test_render_does_not_pad_cfgfile(self):
- # package_ops with cfgfile should not modify --file= content.
- mydata = "foo1: bar1\nk: [l1, l2, l3]\n"
- self.populate_tmp(
- {"snapf1.snap": b"foo1", "snapf1.config": mydata.encode()})
- ret = get_package_ops(
- packages=[], configs={}, installed=[], fspath=self.tmp)
- self.assertEqual(
- ret,
- [makeop_tmpd(self.tmp, 'install', 'snapf1', path="snapf1.snap",
- cfgfile="snapf1.config")])
-
- # now the op was ok, but test that render didn't mess it up.
- render_snap_op(**ret[0])
- data_found = self.snapcmds[0][2]
- # the data found gets loaded in the snapcmd interpretation
- # so this comparison is a bit lossy, but input to snappy config
- # is expected to be yaml loadable, so it should be OK.
- self.assertEqual(yaml.safe_load(mydata), data_found)
-
-
-class TestSnapConfig(FilesystemMockingTestCase):
-
- SYSTEM_USER_ASSERTION = textwrap.dedent("""
- type: system-user
- authority-id: LqvZQdfyfGlYvtep4W6Oj6pFXP9t1Ksp
- brand-id: LqvZQdfyfGlYvtep4W6Oj6pFXP9t1Ksp
- email: foo@bar.com
- password: $6$E5YiAuMIPAwX58jG$miomhVNui/vf7f/3ctB/f0RWSKFxG0YXzrJ9rtJ1ikvzt
- series:
- - 16
- since: 2016-09-10T16:34:00+03:00
- until: 2017-11-10T16:34:00+03:00
- username: baz
- sign-key-sha3-384: RuVvnp4n52GilycjfbbTCI3_L8Y6QlIE75wxMc0KzGV3AUQqVd9GuXoj
-
- AcLBXAQAAQoABgUCV/UU1wAKCRBKnlMoJQLkZVeLD/9/+hIeVywtzsDA3oxl+P+u9D13y9s6svP
- Jd6Wnf4FTw6sq1GjBE4ZA7lrwSaRCUJ9Vcsvf2q9OGPY7mOb2TBxaDe0PbUMjrSrqllSSQwhpNI
- zG+NxkkKuxsUmLzFa+k9m6cyojNbw5LFhQZBQCGlr3JYqC0tIREq/UsZxj+90TUC87lDJwkU8GF
- s4CR+rejZj4itIcDcVxCSnJH6hv6j2JrJskJmvObqTnoOlcab+JXdamXqbldSP3UIhWoyVjqzkj
- +to7mXgx+cCUA9+ngNCcfUG+1huGGTWXPCYkZ78HvErcRlIdeo4d3xwtz1cl/w3vYnq9og1XwsP
- Yfetr3boig2qs1Y+j/LpsfYBYncgWjeDfAB9ZZaqQz/oc8n87tIPZDJHrusTlBfop8CqcM4xsKS
- d+wnEY8e/F24mdSOYmS1vQCIDiRU3MKb6x138Ud6oHXFlRBbBJqMMctPqWDunWzb5QJ7YR0I39q
- BrnEqv5NE0G7w6HOJ1LSPG5Hae3P4T2ea+ATgkb03RPr3KnXnzXg4TtBbW1nytdlgoNc/BafE1H
- f3NThcq9gwX4xWZ2PAWnqVPYdDMyCtzW3Ck+o6sIzx+dh4gDLPHIi/6TPe/pUuMop9CBpWwez7V
- v1z+1+URx6Xlq3Jq18y5pZ6fY3IDJ6km2nQPMzcm4Q==""")
-
- ACCOUNT_ASSERTION = textwrap.dedent("""
- type: account-key
- authority-id: canonical
- revision: 2
- public-key-sha3-384: BWDEoaqyr25nF5SNCvEv2v7QnM9QsfCc0PBMYD_i2NGSQ32EF2d4D0
- account-id: canonical
- name: store
- since: 2016-04-01T00:00:00.0Z
- body-length: 717
- sign-key-sha3-384: -CvQKAwRQ5h3Ffn10FILJoEZUXOv6km9FwA80-Rcj-f-6jadQ89VRswH
-
- AcbBTQRWhcGAARAA0KKYYQWuHOrsFVi4p4l7ZzSvX7kLgJFFeFgOkzdWKBTHEnsMKjl5mefFe9j
- qe8NlmJdfY7BenP7XeBtwKp700H/t9lLrZbpTNAPHXYxEWFJp5bPqIcJYBZ+29oLVLN1Tc5X482
- vCiDqL8+pPYqBrK2fNlyPlNNSum9wI70rDDL4r6FVvr+osTnGejibdV8JphWX+lrSQDnRSdM8KJ
- UM43vTgLGTi9W54oRhsA2OFexRfRksTrnqGoonCjqX5wO3OFSaMDzMsO2MJ/hPfLgDqw53qjzuK
- Iec9OL3k5basvu2cj5u9tKwVFDsCKK2GbKUsWWpx2KTpOifmhmiAbzkTHbH9KaoMS7p0kJwhTQG
- o9aJ9VMTWHJc/NCBx7eu451u6d46sBPCXS/OMUh2766fQmoRtO1OwCTxsRKG2kkjbMn54UdFULl
- VfzvyghMNRKIezsEkmM8wueTqGUGZWa6CEZqZKwhe/PROxOPYzqtDH18XZknbU1n5lNb7vNfem9
- 2ai+3+JyFnW9UhfvpVF7gzAgdyCqNli4C6BIN43uwoS8HkykocZS/+Gv52aUQ/NZ8BKOHLw+7an
- Q0o8W9ltSLZbEMxFIPSN0stiZlkXAp6DLyvh1Y4wXSynDjUondTpej2fSvSlCz/W5v5V7qA4nIc
- vUvV7RjVzv17ut0AEQEAAQ==
-
- AcLDXAQAAQoABgUCV83k9QAKCRDUpVvql9g3IBT8IACKZ7XpiBZ3W4lqbPssY6On81WmxQLtvsM
- WTp6zZpl/wWOSt2vMNUk9pvcmrNq1jG9CuhDfWFLGXEjcrrmVkN3YuCOajMSPFCGrxsIBLSRt/b
- nrKykdLAAzMfG8rP1d82bjFFiIieE+urQ0Kcv09Jtdvavq3JT1Tek5mFyyfhHNlQEKOzWqmRWiL
- 3c3VOZUs1ZD8TSlnuq/x+5T0X0YtOyGjSlVxk7UybbyMNd6MZfNaMpIG4x+mxD3KHFtBAC7O6kL
- eX3i6j5nCY5UABfA3DZEAkWP4zlmdBEOvZ9t293NaDdOpzsUHRkoi0Zez/9BHQ/kwx/uNc2WqrY
- inCmu16JGNeXqsyinnLl7Ghn2RwhvDMlLxF6RTx8xdx1yk6p3PBTwhZMUvuZGjUtN/AG8BmVJQ1
- rsGSRkkSywvnhVJRB2sudnrMBmNS2goJbzSbmJnOlBrd2WsV0T9SgNMWZBiov3LvU4o2SmAb6b+
- rYwh8H5QHcuuYJuxDjFhPswIp6Wes5T6hUicf3SWtObcDS4HSkVS4ImBjjX9YgCuFy7QdnooOWE
- aPvkRw3XCVeYq0K6w9GRsk1YFErD4XmXXZjDYY650MX9v42Sz5MmphHV8jdIY5ssbadwFSe2rCQ
- 6UX08zy7RsIb19hTndE6ncvSNDChUR9eEnCm73eYaWTWTnq1cxdVP/s52r8uss++OYOkPWqh5nO
- haRn7INjH/yZX4qXjNXlTjo0PnHH0q08vNKDwLhxS+D9du+70FeacXFyLIbcWllSbJ7DmbumGpF
- yYbtj3FDDPzachFQdIG3lSt+cSUGeyfSs6wVtc3cIPka/2Urx7RprfmoWSI6+a5NcLdj0u2z8O9
- HxeIgxDpg/3gT8ZIuFKePMcLDM19Fh/p0ysCsX+84B9chNWtsMSmIaE57V+959MVtsLu7SLb9gi
- skrju0pQCwsu2wHMLTNd1f3PTHmrr49hxetTus07HSQUApMtAGKzQilF5zqFjbyaTd4xgQbd+PK
- CjFyzQTDOcUhXpuUGt/IzlqiFfsCsmbj2K4KdSNYMlqIgZ3Azu8KvZLIhsyN7v5vNIZSPfEbjde
- ClU9r0VRiJmtYBUjcSghD9LWn+yRLwOxhfQVjm0cBwIt5R/yPF/qC76yIVuWUtM5Y2/zJR1J8OF
- qWchvlImHtvDzS9FQeLyzJAOjvZ2CnWp2gILgUz0WQdOk1Dq8ax7KS9BQ42zxw9EZAEPw3PEFqR
- IQsRTONp+iVS8YxSmoYZjDlCgRMWUmawez/Fv5b9Fb/XkO5Eq4e+KfrpUujXItaipb+tV8h5v3t
- oG3Ie3WOHrVjCLXIdYslpL1O4nadqR6Xv58pHj6k""")
-
- test_assertions = [ACCOUNT_ASSERTION, SYSTEM_USER_ASSERTION]
-
- def setUp(self):
- super(TestSnapConfig, self).setUp()
- self.subp = util.subp
- self.new_root = tempfile.mkdtemp()
- self.addCleanup(shutil.rmtree, self.new_root)
-
- def _get_cloud(self, distro, metadata=None):
- self.patchUtils(self.new_root)
- paths = helpers.Paths({})
- cls = distros.fetch(distro)
- mydist = cls(distro, {}, paths)
- myds = DataSourceNone.DataSourceNone({}, mydist, paths)
- if metadata:
- myds.metadata.update(metadata)
- return cloud.Cloud(myds, paths, {}, mydist, None)
-
- @mock.patch('cloudinit.util.write_file')
- @mock.patch('cloudinit.util.subp')
- def test_snap_config_add_assertions(self, msubp, mwrite):
- add_assertions(self.test_assertions)
-
- combined = "\n".join(self.test_assertions)
- mwrite.assert_any_call(ASSERTIONS_FILE, combined.encode('utf-8'))
- msubp.assert_called_with(['snap', 'ack', ASSERTIONS_FILE],
- capture=True)
-
- def test_snap_config_add_assertions_empty(self):
- self.assertRaises(ValueError, add_assertions, [])
-
- def test_add_assertions_nonlist(self):
- self.assertRaises(ValueError, add_assertions, {})
-
- @mock.patch('cloudinit.util.write_file')
- @mock.patch('cloudinit.util.subp')
- def test_snap_config_add_assertions_ack_fails(self, msubp, mwrite):
- msubp.side_effect = [util.ProcessExecutionError("Invalid assertion")]
- self.assertRaises(util.ProcessExecutionError, add_assertions,
- self.test_assertions)
-
- @mock.patch('cloudinit.config.cc_snap_config.add_assertions')
- @mock.patch('cloudinit.config.cc_snap_config.util')
- def test_snap_config_handle_no_config(self, mock_util, mock_add):
- cfg = {}
- cc = self._get_cloud('ubuntu')
- cc.distro = mock.MagicMock()
- cc.distro.name = 'ubuntu'
- mock_util.which.return_value = None
- snap_handle('snap_config', cfg, cc, LOG, None)
- mock_add.assert_not_called()
-
- def test_snap_config_add_snap_user_no_config(self):
- usercfg = add_snap_user(cfg=None)
- self.assertIsNone(usercfg)
-
- def test_snap_config_add_snap_user_not_dict(self):
- cfg = ['foobar']
- self.assertRaises(ValueError, add_snap_user, cfg)
-
- def test_snap_config_add_snap_user_no_email(self):
- cfg = {'assertions': [], 'known': True}
- usercfg = add_snap_user(cfg=cfg)
- self.assertIsNone(usercfg)
-
- @mock.patch('cloudinit.config.cc_snap_config.util')
- def test_snap_config_add_snap_user_email_only(self, mock_util):
- email = 'janet@planetjanet.org'
- cfg = {'email': email}
- mock_util.which.return_value = None
- mock_util.system_is_snappy.return_value = True
- mock_util.subp.side_effect = [
- ("false\n", ""), # snap managed
- ]
-
- usercfg = add_snap_user(cfg=cfg)
-
- self.assertEqual(usercfg, {'snapuser': email, 'known': False})
-
- @mock.patch('cloudinit.config.cc_snap_config.util')
- def test_snap_config_add_snap_user_email_known(self, mock_util):
- email = 'janet@planetjanet.org'
- known = True
- cfg = {'email': email, 'known': known}
- mock_util.which.return_value = None
- mock_util.system_is_snappy.return_value = True
- mock_util.subp.side_effect = [
- ("false\n", ""), # snap managed
- (self.SYSTEM_USER_ASSERTION, ""), # snap known system-user
- ]
-
- usercfg = add_snap_user(cfg=cfg)
-
- self.assertEqual(usercfg, {'snapuser': email, 'known': known})
-
- @mock.patch('cloudinit.config.cc_snap_config.add_assertions')
- @mock.patch('cloudinit.config.cc_snap_config.util')
- def test_snap_config_handle_system_not_snappy(self, mock_util, mock_add):
- cfg = {'snappy': {'assertions': self.test_assertions}}
- cc = self._get_cloud('ubuntu')
- cc.distro = mock.MagicMock()
- cc.distro.name = 'ubuntu'
- mock_util.which.return_value = None
- mock_util.system_is_snappy.return_value = False
-
- snap_handle('snap_config', cfg, cc, LOG, None)
-
- mock_add.assert_not_called()
-
- @mock.patch('cloudinit.config.cc_snap_config.add_assertions')
- @mock.patch('cloudinit.config.cc_snap_config.util')
- def test_snap_config_handle_snapuser(self, mock_util, mock_add):
- email = 'janet@planetjanet.org'
- cfg = {
- 'snappy': {
- 'assertions': self.test_assertions,
- 'email': email,
- }
- }
- cc = self._get_cloud('ubuntu')
- cc.distro = mock.MagicMock()
- cc.distro.name = 'ubuntu'
- mock_util.which.return_value = None
- mock_util.system_is_snappy.return_value = True
- mock_util.subp.side_effect = [
- ("false\n", ""), # snap managed
- ]
-
- snap_handle('snap_config', cfg, cc, LOG, None)
-
- mock_add.assert_called_with(self.test_assertions)
- usercfg = {'snapuser': email, 'known': False}
- cc.distro.create_user.assert_called_with(email, **usercfg)
-
- @mock.patch('cloudinit.config.cc_snap_config.add_assertions')
- @mock.patch('cloudinit.config.cc_snap_config.util')
- def test_snap_config_handle_snapuser_known(self, mock_util, mock_add):
- email = 'janet@planetjanet.org'
- cfg = {
- 'snappy': {
- 'assertions': self.test_assertions,
- 'email': email,
- 'known': True,
- }
- }
- cc = self._get_cloud('ubuntu')
- cc.distro = mock.MagicMock()
- cc.distro.name = 'ubuntu'
- mock_util.which.return_value = None
- mock_util.system_is_snappy.return_value = True
- mock_util.subp.side_effect = [
- ("false\n", ""), # snap managed
- (self.SYSTEM_USER_ASSERTION, ""), # snap known system-user
- ]
-
- snap_handle('snap_config', cfg, cc, LOG, None)
-
- mock_add.assert_called_with(self.test_assertions)
- usercfg = {'snapuser': email, 'known': True}
- cc.distro.create_user.assert_called_with(email, **usercfg)
-
- @mock.patch('cloudinit.config.cc_snap_config.add_assertions')
- @mock.patch('cloudinit.config.cc_snap_config.util')
- def test_snap_config_handle_snapuser_known_managed(self, mock_util,
- mock_add):
- email = 'janet@planetjanet.org'
- cfg = {
- 'snappy': {
- 'assertions': self.test_assertions,
- 'email': email,
- 'known': True,
- }
- }
- cc = self._get_cloud('ubuntu')
- cc.distro = mock.MagicMock()
- cc.distro.name = 'ubuntu'
- mock_util.which.return_value = None
- mock_util.system_is_snappy.return_value = True
- mock_util.subp.side_effect = [
- ("true\n", ""), # snap managed
- ]
-
- snap_handle('snap_config', cfg, cc, LOG, None)
-
- mock_add.assert_called_with(self.test_assertions)
- cc.distro.create_user.assert_not_called()
-
- @mock.patch('cloudinit.config.cc_snap_config.add_assertions')
- @mock.patch('cloudinit.config.cc_snap_config.util')
- def test_snap_config_handle_snapuser_known_no_assertion(self, mock_util,
- mock_add):
- email = 'janet@planetjanet.org'
- cfg = {
- 'snappy': {
- 'assertions': [self.ACCOUNT_ASSERTION],
- 'email': email,
- 'known': True,
- }
- }
- cc = self._get_cloud('ubuntu')
- cc.distro = mock.MagicMock()
- cc.distro.name = 'ubuntu'
- mock_util.which.return_value = None
- mock_util.system_is_snappy.return_value = True
- mock_util.subp.side_effect = [
- ("true\n", ""), # snap managed
- ("", ""), # snap known system-user
- ]
-
- snap_handle('snap_config', cfg, cc, LOG, None)
-
- mock_add.assert_called_with([self.ACCOUNT_ASSERTION])
- cc.distro.create_user.assert_not_called()
-
-
-def makeop_tmpd(tmpd, op, name, config=None, path=None, cfgfile=None):
- if cfgfile:
- cfgfile = os.path.sep.join([tmpd, cfgfile])
- if path:
- path = os.path.sep.join([tmpd, path])
- return(makeop(op=op, name=name, config=config, path=path, cfgfile=cfgfile))
-
-
-def apply_patches(patches):
- ret = []
- for (ref, name, replace) in patches:
- if replace is None:
- continue
- orig = getattr(ref, name)
- setattr(ref, name, replace)
- ret.append((ref, name, orig))
- return ret
-
-# vi: ts=4 expandtab
diff --git a/tests/unittests/test_handler/test_handler_spacewalk.py b/tests/unittests/test_handler/test_handler_spacewalk.py
index ddbf4a79..410e6f77 100644
--- a/tests/unittests/test_handler/test_handler_spacewalk.py
+++ b/tests/unittests/test_handler/test_handler_spacewalk.py
@@ -6,11 +6,7 @@ from cloudinit import util
from cloudinit.tests import helpers
import logging
-
-try:
- from unittest import mock
-except ImportError:
- import mock
+from unittest import mock
LOG = logging.getLogger(__name__)
diff --git a/tests/unittests/test_handler/test_handler_timezone.py b/tests/unittests/test_handler/test_handler_timezone.py
index 27eedded..50c45363 100644
--- a/tests/unittests/test_handler/test_handler_timezone.py
+++ b/tests/unittests/test_handler/test_handler_timezone.py
@@ -18,8 +18,8 @@ from cloudinit.tests import helpers as t_help
from configobj import ConfigObj
import logging
import shutil
-from six import BytesIO
import tempfile
+from io import BytesIO
LOG = logging.getLogger(__name__)
diff --git a/tests/unittests/test_handler/test_handler_write_files.py b/tests/unittests/test_handler/test_handler_write_files.py
index bc8756ca..ed0a4da2 100644
--- a/tests/unittests/test_handler/test_handler_write_files.py
+++ b/tests/unittests/test_handler/test_handler_write_files.py
@@ -1,17 +1,16 @@
# This file is part of cloud-init. See LICENSE file for license information.
-from cloudinit.config.cc_write_files import write_files, decode_perms
-from cloudinit import log as logging
-from cloudinit import util
-
-from cloudinit.tests.helpers import CiTestCase, FilesystemMockingTestCase
-
import base64
import gzip
+import io
import shutil
-import six
import tempfile
+from cloudinit import log as logging
+from cloudinit import util
+from cloudinit.config.cc_write_files import write_files, decode_perms
+from cloudinit.tests.helpers import CiTestCase, FilesystemMockingTestCase
+
LOG = logging.getLogger(__name__)
YAML_TEXT = """
@@ -138,7 +137,7 @@ class TestDecodePerms(CiTestCase):
def _gzip_bytes(data):
- buf = six.BytesIO()
+ buf = io.BytesIO()
fp = None
try:
fp = gzip.GzipFile(fileobj=buf, mode="wb")
diff --git a/tests/unittests/test_handler/test_handler_yum_add_repo.py b/tests/unittests/test_handler/test_handler_yum_add_repo.py
index b90a3af3..0675bd8f 100644
--- a/tests/unittests/test_handler/test_handler_yum_add_repo.py
+++ b/tests/unittests/test_handler/test_handler_yum_add_repo.py
@@ -7,8 +7,8 @@ from cloudinit.tests import helpers
import logging
import shutil
-from six import StringIO
import tempfile
+from io import StringIO
LOG = logging.getLogger(__name__)
diff --git a/tests/unittests/test_handler/test_handler_zypper_add_repo.py b/tests/unittests/test_handler/test_handler_zypper_add_repo.py
index 72ab6c08..9685ff28 100644
--- a/tests/unittests/test_handler/test_handler_zypper_add_repo.py
+++ b/tests/unittests/test_handler/test_handler_zypper_add_repo.py
@@ -2,6 +2,7 @@
import glob
import os
+from io import StringIO
from cloudinit.config import cc_zypper_add_repo
from cloudinit import util
@@ -10,7 +11,6 @@ from cloudinit.tests import helpers
from cloudinit.tests.helpers import mock
import logging
-from six import StringIO
LOG = logging.getLogger(__name__)
diff --git a/tests/unittests/test_handler/test_schema.py b/tests/unittests/test_handler/test_schema.py
index 1bad07f6..987a89c9 100644
--- a/tests/unittests/test_handler/test_schema.py
+++ b/tests/unittests/test_handler/test_schema.py
@@ -10,7 +10,7 @@ from cloudinit.tests.helpers import CiTestCase, mock, skipUnlessJsonSchema
from copy import copy
import os
-from six import StringIO
+from io import StringIO
from textwrap import dedent
from yaml import safe_load
@@ -28,6 +28,7 @@ class GetSchemaTest(CiTestCase):
'cc_runcmd',
'cc_snap',
'cc_ubuntu_advantage',
+ 'cc_ubuntu_drivers',
'cc_zypper_add_repo'
],
[subschema['id'] for subschema in schema['allOf']])
diff --git a/tests/unittests/test_log.py b/tests/unittests/test_log.py
index cd6296d6..e069a487 100644
--- a/tests/unittests/test_log.py
+++ b/tests/unittests/test_log.py
@@ -2,14 +2,15 @@
"""Tests for cloudinit.log """
-from cloudinit.analyze.dump import CLOUD_INIT_ASCTIME_FMT
-from cloudinit import log as ci_logging
-from cloudinit.tests.helpers import CiTestCase
import datetime
+import io
import logging
-import six
import time
+from cloudinit import log as ci_logging
+from cloudinit.analyze.dump import CLOUD_INIT_ASCTIME_FMT
+from cloudinit.tests.helpers import CiTestCase
+
class TestCloudInitLogger(CiTestCase):
@@ -18,7 +19,7 @@ class TestCloudInitLogger(CiTestCase):
# of sys.stderr, we'll plug in a StringIO() object so we can see
# what gets logged
logging.Formatter.converter = time.gmtime
- self.ci_logs = six.StringIO()
+ self.ci_logs = io.StringIO()
self.ci_root = logging.getLogger()
console = logging.StreamHandler(self.ci_logs)
console.setFormatter(logging.Formatter(ci_logging.DEF_CON_FORMAT))
diff --git a/tests/unittests/test_merging.py b/tests/unittests/test_merging.py
index 3a5072c7..10871bcf 100644
--- a/tests/unittests/test_merging.py
+++ b/tests/unittests/test_merging.py
@@ -13,13 +13,11 @@ import glob
import os
import random
import re
-import six
import string
SOURCE_PAT = "source*.*yaml"
EXPECTED_PAT = "expected%s.yaml"
-TYPES = [dict, str, list, tuple, None]
-TYPES.extend(six.integer_types)
+TYPES = [dict, str, list, tuple, None, int]
def _old_mergedict(src, cand):
@@ -85,7 +83,7 @@ def _make_dict(current_depth, max_depth, rand):
pass
if t in [tuple]:
base = tuple(base)
- elif t in six.integer_types:
+ elif t in [int]:
base = rand.randint(0, 2 ** 8)
elif t in [str]:
base = _random_str(rand)
diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
index 195f261c..bedd05fe 100644
--- a/tests/unittests/test_net.py
+++ b/tests/unittests/test_net.py
@@ -9,6 +9,7 @@ from cloudinit.net import (
from cloudinit.sources.helpers import openstack
from cloudinit import temp_utils
from cloudinit import util
+from cloudinit import safeyaml as yaml
from cloudinit.tests.helpers import (
CiTestCase, FilesystemMockingTestCase, dir2dict, mock, populate_dir)
@@ -19,8 +20,10 @@ import gzip
import io
import json
import os
+import re
import textwrap
-import yaml
+from yaml.serializer import Serializer
+
DHCP_CONTENT_1 = """
DEVICE='eth0'
@@ -78,7 +81,7 @@ DHCP6_EXPECTED_1 = {
STATIC_CONTENT_1 = """
DEVICE='eth1'
-PROTO='static'
+PROTO='none'
IPV4ADDR='10.0.0.2'
IPV4BROADCAST='10.0.0.255'
IPV4NETMASK='255.255.255.0'
@@ -102,6 +105,357 @@ STATIC_EXPECTED_1 = {
'address': '10.0.0.2'}],
}
+V1_NAMESERVER_ALIAS = """
+config:
+- id: eno1
+ mac_address: 08:94:ef:51:ae:e0
+ mtu: 1500
+ name: eno1
+ subnets:
+ - type: manual
+ type: physical
+- id: eno2
+ mac_address: 08:94:ef:51:ae:e1
+ mtu: 1500
+ name: eno2
+ subnets:
+ - type: manual
+ type: physical
+- id: eno3
+ mac_address: 08:94:ef:51:ae:de
+ mtu: 1500
+ name: eno3
+ subnets:
+ - type: manual
+ type: physical
+- bond_interfaces:
+ - eno1
+ - eno3
+ id: bondM
+ mac_address: 08:94:ef:51:ae:e0
+ mtu: 1500
+ name: bondM
+ params:
+ bond-downdelay: 0
+ bond-lacp-rate: fast
+ bond-miimon: 100
+ bond-mode: 802.3ad
+ bond-updelay: 0
+ bond-xmit-hash-policy: layer3+4
+ subnets:
+ - address: 10.101.10.47/23
+ gateway: 10.101.11.254
+ type: static
+ type: bond
+- id: eno4
+ mac_address: 08:94:ef:51:ae:df
+ mtu: 1500
+ name: eno4
+ subnets:
+ - type: manual
+ type: physical
+- id: enp0s20f0u1u6
+ mac_address: 0a:94:ef:51:a4:b9
+ mtu: 1500
+ name: enp0s20f0u1u6
+ subnets:
+ - type: manual
+ type: physical
+- id: enp216s0f0
+ mac_address: 68:05:ca:81:7c:e8
+ mtu: 9000
+ name: enp216s0f0
+ subnets:
+ - type: manual
+ type: physical
+- id: enp216s0f1
+ mac_address: 68:05:ca:81:7c:e9
+ mtu: 9000
+ name: enp216s0f1
+ subnets:
+ - type: manual
+ type: physical
+- id: enp47s0f0
+ mac_address: 68:05:ca:64:d3:6c
+ mtu: 9000
+ name: enp47s0f0
+ subnets:
+ - type: manual
+ type: physical
+- bond_interfaces:
+ - enp216s0f0
+ - enp47s0f0
+ id: bond0
+ mac_address: 68:05:ca:64:d3:6c
+ mtu: 9000
+ name: bond0
+ params:
+ bond-downdelay: 0
+ bond-lacp-rate: fast
+ bond-miimon: 100
+ bond-mode: 802.3ad
+ bond-updelay: 0
+ bond-xmit-hash-policy: layer3+4
+ subnets:
+ - type: manual
+ type: bond
+- id: bond0.3502
+ mtu: 9000
+ name: bond0.3502
+ subnets:
+ - address: 172.20.80.4/25
+ type: static
+ type: vlan
+ vlan_id: 3502
+ vlan_link: bond0
+- id: bond0.3503
+ mtu: 9000
+ name: bond0.3503
+ subnets:
+ - address: 172.20.80.129/25
+ type: static
+ type: vlan
+ vlan_id: 3503
+ vlan_link: bond0
+- id: enp47s0f1
+ mac_address: 68:05:ca:64:d3:6d
+ mtu: 9000
+ name: enp47s0f1
+ subnets:
+ - type: manual
+ type: physical
+- bond_interfaces:
+ - enp216s0f1
+ - enp47s0f1
+ id: bond1
+ mac_address: 68:05:ca:64:d3:6d
+ mtu: 9000
+ name: bond1
+ params:
+ bond-downdelay: 0
+ bond-lacp-rate: fast
+ bond-miimon: 100
+ bond-mode: 802.3ad
+ bond-updelay: 0
+ bond-xmit-hash-policy: layer3+4
+ subnets:
+ - address: 10.101.8.65/26
+ routes:
+ - destination: 213.119.192.0/24
+ gateway: 10.101.8.126
+ metric: 0
+ type: static
+ type: bond
+- address:
+ - 10.101.10.1
+ - 10.101.10.2
+ - 10.101.10.3
+ - 10.101.10.5
+ search:
+ - foo.bar
+ - maas
+ type: nameserver
+version: 1
+"""
+
+NETPLAN_NO_ALIAS = """
+network:
+ version: 2
+ ethernets:
+ eno1:
+ match:
+ macaddress: 08:94:ef:51:ae:e0
+ mtu: 1500
+ set-name: eno1
+ eno2:
+ match:
+ macaddress: 08:94:ef:51:ae:e1
+ mtu: 1500
+ set-name: eno2
+ eno3:
+ match:
+ macaddress: 08:94:ef:51:ae:de
+ mtu: 1500
+ set-name: eno3
+ eno4:
+ match:
+ macaddress: 08:94:ef:51:ae:df
+ mtu: 1500
+ set-name: eno4
+ enp0s20f0u1u6:
+ match:
+ macaddress: 0a:94:ef:51:a4:b9
+ mtu: 1500
+ set-name: enp0s20f0u1u6
+ enp216s0f0:
+ match:
+ macaddress: 68:05:ca:81:7c:e8
+ mtu: 9000
+ set-name: enp216s0f0
+ enp216s0f1:
+ match:
+ macaddress: 68:05:ca:81:7c:e9
+ mtu: 9000
+ set-name: enp216s0f1
+ enp47s0f0:
+ match:
+ macaddress: 68:05:ca:64:d3:6c
+ mtu: 9000
+ set-name: enp47s0f0
+ enp47s0f1:
+ match:
+ macaddress: 68:05:ca:64:d3:6d
+ mtu: 9000
+ set-name: enp47s0f1
+ bonds:
+ bond0:
+ interfaces:
+ - enp216s0f0
+ - enp47s0f0
+ macaddress: 68:05:ca:64:d3:6c
+ mtu: 9000
+ parameters:
+ down-delay: 0
+ lacp-rate: fast
+ mii-monitor-interval: 100
+ mode: 802.3ad
+ transmit-hash-policy: layer3+4
+ up-delay: 0
+ bond1:
+ addresses:
+ - 10.101.8.65/26
+ interfaces:
+ - enp216s0f1
+ - enp47s0f1
+ macaddress: 68:05:ca:64:d3:6d
+ mtu: 9000
+ nameservers:
+ addresses:
+ - 10.101.10.1
+ - 10.101.10.2
+ - 10.101.10.3
+ - 10.101.10.5
+ search:
+ - foo.bar
+ - maas
+ parameters:
+ down-delay: 0
+ lacp-rate: fast
+ mii-monitor-interval: 100
+ mode: 802.3ad
+ transmit-hash-policy: layer3+4
+ up-delay: 0
+ routes:
+ - metric: 0
+ to: 213.119.192.0/24
+ via: 10.101.8.126
+ bondM:
+ addresses:
+ - 10.101.10.47/23
+ gateway4: 10.101.11.254
+ interfaces:
+ - eno1
+ - eno3
+ macaddress: 08:94:ef:51:ae:e0
+ mtu: 1500
+ nameservers:
+ addresses:
+ - 10.101.10.1
+ - 10.101.10.2
+ - 10.101.10.3
+ - 10.101.10.5
+ search:
+ - foo.bar
+ - maas
+ parameters:
+ down-delay: 0
+ lacp-rate: fast
+ mii-monitor-interval: 100
+ mode: 802.3ad
+ transmit-hash-policy: layer3+4
+ up-delay: 0
+ vlans:
+ bond0.3502:
+ addresses:
+ - 172.20.80.4/25
+ id: 3502
+ link: bond0
+ mtu: 9000
+ nameservers:
+ addresses:
+ - 10.101.10.1
+ - 10.101.10.2
+ - 10.101.10.3
+ - 10.101.10.5
+ search:
+ - foo.bar
+ - maas
+ bond0.3503:
+ addresses:
+ - 172.20.80.129/25
+ id: 3503
+ link: bond0
+ mtu: 9000
+ nameservers:
+ addresses:
+ - 10.101.10.1
+ - 10.101.10.2
+ - 10.101.10.3
+ - 10.101.10.5
+ search:
+ - foo.bar
+ - maas
+"""
+
+NETPLAN_BOND_GRAT_ARP = """
+network:
+ bonds:
+ bond0:
+ interfaces:
+ - ens3
+ macaddress: 68:05:ca:64:d3:6c
+ mtu: 9000
+ parameters:
+ gratuitious-arp: 1
+ bond1:
+ interfaces:
+ - ens4
+ macaddress: 68:05:ca:64:d3:6d
+ mtu: 9000
+ parameters:
+ gratuitous-arp: 2
+ ethernets:
+ ens3:
+ dhcp4: false
+ dhcp6: false
+ match:
+ macaddress: 52:54:00:ab:cd:ef
+ ens4:
+ dhcp4: false
+ dhcp6: false
+ match:
+ macaddress: 52:54:00:11:22:ff
+ version: 2
+"""
+
+NETPLAN_DHCP_FALSE = """
+version: 2
+ethernets:
+ ens3:
+ match:
+ macaddress: 52:54:00:ab:cd:ef
+ dhcp4: false
+ dhcp6: false
+ addresses:
+ - 192.168.42.100/24
+ - 2001:db8::100/32
+ gateway4: 192.168.42.1
+ gateway6: 2001:db8::1
+ nameservers:
+ search: [example.com]
+ addresses: [192.168.42.53, 1.1.1.1]
+"""
+
# Examples (and expected outputs for various renderers).
OS_SAMPLES = [
{
@@ -135,17 +489,11 @@ OS_SAMPLES = [
"""
# Created by cloud-init on instance boot automatically, do not edit.
#
-BOOTPROTO=none
-DEFROUTE=yes
-DEVICE=eth0
-GATEWAY=172.19.3.254
-HWADDR=fa:16:3e:ed:9a:59
+BOOTPROTO=static
IPADDR=172.19.1.34
+LLADDR=fa:16:3e:ed:9a:59
NETMASK=255.255.252.0
-NM_CONTROLLED=no
-ONBOOT=yes
-TYPE=Ethernet
-USERCTL=no
+STARTMODE=auto
""".lstrip()),
('etc/resolv.conf',
"""
@@ -160,7 +508,7 @@ nameserver 172.19.0.12
[main]
dns = none
""".lstrip()),
- ('etc/udev/rules.d/70-persistent-net.rules',
+ ('etc/udev/rules.d/85-persistent-net-cloud-init.rules',
"".join(['SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ',
'ATTR{address}=="fa:16:3e:ed:9a:59", NAME="eth0"\n']))],
'out_sysconfig_rhel': [
@@ -235,19 +583,13 @@ dns = none
"""
# Created by cloud-init on instance boot automatically, do not edit.
#
-BOOTPROTO=none
-DEFROUTE=yes
-DEVICE=eth0
-GATEWAY=172.19.3.254
-HWADDR=fa:16:3e:ed:9a:59
+BOOTPROTO=static
IPADDR=172.19.1.34
IPADDR1=10.0.0.10
+LLADDR=fa:16:3e:ed:9a:59
NETMASK=255.255.252.0
NETMASK1=255.255.255.0
-NM_CONTROLLED=no
-ONBOOT=yes
-TYPE=Ethernet
-USERCTL=no
+STARTMODE=auto
""".lstrip()),
('etc/resolv.conf',
"""
@@ -262,7 +604,7 @@ nameserver 172.19.0.12
[main]
dns = none
""".lstrip()),
- ('etc/udev/rules.d/70-persistent-net.rules',
+ ('etc/udev/rules.d/85-persistent-net-cloud-init.rules',
"".join(['SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ',
'ATTR{address}=="fa:16:3e:ed:9a:59", NAME="eth0"\n']))],
'out_sysconfig_rhel': [
@@ -359,21 +701,14 @@ dns = none
"""
# Created by cloud-init on instance boot automatically, do not edit.
#
-BOOTPROTO=none
-DEFROUTE=yes
-DEVICE=eth0
-GATEWAY=172.19.3.254
-HWADDR=fa:16:3e:ed:9a:59
+BOOTPROTO=static
IPADDR=172.19.1.34
-IPV6ADDR=2001:DB8::10/64
-IPV6ADDR_SECONDARIES="2001:DB9::10/64 2001:DB10::10/64"
-IPV6INIT=yes
-IPV6_DEFAULTGW=2001:DB8::1
+IPADDR6=2001:DB8::10/64
+IPADDR6_1=2001:DB9::10/64
+IPADDR6_2=2001:DB10::10/64
+LLADDR=fa:16:3e:ed:9a:59
NETMASK=255.255.252.0
-NM_CONTROLLED=no
-ONBOOT=yes
-TYPE=Ethernet
-USERCTL=no
+STARTMODE=auto
""".lstrip()),
('etc/resolv.conf',
"""
@@ -388,7 +723,7 @@ nameserver 172.19.0.12
[main]
dns = none
""".lstrip()),
- ('etc/udev/rules.d/70-persistent-net.rules',
+ ('etc/udev/rules.d/85-persistent-net-cloud-init.rules',
"".join(['SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ',
'ATTR{address}=="fa:16:3e:ed:9a:59", NAME="eth0"\n']))],
'out_sysconfig_rhel': [
@@ -518,7 +853,19 @@ NETWORK_CONFIGS = {
via: 65.61.151.37
set-name: eth99
""").rstrip(' '),
- 'expected_sysconfig': {
+ 'expected_sysconfig_opensuse': {
+ 'ifcfg-eth1': textwrap.dedent("""\
+ BOOTPROTO=static
+ LLADDR=cf:d6:af:48:e8:80
+ STARTMODE=auto"""),
+ 'ifcfg-eth99': textwrap.dedent("""\
+ BOOTPROTO=dhcp4
+ LLADDR=c0:d6:9f:2c:e8:80
+ IPADDR=192.168.21.3
+ NETMASK=255.255.255.0
+ STARTMODE=auto"""),
+ },
+ 'expected_sysconfig_rhel': {
'ifcfg-eth1': textwrap.dedent("""\
BOOTPROTO=none
DEVICE=eth1
@@ -531,6 +878,7 @@ NETWORK_CONFIGS = {
BOOTPROTO=dhcp
DEFROUTE=yes
DEVICE=eth99
+ DHCLIENT_SET_DEFAULT_ROUTE=yes
DNS1=8.8.8.8
DNS2=8.8.4.4
DOMAIN="barley.maas sach.maas"
@@ -594,6 +942,12 @@ NETWORK_CONFIGS = {
dhcp4: true
dhcp6: true
""").rstrip(' '),
+ 'expected_sysconfig_opensuse': {
+ 'ifcfg-iface0': textwrap.dedent("""\
+ BOOTPROTO=dhcp
+ DHCLIENT6_MODE=managed
+ STARTMODE=auto""")
+ },
'yaml': textwrap.dedent("""\
version: 1
config:
@@ -627,8 +981,8 @@ NETWORK_CONFIGS = {
addresses:
- 192.168.14.2/24
- 2001:1::1/64
+ ipv6-mtu: 1500
mtu: 9000
- mtu6: 1500
""").rstrip(' '),
'yaml': textwrap.dedent("""\
version: 1
@@ -644,7 +998,17 @@ NETWORK_CONFIGS = {
address: 2001:1::1/64
mtu: 1500
""").rstrip(' '),
- 'expected_sysconfig': {
+ 'expected_sysconfig_opensuse': {
+ 'ifcfg-iface0': textwrap.dedent("""\
+ BOOTPROTO=static
+ IPADDR=192.168.14.2
+ IPADDR6=2001:1::1/64
+ NETMASK=255.255.255.0
+ STARTMODE=auto
+ MTU=9000
+ """),
+ },
+ 'expected_sysconfig_rhel': {
'ifcfg-iface0': textwrap.dedent("""\
BOOTPROTO=none
DEVICE=iface0
@@ -661,6 +1025,23 @@ NETWORK_CONFIGS = {
"""),
},
},
+ 'v6_and_v4': {
+ 'expected_sysconfig_opensuse': {
+ 'ifcfg-iface0': textwrap.dedent("""\
+ BOOTPROTO=dhcp
+ DHCLIENT6_MODE=managed
+ STARTMODE=auto""")
+ },
+ 'yaml': textwrap.dedent("""\
+ version: 1
+ config:
+ - type: 'physical'
+ name: 'iface0'
+ subnets:
+ - type: dhcp6
+ - type: dhcp4
+ """).rstrip(' '),
+ },
'dhcpv6_only': {
'expected_eni': textwrap.dedent("""\
auto lo
@@ -684,7 +1065,14 @@ NETWORK_CONFIGS = {
subnets:
- {'type': 'dhcp6'}
""").rstrip(' '),
- 'expected_sysconfig': {
+ 'expected_sysconfig_opensuse': {
+ 'ifcfg-iface0': textwrap.dedent("""\
+ BOOTPROTO=dhcp6
+ DHCLIENT6_MODE=managed
+ STARTMODE=auto
+ """),
+ },
+ 'expected_sysconfig_rhel': {
'ifcfg-iface0': textwrap.dedent("""\
BOOTPROTO=none
DEVICE=iface0
@@ -698,6 +1086,255 @@ NETWORK_CONFIGS = {
"""),
},
},
+ 'dhcpv6_accept_ra': {
+ 'expected_eni': textwrap.dedent("""\
+ auto lo
+ iface lo inet loopback
+
+ auto iface0
+ iface iface0 inet6 dhcp
+ accept_ra 1
+ """).rstrip(' '),
+ 'expected_netplan': textwrap.dedent("""
+ network:
+ version: 2
+ ethernets:
+ iface0:
+ accept-ra: true
+ dhcp6: true
+ """).rstrip(' '),
+ 'yaml_v1': textwrap.dedent("""\
+ version: 1
+ config:
+ - type: 'physical'
+ name: 'iface0'
+ subnets:
+ - {'type': 'dhcp6'}
+ accept-ra: true
+ """).rstrip(' '),
+ 'yaml_v2': textwrap.dedent("""\
+ version: 2
+ ethernets:
+ iface0:
+ dhcp6: true
+ accept-ra: true
+ """).rstrip(' '),
+ 'expected_sysconfig_opensuse': {
+ 'ifcfg-iface0': textwrap.dedent("""\
+ BOOTPROTO=dhcp6
+ DHCLIENT6_MODE=managed
+ STARTMODE=auto
+ """),
+ },
+ 'expected_sysconfig_rhel': {
+ 'ifcfg-iface0': textwrap.dedent("""\
+ BOOTPROTO=none
+ DEVICE=iface0
+ DHCPV6C=yes
+ IPV6INIT=yes
+ IPV6_FORCE_ACCEPT_RA=yes
+ DEVICE=iface0
+ NM_CONTROLLED=no
+ ONBOOT=yes
+ TYPE=Ethernet
+ USERCTL=no
+ """),
+ },
+ },
+ 'dhcpv6_reject_ra': {
+ 'expected_eni': textwrap.dedent("""\
+ auto lo
+ iface lo inet loopback
+
+ auto iface0
+ iface iface0 inet6 dhcp
+ accept_ra 0
+ """).rstrip(' '),
+ 'expected_netplan': textwrap.dedent("""
+ network:
+ version: 2
+ ethernets:
+ iface0:
+ accept-ra: false
+ dhcp6: true
+ """).rstrip(' '),
+ 'yaml_v1': textwrap.dedent("""\
+ version: 1
+ config:
+ - type: 'physical'
+ name: 'iface0'
+ subnets:
+ - {'type': 'dhcp6'}
+ accept-ra: false
+ """).rstrip(' '),
+ 'yaml_v2': textwrap.dedent("""\
+ version: 2
+ ethernets:
+ iface0:
+ dhcp6: true
+ accept-ra: false
+ """).rstrip(' '),
+ 'expected_sysconfig_opensuse': {
+ 'ifcfg-iface0': textwrap.dedent("""\
+ BOOTPROTO=dhcp6
+ DHCLIENT6_MODE=managed
+ STARTMODE=auto
+ """),
+ },
+ 'expected_sysconfig_rhel': {
+ 'ifcfg-iface0': textwrap.dedent("""\
+ BOOTPROTO=none
+ DEVICE=iface0
+ DHCPV6C=yes
+ IPV6INIT=yes
+ IPV6_FORCE_ACCEPT_RA=no
+ DEVICE=iface0
+ NM_CONTROLLED=no
+ ONBOOT=yes
+ TYPE=Ethernet
+ USERCTL=no
+ """),
+ },
+ },
+ 'ipv6_slaac': {
+ 'expected_eni': textwrap.dedent("""\
+ auto lo
+ iface lo inet loopback
+
+ auto iface0
+ iface iface0 inet6 auto
+ dhcp 0
+ """).rstrip(' '),
+ 'expected_netplan': textwrap.dedent("""
+ network:
+ version: 2
+ ethernets:
+ iface0:
+ dhcp6: true
+ """).rstrip(' '),
+ 'yaml': textwrap.dedent("""\
+ version: 1
+ config:
+ - type: 'physical'
+ name: 'iface0'
+ subnets:
+ - {'type': 'ipv6_slaac'}
+ """).rstrip(' '),
+ 'expected_sysconfig_opensuse': {
+ 'ifcfg-iface0': textwrap.dedent("""\
+ BOOTPROTO=dhcp6
+ DHCLIENT6_MODE=info
+ STARTMODE=auto
+ """),
+ },
+ 'expected_sysconfig_rhel': {
+ 'ifcfg-iface0': textwrap.dedent("""\
+ BOOTPROTO=none
+ DEVICE=iface0
+ IPV6_AUTOCONF=yes
+ IPV6INIT=yes
+ DEVICE=iface0
+ NM_CONTROLLED=no
+ ONBOOT=yes
+ TYPE=Ethernet
+ USERCTL=no
+ """),
+ },
+ },
+ 'dhcpv6_stateless': {
+ 'expected_eni': textwrap.dedent("""\
+ auto lo
+ iface lo inet loopback
+
+ auto iface0
+ iface iface0 inet6 auto
+ dhcp 1
+ """).rstrip(' '),
+ 'expected_netplan': textwrap.dedent("""
+ network:
+ version: 2
+ ethernets:
+ iface0:
+ dhcp6: true
+ """).rstrip(' '),
+ 'yaml': textwrap.dedent("""\
+ version: 1
+ config:
+ - type: 'physical'
+ name: 'iface0'
+ subnets:
+ - {'type': 'ipv6_dhcpv6-stateless'}
+ """).rstrip(' '),
+ 'expected_sysconfig_opensuse': {
+ 'ifcfg-iface0': textwrap.dedent("""\
+ BOOTPROTO=dhcp6
+ DHCLIENT6_MODE=info
+ STARTMODE=auto
+ """),
+ },
+ 'expected_sysconfig_rhel': {
+ 'ifcfg-iface0': textwrap.dedent("""\
+ BOOTPROTO=none
+ DEVICE=iface0
+ DHCPV6C=yes
+ DHCPV6C_OPTIONS=-S
+ IPV6_AUTOCONF=yes
+ IPV6INIT=yes
+ DEVICE=iface0
+ NM_CONTROLLED=no
+ ONBOOT=yes
+ TYPE=Ethernet
+ USERCTL=no
+ """),
+ },
+ },
+ 'dhcpv6_stateful': {
+ 'expected_eni': textwrap.dedent("""\
+ auto lo
+ iface lo inet loopback
+
+ auto iface0
+ iface iface0 inet6 dhcp
+ """).rstrip(' '),
+ 'expected_netplan': textwrap.dedent("""
+ network:
+ version: 2
+ ethernets:
+ iface0:
+ accept-ra: true
+ dhcp6: true
+ """).rstrip(' '),
+ 'yaml': textwrap.dedent("""\
+ version: 1
+ config:
+ - type: 'physical'
+ name: 'iface0'
+ subnets:
+ - {'type': 'ipv6_dhcpv6-stateful'}
+ accept-ra: true
+ """).rstrip(' '),
+ 'expected_sysconfig_opensuse': {
+ 'ifcfg-iface0': textwrap.dedent("""\
+ BOOTPROTO=dhcp6
+ DHCLIENT6_MODE=managed
+ STARTMODE=auto
+ """),
+ },
+ 'expected_sysconfig_rhel': {
+ 'ifcfg-iface0': textwrap.dedent("""\
+ BOOTPROTO=none
+ DEVICE=iface0
+ DHCPV6C=yes
+ IPV6INIT=yes
+ IPV6_FORCE_ACCEPT_RA=yes
+ DEVICE=iface0
+ NM_CONTROLLED=no
+ ONBOOT=yes
+ TYPE=Ethernet
+ USERCTL=no
+ """),
+ },
+ },
'all': {
'expected_eni': ("""\
auto lo
@@ -728,6 +1365,12 @@ iface eth4 inet manual
# control-manual eth5
iface eth5 inet dhcp
+auto ib0
+iface ib0 inet static
+ address 192.168.200.7/24
+ mtu 9000
+ hwaddress a0:00:02:20:fe:80:00:00:00:00:00:00:ec:0d:9a:03:00:15:e2:c1
+
auto bond0
iface bond0 inet6 dhcp
bond-mode active-backup
@@ -781,8 +1424,8 @@ iface eth0.101 inet static
iface eth0.101 inet static
address 192.168.2.10/24
-post-up route add -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true
-pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true
+post-up route add -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
+pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
"""),
'expected_netplan': textwrap.dedent("""
network:
@@ -881,7 +1524,80 @@ pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true
- sacchromyces.maas
- brettanomyces.maas
""").rstrip(' '),
- 'expected_sysconfig': {
+ 'expected_sysconfig_opensuse': {
+ 'ifcfg-bond0': textwrap.dedent("""\
+ BONDING_MASTER=yes
+ BONDING_OPTS="mode=active-backup """
+ """xmit_hash_policy=layer3+4 """
+ """miimon=100"
+ BONDING_SLAVE_0=eth1
+ BONDING_SLAVE_1=eth2
+ BOOTPROTO=dhcp6
+ DHCLIENT6_MODE=managed
+ LLADDR=aa:bb:cc:dd:ee:ff
+ STARTMODE=auto"""),
+ 'ifcfg-bond0.200': textwrap.dedent("""\
+ BOOTPROTO=dhcp4
+ ETHERDEVICE=bond0
+ STARTMODE=auto
+ VLAN_ID=200"""),
+ 'ifcfg-br0': textwrap.dedent("""\
+ BRIDGE_AGEINGTIME=250
+ BOOTPROTO=static
+ IPADDR=192.168.14.2
+ IPADDR6=2001:1::1/64
+ LLADDRESS=bb:bb:bb:bb:bb:aa
+ NETMASK=255.255.255.0
+ BRIDGE_PRIORITY=22
+ BRIDGE_PORTS='eth3 eth4'
+ STARTMODE=auto
+ BRIDGE_STP=off"""),
+ 'ifcfg-eth0': textwrap.dedent("""\
+ BOOTPROTO=static
+ LLADDR=c0:d6:9f:2c:e8:80
+ STARTMODE=auto"""),
+ 'ifcfg-eth0.101': textwrap.dedent("""\
+ BOOTPROTO=static
+ IPADDR=192.168.0.2
+ IPADDR1=192.168.2.10
+ MTU=1500
+ NETMASK=255.255.255.0
+ NETMASK1=255.255.255.0
+ ETHERDEVICE=eth0
+ STARTMODE=auto
+ VLAN_ID=101"""),
+ 'ifcfg-eth1': textwrap.dedent("""\
+ BOOTPROTO=none
+ LLADDR=aa:d6:9f:2c:e8:80
+ STARTMODE=hotplug"""),
+ 'ifcfg-eth2': textwrap.dedent("""\
+ BOOTPROTO=none
+ LLADDR=c0:bb:9f:2c:e8:80
+ STARTMODE=hotplug"""),
+ 'ifcfg-eth3': textwrap.dedent("""\
+ BOOTPROTO=static
+ BRIDGE=yes
+ LLADDR=66:bb:9f:2c:e8:80
+ STARTMODE=auto"""),
+ 'ifcfg-eth4': textwrap.dedent("""\
+ BOOTPROTO=static
+ BRIDGE=yes
+ LLADDR=98:bb:9f:2c:e8:80
+ STARTMODE=auto"""),
+ 'ifcfg-eth5': textwrap.dedent("""\
+ BOOTPROTO=dhcp
+ LLADDR=98:bb:9f:2c:e8:8a
+ STARTMODE=manual"""),
+ 'ifcfg-ib0': textwrap.dedent("""\
+ BOOTPROTO=static
+ LLADDR=a0:00:02:20:fe:80:00:00:00:00:00:00:ec:0d:9a:03:00:15:e2:c1
+ IPADDR=192.168.200.7
+ MTU=9000
+ NETMASK=255.255.255.0
+ STARTMODE=auto
+ TYPE=InfiniBand"""),
+ },
+ 'expected_sysconfig_rhel': {
'ifcfg-bond0': textwrap.dedent("""\
BONDING_MASTER=yes
BONDING_OPTS="mode=active-backup """
@@ -901,6 +1617,7 @@ pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true
'ifcfg-bond0.200': textwrap.dedent("""\
BOOTPROTO=dhcp
DEVICE=bond0.200
+ DHCLIENT_SET_DEFAULT_ROUTE=no
NM_CONTROLLED=no
ONBOOT=yes
PHYSDEV=bond0
@@ -992,11 +1709,23 @@ pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true
'ifcfg-eth5': textwrap.dedent("""\
BOOTPROTO=dhcp
DEVICE=eth5
+ DHCLIENT_SET_DEFAULT_ROUTE=no
HWADDR=98:bb:9f:2c:e8:8a
NM_CONTROLLED=no
ONBOOT=no
TYPE=Ethernet
- USERCTL=no""")
+ USERCTL=no"""),
+ 'ifcfg-ib0': textwrap.dedent("""\
+ BOOTPROTO=none
+ DEVICE=ib0
+ HWADDR=a0:00:02:20:fe:80:00:00:00:00:00:00:ec:0d:9a:03:00:15:e2:c1
+ IPADDR=192.168.200.7
+ MTU=9000
+ NETMASK=255.255.255.0
+ NM_CONTROLLED=no
+ ONBOOT=yes
+ TYPE=InfiniBand
+ USERCTL=no"""),
},
'yaml': textwrap.dedent("""
version: 1
@@ -1071,6 +1800,15 @@ pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true
vlan_id: 200
subnets:
- type: dhcp4
+ # An infiniband
+ - type: infiniband
+ name: ib0
+ mac_address: >-
+ a0:00:02:20:fe:80:00:00:00:00:00:00:ec:0d:9a:03:00:15:e2:c1
+ subnets:
+ - type: static
+ address: 192.168.200.7/24
+ mtu: 9000
# A bridge.
- type: bridge
name: br0
@@ -1155,6 +1893,12 @@ pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true
bond-mode: active-backup
bond_miimon: 100
bond-xmit-hash-policy: "layer3+4"
+ bond-num-grat-arp: 5
+ bond-downdelay: 10
+ bond-updelay: 20
+ bond-fail-over-mac: active
+ bond-primary: bond0s0
+ bond-primary-reselect: always
subnets:
- type: static
address: 192.168.0.2/24
@@ -1163,17 +1907,18 @@ pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true
- gateway: 192.168.0.3
netmask: 255.255.255.0
network: 10.1.3.0
- - gateway: 2001:67c:1562:1
- network: 2001:67c:1
- netmask: ffff:ffff:0
- - gateway: 3001:67c:1562:1
- network: 3001:67c:1
- netmask: ffff:ffff:0
- metric: 10000
- type: static
address: 192.168.1.2/24
- type: static
address: 2001:1::1/92
+ routes:
+ - gateway: 2001:67c:1562:1
+ network: 2001:67c:1
+ netmask: ffff:ffff:0
+ - gateway: 3001:67c:1562:1
+ network: 3001:67c:1
+ netmask: ffff:ffff:0
+ metric: 10000
"""),
'expected_netplan': textwrap.dedent("""
network:
@@ -1200,9 +1945,15 @@ pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true
macaddress: aa:bb:cc:dd:e8:ff
mtu: 9000
parameters:
+ down-delay: 10
+ fail-over-mac-policy: active
+ gratuitious-arp: 5
mii-monitor-interval: 100
mode: active-backup
+ primary: bond0s0
+ primary-reselect-policy: always
transmit-hash-policy: layer3+4
+ up-delay: 20
routes:
- to: 10.1.3.0/24
via: 192.168.0.3
@@ -1212,6 +1963,69 @@ pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true
to: 3001:67c:1/32
via: 3001:67c:1562:1
"""),
+ 'expected_eni': textwrap.dedent("""\
+auto lo
+iface lo inet loopback
+
+auto bond0s0
+iface bond0s0 inet manual
+ bond-downdelay 10
+ bond-fail-over-mac active
+ bond-master bond0
+ bond-mode active-backup
+ bond-num-grat-arp 5
+ bond-primary bond0s0
+ bond-primary-reselect always
+ bond-updelay 20
+ bond-xmit-hash-policy layer3+4
+ bond_miimon 100
+
+auto bond0s1
+iface bond0s1 inet manual
+ bond-downdelay 10
+ bond-fail-over-mac active
+ bond-master bond0
+ bond-mode active-backup
+ bond-num-grat-arp 5
+ bond-primary bond0s0
+ bond-primary-reselect always
+ bond-updelay 20
+ bond-xmit-hash-policy layer3+4
+ bond_miimon 100
+
+auto bond0
+iface bond0 inet static
+ address 192.168.0.2/24
+ gateway 192.168.0.1
+ bond-downdelay 10
+ bond-fail-over-mac active
+ bond-mode active-backup
+ bond-num-grat-arp 5
+ bond-primary bond0s0
+ bond-primary-reselect always
+ bond-slaves none
+ bond-updelay 20
+ bond-xmit-hash-policy layer3+4
+ bond_miimon 100
+ hwaddress aa:bb:cc:dd:e8:ff
+ mtu 9000
+ post-up route add -net 10.1.3.0/24 gw 192.168.0.3 || true
+ pre-down route del -net 10.1.3.0/24 gw 192.168.0.3 || true
+
+# control-alias bond0
+iface bond0 inet static
+ address 192.168.1.2/24
+
+# control-alias bond0
+iface bond0 inet6 static
+ address 2001:1::1/92
+ post-up route add -A inet6 2001:67c:1/32 gw 2001:67c:1562:1 || true
+ pre-down route del -A inet6 2001:67c:1/32 gw 2001:67c:1562:1 || true
+ post-up route add -A inet6 3001:67c:1/32 gw 3001:67c:1562:1 metric 10000 \
+|| true
+ pre-down route del -A inet6 3001:67c:1/32 gw 3001:67c:1562:1 metric 10000 \
+|| true
+ """),
'yaml-v2': textwrap.dedent("""
version: 2
ethernets:
@@ -1235,10 +2049,15 @@ pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true
- eth0
- vf0
parameters:
+ down-delay: 10
+ fail-over-mac-policy: active
+ gratuitious-arp: 5
mii-monitor-interval: 100
mode: active-backup
- primary: vf0
- transmit-hash-policy: "layer3+4"
+ primary: bond0s0
+ primary-reselect-policy: always
+ transmit-hash-policy: layer3+4
+ up-delay: 20
routes:
- to: 10.1.3.0/24
via: 192.168.0.3
@@ -1261,10 +2080,15 @@ pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true
- eth0
- vf0
parameters:
+ down-delay: 10
+ fail-over-mac-policy: active
+ gratuitious-arp: 5
mii-monitor-interval: 100
mode: active-backup
- primary: vf0
+ primary: bond0s0
+ primary-reselect-policy: always
transmit-hash-policy: layer3+4
+ up-delay: 20
routes:
- to: 10.1.3.0/24
via: 192.168.0.3
@@ -1289,59 +2113,44 @@ pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true
'expected_sysconfig_opensuse': {
'ifcfg-bond0': textwrap.dedent("""\
BONDING_MASTER=yes
- BONDING_OPTS="mode=active-backup xmit_hash_policy=layer3+4 miimon=100"
- BONDING_SLAVE0=bond0s0
- BONDING_SLAVE1=bond0s1
- BOOTPROTO=none
- DEFROUTE=yes
- DEVICE=bond0
- GATEWAY=192.168.0.1
- MACADDR=aa:bb:cc:dd:e8:ff
+ BONDING_OPTS="mode=active-backup xmit_hash_policy=layer3+4 """
+ """miimon=100 num_grat_arp=5 """
+ """downdelay=10 updelay=20 """
+ """fail_over_mac=active """
+ """primary=bond0s0 """
+ """primary_reselect=always"
+ BONDING_SLAVE_0=bond0s0
+ BONDING_SLAVE_1=bond0s1
+ BOOTPROTO=static
+ LLADDR=aa:bb:cc:dd:e8:ff
IPADDR=192.168.0.2
IPADDR1=192.168.1.2
- IPV6ADDR=2001:1::1/92
- IPV6INIT=yes
+ IPADDR6=2001:1::1/92
MTU=9000
NETMASK=255.255.255.0
NETMASK1=255.255.255.0
- NM_CONTROLLED=no
- ONBOOT=yes
- TYPE=Bond
- USERCTL=no
+ STARTMODE=auto
"""),
'ifcfg-bond0s0': textwrap.dedent("""\
BOOTPROTO=none
- DEVICE=bond0s0
- HWADDR=aa:bb:cc:dd:e8:00
- MASTER=bond0
- NM_CONTROLLED=no
- ONBOOT=yes
- SLAVE=yes
- TYPE=Ethernet
- USERCTL=no
- """),
- 'ifroute-bond0': textwrap.dedent("""\
- ADDRESS0=10.1.3.0
- GATEWAY0=192.168.0.3
- NETMASK0=255.255.255.0
+ LLADDR=aa:bb:cc:dd:e8:00
+ STARTMODE=hotplug
"""),
'ifcfg-bond0s1': textwrap.dedent("""\
BOOTPROTO=none
- DEVICE=bond0s1
- HWADDR=aa:bb:cc:dd:e8:01
- MASTER=bond0
- NM_CONTROLLED=no
- ONBOOT=yes
- SLAVE=yes
- TYPE=Ethernet
- USERCTL=no
+ LLADDR=aa:bb:cc:dd:e8:01
+ STARTMODE=hotplug
"""),
},
-
'expected_sysconfig_rhel': {
'ifcfg-bond0': textwrap.dedent("""\
BONDING_MASTER=yes
- BONDING_OPTS="mode=active-backup xmit_hash_policy=layer3+4 miimon=100"
+ BONDING_OPTS="mode=active-backup xmit_hash_policy=layer3+4 """
+ """miimon=100 num_grat_arp=5 """
+ """downdelay=10 updelay=20 """
+ """fail_over_mac=active """
+ """primary=bond0s0 """
+ """primary_reselect=always"
BONDING_SLAVE0=bond0s0
BONDING_SLAVE1=bond0s1
BOOTPROTO=none
@@ -1421,7 +2230,26 @@ pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true
netmask: '::'
network: '::'
"""),
- 'expected_sysconfig': {
+ 'expected_sysconfig_opensuse': {
+ # TODO RJS: unknown proper BOOTPROTO setting ask Marius
+ 'ifcfg-en0': textwrap.dedent("""\
+ BOOTPROTO=static
+ LLADDR=aa:bb:cc:dd:e8:00
+ STARTMODE=auto"""),
+ 'ifcfg-en0.99': textwrap.dedent("""\
+ BOOTPROTO=static
+ IPADDR=192.168.2.2
+ IPADDR1=192.168.1.2
+ IPADDR6=2001:1::bbbb/96
+ MTU=2222
+ NETMASK=255.255.255.0
+ NETMASK1=255.255.255.0
+ STARTMODE=auto
+ ETHERDEVICE=en0
+ VLAN_ID=99
+ """),
+ },
+ 'expected_sysconfig_rhel': {
'ifcfg-en0': textwrap.dedent("""\
BOOTPROTO=none
DEVICE=en0
@@ -1478,7 +2306,32 @@ pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true
subnets:
- type: static
address: 192.168.2.2/24"""),
- 'expected_sysconfig': {
+ 'expected_sysconfig_opensuse': {
+ 'ifcfg-br0': textwrap.dedent("""\
+ BOOTPROTO=static
+ IPADDR=192.168.2.2
+ NETMASK=255.255.255.0
+ STARTMODE=auto
+ BRIDGE_STP=off
+ BRIDGE_PRIORITY=22
+ BRIDGE_PORTS='eth0 eth1'
+ """),
+ 'ifcfg-eth0': textwrap.dedent("""\
+ BOOTPROTO=static
+ BRIDGE=yes
+ LLADDR=52:54:00:12:34:00
+ IPADDR6=2001:1::100/96
+ STARTMODE=auto
+ """),
+ 'ifcfg-eth1': textwrap.dedent("""\
+ BOOTPROTO=static
+ BRIDGE=yes
+ LLADDR=52:54:00:12:34:01
+ IPADDR6=2001:1::101/96
+ STARTMODE=auto
+ """),
+ },
+ 'expected_sysconfig_rhel': {
'ifcfg-br0': textwrap.dedent("""\
BOOTPROTO=none
DEVICE=br0
@@ -1577,7 +2430,27 @@ pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true
macaddress: 52:54:00:12:34:ff
set-name: eth2
"""),
- 'expected_sysconfig': {
+ 'expected_sysconfig_opensuse': {
+ 'ifcfg-eth0': textwrap.dedent("""\
+ BOOTPROTO=static
+ LLADDR=52:54:00:12:34:00
+ IPADDR=192.168.1.2
+ NETMASK=255.255.255.0
+ STARTMODE=manual
+ """),
+ 'ifcfg-eth1': textwrap.dedent("""\
+ BOOTPROTO=static
+ LLADDR=52:54:00:12:34:aa
+ MTU=1480
+ STARTMODE=auto
+ """),
+ 'ifcfg-eth2': textwrap.dedent("""\
+ BOOTPROTO=static
+ LLADDR=52:54:00:12:34:ff
+ STARTMODE=manual
+ """),
+ },
+ 'expected_sysconfig_rhel': {
'ifcfg-eth0': textwrap.dedent("""\
BOOTPROTO=none
DEVICE=eth0
@@ -1632,6 +2505,23 @@ CONFIG_V1_SIMPLE_SUBNET = {
'type': 'static'}],
'type': 'physical'}]}
+CONFIG_V1_MULTI_IFACE = {
+ 'version': 1,
+ 'config': [{'type': 'physical',
+ 'mtu': 1500,
+ 'subnets': [{'type': 'static',
+ 'netmask': '255.255.240.0',
+ 'routes': [{'netmask': '0.0.0.0',
+ 'network': '0.0.0.0',
+ 'gateway': '51.68.80.1'}],
+ 'address': '51.68.89.122',
+ 'ipv4': True}],
+ 'mac_address': 'fa:16:3e:25:b4:59',
+ 'name': 'eth0'},
+ {'type': 'physical',
+ 'mtu': 9000,
+ 'subnets': [{'type': 'dhcp4'}],
+ 'mac_address': 'fa:16:3e:b1:ca:29', 'name': 'eth1'}]}
DEFAULT_DEV_ATTRS = {
'eth1000': {
@@ -1639,7 +2529,7 @@ DEFAULT_DEV_ATTRS = {
"carrier": False,
"dormant": False,
"operstate": "down",
- "address": "07-1C-C6-75-A4-BE",
+ "address": "07-1c-c6-75-a4-be",
"device/driver": None,
"device/device": None,
"name_assign_type": "4",
@@ -1690,6 +2580,39 @@ class TestGenerateFallbackConfig(CiTestCase):
@mock.patch("cloudinit.net.sys_dev_path")
@mock.patch("cloudinit.net.read_sys_net")
@mock.patch("cloudinit.net.get_devicelist")
+ def test_device_driver_v2(self, mock_get_devicelist, mock_read_sys_net,
+ mock_sys_dev_path):
+ """Network configuration for generate_fallback_config is version 2."""
+ devices = {
+ 'eth0': {
+ 'bridge': False, 'carrier': False, 'dormant': False,
+ 'operstate': 'down', 'address': '00:11:22:33:44:55',
+ 'device/driver': 'hv_netsvc', 'device/device': '0x3',
+ 'name_assign_type': '4'},
+ 'eth1': {
+ 'bridge': False, 'carrier': False, 'dormant': False,
+ 'operstate': 'down', 'address': '00:11:22:33:44:55',
+ 'device/driver': 'mlx4_core', 'device/device': '0x7',
+ 'name_assign_type': '4'},
+
+ }
+
+ tmp_dir = self.tmp_dir()
+ _setup_test(tmp_dir, mock_get_devicelist,
+ mock_read_sys_net, mock_sys_dev_path,
+ dev_attrs=devices)
+
+ network_cfg = net.generate_fallback_config(config_driver=True)
+ expected = {
+ 'ethernets': {'eth0': {'dhcp4': True, 'set-name': 'eth0',
+ 'match': {'macaddress': '00:11:22:33:44:55',
+ 'driver': 'hv_netsvc'}}},
+ 'version': 2}
+ self.assertEqual(expected, network_cfg)
+
+ @mock.patch("cloudinit.net.sys_dev_path")
+ @mock.patch("cloudinit.net.read_sys_net")
+ @mock.patch("cloudinit.net.get_devicelist")
def test_device_driver(self, mock_get_devicelist, mock_read_sys_net,
mock_sys_dev_path):
devices = {
@@ -1880,11 +2803,12 @@ class TestRhelSysConfigRendering(CiTestCase):
with_logs = True
+ nm_cfg_file = "/etc/NetworkManager/NetworkManager.conf"
scripts_dir = '/etc/sysconfig/network-scripts'
header = ('# Created by cloud-init on instance boot automatically, '
'do not edit.\n#\n')
- expected_name = 'expected_sysconfig'
+ expected_name = 'expected_sysconfig_rhel'
def _get_renderer(self):
distro_cls = distros.fetch('rhel')
@@ -1968,7 +2892,7 @@ class TestRhelSysConfigRendering(CiTestCase):
#
BOOTPROTO=dhcp
DEVICE=eth1000
-HWADDR=07-1C-C6-75-A4-BE
+HWADDR=07-1c-c6-75-a4-be
NM_CONTROLLED=no
ONBOOT=yes
TYPE=Ethernet
@@ -2096,11 +3020,60 @@ TYPE=Ethernet
USERCTL=no
"""
self.assertEqual(expected, found[nspath + 'ifcfg-interface0'])
+ # The configuration has no nameserver information make sure we
+ # do not write the resolv.conf file
+ respath = '/etc/resolv.conf'
+ self.assertNotIn(respath, found.keys())
+
+ def test_network_config_v1_multi_iface_samples(self):
+ ns = network_state.parse_net_config_data(CONFIG_V1_MULTI_IFACE)
+ render_dir = self.tmp_path("render")
+ os.makedirs(render_dir)
+ renderer = self._get_renderer()
+ renderer.render_network_state(ns, target=render_dir)
+ found = dir2dict(render_dir)
+ nspath = '/etc/sysconfig/network-scripts/'
+ self.assertNotIn(nspath + 'ifcfg-lo', found.keys())
+ expected_i1 = """\
+# Created by cloud-init on instance boot automatically, do not edit.
+#
+BOOTPROTO=none
+DEFROUTE=yes
+DEVICE=eth0
+GATEWAY=51.68.80.1
+HWADDR=fa:16:3e:25:b4:59
+IPADDR=51.68.89.122
+MTU=1500
+NETMASK=255.255.240.0
+NM_CONTROLLED=no
+ONBOOT=yes
+TYPE=Ethernet
+USERCTL=no
+"""
+ self.assertEqual(expected_i1, found[nspath + 'ifcfg-eth0'])
+ expected_i2 = """\
+# Created by cloud-init on instance boot automatically, do not edit.
+#
+BOOTPROTO=dhcp
+DEVICE=eth1
+DHCLIENT_SET_DEFAULT_ROUTE=no
+HWADDR=fa:16:3e:b1:ca:29
+MTU=9000
+NM_CONTROLLED=no
+ONBOOT=yes
+TYPE=Ethernet
+USERCTL=no
+"""
+ self.assertEqual(expected_i2, found[nspath + 'ifcfg-eth1'])
def test_config_with_explicit_loopback(self):
ns = network_state.parse_net_config_data(CONFIG_V1_EXPLICIT_LOOPBACK)
render_dir = self.tmp_path("render")
os.makedirs(render_dir)
+ # write an etc/resolv.conf and expect it to not be modified
+ resolvconf = os.path.join(render_dir, 'etc/resolv.conf')
+ resolvconf_content = "# Original Content"
+ util.write_file(resolvconf, resolvconf_content)
renderer = self._get_renderer()
renderer.render_network_state(ns, target=render_dir)
found = dir2dict(render_dir)
@@ -2117,12 +3090,13 @@ TYPE=Ethernet
USERCTL=no
"""
self.assertEqual(expected, found[nspath + 'ifcfg-eth0'])
+ # a dhcp only config should not modify resolv.conf
+ self.assertEqual(resolvconf_content, found['/etc/resolv.conf'])
def test_bond_config(self):
- expected_name = 'expected_sysconfig_rhel'
entry = NETWORK_CONFIGS['bond']
found = self._render_and_read(network_config=yaml.load(entry['yaml']))
- self._compare_files_to_expected(entry[expected_name], found)
+ self._compare_files_to_expected(entry[self.expected_name], found)
self._assert_headers(found)
def test_vlan_config(self):
@@ -2174,6 +3148,273 @@ USERCTL=no
self._compare_files_to_expected(entry[self.expected_name], found)
self._assert_headers(found)
+ def test_dhcpv6_accept_ra_config_v1(self):
+ entry = NETWORK_CONFIGS['dhcpv6_accept_ra']
+ found = self._render_and_read(network_config=yaml.load(
+ entry['yaml_v1']))
+ self._compare_files_to_expected(entry[self.expected_name], found)
+ self._assert_headers(found)
+
+ def test_dhcpv6_accept_ra_config_v2(self):
+ entry = NETWORK_CONFIGS['dhcpv6_accept_ra']
+ found = self._render_and_read(network_config=yaml.load(
+ entry['yaml_v2']))
+ self._compare_files_to_expected(entry[self.expected_name], found)
+ self._assert_headers(found)
+
+ def test_dhcpv6_reject_ra_config_v1(self):
+ entry = NETWORK_CONFIGS['dhcpv6_reject_ra']
+ found = self._render_and_read(network_config=yaml.load(
+ entry['yaml_v1']))
+ self._compare_files_to_expected(entry[self.expected_name], found)
+ self._assert_headers(found)
+
+ def test_dhcpv6_reject_ra_config_v2(self):
+ entry = NETWORK_CONFIGS['dhcpv6_reject_ra']
+ found = self._render_and_read(network_config=yaml.load(
+ entry['yaml_v2']))
+ self._compare_files_to_expected(entry[self.expected_name], found)
+ self._assert_headers(found)
+
+ def test_dhcpv6_stateless_config(self):
+ entry = NETWORK_CONFIGS['dhcpv6_stateless']
+ found = self._render_and_read(network_config=yaml.load(entry['yaml']))
+ self._compare_files_to_expected(entry[self.expected_name], found)
+ self._assert_headers(found)
+
+ def test_dhcpv6_stateful_config(self):
+ entry = NETWORK_CONFIGS['dhcpv6_stateful']
+ found = self._render_and_read(network_config=yaml.load(entry['yaml']))
+ self._compare_files_to_expected(entry[self.expected_name], found)
+ self._assert_headers(found)
+
+ def test_check_ifcfg_rh(self):
+ """ifcfg-rh plugin is added NetworkManager.conf if conf present."""
+ render_dir = self.tmp_dir()
+ nm_cfg = util.target_path(render_dir, path=self.nm_cfg_file)
+ util.ensure_dir(os.path.dirname(nm_cfg))
+
+ # write a template nm.conf, note plugins is a list here
+ with open(nm_cfg, 'w') as fh:
+ fh.write('# test_check_ifcfg_rh\n[main]\nplugins=foo,bar\n')
+ self.assertTrue(os.path.exists(nm_cfg))
+
+ # render and read
+ entry = NETWORK_CONFIGS['small']
+ found = self._render_and_read(network_config=yaml.load(entry['yaml']),
+ dir=render_dir)
+ self._compare_files_to_expected(entry[self.expected_name], found)
+ self._assert_headers(found)
+
+ # check ifcfg-rh is in the 'plugins' list
+ config = sysconfig.ConfigObj(nm_cfg)
+ self.assertIn('ifcfg-rh', config['main']['plugins'])
+
+ def test_check_ifcfg_rh_plugins_string(self):
+ """ifcfg-rh plugin is append when plugins is a string."""
+ render_dir = self.tmp_path("render")
+ os.makedirs(render_dir)
+ nm_cfg = util.target_path(render_dir, path=self.nm_cfg_file)
+ util.ensure_dir(os.path.dirname(nm_cfg))
+
+ # write a template nm.conf, note plugins is a value here
+ util.write_file(nm_cfg, '# test_check_ifcfg_rh\n[main]\nplugins=foo\n')
+
+ # render and read
+ entry = NETWORK_CONFIGS['small']
+ found = self._render_and_read(network_config=yaml.load(entry['yaml']),
+ dir=render_dir)
+ self._compare_files_to_expected(entry[self.expected_name], found)
+ self._assert_headers(found)
+
+ # check raw content has plugin
+ nm_file_content = util.load_file(nm_cfg)
+ self.assertIn('ifcfg-rh', nm_file_content)
+
+ # check ifcfg-rh is in the 'plugins' list
+ config = sysconfig.ConfigObj(nm_cfg)
+ self.assertIn('ifcfg-rh', config['main']['plugins'])
+
+ def test_check_ifcfg_rh_plugins_no_plugins(self):
+ """enable_ifcfg_plugin creates plugins value if missing."""
+ render_dir = self.tmp_path("render")
+ os.makedirs(render_dir)
+ nm_cfg = util.target_path(render_dir, path=self.nm_cfg_file)
+ util.ensure_dir(os.path.dirname(nm_cfg))
+
+ # write a template nm.conf, note plugins is missing
+ util.write_file(nm_cfg, '# test_check_ifcfg_rh\n[main]\n')
+ self.assertTrue(os.path.exists(nm_cfg))
+
+ # render and read
+ entry = NETWORK_CONFIGS['small']
+ found = self._render_and_read(network_config=yaml.load(entry['yaml']),
+ dir=render_dir)
+ self._compare_files_to_expected(entry[self.expected_name], found)
+ self._assert_headers(found)
+
+ # check ifcfg-rh is in the 'plugins' list
+ config = sysconfig.ConfigObj(nm_cfg)
+ self.assertIn('ifcfg-rh', config['main']['plugins'])
+
+ def test_netplan_dhcp_false_disable_dhcp_in_state(self):
+ """netplan config with dhcp[46]: False should not add dhcp in state"""
+ net_config = yaml.load(NETPLAN_DHCP_FALSE)
+ ns = network_state.parse_net_config_data(net_config,
+ skip_broken=False)
+
+ dhcp_found = [snet for iface in ns.iter_interfaces()
+ for snet in iface['subnets'] if 'dhcp' in snet['type']]
+
+ self.assertEqual([], dhcp_found)
+
+ def test_netplan_dhcp_false_no_dhcp_in_sysconfig(self):
+ """netplan cfg with dhcp[46]: False should not have bootproto=dhcp"""
+
+ entry = {
+ 'yaml': NETPLAN_DHCP_FALSE,
+ 'expected_sysconfig': {
+ 'ifcfg-ens3': textwrap.dedent("""\
+ BOOTPROTO=none
+ DEFROUTE=yes
+ DEVICE=ens3
+ DNS1=192.168.42.53
+ DNS2=1.1.1.1
+ DOMAIN=example.com
+ GATEWAY=192.168.42.1
+ HWADDR=52:54:00:ab:cd:ef
+ IPADDR=192.168.42.100
+ IPV6ADDR=2001:db8::100/32
+ IPV6INIT=yes
+ IPV6_DEFAULTGW=2001:db8::1
+ NETMASK=255.255.255.0
+ NM_CONTROLLED=no
+ ONBOOT=yes
+ TYPE=Ethernet
+ USERCTL=no
+ """),
+ }
+ }
+
+ found = self._render_and_read(network_config=yaml.load(entry['yaml']))
+ self._compare_files_to_expected(entry['expected_sysconfig'], found)
+ self._assert_headers(found)
+
+ def test_from_v2_vlan_mtu(self):
+ """verify mtu gets rendered on bond when source is netplan."""
+ v2data = {
+ 'version': 2,
+ 'ethernets': {'eno1': {}},
+ 'vlans': {
+ 'eno1.1000': {
+ 'addresses': ["192.6.1.9/24"],
+ 'id': 1000, 'link': 'eno1', 'mtu': 1495}}}
+ expected = {
+ 'ifcfg-eno1': textwrap.dedent("""\
+ BOOTPROTO=none
+ DEVICE=eno1
+ NM_CONTROLLED=no
+ ONBOOT=yes
+ TYPE=Ethernet
+ USERCTL=no
+ """),
+ 'ifcfg-eno1.1000': textwrap.dedent("""\
+ BOOTPROTO=none
+ DEVICE=eno1.1000
+ IPADDR=192.6.1.9
+ MTU=1495
+ NETMASK=255.255.255.0
+ NM_CONTROLLED=no
+ ONBOOT=yes
+ PHYSDEV=eno1
+ TYPE=Ethernet
+ USERCTL=no
+ VLAN=yes
+ """)
+ }
+ self._compare_files_to_expected(
+ expected, self._render_and_read(network_config=v2data))
+
+ def test_from_v2_bond_mtu(self):
+ """verify mtu gets rendered on bond when source is netplan."""
+ v2data = {
+ 'version': 2,
+ 'bonds': {
+ 'bond0': {'addresses': ['10.101.8.65/26'],
+ 'interfaces': ['enp0s0', 'enp0s1'],
+ 'mtu': 1334,
+ 'parameters': {}}}
+ }
+ expected = {
+ 'ifcfg-bond0': textwrap.dedent("""\
+ BONDING_MASTER=yes
+ BONDING_SLAVE0=enp0s0
+ BONDING_SLAVE1=enp0s1
+ BOOTPROTO=none
+ DEVICE=bond0
+ IPADDR=10.101.8.65
+ MTU=1334
+ NETMASK=255.255.255.192
+ NM_CONTROLLED=no
+ ONBOOT=yes
+ TYPE=Bond
+ USERCTL=no
+ """),
+ 'ifcfg-enp0s0': textwrap.dedent("""\
+ BONDING_MASTER=yes
+ BOOTPROTO=none
+ DEVICE=enp0s0
+ MASTER=bond0
+ NM_CONTROLLED=no
+ ONBOOT=yes
+ SLAVE=yes
+ TYPE=Bond
+ USERCTL=no
+ """),
+ 'ifcfg-enp0s1': textwrap.dedent("""\
+ BONDING_MASTER=yes
+ BOOTPROTO=none
+ DEVICE=enp0s1
+ MASTER=bond0
+ NM_CONTROLLED=no
+ ONBOOT=yes
+ SLAVE=yes
+ TYPE=Bond
+ USERCTL=no
+ """)
+ }
+ self._compare_files_to_expected(
+ expected, self._render_and_read(network_config=v2data))
+
+ def test_from_v2_route_metric(self):
+ """verify route-metric gets rendered on nic when source is netplan."""
+ overrides = {'route-metric': 100}
+ v2base = {
+ 'version': 2,
+ 'ethernets': {
+ 'eno1': {'dhcp4': True,
+ 'match': {'macaddress': '07-1c-c6-75-a4-be'}}}}
+ expected = {
+ 'ifcfg-eno1': textwrap.dedent("""\
+ BOOTPROTO=dhcp
+ DEVICE=eno1
+ HWADDR=07-1c-c6-75-a4-be
+ METRIC=100
+ NM_CONTROLLED=no
+ ONBOOT=yes
+ TYPE=Ethernet
+ USERCTL=no
+ """),
+ }
+ for dhcp_ver in ('dhcp4', 'dhcp6'):
+ v2data = copy.deepcopy(v2base)
+ if dhcp_ver == 'dhcp6':
+ expected['ifcfg-eno1'] += "IPV6INIT=yes\nDHCPV6C=yes\n"
+ v2data['ethernets']['eno1'].update(
+ {dhcp_ver: True, '{0}-overrides'.format(dhcp_ver): overrides})
+ self._compare_files_to_expected(
+ expected, self._render_and_read(network_config=v2data))
+
class TestOpenSuseSysConfigRendering(CiTestCase):
@@ -2183,7 +3424,7 @@ class TestOpenSuseSysConfigRendering(CiTestCase):
header = ('# Created by cloud-init on instance boot automatically, '
'do not edit.\n#\n')
- expected_name = 'expected_sysconfig'
+ expected_name = 'expected_sysconfig_opensuse'
def _get_renderer(self):
distro_cls = distros.fetch('opensuse')
@@ -2255,91 +3496,89 @@ class TestOpenSuseSysConfigRendering(CiTestCase):
expected_content = """
# Created by cloud-init on instance boot automatically, do not edit.
#
-BOOTPROTO=dhcp
-DEVICE=eth1000
-HWADDR=07-1C-C6-75-A4-BE
-NM_CONTROLLED=no
-ONBOOT=yes
-TYPE=Ethernet
-USERCTL=no
+BOOTPROTO=dhcp4
+LLADDR=07-1c-c6-75-a4-be
+STARTMODE=auto
""".lstrip()
self.assertEqual(expected_content, content)
- def test_multiple_ipv4_default_gateways(self):
- """ValueError is raised when duplicate ipv4 gateways exist."""
- net_json = {
- "services": [{"type": "dns", "address": "172.19.0.12"}],
- "networks": [{
- "network_id": "dacd568d-5be6-4786-91fe-750c374b78b4",
- "type": "ipv4", "netmask": "255.255.252.0",
- "link": "tap1a81968a-79",
- "routes": [{
- "netmask": "0.0.0.0",
- "network": "0.0.0.0",
- "gateway": "172.19.3.254",
- }, {
- "netmask": "0.0.0.0", # A second default gateway
- "network": "0.0.0.0",
- "gateway": "172.20.3.254",
- }],
- "ip_address": "172.19.1.34", "id": "network0"
- }],
- "links": [
- {
- "ethernet_mac_address": "fa:16:3e:ed:9a:59",
- "mtu": None, "type": "bridge", "id":
- "tap1a81968a-79",
- "vif_id": "1a81968a-797a-400f-8a80-567f997eb93f"
- },
- ],
- }
- macs = {'fa:16:3e:ed:9a:59': 'eth0'}
- render_dir = self.tmp_dir()
- network_cfg = openstack.convert_net_json(net_json, known_macs=macs)
- ns = network_state.parse_net_config_data(network_cfg,
- skip_broken=False)
- renderer = self._get_renderer()
- with self.assertRaises(ValueError):
- renderer.render_network_state(ns, target=render_dir)
- self.assertEqual([], os.listdir(render_dir))
-
- def test_multiple_ipv6_default_gateways(self):
- """ValueError is raised when duplicate ipv6 gateways exist."""
- net_json = {
- "services": [{"type": "dns", "address": "172.19.0.12"}],
- "networks": [{
- "network_id": "public-ipv6",
- "type": "ipv6", "netmask": "",
- "link": "tap1a81968a-79",
- "routes": [{
- "gateway": "2001:DB8::1",
- "netmask": "::",
- "network": "::"
- }, {
- "gateway": "2001:DB9::1",
- "netmask": "::",
- "network": "::"
- }],
- "ip_address": "2001:DB8::10", "id": "network1"
- }],
- "links": [
- {
- "ethernet_mac_address": "fa:16:3e:ed:9a:59",
- "mtu": None, "type": "bridge", "id":
- "tap1a81968a-79",
- "vif_id": "1a81968a-797a-400f-8a80-567f997eb93f"
- },
- ],
- }
- macs = {'fa:16:3e:ed:9a:59': 'eth0'}
- render_dir = self.tmp_dir()
- network_cfg = openstack.convert_net_json(net_json, known_macs=macs)
- ns = network_state.parse_net_config_data(network_cfg,
- skip_broken=False)
- renderer = self._get_renderer()
- with self.assertRaises(ValueError):
- renderer.render_network_state(ns, target=render_dir)
- self.assertEqual([], os.listdir(render_dir))
+ # TODO(rjschwei): re-enable test once route writing is implemented
+ # for SUSE distros
+# def test_multiple_ipv4_default_gateways(self):
+# """ValueError is raised when duplicate ipv4 gateways exist."""
+# net_json = {
+# "services": [{"type": "dns", "address": "172.19.0.12"}],
+# "networks": [{
+# "network_id": "dacd568d-5be6-4786-91fe-750c374b78b4",
+# "type": "ipv4", "netmask": "255.255.252.0",
+# "link": "tap1a81968a-79",
+# "routes": [{
+# "netmask": "0.0.0.0",
+# "network": "0.0.0.0",
+# "gateway": "172.19.3.254",
+# }, {
+# "netmask": "0.0.0.0", # A second default gateway
+# "network": "0.0.0.0",
+# "gateway": "172.20.3.254",
+# }],
+# "ip_address": "172.19.1.34", "id": "network0"
+# }],
+# "links": [
+# {
+# "ethernet_mac_address": "fa:16:3e:ed:9a:59",
+# "mtu": None, "type": "bridge", "id":
+# "tap1a81968a-79",
+# "vif_id": "1a81968a-797a-400f-8a80-567f997eb93f"
+# },
+# ],
+# }
+# macs = {'fa:16:3e:ed:9a:59': 'eth0'}
+# render_dir = self.tmp_dir()
+# network_cfg = openstack.convert_net_json(net_json, known_macs=macs)
+# ns = network_state.parse_net_config_data(network_cfg,
+# skip_broken=False)
+# renderer = self._get_renderer()
+# with self.assertRaises(ValueError):
+# renderer.render_network_state(ns, target=render_dir)
+# self.assertEqual([], os.listdir(render_dir))
+#
+# def test_multiple_ipv6_default_gateways(self):
+# """ValueError is raised when duplicate ipv6 gateways exist."""
+# net_json = {
+# "services": [{"type": "dns", "address": "172.19.0.12"}],
+# "networks": [{
+# "network_id": "public-ipv6",
+# "type": "ipv6", "netmask": "",
+# "link": "tap1a81968a-79",
+# "routes": [{
+# "gateway": "2001:DB8::1",
+# "netmask": "::",
+# "network": "::"
+# }, {
+# "gateway": "2001:DB9::1",
+# "netmask": "::",
+# "network": "::"
+# }],
+# "ip_address": "2001:DB8::10", "id": "network1"
+# }],
+# "links": [
+# {
+# "ethernet_mac_address": "fa:16:3e:ed:9a:59",
+# "mtu": None, "type": "bridge", "id":
+# "tap1a81968a-79",
+# "vif_id": "1a81968a-797a-400f-8a80-567f997eb93f"
+# },
+# ],
+# }
+# macs = {'fa:16:3e:ed:9a:59': 'eth0'}
+# render_dir = self.tmp_dir()
+# network_cfg = openstack.convert_net_json(net_json, known_macs=macs)
+# ns = network_state.parse_net_config_data(network_cfg,
+# skip_broken=False)
+# renderer = self._get_renderer()
+# with self.assertRaises(ValueError):
+# renderer.render_network_state(ns, target=render_dir)
+# self.assertEqual([], os.listdir(render_dir))
def test_openstack_rendering_samples(self):
for os_sample in OS_SAMPLES:
@@ -2372,24 +3611,26 @@ USERCTL=no
expected = """\
# Created by cloud-init on instance boot automatically, do not edit.
#
-BOOTPROTO=none
-DEFROUTE=yes
-DEVICE=interface0
-GATEWAY=10.0.2.2
-HWADDR=52:54:00:12:34:00
+BOOTPROTO=static
IPADDR=10.0.2.15
+LLADDR=52:54:00:12:34:00
NETMASK=255.255.255.0
-NM_CONTROLLED=no
-ONBOOT=yes
-TYPE=Ethernet
-USERCTL=no
+STARTMODE=auto
"""
self.assertEqual(expected, found[nspath + 'ifcfg-interface0'])
+ # The configuration has no nameserver information make sure we
+ # do not write the resolv.conf file
+ respath = '/etc/resolv.conf'
+ self.assertNotIn(respath, found.keys())
def test_config_with_explicit_loopback(self):
ns = network_state.parse_net_config_data(CONFIG_V1_EXPLICIT_LOOPBACK)
render_dir = self.tmp_path("render")
os.makedirs(render_dir)
+ # write an etc/resolv.conf and expect it to not be modified
+ resolvconf = os.path.join(render_dir, 'etc/resolv.conf')
+ resolvconf_content = "# Original Content"
+ util.write_file(resolvconf, resolvconf_content)
renderer = self._get_renderer()
renderer.render_network_state(ns, target=render_dir)
found = dir2dict(render_dir)
@@ -2399,13 +3640,11 @@ USERCTL=no
# Created by cloud-init on instance boot automatically, do not edit.
#
BOOTPROTO=dhcp
-DEVICE=eth0
-NM_CONTROLLED=no
-ONBOOT=yes
-TYPE=Ethernet
-USERCTL=no
+STARTMODE=auto
"""
self.assertEqual(expected, found[nspath + 'ifcfg-eth0'])
+ # a dhcp only config should not modify resolv.conf
+ self.assertEqual(resolvconf_content, found['/etc/resolv.conf'])
def test_bond_config(self):
expected_name = 'expected_sysconfig_opensuse'
@@ -2472,6 +3711,30 @@ USERCTL=no
self._compare_files_to_expected(entry[self.expected_name], found)
self._assert_headers(found)
+ def test_simple_render_ipv6_slaac(self):
+ entry = NETWORK_CONFIGS['ipv6_slaac']
+ found = self._render_and_read(network_config=yaml.load(entry['yaml']))
+ self._compare_files_to_expected(entry[self.expected_name], found)
+ self._assert_headers(found)
+
+ def test_dhcpv6_stateless_config(self):
+ entry = NETWORK_CONFIGS['dhcpv6_stateless']
+ found = self._render_and_read(network_config=yaml.load(entry['yaml']))
+ self._compare_files_to_expected(entry[self.expected_name], found)
+ self._assert_headers(found)
+
+ def test_render_v4_and_v6(self):
+ entry = NETWORK_CONFIGS['v4_and_v6']
+ found = self._render_and_read(network_config=yaml.load(entry['yaml']))
+ self._compare_files_to_expected(entry[self.expected_name], found)
+ self._assert_headers(found)
+
+ def test_render_v6_and_v4(self):
+ entry = NETWORK_CONFIGS['v6_and_v4']
+ found = self._render_and_read(network_config=yaml.load(entry['yaml']))
+ self._compare_files_to_expected(entry[self.expected_name], found)
+ self._assert_headers(found)
+
class TestEniNetRendering(CiTestCase):
@@ -2526,6 +3789,30 @@ iface eth0 inet dhcp
self.assertEqual(
expected, dir2dict(tmp_dir)['/etc/network/interfaces'])
+ def test_v2_route_metric_to_eni(self):
+ """Network v2 route-metric overrides are preserved in eni output"""
+ tmp_dir = self.tmp_dir()
+ renderer = eni.Renderer()
+ expected_tmpl = textwrap.dedent("""\
+ auto lo
+ iface lo inet loopback
+
+ auto eth0
+ iface eth0 inet{suffix} dhcp
+ metric 100
+ """)
+ for dhcp_ver in ('dhcp4', 'dhcp6'):
+ suffix = '6' if dhcp_ver == 'dhcp6' else ''
+ dhcp_cfg = {
+ dhcp_ver: True,
+ '{ver}-overrides'.format(ver=dhcp_ver): {'route-metric': 100}}
+ v2_input = {'version': 2, 'ethernets': {'eth0': dhcp_cfg}}
+ ns = network_state.parse_net_config_data(v2_input)
+ renderer.render_network_state(ns, target=tmp_dir)
+ self.assertEqual(
+ expected_tmpl.format(suffix=suffix),
+ dir2dict(tmp_dir)['/etc/network/interfaces'])
+
class TestNetplanNetRendering(CiTestCase):
@@ -2562,13 +3849,13 @@ class TestNetplanNetRendering(CiTestCase):
expected = """
network:
- version: 2
ethernets:
eth1000:
dhcp4: true
match:
macaddress: 07-1c-c6-75-a4-be
set-name: eth1000
+ version: 2
"""
self.assertEqual(expected.lstrip(), contents.lstrip())
self.assertEqual(1, mock_clean_default.call_count)
@@ -2645,7 +3932,9 @@ class TestNetplanPostcommands(CiTestCase):
@mock.patch.object(netplan.Renderer, '_netplan_generate')
@mock.patch.object(netplan.Renderer, '_net_setup_link')
- def test_netplan_render_calls_postcmds(self, mock_netplan_generate,
+ @mock.patch('cloudinit.util.subp')
+ def test_netplan_render_calls_postcmds(self, mock_subp,
+ mock_netplan_generate,
mock_net_setup_link):
tmp_dir = self.tmp_dir()
ns = network_state.parse_net_config_data(self.mycfg,
@@ -2657,14 +3946,18 @@ class TestNetplanPostcommands(CiTestCase):
render_target = 'netplan.yaml'
renderer = netplan.Renderer(
{'netplan_path': render_target, 'postcmds': True})
+ mock_subp.side_effect = iter([util.ProcessExecutionError])
renderer.render_network_state(ns, target=render_dir)
mock_netplan_generate.assert_called_with(run=True)
mock_net_setup_link.assert_called_with(run=True)
+ @mock.patch('cloudinit.util.SeLinuxGuard')
@mock.patch.object(netplan, "get_devicelist")
@mock.patch('cloudinit.util.subp')
- def test_netplan_postcmds(self, mock_subp, mock_devlist):
+ def test_netplan_postcmds(self, mock_subp, mock_devlist, mock_sel):
+ mock_sel.__enter__ = mock.Mock(return_value=False)
+ mock_sel.__exit__ = mock.Mock()
mock_devlist.side_effect = [['lo']]
tmp_dir = self.tmp_dir()
ns = network_state.parse_net_config_data(self.mycfg,
@@ -2676,7 +3969,13 @@ class TestNetplanPostcommands(CiTestCase):
render_target = 'netplan.yaml'
renderer = netplan.Renderer(
{'netplan_path': render_target, 'postcmds': True})
+ mock_subp.side_effect = iter([
+ util.ProcessExecutionError,
+ ('', ''),
+ ('', ''),
+ ])
expected = [
+ mock.call(['netplan', 'info'], capture=True),
mock.call(['netplan', 'generate'], capture=True),
mock.call(['udevadm', 'test-builtin', 'net_setup_link',
'/sys/class/net/lo'], capture=True),
@@ -2775,13 +4074,13 @@ class TestCmdlineConfigParsing(CiTestCase):
self.assertEqual(found, self.simple_cfg)
-class TestCmdlineReadKernelConfig(FilesystemMockingTestCase):
+class TestCmdlineKlibcNetworkConfigSource(FilesystemMockingTestCase):
macs = {
'eth0': '14:02:ec:42:48:00',
'eno1': '14:02:ec:42:48:01',
}
- def test_ip_cmdline_without_ip(self):
+ def test_without_ip(self):
content = {'/run/net-eth0.conf': DHCP_CONTENT_1,
cmdline._OPEN_ISCSI_INTERFACE_FILE: "eth0\n"}
exp1 = copy.deepcopy(DHCP_EXPECTED_1)
@@ -2791,12 +4090,15 @@ class TestCmdlineReadKernelConfig(FilesystemMockingTestCase):
populate_dir(root, content)
self.reRoot(root)
- found = cmdline.read_kernel_cmdline_config(
- cmdline='foo root=/root/bar', mac_addrs=self.macs)
+ src = cmdline.KlibcNetworkConfigSource(
+ _cmdline='foo root=/root/bar', _mac_addrs=self.macs,
+ )
+ self.assertTrue(src.is_applicable())
+ found = src.render_config()
self.assertEqual(found['version'], 1)
self.assertEqual(found['config'], [exp1])
- def test_ip_cmdline_read_kernel_cmdline_ip(self):
+ def test_with_ip(self):
content = {'/run/net-eth0.conf': DHCP_CONTENT_1}
exp1 = copy.deepcopy(DHCP_EXPECTED_1)
exp1['mac_address'] = self.macs['eth0']
@@ -2805,20 +4107,25 @@ class TestCmdlineReadKernelConfig(FilesystemMockingTestCase):
populate_dir(root, content)
self.reRoot(root)
- found = cmdline.read_kernel_cmdline_config(
- cmdline='foo ip=dhcp', mac_addrs=self.macs)
+ src = cmdline.KlibcNetworkConfigSource(
+ _cmdline='foo ip=dhcp', _mac_addrs=self.macs,
+ )
+ self.assertTrue(src.is_applicable())
+ found = src.render_config()
self.assertEqual(found['version'], 1)
self.assertEqual(found['config'], [exp1])
- def test_ip_cmdline_read_kernel_cmdline_ip6(self):
+ def test_with_ip6(self):
content = {'/run/net6-eno1.conf': DHCP6_CONTENT_1}
root = self.tmp_dir()
populate_dir(root, content)
self.reRoot(root)
- found = cmdline.read_kernel_cmdline_config(
- cmdline='foo ip6=dhcp root=/dev/sda',
- mac_addrs=self.macs)
+ src = cmdline.KlibcNetworkConfigSource(
+ _cmdline='foo ip6=dhcp root=/dev/sda', _mac_addrs=self.macs,
+ )
+ self.assertTrue(src.is_applicable())
+ found = src.render_config()
self.assertEqual(
found,
{'version': 1, 'config': [
@@ -2828,15 +4135,16 @@ class TestCmdlineReadKernelConfig(FilesystemMockingTestCase):
{'dns_nameservers': ['2001:67c:1562:8010::2:1'],
'control': 'manual', 'type': 'dhcp6', 'netmask': '64'}]}]})
- def test_ip_cmdline_read_kernel_cmdline_none(self):
+ def test_with_no_ip_or_ip6(self):
# if there is no ip= or ip6= on cmdline, return value should be None
content = {'net6-eno1.conf': DHCP6_CONTENT_1}
files = sorted(populate_dir(self.tmp_dir(), content))
- found = cmdline.read_kernel_cmdline_config(
- files=files, cmdline='foo root=/dev/sda', mac_addrs=self.macs)
- self.assertIsNone(found)
+ src = cmdline.KlibcNetworkConfigSource(
+ _files=files, _cmdline='foo root=/dev/sda', _mac_addrs=self.macs,
+ )
+ self.assertFalse(src.is_applicable())
- def test_ip_cmdline_both_ip_ip6(self):
+ def test_with_both_ip_ip6(self):
content = {
'/run/net-eth0.conf': DHCP_CONTENT_1,
'/run/net6-eth0.conf': DHCP6_CONTENT_1.replace('eno1', 'eth0')}
@@ -2851,14 +4159,92 @@ class TestCmdlineReadKernelConfig(FilesystemMockingTestCase):
populate_dir(root, content)
self.reRoot(root)
- found = cmdline.read_kernel_cmdline_config(
- cmdline='foo ip=dhcp ip6=dhcp', mac_addrs=self.macs)
+ src = cmdline.KlibcNetworkConfigSource(
+ _cmdline='foo ip=dhcp ip6=dhcp', _mac_addrs=self.macs,
+ )
+ self.assertTrue(src.is_applicable())
+ found = src.render_config()
self.assertEqual(found['version'], 1)
self.assertEqual(found['config'], expected)
+class TestReadInitramfsConfig(CiTestCase):
+
+ def _config_source_cls_mock(self, is_applicable, render_config=None):
+ return lambda: mock.Mock(
+ is_applicable=lambda: is_applicable,
+ render_config=lambda: render_config,
+ )
+
+ def test_no_sources(self):
+ with mock.patch('cloudinit.net.cmdline._INITRAMFS_CONFIG_SOURCES', []):
+ self.assertIsNone(cmdline.read_initramfs_config())
+
+ def test_no_applicable_sources(self):
+ sources = [
+ self._config_source_cls_mock(is_applicable=False),
+ self._config_source_cls_mock(is_applicable=False),
+ self._config_source_cls_mock(is_applicable=False),
+ ]
+ with mock.patch('cloudinit.net.cmdline._INITRAMFS_CONFIG_SOURCES',
+ sources):
+ self.assertIsNone(cmdline.read_initramfs_config())
+
+ def test_one_applicable_source(self):
+ expected_config = object()
+ sources = [
+ self._config_source_cls_mock(
+ is_applicable=True, render_config=expected_config,
+ ),
+ ]
+ with mock.patch('cloudinit.net.cmdline._INITRAMFS_CONFIG_SOURCES',
+ sources):
+ self.assertEqual(expected_config, cmdline.read_initramfs_config())
+
+ def test_one_applicable_source_after_inapplicable_sources(self):
+ expected_config = object()
+ sources = [
+ self._config_source_cls_mock(is_applicable=False),
+ self._config_source_cls_mock(is_applicable=False),
+ self._config_source_cls_mock(
+ is_applicable=True, render_config=expected_config,
+ ),
+ ]
+ with mock.patch('cloudinit.net.cmdline._INITRAMFS_CONFIG_SOURCES',
+ sources):
+ self.assertEqual(expected_config, cmdline.read_initramfs_config())
+
+ def test_first_applicable_source_is_used(self):
+ first_config, second_config = object(), object()
+ sources = [
+ self._config_source_cls_mock(
+ is_applicable=True, render_config=first_config,
+ ),
+ self._config_source_cls_mock(
+ is_applicable=True, render_config=second_config,
+ ),
+ ]
+ with mock.patch('cloudinit.net.cmdline._INITRAMFS_CONFIG_SOURCES',
+ sources):
+ self.assertEqual(first_config, cmdline.read_initramfs_config())
+
+
class TestNetplanRoundTrip(CiTestCase):
+
+ NETPLAN_INFO_OUT = textwrap.dedent("""
+ netplan.io:
+ features:
+ - dhcp-use-domains
+ - ipv6-mtu
+ website: https://netplan.io/
+ """)
+
+ def setUp(self):
+ super(TestNetplanRoundTrip, self).setUp()
+ self.add_patch('cloudinit.net.netplan.util.subp', 'm_subp')
+ self.m_subp.return_value = (self.NETPLAN_INFO_OUT, '')
+
def _render_and_read(self, network_config=None, state=None,
netplan_path=None, target=None):
if target is None:
@@ -2929,6 +4315,46 @@ class TestNetplanRoundTrip(CiTestCase):
entry['expected_netplan'].splitlines(),
files['/etc/netplan/50-cloud-init.yaml'].splitlines())
+ def testsimple_render_dhcpv6_accept_ra(self):
+ entry = NETWORK_CONFIGS['dhcpv6_accept_ra']
+ files = self._render_and_read(network_config=yaml.load(
+ entry['yaml_v1']))
+ self.assertEqual(
+ entry['expected_netplan'].splitlines(),
+ files['/etc/netplan/50-cloud-init.yaml'].splitlines())
+
+ def testsimple_render_dhcpv6_reject_ra(self):
+ entry = NETWORK_CONFIGS['dhcpv6_reject_ra']
+ files = self._render_and_read(network_config=yaml.load(
+ entry['yaml_v1']))
+ self.assertEqual(
+ entry['expected_netplan'].splitlines(),
+ files['/etc/netplan/50-cloud-init.yaml'].splitlines())
+
+ def testsimple_render_ipv6_slaac(self):
+ entry = NETWORK_CONFIGS['ipv6_slaac']
+ files = self._render_and_read(network_config=yaml.load(
+ entry['yaml']))
+ self.assertEqual(
+ entry['expected_netplan'].splitlines(),
+ files['/etc/netplan/50-cloud-init.yaml'].splitlines())
+
+ def testsimple_render_dhcpv6_stateless(self):
+ entry = NETWORK_CONFIGS['dhcpv6_stateless']
+ files = self._render_and_read(network_config=yaml.load(
+ entry['yaml']))
+ self.assertEqual(
+ entry['expected_netplan'].splitlines(),
+ files['/etc/netplan/50-cloud-init.yaml'].splitlines())
+
+ def testsimple_render_dhcpv6_stateful(self):
+ entry = NETWORK_CONFIGS['dhcpv6_stateful']
+ files = self._render_and_read(network_config=yaml.load(
+ entry['yaml']))
+ self.assertEqual(
+ entry['expected_netplan'].splitlines(),
+ files['/etc/netplan/50-cloud-init.yaml'].splitlines())
+
def testsimple_render_all(self):
entry = NETWORK_CONFIGS['all']
files = self._render_and_read(network_config=yaml.load(entry['yaml']))
@@ -2946,6 +4372,53 @@ class TestNetplanRoundTrip(CiTestCase):
entry['expected_netplan'].splitlines(),
files['/etc/netplan/50-cloud-init.yaml'].splitlines())
+ def test_render_output_has_yaml_no_aliases(self):
+ entry = {
+ 'yaml': V1_NAMESERVER_ALIAS,
+ 'expected_netplan': NETPLAN_NO_ALIAS,
+ }
+ network_config = yaml.load(entry['yaml'])
+ ns = network_state.parse_net_config_data(network_config)
+ files = self._render_and_read(state=ns)
+ # check for alias
+ content = files['/etc/netplan/50-cloud-init.yaml']
+
+ # test load the yaml to ensure we don't render something not loadable
+ # this allows single aliases, but not duplicate ones
+ parsed = yaml.load(files['/etc/netplan/50-cloud-init.yaml'])
+ self.assertNotEqual(None, parsed)
+
+ # now look for any alias, avoid rendering them entirely
+ # generate the first anchor string using the template
+ # as of this writing, looks like "&id001"
+ anchor = r'&' + Serializer.ANCHOR_TEMPLATE % 1
+ found_alias = re.search(anchor, content, re.MULTILINE)
+ if found_alias:
+ msg = "Error at: %s\nContent:\n%s" % (found_alias, content)
+ raise ValueError('Found yaml alias in rendered netplan: ' + msg)
+
+ print(entry['expected_netplan'])
+ print('-- expected ^ | v rendered --')
+ print(files['/etc/netplan/50-cloud-init.yaml'])
+ self.assertEqual(
+ entry['expected_netplan'].splitlines(),
+ files['/etc/netplan/50-cloud-init.yaml'].splitlines())
+
+ def test_render_output_supports_both_grat_arp_spelling(self):
+ entry = {
+ 'yaml': NETPLAN_BOND_GRAT_ARP,
+ 'expected_netplan': NETPLAN_BOND_GRAT_ARP.replace('gratuitous',
+ 'gratuitious'),
+ }
+ network_config = yaml.load(entry['yaml']).get('network')
+ files = self._render_and_read(network_config=network_config)
+ print(entry['expected_netplan'])
+ print('-- expected ^ | v rendered --')
+ print(files['/etc/netplan/50-cloud-init.yaml'])
+ self.assertEqual(
+ entry['expected_netplan'].splitlines(),
+ files['/etc/netplan/50-cloud-init.yaml'].splitlines())
+
class TestEniRoundTrip(CiTestCase):
@@ -3012,6 +4485,43 @@ class TestEniRoundTrip(CiTestCase):
entry['expected_eni'].splitlines(),
files['/etc/network/interfaces'].splitlines())
+ def testsimple_render_dhcpv6_stateless(self):
+ entry = NETWORK_CONFIGS['dhcpv6_stateless']
+ files = self._render_and_read(network_config=yaml.load(entry['yaml']))
+ self.assertEqual(
+ entry['expected_eni'].splitlines(),
+ files['/etc/network/interfaces'].splitlines())
+
+ def testsimple_render_ipv6_slaac(self):
+ entry = NETWORK_CONFIGS['ipv6_slaac']
+ files = self._render_and_read(network_config=yaml.load(entry['yaml']))
+ self.assertEqual(
+ entry['expected_eni'].splitlines(),
+ files['/etc/network/interfaces'].splitlines())
+
+ def testsimple_render_dhcpv6_stateful(self):
+ entry = NETWORK_CONFIGS['dhcpv6_stateless']
+ files = self._render_and_read(network_config=yaml.load(entry['yaml']))
+ self.assertEqual(
+ entry['expected_eni'].splitlines(),
+ files['/etc/network/interfaces'].splitlines())
+
+ def testsimple_render_dhcpv6_accept_ra(self):
+ entry = NETWORK_CONFIGS['dhcpv6_accept_ra']
+ files = self._render_and_read(network_config=yaml.load(
+ entry['yaml_v1']))
+ self.assertEqual(
+ entry['expected_eni'].splitlines(),
+ files['/etc/network/interfaces'].splitlines())
+
+ def testsimple_render_dhcpv6_reject_ra(self):
+ entry = NETWORK_CONFIGS['dhcpv6_reject_ra']
+ files = self._render_and_read(network_config=yaml.load(
+ entry['yaml_v1']))
+ self.assertEqual(
+ entry['expected_eni'].splitlines(),
+ files['/etc/network/interfaces'].splitlines())
+
def testsimple_render_manual(self):
"""Test rendering of 'manual' for 'type' and 'control'.
@@ -3054,17 +4564,17 @@ class TestEniRoundTrip(CiTestCase):
'iface eth0 inet static',
' address 172.23.31.42/26',
' gateway 172.23.31.2',
- ('post-up route add -net 10.0.0.0 netmask 255.240.0.0 gw '
+ ('post-up route add -net 10.0.0.0/12 gw '
'172.23.31.1 metric 0 || true'),
- ('pre-down route del -net 10.0.0.0 netmask 255.240.0.0 gw '
+ ('pre-down route del -net 10.0.0.0/12 gw '
'172.23.31.1 metric 0 || true'),
- ('post-up route add -net 192.168.2.0 netmask 255.255.0.0 gw '
+ ('post-up route add -net 192.168.2.0/16 gw '
'172.23.31.1 metric 0 || true'),
- ('pre-down route del -net 192.168.2.0 netmask 255.255.0.0 gw '
+ ('pre-down route del -net 192.168.2.0/16 gw '
'172.23.31.1 metric 0 || true'),
- ('post-up route add -net 10.0.200.0 netmask 255.255.0.0 gw '
+ ('post-up route add -net 10.0.200.0/16 gw '
'172.23.31.1 metric 1 || true'),
- ('pre-down route del -net 10.0.200.0 netmask 255.255.0.0 gw '
+ ('pre-down route del -net 10.0.200.0/16 gw '
'172.23.31.1 metric 1 || true'),
]
found = files['/etc/network/interfaces'].splitlines()
@@ -3072,6 +4582,77 @@ class TestEniRoundTrip(CiTestCase):
self.assertEqual(
expected, [line for line in found if line])
+ def test_ipv6_static_routes(self):
+ # as reported in bug 1818669
+ conf = [
+ {'name': 'eno3', 'type': 'physical',
+ 'subnets': [{
+ 'address': 'fd00::12/64',
+ 'dns_nameservers': ['fd00:2::15'],
+ 'gateway': 'fd00::1',
+ 'ipv6': True,
+ 'type': 'static',
+ 'routes': [{'netmask': '32',
+ 'network': 'fd00:12::',
+ 'gateway': 'fd00::2'},
+ {'network': 'fd00:14::',
+ 'gateway': 'fd00::3'},
+ {'destination': 'fe00:14::/48',
+ 'gateway': 'fe00::4',
+ 'metric': 500},
+ {'gateway': '192.168.23.1',
+ 'metric': 999,
+ 'netmask': 24,
+ 'network': '192.168.23.0'},
+ {'destination': '10.23.23.0/24',
+ 'gateway': '10.23.23.2',
+ 'metric': 300}]}]},
+ ]
+
+ files = self._render_and_read(
+ network_config={'config': conf, 'version': 1})
+ expected = [
+ 'auto lo',
+ 'iface lo inet loopback',
+ 'auto eno3',
+ 'iface eno3 inet6 static',
+ ' address fd00::12/64',
+ ' dns-nameservers fd00:2::15',
+ ' gateway fd00::1',
+ (' post-up route add -A inet6 fd00:12::/32 gw '
+ 'fd00::2 || true'),
+ (' pre-down route del -A inet6 fd00:12::/32 gw '
+ 'fd00::2 || true'),
+ (' post-up route add -A inet6 fd00:14::/64 gw '
+ 'fd00::3 || true'),
+ (' pre-down route del -A inet6 fd00:14::/64 gw '
+ 'fd00::3 || true'),
+ (' post-up route add -A inet6 fe00:14::/48 gw '
+ 'fe00::4 metric 500 || true'),
+ (' pre-down route del -A inet6 fe00:14::/48 gw '
+ 'fe00::4 metric 500 || true'),
+ (' post-up route add -net 192.168.23.0/24 gw '
+ '192.168.23.1 metric 999 || true'),
+ (' pre-down route del -net 192.168.23.0/24 gw '
+ '192.168.23.1 metric 999 || true'),
+ (' post-up route add -net 10.23.23.0/24 gw '
+ '10.23.23.2 metric 300 || true'),
+ (' pre-down route del -net 10.23.23.0/24 gw '
+ '10.23.23.2 metric 300 || true'),
+
+ ]
+ found = files['/etc/network/interfaces'].splitlines()
+
+ self.assertEqual(
+ expected, [line for line in found if line])
+
+ def testsimple_render_bond(self):
+ entry = NETWORK_CONFIGS['bond']
+ files = self._render_and_read(network_config=yaml.load(entry['yaml']))
+ self.assertEqual(
+ entry['expected_eni'].splitlines(),
+ files['/etc/network/interfaces'].splitlines())
+
class TestNetRenderers(CiTestCase):
@mock.patch("cloudinit.net.renderers.sysconfig.available")
@@ -3116,6 +4697,66 @@ class TestNetRenderers(CiTestCase):
self.assertRaises(net.RendererNotFoundError, renderers.select,
priority=['sysconfig', 'eni'])
+ @mock.patch("cloudinit.net.renderers.netplan.available")
+ @mock.patch("cloudinit.net.renderers.sysconfig.available")
+ @mock.patch("cloudinit.net.renderers.sysconfig.available_sysconfig")
+ @mock.patch("cloudinit.net.renderers.sysconfig.available_nm")
+ @mock.patch("cloudinit.net.renderers.eni.available")
+ @mock.patch("cloudinit.net.renderers.sysconfig.util.get_linux_distro")
+ def test_sysconfig_selected_on_sysconfig_enabled_distros(self, m_distro,
+ m_eni, m_sys_nm,
+ m_sys_scfg,
+ m_sys_avail,
+ m_netplan):
+ """sysconfig only selected on specific distros (rhel/sles)."""
+
+ # Ubuntu with Network-Manager installed
+ m_eni.return_value = False # no ifupdown (ifquery)
+ m_sys_scfg.return_value = False # no sysconfig/ifup/ifdown
+ m_sys_nm.return_value = True # network-manager is installed
+ m_netplan.return_value = True # netplan is installed
+ m_sys_avail.return_value = False # no sysconfig on Ubuntu
+ m_distro.return_value = ('ubuntu', None, None)
+ self.assertEqual('netplan', renderers.select(priority=None)[0])
+
+ # Centos with Network-Manager installed
+ m_eni.return_value = False # no ifupdown (ifquery)
+ m_sys_scfg.return_value = False # no sysconfig/ifup/ifdown
+ m_sys_nm.return_value = True # network-manager is installed
+ m_netplan.return_value = False # netplan is not installed
+ m_sys_avail.return_value = True # sysconfig is available on centos
+ m_distro.return_value = ('centos', None, None)
+ self.assertEqual('sysconfig', renderers.select(priority=None)[0])
+
+ # OpenSuse with Network-Manager installed
+ m_eni.return_value = False # no ifupdown (ifquery)
+ m_sys_scfg.return_value = False # no sysconfig/ifup/ifdown
+ m_sys_nm.return_value = True # network-manager is installed
+ m_netplan.return_value = False # netplan is not installed
+ m_sys_avail.return_value = True # sysconfig is available on opensuse
+ m_distro.return_value = ('opensuse', None, None)
+ self.assertEqual('sysconfig', renderers.select(priority=None)[0])
+
+ @mock.patch("cloudinit.net.sysconfig.available_sysconfig")
+ @mock.patch("cloudinit.util.get_linux_distro")
+ def test_sysconfig_available_uses_variant_mapping(self, m_distro, m_avail):
+ m_avail.return_value = True
+ distro_values = [
+ ('opensuse', '', ''),
+ ('opensuse-leap', '', ''),
+ ('opensuse-tumbleweed', '', ''),
+ ('sles', '', ''),
+ ('centos', '', ''),
+ ('fedora', '', ''),
+ ('redhat', '', ''),
+ ]
+ for (distro_name, distro_version, flavor) in distro_values:
+ m_distro.return_value = (distro_name, distro_version, flavor)
+ if hasattr(util.system_info, "cache_clear"):
+ util.system_info.cache_clear()
+ result = sysconfig.available()
+ self.assertTrue(result)
+
class TestGetInterfaces(CiTestCase):
_data = {'bonds': ['bond1'],
diff --git a/tests/unittests/test_net_freebsd.py b/tests/unittests/test_net_freebsd.py
new file mode 100644
index 00000000..48296c30
--- /dev/null
+++ b/tests/unittests/test_net_freebsd.py
@@ -0,0 +1,19 @@
+from cloudinit import net
+
+from cloudinit.tests.helpers import (CiTestCase, mock, readResource)
+
+SAMPLE_FREEBSD_IFCONFIG_OUT = readResource("netinfo/freebsd-ifconfig-output")
+
+
+class TestInterfacesByMac(CiTestCase):
+
+ @mock.patch('cloudinit.util.subp')
+ @mock.patch('cloudinit.util.is_FreeBSD')
+ def test_get_interfaces_by_mac(self, mock_is_FreeBSD, mock_subp):
+ mock_is_FreeBSD.return_value = True
+ mock_subp.return_value = (SAMPLE_FREEBSD_IFCONFIG_OUT, 0)
+ a = net.get_interfaces_by_mac()
+ assert a == {'52:54:00:50:b7:0d': 'vtnet0',
+ '80:00:73:63:5c:48': 're0.33',
+ '02:14:39:0e:25:00': 'bridge0',
+ '02:ff:60:8c:f3:72': 'vnet0:11'}
diff --git a/tests/unittests/test_reporting.py b/tests/unittests/test_reporting.py
index e15ba6cf..6814030e 100644
--- a/tests/unittests/test_reporting.py
+++ b/tests/unittests/test_reporting.py
@@ -2,12 +2,12 @@
#
# This file is part of cloud-init. See LICENSE file for license information.
+from unittest import mock
+
from cloudinit import reporting
from cloudinit.reporting import events
from cloudinit.reporting import handlers
-import mock
-
from cloudinit.tests.helpers import TestCase
diff --git a/tests/unittests/test_reporting_hyperv.py b/tests/unittests/test_reporting_hyperv.py
index 2e64c6c7..b3e083c6 100644
--- a/tests/unittests/test_reporting_hyperv.py
+++ b/tests/unittests/test_reporting_hyperv.py
@@ -1,19 +1,24 @@
# This file is part of cloud-init. See LICENSE file for license information.
from cloudinit.reporting import events
-from cloudinit.reporting import handlers
+from cloudinit.reporting.handlers import HyperVKvpReportingHandler
import json
import os
+import struct
+import time
+import re
+from unittest import mock
from cloudinit import util
from cloudinit.tests.helpers import CiTestCase
+from cloudinit.sources.helpers import azure
class TestKvpEncoding(CiTestCase):
def test_encode_decode(self):
kvp = {'key': 'key1', 'value': 'value1'}
- kvp_reporting = handlers.HyperVKvpReportingHandler()
+ kvp_reporting = HyperVKvpReportingHandler()
data = kvp_reporting._encode_kvp_item(kvp['key'], kvp['value'])
self.assertEqual(len(data), kvp_reporting.HV_KVP_RECORD_SIZE)
decoded_kvp = kvp_reporting._decode_kvp_item(data)
@@ -26,57 +31,9 @@ class TextKvpReporter(CiTestCase):
self.tmp_file_path = self.tmp_path('kvp_pool_file')
util.ensure_file(self.tmp_file_path)
- def test_event_type_can_be_filtered(self):
- reporter = handlers.HyperVKvpReportingHandler(
- kvp_file_path=self.tmp_file_path,
- event_types=['foo', 'bar'])
-
- reporter.publish_event(
- events.ReportingEvent('foo', 'name', 'description'))
- reporter.publish_event(
- events.ReportingEvent('some_other', 'name', 'description3'))
- reporter.q.join()
-
- kvps = list(reporter._iterate_kvps(0))
- self.assertEqual(1, len(kvps))
-
- reporter.publish_event(
- events.ReportingEvent('bar', 'name', 'description2'))
- reporter.q.join()
- kvps = list(reporter._iterate_kvps(0))
- self.assertEqual(2, len(kvps))
-
- self.assertIn('foo', kvps[0]['key'])
- self.assertIn('bar', kvps[1]['key'])
- self.assertNotIn('some_other', kvps[0]['key'])
- self.assertNotIn('some_other', kvps[1]['key'])
-
- def test_events_are_over_written(self):
- reporter = handlers.HyperVKvpReportingHandler(
- kvp_file_path=self.tmp_file_path)
-
- self.assertEqual(0, len(list(reporter._iterate_kvps(0))))
-
- reporter.publish_event(
- events.ReportingEvent('foo', 'name1', 'description'))
- reporter.publish_event(
- events.ReportingEvent('foo', 'name2', 'description'))
- reporter.q.join()
- self.assertEqual(2, len(list(reporter._iterate_kvps(0))))
-
- reporter2 = handlers.HyperVKvpReportingHandler(
- kvp_file_path=self.tmp_file_path)
- reporter2.incarnation_no = reporter.incarnation_no + 1
- reporter2.publish_event(
- events.ReportingEvent('foo', 'name3', 'description'))
- reporter2.q.join()
-
- self.assertEqual(2, len(list(reporter2._iterate_kvps(0))))
-
def test_events_with_higher_incarnation_not_over_written(self):
- reporter = handlers.HyperVKvpReportingHandler(
+ reporter = HyperVKvpReportingHandler(
kvp_file_path=self.tmp_file_path)
-
self.assertEqual(0, len(list(reporter._iterate_kvps(0))))
reporter.publish_event(
@@ -86,7 +43,7 @@ class TextKvpReporter(CiTestCase):
reporter.q.join()
self.assertEqual(2, len(list(reporter._iterate_kvps(0))))
- reporter3 = handlers.HyperVKvpReportingHandler(
+ reporter3 = HyperVKvpReportingHandler(
kvp_file_path=self.tmp_file_path)
reporter3.incarnation_no = reporter.incarnation_no - 1
reporter3.publish_event(
@@ -95,7 +52,7 @@ class TextKvpReporter(CiTestCase):
self.assertEqual(3, len(list(reporter3._iterate_kvps(0))))
def test_finish_event_result_is_logged(self):
- reporter = handlers.HyperVKvpReportingHandler(
+ reporter = HyperVKvpReportingHandler(
kvp_file_path=self.tmp_file_path)
reporter.publish_event(
events.FinishReportingEvent('name2', 'description1',
@@ -105,7 +62,7 @@ class TextKvpReporter(CiTestCase):
def test_file_operation_issue(self):
os.remove(self.tmp_file_path)
- reporter = handlers.HyperVKvpReportingHandler(
+ reporter = HyperVKvpReportingHandler(
kvp_file_path=self.tmp_file_path)
reporter.publish_event(
events.FinishReportingEvent('name2', 'description1',
@@ -113,7 +70,7 @@ class TextKvpReporter(CiTestCase):
reporter.q.join()
def test_event_very_long(self):
- reporter = handlers.HyperVKvpReportingHandler(
+ reporter = HyperVKvpReportingHandler(
kvp_file_path=self.tmp_file_path)
description = 'ab' * reporter.HV_KVP_EXCHANGE_MAX_VALUE_SIZE
long_event = events.FinishReportingEvent(
@@ -132,3 +89,123 @@ class TextKvpReporter(CiTestCase):
self.assertEqual(msg_slice['msg_i'], i)
full_description += msg_slice['msg']
self.assertEqual(description, full_description)
+
+ def test_not_truncate_kvp_file_modified_after_boot(self):
+ with open(self.tmp_file_path, "wb+") as f:
+ kvp = {'key': 'key1', 'value': 'value1'}
+ data = (struct.pack("%ds%ds" % (
+ HyperVKvpReportingHandler.HV_KVP_EXCHANGE_MAX_KEY_SIZE,
+ HyperVKvpReportingHandler.HV_KVP_EXCHANGE_MAX_VALUE_SIZE),
+ kvp['key'].encode('utf-8'), kvp['value'].encode('utf-8')))
+ f.write(data)
+ cur_time = time.time()
+ os.utime(self.tmp_file_path, (cur_time, cur_time))
+
+ # reset this because the unit test framework
+ # has already polluted the class variable
+ HyperVKvpReportingHandler._already_truncated_pool_file = False
+
+ reporter = HyperVKvpReportingHandler(kvp_file_path=self.tmp_file_path)
+ kvps = list(reporter._iterate_kvps(0))
+ self.assertEqual(1, len(kvps))
+
+ def test_truncate_stale_kvp_file(self):
+ with open(self.tmp_file_path, "wb+") as f:
+ kvp = {'key': 'key1', 'value': 'value1'}
+ data = (struct.pack("%ds%ds" % (
+ HyperVKvpReportingHandler.HV_KVP_EXCHANGE_MAX_KEY_SIZE,
+ HyperVKvpReportingHandler.HV_KVP_EXCHANGE_MAX_VALUE_SIZE),
+ kvp['key'].encode('utf-8'), kvp['value'].encode('utf-8')))
+ f.write(data)
+
+ # set the time ways back to make it look like
+ # we had an old kvp file
+ os.utime(self.tmp_file_path, (1000000, 1000000))
+
+ # reset this because the unit test framework
+ # has already polluted the class variable
+ HyperVKvpReportingHandler._already_truncated_pool_file = False
+
+ reporter = HyperVKvpReportingHandler(kvp_file_path=self.tmp_file_path)
+ kvps = list(reporter._iterate_kvps(0))
+ self.assertEqual(0, len(kvps))
+
+ @mock.patch('cloudinit.distros.uses_systemd')
+ @mock.patch('cloudinit.util.subp')
+ def test_get_boot_telemetry(self, m_subp, m_sysd):
+ reporter = HyperVKvpReportingHandler(kvp_file_path=self.tmp_file_path)
+ datetime_pattern = r"\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]"
+ r"\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)"
+
+ # get_boot_telemetry makes two subp calls to systemctl. We provide
+ # a list of values that the subp calls should return
+ m_subp.side_effect = [
+ ('UserspaceTimestampMonotonic=1844838', ''),
+ ('InactiveExitTimestampMonotonic=3068203', '')]
+ m_sysd.return_value = True
+
+ reporter.publish_event(azure.get_boot_telemetry())
+ reporter.q.join()
+ kvps = list(reporter._iterate_kvps(0))
+ self.assertEqual(1, len(kvps))
+
+ evt_msg = kvps[0]['value']
+ if not re.search("kernel_start=" + datetime_pattern, evt_msg):
+ raise AssertionError("missing kernel_start timestamp")
+ if not re.search("user_start=" + datetime_pattern, evt_msg):
+ raise AssertionError("missing user_start timestamp")
+ if not re.search("cloudinit_activation=" + datetime_pattern,
+ evt_msg):
+ raise AssertionError(
+ "missing cloudinit_activation timestamp")
+
+ def test_get_system_info(self):
+ reporter = HyperVKvpReportingHandler(kvp_file_path=self.tmp_file_path)
+ pattern = r"[^=\s]+"
+
+ reporter.publish_event(azure.get_system_info())
+ reporter.q.join()
+ kvps = list(reporter._iterate_kvps(0))
+ self.assertEqual(1, len(kvps))
+ evt_msg = kvps[0]['value']
+
+ # the most important information is cloudinit version,
+ # kernel_version, and the distro variant. It is ok if
+ # if the rest is not available
+ if not re.search("cloudinit_version=" + pattern, evt_msg):
+ raise AssertionError("missing cloudinit_version string")
+ if not re.search("kernel_version=" + pattern, evt_msg):
+ raise AssertionError("missing kernel_version string")
+ if not re.search("variant=" + pattern, evt_msg):
+ raise AssertionError("missing distro variant string")
+
+ def test_report_diagnostic_event(self):
+ reporter = HyperVKvpReportingHandler(kvp_file_path=self.tmp_file_path)
+
+ reporter.publish_event(
+ azure.report_diagnostic_event("test_diagnostic"))
+ reporter.q.join()
+ kvps = list(reporter._iterate_kvps(0))
+ self.assertEqual(1, len(kvps))
+ evt_msg = kvps[0]['value']
+
+ if "test_diagnostic" not in evt_msg:
+ raise AssertionError("missing expected diagnostic message")
+
+ def test_unique_kvp_key(self):
+ reporter = HyperVKvpReportingHandler(kvp_file_path=self.tmp_file_path)
+ evt1 = events.ReportingEvent(
+ "event_type", 'event_message',
+ "event_description")
+ reporter.publish_event(evt1)
+
+ evt2 = events.ReportingEvent(
+ "event_type", 'event_message',
+ "event_description", timestamp=evt1.timestamp + 1)
+ reporter.publish_event(evt2)
+
+ reporter.q.join()
+ kvps = list(reporter._iterate_kvps(0))
+ self.assertEqual(2, len(kvps))
+ self.assertNotEqual(kvps[0]["key"], kvps[1]["key"],
+ "duplicate keys for KVP entries")
diff --git a/tests/unittests/test_runs/test_merge_run.py b/tests/unittests/test_runs/test_merge_run.py
index d1ac4942..ff27a280 100644
--- a/tests/unittests/test_runs/test_merge_run.py
+++ b/tests/unittests/test_runs/test_merge_run.py
@@ -7,6 +7,7 @@ import tempfile
from cloudinit.tests import helpers
from cloudinit.settings import PER_INSTANCE
+from cloudinit import safeyaml
from cloudinit import stages
from cloudinit import util
@@ -26,7 +27,7 @@ class TestMergeRun(helpers.FilesystemMockingTestCase):
'system_info': {'paths': {'run_dir': new_root}}
}
ud = helpers.readResource('user_data.1.txt')
- cloud_cfg = util.yaml_dumps(cfg)
+ cloud_cfg = safeyaml.dumps(cfg)
util.ensure_dir(os.path.join(new_root, 'etc', 'cloud'))
util.write_file(os.path.join(new_root, 'etc',
'cloud', 'cloud.cfg'), cloud_cfg)
diff --git a/tests/unittests/test_runs/test_simple_run.py b/tests/unittests/test_runs/test_simple_run.py
index d67c422c..cb3aae60 100644
--- a/tests/unittests/test_runs/test_simple_run.py
+++ b/tests/unittests/test_runs/test_simple_run.py
@@ -5,6 +5,7 @@ import os
from cloudinit.settings import PER_INSTANCE
+from cloudinit import safeyaml
from cloudinit import stages
from cloudinit.tests import helpers
from cloudinit import util
@@ -34,7 +35,7 @@ class TestSimpleRun(helpers.FilesystemMockingTestCase):
],
'cloud_init_modules': ['write-files', 'spacewalk', 'runcmd'],
}
- cloud_cfg = util.yaml_dumps(self.cfg)
+ cloud_cfg = safeyaml.dumps(self.cfg)
util.ensure_dir(os.path.join(self.new_root, 'etc', 'cloud'))
util.write_file(os.path.join(self.new_root, 'etc',
'cloud', 'cloud.cfg'), cloud_cfg)
@@ -130,7 +131,7 @@ class TestSimpleRun(helpers.FilesystemMockingTestCase):
# re-write cloud.cfg with unverified_modules override
cfg = copy.deepcopy(self.cfg)
cfg['unverified_modules'] = ['spacewalk'] # Would have skipped
- cloud_cfg = util.yaml_dumps(cfg)
+ cloud_cfg = safeyaml.dumps(cfg)
util.ensure_dir(os.path.join(self.new_root, 'etc', 'cloud'))
util.write_file(os.path.join(self.new_root, 'etc',
'cloud', 'cloud.cfg'), cloud_cfg)
@@ -159,7 +160,7 @@ class TestSimpleRun(helpers.FilesystemMockingTestCase):
cfg = copy.deepcopy(self.cfg)
# Represent empty configuration in /etc/cloud/cloud.cfg
cfg['cloud_init_modules'] = None
- cloud_cfg = util.yaml_dumps(cfg)
+ cloud_cfg = safeyaml.dumps(cfg)
util.ensure_dir(os.path.join(self.new_root, 'etc', 'cloud'))
util.write_file(os.path.join(self.new_root, 'etc',
'cloud', 'cloud.cfg'), cloud_cfg)
diff --git a/tests/unittests/test_sshutil.py b/tests/unittests/test_sshutil.py
index 73ae897f..0be41924 100644
--- a/tests/unittests/test_sshutil.py
+++ b/tests/unittests/test_sshutil.py
@@ -1,11 +1,19 @@
# This file is part of cloud-init. See LICENSE file for license information.
-from mock import patch
+from collections import namedtuple
+from unittest.mock import patch
from cloudinit import ssh_util
from cloudinit.tests import helpers as test_helpers
from cloudinit import util
+# https://stackoverflow.com/questions/11351032/
+FakePwEnt = namedtuple(
+ 'FakePwEnt',
+ ['pw_dir', 'pw_gecos', 'pw_name', 'pw_passwd', 'pw_shell', 'pwd_uid'])
+FakePwEnt.__new__.__defaults__ = tuple(
+ "UNSET_%s" % n for n in FakePwEnt._fields)
+
VALID_CONTENT = {
'dsa': (
@@ -326,4 +334,79 @@ class TestUpdateSshConfig(test_helpers.CiTestCase):
m_write_file.assert_not_called()
+class TestBasicAuthorizedKeyParse(test_helpers.CiTestCase):
+ def test_user(self):
+ self.assertEqual(
+ ["/opt/bobby/keys"],
+ ssh_util.render_authorizedkeysfile_paths(
+ "/opt/%u/keys", "/home/bobby", "bobby"))
+
+ def test_multiple(self):
+ self.assertEqual(
+ ["/keys/path1", "/keys/path2"],
+ ssh_util.render_authorizedkeysfile_paths(
+ "/keys/path1 /keys/path2", "/home/bobby", "bobby"))
+
+ def test_relative(self):
+ self.assertEqual(
+ ["/home/bobby/.secret/keys"],
+ ssh_util.render_authorizedkeysfile_paths(
+ ".secret/keys", "/home/bobby", "bobby"))
+
+ def test_home(self):
+ self.assertEqual(
+ ["/homedirs/bobby/.keys"],
+ ssh_util.render_authorizedkeysfile_paths(
+ "%h/.keys", "/homedirs/bobby", "bobby"))
+
+
+class TestMultipleSshAuthorizedKeysFile(test_helpers.CiTestCase):
+
+ @patch("cloudinit.ssh_util.pwd.getpwnam")
+ def test_multiple_authorizedkeys_file_order1(self, m_getpwnam):
+ fpw = FakePwEnt(pw_name='bobby', pw_dir='/home2/bobby')
+ m_getpwnam.return_value = fpw
+ authorized_keys = self.tmp_path('authorized_keys')
+ util.write_file(authorized_keys, VALID_CONTENT['rsa'])
+
+ user_keys = self.tmp_path('user_keys')
+ util.write_file(user_keys, VALID_CONTENT['dsa'])
+
+ sshd_config = self.tmp_path('sshd_config')
+ util.write_file(
+ sshd_config,
+ "AuthorizedKeysFile %s %s" % (authorized_keys, user_keys))
+
+ (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
+ fpw.pw_name, sshd_config)
+ content = ssh_util.update_authorized_keys(
+ auth_key_entries, [])
+
+ self.assertEqual("%s/.ssh/authorized_keys" % fpw.pw_dir, auth_key_fn)
+ self.assertTrue(VALID_CONTENT['rsa'] in content)
+ self.assertTrue(VALID_CONTENT['dsa'] in content)
+
+ @patch("cloudinit.ssh_util.pwd.getpwnam")
+ def test_multiple_authorizedkeys_file_order2(self, m_getpwnam):
+ fpw = FakePwEnt(pw_name='suzie', pw_dir='/home/suzie')
+ m_getpwnam.return_value = fpw
+ authorized_keys = self.tmp_path('authorized_keys')
+ util.write_file(authorized_keys, VALID_CONTENT['rsa'])
+
+ user_keys = self.tmp_path('user_keys')
+ util.write_file(user_keys, VALID_CONTENT['dsa'])
+
+ sshd_config = self.tmp_path('sshd_config')
+ util.write_file(
+ sshd_config,
+ "AuthorizedKeysFile %s %s" % (authorized_keys, user_keys))
+
+ (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(
+ fpw.pw_name, sshd_config)
+ content = ssh_util.update_authorized_keys(auth_key_entries, [])
+
+ self.assertEqual("%s/.ssh/authorized_keys" % fpw.pw_dir, auth_key_fn)
+ self.assertTrue(VALID_CONTENT['rsa'] in content)
+ self.assertTrue(VALID_CONTENT['dsa'] in content)
+
# vi: ts=4 expandtab
diff --git a/tests/unittests/test_util.py b/tests/unittests/test_util.py
index 5a14479a..9ff17f52 100644
--- a/tests/unittests/test_util.py
+++ b/tests/unittests/test_util.py
@@ -2,26 +2,21 @@
from __future__ import print_function
+import io
+import json
import logging
import os
import re
import shutil
import stat
-import tempfile
-
-import json
-import six
import sys
+import tempfile
import yaml
+from unittest import mock
from cloudinit import importer, util
from cloudinit.tests import helpers
-try:
- from unittest import mock
-except ImportError:
- import mock
-
BASH = util.which('bash')
BOGUS_COMMAND = 'this-is-not-expected-to-be-a-program-name'
@@ -320,7 +315,7 @@ class TestLoadYaml(helpers.CiTestCase):
def test_python_unicode(self):
# complex type of python/unicode is explicitly allowed
- myobj = {'1': six.text_type("FOOBAR")}
+ myobj = {'1': "FOOBAR"}
safe_yaml = yaml.dump(myobj)
self.assertEqual(util.load_yaml(blob=safe_yaml,
default=self.mydefault),
@@ -663,8 +658,8 @@ class TestMultiLog(helpers.FilesystemMockingTestCase):
self.patchOS(self.root)
self.patchUtils(self.root)
self.patchOpen(self.root)
- self.stdout = six.StringIO()
- self.stderr = six.StringIO()
+ self.stdout = io.StringIO()
+ self.stderr = io.StringIO()
self.patchStdoutAndStderr(self.stdout, self.stderr)
def test_stderr_used_by_default(self):
@@ -879,8 +874,8 @@ class TestSubp(helpers.CiTestCase):
"""Raised exc should have stderr, stdout as string if no decode."""
with self.assertRaises(util.ProcessExecutionError) as cm:
util.subp([BOGUS_COMMAND], decode=True)
- self.assertTrue(isinstance(cm.exception.stdout, six.string_types))
- self.assertTrue(isinstance(cm.exception.stderr, six.string_types))
+ self.assertTrue(isinstance(cm.exception.stdout, str))
+ self.assertTrue(isinstance(cm.exception.stderr, str))
def test_bunch_of_slashes_in_path(self):
self.assertEqual("/target/my/path/",
@@ -1171,4 +1166,10 @@ class TestGetProcEnv(helpers.TestCase):
self.assertEqual({}, util.get_proc_env(1))
self.assertEqual(1, m_load_file.call_count)
+ def test_get_proc_ppid(self):
+ """get_proc_ppid returns correct parent pid value."""
+ my_pid = os.getpid()
+ my_ppid = os.getppid()
+ self.assertEqual(my_ppid, util.get_proc_ppid(my_pid))
+
# vi: ts=4 expandtab
diff --git a/tests/unittests/test_vmware/test_custom_script.py b/tests/unittests/test_vmware/test_custom_script.py
index 2d9519b0..f89f8157 100644
--- a/tests/unittests/test_vmware/test_custom_script.py
+++ b/tests/unittests/test_vmware/test_custom_script.py
@@ -1,10 +1,12 @@
# Copyright (C) 2015 Canonical Ltd.
-# Copyright (C) 2017 VMware INC.
+# Copyright (C) 2017-2019 VMware INC.
#
# Author: Maitreyee Saikia <msaikia@vmware.com>
#
# This file is part of cloud-init. See LICENSE file for license information.
+import os
+import stat
from cloudinit import util
from cloudinit.sources.helpers.vmware.imc.config_custom_script import (
CustomScriptConstant,
@@ -18,6 +20,10 @@ from cloudinit.tests.helpers import CiTestCase, mock
class TestVmwareCustomScript(CiTestCase):
def setUp(self):
self.tmpDir = self.tmp_dir()
+ # Mock the tmpDir as the root dir in VM.
+ self.execDir = os.path.join(self.tmpDir, ".customization")
+ self.execScript = os.path.join(self.execDir,
+ ".customize.sh")
def test_prepare_custom_script(self):
"""
@@ -37,63 +43,67 @@ class TestVmwareCustomScript(CiTestCase):
# Custom script exists.
custScript = self.tmp_path("test-cust", self.tmpDir)
- util.write_file(custScript, "test-CR-strip/r/r")
- postCust = PostCustomScript("test-cust", self.tmpDir)
- self.assertEqual("test-cust", postCust.scriptname)
- self.assertEqual(self.tmpDir, postCust.directory)
- self.assertEqual(custScript, postCust.scriptpath)
- self.assertFalse(postCust.postreboot)
- postCust.prepare_script()
- # Check if all carraige returns are stripped from script.
- self.assertFalse("/r" in custScript)
+ util.write_file(custScript, "test-CR-strip\r\r")
+ with mock.patch.object(CustomScriptConstant,
+ "CUSTOM_TMP_DIR",
+ self.execDir):
+ with mock.patch.object(CustomScriptConstant,
+ "CUSTOM_SCRIPT",
+ self.execScript):
+ postCust = PostCustomScript("test-cust",
+ self.tmpDir,
+ self.tmpDir)
+ self.assertEqual("test-cust", postCust.scriptname)
+ self.assertEqual(self.tmpDir, postCust.directory)
+ self.assertEqual(custScript, postCust.scriptpath)
+ postCust.prepare_script()
- def test_rc_local_exists(self):
- """
- This test is designed to verify the different scenarios associated
- with the presence of rclocal.
- """
- # test when rc local does not exist
- postCust = PostCustomScript("test-cust", self.tmpDir)
- with mock.patch.object(CustomScriptConstant, "RC_LOCAL", "/no/path"):
- rclocal = postCust.find_rc_local()
- self.assertEqual("", rclocal)
-
- # test when rc local exists
- rclocalFile = self.tmp_path("vmware-rclocal", self.tmpDir)
- util.write_file(rclocalFile, "# Run post-reboot guest customization",
- omode="w")
- with mock.patch.object(CustomScriptConstant, "RC_LOCAL", rclocalFile):
- rclocal = postCust.find_rc_local()
- self.assertEqual(rclocalFile, rclocal)
- self.assertTrue(postCust.has_previous_agent, rclocal)
-
- # test when rc local is a symlink
- rclocalLink = self.tmp_path("dummy-rclocal-link", self.tmpDir)
- util.sym_link(rclocalFile, rclocalLink, True)
- with mock.patch.object(CustomScriptConstant, "RC_LOCAL", rclocalLink):
- rclocal = postCust.find_rc_local()
- self.assertEqual(rclocalFile, rclocal)
+ # Custom script is copied with exec privilege
+ self.assertTrue(os.path.exists(self.execScript))
+ st = os.stat(self.execScript)
+ self.assertTrue(st.st_mode & stat.S_IEXEC)
+ with open(self.execScript, "r") as f:
+ content = f.read()
+ self.assertEqual(content, "test-CR-strip")
+ # Check if all carraige returns are stripped from script.
+ self.assertFalse("\r" in content)
def test_execute_post_cust(self):
"""
- This test is to identify if rclocal was properly populated to be
- run after reboot.
+ This test is designed to verify the behavior after execute post
+ customization.
"""
- customscript = self.tmp_path("vmware-post-cust-script", self.tmpDir)
- rclocal = self.tmp_path("vmware-rclocal", self.tmpDir)
- # Create a temporary rclocal file
- open(customscript, "w")
- util.write_file(rclocal, "tests\nexit 0", omode="w")
- postCust = PostCustomScript("vmware-post-cust-script", self.tmpDir)
- with mock.patch.object(CustomScriptConstant, "RC_LOCAL", rclocal):
- # Test that guest customization agent is not installed initially.
- self.assertFalse(postCust.postreboot)
- self.assertIs(postCust.has_previous_agent(rclocal), False)
- postCust.install_agent()
+ # Prepare the customize package
+ postCustRun = self.tmp_path("post-customize-guest.sh", self.tmpDir)
+ util.write_file(postCustRun, "This is the script to run post cust")
+ userScript = self.tmp_path("test-cust", self.tmpDir)
+ util.write_file(userScript, "This is the post cust script")
- # Assert rclocal has been modified to have guest customization
- # agent.
- self.assertTrue(postCust.postreboot)
- self.assertTrue(postCust.has_previous_agent, rclocal)
+ # Mock the cc_scripts_per_instance dir and marker file.
+ # Create another tmp dir for cc_scripts_per_instance.
+ ccScriptDir = self.tmp_dir()
+ ccScript = os.path.join(ccScriptDir, "post-customize-guest.sh")
+ markerFile = os.path.join(self.tmpDir, ".markerFile")
+ with mock.patch.object(CustomScriptConstant,
+ "CUSTOM_TMP_DIR",
+ self.execDir):
+ with mock.patch.object(CustomScriptConstant,
+ "CUSTOM_SCRIPT",
+ self.execScript):
+ with mock.patch.object(CustomScriptConstant,
+ "POST_CUSTOM_PENDING_MARKER",
+ markerFile):
+ postCust = PostCustomScript("test-cust",
+ self.tmpDir,
+ ccScriptDir)
+ postCust.execute()
+ # Check cc_scripts_per_instance and marker file
+ # are created.
+ self.assertTrue(os.path.exists(ccScript))
+ with open(ccScript, "r") as f:
+ content = f.read()
+ self.assertEqual(content,
+ "This is the script to run post cust")
+ self.assertTrue(os.path.exists(markerFile))
# vi: ts=4 expandtab
diff --git a/tests/unittests/test_vmware/test_guestcust_util.py b/tests/unittests/test_vmware/test_guestcust_util.py
new file mode 100644
index 00000000..b175a998
--- /dev/null
+++ b/tests/unittests/test_vmware/test_guestcust_util.py
@@ -0,0 +1,72 @@
+# Copyright (C) 2019 Canonical Ltd.
+# Copyright (C) 2019 VMware INC.
+#
+# Author: Xiaofeng Wang <xiaofengw@vmware.com>
+#
+# This file is part of cloud-init. See LICENSE file for license information.
+
+from cloudinit import util
+from cloudinit.sources.helpers.vmware.imc.guestcust_util import (
+ get_tools_config,
+)
+from cloudinit.tests.helpers import CiTestCase, mock
+
+
+class TestGuestCustUtil(CiTestCase):
+ def test_get_tools_config_not_installed(self):
+ """
+ This test is designed to verify the behavior if vmware-toolbox-cmd
+ is not installed.
+ """
+ with mock.patch.object(util, 'which', return_value=None):
+ self.assertEqual(
+ get_tools_config('section', 'key', 'defaultVal'), 'defaultVal')
+
+ def test_get_tools_config_internal_exception(self):
+ """
+ This test is designed to verify the behavior if internal exception
+ is raised.
+ """
+ with mock.patch.object(util, 'which', return_value='/dummy/path'):
+ with mock.patch.object(util, 'subp',
+ return_value=('key=value', b''),
+ side_effect=util.ProcessExecutionError(
+ "subp failed", exit_code=99)):
+ # verify return value is 'defaultVal', not 'value'.
+ self.assertEqual(
+ get_tools_config('section', 'key', 'defaultVal'),
+ 'defaultVal')
+
+ def test_get_tools_config_normal(self):
+ """
+ This test is designed to verify the value could be parsed from
+ key = value of the given [section]
+ """
+ with mock.patch.object(util, 'which', return_value='/dummy/path'):
+ # value is not blank
+ with mock.patch.object(util, 'subp',
+ return_value=('key = value ', b'')):
+ self.assertEqual(
+ get_tools_config('section', 'key', 'defaultVal'),
+ 'value')
+ # value is blank
+ with mock.patch.object(util, 'subp',
+ return_value=('key = ', b'')):
+ self.assertEqual(
+ get_tools_config('section', 'key', 'defaultVal'),
+ '')
+ # value contains =
+ with mock.patch.object(util, 'subp',
+ return_value=('key=Bar=Wark', b'')):
+ self.assertEqual(
+ get_tools_config('section', 'key', 'defaultVal'),
+ 'Bar=Wark')
+
+ # value contains specific characters
+ with mock.patch.object(util, 'subp',
+ return_value=('[a] b.c_d=e-f', b'')):
+ self.assertEqual(
+ get_tools_config('section', 'key', 'defaultVal'),
+ 'e-f')
+
+# vi: ts=4 expandtab
diff --git a/tests/unittests/test_vmware_config_file.py b/tests/unittests/test_vmware_config_file.py
index f47335ea..16343ed2 100644
--- a/tests/unittests/test_vmware_config_file.py
+++ b/tests/unittests/test_vmware_config_file.py
@@ -62,13 +62,13 @@ class TestVmwareConfigFile(CiTestCase):
(md1, _, _) = read_vmware_imc(conf)
self.assertIn(instance_id_prefix, md1["instance-id"])
- self.assertEqual(len(md1["instance-id"]), len(instance_id_prefix) + 8)
+ self.assertEqual(md1["instance-id"], 'iid-vmware-imc')
(md2, _, _) = read_vmware_imc(conf)
self.assertIn(instance_id_prefix, md2["instance-id"])
- self.assertEqual(len(md2["instance-id"]), len(instance_id_prefix) + 8)
+ self.assertEqual(md2["instance-id"], 'iid-vmware-imc')
- self.assertNotEqual(md1["instance-id"], md2["instance-id"])
+ self.assertEqual(md2["instance-id"], md1["instance-id"])
def test_configfile_static_2nics(self):
"""Tests Config class for a configuration with two static NICs."""