Merge ~powersj/cloud-init:integration-test-revamp into cloud-init:master

Proposed by Joshua Powers
Status: Merged
Merge reported by: Joshua Powers
Merged at revision: 78093d78281b75719c0e11c3c632b05d94b525bc
Proposed branch: ~powersj/cloud-init:integration-test-revamp
Merge into: cloud-init:master
Diff against target: 6646 lines (+2508/-1190)
126 files modified
doc/rtd/topics/tests.rst (+509/-122)
tests/cloud_tests/__init__.py (+4/-3)
tests/cloud_tests/__main__.py (+10/-35)
tests/cloud_tests/args.py (+115/-35)
tests/cloud_tests/bddeb.py (+118/-0)
tests/cloud_tests/collect.py (+64/-50)
tests/cloud_tests/config.py (+94/-45)
tests/cloud_tests/configs/bugs/lp1628337.yaml (+3/-0)
tests/cloud_tests/configs/examples/add_apt_repositories.yaml (+2/-0)
tests/cloud_tests/configs/modules/apt_configure_conf.yaml (+2/-0)
tests/cloud_tests/configs/modules/apt_configure_disable_suites.yaml (+3/-0)
tests/cloud_tests/configs/modules/apt_configure_primary.yaml (+7/-0)
tests/cloud_tests/configs/modules/apt_configure_proxy.yaml (+2/-0)
tests/cloud_tests/configs/modules/apt_configure_security.yaml (+3/-0)
tests/cloud_tests/configs/modules/apt_configure_sources_key.yaml (+3/-0)
tests/cloud_tests/configs/modules/apt_configure_sources_keyserver.yaml (+3/-0)
tests/cloud_tests/configs/modules/apt_configure_sources_list.yaml (+3/-0)
tests/cloud_tests/configs/modules/apt_configure_sources_ppa.yaml (+9/-0)
tests/cloud_tests/configs/modules/apt_pipelining_disable.yaml (+2/-0)
tests/cloud_tests/configs/modules/apt_pipelining_os.yaml (+2/-0)
tests/cloud_tests/configs/modules/byobu.yaml (+2/-0)
tests/cloud_tests/configs/modules/keys_to_console.yaml (+2/-0)
tests/cloud_tests/configs/modules/landscape.yaml (+2/-0)
tests/cloud_tests/configs/modules/locale.yaml (+3/-0)
tests/cloud_tests/configs/modules/lxd_bridge.yaml (+2/-0)
tests/cloud_tests/configs/modules/lxd_dir.yaml (+2/-0)
tests/cloud_tests/configs/modules/ntp.yaml (+11/-0)
tests/cloud_tests/configs/modules/ntp_pools.yaml (+10/-0)
tests/cloud_tests/configs/modules/ntp_servers.yaml (+7/-0)
tests/cloud_tests/configs/modules/package_update_upgrade_install.yaml (+11/-0)
tests/cloud_tests/configs/modules/set_hostname.yaml (+2/-0)
tests/cloud_tests/configs/modules/set_hostname_fqdn.yaml (+2/-0)
tests/cloud_tests/configs/modules/set_password.yaml (+2/-0)
tests/cloud_tests/configs/modules/set_password_expire.yaml (+2/-0)
tests/cloud_tests/configs/modules/snappy.yaml (+2/-0)
tests/cloud_tests/configs/modules/ssh_auth_key_fingerprints_disable.yaml (+2/-0)
tests/cloud_tests/configs/modules/ssh_auth_key_fingerprints_enable.yaml (+5/-0)
tests/cloud_tests/configs/modules/ssh_import_id.yaml (+3/-0)
tests/cloud_tests/configs/modules/ssh_keys_generate.yaml (+2/-0)
tests/cloud_tests/configs/modules/ssh_keys_provided.yaml (+3/-0)
tests/cloud_tests/configs/modules/timezone.yaml (+2/-0)
tests/cloud_tests/configs/modules/user_groups.yaml (+2/-0)
tests/cloud_tests/configs/modules/write_files.yaml (+4/-0)
tests/cloud_tests/images/__init__.py (+3/-4)
tests/cloud_tests/images/base.py (+36/-32)
tests/cloud_tests/images/lxd.py (+137/-39)
tests/cloud_tests/instances/__init__.py (+3/-3)
tests/cloud_tests/instances/base.py (+95/-67)
tests/cloud_tests/instances/lxd.py (+76/-56)
tests/cloud_tests/manage.py (+14/-15)
tests/cloud_tests/platforms.yaml (+49/-1)
tests/cloud_tests/platforms/__init__.py (+3/-3)
tests/cloud_tests/platforms/base.py (+9/-35)
tests/cloud_tests/platforms/lxd.py (+54/-43)
tests/cloud_tests/releases.yaml (+243/-76)
tests/cloud_tests/run_funcs.py (+75/-0)
tests/cloud_tests/setup_image.py (+116/-80)
tests/cloud_tests/snapshots/__init__.py (+3/-3)
tests/cloud_tests/snapshots/base.py (+22/-21)
tests/cloud_tests/snapshots/lxd.py (+27/-24)
tests/cloud_tests/stage.py (+23/-29)
tests/cloud_tests/testcases.yaml (+1/-0)
tests/cloud_tests/testcases/__init__.py (+9/-7)
tests/cloud_tests/testcases/base.py (+22/-29)
tests/cloud_tests/testcases/bugs/__init__.py (+2/-2)
tests/cloud_tests/testcases/bugs/lp1511485.py (+3/-3)
tests/cloud_tests/testcases/bugs/lp1628337.py (+4/-4)
tests/cloud_tests/testcases/examples/__init__.py (+2/-2)
tests/cloud_tests/testcases/examples/add_apt_repositories.py (+4/-4)
tests/cloud_tests/testcases/examples/alter_completion_message.py (+7/-16)
tests/cloud_tests/testcases/examples/configure_instance_trusted_ca_certificates.py (+5/-5)
tests/cloud_tests/testcases/examples/configure_instances_ssh_keys.py (+6/-6)
tests/cloud_tests/testcases/examples/including_user_groups.py (+8/-8)
tests/cloud_tests/testcases/examples/install_arbitrary_packages.py (+4/-4)
tests/cloud_tests/testcases/examples/install_run_chef_recipes.py (+3/-3)
tests/cloud_tests/testcases/examples/run_apt_upgrade.py (+3/-3)
tests/cloud_tests/testcases/examples/run_commands.py (+3/-3)
tests/cloud_tests/testcases/examples/run_commands_first_boot.py (+3/-3)
tests/cloud_tests/testcases/examples/writing_out_arbitrary_files.py (+6/-6)
tests/cloud_tests/testcases/main/__init__.py (+2/-2)
tests/cloud_tests/testcases/main/command_output_simple.py (+3/-6)
tests/cloud_tests/testcases/modules/__init__.py (+2/-2)
tests/cloud_tests/testcases/modules/apt_configure_conf.py (+4/-4)
tests/cloud_tests/testcases/modules/apt_configure_disable_suites.py (+3/-3)
tests/cloud_tests/testcases/modules/apt_configure_primary.py (+4/-4)
tests/cloud_tests/testcases/modules/apt_configure_proxy.py (+3/-3)
tests/cloud_tests/testcases/modules/apt_configure_security.py (+3/-3)
tests/cloud_tests/testcases/modules/apt_configure_sources_key.py (+4/-4)
tests/cloud_tests/testcases/modules/apt_configure_sources_keyserver.py (+4/-4)
tests/cloud_tests/testcases/modules/apt_configure_sources_list.py (+3/-3)
tests/cloud_tests/testcases/modules/apt_configure_sources_ppa.py (+4/-4)
tests/cloud_tests/testcases/modules/apt_pipelining_disable.py (+3/-3)
tests/cloud_tests/testcases/modules/apt_pipelining_os.py (+3/-3)
tests/cloud_tests/testcases/modules/bootcmd.py (+3/-3)
tests/cloud_tests/testcases/modules/byobu.py (+5/-5)
tests/cloud_tests/testcases/modules/ca_certs.py (+4/-4)
tests/cloud_tests/testcases/modules/debug_disable.py (+3/-3)
tests/cloud_tests/testcases/modules/debug_enable.py (+3/-3)
tests/cloud_tests/testcases/modules/final_message.py (+7/-16)
tests/cloud_tests/testcases/modules/keys_to_console.py (+4/-4)
tests/cloud_tests/testcases/modules/locale.py (+4/-4)
tests/cloud_tests/testcases/modules/lxd_bridge.py (+5/-5)
tests/cloud_tests/testcases/modules/lxd_dir.py (+4/-4)
tests/cloud_tests/testcases/modules/ntp.py (+1/-1)
tests/cloud_tests/testcases/modules/ntp_pools.py (+2/-2)
tests/cloud_tests/testcases/modules/package_update_upgrade_install.py (+6/-6)
tests/cloud_tests/testcases/modules/runcmd.py (+3/-3)
tests/cloud_tests/testcases/modules/salt_minion.py (+5/-5)
tests/cloud_tests/testcases/modules/seed_random_data.py (+3/-3)
tests/cloud_tests/testcases/modules/set_hostname.py (+3/-3)
tests/cloud_tests/testcases/modules/set_hostname_fqdn.py (+5/-5)
tests/cloud_tests/testcases/modules/set_password.py (+4/-4)
tests/cloud_tests/testcases/modules/set_password_expire.py (+4/-4)
tests/cloud_tests/testcases/modules/set_password_list.py (+3/-2)
tests/cloud_tests/testcases/modules/set_password_list_string.py (+3/-2)
tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.py (+8/-8)
tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_enable.py (+7/-7)
tests/cloud_tests/testcases/modules/ssh_import_id.py (+3/-3)
tests/cloud_tests/testcases/modules/ssh_keys_generate.py (+11/-11)
tests/cloud_tests/testcases/modules/ssh_keys_provided.py (+12/-12)
tests/cloud_tests/testcases/modules/timezone.py (+3/-3)
tests/cloud_tests/testcases/modules/user_groups.py (+8/-8)
tests/cloud_tests/testcases/modules/write_files.py (+6/-6)
tests/cloud_tests/util.py (+182/-53)
tests/cloud_tests/verify.py (+12/-10)
tox.ini (+1/-1)
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Needs Fixing
Chad Smith Approve
cloud-init Commiters Pending
Review via email: mp+324136@code.launchpad.net

Commit message

Integration Testing: tox env, pyxld 2.2.3, and revamp framework

Massive update to clean up and greatly enhance the integration testing framework developed by Wesley Wiedenmeier.

 - Updated tox environment to run integration test 'citest' to utilize
   pylxd 2.2.3
 - Add support for distro feature flags
   - add framework for feature flags to release config with feature groups
     and overrides allowed in any release conf override level
   - add support for feature flags in platform and config handling
   - during collect, skip testcases that require features not supported by
     the image with a warning message
 - Enable additional distros (i.e. centos, debian)
 - Add 'bddeb' command to build a deb from the current working tree
   cleanly in a container, so deps do not have to be installed on host
 - Adds a command line option '--preserve-data' that ensures that
   collected data will be left after tests run. This also allows the
   directory to store collected data in during the run command to be
   specified using '--data-dir'.
 - Updated Read the Docs testing page and doc strings for pep 257
   compliance

Description of the change

Tests preformed:
$ tox
$ tox -e citest -- bddeb
$ tox -e citest -- bddeb --deb output.deb
$ tox -e citest -- run -v -n xenial
$ tox -e citest -- run -v -n zesty --deb cloud-init_all.deb
$ tox -e citest -- run -v -n yakkety --repo 'deb http://archive.ubuntu.com/ubuntu/ artful main'
$ tox -e citest -- tree_run -v -n artful -t tests/cloud_tests/configs/modules/write_files.yaml
$ tox -e citest -- tree_run -v -n stretch --preserve-data
$ tox -e citest -- tree_run -v -n xenial --preserve-data --data-dir='/tmp/testdata'

Experimental features (functionally work, tests fail):
$ tox -e citest -- tree_run -v -n stretch (works with updated cloud-init)
$ tox -e citest -- tree_run -v -n jessie (works with updated cloud-init)
$ tox -e citest -- tree_run -v -n trusty (works with sysvinit, python2 deb)
$ tox -e citest -- tree_run -v -n centos66
$ tox -e citest -- tree_run -v -n centos70

To post a comment you must log in.
Revision history for this message
Chad Smith (chad.smith) wrote :

Wow thanks for the epic docstring rewrite across all integration tests.
It really is a lot easier to read.

I think all of my comments on the previous branch have been addressed. thank you.

Since the visual diff is so large it isn't fully rendered here.

Couple inline comments remain and

=I can't comment inline for the following:
tests/cloud_tests/config.py:

 - s/[Ss]anatize/[Ss]anitize/

tests/cloud_tests/setup_image.py
 - s/#return_value/@return_value/

tests/cloud_tests/util.py:
  - s/Utilies/Utilities/ in module docstring

  - From a previous branch comment, I still think the tmpdir function defined here is has limited benefit in redefining a wrapper that passes a prefix='cloud_test_util', why don't we just call tempfile.mkdtemp(prefix='cloud_test_util') in the two call sites in tests/cloud_tests/images/lxd.py?

+1 after that set of changes. Thanks for this herculean effort. With such a large diff it's hard to catch all the issues effectively. But, since it's integration testing I'm not worried that we can iron out anything that remains on subsequent passes/iterations.

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

Made Chad's spelling and doc string changes. Looked into the tmpdir changes and agreed that it is useless so removed the function and updated the lxd calls to call tempfile.mkdtemp directly.

Thanks for the review!

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Scott Moser (smoser) wrote :

The commit message needs updating for sure.
You mention tox 'citest', but do not make any changes to it.

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

I made an update to the commit message. That was a combination of the original messages before we had pulled in that tox change earlier.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Needs Fixing (continuous-integration)

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/topics/tests.rst b/doc/rtd/topics/tests.rst
2index 0663811..d668e3f 100644
3--- a/doc/rtd/topics/tests.rst
4+++ b/doc/rtd/topics/tests.rst
5@@ -1,14 +1,186 @@
6-****************
7-Test Development
8-****************
9-
10+*******************
11+Integration Testing
12+*******************
13
14 Overview
15 ========
16
17-The purpose of this page is to describe how to write integration tests for
18-cloud-init. As a test writer you need to develop a test configuration and
19-a verification file:
20+This page describes the execution, development, and architecture of the
21+cloud-init integration tests:
22+
23+* Execution explains the options available and running of tests
24+* Development shows how to write test cases
25+* Architecture explains the internal processes
26+
27+Execution
28+=========
29+
30+Overview
31+--------
32+
33+In order to avoid the need for dependencies and ease the setup and
34+configuration users can run the integration tests via tox:
35+
36+.. code-block:: bash
37+
38+ $ git clone https://git.launchpad.net/cloud-init
39+ $ cd cloud-init
40+ $ tox -e citest -- -h
41+
42+Everything after the double dash will be passed to the integration tests.
43+Executing tests has several options:
44+
45+* ``run`` an alias to run both ``collect`` and ``verify``. The ``tree_run``
46+ command does the same thing, except uses a deb built from the current
47+ working tree.
48+
49+* ``collect`` deploys on the specified platform and distro, patches with the
50+ requested deb or rpm, and finally collects output of the arbitrary
51+ commands. Similarly, ```tree_collect`` will collect output using a deb
52+ built from the current working tree.
53+
54+* ``verify`` given a directory of test data, run the Python unit tests on
55+ it to generate results.
56+
57+* ``bddeb`` will build a deb of the current working tree.
58+
59+Run
60+---
61+
62+The first example will provide a complete end-to-end run of data
63+collection and verification. There are additional examples below
64+explaining how to run one or the other independently.
65+
66+.. code-block:: bash
67+
68+ $ git clone https://git.launchpad.net/cloud-init
69+ $ cd cloud-init
70+ $ tox -e citest -- run --verbose \
71+ --os-name stretch --os-name xenial \
72+ --deb cloud-init_0.7.8~my_patch_all.deb \
73+ --preserve-data --data-dir ~/collection
74+
75+The above command will do the following:
76+
77+* ``run`` both collect output and run tests the output
78+
79+* ``--verbose`` verbose output
80+
81+* ``--os-name stretch`` on the Debian Stretch release
82+
83+* ``--os-name xenial`` on the Ubuntu Xenial release
84+
85+* ``--deb cloud-init_0.7.8~patch_all.deb`` use this deb as the version of
86+ cloud-init to run with
87+
88+* ``--preserve-data`` always preserve collected data, do not remove data
89+ after successful test run
90+
91+* ``--data-dir ~/collection`` write collected data into `~/collection`,
92+ rather than using a temporary directory
93+
94+For a more detailed explanation of each option see below.
95+
96+.. note::
97+ By default, data collected by the run command will be written into a
98+ temporary directory and deleted after a successful. If you would
99+ like to preserve this data, please use the option ``--preserve-data``.
100+
101+Collect
102+-------
103+
104+If developing tests it may be necessary to see if cloud-config works as
105+expected and the correct files are pulled down. In this case only a
106+collect can be ran by running:
107+
108+.. code-block:: bash
109+
110+ $ tox -e citest -- collect -n xenial --data-dir /tmp/collection
111+
112+The above command will run the collection tests on xenial and place
113+all results into `/tmp/collection`.
114+
115+Verify
116+------
117+
118+When developing tests it is much easier to simply rerun the verify scripts
119+without the more lengthy collect process. This can be done by running:
120+
121+.. code-block:: bash
122+
123+ $ tox -e citest -- verify --data-dir /tmp/collection
124+
125+The above command will run the verify scripts on the data discovered in
126+`/tmp/collection`.
127+
128+TreeRun and TreeCollect
129+-----------------------
130+
131+If working on a cloud-init feature or resolving a bug, it may be useful to
132+run the current copy of cloud-init in the integration testing environment.
133+The integration testing suite can automatically build a deb based on the
134+current working tree of cloud-init and run the test suite using this deb.
135+
136+The ``tree_run`` and ``tree_collect`` commands take the same arguments as
137+the ``run`` and ``collect`` commands. These commands will build a deb and
138+write it into a temporary file, then start the test suite and pass that deb
139+in. To build a deb only, and not run the test suite, the ``bddeb`` command
140+can be used.
141+
142+Note that code in the cloud-init working tree that has not been committed
143+when the cloud-init deb is built will still be included. To build a
144+cloud-init deb from or use the ``tree_run`` command using a copy of
145+cloud-init located in a different directory, use the option ``--cloud-init
146+/path/to/cloud-init``.
147+
148+.. code-block:: bash
149+
150+ $ tox -e citest -- tree_run --verbose \
151+ --os-name xenial --os-name stretch \
152+ --test modules/final_message --test modules/write_files \
153+ --result /tmp/result.yaml
154+
155+Bddeb
156+-----
157+
158+The ``bddeb`` command can be used to generate a deb file. This is used by
159+the tree_run and tree_collect commands to build a deb of the current
160+working tree. It can also be used a user to generate a deb for use in other
161+situations and avoid needing to have all the build and test dependencies
162+installed locally.
163+
164+* ``--bddeb-args``: arguments to pass through to bddeb
165+* ``--build-os``: distribution to use as build system (default is xenial)
166+* ``--build-platform``: platform to use for build system (default is lxd)
167+* ``--cloud-init``: path to base of cloud-init tree (default is '.')
168+* ``--deb``: path to write output deb to (default is '.')
169+
170+Setup Image
171+-----------
172+
173+By default an image that is used will remain unmodified, but certain
174+scenarios may require image modification. For example, many images may use
175+a much older cloud-init. As a result tests looking at newer functionality
176+will fail because a newer version of cloud-init may be required. The
177+following options can be used for further customization:
178+
179+* ``--deb``: install the specified deb into the image
180+* ``--rpm``: install the specified rpm into the image
181+* ``--repo``: enable a repository and upgrade cloud-init afterwards
182+* ``--ppa``: enable a ppa and upgrade cloud-init afterwards
183+* ``--upgrade``: upgrade cloud-init from repos
184+* ``--upgrade-full``: run a full system upgrade
185+* ``--script``: execute a script in the image. This can perform any setup
186+ required that is not covered by the other options
187+
188+Test Case Development
189+=====================
190+
191+Overview
192+--------
193+
194+As a test writer you need to develop a test configuration and a
195+verification file:
196
197 * The test configuration specifies a specific cloud-config to be used by
198 cloud-init and a list of arbitrary commands to capture the output of
199@@ -21,20 +193,28 @@ The names must match, however the extensions will of course be different,
200 yaml vs py.
201
202 Configuration
203-=============
204+-------------
205
206 The test configuration is a YAML file such as *ntp_server.yaml* below:
207
208 .. code-block:: yaml
209
210 #
211- # NTP config using specific servers (ntp_server.yaml)
212+ # Empty NTP config to setup using defaults
213 #
214+ # NOTE: this should not require apt feature, use 'which' rather than 'dpkg -l'
215+ # NOTE: this should not require no_ntpdate feature, use 'which' to check for
216+ # installation rather than 'dpkg -l', as 'grep ntp' matches 'ntpdate'
217+ # NOTE: the verifier should check for any ntp server not 'ubuntu.pool.ntp.org'
218 cloud_config: |
219 #cloud-config
220 ntp:
221 servers:
222 - pool.ntp.org
223+ required_features:
224+ - apt
225+ - no_ntpdate
226+ - ubuntu_ntp
227 collect_scripts:
228 ntp_installed_servers: |
229 #!/bin/bash
230@@ -46,21 +226,30 @@ The test configuration is a YAML file such as *ntp_server.yaml* below:
231 #!/bin/bash
232 cat /etc/ntp.conf | grep '^server'
233
234-
235-There are two keys, 1 required and 1 optional, in the YAML file:
236+There are several keys, 1 required and some optional, in the YAML file:
237
238 1. The required key is ``cloud_config``. This should be a string of valid
239- YAML that is exactly what would normally be placed in a cloud-config file,
240- including the cloud-config header. This essentially sets up the scenario
241- under test.
242+ YAML that is exactly what would normally be placed in a cloud-config
243+ file, including the cloud-config header. This essentially sets up the
244+ scenario under test.
245
246-2. The optional key is ``collect_scripts``. This key has one or more
247+2. One optional key is ``collect_scripts``. This key has one or more
248 sub-keys containing strings of arbitrary commands to execute (e.g.
249 ```cat /var/log/cloud-config-output.log```). In the example above the
250 output of dpkg is captured, grep for ntp, and the number of lines
251 reported. The name of the sub-key is important. The sub-key is used by
252 the verification script to recall the output of the commands ran.
253
254+3. The optional ``enabled`` key enables or disables the test case. By
255+ default the test case will be enabled.
256+
257+4. The optional ``required_features`` key may be used to specify a list
258+ of features flags that an image must have to be able to run the test
259+ case. For example, if a test case relies on an image supporting apt,
260+ then the config for the test case should include ``required_features:
261+ [ apt ]``.
262+
263+
264 Default Collect Scripts
265 -----------------------
266
267@@ -75,51 +264,68 @@ no need to specify these items:
268 * ```dpkg-query -W -f='${Version}' cloud-init```
269
270 Verification
271-============
272+------------
273
274 The verification script is a Python file with unit tests like the one,
275 `ntp_server.py`, below:
276
277 .. code-block:: python
278
279- """cloud-init Integration Test Verify Script (ntp_server.yaml)"""
280+ # This file is part of cloud-init. See LICENSE file for license information.
281+
282+ """cloud-init Integration Test Verify Script"""
283 from tests.cloud_tests.testcases import base
284
285
286- class TestNtpServers(base.CloudTestCase):
287+ class TestNtp(base.CloudTestCase):
288 """Test ntp module"""
289
290 def test_ntp_installed(self):
291 """Test ntp installed"""
292- out = self.get_data_file('ntp_installed_servers')
293+ out = self.get_data_file('ntp_installed_empty')
294 self.assertEqual(1, int(out))
295
296 def test_ntp_dist_entries(self):
297 """Test dist config file has one entry"""
298- out = self.get_data_file('ntp_conf_dist_servers')
299+ out = self.get_data_file('ntp_conf_dist_empty')
300 self.assertEqual(1, int(out))
301
302 def test_ntp_entires(self):
303 """Test config entries"""
304- out = self.get_data_file('ntp_conf_servers')
305- self.assertIn('server pool.ntp.org iburst', out)
306+ out = self.get_data_file('ntp_conf_empty')
307+ self.assertIn('pool 0.ubuntu.pool.ntp.org iburst', out)
308+ self.assertIn('pool 1.ubuntu.pool.ntp.org iburst', out)
309+ self.assertIn('pool 2.ubuntu.pool.ntp.org iburst', out)
310+ self.assertIn('pool 3.ubuntu.pool.ntp.org iburst', out)
311+
312+ # vi: ts=4 expandtab
313
314
315 Here is a breakdown of the unit test file:
316
317 * The import statement allows access to the output files.
318
319-* The class can be named anything, but must import the ``base.CloudTestCase``
320+* The class can be named anything, but must import the
321+ ``base.CloudTestCase``, either directly or via another test class.
322
323 * There can be 1 to N number of functions with any name, however only
324- tests starting with ``test_*`` will be executed.
325+ functions starting with ``test_*`` will be executed.
326+
327+* There can be 1 to N number of classes in a test module, however only
328+ classes inheriting from ``base.CloudTestCase`` will be loaded.
329
330 * Output from the commands can be accessed via
331 ``self.get_data_file('key')`` where key is the sub-key of
332 ``collect_scripts`` above.
333
334+* The cloud config that the test ran with can be accessed via
335+ ``self.cloud_config``, or any entry from the cloud config can be accessed
336+ via ``self.get_config_entry('key')``.
337+
338+* See the base ``CloudTestCase`` for additional helper functions.
339+
340 Layout
341-======
342+------
343
344 Integration tests are located under the `tests/cloud_tests` directory.
345 Test configurations are placed under `configs` and the test verification
346@@ -144,126 +350,65 @@ The sub-folders of bugs, examples, main, and modules help organize the
347 tests. View the README.md in each to understand in more detail each
348 directory.
349
350+Test Creation Helper
351+--------------------
352+
353+The integration testing suite has a built in helper to aid in test
354+development. Help can be invoked via ``tox -e citest -- create --help``. It
355+can create a template test case config file with user data passed in from
356+the command line, as well as a template test case verifier module.
357+
358+The following would create a test case named ``example`` under the
359+``modules`` category with the given description, and cloud config data read
360+in from ``/tmp/user_data``.
361+
362+.. code-block:: bash
363+
364+ $ tox -e citest -- create modules/example \
365+ -d "a simple example test case" -c "$(< /tmp/user_data)"
366+
367
368 Development Checklist
369-=====================
370+---------------------
371
372 * Configuration File
373- * Named 'your_test_here.yaml'
374+ * Named 'your_test.yaml'
375 * Contains at least a valid cloud-config
376 * Optionally, commands to capture additional output
377 * Valid YAML
378 * Placed in the appropriate sub-folder in the configs directory
379+ * Any image features required for the test are specified
380 * Verification File
381- * Named 'your_test_here.py'
382+ * Named 'your_test.py'
383 * Valid unit tests validating output collected
384 * Passes pylint & pep8 checks
385- * Placed in the appropriate sub-folder in the testcsaes directory
386+ * Placed in the appropriate sub-folder in the test cases directory
387 * Tested by running the test:
388
389 .. code-block:: bash
390
391- $ python3 -m tests.cloud_tests run -v -n <release of choice> \
392- --deb <build of cloud-init> \
393- -t tests/cloud_tests/configs/<dir>/your_test_here.yaml
394-
395-
396-Execution
397-=========
398-
399-Executing tests has three options:
400-
401-* ``run`` an alias to run both ``collect`` and ``verify``
402-
403-* ``collect`` deploys on the specified platform and os, patches with the
404- requested deb or rpm, and finally collects output of the arbitrary
405- commands.
406-
407-* ``verify`` given a directory of test data, run the Python unit tests on
408- it to generate results.
409-
410-Run
411----
412-The first example will provide a complete end-to-end run of data
413-collection and verification. There are additional examples below
414-explaining how to run one or the other independently.
415-
416-.. code-block:: bash
417-
418- $ git clone https://git.launchpad.net/cloud-init
419- $ cd cloud-init
420- $ python3 -m tests.cloud_tests run -v -n trusty -n xenial \
421- --deb cloud-init_0.7.8~my_patch_all.deb
422-
423-The above command will do the following:
424-
425-* ``-v`` verbose output
426-
427-* ``run`` both collect output and run tests the output
428-
429-* ``-n trusty`` on the Ubuntu Trusty release
430-
431-* ``-n xenial`` on the Ubuntu Xenial release
432-
433-* ``--deb cloud-init_0.7.8~patch_all.deb`` use this deb as the version of
434- cloud-init to run with
435-
436-For a more detailed explanation of each option see below.
437-
438-Collect
439--------
440-
441-If developing tests it may be necessary to see if cloud-config works as
442-expected and the correct files are pulled down. In this case only a
443-collect can be ran by running:
444-
445-.. code-block:: bash
446-
447- $ python3 -m tests.cloud_tests collect -n xenial -d /tmp/collection \
448- --deb cloud-init_0.7.8~my_patch_all.deb
449-
450-The above command will run the collection tests on xenial with the
451-provided deb and place all results into `/tmp/collection`.
452-
453-Verify
454-------
455-
456-When developing tests it is much easier to simply rerun the verify scripts
457-without the more lengthy collect process. This can be done by running:
458-
459-.. code-block:: bash
460-
461- $ python3 -m tests.cloud_tests verify -d /tmp/collection
462-
463-The above command will run the verify scripts on the data discovered in
464-`/tmp/collection`.
465-
466-Run via tox
467------------
468-In order to avoid the need for dependencies and ease the setup and
469-configuration users can run the integration tests via tox:
470-
471-.. code-block:: bash
472-
473- $ tox -e citest -- run [integration test arguments]
474- $ tox -e citest -- run -v -n zesty --deb=cloud-init_all.deb
475- $ tox -e citest -- run -t module/user_groups.yaml
476-
477-Users need to invoke the citest enviornment and then pass any additional
478-arguments.
479-
480+ $ tox -e citest -- run -verbose \
481+ --os-name <release target> \
482+ --test modules/your_test.yaml \
483+ [--deb <build of cloud-init>]
484
485 Architecture
486 ============
487
488-The following outlines the process flow during a complete end-to-end LXD-backed test.
489+The following section outlines the high-level architecture of the
490+integration process.
491+
492+Overview
493+--------
494+The process flow during a complete end-to-end LXD-backed test.
495
496 1. Configuration
497- * The back end and specific OS releases are verified as supported
498- * The test or tests that need to be run are determined either by directory or by individual yaml
499+ * The back end and specific distro releases are verified as supported
500+ * The test or tests that need to be run are determined either by
501+ directory or by individual yaml
502
503 2. Image Creation
504- * Acquire the daily LXD image
505+ * Acquire the request LXD image
506 * Install the specified cloud-init package
507 * Clean the image so that it does not appear to have been booted
508 * A snapshot of the image is created and reused by all tests
509@@ -285,5 +430,247 @@ The following outlines the process flow during a complete end-to-end LXD-backed
510
511 5. Results
512 * If any failures were detected the test suite returns a failure
513+ * Results can be dumped in yaml format to a specified file using the
514+ ``-r <result_file_name>.yaml`` option
515+
516+Configuring the Test Suite
517+--------------------------
518+
519+Most of the behavior of the test suite is configurable through several yaml
520+files. These control the behavior of the test suite's platforms, images, and
521+tests. The main config files for platforms, images and test cases are
522+``platforms.yaml``, ``releases.yaml`` and ``testcases.yaml``.
523
524+Config handling
525+^^^^^^^^^^^^^^^
526+
527+All configurable parts of the test suite use a defaults + overrides system
528+for managing config entries. All base config items are dictionaries.
529+
530+Merging is done on a key-by-key basis, with all keys in the default and
531+override represented in the final result. If a key exists both in
532+the defaults and the overrides, then the behavior depends on the type of data
533+the key refers to. If it is atomic data or a list, then the overrides will
534+replace the default. If the data is a dictionary then the value will be the
535+result of merging that dictionary from the default config and that
536+dictionary from the overrides.
537+
538+Merging is done using the function
539+``tests.cloud_tests.config.merge_config``, which can be examined for more
540+detail on config merging behavior.
541+
542+The following demonstrates merge behavior:
543+
544+.. code-block:: yaml
545+
546+ defaults:
547+ list_item:
548+ - list_entry_1
549+ - list_entry_2
550+ int_item_1: 123
551+ int_item_2: 234
552+ dict_item:
553+ subkey_1: 1
554+ subkey_2: 2
555+ subkey_dict:
556+ subsubkey_1: a
557+ subsubkey_2: b
558+
559+ overrides:
560+ list_item:
561+ - overridden_list_entry
562+ int_item_1: 0
563+ dict_item:
564+ subkey_2: false
565+ subkey_dict:
566+ subsubkey_2: 'new value'
567+
568+ result:
569+ list_item:
570+ - overridden_list_entry
571+ int_item_1: 0
572+ int_item_2: 234
573+ dict_item:
574+ subkey_1: 1
575+ subkey_2: false
576+ subkey_dict:
577+ subsubkey_1: a
578+ subsubkey_2: 'new value'
579+
580+
581+Image Config
582+------------
583+
584+Image configuration is handled in ``releases.yaml``. The image configuration
585+controls how platforms locate and acquire images, how the platforms should
586+interact with the images, how platforms should detect when an image has
587+fully booted, any options that are required to set the image up, and
588+features that the image supports.
589+
590+Since settings for locating an image and interacting with it differ from
591+platform to platform, there are 4 levels of settings available for images on
592+top of the default image settings. The structure of the image config file
593+is:
594+
595+.. code-block:: yaml
596+
597+ default_release_config:
598+ default:
599+ ...
600+ <platform>:
601+ ...
602+ <platform>:
603+ ...
604+
605+ releases:
606+ <release name>:
607+ <default>:
608+ ...
609+ <platform>:
610+ ...
611+ <platform>:
612+ ...
613+
614+
615+The base config is created from the overall defaults and the overrides for
616+the platform. The overrides are created from the default config for the
617+image and the platform specific overrides for the image.
618+
619+System Boot
620+^^^^^^^^^^^
621+
622+The test suite must be able to test if a system has fully booted and if
623+cloud-init has finished running, so that running collect scripts does not
624+race against the target image booting. This is done using the
625+``system_ready_script`` and ``cloud_init_ready_script`` image config keys.
626+
627+Each of these keys accepts a small bash test statement as a string that must
628+return 0 or 1. Since this test statement will be added into a larger bash
629+statement it must be a single statement using the ``[`` test syntax.
630+
631+The default image config provides a system ready script that works for any
632+systemd based image. If the image is not systemd based, then a different
633+test statement must be provided. The default config also provides a test
634+for whether or not cloud-init has finished which checks for the file
635+``/run/cloud-init/result.json``. This should be sufficient for most systems
636+as writing this file is one of the last things cloud-init does.
637+
638+The setting ``boot_timeout`` controls how long, in seconds, the platform
639+should wait for an image to boot. If the system ready script has not
640+indicated that the system is fully booted within this time an error will be
641+raised.
642+
643+Feature Flags
644+^^^^^^^^^^^^^
645+
646+Not all test cases can work on all images due to features the test case
647+requires not being present on that image. If a test case requires features
648+in an image that are not likely to be present across all distros and
649+platforms that the test suite supports, then the test can be skipped
650+everywhere it is not supported.
651+
652+Feature flags, which are names for features supported on some images, but
653+not all that may be required by test cases. Configuration for feature flags
654+is provided in ``releases.yaml`` under the ``features`` top level key. The
655+features config includes a list of all currently defined feature flags,
656+their meanings, and a list of feature groups.
657+
658+Feature groups are groups of features that many images have in common. For
659+example, the ``Ubuntu_specific`` feature group includes features that
660+should be present across most Ubuntu releases, but may or may not be for
661+other distros. Feature groups are specified for an image as a list under
662+the key ``feature_groups``.
663+
664+An image's feature flags are derived from the features groups that that
665+image has and any feature overrides provided. Feature overrides can be
666+specified under the ``features`` key which accepts a dictionary of
667+``{<feature_name>: true/false}`` mappings. If a feature is omitted from an
668+image's feature flags or set to false in the overrides then the test suite
669+will skip any tests that require that feature when using that image.
670+
671+Feature flags may be overridden at run time using the ``--feature-override``
672+command line argument. It accepts a feature flag and value to set in the
673+format ``<feature name>=true/false``. Multiple ``--feature-override``
674+flags can be used, and will all be applied to all feature flags for images
675+used during a test.
676+
677+Setup Overrides
678+^^^^^^^^^^^^^^^
679+
680+If an image requires some of the options for image setup to be used, then it
681+may specify overrides for the command line arguments passed into setup
682+image. These may be specified as a dictionary under the ``setup_overrides``
683+key. When an image is set up, the arguments that control how it is set up
684+will be the arguments from the command line, with any entries in
685+``setup_overrides`` used to override these arguments.
686+
687+For example, images that do not come with cloud-init already installed
688+should have ``setup_overrides: {upgrade: true}`` specified so that in the
689+event that no additional setup options are given, cloud-init will be
690+installed from the image's repos before running tests. Note that if other
691+options such as ``--deb`` are passed in on the command line, these will
692+still work as expected, since apt's policy for cloud-init would prefer the
693+locally installed deb over an older version from the repos.
694+
695+Platform Specific Options
696+^^^^^^^^^^^^^^^^^^^^^^^^^
697+
698+There are many platform specific options in image configuration that allow
699+platforms to locate images and that control additional setup that the
700+platform may have to do to make the image usable. For information on how
701+these work, please consult the documentation for that platform in the
702+integration testing suite and the ``releases.yaml`` file for examples.
703+
704+Error Handling
705+--------------
706+
707+The test suite makes an attempt to run as many tests as possible even in the
708+event of some failing so that automated runs collect as much data as
709+possible. In the event that something goes wrong while setting up for or
710+running a test, the test suite will attempt to continue running any tests
711+which have not been affected by the error.
712+
713+For example, if the test suite was told to run tests on one platform for two
714+releases and an error occurred setting up the first image, all tests for
715+that image would be skipped, and the test suite would continue to set up
716+the second image and run tests on it. Or, if the system does not start
717+properly for one test case out of many to run on that image, that test case
718+will be skipped and the next one will be run.
719+
720+Note that if any errors occur, the test suite will record the failure and
721+where it occurred in the result data and write it out to the specified
722+result file.
723+
724+Results
725+-------
726
727+The test suite generates result data that includes how long each stage of
728+the test suite took and which parts were and were not successful. This data
729+is dumped to the log after the collect and verify stages, and may also be
730+written out in yaml format to a file. If part of the setup failed, the
731+traceback for the failure and the error message will be included in the
732+result file. If a test verifier finds a problem with the collected data
733+from a test run, the class, test function and test will be recorded in the
734+result data.
735+
736+Exit Codes
737+^^^^^^^^^^
738+
739+The test suite counts how many errors occur throughout a run. The exit code
740+after a run is the number of errors that occurred. If the exit code is
741+non-zero then something is wrong either with the test suite, the
742+configuration for an image, a test case, or cloud-init itself.
743+
744+Note that the exit code does not always directly correspond to the number
745+of failed test cases, since in some cases, a single error during image setup
746+can mean that several test cases are not run. If run is used, then the exit
747+code will be the sum of the number of errors in the collect and verify
748+stages.
749+
750+Data Dir
751+^^^^^^^^
752+
753+When using run, the collected data is written into a temporary directory. In
754+the event that all tests pass, this directory is deleted, but if a test
755+fails or an error occurs, this data will be left in place, and a message
756+will be written to the log giving the location of the data.
757diff --git a/tests/cloud_tests/__init__.py b/tests/cloud_tests/__init__.py
758index 099c357..07148c1 100644
759--- a/tests/cloud_tests/__init__.py
760+++ b/tests/cloud_tests/__init__.py
761@@ -1,17 +1,18 @@
762 # This file is part of cloud-init. See LICENSE file for license information.
763
764+"""Main init."""
765+
766 import logging
767 import os
768
769 BASE_DIR = os.path.dirname(os.path.abspath(__file__))
770 TESTCASES_DIR = os.path.join(BASE_DIR, 'testcases')
771 TEST_CONF_DIR = os.path.join(BASE_DIR, 'configs')
772+TREE_BASE = os.sep.join(BASE_DIR.split(os.sep)[:-2])
773
774
775 def _initialize_logging():
776- """
777- configure logging for cloud_tests
778- """
779+ """Configure logging for cloud_tests."""
780 logger = logging.getLogger(__name__)
781 logger.setLevel(logging.DEBUG)
782 formatter = logging.Formatter(
783diff --git a/tests/cloud_tests/__main__.py b/tests/cloud_tests/__main__.py
784index ed654ad..260ddb3 100644
785--- a/tests/cloud_tests/__main__.py
786+++ b/tests/cloud_tests/__main__.py
787@@ -1,19 +1,17 @@
788 # This file is part of cloud-init. See LICENSE file for license information.
789
790+"""Main entry point."""
791+
792 import argparse
793 import logging
794-import shutil
795 import sys
796-import tempfile
797
798-from tests.cloud_tests import (args, collect, manage, verify)
799+from tests.cloud_tests import args, bddeb, collect, manage, run_funcs, verify
800 from tests.cloud_tests import LOG
801
802
803 def configure_log(args):
804- """
805- configure logging
806- """
807+ """Configure logging."""
808 level = logging.INFO
809 if args.verbose:
810 level = logging.DEBUG
811@@ -22,41 +20,15 @@ def configure_log(args):
812 LOG.setLevel(level)
813
814
815-def run(args):
816- """
817- run full test suite
818- """
819- failed = 0
820- args.data_dir = tempfile.mkdtemp(prefix='cloud_test_data_')
821- LOG.debug('using tmpdir %s', args.data_dir)
822- try:
823- failed += collect.collect(args)
824- failed += verify.verify(args)
825- except Exception:
826- failed += 1
827- raise
828- finally:
829- # TODO: make this configurable via environ or cmdline
830- if failed:
831- LOG.warning('some tests failed, leaving data in %s', args.data_dir)
832- else:
833- shutil.rmtree(args.data_dir)
834- return failed
835-
836-
837 def main():
838- """
839- entry point for cloud test suite
840- """
841+ """Entry point for cloud test suite."""
842 # configure parser
843 parser = argparse.ArgumentParser(prog='cloud_tests')
844 subparsers = parser.add_subparsers(dest="subcmd")
845 subparsers.required = True
846
847 def add_subparser(name, description, arg_sets):
848- """
849- add arguments to subparser
850- """
851+ """Add arguments to subparser."""
852 subparser = subparsers.add_parser(name, help=description)
853 for (_args, _kwargs) in (a for arg_set in arg_sets for a in arg_set):
854 subparser.add_argument(*_args, **_kwargs)
855@@ -80,9 +52,12 @@ def main():
856 # run handler
857 LOG.debug('running with args: %s\n', parsed)
858 return {
859+ 'bddeb': bddeb.bddeb,
860 'collect': collect.collect,
861 'create': manage.create,
862- 'run': run,
863+ 'run': run_funcs.run,
864+ 'tree_collect': run_funcs.tree_collect,
865+ 'tree_run': run_funcs.tree_run,
866 'verify': verify.verify,
867 }[parsed.subcmd](parsed)
868
869diff --git a/tests/cloud_tests/args.py b/tests/cloud_tests/args.py
870index 371b044..369d60d 100644
871--- a/tests/cloud_tests/args.py
872+++ b/tests/cloud_tests/args.py
873@@ -1,23 +1,43 @@
874 # This file is part of cloud-init. See LICENSE file for license information.
875
876+"""Argparse argument setup and sanitization."""
877+
878 import os
879
880 from tests.cloud_tests import config, util
881-from tests.cloud_tests import LOG
882+from tests.cloud_tests import LOG, TREE_BASE
883
884 ARG_SETS = {
885+ 'BDDEB': (
886+ (('--bddeb-args',),
887+ {'help': 'args to pass through to bddeb',
888+ 'action': 'store', 'default': None, 'required': False}),
889+ (('--build-os',),
890+ {'help': 'OS to use as build system (default is xenial)',
891+ 'action': 'store', 'choices': config.ENABLED_DISTROS,
892+ 'default': 'xenial', 'required': False}),
893+ (('--build-platform',),
894+ {'help': 'platform to use for build system (default is lxd)',
895+ 'action': 'store', 'choices': config.ENABLED_PLATFORMS,
896+ 'default': 'lxd', 'required': False}),
897+ (('--cloud-init',),
898+ {'help': 'path to base of cloud-init tree', 'metavar': 'DIR',
899+ 'action': 'store', 'required': False, 'default': TREE_BASE}),),
900 'COLLECT': (
901 (('-p', '--platform'),
902 {'help': 'platform(s) to run tests on', 'metavar': 'PLATFORM',
903- 'action': 'append', 'choices': config.list_enabled_platforms(),
904+ 'action': 'append', 'choices': config.ENABLED_PLATFORMS,
905 'default': []}),
906 (('-n', '--os-name'),
907 {'help': 'the name(s) of the OS(s) to test', 'metavar': 'NAME',
908- 'action': 'append', 'choices': config.list_enabled_distros(),
909+ 'action': 'append', 'choices': config.ENABLED_DISTROS,
910 'default': []}),
911 (('-t', '--test-config'),
912 {'help': 'test config file(s) to use', 'metavar': 'FILE',
913- 'action': 'append', 'default': []}),),
914+ 'action': 'append', 'default': []}),
915+ (('--feature-override',),
916+ {'help': 'feature flags override(s), <flagname>=<true/false>',
917+ 'action': 'append', 'default': [], 'required': False}),),
918 'CREATE': (
919 (('-c', '--config'),
920 {'help': 'cloud-config yaml for testcase', 'metavar': 'DATA',
921@@ -41,7 +61,15 @@ ARG_SETS = {
922 'OUTPUT': (
923 (('-d', '--data-dir'),
924 {'help': 'directory to store test data in',
925- 'action': 'store', 'metavar': 'DIR', 'required': True}),),
926+ 'action': 'store', 'metavar': 'DIR', 'required': False}),
927+ (('--preserve-data',),
928+ {'help': 'do not remove collected data after successful run',
929+ 'action': 'store_true', 'default': False, 'required': False}),),
930+ 'OUTPUT_DEB': (
931+ (('--deb',),
932+ {'help': 'path to write output deb to', 'metavar': 'FILE',
933+ 'action': 'store', 'required': False,
934+ 'default': 'cloud-init_all.deb'}),),
935 'RESULT': (
936 (('-r', '--result'),
937 {'help': 'file to write results to',
938@@ -61,31 +89,54 @@ ARG_SETS = {
939 {'help': 'ppa to enable (implies -u)', 'metavar': 'NAME',
940 'action': 'store'}),
941 (('-u', '--upgrade'),
942- {'help': 'upgrade before starting tests', 'action': 'store_true',
943- 'default': False}),),
944+ {'help': 'upgrade or install cloud-init from repo',
945+ 'action': 'store_true', 'default': False}),
946+ (('--upgrade-full',),
947+ {'help': 'do full system upgrade from repo (implies -u)',
948+ 'action': 'store_true', 'default': False}),),
949+
950 }
951
952 SUBCMDS = {
953+ 'bddeb': ('build cloud-init deb from tree',
954+ ('BDDEB', 'OUTPUT_DEB', 'INTERFACE')),
955 'collect': ('collect test data',
956 ('COLLECT', 'INTERFACE', 'OUTPUT', 'RESULT', 'SETUP')),
957 'create': ('create new test case', ('CREATE', 'INTERFACE')),
958- 'run': ('run test suite', ('COLLECT', 'INTERFACE', 'RESULT', 'SETUP')),
959+ 'run': ('run test suite',
960+ ('COLLECT', 'INTERFACE', 'RESULT', 'OUTPUT', 'SETUP')),
961+ 'tree_collect': ('collect using current working tree',
962+ ('BDDEB', 'COLLECT', 'INTERFACE', 'OUTPUT', 'RESULT')),
963+ 'tree_run': ('run using current working tree',
964+ ('BDDEB', 'COLLECT', 'INTERFACE', 'OUTPUT', 'RESULT')),
965 'verify': ('verify test data', ('INTERFACE', 'OUTPUT', 'RESULT')),
966 }
967
968
969 def _empty_normalizer(args):
970+ """Do not normalize arguments."""
971+ return args
972+
973+
974+def normalize_bddeb_args(args):
975+ """Normalize BDDEB arguments.
976+
977+ @param args: parsed args
978+ @return_value: updated args, or None if errors encountered
979 """
980- do not normalize arguments
981- """
982+ # make sure cloud-init dir is accessible
983+ if not (args.cloud_init and os.path.isdir(args.cloud_init)):
984+ LOG.error('invalid cloud-init tree path')
985+ return None
986+
987 return args
988
989
990 def normalize_create_args(args):
991- """
992- normalize CREATE arguments
993- args: parsed args
994- return_value: updated args, or None if errors occurred
995+ """Normalize CREATE arguments.
996+
997+ @param args: parsed args
998+ @return_value: updated args, or None if errors occurred
999 """
1000 # ensure valid name for new test
1001 if len(args.name.split('/')) != 2:
1002@@ -114,22 +165,22 @@ def normalize_create_args(args):
1003
1004
1005 def normalize_collect_args(args):
1006- """
1007- normalize COLLECT arguments
1008- args: parsed args
1009- return_value: updated args, or None if errors occurred
1010+ """Normalize COLLECT arguments.
1011+
1012+ @param args: parsed args
1013+ @return_value: updated args, or None if errors occurred
1014 """
1015 # platform should default to all supported
1016 if len(args.platform) == 0:
1017- args.platform = config.list_enabled_platforms()
1018+ args.platform = config.ENABLED_PLATFORMS
1019 args.platform = util.sorted_unique(args.platform)
1020
1021 # os name should default to all enabled
1022 # if os name is provided ensure that all provided are supported
1023 if len(args.os_name) == 0:
1024- args.os_name = config.list_enabled_distros()
1025+ args.os_name = config.ENABLED_DISTROS
1026 else:
1027- supported = config.list_enabled_distros()
1028+ supported = config.ENABLED_DISTROS
1029 invalid = [os_name for os_name in args.os_name
1030 if os_name not in supported]
1031 if len(invalid) != 0:
1032@@ -158,18 +209,33 @@ def normalize_collect_args(args):
1033 args.test_config = valid
1034 args.test_config = util.sorted_unique(args.test_config)
1035
1036+ # parse feature flag overrides and ensure all are valid
1037+ if args.feature_override:
1038+ overrides = args.feature_override
1039+ args.feature_override = util.parse_conf_list(
1040+ overrides, boolean=True, valid=config.list_feature_flags())
1041+ if not args.feature_override:
1042+ LOG.error('invalid feature flag override(s): %s', overrides)
1043+ return None
1044+ else:
1045+ args.feature_override = {}
1046+
1047 return args
1048
1049
1050 def normalize_output_args(args):
1051+ """Normalize OUTPUT arguments.
1052+
1053+ @param args: parsed args
1054+ @return_value: updated args, or None if errors occurred
1055 """
1056- normalize OUTPUT arguments
1057- args: parsed args
1058- return_value: updated args, or None if errors occurred
1059- """
1060+ if args.data_dir:
1061+ args.data_dir = os.path.abspath(args.data_dir)
1062+ if not os.path.exists(args.data_dir):
1063+ os.mkdir(args.data_dir)
1064+
1065 if not args.data_dir:
1066- LOG.error('--data-dir must be specified')
1067- return None
1068+ args.data_dir = None
1069
1070 # ensure clean output dir if collect
1071 # ensure data exists if verify
1072@@ -177,19 +243,31 @@ def normalize_output_args(args):
1073 if not util.is_clean_writable_dir(args.data_dir):
1074 LOG.error('data_dir must be empty/new and must be writable')
1075 return None
1076- elif args.subcmd == 'verify':
1077- if not os.path.exists(args.data_dir):
1078- LOG.error('data_dir %s does not exist', args.data_dir)
1079- return None
1080
1081 return args
1082
1083
1084-def normalize_setup_args(args):
1085+def normalize_output_deb_args(args):
1086+ """Normalize OUTPUT_DEB arguments.
1087+
1088+ @param args: parsed args
1089+ @return_value: updated args, or None if erros occurred
1090 """
1091- normalize SETUP arguments
1092- args: parsed args
1093- return_value: updated_args, or None if errors occurred
1094+ # make sure to use abspath for deb
1095+ args.deb = os.path.abspath(args.deb)
1096+
1097+ if not args.deb.endswith('.deb'):
1098+ LOG.error('output filename does not end in ".deb"')
1099+ return None
1100+
1101+ return args
1102+
1103+
1104+def normalize_setup_args(args):
1105+ """Normalize SETUP arguments.
1106+
1107+ @param args: parsed args
1108+ @return_value: updated_args, or None if errors occurred
1109 """
1110 # ensure deb or rpm valid if specified
1111 for pkg in (args.deb, args.rpm):
1112@@ -210,10 +288,12 @@ def normalize_setup_args(args):
1113
1114
1115 NORMALIZERS = {
1116+ 'BDDEB': normalize_bddeb_args,
1117 'COLLECT': normalize_collect_args,
1118 'CREATE': normalize_create_args,
1119 'INTERFACE': _empty_normalizer,
1120 'OUTPUT': normalize_output_args,
1121+ 'OUTPUT_DEB': normalize_output_deb_args,
1122 'RESULT': _empty_normalizer,
1123 'SETUP': normalize_setup_args,
1124 }
1125diff --git a/tests/cloud_tests/bddeb.py b/tests/cloud_tests/bddeb.py
1126new file mode 100644
1127index 0000000..53dbf74
1128--- /dev/null
1129+++ b/tests/cloud_tests/bddeb.py
1130@@ -0,0 +1,118 @@
1131+# This file is part of cloud-init. See LICENSE file for license information.
1132+
1133+"""Used to build a deb."""
1134+
1135+from functools import partial
1136+import os
1137+import tempfile
1138+
1139+from cloudinit import util as c_util
1140+from tests.cloud_tests import (config, LOG)
1141+from tests.cloud_tests import (platforms, images, snapshots, instances)
1142+from tests.cloud_tests.stage import (PlatformComponent, run_stage, run_single)
1143+
1144+build_deps = ['devscripts', 'equivs', 'git', 'tar']
1145+
1146+
1147+def _out(cmd_res):
1148+ """Get clean output from cmd result."""
1149+ return cmd_res[0].strip()
1150+
1151+
1152+def build_deb(args, instance):
1153+ """Build deb on system and copy out to location at args.deb.
1154+
1155+ @param args: cmdline arguments
1156+ @return_value: tuple of results and fail count
1157+ """
1158+ # update remote system package list and install build deps
1159+ LOG.debug('installing build deps')
1160+ pkgs = ' '.join(build_deps)
1161+ cmd = 'apt-get update && apt-get install --yes {}'.format(pkgs)
1162+ instance.execute(['/bin/sh', '-c', cmd])
1163+ # TODO Remove this call once we have a ci-deps Makefile target
1164+ instance.execute(['mk-build-deps', '--install', '-t',
1165+ 'apt-get --no-install-recommends --yes', 'cloud-init'])
1166+
1167+ # local tmpfile that must be deleted
1168+ local_tarball = tempfile.NamedTemporaryFile().name
1169+
1170+ # paths to use in remote system
1171+ output_link = '/root/cloud-init_all.deb'
1172+ remote_tarball = _out(instance.execute(['mktemp']))
1173+ extract_dir = _out(instance.execute(['mktemp', '--directory']))
1174+ bddeb_path = os.path.join(extract_dir, 'packages', 'bddeb')
1175+ git_env = {'GIT_DIR': os.path.join(extract_dir, '.git'),
1176+ 'GIT_WORK_TREE': extract_dir}
1177+
1178+ LOG.debug('creating tarball of cloud-init at: %s', local_tarball)
1179+ c_util.subp(['tar', 'cf', local_tarball, '--owner', 'root',
1180+ '--group', 'root', '-C', args.cloud_init, '.'])
1181+ LOG.debug('copying to remote system at: %s', remote_tarball)
1182+ instance.push_file(local_tarball, remote_tarball)
1183+
1184+ LOG.debug('extracting tarball in remote system at: %s', extract_dir)
1185+ instance.execute(['tar', 'xf', remote_tarball, '-C', extract_dir])
1186+ instance.execute(['git', 'commit', '-a', '-m', 'tmp', '--allow-empty'],
1187+ env=git_env)
1188+
1189+ LOG.debug('building deb in remote system at: %s', output_link)
1190+ bddeb_args = args.bddeb_args.split() if args.bddeb_args else []
1191+ instance.execute([bddeb_path, '-d'] + bddeb_args, env=git_env)
1192+
1193+ # copy the deb back to the host system
1194+ LOG.debug('copying built deb to host at: %s', args.deb)
1195+ instance.pull_file(output_link, args.deb)
1196+
1197+
1198+def setup_build(args):
1199+ """Set build system up then run build.
1200+
1201+ @param args: cmdline arguments
1202+ @return_value: tuple of results and fail count
1203+ """
1204+ res = ({}, 1)
1205+
1206+ # set up platform
1207+ LOG.info('setting up platform: %s', args.build_platform)
1208+ platform_config = config.load_platform_config(args.build_platform)
1209+ platform_call = partial(platforms.get_platform, args.build_platform,
1210+ platform_config)
1211+ with PlatformComponent(platform_call) as platform:
1212+
1213+ # set up image
1214+ LOG.info('acquiring image for os: %s', args.build_os)
1215+ img_conf = config.load_os_config(platform.platform_name, args.build_os)
1216+ image_call = partial(images.get_image, platform, img_conf)
1217+ with PlatformComponent(image_call) as image:
1218+
1219+ # set up snapshot
1220+ snapshot_call = partial(snapshots.get_snapshot, image)
1221+ with PlatformComponent(snapshot_call) as snapshot:
1222+
1223+ # create instance with cloud-config to set it up
1224+ LOG.info('creating instance to build deb in')
1225+ empty_cloud_config = "#cloud-config\n{}"
1226+ instance_call = partial(
1227+ instances.get_instance, snapshot, empty_cloud_config,
1228+ use_desc='build cloud-init deb')
1229+ with PlatformComponent(instance_call) as instance:
1230+
1231+ # build the deb
1232+ res = run_single('build deb on system',
1233+ partial(build_deb, args, instance))
1234+
1235+ return res
1236+
1237+
1238+def bddeb(args):
1239+ """Entry point for build deb.
1240+
1241+ @param args: cmdline arguments
1242+ @return_value: fail count
1243+ """
1244+ LOG.info('preparing to build cloud-init deb')
1245+ (res, failed) = run_stage('build deb', [partial(setup_build, args)])
1246+ return failed
1247+
1248+# vi: ts=4 expandtab
1249diff --git a/tests/cloud_tests/collect.py b/tests/cloud_tests/collect.py
1250index 02fc0e5..b44e8bd 100644
1251--- a/tests/cloud_tests/collect.py
1252+++ b/tests/cloud_tests/collect.py
1253@@ -1,34 +1,39 @@
1254 # This file is part of cloud-init. See LICENSE file for license information.
1255
1256-from tests.cloud_tests import (config, LOG, setup_image, util)
1257-from tests.cloud_tests.stage import (PlatformComponent, run_stage, run_single)
1258-from tests.cloud_tests import (platforms, images, snapshots, instances)
1259+"""Used to collect data from platforms during tests."""
1260
1261 from functools import partial
1262 import os
1263
1264+from cloudinit import util as c_util
1265+from tests.cloud_tests import (config, LOG, setup_image, util)
1266+from tests.cloud_tests.stage import (PlatformComponent, run_stage, run_single)
1267+from tests.cloud_tests import (platforms, images, snapshots, instances)
1268+
1269
1270 def collect_script(instance, base_dir, script, script_name):
1271- """
1272- collect script data
1273- instance: instance to run script on
1274- base_dir: base directory for output data
1275- script: script contents
1276- script_name: name of script to run
1277- return_value: None, may raise errors
1278+ """Collect script data.
1279+
1280+ @param instance: instance to run script on
1281+ @param base_dir: base directory for output data
1282+ @param script: script contents
1283+ @param script_name: name of script to run
1284+ @return_value: None, may raise errors
1285 """
1286 LOG.debug('running collect script: %s', script_name)
1287- util.write_file(os.path.join(base_dir, script_name),
1288- instance.run_script(script))
1289+ (out, err, exit) = instance.run_script(
1290+ script, rcs=range(0, 256),
1291+ description='collect: {}'.format(script_name))
1292+ c_util.write_file(os.path.join(base_dir, script_name), out)
1293
1294
1295 def collect_test_data(args, snapshot, os_name, test_name):
1296- """
1297- collect data for test case
1298- args: cmdline arguments
1299- snapshot: instantiated snapshot
1300- test_name: name or path of test to run
1301- return_value: tuple of results and fail count
1302+ """Collect data for test case.
1303+
1304+ @param args: cmdline arguments
1305+ @param snapshot: instantiated snapshot
1306+ @param test_name: name or path of test to run
1307+ @return_value: tuple of results and fail count
1308 """
1309 res = ({}, 1)
1310
1311@@ -39,15 +44,27 @@ def collect_test_data(args, snapshot, os_name, test_name):
1312 test_scripts = test_config['collect_scripts']
1313 test_output_dir = os.sep.join(
1314 (args.data_dir, snapshot.platform_name, os_name, test_name))
1315- boot_timeout = (test_config.get('boot_timeout')
1316- if isinstance(test_config.get('boot_timeout'), int) else
1317- snapshot.config.get('timeout'))
1318
1319 # if test is not enabled, skip and return 0 failures
1320 if not test_config.get('enabled', False):
1321 LOG.warning('test config %s is not enabled, skipping', test_name)
1322 return ({}, 0)
1323
1324+ # if testcase requires a feature flag that the image does not support,
1325+ # skip the testcase with a warning
1326+ req_features = test_config.get('required_features', [])
1327+ if any(feature not in snapshot.features for feature in req_features):
1328+ LOG.warn('test config %s requires features not supported by image, '
1329+ 'skipping.\nrequired features: %s\nsupported features: %s',
1330+ test_name, req_features, snapshot.features)
1331+ return ({}, 0)
1332+
1333+ # if there are user data overrides required for this test case, apply them
1334+ overrides = snapshot.config.get('user_data_overrides', {})
1335+ if overrides:
1336+ LOG.debug('updating user data for collect with: %s', overrides)
1337+ user_data = util.update_user_data(user_data, overrides)
1338+
1339 # create test instance
1340 component = PlatformComponent(
1341 partial(instances.get_instance, snapshot, user_data,
1342@@ -56,7 +73,7 @@ def collect_test_data(args, snapshot, os_name, test_name):
1343 LOG.info('collecting test data for test: %s', test_name)
1344 with component as instance:
1345 start_call = partial(run_single, 'boot instance', partial(
1346- instance.start, wait=True, wait_time=boot_timeout))
1347+ instance.start, wait=True, wait_for_cloud_init=True))
1348 collect_calls = [partial(run_single, 'script {}'.format(script_name),
1349 partial(collect_script, instance,
1350 test_output_dir, script, script_name))
1351@@ -69,11 +86,11 @@ def collect_test_data(args, snapshot, os_name, test_name):
1352
1353
1354 def collect_snapshot(args, image, os_name):
1355- """
1356- collect data for snapshot of image
1357- args: cmdline arguments
1358- image: instantiated image with set up complete
1359- return_value tuple of results and fail count
1360+ """Collect data for snapshot of image.
1361+
1362+ @param args: cmdline arguments
1363+ @param image: instantiated image with set up complete
1364+ @return_value tuple of results and fail count
1365 """
1366 res = ({}, 1)
1367
1368@@ -91,19 +108,18 @@ def collect_snapshot(args, image, os_name):
1369
1370
1371 def collect_image(args, platform, os_name):
1372- """
1373- collect data for image
1374- args: cmdline arguments
1375- platform: instantiated platform
1376- os_name: name of distro to collect for
1377- return_value: tuple of results and fail count
1378+ """Collect data for image.
1379+
1380+ @param args: cmdline arguments
1381+ @param platform: instantiated platform
1382+ @param os_name: name of distro to collect for
1383+ @return_value: tuple of results and fail count
1384 """
1385 res = ({}, 1)
1386
1387- os_config = config.load_os_config(os_name)
1388- if not os_config.get('enabled'):
1389- raise ValueError('OS {} not enabled'.format(os_name))
1390-
1391+ os_config = config.load_os_config(
1392+ platform.platform_name, os_name, require_enabled=True,
1393+ feature_overrides=args.feature_override)
1394 component = PlatformComponent(
1395 partial(images.get_image, platform, os_config))
1396
1397@@ -118,18 +134,16 @@ def collect_image(args, platform, os_name):
1398
1399
1400 def collect_platform(args, platform_name):
1401- """
1402- collect data for platform
1403- args: cmdline arguments
1404- platform_name: platform to collect for
1405- return_value: tuple of results and fail count
1406+ """Collect data for platform.
1407+
1408+ @param args: cmdline arguments
1409+ @param platform_name: platform to collect for
1410+ @return_value: tuple of results and fail count
1411 """
1412 res = ({}, 1)
1413
1414- platform_config = config.load_platform_config(platform_name)
1415- if not platform_config.get('enabled'):
1416- raise ValueError('Platform {} not enabled'.format(platform_name))
1417-
1418+ platform_config = config.load_platform_config(
1419+ platform_name, require_enabled=True)
1420 component = PlatformComponent(
1421 partial(platforms.get_platform, platform_name, platform_config))
1422
1423@@ -143,10 +157,10 @@ def collect_platform(args, platform_name):
1424
1425
1426 def collect(args):
1427- """
1428- entry point for collection
1429- args: cmdline arguments
1430- return_value: fail count
1431+ """Entry point for collection.
1432+
1433+ @param args: cmdline arguments
1434+ @return_value: fail count
1435 """
1436 (res, failed) = run_stage(
1437 'collect data', [partial(collect_platform, args, platform_name)
1438diff --git a/tests/cloud_tests/config.py b/tests/cloud_tests/config.py
1439index f3a13c9..4d5dc80 100644
1440--- a/tests/cloud_tests/config.py
1441+++ b/tests/cloud_tests/config.py
1442@@ -1,5 +1,7 @@
1443 # This file is part of cloud-init. See LICENSE file for license information.
1444
1445+"""Used to setup test configuration."""
1446+
1447 import glob
1448 import os
1449
1450@@ -14,46 +16,44 @@ RELEASES_CONF = os.path.join(BASE_DIR, 'releases.yaml')
1451 TESTCASE_CONF = os.path.join(BASE_DIR, 'testcases.yaml')
1452
1453
1454+def get(base, key):
1455+ """Get config entry 'key' from base, ensuring is dictionary."""
1456+ return base[key] if key in base and base[key] is not None else {}
1457+
1458+
1459+def enabled(config):
1460+ """Test if config item is enabled."""
1461+ return isinstance(config, dict) and config.get('enabled', False)
1462+
1463+
1464 def path_to_name(path):
1465- """
1466- convert abs or rel path to test config to path under configs/
1467- if already a test name, do nothing
1468- """
1469+ """Convert abs or rel path to test config to path under 'sconfigs/'."""
1470 dir_path, file_name = os.path.split(os.path.normpath(path))
1471 name = os.path.splitext(file_name)[0]
1472 return os.sep.join((os.path.basename(dir_path), name))
1473
1474
1475 def name_to_path(name):
1476- """
1477- convert test config path under configs/ to full config path,
1478- if already a full path, do nothing
1479- """
1480+ """Convert test config path under configs/ to full config path."""
1481 name = os.path.normpath(name)
1482 if not name.endswith(CONF_EXT):
1483 name = name + CONF_EXT
1484 return name if os.path.isabs(name) else os.path.join(TEST_CONF_DIR, name)
1485
1486
1487-def name_sanatize(name):
1488- """
1489- sanatize test name to be used as a module name
1490- """
1491+def name_sanitize(name):
1492+ """Sanitize test name to be used as a module name."""
1493 return name.replace('-', '_')
1494
1495
1496 def name_to_module(name):
1497- """
1498- convert test name to a loadable module name under testcases/
1499- """
1500- name = name_sanatize(path_to_name(name))
1501+ """Convert test name to a loadable module name under 'testcases/'."""
1502+ name = name_sanitize(path_to_name(name))
1503 return name.replace(os.path.sep, '.')
1504
1505
1506 def merge_config(base, override):
1507- """
1508- merge config and base
1509- """
1510+ """Merge config and base."""
1511 res = base.copy()
1512 res.update(override)
1513 res.update({k: merge_config(base.get(k, {}), v)
1514@@ -61,53 +61,102 @@ def merge_config(base, override):
1515 return res
1516
1517
1518-def load_platform_config(platform):
1519+def merge_feature_groups(feature_conf, feature_groups, overrides):
1520+ """Combine feature groups and overrides to construct a supported list.
1521+
1522+ @param feature_conf: feature config from releases.yaml
1523+ @param feature_groups: feature groups the release is a member of
1524+ @param overrides: overrides specified by the release's config
1525+ @return_value: dict of {feature: true/false} settings
1526 """
1527- load configuration for platform
1528+ res = dict().fromkeys(feature_conf['all'])
1529+ for group in feature_groups:
1530+ res.update(feature_conf['groups'][group])
1531+ res.update(overrides)
1532+ return res
1533+
1534+
1535+def load_platform_config(platform_name, require_enabled=False):
1536+ """Load configuration for platform.
1537+
1538+ @param platform_name: name of platform to retrieve config for
1539+ @param require_enabled: if true, raise error if 'enabled' not True
1540+ @return_value: config dict
1541 """
1542 main_conf = c_util.read_conf(PLATFORM_CONF)
1543- return merge_config(main_conf.get('default_platform_config'),
1544- main_conf.get('platforms')[platform])
1545+ conf = merge_config(main_conf['default_platform_config'],
1546+ main_conf['platforms'][platform_name])
1547+ if require_enabled and not enabled(conf):
1548+ raise ValueError('Platform is not enabled')
1549+ return conf
1550
1551
1552-def load_os_config(os_name):
1553- """
1554- load configuration for os
1555+def load_os_config(platform_name, os_name, require_enabled=False,
1556+ feature_overrides={}):
1557+ """Load configuration for os.
1558+
1559+ @param platform_name: platform name to load os config for
1560+ @param os_name: name of os to retrieve config for
1561+ @param require_enabled: if true, raise error if 'enabled' not True
1562+ @param feature_overrides: feature flag overrides to merge with features
1563+ @return_value: config dict
1564 """
1565 main_conf = c_util.read_conf(RELEASES_CONF)
1566- return merge_config(main_conf.get('default_release_config'),
1567- main_conf.get('releases')[os_name])
1568+ default = main_conf['default_release_config']
1569+ image = main_conf['releases'][os_name]
1570+ conf = merge_config(merge_config(get(default, 'default'),
1571+ get(default, platform_name)),
1572+ merge_config(get(image, 'default'),
1573+ get(image, platform_name)))
1574+
1575+ feature_conf = main_conf['features']
1576+ feature_groups = conf.get('feature_groups', [])
1577+ overrides = merge_config(get(conf, 'features'), feature_overrides)
1578+ conf['features'] = merge_feature_groups(
1579+ feature_conf, feature_groups, overrides)
1580+
1581+ if require_enabled and not enabled(conf):
1582+ raise ValueError('OS is not enabled')
1583+ return conf
1584
1585
1586 def load_test_config(path):
1587- """
1588- load a test config file by either abs path or rel path
1589- """
1590+ """Load a test config file by either abs path or rel path."""
1591 return merge_config(c_util.read_conf(TESTCASE_CONF)['base_test_data'],
1592 c_util.read_conf(name_to_path(path)))
1593
1594
1595+def list_feature_flags():
1596+ """List all supported feature flags."""
1597+ feature_conf = get(c_util.read_conf(RELEASES_CONF), 'features')
1598+ return feature_conf.get('all', [])
1599+
1600+
1601 def list_enabled_platforms():
1602- """
1603- list all platforms enabled for testing
1604- """
1605- platforms = c_util.read_conf(PLATFORM_CONF).get('platforms')
1606- return [k for k, v in platforms.items() if v.get('enabled')]
1607+ """List all platforms enabled for testing."""
1608+ platforms = get(c_util.read_conf(PLATFORM_CONF), 'platforms')
1609+ return [k for k, v in platforms.items() if enabled(v)]
1610
1611
1612-def list_enabled_distros():
1613- """
1614- list all distros enabled for testing
1615- """
1616- releases = c_util.read_conf(RELEASES_CONF).get('releases')
1617- return [k for k, v in releases.items() if v.get('enabled')]
1618+def list_enabled_distros(platforms):
1619+ """List all distros enabled for testing on specified platforms."""
1620+ def platform_has_enabled(config):
1621+ """List if platform is enabled."""
1622+ return any(enabled(merge_config(get(config, 'default'),
1623+ get(config, platform)))
1624+ for platform in platforms)
1625+
1626+ releases = get(c_util.read_conf(RELEASES_CONF), 'releases')
1627+ return [k for k, v in releases.items() if platform_has_enabled(v)]
1628
1629
1630 def list_test_configs():
1631- """
1632- list all available test config files by abspath
1633- """
1634+ """List all available test config files by abspath."""
1635 return [os.path.abspath(f) for f in
1636 glob.glob(os.sep.join((TEST_CONF_DIR, '*', '*.yaml')))]
1637
1638+
1639+ENABLED_PLATFORMS = sorted(list_enabled_platforms())
1640+ENABLED_DISTROS = sorted(list_enabled_distros(ENABLED_PLATFORMS))
1641+
1642 # vi: ts=4 expandtab
1643diff --git a/tests/cloud_tests/configs/bugs/lp1628337.yaml b/tests/cloud_tests/configs/bugs/lp1628337.yaml
1644index 1d6bf48..e39b3cd 100644
1645--- a/tests/cloud_tests/configs/bugs/lp1628337.yaml
1646+++ b/tests/cloud_tests/configs/bugs/lp1628337.yaml
1647@@ -1,6 +1,9 @@
1648 #
1649 # LP Bug 1628337: cloud-init tries to install NTP before even configuring the archives
1650 #
1651+required_features:
1652+ - apt
1653+ - lsb_release
1654 cloud_config: |
1655 #cloud-config
1656 ntp:
1657diff --git a/tests/cloud_tests/configs/examples/add_apt_repositories.yaml b/tests/cloud_tests/configs/examples/add_apt_repositories.yaml
1658index b896435..4b8575f 100644
1659--- a/tests/cloud_tests/configs/examples/add_apt_repositories.yaml
1660+++ b/tests/cloud_tests/configs/examples/add_apt_repositories.yaml
1661@@ -4,6 +4,8 @@
1662 # 2016-11-17: Disabled as covered by module based tests
1663 #
1664 enabled: False
1665+required_features:
1666+ - apt
1667 cloud_config: |
1668 #cloud-config
1669 apt:
1670diff --git a/tests/cloud_tests/configs/modules/apt_configure_conf.yaml b/tests/cloud_tests/configs/modules/apt_configure_conf.yaml
1671index 163ae3f..de45300 100644
1672--- a/tests/cloud_tests/configs/modules/apt_configure_conf.yaml
1673+++ b/tests/cloud_tests/configs/modules/apt_configure_conf.yaml
1674@@ -1,6 +1,8 @@
1675 #
1676 # Provide a configuration for APT
1677 #
1678+required_features:
1679+ - apt
1680 cloud_config: |
1681 #cloud-config
1682 apt:
1683diff --git a/tests/cloud_tests/configs/modules/apt_configure_disable_suites.yaml b/tests/cloud_tests/configs/modules/apt_configure_disable_suites.yaml
1684index 73e4a53..9880067 100644
1685--- a/tests/cloud_tests/configs/modules/apt_configure_disable_suites.yaml
1686+++ b/tests/cloud_tests/configs/modules/apt_configure_disable_suites.yaml
1687@@ -1,6 +1,9 @@
1688 #
1689 # Disables everything in sources.list
1690 #
1691+required_features:
1692+ - apt
1693+ - lsb_release
1694 cloud_config: |
1695 #cloud-config
1696 apt:
1697diff --git a/tests/cloud_tests/configs/modules/apt_configure_primary.yaml b/tests/cloud_tests/configs/modules/apt_configure_primary.yaml
1698index 2ec30ca..41bcf2f 100644
1699--- a/tests/cloud_tests/configs/modules/apt_configure_primary.yaml
1700+++ b/tests/cloud_tests/configs/modules/apt_configure_primary.yaml
1701@@ -1,6 +1,9 @@
1702 #
1703 # Setup a custome primary sources.list
1704 #
1705+required_features:
1706+ - apt
1707+ - apt_src_cont
1708 cloud_config: |
1709 #cloud-config
1710 apt:
1711@@ -16,4 +19,8 @@ collect_scripts:
1712 #!/bin/bash
1713 grep -v '^#' /etc/apt/sources.list | sed '/^\s*$/d' | grep -c gtlib.gatech.edu
1714
1715+ sources.list: |
1716+ #!/bin/bash
1717+ cat /etc/apt/sources.list
1718+
1719 # vi: ts=4 expandtab
1720diff --git a/tests/cloud_tests/configs/modules/apt_configure_proxy.yaml b/tests/cloud_tests/configs/modules/apt_configure_proxy.yaml
1721index e737130..be6c6f8 100644
1722--- a/tests/cloud_tests/configs/modules/apt_configure_proxy.yaml
1723+++ b/tests/cloud_tests/configs/modules/apt_configure_proxy.yaml
1724@@ -1,6 +1,8 @@
1725 #
1726 # Set apt proxy
1727 #
1728+required_features:
1729+ - apt
1730 cloud_config: |
1731 #cloud-config
1732 apt:
1733diff --git a/tests/cloud_tests/configs/modules/apt_configure_security.yaml b/tests/cloud_tests/configs/modules/apt_configure_security.yaml
1734index f6a2c82..83dd51d 100644
1735--- a/tests/cloud_tests/configs/modules/apt_configure_security.yaml
1736+++ b/tests/cloud_tests/configs/modules/apt_configure_security.yaml
1737@@ -1,6 +1,9 @@
1738 #
1739 # Add security to sources.list
1740 #
1741+required_features:
1742+ - apt
1743+ - ubuntu_repos
1744 cloud_config: |
1745 #cloud-config
1746 apt:
1747diff --git a/tests/cloud_tests/configs/modules/apt_configure_sources_key.yaml b/tests/cloud_tests/configs/modules/apt_configure_sources_key.yaml
1748index e7568a6..bde9398 100644
1749--- a/tests/cloud_tests/configs/modules/apt_configure_sources_key.yaml
1750+++ b/tests/cloud_tests/configs/modules/apt_configure_sources_key.yaml
1751@@ -1,6 +1,9 @@
1752 #
1753 # Add a sources.list entry with a given key (Debian Jessie)
1754 #
1755+required_features:
1756+ - apt
1757+ - lsb_release
1758 cloud_config: |
1759 #cloud-config
1760 apt:
1761diff --git a/tests/cloud_tests/configs/modules/apt_configure_sources_keyserver.yaml b/tests/cloud_tests/configs/modules/apt_configure_sources_keyserver.yaml
1762index 1a4a238..11da61e 100644
1763--- a/tests/cloud_tests/configs/modules/apt_configure_sources_keyserver.yaml
1764+++ b/tests/cloud_tests/configs/modules/apt_configure_sources_keyserver.yaml
1765@@ -1,6 +1,9 @@
1766 #
1767 # Add a sources.list entry with a key from a keyserver
1768 #
1769+required_features:
1770+ - apt
1771+ - lsb_release
1772 cloud_config: |
1773 #cloud-config
1774 apt:
1775diff --git a/tests/cloud_tests/configs/modules/apt_configure_sources_list.yaml b/tests/cloud_tests/configs/modules/apt_configure_sources_list.yaml
1776index 057fc72..143cb08 100644
1777--- a/tests/cloud_tests/configs/modules/apt_configure_sources_list.yaml
1778+++ b/tests/cloud_tests/configs/modules/apt_configure_sources_list.yaml
1779@@ -1,6 +1,9 @@
1780 #
1781 # Generate a sources.list
1782 #
1783+required_features:
1784+ - apt
1785+ - lsb_release
1786 cloud_config: |
1787 #cloud-config
1788 apt:
1789diff --git a/tests/cloud_tests/configs/modules/apt_configure_sources_ppa.yaml b/tests/cloud_tests/configs/modules/apt_configure_sources_ppa.yaml
1790index dee9dc7..9efdae5 100644
1791--- a/tests/cloud_tests/configs/modules/apt_configure_sources_ppa.yaml
1792+++ b/tests/cloud_tests/configs/modules/apt_configure_sources_ppa.yaml
1793@@ -1,6 +1,12 @@
1794 #
1795 # Add a PPA to source.list
1796 #
1797+# NOTE: on older ubuntu releases the sources file added is named
1798+# 'curtin-dev-test-archive-trusty', without 'ubuntu' in the middle
1799+required_features:
1800+ - apt
1801+ - ppa
1802+ - ppa_file_name
1803 cloud_config: |
1804 #cloud-config
1805 apt:
1806@@ -16,5 +22,8 @@ collect_scripts:
1807 apt-key: |
1808 #!/bin/bash
1809 apt-key finger
1810+ sources_full: |
1811+ #!/bin/bash
1812+ cat /etc/apt/sources.list
1813
1814 # vi: ts=4 expandtab
1815diff --git a/tests/cloud_tests/configs/modules/apt_pipelining_disable.yaml b/tests/cloud_tests/configs/modules/apt_pipelining_disable.yaml
1816index 5fa0cee..bd9b5d0 100644
1817--- a/tests/cloud_tests/configs/modules/apt_pipelining_disable.yaml
1818+++ b/tests/cloud_tests/configs/modules/apt_pipelining_disable.yaml
1819@@ -1,6 +1,8 @@
1820 #
1821 # Disable apt pipelining value
1822 #
1823+required_features:
1824+ - apt
1825 cloud_config: |
1826 #cloud-config
1827 apt:
1828diff --git a/tests/cloud_tests/configs/modules/apt_pipelining_os.yaml b/tests/cloud_tests/configs/modules/apt_pipelining_os.yaml
1829index 87d183e..cbed3ba 100644
1830--- a/tests/cloud_tests/configs/modules/apt_pipelining_os.yaml
1831+++ b/tests/cloud_tests/configs/modules/apt_pipelining_os.yaml
1832@@ -1,6 +1,8 @@
1833 #
1834 # Set apt pipelining value to OS
1835 #
1836+required_features:
1837+ - apt
1838 cloud_config: |
1839 #cloud-config
1840 apt:
1841diff --git a/tests/cloud_tests/configs/modules/byobu.yaml b/tests/cloud_tests/configs/modules/byobu.yaml
1842index fd648c7..a9aa1f3 100644
1843--- a/tests/cloud_tests/configs/modules/byobu.yaml
1844+++ b/tests/cloud_tests/configs/modules/byobu.yaml
1845@@ -1,6 +1,8 @@
1846 #
1847 # Install and enable byobu system wide and default user
1848 #
1849+required_features:
1850+ - byobu
1851 cloud_config: |
1852 #cloud-config
1853 byobu_by_default: enable
1854diff --git a/tests/cloud_tests/configs/modules/keys_to_console.yaml b/tests/cloud_tests/configs/modules/keys_to_console.yaml
1855index a90e42c..5d86e73 100644
1856--- a/tests/cloud_tests/configs/modules/keys_to_console.yaml
1857+++ b/tests/cloud_tests/configs/modules/keys_to_console.yaml
1858@@ -1,6 +1,8 @@
1859 #
1860 # Hide printing of ssh key and fingerprints for specific keys
1861 #
1862+required_features:
1863+ - syslog
1864 cloud_config: |
1865 #cloud-config
1866 ssh_fp_console_blacklist: [ssh-dss, ssh-dsa, ecdsa-sha2-nistp256]
1867diff --git a/tests/cloud_tests/configs/modules/landscape.yaml b/tests/cloud_tests/configs/modules/landscape.yaml
1868index e6f4955..ed2c37c 100644
1869--- a/tests/cloud_tests/configs/modules/landscape.yaml
1870+++ b/tests/cloud_tests/configs/modules/landscape.yaml
1871@@ -4,6 +4,8 @@
1872 # 2016-11-17: Disabled due to this not working
1873 #
1874 enabled: false
1875+required_features:
1876+ - landscape
1877 cloud_config: |
1878 #cloud-conifg
1879 landscape:
1880diff --git a/tests/cloud_tests/configs/modules/locale.yaml b/tests/cloud_tests/configs/modules/locale.yaml
1881index af5ad63..e01518a 100644
1882--- a/tests/cloud_tests/configs/modules/locale.yaml
1883+++ b/tests/cloud_tests/configs/modules/locale.yaml
1884@@ -1,6 +1,9 @@
1885 #
1886 # Set locale to non-default option and verify
1887 #
1888+required_features:
1889+ - engb_locale
1890+ - locale_gen
1891 cloud_config: |
1892 #cloud-config
1893 locale: en_GB.UTF-8
1894diff --git a/tests/cloud_tests/configs/modules/lxd_bridge.yaml b/tests/cloud_tests/configs/modules/lxd_bridge.yaml
1895index 568bb70..e6b7e76 100644
1896--- a/tests/cloud_tests/configs/modules/lxd_bridge.yaml
1897+++ b/tests/cloud_tests/configs/modules/lxd_bridge.yaml
1898@@ -1,6 +1,8 @@
1899 #
1900 # LXD configured with directory backend and IPv4 bridge
1901 #
1902+required_features:
1903+ - lxd
1904 cloud_config: |
1905 #cloud-config
1906 lxd:
1907diff --git a/tests/cloud_tests/configs/modules/lxd_dir.yaml b/tests/cloud_tests/configs/modules/lxd_dir.yaml
1908index 99b9219..f93a3fa 100644
1909--- a/tests/cloud_tests/configs/modules/lxd_dir.yaml
1910+++ b/tests/cloud_tests/configs/modules/lxd_dir.yaml
1911@@ -1,6 +1,8 @@
1912 #
1913 # LXD configured with directory backend
1914 #
1915+required_features:
1916+ - lxd
1917 cloud_config: |
1918 #cloud-config
1919 lxd:
1920diff --git a/tests/cloud_tests/configs/modules/ntp.yaml b/tests/cloud_tests/configs/modules/ntp.yaml
1921index d094157..0d07ef5 100644
1922--- a/tests/cloud_tests/configs/modules/ntp.yaml
1923+++ b/tests/cloud_tests/configs/modules/ntp.yaml
1924@@ -1,6 +1,14 @@
1925 #
1926 # Emtpy NTP config to setup using defaults
1927 #
1928+# NOTE: this should not require apt feature, use 'which' rather than 'dpkg -l'
1929+# NOTE: this should not require no_ntpdate feature, use 'which' to check for
1930+# installation rather than 'dpkg -l', as 'grep ntp' matches 'ntpdate'
1931+# NOTE: the verifier should check for any ntp server not 'ubuntu.pool.ntp.org'
1932+required_features:
1933+ - apt
1934+ - no_ntpdate
1935+ - ubuntu_ntp
1936 cloud_config: |
1937 #cloud-config
1938 ntp:
1939@@ -16,5 +24,8 @@ collect_scripts:
1940 ntp_conf_empty: |
1941 #!/bin/bash
1942 grep '^pool' /etc/ntp.conf
1943+ ntp_installed_list: |
1944+ #!/bin/bash
1945+ dpkg -l | grep ntp
1946
1947 # vi: ts=4 expandtab
1948diff --git a/tests/cloud_tests/configs/modules/ntp_pools.yaml b/tests/cloud_tests/configs/modules/ntp_pools.yaml
1949index e040cc3..7561c7f 100644
1950--- a/tests/cloud_tests/configs/modules/ntp_pools.yaml
1951+++ b/tests/cloud_tests/configs/modules/ntp_pools.yaml
1952@@ -1,6 +1,16 @@
1953 #
1954 # NTP config using specific pools
1955 #
1956+# NOTE: this should not require apt feature, use 'which' rather than 'dpkg -l'
1957+# NOTE: this should not require no_ntpdate feature, use 'which' to check for
1958+# installation rather than 'dpkg -l', as 'grep ntp' matches 'ntpdate'
1959+# NOTE: lsb_release listed here because with recent cloud-init deb with
1960+# (LP: 1628337) resolved, cloud-init will attempt to configure archives.
1961+# this fails without lsb_release as UNAVAILABLE is used for $RELEASE
1962+required_features:
1963+ - apt
1964+ - no_ntpdate
1965+ - lsb_release
1966 cloud_config: |
1967 #cloud-config
1968 ntp:
1969diff --git a/tests/cloud_tests/configs/modules/ntp_servers.yaml b/tests/cloud_tests/configs/modules/ntp_servers.yaml
1970index e0564a0..9d1d65e 100644
1971--- a/tests/cloud_tests/configs/modules/ntp_servers.yaml
1972+++ b/tests/cloud_tests/configs/modules/ntp_servers.yaml
1973@@ -1,6 +1,13 @@
1974 #
1975 # NTP config using specific servers
1976 #
1977+# NOTE: this should not require apt feature, use 'which' rather than 'dpkg -l'
1978+# NOTE: this should not require no_ntpdate feature, use 'which' to check for
1979+# installation rather than 'dpkg -l', as 'grep ntp' matches 'ntpdate'
1980+required_features:
1981+ - apt
1982+ - no_ntpdate
1983+ - lsb_release
1984 cloud_config: |
1985 #cloud-config
1986 ntp:
1987diff --git a/tests/cloud_tests/configs/modules/package_update_upgrade_install.yaml b/tests/cloud_tests/configs/modules/package_update_upgrade_install.yaml
1988index d027d54..71d24b8 100644
1989--- a/tests/cloud_tests/configs/modules/package_update_upgrade_install.yaml
1990+++ b/tests/cloud_tests/configs/modules/package_update_upgrade_install.yaml
1991@@ -1,6 +1,17 @@
1992 #
1993 # Update/upgrade via apt and then install a pair of packages
1994 #
1995+# NOTE: this should not require apt feature, use 'which' rather than 'dpkg -l'
1996+# NOTE: the testcase for this looks for the command in history.log as
1997+# /usr/bin/apt-get..., which is not how it always appears. it should
1998+# instead look for just apt-get...
1999+# NOTE: this testcase should not require 'apt_up_out', and should look for a
2000+# call to 'apt-get upgrade' or 'apt-get dist-upgrade' in cloud-init.log
2001+# rather than 'Calculating upgrade...' in output
2002+required_features:
2003+ - apt
2004+ - apt_hist_fmt
2005+ - apt_up_out
2006 cloud_config: |
2007 #cloud-config
2008 packages:
2009diff --git a/tests/cloud_tests/configs/modules/set_hostname.yaml b/tests/cloud_tests/configs/modules/set_hostname.yaml
2010index 5aae150..c96344c 100644
2011--- a/tests/cloud_tests/configs/modules/set_hostname.yaml
2012+++ b/tests/cloud_tests/configs/modules/set_hostname.yaml
2013@@ -1,6 +1,8 @@
2014 #
2015 # Set the hostname and update /etc/hosts
2016 #
2017+required_features:
2018+ - hostname
2019 cloud_config: |
2020 #cloud-config
2021 hostname: myhostname
2022diff --git a/tests/cloud_tests/configs/modules/set_hostname_fqdn.yaml b/tests/cloud_tests/configs/modules/set_hostname_fqdn.yaml
2023index 0014c19..daf7593 100644
2024--- a/tests/cloud_tests/configs/modules/set_hostname_fqdn.yaml
2025+++ b/tests/cloud_tests/configs/modules/set_hostname_fqdn.yaml
2026@@ -1,6 +1,8 @@
2027 #
2028 # Set the hostname and update /etc/hosts
2029 #
2030+required_features:
2031+ - hostname
2032 cloud_config: |
2033 #cloud-config
2034 manage_etc_hosts: true
2035diff --git a/tests/cloud_tests/configs/modules/set_password.yaml b/tests/cloud_tests/configs/modules/set_password.yaml
2036index 8fa46d9..04d7c58 100644
2037--- a/tests/cloud_tests/configs/modules/set_password.yaml
2038+++ b/tests/cloud_tests/configs/modules/set_password.yaml
2039@@ -1,6 +1,8 @@
2040 #
2041 # Set password of default user
2042 #
2043+required_features:
2044+ - ubuntu_user
2045 cloud_config: |
2046 #cloud-config
2047 password: password
2048diff --git a/tests/cloud_tests/configs/modules/set_password_expire.yaml b/tests/cloud_tests/configs/modules/set_password_expire.yaml
2049index 926731f..789604b 100644
2050--- a/tests/cloud_tests/configs/modules/set_password_expire.yaml
2051+++ b/tests/cloud_tests/configs/modules/set_password_expire.yaml
2052@@ -1,6 +1,8 @@
2053 #
2054 # Expire password for all users
2055 #
2056+required_features:
2057+ - sshd
2058 cloud_config: |
2059 #cloud-config
2060 chpasswd: { expire: True }
2061diff --git a/tests/cloud_tests/configs/modules/snappy.yaml b/tests/cloud_tests/configs/modules/snappy.yaml
2062index 0e7dc85..43f9329 100644
2063--- a/tests/cloud_tests/configs/modules/snappy.yaml
2064+++ b/tests/cloud_tests/configs/modules/snappy.yaml
2065@@ -1,6 +1,8 @@
2066 #
2067 # Install snappy
2068 #
2069+required_features:
2070+ - snap
2071 cloud_config: |
2072 #cloud-config
2073 snappy:
2074diff --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
2075index 33943bd..746653e 100644
2076--- a/tests/cloud_tests/configs/modules/ssh_auth_key_fingerprints_disable.yaml
2077+++ b/tests/cloud_tests/configs/modules/ssh_auth_key_fingerprints_disable.yaml
2078@@ -1,6 +1,8 @@
2079 #
2080 # Disable fingerprint printing
2081 #
2082+required_features:
2083+ - syslog
2084 cloud_config: |
2085 #cloud-config
2086 ssh_genkeytypes: []
2087diff --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
2088index 4c97077..9f5dc34 100644
2089--- a/tests/cloud_tests/configs/modules/ssh_auth_key_fingerprints_enable.yaml
2090+++ b/tests/cloud_tests/configs/modules/ssh_auth_key_fingerprints_enable.yaml
2091@@ -1,6 +1,11 @@
2092 #
2093 # Print auth keys with different hash than md5
2094 #
2095+# NOTE: testcase checks for '256 SHA256:.*(ECDSA)' on output line on trusty
2096+# this fails as line in output reads '256:.*(ECDSA)'
2097+required_features:
2098+ - syslog
2099+ - ssh_key_fmt
2100 cloud_config: |
2101 #cloud-config
2102 ssh_genkeytypes:
2103diff --git a/tests/cloud_tests/configs/modules/ssh_import_id.yaml b/tests/cloud_tests/configs/modules/ssh_import_id.yaml
2104index 6e5a163..b62d3f6 100644
2105--- a/tests/cloud_tests/configs/modules/ssh_import_id.yaml
2106+++ b/tests/cloud_tests/configs/modules/ssh_import_id.yaml
2107@@ -1,6 +1,9 @@
2108 #
2109 # Import a user's ssh key via gh or lp
2110 #
2111+required_features:
2112+ - ubuntu_user
2113+ - sudo
2114 cloud_config: |
2115 #cloud-config
2116 ssh_import_id:
2117diff --git a/tests/cloud_tests/configs/modules/ssh_keys_generate.yaml b/tests/cloud_tests/configs/modules/ssh_keys_generate.yaml
2118index 637d783..659fd93 100644
2119--- a/tests/cloud_tests/configs/modules/ssh_keys_generate.yaml
2120+++ b/tests/cloud_tests/configs/modules/ssh_keys_generate.yaml
2121@@ -1,6 +1,8 @@
2122 #
2123 # SSH keys generated using cloud-init
2124 #
2125+required_features:
2126+ - ubuntu_user
2127 cloud_config: |
2128 #cloud-config
2129 ssh_genkeytypes:
2130diff --git a/tests/cloud_tests/configs/modules/ssh_keys_provided.yaml b/tests/cloud_tests/configs/modules/ssh_keys_provided.yaml
2131index 25df645..5ceb362 100644
2132--- a/tests/cloud_tests/configs/modules/ssh_keys_provided.yaml
2133+++ b/tests/cloud_tests/configs/modules/ssh_keys_provided.yaml
2134@@ -2,6 +2,9 @@
2135 # SSH keys provided via cloud config
2136 #
2137 enabled: False
2138+required_features:
2139+ - ubuntu_user
2140+ - sudo
2141 cloud_config: |
2142 #cloud-config
2143 disable_root: false
2144diff --git a/tests/cloud_tests/configs/modules/timezone.yaml b/tests/cloud_tests/configs/modules/timezone.yaml
2145index 8c96ed4..5112aa9 100644
2146--- a/tests/cloud_tests/configs/modules/timezone.yaml
2147+++ b/tests/cloud_tests/configs/modules/timezone.yaml
2148@@ -1,6 +1,8 @@
2149 #
2150 # Set system timezone
2151 #
2152+required_features:
2153+ - daylight_time
2154 cloud_config: |
2155 #cloud-config
2156 timezone: US/Aleutian
2157diff --git a/tests/cloud_tests/configs/modules/user_groups.yaml b/tests/cloud_tests/configs/modules/user_groups.yaml
2158index 9265595..71cc9da 100644
2159--- a/tests/cloud_tests/configs/modules/user_groups.yaml
2160+++ b/tests/cloud_tests/configs/modules/user_groups.yaml
2161@@ -1,6 +1,8 @@
2162 #
2163 # Create groups and users with various options
2164 #
2165+required_features:
2166+ - ubuntu_user
2167 cloud_config: |
2168 #cloud-config
2169 # Add groups to the system
2170diff --git a/tests/cloud_tests/configs/modules/write_files.yaml b/tests/cloud_tests/configs/modules/write_files.yaml
2171index 4bb2991..ce936b7 100644
2172--- a/tests/cloud_tests/configs/modules/write_files.yaml
2173+++ b/tests/cloud_tests/configs/modules/write_files.yaml
2174@@ -1,6 +1,10 @@
2175 #
2176 # Write various file types
2177 #
2178+# NOTE: on trusty 'file' has an output formatting error for binary files and
2179+# has 2 spaces in 'LSB executable', which causes a failure here
2180+required_features:
2181+ - no_file_fmt_e
2182 cloud_config: |
2183 #cloud-config
2184 write_files:
2185diff --git a/tests/cloud_tests/images/__init__.py b/tests/cloud_tests/images/__init__.py
2186index b27d693..106c59f 100644
2187--- a/tests/cloud_tests/images/__init__.py
2188+++ b/tests/cloud_tests/images/__init__.py
2189@@ -1,11 +1,10 @@
2190 # This file is part of cloud-init. See LICENSE file for license information.
2191
2192+"""Main init."""
2193+
2194
2195 def get_image(platform, config):
2196- """
2197- get image from platform object using os_name, looking up img_conf in main
2198- config file
2199- """
2200+ """Get image from platform object using os_name."""
2201 return platform.get_image(config)
2202
2203 # vi: ts=4 expandtab
2204diff --git a/tests/cloud_tests/images/base.py b/tests/cloud_tests/images/base.py
2205index 394b11f..0a1e056 100644
2206--- a/tests/cloud_tests/images/base.py
2207+++ b/tests/cloud_tests/images/base.py
2208@@ -1,65 +1,69 @@
2209 # This file is part of cloud-init. See LICENSE file for license information.
2210
2211+"""Base class for images."""
2212+
2213
2214 class Image(object):
2215- """
2216- Base class for images
2217- """
2218+ """Base class for images."""
2219+
2220 platform_name = None
2221
2222- def __init__(self, name, config, platform):
2223- """
2224- setup
2225+ def __init__(self, platform, config):
2226+ """Set up image.
2227+
2228+ @param platform: platform object
2229+ @param config: image configuration
2230 """
2231- self.name = name
2232- self.config = config
2233 self.platform = platform
2234+ self.config = config
2235
2236 def __str__(self):
2237- """
2238- a brief description of the image
2239- """
2240+ """A brief description of the image."""
2241 return '-'.join((self.properties['os'], self.properties['release']))
2242
2243 @property
2244 def properties(self):
2245- """
2246- {} containing: 'arch', 'os', 'version', 'release'
2247- """
2248+ """{} containing: 'arch', 'os', 'version', 'release'."""
2249 raise NotImplementedError
2250
2251- # FIXME: instead of having execute and push_file and other instance methods
2252- # here which pass through to a hidden instance, it might be better
2253- # to expose an instance that the image can be modified through
2254- def execute(self, command, stdin=None, stdout=None, stderr=None, env={}):
2255+ @property
2256+ def features(self):
2257+ """Feature flags supported by this image.
2258+
2259+ @return_value: list of feature names
2260 """
2261- execute command in image, modifying image
2262+ return [k for k, v in self.config.get('features', {}).items() if v]
2263+
2264+ @property
2265+ def setup_overrides(self):
2266+ """Setup options that need to be overridden for the image.
2267+
2268+ @return_value: dictionary to update args with
2269 """
2270+ # NOTE: more sophisticated options may be requied at some point
2271+ return self.config.get('setup_overrides', {})
2272+
2273+ def execute(self, *args, **kwargs):
2274+ """Execute command in image, modifying image."""
2275 raise NotImplementedError
2276
2277 def push_file(self, local_path, remote_path):
2278- """
2279- copy file at 'local_path' to instance at 'remote_path', modifying image
2280- """
2281+ """Copy file at 'local_path' to instance at 'remote_path'."""
2282 raise NotImplementedError
2283
2284- def run_script(self, script):
2285- """
2286- run script in image, modifying image
2287- return_value: script output
2288+ def run_script(self, *args, **kwargs):
2289+ """Run script in image, modifying image.
2290+
2291+ @return_value: script output
2292 """
2293 raise NotImplementedError
2294
2295 def snapshot(self):
2296- """
2297- create snapshot of image, block until done
2298- """
2299+ """Create snapshot of image, block until done."""
2300 raise NotImplementedError
2301
2302 def destroy(self):
2303- """
2304- clean up data associated with image
2305- """
2306+ """Clean up data associated with image."""
2307 pass
2308
2309 # vi: ts=4 expandtab
2310diff --git a/tests/cloud_tests/images/lxd.py b/tests/cloud_tests/images/lxd.py
2311index 7a41614..fd4e93c 100644
2312--- a/tests/cloud_tests/images/lxd.py
2313+++ b/tests/cloud_tests/images/lxd.py
2314@@ -1,43 +1,67 @@
2315 # This file is part of cloud-init. See LICENSE file for license information.
2316
2317+"""LXD Image Base Class."""
2318+
2319+import os
2320+import shutil
2321+import tempfile
2322+
2323+from cloudinit import util as c_util
2324 from tests.cloud_tests.images import base
2325 from tests.cloud_tests.snapshots import lxd as lxd_snapshot
2326+from tests.cloud_tests import util
2327
2328
2329 class LXDImage(base.Image):
2330- """
2331- LXD backed image
2332- """
2333+ """LXD backed image."""
2334+
2335 platform_name = "lxd"
2336
2337- def __init__(self, name, config, platform, pylxd_image):
2338- """
2339- setup
2340+ def __init__(self, platform, config, pylxd_image):
2341+ """Set up image.
2342+
2343+ @param platform: platform object
2344+ @param config: image configuration
2345 """
2346- self.platform = platform
2347- self._pylxd_image = pylxd_image
2348+ self.modified = False
2349 self._instance = None
2350- super(LXDImage, self).__init__(name, config, platform)
2351+ self._pylxd_image = None
2352+ self.pylxd_image = pylxd_image
2353+ super(LXDImage, self).__init__(platform, config)
2354
2355 @property
2356 def pylxd_image(self):
2357- self._pylxd_image.sync()
2358+ """Property function."""
2359+ if self._pylxd_image:
2360+ self._pylxd_image.sync()
2361 return self._pylxd_image
2362
2363+ @pylxd_image.setter
2364+ def pylxd_image(self, pylxd_image):
2365+ if self._instance:
2366+ self._instance.destroy()
2367+ self._instance = None
2368+ if (self._pylxd_image and
2369+ (self._pylxd_image is not pylxd_image) and
2370+ (not self.config.get('cache_base_image') or self.modified)):
2371+ self._pylxd_image.delete(wait=True)
2372+ self.modified = False
2373+ self._pylxd_image = pylxd_image
2374+
2375 @property
2376 def instance(self):
2377+ """Property function."""
2378 if not self._instance:
2379 self._instance = self.platform.launch_container(
2380- image=self.pylxd_image.fingerprint,
2381- image_desc=str(self), use_desc='image-modification')
2382- self._instance.start(wait=True, wait_time=self.config.get('timeout'))
2383+ self.properties, self.config, self.features,
2384+ use_desc='image-modification', image_desc=str(self),
2385+ image=self.pylxd_image.fingerprint)
2386+ self._instance.start()
2387 return self._instance
2388
2389 @property
2390 def properties(self):
2391- """
2392- {} containing: 'arch', 'os', 'version', 'release'
2393- """
2394+ """{} containing: 'arch', 'os', 'version', 'release'."""
2395 properties = self.pylxd_image.properties
2396 return {
2397 'arch': properties.get('architecture'),
2398@@ -46,47 +70,121 @@ class LXDImage(base.Image):
2399 'release': properties.get('release'),
2400 }
2401
2402- def execute(self, *args, **kwargs):
2403+ def export_image(self, output_dir):
2404+ """Export image from lxd image store to (split) tarball on disk.
2405+
2406+ @param output_dir: dir to store tarballs in
2407+ @return_value: tuple of path to metadata tarball and rootfs tarball
2408 """
2409- execute command in image, modifying image
2410+ # pylxd's image export feature doesn't do split exports, so use cmdline
2411+ c_util.subp(['lxc', 'image', 'export', self.pylxd_image.fingerprint,
2412+ output_dir], capture=True)
2413+ tarballs = [p for p in os.listdir(output_dir) if p.endswith('tar.xz')]
2414+ metadata = os.path.join(
2415+ output_dir, next(p for p in tarballs if p.startswith('meta-')))
2416+ rootfs = os.path.join(
2417+ output_dir, next(p for p in tarballs if not p.startswith('meta-')))
2418+ return (metadata, rootfs)
2419+
2420+ def import_image(self, metadata, rootfs):
2421+ """Import image to lxd image store from (split) tarball on disk.
2422+
2423+ Note, this will replace and delete the current pylxd_image
2424+
2425+ @param metadata: metadata tarball
2426+ @param rootfs: rootfs tarball
2427+ @return_value: imported image fingerprint
2428+ """
2429+ alias = util.gen_instance_name(
2430+ image_desc=str(self), use_desc='update-metadata')
2431+ c_util.subp(['lxc', 'image', 'import', metadata, rootfs,
2432+ '--alias', alias], capture=True)
2433+ self.pylxd_image = self.platform.query_image_by_alias(alias)
2434+ return self.pylxd_image.fingerprint
2435+
2436+ def update_templates(self, template_config, template_data):
2437+ """Update the image's template configuration.
2438+
2439+ Note, this will replace and delete the current pylxd_image
2440+
2441+ @param template_config: config overrides for template metadata
2442+ @param template_data: template data to place into templates/
2443 """
2444+ # set up tmp files
2445+ export_dir = tempfile.mkdtemp(prefix='cloud_test_util_')
2446+ extract_dir = tempfile.mkdtemp(prefix='cloud_test_util_')
2447+ new_metadata = os.path.join(export_dir, 'new-meta.tar.xz')
2448+ metadata_yaml = os.path.join(extract_dir, 'metadata.yaml')
2449+ template_dir = os.path.join(extract_dir, 'templates')
2450+
2451+ try:
2452+ # extract old data
2453+ (metadata, rootfs) = self.export_image(export_dir)
2454+ shutil.unpack_archive(metadata, extract_dir)
2455+
2456+ # update metadata
2457+ metadata = c_util.read_conf(metadata_yaml)
2458+ templates = metadata.get('templates', {})
2459+ templates.update(template_config)
2460+ metadata['templates'] = templates
2461+ util.yaml_dump(metadata, metadata_yaml)
2462+
2463+ # write out template files
2464+ for name, content in template_data.items():
2465+ path = os.path.join(template_dir, name)
2466+ c_util.write_file(path, content)
2467+
2468+ # store new data, mark new image as modified
2469+ util.flat_tar(new_metadata, extract_dir)
2470+ self.import_image(new_metadata, rootfs)
2471+ self.modified = True
2472+
2473+ finally:
2474+ # remove tmpfiles
2475+ shutil.rmtree(export_dir)
2476+ shutil.rmtree(extract_dir)
2477+
2478+ def execute(self, *args, **kwargs):
2479+ """Execute command in image, modifying image."""
2480 return self.instance.execute(*args, **kwargs)
2481
2482 def push_file(self, local_path, remote_path):
2483- """
2484- copy file at 'local_path' to instance at 'remote_path', modifying image
2485- """
2486+ """Copy file at 'local_path' to instance at 'remote_path'."""
2487 return self.instance.push_file(local_path, remote_path)
2488
2489- def run_script(self, script):
2490- """
2491- run script in image, modifying image
2492- return_value: script output
2493+ def run_script(self, *args, **kwargs):
2494+ """Run script in image, modifying image.
2495+
2496+ @return_value: script output
2497 """
2498- return self.instance.run_script(script)
2499+ return self.instance.run_script(*args, **kwargs)
2500
2501 def snapshot(self):
2502- """
2503- create snapshot of image, block until done
2504- """
2505- # clone current instance, start and freeze clone
2506+ """Create snapshot of image, block until done."""
2507+ # get empty user data to pass in to instance
2508+ # if overrides for user data provided, use them
2509+ empty_userdata = util.update_user_data(
2510+ {}, self.config.get('user_data_overrides', {}))
2511+ conf = {'user.user-data': empty_userdata}
2512+ # clone current instance
2513 instance = self.platform.launch_container(
2514+ self.properties, self.config, self.features,
2515 container=self.instance.name, image_desc=str(self),
2516- use_desc='snapshot')
2517- instance.start(wait=True, wait_time=self.config.get('timeout'))
2518+ use_desc='snapshot', container_config=conf)
2519+ # wait for cloud-init before boot_clean_script is run to ensure
2520+ # /var/lib/cloud is removed cleanly
2521+ instance.start(wait=True, wait_for_cloud_init=True)
2522 if self.config.get('boot_clean_script'):
2523 instance.run_script(self.config.get('boot_clean_script'))
2524+ # freeze current instance and return snapshot
2525 instance.freeze()
2526 return lxd_snapshot.LXDSnapshot(
2527- self.properties, self.config, self.platform, instance)
2528+ self.platform, self.properties, self.config,
2529+ self.features, instance)
2530
2531 def destroy(self):
2532- """
2533- clean up data associated with image
2534- """
2535- if self._instance:
2536- self._instance.destroy()
2537- self.pylxd_image.delete(wait=True)
2538+ """Clean up data associated with image."""
2539+ self.pylxd_image = None
2540 super(LXDImage, self).destroy()
2541
2542 # vi: ts=4 expandtab
2543diff --git a/tests/cloud_tests/instances/__init__.py b/tests/cloud_tests/instances/__init__.py
2544index 85bea99..fc2e9cb 100644
2545--- a/tests/cloud_tests/instances/__init__.py
2546+++ b/tests/cloud_tests/instances/__init__.py
2547@@ -1,10 +1,10 @@
2548 # This file is part of cloud-init. See LICENSE file for license information.
2549
2550+"""Main init."""
2551+
2552
2553 def get_instance(snapshot, *args, **kwargs):
2554- """
2555- get instance from snapshot
2556- """
2557+ """Get instance from snapshot."""
2558 return snapshot.launch(*args, **kwargs)
2559
2560 # vi: ts=4 expandtab
2561diff --git a/tests/cloud_tests/instances/base.py b/tests/cloud_tests/instances/base.py
2562index 9559d28..959e9cc 100644
2563--- a/tests/cloud_tests/instances/base.py
2564+++ b/tests/cloud_tests/instances/base.py
2565@@ -1,120 +1,148 @@
2566 # This file is part of cloud-init. See LICENSE file for license information.
2567
2568-import os
2569-import uuid
2570+"""Base instance."""
2571
2572
2573 class Instance(object):
2574- """
2575- Base instance object
2576- """
2577+ """Base instance object."""
2578+
2579 platform_name = None
2580
2581- def __init__(self, name):
2582- """
2583- setup
2584+ def __init__(self, platform, name, properties, config, features):
2585+ """Set up instance.
2586+
2587+ @param platform: platform object
2588+ @param name: hostname of instance
2589+ @param properties: image properties
2590+ @param config: image config
2591+ @param features: supported feature flags
2592 """
2593+ self.platform = platform
2594 self.name = name
2595+ self.properties = properties
2596+ self.config = config
2597+ self.features = features
2598
2599- def execute(self, command, stdin=None, stdout=None, stderr=None, env={}):
2600- """
2601- command: the command to execute as root inside the image
2602- stdin, stderr, stdout: file handles
2603- env: environment variables
2604+ def execute(self, command, stdout=None, stderr=None, env={},
2605+ rcs=None, description=None):
2606+ """Execute command in instance, recording output, error and exit code.
2607
2608- Execute assumes functional networking and execution as root with the
2609+ Assumes functional networking and execution as root with the
2610 target filesystem being available at /.
2611
2612- return_value: tuple containing stdout data, stderr data, exit code
2613+ @param command: the command to execute as root inside the image
2614+ @param stdout, stderr: file handles to write output and error to
2615+ @param env: environment variables
2616+ @param rcs: allowed return codes from command
2617+ @param description: purpose of command
2618+ @return_value: tuple containing stdout data, stderr data, exit code
2619 """
2620 raise NotImplementedError
2621
2622- def read_data(self, remote_path, encode=False):
2623- """
2624- read_data from instance filesystem
2625- remote_path: path in instance
2626- decode: return as string
2627- return_value: data as str or bytes
2628+ def read_data(self, remote_path, decode=False):
2629+ """Read data from instance filesystem.
2630+
2631+ @param remote_path: path in instance
2632+ @param decode: return as string
2633+ @return_value: data as str or bytes
2634 """
2635 raise NotImplementedError
2636
2637 def write_data(self, remote_path, data):
2638- """
2639- write data to instance filesystem
2640- remote_path: path in instance
2641- data: data to write, either str or bytes
2642+ """Write data to instance filesystem.
2643+
2644+ @param remote_path: path in instance
2645+ @param data: data to write, either str or bytes
2646 """
2647 raise NotImplementedError
2648
2649 def pull_file(self, remote_path, local_path):
2650- """
2651- copy file at 'remote_path', from instance to 'local_path'
2652+ """Copy file at 'remote_path', from instance to 'local_path'.
2653+
2654+ @param remote_path: path on remote instance
2655+ @param local_path: path on local instance
2656 """
2657 with open(local_path, 'wb') as fp:
2658- fp.write(self.read_data(remote_path), encode=True)
2659+ fp.write(self.read_data(remote_path))
2660
2661 def push_file(self, local_path, remote_path):
2662- """
2663- copy file at 'local_path' to instance at 'remote_path'
2664+ """Copy file at 'local_path' to instance at 'remote_path'.
2665+
2666+ @param local_path: path on local instance
2667+ @param remote_path: path on remote instance
2668 """
2669 with open(local_path, 'rb') as fp:
2670 self.write_data(remote_path, fp.read())
2671
2672- def run_script(self, script):
2673+ def run_script(self, script, rcs=None, description=None):
2674+ """Run script in target and return stdout.
2675+
2676+ @param script: script contents
2677+ @param rcs: allowed return codes from script
2678+ @param description: purpose of script
2679+ @return_value: stdout from script
2680 """
2681- run script in target and return stdout
2682+ script_path = self.tmpfile()
2683+ try:
2684+ self.write_data(script_path, script)
2685+ return self.execute(
2686+ ['/bin/bash', script_path], rcs=rcs, description=description)
2687+ finally:
2688+ self.execute(['rm', script_path], rcs=rcs)
2689+
2690+ def tmpfile(self):
2691+ """Get a tmp file in the target.
2692+
2693+ @return_value: path to new file in target
2694 """
2695- script_path = os.path.join('/tmp', str(uuid.uuid1()))
2696- self.write_data(script_path, script)
2697- (out, err, exit_code) = self.execute(['/bin/bash', script_path])
2698- return out
2699+ return self.execute(['mktemp'])[0].strip()
2700
2701 def console_log(self):
2702- """
2703- return_value: bytes of this instance’s console
2704+ """Instance console.
2705+
2706+ @return_value: bytes of this instance’s console
2707 """
2708 raise NotImplementedError
2709
2710 def reboot(self, wait=True):
2711- """
2712- reboot instance
2713- """
2714+ """Reboot instance."""
2715 raise NotImplementedError
2716
2717 def shutdown(self, wait=True):
2718- """
2719- shutdown instance
2720- """
2721+ """Shutdown instance."""
2722 raise NotImplementedError
2723
2724- def start(self, wait=True):
2725- """
2726- start instance
2727- """
2728+ def start(self, wait=True, wait_for_cloud_init=False):
2729+ """Start instance."""
2730 raise NotImplementedError
2731
2732 def destroy(self):
2733- """
2734- clean up instance
2735- """
2736+ """Clean up instance."""
2737 pass
2738
2739- def _wait_for_cloud_init(self, wait_time):
2740- """
2741- wait until system has fully booted and cloud-init has finished
2742+ def _wait_for_system(self, wait_for_cloud_init):
2743+ """Wait until system has fully booted and cloud-init has finished.
2744+
2745+ @param wait_time: maximum time to wait
2746+ @return_value: None, may raise OSError if wait_time exceeded
2747 """
2748- if not wait_time:
2749- return
2750-
2751- found_msg = 'found'
2752- cmd = ('for ((i=0;i<{wait};i++)); do [ -f "{file}" ] && '
2753- '{{ echo "{msg}";break; }} || sleep 1; done').format(
2754- file='/run/cloud-init/result.json',
2755- wait=wait_time, msg=found_msg)
2756-
2757- (out, err, exit) = self.execute(['/bin/bash', '-c', cmd])
2758- if out.strip() != found_msg:
2759- raise OSError('timeout: after {}s, cloud-init has not started'
2760- .format(wait_time))
2761+ def clean_test(test):
2762+ """Clean formatting for system ready test testcase."""
2763+ return ' '.join(l for l in test.strip().splitlines()
2764+ if not l.lstrip().startswith('#'))
2765+
2766+ time = self.config['boot_timeout']
2767+ tests = [self.config['system_ready_script']]
2768+ if wait_for_cloud_init:
2769+ tests.append(self.config['cloud_init_ready_script'])
2770+
2771+ formatted_tests = ' && '.join(clean_test(t) for t in tests)
2772+ test_cmd = ('for ((i=0;i<{time};i++)); do {test} && exit 0; sleep 1; '
2773+ 'done; exit 1;').format(time=time, test=formatted_tests)
2774+ cmd = ['/bin/bash', '-c', test_cmd]
2775+
2776+ if self.execute(cmd, rcs=(0, 1))[-1] != 0:
2777+ raise OSError('timeout: after {}s system not started'.format(time))
2778+
2779
2780 # vi: ts=4 expandtab
2781diff --git a/tests/cloud_tests/instances/lxd.py b/tests/cloud_tests/instances/lxd.py
2782index f0aa121..b9c2cc6 100644
2783--- a/tests/cloud_tests/instances/lxd.py
2784+++ b/tests/cloud_tests/instances/lxd.py
2785@@ -1,115 +1,135 @@
2786 # This file is part of cloud-init. See LICENSE file for license information.
2787
2788+"""Base LXD instance."""
2789+
2790 from tests.cloud_tests.instances import base
2791+from tests.cloud_tests import util
2792
2793
2794 class LXDInstance(base.Instance):
2795- """
2796- LXD container backed instance
2797- """
2798+ """LXD container backed instance."""
2799+
2800 platform_name = "lxd"
2801
2802- def __init__(self, name, platform, pylxd_container):
2803- """
2804- setup
2805+ def __init__(self, platform, name, properties, config, features,
2806+ pylxd_container):
2807+ """Set up instance.
2808+
2809+ @param platform: platform object
2810+ @param name: hostname of instance
2811+ @param properties: image properties
2812+ @param config: image config
2813+ @param features: supported feature flags
2814 """
2815- self.platform = platform
2816 self._pylxd_container = pylxd_container
2817- super(LXDInstance, self).__init__(name)
2818+ super(LXDInstance, self).__init__(
2819+ platform, name, properties, config, features)
2820
2821 @property
2822 def pylxd_container(self):
2823+ """Property function."""
2824 self._pylxd_container.sync()
2825 return self._pylxd_container
2826
2827- def execute(self, command, stdin=None, stdout=None, stderr=None, env={}):
2828- """
2829- command: the command to execute as root inside the image
2830- stdin, stderr, stdout: file handles
2831- env: environment variables
2832+ def execute(self, command, stdout=None, stderr=None, env={},
2833+ rcs=None, description=None):
2834+ """Execute command in instance, recording output, error and exit code.
2835
2836- Execute assumes functional networking and execution as root with the
2837+ Assumes functional networking and execution as root with the
2838 target filesystem being available at /.
2839
2840- return_value: tuple containing stdout data, stderr data, exit code
2841+ @param command: the command to execute as root inside the image
2842+ @param stdout: file handler to write output
2843+ @param stderr: file handler to write error
2844+ @param env: environment variables
2845+ @param rcs: allowed return codes from command
2846+ @param description: purpose of command
2847+ @return_value: tuple containing stdout data, stderr data, exit code
2848 """
2849- # TODO: the pylxd api handler for container.execute needs to be
2850- # extended to properly pass in stdin
2851- # TODO: the pylxd api handler for container.execute needs to be
2852- # extended to get the return code, for now just use 0
2853+ # ensure instance is running and execute the command
2854 self.start()
2855- if stdin:
2856- raise NotImplementedError
2857 res = self.pylxd_container.execute(command, environment=env)
2858- for (f, data) in (i for i in zip((stdout, stderr), res) if i[0]):
2859- f.write(data)
2860- return res + (0,)
2861+
2862+ # get out, exit and err from pylxd return
2863+ if hasattr(res, 'exit_code'):
2864+ # pylxd 2.2 returns ContainerExecuteResult, named tuple of
2865+ # (exit_code, out, err)
2866+ (exit, out, err) = res
2867+ else:
2868+ # pylxd 2.1.3 and earlier only return out and err, no exit
2869+ # LOG.warning('using pylxd version < 2.2')
2870+ (out, err) = res
2871+ exit = 0
2872+
2873+ # write data to file descriptors if needed
2874+ if stdout:
2875+ stdout.write(out)
2876+ if stderr:
2877+ stderr.write(err)
2878+
2879+ # if the command exited with a code not allowed in rcs, then fail
2880+ if exit not in (rcs if rcs else (0,)):
2881+ error_desc = ('Failed command to: {}'.format(description)
2882+ if description else None)
2883+ raise util.InTargetExecuteError(
2884+ out, err, exit, command, self.name, error_desc)
2885+
2886+ return (out, err, exit)
2887
2888 def read_data(self, remote_path, decode=False):
2889- """
2890- read data from instance filesystem
2891- remote_path: path in instance
2892- decode: return as string
2893- return_value: data as str or bytes
2894+ """Read data from instance filesystem.
2895+
2896+ @param remote_path: path in instance
2897+ @param decode: return as string
2898+ @return_value: data as str or bytes
2899 """
2900 data = self.pylxd_container.files.get(remote_path)
2901 return data.decode() if decode and isinstance(data, bytes) else data
2902
2903 def write_data(self, remote_path, data):
2904- """
2905- write data to instance filesystem
2906- remote_path: path in instance
2907- data: data to write, either str or bytes
2908+ """Write data to instance filesystem.
2909+
2910+ @param remote_path: path in instance
2911+ @param data: data to write, either str or bytes
2912 """
2913 self.pylxd_container.files.put(remote_path, data)
2914
2915 def console_log(self):
2916- """
2917- return_value: bytes of this instance’s console
2918+ """Console log.
2919+
2920+ @return_value: bytes of this instance’s console
2921 """
2922 raise NotImplementedError
2923
2924 def reboot(self, wait=True):
2925- """
2926- reboot instance
2927- """
2928+ """Reboot instance."""
2929 self.shutdown(wait=wait)
2930 self.start(wait=wait)
2931
2932 def shutdown(self, wait=True):
2933- """
2934- shutdown instance
2935- """
2936+ """Shutdown instance."""
2937 if self.pylxd_container.status != 'Stopped':
2938 self.pylxd_container.stop(wait=wait)
2939
2940- def start(self, wait=True, wait_time=None):
2941- """
2942- start instance
2943- """
2944+ def start(self, wait=True, wait_for_cloud_init=False):
2945+ """Start instance."""
2946 if self.pylxd_container.status != 'Running':
2947 self.pylxd_container.start(wait=wait)
2948- if wait and isinstance(wait_time, int):
2949- self._wait_for_cloud_init(wait_time)
2950+ if wait:
2951+ self._wait_for_system(wait_for_cloud_init)
2952
2953 def freeze(self):
2954- """
2955- freeze instance
2956- """
2957+ """Freeze instance."""
2958 if self.pylxd_container.status != 'Frozen':
2959 self.pylxd_container.freeze(wait=True)
2960
2961 def unfreeze(self):
2962- """
2963- unfreeze instance
2964- """
2965+ """Unfreeze instance."""
2966 if self.pylxd_container.status == 'Frozen':
2967 self.pylxd_container.unfreeze(wait=True)
2968
2969 def destroy(self):
2970- """
2971- clean up instance
2972- """
2973+ """Clean up instance."""
2974 self.unfreeze()
2975 self.shutdown()
2976 self.pylxd_container.delete(wait=True)
2977diff --git a/tests/cloud_tests/manage.py b/tests/cloud_tests/manage.py
2978index 5342612..5f0cfd2 100644
2979--- a/tests/cloud_tests/manage.py
2980+++ b/tests/cloud_tests/manage.py
2981@@ -1,11 +1,15 @@
2982 # This file is part of cloud-init. See LICENSE file for license information.
2983
2984+"""Create test cases automatically given a user_data script."""
2985+
2986+import os
2987+import textwrap
2988+
2989+from cloudinit import util as c_util
2990 from tests.cloud_tests.config import VERIFY_EXT
2991 from tests.cloud_tests import (config, util)
2992 from tests.cloud_tests import TESTCASES_DIR
2993
2994-import os
2995-import textwrap
2996
2997 _verifier_fmt = textwrap.dedent(
2998 """
2999@@ -35,29 +39,24 @@ _config_fmt = textwrap.dedent(
3000
3001
3002 def write_testcase_config(args, fmt_args, testcase_file):
3003- """
3004- write the testcase config file
3005- """
3006+ """Write the testcase config file."""
3007 testcase_config = {'enabled': args.enable, 'collect_scripts': {}}
3008 if args.config:
3009 testcase_config['cloud_config'] = args.config
3010 fmt_args['config'] = util.yaml_format(testcase_config)
3011- util.write_file(testcase_file, _config_fmt.format(**fmt_args), omode='w')
3012+ c_util.write_file(testcase_file, _config_fmt.format(**fmt_args), omode='w')
3013
3014
3015 def write_verifier(args, fmt_args, verifier_file):
3016- """
3017- write the verifier script
3018- """
3019+ """Write the verifier script."""
3020 fmt_args['test_class'] = 'Test{}'.format(
3021- config.name_sanatize(fmt_args['test_name']).title())
3022- util.write_file(verifier_file, _verifier_fmt.format(**fmt_args), omode='w')
3023+ config.name_sanitize(fmt_args['test_name']).title())
3024+ c_util.write_file(verifier_file,
3025+ _verifier_fmt.format(**fmt_args), omode='w')
3026
3027
3028 def create(args):
3029- """
3030- create a new testcase
3031- """
3032+ """Create a new testcase."""
3033 (test_category, test_name) = args.name.split('/')
3034 fmt_args = {'test_name': test_name, 'test_category': test_category,
3035 'test_description': str(args.description)}
3036@@ -65,7 +64,7 @@ def create(args):
3037 testcase_file = config.name_to_path(args.name)
3038 verifier_file = os.path.join(
3039 TESTCASES_DIR, test_category,
3040- config.name_sanatize(test_name) + VERIFY_EXT)
3041+ config.name_sanitize(test_name) + VERIFY_EXT)
3042
3043 write_testcase_config(args, fmt_args, testcase_file)
3044 write_verifier(args, fmt_args, verifier_file)
3045diff --git a/tests/cloud_tests/platforms.yaml b/tests/cloud_tests/platforms.yaml
3046index 5972b32..b91834a 100644
3047--- a/tests/cloud_tests/platforms.yaml
3048+++ b/tests/cloud_tests/platforms.yaml
3049@@ -10,7 +10,55 @@ default_platform_config:
3050 platforms:
3051 lxd:
3052 enabled: true
3053- get_image_timeout: 600
3054+ # overrides for image templates
3055+ template_overrides:
3056+ /var/lib/cloud/seed/nocloud-net/meta-data:
3057+ when:
3058+ - create
3059+ - copy
3060+ template: cloud-init-meta.tpl
3061+ /var/lib/cloud/seed/nocloud-net/network-config:
3062+ when:
3063+ - create
3064+ - copy
3065+ template: cloud-init-network.tpl
3066+ /var/lib/cloud/seed/nocloud-net/user-data:
3067+ when:
3068+ - create
3069+ - copy
3070+ template: cloud-init-user.tpl
3071+ properties:
3072+ default: |
3073+ #cloud-config
3074+ {}
3075+ /var/lib/cloud/seed/nocloud-net/vendor-data:
3076+ when:
3077+ - create
3078+ - copy
3079+ template: cloud-init-vendor.tpl
3080+ properties:
3081+ default: |
3082+ #cloud-config
3083+ {}
3084+ # overrides image template files
3085+ template_files:
3086+ cloud-init-meta.tpl: |
3087+ #cloud-config
3088+ instance-id: {{ container.name }}
3089+ local-hostname: {{ container.name }}
3090+ {{ config_get("user.meta-data", "") }}
3091+ cloud-init-network.tpl: |
3092+ {% if config_get("user.network-config", "") == "" %}version: 1
3093+ config:
3094+ - type: physical
3095+ name: eth0
3096+ subnets:
3097+ - type: {% if config_get("user.network_mode", "") == "link-local" %}manual{% else %}dhcp{% endif %}
3098+ control: auto{% else %}{{ config_get("user.network-config", "") }}{% endif %}
3099+ cloud-init-user.tpl: |
3100+ {{ config_get("user.user-data", properties.default) }}
3101+ cloud-init-vendor.tpl: |
3102+ {{ config_get("user.vendor-data", properties.default) }}
3103 ec2: {}
3104 azure: {}
3105
3106diff --git a/tests/cloud_tests/platforms/__init__.py b/tests/cloud_tests/platforms/__init__.py
3107index f9f5603..443f6d4 100644
3108--- a/tests/cloud_tests/platforms/__init__.py
3109+++ b/tests/cloud_tests/platforms/__init__.py
3110@@ -1,5 +1,7 @@
3111 # This file is part of cloud-init. See LICENSE file for license information.
3112
3113+"""Main init."""
3114+
3115 from tests.cloud_tests.platforms import lxd
3116
3117 PLATFORMS = {
3118@@ -8,9 +10,7 @@ PLATFORMS = {
3119
3120
3121 def get_platform(platform_name, config):
3122- """
3123- Get the platform object for 'platform_name' and init
3124- """
3125+ """Get the platform object for 'platform_name' and init."""
3126 platform_cls = PLATFORMS.get(platform_name)
3127 if not platform_cls:
3128 raise ValueError('invalid platform name: {}'.format(platform_name))
3129diff --git a/tests/cloud_tests/platforms/base.py b/tests/cloud_tests/platforms/base.py
3130index 615e2e0..2897536 100644
3131--- a/tests/cloud_tests/platforms/base.py
3132+++ b/tests/cloud_tests/platforms/base.py
3133@@ -1,53 +1,27 @@
3134 # This file is part of cloud-init. See LICENSE file for license information.
3135
3136+"""Base platform class."""
3137+
3138
3139 class Platform(object):
3140- """
3141- Base class for platforms
3142- """
3143+ """Base class for platforms."""
3144+
3145 platform_name = None
3146
3147 def __init__(self, config):
3148- """
3149- Set up platform
3150- """
3151+ """Set up platform."""
3152 self.config = config
3153
3154 def get_image(self, img_conf):
3155- """
3156- Get image using 'img_conf', where img_conf is a dict containing all
3157- image configuration parameters
3158-
3159- in this dict there must be a 'platform_ident' key containing
3160- configuration for identifying each image on a per platform basis
3161-
3162- see implementations for get_image() for details about the contents
3163- of the platform's config entry
3164+ """Get image using specified image configuration.
3165
3166- note: see 'releases' main_config.yaml for example entries
3167-
3168- img_conf: configuration for image
3169- return_value: cloud_tests.images instance
3170+ @param img_conf: configuration for image
3171+ @return_value: cloud_tests.images instance
3172 """
3173 raise NotImplementedError
3174
3175 def destroy(self):
3176- """
3177- Clean up platform data
3178- """
3179+ """Clean up platform data."""
3180 pass
3181
3182- def _extract_img_platform_config(self, img_conf):
3183- """
3184- extract platform configuration for current platform from img_conf
3185- """
3186- platform_ident = img_conf.get('platform_ident')
3187- if not platform_ident:
3188- raise ValueError('invalid img_conf, missing \'platform_ident\'')
3189- ident = platform_ident.get(self.platform_name)
3190- if not ident:
3191- raise ValueError('img_conf: {} missing config for platform {}'
3192- .format(img_conf, self.platform_name))
3193- return ident
3194-
3195 # vi: ts=4 expandtab
3196diff --git a/tests/cloud_tests/platforms/lxd.py b/tests/cloud_tests/platforms/lxd.py
3197index 847cc54..ead0955 100644
3198--- a/tests/cloud_tests/platforms/lxd.py
3199+++ b/tests/cloud_tests/platforms/lxd.py
3200@@ -1,5 +1,7 @@
3201 # This file is part of cloud-init. See LICENSE file for license information.
3202
3203+"""Base LXD platform."""
3204+
3205 from pylxd import (Client, exceptions)
3206
3207 from tests.cloud_tests.images import lxd as lxd_image
3208@@ -11,48 +13,49 @@ DEFAULT_SSTREAMS_SERVER = "https://images.linuxcontainers.org:8443"
3209
3210
3211 class LXDPlatform(base.Platform):
3212- """
3213- Lxd test platform
3214- """
3215+ """LXD test platform."""
3216+
3217 platform_name = 'lxd'
3218
3219 def __init__(self, config):
3220- """
3221- Set up platform
3222- """
3223+ """Set up platform."""
3224 super(LXDPlatform, self).__init__(config)
3225 # TODO: allow configuration of remote lxd host via env variables
3226 # set up lxd connection
3227 self.client = Client()
3228
3229 def get_image(self, img_conf):
3230+ """Get image using specified image configuration.
3231+
3232+ @param img_conf: configuration for image
3233+ @return_value: cloud_tests.images instance
3234 """
3235- Get image
3236- img_conf: dict containing config for image. platform_ident must have:
3237- alias: alias to use for simplestreams server
3238- sstreams_server: simplestreams server to use, or None for default
3239- return_value: cloud_tests.images instance
3240- """
3241- lxd_conf = self._extract_img_platform_config(img_conf)
3242- image = self.client.images.create_from_simplestreams(
3243- lxd_conf.get('sstreams_server', DEFAULT_SSTREAMS_SERVER),
3244- lxd_conf['alias'])
3245- return lxd_image.LXDImage(
3246- image.properties['description'], img_conf, self, image)
3247-
3248- def launch_container(self, image=None, container=None, ephemeral=False,
3249- config=None, block=True,
3250- image_desc=None, use_desc=None):
3251- """
3252- launch a container
3253- image: image fingerprint to launch from
3254- container: container to copy
3255- ephemeral: delete image after first shutdown
3256- config: config options for instance as dict
3257- block: wait until container created
3258- image_desc: description of image being launched
3259- use_desc: description of container's use
3260- return_value: cloud_tests.instances instance
3261+ pylxd_image = self.client.images.create_from_simplestreams(
3262+ img_conf.get('sstreams_server', DEFAULT_SSTREAMS_SERVER),
3263+ img_conf['alias'])
3264+ image = lxd_image.LXDImage(self, img_conf, pylxd_image)
3265+ if img_conf.get('override_templates', False):
3266+ image.update_templates(self.config.get('template_overrides', {}),
3267+ self.config.get('template_files', {}))
3268+ return image
3269+
3270+ def launch_container(self, properties, config, features,
3271+ image=None, container=None, ephemeral=False,
3272+ container_config=None, block=True, image_desc=None,
3273+ use_desc=None):
3274+ """Launch a container.
3275+
3276+ @param properties: image properties
3277+ @param config: image configuration
3278+ @param features: image features
3279+ @param image: image fingerprint to launch from
3280+ @param container: container to copy
3281+ @param ephemeral: delete image after first shutdown
3282+ @param container_config: config options for instance as dict
3283+ @param block: wait until container created
3284+ @param image_desc: description of image being launched
3285+ @param use_desc: description of container's use
3286+ @return_value: cloud_tests.instances instance
3287 """
3288 if not (image or container):
3289 raise ValueError("either image or container must be specified")
3290@@ -61,16 +64,18 @@ class LXDPlatform(base.Platform):
3291 use_desc=use_desc,
3292 used_list=self.list_containers()),
3293 'ephemeral': bool(ephemeral),
3294- 'config': config if isinstance(config, dict) else {},
3295+ 'config': (container_config
3296+ if isinstance(container_config, dict) else {}),
3297 'source': ({'type': 'image', 'fingerprint': image} if image else
3298 {'type': 'copy', 'source': container})
3299 }, wait=block)
3300- return lxd_instance.LXDInstance(container.name, self, container)
3301+ return lxd_instance.LXDInstance(self, container.name, properties,
3302+ config, features, container)
3303
3304 def container_exists(self, container_name):
3305- """
3306- check if container with name 'container_name' exists
3307- return_value: True if exists else False
3308+ """Check if container with name 'container_name' exists.
3309+
3310+ @return_value: True if exists else False
3311 """
3312 res = True
3313 try:
3314@@ -82,16 +87,22 @@ class LXDPlatform(base.Platform):
3315 return res
3316
3317 def list_containers(self):
3318- """
3319- list names of all containers
3320- return_value: list of names
3321+ """List names of all containers.
3322+
3323+ @return_value: list of names
3324 """
3325 return [container.name for container in self.client.containers.all()]
3326
3327- def destroy(self):
3328- """
3329- Clean up platform data
3330+ def query_image_by_alias(self, alias):
3331+ """Get image by alias in local image store.
3332+
3333+ @param alias: alias of image
3334+ @return_value: pylxd image (not cloud_tests.images instance)
3335 """
3336+ return self.client.images.get_by_alias(alias)
3337+
3338+ def destroy(self):
3339+ """Clean up platform data."""
3340 super(LXDPlatform, self).destroy()
3341
3342 # vi: ts=4 expandtab
3343diff --git a/tests/cloud_tests/releases.yaml b/tests/cloud_tests/releases.yaml
3344index 183f78c..45deb58 100644
3345--- a/tests/cloud_tests/releases.yaml
3346+++ b/tests/cloud_tests/releases.yaml
3347@@ -1,86 +1,253 @@
3348 # ============================= Release Config ================================
3349 default_release_config:
3350- # all are disabled by default
3351- enabled: false
3352- # timeout for booting image and running cloud init
3353- timeout: 120
3354- # platform_ident values for the image, with data to identify the image
3355- # on that platform. see platforms.base for more information
3356- platform_ident: {}
3357- # a script to run after a boot that is used to modify an image, before
3358- # making a snapshot of the image. may be useful for removing data left
3359- # behind from cloud-init booting, such as logs, to ensure that data from
3360- # snapshot.launch() will not include a cloud-init.log from a boot used to
3361- # create the snapshot, if cloud-init has not run
3362- boot_clean_script: |
3363- #!/bin/bash
3364- rm -rf /var/log/cloud-init.log /var/log/cloud-init-output.log \
3365- /var/lib/cloud/ /run/cloud-init/ /var/log/syslog
3366+ # global default configuration options
3367+ default:
3368+ # all are disabled by default
3369+ enabled: false
3370+ # timeout for booting image and running cloud init
3371+ boot_timeout: 120
3372+ # a script to run after a boot that is used to modify an image, before
3373+ # making a snapshot of the image. may be useful for removing data left
3374+ # behind from cloud-init booting, such as logs, to ensure that data
3375+ # from snapshot.launch() will not include a cloud-init.log from a boot
3376+ # used to create the snapshot, if cloud-init has not run
3377+ boot_clean_script: |
3378+ #!/bin/bash
3379+ rm -rf /var/log/cloud-init.log /var/log/cloud-init-output.log \
3380+ /var/lib/cloud/ /run/cloud-init/ /var/log/syslog
3381+ # test script to determine if system is booted fully
3382+ system_ready_script: |
3383+ # permit running or degraded state as both indicate complete boot
3384+ [ $(systemctl is-system-running) = 'running' -o
3385+ $(systemctl is-system-running) = 'degraded' ]
3386+ # test script to determine if cloud-init has finished
3387+ cloud_init_ready_script: |
3388+ [ -f '/run/cloud-init/result.json' ]
3389+ # currently used features and their uses are:
3390+ # features groups and additional feature settings
3391+ feature_groups: []
3392+ features: {}
3393+
3394+ # lxd specific default configuration options
3395+ lxd:
3396+ # default sstreams server to use for lxd image retrieval
3397+ sstreams_server: https://us.images.linuxcontainers.org:8443
3398+ # keep base image, avoids downloading again next run
3399+ cache_base_image: true
3400+ # lxd images from linuxcontainers.org do not have the nocloud seed
3401+ # templates in place, so the image metadata must be modified
3402+ override_templates: true
3403+ # arg overrides to set image up
3404+ setup_overrides:
3405+ # lxd images from linuxcontainers.org do not come with
3406+ # cloud-init, so must pull cloud-init in from repo using
3407+ # setup_image.upgrade
3408+ upgrade: true
3409+
3410+features:
3411+ # all currently supported feature flags
3412+ all:
3413+ - apt # image supports apt package manager
3414+ - byobu # byobu is available in repositories
3415+ - landscape # landscape-client available in repos
3416+ - lxd # lxd is available in the image
3417+ - ppa # image supports ppas
3418+ - rpm # image supports rpms
3419+ - snap # supports snapd
3420+ # NOTE: the following feature flags are to work around bugs in the
3421+ # images, and can be removed when no longer needed
3422+ - hostname # setting system hostname works
3423+ # NOTE: the following feature flags are to work around issues in the
3424+ # testcases, and can be removed when no longer needed
3425+ - apt_src_cont # default contents and format of sources.list matches
3426+ # ubuntu sources.list
3427+ - apt_hist_fmt # apt command history entries use full paths to apt
3428+ # executable rather than relative paths
3429+ - daylight_time # timezones are daylight not standard time
3430+ - apt_up_out # 'Calculating upgrade..' present in log output from
3431+ # apt-get dist-upgrade output
3432+ - engb_locale # locale en_GB.UTF-8 is available
3433+ - locale_gen # the /etc/locale.gen file exists
3434+ - no_ntpdate # 'ntpdate' is not installed by default
3435+ - no_file_fmt_e # the 'file' utility does not have a formatting error
3436+ - ppa_file_name # the name of the source file added to sources.list.d has
3437+ # the expected format for newer ubuntu releases
3438+ - sshd # requires ssh server to be installed by default
3439+ - ssh_key_fmt # ssh auth keys printed to console have expected format
3440+ - syslog # test case requires syslog to be written by default
3441+ - ubuntu_ntp # expect ubuntu.pool.ntp.org to be used as ntp server
3442+ - ubuntu_repos # test case requres ubuntu repositories to be used
3443+ - ubuntu_user # test case needs user with the name 'ubuntu' to exist
3444+ # NOTE: the following feature flags are to work around issues that may
3445+ # be considered bugs in cloud-init
3446+ - lsb_release # image has lsb_release installed, maybe should install
3447+ # if missing by default
3448+ - sudo # image has sudo installed, should not be required
3449+ # feature flag groups
3450+ groups:
3451+ base:
3452+ hostname: true
3453+ no_file_fmt_e: true
3454+ ubuntu_specific:
3455+ apt_src_cont: true
3456+ apt_hist_fmt: true
3457+ byobu: true
3458+ daylight_time: true
3459+ engb_locale: true
3460+ landscape: true
3461+ locale_gen: true
3462+ lsb_release: true
3463+ lxd: true
3464+ ppa: true
3465+ ppa_file_name: true
3466+ snap: true
3467+ sshd: true
3468+ ssh_key_fmt: true
3469+ sudo: true
3470+ syslog: true
3471+ ubuntu_ntp: true
3472+ ubuntu_repos: true
3473+ ubuntu_user: true
3474+ debian_base:
3475+ apt: true
3476+ apt_up_out: true
3477+ no_ntpdate: true
3478+ rhel_base:
3479+ rpm: true
3480
3481 releases:
3482- trusty:
3483- enabled: true
3484- platform_ident:
3485- lxd:
3486- # if sstreams_server is omitted, default is used, defined in
3487- # tests.cloud_tests.platforms.lxd.DEFAULT_SSTREAMS_SERVER as:
3488- # sstreams_server: https://us.images.linuxcontainers.org:8443
3489- #alias: ubuntu/trusty/default
3490- alias: t
3491- sstreams_server: https://cloud-images.ubuntu.com/daily
3492- xenial:
3493- enabled: true
3494- platform_ident:
3495- lxd:
3496- #alias: ubuntu/xenial/default
3497- alias: x
3498- sstreams_server: https://cloud-images.ubuntu.com/daily
3499- yakkety:
3500- enabled: true
3501- platform_ident:
3502- lxd:
3503- #alias: ubuntu/yakkety/default
3504- alias: y
3505- sstreams_server: https://cloud-images.ubuntu.com/daily
3506- zesty:
3507- enabled: true
3508- platform_ident:
3509- lxd:
3510- #alias: ubuntu/zesty/default
3511- alias: z
3512- sstreams_server: https://cloud-images.ubuntu.com/daily
3513+ # UBUNTU =================================================================
3514 artful:
3515- enabled: true
3516- platform_ident:
3517- lxd:
3518- #alias: ubuntu/artful/default
3519- alias: a
3520- sstreams_server: https://cloud-images.ubuntu.com/daily
3521- jessie:
3522- platform_ident:
3523- lxd:
3524- alias: debian/jessie/default
3525- sid:
3526- platform_ident:
3527- lxd:
3528- alias: debian/sid/default
3529+ # EOL: Jul 2018
3530+ default:
3531+ enabled: true
3532+ feature_groups:
3533+ - base
3534+ - debian_base
3535+ - ubuntu_specific
3536+ lxd:
3537+ sstreams_server: https://cloud-images.ubuntu.com/daily
3538+ alias: artful
3539+ setup_overrides: null
3540+ override_templates: false
3541+ zesty:
3542+ # EOL: Jan 2018
3543+ default:
3544+ enabled: true
3545+ feature_groups:
3546+ - base
3547+ - debian_base
3548+ - ubuntu_specific
3549+ lxd:
3550+ sstreams_server: https://cloud-images.ubuntu.com/daily
3551+ alias: zesty
3552+ setup_overrides: null
3553+ override_templates: false
3554+ yakkety:
3555+ # EOL: Jul 2017
3556+ default:
3557+ enabled: true
3558+ feature_groups:
3559+ - base
3560+ - debian_base
3561+ - ubuntu_specific
3562+ lxd:
3563+ sstreams_server: https://cloud-images.ubuntu.com/daily
3564+ alias: yakkety
3565+ setup_overrides: null
3566+ override_templates: false
3567+ xenial:
3568+ # EOL: Apr 2021
3569+ default:
3570+ enabled: true
3571+ feature_groups:
3572+ - base
3573+ - debian_base
3574+ - ubuntu_specific
3575+ lxd:
3576+ sstreams_server: https://cloud-images.ubuntu.com/daily
3577+ alias: xenial
3578+ setup_overrides: null
3579+ override_templates: false
3580+ trusty:
3581+ # EOL: Apr 2019
3582+ default:
3583+ enabled: true
3584+ feature_groups:
3585+ - base
3586+ - debian_base
3587+ - ubuntu_specific
3588+ features:
3589+ apt_up_out: false
3590+ locale_gen: false
3591+ lxd: false
3592+ ppa_file_name: false
3593+ snap: false
3594+ ssh_key_fmt: false
3595+ no_ntpdate: false
3596+ no_file_fmt_e: false
3597+ system_ready_script: |
3598+ #!/bin/bash
3599+ # upstart based, so use old style runlevels
3600+ [ $(runlevel | awk '{print $2}') = '2' ]
3601+ lxd:
3602+ sstreams_server: https://cloud-images.ubuntu.com/daily
3603+ alias: trusty
3604+ setup_overrides: null
3605+ override_templates: false
3606+ # DEBIAN =================================================================
3607 stretch:
3608- platform_ident:
3609- lxd:
3610- alias: debian/stretch/default
3611- wheezy:
3612- platform_ident:
3613- lxd:
3614- alias: debian/wheezy/default
3615+ # EOL: Not yet released
3616+ default:
3617+ enabled: true
3618+ feature_groups:
3619+ - base
3620+ - debian_base
3621+ lxd:
3622+ alias: debian/stretch/default
3623+ jessie:
3624+ # EOL: Jun 2020
3625+ # NOTE: the cloud-init version shipped with jessie is out of date
3626+ # tests work if an up to date deb is used
3627+ default:
3628+ enabled: true
3629+ feature_groups:
3630+ - base
3631+ - debian_base
3632+ lxd:
3633+ alias: debian/jessie/default
3634+ # CENTOS =================================================================
3635 centos70:
3636- timeout: 180
3637- platform_ident:
3638- lxd:
3639- alias: centos/7/default
3640+ # EOL: Jun 2024 (2020 - end of full updates)
3641+ default:
3642+ enabled: true
3643+ feature_groups:
3644+ - base
3645+ - rhel_base
3646+ user_data_overrides:
3647+ preserve_hostname: true
3648+ lxd:
3649+ features:
3650+ # NOTE: (LP: #1575779)
3651+ hostname: false
3652+ alias: centos/7/default
3653 centos66:
3654- timeout: 180
3655- platform_ident:
3656- lxd:
3657- alias: centos/6/default
3658+ # EOL: Nov 2020
3659+ default:
3660+ enabled: true
3661+ feature_groups:
3662+ - base
3663+ - rhel_base
3664+ # still supported, but only bugfixes after may 2017
3665+ system_ready_script: |
3666+ #!/bin/bash
3667+ [ $(runlevel | awk '{print $2}') = '3' ]
3668+ user_data_overrides:
3669+ preserve_hostname: true
3670+ lxd:
3671+ features:
3672+ # NOTE: (LP: #1575779)
3673+ hostname: false
3674+ alias: centos/6/default
3675
3676 # vi: ts=4 expandtab
3677diff --git a/tests/cloud_tests/run_funcs.py b/tests/cloud_tests/run_funcs.py
3678new file mode 100644
3679index 0000000..8ae9112
3680--- /dev/null
3681+++ b/tests/cloud_tests/run_funcs.py
3682@@ -0,0 +1,75 @@
3683+# This file is part of cloud-init. See LICENSE file for license information.
3684+
3685+"""Run functions."""
3686+
3687+import os
3688+
3689+from tests.cloud_tests import bddeb, collect, util, verify
3690+
3691+
3692+def tree_collect(args):
3693+ """Collect data using deb build from current tree.
3694+
3695+ @param args: cmdline args
3696+ @return_value: fail count
3697+ """
3698+ failed = 0
3699+ tmpdir = util.TempDir(tmpdir=args.data_dir, preserve=args.preserve_data)
3700+
3701+ with tmpdir as data_dir:
3702+ args.data_dir = data_dir
3703+ args.deb = os.path.join(tmpdir.tmpdir, 'cloud-init_all.deb')
3704+ try:
3705+ failed += bddeb.bddeb(args)
3706+ failed += collect.collect(args)
3707+ except Exception:
3708+ failed += 1
3709+ raise
3710+
3711+ return failed
3712+
3713+
3714+def tree_run(args):
3715+ """Run test suite using deb build from current tree.
3716+
3717+ @param args: cmdline args
3718+ @return_value: fail count
3719+ """
3720+ failed = 0
3721+ tmpdir = util.TempDir(tmpdir=args.data_dir, preserve=args.preserve_data)
3722+
3723+ with tmpdir as data_dir:
3724+ args.data_dir = data_dir
3725+ args.deb = os.path.join(tmpdir.tmpdir, 'cloud-init_all.deb')
3726+ try:
3727+ failed += bddeb.bddeb(args)
3728+ failed += collect.collect(args)
3729+ failed += verify.verify(args)
3730+ except Exception:
3731+ failed += 1
3732+ raise
3733+
3734+ return failed
3735+
3736+
3737+def run(args):
3738+ """Run test suite.
3739+
3740+ @param args: cmdline args
3741+ @return_value: fail count
3742+ """
3743+ failed = 0
3744+ tmpdir = util.TempDir(tmpdir=args.data_dir, preserve=args.preserve_data)
3745+
3746+ with tmpdir as data_dir:
3747+ args.data_dir = data_dir
3748+ try:
3749+ failed += collect.collect(args)
3750+ failed += verify.verify(args)
3751+ except Exception:
3752+ failed += 1
3753+ raise
3754+
3755+ return failed
3756+
3757+# vi: ts=4 expandtab
3758diff --git a/tests/cloud_tests/setup_image.py b/tests/cloud_tests/setup_image.py
3759index 5d6c638..8053a09 100644
3760--- a/tests/cloud_tests/setup_image.py
3761+++ b/tests/cloud_tests/setup_image.py
3762@@ -1,18 +1,42 @@
3763 # This file is part of cloud-init. See LICENSE file for license information.
3764
3765-from tests.cloud_tests import LOG
3766-from tests.cloud_tests import stage, util
3767+"""Setup image for testing."""
3768
3769 from functools import partial
3770 import os
3771
3772+from tests.cloud_tests import LOG
3773+from tests.cloud_tests import stage, util
3774
3775-def install_deb(args, image):
3776+
3777+def installed_package_version(image, package, ensure_installed=True):
3778+ """Get installed version of package.
3779+
3780+ @param image: cloud_tests.images instance to operate on
3781+ @param package: name of package
3782+ @param ensure_installed: raise error if not installed
3783+ @return_value: cloud-init version string
3784 """
3785- install deb into image
3786- args: cmdline arguments, must contain --deb
3787- image: cloud_tests.images instance to operate on
3788- return_value: None, may raise errors
3789+ os_family = util.get_os_family(image.properties['os'])
3790+ if os_family == 'debian':
3791+ cmd = ['dpkg-query', '-W', "--showformat='${Version}'", package]
3792+ elif os_family == 'redhat':
3793+ cmd = ['rpm', '-q', '--queryformat', "'%{VERSION}'", package]
3794+ else:
3795+ raise NotImplementedError
3796+
3797+ msg = 'query version for package: {}'.format(package)
3798+ (out, err, exit) = image.execute(
3799+ cmd, description=msg, rcs=(0,) if ensure_installed else range(0, 256))
3800+ return out.strip()
3801+
3802+
3803+def install_deb(args, image):
3804+ """Install deb into image.
3805+
3806+ @param args: cmdline arguments, must contain --deb
3807+ @param image: cloud_tests.images instance to operate on
3808+ @return_value: None, may raise errors
3809 """
3810 # ensure system is compatible with package format
3811 os_family = util.get_os_family(image.properties['os'])
3812@@ -21,20 +45,18 @@ def install_deb(args, image):
3813 'family: {}'.format(args.deb, os_family))
3814
3815 # install deb
3816- LOG.debug('installing deb: %s into target', args.deb)
3817+ msg = 'install deb: "{}" into target'.format(args.deb)
3818+ LOG.debug(msg)
3819 remote_path = os.path.join('/tmp', os.path.basename(args.deb))
3820 image.push_file(args.deb, remote_path)
3821- (out, err, exit) = image.execute(['dpkg', '-i', remote_path])
3822- if exit != 0:
3823- raise OSError('failed install deb: {}\n\tstdout: {}\n\tstderr: {}'
3824- .format(args.deb, out, err))
3825+ cmd = 'dpkg -i {} || apt-get install --yes -f'.format(remote_path)
3826+ image.execute(['/bin/sh', '-c', cmd], description=msg)
3827
3828 # check installed deb version matches package
3829 fmt = ['-W', "--showformat='${Version}'"]
3830 (out, err, exit) = image.execute(['dpkg-deb'] + fmt + [remote_path])
3831 expected_version = out.strip()
3832- (out, err, exit) = image.execute(['dpkg-query'] + fmt + ['cloud-init'])
3833- found_version = out.strip()
3834+ found_version = installed_package_version(image, 'cloud-init')
3835 if expected_version != found_version:
3836 raise OSError('install deb version "{}" does not match expected "{}"'
3837 .format(found_version, expected_version))
3838@@ -44,32 +66,28 @@ def install_deb(args, image):
3839
3840
3841 def install_rpm(args, image):
3842+ """Install rpm into image.
3843+
3844+ @param args: cmdline arguments, must contain --rpm
3845+ @param image: cloud_tests.images instance to operate on
3846+ @return_value: None, may raise errors
3847 """
3848- install rpm into image
3849- args: cmdline arguments, must contain --rpm
3850- image: cloud_tests.images instance to operate on
3851- return_value: None, may raise errors
3852- """
3853- # ensure system is compatible with package format
3854 os_family = util.get_os_family(image.properties['os'])
3855- if os_family not in ['redhat', 'sles']:
3856+ if os_family != 'redhat':
3857 raise NotImplementedError('install rpm: {} not supported on os '
3858 'family: {}'.format(args.rpm, os_family))
3859
3860 # install rpm
3861- LOG.debug('installing rpm: %s into target', args.rpm)
3862+ msg = 'install rpm: "{}" into target'.format(args.rpm)
3863+ LOG.debug(msg)
3864 remote_path = os.path.join('/tmp', os.path.basename(args.rpm))
3865 image.push_file(args.rpm, remote_path)
3866- (out, err, exit) = image.execute(['rpm', '-U', remote_path])
3867- if exit != 0:
3868- raise OSError('failed to install rpm: {}\n\tstdout: {}\n\tstderr: {}'
3869- .format(args.rpm, out, err))
3870+ image.execute(['rpm', '-U', remote_path], description=msg)
3871
3872 fmt = ['--queryformat', '"%{VERSION}"']
3873 (out, err, exit) = image.execute(['rpm', '-q'] + fmt + [remote_path])
3874 expected_version = out.strip()
3875- (out, err, exit) = image.execute(['rpm', '-q'] + fmt + ['cloud-init'])
3876- found_version = out.strip()
3877+ found_version = installed_package_version(image, 'cloud-init')
3878 if expected_version != found_version:
3879 raise OSError('install rpm version "{}" does not match expected "{}"'
3880 .format(found_version, expected_version))
3881@@ -79,14 +97,32 @@ def install_rpm(args, image):
3882
3883
3884 def upgrade(args, image):
3885+ """Upgrade or install cloud-init from repo.
3886+
3887+ @param args: cmdline arguments
3888+ @param image: cloud_tests.images instance to operate on
3889+ @return_value: None, may raise errors
3890 """
3891- run the system's upgrade command
3892- args: cmdline arguments
3893- image: cloud_tests.images instance to operate on
3894- return_value: None, may raise errors
3895+ os_family = util.get_os_family(image.properties['os'])
3896+ if os_family == 'debian':
3897+ cmd = 'apt-get update && apt-get install cloud-init --yes'
3898+ elif os_family == 'redhat':
3899+ cmd = 'sleep 10 && yum install cloud-init --assumeyes'
3900+ else:
3901+ raise NotImplementedError
3902+
3903+ msg = 'upgrading cloud-init'
3904+ LOG.debug(msg)
3905+ image.execute(['/bin/sh', '-c', cmd], description=msg)
3906+
3907+
3908+def upgrade_full(args, image):
3909+ """Run the system's full upgrade command.
3910+
3911+ @param args: cmdline arguments
3912+ @param image: cloud_tests.images instance to operate on
3913+ @return_value: None, may raise errors
3914 """
3915- # determine appropriate upgrade command for os_family
3916- # TODO: maybe use cloudinit.distros for this?
3917 os_family = util.get_os_family(image.properties['os'])
3918 if os_family == 'debian':
3919 cmd = 'apt-get update && apt-get upgrade --yes'
3920@@ -96,53 +132,48 @@ def upgrade(args, image):
3921 raise NotImplementedError('upgrade command not configured for distro '
3922 'from family: {}'.format(os_family))
3923
3924- # upgrade system
3925- LOG.debug('upgrading system')
3926- (out, err, exit) = image.execute(['/bin/sh', '-c', cmd])
3927- if exit != 0:
3928- raise OSError('failed to upgrade system\n\tstdout: {}\n\tstderr:{}'
3929- .format(out, err))
3930+ msg = 'full system upgrade'
3931+ LOG.debug(msg)
3932+ image.execute(['/bin/sh', '-c', cmd], description=msg)
3933
3934
3935 def run_script(args, image):
3936+ """Run a script in the target image.
3937+
3938+ @param args: cmdline arguments, must contain --script
3939+ @param image: cloud_tests.images instance to operate on
3940+ @return_value: None, may raise errors
3941 """
3942- run a script in the target image
3943- args: cmdline arguments, must contain --script
3944- image: cloud_tests.images instance to operate on
3945- return_value: None, may raise errors
3946- """
3947- # TODO: get exit status back from script and add error handling here
3948- LOG.debug('running setup image script in target image')
3949- image.run_script(args.script)
3950+ msg = 'run setup image script in target image'
3951+ LOG.debug(msg)
3952+ image.run_script(args.script, description=msg)
3953
3954
3955 def enable_ppa(args, image):
3956- """
3957- enable a ppa in the target image
3958- args: cmdline arguments, must contain --ppa
3959- image: cloud_tests.image instance to operate on
3960- return_value: None, may raise errors
3961+ """Enable a ppa in the target image.
3962+
3963+ @param args: cmdline arguments, must contain --ppa
3964+ @param image: cloud_tests.image instance to operate on
3965+ @return_value: None, may raise errors
3966 """
3967 # ppa only supported on ubuntu (maybe debian?)
3968- if image.properties['os'] != 'ubuntu':
3969+ if image.properties['os'].lower() != 'ubuntu':
3970 raise NotImplementedError('enabling a ppa is only available on ubuntu')
3971
3972 # add ppa with add-apt-repository and update
3973 ppa = 'ppa:{}'.format(args.ppa)
3974- LOG.debug('enabling %s', ppa)
3975+ msg = 'enable ppa: "{}" in target'.format(ppa)
3976+ LOG.debug(msg)
3977 cmd = 'add-apt-repository --yes {} && apt-get update'.format(ppa)
3978- (out, err, exit) = image.execute(['/bin/sh', '-c', cmd])
3979- if exit != 0:
3980- raise OSError('enable ppa for {} failed\n\tstdout: {}\n\tstderr: {}'
3981- .format(ppa, out, err))
3982+ image.execute(['/bin/sh', '-c', cmd], description=msg)
3983
3984
3985 def enable_repo(args, image):
3986- """
3987- enable a repository in the target image
3988- args: cmdline arguments, must contain --repo
3989- image: cloud_tests.image instance to operate on
3990- return_value: None, may raise errors
3991+ """Enable a repository in the target image.
3992+
3993+ @param args: cmdline arguments, must contain --repo
3994+ @param image: cloud_tests.image instance to operate on
3995+ @return_value: None, may raise errors
3996 """
3997 # find enable repo command for the distro
3998 os_family = util.get_os_family(image.properties['os'])
3999@@ -155,20 +186,23 @@ def enable_repo(args, image):
4000 raise NotImplementedError('enable repo command not configured for '
4001 'distro from family: {}'.format(os_family))
4002
4003- LOG.debug('enabling repo: "%s"', args.repo)
4004- (out, err, exit) = image.execute(['/bin/sh', '-c', cmd])
4005- if exit != 0:
4006- raise OSError('enable repo {} failed\n\tstdout: {}\n\tstderr: {}'
4007- .format(args.repo, out, err))
4008+ msg = 'enable repo: "{}" in target'.format(args.repo)
4009+ LOG.debug(msg)
4010+ image.execute(['/bin/sh', '-c', cmd], description=msg)
4011
4012
4013 def setup_image(args, image):
4014+ """Set up image as specified in args.
4015+
4016+ @param args: cmdline arguments
4017+ @param image: cloud_tests.image instance to operate on
4018+ @return_value: tuple of results and fail count
4019 """
4020- set up image as specified in args
4021- args: cmdline arguments
4022- image: cloud_tests.image instance to operate on
4023- return_value: tuple of results and fail count
4024- """
4025+ # update the args if necessary for this image
4026+ overrides = image.setup_overrides
4027+ LOG.debug('updating args for setup with: %s', overrides)
4028+ args = util.update_args(args, overrides, preserve_old=True)
4029+
4030 # mapping of setup cmdline arg name to setup function
4031 # represented as a tuple rather than a dict or odict as lookup by name not
4032 # needed, and order is important as --script and --upgrade go at the end
4033@@ -179,17 +213,19 @@ def setup_image(args, image):
4034 ('repo', enable_repo, 'setup func for --repo, enable repo'),
4035 ('ppa', enable_ppa, 'setup func for --ppa, enable ppa'),
4036 ('script', run_script, 'setup func for --script, run script'),
4037- ('upgrade', upgrade, 'setup func for --upgrade, upgrade pkgs'),
4038+ ('upgrade', upgrade, 'setup func for --upgrade, upgrade cloud-init'),
4039+ ('upgrade-full', upgrade_full, 'setup func for --upgrade-full'),
4040 )
4041
4042 # determine which setup functions needed
4043 calls = [partial(stage.run_single, desc, partial(func, args, image))
4044 for name, func, desc in handlers if getattr(args, name, None)]
4045
4046- image_name = 'image: distro={}, release={}'.format(
4047- image.properties['os'], image.properties['release'])
4048- LOG.info('setting up %s', image_name)
4049- return stage.run_stage('set up for {}'.format(image_name), calls,
4050- continue_after_error=False)
4051+ LOG.info('setting up %s', image)
4052+ res = stage.run_stage(
4053+ 'set up for {}'.format(image), calls, continue_after_error=False)
4054+ LOG.debug('after setup complete, installed cloud-init version is: %s',
4055+ installed_package_version(image, 'cloud-init'))
4056+ return res
4057
4058 # vi: ts=4 expandtab
4059diff --git a/tests/cloud_tests/snapshots/__init__.py b/tests/cloud_tests/snapshots/__init__.py
4060index 2ab654d..93a54f5 100644
4061--- a/tests/cloud_tests/snapshots/__init__.py
4062+++ b/tests/cloud_tests/snapshots/__init__.py
4063@@ -1,10 +1,10 @@
4064 # This file is part of cloud-init. See LICENSE file for license information.
4065
4066+"""Main init."""
4067+
4068
4069 def get_snapshot(image):
4070- """
4071- get snapshot from image
4072- """
4073+ """Get snapshot from image."""
4074 return image.snapshot()
4075
4076 # vi: ts=4 expandtab
4077diff --git a/tests/cloud_tests/snapshots/base.py b/tests/cloud_tests/snapshots/base.py
4078index d715f03..9432898 100644
4079--- a/tests/cloud_tests/snapshots/base.py
4080+++ b/tests/cloud_tests/snapshots/base.py
4081@@ -1,44 +1,45 @@
4082 # This file is part of cloud-init. See LICENSE file for license information.
4083
4084+"""Base snapshot."""
4085+
4086
4087 class Snapshot(object):
4088- """
4089- Base class for snapshots
4090- """
4091+ """Base class for snapshots."""
4092+
4093 platform_name = None
4094
4095- def __init__(self, properties, config):
4096- """
4097- Set up snapshot
4098+ def __init__(self, platform, properties, config, features):
4099+ """Set up snapshot.
4100+
4101+ @param platform: platform object
4102+ @param properties: image properties
4103+ @param config: image config
4104+ @param features: supported feature flags
4105 """
4106+ self.platform = platform
4107 self.properties = properties
4108 self.config = config
4109+ self.features = features
4110
4111 def __str__(self):
4112- """
4113- a brief description of the snapshot
4114- """
4115+ """A brief description of the snapshot."""
4116 return '-'.join((self.properties['os'], self.properties['release']))
4117
4118 def launch(self, user_data, meta_data=None, block=True, start=True,
4119 use_desc=None):
4120- """
4121- launch instance
4122-
4123- user_data: user-data for the instance
4124- instance_id: instance-id for the instance
4125- block: wait until instance is created
4126- start: start instance and wait until fully started
4127- use_desc: description of snapshot instance use
4128+ """Launch instance.
4129
4130- return_value: an Instance
4131+ @param user_data: user-data for the instance
4132+ @param instance_id: instance-id for the instance
4133+ @param block: wait until instance is created
4134+ @param start: start instance and wait until fully started
4135+ @param use_desc: description of snapshot instance use
4136+ @return_value: an Instance
4137 """
4138 raise NotImplementedError
4139
4140 def destroy(self):
4141- """
4142- Clean up snapshot data
4143- """
4144+ """Clean up snapshot data."""
4145 pass
4146
4147 # vi: ts=4 expandtab
4148diff --git a/tests/cloud_tests/snapshots/lxd.py b/tests/cloud_tests/snapshots/lxd.py
4149index eabbce3..39c55c5 100644
4150--- a/tests/cloud_tests/snapshots/lxd.py
4151+++ b/tests/cloud_tests/snapshots/lxd.py
4152@@ -1,49 +1,52 @@
4153 # This file is part of cloud-init. See LICENSE file for license information.
4154
4155+"""Base LXD snapshot."""
4156+
4157 from tests.cloud_tests.snapshots import base
4158
4159
4160 class LXDSnapshot(base.Snapshot):
4161- """
4162- LXD image copy backed snapshot
4163- """
4164+ """LXD image copy backed snapshot."""
4165+
4166 platform_name = "lxd"
4167
4168- def __init__(self, properties, config, platform, pylxd_frozen_instance):
4169- """
4170- Set up snapshot
4171+ def __init__(self, platform, properties, config, features,
4172+ pylxd_frozen_instance):
4173+ """Set up snapshot.
4174+
4175+ @param platform: platform object
4176+ @param properties: image properties
4177+ @param config: image config
4178+ @param features: supported feature flags
4179 """
4180- self.platform = platform
4181 self.pylxd_frozen_instance = pylxd_frozen_instance
4182- super(LXDSnapshot, self).__init__(properties, config)
4183+ super(LXDSnapshot, self).__init__(
4184+ platform, properties, config, features)
4185
4186 def launch(self, user_data, meta_data=None, block=True, start=True,
4187 use_desc=None):
4188- """
4189- launch instance
4190-
4191- user_data: user-data for the instance
4192- instance_id: instance-id for the instance
4193- block: wait until instance is created
4194- start: start instance and wait until fully started
4195- use_desc: description of snapshot instance use
4196-
4197- return_value: an Instance
4198+ """Launch instance.
4199+
4200+ @param user_data: user-data for the instance
4201+ @param instance_id: instance-id for the instance
4202+ @param block: wait until instance is created
4203+ @param start: start instance and wait until fully started
4204+ @param use_desc: description of snapshot instance use
4205+ @return_value: an Instance
4206 """
4207 inst_config = {'user.user-data': user_data}
4208 if meta_data:
4209 inst_config['user.meta-data'] = meta_data
4210 instance = self.platform.launch_container(
4211- container=self.pylxd_frozen_instance.name, config=inst_config,
4212- block=block, image_desc=str(self), use_desc=use_desc)
4213+ self.properties, self.config, self.features, block=block,
4214+ image_desc=str(self), container=self.pylxd_frozen_instance.name,
4215+ use_desc=use_desc, container_config=inst_config)
4216 if start:
4217- instance.start(wait=True, wait_time=self.config.get('timeout'))
4218+ instance.start()
4219 return instance
4220
4221 def destroy(self):
4222- """
4223- Clean up snapshot data
4224- """
4225+ """Clean up snapshot data."""
4226 self.pylxd_frozen_instance.destroy()
4227 super(LXDSnapshot, self).destroy()
4228
4229diff --git a/tests/cloud_tests/stage.py b/tests/cloud_tests/stage.py
4230index 584cdae..74a7d46 100644
4231--- a/tests/cloud_tests/stage.py
4232+++ b/tests/cloud_tests/stage.py
4233@@ -1,5 +1,7 @@
4234 # This file is part of cloud-init. See LICENSE file for license information.
4235
4236+"""Stage a run."""
4237+
4238 import sys
4239 import time
4240 import traceback
4241@@ -8,38 +10,29 @@ from tests.cloud_tests import LOG
4242
4243
4244 class PlatformComponent(object):
4245- """
4246- context manager to safely handle platform components, ensuring that
4247- .destroy() is called
4248- """
4249+ """Context manager to safely handle platform components."""
4250
4251 def __init__(self, get_func):
4252- """
4253- store get_<platform component> function as partial taking no args
4254- """
4255+ """Store get_<platform component> function as partial with no args."""
4256 self.get_func = get_func
4257
4258 def __enter__(self):
4259- """
4260- create instance of platform component
4261- """
4262+ """Create instance of platform component."""
4263 self.instance = self.get_func()
4264 return self.instance
4265
4266 def __exit__(self, etype, value, trace):
4267- """
4268- destroy instance
4269- """
4270+ """Destroy instance."""
4271 if self.instance is not None:
4272 self.instance.destroy()
4273
4274
4275 def run_single(name, call):
4276- """
4277- run a single function, keeping track of results and failures and time
4278- name: name of part
4279- call: call to make
4280- return_value: a tuple of result and fail count
4281+ """Run a single function, keeping track of results and time.
4282+
4283+ @param name: name of part
4284+ @param call: call to make
4285+ @return_value: a tuple of result and fail count
4286 """
4287 res = {
4288 'name': name,
4289@@ -67,17 +60,18 @@ def run_single(name, call):
4290
4291
4292 def run_stage(parent_name, calls, continue_after_error=True):
4293- """
4294- run a stage of collection, keeping track of results and failures
4295- parent_name: name of stage calls are under
4296- calls: list of function call taking no params. must return a tuple
4297- of results and failures. may raise exceptions
4298- continue_after_error: whether or not to proceed to the next call after
4299- catching an exception or recording a failure
4300- return_value: a tuple of results and failures, with result containing
4301- results from the function call under 'stages', and a list
4302- of errors (if any on this level), and elapsed time
4303- running stage, and the name
4304+ """Run a stage of collection, keeping track of results and failures.
4305+
4306+ @param parent_name: name of stage calls are under
4307+ @param calls: list of function call taking no params. must return a tuple
4308+ of results and failures. may raise exceptions
4309+ @param continue_after_error: whether or not to proceed to the next call
4310+ after catching an exception or recording a
4311+ failure
4312+ @return_value: a tuple of results and failures, with result containing
4313+ results from the function call under 'stages', and a list
4314+ of errors (if any on this level), and elapsed time
4315+ running stage, and the name
4316 """
4317 res = {
4318 'name': parent_name,
4319diff --git a/tests/cloud_tests/testcases.yaml b/tests/cloud_tests/testcases.yaml
4320index c22b08e..7183e01 100644
4321--- a/tests/cloud_tests/testcases.yaml
4322+++ b/tests/cloud_tests/testcases.yaml
4323@@ -2,6 +2,7 @@
4324 base_test_data:
4325 script_timeout: 20
4326 enabled: True
4327+ required_features: []
4328 cloud_config: |
4329 #cloud-config
4330 collect_scripts:
4331diff --git a/tests/cloud_tests/testcases/__init__.py b/tests/cloud_tests/testcases/__init__.py
4332index a1d86d4..47217ce 100644
4333--- a/tests/cloud_tests/testcases/__init__.py
4334+++ b/tests/cloud_tests/testcases/__init__.py
4335@@ -1,5 +1,7 @@
4336 # This file is part of cloud-init. See LICENSE file for license information.
4337
4338+"""Main init."""
4339+
4340 import importlib
4341 import inspect
4342 import unittest
4343@@ -9,12 +11,12 @@ from tests.cloud_tests.testcases.base import CloudTestCase as base_test
4344
4345
4346 def discover_tests(test_name):
4347- """
4348- discover tests in test file for 'testname'
4349- return_value: list of test classes
4350+ """Discover tests in test file for 'testname'.
4351+
4352+ @return_value: list of test classes
4353 """
4354 testmod_name = 'tests.cloud_tests.testcases.{}'.format(
4355- config.name_sanatize(test_name))
4356+ config.name_sanitize(test_name))
4357 try:
4358 testmod = importlib.import_module(testmod_name)
4359 except NameError:
4360@@ -26,9 +28,9 @@ def discover_tests(test_name):
4361
4362
4363 def get_suite(test_name, data, conf):
4364- """
4365- get test suite with all tests for 'testname'
4366- return_value: a test suite
4367+ """Get test suite with all tests for 'testname'.
4368+
4369+ @return_value: a test suite
4370 """
4371 suite = unittest.TestSuite()
4372 for test_class in discover_tests(test_name):
4373diff --git a/tests/cloud_tests/testcases/base.py b/tests/cloud_tests/testcases/base.py
4374index 64d5507..bb545ab 100644
4375--- a/tests/cloud_tests/testcases/base.py
4376+++ b/tests/cloud_tests/testcases/base.py
4377@@ -1,61 +1,55 @@
4378 # This file is part of cloud-init. See LICENSE file for license information.
4379
4380-from cloudinit import util as c_util
4381+"""Base test case module."""
4382
4383 import crypt
4384 import json
4385 import unittest
4386
4387+from cloudinit import util as c_util
4388+
4389
4390 class CloudTestCase(unittest.TestCase):
4391- """
4392- base test class for verifiers
4393- """
4394+ """Base test class for verifiers."""
4395+
4396 data = None
4397 conf = None
4398 _cloud_config = None
4399
4400 def shortDescription(self):
4401+ """Prevent nose from using docstrings."""
4402 return None
4403
4404 @property
4405 def cloud_config(self):
4406- """
4407- get the cloud-config used by the test
4408- """
4409+ """Get the cloud-config used by the test."""
4410 if not self._cloud_config:
4411 self._cloud_config = c_util.load_yaml(self.conf)
4412 return self._cloud_config
4413
4414 def get_config_entry(self, name):
4415- """
4416- get a config entry from cloud-config ensuring that it is present
4417- """
4418+ """Get a config entry from cloud-config ensuring that it is present."""
4419 if name not in self.cloud_config:
4420 raise AssertionError('Key "{}" not in cloud config'.format(name))
4421 return self.cloud_config[name]
4422
4423 def get_data_file(self, name):
4424- """
4425- get data file failing test if it is not present
4426- """
4427+ """Get data file failing test if it is not present."""
4428 if name not in self.data:
4429 raise AssertionError('File "{}" missing from collect data'
4430 .format(name))
4431 return self.data[name]
4432
4433 def get_instance_id(self):
4434- """
4435- get recorded instance id
4436- """
4437+ """Get recorded instance id."""
4438 return self.get_data_file('instance-id').strip()
4439
4440 def get_status_data(self, data, version=None):
4441- """
4442- parse result.json and status.json like data files
4443- data: data to load
4444- version: cloud-init output version, defaults to 'v1'
4445- return_value: dict of data or None if missing
4446+ """Parse result.json and status.json like data files.
4447+
4448+ @param data: data to load
4449+ @param version: cloud-init output version, defaults to 'v1'
4450+ @return_value: dict of data or None if missing
4451 """
4452 if not version:
4453 version = 'v1'
4454@@ -63,16 +57,12 @@ class CloudTestCase(unittest.TestCase):
4455 return data.get(version)
4456
4457 def get_datasource(self):
4458- """
4459- get datasource name
4460- """
4461+ """Get datasource name."""
4462 data = self.get_status_data(self.get_data_file('result.json'))
4463 return data.get('datasource')
4464
4465 def test_no_stages_errors(self):
4466- """
4467- ensure that there were no errors in any stage
4468- """
4469+ """Ensure that there were no errors in any stage."""
4470 status = self.get_status_data(self.get_data_file('status.json'))
4471 for stage in ('init', 'init-local', 'modules-config', 'modules-final'):
4472 self.assertIn(stage, status)
4473@@ -84,7 +74,10 @@ class CloudTestCase(unittest.TestCase):
4474
4475
4476 class PasswordListTest(CloudTestCase):
4477+ """Base password test case class."""
4478+
4479 def test_shadow_passwords(self):
4480+ """Test shadow passwords."""
4481 shadow = self.get_data_file('shadow')
4482 users = {}
4483 dupes = []
4484@@ -121,7 +114,7 @@ class PasswordListTest(CloudTestCase):
4485 self.assertNotEqual(users['harry'], users['dick'])
4486
4487 def test_shadow_expected_users(self):
4488- """Test every tom, dick, and harry user in shadow"""
4489+ """Test every tom, dick, and harry user in shadow."""
4490 out = self.get_data_file('shadow')
4491 self.assertIn('tom:', out)
4492 self.assertIn('dick:', out)
4493@@ -130,7 +123,7 @@ class PasswordListTest(CloudTestCase):
4494 self.assertIn('mikey:', out)
4495
4496 def test_sshd_config(self):
4497- """Test sshd config allows passwords"""
4498+ """Test sshd config allows passwords."""
4499 out = self.get_data_file('sshd_config')
4500 self.assertIn('PasswordAuthentication yes', out)
4501
4502diff --git a/tests/cloud_tests/testcases/bugs/__init__.py b/tests/cloud_tests/testcases/bugs/__init__.py
4503index 5251d7c..c6452f9 100644
4504--- a/tests/cloud_tests/testcases/bugs/__init__.py
4505+++ b/tests/cloud_tests/testcases/bugs/__init__.py
4506@@ -1,7 +1,7 @@
4507 # This file is part of cloud-init. See LICENSE file for license information.
4508
4509-"""
4510-Test verifiers for cloud-init bugs
4511+"""Test verifiers for cloud-init bugs.
4512+
4513 See configs/bugs/README.md for more information
4514 """
4515
4516diff --git a/tests/cloud_tests/testcases/bugs/lp1511485.py b/tests/cloud_tests/testcases/bugs/lp1511485.py
4517index ac5ccb4..670d3af 100644
4518--- a/tests/cloud_tests/testcases/bugs/lp1511485.py
4519+++ b/tests/cloud_tests/testcases/bugs/lp1511485.py
4520@@ -1,14 +1,14 @@
4521 # This file is part of cloud-init. See LICENSE file for license information.
4522
4523-"""cloud-init Integration Test Verify Script"""
4524+"""cloud-init Integration Test Verify Script."""
4525 from tests.cloud_tests.testcases import base
4526
4527
4528 class TestLP1511485(base.CloudTestCase):
4529- """Test LP# 1511485"""
4530+ """Test LP# 1511485."""
4531
4532 def test_final_message(self):
4533- """Test final message exists"""
4534+ """Test final message exists."""
4535 out = self.get_data_file('cloud-init-output.log')
4536 self.assertIn('Final message from cloud-config', out)
4537
4538diff --git a/tests/cloud_tests/testcases/bugs/lp1628337.py b/tests/cloud_tests/testcases/bugs/lp1628337.py
4539index af0ffc7..a2c9048 100644
4540--- a/tests/cloud_tests/testcases/bugs/lp1628337.py
4541+++ b/tests/cloud_tests/testcases/bugs/lp1628337.py
4542@@ -1,14 +1,14 @@
4543 # This file is part of cloud-init. See LICENSE file for license information.
4544
4545-"""cloud-init Integration Test Verify Script"""
4546+"""cloud-init Integration Test Verify Script."""
4547 from tests.cloud_tests.testcases import base
4548
4549
4550 class TestLP1628337(base.CloudTestCase):
4551- """Test LP# 1511485"""
4552+ """Test LP# 1511485."""
4553
4554 def test_fetch_indices(self):
4555- """Verify no apt errors"""
4556+ """Verify no apt errors."""
4557 out = self.get_data_file('cloud-init-output.log')
4558 self.assertNotIn('W: Failed to fetch', out)
4559 self.assertNotIn('W: Some index files failed to download. '
4560@@ -16,7 +16,7 @@ class TestLP1628337(base.CloudTestCase):
4561 out)
4562
4563 def test_ntp(self):
4564- """Verify can find ntp and install it"""
4565+ """Verify can find ntp and install it."""
4566 out = self.get_data_file('cloud-init-output.log')
4567 self.assertNotIn('E: Unable to locate package ntp', out)
4568
4569diff --git a/tests/cloud_tests/testcases/examples/__init__.py b/tests/cloud_tests/testcases/examples/__init__.py
4570index b3af7f8..39af88c 100644
4571--- a/tests/cloud_tests/testcases/examples/__init__.py
4572+++ b/tests/cloud_tests/testcases/examples/__init__.py
4573@@ -1,7 +1,7 @@
4574 # This file is part of cloud-init. See LICENSE file for license information.
4575
4576-"""
4577-Test verifiers for cloud-init examples
4578+"""Test verifiers for cloud-init examples.
4579+
4580 See configs/examples/README.md for more information
4581 """
4582
4583diff --git a/tests/cloud_tests/testcases/examples/add_apt_repositories.py b/tests/cloud_tests/testcases/examples/add_apt_repositories.py
4584index 15b8f01..71eede9 100644
4585--- a/tests/cloud_tests/testcases/examples/add_apt_repositories.py
4586+++ b/tests/cloud_tests/testcases/examples/add_apt_repositories.py
4587@@ -1,19 +1,19 @@
4588 # This file is part of cloud-init. See LICENSE file for license information.
4589
4590-"""cloud-init Integration Test Verify Script"""
4591+"""cloud-init Integration Test Verify Script."""
4592 from tests.cloud_tests.testcases import base
4593
4594
4595 class TestAptconfigurePrimary(base.CloudTestCase):
4596- """Example cloud-config test"""
4597+ """Example cloud-config test."""
4598
4599 def test_ubuntu_sources(self):
4600- """Test no default Ubuntu entries exist"""
4601+ """Test no default Ubuntu entries exist."""
4602 out = self.get_data_file('ubuntu.sources.list')
4603 self.assertEqual(0, int(out))
4604
4605 def test_gatech_sources(self):
4606- """Test GaTech entires exist"""
4607+ """Test GaTech entires exist."""
4608 out = self.get_data_file('gatech.sources.list')
4609 self.assertEqual(20, int(out))
4610
4611diff --git a/tests/cloud_tests/testcases/examples/alter_completion_message.py b/tests/cloud_tests/testcases/examples/alter_completion_message.py
4612index b06ad01..b7b5d5e 100644
4613--- a/tests/cloud_tests/testcases/examples/alter_completion_message.py
4614+++ b/tests/cloud_tests/testcases/examples/alter_completion_message.py
4615@@ -1,34 +1,27 @@
4616 # This file is part of cloud-init. See LICENSE file for license information.
4617
4618-"""cloud-init Integration Test Verify Script"""
4619+"""cloud-init Integration Test Verify Script."""
4620 from tests.cloud_tests.testcases import base
4621
4622
4623 class TestFinalMessage(base.CloudTestCase):
4624- """
4625- test cloud init module `cc_final_message`
4626- """
4627+ """Test cloud init module `cc_final_message`."""
4628+
4629 subs_char = '$'
4630
4631 def get_final_message_config(self):
4632- """
4633- get config for final message
4634- """
4635+ """Get config for final message."""
4636 self.assertIn('final_message', self.cloud_config)
4637 return self.cloud_config['final_message']
4638
4639 def get_final_message(self):
4640- """
4641- get final message from log
4642- """
4643+ """Get final message from log."""
4644 out = self.get_data_file('cloud-init-output.log')
4645 lines = len(self.get_final_message_config().splitlines())
4646 return '\n'.join(out.splitlines()[-1 * lines:])
4647
4648 def test_final_message_string(self):
4649- """
4650- ensure final handles regular strings
4651- """
4652+ """Ensure final handles regular strings."""
4653 for actual, config in zip(
4654 self.get_final_message().splitlines(),
4655 self.get_final_message_config().splitlines()):
4656@@ -36,9 +29,7 @@ class TestFinalMessage(base.CloudTestCase):
4657 self.assertEqual(actual, config)
4658
4659 def test_final_message_subs(self):
4660- """
4661- test variable substitution in final message
4662- """
4663+ """Test variable substitution in final message."""
4664 # TODO: add verification of other substitutions
4665 patterns = {'$datasource': self.get_datasource()}
4666 for key, expected in patterns.items():
4667diff --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
4668index 8a4a0db..38540eb 100644
4669--- a/tests/cloud_tests/testcases/examples/configure_instance_trusted_ca_certificates.py
4670+++ b/tests/cloud_tests/testcases/examples/configure_instance_trusted_ca_certificates.py
4671@@ -1,24 +1,24 @@
4672 # This file is part of cloud-init. See LICENSE file for license information.
4673
4674-"""cloud-init Integration Test Verify Script"""
4675+"""cloud-init Integration Test Verify Script."""
4676 from tests.cloud_tests.testcases import base
4677
4678
4679 class TestTrustedCA(base.CloudTestCase):
4680- """Example cloud-config test"""
4681+ """Example cloud-config test."""
4682
4683 def test_cert_count_ca(self):
4684- """Test correct count of CAs in .crt"""
4685+ """Test correct count of CAs in .crt."""
4686 out = self.get_data_file('cert_count_ca')
4687 self.assertIn('7 /etc/ssl/certs/ca-certificates.crt', out)
4688
4689 def test_cert_count_cloudinit(self):
4690- """Test correct count of CAs in .pem"""
4691+ """Test correct count of CAs in .pem."""
4692 out = self.get_data_file('cert_count_cloudinit')
4693 self.assertIn('7 /etc/ssl/certs/cloud-init-ca-certs.pem', out)
4694
4695 def test_cloudinit_certs(self):
4696- """Test text of cert"""
4697+ """Test text of cert."""
4698 out = self.get_data_file('cloudinit_certs')
4699 self.assertIn('-----BEGIN CERTIFICATE-----', out)
4700 self.assertIn('YOUR-ORGS-TRUSTED-CA-CERT-HERE', out)
4701diff --git a/tests/cloud_tests/testcases/examples/configure_instances_ssh_keys.py b/tests/cloud_tests/testcases/examples/configure_instances_ssh_keys.py
4702index 4f65170..691a316 100644
4703--- a/tests/cloud_tests/testcases/examples/configure_instances_ssh_keys.py
4704+++ b/tests/cloud_tests/testcases/examples/configure_instances_ssh_keys.py
4705@@ -1,29 +1,29 @@
4706 # This file is part of cloud-init. See LICENSE file for license information.
4707
4708-"""cloud-init Integration Test Verify Script"""
4709+"""cloud-init Integration Test Verify Script."""
4710 from tests.cloud_tests.testcases import base
4711
4712
4713 class TestSSHKeys(base.CloudTestCase):
4714- """Example cloud-config test"""
4715+ """Example cloud-config test."""
4716
4717 def test_cert_count(self):
4718- """Test cert count"""
4719+ """Test cert count."""
4720 out = self.get_data_file('cert_count')
4721 self.assertEqual(20, int(out))
4722
4723 def test_dsa_public(self):
4724- """Test DSA key has ending"""
4725+ """Test DSA key has ending."""
4726 out = self.get_data_file('dsa_public')
4727 self.assertIn('ZN4XnifuO5krqAybngIy66PMEoQ= smoser@localhost', out)
4728
4729 def test_rsa_public(self):
4730- """Test RSA key has specific ending"""
4731+ """Test RSA key has specific ending."""
4732 out = self.get_data_file('rsa_public')
4733 self.assertIn('PemAWthxHO18QJvWPocKJtlsDNi3 smoser@localhost', out)
4734
4735 def test_auth_keys(self):
4736- """Test authorized keys has specific ending"""
4737+ """Test authorized keys has specific ending."""
4738 out = self.get_data_file('auth_keys')
4739 self.assertIn('QPOt5Q8zWd9qG7PBl9+eiH5qV7NZ mykey@host', out)
4740 self.assertIn('Hj29SCmXp5Kt5/82cD/VN3NtHw== smoser@brickies', out)
4741diff --git a/tests/cloud_tests/testcases/examples/including_user_groups.py b/tests/cloud_tests/testcases/examples/including_user_groups.py
4742index e573232..67af527 100644
4743--- a/tests/cloud_tests/testcases/examples/including_user_groups.py
4744+++ b/tests/cloud_tests/testcases/examples/including_user_groups.py
4745@@ -1,42 +1,42 @@
4746 # This file is part of cloud-init. See LICENSE file for license information.
4747
4748-"""cloud-init Integration Test Verify Script"""
4749+"""cloud-init Integration Test Verify Script."""
4750 from tests.cloud_tests.testcases import base
4751
4752
4753 class TestUserGroups(base.CloudTestCase):
4754- """Example cloud-config test"""
4755+ """Example cloud-config test."""
4756
4757 def test_group_ubuntu(self):
4758- """Test ubuntu group exists"""
4759+ """Test ubuntu group exists."""
4760 out = self.get_data_file('group_ubuntu')
4761 self.assertRegex(out, r'ubuntu:x:[0-9]{4}:')
4762
4763 def test_group_cloud_users(self):
4764- """Test cloud users group exists"""
4765+ """Test cloud users group exists."""
4766 out = self.get_data_file('group_cloud_users')
4767 self.assertRegex(out, r'cloud-users:x:[0-9]{4}:barfoo')
4768
4769 def test_user_ubuntu(self):
4770- """Test ubuntu user exists"""
4771+ """Test ubuntu user exists."""
4772 out = self.get_data_file('user_ubuntu')
4773 self.assertRegex(
4774 out, r'ubuntu:x:[0-9]{4}:[0-9]{4}:Ubuntu:/home/ubuntu:/bin/bash')
4775
4776 def test_user_foobar(self):
4777- """Test foobar user exists"""
4778+ """Test foobar user exists."""
4779 out = self.get_data_file('user_foobar')
4780 self.assertRegex(
4781 out, r'foobar:x:[0-9]{4}:[0-9]{4}:Foo B. Bar:/home/foobar:')
4782
4783 def test_user_barfoo(self):
4784- """Test barfoo user exists"""
4785+ """Test barfoo user exists."""
4786 out = self.get_data_file('user_barfoo')
4787 self.assertRegex(
4788 out, r'barfoo:x:[0-9]{4}:[0-9]{4}:Bar B. Foo:/home/barfoo:')
4789
4790 def test_user_cloudy(self):
4791- """Test cloudy user exists"""
4792+ """Test cloudy user exists."""
4793 out = self.get_data_file('user_cloudy')
4794 self.assertRegex(out, r'cloudy:x:[0-9]{3,4}:')
4795
4796diff --git a/tests/cloud_tests/testcases/examples/install_arbitrary_packages.py b/tests/cloud_tests/testcases/examples/install_arbitrary_packages.py
4797index 660d1aa..df13384 100644
4798--- a/tests/cloud_tests/testcases/examples/install_arbitrary_packages.py
4799+++ b/tests/cloud_tests/testcases/examples/install_arbitrary_packages.py
4800@@ -1,19 +1,19 @@
4801 # This file is part of cloud-init. See LICENSE file for license information.
4802
4803-"""cloud-init Integration Test Verify Script"""
4804+"""cloud-init Integration Test Verify Script."""
4805 from tests.cloud_tests.testcases import base
4806
4807
4808 class TestInstall(base.CloudTestCase):
4809- """Example cloud-config test"""
4810+ """Example cloud-config test."""
4811
4812 def test_htop(self):
4813- """Verify htop installed"""
4814+ """Verify htop installed."""
4815 out = self.get_data_file('htop')
4816 self.assertEqual(1, int(out))
4817
4818 def test_tree(self):
4819- """Verify tree installed"""
4820+ """Verify tree installed."""
4821 out = self.get_data_file('treeutils')
4822 self.assertEqual(1, int(out))
4823
4824diff --git a/tests/cloud_tests/testcases/examples/install_run_chef_recipes.py b/tests/cloud_tests/testcases/examples/install_run_chef_recipes.py
4825index b36486f..4ec26b8 100644
4826--- a/tests/cloud_tests/testcases/examples/install_run_chef_recipes.py
4827+++ b/tests/cloud_tests/testcases/examples/install_run_chef_recipes.py
4828@@ -1,14 +1,14 @@
4829 # This file is part of cloud-init. See LICENSE file for license information.
4830
4831-"""cloud-init Integration Test Verify Script"""
4832+"""cloud-init Integration Test Verify Script."""
4833 from tests.cloud_tests.testcases import base
4834
4835
4836 class TestChefExample(base.CloudTestCase):
4837- """Test chef module"""
4838+ """Test chef module."""
4839
4840 def test_chef_basic(self):
4841- """Test chef installed"""
4842+ """Test chef installed."""
4843 out = self.get_data_file('chef_installed')
4844 self.assertIn('install ok', out)
4845
4846diff --git a/tests/cloud_tests/testcases/examples/run_apt_upgrade.py b/tests/cloud_tests/testcases/examples/run_apt_upgrade.py
4847index 4c04d31..744e49c 100644
4848--- a/tests/cloud_tests/testcases/examples/run_apt_upgrade.py
4849+++ b/tests/cloud_tests/testcases/examples/run_apt_upgrade.py
4850@@ -1,14 +1,14 @@
4851 # This file is part of cloud-init. See LICENSE file for license information.
4852
4853-"""cloud-init Integration Test Verify Script"""
4854+"""cloud-init Integration Test Verify Script."""
4855 from tests.cloud_tests.testcases import base
4856
4857
4858 class TestUpgrade(base.CloudTestCase):
4859- """Example cloud-config test"""
4860+ """Example cloud-config test."""
4861
4862 def test_upgrade(self):
4863- """Test upgrade exists in apt history"""
4864+ """Test upgrade exists in apt history."""
4865 out = self.get_data_file('cloud-init.log')
4866 self.assertIn(
4867 '[CLOUDINIT] util.py[DEBUG]: apt-upgrade '
4868diff --git a/tests/cloud_tests/testcases/examples/run_commands.py b/tests/cloud_tests/testcases/examples/run_commands.py
4869index 0be21d0..01d5d4f 100644
4870--- a/tests/cloud_tests/testcases/examples/run_commands.py
4871+++ b/tests/cloud_tests/testcases/examples/run_commands.py
4872@@ -1,14 +1,14 @@
4873 # This file is part of cloud-init. See LICENSE file for license information.
4874
4875-"""cloud-init Integration Test Verify Script"""
4876+"""cloud-init Integration Test Verify Script."""
4877 from tests.cloud_tests.testcases import base
4878
4879
4880 class TestRunCmd(base.CloudTestCase):
4881- """Example cloud-config test"""
4882+ """Example cloud-config test."""
4883
4884 def test_run_cmd(self):
4885- """Test run command worked"""
4886+ """Test run command worked."""
4887 out = self.get_data_file('run_cmd')
4888 self.assertIn('cloud-init run cmd test', out)
4889
4890diff --git a/tests/cloud_tests/testcases/examples/run_commands_first_boot.py b/tests/cloud_tests/testcases/examples/run_commands_first_boot.py
4891index baa2313..3f3d8f8 100644
4892--- a/tests/cloud_tests/testcases/examples/run_commands_first_boot.py
4893+++ b/tests/cloud_tests/testcases/examples/run_commands_first_boot.py
4894@@ -1,14 +1,14 @@
4895 # This file is part of cloud-init. See LICENSE file for license information.
4896
4897-"""cloud-init Integration Test Verify Script"""
4898+"""cloud-init Integration Test Verify Script."""
4899 from tests.cloud_tests.testcases import base
4900
4901
4902 class TestBootCmd(base.CloudTestCase):
4903- """Example cloud-config test"""
4904+ """Example cloud-config test."""
4905
4906 def test_bootcmd_host(self):
4907- """Test boot command worked"""
4908+ """Test boot command worked."""
4909 out = self.get_data_file('hosts')
4910 self.assertIn('192.168.1.130 us.archive.ubuntu.com', out)
4911
4912diff --git a/tests/cloud_tests/testcases/examples/writing_out_arbitrary_files.py b/tests/cloud_tests/testcases/examples/writing_out_arbitrary_files.py
4913index 97dfeec..7bd520f 100644
4914--- a/tests/cloud_tests/testcases/examples/writing_out_arbitrary_files.py
4915+++ b/tests/cloud_tests/testcases/examples/writing_out_arbitrary_files.py
4916@@ -1,29 +1,29 @@
4917 # This file is part of cloud-init. See LICENSE file for license information.
4918
4919-"""cloud-init Integration Test Verify Script"""
4920+"""cloud-init Integration Test Verify Script."""
4921 from tests.cloud_tests.testcases import base
4922
4923
4924 class TestWriteFiles(base.CloudTestCase):
4925- """Example cloud-config test"""
4926+ """Example cloud-config test."""
4927
4928 def test_b64(self):
4929- """Test b64 encoded file reads as ascii"""
4930+ """Test b64 encoded file reads as ascii."""
4931 out = self.get_data_file('file_b64')
4932 self.assertIn('ASCII text', out)
4933
4934 def test_binary(self):
4935- """Test binary file reads as executable"""
4936+ """Test binary file reads as executable."""
4937 out = self.get_data_file('file_binary')
4938 self.assertIn('ELF 64-bit LSB executable, x86-64, version 1', out)
4939
4940 def test_gzip(self):
4941- """Test gzip file shows up as a shell script"""
4942+ """Test gzip file shows up as a shell script."""
4943 out = self.get_data_file('file_gzip')
4944 self.assertIn('POSIX shell script, ASCII text executable', out)
4945
4946 def test_text(self):
4947- """Test text shows up as ASCII text"""
4948+ """Test text shows up as ASCII text."""
4949 out = self.get_data_file('file_text')
4950 self.assertIn('ASCII text', out)
4951
4952diff --git a/tests/cloud_tests/testcases/main/__init__.py b/tests/cloud_tests/testcases/main/__init__.py
4953index 5888990..0a59263 100644
4954--- a/tests/cloud_tests/testcases/main/__init__.py
4955+++ b/tests/cloud_tests/testcases/main/__init__.py
4956@@ -1,7 +1,7 @@
4957 # This file is part of cloud-init. See LICENSE file for license information.
4958
4959-"""
4960-Test verifiers for cloud-init main features
4961+"""Test verifiers for cloud-init main features.
4962+
4963 See configs/main/README.md for more information
4964 """
4965
4966diff --git a/tests/cloud_tests/testcases/main/command_output_simple.py b/tests/cloud_tests/testcases/main/command_output_simple.py
4967index c0461a0..fe4c767 100644
4968--- a/tests/cloud_tests/testcases/main/command_output_simple.py
4969+++ b/tests/cloud_tests/testcases/main/command_output_simple.py
4970@@ -1,17 +1,14 @@
4971 # This file is part of cloud-init. See LICENSE file for license information.
4972
4973+"""cloud-init Integration Test Verify Script."""
4974 from tests.cloud_tests.testcases import base
4975
4976
4977 class TestCommandOutputSimple(base.CloudTestCase):
4978- """
4979- test functionality of simple output redirection
4980- """
4981+ """Test functionality of simple output redirection."""
4982
4983 def test_output_file(self):
4984- """
4985- ensure that the output file is not empty and has all stages
4986- """
4987+ """Ensure that the output file is not empty and has all stages."""
4988 data = self.get_data_file('cloud-init-test-output')
4989 self.assertNotEqual(len(data), 0, "specified log empty")
4990 self.assertEqual(self.get_config_entry('final_message'),
4991diff --git a/tests/cloud_tests/testcases/modules/__init__.py b/tests/cloud_tests/testcases/modules/__init__.py
4992index 9560fb2..6ab8114 100644
4993--- a/tests/cloud_tests/testcases/modules/__init__.py
4994+++ b/tests/cloud_tests/testcases/modules/__init__.py
4995@@ -1,7 +1,7 @@
4996 # This file is part of cloud-init. See LICENSE file for license information.
4997
4998-"""
4999-Test verifiers for cloud-init cc modules
5000+"""Test verifiers for cloud-init cc modules.
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches