Merge ~powersj/cloud-init:integration-testing into cloud-init:master

Proposed by Joshua Powers
Status: Merged
Merged at revision: f53fc46aa732e3b29991b3e5e39da31a722945ee
Proposed branch: ~powersj/cloud-init:integration-testing
Merge into: cloud-init:master
Diff against target: 6012 lines (+5058/-5)
155 files modified
doc/rtd/index.rst (+1/-0)
doc/rtd/topics/tests.rst (+238/-0)
tests/cloud_tests/__init__.py (+26/-0)
tests/cloud_tests/__main__.py (+89/-0)
tests/cloud_tests/args.py (+217/-0)
tests/cloud_tests/collect.py (+157/-0)
tests/cloud_tests/config.py (+109/-0)
tests/cloud_tests/configs/bugs/README.md (+11/-0)
tests/cloud_tests/configs/bugs/lp1511485.yaml (+9/-0)
tests/cloud_tests/configs/bugs/lp1611074.yaml (+6/-0)
tests/cloud_tests/configs/bugs/lp1628337.yaml (+18/-0)
tests/cloud_tests/configs/examples/README.md (+10/-0)
tests/cloud_tests/configs/examples/TODO.md (+13/-0)
tests/cloud_tests/configs/examples/add_apt_repositories.yaml (+19/-0)
tests/cloud_tests/configs/examples/alter_completion_message.yaml (+14/-0)
tests/cloud_tests/configs/examples/configure_instance_trusted_ca_certificates.yaml (+39/-0)
tests/cloud_tests/configs/examples/configure_instances_ssh_keys.yaml (+61/-0)
tests/cloud_tests/configs/examples/including_user_groups.yaml (+51/-0)
tests/cloud_tests/configs/examples/install_arbitrary_packages.yaml (+18/-0)
tests/cloud_tests/configs/examples/install_run_chef_recipes.yaml (+92/-0)
tests/cloud_tests/configs/examples/run_apt_upgrade.yaml (+9/-0)
tests/cloud_tests/configs/examples/run_commands.yaml (+14/-0)
tests/cloud_tests/configs/examples/run_commands_first_boot.yaml (+14/-0)
tests/cloud_tests/configs/examples/setup_run_puppet.yaml (+53/-0)
tests/cloud_tests/configs/examples/writing_out_arbitrary_files.yaml (+43/-0)
tests/cloud_tests/configs/main/README.md (+9/-0)
tests/cloud_tests/configs/main/command_output_simple.yaml (+11/-0)
tests/cloud_tests/configs/modules/README.md (+10/-0)
tests/cloud_tests/configs/modules/TODO.md (+98/-0)
tests/cloud_tests/configs/modules/apt_configure_conf.yaml (+17/-0)
tests/cloud_tests/configs/modules/apt_configure_disable_suites.yaml (+15/-0)
tests/cloud_tests/configs/modules/apt_configure_primary.yaml (+17/-0)
tests/cloud_tests/configs/modules/apt_configure_proxy.yaml (+14/-0)
tests/cloud_tests/configs/modules/apt_configure_security.yaml (+13/-0)
tests/cloud_tests/configs/modules/apt_configure_sources_key.yaml (+45/-0)
tests/cloud_tests/configs/modules/apt_configure_sources_keyserver.yaml (+18/-0)
tests/cloud_tests/configs/modules/apt_configure_sources_list.yaml (+17/-0)
tests/cloud_tests/configs/modules/apt_configure_sources_ppa.yaml (+18/-0)
tests/cloud_tests/configs/modules/apt_pipelining_disable.yaml (+11/-0)
tests/cloud_tests/configs/modules/apt_pipelining_os.yaml (+11/-0)
tests/cloud_tests/configs/modules/bootcmd.yaml (+11/-0)
tests/cloud_tests/configs/modules/byobu.yaml (+16/-0)
tests/cloud_tests/configs/modules/ca_certs.yaml (+51/-0)
tests/cloud_tests/configs/modules/debug_disable.yaml (+7/-0)
tests/cloud_tests/configs/modules/debug_enable.yaml (+7/-0)
tests/cloud_tests/configs/modules/final_message.yaml (+11/-0)
tests/cloud_tests/configs/modules/keys_to_console.yaml (+11/-0)
tests/cloud_tests/configs/modules/landscape.yaml (+24/-0)
tests/cloud_tests/configs/modules/locale.yaml (+17/-0)
tests/cloud_tests/configs/modules/lxd_bridge.yaml (+28/-0)
tests/cloud_tests/configs/modules/lxd_dir.yaml (+15/-0)
tests/cloud_tests/configs/modules/ntp.yaml (+18/-0)
tests/cloud_tests/configs/modules/ntp_pools.yaml (+21/-0)
tests/cloud_tests/configs/modules/ntp_servers.yaml (+18/-0)
tests/cloud_tests/configs/modules/package_update_upgrade_install.yaml (+20/-0)
tests/cloud_tests/configs/modules/runcmd.yaml (+11/-0)
tests/cloud_tests/configs/modules/salt_minion.yaml (+32/-0)
tests/cloud_tests/configs/modules/seed_random_command.yaml (+16/-0)
tests/cloud_tests/configs/modules/seed_random_data.yaml (+13/-0)
tests/cloud_tests/configs/modules/set_hostname.yaml (+16/-0)
tests/cloud_tests/configs/modules/set_hostname_fqdn.yaml (+18/-0)
tests/cloud_tests/configs/modules/set_password.yaml (+15/-0)
tests/cloud_tests/configs/modules/set_password_expire.yaml (+26/-0)
tests/cloud_tests/configs/modules/set_password_list.yaml (+31/-0)
tests/cloud_tests/configs/modules/snappy.yaml (+11/-0)
tests/cloud_tests/configs/modules/ssh_auth_key_fingerprints_disable.yaml (+11/-0)
tests/cloud_tests/configs/modules/ssh_auth_key_fingerprints_enable.yaml (+14/-0)
tests/cloud_tests/configs/modules/ssh_import_id.yaml (+12/-0)
tests/cloud_tests/configs/modules/ssh_keys_generate.yaml (+40/-0)
tests/cloud_tests/configs/modules/ssh_keys_provided.yaml (+100/-0)
tests/cloud_tests/configs/modules/timezone.yaml (+10/-0)
tests/cloud_tests/configs/modules/user_groups.yaml (+48/-0)
tests/cloud_tests/configs/modules/write_files.yaml (+40/-0)
tests/cloud_tests/images/__init__.py (+6/-0)
tests/cloud_tests/images/base.py (+60/-0)
tests/cloud_tests/images/lxd.py (+88/-0)
tests/cloud_tests/instances/__init__.py (+5/-0)
tests/cloud_tests/instances/base.py (+116/-0)
tests/cloud_tests/instances/lxd.py (+117/-0)
tests/cloud_tests/manage.py (+71/-0)
tests/cloud_tests/platforms.yaml (+15/-0)
tests/cloud_tests/platforms/__init__.py (+15/-0)
tests/cloud_tests/platforms/base.py (+48/-0)
tests/cloud_tests/platforms/lxd.py (+93/-0)
tests/cloud_tests/releases.yaml (+77/-0)
tests/cloud_tests/setup_image.py (+191/-0)
tests/cloud_tests/snapshots/__init__.py (+5/-0)
tests/cloud_tests/snapshots/base.py (+39/-0)
tests/cloud_tests/snapshots/lxd.py (+46/-0)
tests/cloud_tests/stage.py (+109/-0)
tests/cloud_tests/testcases.yaml (+25/-0)
tests/cloud_tests/testcases/__init__.py (+43/-0)
tests/cloud_tests/testcases/base.py (+77/-0)
tests/cloud_tests/testcases/bugs/__init__.py (+4/-0)
tests/cloud_tests/testcases/bugs/lp1511485.py (+11/-0)
tests/cloud_tests/testcases/bugs/lp1628337.py (+19/-0)
tests/cloud_tests/testcases/examples/__init__.py (+4/-0)
tests/cloud_tests/testcases/examples/add_apt_repositories.py (+16/-0)
tests/cloud_tests/testcases/examples/alter_completion_message.py (+45/-0)
tests/cloud_tests/testcases/examples/configure_instance_trusted_ca_certificates.py (+23/-0)
tests/cloud_tests/testcases/examples/configure_instances_ssh_keys.py (+27/-0)
tests/cloud_tests/testcases/examples/including_user_groups.py (+39/-0)
tests/cloud_tests/testcases/examples/install_arbitrary_packages.py (+16/-0)
tests/cloud_tests/testcases/examples/run_apt_upgrade.py (+15/-0)
tests/cloud_tests/testcases/examples/run_commands.py (+11/-0)
tests/cloud_tests/testcases/examples/run_commands_first_boot.py (+11/-0)
tests/cloud_tests/testcases/examples/writing_out_arbitrary_files.py (+26/-0)
tests/cloud_tests/testcases/main/__init__.py (+4/-0)
tests/cloud_tests/testcases/main/command_output_simple.py (+17/-0)
tests/cloud_tests/testcases/modules/__init__.py (+4/-0)
tests/cloud_tests/testcases/modules/apt_configure_conf.py (+16/-0)
tests/cloud_tests/testcases/modules/apt_configure_disable_suites.py (+11/-0)
tests/cloud_tests/testcases/modules/apt_configure_primary.py (+16/-0)
tests/cloud_tests/testcases/modules/apt_configure_proxy.py (+18/-0)
tests/cloud_tests/testcases/modules/apt_configure_security.py (+11/-0)
tests/cloud_tests/testcases/modules/apt_configure_sources_key.py (+19/-0)
tests/cloud_tests/testcases/modules/apt_configure_sources_keyserver.py (+19/-0)
tests/cloud_tests/testcases/modules/apt_configure_sources_list.py (+22/-0)
tests/cloud_tests/testcases/modules/apt_configure_sources_ppa.py (+19/-0)
tests/cloud_tests/testcases/modules/apt_pipelining_disable.py (+11/-0)
tests/cloud_tests/testcases/modules/apt_pipelining_os.py (+11/-0)
tests/cloud_tests/testcases/modules/bootcmd.py (+11/-0)
tests/cloud_tests/testcases/modules/byobu.py (+21/-0)
tests/cloud_tests/testcases/modules/ca_certs.py (+16/-0)
tests/cloud_tests/testcases/modules/debug_disable.py (+12/-0)
tests/cloud_tests/testcases/modules/debug_enable.py (+11/-0)
tests/cloud_tests/testcases/modules/final_message.py (+45/-0)
tests/cloud_tests/testcases/modules/keys_to_console.py (+18/-0)
tests/cloud_tests/testcases/modules/locale.py (+23/-0)
tests/cloud_tests/testcases/modules/lxd_bridge.py (+22/-0)
tests/cloud_tests/testcases/modules/lxd_dir.py (+16/-0)
tests/cloud_tests/testcases/modules/ntp.py (+24/-0)
tests/cloud_tests/testcases/modules/ntp_pools.py (+24/-0)
tests/cloud_tests/testcases/modules/ntp_servers.py (+21/-0)
tests/cloud_tests/testcases/modules/package_update_upgrade_install.py (+34/-0)
tests/cloud_tests/testcases/modules/runcmd.py (+11/-0)
tests/cloud_tests/testcases/modules/salt_minion.py (+25/-0)
tests/cloud_tests/testcases/modules/seed_random_data.py (+11/-0)
tests/cloud_tests/testcases/modules/set_hostname.py (+11/-0)
tests/cloud_tests/testcases/modules/set_hostname_fqdn.py (+22/-0)
tests/cloud_tests/testcases/modules/set_password.py (+18/-0)
tests/cloud_tests/testcases/modules/set_password_expire.py (+19/-0)
tests/cloud_tests/testcases/modules/set_password_list.py (+21/-0)
tests/cloud_tests/testcases/modules/snappy.py (+14/-0)
tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.py (+20/-0)
tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_enable.py (+14/-0)
tests/cloud_tests/testcases/modules/ssh_import_id.py (+22/-0)
tests/cloud_tests/testcases/modules/ssh_keys_generate.py (+53/-0)
tests/cloud_tests/testcases/modules/ssh_keys_provided.py (+65/-0)
tests/cloud_tests/testcases/modules/timezone.py (+11/-0)
tests/cloud_tests/testcases/modules/user_groups.py (+39/-0)
tests/cloud_tests/testcases/modules/write_files.py (+26/-0)
tests/cloud_tests/util.py (+159/-0)
tests/cloud_tests/verify.py (+89/-0)
tox.ini (+5/-5)
Reviewer Review Type Date Requested Status
Joshua Powers (community) Needs Resubmitting
Scott Moser Needs Fixing
Review via email: mp+312830@code.launchpad.net

Commit message

inttests: initial commit of integration test framework

The adds in end-to-end testing of cloud-init. The framework utilizes
LXD and cloud images as a backend to test user-data passed in.
Arbitrary data is then captured from predefined commands specified
by the user. After collection, data verification is completed by
running a series of Python unit tests against the collected data.

Currently only the Ubuntu Trusty, Xenial, Yakkety, and Zesty
releases are supported. Test cases for 50% of the modules is
complete and available.

Additionally a Read the Docs file was created to guide test
writing and execution.

Description of the change

* initial commit of integration test framework
* added tests.rst to Read the Docs and updated index.rst
* updated tox.ini to not run our integration unit test scripts when running unit tests

To post a comment you must log in.
Revision history for this message
Scott Moser (smoser) wrote :

some comments inline

review: Needs Fixing
Revision history for this message
Joshua Powers (powersj) wrote :

Updating PPA and other sources.list tests to no longer use my personal
information. This will use the generic cloud-init testing and
development information. Also modifies the tox config to only run the
tests under unittests for Py3. Disabling the SSH tests that utilize my
own key until we have a function to create keys on the fly. Also
removing numerous bashisms.

review: Needs Resubmitting
Revision history for this message
Scott Moser (smoser) wrote :

I pulled this into trunk.
I had some issues trying to just 'rebase' and then squash so i just diffed and applied locally.

other changes i made:
 * add vi: on each python file
 * add cloud-init license on each test python file
 * keep tests.rst under 80 chars wide.

Revision history for this message
Joshua Powers (powersj) wrote :

Thank you very much!

Is updating read-the-docs something we can do to? Or does that happen
automatically?

Josh

On Thu, Dec 22, 2016 at 3:44 PM, Scott Moser <email address hidden> wrote:

> I pulled this into trunk.
> I had some issues trying to just 'rebase' and then squash so i just diffed
> and applied locally.
>
> other changes i made:
> * add vi: on each python file
> * add cloud-init license on each test python file
> * keep tests.rst under 80 chars wide.
>
> --
> https://code.launchpad.net/~powersj/cloud-init/+git/cloud-
> init/+merge/312830
> You are the owner of ~powersj/cloud-init:integration-testing.
>

Revision history for this message
Scott Moser (smoser) wrote :

Should happen automatically. I'll check to make sure tomorrow

On December 22, 2016 6:12:42 PM EST, Joshua Powers <email address hidden> wrote:
>Thank you very much!
>
>Is updating read-the-docs something we can do to? Or does that happen
>automatically?
>
>Josh
>
>On Thu, Dec 22, 2016 at 3:44 PM, Scott Moser <email address hidden> wrote:
>
>> I pulled this into trunk.
>> I had some issues trying to just 'rebase' and then squash so i just
>diffed
>> and applied locally.
>>
>> other changes i made:
>> * add vi: on each python file
>> * add cloud-init license on each test python file
>> * keep tests.rst under 80 chars wide.
>>
>> --
>> https://code.launchpad.net/~powersj/cloud-init/+git/cloud-
>> init/+merge/312830
>> You are the owner of ~powersj/cloud-init:integration-testing.
>>
>
>--
>https://code.launchpad.net/~powersj/cloud-init/+git/cloud-init/+merge/312830
>You are reviewing the proposed merge of
>~powersj/cloud-init:integration-testing into cloud-init:master.

There was an error fetching revisions from git servers. Please try again in a few minutes. If the problem persists, contact Launchpad support.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/doc/rtd/index.rst b/doc/rtd/index.rst
2index 3caf33f..8f163b6 100644
3--- a/doc/rtd/index.rst
4+++ b/doc/rtd/index.rst
5@@ -41,6 +41,7 @@ initialization of a cloud instance.
6 topics/vendordata.rst
7 topics/moreinfo.rst
8 topics/hacking.rst
9+ topics/tests.rst
10
11 .. _Cloud-init: https://launchpad.net/cloud-init
12 .. vi: textwidth=78
13diff --git a/doc/rtd/topics/tests.rst b/doc/rtd/topics/tests.rst
14new file mode 100644
15index 0000000..d2db140
16--- /dev/null
17+++ b/doc/rtd/topics/tests.rst
18@@ -0,0 +1,238 @@
19+.. contents:: Table of Contents
20+ :depth: 2
21+
22+============================
23+Test Development
24+============================
25+
26+
27+Overview
28+--------
29+
30+The purpose of this page is to describe how to write integration tests for cloud-init. As a test writer you need to develop a test configuration and a verification file:
31+
32+ * The test configuration specifies a specific cloud-config to be used by cloud-init and a list of arbitrary commands to capture the output of (e.g my_test.yaml)
33+
34+ * The verification file runs tests on the collected output to determine the result of the test (e.g. my_test.py)
35+
36+The names must match, however the extensions will of course be different, yaml vs py.
37+
38+Configuration
39+-------------
40+
41+The test configuration is a YAML file such as *ntp_server.yaml* below:
42+
43+.. code-block:: yaml
44+
45+ #
46+ # NTP config using specific servers (ntp_server.yaml)
47+ #
48+ cloud_config: |
49+ #cloud-config
50+ ntp:
51+ servers:
52+ - pool.ntp.org
53+ collect_scripts:
54+ ntp_installed_servers: |
55+ #!/bin/bash
56+ dpkg -l | grep ntp | wc -l
57+ ntp_conf_dist_servers: |
58+ #!/bin/bash
59+ ls /etc/ntp.conf.dist | wc -l
60+ ntp_conf_servers: |
61+ #!/bin/bash
62+ cat /etc/ntp.conf | grep '^server'
63+
64+
65+There are two keys, 1 required and 1 optional, in the YAML file:
66+
67+1. The required key is ``cloud_config``. This should be a string of valid YAML that is exactly what would normally be placed in a cloud-config file, including the cloud-config header. This essentially sets up the scenario under test.
68+
69+2. The optional key is ``collect_scripts``. This key has one or more sub-keys containing strings of arbitrary commands to execute (e.g. ```cat /var/log/cloud-config-output.log```). In the example above the output of dpkg is captured, grep for ntp, and the number of lines reported. The name of the sub-key is important. The sub-key is used by the verification script to recall the output of the commands ran.
70+
71+Default Collect Scripts
72+~~~~~~~~~~~~~~~~~~~~~~~
73+
74+By default the following files will be collected for every test. There is no need to specify these items:
75+
76+* ``/var/log/cloud-init.log``
77+* ``/var/log/cloud-init-output.log``
78+* ``/run/cloud-init/.instance-id``
79+* ``/run/cloud-init/result.json``
80+* ``/run/cloud-init/status.json``
81+* ```dpkg-query -W -f='${Version}' cloud-init```
82+
83+Verification
84+------------
85+
86+The verification script is a Python file with unit tests like the one, `ntp_server.py`, below:
87+
88+.. code-block:: python
89+
90+ """cloud-init Integration Test Verify Script (ntp_server.yaml)"""
91+ from tests.cloud_tests.testcases import base
92+
93+
94+ class TestNtpServers(base.CloudTestCase):
95+ """Test ntp module"""
96+
97+ def test_ntp_installed(self):
98+ """Test ntp installed"""
99+ out = self.get_data_file('ntp_installed_servers')
100+ self.assertEqual(1, int(out))
101+
102+ def test_ntp_dist_entries(self):
103+ """Test dist config file has one entry"""
104+ out = self.get_data_file('ntp_conf_dist_servers')
105+ self.assertEqual(1, int(out))
106+
107+ def test_ntp_entires(self):
108+ """Test config entries"""
109+ out = self.get_data_file('ntp_conf_servers')
110+ self.assertIn('server pool.ntp.org iburst', out)
111+
112+
113+Here is a breakdown of the unit test file:
114+
115+* The import statement allows access to the output files.
116+
117+* The class can be named anything, but must import the ``base.CloudTestCase``
118+
119+* There can be 1 to N number of functions with any name, however only tests starting with ``test_*`` will be executed.
120+
121+* Output from the commands can be accessed via ``self.get_data_file('key')`` where key is the sub-key of ``collect_scripts`` above.
122+
123+Layout
124+------
125+
126+Integration tests are located under the `tests/cloud_tests` directory. Test configurations are placed under `configs` and the test verification scripts under `testcases`:
127+
128+.. code-block:: bash
129+
130+ cloud-init$ tree -d tests/cloud_tests/
131+ tests/cloud_tests/
132+ ├── configs
133+ │   ├── bugs
134+ │   ├── examples
135+ │   ├── main
136+ │   └── modules
137+ └── testcases
138+ ├── bugs
139+ ├── examples
140+ ├── main
141+ └── modules
142+
143+The sub-folders of bugs, examples, main, and modules help organize the tests. View the README.md in each to understand in more detail each directory.
144+
145+
146+=====================
147+Development Checklist
148+=====================
149+
150+* Configuration File
151+ * Named 'your_test_here.yaml'
152+ * Contains at least a valid cloud-config
153+ * Optionally, commands to capture additional output
154+ * Valid YAML
155+ * Placed in the appropriate sub-folder in the configs directory
156+* Verification File
157+ * Named 'your_test_here.py'
158+ * Valid unit tests validating output collected
159+ * Passes pylint & pep8 checks
160+ * Placed in the appropriate sub-folder in the testcsaes directory
161+* Tested by running the test:
162+
163+.. code-block:: bash
164+
165+ $ python3 -m tests.cloud_tests run -v -n <release of choice> --deb <build of cloud-init> -t tests/cloud_tests/configs/<dir>/your_test_here.yaml
166+
167+
168+=========
169+Execution
170+=========
171+
172+Executing tests has three options:
173+
174+* ``run`` an alias to run both ``collect`` and ``verify``
175+
176+* ``collect`` deploys on the specified platform and os, patches with the requested deb or rpm, and finally collects output of the arbitrary commands.
177+
178+* ``verify`` given a directory of test data, run the Python unit tests on it to generate results.
179+
180+Run
181+---
182+The first example will provide a complete end-to-end run of data collection and verification. There are additional examples below explaining how to run one or the other independently.
183+
184+.. code-block:: bash
185+
186+ $ git clone https://git.launchpad.net/cloud-init
187+ $ cd cloud-init
188+ $ python3 -m tests.cloud_tests run -v -n trusty -n xenial --deb cloud-init_0.7.8~my_patch_all.deb
189+
190+The above command will do the following:
191+
192+* ``-v`` verbose output
193+
194+* ``run`` both collect output and run tests the output
195+
196+* ``-n trusty`` on the Ubuntu Trusty release
197+
198+* ``-n xenial`` on the Ubuntu Xenial release
199+
200+* ``--deb cloud-init_0.7.8~patch_all.deb`` use this deb as the version of cloud-init to run with
201+
202+For a more detailed explanation of each option see below.
203+
204+Collect
205+-------
206+
207+If developing tests it may be necessary to see if cloud-config works as expected and the correct files are pulled down. In this case only a collect can be ran by running:
208+
209+.. code-block:: bash
210+
211+ $ python3 -m tests.cloud_tests collect -n xenial -d /tmp/collection --deb cloud-init_0.7.8~my_patch_all.deb
212+
213+The above command will run the collection tests on xenial with the provided deb and place all results into `/tmp/collection`.
214+
215+Verify
216+------
217+
218+When developing tests it is much easier to simply rerun the verify scripts without the more lengthy collect process. This can be done by running:
219+
220+.. code-block:: bash
221+
222+ $ python3 -m tests.cloud_tests verify -d /tmp/collection
223+
224+The above command will run the verify scripts on the data discovered in `/tmp/collection`.
225+
226+
227+============
228+Architecture
229+============
230+
231+The following outlines the process flow during a complete end-to-end LXD-backed test.
232+
233+1. Configuration
234+ * The back end and specific OS releases are verified as supported
235+ * The test or tests that need to be run are determined either by directory or by individual yaml
236+
237+2. Image Creation
238+ * Acquire the daily LXD image
239+ * Install the specified cloud-init package
240+ * Clean the image so that it does not appear to have been booted
241+ * A snapshot of the image is created and reused by all tests
242+
243+3. Configuration
244+ * For each test, the cloud-config is injected into a copy of the snapshot and booted
245+ * The framework waits for ``/var/lib/cloud/instance/boot-finished`` (up to 120 seconds)
246+ * All default commands are ran and output collected
247+ * Any commands the user specified are executed and output collected
248+
249+4. Verification
250+ * The default commands are checked for any failures, errors, and warnings to validate basic functionality of cloud-init completed successfully
251+ * The user generated unit tests are then ran validating against the collected output
252+
253+5. Results
254+ * If any failures were detected the test suite returns a failure
255+
256+
257diff --git a/tests/cloud_tests/__init__.py b/tests/cloud_tests/__init__.py
258new file mode 100644
259index 0000000..8c1b6d1
260--- /dev/null
261+++ b/tests/cloud_tests/__init__.py
262@@ -0,0 +1,26 @@
263+import logging
264+import os
265+
266+BASE_DIR = os.path.dirname(os.path.abspath(__file__))
267+TESTCASES_DIR = os.path.join(BASE_DIR, 'testcases')
268+TEST_CONF_DIR = os.path.join(BASE_DIR, 'configs')
269+
270+
271+def _initialize_logging():
272+ """
273+ configure logging for cloud_tests
274+ """
275+ logger = logging.getLogger(__name__)
276+ logger.setLevel(logging.DEBUG)
277+ formatter = logging.Formatter(
278+ '%(asctime)s - %(name)s - %(levelname)s - %(message)s')
279+
280+ console = logging.StreamHandler()
281+ console.setLevel(logging.DEBUG)
282+ console.setFormatter(formatter)
283+
284+ logger.addHandler(console)
285+
286+ return logger
287+
288+LOG = _initialize_logging()
289diff --git a/tests/cloud_tests/__main__.py b/tests/cloud_tests/__main__.py
290new file mode 100644
291index 0000000..dbd4cbd
292--- /dev/null
293+++ b/tests/cloud_tests/__main__.py
294@@ -0,0 +1,89 @@
295+import argparse
296+import logging
297+import shutil
298+import sys
299+import tempfile
300+
301+from tests.cloud_tests import (args, collect, manage, verify)
302+from tests.cloud_tests import LOG
303+
304+
305+def configure_log(args):
306+ """
307+ configure logging
308+ """
309+ level = logging.INFO
310+ if args.verbose:
311+ level = logging.DEBUG
312+ elif args.quiet:
313+ level = logging.WARN
314+ LOG.setLevel(level)
315+
316+
317+def run(args):
318+ """
319+ run full test suite
320+ """
321+ failed = 0
322+ args.data_dir = tempfile.mkdtemp(prefix='cloud_test_data_')
323+ LOG.debug('using tmpdir %s', args.data_dir)
324+ try:
325+ failed += collect.collect(args)
326+ failed += verify.verify(args)
327+ except Exception:
328+ failed += 1
329+ raise
330+ finally:
331+ # TODO: make this configurable via environ or cmdline
332+ if failed:
333+ LOG.warn('some tests failed, leaving data in %s', args.data_dir)
334+ else:
335+ shutil.rmtree(args.data_dir)
336+ return failed
337+
338+
339+def main():
340+ """
341+ entry point for cloud test suite
342+ """
343+ # configure parser
344+ parser = argparse.ArgumentParser(prog='cloud_tests')
345+ subparsers = parser.add_subparsers(dest="subcmd")
346+ subparsers.required = True
347+
348+ def add_subparser(name, description, arg_sets):
349+ """
350+ add arguments to subparser
351+ """
352+ subparser = subparsers.add_parser(name, help=description)
353+ for (_args, _kwargs) in (a for arg_set in arg_sets for a in arg_set):
354+ subparser.add_argument(*_args, **_kwargs)
355+
356+ # configure subparsers
357+ for (name, (description, arg_sets)) in args.SUBCMDS.items():
358+ add_subparser(name, description,
359+ [args.ARG_SETS[arg_set] for arg_set in arg_sets])
360+
361+ # parse arguments
362+ parsed = parser.parse_args()
363+
364+ # process arguments
365+ configure_log(parsed)
366+ (_, arg_sets) = args.SUBCMDS[parsed.subcmd]
367+ for normalizer in [args.NORMALIZERS[arg_set] for arg_set in arg_sets]:
368+ parsed = normalizer(parsed)
369+ if not parsed:
370+ return -1
371+
372+ # run handler
373+ LOG.debug('running with args: %s\n', parsed)
374+ return {
375+ 'collect': collect.collect,
376+ 'create': manage.create,
377+ 'run': run,
378+ 'verify': verify.verify,
379+ }[parsed.subcmd](parsed)
380+
381+
382+if __name__ == "__main__":
383+ sys.exit(main())
384diff --git a/tests/cloud_tests/args.py b/tests/cloud_tests/args.py
385new file mode 100644
386index 0000000..6a5c722
387--- /dev/null
388+++ b/tests/cloud_tests/args.py
389@@ -0,0 +1,217 @@
390+import os
391+
392+from tests.cloud_tests import config, util
393+from tests.cloud_tests import LOG
394+
395+ARG_SETS = {
396+ 'COLLECT': (
397+ (('-p', '--platform'),
398+ {'help': 'platform(s) to run tests on', 'metavar': 'PLATFORM',
399+ 'action': 'append', 'choices': config.list_enabled_platforms(),
400+ 'default': []}),
401+ (('-n', '--os-name'),
402+ {'help': 'the name(s) of the OS(s) to test', 'metavar': 'NAME',
403+ 'action': 'append', 'choices': config.list_enabled_distros(),
404+ 'default': []}),
405+ (('-t', '--test-config'),
406+ {'help': 'test config file(s) to use', 'metavar': 'FILE',
407+ 'action': 'append', 'default': []}),),
408+ 'CREATE': (
409+ (('-c', '--config'),
410+ {'help': 'cloud-config yaml for testcase', 'metavar': 'DATA',
411+ 'action': 'store', 'required': False, 'default': None}),
412+ (('-e', '--enable'),
413+ {'help': 'enable testcase', 'required': False, 'default': False,
414+ 'action': 'store_true'}),
415+ (('name',),
416+ {'help': 'testcase name, in format "<category>/<test>"',
417+ 'action': 'store'}),
418+ (('-d', '--description'),
419+ {'help': 'description of testcase', 'required': False}),
420+ (('-f', '--force'),
421+ {'help': 'overwrite already existing test', 'required': False,
422+ 'action': 'store_true', 'default': False}),),
423+ 'INTERFACE': (
424+ (('-v', '--verbose'),
425+ {'help': 'verbose output', 'action': 'store_true', 'default': False}),
426+ (('-q', '--quiet'),
427+ {'help': 'quiet output', 'action': 'store_true', 'default': False}),),
428+ 'OUTPUT': (
429+ (('-d', '--data-dir'),
430+ {'help': 'directory to store test data in',
431+ 'action': 'store', 'metavar': 'DIR', 'required': True}),),
432+ 'RESULT': (
433+ (('-r', '--result'),
434+ {'help': 'file to write results to',
435+ 'action': 'store', 'metavar': 'FILE'}),),
436+ 'SETUP': (
437+ (('--deb',),
438+ {'help': 'install deb', 'metavar': 'FILE', 'action': 'store'}),
439+ (('--rpm',),
440+ {'help': 'install rpm', 'metavar': 'FILE', 'action': 'store'}),
441+ (('--script',),
442+ {'help': 'script to set up image', 'metavar': 'DATA',
443+ 'action': 'store'}),
444+ (('--repo',),
445+ {'help': 'repo to enable (implies -u)', 'metavar': 'NAME',
446+ 'action': 'store'}),
447+ (('--ppa',),
448+ {'help': 'ppa to enable (implies -u)', 'metavar': 'NAME',
449+ 'action': 'store'}),
450+ (('-u', '--upgrade'),
451+ {'help': 'upgrade before starting tests', 'action': 'store_true',
452+ 'default': False}),),
453+}
454+
455+SUBCMDS = {
456+ 'collect': ('collect test data',
457+ ('COLLECT', 'INTERFACE', 'OUTPUT', 'RESULT', 'SETUP')),
458+ 'create': ('create new test case', ('CREATE', 'INTERFACE')),
459+ 'run': ('run test suite', ('COLLECT', 'INTERFACE', 'RESULT', 'SETUP')),
460+ 'verify': ('verify test data', ('INTERFACE', 'OUTPUT', 'RESULT')),
461+}
462+
463+
464+def _empty_normalizer(args):
465+ """
466+ do not normalize arguments
467+ """
468+ return args
469+
470+
471+def normalize_create_args(args):
472+ """
473+ normalize CREATE arguments
474+ args: parsed args
475+ return_value: updated args, or None if errors occurred
476+ """
477+ # ensure valid name for new test
478+ if len(args.name.split('/')) != 2:
479+ LOG.error('invalid test name: %s', args.name)
480+ return None
481+ if os.path.exists(config.name_to_path(args.name)):
482+ msg = 'test: {} already exists'.format(args.name)
483+ if args.force:
484+ LOG.warn('%s but ignoring due to --force', msg)
485+ else:
486+ LOG.error(msg)
487+ return None
488+
489+ # ensure test config valid if specified
490+ if isinstance(args.config, str) and len(args.config) == 0:
491+ LOG.error('test config cannot be empty if specified')
492+ return None
493+
494+ # ensure description valid if specified
495+ if (isinstance(args.description, str) and
496+ (len(args.description) > 70 or len(args.description) == 0)):
497+ LOG.error('test description must be between 1 and 70 characters')
498+ return None
499+
500+ return args
501+
502+
503+def normalize_collect_args(args):
504+ """
505+ normalize COLLECT arguments
506+ args: parsed args
507+ return_value: updated args, or None if errors occurred
508+ """
509+ # platform should default to all supported
510+ if len(args.platform) == 0:
511+ args.platform = config.list_enabled_platforms()
512+ args.platform = util.sorted_unique(args.platform)
513+
514+ # os name should default to all enabled
515+ # if os name is provided ensure that all provided are supported
516+ if len(args.os_name) == 0:
517+ args.os_name = config.list_enabled_distros()
518+ else:
519+ supported = config.list_enabled_distros()
520+ invalid = [os_name for os_name in args.os_name
521+ if os_name not in supported]
522+ if len(invalid) != 0:
523+ LOG.error('invalid os name(s): %s', invalid)
524+ return None
525+ args.os_name = util.sorted_unique(args.os_name)
526+
527+ # test configs should default to all enabled
528+ # if test configs are provided, ensure that all provided are valid
529+ if len(args.test_config) == 0:
530+ args.test_config = config.list_test_configs()
531+ else:
532+ valid = []
533+ invalid = []
534+ for name in args.test_config:
535+ if os.path.exists(name):
536+ valid.append(name)
537+ elif os.path.exists(config.name_to_path(name)):
538+ valid.append(config.name_to_path(name))
539+ else:
540+ invalid.append(name)
541+ if len(invalid) != 0:
542+ LOG.error('invalid test config(s): %s', invalid)
543+ return None
544+ else:
545+ args.test_config = valid
546+ args.test_config = util.sorted_unique(args.test_config)
547+
548+ return args
549+
550+
551+def normalize_output_args(args):
552+ """
553+ normalize OUTPUT arguments
554+ args: parsed args
555+ return_value: updated args, or None if errors occurred
556+ """
557+ if not args.data_dir:
558+ LOG.error('--data-dir must be specified')
559+ return None
560+
561+ # ensure clean output dir if collect
562+ # ensure data exists if verify
563+ if args.subcmd == 'collect':
564+ if not util.is_clean_writable_dir(args.data_dir):
565+ LOG.error('data_dir must be empty/new and must be writable')
566+ return None
567+ elif args.subcmd == 'verify':
568+ if not os.path.exists(args.data_dir):
569+ LOG.error('data_dir %s does not exist', args.data_dir)
570+ return None
571+
572+ return args
573+
574+
575+def normalize_setup_args(args):
576+ """
577+ normalize SETUP arguments
578+ args: parsed args
579+ return_value: updated_args, or None if errors occurred
580+ """
581+ # ensure deb or rpm valid if specified
582+ for pkg in (args.deb, args.rpm):
583+ if pkg is not None and not os.path.exists(pkg):
584+ LOG.error('cannot find package: %s', pkg)
585+ return None
586+
587+ # if repo or ppa to be enabled run upgrade
588+ if args.repo or args.ppa:
589+ args.upgrade = True
590+
591+ # if ppa is specified, remove leading 'ppa:' if any
592+ _ppa_header = 'ppa:'
593+ if args.ppa and args.ppa.startswith(_ppa_header):
594+ args.ppa = args.ppa[len(_ppa_header):]
595+
596+ return args
597+
598+
599+NORMALIZERS = {
600+ 'COLLECT': normalize_collect_args,
601+ 'CREATE': normalize_create_args,
602+ 'INTERFACE': _empty_normalizer,
603+ 'OUTPUT': normalize_output_args,
604+ 'RESULT': _empty_normalizer,
605+ 'SETUP': normalize_setup_args,
606+}
607diff --git a/tests/cloud_tests/collect.py b/tests/cloud_tests/collect.py
608new file mode 100644
609index 0000000..f2767e6
610--- /dev/null
611+++ b/tests/cloud_tests/collect.py
612@@ -0,0 +1,157 @@
613+from tests.cloud_tests import (config, LOG, setup_image, util)
614+from tests.cloud_tests.stage import (PlatformComponent, run_stage, run_single)
615+from tests.cloud_tests import (platforms, images, snapshots, instances)
616+
617+from functools import partial
618+import os
619+
620+
621+def collect_script(instance, base_dir, script, script_name):
622+ """
623+ collect script data
624+ instance: instance to run script on
625+ base_dir: base directory for output data
626+ script: script contents
627+ script_name: name of script to run
628+ return_value: None, may raise errors
629+ """
630+ LOG.debug('running collect script: %s', script_name)
631+ util.write_file(os.path.join(base_dir, script_name),
632+ instance.run_script(script))
633+
634+
635+def collect_test_data(args, snapshot, os_name, test_name):
636+ """
637+ collect data for test case
638+ args: cmdline arguments
639+ snapshot: instantiated snapshot
640+ test_name: name or path of test to run
641+ return_value: tuple of results and fail count
642+ """
643+ res = ({}, 1)
644+
645+ # load test config
646+ test_name = config.path_to_name(test_name)
647+ test_config = config.load_test_config(test_name)
648+ user_data = test_config['cloud_config']
649+ test_scripts = test_config['collect_scripts']
650+ test_output_dir = os.sep.join(
651+ (args.data_dir, snapshot.platform_name, os_name, test_name))
652+ boot_timeout = (test_config.get('boot_timeout')
653+ if isinstance(test_config.get('boot_timeout'), int) else
654+ snapshot.config.get('timeout'))
655+
656+ # if test is not enabled, skip and return 0 failures
657+ if not test_config.get('enabled', False):
658+ LOG.warn('test config %s is not enabled, skipping', test_name)
659+ return ({}, 0)
660+
661+ # create test instance
662+ component = PlatformComponent(
663+ partial(instances.get_instance, snapshot, user_data,
664+ block=True, start=False, use_desc=test_name))
665+
666+ LOG.info('collecting test data for test: %s', test_name)
667+ with component as instance:
668+ start_call = partial(run_single, 'boot instance', partial(
669+ instance.start, wait=True, wait_time=boot_timeout))
670+ collect_calls = [partial(run_single, 'script {}'.format(script_name),
671+ partial(collect_script, instance,
672+ test_output_dir, script, script_name))
673+ for script_name, script in test_scripts.items()]
674+
675+ res = run_stage('collect for test: {}'.format(test_name),
676+ [start_call] + collect_calls)
677+
678+ return res
679+
680+
681+def collect_snapshot(args, image, os_name):
682+ """
683+ collect data for snapshot of image
684+ args: cmdline arguments
685+ image: instantiated image with set up complete
686+ return_value tuple of results and fail count
687+ """
688+ res = ({}, 1)
689+
690+ component = PlatformComponent(partial(snapshots.get_snapshot, image))
691+
692+ LOG.debug('creating snapshot for %s', os_name)
693+ with component as snapshot:
694+ LOG.info('collecting test data for os: %s', os_name)
695+ res = run_stage(
696+ 'collect test data for {}'.format(os_name),
697+ [partial(collect_test_data, args, snapshot, os_name, test_name)
698+ for test_name in args.test_config])
699+
700+ return res
701+
702+
703+def collect_image(args, platform, os_name):
704+ """
705+ collect data for image
706+ args: cmdline arguments
707+ platform: instantiated platform
708+ os_name: name of distro to collect for
709+ return_value: tuple of results and fail count
710+ """
711+ res = ({}, 1)
712+
713+ os_config = config.load_os_config(os_name)
714+ if not os_config.get('enabled'):
715+ raise ValueError('OS {} not enabled'.format(os_name))
716+
717+ component = PlatformComponent(
718+ partial(images.get_image, platform, os_config))
719+
720+ LOG.info('acquiring image for os: %s', os_name)
721+ with component as image:
722+ res = run_stage('set up and collect data for os: {}'.format(os_name),
723+ [partial(setup_image.setup_image, args, image)] +
724+ [partial(collect_snapshot, args, image, os_name)],
725+ continue_after_error=False)
726+
727+ return res
728+
729+
730+def collect_platform(args, platform_name):
731+ """
732+ collect data for platform
733+ args: cmdline arguments
734+ platform_name: platform to collect for
735+ return_value: tuple of results and fail count
736+ """
737+ res = ({}, 1)
738+
739+ platform_config = config.load_platform_config(platform_name)
740+ if not platform_config.get('enabled'):
741+ raise ValueError('Platform {} not enabled'.format(platform_name))
742+
743+ component = PlatformComponent(
744+ partial(platforms.get_platform, platform_name, platform_config))
745+
746+ LOG.info('setting up platform: %s', platform_name)
747+ with component as platform:
748+ res = run_stage('collect for platform: {}'.format(platform_name),
749+ [partial(collect_image, args, platform, os_name)
750+ for os_name in args.os_name])
751+
752+ return res
753+
754+
755+def collect(args):
756+ """
757+ entry point for collection
758+ args: cmdline arguments
759+ return_value: fail count
760+ """
761+ (res, failed) = run_stage(
762+ 'collect data', [partial(collect_platform, args, platform_name)
763+ for platform_name in args.platform])
764+
765+ LOG.debug('collect stages: %s', res)
766+ if args.result:
767+ util.merge_results({'collect_stages': res}, args.result)
768+
769+ return failed
770diff --git a/tests/cloud_tests/config.py b/tests/cloud_tests/config.py
771new file mode 100644
772index 0000000..d37eb75
773--- /dev/null
774+++ b/tests/cloud_tests/config.py
775@@ -0,0 +1,109 @@
776+import glob
777+import os
778+
779+from cloudinit import util as c_util
780+from tests.cloud_tests import (BASE_DIR, TEST_CONF_DIR)
781+
782+# conf files
783+CONF_EXT = '.yaml'
784+VERIFY_EXT = '.py'
785+PLATFORM_CONF = os.path.join(BASE_DIR, 'platforms.yaml')
786+RELEASES_CONF = os.path.join(BASE_DIR, 'releases.yaml')
787+TESTCASE_CONF = os.path.join(BASE_DIR, 'testcases.yaml')
788+
789+
790+def path_to_name(path):
791+ """
792+ convert abs or rel path to test config to path under configs/
793+ if already a test name, do nothing
794+ """
795+ dir_path, file_name = os.path.split(os.path.normpath(path))
796+ name = os.path.splitext(file_name)[0]
797+ return os.sep.join((os.path.basename(dir_path), name))
798+
799+
800+def name_to_path(name):
801+ """
802+ convert test config path under configs/ to full config path,
803+ if already a full path, do nothing
804+ """
805+ name = os.path.normpath(name)
806+ if not name.endswith(CONF_EXT):
807+ name = name + CONF_EXT
808+ return name if os.path.isabs(name) else os.path.join(TEST_CONF_DIR, name)
809+
810+
811+def name_sanatize(name):
812+ """
813+ sanatize test name to be used as a module name
814+ """
815+ return name.replace('-', '_')
816+
817+
818+def name_to_module(name):
819+ """
820+ convert test name to a loadable module name under testcases/
821+ """
822+ name = name_sanatize(path_to_name(name))
823+ return name.replace(os.path.sep, '.')
824+
825+
826+def merge_config(base, override):
827+ """
828+ merge config and base
829+ """
830+ res = base.copy()
831+ res.update(override)
832+ res.update({k: merge_config(base.get(k, {}), v)
833+ for k, v in override.items() if isinstance(v, dict)})
834+ return res
835+
836+
837+def load_platform_config(platform):
838+ """
839+ load configuration for platform
840+ """
841+ main_conf = c_util.read_conf(PLATFORM_CONF)
842+ return merge_config(main_conf.get('default_platform_config'),
843+ main_conf.get('platforms')[platform])
844+
845+
846+def load_os_config(os_name):
847+ """
848+ load configuration for os
849+ """
850+ main_conf = c_util.read_conf(RELEASES_CONF)
851+ return merge_config(main_conf.get('default_release_config'),
852+ main_conf.get('releases')[os_name])
853+
854+
855+def load_test_config(path):
856+ """
857+ load a test config file by either abs path or rel path
858+ """
859+ return merge_config(c_util.read_conf(TESTCASE_CONF)['base_test_data'],
860+ c_util.read_conf(name_to_path(path)))
861+
862+
863+def list_enabled_platforms():
864+ """
865+ list all platforms enabled for testing
866+ """
867+ platforms = c_util.read_conf(PLATFORM_CONF).get('platforms')
868+ return [k for k, v in platforms.items() if v.get('enabled')]
869+
870+
871+def list_enabled_distros():
872+ """
873+ list all distros enabled for testing
874+ """
875+ releases = c_util.read_conf(RELEASES_CONF).get('releases')
876+ return [k for k, v in releases.items() if v.get('enabled')]
877+
878+
879+def list_test_configs():
880+ """
881+ list all available test config files by abspath
882+ """
883+ return [os.path.abspath(f) for f in
884+ glob.glob(os.sep.join((TEST_CONF_DIR, '*', '*.yaml')))]
885diff --git a/tests/cloud_tests/configs/bugs/README.md b/tests/cloud_tests/configs/bugs/README.md
886new file mode 100644
887index 0000000..269059a
888--- /dev/null
889+++ b/tests/cloud_tests/configs/bugs/README.md
890@@ -0,0 +1,11 @@
891+# Bug Test Configs
892+
893+## purpose
894+Configs that reproduce bugs filed against cloud-init. Having test configs for
895+cloud-init bugs ensures that the fixes do not break in the future, and makes it
896+easy to see how many systems and platforms are effected by a new bug.
897+
898+## structure
899+Should have one test config for most bugs filed. The name of the test should
900+contain ``lp`` followed by the bug number. It may also be useful to add a
901+comment to each bug config with a summary copied from the bug report.
902diff --git a/tests/cloud_tests/configs/bugs/lp1511485.yaml b/tests/cloud_tests/configs/bugs/lp1511485.yaml
903new file mode 100644
904index 0000000..846162f
905--- /dev/null
906+++ b/tests/cloud_tests/configs/bugs/lp1511485.yaml
907@@ -0,0 +1,9 @@
908+#
909+# LP Bug 1511485: final_message is silent on ubuntu-12.04.5 / cloud-init 0.6.3
910+#
911+# 2016-11-17: Disabled as covered by module based tests
912+#
913+enabled: False
914+cloud_config: |
915+ #cloud-config
916+ final_message: "Final message from cloud-config"
917diff --git a/tests/cloud_tests/configs/bugs/lp1611074.yaml b/tests/cloud_tests/configs/bugs/lp1611074.yaml
918new file mode 100644
919index 0000000..94005e8
920--- /dev/null
921+++ b/tests/cloud_tests/configs/bugs/lp1611074.yaml
922@@ -0,0 +1,6 @@
923+#
924+# LP Bug 1611074: Reformatting of ephemeral drive fails on resize of Azure VM
925+#
926+# 2016-11-18: Disabled until test written
927+#
928+enabled: False
929diff --git a/tests/cloud_tests/configs/bugs/lp1628337.yaml b/tests/cloud_tests/configs/bugs/lp1628337.yaml
930new file mode 100644
931index 0000000..251662a
932--- /dev/null
933+++ b/tests/cloud_tests/configs/bugs/lp1628337.yaml
934@@ -0,0 +1,18 @@
935+#
936+# LP Bug 1628337: cloud-init tries to install NTP before even configuring the archives
937+#
938+cloud_config: |
939+ #cloud-config
940+ ntp:
941+ servers: ['ntp.ubuntu.com']
942+ apt:
943+ primary:
944+ - arches: [default]
945+ uri: http://us.archive.ubuntu.com/ubuntu/
946+collect_sciprts:
947+ ntp.conf: |
948+ #!/bin/bash
949+ cat /etc/ntp.conf
950+ sources.list: |
951+ #!/bin/bash
952+ cat /etc/apt/sources.list
953diff --git a/tests/cloud_tests/configs/examples/README.md b/tests/cloud_tests/configs/examples/README.md
954new file mode 100644
955index 0000000..73b8fce
956--- /dev/null
957+++ b/tests/cloud_tests/configs/examples/README.md
958@@ -0,0 +1,10 @@
959+# Example Test Configs
960+
961+## Purpose
962+This folder contains example cloud configs found on
963+[cloudinit.readthedocs.io](https://cloudinit.readthedocs.io/en/latest/topics/examples.html).
964+Examples covered by other tests, like modules, are excluded from tests here
965+to prevent duplication and reduce test time.
966+
967+## Structure
968+One test per example test config on cloudinit.readthedocs.io
969diff --git a/tests/cloud_tests/configs/examples/TODO.md b/tests/cloud_tests/configs/examples/TODO.md
970new file mode 100644
971index 0000000..66404cb
972--- /dev/null
973+++ b/tests/cloud_tests/configs/examples/TODO.md
974@@ -0,0 +1,13 @@
975+# Missing Examples
976+
977+Below lists each of the issing examples and why it is not currently added.
978+
979+ - Chef (takes > 60 seconds to run)
980+ - Puppet (takes > 60 seconds to run)
981+ - Manage resolve.conf (lxd backend overrides changes)
982+ - Adding a yum repository (need centos system)
983+ - Register RedHat Subscription (need centos system + subscription)
984+ - Adjust mount points mounted (need multiple disks)
985+ - Call a url when finished (need end point)
986+ - Reboot/poweroff when finished (how to test)
987+ - Disk setup (need multiple disks)
988diff --git a/tests/cloud_tests/configs/examples/add_apt_repositories.yaml b/tests/cloud_tests/configs/examples/add_apt_repositories.yaml
989new file mode 100644
990index 0000000..91ca1a6
991--- /dev/null
992+++ b/tests/cloud_tests/configs/examples/add_apt_repositories.yaml
993@@ -0,0 +1,19 @@
994+#
995+# From cloud config examples on cloudinit.readthedocs.io
996+#
997+# 2016-11-17: Disabled as covered by module based tests
998+#
999+enabled: False
1000+cloud_config: |
1001+ #cloud-config
1002+ apt:
1003+ primary:
1004+ - arches: [default]
1005+ uri: "http://www.gtlib.gatech.edu/pub/ubuntu-releases/"
1006+collect_scripts:
1007+ ubuntu.sources.list: |
1008+ #!/bin/bash
1009+ cat /etc/apt/sources.list | grep -v '^#' | sed '/^\s*$/d' | grep archive.ubuntu.com | wc -l
1010+ gatech.sources.list: |
1011+ #!/bin/bash
1012+ cat /etc/apt/sources.list | grep -v '^#' | sed '/^\s*$/d' | grep gtlib.gatech.edu | wc -l
1013diff --git a/tests/cloud_tests/configs/examples/alter_completion_message.yaml b/tests/cloud_tests/configs/examples/alter_completion_message.yaml
1014new file mode 100644
1015index 0000000..a5aa44b
1016--- /dev/null
1017+++ b/tests/cloud_tests/configs/examples/alter_completion_message.yaml
1018@@ -0,0 +1,14 @@
1019+#
1020+# From cloud config examples on cloudinit.readthedocs.io
1021+#
1022+# 2016-11-17: Disabled as covered by module based tests
1023+#
1024+enabled: False
1025+cloud_config: |
1026+ #cloud-config
1027+ final_message: |
1028+ This is my final message!
1029+ $version
1030+ $timestamp
1031+ $datasource
1032+ $uptime
1033diff --git a/tests/cloud_tests/configs/examples/configure_instance_trusted_ca_certificates.yaml b/tests/cloud_tests/configs/examples/configure_instance_trusted_ca_certificates.yaml
1034new file mode 100644
1035index 0000000..d0f7794
1036--- /dev/null
1037+++ b/tests/cloud_tests/configs/examples/configure_instance_trusted_ca_certificates.yaml
1038@@ -0,0 +1,39 @@
1039+#
1040+# From cloud config examples on cloudinit.readthedocs.io
1041+#
1042+# 2016-11-17: Disabled as covered by module based tests
1043+#
1044+enabled: False
1045+cloud_config: |
1046+ #cloud-config
1047+ ca-certs:
1048+ # If present and set to True, the 'remove-defaults' parameter will remove
1049+ # all the default trusted CA certificates that are normally shipped with
1050+ # Ubuntu.
1051+ # This is mainly for paranoid admins - most users will not need this
1052+ # functionality.
1053+ remove-defaults: true
1054+
1055+ # If present, the 'trusted' parameter should contain a certificate (or list
1056+ # of certificates) to add to the system as trusted CA certificates.
1057+ # Pay close attention to the YAML multiline list syntax. The example shown
1058+ # here is for a list of multiline certificates.
1059+ trusted:
1060+ - |
1061+ -----BEGIN CERTIFICATE-----
1062+ YOUR-ORGS-TRUSTED-CA-CERT-HERE
1063+ -----END CERTIFICATE-----
1064+ - |
1065+ -----BEGIN CERTIFICATE-----
1066+ YOUR-ORGS-TRUSTED-CA-CERT-HERE
1067+ -----END CERTIFICATE-----
1068+collect_scripts:
1069+ cloudinit_certs: |
1070+ #!/bin/bash
1071+ cat /etc/ssl/certs/cloud-init-ca-certs.pem
1072+ cert_count_ca: |
1073+ #!/bin/bash
1074+ wc -l /etc/ssl/certs/ca-certificates.crt
1075+ cert_count_cloudinit: |
1076+ #!/bin/bash
1077+ wc -l /etc/ssl/certs/cloud-init-ca-certs.pem
1078diff --git a/tests/cloud_tests/configs/examples/configure_instances_ssh_keys.yaml b/tests/cloud_tests/configs/examples/configure_instances_ssh_keys.yaml
1079new file mode 100644
1080index 0000000..c2f03be
1081--- /dev/null
1082+++ b/tests/cloud_tests/configs/examples/configure_instances_ssh_keys.yaml
1083@@ -0,0 +1,61 @@
1084+#
1085+# From cloud config examples on cloudinit.readthedocs.io
1086+#
1087+# 2016-11-17: Disabled as covered by module based tests
1088+#
1089+enabled: False
1090+cloud_config: |
1091+ #cloud-config
1092+ ssh_authorized_keys:
1093+ - ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAGEA3FSyQwBI6Z+nCSjUUk8EEAnnkhXlukKoUPND/RRClWz2s5TCzIkd3Ou5+Cyz71X0XmazM3l5WgeErvtIwQMyT1KjNoMhoJMrJnWqQPOt5Q8zWd9qG7PBl9+eiH5qV7NZ mykey@host
1094+ - ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA3I7VUf2l5gSn5uavROsc5HRDpZdQueUq5ozemNSj8T7enqKHOEaFoU2VoPgGEWC9RyzSQVeyD6s7APMcE82EtmW4skVEgEGSbDc1pvxzxtchBj78hJP6Cf5TCMFSXw+Fz5rF1dR23QDbN1mkHs7adr8GW4kSWqU7Q7NDwfIrJJtO7Hi42GyXtvEONHbiRPOe8stqUly7MvUoN+5kfjBM8Qqpfl2+FNhTYWpMfYdPUnE7u536WqzFmsaqJctz3gBxH9Ex7dFtrxR4qiqEr9Qtlu3xGn7Bw07/+i1D+ey3ONkZLN+LQ714cgj8fRS4Hj29SCmXp5Kt5/82cD/VN3NtHw== smoser@brickies
1095+
1096+ # Send pre-generated ssh private keys to the server
1097+ # If these are present, they will be written to /etc/ssh and
1098+ # new random keys will not be generated
1099+ # in addition to 'rsa' and 'dsa' as shown below, 'ecdsa' is also supported
1100+ ssh_keys:
1101+ rsa_private: |
1102+ -----BEGIN RSA PRIVATE KEY-----
1103+ MIIBxwIBAAJhAKD0YSHy73nUgysO13XsJmd4fHiFyQ+00R7VVu2iV9Qcon2LZS/x
1104+ 1cydPZ4pQpfjEha6WxZ6o8ci/Ea/w0n+0HGPwaxlEG2Z9inNtj3pgFrYcRztfECb
1105+ 1j6HCibZbAzYtwIBIwJgO8h72WjcmvcpZ8OvHSvTwAguO2TkR6mPgHsgSaKy6GJo
1106+ PUJnaZRWuba/HX0KGyhz19nPzLpzG5f0fYahlMJAyc13FV7K6kMBPXTRR6FxgHEg
1107+ L0MPC7cdqAwOVNcPY6A7AjEA1bNaIjOzFN2sfZX0j7OMhQuc4zP7r80zaGc5oy6W
1108+ p58hRAncFKEvnEq2CeL3vtuZAjEAwNBHpbNsBYTRPCHM7rZuG/iBtwp8Rxhc9I5w
1109+ ixvzMgi+HpGLWzUIBS+P/XhekIjPAjA285rVmEP+DR255Ls65QbgYhJmTzIXQ2T9
1110+ luLvcmFBC6l35Uc4gTgg4ALsmXLn71MCMGMpSWspEvuGInayTCL+vEjmNBT+FAdO
1111+ W7D4zCpI43jRS9U06JVOeSc9CDk2lwiA3wIwCTB/6uc8Cq85D9YqpM10FuHjKpnP
1112+ REPPOyrAspdeOAV+6VKRavstea7+2DZmSUgE
1113+ -----END RSA PRIVATE KEY-----
1114+
1115+ rsa_public: ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAGEAoPRhIfLvedSDKw7XdewmZ3h8eIXJD7TRHtVW7aJX1ByifYtlL/HVzJ09nilCl+MSFrpbFnqjxyL8Rr/DSf7QcY/BrGUQbZn2Kc22PemAWthxHO18QJvWPocKJtlsDNi3 smoser@localhost
1116+
1117+ dsa_private: |
1118+ -----BEGIN DSA PRIVATE KEY-----
1119+ MIIBuwIBAAKBgQDP2HLu7pTExL89USyM0264RCyWX/CMLmukxX0Jdbm29ax8FBJT
1120+ pLrO8TIXVY5rPAJm1dTHnpuyJhOvU9G7M8tPUABtzSJh4GVSHlwaCfycwcpLv9TX
1121+ DgWIpSj+6EiHCyaRlB1/CBp9RiaB+10QcFbm+lapuET+/Au6vSDp9IRtlQIVAIMR
1122+ 8KucvUYbOEI+yv+5LW9u3z/BAoGBAI0q6JP+JvJmwZFaeCMMVxXUbqiSko/P1lsa
1123+ LNNBHZ5/8MOUIm8rB2FC6ziidfueJpqTMqeQmSAlEBCwnwreUnGfRrKoJpyPNENY
1124+ d15MG6N5J+z81sEcHFeprryZ+D3Ge9VjPq3Tf3NhKKwCDQ0240aPezbnjPeFm4mH
1125+ bYxxcZ9GAoGAXmLIFSQgiAPu459rCKxT46tHJtM0QfnNiEnQLbFluefZ/yiI4DI3
1126+ 8UzTCOXLhUA7ybmZha+D/csj15Y9/BNFuO7unzVhikCQV9DTeXX46pG4s1o23JKC
1127+ /QaYWNMZ7kTRv+wWow9MhGiVdML4ZN4XnifuO5krqAybngIy66PMEoQCFEIsKKWv
1128+ 99iziAH0KBMVbxy03Trz
1129+ -----END DSA PRIVATE KEY-----
1130+
1131+ dsa_public: ssh-dsa AAAAB3NzaC1kc3MAAACBAM/Ycu7ulMTEvz1RLIzTbrhELJZf8Iwua6TFfQl1ubb1rHwUElOkus7xMhdVjms8AmbV1Meem7ImE69T0bszy09QAG3NImHgZVIeXBoJ/JzByku/1NcOBYilKP7oSIcLJpGUHX8IGn1GJoH7XRBwVub6Vqm4RP78C7q9IOn0hG2VAAAAFQCDEfCrnL1GGzhCPsr/uS1vbt8/wQAAAIEAjSrok/4m8mbBkVp4IwxXFdRuqJKSj8/WWxos00Ednn/ww5QibysHYULrOKJ1+54mmpMyp5CZICUQELCfCt5ScZ9GsqgmnI80Q1h3Xkwbo3kn7PzWwRwcV6muvJn4PcZ71WM+rdN/c2EorAINDTbjRo97NueM94WbiYdtjHFxn0YAAACAXmLIFSQgiAPu459rCKxT46tHJtM0QfnNiEnQLbFluefZ/yiI4DI38UzTCOXLhUA7ybmZha+D/csj15Y9/BNFuO7unzVhikCQV9DTeXX46pG4s1o23JKC/QaYWNMZ7kTRv+wWow9MhGiVdML4ZN4XnifuO5krqAybngIy66PMEoQ= smoser@localhost
1132+collect_scripts:
1133+ cert_count: |
1134+ #!/bin/bash
1135+ ls | wc -l
1136+ dsa_public: |
1137+ #!/bin/bash
1138+ cat /etc/ssh/ssh_host_dsa_key.pub
1139+ rsa_public: |
1140+ #!/bin/bash
1141+ cat /etc/ssh/ssh_host_rsa_key.pub
1142+ auth_keys: |
1143+ #!/bin/bash
1144+ cat /home/ubuntu/.ssh/authorized_keys
1145diff --git a/tests/cloud_tests/configs/examples/including_user_groups.yaml b/tests/cloud_tests/configs/examples/including_user_groups.yaml
1146new file mode 100644
1147index 0000000..67e4fea
1148--- /dev/null
1149+++ b/tests/cloud_tests/configs/examples/including_user_groups.yaml
1150@@ -0,0 +1,51 @@
1151+#
1152+# From cloud config examples on cloudinit.readthedocs.io
1153+#
1154+# 2016-11-17: Disabled as covered by module based tests
1155+#
1156+enabled: False
1157+cloud_config: |
1158+ #cloud-config
1159+ # Add groups to the system
1160+ groups:
1161+ - secret: [foobar,barfoo]
1162+ - cloud-users
1163+
1164+ # Add users to the system. Users are added after groups are added.
1165+ users:
1166+ - default
1167+ - name: foobar
1168+ gecos: Foo B. Bar
1169+ primary-group: foobar
1170+ groups: users
1171+ expiredate: 2038-01-19
1172+ lock_passwd: false
1173+ passwd: $6$j212wezy$7H/1LT4f9/N3wpgNunhsIqtMj62OKiS3nyNwuizouQc3u7MbYCarYeAHWYPYb2FT.lbioDm2RrkJPb9BZMN1O/
1174+ - name: barfoo
1175+ gecos: Bar B. Foo
1176+ sudo: ALL=(ALL) NOPASSWD:ALL
1177+ groups: cloud-users
1178+ lock_passwd: true
1179+ - name: cloudy
1180+ gecos: Magic Cloud App Daemon User
1181+ inactive: true
1182+ system: true
1183+collect_scripts:
1184+ group_ubuntu: |
1185+ #!/bin/bash
1186+ getent group ubuntu
1187+ group_cloud_users: |
1188+ #!/bin/bash
1189+ getent group cloud-users
1190+ user_ubuntu: |
1191+ #!/bin/bash
1192+ getent passwd ubuntu
1193+ user_foobar: |
1194+ #!/bin/bash
1195+ getent passwd foobar
1196+ user_barfoo: |
1197+ #!/bin/bash
1198+ getent passwd barfoo
1199+ user_cloudy: |
1200+ #!/bin/bash
1201+ getent passwd cloudy
1202diff --git a/tests/cloud_tests/configs/examples/install_arbitrary_packages.yaml b/tests/cloud_tests/configs/examples/install_arbitrary_packages.yaml
1203new file mode 100644
1204index 0000000..65ed649
1205--- /dev/null
1206+++ b/tests/cloud_tests/configs/examples/install_arbitrary_packages.yaml
1207@@ -0,0 +1,18 @@
1208+#
1209+# From cloud config examples on cloudinit.readthedocs.io
1210+#
1211+# 2016-11-17: Disabled as covered by module based tests
1212+#
1213+enabled: False
1214+cloud_config: |
1215+ #cloud-config
1216+ packages:
1217+ - htop
1218+ - tree
1219+collect_scripts:
1220+ htop: |
1221+ #!/bin/bash
1222+ dpkg -l | grep htop | wc -l
1223+ tree: |
1224+ #!/bin/bash
1225+ dpkg -l | grep tree | wc -l
1226diff --git a/tests/cloud_tests/configs/examples/install_run_chef_recipes.yaml b/tests/cloud_tests/configs/examples/install_run_chef_recipes.yaml
1227new file mode 100644
1228index 0000000..7b1622b
1229--- /dev/null
1230+++ b/tests/cloud_tests/configs/examples/install_run_chef_recipes.yaml
1231@@ -0,0 +1,92 @@
1232+#
1233+# From cloud config examples on cloudinit.readthedocs.io
1234+#
1235+# 2016-11-17: Disabled as test suite fails this long running test currently
1236+#
1237+enabled: False
1238+cloud_config: |
1239+ #cloud-config
1240+ # Key from http://apt.opscode.com/packages@opscode.com.gpg.key
1241+ apt:
1242+ sources:
1243+ - source: "deb http://apt.opscode.com/ $RELEASE-0.10 main"
1244+ key: |
1245+ -----BEGIN PGP PUBLIC KEY BLOCK-----
1246+ Version: GnuPG v1.4.9 (GNU/Linux)
1247+
1248+ mQGiBEppC7QRBADfsOkZU6KZK+YmKw4wev5mjKJEkVGlus+NxW8wItX5sGa6kdUu
1249+ twAyj7Yr92rF+ICFEP3gGU6+lGo0Nve7KxkN/1W7/m3G4zuk+ccIKmjp8KS3qn99
1250+ dxy64vcji9jIllVa+XXOGIp0G8GEaj7mbkixL/bMeGfdMlv8Gf2XPpp9vwCgn/GC
1251+ JKacfnw7MpLKUHOYSlb//JsEAJqao3ViNfav83jJKEkD8cf59Y8xKia5OpZqTK5W
1252+ ShVnNWS3U5IVQk10ZDH97Qn/YrK387H4CyhLE9mxPXs/ul18ioiaars/q2MEKU2I
1253+ XKfV21eMLO9LYd6Ny/Kqj8o5WQK2J6+NAhSwvthZcIEphcFignIuobP+B5wNFQpe
1254+ DbKfA/0WvN2OwFeWRcmmd3Hz7nHTpcnSF+4QX6yHRF/5BgxkG6IqBIACQbzPn6Hm
1255+ sMtm/SVf11izmDqSsQptCrOZILfLX/mE+YOl+CwWSHhl+YsFts1WOuh1EhQD26aO
1256+ Z84HuHV5HFRWjDLw9LriltBVQcXbpfSrRP5bdr7Wh8vhqJTPjrQnT3BzY29kZSBQ
1257+ YWNrYWdlcyA8cGFja2FnZXNAb3BzY29kZS5jb20+iGAEExECACAFAkppC7QCGwMG
1258+ CwkIBwMCBBUCCAMEFgIDAQIeAQIXgAAKCRApQKupg++Caj8sAKCOXmdG36gWji/K
1259+ +o+XtBfvdMnFYQCfTCEWxRy2BnzLoBBFCjDSK6sJqCu5Ag0ESmkLtBAIAIO2SwlR
1260+ lU5i6gTOp42RHWW7/pmW78CwUqJnYqnXROrt3h9F9xrsGkH0Fh1FRtsnncgzIhvh
1261+ DLQnRHnkXm0ws0jV0PF74ttoUT6BLAUsFi2SPP1zYNJ9H9fhhK/pjijtAcQwdgxu
1262+ wwNJ5xCEscBZCjhSRXm0d30bK1o49Cow8ZIbHtnXVP41c9QWOzX/LaGZsKQZnaMx
1263+ EzDk8dyyctR2f03vRSVyTFGgdpUcpbr9eTFVgikCa6ODEBv+0BnCH6yGTXwBid9g
1264+ w0o1e/2DviKUWCC+AlAUOubLmOIGFBuI4UR+rux9affbHcLIOTiKQXv79lW3P7W8
1265+ AAfniSQKfPWXrrcAAwUH/2XBqD4Uxhbs25HDUUiM/m6Gnlj6EsStg8n0nMggLhuN
1266+ QmPfoNByMPUqvA7sULyfr6xCYzbzRNxABHSpf85FzGQ29RF4xsA4vOOU8RDIYQ9X
1267+ Q8NqqR6pydprRFqWe47hsAN7BoYuhWqTtOLSBmnAnzTR5pURoqcquWYiiEavZixJ
1268+ 3ZRAq/HMGioJEtMFrvsZjGXuzef7f0ytfR1zYeLVWnL9Bd32CueBlI7dhYwkFe+V
1269+ Ep5jWOCj02C1wHcwt+uIRDJV6TdtbIiBYAdOMPk15+VBdweBXwMuYXr76+A7VeDL
1270+ zIhi7tKFo6WiwjKZq0dzctsJJjtIfr4K4vbiD9Ojg1iISQQYEQIACQUCSmkLtAIb
1271+ DAAKCRApQKupg++CauISAJ9CxYPOKhOxalBnVTLeNUkAHGg2gACeIsbobtaD4ZHG
1272+ 0GLl8EkfA8uhluM=
1273+ =zKAm
1274+ -----END PGP PUBLIC KEY BLOCK-----
1275+
1276+ chef:
1277+
1278+ # Valid values are 'gems' and 'packages' and 'omnibus'
1279+ install_type: "packages"
1280+
1281+ # Boolean: run 'install_type' code even if chef-client
1282+ # appears already installed.
1283+ force_install: false
1284+
1285+ # Chef settings
1286+ server_url: "https://chef.yourorg.com:4000"
1287+
1288+ # Node Name
1289+ # Defaults to the instance-id if not present
1290+ node_name: "your-node-name"
1291+
1292+ # Environment
1293+ # Defaults to '_default' if not present
1294+ environment: "production"
1295+
1296+ # Default validation name is chef-validator
1297+ validation_name: "yourorg-validator"
1298+ # if validation_cert's value is "system" then it is expected
1299+ # that the file already exists on the system.
1300+ validation_cert: |
1301+ -----BEGIN RSA PRIVATE KEY-----
1302+ YOUR-ORGS-VALIDATION-KEY-HERE
1303+ -----END RSA PRIVATE KEY-----
1304+
1305+ # A run list for a first boot json
1306+ run_list:
1307+ - "recipe[apache2]"
1308+ - "role[db]"
1309+
1310+ # Specify a list of initial attributes used by the cookbooks
1311+ initial_attributes:
1312+ apache:
1313+ prefork:
1314+ maxclients: 100
1315+ keepalive: "off"
1316+
1317+ # if install_type is 'omnibus', change the url to download
1318+ omnibus_url: "https://www.opscode.com/chef/install.sh"
1319+
1320+
1321+ # Capture all subprocess output into a logfile
1322+ # Useful for troubleshooting cloud-init issues
1323+ output: {all: '| tee -a /var/log/cloud-init-output.log'}
1324diff --git a/tests/cloud_tests/configs/examples/run_apt_upgrade.yaml b/tests/cloud_tests/configs/examples/run_apt_upgrade.yaml
1325new file mode 100644
1326index 0000000..b7aa53f
1327--- /dev/null
1328+++ b/tests/cloud_tests/configs/examples/run_apt_upgrade.yaml
1329@@ -0,0 +1,9 @@
1330+#
1331+# From cloud config examples on cloudinit.readthedocs.io
1332+#
1333+# 2016-11-17: Disabled as covered by module based tests
1334+#
1335+enabled: False
1336+cloud_config: |
1337+ #cloud-config
1338+ package_upgrade: true
1339diff --git a/tests/cloud_tests/configs/examples/run_commands.yaml b/tests/cloud_tests/configs/examples/run_commands.yaml
1340new file mode 100644
1341index 0000000..7305048
1342--- /dev/null
1343+++ b/tests/cloud_tests/configs/examples/run_commands.yaml
1344@@ -0,0 +1,14 @@
1345+#
1346+# From cloud config examples on cloudinit.readthedocs.io
1347+#
1348+# 2016-11-17: Disabled as covered by module based tests
1349+#
1350+enabled: False
1351+cloud_config: |
1352+ #cloud-config
1353+ runcmd:
1354+ - echo cloud-init run cmd test > /tmp/run_cmd
1355+collect_scripts:
1356+ run_cmd: |
1357+ #!/bin/bash
1358+ cat /tmp/run_cmd
1359diff --git a/tests/cloud_tests/configs/examples/run_commands_first_boot.yaml b/tests/cloud_tests/configs/examples/run_commands_first_boot.yaml
1360new file mode 100644
1361index 0000000..8a8467e
1362--- /dev/null
1363+++ b/tests/cloud_tests/configs/examples/run_commands_first_boot.yaml
1364@@ -0,0 +1,14 @@
1365+#
1366+# From cloud config examples on cloudinit.readthedocs.io
1367+#
1368+# 2016-11-17: Disabled as covered by module based tests
1369+#
1370+enabled: False
1371+cloud_config: |
1372+ #cloud-config
1373+ bootcmd:
1374+ - echo 192.168.1.130 us.archive.ubuntu.com > /etc/hosts
1375+collect_scripts:
1376+ hosts: |
1377+ #!/bin/bash
1378+ cat /etc/hosts
1379diff --git a/tests/cloud_tests/configs/examples/setup_run_puppet.yaml b/tests/cloud_tests/configs/examples/setup_run_puppet.yaml
1380new file mode 100644
1381index 0000000..abd2177
1382--- /dev/null
1383+++ b/tests/cloud_tests/configs/examples/setup_run_puppet.yaml
1384@@ -0,0 +1,53 @@
1385+#
1386+# From cloud config examples on cloudinit.readthedocs.io
1387+#
1388+# 2016-11-17: Disabled as test suite fails this long running test currently
1389+#
1390+enabled: False
1391+cloud_config: |
1392+ #cloud-config
1393+ puppet:
1394+ # Every key present in the conf object will be added to puppet.conf:
1395+ # [name]
1396+ # subkey=value
1397+ #
1398+ # For example the configuration below will have the following section
1399+ # added to puppet.conf:
1400+ # [puppetd]
1401+ # server=puppetmaster.example.org
1402+ # certname=i-0123456.ip-X-Y-Z.cloud.internal
1403+ #
1404+ # The puppmaster ca certificate will be available in
1405+ # /var/lib/puppet/ssl/certs/ca.pem
1406+ conf:
1407+ agent:
1408+ server: "puppetmaster.example.org"
1409+ # certname supports substitutions at runtime:
1410+ # %i: instanceid
1411+ # Example: i-0123456
1412+ # %f: fqdn of the machine
1413+ # Example: ip-X-Y-Z.cloud.internal
1414+ #
1415+ # NB: the certname will automatically be lowercased as required by puppet
1416+ certname: "%i.%f"
1417+ # ca_cert is a special case. It won't be added to puppet.conf.
1418+ # It holds the puppetmaster certificate in pem format.
1419+ # It should be a multi-line string (using the | yaml notation for
1420+ # multi-line strings).
1421+ # The puppetmaster certificate is located in
1422+ # /var/lib/puppet/ssl/ca/ca_crt.pem on the puppetmaster host.
1423+ #
1424+ ca_cert: |
1425+ -----BEGIN CERTIFICATE-----
1426+ MIICCTCCAXKgAwIBAgIBATANBgkqhkiG9w0BAQUFADANMQswCQYDVQQDDAJjYTAe
1427+ Fw0xMDAyMTUxNzI5MjFaFw0xNTAyMTQxNzI5MjFaMA0xCzAJBgNVBAMMAmNhMIGf
1428+ MA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCu7Q40sm47/E1Pf+r8AYb/V/FWGPgc
1429+ b014OmNoX7dgCxTDvps/h8Vw555PdAFsW5+QhsGr31IJNI3kSYprFQcYf7A8tNWu
1430+ 1MASW2CfaEiOEi9F1R3R4Qlz4ix+iNoHiUDTjazw/tZwEdxaQXQVLwgTGRwVa+aA
1431+ qbutJKi93MILLwIDAQABo3kwdzA4BglghkgBhvhCAQ0EKxYpUHVwcGV0IFJ1Ynkv
1432+ T3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwDwYDVR0TAQH/BAUwAwEB/zAd
1433+ BgNVHQ4EFgQUu4+jHB+GYE5Vxo+ol1OAhevspjAwCwYDVR0PBAQDAgEGMA0GCSqG
1434+ SIb3DQEBBQUAA4GBAH/rxlUIjwNb3n7TXJcDJ6MMHUlwjr03BDJXKb34Ulndkpaf
1435+ +GAlzPXWa7bO908M9I8RnPfvtKnteLbvgTK+h+zX1XCty+S2EQWk29i2AdoqOTxb
1436+ hppiGMp0tT5Havu4aceCXiy2crVcudj3NFciy8X66SoECemW9UYDCb9T5D0d
1437+ -----END CERTIFICATE-----
1438diff --git a/tests/cloud_tests/configs/examples/writing_out_arbitrary_files.yaml b/tests/cloud_tests/configs/examples/writing_out_arbitrary_files.yaml
1439new file mode 100644
1440index 0000000..37c5fb8
1441--- /dev/null
1442+++ b/tests/cloud_tests/configs/examples/writing_out_arbitrary_files.yaml
1443@@ -0,0 +1,43 @@
1444+#
1445+# From cloud config examples on cloudinit.readthedocs.io
1446+#
1447+# 2016-11-17: Disabled as covered by module based tests
1448+#
1449+enabled: False
1450+cloud_config: |
1451+ #cloud-config
1452+ write_files:
1453+ - encoding: b64
1454+ content: CiMgVGhpcyBmaWxlIGNvbnRyb2xzIHRoZSBzdGF0ZSBvZiBTRUxpbnV4
1455+ owner: root:root
1456+ path: /root/file_b64
1457+ permissions: '0644'
1458+ - content: |
1459+ # My new /root/file_text
1460+
1461+ SMBDOPTIONS="-D"
1462+ path: /root/file_text
1463+ - content: !!binary |
1464+ f0VMRgIBAQAAAAAAAAAAAAIAPgABAAAAwARAAAAAAABAAAAAAAAAAJAVAAAAAAAAAAAAAEAAOAAI
1465+ AEAAHgAdAAYAAAAFAAAAQAAAAAAAAABAAEAAAAAAAEAAQAAAAAAAwAEAAAAAAADAAQAAAAAAAAgA
1466+ AAAAAAAAAwAAAAQAAAAAAgAAAAAAAAACQAAAAAAAAAJAAAAAAAAcAAAAAAAAABwAAAAAAAAAAQAA
1467+ path: /root/file_binary
1468+ permissions: '0555'
1469+ - encoding: gzip
1470+ content: !!binary |
1471+ H4sIAIDb/U8C/1NW1E/KzNMvzuBKTc7IV8hIzcnJVyjPL8pJ4QIA6N+MVxsAAAA=
1472+ path: /root/file_gzip
1473+ permissions: '0755'
1474+collect_scripts:
1475+ file_b64: |
1476+ #!/bin/bash
1477+ file /root/file_b64
1478+ file_text: |
1479+ #!/bin/bash
1480+ file /root/file_text
1481+ file_binary: |
1482+ #!/bin/bash
1483+ file /root/file_binary
1484+ file_gzip: |
1485+ #!/bin/bash
1486+ file /root/file_gzip
1487diff --git a/tests/cloud_tests/configs/main/README.md b/tests/cloud_tests/configs/main/README.md
1488new file mode 100644
1489index 0000000..1418e7f
1490--- /dev/null
1491+++ b/tests/cloud_tests/configs/main/README.md
1492@@ -0,0 +1,9 @@
1493+# Main Functionality Test Configs
1494+
1495+## purpose
1496+Test main features and config options of cloud-init such as logging, output
1497+redirection, early init and integration with init system
1498+
1499+## structure
1500+Should have one or more test configs for all main cloud-init output and logging
1501+options, and basic functionality test cases
1502diff --git a/tests/cloud_tests/configs/main/command_output_simple.yaml b/tests/cloud_tests/configs/main/command_output_simple.yaml
1503new file mode 100644
1504index 0000000..e418ac4
1505--- /dev/null
1506+++ b/tests/cloud_tests/configs/main/command_output_simple.yaml
1507@@ -0,0 +1,11 @@
1508+#
1509+# Test functionality of simple output redirection
1510+#
1511+cloud_config: |
1512+ #cloud-config
1513+ output: { all: "| tee -a /var/log/cloud-init-test-output" }
1514+ final_message: "should be last line in cloud-init-test-output file"
1515+collect_scripts:
1516+ cloud-init-test-output: |
1517+ #!/bin/bash
1518+ cat /var/log/cloud-init-test-output
1519diff --git a/tests/cloud_tests/configs/modules/README.md b/tests/cloud_tests/configs/modules/README.md
1520new file mode 100644
1521index 0000000..91d2964
1522--- /dev/null
1523+++ b/tests/cloud_tests/configs/modules/README.md
1524@@ -0,0 +1,10 @@
1525+# Module Test Configs
1526+
1527+## Purpose
1528+Test functionality of cloud config modules. See
1529+[here](https://cloudinit.readthedocs.io/en/latest/topics/modules.html) for
1530+a full list.
1531+
1532+## Structure
1533+Should have one or more test configs for each module in cloudinit/config/. The
1534+name of the test should indicate which module the config is verifying.
1535diff --git a/tests/cloud_tests/configs/modules/TODO.md b/tests/cloud_tests/configs/modules/TODO.md
1536new file mode 100644
1537index 0000000..8a9e162
1538--- /dev/null
1539+++ b/tests/cloud_tests/configs/modules/TODO.md
1540@@ -0,0 +1,98 @@
1541+# TODO
1542+
1543+The following lists complete or partially misisng modules. If a module is
1544+listed with nothing below it indicates that no work is completed on that
1545+module. If there is a list below the module name that is the remainig
1546+identified work.
1547+
1548+## apt_configure
1549+
1550+ * apt_get_wrapper
1551+ * What does this do? How to use it?
1552+ * apt_get_command
1553+ * To specify a different 'apt-get' command, set 'apt_get_command'.
1554+ This must be a list, and the subcommand (update, upgrade) is appended to it.
1555+ * Modify default and verify the options got passed correctly.
1556+ * preserve sources
1557+ * TBD
1558+
1559+## chef
1560+2016-11-17: Tests took > 60 seconds and test framework times out currently.
1561+
1562+## disable EC2 metadata
1563+
1564+## disk setup
1565+
1566+## emit upstart
1567+
1568+## fan
1569+
1570+## growpart
1571+
1572+## grub dpkg
1573+
1574+## landscape
1575+2016-11-17: Module is not working
1576+
1577+## lxd
1578+2016-11-17: Need a zfs backed test written
1579+
1580+## mcollective
1581+
1582+## migrator
1583+
1584+## mounts
1585+
1586+## phone home
1587+
1588+## power state change
1589+
1590+## puppet
1591+2016-11-17: Tests took > 60 seconds and test framework times out currently.
1592+
1593+## resizefs
1594+
1595+## resolv conf
1596+2016-11-17: Issues with changing resolv.conf and lxc backend.
1597+
1598+## redhat subscription
1599+2016-11-17: Need RH support in test framework.
1600+
1601+## rightscale userdata
1602+2016-11-17: Specific to RightScale cloud enviornment.
1603+
1604+## rsyslog
1605+
1606+## scripts per boot
1607+Not applicable to write a test for this as it specifies when something should be run.
1608+
1609+## scripts per instance
1610+Not applicable to write a test for this as it specifies when something should be run.
1611+
1612+## scripts per once
1613+Not applicable to write a test for this as it specifies when something should be run.
1614+
1615+## scripts user
1616+Not applicable to write a test for this as it specifies when something should be run.
1617+
1618+## scripts vendor
1619+Not applicable to write a test for this as it specifies when something should be run.
1620+
1621+## snappy
1622+2016-11-17: Need test to install snaps from store
1623+
1624+## snap-config
1625+2016-11-17: Need to investigate
1626+
1627+## spacewalk
1628+
1629+## ssh authkey fingerprints
1630+The authkey_hash key does not appear to work. In fact the default claims to be md5, however syslog only shows sha256
1631+
1632+## ubuntu init switch
1633+
1634+## update etc hosts
1635+2016-11-17: Issues with changing /etc/hosts and lxc backend.
1636+
1637+## yum add repo
1638+2016-11-17: Need RH support in test framework.
1639diff --git a/tests/cloud_tests/configs/modules/apt_configure_conf.yaml b/tests/cloud_tests/configs/modules/apt_configure_conf.yaml
1640new file mode 100644
1641index 0000000..222c24b
1642--- /dev/null
1643+++ b/tests/cloud_tests/configs/modules/apt_configure_conf.yaml
1644@@ -0,0 +1,17 @@
1645+#
1646+# Provide a configuration for APT
1647+#
1648+cloud_config: |
1649+ #cloud-config
1650+ apt:
1651+ conf: |
1652+ APT {
1653+ Get {
1654+ Assume-Yes "true";
1655+ Fix-Broken "true";
1656+ }
1657+ }
1658+collect_scripts:
1659+ 94cloud-init-config: |
1660+ #!/bin/bash
1661+ cat /etc/apt/apt.conf.d/94cloud-init-config
1662diff --git a/tests/cloud_tests/configs/modules/apt_configure_disable_suites.yaml b/tests/cloud_tests/configs/modules/apt_configure_disable_suites.yaml
1663new file mode 100644
1664index 0000000..51ce43e
1665--- /dev/null
1666+++ b/tests/cloud_tests/configs/modules/apt_configure_disable_suites.yaml
1667@@ -0,0 +1,15 @@
1668+#
1669+# Disables everything in sources.list
1670+#
1671+cloud_config: |
1672+ #cloud-config
1673+ apt:
1674+ disable_suites:
1675+ - $RELEASE
1676+ - $RELEASE-updates
1677+ - $RELEASE-backports
1678+ - $RELEASE-security
1679+collect_scripts:
1680+ sources.list: |
1681+ #!/bin/bash
1682+ grep -v '^#' /etc/apt/sources.list | sed '/^\s*$/d'
1683diff --git a/tests/cloud_tests/configs/modules/apt_configure_primary.yaml b/tests/cloud_tests/configs/modules/apt_configure_primary.yaml
1684new file mode 100644
1685index 0000000..ff7cb70
1686--- /dev/null
1687+++ b/tests/cloud_tests/configs/modules/apt_configure_primary.yaml
1688@@ -0,0 +1,17 @@
1689+#
1690+# Setup a custome primary sources.list
1691+#
1692+cloud_config: |
1693+ #cloud-config
1694+ apt:
1695+ primary:
1696+ - arches:
1697+ - default
1698+ uri: "http://www.gtlib.gatech.edu/pub/ubuntu-releases/"
1699+collect_scripts:
1700+ ubuntu.sources.list: |
1701+ #!/bin/bash
1702+ grep -v '^#' /etc/apt/sources.list | sed '/^\s*$/d' | grep -c archive.ubuntu.com
1703+ gatech.sources.list: |
1704+ #!/bin/bash
1705+ grep -v '^#' /etc/apt/sources.list | sed '/^\s*$/d' | grep -c gtlib.gatech.edu
1706diff --git a/tests/cloud_tests/configs/modules/apt_configure_proxy.yaml b/tests/cloud_tests/configs/modules/apt_configure_proxy.yaml
1707new file mode 100644
1708index 0000000..e73778f
1709--- /dev/null
1710+++ b/tests/cloud_tests/configs/modules/apt_configure_proxy.yaml
1711@@ -0,0 +1,14 @@
1712+#
1713+# Set apt proxy
1714+#
1715+cloud_config: |
1716+ #cloud-config
1717+ apt:
1718+ proxy: "http://squid.internal:3128"
1719+ http_proxy: "http://squid.internal:3128"
1720+ ftp_proxy: "ftp://squid.internal:3128"
1721+ https_proxy: "https://squid.internal:3128"
1722+collect_scripts:
1723+ 90cloud-init-aptproxy: |
1724+ #!/bin/bash
1725+ cat /etc/apt/apt.conf.d/90cloud-init-aptproxy
1726diff --git a/tests/cloud_tests/configs/modules/apt_configure_security.yaml b/tests/cloud_tests/configs/modules/apt_configure_security.yaml
1727new file mode 100644
1728index 0000000..933e875
1729--- /dev/null
1730+++ b/tests/cloud_tests/configs/modules/apt_configure_security.yaml
1731@@ -0,0 +1,13 @@
1732+#
1733+# Add security to sources.list
1734+#
1735+cloud_config: |
1736+ #cloud-config
1737+ apt:
1738+ security:
1739+ - arches:
1740+ - default
1741+collect_scripts:
1742+ sources.list: |
1743+ #!/bin/bash
1744+ grep -c security.ubuntu.com /etc/apt/sources.list
1745diff --git a/tests/cloud_tests/configs/modules/apt_configure_sources_key.yaml b/tests/cloud_tests/configs/modules/apt_configure_sources_key.yaml
1746new file mode 100644
1747index 0000000..02fb006
1748--- /dev/null
1749+++ b/tests/cloud_tests/configs/modules/apt_configure_sources_key.yaml
1750@@ -0,0 +1,45 @@
1751+#
1752+# Add a sources.list entry with a given key (Debian Jessie)
1753+#
1754+cloud_config: |
1755+ #cloud-config
1756+ apt:
1757+ sources:
1758+ source1:
1759+ source: "deb http://ppa.launchpad.net/cloud-init-dev/test-archive/ubuntu $RELEASE main"
1760+ key: |
1761+ -----BEGIN PGP PUBLIC KEY BLOCK-----
1762+ Version: SKS 1.1.6
1763+ Comment: Hostname: keyserver.ubuntu.com
1764+
1765+ mQINBFbZRUIBEAC+A0PIKYBP9kLC4hQtRrffRS11uLo8/BdtmOdrlW0hpPHzCfKnjR3tvSEI
1766+ lqPHG1QrrjAXKZDnZMRz+h/px7lUztvytGzHPSJd5ARUzAyjyRezUhoJ3VSCxrPqx62avuWf
1767+ RfoJaIeHfDehL5/dTVkyiWxfVZ369ZX6JN2AgLsQTeybTQ75+2z0xPrrhnGmgh6g0qTYcAaq
1768+ M5ONOGiqeSBX/Smjh6ALy5XkhUiFGLsI7Yluf6XSICY/x7gd6RAfgSIQrUTNMoS1sqhT4aot
1769+ +xvOfQy8ySkfAK4NddXql6E/+ZqTmBY/Lr0YklFBy8jGT+UysfiIznPMIwbmgq5Li7BtDDtX
1770+ b8Uyi4edPpjtextezfXYn4NVIpPL5dPZS/FXh4HpzyH0pYCfrH4QDGA7i52AGmhpiOFjJMo6
1771+ N33sdjZHOH/2Vyp+QZaQnsdUAi1N4M6c33tQbpIScn1SY+El8z5JDA4PBzkw8HpLCi1gGoa6
1772+ V4kfbWqXXbGAJFkLkP/vc4+pY9axOlmCkJg7xCPwhI75y1cONgovhz+BEXOzolh5KZuGbGbj
1773+ xe0wva5DLBeIg7EQFf+99pOS7Syby3Xpm6ZbswEFV0cllK4jf/QMjtfInxobuMoI0GV0bE5l
1774+ WlRtPCK5FnbHwxi0wPNzB/5fwzJ77r6HgPrR0OkT0lWmbUyoOQARAQABtC1MYXVuY2hwYWQg
1775+ UFBBIGZvciBjbG91ZCBpbml0IGRldmVsb3BtZW50IHRlYW2JAjgEEwECACIFAlbZRUICGwMG
1776+ CwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEAg9Bvvk0wTfHfcP/REK5N2s1JYc69qEa9ZN
1777+ o6oi+A7l6AYw+ZY88O5TJe7F9otv5VXCIKSUT0Vsepjgf0mtXAgf/sb2lsJn/jp7tzgov3YH
1778+ vSrkTkRydz8xcA87gwQKePuvTLxQpftF4flrBxgSueIn5O/tPrBOxLz7EVYBc78SKg9aj9L2
1779+ yUp+YuNevlwfZCTYeBb9r3FHaab2HcgkwqYch66+nKYfwiLuQ9NzXXm0Wn0JcEQ6pWvJscbj
1780+ C9BdawWovfvMK5/YLfI6Btm7F4mIpQBdhSOUp/YXKmdvHpmwxMCN2QhqYK49SM7qE9aUDbJL
1781+ arppSEBtlCLWhRBZYLTUna+BkuQ1bHz4St++XTR49Qd7vDERALpApDjB2dxPfMiBzCMwQQyq
1782+ uy13exU8o2ETLg+dZSLfDTzrBNsBFmXlw8WW17nTISYdKeGKL+QdlUjpzdwUMMzHhAO8SmMH
1783+ zjeSlDSRMXBJFAFSbCl7EwmMKa3yVX0zInT91fNllZ3iatAmtVdqVH/BFQfTIMH2ET7A8WzJ
1784+ ZzVSuMRhqoKdr5AMcHuJGPUoVkVJHQA+NNvEiXSysF3faL7jmKapmUwrhpYYX2H8pf+VMu2e
1785+ cLflKTI28dl+ZQ4Pl/aVsxrti/pzhdYy05Sn5ddtySyIkvo8L1cU5MWpbvSlFPkTstBUDLBf
1786+ pb0uBy+g0oxJQg15
1787+ =uy53
1788+ -----END PGP PUBLIC KEY BLOCK-----
1789+collect_scripts:
1790+ sources.list: |
1791+ #!/bin/bash
1792+ cat /etc/apt/sources.list.d/source1.list
1793+ apt_key_list: |
1794+ #!/bin/bash
1795+ apt-key finger
1796diff --git a/tests/cloud_tests/configs/modules/apt_configure_sources_keyserver.yaml b/tests/cloud_tests/configs/modules/apt_configure_sources_keyserver.yaml
1797new file mode 100644
1798index 0000000..323c255
1799--- /dev/null
1800+++ b/tests/cloud_tests/configs/modules/apt_configure_sources_keyserver.yaml
1801@@ -0,0 +1,18 @@
1802+#
1803+# Add a sources.list entry with a key from a keyserver
1804+#
1805+cloud_config: |
1806+ #cloud-config
1807+ apt:
1808+ sources:
1809+ source1:
1810+ keyid: 0165013E
1811+ keyserver: keyserver.ubuntu.com
1812+ source: "deb http://ppa.launchpad.net/cloud-init-dev/test-archive/ubuntu $RELEASE main"
1813+collect_scripts:
1814+ sources.list: |
1815+ #!/bin/bash
1816+ cat /etc/apt/sources.list.d/source1.list
1817+ apt_key_list: |
1818+ #!/bin/bash
1819+ apt-key finger
1820diff --git a/tests/cloud_tests/configs/modules/apt_configure_sources_list.yaml b/tests/cloud_tests/configs/modules/apt_configure_sources_list.yaml
1821new file mode 100644
1822index 0000000..94a2101
1823--- /dev/null
1824+++ b/tests/cloud_tests/configs/modules/apt_configure_sources_list.yaml
1825@@ -0,0 +1,17 @@
1826+#
1827+# Generate a sources.list
1828+#
1829+cloud_config: |
1830+ #cloud-config
1831+ apt:
1832+ sources_list: |
1833+ deb $MIRROR $RELEASE main restricted
1834+ deb-src $MIRROR $RELEASE main restricted
1835+ deb $PRIMARY $RELEASE universe restricted
1836+ deb-src $PRIMARY $RELEASE universe restricted
1837+ deb $SECURITY $RELEASE-security multiverse
1838+ deb-src $SECURITY $RELEASE-security multiverse
1839+collect_scripts:
1840+ sources.list: |
1841+ #/bin/bash
1842+ cat /etc/apt/sources.list
1843diff --git a/tests/cloud_tests/configs/modules/apt_configure_sources_ppa.yaml b/tests/cloud_tests/configs/modules/apt_configure_sources_ppa.yaml
1844new file mode 100644
1845index 0000000..4045ee4
1846--- /dev/null
1847+++ b/tests/cloud_tests/configs/modules/apt_configure_sources_ppa.yaml
1848@@ -0,0 +1,18 @@
1849+#
1850+# Add a PPA to source.list
1851+#
1852+cloud_config: |
1853+ #cloud-config
1854+ apt:
1855+ sources:
1856+ source1:
1857+ keyid: 0165013E
1858+ keyserver: keyserver.ubuntu.com
1859+ source: "ppa:curtin-dev/test-archive"
1860+collect_scripts:
1861+ sources.list: |
1862+ #!/bin/bash
1863+ cat /etc/apt/sources.list.d/curtin-dev-ubuntu-test-archive-*.list
1864+ apt-key: |
1865+ #!/bin/bash
1866+ apt-key finger
1867diff --git a/tests/cloud_tests/configs/modules/apt_pipelining_disable.yaml b/tests/cloud_tests/configs/modules/apt_pipelining_disable.yaml
1868new file mode 100644
1869index 0000000..25545ab
1870--- /dev/null
1871+++ b/tests/cloud_tests/configs/modules/apt_pipelining_disable.yaml
1872@@ -0,0 +1,11 @@
1873+#
1874+# Disable apt pipelining value
1875+#
1876+cloud_config: |
1877+ #cloud-config
1878+ apt:
1879+ apt_pipelining: false
1880+collect_scripts:
1881+ 90cloud-init-pipelining: |
1882+ #!/bin/bash
1883+ cat /etc/apt/apt.conf.d/90cloud-init-pipelining
1884diff --git a/tests/cloud_tests/configs/modules/apt_pipelining_os.yaml b/tests/cloud_tests/configs/modules/apt_pipelining_os.yaml
1885new file mode 100644
1886index 0000000..16ed3cb
1887--- /dev/null
1888+++ b/tests/cloud_tests/configs/modules/apt_pipelining_os.yaml
1889@@ -0,0 +1,11 @@
1890+#
1891+# Set apt pipelining value to OS
1892+#
1893+cloud_config: |
1894+ #cloud-config
1895+ apt:
1896+ apt_pipelining: os
1897+collect_scripts:
1898+ 90cloud-init-pipelining: |
1899+ #!/bin/bash
1900+ cat /etc/apt/apt.conf.d/90cloud-init-pipelining
1901diff --git a/tests/cloud_tests/configs/modules/bootcmd.yaml b/tests/cloud_tests/configs/modules/bootcmd.yaml
1902new file mode 100644
1903index 0000000..4767d75
1904--- /dev/null
1905+++ b/tests/cloud_tests/configs/modules/bootcmd.yaml
1906@@ -0,0 +1,11 @@
1907+#
1908+# Early boot command
1909+#
1910+cloud_config: |
1911+ #cloud-config
1912+ bootcmd:
1913+ - echo 192.168.1.130 us.archive.ubuntu.com > /etc/hosts
1914+collect_scripts:
1915+ hosts: |
1916+ #!/bin/bash
1917+ cat /etc/hosts
1918diff --git a/tests/cloud_tests/configs/modules/byobu.yaml b/tests/cloud_tests/configs/modules/byobu.yaml
1919new file mode 100644
1920index 0000000..6f9e4f8
1921--- /dev/null
1922+++ b/tests/cloud_tests/configs/modules/byobu.yaml
1923@@ -0,0 +1,16 @@
1924+#
1925+# Install and enable byobu system wide and default user
1926+#
1927+cloud_config: |
1928+ #cloud-config
1929+ byobu_by_default: enable
1930+collect_scripts:
1931+ byobu_installed: |
1932+ #!/bin/bash
1933+ which byobu
1934+ byobu_profile_enabled: |
1935+ #!/bin/bash
1936+ ls /etc/profile.d/Z97-byobu.sh
1937+ byobu_launch_exists: |
1938+ #!/bin/bash
1939+ which /usr/bin/byobu-launch
1940diff --git a/tests/cloud_tests/configs/modules/ca_certs.yaml b/tests/cloud_tests/configs/modules/ca_certs.yaml
1941new file mode 100644
1942index 0000000..5e386ae
1943--- /dev/null
1944+++ b/tests/cloud_tests/configs/modules/ca_certs.yaml
1945@@ -0,0 +1,51 @@
1946+#
1947+# Remove existing ca_certs and install custom ca-cert
1948+#
1949+cloud_config: |
1950+ #cloud-config
1951+ ca-certs:
1952+ remove-defaults: true
1953+ trusted:
1954+ - |
1955+ -----BEGIN CERTIFICATE-----
1956+ MIIGJzCCBA+gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBsjELMAkGA1UEBhMCRlIx
1957+ DzANBgNVBAgMBkFsc2FjZTETMBEGA1UEBwwKU3RyYXNib3VyZzEYMBYGA1UECgwP
1958+ d3d3LmZyZWVsYW4ub3JnMRAwDgYDVQQLDAdmcmVlbGFuMS0wKwYDVQQDDCRGcmVl
1959+ bGFuIFNhbXBsZSBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxIjAgBgkqhkiG9w0BCQEW
1960+ E2NvbnRhY3RAZnJlZWxhbi5vcmcwHhcNMTIwNDI3MTAzMTE4WhcNMjIwNDI1MTAz
1961+ MTE4WjB+MQswCQYDVQQGEwJGUjEPMA0GA1UECAwGQWxzYWNlMRgwFgYDVQQKDA93
1962+ d3cuZnJlZWxhbi5vcmcxEDAOBgNVBAsMB2ZyZWVsYW4xDjAMBgNVBAMMBWFsaWNl
1963+ MSIwIAYJKoZIhvcNAQkBFhNjb250YWN0QGZyZWVsYW4ub3JnMIICIjANBgkqhkiG
1964+ 9w0BAQEFAAOCAg8AMIICCgKCAgEA3W29+ID6194bH6ejLrIC4hb2Ugo8v6ZC+Mrc
1965+ k2dNYMNPjcOKABvxxEtBamnSaeU/IY7FC/giN622LEtV/3oDcrua0+yWuVafyxmZ
1966+ yTKUb4/GUgafRQPf/eiX9urWurtIK7XgNGFNUjYPq4dSJQPPhwCHE/LKAykWnZBX
1967+ RrX0Dq4XyApNku0IpjIjEXH+8ixE12wH8wt7DEvdO7T3N3CfUbaITl1qBX+Nm2Z6
1968+ q4Ag/u5rl8NJfXg71ZmXA3XOj7zFvpyapRIZcPmkvZYn7SMCp8dXyXHPdpSiIWL2
1969+ uB3KiO4JrUYvt2GzLBUThp+lNSZaZ/Q3yOaAAUkOx+1h08285Pi+P8lO+H2Xic4S
1970+ vMq1xtLg2bNoPC5KnbRfuFPuUD2/3dSiiragJ6uYDLOyWJDivKGt/72OVTEPAL9o
1971+ 6T2pGZrwbQuiFGrGTMZOvWMSpQtNl+tCCXlT4mWqJDRwuMGrI4DnnGzt3IKqNwS4
1972+ Qyo9KqjMIPwnXZAmWPm3FOKe4sFwc5fpawKO01JZewDsYTDxVj+cwXwFxbE2yBiF
1973+ z2FAHwfopwaH35p3C6lkcgP2k/zgAlnBluzACUI+MKJ/G0gv/uAhj1OHJQ3L6kn1
1974+ SpvQ41/ueBjlunExqQSYD7GtZ1Kg8uOcq2r+WISE3Qc9MpQFFkUVllmgWGwYDuN3
1975+ Zsez95kCAwEAAaN7MHkwCQYDVR0TBAIwADAsBglghkgBhvhCAQ0EHxYdT3BlblNT
1976+ TCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYEFFlfyRO6G8y5qEFKikl5
1977+ ajb2fT7XMB8GA1UdIwQYMBaAFCNsLT0+KV14uGw+quK7Lh5sh/JTMA0GCSqGSIb3
1978+ DQEBBQUAA4ICAQAT5wJFPqervbja5+90iKxi1d0QVtVGB+z6aoAMuWK+qgi0vgvr
1979+ mu9ot2lvTSCSnRhjeiP0SIdqFMORmBtOCFk/kYDp9M/91b+vS+S9eAlxrNCB5VOf
1980+ PqxEPp/wv1rBcE4GBO/c6HcFon3F+oBYCsUQbZDKSSZxhDm3mj7pb67FNbZbJIzJ
1981+ 70HDsRe2O04oiTx+h6g6pW3cOQMgIAvFgKN5Ex727K4230B0NIdGkzuj4KSML0NM
1982+ slSAcXZ41OoSKNjy44BVEZv0ZdxTDrRM4EwJtNyggFzmtTuV02nkUj1bYYYC5f0L
1983+ ADr6s0XMyaNk8twlWYlYDZ5uKDpVRVBfiGcq0uJIzIvemhuTrofh8pBQQNkPRDFT
1984+ Rq1iTo1Ihhl3/Fl1kXk1WR3jTjNb4jHX7lIoXwpwp767HAPKGhjQ9cFbnHMEtkro
1985+ RlJYdtRq5mccDtwT0GFyoJLLBZdHHMHJz0F9H7FNk2tTQQMhK5MVYwg+LIaee586
1986+ CQVqfbscp7evlgjLW98H+5zylRHAgoH2G79aHljNKMp9BOuq6SnEglEsiWGVtu2l
1987+ hnx8SB3sVJZHeer8f/UQQwqbAO+Kdy70NmbSaqaVtp8jOxLiidWkwSyRTsuU6D8i
1988+ DiH5uEqBXExjrj0FslxcVKdVj5glVcSmkLwZKbEU1OKwleT/iXFhvooWhQ==
1989+ -----END CERTIFICATE-----
1990+collect_scripts:
1991+ cert_count: |
1992+ #!/bin/bash
1993+ ls -l /etc/ssl/certs | wc -l
1994+ cert: |
1995+ #!/bin/bash
1996+ md5sum /etc/ssl/certs/ca-certificates.crt
1997\ No newline at end of file
1998diff --git a/tests/cloud_tests/configs/modules/debug_disable.yaml b/tests/cloud_tests/configs/modules/debug_disable.yaml
1999new file mode 100644
2000index 0000000..cf94910
2001--- /dev/null
2002+++ b/tests/cloud_tests/configs/modules/debug_disable.yaml
2003@@ -0,0 +1,7 @@
2004+#
2005+# Do not run in debug mode
2006+#
2007+cloud_config: |
2008+ #cloud-config
2009+ debug:
2010+ verbose: False
2011diff --git a/tests/cloud_tests/configs/modules/debug_enable.yaml b/tests/cloud_tests/configs/modules/debug_enable.yaml
2012new file mode 100644
2013index 0000000..ba8080f
2014--- /dev/null
2015+++ b/tests/cloud_tests/configs/modules/debug_enable.yaml
2016@@ -0,0 +1,7 @@
2017+#
2018+# Run in debug mode
2019+#
2020+cloud_config: |
2021+ #cloud-config
2022+ debug:
2023+ verbose: True
2024diff --git a/tests/cloud_tests/configs/modules/final_message.yaml b/tests/cloud_tests/configs/modules/final_message.yaml
2025new file mode 100644
2026index 0000000..d2a760e
2027--- /dev/null
2028+++ b/tests/cloud_tests/configs/modules/final_message.yaml
2029@@ -0,0 +1,11 @@
2030+#
2031+# Print a final message with various predefined variables
2032+#
2033+cloud_config: |
2034+ #cloud-config
2035+ final_message: |
2036+ This is my final message!
2037+ $version
2038+ $timestamp
2039+ $datasource
2040+ $uptime
2041diff --git a/tests/cloud_tests/configs/modules/keys_to_console.yaml b/tests/cloud_tests/configs/modules/keys_to_console.yaml
2042new file mode 100644
2043index 0000000..504a127
2044--- /dev/null
2045+++ b/tests/cloud_tests/configs/modules/keys_to_console.yaml
2046@@ -0,0 +1,11 @@
2047+#
2048+# Hide printing of ssh key and fingerprints for specific keys
2049+#
2050+cloud_config: |
2051+ #cloud-config
2052+ ssh_fp_console_blacklist: [ssh-dss, ssh-dsa, ecdsa-sha2-nistp256]
2053+ ssh_key_console_blacklist: [ssh-dss, ssh-dsa, ecdsa-sha2-nistp256]
2054+collect_scripts:
2055+ syslog: |
2056+ #!/bin/bash
2057+ cat /var/log/syslog
2058diff --git a/tests/cloud_tests/configs/modules/landscape.yaml b/tests/cloud_tests/configs/modules/landscape.yaml
2059new file mode 100644
2060index 0000000..6eb14a0
2061--- /dev/null
2062+++ b/tests/cloud_tests/configs/modules/landscape.yaml
2063@@ -0,0 +1,24 @@
2064+#
2065+# Setup landscape client settings
2066+#
2067+# 2016-11-17: Disabled due to this not working
2068+#
2069+enabled: false
2070+cloud_config: |
2071+ #cloud-conifg
2072+ landscape:
2073+ client:
2074+ log_level: "info"
2075+ url: "https://landscape.canonical.com/message-system"
2076+ ping_url: "http://landscape.canonical.com/ping"
2077+ data_path: "/var/lib/landscape/client"
2078+ http_proxy: "http://my.proxy.com/foobar"
2079+ https_proxy: "https://my.proxy.com/foobar"
2080+ tags: "server,cloud"
2081+ computer_title: "footitle"
2082+ registration_key: "fookey"
2083+ account_name: "fooaccount"
2084+collect_scripts:
2085+ client.conf: |
2086+ #!/bin/bash
2087+ cat /etc/landscape/client.conf
2088diff --git a/tests/cloud_tests/configs/modules/locale.yaml b/tests/cloud_tests/configs/modules/locale.yaml
2089new file mode 100644
2090index 0000000..be3c747
2091--- /dev/null
2092+++ b/tests/cloud_tests/configs/modules/locale.yaml
2093@@ -0,0 +1,17 @@
2094+#
2095+# Set locale to non-default option and verify
2096+#
2097+cloud_config: |
2098+ #cloud-config
2099+ locale: en_GB.UTF-8
2100+ locale_configfile: /etc/default/locale
2101+collect_scripts:
2102+ locale_default: |
2103+ #!/bin/bash
2104+ cat /etc/default/locale
2105+ locale_a: |
2106+ #!/bin/bash
2107+ locale -a
2108+ locale_gen: |
2109+ #!/bin/bash
2110+ cat /etc/locale.gen | grep -v '^#' | uniq
2111diff --git a/tests/cloud_tests/configs/modules/lxd_bridge.yaml b/tests/cloud_tests/configs/modules/lxd_bridge.yaml
2112new file mode 100644
2113index 0000000..956d888
2114--- /dev/null
2115+++ b/tests/cloud_tests/configs/modules/lxd_bridge.yaml
2116@@ -0,0 +1,28 @@
2117+#
2118+# LXD configured with directory backend and IPv4 bridge
2119+#
2120+cloud_config: |
2121+ #cloud-config
2122+ lxd:
2123+ init:
2124+ storage_backend: dir
2125+ bridge:
2126+ mode: new
2127+ name: lxdbr0
2128+ ipv4_address: 10.100.100.1
2129+ ipv4_netmask: 24
2130+ ipv4_dhcp_first: 10.100.100.100
2131+ ipv4_dhcp_last: 10.100.100.200
2132+ ipv4_nat: true
2133+ domain: lxd
2134+collect_scripts:
2135+ lxc: |
2136+ #!/bin/bash
2137+ which lxc
2138+ lxd: |
2139+ #!/bin/bash
2140+ which lxd
2141+ lxc-bridge: |
2142+ #!/bin/bash
2143+ ip addr show lxdbr0
2144+ cat /etc/default/lxd-bridge 2>/dev/null | grep -v ^# | sort -u
2145diff --git a/tests/cloud_tests/configs/modules/lxd_dir.yaml b/tests/cloud_tests/configs/modules/lxd_dir.yaml
2146new file mode 100644
2147index 0000000..1e24670
2148--- /dev/null
2149+++ b/tests/cloud_tests/configs/modules/lxd_dir.yaml
2150@@ -0,0 +1,15 @@
2151+#
2152+# LXD configured with directory backend
2153+#
2154+cloud_config: |
2155+ #cloud-config
2156+ lxd:
2157+ init:
2158+ storage_backend: dir
2159+collect_scripts:
2160+ lxc: |
2161+ #!/bin/bash
2162+ which lxc
2163+ lxd: |
2164+ #!/bin/bash
2165+ which lxd
2166diff --git a/tests/cloud_tests/configs/modules/ntp.yaml b/tests/cloud_tests/configs/modules/ntp.yaml
2167new file mode 100644
2168index 0000000..94243c7
2169--- /dev/null
2170+++ b/tests/cloud_tests/configs/modules/ntp.yaml
2171@@ -0,0 +1,18 @@
2172+#
2173+# Emtpy NTP config to setup using defaults
2174+#
2175+cloud_config: |
2176+ #cloud-config
2177+ ntp:
2178+ pools: {}
2179+ servers: {}
2180+collect_scripts:
2181+ ntp_installed_empty: |
2182+ #!/bin/bash
2183+ dpkg -l | grep ntp | wc -l
2184+ ntp_conf_dist_empty: |
2185+ #!/bin/bash
2186+ ls /etc/ntp.conf.dist | wc -l
2187+ ntp_conf_empty: |
2188+ #!/bin/bash
2189+ grep '^pool' /etc/ntp.conf
2190diff --git a/tests/cloud_tests/configs/modules/ntp_pools.yaml b/tests/cloud_tests/configs/modules/ntp_pools.yaml
2191new file mode 100644
2192index 0000000..394077d
2193--- /dev/null
2194+++ b/tests/cloud_tests/configs/modules/ntp_pools.yaml
2195@@ -0,0 +1,21 @@
2196+#
2197+# NTP config using specific pools
2198+#
2199+cloud_config: |
2200+ #cloud-config
2201+ ntp:
2202+ pools:
2203+ - 0.pool.ntp.org
2204+ - 1.pool.ntp.org
2205+ - 2.pool.ntp.org
2206+ - 3.pool.ntp.org
2207+collect_scripts:
2208+ ntp_installed_pools: |
2209+ #!/bin/bash
2210+ dpkg -l | grep ntp | wc -l
2211+ ntp_conf_dist_pools: |
2212+ #!/bin/bash
2213+ ls /etc/ntp.conf.dist | wc -l
2214+ ntp_conf_pools: |
2215+ #!/bin/bash
2216+ grep '^pool' /etc/ntp.conf
2217diff --git a/tests/cloud_tests/configs/modules/ntp_servers.yaml b/tests/cloud_tests/configs/modules/ntp_servers.yaml
2218new file mode 100644
2219index 0000000..b041526
2220--- /dev/null
2221+++ b/tests/cloud_tests/configs/modules/ntp_servers.yaml
2222@@ -0,0 +1,18 @@
2223+#
2224+# NTP config using specific servers
2225+#
2226+cloud_config: |
2227+ #cloud-config
2228+ ntp:
2229+ servers:
2230+ - pool.ntp.org
2231+collect_scripts:
2232+ ntp_installed_servers: |
2233+ #!/bin/bash
2234+ dpkg -l | grep ntp | wc -l
2235+ ntp_conf_dist_servers: |
2236+ #!/bin/bash
2237+ ls /etc/ntp.conf.dist | wc -l
2238+ ntp_conf_servers: |
2239+ #!/bin/bash
2240+ grep '^server' /etc/ntp.conf
2241diff --git a/tests/cloud_tests/configs/modules/package_update_upgrade_install.yaml b/tests/cloud_tests/configs/modules/package_update_upgrade_install.yaml
2242new file mode 100644
2243index 0000000..7fe8bd2
2244--- /dev/null
2245+++ b/tests/cloud_tests/configs/modules/package_update_upgrade_install.yaml
2246@@ -0,0 +1,20 @@
2247+#
2248+# Update/upgrade via apt and then install a pair of packages
2249+#
2250+cloud_config: |
2251+ #cloud-config
2252+ packages:
2253+ - htop
2254+ - tree
2255+ package_update: true
2256+ package_upgrade: true
2257+collect_scripts:
2258+ apt_history_cmdline: |
2259+ #!/bin/bash
2260+ grep ^Commandline: /var/log/apt/history.log
2261+ dpkg_htop: |
2262+ #!/bin/bash
2263+ dpkg -l | grep htop | wc -l
2264+ dpkg_tree: |
2265+ #!/bin/bash
2266+ dpkg -l | grep tree | wc -l
2267diff --git a/tests/cloud_tests/configs/modules/runcmd.yaml b/tests/cloud_tests/configs/modules/runcmd.yaml
2268new file mode 100644
2269index 0000000..fc31657
2270--- /dev/null
2271+++ b/tests/cloud_tests/configs/modules/runcmd.yaml
2272@@ -0,0 +1,11 @@
2273+#
2274+# Run a simple command
2275+#
2276+cloud_config: |
2277+ #cloud-config
2278+ runcmd:
2279+ - echo cloud-init run cmd test > /tmp/run_cmd
2280+collect_scripts:
2281+ run_cmd: |
2282+ #!/bin/bash
2283+ cat /tmp/run_cmd
2284diff --git a/tests/cloud_tests/configs/modules/salt_minion.yaml b/tests/cloud_tests/configs/modules/salt_minion.yaml
2285new file mode 100644
2286index 0000000..ea091a3
2287--- /dev/null
2288+++ b/tests/cloud_tests/configs/modules/salt_minion.yaml
2289@@ -0,0 +1,32 @@
2290+#
2291+# Create config for a salt minion
2292+#
2293+# 2016-11-17: Currently takes >60 seconds results in test failure
2294+#
2295+enabled: False
2296+cloud_config: |
2297+ #cloud-config
2298+ salt_minion:
2299+ conf:
2300+ master: salt.mydomain.com
2301+ public_key: |
2302+ ------BEGIN PUBLIC KEY-------
2303+ <key data>
2304+ ------END PUBLIC KEY-------
2305+ private_key: |
2306+ ------BEGIN PRIVATE KEY------
2307+ <key data>
2308+ ------END PRIVATE KEY-------
2309+collect_scripts:
2310+ minion: |
2311+ #!/bin/bash
2312+ cat /etc/salt/minion
2313+ minion_id: |
2314+ #!/bin/bash
2315+ cat /etc/salt/minion_id
2316+ minion.pem: |
2317+ #!/bin/bash
2318+ cat /etc/salt/pki/minion/minion.pem
2319+ minion.pub: |
2320+ #!/bin/bash
2321+ cat /etc/salt/pki/minion/minion.pub
2322diff --git a/tests/cloud_tests/configs/modules/seed_random_command.yaml b/tests/cloud_tests/configs/modules/seed_random_command.yaml
2323new file mode 100644
2324index 0000000..aa2c681
2325--- /dev/null
2326+++ b/tests/cloud_tests/configs/modules/seed_random_command.yaml
2327@@ -0,0 +1,16 @@
2328+#
2329+# Use uuid to create a random string
2330+#
2331+# 2016-11-15 Disabled as this is not working currently
2332+#
2333+enabled: False
2334+cloud_config: |
2335+ #cloud-config
2336+ random_seed:
2337+ command: ["cat", "/proc/sys/kernel/random/uuid"]
2338+ command_required: true
2339+ file: /root/seed
2340+collect_scripts:
2341+ seed_data: |
2342+ #!/bin/bash
2343+ cat /root/seed
2344diff --git a/tests/cloud_tests/configs/modules/seed_random_data.yaml b/tests/cloud_tests/configs/modules/seed_random_data.yaml
2345new file mode 100644
2346index 0000000..6f8fd4a
2347--- /dev/null
2348+++ b/tests/cloud_tests/configs/modules/seed_random_data.yaml
2349@@ -0,0 +1,13 @@
2350+#
2351+# Push in random raw string to set as seed
2352+#
2353+cloud_config: |
2354+ #cloud-config
2355+ random_seed:
2356+ data: 'MYUb34023nD:LFDK10913jk;dfnk:Df'
2357+ encoding: raw
2358+ file: /root/seed
2359+collect_scripts:
2360+ seed_data: |
2361+ #!/bin/bash
2362+ cat /root/seed
2363diff --git a/tests/cloud_tests/configs/modules/set_hostname.yaml b/tests/cloud_tests/configs/modules/set_hostname.yaml
2364new file mode 100644
2365index 0000000..35987fd
2366--- /dev/null
2367+++ b/tests/cloud_tests/configs/modules/set_hostname.yaml
2368@@ -0,0 +1,16 @@
2369+#
2370+# Set the hostname and update /etc/hosts
2371+#
2372+cloud_config: |
2373+ #cloud-config
2374+ hostname: myhostname
2375+collect_scripts:
2376+ hosts: |
2377+ #!/bin/bash
2378+ grep ^127 /etc/hosts
2379+ hostname: |
2380+ #!/bin/bash
2381+ hostname
2382+ fqdn: |
2383+ #!/bin/bash
2384+ hostname --fqdn
2385diff --git a/tests/cloud_tests/configs/modules/set_hostname_fqdn.yaml b/tests/cloud_tests/configs/modules/set_hostname_fqdn.yaml
2386new file mode 100644
2387index 0000000..5c62895
2388--- /dev/null
2389+++ b/tests/cloud_tests/configs/modules/set_hostname_fqdn.yaml
2390@@ -0,0 +1,18 @@
2391+#
2392+# Set the hostname and update /etc/hosts
2393+#
2394+cloud_config: |
2395+ #cloud-config
2396+ manage_etc_hosts: true
2397+ hostname: myhostname
2398+ fqdn: host.myorg.com
2399+collect_scripts:
2400+ hosts: |
2401+ #!/bin/bash
2402+ grep ^127 /etc/hosts
2403+ hostname: |
2404+ #!/bin/bash
2405+ hostname
2406+ fqdn: |
2407+ #!/bin/bash
2408+ hostname --fqdn
2409diff --git a/tests/cloud_tests/configs/modules/set_password.yaml b/tests/cloud_tests/configs/modules/set_password.yaml
2410new file mode 100644
2411index 0000000..6403d4c
2412--- /dev/null
2413+++ b/tests/cloud_tests/configs/modules/set_password.yaml
2414@@ -0,0 +1,15 @@
2415+#
2416+# Set password of default user
2417+#
2418+cloud_config: |
2419+ #cloud-config
2420+ password: password
2421+ chpasswd: { expire: False }
2422+ ssh_pwauth: True
2423+collect_scripts:
2424+ shadow: |
2425+ #!/bin/bash
2426+ cat /etc/shadow
2427+ sshd_config: |
2428+ #!/bin/bash
2429+ grep '^PasswordAuth' /etc/ssh/sshd_config
2430diff --git a/tests/cloud_tests/configs/modules/set_password_expire.yaml b/tests/cloud_tests/configs/modules/set_password_expire.yaml
2431new file mode 100644
2432index 0000000..6b8ae18
2433--- /dev/null
2434+++ b/tests/cloud_tests/configs/modules/set_password_expire.yaml
2435@@ -0,0 +1,26 @@
2436+#
2437+# Expire password for all users
2438+#
2439+cloud_config: |
2440+ #cloud-config
2441+ chpasswd: { expire: True }
2442+ users:
2443+ - name: tom
2444+ password: $1$xyz$sPMsLNmf66Ohl.ol6JvzE.
2445+ lock_passwd: false
2446+ - name: dick
2447+ password: $1$xyz$sPMsLNmf66Ohl.ol6JvzE.
2448+ lock_passwd: false
2449+ - name: harry
2450+ password: $1$xyz$sPMsLNmf66Ohl.ol6JvzE.
2451+ lock_passwd: false
2452+ - name: jane
2453+ password: $1$xyz$sPMsLNmf66Ohl.ol6JvzE.
2454+ lock_passwd: false
2455+collect_scripts:
2456+ shadow: |
2457+ #!/bin/bash
2458+ cat /etc/shadow
2459+ sshd_config: |
2460+ #!/bin/bash
2461+ grep '^PasswordAuth' /etc/ssh/sshd_config
2462diff --git a/tests/cloud_tests/configs/modules/set_password_list.yaml b/tests/cloud_tests/configs/modules/set_password_list.yaml
2463new file mode 100644
2464index 0000000..6b6e4c7
2465--- /dev/null
2466+++ b/tests/cloud_tests/configs/modules/set_password_list.yaml
2467@@ -0,0 +1,31 @@
2468+#
2469+# Set password of list of users
2470+#
2471+cloud_config: |
2472+ #cloud-config
2473+ ssh_pwauth: yes
2474+ users:
2475+ - name: tom
2476+ password: $1$xyz$sPMsLNmf66Ohl.ol6JvzE.
2477+ lock_passwd: false
2478+ - name: dick
2479+ password: $1$xyz$sPMsLNmf66Ohl.ol6JvzE.
2480+ lock_passwd: false
2481+ - name: harry
2482+ password: $1$xyz$sPMsLNmf66Ohl.ol6JvzE.
2483+ lock_passwd: false
2484+ - name: jane
2485+ password: $1$xyz$sPMsLNmf66Ohl.ol6JvzE.
2486+ lock_passwd: false
2487+ chpasswd:
2488+ list: |
2489+ tom:mypassword123!
2490+ dick:R
2491+ harry:Random
2492+collect_scripts:
2493+ shadow: |
2494+ #!/bin/bash
2495+ cat /etc/shadow
2496+ sshd_config: |
2497+ #!/bin/bash
2498+ grep '^PasswordAuth' /etc/ssh/sshd_config
2499diff --git a/tests/cloud_tests/configs/modules/snappy.yaml b/tests/cloud_tests/configs/modules/snappy.yaml
2500new file mode 100644
2501index 0000000..2e0b0d9
2502--- /dev/null
2503+++ b/tests/cloud_tests/configs/modules/snappy.yaml
2504@@ -0,0 +1,11 @@
2505+#
2506+# Install snappy
2507+#
2508+cloud_config: |
2509+ #cloud-config
2510+ snappy:
2511+ system_snappy: auto
2512+collect_scripts:
2513+ snap_version: |
2514+ #!/bin/bash
2515+ snap --version
2516diff --git a/tests/cloud_tests/configs/modules/ssh_auth_key_fingerprints_disable.yaml b/tests/cloud_tests/configs/modules/ssh_auth_key_fingerprints_disable.yaml
2517new file mode 100644
2518index 0000000..715331a
2519--- /dev/null
2520+++ b/tests/cloud_tests/configs/modules/ssh_auth_key_fingerprints_disable.yaml
2521@@ -0,0 +1,11 @@
2522+#
2523+# Disable fingerprint printing
2524+#
2525+cloud_config: |
2526+ #cloud-config
2527+ ssh_genkeytypes: []
2528+ no_ssh_fingerprints: true
2529+collect_scripts:
2530+ syslog: |
2531+ #!/bin/bash
2532+ cat /var/log/syslog
2533diff --git a/tests/cloud_tests/configs/modules/ssh_auth_key_fingerprints_enable.yaml b/tests/cloud_tests/configs/modules/ssh_auth_key_fingerprints_enable.yaml
2534new file mode 100644
2535index 0000000..9d37723
2536--- /dev/null
2537+++ b/tests/cloud_tests/configs/modules/ssh_auth_key_fingerprints_enable.yaml
2538@@ -0,0 +1,14 @@
2539+#
2540+# Print auth keys with different hash than md5
2541+#
2542+cloud_config: |
2543+ #cloud-config
2544+ ssh_genkeytypes:
2545+ - ecdsa
2546+ - ed25519
2547+ ssh_authorized_keys:
2548+ - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDXW9Gg5H7ehjdSc6qDzwNtgCy94XYHhEYlXZMO2+FJrH3wfHGiMfCwOHxcOMt2QiXItULthdeQWS9QjBSSjVRXf6731igFrqPFyS9qBlOQ5D29C4HBXFnQggGVpBNJ82IRJv7szbbe/vpgLBP4kttUza9Dr4e1YM1ln4PRnjfXea6T0m+m1ixNb5432pTXlqYOnNOxSIm1gHgMLxPuDrJvQERDKrSiKSjIdyC9Jd8t2e1tkNLY0stmckVRbhShmcJvlyofHWbc2Ca1mmtP7MlS1VQnfLkvU1IrFwkmaQmaggX6WR6coRJ6XFXdWcq/AI2K6GjSnl1dnnCxE8VCEXBlXgFzad+PMSG4yiL5j8Oo1ZVpkTdgBnw4okGqTYCXyZg6X00As9IBNQfZMFlQXlIo4FiWgj3CO5QHQOyOX6FuEumaU13GnERrSSdp9tCs1Qm3/DG2RSCQBWTfcgMcStIvKqvJ3IjFn0vGLvI3Ampnq9q1SHwmmzAPSdzcMA76HyMUA5VWaBvWHlUxzIM6unxZASnwvuCzpywSEB5J2OF+p6H+cStJwQ32XwmOG8pLp1srlVWpqZI58Du/lzrkPqONphoZx0LDV86w7RUz1ksDzAdcm0tvmNRFMN1a0frDs506oA3aWK0oDk4Nmvk8sXGTYYw3iQSkOvDUUlIsqdaO+w==
2549+collect_scripts:
2550+ syslog: |
2551+ #!/bin/bash
2552+ cat /var/log/syslog
2553diff --git a/tests/cloud_tests/configs/modules/ssh_import_id.yaml b/tests/cloud_tests/configs/modules/ssh_import_id.yaml
2554new file mode 100644
2555index 0000000..6e08a4c
2556--- /dev/null
2557+++ b/tests/cloud_tests/configs/modules/ssh_import_id.yaml
2558@@ -0,0 +1,12 @@
2559+#
2560+# Import a user's ssh key via gh or lp
2561+#
2562+cloud_config: |
2563+ #cloud-config
2564+ ssh_import_id:
2565+ - gh:powersj
2566+ - lp:smoser
2567+collect_scripts:
2568+ auth_keys_ubuntu: |
2569+ #!/bin/bash
2570+ cat /home/ubuntu/.ssh/authorized_keys
2571diff --git a/tests/cloud_tests/configs/modules/ssh_keys_generate.yaml b/tests/cloud_tests/configs/modules/ssh_keys_generate.yaml
2572new file mode 100644
2573index 0000000..3c74ad5
2574--- /dev/null
2575+++ b/tests/cloud_tests/configs/modules/ssh_keys_generate.yaml
2576@@ -0,0 +1,40 @@
2577+#
2578+# SSH keys generated using cloud-init
2579+#
2580+cloud_config: |
2581+ #cloud-config
2582+ ssh_genkeytypes:
2583+ - ecdsa
2584+ - ed25519
2585+ authkey_hash: sha512
2586+collect_scripts:
2587+ auth_keys_root: |
2588+ #!/bin/bash
2589+ cat /root/.ssh/authorized_keys
2590+ auth_keys_ubuntu: |
2591+ #!/bin/bash
2592+ cat /home/ubuntu/ssh/authorized_keys
2593+ dsa_public: |
2594+ #!/bin/bash
2595+ cat /etc/ssh/ssh_host_dsa_key.pub
2596+ dsa_private: |
2597+ #!/bin/bash
2598+ cat /etc/ssh/ssh_host_dsa_key
2599+ rsa_public: |
2600+ #!/bin/bash
2601+ cat /etc/ssh/ssh_host_rsa_key.pub
2602+ rsa_private: |
2603+ #!/bin/bash
2604+ cat /etc/ssh/ssh_host_rsa_key
2605+ ecdsa_public: |
2606+ #!/bin/bash
2607+ cat /etc/ssh/ssh_host_ecdsa_key.pub
2608+ ecdsa_private: |
2609+ #!/bin/bash
2610+ cat /etc/ssh/ssh_host_ecdsa_key
2611+ ed25519_public: |
2612+ #!/bin/bash
2613+ cat /etc/ssh/ssh_host_ed25519_key.pub
2614+ ed25519_private: |
2615+ #!/bin/bash
2616+ cat /etc/ssh/ssh_host_ed25519_key
2617diff --git a/tests/cloud_tests/configs/modules/ssh_keys_provided.yaml b/tests/cloud_tests/configs/modules/ssh_keys_provided.yaml
2618new file mode 100644
2619index 0000000..2590630
2620--- /dev/null
2621+++ b/tests/cloud_tests/configs/modules/ssh_keys_provided.yaml
2622@@ -0,0 +1,100 @@
2623+#
2624+# SSH keys provided via cloud config
2625+#
2626+enabled: False
2627+cloud_config: |
2628+ #cloud-config
2629+ disable_root: false
2630+ ssh_authorized_keys:
2631+ - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDXW9Gg5H7ehjdSc6qDzwNtgCy94XYHhEYlXZMO2+FJrH3wfHGiMfCwOHxcOMt2QiXItULthdeQWS9QjBSSjVRXf6731igFrqPFyS9qBlOQ5D29C4HBXFnQggGVpBNJ82IRJv7szbbe/vpgLBP4kttUza9Dr4e1YM1ln4PRnjfXea6T0m+m1ixNb5432pTXlqYOnNOxSIm1gHgMLxPuDrJvQERDKrSiKSjIdyC9Jd8t2e1tkNLY0stmckVRbhShmcJvlyofHWbc2Ca1mmtP7MlS1VQnfLkvU1IrFwkmaQmaggX6WR6coRJ6XFXdWcq/AI2K6GjSnl1dnnCxE8VCEXBlXgFzad+PMSG4yiL5j8Oo1ZVpkTdgBnw4okGqTYCXyZg6X00As9IBNQfZMFlQXlIo4FiWgj3CO5QHQOyOX6FuEumaU13GnERrSSdp9tCs1Qm3/DG2RSCQBWTfcgMcStIvKqvJ3IjFn0vGLvI3Ampnq9q1SHwmmzAPSdzcMA76HyMUA5VWaBvWHlUxzIM6unxZASnwvuCzpywSEB5J2OF+p6H+cStJwQ32XwmOG8pLp1srlVWpqZI58Du/lzrkPqONphoZx0LDV86w7RUz1ksDzAdcm0tvmNRFMN1a0frDs506oA3aWK0oDk4Nmvk8sXGTYYw3iQSkOvDUUlIsqdaO+w==
2632+ ssh_keys:
2633+ rsa_private: |
2634+ -----BEGIN RSA PRIVATE KEY-----
2635+ MIIEowIBAAKCAQEAtPx6PqN3iSEsnTtibyIEy52Tra8T5fn0ryXyg46Di2NBwdnj
2636+ o8trNv9jenfV/UhmePl58lXjT43wV8OCMl6KsYXyBdegM35NNtono4I4mLLKFMR9
2637+ 9TOtDn6iYcaNenVhF3ZCj9Z2nNOlTrdc0uchHqKMrxLjCRCUrL91Uf+xioTF901Y
2638+ RM+ZqC5lT92yAL76F4qPF+Lq1QtUfNfUIwwvOp5ccDZLPxij0YvyBzubYye9hJHu
2639+ yjbJv78R4JHV+L2WhzSoX3W/6WrxVzeXqFGqH894ccOaC/7tnqSP6V8lIQ6fE2+c
2640+ DurJcpM3CJRgkndGHjtU55Y71YkcdLksSMvezQIDAQABAoIBAQCrU4IJP8dNeaj5
2641+ IpkY6NQvR/jfZqfogYi+MKb1IHin/4rlDfUvPcY9pt8ttLlObjYK+OcWn3Vx/sRw
2642+ 4DOkqNiUGl80Zp1RgZNohHUXlJMtAbrIlAVEk+mTmg7vjfyp2unRQvLZpMRdywBm
2643+ lq95OrCghnG03aUsFJUZPpi5ydnwbA12ma+KHkG0EzaVlhA7X9N6z0K6U+zue2gl
2644+ goMLt/MH0rsYawkHrwiwXaIFQeyV4MJP0vmrZLbFk1bycu9X/xPtTYotWyWo4eKA
2645+ cb05uu04qwexkKHDM0KXtT0JecbTo2rOefFo8Uuab6uJY+fEHNocZ+v1vLA4aOxJ
2646+ ovp1JuXlAoGBAOWYNgKrlTfy5n0sKsNk+1RuL2jHJZJ3HMd0EIt7/fFQN3Fi08Hu
2647+ jtntqD30Wj+DJK8b8Lrt66FruxyEJm5VhVmwkukrLR5ige2f6ftZnoFCmdyy+0zP
2648+ dnPZSUe2H5ZPHa+qthJgHLn+al2P04tGh+1fGHC2PbP+e0Co+/ZRIOxrAoGBAMnN
2649+ IEen9/FRsqvnDd36I8XnJGskVRTZNjylxBmbKcuMWm+gNhOI7gsCAcqzD4BYZjjW
2650+ pLhrt/u9p+l4MOJy6OUUdM/okg12SnJEGryysOcVBcXyrvOfklWnANG4EAH5jt1N
2651+ ftTb1XTxzvWVuR/WJK0B5MZNYM71cumBdUDtPi+nAoGAYmoIXMSnxb+8xNL10aOr
2652+ h9ljQQp8NHgSQfyiSufvRk0YNuYh1vMnEIsqnsPrG2Zfhx/25GmvoxXGssaCorDN
2653+ 5FAn6QK06F1ZTD5L0Y3sv4OI6G1gAuC66ZWuL6sFhyyKkQ4f1WiVZ7SCa3CHQSAO
2654+ i9VDaKz1bf4bXvAQcNj9v9kCgYACSOZCqW4vN0OUmqsXhkt9ZB6Pb/veno70pNPR
2655+ jmYsvcwQU3oJQpWfXkhy6RAV3epaXmPDCsUsfns2M3wqNC7a2R5xdCqjKGGzZX4A
2656+ AO3rz9se4J6Gd5oKijeCKFlWDGNHsibrdgm2pz42nZlY+O21X74dWKbt8O16I1MW
2657+ hxkbJQKBgAXfuen/srVkJgPuqywUYag90VWCpHsuxdn+fZJa50SyZADr+RbiDfH2
2658+ vek8Uo8ap8AEsv4Rfs9opUcUZevLp3g2741eOaidHVLm0l4iLIVl03otGOqvSzs+
2659+ A3tFPEOxauXpzCt8f8eXsz0WQXAgIKW2h8zu5QHjomioU3i27mtE
2660+ -----END RSA PRIVATE KEY-----
2661+ rsa_public: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC0/Ho+o3eJISydO2JvIgTLnZOtrxPl+fSvJfKDjoOLY0HB2eOjy2s2/2N6d9X9SGZ4+XnyVeNPjfBXw4IyXoqxhfIF16Azfk022iejgjiYssoUxH31M60OfqJhxo16dWEXdkKP1nac06VOt1zS5yEeooyvEuMJEJSsv3VR/7GKhMX3TVhEz5moLmVP3bIAvvoXio8X4urVC1R819QjDC86nlxwNks/GKPRi/IHO5tjJ72Eke7KNsm/vxHgkdX4vZaHNKhfdb/pavFXN5eoUaofz3hxw5oL/u2epI/pXyUhDp8Tb5wO6slykzcIlGCSd0YeO1TnljvViRx0uSxIy97N root@xenial-lxd
2662+ dsa_private: |
2663+ -----BEGIN DSA PRIVATE KEY-----
2664+ MIIBuwIBAAKBgQD5Fstc23IVSDe6k4DNP8smPKuEWUvHDTOGaXrhOVAfzZ6+jklP
2665+ 55mzvC7jO53PWWC31hq10xBoWdev0WtcNF9Tv+4bAa1263y51Rqo4GI7xx+xic1d
2666+ mLqqfYijBT9k48J/1tV0cs1Wjs6FP/IJTD/kYVC930JjYQMi722lBnUxsQIVAL7i
2667+ z3fTGKTvSzvW0wQlwnYpS2QFAoGANp+KdyS9V93HgxGQEN1rlj/TSv/a3EVdCKtE
2668+ nQf55aPHxDAVDVw5JtRh4pZbbRV4oGRPc9KOdjo5BU28vSM3Lmhkb+UaaDXwHkgI
2669+ nK193o74DKjADWZxuLyyiKHiMOhxozoxDfjWxs8nz6uqvSW0pr521EwIY6RajbED
2670+ nZ2a3GkCgYEAyoUomNRB6bmpsIfzt8zdtqLP5umIj2uhr9MVPL8/QdbxmJ72Z7pf
2671+ Q2z1B7QAdIBGOlqJXtlau7ABhWK29Efe+99ObyTSSdDc6RCDeAwUmBAiPRQhDH2E
2672+ wExw3doDSCUb28L1B50wBzQ8mC3KXp6C7IkBXWspb16DLHUHFSI8bkICFA5kVUcW
2673+ nCPOXEQsayANi8+Cb7BH
2674+ -----END DSA PRIVATE KEY-----
2675+ dsa_public: ssh-dss AAAAB3NzaC1kc3MAAACBAPkWy1zbchVIN7qTgM0/yyY8q4RZS8cNM4ZpeuE5UB/Nnr6OSU/nmbO8LuM7nc9ZYLfWGrXTEGhZ16/Ra1w0X1O/7hsBrXbrfLnVGqjgYjvHH7GJzV2Yuqp9iKMFP2Tjwn/W1XRyzVaOzoU/8glMP+RhUL3fQmNhAyLvbaUGdTGxAAAAFQC+4s930xik70s71tMEJcJ2KUtkBQAAAIA2n4p3JL1X3ceDEZAQ3WuWP9NK/9rcRV0Iq0SdB/nlo8fEMBUNXDkm1GHillttFXigZE9z0o52OjkFTby9IzcuaGRv5RpoNfAeSAicrX3ejvgMqMANZnG4vLKIoeIw6HGjOjEN+NbGzyfPq6q9JbSmvnbUTAhjpFqNsQOdnZrcaQAAAIEAyoUomNRB6bmpsIfzt8zdtqLP5umIj2uhr9MVPL8/QdbxmJ72Z7pfQ2z1B7QAdIBGOlqJXtlau7ABhWK29Efe+99ObyTSSdDc6RCDeAwUmBAiPRQhDH2EwExw3doDSCUb28L1B50wBzQ8mC3KXp6C7IkBXWspb16DLHUHFSI8bkI= root@xenial-lxd
2676+ ed25519_private: |
2677+ -----BEGIN OPENSSH PRIVATE KEY-----
2678+ b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
2679+ QyNTUxOQAAACDbnQGUruL42aVVsyHeaV5mYNTOhteXao0Nl5DVThJ2+QAAAJgwt+lcMLfp
2680+ XAAAAAtzc2gtZWQyNTUxOQAAACDbnQGUruL42aVVsyHeaV5mYNTOhteXao0Nl5DVThJ2+Q
2681+ AAAEDQlFZpz9q8+/YJHS9+jPAqy2ZT6cGEv8HTB6RZtTjd/dudAZSu4vjZpVWzId5pXmZg
2682+ 1M6G15dqjQ2XkNVOEnb5AAAAD3Jvb3RAeGVuaWFsLWx4ZAECAwQFBg==
2683+ -----END OPENSSH PRIVATE KEY-----
2684+ ed25519_public: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINudAZSu4vjZpVWzId5pXmZg1M6G15dqjQ2XkNVOEnb5 root@xenial-lxd
2685+ ecdsa_private: |
2686+ -----BEGIN EC PRIVATE KEY-----
2687+ MHcCAQEEIDuK+QFc1wmyJY8uDqQVa1qHte30Rk/fdLxGIBkwJAyOoAoGCCqGSM49
2688+ AwEHoUQDQgAEWxLlO+TL8gL91eET9p/HFQbqR1A691AkJgZk3jY5mpZqxgX4vcgb
2689+ 7f/CtXuM6s2svcDJqAeXr6Wk8OJJcMxylA==
2690+ -----END EC PRIVATE KEY-----
2691+ ecdsa_public: ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFsS5Tvky/IC/dXhE/afxxUG6kdQOvdQJCYGZN42OZqWasYF+L3IG+3/wrV7jOrNrL3AyagHl6+lpPDiSXDMcpQ= root@xenial-lxd
2692+collect_scripts:
2693+ auth_keys_root: |
2694+ #!/bin/bash
2695+ cat /root/.ssh/authorized_keys
2696+ auth_keys_ubuntu: |
2697+ #!/bin/bash
2698+ cat /home/ubuntu/ssh/authorized_keys
2699+ dsa_public: |
2700+ #!/bin/bash
2701+ cat /etc/ssh/ssh_host_dsa_key.pub
2702+ dsa_private: |
2703+ #!/bin/bash
2704+ cat /etc/ssh/ssh_host_dsa_key
2705+ rsa_public: |
2706+ #!/bin/bash
2707+ cat /etc/ssh/ssh_host_rsa_key.pub
2708+ rsa_private: |
2709+ #!/bin/bash
2710+ cat /etc/ssh/ssh_host_rsa_key
2711+ ecdsa_public: |
2712+ #!/bin/bash
2713+ cat /etc/ssh/ssh_host_ecdsa_key.pub
2714+ ecdsa_private: |
2715+ #!/bin/bash
2716+ cat /etc/ssh/ssh_host_ecdsa_key
2717+ ed25519_public: |
2718+ #!/bin/bash
2719+ cat /etc/ssh/ssh_host_ed25519_key.pub
2720+ ed25519_private: |
2721+ #!/bin/bash
2722+ cat /etc/ssh/ssh_host_ed25519_key
2723diff --git a/tests/cloud_tests/configs/modules/timezone.yaml b/tests/cloud_tests/configs/modules/timezone.yaml
2724new file mode 100644
2725index 0000000..b0f7413
2726--- /dev/null
2727+++ b/tests/cloud_tests/configs/modules/timezone.yaml
2728@@ -0,0 +1,10 @@
2729+#
2730+# Set system timezone
2731+#
2732+cloud_config: |
2733+ #cloud-config
2734+ timezone: US/Aleutian
2735+collect_scripts:
2736+ timezone: |
2737+ #!/bin/bash
2738+ date +%Z
2739diff --git a/tests/cloud_tests/configs/modules/user_groups.yaml b/tests/cloud_tests/configs/modules/user_groups.yaml
2740new file mode 100644
2741index 0000000..1356efb
2742--- /dev/null
2743+++ b/tests/cloud_tests/configs/modules/user_groups.yaml
2744@@ -0,0 +1,48 @@
2745+#
2746+# Create groups and users with various options
2747+#
2748+cloud_config: |
2749+ #cloud-config
2750+ # Add groups to the system
2751+ groups:
2752+ - secret: [foobar,barfoo]
2753+ - cloud-users
2754+
2755+ # Add users to the system. Users are added after groups are added.
2756+ users:
2757+ - default
2758+ - name: foobar
2759+ gecos: Foo B. Bar
2760+ primary-group: foobar
2761+ groups: users
2762+ expiredate: 2038-01-19
2763+ lock_passwd: false
2764+ passwd: $6$j212wezy$7H/1LT4f9/N3wpgNunhsIqtMj62OKiS3nyNwuizouQc3u7MbYCarYeAHWYPYb2FT.lbioDm2RrkJPb9BZMN1O/
2765+ - name: barfoo
2766+ gecos: Bar B. Foo
2767+ sudo: ALL=(ALL) NOPASSWD:ALL
2768+ groups: cloud-users
2769+ lock_passwd: true
2770+ - name: cloudy
2771+ gecos: Magic Cloud App Daemon User
2772+ inactive: true
2773+ system: true
2774+collect_scripts:
2775+ group_ubuntu: |
2776+ #!/bin/bash
2777+ getent group ubuntu
2778+ group_cloud_users: |
2779+ #!/bin/bash
2780+ getent group cloud-users
2781+ user_ubuntu: |
2782+ #!/bin/bash
2783+ getent passwd ubuntu
2784+ user_foobar: |
2785+ #!/bin/bash
2786+ getent passwd foobar
2787+ user_barfoo: |
2788+ #!/bin/bash
2789+ getent passwd barfoo
2790+ user_cloudy: |
2791+ #!/bin/bash
2792+ getent passwd cloudy
2793diff --git a/tests/cloud_tests/configs/modules/write_files.yaml b/tests/cloud_tests/configs/modules/write_files.yaml
2794new file mode 100644
2795index 0000000..3a3bbce
2796--- /dev/null
2797+++ b/tests/cloud_tests/configs/modules/write_files.yaml
2798@@ -0,0 +1,40 @@
2799+#
2800+# Write various file types
2801+#
2802+cloud_config: |
2803+ #cloud-config
2804+ write_files:
2805+ - encoding: b64
2806+ content: CiMgVGhpcyBmaWxlIGNvbnRyb2xzIHRoZSBzdGF0ZSBvZiBTRUxpbnV4
2807+ owner: root:root
2808+ path: /root/file_b64
2809+ permissions: '0644'
2810+ - content: |
2811+ # My new /root/file_text
2812+
2813+ SMBDOPTIONS="-D"
2814+ path: /root/file_text
2815+ - content: !!binary |
2816+ f0VMRgIBAQAAAAAAAAAAAAIAPgABAAAAwARAAAAAAABAAAAAAAAAAJAVAAAAAAAAAAAAAEAAOAAI
2817+ AEAAHgAdAAYAAAAFAAAAQAAAAAAAAABAAEAAAAAAAEAAQAAAAAAAwAEAAAAAAADAAQAAAAAAAAgA
2818+ AAAAAAAAAwAAAAQAAAAAAgAAAAAAAAACQAAAAAAAAAJAAAAAAAAcAAAAAAAAABwAAAAAAAAAAQAA
2819+ path: /root/file_binary
2820+ permissions: '0555'
2821+ - encoding: gzip
2822+ content: !!binary |
2823+ H4sIAIDb/U8C/1NW1E/KzNMvzuBKTc7IV8hIzcnJVyjPL8pJ4QIA6N+MVxsAAAA=
2824+ path: /root/file_gzip
2825+ permissions: '0755'
2826+collect_scripts:
2827+ file_b64: |
2828+ #!/bin/bash
2829+ file /root/file_b64
2830+ file_text: |
2831+ #!/bin/bash
2832+ file /root/file_text
2833+ file_binary: |
2834+ #!/bin/bash
2835+ file /root/file_binary
2836+ file_gzip: |
2837+ #!/bin/bash
2838+ file /root/file_gzip
2839diff --git a/tests/cloud_tests/images/__init__.py b/tests/cloud_tests/images/__init__.py
2840new file mode 100644
2841index 0000000..997865e
2842--- /dev/null
2843+++ b/tests/cloud_tests/images/__init__.py
2844@@ -0,0 +1,6 @@
2845+def get_image(platform, config):
2846+ """
2847+ get image from platform object using os_name, looking up img_conf in main
2848+ config file
2849+ """
2850+ return platform.get_image(config)
2851diff --git a/tests/cloud_tests/images/base.py b/tests/cloud_tests/images/base.py
2852new file mode 100644
2853index 0000000..02d928c
2854--- /dev/null
2855+++ b/tests/cloud_tests/images/base.py
2856@@ -0,0 +1,60 @@
2857+class Image(object):
2858+ """
2859+ Base class for images
2860+ """
2861+ platform_name = None
2862+
2863+ def __init__(self, name, config, platform):
2864+ """
2865+ setup
2866+ """
2867+ self.name = name
2868+ self.config = config
2869+ self.platform = platform
2870+
2871+ def __str__(self):
2872+ """
2873+ a brief description of the image
2874+ """
2875+ return '-'.join((self.properties['os'], self.properties['release']))
2876+
2877+ @property
2878+ def properties(self):
2879+ """
2880+ {} containing: 'arch', 'os', 'version', 'release'
2881+ """
2882+ raise NotImplementedError
2883+
2884+ # FIXME: instead of having execute and push_file and other instance methods
2885+ # here which pass through to a hidden instance, it might be better
2886+ # to expose an instance that the image can be modified through
2887+ def execute(self, command, stdin=None, stdout=None, stderr=None, env={}):
2888+ """
2889+ execute command in image, modifying image
2890+ """
2891+ raise NotImplementedError
2892+
2893+ def push_file(self, local_path, remote_path):
2894+ """
2895+ copy file at 'local_path' to instance at 'remote_path', modifying image
2896+ """
2897+ raise NotImplementedError
2898+
2899+ def run_script(self, script):
2900+ """
2901+ run script in image, modifying image
2902+ return_value: script output
2903+ """
2904+ raise NotImplementedError
2905+
2906+ def snapshot(self):
2907+ """
2908+ create snapshot of image, block until done
2909+ """
2910+ raise NotImplementedError
2911+
2912+ def destroy(self):
2913+ """
2914+ clean up data associated with image
2915+ """
2916+ pass
2917diff --git a/tests/cloud_tests/images/lxd.py b/tests/cloud_tests/images/lxd.py
2918new file mode 100644
2919index 0000000..b06c980
2920--- /dev/null
2921+++ b/tests/cloud_tests/images/lxd.py
2922@@ -0,0 +1,88 @@
2923+from tests.cloud_tests.images import base
2924+from tests.cloud_tests.snapshots import lxd as lxd_snapshot
2925+
2926+
2927+class LXDImage(base.Image):
2928+ """
2929+ LXD backed image
2930+ """
2931+ platform_name = "lxd"
2932+
2933+ def __init__(self, name, config, platform, pylxd_image):
2934+ """
2935+ setup
2936+ """
2937+ self.platform = platform
2938+ self._pylxd_image = pylxd_image
2939+ self._instance = None
2940+ super(LXDImage, self).__init__(name, config, platform)
2941+
2942+ @property
2943+ def pylxd_image(self):
2944+ self._pylxd_image.sync()
2945+ return self._pylxd_image
2946+
2947+ @property
2948+ def instance(self):
2949+ if not self._instance:
2950+ self._instance = self.platform.launch_container(
2951+ image=self.pylxd_image.fingerprint,
2952+ image_desc=str(self), use_desc='image-modification')
2953+ self._instance.start(wait=True, wait_time=self.config.get('timeout'))
2954+ return self._instance
2955+
2956+ @property
2957+ def properties(self):
2958+ """
2959+ {} containing: 'arch', 'os', 'version', 'release'
2960+ """
2961+ properties = self.pylxd_image.properties
2962+ return {
2963+ 'arch': properties.get('architecture'),
2964+ 'os': properties.get('os'),
2965+ 'version': properties.get('version'),
2966+ 'release': properties.get('release'),
2967+ }
2968+
2969+ def execute(self, *args, **kwargs):
2970+ """
2971+ execute command in image, modifying image
2972+ """
2973+ return self.instance.execute(*args, **kwargs)
2974+
2975+ def push_file(self, local_path, remote_path):
2976+ """
2977+ copy file at 'local_path' to instance at 'remote_path', modifying image
2978+ """
2979+ return self.instance.push_file(local_path, remote_path)
2980+
2981+ def run_script(self, script):
2982+ """
2983+ run script in image, modifying image
2984+ return_value: script output
2985+ """
2986+ return self.instance.run_script(script)
2987+
2988+ def snapshot(self):
2989+ """
2990+ create snapshot of image, block until done
2991+ """
2992+ # clone current instance, start and freeze clone
2993+ instance = self.platform.launch_container(
2994+ container=self.instance.name, image_desc=str(self),
2995+ use_desc='snapshot')
2996+ instance.start(wait=True, wait_time=self.config.get('timeout'))
2997+ if self.config.get('boot_clean_script'):
2998+ instance.run_script(self.config.get('boot_clean_script'))
2999+ instance.freeze()
3000+ return lxd_snapshot.LXDSnapshot(
3001+ self.properties, self.config, self.platform, instance)
3002+
3003+ def destroy(self):
3004+ """
3005+ clean up data associated with image
3006+ """
3007+ if self._instance:
3008+ self._instance.destroy()
3009+ self.pylxd_image.delete(wait=True)
3010+ super(LXDImage, self).destroy()
3011diff --git a/tests/cloud_tests/instances/__init__.py b/tests/cloud_tests/instances/__init__.py
3012new file mode 100644
3013index 0000000..fd9cbaf
3014--- /dev/null
3015+++ b/tests/cloud_tests/instances/__init__.py
3016@@ -0,0 +1,5 @@
3017+def get_instance(snapshot, *args, **kwargs):
3018+ """
3019+ get instance from snapshot
3020+ """
3021+ return snapshot.launch(*args, **kwargs)
3022diff --git a/tests/cloud_tests/instances/base.py b/tests/cloud_tests/instances/base.py
3023new file mode 100644
3024index 0000000..fd5bd41
3025--- /dev/null
3026+++ b/tests/cloud_tests/instances/base.py
3027@@ -0,0 +1,116 @@
3028+import os
3029+import uuid
3030+
3031+
3032+class Instance(object):
3033+ """
3034+ Base instance object
3035+ """
3036+ platform_name = None
3037+
3038+ def __init__(self, name):
3039+ """
3040+ setup
3041+ """
3042+ self.name = name
3043+
3044+ def execute(self, command, stdin=None, stdout=None, stderr=None, env={}):
3045+ """
3046+ command: the command to execute as root inside the image
3047+ stdin, stderr, stdout: file handles
3048+ env: environment variables
3049+
3050+ Execute assumes functional networking and execution as root with the
3051+ target filesystem being available at /.
3052+
3053+ return_value: tuple containing stdout data, stderr data, exit code
3054+ """
3055+ raise NotImplementedError
3056+
3057+ def read_data(self, remote_path, encode=False):
3058+ """
3059+ read_data from instance filesystem
3060+ remote_path: path in instance
3061+ decode: return as string
3062+ return_value: data as str or bytes
3063+ """
3064+ raise NotImplementedError
3065+
3066+ def write_data(self, remote_path, data):
3067+ """
3068+ write data to instance filesystem
3069+ remote_path: path in instance
3070+ data: data to write, either str or bytes
3071+ """
3072+ raise NotImplementedError
3073+
3074+ def pull_file(self, remote_path, local_path):
3075+ """
3076+ copy file at 'remote_path', from instance to 'local_path'
3077+ """
3078+ with open(local_path, 'wb') as fp:
3079+ fp.write(self.read_data(remote_path), encode=True)
3080+
3081+ def push_file(self, local_path, remote_path):
3082+ """
3083+ copy file at 'local_path' to instance at 'remote_path'
3084+ """
3085+ with open(local_path, 'rb') as fp:
3086+ self.write_data(remote_path, fp.read())
3087+
3088+ def run_script(self, script):
3089+ """
3090+ run script in target and return stdout
3091+ """
3092+ script_path = os.path.join('/tmp', str(uuid.uuid1()))
3093+ self.write_data(script_path, script)
3094+ (out, err, exit_code) = self.execute(['/bin/bash', script_path])
3095+ return out
3096+
3097+ def console_log(self):
3098+ """
3099+ return_value: bytes of this instance’s console
3100+ """
3101+ raise NotImplementedError
3102+
3103+ def reboot(self, wait=True):
3104+ """
3105+ reboot instance
3106+ """
3107+ raise NotImplementedError
3108+
3109+ def shutdown(self, wait=True):
3110+ """
3111+ shutdown instance
3112+ """
3113+ raise NotImplementedError
3114+
3115+ def start(self, wait=True):
3116+ """
3117+ start instance
3118+ """
3119+ raise NotImplementedError
3120+
3121+ def destroy(self):
3122+ """
3123+ clean up instance
3124+ """
3125+ pass
3126+
3127+ def _wait_for_cloud_init(self, wait_time):
3128+ """
3129+ wait until system has fully booted and cloud-init has finished
3130+ """
3131+ if not wait_time:
3132+ return
3133+
3134+ found_msg = 'found'
3135+ cmd = ('for ((i=0;i<{wait};i++)); do [ -f "{file}" ] && '
3136+ '{{ echo "{msg}";break; }} || sleep 1; done').format(
3137+ file='/run/cloud-init/result.json',
3138+ wait=wait_time, msg=found_msg)
3139+
3140+ (out, err, exit) = self.execute(['/bin/bash', '-c', cmd])
3141+ if out.strip() != found_msg:
3142+ raise OSError('timeout: after {}s, cloud-init has not started'
3143+ .format(wait_time))
3144diff --git a/tests/cloud_tests/instances/lxd.py b/tests/cloud_tests/instances/lxd.py
3145new file mode 100644
3146index 0000000..39bd5e5
3147--- /dev/null
3148+++ b/tests/cloud_tests/instances/lxd.py
3149@@ -0,0 +1,117 @@
3150+from tests.cloud_tests.instances import base
3151+
3152+
3153+class LXDInstance(base.Instance):
3154+ """
3155+ LXD container backed instance
3156+ """
3157+ platform_name = "lxd"
3158+
3159+ def __init__(self, name, platform, pylxd_container):
3160+ """
3161+ setup
3162+ """
3163+ self.platform = platform
3164+ self._pylxd_container = pylxd_container
3165+ super(LXDInstance, self).__init__(name)
3166+
3167+ @property
3168+ def pylxd_container(self):
3169+ self._pylxd_container.sync()
3170+ return self._pylxd_container
3171+
3172+ def execute(self, command, stdin=None, stdout=None, stderr=None, env={}):
3173+ """
3174+ command: the command to execute as root inside the image
3175+ stdin, stderr, stdout: file handles
3176+ env: environment variables
3177+
3178+ Execute assumes functional networking and execution as root with the
3179+ target filesystem being available at /.
3180+
3181+ return_value: tuple containing stdout data, stderr data, exit code
3182+ """
3183+ # TODO: the pylxd api handler for container.execute needs to be
3184+ # extended to properly pass in stdin
3185+ # TODO: the pylxd api handler for container.execute needs to be
3186+ # extended to get the return code, for now just use 0
3187+ self.start()
3188+ if stdin:
3189+ raise NotImplementedError
3190+ res = self.pylxd_container.execute(command, environment=env)
3191+ for (f, data) in (i for i in zip((stdout, stderr), res) if i[0]):
3192+ f.write(data)
3193+ return res + (0,)
3194+
3195+ def read_data(self, remote_path, decode=False):
3196+ """
3197+ read data from instance filesystem
3198+ remote_path: path in instance
3199+ decode: return as string
3200+ return_value: data as str or bytes
3201+ """
3202+ data = self.pylxd_container.files.get(remote_path)
3203+ return data.decode() if decode and isinstance(data, bytes) else data
3204+
3205+ def write_data(self, remote_path, data):
3206+ """
3207+ write data to instance filesystem
3208+ remote_path: path in instance
3209+ data: data to write, either str or bytes
3210+ """
3211+ self.pylxd_container.files.put(remote_path, data)
3212+
3213+ def console_log(self):
3214+ """
3215+ return_value: bytes of this instance’s console
3216+ """
3217+ raise NotImplementedError
3218+
3219+ def reboot(self, wait=True):
3220+ """
3221+ reboot instance
3222+ """
3223+ self.shutdown(wait=wait)
3224+ self.start(wait=wait)
3225+
3226+ def shutdown(self, wait=True):
3227+ """
3228+ shutdown instance
3229+ """
3230+ if self.pylxd_container.status != 'Stopped':
3231+ self.pylxd_container.stop(wait=wait)
3232+
3233+ def start(self, wait=True, wait_time=None):
3234+ """
3235+ start instance
3236+ """
3237+ if self.pylxd_container.status != 'Running':
3238+ self.pylxd_container.start(wait=wait)
3239+ if wait and isinstance(wait_time, int):
3240+ self._wait_for_cloud_init(wait_time)
3241+
3242+ def freeze(self):
3243+ """
3244+ freeze instance
3245+ """
3246+ if self.pylxd_container.status != 'Frozen':
3247+ self.pylxd_container.freeze(wait=True)
3248+
3249+ def unfreeze(self):
3250+ """
3251+ unfreeze instance
3252+ """
3253+ if self.pylxd_container.status == 'Frozen':
3254+ self.pylxd_container.unfreeze(wait=True)
3255+
3256+ def destroy(self):
3257+ """
3258+ clean up instance
3259+ """
3260+ self.unfreeze()
3261+ self.shutdown()
3262+ self.pylxd_container.delete(wait=True)
3263+ if self.platform.container_exists(self.name):
3264+ raise OSError('container {} was not properly removed'
3265+ .format(self.name))
3266+ super(LXDInstance, self).destroy()
3267diff --git a/tests/cloud_tests/manage.py b/tests/cloud_tests/manage.py
3268new file mode 100644
3269index 0000000..55d7dd9
3270--- /dev/null
3271+++ b/tests/cloud_tests/manage.py
3272@@ -0,0 +1,71 @@
3273+from tests.cloud_tests.config import VERIFY_EXT
3274+from tests.cloud_tests import (config, util)
3275+from tests.cloud_tests import TESTCASES_DIR
3276+
3277+import os
3278+import textwrap
3279+
3280+_verifier_fmt = textwrap.dedent(
3281+ """
3282+ \"\"\"cloud-init Integration Test Verify Script\"\"\"
3283+ from tests.cloud_tests.testcases import base
3284+
3285+
3286+ class {test_class}(base.CloudTestCase):
3287+ \"\"\"
3288+ Name: {test_name}
3289+ Category: {test_category}
3290+ Description: {test_description}
3291+ \"\"\"
3292+ pass
3293+ """
3294+).lstrip()
3295+_config_fmt = textwrap.dedent(
3296+ """
3297+ #
3298+ # Name: {test_name}
3299+ # Category: {test_category}
3300+ # Description: {test_description}
3301+ #
3302+ {config}
3303+ """
3304+).strip()
3305+
3306+
3307+def write_testcase_config(args, fmt_args, testcase_file):
3308+ """
3309+ write the testcase config file
3310+ """
3311+ testcase_config = {'enabled': args.enable, 'collect_scripts': {}}
3312+ if args.config:
3313+ testcase_config['cloud_config'] = args.config
3314+ fmt_args['config'] = util.yaml_format(testcase_config)
3315+ util.write_file(testcase_file, _config_fmt.format(**fmt_args), omode='w')
3316+
3317+
3318+def write_verifier(args, fmt_args, verifier_file):
3319+ """
3320+ write the verifier script
3321+ """
3322+ fmt_args['test_class'] = 'Test{}'.format(
3323+ config.name_sanatize(fmt_args['test_name']).title())
3324+ util.write_file(verifier_file, _verifier_fmt.format(**fmt_args), omode='w')
3325+
3326+
3327+def create(args):
3328+ """
3329+ create a new testcase
3330+ """
3331+ (test_category, test_name) = args.name.split('/')
3332+ fmt_args = {'test_name': test_name, 'test_category': test_category,
3333+ 'test_description': str(args.description)}
3334+
3335+ testcase_file = config.name_to_path(args.name)
3336+ verifier_file = os.path.join(
3337+ TESTCASES_DIR, test_category,
3338+ config.name_sanatize(test_name) + VERIFY_EXT)
3339+
3340+ write_testcase_config(args, fmt_args, testcase_file)
3341+ write_verifier(args, fmt_args, verifier_file)
3342+
3343+ return 0
3344diff --git a/tests/cloud_tests/platforms.yaml b/tests/cloud_tests/platforms.yaml
3345new file mode 100644
3346index 0000000..ffed29a
3347--- /dev/null
3348+++ b/tests/cloud_tests/platforms.yaml
3349@@ -0,0 +1,15 @@
3350+# ============================= Platform Config ===============================
3351+default_platform_config:
3352+ # all disabled by default
3353+ enabled: false
3354+ # maximum time to retrieve image
3355+ get_image_timeout: 300
3356+ # maximum time to create instance (before waiting for cloud-init)
3357+ create_instance_timeout: 60
3358+
3359+platforms:
3360+ lxd:
3361+ enabled: true
3362+ get_image_timeout: 600
3363+ ec2: {}
3364+ azure: {}
3365diff --git a/tests/cloud_tests/platforms/__init__.py b/tests/cloud_tests/platforms/__init__.py
3366new file mode 100644
3367index 0000000..52e453f
3368--- /dev/null
3369+++ b/tests/cloud_tests/platforms/__init__.py
3370@@ -0,0 +1,15 @@
3371+from tests.cloud_tests.platforms import lxd
3372+
3373+PLATFORMS = {
3374+ 'lxd': lxd.LXDPlatform,
3375+}
3376+
3377+
3378+def get_platform(platform_name, config):
3379+ """
3380+ Get the platform object for 'platform_name' and init
3381+ """
3382+ platform_cls = PLATFORMS.get(platform_name)
3383+ if not platform_cls:
3384+ raise ValueError('invalid platform name: {}'.format(platform_name))
3385+ return platform_cls(config)
3386diff --git a/tests/cloud_tests/platforms/base.py b/tests/cloud_tests/platforms/base.py
3387new file mode 100644
3388index 0000000..3679e85
3389--- /dev/null
3390+++ b/tests/cloud_tests/platforms/base.py
3391@@ -0,0 +1,48 @@
3392+class Platform(object):
3393+ """
3394+ Base class for platforms
3395+ """
3396+ platform_name = None
3397+
3398+ def __init__(self, config):
3399+ """
3400+ Set up platform
3401+ """
3402+ self.config = config
3403+
3404+ def get_image(self, img_conf):
3405+ """
3406+ Get image using 'img_conf', where img_conf is a dict containing all
3407+ image configuration parameters
3408+
3409+ in this dict there must be a 'platform_ident' key containing
3410+ configuration for identifying each image on a per platform basis
3411+
3412+ see implementations for get_image() for details about the contents
3413+ of the platform's config entry
3414+
3415+ note: see 'releases' main_config.yaml for example entries
3416+
3417+ img_conf: configuration for image
3418+ return_value: cloud_tests.images instance
3419+ """
3420+ raise NotImplementedError
3421+
3422+ def destroy(self):
3423+ """
3424+ Clean up platform data
3425+ """
3426+ pass
3427+
3428+ def _extract_img_platform_config(self, img_conf):
3429+ """
3430+ extract platform configuration for current platform from img_conf
3431+ """
3432+ platform_ident = img_conf.get('platform_ident')
3433+ if not platform_ident:
3434+ raise ValueError('invalid img_conf, missing \'platform_ident\'')
3435+ ident = platform_ident.get(self.platform_name)
3436+ if not ident:
3437+ raise ValueError('img_conf: {} missing config for platform {}'
3438+ .format(img_conf, self.platform_name))
3439+ return ident
3440diff --git a/tests/cloud_tests/platforms/lxd.py b/tests/cloud_tests/platforms/lxd.py
3441new file mode 100644
3442index 0000000..414a162
3443--- /dev/null
3444+++ b/tests/cloud_tests/platforms/lxd.py
3445@@ -0,0 +1,93 @@
3446+from pylxd import (Client, exceptions)
3447+
3448+from tests.cloud_tests.images import lxd as lxd_image
3449+from tests.cloud_tests.instances import lxd as lxd_instance
3450+from tests.cloud_tests.platforms import base
3451+from tests.cloud_tests import util
3452+
3453+DEFAULT_SSTREAMS_SERVER = "https://images.linuxcontainers.org:8443"
3454+
3455+
3456+class LXDPlatform(base.Platform):
3457+ """
3458+ Lxd test platform
3459+ """
3460+ platform_name = 'lxd'
3461+
3462+ def __init__(self, config):
3463+ """
3464+ Set up platform
3465+ """
3466+ super(LXDPlatform, self).__init__(config)
3467+ # TODO: allow configuration of remote lxd host via env variables
3468+ # set up lxd connection
3469+ self.client = Client()
3470+
3471+ def get_image(self, img_conf):
3472+ """
3473+ Get image
3474+ img_conf: dict containing config for image. platform_ident must have:
3475+ alias: alias to use for simplestreams server
3476+ sstreams_server: simplestreams server to use, or None for default
3477+ return_value: cloud_tests.images instance
3478+ """
3479+ lxd_conf = self._extract_img_platform_config(img_conf)
3480+ image = self.client.images.create_from_simplestreams(
3481+ lxd_conf.get('sstreams_server', DEFAULT_SSTREAMS_SERVER),
3482+ lxd_conf['alias'])
3483+ return lxd_image.LXDImage(
3484+ image.properties['description'], img_conf, self, image)
3485+
3486+ def launch_container(self, image=None, container=None, ephemeral=False,
3487+ config=None, block=True,
3488+ image_desc=None, use_desc=None):
3489+ """
3490+ launch a container
3491+ image: image fingerprint to launch from
3492+ container: container to copy
3493+ ephemeral: delete image after first shutdown
3494+ config: config options for instance as dict
3495+ block: wait until container created
3496+ image_desc: description of image being launched
3497+ use_desc: description of container's use
3498+ return_value: cloud_tests.instances instance
3499+ """
3500+ if not (image or container):
3501+ raise ValueError("either image or container must be specified")
3502+ container = self.client.containers.create({
3503+ 'name': util.gen_instance_name(image_desc=image_desc,
3504+ use_desc=use_desc,
3505+ used_list=self.list_containers()),
3506+ 'ephemeral': bool(ephemeral),
3507+ 'config': config if isinstance(config, dict) else {},
3508+ 'source': ({'type': 'image', 'fingerprint': image} if image else
3509+ {'type': 'copy', 'source': container})
3510+ }, wait=block)
3511+ return lxd_instance.LXDInstance(container.name, self, container)
3512+
3513+ def container_exists(self, container_name):
3514+ """
3515+ check if container with name 'container_name' exists
3516+ return_value: True if exists else False
3517+ """
3518+ res = True
3519+ try:
3520+ self.client.containers.get(container_name)
3521+ except exceptions.LXDAPIException as e:
3522+ res = False
3523+ if e.response.status_code != 404:
3524+ raise
3525+ return res
3526+
3527+ def list_containers(self):
3528+ """
3529+ list names of all containers
3530+ return_value: list of names
3531+ """
3532+ return [container.name for container in self.client.containers.all()]
3533+
3534+ def destroy(self):
3535+ """
3536+ Clean up platform data
3537+ """
3538+ super(LXDPlatform, self).destroy()
3539diff --git a/tests/cloud_tests/releases.yaml b/tests/cloud_tests/releases.yaml
3540new file mode 100644
3541index 0000000..a3a6e4d
3542--- /dev/null
3543+++ b/tests/cloud_tests/releases.yaml
3544@@ -0,0 +1,77 @@
3545+# ============================= Release Config ================================
3546+default_release_config:
3547+ # all are disabled by default
3548+ enabled: false
3549+ # timeout for booting image and running cloud init
3550+ timeout: 120
3551+ # platform_ident values for the image, with data to identify the image
3552+ # on that platform. see platforms.base for more information
3553+ platform_ident: {}
3554+ # a script to run after a boot that is used to modify an image, before
3555+ # making a snapshot of the image. may be useful for removing data left
3556+ # behind from cloud-init booting, such as logs, to ensure that data from
3557+ # snapshot.launch() will not include a cloud-init.log from a boot used to
3558+ # create the snapshot, if cloud-init has not run
3559+ boot_clean_script: |
3560+ #!/bin/bash
3561+ rm -rf /var/log/cloud-init.log /var/log/cloud-init-output.log \
3562+ /var/lib/cloud/ /run/cloud-init/ /var/log/syslog
3563+
3564+releases:
3565+ trusty:
3566+ enabled: true
3567+ platform_ident:
3568+ lxd:
3569+ # if sstreams_server is omitted, default is used, defined in
3570+ # tests.cloud_tests.platforms.lxd.DEFAULT_SSTREAMS_SERVER as:
3571+ # sstreams_server: https://us.images.linuxcontainers.org:8443
3572+ #alias: ubuntu/trusty/default
3573+ alias: t
3574+ sstreams_server: https://cloud-images.ubuntu.com/daily
3575+ xenial:
3576+ enabled: true
3577+ platform_ident:
3578+ lxd:
3579+ #alias: ubuntu/xenial/default
3580+ alias: x
3581+ sstreams_server: https://cloud-images.ubuntu.com/daily
3582+ yakkety:
3583+ enabled: true
3584+ platform_ident:
3585+ lxd:
3586+ #alias: ubuntu/yakkety/default
3587+ alias: y
3588+ sstreams_server: https://cloud-images.ubuntu.com/daily
3589+ zesty:
3590+ enabled: true
3591+ platform_ident:
3592+ lxd:
3593+ #alias: ubuntu/zesty/default
3594+ alias: z
3595+ sstreams_server: https://cloud-images.ubuntu.com/daily
3596+ jessie:
3597+ platform_ident:
3598+ lxd:
3599+ alias: debian/jessie/default
3600+ sid:
3601+ platform_ident:
3602+ lxd:
3603+ alias: debian/sid/default
3604+ stretch:
3605+ platform_ident:
3606+ lxd:
3607+ alias: debian/stretch/default
3608+ wheezy:
3609+ platform_ident:
3610+ lxd:
3611+ alias: debian/wheezy/default
3612+ centos70:
3613+ timeout: 180
3614+ platform_ident:
3615+ lxd:
3616+ alias: centos/7/default
3617+ centos66:
3618+ timeout: 180
3619+ platform_ident:
3620+ lxd:
3621+ alias: centos/6/default
3622diff --git a/tests/cloud_tests/setup_image.py b/tests/cloud_tests/setup_image.py
3623new file mode 100644
3624index 0000000..488c7fa
3625--- /dev/null
3626+++ b/tests/cloud_tests/setup_image.py
3627@@ -0,0 +1,191 @@
3628+from tests.cloud_tests import LOG
3629+from tests.cloud_tests import stage, util
3630+
3631+from functools import partial
3632+import os
3633+
3634+
3635+def install_deb(args, image):
3636+ """
3637+ install deb into image
3638+ args: cmdline arguments, must contain --deb
3639+ image: cloud_tests.images instance to operate on
3640+ return_value: None, may raise errors
3641+ """
3642+ # ensure system is compatible with package format
3643+ os_family = util.get_os_family(image.properties['os'])
3644+ if os_family != 'debian':
3645+ raise NotImplementedError('install deb: {} not supported on os '
3646+ 'family: {}'.format(args.deb, os_family))
3647+
3648+ # install deb
3649+ LOG.debug('installing deb: %s into target', args.deb)
3650+ remote_path = os.path.join('/tmp', os.path.basename(args.deb))
3651+ image.push_file(args.deb, remote_path)
3652+ (out, err, exit) = image.execute(['dpkg', '-i', remote_path])
3653+ if exit != 0:
3654+ raise OSError('failed install deb: {}\n\tstdout: {}\n\tstderr: {}'
3655+ .format(args.deb, out, err))
3656+
3657+ # check installed deb version matches package
3658+ fmt = ['-W', "--showformat='${Version}'"]
3659+ (out, err, exit) = image.execute(['dpkg-deb'] + fmt + [remote_path])
3660+ expected_version = out.strip()
3661+ (out, err, exit) = image.execute(['dpkg-query'] + fmt + ['cloud-init'])
3662+ found_version = out.strip()
3663+ if expected_version != found_version:
3664+ raise OSError('install deb version "{}" does not match expected "{}"'
3665+ .format(found_version, expected_version))
3666+
3667+ LOG.debug('successfully installed: %s, version: %s', args.deb,
3668+ found_version)
3669+
3670+
3671+def install_rpm(args, image):
3672+ """
3673+ install rpm into image
3674+ args: cmdline arguments, must contain --rpm
3675+ image: cloud_tests.images instance to operate on
3676+ return_value: None, may raise errors
3677+ """
3678+ # ensure system is compatible with package format
3679+ os_family = util.get_os_family(image.properties['os'])
3680+ if os_family not in ['redhat', 'sles']:
3681+ raise NotImplementedError('install rpm: {} not supported on os '
3682+ 'family: {}'.format(args.rpm, os_family))
3683+
3684+ # install rpm
3685+ LOG.debug('installing rpm: %s into target', args.rpm)
3686+ remote_path = os.path.join('/tmp', os.path.basename(args.rpm))
3687+ image.push_file(args.rpm, remote_path)
3688+ (out, err, exit) = image.execute(['rpm', '-U', remote_path])
3689+ if exit != 0:
3690+ raise OSError('failed to install rpm: {}\n\tstdout: {}\n\tstderr: {}'
3691+ .format(args.rpm, out, err))
3692+
3693+ fmt = ['--queryformat', '"%{VERSION}"']
3694+ (out, err, exit) = image.execute(['rpm', '-q'] + fmt + [remote_path])
3695+ expected_version = out.strip()
3696+ (out, err, exit) = image.execute(['rpm', '-q'] + fmt + ['cloud-init'])
3697+ found_version = out.strip()
3698+ if expected_version != found_version:
3699+ raise OSError('install rpm version "{}" does not match expected "{}"'
3700+ .format(found_version, expected_version))
3701+
3702+ LOG.debug('successfully installed: %s, version %s', args.rpm,
3703+ found_version)
3704+
3705+
3706+def upgrade(args, image):
3707+ """
3708+ run the system's upgrade command
3709+ args: cmdline arguments
3710+ image: cloud_tests.images instance to operate on
3711+ return_value: None, may raise errors
3712+ """
3713+ # determine appropriate upgrade command for os_family
3714+ # TODO: maybe use cloudinit.distros for this?
3715+ os_family = util.get_os_family(image.properties['os'])
3716+ if os_family == 'debian':
3717+ cmd = 'apt-get update && apt-get upgrade --yes'
3718+ elif os_family == 'redhat':
3719+ cmd = 'yum upgrade --assumeyes'
3720+ else:
3721+ raise NotImplementedError('upgrade command not configured for distro '
3722+ 'from family: {}'.format(os_family))
3723+
3724+ # upgrade system
3725+ LOG.debug('upgrading system')
3726+ (out, err, exit) = image.execute(['/bin/sh', '-c', cmd])
3727+ if exit != 0:
3728+ raise OSError('failed to upgrade system\n\tstdout: {}\n\tstderr:{}'
3729+ .format(out, err))
3730+
3731+
3732+def run_script(args, image):
3733+ """
3734+ run a script in the target image
3735+ args: cmdline arguments, must contain --script
3736+ image: cloud_tests.images instance to operate on
3737+ return_value: None, may raise errors
3738+ """
3739+ # TODO: get exit status back from script and add error handling here
3740+ LOG.debug('running setup image script in target image')
3741+ image.run_script(args.script)
3742+
3743+
3744+def enable_ppa(args, image):
3745+ """
3746+ enable a ppa in the target image
3747+ args: cmdline arguments, must contain --ppa
3748+ image: cloud_tests.image instance to operate on
3749+ return_value: None, may raise errors
3750+ """
3751+ # ppa only supported on ubuntu (maybe debian?)
3752+ if image.properties['os'] != 'ubuntu':
3753+ raise NotImplementedError('enabling a ppa is only available on ubuntu')
3754+
3755+ # add ppa with add-apt-repository and update
3756+ ppa = 'ppa:{}'.format(args.ppa)
3757+ LOG.debug('enabling %s', ppa)
3758+ cmd = 'add-apt-repository --yes {} && apt-get update'.format(ppa)
3759+ (out, err, exit) = image.execute(['/bin/sh', '-c', cmd])
3760+ if exit != 0:
3761+ raise OSError('enable ppa for {} failed\n\tstdout: {}\n\tstderr: {}'
3762+ .format(ppa, out, err))
3763+
3764+
3765+def enable_repo(args, image):
3766+ """
3767+ enable a repository in the target image
3768+ args: cmdline arguments, must contain --repo
3769+ image: cloud_tests.image instance to operate on
3770+ return_value: None, may raise errors
3771+ """
3772+ # find enable repo command for the distro
3773+ os_family = util.get_os_family(image.properties['os'])
3774+ if os_family == 'debian':
3775+ cmd = ('echo "{}" >> "/etc/apt/sources.list" '.format(args.repo) +
3776+ '&& apt-get update')
3777+ elif os_family == 'centos':
3778+ cmd = 'yum-config-manager --add-repo="{}"'.format(args.repo)
3779+ else:
3780+ raise NotImplementedError('enable repo command not configured for '
3781+ 'distro from family: {}'.format(os_family))
3782+
3783+ LOG.debug('enabling repo: "%s"', args.repo)
3784+ (out, err, exit) = image.execute(['/bin/sh', '-c', cmd])
3785+ if exit != 0:
3786+ raise OSError('enable repo {} failed\n\tstdout: {}\n\tstderr: {}'
3787+ .format(args.repo, out, err))
3788+
3789+
3790+def setup_image(args, image):
3791+ """
3792+ set up image as specified in args
3793+ args: cmdline arguments
3794+ image: cloud_tests.image instance to operate on
3795+ return_value: tuple of results and fail count
3796+ """
3797+ # mapping of setup cmdline arg name to setup function
3798+ # represented as a tuple rather than a dict or odict as lookup by name not
3799+ # needed, and order is important as --script and --upgrade go at the end
3800+ handlers = (
3801+ # arg handler description
3802+ ('deb', install_deb, 'setup func for --deb, install deb'),
3803+ ('rpm', install_rpm, 'setup func for --rpm, install rpm'),
3804+ ('repo', enable_repo, 'setup func for --repo, enable repo'),
3805+ ('ppa', enable_ppa, 'setup func for --ppa, enable ppa'),
3806+ ('script', run_script, 'setup func for --script, run script'),
3807+ ('upgrade', upgrade, 'setup func for --upgrade, upgrade pkgs'),
3808+ )
3809+
3810+ # determine which setup functions needed
3811+ calls = [partial(stage.run_single, desc, partial(func, args, image))
3812+ for name, func, desc in handlers if getattr(args, name, None)]
3813+
3814+ image_name = 'image: distro={}, release={}'.format(
3815+ image.properties['os'], image.properties['release'])
3816+ LOG.info('setting up %s', image_name)
3817+ return stage.run_stage('set up for {}'.format(image_name), calls,
3818+ continue_after_error=False)
3819diff --git a/tests/cloud_tests/snapshots/__init__.py b/tests/cloud_tests/snapshots/__init__.py
3820new file mode 100644
3821index 0000000..328275c
3822--- /dev/null
3823+++ b/tests/cloud_tests/snapshots/__init__.py
3824@@ -0,0 +1,5 @@
3825+def get_snapshot(image):
3826+ """
3827+ get snapshot from image
3828+ """
3829+ return image.snapshot()
3830diff --git a/tests/cloud_tests/snapshots/base.py b/tests/cloud_tests/snapshots/base.py
3831new file mode 100644
3832index 0000000..5c4a6ce
3833--- /dev/null
3834+++ b/tests/cloud_tests/snapshots/base.py
3835@@ -0,0 +1,39 @@
3836+class Snapshot(object):
3837+ """
3838+ Base class for snapshots
3839+ """
3840+ platform_name = None
3841+
3842+ def __init__(self, properties, config):
3843+ """
3844+ Set up snapshot
3845+ """
3846+ self.properties = properties
3847+ self.config = config
3848+
3849+ def __str__(self):
3850+ """
3851+ a brief description of the snapshot
3852+ """
3853+ return '-'.join((self.properties['os'], self.properties['release']))
3854+
3855+ def launch(self, user_data, meta_data=None, block=True, start=True,
3856+ use_desc=None):
3857+ """
3858+ launch instance
3859+
3860+ user_data: user-data for the instance
3861+ instance_id: instance-id for the instance
3862+ block: wait until instance is created
3863+ start: start instance and wait until fully started
3864+ use_desc: description of snapshot instance use
3865+
3866+ return_value: an Instance
3867+ """
3868+ raise NotImplementedError
3869+
3870+ def destroy(self):
3871+ """
3872+ Clean up snapshot data
3873+ """
3874+ pass
3875diff --git a/tests/cloud_tests/snapshots/lxd.py b/tests/cloud_tests/snapshots/lxd.py
3876new file mode 100644
3877index 0000000..dd71b20
3878--- /dev/null
3879+++ b/tests/cloud_tests/snapshots/lxd.py
3880@@ -0,0 +1,46 @@
3881+from tests.cloud_tests.snapshots import base
3882+
3883+
3884+class LXDSnapshot(base.Snapshot):
3885+ """
3886+ LXD image copy backed snapshot
3887+ """
3888+ platform_name = "lxd"
3889+
3890+ def __init__(self, properties, config, platform, pylxd_frozen_instance):
3891+ """
3892+ Set up snapshot
3893+ """
3894+ self.platform = platform
3895+ self.pylxd_frozen_instance = pylxd_frozen_instance
3896+ super(LXDSnapshot, self).__init__(properties, config)
3897+
3898+ def launch(self, user_data, meta_data=None, block=True, start=True,
3899+ use_desc=None):
3900+ """
3901+ launch instance
3902+
3903+ user_data: user-data for the instance
3904+ instance_id: instance-id for the instance
3905+ block: wait until instance is created
3906+ start: start instance and wait until fully started
3907+ use_desc: description of snapshot instance use
3908+
3909+ return_value: an Instance
3910+ """
3911+ inst_config = {'user.user-data': user_data}
3912+ if meta_data:
3913+ inst_config['user.meta-data'] = meta_data
3914+ instance = self.platform.launch_container(
3915+ container=self.pylxd_frozen_instance.name, config=inst_config,
3916+ block=block, image_desc=str(self), use_desc=use_desc)
3917+ if start:
3918+ instance.start(wait=True, wait_time=self.config.get('timeout'))
3919+ return instance
3920+
3921+ def destroy(self):
3922+ """
3923+ Clean up snapshot data
3924+ """
3925+ self.pylxd_frozen_instance.destroy()
3926+ super(LXDSnapshot, self).destroy()
3927diff --git a/tests/cloud_tests/stage.py b/tests/cloud_tests/stage.py
3928new file mode 100644
3929index 0000000..2cf7f82
3930--- /dev/null
3931+++ b/tests/cloud_tests/stage.py
3932@@ -0,0 +1,109 @@
3933+import sys
3934+import time
3935+import traceback
3936+
3937+from tests.cloud_tests import LOG
3938+
3939+
3940+class PlatformComponent(object):
3941+ """
3942+ context manager to safely handle platform components, ensuring that
3943+ .destroy() is called
3944+ """
3945+
3946+ def __init__(self, get_func):
3947+ """
3948+ store get_<platform component> function as partial taking no args
3949+ """
3950+ self.get_func = get_func
3951+
3952+ def __enter__(self):
3953+ """
3954+ create instance of platform component
3955+ """
3956+ self.instance = self.get_func()
3957+ return self.instance
3958+
3959+ def __exit__(self, etype, value, trace):
3960+ """
3961+ destroy instance
3962+ """
3963+ if self.instance is not None:
3964+ self.instance.destroy()
3965+
3966+
3967+def run_single(name, call):
3968+ """
3969+ run a single function, keeping track of results and failures and time
3970+ name: name of part
3971+ call: call to make
3972+ return_value: a tuple of result and fail count
3973+ """
3974+ res = {
3975+ 'name': name,
3976+ 'time': 0,
3977+ 'errors': [],
3978+ 'success': False
3979+ }
3980+ failed = 0
3981+ start_time = time.time()
3982+
3983+ try:
3984+ call()
3985+ except Exception as e:
3986+ failed += 1
3987+ res['errors'].append(str(e))
3988+ LOG.error('stage part: %s encountered error: %s', name, str(e))
3989+ trace = traceback.extract_tb(sys.exc_info()[-1])
3990+ LOG.error('traceback:\n%s', ''.join(traceback.format_list(trace)))
3991+
3992+ res['time'] = time.time() - start_time
3993+ if failed == 0:
3994+ res['success'] = True
3995+
3996+ return res, failed
3997+
3998+
3999+def run_stage(parent_name, calls, continue_after_error=True):
4000+ """
4001+ run a stage of collection, keeping track of results and failures
4002+ parent_name: name of stage calls are under
4003+ calls: list of function call taking no params. must return a tuple
4004+ of results and failures. may raise exceptions
4005+ continue_after_error: whether or not to proceed to the next call after
4006+ catching an exception or recording a failure
4007+ return_value: a tuple of results and failures, with result containing
4008+ results from the function call under 'stages', and a list
4009+ of errors (if any on this level), and elapsed time
4010+ running stage, and the name
4011+ """
4012+ res = {
4013+ 'name': parent_name,
4014+ 'time': 0,
4015+ 'errors': [],
4016+ 'stages': [],
4017+ 'success': False,
4018+ }
4019+ failed = 0
4020+ start_time = time.time()
4021+
4022+ for call in calls:
4023+ try:
4024+ (call_res, call_failed) = call()
4025+ res['stages'].append(call_res)
4026+ except Exception as e:
4027+ call_failed = 1
4028+ res['errors'].append(str(e))
4029+ LOG.error('stage: %s encountered error: %s', parent_name, str(e))
4030+ trace = traceback.extract_tb(sys.exc_info()[-1])
4031+ LOG.error('traceback:\n%s', ''.join(traceback.format_list(trace)))
4032+
4033+ failed += call_failed
4034+ if call_failed and not continue_after_error:
4035+ break
4036+
4037+ res['time'] = time.time() - start_time
4038+ if not failed:
4039+ res['success'] = True
4040+
4041+ return (res, failed)
4042diff --git a/tests/cloud_tests/testcases.yaml b/tests/cloud_tests/testcases.yaml
4043new file mode 100644
4044index 0000000..02701bd
4045--- /dev/null
4046+++ b/tests/cloud_tests/testcases.yaml
4047@@ -0,0 +1,25 @@
4048+# ============================= Base Test Config ==============================
4049+base_test_data:
4050+ script_timeout: 20
4051+ enabled: True
4052+ cloud_config: |
4053+ #cloud-config
4054+ collect_scripts:
4055+ cloud-init.log: |
4056+ #!/bin/bash
4057+ cat /var/log/cloud-init.log
4058+ cloud-init-output.log: |
4059+ #!/bin/bash
4060+ cat /var/log/cloud-init-output.log
4061+ instance-id: |
4062+ #!/bin/bash
4063+ cat /run/cloud-init/.instance-id
4064+ result.json: |
4065+ #!/bin/bash
4066+ cat /run/cloud-init/result.json
4067+ status.json: |
4068+ #!/bin/bash
4069+ cat /run/cloud-init/status.json
4070+ cloud-init-version: |
4071+ #!/bin/bash
4072+ dpkg-query -W -f='${Version}' cloud-init
4073diff --git a/tests/cloud_tests/testcases/__init__.py b/tests/cloud_tests/testcases/__init__.py
4074new file mode 100644
4075index 0000000..f5cb0fd
4076--- /dev/null
4077+++ b/tests/cloud_tests/testcases/__init__.py
4078@@ -0,0 +1,43 @@
4079+import importlib
4080+import inspect
4081+import unittest
4082+
4083+from tests.cloud_tests import config
4084+from tests.cloud_tests.testcases.base import CloudTestCase as base_test
4085+
4086+
4087+def discover_tests(test_name):
4088+ """
4089+ discover tests in test file for 'testname'
4090+ return_value: list of test classes
4091+ """
4092+ testmod_name = 'tests.cloud_tests.testcases.{}'.format(
4093+ config.name_sanatize(test_name))
4094+ try:
4095+ testmod = importlib.import_module(testmod_name)
4096+ except NameError:
4097+ raise ValueError('no test verifier found at: {}'.format(testmod_name))
4098+
4099+ return [mod for name, mod in inspect.getmembers(testmod)
4100+ if inspect.isclass(mod) and base_test in mod.__bases__ and
4101+ getattr(mod, '__test__', True)]
4102+
4103+
4104+def get_suite(test_name, data, conf):
4105+ """
4106+ get test suite with all tests for 'testname'
4107+ return_value: a test suite
4108+ """
4109+ suite = unittest.TestSuite()
4110+ for test_class in discover_tests(test_name):
4111+
4112+ class tmp(test_class):
4113+
4114+ @classmethod
4115+ def setUpClass(cls):
4116+ cls.data = data
4117+ cls.conf = conf
4118+
4119+ suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(tmp))
4120+
4121+ return suite
4122diff --git a/tests/cloud_tests/testcases/base.py b/tests/cloud_tests/testcases/base.py
4123new file mode 100644
4124index 0000000..6d6c282
4125--- /dev/null
4126+++ b/tests/cloud_tests/testcases/base.py
4127@@ -0,0 +1,77 @@
4128+from cloudinit import util as c_util
4129+
4130+import json
4131+import unittest
4132+
4133+
4134+class CloudTestCase(unittest.TestCase):
4135+ """
4136+ base test class for verifiers
4137+ """
4138+ data = None
4139+ conf = None
4140+ _cloud_config = None
4141+
4142+ @property
4143+ def cloud_config(self):
4144+ """
4145+ get the cloud-config used by the test
4146+ """
4147+ if not self._cloud_config:
4148+ self._cloud_config = c_util.load_yaml(self.conf)
4149+ return self._cloud_config
4150+
4151+ def get_config_entry(self, name):
4152+ """
4153+ get a config entry from cloud-config ensuring that it is present
4154+ """
4155+ if name not in self.cloud_config:
4156+ raise AssertionError('Key "{}" not in cloud config'.format(name))
4157+ return self.cloud_config[name]
4158+
4159+ def get_data_file(self, name):
4160+ """
4161+ get data file failing test if it is not present
4162+ """
4163+ if name not in self.data:
4164+ raise AssertionError('File "{}" missing from collect data'
4165+ .format(name))
4166+ return self.data[name]
4167+
4168+ def get_instance_id(self):
4169+ """
4170+ get recorded instance id
4171+ """
4172+ return self.get_data_file('instance-id').strip()
4173+
4174+ def get_status_data(self, data, version=None):
4175+ """
4176+ parse result.json and status.json like data files
4177+ data: data to load
4178+ version: cloud-init output version, defaults to 'v1'
4179+ return_value: dict of data or None if missing
4180+ """
4181+ if not version:
4182+ version = 'v1'
4183+ data = json.loads(data)
4184+ return data.get(version)
4185+
4186+ def get_datasource(self):
4187+ """
4188+ get datasource name
4189+ """
4190+ data = self.get_status_data(self.get_data_file('result.json'))
4191+ return data.get('datasource')
4192+
4193+ def test_no_stages_errors(self):
4194+ """
4195+ ensure that there were no errors in any stage
4196+ """
4197+ status = self.get_status_data(self.get_data_file('status.json'))
4198+ for stage in ('init', 'init-local', 'modules-config', 'modules-final'):
4199+ self.assertIn(stage, status)
4200+ self.assertEqual(len(status[stage]['errors']), 0,
4201+ 'errors {} were encountered in stage {}'
4202+ .format(status[stage]['errors'], stage))
4203+ result = self.get_status_data(self.get_data_file('result.json'))
4204+ self.assertEqual(len(result['errors']), 0)
4205diff --git a/tests/cloud_tests/testcases/bugs/__init__.py b/tests/cloud_tests/testcases/bugs/__init__.py
4206new file mode 100644
4207index 0000000..6c078a5
4208--- /dev/null
4209+++ b/tests/cloud_tests/testcases/bugs/__init__.py
4210@@ -0,0 +1,4 @@
4211+"""
4212+Test verifiers for cloud-init bugs
4213+See configs/bugs/README.md for more information
4214+"""
4215diff --git a/tests/cloud_tests/testcases/bugs/lp1511485.py b/tests/cloud_tests/testcases/bugs/lp1511485.py
4216new file mode 100644
4217index 0000000..962215b
4218--- /dev/null
4219+++ b/tests/cloud_tests/testcases/bugs/lp1511485.py
4220@@ -0,0 +1,11 @@
4221+"""cloud-init Integration Test Verify Script"""
4222+from tests.cloud_tests.testcases import base
4223+
4224+
4225+class TestLP1511485(base.CloudTestCase):
4226+ """Test LP# 1511485"""
4227+
4228+ def test_final_message(self):
4229+ """Test final message exists"""
4230+ out = self.get_data_file('cloud-init-output.log')
4231+ self.assertIn('Final message from cloud-config', out)
4232diff --git a/tests/cloud_tests/testcases/bugs/lp1628337.py b/tests/cloud_tests/testcases/bugs/lp1628337.py
4233new file mode 100644
4234index 0000000..f9ae7ae
4235--- /dev/null
4236+++ b/tests/cloud_tests/testcases/bugs/lp1628337.py
4237@@ -0,0 +1,19 @@
4238+"""cloud-init Integration Test Verify Script"""
4239+from tests.cloud_tests.testcases import base
4240+
4241+
4242+class TestLP1628337(base.CloudTestCase):
4243+ """Test LP# 1511485"""
4244+
4245+ def test_fetch_indices(self):
4246+ """Verify no apt errors"""
4247+ out = self.get_data_file('cloud-init-output.log')
4248+ self.assertNotIn('W: Failed to fetch', out)
4249+ self.assertNotIn('W: Some index files failed to download. '
4250+ 'They have been ignored, or old ones used instead.',
4251+ out)
4252+
4253+ def test_ntp(self):
4254+ """Verify can find ntp and install it"""
4255+ out = self.get_data_file('cloud-init-output.log')
4256+ self.assertNotIn('E: Unable to locate package ntp', out)
4257diff --git a/tests/cloud_tests/testcases/examples/__init__.py b/tests/cloud_tests/testcases/examples/__init__.py
4258new file mode 100644
4259index 0000000..8d047ee
4260--- /dev/null
4261+++ b/tests/cloud_tests/testcases/examples/__init__.py
4262@@ -0,0 +1,4 @@
4263+"""
4264+Test verifiers for cloud-init examples
4265+See configs/examples/README.md for more information
4266+"""
4267diff --git a/tests/cloud_tests/testcases/examples/add_apt_repositories.py b/tests/cloud_tests/testcases/examples/add_apt_repositories.py
4268new file mode 100644
4269index 0000000..f45eaa7
4270--- /dev/null
4271+++ b/tests/cloud_tests/testcases/examples/add_apt_repositories.py
4272@@ -0,0 +1,16 @@
4273+"""cloud-init Integration Test Verify Script"""
4274+from tests.cloud_tests.testcases import base
4275+
4276+
4277+class TestAptconfigurePrimary(base.CloudTestCase):
4278+ """Example cloud-config test"""
4279+
4280+ def test_ubuntu_sources(self):
4281+ """Test no default Ubuntu entries exist"""
4282+ out = self.get_data_file('ubuntu.sources.list')
4283+ self.assertEqual(0, int(out))
4284+
4285+ def test_gatech_sources(self):
4286+ """Test GaTech entires exist"""
4287+ out = self.get_data_file('gatech.sources.list')
4288+ self.assertEqual(20, int(out))
4289diff --git a/tests/cloud_tests/testcases/examples/alter_completion_message.py b/tests/cloud_tests/testcases/examples/alter_completion_message.py
4290new file mode 100644
4291index 0000000..21e3536
4292--- /dev/null
4293+++ b/tests/cloud_tests/testcases/examples/alter_completion_message.py
4294@@ -0,0 +1,45 @@
4295+"""cloud-init Integration Test Verify Script"""
4296+from tests.cloud_tests.testcases import base
4297+
4298+
4299+class TestFinalMessage(base.CloudTestCase):
4300+ """
4301+ test cloud init module `cc_final_message`
4302+ """
4303+ subs_char = '$'
4304+
4305+ def get_final_message_config(self):
4306+ """
4307+ get config for final message
4308+ """
4309+ self.assertIn('final_message', self.cloud_config)
4310+ return self.cloud_config['final_message']
4311+
4312+ def get_final_message(self):
4313+ """
4314+ get final message from log
4315+ """
4316+ out = self.get_data_file('cloud-init-output.log')
4317+ lines = len(self.get_final_message_config().splitlines())
4318+ return '\n'.join(out.splitlines()[-1 * lines:])
4319+
4320+ def test_final_message_string(self):
4321+ """
4322+ ensure final handles regular strings
4323+ """
4324+ for actual, config in zip(
4325+ self.get_final_message().splitlines(),
4326+ self.get_final_message_config().splitlines()):
4327+ if self.subs_char not in config:
4328+ self.assertEqual(actual, config)
4329+
4330+ def test_final_message_subs(self):
4331+ """
4332+ test variable substitution in final message
4333+ """
4334+ # TODO: add verification of other substitutions
4335+ patterns = {'$datasource': self.get_datasource()}
4336+ for key, expected in patterns.items():
4337+ index = self.get_final_message_config().splitlines().index(key)
4338+ actual = self.get_final_message().splitlines()[index]
4339+ self.assertEqual(actual, expected)
4340diff --git a/tests/cloud_tests/testcases/examples/configure_instance_trusted_ca_certificates.py b/tests/cloud_tests/testcases/examples/configure_instance_trusted_ca_certificates.py
4341new file mode 100644
4342index 0000000..c50277c
4343--- /dev/null
4344+++ b/tests/cloud_tests/testcases/examples/configure_instance_trusted_ca_certificates.py
4345@@ -0,0 +1,23 @@
4346+"""cloud-init Integration Test Verify Script"""
4347+from tests.cloud_tests.testcases import base
4348+
4349+
4350+class TestTrustedCA(base.CloudTestCase):
4351+ """Example cloud-config test"""
4352+
4353+ def test_cert_count_ca(self):
4354+ """Test correct count of CAs in .crt"""
4355+ out = self.get_data_file('cert_count_ca')
4356+ self.assertIn('7 /etc/ssl/certs/ca-certificates.crt', out)
4357+
4358+ def test_cert_count_cloudinit(self):
4359+ """Test correct count of CAs in .pem"""
4360+ out = self.get_data_file('cert_count_cloudinit')
4361+ self.assertIn('7 /etc/ssl/certs/cloud-init-ca-certs.pem', out)
4362+
4363+ def test_cloudinit_certs(self):
4364+ """Test text of cert"""
4365+ out = self.get_data_file('cloudinit_certs')
4366+ self.assertIn('-----BEGIN CERTIFICATE-----', out)
4367+ self.assertIn('YOUR-ORGS-TRUSTED-CA-CERT-HERE', out)
4368+ self.assertIn('-----END CERTIFICATE-----', out)
4369diff --git a/tests/cloud_tests/testcases/examples/configure_instances_ssh_keys.py b/tests/cloud_tests/testcases/examples/configure_instances_ssh_keys.py
4370new file mode 100644
4371index 0000000..f5afbe7
4372--- /dev/null
4373+++ b/tests/cloud_tests/testcases/examples/configure_instances_ssh_keys.py
4374@@ -0,0 +1,27 @@
4375+"""cloud-init Integration Test Verify Script"""
4376+from tests.cloud_tests.testcases import base
4377+
4378+
4379+class TestSSHKeys(base.CloudTestCase):
4380+ """Example cloud-config test"""
4381+
4382+ def test_cert_count(self):
4383+ """Test cert count"""
4384+ out = self.get_data_file('cert_count')
4385+ self.assertEqual(20, int(out))
4386+
4387+ def test_dsa_public(self):
4388+ """Test DSA key has ending"""
4389+ out = self.get_data_file('dsa_public')
4390+ self.assertIn('ZN4XnifuO5krqAybngIy66PMEoQ= smoser@localhost', out)
4391+
4392+ def test_rsa_public(self):
4393+ """Test RSA key has specific ending"""
4394+ out = self.get_data_file('rsa_public')
4395+ self.assertIn('PemAWthxHO18QJvWPocKJtlsDNi3 smoser@localhost', out)
4396+
4397+ def test_auth_keys(self):
4398+ """Test authorized keys has specific ending"""
4399+ out = self.get_data_file('auth_keys')
4400+ self.assertIn('QPOt5Q8zWd9qG7PBl9+eiH5qV7NZ mykey@host', out)
4401+ self.assertIn('Hj29SCmXp5Kt5/82cD/VN3NtHw== smoser@brickies', out)
4402diff --git a/tests/cloud_tests/testcases/examples/including_user_groups.py b/tests/cloud_tests/testcases/examples/including_user_groups.py
4403new file mode 100644
4404index 0000000..704f699
4405--- /dev/null
4406+++ b/tests/cloud_tests/testcases/examples/including_user_groups.py
4407@@ -0,0 +1,39 @@
4408+"""cloud-init Integration Test Verify Script"""
4409+from tests.cloud_tests.testcases import base
4410+
4411+
4412+class TestUserGroups(base.CloudTestCase):
4413+ """Example cloud-config test"""
4414+
4415+ def test_group_ubuntu(self):
4416+ """Test ubuntu group exists"""
4417+ out = self.get_data_file('group_ubuntu')
4418+ self.assertRegex(out, r'ubuntu:x:[0-9]{4}:')
4419+
4420+ def test_group_cloud_users(self):
4421+ """Test cloud users group exists"""
4422+ out = self.get_data_file('group_cloud_users')
4423+ self.assertRegex(out, r'cloud-users:x:[0-9]{4}:barfoo')
4424+
4425+ def test_user_ubuntu(self):
4426+ """Test ubuntu user exists"""
4427+ out = self.get_data_file('user_ubuntu')
4428+ self.assertRegex(
4429+ out, r'ubuntu:x:[0-9]{4}:[0-9]{4}:Ubuntu:/home/ubuntu:/bin/bash')
4430+
4431+ def test_user_foobar(self):
4432+ """Test foobar user exists"""
4433+ out = self.get_data_file('user_foobar')
4434+ self.assertRegex(
4435+ out, r'foobar:x:[0-9]{4}:[0-9]{4}:Foo B. Bar:/home/foobar:')
4436+
4437+ def test_user_barfoo(self):
4438+ """Test barfoo user exists"""
4439+ out = self.get_data_file('user_barfoo')
4440+ self.assertRegex(
4441+ out, r'barfoo:x:[0-9]{4}:[0-9]{4}:Bar B. Foo:/home/barfoo:')
4442+
4443+ def test_user_cloudy(self):
4444+ """Test cloudy user exists"""
4445+ out = self.get_data_file('user_cloudy')
4446+ self.assertRegex(out, r'cloudy:x:[0-9]{3,4}:')
4447diff --git a/tests/cloud_tests/testcases/examples/install_arbitrary_packages.py b/tests/cloud_tests/testcases/examples/install_arbitrary_packages.py
4448new file mode 100644
4449index 0000000..08599be
4450--- /dev/null
4451+++ b/tests/cloud_tests/testcases/examples/install_arbitrary_packages.py
4452@@ -0,0 +1,16 @@
4453+"""cloud-init Integration Test Verify Script"""
4454+from tests.cloud_tests.testcases import base
4455+
4456+
4457+class TestInstall(base.CloudTestCase):
4458+ """Example cloud-config test"""
4459+
4460+ def test_htop(self):
4461+ """Verify htop installed"""
4462+ out = self.get_data_file('htop')
4463+ self.assertEqual(1, int(out))
4464+
4465+ def test_tree(self):
4466+ """Verify tree installed"""
4467+ out = self.get_data_file('treeutils')
4468+ self.assertEqual(1, int(out))
4469diff --git a/tests/cloud_tests/testcases/examples/run_apt_upgrade.py b/tests/cloud_tests/testcases/examples/run_apt_upgrade.py
4470new file mode 100644
4471index 0000000..6c07e32
4472--- /dev/null
4473+++ b/tests/cloud_tests/testcases/examples/run_apt_upgrade.py
4474@@ -0,0 +1,15 @@
4475+"""cloud-init Integration Test Verify Script"""
4476+from tests.cloud_tests.testcases import base
4477+
4478+
4479+class TestUpgrade(base.CloudTestCase):
4480+ """Example cloud-config test"""
4481+
4482+ def test_upgrade(self):
4483+ """Test upgrade exists in apt history"""
4484+ out = self.get_data_file('cloud-init.log')
4485+ self.assertIn(
4486+ '[CLOUDINIT] util.py[DEBUG]: apt-upgrade '
4487+ '[eatmydata apt-get --option=Dpkg::Options::=--force-confold '
4488+ '--option=Dpkg::options::=--force-unsafe-io --assume-yes --quiet '
4489+ 'dist-upgrade] took', out)
4490diff --git a/tests/cloud_tests/testcases/examples/run_commands.py b/tests/cloud_tests/testcases/examples/run_commands.py
4491new file mode 100644
4492index 0000000..b4d311f
4493--- /dev/null
4494+++ b/tests/cloud_tests/testcases/examples/run_commands.py
4495@@ -0,0 +1,11 @@
4496+"""cloud-init Integration Test Verify Script"""
4497+from tests.cloud_tests.testcases import base
4498+
4499+
4500+class TestRunCmd(base.CloudTestCase):
4501+ """Example cloud-config test"""
4502+
4503+ def test_run_cmd(self):
4504+ """Test run command worked"""
4505+ out = self.get_data_file('run_cmd')
4506+ self.assertIn('cloud-init run cmd test', out)
4507diff --git a/tests/cloud_tests/testcases/examples/run_commands_first_boot.py b/tests/cloud_tests/testcases/examples/run_commands_first_boot.py
4508new file mode 100644
4509index 0000000..a7577ae
4510--- /dev/null
4511+++ b/tests/cloud_tests/testcases/examples/run_commands_first_boot.py
4512@@ -0,0 +1,11 @@
4513+"""cloud-init Integration Test Verify Script"""
4514+from tests.cloud_tests.testcases import base
4515+
4516+
4517+class TestBootCmd(base.CloudTestCase):
4518+ """Example cloud-config test"""
4519+
4520+ def test_bootcmd_host(self):
4521+ """Test boot command worked"""
4522+ out = self.get_data_file('hosts')
4523+ self.assertIn('192.168.1.130 us.archive.ubuntu.com', out)
4524diff --git a/tests/cloud_tests/testcases/examples/writing_out_arbitrary_files.py b/tests/cloud_tests/testcases/examples/writing_out_arbitrary_files.py
4525new file mode 100644
4526index 0000000..97b6d2c
4527--- /dev/null
4528+++ b/tests/cloud_tests/testcases/examples/writing_out_arbitrary_files.py
4529@@ -0,0 +1,26 @@
4530+"""cloud-init Integration Test Verify Script"""
4531+from tests.cloud_tests.testcases import base
4532+
4533+
4534+class TestWriteFiles(base.CloudTestCase):
4535+ """Example cloud-config test"""
4536+
4537+ def test_b64(self):
4538+ """Test b64 encoded file reads as ascii"""
4539+ out = self.get_data_file('file_b64')
4540+ self.assertIn('ASCII text', out)
4541+
4542+ def test_binary(self):
4543+ """Test binary file reads as executable"""
4544+ out = self.get_data_file('file_binary')
4545+ self.assertIn('ELF 64-bit LSB executable, x86-64, version 1', out)
4546+
4547+ def test_gzip(self):
4548+ """Test gzip file shows up as a shell script"""
4549+ out = self.get_data_file('file_gzip')
4550+ self.assertIn('POSIX shell script, ASCII text executable', out)
4551+
4552+ def test_text(self):
4553+ """Test text shows up as ASCII text"""
4554+ out = self.get_data_file('file_text')
4555+ self.assertIn('ASCII text', out)
4556diff --git a/tests/cloud_tests/testcases/main/__init__.py b/tests/cloud_tests/testcases/main/__init__.py
4557new file mode 100644
4558index 0000000..460550a
4559--- /dev/null
4560+++ b/tests/cloud_tests/testcases/main/__init__.py
4561@@ -0,0 +1,4 @@
4562+"""
4563+Test verifiers for cloud-init main features
4564+See configs/main/README.md for more information
4565+"""
4566diff --git a/tests/cloud_tests/testcases/main/command_output_simple.py b/tests/cloud_tests/testcases/main/command_output_simple.py
4567new file mode 100644
4568index 0000000..281f221
4569--- /dev/null
4570+++ b/tests/cloud_tests/testcases/main/command_output_simple.py
4571@@ -0,0 +1,17 @@
4572+from tests.cloud_tests.testcases import base
4573+
4574+
4575+class TestCommandOutputSimple(base.CloudTestCase):
4576+ """
4577+ test functionality of simple output redirection
4578+ """
4579+
4580+ def test_output_file(self):
4581+ """
4582+ ensure that the output file is not empty and has all stages
4583+ """
4584+ data = self.get_data_file('cloud-init-test-output')
4585+ self.assertNotEqual(len(data), 0, "specified log empty")
4586+ self.assertEqual(self.get_config_entry('final_message'),
4587+ data.splitlines()[-1].strip())
4588+ # TODO: need to test that all stages redirected here
4589diff --git a/tests/cloud_tests/testcases/modules/__init__.py b/tests/cloud_tests/testcases/modules/__init__.py
4590new file mode 100644
4591index 0000000..d01fa9e
4592--- /dev/null
4593+++ b/tests/cloud_tests/testcases/modules/__init__.py
4594@@ -0,0 +1,4 @@
4595+"""
4596+Test verifiers for cloud-init cc modules
4597+See configs/modules/README.md for more information
4598+"""
4599diff --git a/tests/cloud_tests/testcases/modules/apt_configure_conf.py b/tests/cloud_tests/testcases/modules/apt_configure_conf.py
4600new file mode 100644
4601index 0000000..06022f1
4602--- /dev/null
4603+++ b/tests/cloud_tests/testcases/modules/apt_configure_conf.py
4604@@ -0,0 +1,16 @@
4605+"""cloud-init Integration Test Verify Script"""
4606+from tests.cloud_tests.testcases import base
4607+
4608+
4609+class TestAptconfigureConf(base.CloudTestCase):
4610+ """Test apt-configure module"""
4611+
4612+ def test_apt_conf_assumeyes(self):
4613+ """Test config assumes true"""
4614+ out = self.get_data_file('94cloud-init-config')
4615+ self.assertIn('Assume-Yes "true";', out)
4616+
4617+ def test_apt_conf_fixbroken(self):
4618+ """Test config fixes broken"""
4619+ out = self.get_data_file('94cloud-init-config')
4620+ self.assertIn('Fix-Broken "true";', out)
4621diff --git a/tests/cloud_tests/testcases/modules/apt_configure_disable_suites.py b/tests/cloud_tests/testcases/modules/apt_configure_disable_suites.py
4622new file mode 100644
4623index 0000000..e5fe979
4624--- /dev/null
4625+++ b/tests/cloud_tests/testcases/modules/apt_configure_disable_suites.py
4626@@ -0,0 +1,11 @@
4627+"""cloud-init Integration Test Verify Script"""
4628+from tests.cloud_tests.testcases import base
4629+
4630+
4631+class TestAptconfigureDisableSuites(base.CloudTestCase):
4632+ """Test apt-configure module"""
4633+
4634+ def test_empty_sourcelist(self):
4635+ """Test source list is empty"""
4636+ out = self.get_data_file('sources.list')
4637+ self.assertEqual('', out)
4638diff --git a/tests/cloud_tests/testcases/modules/apt_configure_primary.py b/tests/cloud_tests/testcases/modules/apt_configure_primary.py
4639new file mode 100644
4640index 0000000..4ec730b
4641--- /dev/null
4642+++ b/tests/cloud_tests/testcases/modules/apt_configure_primary.py
4643@@ -0,0 +1,16 @@
4644+"""cloud-init Integration Test Verify Script"""
4645+from tests.cloud_tests.testcases import base
4646+
4647+
4648+class TestAptconfigurePrimary(base.CloudTestCase):
4649+ """Test apt-configure module"""
4650+
4651+ def test_ubuntu_sources(self):
4652+ """Test no default Ubuntu entries exist"""
4653+ out = self.get_data_file('ubuntu.sources.list')
4654+ self.assertEqual(0, int(out))
4655+
4656+ def test_gatech_sources(self):
4657+ """Test GaTech entires exist"""
4658+ out = self.get_data_file('gatech.sources.list')
4659+ self.assertEqual(20, int(out))
4660diff --git a/tests/cloud_tests/testcases/modules/apt_configure_proxy.py b/tests/cloud_tests/testcases/modules/apt_configure_proxy.py
4661new file mode 100644
4662index 0000000..3079b9f
4663--- /dev/null
4664+++ b/tests/cloud_tests/testcases/modules/apt_configure_proxy.py
4665@@ -0,0 +1,18 @@
4666+"""cloud-init Integration Test Verify Script"""
4667+from tests.cloud_tests.testcases import base
4668+
4669+
4670+class TestAptconfigureProxy(base.CloudTestCase):
4671+ """Test apt-configure module"""
4672+
4673+ def test_proxy_config(self):
4674+ """Test proxy options added to apt config"""
4675+ out = self.get_data_file('90cloud-init-aptproxy')
4676+ self.assertIn(
4677+ 'Acquire::http::Proxy "http://squid.internal:3128";', out)
4678+ self.assertIn(
4679+ 'Acquire::http::Proxy "http://squid.internal:3128";', out)
4680+ self.assertIn(
4681+ 'Acquire::ftp::Proxy "ftp://squid.internal:3128";', out)
4682+ self.assertIn(
4683+ 'Acquire::https::Proxy "https://squid.internal:3128";', out)
4684diff --git a/tests/cloud_tests/testcases/modules/apt_configure_security.py b/tests/cloud_tests/testcases/modules/apt_configure_security.py
4685new file mode 100644
4686index 0000000..a74d07f
4687--- /dev/null
4688+++ b/tests/cloud_tests/testcases/modules/apt_configure_security.py
4689@@ -0,0 +1,11 @@
4690+"""cloud-init Integration Test Verify Script"""
4691+from tests.cloud_tests.testcases import base
4692+
4693+
4694+class TestAptconfigureSecurity(base.CloudTestCase):
4695+ """Test apt-configure module"""
4696+
4697+ def test_security_mirror(self):
4698+ """Test security lines added and uncommented in source.list"""
4699+ out = self.get_data_file('sources.list')
4700+ self.assertEqual(6, int(out))
4701diff --git a/tests/cloud_tests/testcases/modules/apt_configure_sources_key.py b/tests/cloud_tests/testcases/modules/apt_configure_sources_key.py
4702new file mode 100644
4703index 0000000..2517e31
4704--- /dev/null
4705+++ b/tests/cloud_tests/testcases/modules/apt_configure_sources_key.py
4706@@ -0,0 +1,19 @@
4707+"""cloud-init Integration Test Verify Script"""
4708+from tests.cloud_tests.testcases import base
4709+
4710+
4711+class TestAptconfigureSourcesKey(base.CloudTestCase):
4712+ """Test apt-configure module"""
4713+
4714+ def test_apt_key_list(self):
4715+ """Test key list updated"""
4716+ out = self.get_data_file('apt_key_list')
4717+ self.assertIn(
4718+ '1FF0 D853 5EF7 E719 E5C8 1B9C 083D 06FB E4D3 04DF', out)
4719+ self.assertIn('Launchpad PPA for cloud init development team', out)
4720+
4721+ def test_source_list(self):
4722+ """Test source.list updated"""
4723+ out = self.get_data_file('sources.list')
4724+ self.assertIn(
4725+ 'http://ppa.launchpad.net/cloud-init-dev/test-archive/ubuntu', out)
4726diff --git a/tests/cloud_tests/testcases/modules/apt_configure_sources_keyserver.py b/tests/cloud_tests/testcases/modules/apt_configure_sources_keyserver.py
4727new file mode 100644
4728index 0000000..68b9308
4729--- /dev/null
4730+++ b/tests/cloud_tests/testcases/modules/apt_configure_sources_keyserver.py
4731@@ -0,0 +1,19 @@
4732+"""cloud-init Integration Test Verify Script"""
4733+from tests.cloud_tests.testcases import base
4734+
4735+
4736+class TestAptconfigureSourcesKeyserver(base.CloudTestCase):
4737+ """Test apt-configure module"""
4738+
4739+ def test_apt_key_list(self):
4740+ """Test specific key added"""
4741+ out = self.get_data_file('apt_key_list')
4742+ self.assertIn(
4743+ '1BC3 0F71 5A3B 8612 47A8 1A5E 55FE 7C8C 0165 013E', out)
4744+ self.assertIn('Launchpad PPA for curtin developers', out)
4745+
4746+ def test_source_list(self):
4747+ """Test source.list updated"""
4748+ out = self.get_data_file('sources.list')
4749+ self.assertIn(
4750+ 'http://ppa.launchpad.net/cloud-init-dev/test-archive/ubuntu', out)
4751diff --git a/tests/cloud_tests/testcases/modules/apt_configure_sources_list.py b/tests/cloud_tests/testcases/modules/apt_configure_sources_list.py
4752new file mode 100644
4753index 0000000..aef4645
4754--- /dev/null
4755+++ b/tests/cloud_tests/testcases/modules/apt_configure_sources_list.py
4756@@ -0,0 +1,22 @@
4757+"""cloud-init Integration Test Verify Script"""
4758+from tests.cloud_tests.testcases import base
4759+
4760+
4761+class TestAptconfigureSourcesList(base.CloudTestCase):
4762+ """Test apt-configure module"""
4763+
4764+ def test_sources_list(self):
4765+ """Test sources.list includes sources"""
4766+ out = self.get_data_file('sources.list')
4767+ self.assertRegex(out, r'deb http:\/\/archive.ubuntu.com\/ubuntu '
4768+ '[a-z].* main restricted')
4769+ self.assertRegex(out, r'deb-src http:\/\/archive.ubuntu.com\/ubuntu '
4770+ '[a-z].* main restricted')
4771+ self.assertRegex(out, r'deb http:\/\/archive.ubuntu.com\/ubuntu '
4772+ '[a-z].* universe restricted')
4773+ self.assertRegex(out, r'deb-src http:\/\/archive.ubuntu.com\/ubuntu '
4774+ '[a-z].* universe restricted')
4775+ self.assertRegex(out, r'deb http:\/\/security.ubuntu.com\/ubuntu '
4776+ '[a-z].*security multiverse')
4777+ self.assertRegex(out, r'deb-src http:\/\/security.ubuntu.com\/ubuntu '
4778+ '[a-z].*security multiverse')
4779diff --git a/tests/cloud_tests/testcases/modules/apt_configure_sources_ppa.py b/tests/cloud_tests/testcases/modules/apt_configure_sources_ppa.py
4780new file mode 100644
4781index 0000000..a44a565
4782--- /dev/null
4783+++ b/tests/cloud_tests/testcases/modules/apt_configure_sources_ppa.py
4784@@ -0,0 +1,19 @@
4785+"""cloud-init Integration Test Verify Script"""
4786+from tests.cloud_tests.testcases import base
4787+
4788+
4789+class TestAptconfigureSourcesPPA(base.CloudTestCase):
4790+ """Test apt-configure module"""
4791+
4792+ def test_ppa(self):
4793+ """test specific ppa added"""
4794+ out = self.get_data_file('sources.list')
4795+ self.assertIn(
4796+ 'http://ppa.launchpad.net/curtin-dev/test-archive/ubuntu', out)
4797+
4798+ def test_ppa_key(self):
4799+ """test ppa key added"""
4800+ out = self.get_data_file('apt-key')
4801+ self.assertIn(
4802+ '1BC3 0F71 5A3B 8612 47A8 1A5E 55FE 7C8C 0165 013E', out)
4803+ self.assertIn('Launchpad PPA for curtin developers', out)
4804diff --git a/tests/cloud_tests/testcases/modules/apt_pipelining_disable.py b/tests/cloud_tests/testcases/modules/apt_pipelining_disable.py
4805new file mode 100644
4806index 0000000..255c398
4807--- /dev/null
4808+++ b/tests/cloud_tests/testcases/modules/apt_pipelining_disable.py
4809@@ -0,0 +1,11 @@
4810+"""cloud-init Integration Test Verify Script"""
4811+from tests.cloud_tests.testcases import base
4812+
4813+
4814+class TestAptPipeliningDisable(base.CloudTestCase):
4815+ """Test apt-pipelining module"""
4816+
4817+ def test_disable_pipelining(self):
4818+ """Test pipelining disabled"""
4819+ out = self.get_data_file('90cloud-init-pipelining')
4820+ self.assertIn('Acquire::http::Pipeline-Depth "0";', out)
4821diff --git a/tests/cloud_tests/testcases/modules/apt_pipelining_os.py b/tests/cloud_tests/testcases/modules/apt_pipelining_os.py
4822new file mode 100644
4823index 0000000..301f96c
4824--- /dev/null
4825+++ b/tests/cloud_tests/testcases/modules/apt_pipelining_os.py
4826@@ -0,0 +1,11 @@
4827+"""cloud-init Integration Test Verify Script"""
4828+from tests.cloud_tests.testcases import base
4829+
4830+
4831+class TestAptPipeliningOS(base.CloudTestCase):
4832+ """Test apt-pipelining module"""
4833+
4834+ def test_os_pipelining(self):
4835+ """Test pipelining set to os"""
4836+ out = self.get_data_file('90cloud-init-pipelining')
4837+ self.assertIn('Acquire::http::Pipeline-Depth "0";', out)
4838diff --git a/tests/cloud_tests/testcases/modules/bootcmd.py b/tests/cloud_tests/testcases/modules/bootcmd.py
4839new file mode 100644
4840index 0000000..ca6eaaa
4841--- /dev/null
4842+++ b/tests/cloud_tests/testcases/modules/bootcmd.py
4843@@ -0,0 +1,11 @@
4844+"""cloud-init Integration Test Verify Script"""
4845+from tests.cloud_tests.testcases import base
4846+
4847+
4848+class TestBootCmd(base.CloudTestCase):
4849+ """Test bootcmd module"""
4850+
4851+ def test_bootcmd_host(self):
4852+ """Test boot cmd worked"""
4853+ out = self.get_data_file('hosts')
4854+ self.assertIn('192.168.1.130 us.archive.ubuntu.com', out)
4855diff --git a/tests/cloud_tests/testcases/modules/byobu.py b/tests/cloud_tests/testcases/modules/byobu.py
4856new file mode 100644
4857index 0000000..59169a3
4858--- /dev/null
4859+++ b/tests/cloud_tests/testcases/modules/byobu.py
4860@@ -0,0 +1,21 @@
4861+"""cloud-init Integration Test Verify Script"""
4862+from tests.cloud_tests.testcases import base
4863+
4864+
4865+class TestByobu(base.CloudTestCase):
4866+ """Test Byobu module"""
4867+
4868+ def test_byobu_installed(self):
4869+ """Test byobu installed"""
4870+ out = self.get_data_file('byobu_installed')
4871+ self.assertIn('/usr/bin/byobu', out)
4872+
4873+ def test_byobu_profile_enabled(self):
4874+ """Test byobu profile.d file exists"""
4875+ out = self.get_data_file('byobu_profile_enabled')
4876+ self.assertIn('/etc/profile.d/Z97-byobu.sh', out)
4877+
4878+ def test_byobu_launch_exists(self):
4879+ """Test byobu-launch exists"""
4880+ out = self.get_data_file('byobu_launch_exists')
4881+ self.assertIn('/usr/bin/byobu-launch', out)
4882diff --git a/tests/cloud_tests/testcases/modules/ca_certs.py b/tests/cloud_tests/testcases/modules/ca_certs.py
4883new file mode 100644
4884index 0000000..f734240
4885--- /dev/null
4886+++ b/tests/cloud_tests/testcases/modules/ca_certs.py
4887@@ -0,0 +1,16 @@
4888+"""cloud-init Integration Test Verify Script"""
4889+from tests.cloud_tests.testcases import base
4890+
4891+
4892+class TestCaCerts(base.CloudTestCase):
4893+ """Test ca certs module"""
4894+
4895+ def test_cert_count(self):
4896+ """Test the count is proper"""
4897+ out = self.get_data_file('cert_count')
4898+ self.assertEqual(5, int(out))
4899+
4900+ def test_cert_installed(self):
4901+ """Test line from our cert exists"""
4902+ out = self.get_data_file('cert')
4903+ self.assertIn('a36c744454555024e7f82edc420fd2c8', out)
4904diff --git a/tests/cloud_tests/testcases/modules/debug_disable.py b/tests/cloud_tests/testcases/modules/debug_disable.py
4905new file mode 100644
4906index 0000000..ec3a46e
4907--- /dev/null
4908+++ b/tests/cloud_tests/testcases/modules/debug_disable.py
4909@@ -0,0 +1,12 @@
4910+"""cloud-init Integration Test Verify Script"""
4911+from tests.cloud_tests.testcases import base
4912+
4913+
4914+class TestDebugDisable(base.CloudTestCase):
4915+ """Disable debug messages"""
4916+
4917+ def test_debug_disable(self):
4918+ """Test verbose output missing from logs"""
4919+ out = self.get_data_file('cloud-init.log')
4920+ self.assertNotIn(
4921+ out, r'Skipping module named [a-z].* verbose printing disabled')
4922diff --git a/tests/cloud_tests/testcases/modules/debug_enable.py b/tests/cloud_tests/testcases/modules/debug_enable.py
4923new file mode 100644
4924index 0000000..b33804d
4925--- /dev/null
4926+++ b/tests/cloud_tests/testcases/modules/debug_enable.py
4927@@ -0,0 +1,11 @@
4928+"""cloud-init Integration Test Verify Script"""
4929+from tests.cloud_tests.testcases import base
4930+
4931+
4932+class TestDebugEnable(base.CloudTestCase):
4933+ """Test debug messages"""
4934+
4935+ def test_debug_enable(self):
4936+ """Test debug messages in cloud-init log"""
4937+ out = self.get_data_file('cloud-init.log')
4938+ self.assertIn('[DEBUG]', out)
4939diff --git a/tests/cloud_tests/testcases/modules/final_message.py b/tests/cloud_tests/testcases/modules/final_message.py
4940new file mode 100644
4941index 0000000..21e3536
4942--- /dev/null
4943+++ b/tests/cloud_tests/testcases/modules/final_message.py
4944@@ -0,0 +1,45 @@
4945+"""cloud-init Integration Test Verify Script"""
4946+from tests.cloud_tests.testcases import base
4947+
4948+
4949+class TestFinalMessage(base.CloudTestCase):
4950+ """
4951+ test cloud init module `cc_final_message`
4952+ """
4953+ subs_char = '$'
4954+
4955+ def get_final_message_config(self):
4956+ """
4957+ get config for final message
4958+ """
4959+ self.assertIn('final_message', self.cloud_config)
4960+ return self.cloud_config['final_message']
4961+
4962+ def get_final_message(self):
4963+ """
4964+ get final message from log
4965+ """
4966+ out = self.get_data_file('cloud-init-output.log')
4967+ lines = len(self.get_final_message_config().splitlines())
4968+ return '\n'.join(out.splitlines()[-1 * lines:])
4969+
4970+ def test_final_message_string(self):
4971+ """
4972+ ensure final handles regular strings
4973+ """
4974+ for actual, config in zip(
4975+ self.get_final_message().splitlines(),
4976+ self.get_final_message_config().splitlines()):
4977+ if self.subs_char not in config:
4978+ self.assertEqual(actual, config)
4979+
4980+ def test_final_message_subs(self):
4981+ """
4982+ test variable substitution in final message
4983+ """
4984+ # TODO: add verification of other substitutions
4985+ patterns = {'$datasource': self.get_datasource()}
4986+ for key, expected in patterns.items():
4987+ index = self.get_final_message_config().splitlines().index(key)
4988+ actual = self.get_final_message().splitlines()[index]
4989+ self.assertEqual(actual, expected)
4990diff --git a/tests/cloud_tests/testcases/modules/keys_to_console.py b/tests/cloud_tests/testcases/modules/keys_to_console.py
4991new file mode 100644
4992index 0000000..bee8810
4993--- /dev/null
4994+++ b/tests/cloud_tests/testcases/modules/keys_to_console.py
4995@@ -0,0 +1,18 @@
4996+"""cloud-init Integration Test Verify Script"""
4997+from tests.cloud_tests.testcases import base
4998+
4999+
5000+class TestKeysToConsole(base.CloudTestCase):
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches