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

Proposed by Wesley Wiedenmeier on 2016-10-12
Status: Merged
Merged at revision: 76d58265e34851b78e952a7f275340863c90a9f5
Proposed branch: ~wesley-wiedenmeier/cloud-init:integration-testing
Merge into: cloud-init:master
Diff against target: 1567 lines (+700/-271)
16 files modified
tests/cloud_tests/args.py (+11/-7)
tests/cloud_tests/collect.py (+9/-14)
tests/cloud_tests/config.py (+53/-12)
tests/cloud_tests/images/base.py (+16/-9)
tests/cloud_tests/images/lxd.py (+108/-18)
tests/cloud_tests/instances/base.py (+65/-29)
tests/cloud_tests/instances/lxd.py (+47/-21)
tests/cloud_tests/platforms.yaml (+49/-1)
tests/cloud_tests/platforms/base.py (+1/-24)
tests/cloud_tests/platforms/lxd.py (+27/-15)
tests/cloud_tests/releases.yaml (+133/-67)
tests/cloud_tests/setup_image.py (+83/-42)
tests/cloud_tests/snapshots/base.py (+2/-1)
tests/cloud_tests/snapshots/lxd.py (+6/-6)
tests/cloud_tests/util.py (+89/-4)
tox.ini (+1/-1)
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Approve on 2017-03-17
Joshua Powers (community) Approve on 2017-01-18
cloud-init commiters 2016-10-12 Pending
Review via email: mp+308218@code.launchpad.net

Description of the Change

integration-testing updates

 - enable additional distros
 - allow use of either pylxd 2.1.3 or 2.2.3
   - detected at runtime during instance.execute()
   - when using 2.1.3, the test suite will not have proper
     error handling during setup phase, as 2.1.3 does not
     return command exit codes. this is adequate for use on
     jenkins until the fix for 2.2 is merged in upstream
 - refactor image setup code
   - improve error handling throughout
   - change behavior of --upgrade setup_image to only
     upgrade cloud-init
   - add --upgrade-all setup_image flag to upgrade all
     packages on system
   - clean up detection of cloud-init version
   - output cloud-init version at end of setup_image
   - use 'yum install' rather than 'yum update' in
     setup_image.upgrade in case cloud-init not installed
   - update help in setup_image args
 - clean up image config
   - new image config format allows finer control over
     platform specific image config, with less verbosity
   - clean up handling of image config throughout platform
   - added additional releases and distros
   - re-ordered releases.yaml in order of release date, and
     added EOL comments for future reference
   - allow image config to override setup options with
     'setup_overrides'
   - allow image config to specify preserving base images
     for lxd
   - allow using images from images.linuxcontainers.org for
     releases other than ubuntu
 - improve waiting for system to boot
   - separated out determining if the system has booted from
     determining if cloud-init has finished
   - do not wait for cloud-init during setup phases, as it
     may not be installed
   - use 'systemctl is-system-running' or 'runlevel'
     depending on init system
   - poll for system ready inside the image rather than
     through instance.execute() to make pylxd too many open
     files error take longer to strike
   - clean up code for handling waiting
 - update lxd image metadata and templates if necessary
   - many lxd images that do not come with cloud-init
     installed do not have the necessary templates to write
     a nocloud seed into the image
   - if necessary, export image, extract metadata, add new
     template scripts, and import as new image
 - add new tox environment to run tests
   - new versions of pylxd only available through pip on
     xenial, best to avoid using pip outside a venv
   - 'citest' provides an entry points into the cli,
     allowing use of any test commands, using pylxd 2.2.3

To post a comment you must log in.

There are 2 outstanding issues with releases that are now enabled:

Debian Jessie encounteres errors with cc_apt_configure, even with no 'apt'
config keys presented. This does not cause cloud-init to completely fail;
however, the tests fail as they all check that cloud-init stages did not
encounter any errors. I suspect there may be a bug in cloud-init here, as
it is failing on a supported system even with default config. It's worth
noting that on daily builds of Stretch, everything works properly, and
everything works on Sid, so this only affects Wheezy.

Errors:
  2017-01-05 00:15:47,016 - util.py[WARNING]: Running apt-configure (<module 'cloudinit.config.cc_apt_configure' from '/usr/lib/python2.7/dist-packages/cloudinit/config/cc_apt_configure.pyc'>) failed
 "('apt-configure', NotFound(u\"cannot find 'security'\",))"

All CentOS releases, Debian Wheezy and Ubuntu Precise fail while pylxd
attempts to create a websocket to communicate with the image in execute().
Since the failures are consistant and always on older releases (counting
centos 7 as an old release, as most versions in it are fairly old), I
suspect that this has something to do with library versions in target.

The actual error message is:
  [Errno 24] Too many open files

I will mark this is 'needs review' once I have fixed these issues or found
better explainations.

Switching to polling in the image rather than with repeated calls to pylxd's execute() seems to have fixed the too many open files issue, although once the test suite is made to run in parallel this may become an issue again. Centos tests are not running smoothly yet because of what looks like a bug in cc_set_hostname, but there is no longer an issue with the test infrastructure on centos and old releases of other distros.

It looks like the remaining centos70 issue is not an error in the test suite or in cloud-init; its a systemd bug in the centos images from images.linuxcontainers.org. I haven't tested on official centos images yet, so its possible either that it is a bug in centos, in centos/lxd integration, or in the centos lxd image build.

Setting the system hostname using hostnamectl fails with:
-[laptop1]- -[~]-
-[08:20 PM]-lxc launch images:centos/7/default
Creating picked-snapper
Starting picked-snapper
-[laptop1]- -[~]-
-[08:21 PM]-lxc exec picked-snapper bash
[root@picked-snapper ~]# hostnamectl set-hostname "test-hostname"
Could not set property: Connection timed out

If 'preserve_hostname: true' is added to the cloud-config for a test that should work on centos the test will pass.

Once I figure out whether the bug with hostnamectl is in the image itself or in all of centos I will get a bug filed. I don't think that further work on the test suite should be done to resolve this, as modifying the test configs as a workaround does not seem right here.

This is blocked on pylxd:
https://github.com/lxc/pylxd/issues/209

A fix for the pylxd bug is available at:
https://github.com/lxc/pylxd/pull/211

This should be ready to merge and includes all changes from:
https://code.launchpad.net/~wesley-wiedenmeier/cloud-init/+git/cloud-init/+merge/314496

Joshua Powers (powersj) wrote :

@wesley, per the commit message you enabled additional distros. It looks like wheezy, jessie and centros66, and centos70. Do these require the newer version of pylxd for running? Should they work as expected? Trying to understand how to test these.

All distros will work on both pylxd 2.1.3 and pylxd 2.2, the only problem running with 2.1.3 is the lack of error handling, which makes debugging setup_image difficult when it isn't working.

Not all of the new distros work correctly, centos66 will fail due to not having cloud-init in its repos, although I have gotten a run to go through by installing a rpm + a couple deps. It probably shouldn't be used for any sort of testing. I'll go ahead and disable it.

Debian wheezy is not enabled, as it is very old. I could probably get it working but it wasn't a priority for testing.

Debian stretch works, at least for single tests. I haven't run through a full test yet, although I suspect that modules/apt_configure_sources_ppa will fail. I haven't run into any issue with it in terms of distro and platform support, so the only issues should be cloud-init features that are not supported on debian.

Debian jessie is enabled, but does not work due to what I believe is a bug in cc_apt_configure which I haven't had time to resolve yet. Other than that bug, it should work.

Centos70 does not work because of a bug in hostnamectl on the centos images from images.linuxcontainers.org. I have an example of the bug in a comment a few above this one. I haven't had time to look into that bug any more, but the test suite and distro do work together properly there, as adding 'preserve_hostname: true' to a test config and running that test seems to work for most simple test cases (modules/final_message, modules/debug_enable...)

Due to feature incompatibility between ubuntu/debian and centos, and lack of support for modules/cc_apt_configure_sources_ppa outside of ubuntu, distro feature groups are probably going to need to be added soon. For the time being, tests which make sense to work on these distos can be run manually for testing, or perhaps a list could be put together for jenkins. I think it makes the most sense to just get this merged in soon and deal with distro feature flags and platform feature flags in a new merge proposal, since most distros other than ubuntu are blocked by bugs right now.

The supported distro/release matrix has been updated at: goo.gl/q78sY8

Joshua Powers (powersj) wrote :

LGTM, with two minor comments below. Reusing images has really sped up execution and printing the cloud-init version is great. I ran through the following tests:

$ tox
$ tox -e citest -- run -v -n zesty -t tests/cloud_tests/configs/modules/write_files.yaml
$ tox -e citest_new_pylxd -- run -v -n zesty -t tests/cloud_tests/configs/modules/write_files.yaml
$ tox -e citest_tree_run
$ python3 -m tests.cloud_tests run -v -n zesty -t tests/cloud_tests/configs/modules/write_files.yaml
$ python3 -m tests.cloud_tests run -v -n stretch -t tests/cloud_tests/configs/modules/write_files.yaml
$ python3 -m tests.cloud_tests run -v -n jessie -t tests/cloud_tests/configs/modules/write_files.yaml

Comment 1:
The warning messages about using pylxd < 2.2 [1]. There are far too many messages right now. I see the message in pylxd.py as it checks for the exit code; can we suppress the warning until 2.2 is stable or even consider changing it to show up when the user is using less than 2.1.3?

Comment 2:
Running on jessie I get a failure running no_stages_errors [2] about security. Is it trying to add the jessie security repo?

[1] https://paste.ubuntu.com/23817502/
[2] https://paste.ubuntu.com/23817710/

review: Approve

> LGTM, with two minor comments below. Reusing images has really sped up
> execution and printing the cloud-init version is great.

Thanks. Yeah, re-using the images should help quite a bit with runtime.

> Comment 1:
> The warning messages about using pylxd < 2.2 [1]. There are far too many
> messages right now. I see the message in pylxd.py as it checks for the exit
> code; can we suppress the warning until 2.2 is stable or even consider
> changing it to show up when the user is using less than 2.1.3?

I'll go ahead and comment the message out, we can always put it back later.
Having the message display each execute() does make things harder to read

> Comment 2:
> Running on jessie I get a failure running no_stages_errors [2] about security.
> Is it trying to add the jessie security repo?

Yeah, jessie fails right now due to cc_apt_configure trying to find the security
repo in sources. I'm not 100% sure why its looking for it, as it won't be there.
I think this is likely a bug that needs to be filed, I just haven't had time to
find a way to reproduce the issue outside of cloud tests yet. I'll try to get
that done later this week.

Scott Moser (smoser) wrote :
Download full text (3.2 KiB)

lots of comments...
Thanks for all the work here, we need to try to do things more iteratively. I know we're still bringing this all up, but there is just so much here that its almost impossible to review.

General:
 * please lets try to do smaller merge proposals, doing one set of things
   at a time.
 * full chart of enabled/disabled at: http://goo.gl/q78sY8
   this is a private url, not suitable for a commit message
   possibly we just move this into doc in cloud-init?
 * where is the upstream bug that prevents 2.2 from working correctly?

 * i think we can live without 'alias: t'.
 * apparently we should be using dnf rather than yum in any recent RH distro
 * "switch to using images.linuxcontainers.org from using ubuntu daily images"
    why ?? we really need to be testing Ubuntu images, not linuxcontainers.org
    I like using them for other distros where we have nothing else, but
    ubuntu should test primarily on ubuntu images.

 * system_ready_script can be simplified by being a command, passed
   directly to 'execute'.
     system_ready_script: ['test', '-e', '/run/cloud-init/result.json']
   that saves writing the file and executing it on the other side, and
   also then assuming 'bash'.
 * "Pass $HOME through in tox citest envs for lxc client use"
   message doesn't make sense. where are we using the lxc client? just
   for developer debug ?
 * "Disable centos66 test"
   why ?

tox.ini:
 * multiple citest_* are fine for now, but i really do not want them
   going forward.

Easily droppable commits / things that can be done separartely:
- "Use LOG.warning instead of deprectated LOG.warn"
   why? This is just not necessary in this merge proposal.
- "Change behavior of upgrade in setup_image"

tests/cloud_tests/bddeb.py
 * 7cd4a3b5 source_archive: can we either
   a.) just assume the deb-src lines are present
   b.) hueristically grab one... i'd like to not use us.archive.com if
       the image is otherwise using archive.ubuntu.com as that will
       avoid any proxy in the path. this is kind of tough actually..

 * creating the tarball..
   I think what we want to do here is:
     a.) make-tarball on "this side"
     b.) modify bddeb to be able to just create a debian/ dir
         either
         i.) have it create the orig tarball and a debian.tar.gz into a
             provided (empty) output dir.
         ii.) have it just create the debian.tar.gz and we create the tarball
              example of doing that http://paste.ubuntu.com/23870964/
     c.) move the tarball and debian dir over, extract tarball, extract debian/
     d.) mk-build-deps --install .... debian/control
     e.) dpkg-buildpackage

   doing this should mean we do not have to 'install additional build deps'
   but rather just have mk-build-deps do the right thing.

 * when building with dpkg-buildpackage or debuild, good to
   allow setting DEB_BUILD_OPTIONS=nocheck (which will not run pep8 and
   nosetests)

 * i think we can drop 'git' due to above, 'tar' is essential so you
   do not have to declare it.
   lets do the install of devscripts and equivs with --no-install-recommends
   and also eatmydata everywhere

 * i think the above path means we do no...

Read more...

Download full text (7.5 KiB)

> lots of comments...
> Thanks for all the work here, we need to try to do things more
> iteratively. I know we're still bringing this all up, but there
> is just so much here that its almost impossible to review.

Yeah this was definitely too large a merge, we just got stuck with
lots of changes that depend on each other. This replaces a pretty
huge chunk of the current codebase as most of that was just
temporary to get some tests running while the main part of this
got finished.

> General:
> * please lets try to do smaller merge proposals, doing one set
> of things at a time.

Definitely, this should be the last major merge, since all of the
pieces that had to be rewritten are finished. Next merge should just
be the new platform.

> * full chart of enabled/disabled at: http://goo.gl/q78sY8
> this is a private url, not suitable for a commit message
> possibly we just move this into doc in cloud-init?

I'll go ahead and pull this link out of the commit message. I have
been meaning to do some doc updates, I'll add a link to the
spreadsheet and make it public when I put together the merge for
that.

> * where is the upstream bug that prevents 2.2 from working
> correctly?

It was at https://github.com/lxc/pylxd/issues/209, but the fix has
been pulled in now. I'm going to set up a ppa to build a version with
the fix for devel use. However, for now using 2.1.3 works for
jenkins, the only disadvantage there is that if anything goes wrong
during setup it will fail silently. This version of the code will run
with either 2.1.3 or 2.2, so when the fix is released all we have to
do to switch is change the version in the tox env.

> * i think we can live without 'alias: t'.

The 'alias' key is used to reference the image, on the ubuntu daily
sstream server the aliases I found were p, t, x, y, z. It's now been
switched over to ubuntu/trusty/default with the switch to images from
images.linuxcontainers.org.

> * apparently we should be using dnf rather than yum in any recent
> RH distro

Oh, I didn't know that dnf was officially supported outside Fedora.
It shouldn't take much to switch that back over, just gotta update
the setup image functions.

> * "switch to using images.linuxcontainers.org from using ubuntu
> daily images" why ?? we really need to be testing Ubuntu
> images, not linuxcontainers.org I like using them for other
> distros where we have nothing else, but ubuntu should test
> primarily on ubuntu images.

I had mainly done it for consistancy across the different distros and
because linuxcontainers images download way faster than ubuntu daily
images (at least for me) but its just a config change to switch back
to the ubuntu daily repo for ubuntu images. I can switch that back
tonight.

> * system_ready_script can be simplified by being a command,
> passed directly to 'execute'.
> system_ready_script: ['test', '-e',
> '/run/cloud-init/result.json']
> that saves writing the file and executing it on the other
> side, and also then assuming 'bash'.

That makes sense, although it does limit how complex the scripts can
get. Its probably best to keep them simple though...

Read more...

Scott Moser (smoser) wrote :
Download full text (7.4 KiB)

>> * full chart of enabled/disabled at: http://goo.gl/q78sY8
>> this is a private url, not suitable for a commit message
>> possibly we just move this into doc in cloud-init?
>
>I'll go ahead and pull this link out of the commit message. I have
>been meaning to do some doc updates, I'll add a link to the
>spreadsheet and make it public when I put together the merge for
>that.

Great.

>> * where is the upstream bug that prevents 2.2 from working
>> correctly?

> It was at https://github.com/lxc/pylxd/issues/209, but the fix has

Thanks, please mention it in the code.

>> * i think we can live without 'alias: t'.

> The 'alias' key is used to reference the image, on the ubuntu daily
> sstream server the aliases I found were p, t, x, y, z. It's now been switched
> over to ubuntu/trusty/default with the switch to images from
> images.linuxcontainers.org.

The ubuntu/daily streams also have other more explicit alias:
  lxc launch ubuntu-daily:xenial
  lxc launch ubuntu-daily:16.04

>> * apparently we should be using dnf rather than yum in any recent
>> RH distro

> Oh, I didn't know that dnf was officially supported outside Fedora.
> It shouldn't take much to switch that back over, just gotta update
> the setup image functions.

I only know because cloud-init had a MP from a RH developer asking for it (see
commit log)
 https://bugs.launchpad.net/cloud-init/+bug/1647118

>> * system_ready_script can be simplified by being a command,
>> passed directly to 'execute'.
>> system_ready_script: ['test', '-e',
>> '/run/cloud-init/result.json']
>> that saves writing the file and executing it on the other
>> side, and also then assuming 'bash'.

>That makes sense, although it does limit how complex the scripts can
>get. Its probably best to keep them simple though.

If we needed, we could allow the config to provide stdin also, but with
the above, the only limit on complexity that i can see is the command
line length limit, which shouldn't be too big of a problem
  http://stackoverflow.com/questions/6846263/maximum-length-of-command-line-argument-that-can-be-passed-to-sqlplus-from-lin

xargs --show-limits:
  POSIX upper limit on argument length (this system): 2091520

:)

>> * "Pass $HOME through in tox citest envs for lxc client use"
>> message doesn't make sense. where are we using the lxc client? just
>> for developer debug ?

> It is used for

Sentance fragment ^ ?

>> * "Disable centos66 test"
>> why ?

> Its such an old release that pretty much nothing is going to be
> backported there, testing on it doesn't really make sense IMO. I
> think the one time I tried running centos 6 tests they failed, but I
> don't recall why. I can get tests going there in the future if
> they're needed.

Well, if they were working, then it makes sense to run it, as a stable
platform likely means regression/change-of-behavior are the fault of
cloud-init, not the OS.

>> Easily droppable commits / things that can be done separartely:
>> - "Use LOG.warning instead of deprectated LOG.warn"
>> why? This is just not necessary in this merge proposal.
>> - "Change behavior of upgrade in setup_image"

>Th...

Read more...

Download full text (9.5 KiB)

> >> * where is the upstream bug that prevents 2.2 from working
> >> correctly?
>
> > It was at https://github.com/lxc/pylxd/issues/209, but the fix has
>
> Thanks, please mention it in the code.

Sure, I'll add a comment in there.

> >> * i think we can live without 'alias: t'.
>
> > The 'alias' key is used to reference the image, on the ubuntu daily
> > sstream server the aliases I found were p, t, x, y, z. It's now been
> switched
> > over to ubuntu/trusty/default with the switch to images from
> > images.linuxcontainers.org.
>
> The ubuntu/daily streams also have other more explicit alias:
> lxc launch ubuntu-daily:xenial
> lxc launch ubuntu-daily:16.04

Oh, nice. I'll switch to using those.

> >> * apparently we should be using dnf rather than yum in any recent
> >> RH distro
>
> > Oh, I didn't know that dnf was officially supported outside Fedora.
> > It shouldn't take much to switch that back over, just gotta update
> > the setup image functions.
>
> I only know because cloud-init had a MP from a RH developer asking
> for it (see commit log)
> https://bugs.launchpad.net/cloud-init/+bug/1647118

It may be good to keep both the yum path in place and add a config
directive to tell the platform which to use, just in case things
switch around more.

> >> * system_ready_script can be simplified by being a command,
> >> passed directly to 'execute'.
> >> system_ready_script: ['test', '-e',
> >> '/run/cloud-init/result.json']
> >> that saves writing the file and executing it on the other
> >> side, and also then assuming 'bash'.
>
> >That makes sense, although it does limit how complex the scripts can
> >get. Its probably best to keep them simple though.
>
> If we needed, we could allow the config to provide stdin also, but with
> the above, the only limit on complexity that i can see is the command
> line length limit, which shouldn't be too big of a problem
> http://stackoverflow.com/questions/6846263/maximum-length-of-command-line-
> argument-that-can-be-passed-to-sqlplus-from-lin
>
> xargs --show-limits:
> POSIX upper limit on argument length (this system): 2091520
>
> :)

Haha yeah, we're definitely not in danger of hitting that limit.

> >> * "Pass $HOME through in tox citest envs for lxc client use"
> >> message doesn't make sense. where are we using the lxc client? just
> >> for developer debug ?
>
> > It is used for
>
> Sentance fragment ^ ?

Whoops, I must have forgotten to finish that. It is used for exporting
lxd images that need to have their metadata modified to write the
cloud seed into the target on. Pylxd does have an export function for
images, but it does the export as a tmpfile with handle, not as a split
tarball in the filesystem like we need. The lxc client handles that
nicely, but it needs $HOME set so it can locate its config file.

> >> * "Disable centos66 test"
> >> why ?
>
> > Its such an old release that pretty much nothing is going to be
> > backported there, testing on it doesn't really make sense IMO. I
> > think the one time I tried running centos 6 tests they failed, but I
> > don't recall why. I can get tests going there in th...

Read more...

This branch should mostly be ready to merge in now. There is additonal work to be done on getting the cc_apt_configure issue resolved in debian jessie, and the hostnamectl issue resolved on centos70, but that can wait, as these are most likely not issues caused by the test suite. Support for package management on centos still needs to be switched to dnf, but since centos is blocked on the hostnamectl issue, that can wait as well.

The cleanup to system_ready_scripts has been done, and the ubuntu release config is back to using the ubuntu daily images. Ubuntu releases xenial -> zesty have been run with a couple of test configs successfully, both installing from ppa:cloud-init-dev/daily and from --deb with a deb of 0.7.9, and trusty has been run successfully with the version of cloud-init it ships with on modules/final_message. Additional testing of this merge may be good, but the tests currently being run by jenkins should work.

Note: I commented on the wrong MP a couple days ago, this was meant to be here

0bb1f96... by Wesley Wiedenmeier on 2017-03-12

Integration Testing: Update platform command execution and util

 - Add util.InTargetExecuteError, from cloudinit.util.ProcessExecutionError:
   - raise error if command has non-zero exit by default
   - allow non-zero exit codes if ignore_errors=True is set on execute calls
   - add description for in-target commands that is output in the event of a
     command error to simplify tracking down errors
 - Update calls to instance.execute() and instance.run_script()
 - Update image calls to instance execute and run
 - Update docstrings where required
 - Fix handling of os family mappings in util

4ebe02d... by Wesley Wiedenmeier on 2017-03-12

Integration Testing: Update setup_image and fix issues with package handling

 - Change behavior of upgrade in setup_image:
   - The --upgrade flag only upgrades or installs cloud-init
   - The --upgrade-full flag has been added to do a full system upgrade
 - Clean up detection of installed cloud-init version and output after setup
 - Use yum install rather than update for setup_image.upgrade to handle rpm
   based images that don't have cloud-init installed
 - Improve handling of deb installation, use 'apt-get instll -f' if a dep was
   missing

5952da7... by Wesley Wiedenmeier on 2017-03-12

Integration Testing: Clean up and improve instance boot and execute

 - Improve instance.run_script():
   - return full (out, err, exit) tuple instead of just the stdout,
     allowing additional error checking
   - clean up tempfile after script has been run
   - generate tempfile names using 'mktemp' to avoid possible name conflicts
 - Refactor code to wait for the system to boot
   - use 'system_ready_script' from image config rather than hardcoded script
   - poll for in the instance rather than on the host to avoid excessive calls
     to execute()
   - check for the system to boot and cloud-init to complete separately,
     allowing images with cloud-init not installed to be booted by setup_image
   - execute ready script commands directly in loop, instead of writing tempfile
   - use .execute() instead of .run_script() to avoid file writes

I've squashed the commit history for this branch together into a patch series, so it should be easier to review commit by commit now.

c931e06... by Wesley Wiedenmeier on 2017-03-15

Integration Testing: remove citest_old_pylxd tox environment

 - pylxd 2.1.3 does not allow error checking during setup as it does nto return
   command exit code
 - remove the tox env for running integration tests using pylxd 2.1.3 and keep
   default environment using 2.2.3

7918b5c... by Wesley Wiedenmeier on 2017-03-15

Integration Testing: improve code to allow additional error codes in execute

 - Old behavior was to allow execute to ignore any return code from an
   in-target command with ignore_errors
 - Replace this with a list of acceptable return codes similar to util.subp
   which allows better control over command execution

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!

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/tests/cloud_tests/args.py b/tests/cloud_tests/args.py
2index b68cc98..a96714c 100644
3--- a/tests/cloud_tests/args.py
4+++ b/tests/cloud_tests/args.py
5@@ -9,11 +9,11 @@ ARG_SETS = {
6 'COLLECT': (
7 (('-p', '--platform'),
8 {'help': 'platform(s) to run tests on', 'metavar': 'PLATFORM',
9- 'action': 'append', 'choices': config.list_enabled_platforms(),
10+ 'action': 'append', 'choices': config.ENABLED_PLATFORMS,
11 'default': []}),
12 (('-n', '--os-name'),
13 {'help': 'the name(s) of the OS(s) to test', 'metavar': 'NAME',
14- 'action': 'append', 'choices': config.list_enabled_distros(),
15+ 'action': 'append', 'choices': config.ENABLED_DISTROS,
16 'default': []}),
17 (('-t', '--test-config'),
18 {'help': 'test config file(s) to use', 'metavar': 'FILE',
19@@ -61,8 +61,12 @@ ARG_SETS = {
20 {'help': 'ppa to enable (implies -u)', 'metavar': 'NAME',
21 'action': 'store'}),
22 (('-u', '--upgrade'),
23- {'help': 'upgrade before starting tests', 'action': 'store_true',
24- 'default': False}),),
25+ {'help': 'upgrade or install cloud-init from repo',
26+ 'action': 'store_true', 'default': False}),
27+ (('--upgrade-full',),
28+ {'help': 'do full system upgrade from repo (implies -u)',
29+ 'action': 'store_true', 'default': False}),),
30+
31 }
32
33 SUBCMDS = {
34@@ -121,15 +125,15 @@ def normalize_collect_args(args):
35 """
36 # platform should default to all supported
37 if len(args.platform) == 0:
38- args.platform = config.list_enabled_platforms()
39+ args.platform = config.ENABLED_PLATFORMS
40 args.platform = util.sorted_unique(args.platform)
41
42 # os name should default to all enabled
43 # if os name is provided ensure that all provided are supported
44 if len(args.os_name) == 0:
45- args.os_name = config.list_enabled_distros()
46+ args.os_name = config.ENABLED_DISTROS
47 else:
48- supported = config.list_enabled_distros()
49+ supported = config.ENABLED_DISTROS
50 invalid = [os_name for os_name in args.os_name
51 if os_name not in supported]
52 if len(invalid) != 0:
53diff --git a/tests/cloud_tests/collect.py b/tests/cloud_tests/collect.py
54index 68b47d7..1d77d1c 100644
55--- a/tests/cloud_tests/collect.py
56+++ b/tests/cloud_tests/collect.py
57@@ -18,8 +18,10 @@ def collect_script(instance, base_dir, script, script_name):
58 return_value: None, may raise errors
59 """
60 LOG.debug('running collect script: %s', script_name)
61- util.write_file(os.path.join(base_dir, script_name),
62- instance.run_script(script))
63+ (out, err, exit) = instance.run_script(
64+ script, rcs=range(0, 256),
65+ description='collect: {}'.format(script_name))
66+ util.write_file(os.path.join(base_dir, script_name), out)
67
68
69 def collect_test_data(args, snapshot, os_name, test_name):
70@@ -39,9 +41,6 @@ def collect_test_data(args, snapshot, os_name, test_name):
71 test_scripts = test_config['collect_scripts']
72 test_output_dir = os.sep.join(
73 (args.data_dir, snapshot.platform_name, os_name, test_name))
74- boot_timeout = (test_config.get('boot_timeout')
75- if isinstance(test_config.get('boot_timeout'), int) else
76- snapshot.config.get('timeout'))
77
78 # if test is not enabled, skip and return 0 failures
79 if not test_config.get('enabled', False):
80@@ -56,7 +55,7 @@ def collect_test_data(args, snapshot, os_name, test_name):
81 LOG.info('collecting test data for test: %s', test_name)
82 with component as instance:
83 start_call = partial(run_single, 'boot instance', partial(
84- instance.start, wait=True, wait_time=boot_timeout))
85+ instance.start, wait=True, wait_for_cloud_init=True))
86 collect_calls = [partial(run_single, 'script {}'.format(script_name),
87 partial(collect_script, instance,
88 test_output_dir, script, script_name))
89@@ -100,10 +99,8 @@ def collect_image(args, platform, os_name):
90 """
91 res = ({}, 1)
92
93- os_config = config.load_os_config(os_name)
94- if not os_config.get('enabled'):
95- raise ValueError('OS {} not enabled'.format(os_name))
96-
97+ os_config = config.load_os_config(
98+ platform.platform_name, os_name, require_enabled=True)
99 component = PlatformComponent(
100 partial(images.get_image, platform, os_config))
101
102@@ -126,10 +123,8 @@ def collect_platform(args, platform_name):
103 """
104 res = ({}, 1)
105
106- platform_config = config.load_platform_config(platform_name)
107- if not platform_config.get('enabled'):
108- raise ValueError('Platform {} not enabled'.format(platform_name))
109-
110+ platform_config = config.load_platform_config(
111+ platform_name, require_enabled=True)
112 component = PlatformComponent(
113 partial(platforms.get_platform, platform_name, platform_config))
114
115diff --git a/tests/cloud_tests/config.py b/tests/cloud_tests/config.py
116index f3a13c9..01cddf7 100644
117--- a/tests/cloud_tests/config.py
118+++ b/tests/cloud_tests/config.py
119@@ -14,6 +14,20 @@ RELEASES_CONF = os.path.join(BASE_DIR, 'releases.yaml')
120 TESTCASE_CONF = os.path.join(BASE_DIR, 'testcases.yaml')
121
122
123+def get(base, key):
124+ """
125+ get config entry 'key' from base, ensuring is dictionary
126+ """
127+ return base[key] if key in base and base[key] is not None else {}
128+
129+
130+def enabled(config):
131+ """
132+ test if config item is enabled
133+ """
134+ return isinstance(config, dict) and config.get('enabled', False)
135+
136+
137 def path_to_name(path):
138 """
139 convert abs or rel path to test config to path under configs/
140@@ -61,22 +75,39 @@ def merge_config(base, override):
141 return res
142
143
144-def load_platform_config(platform):
145+def load_platform_config(platform_name, require_enabled=False):
146 """
147 load configuration for platform
148+ platform_name: name of platform to retrieve config for
149+ require_enabled: if true, raise error if 'enabled' not True
150+ return_value: config dict
151 """
152 main_conf = c_util.read_conf(PLATFORM_CONF)
153- return merge_config(main_conf.get('default_platform_config'),
154- main_conf.get('platforms')[platform])
155+ conf = merge_config(main_conf['default_platform_config'],
156+ main_conf['platforms'][platform_name])
157+ if require_enabled and not enabled(conf):
158+ raise ValueError('Platform is not enabled')
159+ return conf
160
161
162-def load_os_config(os_name):
163+def load_os_config(platform_name, os_name, require_enabled=False):
164 """
165 load configuration for os
166+ platform_name: platform name to load os config for
167+ os_name: name of os to retrieve config for
168+ require_enabled: if true, raise error if 'enabled' not True
169+ return_value: config dict
170 """
171 main_conf = c_util.read_conf(RELEASES_CONF)
172- return merge_config(main_conf.get('default_release_config'),
173- main_conf.get('releases')[os_name])
174+ default = main_conf['default_release_config']
175+ image = main_conf['releases'][os_name]
176+ conf = merge_config(merge_config(get(default, 'default'),
177+ get(default, platform_name)),
178+ merge_config(get(image, 'default'),
179+ get(image, platform_name)))
180+ if require_enabled and not enabled(conf):
181+ raise ValueError('OS is not enabled')
182+ return conf
183
184
185 def load_test_config(path):
186@@ -91,16 +122,22 @@ def list_enabled_platforms():
187 """
188 list all platforms enabled for testing
189 """
190- platforms = c_util.read_conf(PLATFORM_CONF).get('platforms')
191- return [k for k, v in platforms.items() if v.get('enabled')]
192+ platforms = get(c_util.read_conf(PLATFORM_CONF), 'platforms')
193+ return [k for k, v in platforms.items() if enabled(v)]
194
195
196-def list_enabled_distros():
197+def list_enabled_distros(platforms):
198 """
199- list all distros enabled for testing
200+ list all distros enabled for testing on specified platforms
201 """
202- releases = c_util.read_conf(RELEASES_CONF).get('releases')
203- return [k for k, v in releases.items() if v.get('enabled')]
204+
205+ def platform_has_enabled(config):
206+ return any(enabled(merge_config(get(config, 'default'),
207+ get(config, platform)))
208+ for platform in platforms)
209+
210+ releases = get(c_util.read_conf(RELEASES_CONF), 'releases')
211+ return [k for k, v in releases.items() if platform_has_enabled(v)]
212
213
214 def list_test_configs():
215@@ -110,4 +147,8 @@ def list_test_configs():
216 return [os.path.abspath(f) for f in
217 glob.glob(os.sep.join((TEST_CONF_DIR, '*', '*.yaml')))]
218
219+
220+ENABLED_PLATFORMS = list_enabled_platforms()
221+ENABLED_DISTROS = list_enabled_distros(ENABLED_PLATFORMS)
222+
223 # vi: ts=4 expandtab
224diff --git a/tests/cloud_tests/images/base.py b/tests/cloud_tests/images/base.py
225index 394b11f..e61a928 100644
226--- a/tests/cloud_tests/images/base.py
227+++ b/tests/cloud_tests/images/base.py
228@@ -7,13 +7,14 @@ class Image(object):
229 """
230 platform_name = None
231
232- def __init__(self, name, config, platform):
233+ def __init__(self, platform, config):
234 """
235- setup
236+ Set up image
237+ platform: platform object
238+ config: image configuration
239 """
240- self.name = name
241- self.config = config
242 self.platform = platform
243+ self.config = config
244
245 def __str__(self):
246 """
247@@ -28,10 +29,16 @@ class Image(object):
248 """
249 raise NotImplementedError
250
251- # FIXME: instead of having execute and push_file and other instance methods
252- # here which pass through to a hidden instance, it might be better
253- # to expose an instance that the image can be modified through
254- def execute(self, command, stdin=None, stdout=None, stderr=None, env={}):
255+ @property
256+ def setup_overrides(self):
257+ """
258+ setup options that need to be overridden for the image
259+ return_value: dictionary to update args with
260+ """
261+ # NOTE: more sophisticated options may be requied at some point
262+ return self.config.get('setup_overrides', {})
263+
264+ def execute(self, *args, **kwargs):
265 """
266 execute command in image, modifying image
267 """
268@@ -43,7 +50,7 @@ class Image(object):
269 """
270 raise NotImplementedError
271
272- def run_script(self, script):
273+ def run_script(self, *args, **kwargs):
274 """
275 run script in image, modifying image
276 return_value: script output
277diff --git a/tests/cloud_tests/images/lxd.py b/tests/cloud_tests/images/lxd.py
278index 7a41614..1fdb91e 100644
279--- a/tests/cloud_tests/images/lxd.py
280+++ b/tests/cloud_tests/images/lxd.py
281@@ -2,6 +2,10 @@
282
283 from tests.cloud_tests.images import base
284 from tests.cloud_tests.snapshots import lxd as lxd_snapshot
285+from tests.cloud_tests import util
286+
287+import os
288+import shutil
289
290
291 class LXDImage(base.Image):
292@@ -10,27 +14,43 @@ class LXDImage(base.Image):
293 """
294 platform_name = "lxd"
295
296- def __init__(self, name, config, platform, pylxd_image):
297+ def __init__(self, platform, config, pylxd_image):
298 """
299- setup
300+ Set up image
301+ platform: platform object
302+ config: image configuration
303 """
304- self.platform = platform
305- self._pylxd_image = pylxd_image
306+ self.modified = False
307 self._instance = None
308- super(LXDImage, self).__init__(name, config, platform)
309+ self._pylxd_image = None
310+ self.pylxd_image = pylxd_image
311+ super(LXDImage, self).__init__(platform, config)
312
313 @property
314 def pylxd_image(self):
315- self._pylxd_image.sync()
316+ if self._pylxd_image:
317+ self._pylxd_image.sync()
318 return self._pylxd_image
319
320+ @pylxd_image.setter
321+ def pylxd_image(self, pylxd_image):
322+ if self._instance:
323+ self._instance.destroy()
324+ self._instance = None
325+ if (self._pylxd_image and
326+ (self._pylxd_image is not pylxd_image) and
327+ (not self.config.get('cache_base_image') or self.modified)):
328+ self._pylxd_image.delete(wait=True)
329+ self.modified = False
330+ self._pylxd_image = pylxd_image
331+
332 @property
333 def instance(self):
334 if not self._instance:
335 self._instance = self.platform.launch_container(
336- image=self.pylxd_image.fingerprint,
337- image_desc=str(self), use_desc='image-modification')
338- self._instance.start(wait=True, wait_time=self.config.get('timeout'))
339+ self.properties, self.config, use_desc='image-modification',
340+ image_desc=str(self), image=self.pylxd_image.fingerprint)
341+ self._instance.start()
342 return self._instance
343
344 @property
345@@ -46,6 +66,78 @@ class LXDImage(base.Image):
346 'release': properties.get('release'),
347 }
348
349+ def export_image(self, output_dir):
350+ """
351+ export image from lxd image store to (split) tarball on disk
352+ output_dir: dir to store tarballs in
353+ return_value: tuple of path to metadata tarball and rootfs tarball
354+ """
355+ # pylxd's image export feature doesn't do split exports, so use cmdline
356+ util.subp(['lxc', 'image', 'export', self.pylxd_image.fingerprint,
357+ output_dir], capture=True)
358+ tarballs = [p for p in os.listdir(output_dir) if p.endswith('tar.xz')]
359+ metadata = os.path.join(
360+ output_dir, next(p for p in tarballs if p.startswith('meta-')))
361+ rootfs = os.path.join(
362+ output_dir, next(p for p in tarballs if not p.startswith('meta-')))
363+ return (metadata, rootfs)
364+
365+ def import_image(self, metadata, rootfs):
366+ """
367+ import image to lxd image store from (split) tarball on disk
368+ note, this will replace and delete the current pylxd_image
369+ metadata: metadata tarball
370+ rootfs: rootfs tarball
371+ return_value: imported image fingerprint
372+ """
373+ alias = util.gen_instance_name(
374+ image_desc=str(self), use_desc='update-metadata')
375+ util.subp(['lxc', 'image', 'import', metadata, rootfs,
376+ '--alias', alias], capture=True)
377+ self.pylxd_image = self.platform.query_image_by_alias(alias)
378+ return self.pylxd_image.fingerprint
379+
380+ def update_templates(self, template_config, template_data):
381+ """
382+ update the image's template configuration
383+ note, this will replace and delete the current pylxd_image
384+ template_config: config overrides for template portion of metadata
385+ template_data: template data to place into templates/
386+ """
387+ # set up tmp files
388+ export_dir = util.tmpdir()
389+ extract_dir = util.tmpdir()
390+ new_metadata = os.path.join(export_dir, 'new-meta.tar.xz')
391+ metadata_yaml = os.path.join(extract_dir, 'metadata.yaml')
392+ template_dir = os.path.join(extract_dir, 'templates')
393+
394+ try:
395+ # extract old data
396+ (metadata, rootfs) = self.export_image(export_dir)
397+ shutil.unpack_archive(metadata, extract_dir)
398+
399+ # update metadata
400+ metadata = util.read_conf(metadata_yaml)
401+ templates = metadata.get('templates', {})
402+ templates.update(template_config)
403+ metadata['templates'] = templates
404+ util.yaml_dump(metadata, metadata_yaml)
405+
406+ # write out template files
407+ for name, content in template_data.items():
408+ path = os.path.join(template_dir, name)
409+ util.write_file(path, content)
410+
411+ # store new data, mark new image as modified
412+ util.flat_tar(new_metadata, extract_dir)
413+ self.import_image(new_metadata, rootfs)
414+ self.modified = True
415+
416+ finally:
417+ # remove tmpfiles
418+ shutil.rmtree(export_dir)
419+ shutil.rmtree(extract_dir)
420+
421 def execute(self, *args, **kwargs):
422 """
423 execute command in image, modifying image
424@@ -58,12 +150,12 @@ class LXDImage(base.Image):
425 """
426 return self.instance.push_file(local_path, remote_path)
427
428- def run_script(self, script):
429+ def run_script(self, *args, **kwargs):
430 """
431 run script in image, modifying image
432 return_value: script output
433 """
434- return self.instance.run_script(script)
435+ return self.instance.run_script(*args, **kwargs)
436
437 def snapshot(self):
438 """
439@@ -71,22 +163,20 @@ class LXDImage(base.Image):
440 """
441 # clone current instance, start and freeze clone
442 instance = self.platform.launch_container(
443- container=self.instance.name, image_desc=str(self),
444- use_desc='snapshot')
445- instance.start(wait=True, wait_time=self.config.get('timeout'))
446+ self.properties, self.config, container=self.instance.name,
447+ image_desc=str(self), use_desc='snapshot')
448+ instance.start()
449 if self.config.get('boot_clean_script'):
450 instance.run_script(self.config.get('boot_clean_script'))
451 instance.freeze()
452 return lxd_snapshot.LXDSnapshot(
453- self.properties, self.config, self.platform, instance)
454+ self.platform, self.properties, self.config, instance)
455
456 def destroy(self):
457 """
458 clean up data associated with image
459 """
460- if self._instance:
461- self._instance.destroy()
462- self.pylxd_image.delete(wait=True)
463+ self.pylxd_image = None
464 super(LXDImage, self).destroy()
465
466 # vi: ts=4 expandtab
467diff --git a/tests/cloud_tests/instances/base.py b/tests/cloud_tests/instances/base.py
468index 9559d28..9bcba6a 100644
469--- a/tests/cloud_tests/instances/base.py
470+++ b/tests/cloud_tests/instances/base.py
471@@ -1,8 +1,5 @@
472 # This file is part of cloud-init. See LICENSE file for license information.
473
474-import os
475-import uuid
476-
477
478 class Instance(object):
479 """
480@@ -10,26 +7,37 @@ class Instance(object):
481 """
482 platform_name = None
483
484- def __init__(self, name):
485+ def __init__(self, platform, name, properties, config):
486 """
487- setup
488+ Set up instance
489+ platform: platform object
490+ name: hostname of instance
491+ properties: image properties
492+ config: image config
493 """
494+ self.platform = platform
495 self.name = name
496+ self.properties = properties
497+ self.config = config
498
499- def execute(self, command, stdin=None, stdout=None, stderr=None, env={}):
500+ def execute(self, command, stdout=None, stderr=None, env={},
501+ rcs=None, description=None):
502 """
503+ Execute command in instance, recording output, error and exit code.
504+ Assumes functional networking and execution as root with the
505+ target filesystem being available at /.
506+
507 command: the command to execute as root inside the image
508- stdin, stderr, stdout: file handles
509+ stdout, stderr: file handles to write output and error to
510 env: environment variables
511-
512- Execute assumes functional networking and execution as root with the
513- target filesystem being available at /.
514+ rcs: allowed return codes from command
515+ description: purpose of command
516
517 return_value: tuple containing stdout data, stderr data, exit code
518 """
519 raise NotImplementedError
520
521- def read_data(self, remote_path, encode=False):
522+ def read_data(self, remote_path, decode=False):
523 """
524 read_data from instance filesystem
525 remote_path: path in instance
526@@ -49,6 +57,8 @@ class Instance(object):
527 def pull_file(self, remote_path, local_path):
528 """
529 copy file at 'remote_path', from instance to 'local_path'
530+ remote_path: path on remote instance
531+ local_path: path on local instance
532 """
533 with open(local_path, 'wb') as fp:
534 fp.write(self.read_data(remote_path), encode=True)
535@@ -56,18 +66,34 @@ class Instance(object):
536 def push_file(self, local_path, remote_path):
537 """
538 copy file at 'local_path' to instance at 'remote_path'
539+ local_path: path on local instance
540+ remote_path: path on remote instance
541 """
542 with open(local_path, 'rb') as fp:
543 self.write_data(remote_path, fp.read())
544
545- def run_script(self, script):
546+ def run_script(self, script, rcs=None, description=None):
547 """
548 run script in target and return stdout
549+ script: script contents
550+ rcs: allowed return codes from script
551+ description: purpose of script
552+ return_value: stdout from script
553 """
554- script_path = os.path.join('/tmp', str(uuid.uuid1()))
555- self.write_data(script_path, script)
556- (out, err, exit_code) = self.execute(['/bin/bash', script_path])
557- return out
558+ script_path = self.tmpfile()
559+ try:
560+ self.write_data(script_path, script)
561+ return self.execute(
562+ ['/bin/bash', script_path], rcs=rcs, description=description)
563+ finally:
564+ self.execute(['rm', script_path], rcs=rcs)
565+
566+ def tmpfile(self):
567+ """
568+ get a tmp file in the target
569+ return_value: path to new file in target
570+ """
571+ return self.execute(['mktemp'])[0].strip()
572
573 def console_log(self):
574 """
575@@ -87,7 +113,7 @@ class Instance(object):
576 """
577 raise NotImplementedError
578
579- def start(self, wait=True):
580+ def start(self, wait=True, wait_for_cloud_init=False):
581 """
582 start instance
583 """
584@@ -99,22 +125,32 @@ class Instance(object):
585 """
586 pass
587
588- def _wait_for_cloud_init(self, wait_time):
589+ def _wait_for_system(self, wait_for_cloud_init):
590 """
591 wait until system has fully booted and cloud-init has finished
592+ wait_time: maximum time to wait
593+ return_value: None, may raise OSError if wait_time exceeded
594 """
595- if not wait_time:
596- return
597
598- found_msg = 'found'
599- cmd = ('for ((i=0;i<{wait};i++)); do [ -f "{file}" ] && '
600- '{{ echo "{msg}";break; }} || sleep 1; done').format(
601- file='/run/cloud-init/result.json',
602- wait=wait_time, msg=found_msg)
603+ def clean_test(test):
604+ """
605+ clean formatting for system ready test testcase
606+ """
607+ return ' '.join(l for l in test.strip().splitlines()
608+ if not l.lstrip().startswith('#'))
609+
610+ time = self.config['boot_timeout']
611+ tests = [self.config['system_ready_script']]
612+ if wait_for_cloud_init:
613+ tests.append(self.config['cloud_init_ready_script'])
614+
615+ formatted_tests = ' && '.join(clean_test(t) for t in tests)
616+ test_cmd = ('for ((i=0;i<{time};i++)); do {test} && exit 0; sleep 1; '
617+ 'done; exit 1;').format(time=time, test=formatted_tests)
618+ cmd = ['/bin/bash', '-c', test_cmd]
619+
620+ if self.execute(cmd, rcs=(0, 1))[-1] != 0:
621+ raise OSError('timeout: after {}s system not started'.format(time))
622
623- (out, err, exit) = self.execute(['/bin/bash', '-c', cmd])
624- if out.strip() != found_msg:
625- raise OSError('timeout: after {}s, cloud-init has not started'
626- .format(wait_time))
627
628 # vi: ts=4 expandtab
629diff --git a/tests/cloud_tests/instances/lxd.py b/tests/cloud_tests/instances/lxd.py
630index f0aa121..a90c88d 100644
631--- a/tests/cloud_tests/instances/lxd.py
632+++ b/tests/cloud_tests/instances/lxd.py
633@@ -1,6 +1,7 @@
634 # This file is part of cloud-init. See LICENSE file for license information.
635
636 from tests.cloud_tests.instances import base
637+from tests.cloud_tests import util
638
639
640 class LXDInstance(base.Instance):
641@@ -9,41 +10,66 @@ class LXDInstance(base.Instance):
642 """
643 platform_name = "lxd"
644
645- def __init__(self, name, platform, pylxd_container):
646+ def __init__(self, platform, name, properties, config, pylxd_container):
647 """
648- setup
649+ Set up instance
650+ platform: platform object
651+ name: hostname of instance
652+ properties: image properties
653+ config: image config
654 """
655- self.platform = platform
656 self._pylxd_container = pylxd_container
657- super(LXDInstance, self).__init__(name)
658+ super(LXDInstance, self).__init__(platform, name, properties, config)
659
660 @property
661 def pylxd_container(self):
662 self._pylxd_container.sync()
663 return self._pylxd_container
664
665- def execute(self, command, stdin=None, stdout=None, stderr=None, env={}):
666+ def execute(self, command, stdout=None, stderr=None, env={},
667+ rcs=None, description=None):
668 """
669+ Execute command in instance, recording output, error and exit code.
670+ Assumes functional networking and execution as root with the
671+ target filesystem being available at /.
672+
673 command: the command to execute as root inside the image
674- stdin, stderr, stdout: file handles
675+ stdout, stderr: file handles to write output and error to
676 env: environment variables
677-
678- Execute assumes functional networking and execution as root with the
679- target filesystem being available at /.
680+ rcs: allowed return codes from command
681+ description: purpose of command
682
683 return_value: tuple containing stdout data, stderr data, exit code
684 """
685- # TODO: the pylxd api handler for container.execute needs to be
686- # extended to properly pass in stdin
687- # TODO: the pylxd api handler for container.execute needs to be
688- # extended to get the return code, for now just use 0
689+ # ensure instance is running and execute the command
690 self.start()
691- if stdin:
692- raise NotImplementedError
693 res = self.pylxd_container.execute(command, environment=env)
694- for (f, data) in (i for i in zip((stdout, stderr), res) if i[0]):
695- f.write(data)
696- return res + (0,)
697+
698+ # get out, exit and err from pylxd return
699+ if hasattr(res, 'exit_code'):
700+ # pylxd 2.2 returns ContainerExecuteResult, named tuple of
701+ # (exit_code, out, err)
702+ (exit, out, err) = res
703+ else:
704+ # pylxd 2.1.3 and earlier only return out and err, no exit
705+ # LOG.warning('using pylxd version < 2.2')
706+ (out, err) = res
707+ exit = 0
708+
709+ # write data to file descriptors if needed
710+ if stdout:
711+ stdout.write(out)
712+ if stderr:
713+ stderr.write(err)
714+
715+ # if the command exited with a code not allowed in rcs, then fail
716+ if exit not in (rcs if rcs else (0,)):
717+ error_desc = ('Failed command to: {}'.format(description)
718+ if description else None)
719+ raise util.InTargetExecuteError(
720+ out, err, exit, command, self.name, error_desc)
721+
722+ return (out, err, exit)
723
724 def read_data(self, remote_path, decode=False):
725 """
726@@ -83,14 +109,14 @@ class LXDInstance(base.Instance):
727 if self.pylxd_container.status != 'Stopped':
728 self.pylxd_container.stop(wait=wait)
729
730- def start(self, wait=True, wait_time=None):
731+ def start(self, wait=True, wait_for_cloud_init=False):
732 """
733 start instance
734 """
735 if self.pylxd_container.status != 'Running':
736 self.pylxd_container.start(wait=wait)
737- if wait and isinstance(wait_time, int):
738- self._wait_for_cloud_init(wait_time)
739+ if wait:
740+ self._wait_for_system(wait_for_cloud_init)
741
742 def freeze(self):
743 """
744diff --git a/tests/cloud_tests/platforms.yaml b/tests/cloud_tests/platforms.yaml
745index 5972b32..b91834a 100644
746--- a/tests/cloud_tests/platforms.yaml
747+++ b/tests/cloud_tests/platforms.yaml
748@@ -10,7 +10,55 @@ default_platform_config:
749 platforms:
750 lxd:
751 enabled: true
752- get_image_timeout: 600
753+ # overrides for image templates
754+ template_overrides:
755+ /var/lib/cloud/seed/nocloud-net/meta-data:
756+ when:
757+ - create
758+ - copy
759+ template: cloud-init-meta.tpl
760+ /var/lib/cloud/seed/nocloud-net/network-config:
761+ when:
762+ - create
763+ - copy
764+ template: cloud-init-network.tpl
765+ /var/lib/cloud/seed/nocloud-net/user-data:
766+ when:
767+ - create
768+ - copy
769+ template: cloud-init-user.tpl
770+ properties:
771+ default: |
772+ #cloud-config
773+ {}
774+ /var/lib/cloud/seed/nocloud-net/vendor-data:
775+ when:
776+ - create
777+ - copy
778+ template: cloud-init-vendor.tpl
779+ properties:
780+ default: |
781+ #cloud-config
782+ {}
783+ # overrides image template files
784+ template_files:
785+ cloud-init-meta.tpl: |
786+ #cloud-config
787+ instance-id: {{ container.name }}
788+ local-hostname: {{ container.name }}
789+ {{ config_get("user.meta-data", "") }}
790+ cloud-init-network.tpl: |
791+ {% if config_get("user.network-config", "") == "" %}version: 1
792+ config:
793+ - type: physical
794+ name: eth0
795+ subnets:
796+ - type: {% if config_get("user.network_mode", "") == "link-local" %}manual{% else %}dhcp{% endif %}
797+ control: auto{% else %}{{ config_get("user.network-config", "") }}{% endif %}
798+ cloud-init-user.tpl: |
799+ {{ config_get("user.user-data", properties.default) }}
800+ cloud-init-vendor.tpl: |
801+ {{ config_get("user.vendor-data", properties.default) }}
802 ec2: {}
803 azure: {}
804
805diff --git a/tests/cloud_tests/platforms/base.py b/tests/cloud_tests/platforms/base.py
806index 615e2e0..2b6e514 100644
807--- a/tests/cloud_tests/platforms/base.py
808+++ b/tests/cloud_tests/platforms/base.py
809@@ -15,17 +15,7 @@ class Platform(object):
810
811 def get_image(self, img_conf):
812 """
813- Get image using 'img_conf', where img_conf is a dict containing all
814- image configuration parameters
815-
816- in this dict there must be a 'platform_ident' key containing
817- configuration for identifying each image on a per platform basis
818-
819- see implementations for get_image() for details about the contents
820- of the platform's config entry
821-
822- note: see 'releases' main_config.yaml for example entries
823-
824+ get image using specified image configuration
825 img_conf: configuration for image
826 return_value: cloud_tests.images instance
827 """
828@@ -37,17 +27,4 @@ class Platform(object):
829 """
830 pass
831
832- def _extract_img_platform_config(self, img_conf):
833- """
834- extract platform configuration for current platform from img_conf
835- """
836- platform_ident = img_conf.get('platform_ident')
837- if not platform_ident:
838- raise ValueError('invalid img_conf, missing \'platform_ident\'')
839- ident = platform_ident.get(self.platform_name)
840- if not ident:
841- raise ValueError('img_conf: {} missing config for platform {}'
842- .format(img_conf, self.platform_name))
843- return ident
844-
845 # vi: ts=4 expandtab
846diff --git a/tests/cloud_tests/platforms/lxd.py b/tests/cloud_tests/platforms/lxd.py
847index 847cc54..82e2710 100644
848--- a/tests/cloud_tests/platforms/lxd.py
849+++ b/tests/cloud_tests/platforms/lxd.py
850@@ -27,28 +27,30 @@ class LXDPlatform(base.Platform):
851
852 def get_image(self, img_conf):
853 """
854- Get image
855- img_conf: dict containing config for image. platform_ident must have:
856- alias: alias to use for simplestreams server
857- sstreams_server: simplestreams server to use, or None for default
858+ get image using specified image configuration
859+ img_conf: configuration for image
860 return_value: cloud_tests.images instance
861 """
862- lxd_conf = self._extract_img_platform_config(img_conf)
863- image = self.client.images.create_from_simplestreams(
864- lxd_conf.get('sstreams_server', DEFAULT_SSTREAMS_SERVER),
865- lxd_conf['alias'])
866- return lxd_image.LXDImage(
867- image.properties['description'], img_conf, self, image)
868+ pylxd_image = self.client.images.create_from_simplestreams(
869+ img_conf.get('sstreams_server', DEFAULT_SSTREAMS_SERVER),
870+ img_conf['alias'])
871+ image = lxd_image.LXDImage(self, img_conf, pylxd_image)
872+ if img_conf.get('override_templates', False):
873+ image.update_templates(self.config.get('template_overrides', {}),
874+ self.config.get('template_files', {}))
875+ return image
876
877- def launch_container(self, image=None, container=None, ephemeral=False,
878- config=None, block=True,
879+ def launch_container(self, properties, config, image=None, container=None,
880+ ephemeral=False, container_config=None, block=True,
881 image_desc=None, use_desc=None):
882 """
883 launch a container
884+ properties: image properties
885+ config: image configuration
886 image: image fingerprint to launch from
887 container: container to copy
888 ephemeral: delete image after first shutdown
889- config: config options for instance as dict
890+ container_config: config options for instance as dict
891 block: wait until container created
892 image_desc: description of image being launched
893 use_desc: description of container's use
894@@ -61,11 +63,13 @@ class LXDPlatform(base.Platform):
895 use_desc=use_desc,
896 used_list=self.list_containers()),
897 'ephemeral': bool(ephemeral),
898- 'config': config if isinstance(config, dict) else {},
899+ 'config': (container_config
900+ if isinstance(container_config, dict) else {}),
901 'source': ({'type': 'image', 'fingerprint': image} if image else
902 {'type': 'copy', 'source': container})
903 }, wait=block)
904- return lxd_instance.LXDInstance(container.name, self, container)
905+ return lxd_instance.LXDInstance(self, container.name, properties,
906+ config, container)
907
908 def container_exists(self, container_name):
909 """
910@@ -88,6 +92,14 @@ class LXDPlatform(base.Platform):
911 """
912 return [container.name for container in self.client.containers.all()]
913
914+ def query_image_by_alias(self, alias):
915+ """
916+ get image by alias in local image store
917+ alias: alias of image
918+ return_value: pylxd image (not cloud_tests.images instance)
919+ """
920+ return self.client.images.get_by_alias(alias)
921+
922 def destroy(self):
923 """
924 Clean up platform data
925diff --git a/tests/cloud_tests/releases.yaml b/tests/cloud_tests/releases.yaml
926index 3ffa68f..b18b095 100644
927--- a/tests/cloud_tests/releases.yaml
928+++ b/tests/cloud_tests/releases.yaml
929@@ -1,79 +1,145 @@
930 # ============================= Release Config ================================
931 default_release_config:
932- # all are disabled by default
933- enabled: false
934- # timeout for booting image and running cloud init
935- timeout: 120
936- # platform_ident values for the image, with data to identify the image
937- # on that platform. see platforms.base for more information
938- platform_ident: {}
939- # a script to run after a boot that is used to modify an image, before
940- # making a snapshot of the image. may be useful for removing data left
941- # behind from cloud-init booting, such as logs, to ensure that data from
942- # snapshot.launch() will not include a cloud-init.log from a boot used to
943- # create the snapshot, if cloud-init has not run
944- boot_clean_script: |
945- #!/bin/bash
946- rm -rf /var/log/cloud-init.log /var/log/cloud-init-output.log \
947- /var/lib/cloud/ /run/cloud-init/ /var/log/syslog
948+ # global default configuration options
949+ default:
950+ # all are disabled by default
951+ enabled: false
952+ # timeout for booting image and running cloud init
953+ boot_timeout: 120
954+ # a script to run after a boot that is used to modify an image, before
955+ # making a snapshot of the image. may be useful for removing data left
956+ # behind from cloud-init booting, such as logs, to ensure that data
957+ # from snapshot.launch() will not include a cloud-init.log from a boot
958+ # used to create the snapshot, if cloud-init has not run
959+ boot_clean_script: |
960+ #!/bin/bash
961+ rm -rf /var/log/cloud-init.log /var/log/cloud-init-output.log \
962+ /var/lib/cloud/ /run/cloud-init/ /var/log/syslog
963+ # test script to determine if system is booted fully
964+ system_ready_script: |
965+ # permit running or degraded state as both indicate complete boot
966+ [ $(systemctl is-system-running) = 'running' -o
967+ $(systemctl is-system-running) = 'degraded' ]
968+ # test script to determine if cloud-init has finished
969+ cloud_init_ready_script: |
970+ [ -f '/run/cloud-init/result.json' ]
971+
972+ # lxd specific default configuration options
973+ lxd:
974+ # default sstreams server to use for lxd image retrieval
975+ sstreams_server: https://us.images.linuxcontainers.org:8443
976+ # keep base image, avoids downloading again next run
977+ cache_base_image: true
978+ # lxd images from linuxcontainers.org do not have the nocloud seed
979+ # templates in place, so the image metadata must be modified
980+ override_templates: true
981+ # arg overrides to set image up
982+ setup_overrides:
983+ # lxd images from linuxcontainers.org do not come with
984+ # cloud-init, so must pull cloud-init in from repo using
985+ # setup_image.upgrade
986+ upgrade: true
987
988 releases:
989- trusty:
990- enabled: true
991- platform_ident:
992- lxd:
993- # if sstreams_server is omitted, default is used, defined in
994- # tests.cloud_tests.platforms.lxd.DEFAULT_SSTREAMS_SERVER as:
995- # sstreams_server: https://us.images.linuxcontainers.org:8443
996- #alias: ubuntu/trusty/default
997- alias: t
998- sstreams_server: https://cloud-images.ubuntu.com/daily
999- xenial:
1000- enabled: true
1001- platform_ident:
1002- lxd:
1003- #alias: ubuntu/xenial/default
1004- alias: x
1005- sstreams_server: https://cloud-images.ubuntu.com/daily
1006- yakkety:
1007- enabled: true
1008- platform_ident:
1009- lxd:
1010- #alias: ubuntu/yakkety/default
1011- alias: y
1012- sstreams_server: https://cloud-images.ubuntu.com/daily
1013+ # UBUNTU =================================================================
1014 zesty:
1015- enabled: true
1016- platform_ident:
1017- lxd:
1018- #alias: ubuntu/zesty/default
1019- alias: z
1020- sstreams_server: https://cloud-images.ubuntu.com/daily
1021- jessie:
1022- platform_ident:
1023- lxd:
1024- alias: debian/jessie/default
1025+ # EOL: Jan 2018
1026+ default:
1027+ enabled: true
1028+ lxd:
1029+ sstreams_server: https://cloud-images.ubuntu.com/daily
1030+ alias: zesty
1031+ setup_overrides: null
1032+ override_templates: false
1033+ yakkety:
1034+ # EOL: Jul 2017
1035+ default:
1036+ enabled: true
1037+ lxd:
1038+ sstreams_server: https://cloud-images.ubuntu.com/daily
1039+ alias: yakkety
1040+ setup_overrides: null
1041+ override_templates: false
1042+ xenial:
1043+ # EOL: Apr 2021
1044+ default:
1045+ enabled: true
1046+ lxd:
1047+ sstreams_server: https://cloud-images.ubuntu.com/daily
1048+ alias: xenial
1049+ setup_overrides: null
1050+ override_templates: false
1051+ trusty:
1052+ # EOL: Apr 2019
1053+ default:
1054+ enabled: true
1055+ system_ready_script: |
1056+ #!/bin/bash
1057+ # upstart based, so use old style runlevels
1058+ [ $(runlevel | awk '{print $2}') = '2' ]
1059+ lxd:
1060+ sstreams_server: https://cloud-images.ubuntu.com/daily
1061+ alias: trusty
1062+ setup_overrides: null
1063+ override_templates: false
1064+ precise:
1065+ # EOL: Apr 2017
1066+ default:
1067+ # still supported but not relevant for development, not enabled
1068+ # tests should still work though unless they use newer features
1069+ enabled: false
1070+ system_ready_script: |
1071+ #!/bin/bash
1072+ # upstart based, so use old style runlevels
1073+ [ $(runlevel | awk '{print $2}') = '2' ]
1074+ lxd:
1075+ sstreams_server: https://cloud-images.ubuntu.com/daily
1076+ alias: precise
1077+ setup_overrides: null
1078+ override_templates: false
1079+ # DEBIAN =================================================================
1080 sid:
1081- platform_ident:
1082- lxd:
1083- alias: debian/sid/default
1084+ # EOL: N/A
1085+ default:
1086+ # tests should work on sid, however it is not always stable
1087+ enabled: false
1088+ lxd:
1089+ alias: debian/sid/default
1090 stretch:
1091- platform_ident:
1092- lxd:
1093- alias: debian/stretch/default
1094+ # EOL: Not yet released
1095+ default:
1096+ enabled: true
1097+ lxd:
1098+ alias: debian/stretch/default
1099+ jessie:
1100+ # EOL: Jun 2020
1101+ default:
1102+ enabled: true
1103+ lxd:
1104+ alias: debian/jessie/default
1105 wheezy:
1106- platform_ident:
1107- lxd:
1108- alias: debian/wheezy/default
1109+ # EOL: May 2018 (Apr 2016 - end of full updates)
1110+ default:
1111+ # this is old enough that it is no longer relevant for development
1112+ enabled: false
1113+ lxd:
1114+ alias: debian/wheezy/default
1115+ # CENTOS =================================================================
1116 centos70:
1117- timeout: 180
1118- platform_ident:
1119- lxd:
1120- alias: centos/7/default
1121+ # EOL: Jun 2024 (2020 - end of full updates)
1122+ default:
1123+ enabled: true
1124+ lxd:
1125+ alias: centos/7/default
1126 centos66:
1127- timeout: 180
1128- platform_ident:
1129- lxd:
1130- alias: centos/6/default
1131+ # EOL: Nov 2020
1132+ default:
1133+ enabled: true
1134+ # still supported, but only bugfixes after may 2017
1135+ system_ready_script: |
1136+ #!/bin/bash
1137+ [ $(runlevel | awk '{print $2}') = '3' ]
1138+ lxd:
1139+ alias: centos/6/default
1140
1141 # vi: ts=4 expandtab
1142diff --git a/tests/cloud_tests/setup_image.py b/tests/cloud_tests/setup_image.py
1143index 5d6c638..1b74ceb 100644
1144--- a/tests/cloud_tests/setup_image.py
1145+++ b/tests/cloud_tests/setup_image.py
1146@@ -7,6 +7,30 @@ from functools import partial
1147 import os
1148
1149
1150+def installed_version(image, package, ensure_installed=True):
1151+ """
1152+ get installed version of package
1153+ image: cloud_tests.images instance to operate on
1154+ package: name of package
1155+ ensure_installed: raise error if not installed
1156+ return_value: cloud-init version string
1157+ """
1158+ # get right cmd for os family
1159+ os_family = util.get_os_family(image.properties['os'])
1160+ if os_family == 'debian':
1161+ cmd = ['dpkg-query', '-W', "--showformat='${Version}'", package]
1162+ elif os_family == 'redhat':
1163+ cmd = ['rpm', '-q', '--queryformat', "'%{VERSION}'", package]
1164+ else:
1165+ raise NotImplementedError
1166+
1167+ # query version
1168+ msg = 'query version for package: {}'.format(package)
1169+ (out, err, exit) = image.execute(
1170+ cmd, description=msg, rcs=(0,) if ensure_installed else range(0, 256))
1171+ return out.strip()
1172+
1173+
1174 def install_deb(args, image):
1175 """
1176 install deb into image
1177@@ -21,20 +45,18 @@ def install_deb(args, image):
1178 'family: {}'.format(args.deb, os_family))
1179
1180 # install deb
1181- LOG.debug('installing deb: %s into target', args.deb)
1182+ msg = 'install deb: "{}" into target'.format(args.deb)
1183+ LOG.debug(msg)
1184 remote_path = os.path.join('/tmp', os.path.basename(args.deb))
1185 image.push_file(args.deb, remote_path)
1186- (out, err, exit) = image.execute(['dpkg', '-i', remote_path])
1187- if exit != 0:
1188- raise OSError('failed install deb: {}\n\tstdout: {}\n\tstderr: {}'
1189- .format(args.deb, out, err))
1190+ cmd = 'dpkg -i {} || apt-get install --yes -f'.format(remote_path)
1191+ image.execute(['/bin/sh', '-c', cmd], description=msg)
1192
1193 # check installed deb version matches package
1194 fmt = ['-W', "--showformat='${Version}'"]
1195 (out, err, exit) = image.execute(['dpkg-deb'] + fmt + [remote_path])
1196 expected_version = out.strip()
1197- (out, err, exit) = image.execute(['dpkg-query'] + fmt + ['cloud-init'])
1198- found_version = out.strip()
1199+ found_version = installed_version(image, 'cloud-init')
1200 if expected_version != found_version:
1201 raise OSError('install deb version "{}" does not match expected "{}"'
1202 .format(found_version, expected_version))
1203@@ -52,24 +74,21 @@ def install_rpm(args, image):
1204 """
1205 # ensure system is compatible with package format
1206 os_family = util.get_os_family(image.properties['os'])
1207- if os_family not in ['redhat', 'sles']:
1208+ if os_family != 'redhat':
1209 raise NotImplementedError('install rpm: {} not supported on os '
1210 'family: {}'.format(args.rpm, os_family))
1211
1212 # install rpm
1213- LOG.debug('installing rpm: %s into target', args.rpm)
1214+ msg = 'install rpm: "{}" into target'.format(args.rpm)
1215+ LOG.debug(msg)
1216 remote_path = os.path.join('/tmp', os.path.basename(args.rpm))
1217 image.push_file(args.rpm, remote_path)
1218- (out, err, exit) = image.execute(['rpm', '-U', remote_path])
1219- if exit != 0:
1220- raise OSError('failed to install rpm: {}\n\tstdout: {}\n\tstderr: {}'
1221- .format(args.rpm, out, err))
1222+ image.execute(['rpm', '-U', remote_path], description=msg)
1223
1224 fmt = ['--queryformat', '"%{VERSION}"']
1225 (out, err, exit) = image.execute(['rpm', '-q'] + fmt + [remote_path])
1226 expected_version = out.strip()
1227- (out, err, exit) = image.execute(['rpm', '-q'] + fmt + ['cloud-init'])
1228- found_version = out.strip()
1229+ found_version = installed_version(image, 'cloud-init')
1230 if expected_version != found_version:
1231 raise OSError('install rpm version "{}" does not match expected "{}"'
1232 .format(found_version, expected_version))
1233@@ -80,13 +99,34 @@ def install_rpm(args, image):
1234
1235 def upgrade(args, image):
1236 """
1237- run the system's upgrade command
1238+ upgrade or install cloud-init from repo
1239+ args: cmdline arguments
1240+ image: cloud_tests.images instance to operate on
1241+ return_value: None, may raise errors
1242+ """
1243+ # determine command for os_family
1244+ os_family = util.get_os_family(image.properties['os'])
1245+ if os_family == 'debian':
1246+ cmd = 'apt-get update && apt-get install cloud-init --yes'
1247+ elif os_family == 'redhat':
1248+ cmd = 'yum install cloud-init --assumeyes'
1249+ else:
1250+ raise NotImplementedError
1251+
1252+ # upgrade cloud-init
1253+ msg = 'upgrading cloud-init'
1254+ LOG.debug(msg)
1255+ image.execute(['/bin/sh', '-c', cmd], description=msg)
1256+
1257+
1258+def upgrade_full(args, image):
1259+ """
1260+ run the system's full upgrade command
1261 args: cmdline arguments
1262 image: cloud_tests.images instance to operate on
1263 return_value: None, may raise errors
1264 """
1265 # determine appropriate upgrade command for os_family
1266- # TODO: maybe use cloudinit.distros for this?
1267 os_family = util.get_os_family(image.properties['os'])
1268 if os_family == 'debian':
1269 cmd = 'apt-get update && apt-get upgrade --yes'
1270@@ -97,11 +137,9 @@ def upgrade(args, image):
1271 'from family: {}'.format(os_family))
1272
1273 # upgrade system
1274- LOG.debug('upgrading system')
1275- (out, err, exit) = image.execute(['/bin/sh', '-c', cmd])
1276- if exit != 0:
1277- raise OSError('failed to upgrade system\n\tstdout: {}\n\tstderr:{}'
1278- .format(out, err))
1279+ msg = 'full system upgrade'
1280+ LOG.debug(msg)
1281+ image.execute(['/bin/sh', '-c', cmd], description=msg)
1282
1283
1284 def run_script(args, image):
1285@@ -111,9 +149,9 @@ def run_script(args, image):
1286 image: cloud_tests.images instance to operate on
1287 return_value: None, may raise errors
1288 """
1289- # TODO: get exit status back from script and add error handling here
1290- LOG.debug('running setup image script in target image')
1291- image.run_script(args.script)
1292+ msg = 'run setup image script in target image'
1293+ LOG.debug(msg)
1294+ image.run_script(args.script, description=msg)
1295
1296
1297 def enable_ppa(args, image):
1298@@ -124,17 +162,15 @@ def enable_ppa(args, image):
1299 return_value: None, may raise errors
1300 """
1301 # ppa only supported on ubuntu (maybe debian?)
1302- if image.properties['os'] != 'ubuntu':
1303+ if image.properties['os'].lower() != 'ubuntu':
1304 raise NotImplementedError('enabling a ppa is only available on ubuntu')
1305
1306 # add ppa with add-apt-repository and update
1307 ppa = 'ppa:{}'.format(args.ppa)
1308- LOG.debug('enabling %s', ppa)
1309+ msg = 'enable ppa: "{}" in target'.format(ppa)
1310+ LOG.debug(msg)
1311 cmd = 'add-apt-repository --yes {} && apt-get update'.format(ppa)
1312- (out, err, exit) = image.execute(['/bin/sh', '-c', cmd])
1313- if exit != 0:
1314- raise OSError('enable ppa for {} failed\n\tstdout: {}\n\tstderr: {}'
1315- .format(ppa, out, err))
1316+ image.execute(['/bin/sh', '-c', cmd], description=msg)
1317
1318
1319 def enable_repo(args, image):
1320@@ -155,11 +191,9 @@ def enable_repo(args, image):
1321 raise NotImplementedError('enable repo command not configured for '
1322 'distro from family: {}'.format(os_family))
1323
1324- LOG.debug('enabling repo: "%s"', args.repo)
1325- (out, err, exit) = image.execute(['/bin/sh', '-c', cmd])
1326- if exit != 0:
1327- raise OSError('enable repo {} failed\n\tstdout: {}\n\tstderr: {}'
1328- .format(args.repo, out, err))
1329+ msg = 'enable repo: "{}" in target'.format(args.repo)
1330+ LOG.debug(msg)
1331+ image.execute(['/bin/sh', '-c', cmd], description=msg)
1332
1333
1334 def setup_image(args, image):
1335@@ -169,6 +203,11 @@ def setup_image(args, image):
1336 image: cloud_tests.image instance to operate on
1337 return_value: tuple of results and fail count
1338 """
1339+ # update the args if necessary for this image
1340+ overrides = image.setup_overrides
1341+ LOG.debug('updating args for setup with: %s', overrides)
1342+ args = util.update_args(args, overrides, preserve_old=True)
1343+
1344 # mapping of setup cmdline arg name to setup function
1345 # represented as a tuple rather than a dict or odict as lookup by name not
1346 # needed, and order is important as --script and --upgrade go at the end
1347@@ -179,17 +218,19 @@ def setup_image(args, image):
1348 ('repo', enable_repo, 'setup func for --repo, enable repo'),
1349 ('ppa', enable_ppa, 'setup func for --ppa, enable ppa'),
1350 ('script', run_script, 'setup func for --script, run script'),
1351- ('upgrade', upgrade, 'setup func for --upgrade, upgrade pkgs'),
1352+ ('upgrade', upgrade, 'setup func for --upgrade, upgrade cloud-init'),
1353+ ('upgrade-full', upgrade_full, 'setup func for --upgrade-full'),
1354 )
1355
1356 # determine which setup functions needed
1357 calls = [partial(stage.run_single, desc, partial(func, args, image))
1358 for name, func, desc in handlers if getattr(args, name, None)]
1359
1360- image_name = 'image: distro={}, release={}'.format(
1361- image.properties['os'], image.properties['release'])
1362- LOG.info('setting up %s', image_name)
1363- return stage.run_stage('set up for {}'.format(image_name), calls,
1364- continue_after_error=False)
1365+ LOG.info('setting up %s', image)
1366+ res = stage.run_stage(
1367+ 'set up for {}'.format(image), calls, continue_after_error=False)
1368+ LOG.debug('after setup complete, installed cloud-init version is: %s',
1369+ installed_version(image, 'cloud-init'))
1370+ return res
1371
1372 # vi: ts=4 expandtab
1373diff --git a/tests/cloud_tests/snapshots/base.py b/tests/cloud_tests/snapshots/base.py
1374index d715f03..e9b721e 100644
1375--- a/tests/cloud_tests/snapshots/base.py
1376+++ b/tests/cloud_tests/snapshots/base.py
1377@@ -7,10 +7,11 @@ class Snapshot(object):
1378 """
1379 platform_name = None
1380
1381- def __init__(self, properties, config):
1382+ def __init__(self, platform, properties, config):
1383 """
1384 Set up snapshot
1385 """
1386+ self.platform = platform
1387 self.properties = properties
1388 self.config = config
1389
1390diff --git a/tests/cloud_tests/snapshots/lxd.py b/tests/cloud_tests/snapshots/lxd.py
1391index eabbce3..d2a123a 100644
1392--- a/tests/cloud_tests/snapshots/lxd.py
1393+++ b/tests/cloud_tests/snapshots/lxd.py
1394@@ -9,13 +9,12 @@ class LXDSnapshot(base.Snapshot):
1395 """
1396 platform_name = "lxd"
1397
1398- def __init__(self, properties, config, platform, pylxd_frozen_instance):
1399+ def __init__(self, platform, properties, config, pylxd_frozen_instance):
1400 """
1401 Set up snapshot
1402 """
1403- self.platform = platform
1404 self.pylxd_frozen_instance = pylxd_frozen_instance
1405- super(LXDSnapshot, self).__init__(properties, config)
1406+ super(LXDSnapshot, self).__init__(platform, properties, config)
1407
1408 def launch(self, user_data, meta_data=None, block=True, start=True,
1409 use_desc=None):
1410@@ -34,10 +33,11 @@ class LXDSnapshot(base.Snapshot):
1411 if meta_data:
1412 inst_config['user.meta-data'] = meta_data
1413 instance = self.platform.launch_container(
1414- container=self.pylxd_frozen_instance.name, config=inst_config,
1415- block=block, image_desc=str(self), use_desc=use_desc)
1416+ self.properties, self.config, block=block, image_desc=str(self),
1417+ container=self.pylxd_frozen_instance.name, use_desc=use_desc,
1418+ container_config=inst_config)
1419 if start:
1420- instance.start(wait=True, wait_time=self.config.get('timeout'))
1421+ instance.start()
1422 return instance
1423
1424 def destroy(self):
1425diff --git a/tests/cloud_tests/util.py b/tests/cloud_tests/util.py
1426index 64a8667..b47862e 100644
1427--- a/tests/cloud_tests/util.py
1428+++ b/tests/cloud_tests/util.py
1429@@ -1,5 +1,6 @@
1430 # This file is part of cloud-init. See LICENSE file for license information.
1431
1432+import copy
1433 import glob
1434 import os
1435 import random
1436@@ -7,10 +8,18 @@ import string
1437 import tempfile
1438 import yaml
1439
1440-from cloudinit.distros import OSFAMILIES
1441 from cloudinit import util as c_util
1442 from tests.cloud_tests import LOG
1443
1444+OS_FAMILY_MAPPING = {
1445+ 'debian': ['debian', 'ubuntu'],
1446+ 'redhat': ['centos', 'rhel', 'fedora'],
1447+ 'gentoo': ['gentoo'],
1448+ 'freebsd': ['freebsd'],
1449+ 'suse': ['sles'],
1450+ 'arch': ['arch'],
1451+}
1452+
1453
1454 def list_test_data(data_dir):
1455 """
1456@@ -68,7 +77,7 @@ def gen_instance_name(prefix='cloud-test', image_desc=None, use_desc=None,
1457 """
1458 filter bad characters out of elem and trim to length
1459 """
1460- elem = elem[:max_len] if elem else unknown
1461+ elem = elem.lower()[:max_len] if elem else unknown
1462 return ''.join(c if c in valid else delim for c in elem)
1463
1464 return next(name for name in
1465@@ -88,7 +97,8 @@ def get_os_family(os_name):
1466 """
1467 get os family type for os_name
1468 """
1469- return next((k for k, v in OSFAMILIES.items() if os_name in v), None)
1470+ return next((k for k, v in OS_FAMILY_MAPPING.items()
1471+ if os_name.lower() in v), None)
1472
1473
1474 def current_verbosity():
1475@@ -158,6 +168,81 @@ def write_file(*args, **kwargs):
1476 """
1477 write a file using cloudinit.util.write_file
1478 """
1479- c_util.write_file(*args, **kwargs)
1480+ return c_util.write_file(*args, **kwargs)
1481+
1482+
1483+def read_conf(*args, **kwargs):
1484+ """
1485+ read configuration using cloudinit.util.read_conf
1486+ """
1487+ return c_util.read_conf(*args, **kwargs)
1488+
1489+
1490+def subp(*args, **kwargs):
1491+ """
1492+ execute a command on the system shell using cloudinit.util.subp
1493+ """
1494+ return c_util.subp(*args, **kwargs)
1495+
1496+
1497+def tmpdir(prefix='cloud_test_util_'):
1498+ return tempfile.mkdtemp(prefix=prefix)
1499+
1500+
1501+def rel_files(basedir):
1502+ """
1503+ list of files under directory by relative path, not including directories
1504+ return_value: list or relative paths
1505+ """
1506+ basedir = os.path.normpath(basedir)
1507+ return [path[len(basedir) + 1:] for path in
1508+ glob.glob(os.path.join(basedir, '**'), recursive=True)
1509+ if not os.path.isdir(path)]
1510+
1511+
1512+def flat_tar(output, basedir, owner='root', group='root'):
1513+ """
1514+ create a flat tar archive (no leading ./) from basedir
1515+ output: output tar file to write
1516+ basedir: base directory for archive
1517+ owner: owner of archive files
1518+ group: group archive files belong to
1519+ return_value: none
1520+ """
1521+ c_util.subp(['tar', 'cf', output, '--owner', owner, '--group', group,
1522+ '-C', basedir] + rel_files(basedir), capture=True)
1523+
1524+
1525+def update_args(args, updates, preserve_old=True):
1526+ """
1527+ update cmdline arguments from a dictionary
1528+ args: cmdline arguments
1529+ updates: dictionary of {arg_name: new_value} mappings
1530+ preserve_old: if true, create a deep copy of args before updating
1531+ return_value: updated cmdline arguments, as new object if preserve_old=True
1532+ """
1533+ args = copy.deepcopy(args) if preserve_old else args
1534+ if updates:
1535+ vars(args).update(updates)
1536+ return args
1537+
1538+
1539+class InTargetExecuteError(c_util.ProcessExecutionError):
1540+ """
1541+ Error type for in target commands that fail
1542+ """
1543+ default_desc = 'Unexpected error while running command in target instance'
1544+
1545+ def __init__(self, stdout, stderr, exit_code, cmd, instance,
1546+ description=None):
1547+ """
1548+ init error and parent error class
1549+ """
1550+ if isinstance(cmd, (tuple, list)):
1551+ cmd = ' '.join(cmd)
1552+ super(InTargetExecuteError, self).__init__(
1553+ stdout=stdout, stderr=stderr, exit_code=exit_code, cmd=cmd,
1554+ reason="Instance: {}".format(instance),
1555+ description=description if description else self.default_desc)
1556
1557 # vi: ts=4 expandtab
1558diff --git a/tox.ini b/tox.ini
1559index f016f20..d8e99e0 100644
1560--- a/tox.ini
1561+++ b/tox.ini
1562@@ -93,4 +93,4 @@ basepython = python3
1563 commands = {envpython} -m tests.cloud_tests {posargs}
1564 passenv = HOME
1565 deps =
1566- pylxd==2.1.3
1567+ pylxd==2.2.3

Subscribers

People subscribed via source and target branches