Merge ~powersj/cloud-init:integration-test-revamp into cloud-init:master
- Git
- lp:~powersj/cloud-init
- integration-test-revamp
- Merge into master
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) |
Related bugs: |
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://
$ tox -e citest -- tree_run -v -n artful -t tests/cloud_
$ tox -e citest -- tree_run -v -n stretch --preserve-data
$ tox -e citest -- tree_run -v -n xenial --preserve-data --data-
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
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!
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:e96ce095d2b
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild:
https:/
Scott Moser (smoser) wrote : | # |
The commit message needs updating for sure.
You mention tox 'citest', but do not make any changes to it.
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.
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:5a8038ee981
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild:
https:/
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:a7f7c6e920a
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild:
https:/
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:78093d78281
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild:
https:/
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:0
https:/
Executed test runs:
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
Click here to trigger a rebuild:
https:/
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
1 | diff --git a/doc/rtd/topics/tests.rst b/doc/rtd/topics/tests.rst |
2 | index 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. |
757 | diff --git a/tests/cloud_tests/__init__.py b/tests/cloud_tests/__init__.py |
758 | index 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( |
783 | diff --git a/tests/cloud_tests/__main__.py b/tests/cloud_tests/__main__.py |
784 | index 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 | |
869 | diff --git a/tests/cloud_tests/args.py b/tests/cloud_tests/args.py |
870 | index 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 | } |
1125 | diff --git a/tests/cloud_tests/bddeb.py b/tests/cloud_tests/bddeb.py |
1126 | new file mode 100644 |
1127 | index 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 |
1249 | diff --git a/tests/cloud_tests/collect.py b/tests/cloud_tests/collect.py |
1250 | index 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) |
1438 | diff --git a/tests/cloud_tests/config.py b/tests/cloud_tests/config.py |
1439 | index 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 |
1643 | diff --git a/tests/cloud_tests/configs/bugs/lp1628337.yaml b/tests/cloud_tests/configs/bugs/lp1628337.yaml |
1644 | index 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: |
1657 | diff --git a/tests/cloud_tests/configs/examples/add_apt_repositories.yaml b/tests/cloud_tests/configs/examples/add_apt_repositories.yaml |
1658 | index 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: |
1670 | diff --git a/tests/cloud_tests/configs/modules/apt_configure_conf.yaml b/tests/cloud_tests/configs/modules/apt_configure_conf.yaml |
1671 | index 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: |
1683 | diff --git a/tests/cloud_tests/configs/modules/apt_configure_disable_suites.yaml b/tests/cloud_tests/configs/modules/apt_configure_disable_suites.yaml |
1684 | index 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: |
1697 | diff --git a/tests/cloud_tests/configs/modules/apt_configure_primary.yaml b/tests/cloud_tests/configs/modules/apt_configure_primary.yaml |
1698 | index 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 |
1720 | diff --git a/tests/cloud_tests/configs/modules/apt_configure_proxy.yaml b/tests/cloud_tests/configs/modules/apt_configure_proxy.yaml |
1721 | index 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: |
1733 | diff --git a/tests/cloud_tests/configs/modules/apt_configure_security.yaml b/tests/cloud_tests/configs/modules/apt_configure_security.yaml |
1734 | index 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: |
1747 | diff --git a/tests/cloud_tests/configs/modules/apt_configure_sources_key.yaml b/tests/cloud_tests/configs/modules/apt_configure_sources_key.yaml |
1748 | index 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: |
1761 | diff --git a/tests/cloud_tests/configs/modules/apt_configure_sources_keyserver.yaml b/tests/cloud_tests/configs/modules/apt_configure_sources_keyserver.yaml |
1762 | index 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: |
1775 | diff --git a/tests/cloud_tests/configs/modules/apt_configure_sources_list.yaml b/tests/cloud_tests/configs/modules/apt_configure_sources_list.yaml |
1776 | index 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: |
1789 | diff --git a/tests/cloud_tests/configs/modules/apt_configure_sources_ppa.yaml b/tests/cloud_tests/configs/modules/apt_configure_sources_ppa.yaml |
1790 | index 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 |
1815 | diff --git a/tests/cloud_tests/configs/modules/apt_pipelining_disable.yaml b/tests/cloud_tests/configs/modules/apt_pipelining_disable.yaml |
1816 | index 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: |
1828 | diff --git a/tests/cloud_tests/configs/modules/apt_pipelining_os.yaml b/tests/cloud_tests/configs/modules/apt_pipelining_os.yaml |
1829 | index 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: |
1841 | diff --git a/tests/cloud_tests/configs/modules/byobu.yaml b/tests/cloud_tests/configs/modules/byobu.yaml |
1842 | index 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 |
1854 | diff --git a/tests/cloud_tests/configs/modules/keys_to_console.yaml b/tests/cloud_tests/configs/modules/keys_to_console.yaml |
1855 | index 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] |
1867 | diff --git a/tests/cloud_tests/configs/modules/landscape.yaml b/tests/cloud_tests/configs/modules/landscape.yaml |
1868 | index 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: |
1880 | diff --git a/tests/cloud_tests/configs/modules/locale.yaml b/tests/cloud_tests/configs/modules/locale.yaml |
1881 | index 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 |
1894 | diff --git a/tests/cloud_tests/configs/modules/lxd_bridge.yaml b/tests/cloud_tests/configs/modules/lxd_bridge.yaml |
1895 | index 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: |
1907 | diff --git a/tests/cloud_tests/configs/modules/lxd_dir.yaml b/tests/cloud_tests/configs/modules/lxd_dir.yaml |
1908 | index 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: |
1920 | diff --git a/tests/cloud_tests/configs/modules/ntp.yaml b/tests/cloud_tests/configs/modules/ntp.yaml |
1921 | index 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 |
1948 | diff --git a/tests/cloud_tests/configs/modules/ntp_pools.yaml b/tests/cloud_tests/configs/modules/ntp_pools.yaml |
1949 | index 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: |
1969 | diff --git a/tests/cloud_tests/configs/modules/ntp_servers.yaml b/tests/cloud_tests/configs/modules/ntp_servers.yaml |
1970 | index 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: |
1987 | diff --git a/tests/cloud_tests/configs/modules/package_update_upgrade_install.yaml b/tests/cloud_tests/configs/modules/package_update_upgrade_install.yaml |
1988 | index 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: |
2009 | diff --git a/tests/cloud_tests/configs/modules/set_hostname.yaml b/tests/cloud_tests/configs/modules/set_hostname.yaml |
2010 | index 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 |
2022 | diff --git a/tests/cloud_tests/configs/modules/set_hostname_fqdn.yaml b/tests/cloud_tests/configs/modules/set_hostname_fqdn.yaml |
2023 | index 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 |
2035 | diff --git a/tests/cloud_tests/configs/modules/set_password.yaml b/tests/cloud_tests/configs/modules/set_password.yaml |
2036 | index 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 |
2048 | diff --git a/tests/cloud_tests/configs/modules/set_password_expire.yaml b/tests/cloud_tests/configs/modules/set_password_expire.yaml |
2049 | index 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 } |
2061 | diff --git a/tests/cloud_tests/configs/modules/snappy.yaml b/tests/cloud_tests/configs/modules/snappy.yaml |
2062 | index 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: |
2074 | diff --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 |
2075 | index 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: [] |
2087 | diff --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 |
2088 | index 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: |
2103 | diff --git a/tests/cloud_tests/configs/modules/ssh_import_id.yaml b/tests/cloud_tests/configs/modules/ssh_import_id.yaml |
2104 | index 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: |
2117 | diff --git a/tests/cloud_tests/configs/modules/ssh_keys_generate.yaml b/tests/cloud_tests/configs/modules/ssh_keys_generate.yaml |
2118 | index 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: |
2130 | diff --git a/tests/cloud_tests/configs/modules/ssh_keys_provided.yaml b/tests/cloud_tests/configs/modules/ssh_keys_provided.yaml |
2131 | index 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 |
2144 | diff --git a/tests/cloud_tests/configs/modules/timezone.yaml b/tests/cloud_tests/configs/modules/timezone.yaml |
2145 | index 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 |
2157 | diff --git a/tests/cloud_tests/configs/modules/user_groups.yaml b/tests/cloud_tests/configs/modules/user_groups.yaml |
2158 | index 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 |
2170 | diff --git a/tests/cloud_tests/configs/modules/write_files.yaml b/tests/cloud_tests/configs/modules/write_files.yaml |
2171 | index 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: |
2185 | diff --git a/tests/cloud_tests/images/__init__.py b/tests/cloud_tests/images/__init__.py |
2186 | index 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 |
2204 | diff --git a/tests/cloud_tests/images/base.py b/tests/cloud_tests/images/base.py |
2205 | index 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 |
2310 | diff --git a/tests/cloud_tests/images/lxd.py b/tests/cloud_tests/images/lxd.py |
2311 | index 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 |
2543 | diff --git a/tests/cloud_tests/instances/__init__.py b/tests/cloud_tests/instances/__init__.py |
2544 | index 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 |
2561 | diff --git a/tests/cloud_tests/instances/base.py b/tests/cloud_tests/instances/base.py |
2562 | index 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 |
2781 | diff --git a/tests/cloud_tests/instances/lxd.py b/tests/cloud_tests/instances/lxd.py |
2782 | index 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) |
2977 | diff --git a/tests/cloud_tests/manage.py b/tests/cloud_tests/manage.py |
2978 | index 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) |
3045 | diff --git a/tests/cloud_tests/platforms.yaml b/tests/cloud_tests/platforms.yaml |
3046 | index 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 | |
3106 | diff --git a/tests/cloud_tests/platforms/__init__.py b/tests/cloud_tests/platforms/__init__.py |
3107 | index 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)) |
3129 | diff --git a/tests/cloud_tests/platforms/base.py b/tests/cloud_tests/platforms/base.py |
3130 | index 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 |
3196 | diff --git a/tests/cloud_tests/platforms/lxd.py b/tests/cloud_tests/platforms/lxd.py |
3197 | index 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 |
3343 | diff --git a/tests/cloud_tests/releases.yaml b/tests/cloud_tests/releases.yaml |
3344 | index 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 |
3677 | diff --git a/tests/cloud_tests/run_funcs.py b/tests/cloud_tests/run_funcs.py |
3678 | new file mode 100644 |
3679 | index 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 |
3758 | diff --git a/tests/cloud_tests/setup_image.py b/tests/cloud_tests/setup_image.py |
3759 | index 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 |
4059 | diff --git a/tests/cloud_tests/snapshots/__init__.py b/tests/cloud_tests/snapshots/__init__.py |
4060 | index 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 |
4077 | diff --git a/tests/cloud_tests/snapshots/base.py b/tests/cloud_tests/snapshots/base.py |
4078 | index 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 |
4148 | diff --git a/tests/cloud_tests/snapshots/lxd.py b/tests/cloud_tests/snapshots/lxd.py |
4149 | index 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 | |
4229 | diff --git a/tests/cloud_tests/stage.py b/tests/cloud_tests/stage.py |
4230 | index 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, |
4319 | diff --git a/tests/cloud_tests/testcases.yaml b/tests/cloud_tests/testcases.yaml |
4320 | index 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: |
4331 | diff --git a/tests/cloud_tests/testcases/__init__.py b/tests/cloud_tests/testcases/__init__.py |
4332 | index 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): |
4373 | diff --git a/tests/cloud_tests/testcases/base.py b/tests/cloud_tests/testcases/base.py |
4374 | index 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 | |
4502 | diff --git a/tests/cloud_tests/testcases/bugs/__init__.py b/tests/cloud_tests/testcases/bugs/__init__.py |
4503 | index 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 | |
4516 | diff --git a/tests/cloud_tests/testcases/bugs/lp1511485.py b/tests/cloud_tests/testcases/bugs/lp1511485.py |
4517 | index 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 | |
4538 | diff --git a/tests/cloud_tests/testcases/bugs/lp1628337.py b/tests/cloud_tests/testcases/bugs/lp1628337.py |
4539 | index 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 | |
4569 | diff --git a/tests/cloud_tests/testcases/examples/__init__.py b/tests/cloud_tests/testcases/examples/__init__.py |
4570 | index 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 | |
4583 | diff --git a/tests/cloud_tests/testcases/examples/add_apt_repositories.py b/tests/cloud_tests/testcases/examples/add_apt_repositories.py |
4584 | index 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 | |
4611 | diff --git a/tests/cloud_tests/testcases/examples/alter_completion_message.py b/tests/cloud_tests/testcases/examples/alter_completion_message.py |
4612 | index 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(): |
4667 | diff --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 |
4668 | index 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) |
4701 | diff --git a/tests/cloud_tests/testcases/examples/configure_instances_ssh_keys.py b/tests/cloud_tests/testcases/examples/configure_instances_ssh_keys.py |
4702 | index 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) |
4741 | diff --git a/tests/cloud_tests/testcases/examples/including_user_groups.py b/tests/cloud_tests/testcases/examples/including_user_groups.py |
4742 | index 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 | |
4796 | diff --git a/tests/cloud_tests/testcases/examples/install_arbitrary_packages.py b/tests/cloud_tests/testcases/examples/install_arbitrary_packages.py |
4797 | index 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 | |
4824 | diff --git a/tests/cloud_tests/testcases/examples/install_run_chef_recipes.py b/tests/cloud_tests/testcases/examples/install_run_chef_recipes.py |
4825 | index 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 | |
4846 | diff --git a/tests/cloud_tests/testcases/examples/run_apt_upgrade.py b/tests/cloud_tests/testcases/examples/run_apt_upgrade.py |
4847 | index 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 ' |
4868 | diff --git a/tests/cloud_tests/testcases/examples/run_commands.py b/tests/cloud_tests/testcases/examples/run_commands.py |
4869 | index 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 | |
4890 | diff --git a/tests/cloud_tests/testcases/examples/run_commands_first_boot.py b/tests/cloud_tests/testcases/examples/run_commands_first_boot.py |
4891 | index 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 | |
4912 | diff --git a/tests/cloud_tests/testcases/examples/writing_out_arbitrary_files.py b/tests/cloud_tests/testcases/examples/writing_out_arbitrary_files.py |
4913 | index 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 | |
4952 | diff --git a/tests/cloud_tests/testcases/main/__init__.py b/tests/cloud_tests/testcases/main/__init__.py |
4953 | index 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 | |
4966 | diff --git a/tests/cloud_tests/testcases/main/command_output_simple.py b/tests/cloud_tests/testcases/main/command_output_simple.py |
4967 | index 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'), |
4991 | diff --git a/tests/cloud_tests/testcases/modules/__init__.py b/tests/cloud_tests/testcases/modules/__init__.py |
4992 | index 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. |
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/config. py:
tests/cloud_
- s/[Ss]anatize/ [Ss]anitize/
tests/cloud_ tests/setup_ image.py value/@ return_ value/
- s/#return_
tests/cloud_ tests/util. py: Utilities/ in module docstring
- s/Utilies/
- 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.