Merge ~wesley-wiedenmeier/cloud-init:integration-testing-distro-features into cloud-init:master
- Git
- lp:~wesley-wiedenmeier/cloud-init
- integration-testing-distro-features
- Merge into master
Status: | Merged |
---|---|
Merged at revision: | 76d58265e34851b78e952a7f275340863c90a9f5 |
Proposed branch: | ~wesley-wiedenmeier/cloud-init:integration-testing-distro-features |
Merge into: | cloud-init:master |
Diff against target: |
2709 lines (+1349/-276) 54 files modified
doc/rtd/topics/tests.rst (+249/-1) tests/cloud_tests/args.py (+26/-8) tests/cloud_tests/collect.py (+25/-14) tests/cloud_tests/config.py (+85/-12) 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 (+6/-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/base.py (+24/-9) tests/cloud_tests/images/lxd.py (+119/-18) tests/cloud_tests/instances/base.py (+67/-29) tests/cloud_tests/instances/lxd.py (+50/-21) tests/cloud_tests/platforms.yaml (+49/-1) tests/cloud_tests/platforms/base.py (+1/-24) tests/cloud_tests/platforms/lxd.py (+30/-16) tests/cloud_tests/releases.yaml (+269/-67) tests/cloud_tests/setup_image.py (+83/-42) tests/cloud_tests/snapshots/base.py (+7/-1) tests/cloud_tests/snapshots/lxd.py (+12/-6) tests/cloud_tests/testcases.yaml (+1/-0) tests/cloud_tests/util.py (+123/-6) tox.ini (+1/-1) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Server Team CI bot | continuous-integration | Approve | |
cloud-init Commiters | Pending | ||
Review via email: mp+321029@code.launchpad.net |
Commit message
Description of the change
Integration Testing: Improvments to testing on alternate distros
- Allow images to override user data settings
- image config option 'user_data_
attributes to update a testcase's user data with before
launching an image
- workaround for (LP #1575779)
- should only be used if this is the only way to get an image
working
- 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
- Add required features to testcase configs
- skip testcases with distro compatibility issues
- allow test suite to be run in full on other distros
- many testcases with compatibility issues can be updated
to allow them to work across distros
- Updated documentation
- updated documentation for testsuite config
- explain how config is merged
- describe new image config format
- explain how to configure an image's system_ready_script
- add documentation on how feature flags work
- add documentation on how error handling works
- add documentation on setup_image options
Wesley Wiedenmeier (wesley-wiedenmeier) wrote : | # |
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:50aa692d246
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild:
https:/
- 46d291a... by Wesley Wiedenmeier
-
Integration Testing: add support for overriding distro feature flags
This provides a mechanism to override feature flag settings for a test run via
the command line option '--feature-override <feature flag name>=<true/false> '.
In some situations tests may depend on external resources that are not always
available to the test suite. A feature flag representing that external resource
can be set to false in release config by default, and enabled via the command
line only when the user has ensured that that resource is available.
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:a01ef1506e9
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild:
https:/
- 685ac9c... by Wesley Wiedenmeier
-
Integration Testing: improve lxd snapshot creation
Avoid a potential race between cloud-init starting and files that need to be
removed while creating a snapshot. This race condition only caused errors when
testing on trusty, but is there on any release. It is fixed by waiting for
cloud-init to start fully before running the boot_clean_script, as this
guarantees that files that should be cleaned will not be written after the
boot_clean_script has run.
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:13bdee594be
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild:
https:/
Scott Moser (smoser) wrote : | # |
I'm going to mark this 'work in progress'. Josh has integrated this branch and others into his merge proposal at https:/
Thanks for your work, Wesley!
Scott Moser (smoser) wrote : | # |
Hi.
I've marked this 'merged' as I think it is now in trunk under 76d58265e34851b
If you disagree, please feel free to re-open.
Preview Diff
1 | diff --git a/doc/rtd/topics/tests.rst b/doc/rtd/topics/tests.rst |
2 | index 0663811..b6b8da1 100644 |
3 | --- a/doc/rtd/topics/tests.rst |
4 | +++ b/doc/rtd/topics/tests.rst |
5 | @@ -47,7 +47,7 @@ The test configuration is a YAML file such as *ntp_server.yaml* below: |
6 | cat /etc/ntp.conf | grep '^server' |
7 | |
8 | |
9 | -There are two keys, 1 required and 1 optional, in the YAML file: |
10 | +There are several keys, 1 required and some optional, in the YAML file: |
11 | |
12 | 1. The required key is ``cloud_config``. This should be a string of valid |
13 | YAML that is exactly what would normally be placed in a cloud-config file, |
14 | @@ -61,6 +61,11 @@ There are two keys, 1 required and 1 optional, in the YAML file: |
15 | reported. The name of the sub-key is important. The sub-key is used by |
16 | the verification script to recall the output of the commands ran. |
17 | |
18 | +3. The ``required_features`` key may be used to specify a list of features |
19 | + flags that an image must have to be able to run the testcase. For example, |
20 | + if a testcase relies on an image supporting apt, then the config for the |
21 | + testcase should include ``required_features: [ apt ]``. |
22 | + |
23 | Default Collect Scripts |
24 | ----------------------- |
25 | |
26 | @@ -154,6 +159,7 @@ Development Checklist |
27 | * Optionally, commands to capture additional output |
28 | * Valid YAML |
29 | * Placed in the appropriate sub-folder in the configs directory |
30 | + * Any image features required for the test are specified |
31 | * Verification File |
32 | * Named 'your_test_here.py' |
33 | * Valid unit tests validating output collected |
34 | @@ -252,6 +258,248 @@ configuration users can run the integration tests via tox: |
35 | Users need to invoke the citest enviornment and then pass any additional |
36 | arguments. |
37 | |
38 | +Setup Image |
39 | +----------- |
40 | +The ``run`` and ``collect`` commands have many options to setup the image |
41 | +before running tests in addition to installing a deb in the target. Any |
42 | +combination of the following can be used: |
43 | + |
44 | +* ``--deb``: install a deb into the image |
45 | +* ``--rpm``: install a rpm into the image |
46 | +* ``--repo``: enable a repository and update cloud-init afterwards |
47 | +* ``--ppa``: enable a ppa and update cloud-init afterwards |
48 | +* ``--upgrade``: upgrade cloud-init from repos |
49 | +* ``--upgrade-full``: run a full system upgrade |
50 | +* ``--script``: execute a script in the image. this can perform any setup |
51 | + required that is not covered by the other options |
52 | + |
53 | +Configuring the Test Suite |
54 | +========================== |
55 | + |
56 | +Most of the behavior of the test suite is configurable through several yaml |
57 | +files. These control the behavior of the test suite's platforms, images, and |
58 | +tests. The main config files for platforms, images and testcases are |
59 | +``platforms.yaml``, ``releases.yaml`` and ``testcases.yaml``. |
60 | + |
61 | +Config handling |
62 | +--------------- |
63 | +All configurable parts of the test suite use a defaults + overrides system for |
64 | +managing config entries. All base config items are dictionaries. |
65 | + |
66 | +Merging is done on a key by key basis, with all keys in the default and |
67 | +overrides represented in the final result. If a key exists both in |
68 | +the defaults and the overrides, then behavior depends on the type of data the |
69 | +key refers to. If it is atomic data or a list, then the overrides will replace |
70 | +the default. If the data is a dictionary then the value will be the result of |
71 | +merging that dictionary from the default config and that dictionary from the |
72 | +overrides. |
73 | + |
74 | +Merging is done using the function ``tests.cloud_tests.config.merge_config``, |
75 | +which can be examined for more detail on config merging behavior. |
76 | + |
77 | +The following demonstrates merge behavior: |
78 | + |
79 | +.. code-block:: yaml |
80 | + |
81 | + defaults: |
82 | + list_item: |
83 | + - list_entry_1 |
84 | + - list_entry_2 |
85 | + int_item_1: 123 |
86 | + int_item_2: 234 |
87 | + dict_item: |
88 | + subkey_1: 1 |
89 | + subkey_2: 2 |
90 | + subkey_dict: |
91 | + subsubkey_1: a |
92 | + subsubkey_2: b |
93 | + |
94 | + overrides: |
95 | + list_item: |
96 | + - overridden_list_entry |
97 | + int_item_1: 0 |
98 | + dict_item: |
99 | + subkey_2: false |
100 | + subkey_dict: |
101 | + subsubkey_2: 'new value' |
102 | + |
103 | + result: |
104 | + list_item: |
105 | + - overridden_list_entry |
106 | + int_item_1: 0 |
107 | + int_item_2: 234 |
108 | + dict_item: |
109 | + subkey_1: 1 |
110 | + subkey_2: false |
111 | + subkey_dict: |
112 | + subsubkey_1: a |
113 | + subsubkey_2: 'new value' |
114 | + |
115 | + |
116 | +Image Config Structure |
117 | +---------------------- |
118 | +Image configuration is handled in ``releases.yaml``. The image configuration |
119 | +controls how platforms locate and acquire images, how the platforms should |
120 | +interact with the images, how platforms should detect when an image has fully |
121 | +booted, any options that are required to set the image up, and features that |
122 | +the image supports. |
123 | + |
124 | +Since settings for locating an image and interacting with it differ from |
125 | +platform to platform, there are 4 levels of settings available for images on |
126 | +top of the default image settings. The structure of the image config file is: |
127 | + |
128 | +.. code-block:: yaml |
129 | + |
130 | + default_release_config: |
131 | + default: |
132 | + ... |
133 | + <platform>: |
134 | + ... |
135 | + <platform>: |
136 | + ... |
137 | + |
138 | + releases: |
139 | + <release name>: |
140 | + <default>: |
141 | + ... |
142 | + <platform>: |
143 | + ... |
144 | + <platform>: |
145 | + ... |
146 | + |
147 | + |
148 | +The base config is created from the overall defaults and the overrides for the |
149 | +platform. The overrides are created from the default config for the image and |
150 | +the platform specific overrides for the image. |
151 | + |
152 | +Image Config for System Boot |
153 | +---------------------------- |
154 | +The test suite must be able to test if a system has fully booted and if |
155 | +cloud-init has finished running, so that running collect scripts does not race |
156 | +against the target image booting. This is done using the |
157 | +``system_ready_script`` and ``cloud_init_ready_script`` image config keys. |
158 | + |
159 | +Each of these keys accepts a small bash test statement as a string that must |
160 | +return 0 or 1. Since this test statement will be added into a larger bash |
161 | +statement it must be a single statement using the ``[`` test syntax. |
162 | + |
163 | +The default image config provides a system ready script that works for any |
164 | +systemd based image. If the iamge is not systmed based, then a different test |
165 | +statement must be provided. The default config also provides a test for whether |
166 | +or not cloud-init has finished which checks for the file |
167 | +``/run/cloud-init/result.json``. This should be sufficient for most systems, as |
168 | +writing to this file is one of the last things cloud-init does. |
169 | + |
170 | +The setting ``boot_timeout`` controls how long, in seconds, the platform should |
171 | +wait for an image to boot. If the system ready script has not indicated that |
172 | +the system is fully booted within this time an error will be raised. |
173 | + |
174 | +Image Config Feature Flags |
175 | +-------------------------- |
176 | +Not all testcases can work on all images due to features the testcase requires |
177 | +not being present on that image. If a testcase requires features in an image |
178 | +that are not likely to be present across all distros and platforms that the |
179 | +test suite supports, then the test can be skipped everywhere it is not |
180 | +supported. |
181 | + |
182 | +This is done through feature flags, which are names for features supported on |
183 | +some images but not all that may be required by testcases. Configuration for |
184 | +feature flags is provided in ``releases.yaml`` under the ``features`` top level |
185 | +key. The features config includes a list of all currently defined feature flags |
186 | +and their meanings, and a list of feature groups. |
187 | + |
188 | +Feature groups are groups of features that many images have in common. For |
189 | +example, the ``ubuntu_specific`` feature group includes features that should be |
190 | +present across most ubuntu releases, but may or may not be for other distros. |
191 | +Feature groups are specified for an image as a list under the key |
192 | +``feature_groups``. |
193 | + |
194 | +An image's feature flags are derived from the features groups that that image |
195 | +has and any feature overrides provided. Feature overrides can be specified |
196 | +under the ``features`` key which accepts a dictionary of |
197 | +``{<feature_name>: true/false}`` mappings. If a feature is omitted from an |
198 | +image's feature flags or set to false in the overrides then the test suite will |
199 | +skip any tests that require that feature when using that image. |
200 | + |
201 | +Feature flags may be overridden at runtime using the ``--feature-override`` |
202 | +command line argument. It accepts a feature flag and value to set in the format |
203 | +``<feature name>: true/false``. Multiple ``--feature-override`` flags can be |
204 | +used, and will all be applied to all feature flags for images used during a |
205 | +test. |
206 | + |
207 | +Image Config Setup Overrides |
208 | +---------------------------- |
209 | +If an image requires some of the options for image setup to be used, then it |
210 | +may specify overrides for the command line arguments passed into setup image. |
211 | +These may be specified as a dictionary under the ``setup_overrides`` key. When |
212 | +an image is set up, the arguments that control how it is set up will be the |
213 | +arguments from the command line, with any entries in ``setup_overrides`` used |
214 | +to override these arguments. |
215 | + |
216 | +For example, images that do not come with cloud-init already installed should |
217 | +have ``setup_overrides: {upgrade: true}`` specified so that in the event that |
218 | +no additional setup options are given, cloud-init will be installed from the |
219 | +image's repos before running tests. Note that if other options such as |
220 | +``--deb`` are passed in on the command line, these will still work as expected, |
221 | +since apt's policy for cloud-init would prefer the locally installed deb over |
222 | +an older version from the repos. |
223 | + |
224 | +Image Config Platform Specific Options |
225 | +-------------------------------------- |
226 | +There are many platform specific options in image configuration that allow |
227 | +platforms to locate images and that control additional setup that the platform |
228 | +may have to do to make the image useable. For information on how these work, |
229 | +please consult the documentation for that platform in the integration testing |
230 | +suite and the ``releases.yaml`` file for examples. |
231 | + |
232 | +Error Handling Behavior |
233 | +======================= |
234 | + |
235 | +The test suite makes an attempt to run as many tests as possible even in the |
236 | +event of some failing so that automated runs collect as much data as possible. |
237 | +In the event that something goes wrong while setting up for or running a test, |
238 | +the test suite will attempt to continue running any tests which have not been |
239 | +effected by the error. |
240 | + |
241 | +For example, if the test suite was told to run tests on one platform for two |
242 | +releases and an error occured setting up the first image, all tests for that |
243 | +image would be skipped, and the test suite would continue to set up the second |
244 | +image and run tests on it. Or, if the system does not start properly for one |
245 | +testcase out of many to run on that image, that testcase will be skipped and |
246 | +the next one will be run. |
247 | + |
248 | +Note that if any errors at all occur, the test suite will record the failure |
249 | +and where it occurred in the result data and write it out to the specified |
250 | +result file. |
251 | + |
252 | +Exit Codes |
253 | +---------- |
254 | +The test suite counts how many errors occur throughout a run. The exit code |
255 | +after a run is the number of errors that occured. If the exit code is non-zero |
256 | +than something is wrong either with the test suite, the configuration for an |
257 | +image, a testcase, or cloud-init itself. |
258 | + |
259 | +Note that the exit code does not always direclty correspond to the number |
260 | +of failed testcases, since in some cases, a single error during image setup |
261 | +can mean that several testcases are not run. If run is used, then the exit code |
262 | +will be the sum of the number of errors in the collect and verify stages. |
263 | + |
264 | +Result Data |
265 | +----------- |
266 | +The test suite generates result data that includes how long each stage of the |
267 | +test suite took and which parts were and were not successful. This data is |
268 | +dumped to the log after the collect and verify stages, and may also be written |
269 | +out in yaml format to a file. If part of the setup failed, the traceback for |
270 | +the failure and the error message will be included in the result file. If a |
271 | +test verifier finds a problem with the collected data from a test run, the |
272 | +class, test function and test will be recorded in the result data. |
273 | + |
274 | +Data Dir |
275 | +-------- |
276 | +When using run, the collected data is written into a temporary directory. In |
277 | +the even that all tests pass, this directory is deleted. In the even that a |
278 | +test fails or an error occurs, this data will be left in place, and a message |
279 | +will be written to the log giving the location of the data. |
280 | |
281 | Architecture |
282 | ============ |
283 | diff --git a/tests/cloud_tests/args.py b/tests/cloud_tests/args.py |
284 | index b68cc98..8f28b94 100644 |
285 | --- a/tests/cloud_tests/args.py |
286 | +++ b/tests/cloud_tests/args.py |
287 | @@ -9,15 +9,18 @@ ARG_SETS = { |
288 | 'COLLECT': ( |
289 | (('-p', '--platform'), |
290 | {'help': 'platform(s) to run tests on', 'metavar': 'PLATFORM', |
291 | - 'action': 'append', 'choices': config.list_enabled_platforms(), |
292 | + 'action': 'append', 'choices': config.ENABLED_PLATFORMS, |
293 | 'default': []}), |
294 | (('-n', '--os-name'), |
295 | {'help': 'the name(s) of the OS(s) to test', 'metavar': 'NAME', |
296 | - 'action': 'append', 'choices': config.list_enabled_distros(), |
297 | + 'action': 'append', 'choices': config.ENABLED_DISTROS, |
298 | 'default': []}), |
299 | (('-t', '--test-config'), |
300 | {'help': 'test config file(s) to use', 'metavar': 'FILE', |
301 | - 'action': 'append', 'default': []}),), |
302 | + 'action': 'append', 'default': []}), |
303 | + (('--feature-override',), |
304 | + {'help': 'feature flags override(s), <flagname>=<true/false>', |
305 | + 'action': 'append', 'default': [], 'required': False}),), |
306 | 'CREATE': ( |
307 | (('-c', '--config'), |
308 | {'help': 'cloud-config yaml for testcase', 'metavar': 'DATA', |
309 | @@ -61,8 +64,12 @@ ARG_SETS = { |
310 | {'help': 'ppa to enable (implies -u)', 'metavar': 'NAME', |
311 | 'action': 'store'}), |
312 | (('-u', '--upgrade'), |
313 | - {'help': 'upgrade before starting tests', 'action': 'store_true', |
314 | - 'default': False}),), |
315 | + {'help': 'upgrade or install cloud-init from repo', |
316 | + 'action': 'store_true', 'default': False}), |
317 | + (('--upgrade-full',), |
318 | + {'help': 'do full system upgrade from repo (implies -u)', |
319 | + 'action': 'store_true', 'default': False}),), |
320 | + |
321 | } |
322 | |
323 | SUBCMDS = { |
324 | @@ -121,15 +128,15 @@ def normalize_collect_args(args): |
325 | """ |
326 | # platform should default to all supported |
327 | if len(args.platform) == 0: |
328 | - args.platform = config.list_enabled_platforms() |
329 | + args.platform = config.ENABLED_PLATFORMS |
330 | args.platform = util.sorted_unique(args.platform) |
331 | |
332 | # os name should default to all enabled |
333 | # if os name is provided ensure that all provided are supported |
334 | if len(args.os_name) == 0: |
335 | - args.os_name = config.list_enabled_distros() |
336 | + args.os_name = config.ENABLED_DISTROS |
337 | else: |
338 | - supported = config.list_enabled_distros() |
339 | + supported = config.ENABLED_DISTROS |
340 | invalid = [os_name for os_name in args.os_name |
341 | if os_name not in supported] |
342 | if len(invalid) != 0: |
343 | @@ -158,6 +165,17 @@ def normalize_collect_args(args): |
344 | args.test_config = valid |
345 | args.test_config = util.sorted_unique(args.test_config) |
346 | |
347 | + # parse feature flag overrides and ensure all are valid |
348 | + if args.feature_override: |
349 | + overrides = args.feature_override |
350 | + args.feature_override = util.parse_conf_list( |
351 | + overrides, boolean=True, valid=config.list_feature_flags()) |
352 | + if not args.feature_override: |
353 | + LOG.error('invalid feature flag override(s): %s', overrides) |
354 | + return None |
355 | + else: |
356 | + args.feature_override = {} |
357 | + |
358 | return args |
359 | |
360 | |
361 | diff --git a/tests/cloud_tests/collect.py b/tests/cloud_tests/collect.py |
362 | index 68b47d7..5b004c3 100644 |
363 | --- a/tests/cloud_tests/collect.py |
364 | +++ b/tests/cloud_tests/collect.py |
365 | @@ -18,8 +18,10 @@ def collect_script(instance, base_dir, script, script_name): |
366 | return_value: None, may raise errors |
367 | """ |
368 | LOG.debug('running collect script: %s', script_name) |
369 | - util.write_file(os.path.join(base_dir, script_name), |
370 | - instance.run_script(script)) |
371 | + (out, err, exit) = instance.run_script( |
372 | + script, rcs=range(0, 256), |
373 | + description='collect: {}'.format(script_name)) |
374 | + util.write_file(os.path.join(base_dir, script_name), out) |
375 | |
376 | |
377 | def collect_test_data(args, snapshot, os_name, test_name): |
378 | @@ -39,15 +41,27 @@ def collect_test_data(args, snapshot, os_name, test_name): |
379 | test_scripts = test_config['collect_scripts'] |
380 | test_output_dir = os.sep.join( |
381 | (args.data_dir, snapshot.platform_name, os_name, test_name)) |
382 | - boot_timeout = (test_config.get('boot_timeout') |
383 | - if isinstance(test_config.get('boot_timeout'), int) else |
384 | - snapshot.config.get('timeout')) |
385 | |
386 | # if test is not enabled, skip and return 0 failures |
387 | if not test_config.get('enabled', False): |
388 | LOG.warn('test config %s is not enabled, skipping', test_name) |
389 | return ({}, 0) |
390 | |
391 | + # if testcase requires a feature flag that the image does not support, |
392 | + # skip the testcase with a warning |
393 | + req_features = test_config.get('required_features', []) |
394 | + if any(feature not in snapshot.features for feature in req_features): |
395 | + LOG.warn('test config %s requires features not supported by image, ' |
396 | + 'skipping.\nrequired features: %s\nsupported features: %s', |
397 | + test_name, req_features, snapshot.features) |
398 | + return ({}, 0) |
399 | + |
400 | + # if there are user data overrides required for this test case, apply them |
401 | + overrides = snapshot.config.get('user_data_overrides', {}) |
402 | + if overrides: |
403 | + LOG.debug('updating user data for collect with: %s', overrides) |
404 | + user_data = util.update_user_data(user_data, overrides) |
405 | + |
406 | # create test instance |
407 | component = PlatformComponent( |
408 | partial(instances.get_instance, snapshot, user_data, |
409 | @@ -56,7 +70,7 @@ def collect_test_data(args, snapshot, os_name, test_name): |
410 | LOG.info('collecting test data for test: %s', test_name) |
411 | with component as instance: |
412 | start_call = partial(run_single, 'boot instance', partial( |
413 | - instance.start, wait=True, wait_time=boot_timeout)) |
414 | + instance.start, wait=True, wait_for_cloud_init=True)) |
415 | collect_calls = [partial(run_single, 'script {}'.format(script_name), |
416 | partial(collect_script, instance, |
417 | test_output_dir, script, script_name)) |
418 | @@ -100,10 +114,9 @@ def collect_image(args, platform, os_name): |
419 | """ |
420 | res = ({}, 1) |
421 | |
422 | - os_config = config.load_os_config(os_name) |
423 | - if not os_config.get('enabled'): |
424 | - raise ValueError('OS {} not enabled'.format(os_name)) |
425 | - |
426 | + os_config = config.load_os_config( |
427 | + platform.platform_name, os_name, require_enabled=True, |
428 | + feature_overrides=args.feature_override) |
429 | component = PlatformComponent( |
430 | partial(images.get_image, platform, os_config)) |
431 | |
432 | @@ -126,10 +139,8 @@ def collect_platform(args, platform_name): |
433 | """ |
434 | res = ({}, 1) |
435 | |
436 | - platform_config = config.load_platform_config(platform_name) |
437 | - if not platform_config.get('enabled'): |
438 | - raise ValueError('Platform {} not enabled'.format(platform_name)) |
439 | - |
440 | + platform_config = config.load_platform_config( |
441 | + platform_name, require_enabled=True) |
442 | component = PlatformComponent( |
443 | partial(platforms.get_platform, platform_name, platform_config)) |
444 | |
445 | diff --git a/tests/cloud_tests/config.py b/tests/cloud_tests/config.py |
446 | index f3a13c9..9a0ad4d 100644 |
447 | --- a/tests/cloud_tests/config.py |
448 | +++ b/tests/cloud_tests/config.py |
449 | @@ -14,6 +14,20 @@ RELEASES_CONF = os.path.join(BASE_DIR, 'releases.yaml') |
450 | TESTCASE_CONF = os.path.join(BASE_DIR, 'testcases.yaml') |
451 | |
452 | |
453 | +def get(base, key): |
454 | + """ |
455 | + get config entry 'key' from base, ensuring is dictionary |
456 | + """ |
457 | + return base[key] if key in base and base[key] is not None else {} |
458 | + |
459 | + |
460 | +def enabled(config): |
461 | + """ |
462 | + test if config item is enabled |
463 | + """ |
464 | + return isinstance(config, dict) and config.get('enabled', False) |
465 | + |
466 | + |
467 | def path_to_name(path): |
468 | """ |
469 | convert abs or rel path to test config to path under configs/ |
470 | @@ -61,22 +75,63 @@ def merge_config(base, override): |
471 | return res |
472 | |
473 | |
474 | -def load_platform_config(platform): |
475 | +def merge_feature_groups(feature_conf, feature_groups, overrides): |
476 | + """ |
477 | + combine feature groups and overrides to construct a supported feature list |
478 | + feature_conf: feature config from releases.yaml |
479 | + feature_groups: feature groups the release is a member of |
480 | + overrides: overrides specified by the release's config |
481 | + return_value: dict of {feature: true/false} settings |
482 | + """ |
483 | + res = dict().fromkeys(feature_conf['all']) |
484 | + for group in feature_groups: |
485 | + res.update(feature_conf['groups'][group]) |
486 | + res.update(overrides) |
487 | + return res |
488 | + |
489 | + |
490 | +def load_platform_config(platform_name, require_enabled=False): |
491 | """ |
492 | load configuration for platform |
493 | + platform_name: name of platform to retrieve config for |
494 | + require_enabled: if true, raise error if 'enabled' not True |
495 | + return_value: config dict |
496 | """ |
497 | main_conf = c_util.read_conf(PLATFORM_CONF) |
498 | - return merge_config(main_conf.get('default_platform_config'), |
499 | - main_conf.get('platforms')[platform]) |
500 | + conf = merge_config(main_conf['default_platform_config'], |
501 | + main_conf['platforms'][platform_name]) |
502 | + if require_enabled and not enabled(conf): |
503 | + raise ValueError('Platform is not enabled') |
504 | + return conf |
505 | |
506 | |
507 | -def load_os_config(os_name): |
508 | +def load_os_config(platform_name, os_name, require_enabled=False, |
509 | + feature_overrides={}): |
510 | """ |
511 | load configuration for os |
512 | + platform_name: platform name to load os config for |
513 | + os_name: name of os to retrieve config for |
514 | + require_enabled: if true, raise error if 'enabled' not True |
515 | + feature_overrides: feature flag overrides to merge with features config |
516 | + return_value: config dict |
517 | """ |
518 | main_conf = c_util.read_conf(RELEASES_CONF) |
519 | - return merge_config(main_conf.get('default_release_config'), |
520 | - main_conf.get('releases')[os_name]) |
521 | + default = main_conf['default_release_config'] |
522 | + image = main_conf['releases'][os_name] |
523 | + conf = merge_config(merge_config(get(default, 'default'), |
524 | + get(default, platform_name)), |
525 | + merge_config(get(image, 'default'), |
526 | + get(image, platform_name))) |
527 | + |
528 | + feature_conf = main_conf['features'] |
529 | + feature_groups = conf.get('feature_groups', []) |
530 | + overrides = merge_config(get(conf, 'features'), feature_overrides) |
531 | + conf['features'] = merge_feature_groups( |
532 | + feature_conf, feature_groups, overrides) |
533 | + |
534 | + if require_enabled and not enabled(conf): |
535 | + raise ValueError('OS is not enabled') |
536 | + return conf |
537 | |
538 | |
539 | def load_test_config(path): |
540 | @@ -87,20 +142,34 @@ def load_test_config(path): |
541 | c_util.read_conf(name_to_path(path))) |
542 | |
543 | |
544 | +def list_feature_flags(): |
545 | + """ |
546 | + list all supported feature flags |
547 | + """ |
548 | + feature_conf = get(c_util.read_conf(RELEASES_CONF), 'features') |
549 | + return feature_conf.get('all', []) |
550 | + |
551 | + |
552 | def list_enabled_platforms(): |
553 | """ |
554 | list all platforms enabled for testing |
555 | """ |
556 | - platforms = c_util.read_conf(PLATFORM_CONF).get('platforms') |
557 | - return [k for k, v in platforms.items() if v.get('enabled')] |
558 | + platforms = get(c_util.read_conf(PLATFORM_CONF), 'platforms') |
559 | + return [k for k, v in platforms.items() if enabled(v)] |
560 | |
561 | |
562 | -def list_enabled_distros(): |
563 | +def list_enabled_distros(platforms): |
564 | """ |
565 | - list all distros enabled for testing |
566 | + list all distros enabled for testing on specified platforms |
567 | """ |
568 | - releases = c_util.read_conf(RELEASES_CONF).get('releases') |
569 | - return [k for k, v in releases.items() if v.get('enabled')] |
570 | + |
571 | + def platform_has_enabled(config): |
572 | + return any(enabled(merge_config(get(config, 'default'), |
573 | + get(config, platform))) |
574 | + for platform in platforms) |
575 | + |
576 | + releases = get(c_util.read_conf(RELEASES_CONF), 'releases') |
577 | + return [k for k, v in releases.items() if platform_has_enabled(v)] |
578 | |
579 | |
580 | def list_test_configs(): |
581 | @@ -110,4 +179,8 @@ def list_test_configs(): |
582 | return [os.path.abspath(f) for f in |
583 | glob.glob(os.sep.join((TEST_CONF_DIR, '*', '*.yaml')))] |
584 | |
585 | + |
586 | +ENABLED_PLATFORMS = list_enabled_platforms() |
587 | +ENABLED_DISTROS = list_enabled_distros(ENABLED_PLATFORMS) |
588 | + |
589 | # vi: ts=4 expandtab |
590 | diff --git a/tests/cloud_tests/configs/bugs/lp1628337.yaml b/tests/cloud_tests/configs/bugs/lp1628337.yaml |
591 | index 1d6bf48..e39b3cd 100644 |
592 | --- a/tests/cloud_tests/configs/bugs/lp1628337.yaml |
593 | +++ b/tests/cloud_tests/configs/bugs/lp1628337.yaml |
594 | @@ -1,6 +1,9 @@ |
595 | # |
596 | # LP Bug 1628337: cloud-init tries to install NTP before even configuring the archives |
597 | # |
598 | +required_features: |
599 | + - apt |
600 | + - lsb_release |
601 | cloud_config: | |
602 | #cloud-config |
603 | ntp: |
604 | diff --git a/tests/cloud_tests/configs/examples/add_apt_repositories.yaml b/tests/cloud_tests/configs/examples/add_apt_repositories.yaml |
605 | index b896435..4b8575f 100644 |
606 | --- a/tests/cloud_tests/configs/examples/add_apt_repositories.yaml |
607 | +++ b/tests/cloud_tests/configs/examples/add_apt_repositories.yaml |
608 | @@ -4,6 +4,8 @@ |
609 | # 2016-11-17: Disabled as covered by module based tests |
610 | # |
611 | enabled: False |
612 | +required_features: |
613 | + - apt |
614 | cloud_config: | |
615 | #cloud-config |
616 | apt: |
617 | diff --git a/tests/cloud_tests/configs/modules/apt_configure_conf.yaml b/tests/cloud_tests/configs/modules/apt_configure_conf.yaml |
618 | index 163ae3f..de45300 100644 |
619 | --- a/tests/cloud_tests/configs/modules/apt_configure_conf.yaml |
620 | +++ b/tests/cloud_tests/configs/modules/apt_configure_conf.yaml |
621 | @@ -1,6 +1,8 @@ |
622 | # |
623 | # Provide a configuration for APT |
624 | # |
625 | +required_features: |
626 | + - apt |
627 | cloud_config: | |
628 | #cloud-config |
629 | apt: |
630 | diff --git a/tests/cloud_tests/configs/modules/apt_configure_disable_suites.yaml b/tests/cloud_tests/configs/modules/apt_configure_disable_suites.yaml |
631 | index 73e4a53..9880067 100644 |
632 | --- a/tests/cloud_tests/configs/modules/apt_configure_disable_suites.yaml |
633 | +++ b/tests/cloud_tests/configs/modules/apt_configure_disable_suites.yaml |
634 | @@ -1,6 +1,9 @@ |
635 | # |
636 | # Disables everything in sources.list |
637 | # |
638 | +required_features: |
639 | + - apt |
640 | + - lsb_release |
641 | cloud_config: | |
642 | #cloud-config |
643 | apt: |
644 | diff --git a/tests/cloud_tests/configs/modules/apt_configure_primary.yaml b/tests/cloud_tests/configs/modules/apt_configure_primary.yaml |
645 | index 2ec30ca..41bcf2f 100644 |
646 | --- a/tests/cloud_tests/configs/modules/apt_configure_primary.yaml |
647 | +++ b/tests/cloud_tests/configs/modules/apt_configure_primary.yaml |
648 | @@ -1,6 +1,9 @@ |
649 | # |
650 | # Setup a custome primary sources.list |
651 | # |
652 | +required_features: |
653 | + - apt |
654 | + - apt_src_cont |
655 | cloud_config: | |
656 | #cloud-config |
657 | apt: |
658 | @@ -16,4 +19,8 @@ collect_scripts: |
659 | #!/bin/bash |
660 | grep -v '^#' /etc/apt/sources.list | sed '/^\s*$/d' | grep -c gtlib.gatech.edu |
661 | |
662 | + sources.list: | |
663 | + #!/bin/bash |
664 | + cat /etc/apt/sources.list |
665 | + |
666 | # vi: ts=4 expandtab |
667 | diff --git a/tests/cloud_tests/configs/modules/apt_configure_proxy.yaml b/tests/cloud_tests/configs/modules/apt_configure_proxy.yaml |
668 | index e737130..be6c6f8 100644 |
669 | --- a/tests/cloud_tests/configs/modules/apt_configure_proxy.yaml |
670 | +++ b/tests/cloud_tests/configs/modules/apt_configure_proxy.yaml |
671 | @@ -1,6 +1,8 @@ |
672 | # |
673 | # Set apt proxy |
674 | # |
675 | +required_features: |
676 | + - apt |
677 | cloud_config: | |
678 | #cloud-config |
679 | apt: |
680 | diff --git a/tests/cloud_tests/configs/modules/apt_configure_security.yaml b/tests/cloud_tests/configs/modules/apt_configure_security.yaml |
681 | index f6a2c82..83dd51d 100644 |
682 | --- a/tests/cloud_tests/configs/modules/apt_configure_security.yaml |
683 | +++ b/tests/cloud_tests/configs/modules/apt_configure_security.yaml |
684 | @@ -1,6 +1,9 @@ |
685 | # |
686 | # Add security to sources.list |
687 | # |
688 | +required_features: |
689 | + - apt |
690 | + - ubuntu_repos |
691 | cloud_config: | |
692 | #cloud-config |
693 | apt: |
694 | diff --git a/tests/cloud_tests/configs/modules/apt_configure_sources_key.yaml b/tests/cloud_tests/configs/modules/apt_configure_sources_key.yaml |
695 | index e7568a6..bde9398 100644 |
696 | --- a/tests/cloud_tests/configs/modules/apt_configure_sources_key.yaml |
697 | +++ b/tests/cloud_tests/configs/modules/apt_configure_sources_key.yaml |
698 | @@ -1,6 +1,9 @@ |
699 | # |
700 | # Add a sources.list entry with a given key (Debian Jessie) |
701 | # |
702 | +required_features: |
703 | + - apt |
704 | + - lsb_release |
705 | cloud_config: | |
706 | #cloud-config |
707 | apt: |
708 | diff --git a/tests/cloud_tests/configs/modules/apt_configure_sources_keyserver.yaml b/tests/cloud_tests/configs/modules/apt_configure_sources_keyserver.yaml |
709 | index 1a4a238..11da61e 100644 |
710 | --- a/tests/cloud_tests/configs/modules/apt_configure_sources_keyserver.yaml |
711 | +++ b/tests/cloud_tests/configs/modules/apt_configure_sources_keyserver.yaml |
712 | @@ -1,6 +1,9 @@ |
713 | # |
714 | # Add a sources.list entry with a key from a keyserver |
715 | # |
716 | +required_features: |
717 | + - apt |
718 | + - lsb_release |
719 | cloud_config: | |
720 | #cloud-config |
721 | apt: |
722 | diff --git a/tests/cloud_tests/configs/modules/apt_configure_sources_list.yaml b/tests/cloud_tests/configs/modules/apt_configure_sources_list.yaml |
723 | index 057fc72..143cb08 100644 |
724 | --- a/tests/cloud_tests/configs/modules/apt_configure_sources_list.yaml |
725 | +++ b/tests/cloud_tests/configs/modules/apt_configure_sources_list.yaml |
726 | @@ -1,6 +1,9 @@ |
727 | # |
728 | # Generate a sources.list |
729 | # |
730 | +required_features: |
731 | + - apt |
732 | + - lsb_release |
733 | cloud_config: | |
734 | #cloud-config |
735 | apt: |
736 | diff --git a/tests/cloud_tests/configs/modules/apt_configure_sources_ppa.yaml b/tests/cloud_tests/configs/modules/apt_configure_sources_ppa.yaml |
737 | index dee9dc7..9efdae5 100644 |
738 | --- a/tests/cloud_tests/configs/modules/apt_configure_sources_ppa.yaml |
739 | +++ b/tests/cloud_tests/configs/modules/apt_configure_sources_ppa.yaml |
740 | @@ -1,6 +1,12 @@ |
741 | # |
742 | # Add a PPA to source.list |
743 | # |
744 | +# NOTE: on older ubuntu releases the sources file added is named |
745 | +# 'curtin-dev-test-archive-trusty', without 'ubuntu' in the middle |
746 | +required_features: |
747 | + - apt |
748 | + - ppa |
749 | + - ppa_file_name |
750 | cloud_config: | |
751 | #cloud-config |
752 | apt: |
753 | @@ -16,5 +22,8 @@ collect_scripts: |
754 | apt-key: | |
755 | #!/bin/bash |
756 | apt-key finger |
757 | + sources_full: | |
758 | + #!/bin/bash |
759 | + cat /etc/apt/sources.list |
760 | |
761 | # vi: ts=4 expandtab |
762 | diff --git a/tests/cloud_tests/configs/modules/apt_pipelining_disable.yaml b/tests/cloud_tests/configs/modules/apt_pipelining_disable.yaml |
763 | index 5fa0cee..bd9b5d0 100644 |
764 | --- a/tests/cloud_tests/configs/modules/apt_pipelining_disable.yaml |
765 | +++ b/tests/cloud_tests/configs/modules/apt_pipelining_disable.yaml |
766 | @@ -1,6 +1,8 @@ |
767 | # |
768 | # Disable apt pipelining value |
769 | # |
770 | +required_features: |
771 | + - apt |
772 | cloud_config: | |
773 | #cloud-config |
774 | apt: |
775 | diff --git a/tests/cloud_tests/configs/modules/apt_pipelining_os.yaml b/tests/cloud_tests/configs/modules/apt_pipelining_os.yaml |
776 | index 87d183e..cbed3ba 100644 |
777 | --- a/tests/cloud_tests/configs/modules/apt_pipelining_os.yaml |
778 | +++ b/tests/cloud_tests/configs/modules/apt_pipelining_os.yaml |
779 | @@ -1,6 +1,8 @@ |
780 | # |
781 | # Set apt pipelining value to OS |
782 | # |
783 | +required_features: |
784 | + - apt |
785 | cloud_config: | |
786 | #cloud-config |
787 | apt: |
788 | diff --git a/tests/cloud_tests/configs/modules/byobu.yaml b/tests/cloud_tests/configs/modules/byobu.yaml |
789 | index fd648c7..a9aa1f3 100644 |
790 | --- a/tests/cloud_tests/configs/modules/byobu.yaml |
791 | +++ b/tests/cloud_tests/configs/modules/byobu.yaml |
792 | @@ -1,6 +1,8 @@ |
793 | # |
794 | # Install and enable byobu system wide and default user |
795 | # |
796 | +required_features: |
797 | + - byobu |
798 | cloud_config: | |
799 | #cloud-config |
800 | byobu_by_default: enable |
801 | diff --git a/tests/cloud_tests/configs/modules/keys_to_console.yaml b/tests/cloud_tests/configs/modules/keys_to_console.yaml |
802 | index a90e42c..5d86e73 100644 |
803 | --- a/tests/cloud_tests/configs/modules/keys_to_console.yaml |
804 | +++ b/tests/cloud_tests/configs/modules/keys_to_console.yaml |
805 | @@ -1,6 +1,8 @@ |
806 | # |
807 | # Hide printing of ssh key and fingerprints for specific keys |
808 | # |
809 | +required_features: |
810 | + - syslog |
811 | cloud_config: | |
812 | #cloud-config |
813 | ssh_fp_console_blacklist: [ssh-dss, ssh-dsa, ecdsa-sha2-nistp256] |
814 | diff --git a/tests/cloud_tests/configs/modules/landscape.yaml b/tests/cloud_tests/configs/modules/landscape.yaml |
815 | index e6f4955..ed2c37c 100644 |
816 | --- a/tests/cloud_tests/configs/modules/landscape.yaml |
817 | +++ b/tests/cloud_tests/configs/modules/landscape.yaml |
818 | @@ -4,6 +4,8 @@ |
819 | # 2016-11-17: Disabled due to this not working |
820 | # |
821 | enabled: false |
822 | +required_features: |
823 | + - landscape |
824 | cloud_config: | |
825 | #cloud-conifg |
826 | landscape: |
827 | diff --git a/tests/cloud_tests/configs/modules/locale.yaml b/tests/cloud_tests/configs/modules/locale.yaml |
828 | index af5ad63..e01518a 100644 |
829 | --- a/tests/cloud_tests/configs/modules/locale.yaml |
830 | +++ b/tests/cloud_tests/configs/modules/locale.yaml |
831 | @@ -1,6 +1,9 @@ |
832 | # |
833 | # Set locale to non-default option and verify |
834 | # |
835 | +required_features: |
836 | + - engb_locale |
837 | + - locale_gen |
838 | cloud_config: | |
839 | #cloud-config |
840 | locale: en_GB.UTF-8 |
841 | diff --git a/tests/cloud_tests/configs/modules/lxd_bridge.yaml b/tests/cloud_tests/configs/modules/lxd_bridge.yaml |
842 | index 568bb70..e6b7e76 100644 |
843 | --- a/tests/cloud_tests/configs/modules/lxd_bridge.yaml |
844 | +++ b/tests/cloud_tests/configs/modules/lxd_bridge.yaml |
845 | @@ -1,6 +1,8 @@ |
846 | # |
847 | # LXD configured with directory backend and IPv4 bridge |
848 | # |
849 | +required_features: |
850 | + - lxd |
851 | cloud_config: | |
852 | #cloud-config |
853 | lxd: |
854 | diff --git a/tests/cloud_tests/configs/modules/lxd_dir.yaml b/tests/cloud_tests/configs/modules/lxd_dir.yaml |
855 | index 99b9219..f93a3fa 100644 |
856 | --- a/tests/cloud_tests/configs/modules/lxd_dir.yaml |
857 | +++ b/tests/cloud_tests/configs/modules/lxd_dir.yaml |
858 | @@ -1,6 +1,8 @@ |
859 | # |
860 | # LXD configured with directory backend |
861 | # |
862 | +required_features: |
863 | + - lxd |
864 | cloud_config: | |
865 | #cloud-config |
866 | lxd: |
867 | diff --git a/tests/cloud_tests/configs/modules/ntp.yaml b/tests/cloud_tests/configs/modules/ntp.yaml |
868 | index d094157..0d07ef5 100644 |
869 | --- a/tests/cloud_tests/configs/modules/ntp.yaml |
870 | +++ b/tests/cloud_tests/configs/modules/ntp.yaml |
871 | @@ -1,6 +1,14 @@ |
872 | # |
873 | # Emtpy NTP config to setup using defaults |
874 | # |
875 | +# NOTE: this should not require apt feature, use 'which' rather than 'dpkg -l' |
876 | +# NOTE: this should not require no_ntpdate feature, use 'which' to check for |
877 | +# installation rather than 'dpkg -l', as 'grep ntp' matches 'ntpdate' |
878 | +# NOTE: the verifier should check for any ntp server not 'ubuntu.pool.ntp.org' |
879 | +required_features: |
880 | + - apt |
881 | + - no_ntpdate |
882 | + - ubuntu_ntp |
883 | cloud_config: | |
884 | #cloud-config |
885 | ntp: |
886 | @@ -16,5 +24,8 @@ collect_scripts: |
887 | ntp_conf_empty: | |
888 | #!/bin/bash |
889 | grep '^pool' /etc/ntp.conf |
890 | + ntp_installed_list: | |
891 | + #!/bin/bash |
892 | + dpkg -l | grep ntp |
893 | |
894 | # vi: ts=4 expandtab |
895 | diff --git a/tests/cloud_tests/configs/modules/ntp_pools.yaml b/tests/cloud_tests/configs/modules/ntp_pools.yaml |
896 | index bd0ac29..6ec1bfe 100644 |
897 | --- a/tests/cloud_tests/configs/modules/ntp_pools.yaml |
898 | +++ b/tests/cloud_tests/configs/modules/ntp_pools.yaml |
899 | @@ -1,6 +1,16 @@ |
900 | # |
901 | # NTP config using specific pools |
902 | # |
903 | +# NOTE: this should not require apt feature, use 'which' rather than 'dpkg -l' |
904 | +# NOTE: this should not require no_ntpdate feature, use 'which' to check for |
905 | +# installation rather than 'dpkg -l', as 'grep ntp' matches 'ntpdate' |
906 | +# NOTE: lsb_release listed here because with recent cloud-init deb with |
907 | +# (LP: 1628337) resolved, cloud-init will attempt to configure archives. |
908 | +# this fails without lsb_release as UNAVAILABLE is used for $RELEASE |
909 | +required_features: |
910 | + - apt |
911 | + - no_ntpdate |
912 | + - lsb_release |
913 | cloud_config: | |
914 | #cloud-config |
915 | ntp: |
916 | diff --git a/tests/cloud_tests/configs/modules/ntp_servers.yaml b/tests/cloud_tests/configs/modules/ntp_servers.yaml |
917 | index 934b9c5..b61ec37 100644 |
918 | --- a/tests/cloud_tests/configs/modules/ntp_servers.yaml |
919 | +++ b/tests/cloud_tests/configs/modules/ntp_servers.yaml |
920 | @@ -1,6 +1,12 @@ |
921 | # |
922 | # NTP config using specific servers |
923 | # |
924 | +# NOTE: this should not require apt feature, use 'which' rather than 'dpkg -l' |
925 | +# NOTE: this should not require no_ntpdate feature, use 'which' to check for |
926 | +# installation rather than 'dpkg -l', as 'grep ntp' matches 'ntpdate' |
927 | +required_features: |
928 | + - apt |
929 | + - no_ntpdate |
930 | cloud_config: | |
931 | #cloud-config |
932 | ntp: |
933 | diff --git a/tests/cloud_tests/configs/modules/package_update_upgrade_install.yaml b/tests/cloud_tests/configs/modules/package_update_upgrade_install.yaml |
934 | index d027d54..71d24b8 100644 |
935 | --- a/tests/cloud_tests/configs/modules/package_update_upgrade_install.yaml |
936 | +++ b/tests/cloud_tests/configs/modules/package_update_upgrade_install.yaml |
937 | @@ -1,6 +1,17 @@ |
938 | # |
939 | # Update/upgrade via apt and then install a pair of packages |
940 | # |
941 | +# NOTE: this should not require apt feature, use 'which' rather than 'dpkg -l' |
942 | +# NOTE: the testcase for this looks for the command in history.log as |
943 | +# /usr/bin/apt-get..., which is not how it always appears. it should |
944 | +# instead look for just apt-get... |
945 | +# NOTE: this testcase should not require 'apt_up_out', and should look for a |
946 | +# call to 'apt-get upgrade' or 'apt-get dist-upgrade' in cloud-init.log |
947 | +# rather than 'Calculating upgrade...' in output |
948 | +required_features: |
949 | + - apt |
950 | + - apt_hist_fmt |
951 | + - apt_up_out |
952 | cloud_config: | |
953 | #cloud-config |
954 | packages: |
955 | diff --git a/tests/cloud_tests/configs/modules/set_hostname.yaml b/tests/cloud_tests/configs/modules/set_hostname.yaml |
956 | index 5aae150..c96344c 100644 |
957 | --- a/tests/cloud_tests/configs/modules/set_hostname.yaml |
958 | +++ b/tests/cloud_tests/configs/modules/set_hostname.yaml |
959 | @@ -1,6 +1,8 @@ |
960 | # |
961 | # Set the hostname and update /etc/hosts |
962 | # |
963 | +required_features: |
964 | + - hostname |
965 | cloud_config: | |
966 | #cloud-config |
967 | hostname: myhostname |
968 | diff --git a/tests/cloud_tests/configs/modules/set_hostname_fqdn.yaml b/tests/cloud_tests/configs/modules/set_hostname_fqdn.yaml |
969 | index 0014c19..daf7593 100644 |
970 | --- a/tests/cloud_tests/configs/modules/set_hostname_fqdn.yaml |
971 | +++ b/tests/cloud_tests/configs/modules/set_hostname_fqdn.yaml |
972 | @@ -1,6 +1,8 @@ |
973 | # |
974 | # Set the hostname and update /etc/hosts |
975 | # |
976 | +required_features: |
977 | + - hostname |
978 | cloud_config: | |
979 | #cloud-config |
980 | manage_etc_hosts: true |
981 | diff --git a/tests/cloud_tests/configs/modules/set_password.yaml b/tests/cloud_tests/configs/modules/set_password.yaml |
982 | index 8fa46d9..04d7c58 100644 |
983 | --- a/tests/cloud_tests/configs/modules/set_password.yaml |
984 | +++ b/tests/cloud_tests/configs/modules/set_password.yaml |
985 | @@ -1,6 +1,8 @@ |
986 | # |
987 | # Set password of default user |
988 | # |
989 | +required_features: |
990 | + - ubuntu_user |
991 | cloud_config: | |
992 | #cloud-config |
993 | password: password |
994 | diff --git a/tests/cloud_tests/configs/modules/set_password_expire.yaml b/tests/cloud_tests/configs/modules/set_password_expire.yaml |
995 | index 926731f..789604b 100644 |
996 | --- a/tests/cloud_tests/configs/modules/set_password_expire.yaml |
997 | +++ b/tests/cloud_tests/configs/modules/set_password_expire.yaml |
998 | @@ -1,6 +1,8 @@ |
999 | # |
1000 | # Expire password for all users |
1001 | # |
1002 | +required_features: |
1003 | + - sshd |
1004 | cloud_config: | |
1005 | #cloud-config |
1006 | chpasswd: { expire: True } |
1007 | diff --git a/tests/cloud_tests/configs/modules/snappy.yaml b/tests/cloud_tests/configs/modules/snappy.yaml |
1008 | index 923bfe1..030b790 100644 |
1009 | --- a/tests/cloud_tests/configs/modules/snappy.yaml |
1010 | +++ b/tests/cloud_tests/configs/modules/snappy.yaml |
1011 | @@ -1,6 +1,8 @@ |
1012 | # |
1013 | # Install snappy |
1014 | # |
1015 | +required_features: |
1016 | + - snap |
1017 | cloud_config: | |
1018 | #cloud-config |
1019 | snappy: |
1020 | 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 |
1021 | index 33943bd..746653e 100644 |
1022 | --- a/tests/cloud_tests/configs/modules/ssh_auth_key_fingerprints_disable.yaml |
1023 | +++ b/tests/cloud_tests/configs/modules/ssh_auth_key_fingerprints_disable.yaml |
1024 | @@ -1,6 +1,8 @@ |
1025 | # |
1026 | # Disable fingerprint printing |
1027 | # |
1028 | +required_features: |
1029 | + - syslog |
1030 | cloud_config: | |
1031 | #cloud-config |
1032 | ssh_genkeytypes: [] |
1033 | 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 |
1034 | index 4c97077..9f5dc34 100644 |
1035 | --- a/tests/cloud_tests/configs/modules/ssh_auth_key_fingerprints_enable.yaml |
1036 | +++ b/tests/cloud_tests/configs/modules/ssh_auth_key_fingerprints_enable.yaml |
1037 | @@ -1,6 +1,11 @@ |
1038 | # |
1039 | # Print auth keys with different hash than md5 |
1040 | # |
1041 | +# NOTE: testcase checks for '256 SHA256:.*(ECDSA)' on output line on trusty |
1042 | +# this fails as line in output reads '256:.*(ECDSA)' |
1043 | +required_features: |
1044 | + - syslog |
1045 | + - ssh_key_fmt |
1046 | cloud_config: | |
1047 | #cloud-config |
1048 | ssh_genkeytypes: |
1049 | diff --git a/tests/cloud_tests/configs/modules/ssh_import_id.yaml b/tests/cloud_tests/configs/modules/ssh_import_id.yaml |
1050 | index 6e5a163..b62d3f6 100644 |
1051 | --- a/tests/cloud_tests/configs/modules/ssh_import_id.yaml |
1052 | +++ b/tests/cloud_tests/configs/modules/ssh_import_id.yaml |
1053 | @@ -1,6 +1,9 @@ |
1054 | # |
1055 | # Import a user's ssh key via gh or lp |
1056 | # |
1057 | +required_features: |
1058 | + - ubuntu_user |
1059 | + - sudo |
1060 | cloud_config: | |
1061 | #cloud-config |
1062 | ssh_import_id: |
1063 | diff --git a/tests/cloud_tests/configs/modules/ssh_keys_generate.yaml b/tests/cloud_tests/configs/modules/ssh_keys_generate.yaml |
1064 | index 637d783..659fd93 100644 |
1065 | --- a/tests/cloud_tests/configs/modules/ssh_keys_generate.yaml |
1066 | +++ b/tests/cloud_tests/configs/modules/ssh_keys_generate.yaml |
1067 | @@ -1,6 +1,8 @@ |
1068 | # |
1069 | # SSH keys generated using cloud-init |
1070 | # |
1071 | +required_features: |
1072 | + - ubuntu_user |
1073 | cloud_config: | |
1074 | #cloud-config |
1075 | ssh_genkeytypes: |
1076 | diff --git a/tests/cloud_tests/configs/modules/ssh_keys_provided.yaml b/tests/cloud_tests/configs/modules/ssh_keys_provided.yaml |
1077 | index 25df645..5ceb362 100644 |
1078 | --- a/tests/cloud_tests/configs/modules/ssh_keys_provided.yaml |
1079 | +++ b/tests/cloud_tests/configs/modules/ssh_keys_provided.yaml |
1080 | @@ -2,6 +2,9 @@ |
1081 | # SSH keys provided via cloud config |
1082 | # |
1083 | enabled: False |
1084 | +required_features: |
1085 | + - ubuntu_user |
1086 | + - sudo |
1087 | cloud_config: | |
1088 | #cloud-config |
1089 | disable_root: false |
1090 | diff --git a/tests/cloud_tests/configs/modules/timezone.yaml b/tests/cloud_tests/configs/modules/timezone.yaml |
1091 | index 8c96ed4..5112aa9 100644 |
1092 | --- a/tests/cloud_tests/configs/modules/timezone.yaml |
1093 | +++ b/tests/cloud_tests/configs/modules/timezone.yaml |
1094 | @@ -1,6 +1,8 @@ |
1095 | # |
1096 | # Set system timezone |
1097 | # |
1098 | +required_features: |
1099 | + - daylight_time |
1100 | cloud_config: | |
1101 | #cloud-config |
1102 | timezone: US/Aleutian |
1103 | diff --git a/tests/cloud_tests/configs/modules/user_groups.yaml b/tests/cloud_tests/configs/modules/user_groups.yaml |
1104 | index 9265595..71cc9da 100644 |
1105 | --- a/tests/cloud_tests/configs/modules/user_groups.yaml |
1106 | +++ b/tests/cloud_tests/configs/modules/user_groups.yaml |
1107 | @@ -1,6 +1,8 @@ |
1108 | # |
1109 | # Create groups and users with various options |
1110 | # |
1111 | +required_features: |
1112 | + - ubuntu_user |
1113 | cloud_config: | |
1114 | #cloud-config |
1115 | # Add groups to the system |
1116 | diff --git a/tests/cloud_tests/configs/modules/write_files.yaml b/tests/cloud_tests/configs/modules/write_files.yaml |
1117 | index 4bb2991..ce936b7 100644 |
1118 | --- a/tests/cloud_tests/configs/modules/write_files.yaml |
1119 | +++ b/tests/cloud_tests/configs/modules/write_files.yaml |
1120 | @@ -1,6 +1,10 @@ |
1121 | # |
1122 | # Write various file types |
1123 | # |
1124 | +# NOTE: on trusty 'file' has an output formatting error for binary files and |
1125 | +# has 2 spaces in 'LSB executable', which causes a failure here |
1126 | +required_features: |
1127 | + - no_file_fmt_e |
1128 | cloud_config: | |
1129 | #cloud-config |
1130 | write_files: |
1131 | diff --git a/tests/cloud_tests/images/base.py b/tests/cloud_tests/images/base.py |
1132 | index 394b11f..1f604cf 100644 |
1133 | --- a/tests/cloud_tests/images/base.py |
1134 | +++ b/tests/cloud_tests/images/base.py |
1135 | @@ -7,13 +7,14 @@ class Image(object): |
1136 | """ |
1137 | platform_name = None |
1138 | |
1139 | - def __init__(self, name, config, platform): |
1140 | + def __init__(self, platform, config): |
1141 | """ |
1142 | - setup |
1143 | + Set up image |
1144 | + platform: platform object |
1145 | + config: image configuration |
1146 | """ |
1147 | - self.name = name |
1148 | - self.config = config |
1149 | self.platform = platform |
1150 | + self.config = config |
1151 | |
1152 | def __str__(self): |
1153 | """ |
1154 | @@ -28,10 +29,24 @@ class Image(object): |
1155 | """ |
1156 | raise NotImplementedError |
1157 | |
1158 | - # FIXME: instead of having execute and push_file and other instance methods |
1159 | - # here which pass through to a hidden instance, it might be better |
1160 | - # to expose an instance that the image can be modified through |
1161 | - def execute(self, command, stdin=None, stdout=None, stderr=None, env={}): |
1162 | + @property |
1163 | + def features(self): |
1164 | + """ |
1165 | + feature flags supported by this image |
1166 | + return_value: list of feature names |
1167 | + """ |
1168 | + return [k for k, v in self.config.get('features', {}).items() if v] |
1169 | + |
1170 | + @property |
1171 | + def setup_overrides(self): |
1172 | + """ |
1173 | + setup options that need to be overridden for the image |
1174 | + return_value: dictionary to update args with |
1175 | + """ |
1176 | + # NOTE: more sophisticated options may be requied at some point |
1177 | + return self.config.get('setup_overrides', {}) |
1178 | + |
1179 | + def execute(self, *args, **kwargs): |
1180 | """ |
1181 | execute command in image, modifying image |
1182 | """ |
1183 | @@ -43,7 +58,7 @@ class Image(object): |
1184 | """ |
1185 | raise NotImplementedError |
1186 | |
1187 | - def run_script(self, script): |
1188 | + def run_script(self, *args, **kwargs): |
1189 | """ |
1190 | run script in image, modifying image |
1191 | return_value: script output |
1192 | diff --git a/tests/cloud_tests/images/lxd.py b/tests/cloud_tests/images/lxd.py |
1193 | index 7a41614..f820582 100644 |
1194 | --- a/tests/cloud_tests/images/lxd.py |
1195 | +++ b/tests/cloud_tests/images/lxd.py |
1196 | @@ -2,6 +2,10 @@ |
1197 | |
1198 | from tests.cloud_tests.images import base |
1199 | from tests.cloud_tests.snapshots import lxd as lxd_snapshot |
1200 | +from tests.cloud_tests import util |
1201 | + |
1202 | +import os |
1203 | +import shutil |
1204 | |
1205 | |
1206 | class LXDImage(base.Image): |
1207 | @@ -10,27 +14,44 @@ class LXDImage(base.Image): |
1208 | """ |
1209 | platform_name = "lxd" |
1210 | |
1211 | - def __init__(self, name, config, platform, pylxd_image): |
1212 | + def __init__(self, platform, config, pylxd_image): |
1213 | """ |
1214 | - setup |
1215 | + Set up image |
1216 | + platform: platform object |
1217 | + config: image configuration |
1218 | """ |
1219 | - self.platform = platform |
1220 | - self._pylxd_image = pylxd_image |
1221 | + self.modified = False |
1222 | self._instance = None |
1223 | - super(LXDImage, self).__init__(name, config, platform) |
1224 | + self._pylxd_image = None |
1225 | + self.pylxd_image = pylxd_image |
1226 | + super(LXDImage, self).__init__(platform, config) |
1227 | |
1228 | @property |
1229 | def pylxd_image(self): |
1230 | - self._pylxd_image.sync() |
1231 | + if self._pylxd_image: |
1232 | + self._pylxd_image.sync() |
1233 | return self._pylxd_image |
1234 | |
1235 | + @pylxd_image.setter |
1236 | + def pylxd_image(self, pylxd_image): |
1237 | + if self._instance: |
1238 | + self._instance.destroy() |
1239 | + self._instance = None |
1240 | + if (self._pylxd_image and |
1241 | + (self._pylxd_image is not pylxd_image) and |
1242 | + (not self.config.get('cache_base_image') or self.modified)): |
1243 | + self._pylxd_image.delete(wait=True) |
1244 | + self.modified = False |
1245 | + self._pylxd_image = pylxd_image |
1246 | + |
1247 | @property |
1248 | def instance(self): |
1249 | if not self._instance: |
1250 | self._instance = self.platform.launch_container( |
1251 | - image=self.pylxd_image.fingerprint, |
1252 | - image_desc=str(self), use_desc='image-modification') |
1253 | - self._instance.start(wait=True, wait_time=self.config.get('timeout')) |
1254 | + self.properties, self.config, self.features, |
1255 | + use_desc='image-modification', image_desc=str(self), |
1256 | + image=self.pylxd_image.fingerprint) |
1257 | + self._instance.start() |
1258 | return self._instance |
1259 | |
1260 | @property |
1261 | @@ -46,6 +67,78 @@ class LXDImage(base.Image): |
1262 | 'release': properties.get('release'), |
1263 | } |
1264 | |
1265 | + def export_image(self, output_dir): |
1266 | + """ |
1267 | + export image from lxd image store to (split) tarball on disk |
1268 | + output_dir: dir to store tarballs in |
1269 | + return_value: tuple of path to metadata tarball and rootfs tarball |
1270 | + """ |
1271 | + # pylxd's image export feature doesn't do split exports, so use cmdline |
1272 | + util.subp(['lxc', 'image', 'export', self.pylxd_image.fingerprint, |
1273 | + output_dir], capture=True) |
1274 | + tarballs = [p for p in os.listdir(output_dir) if p.endswith('tar.xz')] |
1275 | + metadata = os.path.join( |
1276 | + output_dir, next(p for p in tarballs if p.startswith('meta-'))) |
1277 | + rootfs = os.path.join( |
1278 | + output_dir, next(p for p in tarballs if not p.startswith('meta-'))) |
1279 | + return (metadata, rootfs) |
1280 | + |
1281 | + def import_image(self, metadata, rootfs): |
1282 | + """ |
1283 | + import image to lxd image store from (split) tarball on disk |
1284 | + note, this will replace and delete the current pylxd_image |
1285 | + metadata: metadata tarball |
1286 | + rootfs: rootfs tarball |
1287 | + return_value: imported image fingerprint |
1288 | + """ |
1289 | + alias = util.gen_instance_name( |
1290 | + image_desc=str(self), use_desc='update-metadata') |
1291 | + util.subp(['lxc', 'image', 'import', metadata, rootfs, |
1292 | + '--alias', alias], capture=True) |
1293 | + self.pylxd_image = self.platform.query_image_by_alias(alias) |
1294 | + return self.pylxd_image.fingerprint |
1295 | + |
1296 | + def update_templates(self, template_config, template_data): |
1297 | + """ |
1298 | + update the image's template configuration |
1299 | + note, this will replace and delete the current pylxd_image |
1300 | + template_config: config overrides for template portion of metadata |
1301 | + template_data: template data to place into templates/ |
1302 | + """ |
1303 | + # set up tmp files |
1304 | + export_dir = util.tmpdir() |
1305 | + extract_dir = util.tmpdir() |
1306 | + new_metadata = os.path.join(export_dir, 'new-meta.tar.xz') |
1307 | + metadata_yaml = os.path.join(extract_dir, 'metadata.yaml') |
1308 | + template_dir = os.path.join(extract_dir, 'templates') |
1309 | + |
1310 | + try: |
1311 | + # extract old data |
1312 | + (metadata, rootfs) = self.export_image(export_dir) |
1313 | + shutil.unpack_archive(metadata, extract_dir) |
1314 | + |
1315 | + # update metadata |
1316 | + metadata = util.read_conf(metadata_yaml) |
1317 | + templates = metadata.get('templates', {}) |
1318 | + templates.update(template_config) |
1319 | + metadata['templates'] = templates |
1320 | + util.yaml_dump(metadata, metadata_yaml) |
1321 | + |
1322 | + # write out template files |
1323 | + for name, content in template_data.items(): |
1324 | + path = os.path.join(template_dir, name) |
1325 | + util.write_file(path, content) |
1326 | + |
1327 | + # store new data, mark new image as modified |
1328 | + util.flat_tar(new_metadata, extract_dir) |
1329 | + self.import_image(new_metadata, rootfs) |
1330 | + self.modified = True |
1331 | + |
1332 | + finally: |
1333 | + # remove tmpfiles |
1334 | + shutil.rmtree(export_dir) |
1335 | + shutil.rmtree(extract_dir) |
1336 | + |
1337 | def execute(self, *args, **kwargs): |
1338 | """ |
1339 | execute command in image, modifying image |
1340 | @@ -58,35 +151,43 @@ class LXDImage(base.Image): |
1341 | """ |
1342 | return self.instance.push_file(local_path, remote_path) |
1343 | |
1344 | - def run_script(self, script): |
1345 | + def run_script(self, *args, **kwargs): |
1346 | """ |
1347 | run script in image, modifying image |
1348 | return_value: script output |
1349 | """ |
1350 | - return self.instance.run_script(script) |
1351 | + return self.instance.run_script(*args, **kwargs) |
1352 | |
1353 | def snapshot(self): |
1354 | """ |
1355 | create snapshot of image, block until done |
1356 | """ |
1357 | - # clone current instance, start and freeze clone |
1358 | + # get empty user data to pass in to instance |
1359 | + # if overrides for user data provided, use them |
1360 | + empty_userdata = util.update_user_data( |
1361 | + {}, self.config.get('user_data_overrides', {})) |
1362 | + conf = {'user.user-data': empty_userdata} |
1363 | + # clone current instance |
1364 | instance = self.platform.launch_container( |
1365 | + self.properties, self.config, self.features, |
1366 | container=self.instance.name, image_desc=str(self), |
1367 | - use_desc='snapshot') |
1368 | - instance.start(wait=True, wait_time=self.config.get('timeout')) |
1369 | + use_desc='snapshot', container_config=conf) |
1370 | + # wait for cloud-init before boot_clean_script is run to ensure |
1371 | + # /var/lib/cloud is removed cleanly |
1372 | + instance.start(wait=True, wait_for_cloud_init=True) |
1373 | if self.config.get('boot_clean_script'): |
1374 | instance.run_script(self.config.get('boot_clean_script')) |
1375 | + # freeze current instance and return snapshot |
1376 | instance.freeze() |
1377 | return lxd_snapshot.LXDSnapshot( |
1378 | - self.properties, self.config, self.platform, instance) |
1379 | + self.platform, self.properties, self.config, |
1380 | + self.features, instance) |
1381 | |
1382 | def destroy(self): |
1383 | """ |
1384 | clean up data associated with image |
1385 | """ |
1386 | - if self._instance: |
1387 | - self._instance.destroy() |
1388 | - self.pylxd_image.delete(wait=True) |
1389 | + self.pylxd_image = None |
1390 | super(LXDImage, self).destroy() |
1391 | |
1392 | # vi: ts=4 expandtab |
1393 | diff --git a/tests/cloud_tests/instances/base.py b/tests/cloud_tests/instances/base.py |
1394 | index 9559d28..252c4c5 100644 |
1395 | --- a/tests/cloud_tests/instances/base.py |
1396 | +++ b/tests/cloud_tests/instances/base.py |
1397 | @@ -1,8 +1,5 @@ |
1398 | # This file is part of cloud-init. See LICENSE file for license information. |
1399 | |
1400 | -import os |
1401 | -import uuid |
1402 | - |
1403 | |
1404 | class Instance(object): |
1405 | """ |
1406 | @@ -10,26 +7,39 @@ class Instance(object): |
1407 | """ |
1408 | platform_name = None |
1409 | |
1410 | - def __init__(self, name): |
1411 | + def __init__(self, platform, name, properties, config, features): |
1412 | """ |
1413 | - setup |
1414 | + Set up instance |
1415 | + platform: platform object |
1416 | + name: hostname of instance |
1417 | + properties: image properties |
1418 | + config: image config |
1419 | + features: supported feature flags |
1420 | """ |
1421 | + self.platform = platform |
1422 | self.name = name |
1423 | + self.properties = properties |
1424 | + self.config = config |
1425 | + self.features = features |
1426 | |
1427 | - def execute(self, command, stdin=None, stdout=None, stderr=None, env={}): |
1428 | + def execute(self, command, stdout=None, stderr=None, env={}, |
1429 | + rcs=None, description=None): |
1430 | """ |
1431 | + Execute command in instance, recording output, error and exit code. |
1432 | + Assumes functional networking and execution as root with the |
1433 | + target filesystem being available at /. |
1434 | + |
1435 | command: the command to execute as root inside the image |
1436 | - stdin, stderr, stdout: file handles |
1437 | + stdout, stderr: file handles to write output and error to |
1438 | env: environment variables |
1439 | - |
1440 | - Execute assumes functional networking and execution as root with the |
1441 | - target filesystem being available at /. |
1442 | + rcs: allowed return codes from command |
1443 | + description: purpose of command |
1444 | |
1445 | return_value: tuple containing stdout data, stderr data, exit code |
1446 | """ |
1447 | raise NotImplementedError |
1448 | |
1449 | - def read_data(self, remote_path, encode=False): |
1450 | + def read_data(self, remote_path, decode=False): |
1451 | """ |
1452 | read_data from instance filesystem |
1453 | remote_path: path in instance |
1454 | @@ -49,6 +59,8 @@ class Instance(object): |
1455 | def pull_file(self, remote_path, local_path): |
1456 | """ |
1457 | copy file at 'remote_path', from instance to 'local_path' |
1458 | + remote_path: path on remote instance |
1459 | + local_path: path on local instance |
1460 | """ |
1461 | with open(local_path, 'wb') as fp: |
1462 | fp.write(self.read_data(remote_path), encode=True) |
1463 | @@ -56,18 +68,34 @@ class Instance(object): |
1464 | def push_file(self, local_path, remote_path): |
1465 | """ |
1466 | copy file at 'local_path' to instance at 'remote_path' |
1467 | + local_path: path on local instance |
1468 | + remote_path: path on remote instance |
1469 | """ |
1470 | with open(local_path, 'rb') as fp: |
1471 | self.write_data(remote_path, fp.read()) |
1472 | |
1473 | - def run_script(self, script): |
1474 | + def run_script(self, script, rcs=None, description=None): |
1475 | """ |
1476 | run script in target and return stdout |
1477 | + script: script contents |
1478 | + rcs: allowed return codes from script |
1479 | + description: purpose of script |
1480 | + return_value: stdout from script |
1481 | """ |
1482 | - script_path = os.path.join('/tmp', str(uuid.uuid1())) |
1483 | - self.write_data(script_path, script) |
1484 | - (out, err, exit_code) = self.execute(['/bin/bash', script_path]) |
1485 | - return out |
1486 | + script_path = self.tmpfile() |
1487 | + try: |
1488 | + self.write_data(script_path, script) |
1489 | + return self.execute( |
1490 | + ['/bin/bash', script_path], rcs=rcs, description=description) |
1491 | + finally: |
1492 | + self.execute(['rm', script_path], rcs=rcs) |
1493 | + |
1494 | + def tmpfile(self): |
1495 | + """ |
1496 | + get a tmp file in the target |
1497 | + return_value: path to new file in target |
1498 | + """ |
1499 | + return self.execute(['mktemp'])[0].strip() |
1500 | |
1501 | def console_log(self): |
1502 | """ |
1503 | @@ -87,7 +115,7 @@ class Instance(object): |
1504 | """ |
1505 | raise NotImplementedError |
1506 | |
1507 | - def start(self, wait=True): |
1508 | + def start(self, wait=True, wait_for_cloud_init=False): |
1509 | """ |
1510 | start instance |
1511 | """ |
1512 | @@ -99,22 +127,32 @@ class Instance(object): |
1513 | """ |
1514 | pass |
1515 | |
1516 | - def _wait_for_cloud_init(self, wait_time): |
1517 | + def _wait_for_system(self, wait_for_cloud_init): |
1518 | """ |
1519 | wait until system has fully booted and cloud-init has finished |
1520 | + wait_time: maximum time to wait |
1521 | + return_value: None, may raise OSError if wait_time exceeded |
1522 | """ |
1523 | - if not wait_time: |
1524 | - return |
1525 | |
1526 | - found_msg = 'found' |
1527 | - cmd = ('for ((i=0;i<{wait};i++)); do [ -f "{file}" ] && ' |
1528 | - '{{ echo "{msg}";break; }} || sleep 1; done').format( |
1529 | - file='/run/cloud-init/result.json', |
1530 | - wait=wait_time, msg=found_msg) |
1531 | + def clean_test(test): |
1532 | + """ |
1533 | + clean formatting for system ready test testcase |
1534 | + """ |
1535 | + return ' '.join(l for l in test.strip().splitlines() |
1536 | + if not l.lstrip().startswith('#')) |
1537 | + |
1538 | + time = self.config['boot_timeout'] |
1539 | + tests = [self.config['system_ready_script']] |
1540 | + if wait_for_cloud_init: |
1541 | + tests.append(self.config['cloud_init_ready_script']) |
1542 | + |
1543 | + formatted_tests = ' && '.join(clean_test(t) for t in tests) |
1544 | + test_cmd = ('for ((i=0;i<{time};i++)); do {test} && exit 0; sleep 1; ' |
1545 | + 'done; exit 1;').format(time=time, test=formatted_tests) |
1546 | + cmd = ['/bin/bash', '-c', test_cmd] |
1547 | + |
1548 | + if self.execute(cmd, rcs=(0, 1))[-1] != 0: |
1549 | + raise OSError('timeout: after {}s system not started'.format(time)) |
1550 | |
1551 | - (out, err, exit) = self.execute(['/bin/bash', '-c', cmd]) |
1552 | - if out.strip() != found_msg: |
1553 | - raise OSError('timeout: after {}s, cloud-init has not started' |
1554 | - .format(wait_time)) |
1555 | |
1556 | # vi: ts=4 expandtab |
1557 | diff --git a/tests/cloud_tests/instances/lxd.py b/tests/cloud_tests/instances/lxd.py |
1558 | index f0aa121..dfc8363 100644 |
1559 | --- a/tests/cloud_tests/instances/lxd.py |
1560 | +++ b/tests/cloud_tests/instances/lxd.py |
1561 | @@ -1,6 +1,7 @@ |
1562 | # This file is part of cloud-init. See LICENSE file for license information. |
1563 | |
1564 | from tests.cloud_tests.instances import base |
1565 | +from tests.cloud_tests import util |
1566 | |
1567 | |
1568 | class LXDInstance(base.Instance): |
1569 | @@ -9,41 +10,69 @@ class LXDInstance(base.Instance): |
1570 | """ |
1571 | platform_name = "lxd" |
1572 | |
1573 | - def __init__(self, name, platform, pylxd_container): |
1574 | + def __init__(self, platform, name, properties, config, features, |
1575 | + pylxd_container): |
1576 | """ |
1577 | - setup |
1578 | + Set up instance |
1579 | + platform: platform object |
1580 | + name: hostname of instance |
1581 | + properties: image properties |
1582 | + config: image config |
1583 | + features: supported feature flags |
1584 | """ |
1585 | - self.platform = platform |
1586 | self._pylxd_container = pylxd_container |
1587 | - super(LXDInstance, self).__init__(name) |
1588 | + super(LXDInstance, self).__init__( |
1589 | + platform, name, properties, config, features) |
1590 | |
1591 | @property |
1592 | def pylxd_container(self): |
1593 | self._pylxd_container.sync() |
1594 | return self._pylxd_container |
1595 | |
1596 | - def execute(self, command, stdin=None, stdout=None, stderr=None, env={}): |
1597 | + def execute(self, command, stdout=None, stderr=None, env={}, |
1598 | + rcs=None, description=None): |
1599 | """ |
1600 | + Execute command in instance, recording output, error and exit code. |
1601 | + Assumes functional networking and execution as root with the |
1602 | + target filesystem being available at /. |
1603 | + |
1604 | command: the command to execute as root inside the image |
1605 | - stdin, stderr, stdout: file handles |
1606 | + stdout, stderr: file handles to write output and error to |
1607 | env: environment variables |
1608 | - |
1609 | - Execute assumes functional networking and execution as root with the |
1610 | - target filesystem being available at /. |
1611 | + rcs: allowed return codes from command |
1612 | + description: purpose of command |
1613 | |
1614 | return_value: tuple containing stdout data, stderr data, exit code |
1615 | """ |
1616 | - # TODO: the pylxd api handler for container.execute needs to be |
1617 | - # extended to properly pass in stdin |
1618 | - # TODO: the pylxd api handler for container.execute needs to be |
1619 | - # extended to get the return code, for now just use 0 |
1620 | + # ensure instance is running and execute the command |
1621 | self.start() |
1622 | - if stdin: |
1623 | - raise NotImplementedError |
1624 | res = self.pylxd_container.execute(command, environment=env) |
1625 | - for (f, data) in (i for i in zip((stdout, stderr), res) if i[0]): |
1626 | - f.write(data) |
1627 | - return res + (0,) |
1628 | + |
1629 | + # get out, exit and err from pylxd return |
1630 | + if hasattr(res, 'exit_code'): |
1631 | + # pylxd 2.2 returns ContainerExecuteResult, named tuple of |
1632 | + # (exit_code, out, err) |
1633 | + (exit, out, err) = res |
1634 | + else: |
1635 | + # pylxd 2.1.3 and earlier only return out and err, no exit |
1636 | + # LOG.warning('using pylxd version < 2.2') |
1637 | + (out, err) = res |
1638 | + exit = 0 |
1639 | + |
1640 | + # write data to file descriptors if needed |
1641 | + if stdout: |
1642 | + stdout.write(out) |
1643 | + if stderr: |
1644 | + stderr.write(err) |
1645 | + |
1646 | + # if the command exited with a code not allowed in rcs, then fail |
1647 | + if exit not in (rcs if rcs else (0,)): |
1648 | + error_desc = ('Failed command to: {}'.format(description) |
1649 | + if description else None) |
1650 | + raise util.InTargetExecuteError( |
1651 | + out, err, exit, command, self.name, error_desc) |
1652 | + |
1653 | + return (out, err, exit) |
1654 | |
1655 | def read_data(self, remote_path, decode=False): |
1656 | """ |
1657 | @@ -83,14 +112,14 @@ class LXDInstance(base.Instance): |
1658 | if self.pylxd_container.status != 'Stopped': |
1659 | self.pylxd_container.stop(wait=wait) |
1660 | |
1661 | - def start(self, wait=True, wait_time=None): |
1662 | + def start(self, wait=True, wait_for_cloud_init=False): |
1663 | """ |
1664 | start instance |
1665 | """ |
1666 | if self.pylxd_container.status != 'Running': |
1667 | self.pylxd_container.start(wait=wait) |
1668 | - if wait and isinstance(wait_time, int): |
1669 | - self._wait_for_cloud_init(wait_time) |
1670 | + if wait: |
1671 | + self._wait_for_system(wait_for_cloud_init) |
1672 | |
1673 | def freeze(self): |
1674 | """ |
1675 | diff --git a/tests/cloud_tests/platforms.yaml b/tests/cloud_tests/platforms.yaml |
1676 | index 5972b32..b91834a 100644 |
1677 | --- a/tests/cloud_tests/platforms.yaml |
1678 | +++ b/tests/cloud_tests/platforms.yaml |
1679 | @@ -10,7 +10,55 @@ default_platform_config: |
1680 | platforms: |
1681 | lxd: |
1682 | enabled: true |
1683 | - get_image_timeout: 600 |
1684 | + # overrides for image templates |
1685 | + template_overrides: |
1686 | + /var/lib/cloud/seed/nocloud-net/meta-data: |
1687 | + when: |
1688 | + - create |
1689 | + - copy |
1690 | + template: cloud-init-meta.tpl |
1691 | + /var/lib/cloud/seed/nocloud-net/network-config: |
1692 | + when: |
1693 | + - create |
1694 | + - copy |
1695 | + template: cloud-init-network.tpl |
1696 | + /var/lib/cloud/seed/nocloud-net/user-data: |
1697 | + when: |
1698 | + - create |
1699 | + - copy |
1700 | + template: cloud-init-user.tpl |
1701 | + properties: |
1702 | + default: | |
1703 | + #cloud-config |
1704 | + {} |
1705 | + /var/lib/cloud/seed/nocloud-net/vendor-data: |
1706 | + when: |
1707 | + - create |
1708 | + - copy |
1709 | + template: cloud-init-vendor.tpl |
1710 | + properties: |
1711 | + default: | |
1712 | + #cloud-config |
1713 | + {} |
1714 | + # overrides image template files |
1715 | + template_files: |
1716 | + cloud-init-meta.tpl: | |
1717 | + #cloud-config |
1718 | + instance-id: {{ container.name }} |
1719 | + local-hostname: {{ container.name }} |
1720 | + {{ config_get("user.meta-data", "") }} |
1721 | + cloud-init-network.tpl: | |
1722 | + {% if config_get("user.network-config", "") == "" %}version: 1 |
1723 | + config: |
1724 | + - type: physical |
1725 | + name: eth0 |
1726 | + subnets: |
1727 | + - type: {% if config_get("user.network_mode", "") == "link-local" %}manual{% else %}dhcp{% endif %} |
1728 | + control: auto{% else %}{{ config_get("user.network-config", "") }}{% endif %} |
1729 | + cloud-init-user.tpl: | |
1730 | + {{ config_get("user.user-data", properties.default) }} |
1731 | + cloud-init-vendor.tpl: | |
1732 | + {{ config_get("user.vendor-data", properties.default) }} |
1733 | ec2: {} |
1734 | azure: {} |
1735 | |
1736 | diff --git a/tests/cloud_tests/platforms/base.py b/tests/cloud_tests/platforms/base.py |
1737 | index 615e2e0..2b6e514 100644 |
1738 | --- a/tests/cloud_tests/platforms/base.py |
1739 | +++ b/tests/cloud_tests/platforms/base.py |
1740 | @@ -15,17 +15,7 @@ class Platform(object): |
1741 | |
1742 | def get_image(self, img_conf): |
1743 | """ |
1744 | - Get image using 'img_conf', where img_conf is a dict containing all |
1745 | - image configuration parameters |
1746 | - |
1747 | - in this dict there must be a 'platform_ident' key containing |
1748 | - configuration for identifying each image on a per platform basis |
1749 | - |
1750 | - see implementations for get_image() for details about the contents |
1751 | - of the platform's config entry |
1752 | - |
1753 | - note: see 'releases' main_config.yaml for example entries |
1754 | - |
1755 | + get image using specified image configuration |
1756 | img_conf: configuration for image |
1757 | return_value: cloud_tests.images instance |
1758 | """ |
1759 | @@ -37,17 +27,4 @@ class Platform(object): |
1760 | """ |
1761 | pass |
1762 | |
1763 | - def _extract_img_platform_config(self, img_conf): |
1764 | - """ |
1765 | - extract platform configuration for current platform from img_conf |
1766 | - """ |
1767 | - platform_ident = img_conf.get('platform_ident') |
1768 | - if not platform_ident: |
1769 | - raise ValueError('invalid img_conf, missing \'platform_ident\'') |
1770 | - ident = platform_ident.get(self.platform_name) |
1771 | - if not ident: |
1772 | - raise ValueError('img_conf: {} missing config for platform {}' |
1773 | - .format(img_conf, self.platform_name)) |
1774 | - return ident |
1775 | - |
1776 | # vi: ts=4 expandtab |
1777 | diff --git a/tests/cloud_tests/platforms/lxd.py b/tests/cloud_tests/platforms/lxd.py |
1778 | index 847cc54..4d8b58c 100644 |
1779 | --- a/tests/cloud_tests/platforms/lxd.py |
1780 | +++ b/tests/cloud_tests/platforms/lxd.py |
1781 | @@ -27,28 +27,32 @@ class LXDPlatform(base.Platform): |
1782 | |
1783 | def get_image(self, img_conf): |
1784 | """ |
1785 | - Get image |
1786 | - img_conf: dict containing config for image. platform_ident must have: |
1787 | - alias: alias to use for simplestreams server |
1788 | - sstreams_server: simplestreams server to use, or None for default |
1789 | + get image using specified image configuration |
1790 | + img_conf: configuration for image |
1791 | return_value: cloud_tests.images instance |
1792 | """ |
1793 | - lxd_conf = self._extract_img_platform_config(img_conf) |
1794 | - image = self.client.images.create_from_simplestreams( |
1795 | - lxd_conf.get('sstreams_server', DEFAULT_SSTREAMS_SERVER), |
1796 | - lxd_conf['alias']) |
1797 | - return lxd_image.LXDImage( |
1798 | - image.properties['description'], img_conf, self, image) |
1799 | + pylxd_image = self.client.images.create_from_simplestreams( |
1800 | + img_conf.get('sstreams_server', DEFAULT_SSTREAMS_SERVER), |
1801 | + img_conf['alias']) |
1802 | + image = lxd_image.LXDImage(self, img_conf, pylxd_image) |
1803 | + if img_conf.get('override_templates', False): |
1804 | + image.update_templates(self.config.get('template_overrides', {}), |
1805 | + self.config.get('template_files', {})) |
1806 | + return image |
1807 | |
1808 | - def launch_container(self, image=None, container=None, ephemeral=False, |
1809 | - config=None, block=True, |
1810 | - image_desc=None, use_desc=None): |
1811 | + def launch_container(self, properties, config, features, |
1812 | + image=None, container=None, ephemeral=False, |
1813 | + container_config=None, block=True, image_desc=None, |
1814 | + use_desc=None): |
1815 | """ |
1816 | launch a container |
1817 | + properties: image properties |
1818 | + config: image configuration |
1819 | + features: image features |
1820 | image: image fingerprint to launch from |
1821 | container: container to copy |
1822 | ephemeral: delete image after first shutdown |
1823 | - config: config options for instance as dict |
1824 | + container_config: config options for instance as dict |
1825 | block: wait until container created |
1826 | image_desc: description of image being launched |
1827 | use_desc: description of container's use |
1828 | @@ -61,11 +65,13 @@ class LXDPlatform(base.Platform): |
1829 | use_desc=use_desc, |
1830 | used_list=self.list_containers()), |
1831 | 'ephemeral': bool(ephemeral), |
1832 | - 'config': config if isinstance(config, dict) else {}, |
1833 | + 'config': (container_config |
1834 | + if isinstance(container_config, dict) else {}), |
1835 | 'source': ({'type': 'image', 'fingerprint': image} if image else |
1836 | {'type': 'copy', 'source': container}) |
1837 | }, wait=block) |
1838 | - return lxd_instance.LXDInstance(container.name, self, container) |
1839 | + return lxd_instance.LXDInstance(self, container.name, properties, |
1840 | + config, features, container) |
1841 | |
1842 | def container_exists(self, container_name): |
1843 | """ |
1844 | @@ -88,6 +94,14 @@ class LXDPlatform(base.Platform): |
1845 | """ |
1846 | return [container.name for container in self.client.containers.all()] |
1847 | |
1848 | + def query_image_by_alias(self, alias): |
1849 | + """ |
1850 | + get image by alias in local image store |
1851 | + alias: alias of image |
1852 | + return_value: pylxd image (not cloud_tests.images instance) |
1853 | + """ |
1854 | + return self.client.images.get_by_alias(alias) |
1855 | + |
1856 | def destroy(self): |
1857 | """ |
1858 | Clean up platform data |
1859 | diff --git a/tests/cloud_tests/releases.yaml b/tests/cloud_tests/releases.yaml |
1860 | index 3ffa68f..4b5d0c3 100644 |
1861 | --- a/tests/cloud_tests/releases.yaml |
1862 | +++ b/tests/cloud_tests/releases.yaml |
1863 | @@ -1,79 +1,281 @@ |
1864 | # ============================= Release Config ================================ |
1865 | default_release_config: |
1866 | - # all are disabled by default |
1867 | - enabled: false |
1868 | - # timeout for booting image and running cloud init |
1869 | - timeout: 120 |
1870 | - # platform_ident values for the image, with data to identify the image |
1871 | - # on that platform. see platforms.base for more information |
1872 | - platform_ident: {} |
1873 | - # a script to run after a boot that is used to modify an image, before |
1874 | - # making a snapshot of the image. may be useful for removing data left |
1875 | - # behind from cloud-init booting, such as logs, to ensure that data from |
1876 | - # snapshot.launch() will not include a cloud-init.log from a boot used to |
1877 | - # create the snapshot, if cloud-init has not run |
1878 | - boot_clean_script: | |
1879 | - #!/bin/bash |
1880 | - rm -rf /var/log/cloud-init.log /var/log/cloud-init-output.log \ |
1881 | - /var/lib/cloud/ /run/cloud-init/ /var/log/syslog |
1882 | + # global default configuration options |
1883 | + default: |
1884 | + # all are disabled by default |
1885 | + enabled: false |
1886 | + # timeout for booting image and running cloud init |
1887 | + boot_timeout: 120 |
1888 | + # a script to run after a boot that is used to modify an image, before |
1889 | + # making a snapshot of the image. may be useful for removing data left |
1890 | + # behind from cloud-init booting, such as logs, to ensure that data |
1891 | + # from snapshot.launch() will not include a cloud-init.log from a boot |
1892 | + # used to create the snapshot, if cloud-init has not run |
1893 | + boot_clean_script: | |
1894 | + #!/bin/bash |
1895 | + rm -rf /var/log/cloud-init.log /var/log/cloud-init-output.log \ |
1896 | + /var/lib/cloud/ /run/cloud-init/ /var/log/syslog |
1897 | + # test script to determine if system is booted fully |
1898 | + system_ready_script: | |
1899 | + # permit running or degraded state as both indicate complete boot |
1900 | + [ $(systemctl is-system-running) = 'running' -o |
1901 | + $(systemctl is-system-running) = 'degraded' ] |
1902 | + # test script to determine if cloud-init has finished |
1903 | + cloud_init_ready_script: | |
1904 | + [ -f '/run/cloud-init/result.json' ] |
1905 | + # currently used features and their uses are: |
1906 | + # features groups and additional feature settings |
1907 | + feature_groups: [] |
1908 | + features: {} |
1909 | + |
1910 | + # lxd specific default configuration options |
1911 | + lxd: |
1912 | + # default sstreams server to use for lxd image retrieval |
1913 | + sstreams_server: https://us.images.linuxcontainers.org:8443 |
1914 | + # keep base image, avoids downloading again next run |
1915 | + cache_base_image: true |
1916 | + # lxd images from linuxcontainers.org do not have the nocloud seed |
1917 | + # templates in place, so the image metadata must be modified |
1918 | + override_templates: true |
1919 | + # arg overrides to set image up |
1920 | + setup_overrides: |
1921 | + # lxd images from linuxcontainers.org do not come with |
1922 | + # cloud-init, so must pull cloud-init in from repo using |
1923 | + # setup_image.upgrade |
1924 | + upgrade: true |
1925 | + |
1926 | +features: |
1927 | + # all currently supported feature flags |
1928 | + all: |
1929 | + - apt # image supports apt package manager |
1930 | + - byobu # byobu is available in repositories |
1931 | + - landscape # landscape-client available in repos |
1932 | + - lxd # lxd is available in the image |
1933 | + - ppa # image supports ppas |
1934 | + - rpm # image supports rpms |
1935 | + - snap # supports snapd |
1936 | + # NOTE: the following feature flags are to work around bugs in the |
1937 | + # images, and can be removed when no longer needed |
1938 | + - hostname # setting system hostname works |
1939 | + # NOTE: the following feature flags are to work around issues in the |
1940 | + # testcases, and can be removed when no longer needed |
1941 | + - apt_src_cont # default contents and format of sources.list matches |
1942 | + # ubuntu sources.list |
1943 | + - apt_hist_fmt # apt command history entries use full paths to apt |
1944 | + # executable rather than relative paths |
1945 | + - daylight_time # timezones are daylight not standard time |
1946 | + - apt_up_out # 'Calculating upgrade..' present in log output from |
1947 | + # apt-get dist-upgrade output |
1948 | + - engb_locale # locale en_GB.UTF-8 is available |
1949 | + - locale_gen # the /etc/locale.gen file exists |
1950 | + - no_ntpdate # 'ntpdate' is not installed by default |
1951 | + - no_file_fmt_e # the 'file' utility does not have a formatting error |
1952 | + - ppa_file_name # the name of the source file added to sources.list.d has |
1953 | + # the expected format for newer ubuntu releases |
1954 | + - sshd # requires ssh server to be installed by default |
1955 | + - ssh_key_fmt # ssh auth keys printed to console have expected format |
1956 | + - syslog # test case requires syslog to be written by default |
1957 | + - ubuntu_ntp # expect ubuntu.pool.ntp.org to be used as ntp server |
1958 | + - ubuntu_repos # test case requres ubuntu repositories to be used |
1959 | + - ubuntu_user # test case needs user with the name 'ubuntu' to exist |
1960 | + # NOTE: the following feature flags are to work around issues that may |
1961 | + # be considered bugs in cloud-init |
1962 | + - lsb_release # image has lsb_release installed, maybe should install |
1963 | + # if missing by default |
1964 | + - sudo # image has sudo installed, should not be required |
1965 | + # feature flag groups |
1966 | + groups: |
1967 | + base: |
1968 | + hostname: true |
1969 | + no_file_fmt_e: true |
1970 | + ubuntu_specific: |
1971 | + apt_src_cont: true |
1972 | + apt_hist_fmt: true |
1973 | + byobu: true |
1974 | + daylight_time: true |
1975 | + engb_locale: true |
1976 | + landscape: true |
1977 | + locale_gen: true |
1978 | + lsb_release: true |
1979 | + lxd: true |
1980 | + ppa: true |
1981 | + ppa_file_name: true |
1982 | + snap: true |
1983 | + sshd: true |
1984 | + ssh_key_fmt: true |
1985 | + sudo: true |
1986 | + syslog: true |
1987 | + ubuntu_ntp: true |
1988 | + ubuntu_repos: true |
1989 | + ubuntu_user: true |
1990 | + debian_base: |
1991 | + apt: true |
1992 | + apt_up_out: true |
1993 | + no_ntpdate: true |
1994 | + rhel_base: |
1995 | + rpm: true |
1996 | |
1997 | releases: |
1998 | - trusty: |
1999 | - enabled: true |
2000 | - platform_ident: |
2001 | - lxd: |
2002 | - # if sstreams_server is omitted, default is used, defined in |
2003 | - # tests.cloud_tests.platforms.lxd.DEFAULT_SSTREAMS_SERVER as: |
2004 | - # sstreams_server: https://us.images.linuxcontainers.org:8443 |
2005 | - #alias: ubuntu/trusty/default |
2006 | - alias: t |
2007 | - sstreams_server: https://cloud-images.ubuntu.com/daily |
2008 | - xenial: |
2009 | - enabled: true |
2010 | - platform_ident: |
2011 | - lxd: |
2012 | - #alias: ubuntu/xenial/default |
2013 | - alias: x |
2014 | - sstreams_server: https://cloud-images.ubuntu.com/daily |
2015 | - yakkety: |
2016 | - enabled: true |
2017 | - platform_ident: |
2018 | - lxd: |
2019 | - #alias: ubuntu/yakkety/default |
2020 | - alias: y |
2021 | - sstreams_server: https://cloud-images.ubuntu.com/daily |
2022 | + # UBUNTU ================================================================= |
2023 | zesty: |
2024 | - enabled: true |
2025 | - platform_ident: |
2026 | - lxd: |
2027 | - #alias: ubuntu/zesty/default |
2028 | - alias: z |
2029 | - sstreams_server: https://cloud-images.ubuntu.com/daily |
2030 | - jessie: |
2031 | - platform_ident: |
2032 | - lxd: |
2033 | - alias: debian/jessie/default |
2034 | + # EOL: Jan 2018 |
2035 | + default: |
2036 | + enabled: true |
2037 | + feature_groups: |
2038 | + - base |
2039 | + - debian_base |
2040 | + - ubuntu_specific |
2041 | + lxd: |
2042 | + sstreams_server: https://cloud-images.ubuntu.com/daily |
2043 | + alias: zesty |
2044 | + setup_overrides: null |
2045 | + override_templates: false |
2046 | + yakkety: |
2047 | + # EOL: Jul 2017 |
2048 | + default: |
2049 | + enabled: true |
2050 | + feature_groups: |
2051 | + - base |
2052 | + - debian_base |
2053 | + - ubuntu_specific |
2054 | + lxd: |
2055 | + sstreams_server: https://cloud-images.ubuntu.com/daily |
2056 | + alias: yakkety |
2057 | + setup_overrides: null |
2058 | + override_templates: false |
2059 | + xenial: |
2060 | + # EOL: Apr 2021 |
2061 | + default: |
2062 | + enabled: true |
2063 | + feature_groups: |
2064 | + - base |
2065 | + - debian_base |
2066 | + - ubuntu_specific |
2067 | + lxd: |
2068 | + sstreams_server: https://cloud-images.ubuntu.com/daily |
2069 | + alias: xenial |
2070 | + setup_overrides: null |
2071 | + override_templates: false |
2072 | + trusty: |
2073 | + # EOL: Apr 2019 |
2074 | + default: |
2075 | + enabled: true |
2076 | + feature_groups: |
2077 | + - base |
2078 | + - debian_base |
2079 | + - ubuntu_specific |
2080 | + features: |
2081 | + apt_up_out: false |
2082 | + locale_gen: false |
2083 | + lxd: false |
2084 | + ppa_file_name: false |
2085 | + snap: false |
2086 | + ssh_key_fmt: false |
2087 | + no_ntpdate: false |
2088 | + no_file_fmt_e: false |
2089 | + system_ready_script: | |
2090 | + #!/bin/bash |
2091 | + # upstart based, so use old style runlevels |
2092 | + [ $(runlevel | awk '{print $2}') = '2' ] |
2093 | + lxd: |
2094 | + sstreams_server: https://cloud-images.ubuntu.com/daily |
2095 | + alias: trusty |
2096 | + setup_overrides: null |
2097 | + override_templates: false |
2098 | + precise: |
2099 | + # EOL: Apr 2017 |
2100 | + default: |
2101 | + # still supported but not relevant for development, not enabled |
2102 | + # tests should still work though unless they use newer features |
2103 | + enabled: false |
2104 | + feature_groups: |
2105 | + - base |
2106 | + - debian_base |
2107 | + - ubuntu_specific |
2108 | + features: |
2109 | + lxd: false |
2110 | + system_ready_script: | |
2111 | + #!/bin/bash |
2112 | + # upstart based, so use old style runlevels |
2113 | + [ $(runlevel | awk '{print $2}') = '2' ] |
2114 | + lxd: |
2115 | + sstreams_server: https://cloud-images.ubuntu.com/daily |
2116 | + alias: precise |
2117 | + setup_overrides: null |
2118 | + override_templates: false |
2119 | + # DEBIAN ================================================================= |
2120 | sid: |
2121 | - platform_ident: |
2122 | - lxd: |
2123 | - alias: debian/sid/default |
2124 | + # EOL: N/A |
2125 | + default: |
2126 | + # tests should work on sid, however it is not always stable |
2127 | + enabled: false |
2128 | + feature_groups: |
2129 | + - base |
2130 | + - debian_base |
2131 | + lxd: |
2132 | + alias: debian/sid/default |
2133 | stretch: |
2134 | - platform_ident: |
2135 | - lxd: |
2136 | - alias: debian/stretch/default |
2137 | + # EOL: Not yet released |
2138 | + default: |
2139 | + enabled: true |
2140 | + feature_groups: |
2141 | + - base |
2142 | + - debian_base |
2143 | + lxd: |
2144 | + alias: debian/stretch/default |
2145 | + jessie: |
2146 | + # EOL: Jun 2020 |
2147 | + # NOTE: the cloud-init version shipped with jessie is out of date |
2148 | + # tests work if an up to date deb is used |
2149 | + default: |
2150 | + enabled: true |
2151 | + feature_groups: |
2152 | + - base |
2153 | + - debian_base |
2154 | + lxd: |
2155 | + alias: debian/jessie/default |
2156 | wheezy: |
2157 | - platform_ident: |
2158 | - lxd: |
2159 | - alias: debian/wheezy/default |
2160 | + # EOL: May 2018 (Apr 2016 - end of full updates) |
2161 | + default: |
2162 | + # this is old enough that it is no longer relevant for development |
2163 | + enabled: false |
2164 | + feature_groups: |
2165 | + - base |
2166 | + - debian_base |
2167 | + lxd: |
2168 | + alias: debian/wheezy/default |
2169 | + # CENTOS ================================================================= |
2170 | centos70: |
2171 | - timeout: 180 |
2172 | - platform_ident: |
2173 | - lxd: |
2174 | - alias: centos/7/default |
2175 | + # EOL: Jun 2024 (2020 - end of full updates) |
2176 | + default: |
2177 | + enabled: true |
2178 | + feature_groups: |
2179 | + - base |
2180 | + - rhel_base |
2181 | + user_data_overrides: |
2182 | + preserve_hostname: true |
2183 | + lxd: |
2184 | + features: |
2185 | + # NOTE: (LP: #1575779) |
2186 | + hostname: false |
2187 | + alias: centos/7/default |
2188 | centos66: |
2189 | - timeout: 180 |
2190 | - platform_ident: |
2191 | - lxd: |
2192 | - alias: centos/6/default |
2193 | + # EOL: Nov 2020 |
2194 | + default: |
2195 | + enabled: true |
2196 | + feature_groups: |
2197 | + - base |
2198 | + - rhel_base |
2199 | + # still supported, but only bugfixes after may 2017 |
2200 | + system_ready_script: | |
2201 | + #!/bin/bash |
2202 | + [ $(runlevel | awk '{print $2}') = '3' ] |
2203 | + user_data_overrides: |
2204 | + preserve_hostname: true |
2205 | + lxd: |
2206 | + features: |
2207 | + # NOTE: (LP: #1575779) |
2208 | + hostname: false |
2209 | + alias: centos/6/default |
2210 | |
2211 | # vi: ts=4 expandtab |
2212 | diff --git a/tests/cloud_tests/setup_image.py b/tests/cloud_tests/setup_image.py |
2213 | index 5d6c638..1b74ceb 100644 |
2214 | --- a/tests/cloud_tests/setup_image.py |
2215 | +++ b/tests/cloud_tests/setup_image.py |
2216 | @@ -7,6 +7,30 @@ from functools import partial |
2217 | import os |
2218 | |
2219 | |
2220 | +def installed_version(image, package, ensure_installed=True): |
2221 | + """ |
2222 | + get installed version of package |
2223 | + image: cloud_tests.images instance to operate on |
2224 | + package: name of package |
2225 | + ensure_installed: raise error if not installed |
2226 | + return_value: cloud-init version string |
2227 | + """ |
2228 | + # get right cmd for os family |
2229 | + os_family = util.get_os_family(image.properties['os']) |
2230 | + if os_family == 'debian': |
2231 | + cmd = ['dpkg-query', '-W', "--showformat='${Version}'", package] |
2232 | + elif os_family == 'redhat': |
2233 | + cmd = ['rpm', '-q', '--queryformat', "'%{VERSION}'", package] |
2234 | + else: |
2235 | + raise NotImplementedError |
2236 | + |
2237 | + # query version |
2238 | + msg = 'query version for package: {}'.format(package) |
2239 | + (out, err, exit) = image.execute( |
2240 | + cmd, description=msg, rcs=(0,) if ensure_installed else range(0, 256)) |
2241 | + return out.strip() |
2242 | + |
2243 | + |
2244 | def install_deb(args, image): |
2245 | """ |
2246 | install deb into image |
2247 | @@ -21,20 +45,18 @@ def install_deb(args, image): |
2248 | 'family: {}'.format(args.deb, os_family)) |
2249 | |
2250 | # install deb |
2251 | - LOG.debug('installing deb: %s into target', args.deb) |
2252 | + msg = 'install deb: "{}" into target'.format(args.deb) |
2253 | + LOG.debug(msg) |
2254 | remote_path = os.path.join('/tmp', os.path.basename(args.deb)) |
2255 | image.push_file(args.deb, remote_path) |
2256 | - (out, err, exit) = image.execute(['dpkg', '-i', remote_path]) |
2257 | - if exit != 0: |
2258 | - raise OSError('failed install deb: {}\n\tstdout: {}\n\tstderr: {}' |
2259 | - .format(args.deb, out, err)) |
2260 | + cmd = 'dpkg -i {} || apt-get install --yes -f'.format(remote_path) |
2261 | + image.execute(['/bin/sh', '-c', cmd], description=msg) |
2262 | |
2263 | # check installed deb version matches package |
2264 | fmt = ['-W', "--showformat='${Version}'"] |
2265 | (out, err, exit) = image.execute(['dpkg-deb'] + fmt + [remote_path]) |
2266 | expected_version = out.strip() |
2267 | - (out, err, exit) = image.execute(['dpkg-query'] + fmt + ['cloud-init']) |
2268 | - found_version = out.strip() |
2269 | + found_version = installed_version(image, 'cloud-init') |
2270 | if expected_version != found_version: |
2271 | raise OSError('install deb version "{}" does not match expected "{}"' |
2272 | .format(found_version, expected_version)) |
2273 | @@ -52,24 +74,21 @@ def install_rpm(args, image): |
2274 | """ |
2275 | # ensure system is compatible with package format |
2276 | os_family = util.get_os_family(image.properties['os']) |
2277 | - if os_family not in ['redhat', 'sles']: |
2278 | + if os_family != 'redhat': |
2279 | raise NotImplementedError('install rpm: {} not supported on os ' |
2280 | 'family: {}'.format(args.rpm, os_family)) |
2281 | |
2282 | # install rpm |
2283 | - LOG.debug('installing rpm: %s into target', args.rpm) |
2284 | + msg = 'install rpm: "{}" into target'.format(args.rpm) |
2285 | + LOG.debug(msg) |
2286 | remote_path = os.path.join('/tmp', os.path.basename(args.rpm)) |
2287 | image.push_file(args.rpm, remote_path) |
2288 | - (out, err, exit) = image.execute(['rpm', '-U', remote_path]) |
2289 | - if exit != 0: |
2290 | - raise OSError('failed to install rpm: {}\n\tstdout: {}\n\tstderr: {}' |
2291 | - .format(args.rpm, out, err)) |
2292 | + image.execute(['rpm', '-U', remote_path], description=msg) |
2293 | |
2294 | fmt = ['--queryformat', '"%{VERSION}"'] |
2295 | (out, err, exit) = image.execute(['rpm', '-q'] + fmt + [remote_path]) |
2296 | expected_version = out.strip() |
2297 | - (out, err, exit) = image.execute(['rpm', '-q'] + fmt + ['cloud-init']) |
2298 | - found_version = out.strip() |
2299 | + found_version = installed_version(image, 'cloud-init') |
2300 | if expected_version != found_version: |
2301 | raise OSError('install rpm version "{}" does not match expected "{}"' |
2302 | .format(found_version, expected_version)) |
2303 | @@ -80,13 +99,34 @@ def install_rpm(args, image): |
2304 | |
2305 | def upgrade(args, image): |
2306 | """ |
2307 | - run the system's upgrade command |
2308 | + upgrade or install cloud-init from repo |
2309 | + args: cmdline arguments |
2310 | + image: cloud_tests.images instance to operate on |
2311 | + return_value: None, may raise errors |
2312 | + """ |
2313 | + # determine command for os_family |
2314 | + os_family = util.get_os_family(image.properties['os']) |
2315 | + if os_family == 'debian': |
2316 | + cmd = 'apt-get update && apt-get install cloud-init --yes' |
2317 | + elif os_family == 'redhat': |
2318 | + cmd = 'yum install cloud-init --assumeyes' |
2319 | + else: |
2320 | + raise NotImplementedError |
2321 | + |
2322 | + # upgrade cloud-init |
2323 | + msg = 'upgrading cloud-init' |
2324 | + LOG.debug(msg) |
2325 | + image.execute(['/bin/sh', '-c', cmd], description=msg) |
2326 | + |
2327 | + |
2328 | +def upgrade_full(args, image): |
2329 | + """ |
2330 | + run the system's full upgrade command |
2331 | args: cmdline arguments |
2332 | image: cloud_tests.images instance to operate on |
2333 | return_value: None, may raise errors |
2334 | """ |
2335 | # determine appropriate upgrade command for os_family |
2336 | - # TODO: maybe use cloudinit.distros for this? |
2337 | os_family = util.get_os_family(image.properties['os']) |
2338 | if os_family == 'debian': |
2339 | cmd = 'apt-get update && apt-get upgrade --yes' |
2340 | @@ -97,11 +137,9 @@ def upgrade(args, image): |
2341 | 'from family: {}'.format(os_family)) |
2342 | |
2343 | # upgrade system |
2344 | - LOG.debug('upgrading system') |
2345 | - (out, err, exit) = image.execute(['/bin/sh', '-c', cmd]) |
2346 | - if exit != 0: |
2347 | - raise OSError('failed to upgrade system\n\tstdout: {}\n\tstderr:{}' |
2348 | - .format(out, err)) |
2349 | + msg = 'full system upgrade' |
2350 | + LOG.debug(msg) |
2351 | + image.execute(['/bin/sh', '-c', cmd], description=msg) |
2352 | |
2353 | |
2354 | def run_script(args, image): |
2355 | @@ -111,9 +149,9 @@ def run_script(args, image): |
2356 | image: cloud_tests.images instance to operate on |
2357 | return_value: None, may raise errors |
2358 | """ |
2359 | - # TODO: get exit status back from script and add error handling here |
2360 | - LOG.debug('running setup image script in target image') |
2361 | - image.run_script(args.script) |
2362 | + msg = 'run setup image script in target image' |
2363 | + LOG.debug(msg) |
2364 | + image.run_script(args.script, description=msg) |
2365 | |
2366 | |
2367 | def enable_ppa(args, image): |
2368 | @@ -124,17 +162,15 @@ def enable_ppa(args, image): |
2369 | return_value: None, may raise errors |
2370 | """ |
2371 | # ppa only supported on ubuntu (maybe debian?) |
2372 | - if image.properties['os'] != 'ubuntu': |
2373 | + if image.properties['os'].lower() != 'ubuntu': |
2374 | raise NotImplementedError('enabling a ppa is only available on ubuntu') |
2375 | |
2376 | # add ppa with add-apt-repository and update |
2377 | ppa = 'ppa:{}'.format(args.ppa) |
2378 | - LOG.debug('enabling %s', ppa) |
2379 | + msg = 'enable ppa: "{}" in target'.format(ppa) |
2380 | + LOG.debug(msg) |
2381 | cmd = 'add-apt-repository --yes {} && apt-get update'.format(ppa) |
2382 | - (out, err, exit) = image.execute(['/bin/sh', '-c', cmd]) |
2383 | - if exit != 0: |
2384 | - raise OSError('enable ppa for {} failed\n\tstdout: {}\n\tstderr: {}' |
2385 | - .format(ppa, out, err)) |
2386 | + image.execute(['/bin/sh', '-c', cmd], description=msg) |
2387 | |
2388 | |
2389 | def enable_repo(args, image): |
2390 | @@ -155,11 +191,9 @@ def enable_repo(args, image): |
2391 | raise NotImplementedError('enable repo command not configured for ' |
2392 | 'distro from family: {}'.format(os_family)) |
2393 | |
2394 | - LOG.debug('enabling repo: "%s"', args.repo) |
2395 | - (out, err, exit) = image.execute(['/bin/sh', '-c', cmd]) |
2396 | - if exit != 0: |
2397 | - raise OSError('enable repo {} failed\n\tstdout: {}\n\tstderr: {}' |
2398 | - .format(args.repo, out, err)) |
2399 | + msg = 'enable repo: "{}" in target'.format(args.repo) |
2400 | + LOG.debug(msg) |
2401 | + image.execute(['/bin/sh', '-c', cmd], description=msg) |
2402 | |
2403 | |
2404 | def setup_image(args, image): |
2405 | @@ -169,6 +203,11 @@ def setup_image(args, image): |
2406 | image: cloud_tests.image instance to operate on |
2407 | return_value: tuple of results and fail count |
2408 | """ |
2409 | + # update the args if necessary for this image |
2410 | + overrides = image.setup_overrides |
2411 | + LOG.debug('updating args for setup with: %s', overrides) |
2412 | + args = util.update_args(args, overrides, preserve_old=True) |
2413 | + |
2414 | # mapping of setup cmdline arg name to setup function |
2415 | # represented as a tuple rather than a dict or odict as lookup by name not |
2416 | # needed, and order is important as --script and --upgrade go at the end |
2417 | @@ -179,17 +218,19 @@ def setup_image(args, image): |
2418 | ('repo', enable_repo, 'setup func for --repo, enable repo'), |
2419 | ('ppa', enable_ppa, 'setup func for --ppa, enable ppa'), |
2420 | ('script', run_script, 'setup func for --script, run script'), |
2421 | - ('upgrade', upgrade, 'setup func for --upgrade, upgrade pkgs'), |
2422 | + ('upgrade', upgrade, 'setup func for --upgrade, upgrade cloud-init'), |
2423 | + ('upgrade-full', upgrade_full, 'setup func for --upgrade-full'), |
2424 | ) |
2425 | |
2426 | # determine which setup functions needed |
2427 | calls = [partial(stage.run_single, desc, partial(func, args, image)) |
2428 | for name, func, desc in handlers if getattr(args, name, None)] |
2429 | |
2430 | - image_name = 'image: distro={}, release={}'.format( |
2431 | - image.properties['os'], image.properties['release']) |
2432 | - LOG.info('setting up %s', image_name) |
2433 | - return stage.run_stage('set up for {}'.format(image_name), calls, |
2434 | - continue_after_error=False) |
2435 | + LOG.info('setting up %s', image) |
2436 | + res = stage.run_stage( |
2437 | + 'set up for {}'.format(image), calls, continue_after_error=False) |
2438 | + LOG.debug('after setup complete, installed cloud-init version is: %s', |
2439 | + installed_version(image, 'cloud-init')) |
2440 | + return res |
2441 | |
2442 | # vi: ts=4 expandtab |
2443 | diff --git a/tests/cloud_tests/snapshots/base.py b/tests/cloud_tests/snapshots/base.py |
2444 | index d715f03..cbe3f5f 100644 |
2445 | --- a/tests/cloud_tests/snapshots/base.py |
2446 | +++ b/tests/cloud_tests/snapshots/base.py |
2447 | @@ -7,12 +7,18 @@ class Snapshot(object): |
2448 | """ |
2449 | platform_name = None |
2450 | |
2451 | - def __init__(self, properties, config): |
2452 | + def __init__(self, platform, properties, config, features): |
2453 | """ |
2454 | Set up snapshot |
2455 | + platform: platform object |
2456 | + properties: image properties |
2457 | + config: image config |
2458 | + features: supported feature flags |
2459 | """ |
2460 | + self.platform = platform |
2461 | self.properties = properties |
2462 | self.config = config |
2463 | + self.features = features |
2464 | |
2465 | def __str__(self): |
2466 | """ |
2467 | diff --git a/tests/cloud_tests/snapshots/lxd.py b/tests/cloud_tests/snapshots/lxd.py |
2468 | index eabbce3..2241035 100644 |
2469 | --- a/tests/cloud_tests/snapshots/lxd.py |
2470 | +++ b/tests/cloud_tests/snapshots/lxd.py |
2471 | @@ -9,13 +9,18 @@ class LXDSnapshot(base.Snapshot): |
2472 | """ |
2473 | platform_name = "lxd" |
2474 | |
2475 | - def __init__(self, properties, config, platform, pylxd_frozen_instance): |
2476 | + def __init__(self, platform, properties, config, features, |
2477 | + pylxd_frozen_instance): |
2478 | """ |
2479 | Set up snapshot |
2480 | + platform: platform object |
2481 | + properties: image properties |
2482 | + config: image config |
2483 | + features: supported feature flags |
2484 | """ |
2485 | - self.platform = platform |
2486 | self.pylxd_frozen_instance = pylxd_frozen_instance |
2487 | - super(LXDSnapshot, self).__init__(properties, config) |
2488 | + super(LXDSnapshot, self).__init__( |
2489 | + platform, properties, config, features) |
2490 | |
2491 | def launch(self, user_data, meta_data=None, block=True, start=True, |
2492 | use_desc=None): |
2493 | @@ -34,10 +39,11 @@ class LXDSnapshot(base.Snapshot): |
2494 | if meta_data: |
2495 | inst_config['user.meta-data'] = meta_data |
2496 | instance = self.platform.launch_container( |
2497 | - container=self.pylxd_frozen_instance.name, config=inst_config, |
2498 | - block=block, image_desc=str(self), use_desc=use_desc) |
2499 | + self.properties, self.config, self.features, block=block, |
2500 | + image_desc=str(self), container=self.pylxd_frozen_instance.name, |
2501 | + use_desc=use_desc, container_config=inst_config) |
2502 | if start: |
2503 | - instance.start(wait=True, wait_time=self.config.get('timeout')) |
2504 | + instance.start() |
2505 | return instance |
2506 | |
2507 | def destroy(self): |
2508 | diff --git a/tests/cloud_tests/testcases.yaml b/tests/cloud_tests/testcases.yaml |
2509 | index c22b08e..7183e01 100644 |
2510 | --- a/tests/cloud_tests/testcases.yaml |
2511 | +++ b/tests/cloud_tests/testcases.yaml |
2512 | @@ -2,6 +2,7 @@ |
2513 | base_test_data: |
2514 | script_timeout: 20 |
2515 | enabled: True |
2516 | + required_features: [] |
2517 | cloud_config: | |
2518 | #cloud-config |
2519 | collect_scripts: |
2520 | diff --git a/tests/cloud_tests/util.py b/tests/cloud_tests/util.py |
2521 | index 64a8667..52a702e 100644 |
2522 | --- a/tests/cloud_tests/util.py |
2523 | +++ b/tests/cloud_tests/util.py |
2524 | @@ -1,5 +1,6 @@ |
2525 | # This file is part of cloud-init. See LICENSE file for license information. |
2526 | |
2527 | +import copy |
2528 | import glob |
2529 | import os |
2530 | import random |
2531 | @@ -7,10 +8,18 @@ import string |
2532 | import tempfile |
2533 | import yaml |
2534 | |
2535 | -from cloudinit.distros import OSFAMILIES |
2536 | from cloudinit import util as c_util |
2537 | from tests.cloud_tests import LOG |
2538 | |
2539 | +OS_FAMILY_MAPPING = { |
2540 | + 'debian': ['debian', 'ubuntu'], |
2541 | + 'redhat': ['centos', 'rhel', 'fedora'], |
2542 | + 'gentoo': ['gentoo'], |
2543 | + 'freebsd': ['freebsd'], |
2544 | + 'suse': ['sles'], |
2545 | + 'arch': ['arch'], |
2546 | +} |
2547 | + |
2548 | |
2549 | def list_test_data(data_dir): |
2550 | """ |
2551 | @@ -68,7 +77,7 @@ def gen_instance_name(prefix='cloud-test', image_desc=None, use_desc=None, |
2552 | """ |
2553 | filter bad characters out of elem and trim to length |
2554 | """ |
2555 | - elem = elem[:max_len] if elem else unknown |
2556 | + elem = elem.lower()[:max_len] if elem else unknown |
2557 | return ''.join(c if c in valid else delim for c in elem) |
2558 | |
2559 | return next(name for name in |
2560 | @@ -88,7 +97,8 @@ def get_os_family(os_name): |
2561 | """ |
2562 | get os family type for os_name |
2563 | """ |
2564 | - return next((k for k, v in OSFAMILIES.items() if os_name in v), None) |
2565 | + return next((k for k, v in OS_FAMILY_MAPPING.items() |
2566 | + if os_name.lower() in v), None) |
2567 | |
2568 | |
2569 | def current_verbosity(): |
2570 | @@ -127,12 +137,17 @@ def configure_yaml(): |
2571 | 'tag:yaml.org,2002:str', data, style='|' if '\n' in data else ''))) |
2572 | |
2573 | |
2574 | -def yaml_format(data): |
2575 | +def yaml_format(data, content_type=None): |
2576 | """ |
2577 | format data as yaml |
2578 | + data: data to dump |
2579 | + header: is specified, add a header to the dumped data |
2580 | + return_value: yaml string |
2581 | """ |
2582 | configure_yaml() |
2583 | - return yaml.dump(data, indent=2, default_flow_style=False) |
2584 | + content_type = ( |
2585 | + '#{}\n'.format(content_type.strip('#\n')) if content_type else '') |
2586 | + return content_type + yaml.dump(data, indent=2, default_flow_style=False) |
2587 | |
2588 | |
2589 | def yaml_dump(data, path): |
2590 | @@ -158,6 +173,108 @@ def write_file(*args, **kwargs): |
2591 | """ |
2592 | write a file using cloudinit.util.write_file |
2593 | """ |
2594 | - c_util.write_file(*args, **kwargs) |
2595 | + return c_util.write_file(*args, **kwargs) |
2596 | + |
2597 | + |
2598 | +def read_conf(*args, **kwargs): |
2599 | + """ |
2600 | + read configuration using cloudinit.util.read_conf |
2601 | + """ |
2602 | + return c_util.read_conf(*args, **kwargs) |
2603 | + |
2604 | + |
2605 | +def subp(*args, **kwargs): |
2606 | + """ |
2607 | + execute a command on the system shell using cloudinit.util.subp |
2608 | + """ |
2609 | + return c_util.subp(*args, **kwargs) |
2610 | + |
2611 | + |
2612 | +def tmpdir(prefix='cloud_test_util_'): |
2613 | + return tempfile.mkdtemp(prefix=prefix) |
2614 | + |
2615 | + |
2616 | +def rel_files(basedir): |
2617 | + """ |
2618 | + list of files under directory by relative path, not including directories |
2619 | + return_value: list or relative paths |
2620 | + """ |
2621 | + basedir = os.path.normpath(basedir) |
2622 | + return [path[len(basedir) + 1:] for path in |
2623 | + glob.glob(os.path.join(basedir, '**'), recursive=True) |
2624 | + if not os.path.isdir(path)] |
2625 | + |
2626 | + |
2627 | +def flat_tar(output, basedir, owner='root', group='root'): |
2628 | + """ |
2629 | + create a flat tar archive (no leading ./) from basedir |
2630 | + output: output tar file to write |
2631 | + basedir: base directory for archive |
2632 | + owner: owner of archive files |
2633 | + group: group archive files belong to |
2634 | + return_value: none |
2635 | + """ |
2636 | + c_util.subp(['tar', 'cf', output, '--owner', owner, '--group', group, |
2637 | + '-C', basedir] + rel_files(basedir), capture=True) |
2638 | + |
2639 | + |
2640 | +def parse_conf_list(entries, valid=None, boolean=False): |
2641 | + """ |
2642 | + parse config in a list of strings in key=value format |
2643 | + entries: list of key=value strings |
2644 | + valid: list of valid keys in result, return None if invalid input |
2645 | + boolean: if true, then interpret all values as booleans where 'true' = True |
2646 | + return_value: dict of configuration or None if invalid |
2647 | + """ |
2648 | + res = {key: value.lower() == 'true' if boolean else value |
2649 | + for key, value in (i.split('=') for i in entries)} |
2650 | + return res if not valid or all(k in valid for k in res.keys()) else None |
2651 | + |
2652 | + |
2653 | +def update_args(args, updates, preserve_old=True): |
2654 | + """ |
2655 | + update cmdline arguments from a dictionary |
2656 | + args: cmdline arguments |
2657 | + updates: dictionary of {arg_name: new_value} mappings |
2658 | + preserve_old: if true, create a deep copy of args before updating |
2659 | + return_value: updated cmdline arguments, as new object if preserve_old=True |
2660 | + """ |
2661 | + args = copy.deepcopy(args) if preserve_old else args |
2662 | + if updates: |
2663 | + vars(args).update(updates) |
2664 | + return args |
2665 | + |
2666 | + |
2667 | +def update_user_data(user_data, updates, dump_to_yaml=True): |
2668 | + """ |
2669 | + user_data: user data as yaml string or dict |
2670 | + updates: dictionary to merge with user data |
2671 | + dump_to_yaml: return as yaml dumped string if true |
2672 | + return_value: updated user data, as yaml string if dump_to_yaml is true |
2673 | + """ |
2674 | + user_data = (c_util.load_yaml(user_data) |
2675 | + if isinstance(user_data, str) else copy.deepcopy(user_data)) |
2676 | + user_data.update(updates) |
2677 | + return (yaml_format(user_data, content_type='cloud-config') |
2678 | + if dump_to_yaml else user_data) |
2679 | + |
2680 | + |
2681 | +class InTargetExecuteError(c_util.ProcessExecutionError): |
2682 | + """ |
2683 | + Error type for in target commands that fail |
2684 | + """ |
2685 | + default_desc = 'Unexpected error while running command in target instance' |
2686 | + |
2687 | + def __init__(self, stdout, stderr, exit_code, cmd, instance, |
2688 | + description=None): |
2689 | + """ |
2690 | + init error and parent error class |
2691 | + """ |
2692 | + if isinstance(cmd, (tuple, list)): |
2693 | + cmd = ' '.join(cmd) |
2694 | + super(InTargetExecuteError, self).__init__( |
2695 | + stdout=stdout, stderr=stderr, exit_code=exit_code, cmd=cmd, |
2696 | + reason="Instance: {}".format(instance), |
2697 | + description=description if description else self.default_desc) |
2698 | |
2699 | # vi: ts=4 expandtab |
2700 | diff --git a/tox.ini b/tox.ini |
2701 | index bf9046a..5cf8d22 100644 |
2702 | --- a/tox.ini |
2703 | +++ b/tox.ini |
2704 | @@ -101,4 +101,4 @@ basepython = python3 |
2705 | commands = {envpython} -m tests.cloud_tests {posargs} |
2706 | passenv = HOME |
2707 | deps = |
2708 | - pylxd==2.1.3 |
2709 | + pylxd==2.2.3 |
This is a continuation of the main integration testing update at: /code.launchpad .net/~wesley- wiedenmeier/ cloud-init/ +git/cloud- init/+merge/ 308218
https:/
The main update reworks the image config format and enables use of other distros on lxd, both of which are required for the features in this branch.
Adding support for distro specific user data overrides provides a workaround for the hostnamectl in an unpriv container bug. This allows centos tests to run, and can easily be disabled via image config once this bug is resolved.
Adding the distro feature flags makes the support for other distros added by the main update useful. Without the distro feature flags, a full run of all available testcases on a distro does not work for non-ubuntu distros as many testcases are not compatible with multiple distros. Many of the testcases that do not work outside of ubuntu can be made to work on other distros, but a separate branch based on this one will be used for resolving testcase bugs.
The distro feature flags have been set to skip any tests which do not work on most non-ubuntu distros. There may be some additional minor updates to the feature flags to enable running the full testsuite on centos70 and older releases of ubuntu, debian and centos. All testscases which are expected to run with an up to date cloud-init on centos70 are enabled for it, but the test suite has not yet been run with an up to date rpm for centos, so centos tests are not confirmed to work fully yet. /docs.google. com/spreadsheet s/d/1DAzBlh- wk-rv-WRjllNRG6 nnHtAmD0EFBLEYt u8weII/
Current status of working/non-working distros is here:
https:/