Merge ~wesley-wiedenmeier/cloud-init:integration-testing-distro-features into cloud-init:master

Proposed by Wesley Wiedenmeier
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)
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

Description of the change

Integration Testing: Improvments to testing on alternate distros

 - Allow images to override user data settings
   - image config option 'user_data_overrides' accepts dict of
     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

To post a comment you must log in.
Revision history for this message
Wesley Wiedenmeier (wesley-wiedenmeier) wrote :

This is a continuation of the main integration testing update at:
https://code.launchpad.net/~wesley-wiedenmeier/cloud-init/+git/cloud-init/+merge/308218
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.
Current status of working/non-working distros is here:
https://docs.google.com/spreadsheets/d/1DAzBlh-wk-rv-WRjllNRG6nnHtAmD0EFBLEYtu8weII/

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
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.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
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.

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

I'm going to mark this 'work in progress'. Josh has integrated this branch and others into his merge proposal at https://code.launchpad.net/~powersj/cloud-init/+git/cloud-init/+merge/324136 . We do plan to pull that all soon.

Thanks for your work, Wesley!

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

Hi.
I've marked this 'merged' as I think it is now in trunk under 76d58265e34851b78e952a7f275340863c90a9f5.
If you disagree, please feel free to re-open.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/doc/rtd/topics/tests.rst b/doc/rtd/topics/tests.rst
2index 0663811..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 ============
283diff --git a/tests/cloud_tests/args.py b/tests/cloud_tests/args.py
284index 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
361diff --git a/tests/cloud_tests/collect.py b/tests/cloud_tests/collect.py
362index 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
445diff --git a/tests/cloud_tests/config.py b/tests/cloud_tests/config.py
446index 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
590diff --git a/tests/cloud_tests/configs/bugs/lp1628337.yaml b/tests/cloud_tests/configs/bugs/lp1628337.yaml
591index 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:
604diff --git a/tests/cloud_tests/configs/examples/add_apt_repositories.yaml b/tests/cloud_tests/configs/examples/add_apt_repositories.yaml
605index 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:
617diff --git a/tests/cloud_tests/configs/modules/apt_configure_conf.yaml b/tests/cloud_tests/configs/modules/apt_configure_conf.yaml
618index 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:
630diff --git a/tests/cloud_tests/configs/modules/apt_configure_disable_suites.yaml b/tests/cloud_tests/configs/modules/apt_configure_disable_suites.yaml
631index 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:
644diff --git a/tests/cloud_tests/configs/modules/apt_configure_primary.yaml b/tests/cloud_tests/configs/modules/apt_configure_primary.yaml
645index 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
667diff --git a/tests/cloud_tests/configs/modules/apt_configure_proxy.yaml b/tests/cloud_tests/configs/modules/apt_configure_proxy.yaml
668index 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:
680diff --git a/tests/cloud_tests/configs/modules/apt_configure_security.yaml b/tests/cloud_tests/configs/modules/apt_configure_security.yaml
681index 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:
694diff --git a/tests/cloud_tests/configs/modules/apt_configure_sources_key.yaml b/tests/cloud_tests/configs/modules/apt_configure_sources_key.yaml
695index 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:
708diff --git a/tests/cloud_tests/configs/modules/apt_configure_sources_keyserver.yaml b/tests/cloud_tests/configs/modules/apt_configure_sources_keyserver.yaml
709index 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:
722diff --git a/tests/cloud_tests/configs/modules/apt_configure_sources_list.yaml b/tests/cloud_tests/configs/modules/apt_configure_sources_list.yaml
723index 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:
736diff --git a/tests/cloud_tests/configs/modules/apt_configure_sources_ppa.yaml b/tests/cloud_tests/configs/modules/apt_configure_sources_ppa.yaml
737index 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
762diff --git a/tests/cloud_tests/configs/modules/apt_pipelining_disable.yaml b/tests/cloud_tests/configs/modules/apt_pipelining_disable.yaml
763index 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:
775diff --git a/tests/cloud_tests/configs/modules/apt_pipelining_os.yaml b/tests/cloud_tests/configs/modules/apt_pipelining_os.yaml
776index 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:
788diff --git a/tests/cloud_tests/configs/modules/byobu.yaml b/tests/cloud_tests/configs/modules/byobu.yaml
789index 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
801diff --git a/tests/cloud_tests/configs/modules/keys_to_console.yaml b/tests/cloud_tests/configs/modules/keys_to_console.yaml
802index 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]
814diff --git a/tests/cloud_tests/configs/modules/landscape.yaml b/tests/cloud_tests/configs/modules/landscape.yaml
815index 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:
827diff --git a/tests/cloud_tests/configs/modules/locale.yaml b/tests/cloud_tests/configs/modules/locale.yaml
828index 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
841diff --git a/tests/cloud_tests/configs/modules/lxd_bridge.yaml b/tests/cloud_tests/configs/modules/lxd_bridge.yaml
842index 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:
854diff --git a/tests/cloud_tests/configs/modules/lxd_dir.yaml b/tests/cloud_tests/configs/modules/lxd_dir.yaml
855index 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:
867diff --git a/tests/cloud_tests/configs/modules/ntp.yaml b/tests/cloud_tests/configs/modules/ntp.yaml
868index 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
895diff --git a/tests/cloud_tests/configs/modules/ntp_pools.yaml b/tests/cloud_tests/configs/modules/ntp_pools.yaml
896index 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:
916diff --git a/tests/cloud_tests/configs/modules/ntp_servers.yaml b/tests/cloud_tests/configs/modules/ntp_servers.yaml
917index 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:
933diff --git a/tests/cloud_tests/configs/modules/package_update_upgrade_install.yaml b/tests/cloud_tests/configs/modules/package_update_upgrade_install.yaml
934index 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:
955diff --git a/tests/cloud_tests/configs/modules/set_hostname.yaml b/tests/cloud_tests/configs/modules/set_hostname.yaml
956index 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
968diff --git a/tests/cloud_tests/configs/modules/set_hostname_fqdn.yaml b/tests/cloud_tests/configs/modules/set_hostname_fqdn.yaml
969index 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
981diff --git a/tests/cloud_tests/configs/modules/set_password.yaml b/tests/cloud_tests/configs/modules/set_password.yaml
982index 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
994diff --git a/tests/cloud_tests/configs/modules/set_password_expire.yaml b/tests/cloud_tests/configs/modules/set_password_expire.yaml
995index 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 }
1007diff --git a/tests/cloud_tests/configs/modules/snappy.yaml b/tests/cloud_tests/configs/modules/snappy.yaml
1008index 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:
1020diff --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
1021index 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: []
1033diff --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
1034index 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:
1049diff --git a/tests/cloud_tests/configs/modules/ssh_import_id.yaml b/tests/cloud_tests/configs/modules/ssh_import_id.yaml
1050index 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:
1063diff --git a/tests/cloud_tests/configs/modules/ssh_keys_generate.yaml b/tests/cloud_tests/configs/modules/ssh_keys_generate.yaml
1064index 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:
1076diff --git a/tests/cloud_tests/configs/modules/ssh_keys_provided.yaml b/tests/cloud_tests/configs/modules/ssh_keys_provided.yaml
1077index 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
1090diff --git a/tests/cloud_tests/configs/modules/timezone.yaml b/tests/cloud_tests/configs/modules/timezone.yaml
1091index 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
1103diff --git a/tests/cloud_tests/configs/modules/user_groups.yaml b/tests/cloud_tests/configs/modules/user_groups.yaml
1104index 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
1116diff --git a/tests/cloud_tests/configs/modules/write_files.yaml b/tests/cloud_tests/configs/modules/write_files.yaml
1117index 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:
1131diff --git a/tests/cloud_tests/images/base.py b/tests/cloud_tests/images/base.py
1132index 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
1192diff --git a/tests/cloud_tests/images/lxd.py b/tests/cloud_tests/images/lxd.py
1193index 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
1393diff --git a/tests/cloud_tests/instances/base.py b/tests/cloud_tests/instances/base.py
1394index 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
1557diff --git a/tests/cloud_tests/instances/lxd.py b/tests/cloud_tests/instances/lxd.py
1558index 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 """
1675diff --git a/tests/cloud_tests/platforms.yaml b/tests/cloud_tests/platforms.yaml
1676index 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
1736diff --git a/tests/cloud_tests/platforms/base.py b/tests/cloud_tests/platforms/base.py
1737index 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
1777diff --git a/tests/cloud_tests/platforms/lxd.py b/tests/cloud_tests/platforms/lxd.py
1778index 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
1859diff --git a/tests/cloud_tests/releases.yaml b/tests/cloud_tests/releases.yaml
1860index 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
2212diff --git a/tests/cloud_tests/setup_image.py b/tests/cloud_tests/setup_image.py
2213index 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
2443diff --git a/tests/cloud_tests/snapshots/base.py b/tests/cloud_tests/snapshots/base.py
2444index 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 """
2467diff --git a/tests/cloud_tests/snapshots/lxd.py b/tests/cloud_tests/snapshots/lxd.py
2468index 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):
2508diff --git a/tests/cloud_tests/testcases.yaml b/tests/cloud_tests/testcases.yaml
2509index 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:
2520diff --git a/tests/cloud_tests/util.py b/tests/cloud_tests/util.py
2521index 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
2700diff --git a/tox.ini b/tox.ini
2701index 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

Subscribers

People subscribed via source and target branches