Merge ~smoser/cloud-init:cleanup/cii-cleanup into cloud-init:master

Proposed by Scott Moser on 2017-10-31
Status: Merged
Approved by: Scott Moser on 2017-11-06
Approved revision: 7897d64b94fd3aa6d119d533f808f2f11d6ecd6a
Merged at revision: 8622491c29f30862a1a1d7ad2cba023981acc8ce
Proposed branch: ~smoser/cloud-init:cleanup/cii-cleanup
Merge into: cloud-init:master
Diff against target: 1197 lines (+348/-333)
25 files modified
tests/cloud_tests/collect.py (+16/-2)
tests/cloud_tests/images/base.py (+3/-16)
tests/cloud_tests/images/lxd.py (+18/-14)
tests/cloud_tests/images/nocloudkvm.py (+18/-24)
tests/cloud_tests/instances/base.py (+4/-77)
tests/cloud_tests/instances/lxd.py (+56/-48)
tests/cloud_tests/instances/nocloudkvm.py (+52/-91)
tests/cloud_tests/testcases/examples/run_commands.yaml (+2/-2)
tests/cloud_tests/testcases/modules/keys_to_console.py (+4/-4)
tests/cloud_tests/testcases/modules/runcmd.yaml (+2/-2)
tests/cloud_tests/testcases/modules/set_hostname.py (+3/-1)
tests/cloud_tests/testcases/modules/set_hostname.yaml (+2/-1)
tests/cloud_tests/testcases/modules/set_hostname_fqdn.py (+7/-3)
tests/cloud_tests/testcases/modules/set_hostname_fqdn.yaml (+2/-2)
tests/cloud_tests/testcases/modules/set_password_expire.py (+1/-1)
tests/cloud_tests/testcases/modules/set_password_expire.yaml (+2/-0)
tests/cloud_tests/testcases/modules/set_password_list.yaml (+1/-0)
tests/cloud_tests/testcases/modules/set_password_list_string.yaml (+1/-0)
tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.py (+0/-8)
tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.yaml (+0/-1)
tests/cloud_tests/testcases/modules/ssh_keys_generate.py (+0/-5)
tests/cloud_tests/testcases/modules/ssh_keys_generate.yaml (+0/-6)
tests/cloud_tests/testcases/modules/ssh_keys_provided.py (+0/-11)
tests/cloud_tests/testcases/modules/ssh_keys_provided.yaml (+0/-6)
tests/cloud_tests/util.py (+154/-8)
Reviewer Review Type Date Requested Status
Joshua Powers (community) Approve on 2017-11-06
Chad Smith 2017-10-31 Approve on 2017-11-06
Server Team CI bot continuous-integration Needs Fixing on 2017-11-06
Review via email: mp+333059@code.launchpad.net

Commit Message

tests: integration test cleanup and full pass of nocloud-kvm.

Integration test harness changes:
 * Enable collection of console log in nocloud-kvm and lxd.
 * Collect the console log to results for all test runs.
 * change 'tmpfile' to pick name locally instead of using 'mktemp'.
 * drop the 'instance' attribute from nocloud-kvm Image and
   demote LXDImage.instance to a private attribute.
   This is because Images do not actually have instances.
   (LXDImage internally uses a booted system to modify the image).
 * Add 'TargetBase' as a superclass of Image and Instance providing
   implementations of execute, read_data, write_data, pull_file,
   and push_file. These all depend on an implementation of _execute.
 * Improve '_execute' implementations to support accepting stdin.
 * execute supports 'rcs=False' meaning 'do not raise exception'.
 * Drop support for pylxd < 2.2. older versions cannot determine
   exit code of 'execute', which makes them unusable.
 * make NoCloudKVMInstance._execute run as root via sudo. This required
   some changes so that 'hostname' could be reverse-looked up in order
   to avoid sudo taking a long time (~20 seconds).

Test changes here:
 * do not use /tmp, but rather /var/tmp (LP: #1707222)
 * make keys_to_console assertions more strict.
 * change user test cases to always add default (ubuntu) user
   so that nocloud-kvm's execute which operates over ssh can work.

Description of the Change

Lots of things here.

following onto Josh's branch from
 https://code.launchpad.net/~powersj/cloud-init/+git/cloud-init/+merge/331736

read commit message for ididivudal things until i write a better commit message.

To post a comment you must log in.

PASSED: Continuous integration, rev:e8f618c9c72e723e66f63eaaafafb9d392b15593
https://jenkins.ubuntu.com/server/job/cloud-init-ci/456/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/456/rebuild

review: Approve (continuous-integration)

PASSED: Continuous integration, rev:4ce1002cb5956e821e27087008221b5ffef0af45
https://jenkins.ubuntu.com/server/job/cloud-init-ci/457/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/457/rebuild

review: Approve (continuous-integration)

PASSED: Continuous integration, rev:773013c4f058f1f3daec544ff1211eee52933988
https://jenkins.ubuntu.com/server/job/cloud-init-ci/458/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/458/rebuild

review: Approve (continuous-integration)

PASSED: Continuous integration, rev:3f395a74479c236d50668761f7ff4cf3d8a949d6
https://jenkins.ubuntu.com/server/job/cloud-init-ci/459/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/459/rebuild

review: Approve (continuous-integration)
Ryan Harper (raharper) wrote :

I'm not sure I understand the dropping of instance property from the NoCloud KVM class vs. the LXD one.

Scott Moser (smoser) wrote :

> I'm not sure I understand the dropping of instance property from the NoCloud
> KVM class vs. the LXD one.

An Image does not have an instance associated with it.

An 'Image' is something you can manipulate the contents of (execute,
write_file, read_file) but is not booted. Imagine it being a .img file
that you can mount-image-callback on (as it is in nocloud-kvm).

Currently the lxd Image is something that has to be booted in order to
provide those operations. It uses an actual lxd.Instance to do that.
There is nothing specifically wrong about this, and may well be how
we have to do things on some platforms.

My goal on lxd though would be to replace that with 'lxd-pstart' (or
lxd-pstart like function). That way there is no 'cleaning' required
as there are no side-effects of the original boot.

I believe that its name and "important look" made Josh believe he needed
to implement .instance in nocloudkvm's Image. So, I demoted the .instance
to be private.

8973cb2... by Scott Moser on 2017-11-01

adjust write_data and read_data docstrings.

989b215... by Scott Moser on 2017-11-01

nocloudkvm.Instance: use parent class's write_data drop un-used sftp

write_data was still using 'sftp', which would write the data as
the user we connected to. Thats wrong, in that it is supposed
to write as root.

At some point that would likely be converted to have 'mode' and 'owner'
arguments.

sftp is no longer used, so drop it.

Ryan Harper (raharper) wrote :

On Wed, Nov 1, 2017 at 1:45 PM, Scott Moser <email address hidden>
wrote:

> > I'm not sure I understand the dropping of instance property from the
> NoCloud
> > KVM class vs. the LXD one.
>
> An Image does not have an instance associated with it.
>
> An 'Image' is something you can manipulate the contents of (execute,
> write_file, read_file) but is not booted. Imagine it being a .img file
> that you can mount-image-callback on (as it is in nocloud-kvm).
>
> Currently the lxd Image is something that has to be booted in order to
> provide those operations. It uses an actual lxd.Instance to do that.
> There is nothing specifically wrong about this, and may well be how
> we have to do things on some platforms.
>

OK, IIUC, since NoCloud uses mount-image-callback, we can modify
the image without the need for an "instance" of it (aka, booted).

> --
> https://code.launchpad.net/~smoser/cloud-init/+git/cloud-
> init/+merge/333059
> Your team cloud-init commiters is requested to review the proposed merge
> of ~smoser/cloud-init:cleanup/cii-cleanup into cloud-init:master.
>
> _______________________________________________
> Mailing list: https://launchpad.net/~cloud-init-dev
> Post to : <email address hidden>
> Unsubscribe : https://launchpad.net/~cloud-init-dev
> More help : https://help.launchpad.net/ListHelp
>

Scott Moser (smoser) :
Ryan Harper (raharper) wrote :

One more round on the @property structure, see inline.

PASSED: Continuous integration, rev:989b2154a380420b73ed5f098705647882b81735
https://jenkins.ubuntu.com/server/job/cloud-init-ci/460/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/460/rebuild

review: Approve (continuous-integration)
b6409c0... by Scott Moser on 2017-11-01

more info in docstring

f385dc4... by Scott Moser on 2017-11-01

mount-image-callback: do not decode data

FAILED: Continuous integration, rev:b4f6b54f3c3e87253cec1d4aef895c613ebed92a
https://jenkins.ubuntu.com/server/job/cloud-init-ci/461/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/461/rebuild

review: Needs Fixing (continuous-integration)
dd3b6c0... by Scott Moser on 2017-11-02

rework of execute and _execute.

Also here:
 * make run_script only use a single execute
 * fix (i think) the paramiko client to correctly send and wait
   on output.

FAILED: Continuous integration, rev:ac32c998307716217d309da66317055b817daad6
https://jenkins.ubuntu.com/server/job/cloud-init-ci/462/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    FAILED: Ubuntu LTS: Integration

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/462/rebuild

review: Needs Fixing (continuous-integration)

FAILED: Continuous integration, rev:d4b5f240c6cf48fcb34c7671e54b903d0dfe760d
https://jenkins.ubuntu.com/server/job/cloud-init-ci/463/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    FAILED: Ubuntu LTS: Integration

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/463/rebuild

review: Needs Fixing (continuous-integration)

FAILED: Continuous integration, rev:5f6aa3bc02f9b5da251c3df2b0c6b6b3d50c903d
https://jenkins.ubuntu.com/server/job/cloud-init-ci/464/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    FAILED: Ubuntu LTS: Integration

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/464/rebuild

review: Needs Fixing (continuous-integration)
3de1be3... by Scott Moser on 2017-11-02

_execute doesnt need string support anymore

dbb2a8e... by Scott Moser on 2017-11-02

use rcs=False

9b4fbdf... by Scott Moser on 2017-11-02

and no more convert to bytes in ssh

PASSED: Continuous integration, rev:3de1be31b1be84b2418bbc7ea17a805837aa1149
https://jenkins.ubuntu.com/server/job/cloud-init-ci/465/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/465/rebuild

review: Approve (continuous-integration)

FAILED: Continuous integration, rev:7d1c5e8092ecc32521ab560b29df4c23338158dd
https://jenkins.ubuntu.com/server/job/cloud-init-ci/466/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/466/rebuild

review: Needs Fixing (continuous-integration)

FAILED: Continuous integration, rev:9b4fbdf073c38f5284efa05373a616fd25a0fb2b
https://jenkins.ubuntu.com/server/job/cloud-init-ci/467/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    FAILED: Ubuntu LTS: Integration

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/467/rebuild

review: Needs Fixing (continuous-integration)

PASSED: Continuous integration, rev:9b4fbdf073c38f5284efa05373a616fd25a0fb2b
https://jenkins.ubuntu.com/server/job/cloud-init-ci/468/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/468/rebuild

review: Approve (continuous-integration)
5d38855... by Scott Moser on 2017-11-02

minor fix for readability

PASSED: Continuous integration, rev:5d38855c6254d82df54ce31eea8863798b892c6c
https://jenkins.ubuntu.com/server/job/cloud-init-ci/469/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/469/rebuild

review: Approve (continuous-integration)
Joshua Powers (powersj) wrote :

I cannot get the nocloud-kvm datasource to even run with the following commit:

$ git rev-parse HEAD
5d38855c6254d82df54ce31eea8863798b892c6c

One comment below about a debug message and should the commit message more verbosely call out the new TargetBased class?

* PASS - lxd run xenial
* PASS - lxd tree_run xenial
* FAIL - nocloud-kvm run xenial
* FAIL - nocloud-kvm run artful
* FAIL - nocloud-kvm tree_run artful

nocloud-kvm failure:

2017-11-03 18:14:58,081 - tests.cloud_tests - ERROR - stage part: script file_gzip encountered error: [Errno None] Unable to connect to port 47557 on 127.0.0.1
2017-11-03 18:14:58,081 - tests.cloud_tests - ERROR - traceback:
  File "/tmp/cloud-init/tests/cloud_tests/stage.py", line 47, in run_single
    call()
  File "/tmp/cloud-init/tests/cloud_tests/collect.py", line 26, in collect_script
    description='collect: {}'.format(script_name))
  File "/tmp/cloud-init/tests/cloud_tests/util.py", line 422, in run_script
    stdin=script, description=description, rcs=rcs)
  File "/tmp/cloud-init/tests/cloud_tests/util.py", line 327, in execute
    out, err, rc = self._execute(command=command, stdin=stdin, env=env)
  File "/tmp/cloud-init/tests/cloud_tests/instances/nocloudkvm.py", line 62, in _execute
    return self.ssh(['sudo'] + env_args + list(command), stdin=stdin)
  File "/tmp/cloud-init/tests/cloud_tests/instances/nocloudkvm.py", line 86, in ssh
    client = self._ssh_connect()
  File "/tmp/cloud-init/tests/cloud_tests/instances/nocloudkvm.py", line 115, in _ssh_connect
    banner_timeout=banner_timeout)
  File "/usr/lib/python3/dist-packages/paramiko/client.py", line 324, in connect
    raise NoValidConnectionsError(errors)

review: Needs Fixing
Scott Moser (smoser) wrote :

Can you paste a full log of what you did ?
This works fine for me on diglett. here is what I did:

$ tox-venv citest python3 -m tests.cloud_tests run --platform=nocloud-kvm --os-name=xenial --preserve-data --dat
a-dir=results.nocloud-kvm.d --verbose --test=tests/cloud_tests/testcases/modules/final_message.py

http://paste.ubuntu.com/25881300/

ec53bbb... by Scott Moser on 2017-11-03

LXDImage: do not [attempt] start instance everytime property referenced

397ae68... by Scott Moser on 2017-11-03

only log debug of command show command if no description

PASSED: Continuous integration, rev:397ae68232a5ef8a00c83f5ad8172047fef6a065
https://jenkins.ubuntu.com/server/job/cloud-init-ci/470/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/470/rebuild

review: Approve (continuous-integration)
Chad Smith (chad.smith) wrote :

Per the issue JoshPowers brought up on Friday, sudo times out after 20 seconds on the kvm instances due to the inability to resolve the 'ubuntu' hostname. That 20 second timeout affects every single instance.execute which runs 'sudo'.

I think smoser & rharper brought up the possible solution to
   1. reuse ssh connection
   2. Possibly add ubuntu to /etc/hosts so we don't hit DNS resolution errors for nocloud.

Here is a patch which takes smoser's ssh connection caching/reuse + my changes to update etc_hosts on nocloudkvm images
http://pastebin.ubuntu.com/25881811/

Chad Smith (chad.smith) wrote :

After applying the above patch I see all collect artifact calls returning quickly instead of being affected by the sudo timeout

http://pastebin.ubuntu.com/25884331/

Chad Smith (chad.smith) wrote :

..... haven't run through the full test suite though. it seems some tests are still hitting a timeout at 20 seconds on each collect artifact call. not sure if that's because we've reset the hostname in those tests :/

15549a3... by Scott Moser on 2017-11-06

namespace sudo issues by moving hostnames to i9n.cloud-init.io domain

This just makes each of the hostnames we use under the i9.cloud-init.io
domain. cloud-init.io can be updated to provide these dns records
(or wildcard dns record). At this point the easiest fix is to add
the following lines to your /etc/hosts (line break is just to keep
it less than 80 chars, you could put all hosts on the same long line).

127.0.1.126 cloudinit1.i9n.cloud-init.io cloudinit2.i9n.cloud-init.io
127.0.1.126 ubuntu.i9n.cloud-init.io

7897d64... by Scott Moser on 2017-11-06

re-use ssh connection.

Rather than creating a new ssh connection for each sub-command, re-use
the connection and create a new channel.

In my very un-scientific test with a sample set of 1, and entries in
hosts' /etc/hosts, i found:
 before: 6 commands took 1.277
 after : 6 commands took 0.469

d85fabb... by Scott Moser on 2017-11-06

Do not require modification of /etc/hosts: use actually populated subdomain

I've populated i9n.brickies.net with records for:
   ubuntu, cloudinit1, cloudinit2

Then, the change here was just:
sed s,i9n.cloud-init.io,i9n.brickies.net, -i \
   $(grep -rl --include="*.yaml" --include="*.py" i9n.cloud-init.io tests/)

The result is slower than host having entries in /etc/hosts, which
does make sense (.469 -> .557 for the 6 executes in the test i ran).
But it means the user doesnt have to modify anything in /etc/hosts.

Chad Smith (chad.smith) wrote :

Let's make that a proper cut-n-paste
http://pastebin.ubuntu.com/25884331/

FAILED: Continuous integration, rev:2c6ac709bf42fe7255bea46a10a9960029322ff4
https://jenkins.ubuntu.com/server/job/cloud-init-ci/471/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/471/rebuild

review: Needs Fixing (continuous-integration)

PASSED: Continuous integration, rev:b5aea7d2986d003dc82fde92351e7382703681d0
https://jenkins.ubuntu.com/server/job/cloud-init-ci/472/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/472/rebuild

review: Approve (continuous-integration)

FAILED: Continuous integration, rev:33d646d4a95b2b00395a4cc83be8f58c73133ef3
https://jenkins.ubuntu.com/server/job/cloud-init-ci/473/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    FAILED: Ubuntu LTS: Integration

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/473/rebuild

review: Needs Fixing (continuous-integration)
Scott Moser (smoser) wrote :

Josh,
Could you re-review?

I think I addressed your issues, making execute show the description.

FAILED: Continuous integration, rev:7897d64b94fd3aa6d119d533f808f2f11d6ecd6a
https://jenkins.ubuntu.com/server/job/cloud-init-ci/474/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    FAILED: Ubuntu LTS: Integration

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/474/rebuild

review: Needs Fixing (continuous-integration)
Chad Smith (chad.smith) wrote :

Performance is all good on my side now that your brickeies A-records are registered. I no longer see 20 second timeouts between collect calls. Minor nits inline. Let's land this and iterate on ec2.

review: Approve
Chad Smith (chad.smith) wrote :

... once ci passes and torkoal is fixed.

Joshua Powers (powersj) wrote :

You already pulled this, but it looks good

review: Approve
Scott Moser (smoser) wrote :

Bah. I saw your approved and didn't read your comments. Sorry for setting presumptuous.

On November 6, 2017 4:42:33 PM EST, Chad Smith <email address hidden> wrote:
>... once ci passes and torkoal is fixed.
>--
>https://code.launchpad.net/~smoser/cloud-init/+git/cloud-init/+merge/333059
>You are the owner of ~smoser/cloud-init:cleanup/cii-cleanup.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/tests/cloud_tests/collect.py b/tests/cloud_tests/collect.py
2index 4a2422e..71ee764 100644
3--- a/tests/cloud_tests/collect.py
4+++ b/tests/cloud_tests/collect.py
5@@ -22,11 +22,21 @@ def collect_script(instance, base_dir, script, script_name):
6 """
7 LOG.debug('running collect script: %s', script_name)
8 (out, err, exit) = instance.run_script(
9- script, rcs=range(0, 256),
10+ script.encode(), rcs=False,
11 description='collect: {}'.format(script_name))
12 c_util.write_file(os.path.join(base_dir, script_name), out)
13
14
15+def collect_console(instance, base_dir):
16+ LOG.debug('getting console log')
17+ try:
18+ data = instance.console_log()
19+ except NotImplementedError as e:
20+ data = 'Not Implemented: %s' % e
21+ with open(os.path.join(base_dir, 'console.log'), "wb") as fp:
22+ fp.write(data)
23+
24+
25 def collect_test_data(args, snapshot, os_name, test_name):
26 """Collect data for test case.
27
28@@ -79,8 +89,12 @@ def collect_test_data(args, snapshot, os_name, test_name):
29 test_output_dir, script, script_name))
30 for script_name, script in test_scripts.items()]
31
32+ console_log = partial(
33+ run_single, 'collect console',
34+ partial(collect_console, instance, test_output_dir))
35+
36 res = run_stage('collect for test: {}'.format(test_name),
37- [start_call] + collect_calls)
38+ [start_call] + collect_calls + [console_log])
39
40 return res
41
42diff --git a/tests/cloud_tests/images/base.py b/tests/cloud_tests/images/base.py
43index 0a1e056..d503108 100644
44--- a/tests/cloud_tests/images/base.py
45+++ b/tests/cloud_tests/images/base.py
46@@ -2,8 +2,10 @@
47
48 """Base class for images."""
49
50+from ..util import TargetBase
51
52-class Image(object):
53+
54+class Image(TargetBase):
55 """Base class for images."""
56
57 platform_name = None
58@@ -43,21 +45,6 @@ class Image(object):
59 # NOTE: more sophisticated options may be requied at some point
60 return self.config.get('setup_overrides', {})
61
62- def execute(self, *args, **kwargs):
63- """Execute command in image, modifying image."""
64- raise NotImplementedError
65-
66- def push_file(self, local_path, remote_path):
67- """Copy file at 'local_path' to instance at 'remote_path'."""
68- raise NotImplementedError
69-
70- def run_script(self, *args, **kwargs):
71- """Run script in image, modifying image.
72-
73- @return_value: script output
74- """
75- raise NotImplementedError
76-
77 def snapshot(self):
78 """Create snapshot of image, block until done."""
79 raise NotImplementedError
80diff --git a/tests/cloud_tests/images/lxd.py b/tests/cloud_tests/images/lxd.py
81index fd4e93c..5caeba4 100644
82--- a/tests/cloud_tests/images/lxd.py
83+++ b/tests/cloud_tests/images/lxd.py
84@@ -24,7 +24,7 @@ class LXDImage(base.Image):
85 @param config: image configuration
86 """
87 self.modified = False
88- self._instance = None
89+ self._img_instance = None
90 self._pylxd_image = None
91 self.pylxd_image = pylxd_image
92 super(LXDImage, self).__init__(platform, config)
93@@ -38,9 +38,9 @@ class LXDImage(base.Image):
94
95 @pylxd_image.setter
96 def pylxd_image(self, pylxd_image):
97- if self._instance:
98+ if self._img_instance:
99 self._instance.destroy()
100- self._instance = None
101+ self._img_instance = None
102 if (self._pylxd_image and
103 (self._pylxd_image is not pylxd_image) and
104 (not self.config.get('cache_base_image') or self.modified)):
105@@ -49,15 +49,19 @@ class LXDImage(base.Image):
106 self._pylxd_image = pylxd_image
107
108 @property
109- def instance(self):
110- """Property function."""
111- if not self._instance:
112- self._instance = self.platform.launch_container(
113+ def _instance(self):
114+ """Internal use only, returns a instance
115+
116+ This starts an lxc instance from the image, so it is "dirty".
117+ Better would be some way to modify this "at rest".
118+ lxc-pstart would be an option."""
119+ if not self._img_instance:
120+ self._img_instance = self.platform.launch_container(
121 self.properties, self.config, self.features,
122 use_desc='image-modification', image_desc=str(self),
123 image=self.pylxd_image.fingerprint)
124- self._instance.start()
125- return self._instance
126+ self._img_instance.start()
127+ return self._img_instance
128
129 @property
130 def properties(self):
131@@ -144,20 +148,20 @@ class LXDImage(base.Image):
132 shutil.rmtree(export_dir)
133 shutil.rmtree(extract_dir)
134
135- def execute(self, *args, **kwargs):
136+ def _execute(self, *args, **kwargs):
137 """Execute command in image, modifying image."""
138- return self.instance.execute(*args, **kwargs)
139+ return self._instance._execute(*args, **kwargs)
140
141 def push_file(self, local_path, remote_path):
142 """Copy file at 'local_path' to instance at 'remote_path'."""
143- return self.instance.push_file(local_path, remote_path)
144+ return self._instance.push_file(local_path, remote_path)
145
146 def run_script(self, *args, **kwargs):
147 """Run script in image, modifying image.
148
149 @return_value: script output
150 """
151- return self.instance.run_script(*args, **kwargs)
152+ return self._instance.run_script(*args, **kwargs)
153
154 def snapshot(self):
155 """Create snapshot of image, block until done."""
156@@ -169,7 +173,7 @@ class LXDImage(base.Image):
157 # clone current instance
158 instance = self.platform.launch_container(
159 self.properties, self.config, self.features,
160- container=self.instance.name, image_desc=str(self),
161+ container=self._instance.name, image_desc=str(self),
162 use_desc='snapshot', container_config=conf)
163 # wait for cloud-init before boot_clean_script is run to ensure
164 # /var/lib/cloud is removed cleanly
165diff --git a/tests/cloud_tests/images/nocloudkvm.py b/tests/cloud_tests/images/nocloudkvm.py
166index a7af0e5..1e7962c 100644
167--- a/tests/cloud_tests/images/nocloudkvm.py
168+++ b/tests/cloud_tests/images/nocloudkvm.py
169@@ -2,6 +2,8 @@
170
171 """NoCloud KVM Image Base Class."""
172
173+from cloudinit import util as c_util
174+
175 from tests.cloud_tests.images import base
176 from tests.cloud_tests.snapshots import nocloudkvm as nocloud_kvm_snapshot
177
178@@ -19,24 +21,11 @@ class NoCloudKVMImage(base.Image):
179 @param img_path: path to the image
180 """
181 self.modified = False
182- self._instance = None
183 self._img_path = img_path
184
185 super(NoCloudKVMImage, self).__init__(platform, config)
186
187 @property
188- def instance(self):
189- """Returns an instance of an image."""
190- if not self._instance:
191- if not self._img_path:
192- raise RuntimeError()
193-
194- self._instance = self.platform.create_image(
195- self.properties, self.config, self.features, self._img_path,
196- image_desc=str(self), use_desc='image-modification')
197- return self._instance
198-
199- @property
200 def properties(self):
201 """Dictionary containing: 'arch', 'os', 'version', 'release'."""
202 return {
203@@ -46,20 +35,26 @@ class NoCloudKVMImage(base.Image):
204 'version': self.config['version'],
205 }
206
207- def execute(self, *args, **kwargs):
208+ def _execute(self, command, stdin=None, env=None):
209 """Execute command in image, modifying image."""
210- return self.instance.execute(*args, **kwargs)
211+ return self.mount_image_callback(command, stdin=stdin, env=env)
212
213- def push_file(self, local_path, remote_path):
214- """Copy file at 'local_path' to instance at 'remote_path'."""
215- return self.instance.push_file(local_path, remote_path)
216+ def mount_image_callback(self, command, stdin=None, env=None):
217+ """Run mount-image-callback."""
218
219- def run_script(self, *args, **kwargs):
220- """Run script in image, modifying image.
221+ env_args = []
222+ if env:
223+ env_args = ['env'] + ["%s=%s" for k, v in env.items()]
224
225- @return_value: script output
226- """
227- return self.instance.run_script(*args, **kwargs)
228+ mic_chroot = ['sudo', 'mount-image-callback', '--system-mounts',
229+ '--system-resolvconf', self._img_path,
230+ '--', 'chroot', '_MOUNTPOINT_']
231+ try:
232+ out, err = c_util.subp(mic_chroot + env_args + list(command),
233+ data=stdin, decode=False)
234+ return (out, err, 0)
235+ except c_util.ProcessExecutionError as e:
236+ return (e.stdout, e.stderr, e.exit_code)
237
238 def snapshot(self):
239 """Create snapshot of image, block until done."""
240@@ -82,7 +77,6 @@ class NoCloudKVMImage(base.Image):
241 framework decide whether to keep or destroy everything.
242 """
243 self._img_path = None
244- self._instance.destroy()
245 super(NoCloudKVMImage, self).destroy()
246
247 # vi: ts=4 expandtab
248diff --git a/tests/cloud_tests/instances/base.py b/tests/cloud_tests/instances/base.py
249index 9bdda60..8c59d62 100644
250--- a/tests/cloud_tests/instances/base.py
251+++ b/tests/cloud_tests/instances/base.py
252@@ -2,8 +2,10 @@
253
254 """Base instance."""
255
256+from ..util import TargetBase
257
258-class Instance(object):
259+
260+class Instance(TargetBase):
261 """Base instance object."""
262
263 platform_name = None
264@@ -22,82 +24,7 @@ class Instance(object):
265 self.properties = properties
266 self.config = config
267 self.features = features
268-
269- def execute(self, command, stdout=None, stderr=None, env=None,
270- rcs=None, description=None):
271- """Execute command in instance, recording output, error and exit code.
272-
273- Assumes functional networking and execution as root with the
274- target filesystem being available at /.
275-
276- @param command: the command to execute as root inside the image
277- if command is a string, then it will be executed as:
278- ['sh', '-c', command]
279- @param stdout, stderr: file handles to write output and error to
280- @param env: environment variables
281- @param rcs: allowed return codes from command
282- @param description: purpose of command
283- @return_value: tuple containing stdout data, stderr data, exit code
284- """
285- raise NotImplementedError
286-
287- def read_data(self, remote_path, decode=False):
288- """Read data from instance filesystem.
289-
290- @param remote_path: path in instance
291- @param decode: return as string
292- @return_value: data as str or bytes
293- """
294- raise NotImplementedError
295-
296- def write_data(self, remote_path, data):
297- """Write data to instance filesystem.
298-
299- @param remote_path: path in instance
300- @param data: data to write, either str or bytes
301- """
302- raise NotImplementedError
303-
304- def pull_file(self, remote_path, local_path):
305- """Copy file at 'remote_path', from instance to 'local_path'.
306-
307- @param remote_path: path on remote instance
308- @param local_path: path on local instance
309- """
310- with open(local_path, 'wb') as fp:
311- fp.write(self.read_data(remote_path))
312-
313- def push_file(self, local_path, remote_path):
314- """Copy file at 'local_path' to instance at 'remote_path'.
315-
316- @param local_path: path on local instance
317- @param remote_path: path on remote instance
318- """
319- with open(local_path, 'rb') as fp:
320- self.write_data(remote_path, fp.read())
321-
322- def run_script(self, script, rcs=None, description=None):
323- """Run script in target and return stdout.
324-
325- @param script: script contents
326- @param rcs: allowed return codes from script
327- @param description: purpose of script
328- @return_value: stdout from script
329- """
330- script_path = self.tmpfile()
331- try:
332- self.write_data(script_path, script)
333- return self.execute(
334- ['/bin/bash', script_path], rcs=rcs, description=description)
335- finally:
336- self.execute(['rm', '-f', script_path], rcs=rcs)
337-
338- def tmpfile(self):
339- """Get a tmp file in the target.
340-
341- @return_value: path to new file in target
342- """
343- return self.execute(['mktemp'])[0].strip()
344+ self._tmp_count = 0
345
346 def console_log(self):
347 """Instance console.
348diff --git a/tests/cloud_tests/instances/lxd.py b/tests/cloud_tests/instances/lxd.py
349index a43918c..3b035d8 100644
350--- a/tests/cloud_tests/instances/lxd.py
351+++ b/tests/cloud_tests/instances/lxd.py
352@@ -2,8 +2,11 @@
353
354 """Base LXD instance."""
355
356-from tests.cloud_tests.instances import base
357-from tests.cloud_tests import util
358+from . import base
359+
360+import os
361+import shutil
362+from tempfile import mkdtemp
363
364
365 class LXDInstance(base.Instance):
366@@ -24,6 +27,8 @@ class LXDInstance(base.Instance):
367 self._pylxd_container = pylxd_container
368 super(LXDInstance, self).__init__(
369 platform, name, properties, config, features)
370+ self.tmpd = mkdtemp(prefix="%s-%s" % (type(self).__name__, name))
371+ self._setup_console_log()
372
373 @property
374 def pylxd_container(self):
375@@ -31,74 +36,69 @@ class LXDInstance(base.Instance):
376 self._pylxd_container.sync()
377 return self._pylxd_container
378
379- def execute(self, command, stdout=None, stderr=None, env=None,
380- rcs=None, description=None):
381- """Execute command in instance, recording output, error and exit code.
382-
383- Assumes functional networking and execution as root with the
384- target filesystem being available at /.
385-
386- @param command: the command to execute as root inside the image
387- if command is a string, then it will be executed as:
388- ['sh', '-c', command]
389- @param stdout: file handler to write output
390- @param stderr: file handler to write error
391- @param env: environment variables
392- @param rcs: allowed return codes from command
393- @param description: purpose of command
394- @return_value: tuple containing stdout data, stderr data, exit code
395- """
396+ def _setup_console_log(self):
397+ logf = os.path.join(self.tmpd, "console.log")
398+
399+ # doing this ensures we can read it. Otherwise it ends up root:root.
400+ with open(logf, "w") as fp:
401+ fp.write("# %s\n" % self.name)
402+
403+ cfg = "lxc.console.logfile=%s" % logf
404+ orig = self._pylxd_container.config.get('raw.lxc', "")
405+ if orig:
406+ orig += "\n"
407+ self._pylxd_container.config['raw.lxc'] = orig + cfg
408+ self._pylxd_container.save()
409+ self._console_log_file = logf
410+
411+ def _execute(self, command, stdin=None, env=None):
412 if env is None:
413 env = {}
414
415- if isinstance(command, str):
416- command = ['sh', '-c', command]
417+ if stdin is not None:
418+ # pylxd does not support input to execute.
419+ # https://github.com/lxc/pylxd/issues/244
420+ #
421+ # The solution here is write a tmp file in the container
422+ # and then execute a shell that sets it standard in to
423+ # be from that file, removes it, and calls the comand.
424+ tmpf = self.tmpfile()
425+ self.write_data(tmpf, stdin)
426+ ncmd = 'exec <"{tmpf}"; rm -f "{tmpf}"; exec "$@"'
427+ command = (['sh', '-c', ncmd.format(tmpf=tmpf), 'stdinhack'] +
428+ list(command))
429
430 # ensure instance is running and execute the command
431 self.start()
432+ # execute returns a ContainerExecuteResult, named tuple
433+ # (exit_code, stdout, stderr)
434 res = self.pylxd_container.execute(command, environment=env)
435
436 # get out, exit and err from pylxd return
437- if hasattr(res, 'exit_code'):
438- # pylxd 2.2 returns ContainerExecuteResult, named tuple of
439- # (exit_code, out, err)
440- (exit, out, err) = res
441- else:
442+ if not hasattr(res, 'exit_code'):
443 # pylxd 2.1.3 and earlier only return out and err, no exit
444- # LOG.warning('using pylxd version < 2.2')
445- (out, err) = res
446- exit = 0
447-
448- # write data to file descriptors if needed
449- if stdout:
450- stdout.write(out)
451- if stderr:
452- stderr.write(err)
453-
454- # if the command exited with a code not allowed in rcs, then fail
455- if exit not in (rcs if rcs else (0,)):
456- error_desc = ('Failed command to: {}'.format(description)
457- if description else None)
458- raise util.InTargetExecuteError(
459- out, err, exit, command, self.name, error_desc)
460+ raise RuntimeError(
461+ "No 'exit_code' in pylxd.container.execute return.\n"
462+ "pylxd > 2.2 is required.")
463
464- return (out, err, exit)
465+ return res.stdout, res.stderr, res.exit_code
466
467 def read_data(self, remote_path, decode=False):
468 """Read data from instance filesystem.
469
470 @param remote_path: path in instance
471- @param decode: return as string
472- @return_value: data as str or bytes
473+ @param decode: decode data before returning.
474+ @return_value: content of remote_path as bytes if 'decode' is False,
475+ and as string if 'decode' is True.
476 """
477 data = self.pylxd_container.files.get(remote_path)
478- return data.decode() if decode and isinstance(data, bytes) else data
479+ return data.decode() if decode else data
480
481 def write_data(self, remote_path, data):
482 """Write data to instance filesystem.
483
484 @param remote_path: path in instance
485- @param data: data to write, either str or bytes
486+ @param data: data to write in bytes
487 """
488 self.pylxd_container.files.put(remote_path, data)
489
490@@ -107,7 +107,14 @@ class LXDInstance(base.Instance):
491
492 @return_value: bytes of this instance’s console
493 """
494- raise NotImplementedError
495+ if not os.path.exists(self._console_log_file):
496+ raise NotImplementedError(
497+ "Console log '%s' does not exist. If this is a remote "
498+ "lxc, then this is really NotImplementedError. If it is "
499+ "A local lxc, then this is a RuntimeError."
500+ "https://github.com/lxc/lxd/issues/1129")
501+ with open(self._console_log_file, "rb") as fp:
502+ return fp.read()
503
504 def reboot(self, wait=True):
505 """Reboot instance."""
506@@ -144,6 +151,7 @@ class LXDInstance(base.Instance):
507 if self.platform.container_exists(self.name):
508 raise OSError('container {} was not properly removed'
509 .format(self.name))
510+ shutil.rmtree(self.tmpd)
511 super(LXDInstance, self).destroy()
512
513 # vi: ts=4 expandtab
514diff --git a/tests/cloud_tests/instances/nocloudkvm.py b/tests/cloud_tests/instances/nocloudkvm.py
515index 8a0e531..fbb04aa 100644
516--- a/tests/cloud_tests/instances/nocloudkvm.py
517+++ b/tests/cloud_tests/instances/nocloudkvm.py
518@@ -12,11 +12,19 @@ from cloudinit import util as c_util
519 from tests.cloud_tests.instances import base
520 from tests.cloud_tests import util
521
522+# This domain contains reverse lookups for hostnames that are used.
523+# The primary reason is so sudo will return quickly when it attempts
524+# to look up the hostname. i9n is just short for 'integration'.
525+# use i9n.brickies.net until i9n.cloud-init.io is popualted:
526+# https://portal.admin.canonical.com/107125
527+CI_DOMAIN = "i9n.brickies.net"
528+
529
530 class NoCloudKVMInstance(base.Instance):
531 """NoCloud KVM backed instance."""
532
533 platform_name = "nocloud-kvm"
534+ _ssh_client = None
535
536 def __init__(self, platform, name, properties, config, features,
537 user_data, meta_data):
538@@ -35,6 +43,7 @@ class NoCloudKVMInstance(base.Instance):
539 self.ssh_port = None
540 self.pid = None
541 self.pid_file = None
542+ self.console_file = None
543
544 super(NoCloudKVMInstance, self).__init__(
545 platform, name, properties, config, features)
546@@ -51,43 +60,18 @@ class NoCloudKVMInstance(base.Instance):
547 os.remove(self.pid_file)
548
549 self.pid = None
550- super(NoCloudKVMInstance, self).destroy()
551-
552- def execute(self, command, stdout=None, stderr=None, env=None,
553- rcs=None, description=None):
554- """Execute command in instance.
555-
556- Assumes functional networking and execution as root with the
557- target filesystem being available at /.
558-
559- @param command: the command to execute as root inside the image
560- if command is a string, then it will be executed as:
561- ['sh', '-c', command]
562- @param stdout, stderr: file handles to write output and error to
563- @param env: environment variables
564- @param rcs: allowed return codes from command
565- @param description: purpose of command
566- @return_value: tuple containing stdout data, stderr data, exit code
567- """
568- if env is None:
569- env = {}
570-
571- if isinstance(command, str):
572- command = ['sh', '-c', command]
573+ if self._ssh_client:
574+ self._ssh_client.close()
575+ self._ssh_client = None
576
577- if self.pid:
578- return self.ssh(command)
579- else:
580- return self.mount_image_callback(command) + (0,)
581+ super(NoCloudKVMInstance, self).destroy()
582
583- def mount_image_callback(self, cmd):
584- """Run mount-image-callback."""
585- out, err = c_util.subp(['sudo', 'mount-image-callback',
586- '--system-mounts', '--system-resolvconf',
587- self.name, '--', 'chroot',
588- '_MOUNTPOINT_'] + cmd)
589+ def _execute(self, command, stdin=None, env=None):
590+ env_args = []
591+ if env:
592+ env_args = ['env'] + ["%s=%s" for k, v in env.items()]
593
594- return out, err
595+ return self.ssh(['sudo'] + env_args + list(command), stdin=stdin)
596
597 def generate_seed(self, tmpdir):
598 """Generate nocloud seed from user-data"""
599@@ -109,57 +93,31 @@ class NoCloudKVMInstance(base.Instance):
600 s.close()
601 return num
602
603- def push_file(self, local_path, remote_path):
604- """Copy file at 'local_path' to instance at 'remote_path'.
605-
606- If we have a pid then SSH is up, otherwise, use
607- mount-image-callback.
608-
609- @param local_path: path on local instance
610- @param remote_path: path on remote instance
611- """
612- if self.pid:
613- super(NoCloudKVMInstance, self).push_file()
614- else:
615- local_file = open(local_path)
616- p = subprocess.Popen(['sudo', 'mount-image-callback',
617- '--system-mounts', '--system-resolvconf',
618- self.name, '--', 'chroot', '_MOUNTPOINT_',
619- '/bin/sh', '-c', 'cat - > %s' % remote_path],
620- stdin=local_file,
621- stdout=subprocess.PIPE,
622- stderr=subprocess.PIPE)
623- p.wait()
624-
625- def sftp_put(self, path, data):
626- """SFTP put a file."""
627- client = self._ssh_connect()
628- sftp = client.open_sftp()
629-
630- with sftp.open(path, 'w') as f:
631- f.write(data)
632-
633- client.close()
634-
635- def ssh(self, command):
636+ def ssh(self, command, stdin=None):
637 """Run a command via SSH."""
638 client = self._ssh_connect()
639
640+ cmd = util.shell_pack(command)
641 try:
642- _, out, err = client.exec_command(util.shell_pack(command))
643- except paramiko.SSHException:
644- raise util.InTargetExecuteError('', '', -1, command, self.name)
645-
646- exit = out.channel.recv_exit_status()
647- out = ''.join(out.readlines())
648- err = ''.join(err.readlines())
649- client.close()
650-
651- return out, err, exit
652+ fp_in, fp_out, fp_err = client.exec_command(cmd)
653+ channel = fp_in.channel
654+ if stdin is not None:
655+ fp_in.write(stdin)
656+ fp_in.close()
657+
658+ channel.shutdown_write()
659+ rc = channel.recv_exit_status()
660+ return (fp_out.read(), fp_err.read(), rc)
661+ except paramiko.SSHException as e:
662+ raise util.InTargetExecuteError(
663+ b'', b'', -1, command, self.name, reason=e)
664
665 def _ssh_connect(self, hostname='localhost', username='ubuntu',
666 banner_timeout=120, retry_attempts=30):
667 """Connect via SSH."""
668+ if self._ssh_client:
669+ return self._ssh_client
670+
671 private_key = paramiko.RSAKey.from_private_key_file(self.ssh_key_file)
672 client = paramiko.SSHClient()
673 client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
674@@ -168,6 +126,7 @@ class NoCloudKVMInstance(base.Instance):
675 client.connect(hostname=hostname, username=username,
676 port=self.ssh_port, pkey=private_key,
677 banner_timeout=banner_timeout)
678+ self._ssh_client = client
679 return client
680 except (paramiko.SSHException, TypeError):
681 time.sleep(1)
682@@ -183,15 +142,19 @@ class NoCloudKVMInstance(base.Instance):
683 tmpdir = self.platform.config['data_dir']
684 seed = self.generate_seed(tmpdir)
685 self.pid_file = os.path.join(tmpdir, '%s.pid' % self.name)
686+ self.console_file = os.path.join(tmpdir, '%s-console.log' % self.name)
687 self.ssh_port = self.get_free_port()
688
689- subprocess.Popen(['./tools/xkvm',
690- '--disk', '%s,cache=unsafe' % self.name,
691- '--disk', '%s,cache=unsafe' % seed,
692- '--netdev',
693- 'user,hostfwd=tcp::%s-:22' % self.ssh_port,
694- '--', '-pidfile', self.pid_file, '-vnc', 'none',
695- '-m', '2G', '-smp', '2'],
696+ cmd = ['./tools/xkvm',
697+ '--disk', '%s,cache=unsafe' % self.name,
698+ '--disk', '%s,cache=unsafe' % seed,
699+ '--netdev', ','.join(['user',
700+ 'hostfwd=tcp::%s-:22' % self.ssh_port,
701+ 'dnssearch=%s' % CI_DOMAIN]),
702+ '--', '-pidfile', self.pid_file, '-vnc', 'none',
703+ '-m', '2G', '-smp', '2', '-nographic',
704+ '-serial', 'file:' + self.console_file]
705+ subprocess.Popen(cmd,
706 close_fds=True,
707 stdin=subprocess.PIPE,
708 stdout=subprocess.PIPE,
709@@ -206,12 +169,10 @@ class NoCloudKVMInstance(base.Instance):
710 if wait:
711 self._wait_for_system(wait_for_cloud_init)
712
713- def write_data(self, remote_path, data):
714- """Write data to instance filesystem.
715-
716- @param remote_path: path in instance
717- @param data: data to write, either str or bytes
718- """
719- self.sftp_put(remote_path, data)
720+ def console_log(self):
721+ if not self.console_file:
722+ return b''
723+ with open(self.console_file, "rb") as fp:
724+ return fp.read()
725
726 # vi: ts=4 expandtab
727diff --git a/tests/cloud_tests/testcases/examples/run_commands.yaml b/tests/cloud_tests/testcases/examples/run_commands.yaml
728index b0e311b..f80eb8c 100644
729--- a/tests/cloud_tests/testcases/examples/run_commands.yaml
730+++ b/tests/cloud_tests/testcases/examples/run_commands.yaml
731@@ -7,10 +7,10 @@ enabled: False
732 cloud_config: |
733 #cloud-config
734 runcmd:
735- - echo cloud-init run cmd test > /tmp/run_cmd
736+ - echo cloud-init run cmd test > /var/tmp/run_cmd
737 collect_scripts:
738 run_cmd: |
739 #!/bin/bash
740- cat /tmp/run_cmd
741+ cat /var/tmp/run_cmd
742
743 # vi: ts=4 expandtab
744diff --git a/tests/cloud_tests/testcases/modules/keys_to_console.py b/tests/cloud_tests/testcases/modules/keys_to_console.py
745index 88b6812..07f3811 100644
746--- a/tests/cloud_tests/testcases/modules/keys_to_console.py
747+++ b/tests/cloud_tests/testcases/modules/keys_to_console.py
748@@ -10,13 +10,13 @@ class TestKeysToConsole(base.CloudTestCase):
749 def test_excluded_keys(self):
750 """Test excluded keys missing."""
751 out = self.get_data_file('syslog')
752- self.assertNotIn('DSA', out)
753- self.assertNotIn('ECDSA', out)
754+ self.assertNotIn('(DSA)', out)
755+ self.assertNotIn('(ECDSA)', out)
756
757 def test_expected_keys(self):
758 """Test expected keys exist."""
759 out = self.get_data_file('syslog')
760- self.assertIn('ED25519', out)
761- self.assertIn('RSA', out)
762+ self.assertIn('(ED25519)', out)
763+ self.assertIn('(RSA)', out)
764
765 # vi: ts=4 expandtab
766diff --git a/tests/cloud_tests/testcases/modules/runcmd.yaml b/tests/cloud_tests/testcases/modules/runcmd.yaml
767index 04e5a05..8309a88 100644
768--- a/tests/cloud_tests/testcases/modules/runcmd.yaml
769+++ b/tests/cloud_tests/testcases/modules/runcmd.yaml
770@@ -4,10 +4,10 @@
771 cloud_config: |
772 #cloud-config
773 runcmd:
774- - echo cloud-init run cmd test > /tmp/run_cmd
775+ - echo cloud-init run cmd test > /var/tmp/run_cmd
776 collect_scripts:
777 run_cmd: |
778 #!/bin/bash
779- cat /tmp/run_cmd
780+ cat /var/tmp/run_cmd
781
782 # vi: ts=4 expandtab
783diff --git a/tests/cloud_tests/testcases/modules/set_hostname.py b/tests/cloud_tests/testcases/modules/set_hostname.py
784index 6e96a75..1dbe64c 100644
785--- a/tests/cloud_tests/testcases/modules/set_hostname.py
786+++ b/tests/cloud_tests/testcases/modules/set_hostname.py
787@@ -7,9 +7,11 @@ from tests.cloud_tests.testcases import base
788 class TestHostname(base.CloudTestCase):
789 """Test hostname module."""
790
791+ ex_hostname = "cloudinit2"
792+
793 def test_hostname(self):
794 """Test hostname command shows correct output."""
795 out = self.get_data_file('hostname')
796- self.assertIn('myhostname', out)
797+ self.assertIn(self.ex_hostname, out)
798
799 # vi: ts=4 expandtab
800diff --git a/tests/cloud_tests/testcases/modules/set_hostname.yaml b/tests/cloud_tests/testcases/modules/set_hostname.yaml
801index c96344c..071fb22 100644
802--- a/tests/cloud_tests/testcases/modules/set_hostname.yaml
803+++ b/tests/cloud_tests/testcases/modules/set_hostname.yaml
804@@ -5,7 +5,8 @@ required_features:
805 - hostname
806 cloud_config: |
807 #cloud-config
808- hostname: myhostname
809+ hostname: cloudinit2
810+
811 collect_scripts:
812 hosts: |
813 #!/bin/bash
814diff --git a/tests/cloud_tests/testcases/modules/set_hostname_fqdn.py b/tests/cloud_tests/testcases/modules/set_hostname_fqdn.py
815index 398f3d4..08ceae0 100644
816--- a/tests/cloud_tests/testcases/modules/set_hostname_fqdn.py
817+++ b/tests/cloud_tests/testcases/modules/set_hostname_fqdn.py
818@@ -7,20 +7,24 @@ from tests.cloud_tests.testcases import base
819 class TestHostnameFqdn(base.CloudTestCase):
820 """Test Hostname module."""
821
822+ ex_hostname = "cloudinit1"
823+ ex_fqdn = "cloudinit2.i9n.brickies.net"
824+
825 def test_hostname(self):
826 """Test hostname output."""
827 out = self.get_data_file('hostname')
828- self.assertIn('myhostname', out)
829+ self.assertIn(self.ex_hostname, out)
830
831 def test_hostname_fqdn(self):
832 """Test hostname fqdn output."""
833 out = self.get_data_file('fqdn')
834- self.assertIn('host.myorg.com', out)
835+ self.assertIn(self.ex_fqdn, out)
836
837 def test_hosts(self):
838 """Test /etc/hosts file."""
839 out = self.get_data_file('hosts')
840- self.assertIn('127.0.1.1 host.myorg.com myhostname', out)
841+ self.assertIn('127.0.1.1 %s %s' % (self.ex_fqdn, self.ex_hostname),
842+ out)
843 self.assertIn('127.0.0.1 localhost', out)
844
845 # vi: ts=4 expandtab
846diff --git a/tests/cloud_tests/testcases/modules/set_hostname_fqdn.yaml b/tests/cloud_tests/testcases/modules/set_hostname_fqdn.yaml
847index daf7593..5320ac5 100644
848--- a/tests/cloud_tests/testcases/modules/set_hostname_fqdn.yaml
849+++ b/tests/cloud_tests/testcases/modules/set_hostname_fqdn.yaml
850@@ -6,8 +6,8 @@ required_features:
851 cloud_config: |
852 #cloud-config
853 manage_etc_hosts: true
854- hostname: myhostname
855- fqdn: host.myorg.com
856+ hostname: cloudinit1
857+ fqdn: cloudinit2.i9n.brickies.net
858 collect_scripts:
859 hosts: |
860 #!/bin/bash
861diff --git a/tests/cloud_tests/testcases/modules/set_password_expire.py b/tests/cloud_tests/testcases/modules/set_password_expire.py
862index a1c3aa0..967aca7 100644
863--- a/tests/cloud_tests/testcases/modules/set_password_expire.py
864+++ b/tests/cloud_tests/testcases/modules/set_password_expire.py
865@@ -18,6 +18,6 @@ class TestPasswordExpire(base.CloudTestCase):
866 def test_sshd_config(self):
867 """Test sshd config allows passwords."""
868 out = self.get_data_file('sshd_config')
869- self.assertIn('PasswordAuthentication no', out)
870+ self.assertIn('PasswordAuthentication yes', out)
871
872 # vi: ts=4 expandtab
873diff --git a/tests/cloud_tests/testcases/modules/set_password_expire.yaml b/tests/cloud_tests/testcases/modules/set_password_expire.yaml
874index 789604b..ba6344b 100644
875--- a/tests/cloud_tests/testcases/modules/set_password_expire.yaml
876+++ b/tests/cloud_tests/testcases/modules/set_password_expire.yaml
877@@ -6,7 +6,9 @@ required_features:
878 cloud_config: |
879 #cloud-config
880 chpasswd: { expire: True }
881+ ssh_pwauth: yes
882 users:
883+ - default
884 - name: tom
885 password: $1$xyz$sPMsLNmf66Ohl.ol6JvzE.
886 lock_passwd: false
887diff --git a/tests/cloud_tests/testcases/modules/set_password_list.yaml b/tests/cloud_tests/testcases/modules/set_password_list.yaml
888index a2a89c9..fd3e1e4 100644
889--- a/tests/cloud_tests/testcases/modules/set_password_list.yaml
890+++ b/tests/cloud_tests/testcases/modules/set_password_list.yaml
891@@ -5,6 +5,7 @@ cloud_config: |
892 #cloud-config
893 ssh_pwauth: yes
894 users:
895+ - default
896 - name: tom
897 # md5 gotomgo
898 passwd: "$1$S7$tT1BEDIYrczeryDQJfdPe0"
899diff --git a/tests/cloud_tests/testcases/modules/set_password_list_string.yaml b/tests/cloud_tests/testcases/modules/set_password_list_string.yaml
900index c2a0f63..e9fe54b 100644
901--- a/tests/cloud_tests/testcases/modules/set_password_list_string.yaml
902+++ b/tests/cloud_tests/testcases/modules/set_password_list_string.yaml
903@@ -5,6 +5,7 @@ cloud_config: |
904 #cloud-config
905 ssh_pwauth: yes
906 users:
907+ - default
908 - name: tom
909 # md5 gotomgo
910 passwd: "$1$S7$tT1BEDIYrczeryDQJfdPe0"
911diff --git a/tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.py b/tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.py
912index 8222321..e7329d4 100644
913--- a/tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.py
914+++ b/tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.py
915@@ -13,12 +13,4 @@ class TestSshKeyFingerprintsDisable(base.CloudTestCase):
916 self.assertIn('Skipping module named ssh-authkey-fingerprints, '
917 'logging of ssh fingerprints disabled', out)
918
919- def test_syslog(self):
920- """Verify output of syslog."""
921- out = self.get_data_file('syslog')
922- self.assertNotRegex(out, r'256 SHA256:.*(ECDSA)')
923- self.assertNotRegex(out, r'256 SHA256:.*(ED25519)')
924- self.assertNotRegex(out, r'1024 SHA256:.*(DSA)')
925- self.assertNotRegex(out, r'2048 SHA256:.*(RSA)')
926-
927 # vi: ts=4 expandtab
928diff --git a/tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.yaml b/tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.yaml
929index 746653e..d93893e 100644
930--- a/tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.yaml
931+++ b/tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.yaml
932@@ -5,7 +5,6 @@ required_features:
933 - syslog
934 cloud_config: |
935 #cloud-config
936- ssh_genkeytypes: []
937 no_ssh_fingerprints: true
938 collect_scripts:
939 syslog: |
940diff --git a/tests/cloud_tests/testcases/modules/ssh_keys_generate.py b/tests/cloud_tests/testcases/modules/ssh_keys_generate.py
941index fd6d9ba..b68f556 100644
942--- a/tests/cloud_tests/testcases/modules/ssh_keys_generate.py
943+++ b/tests/cloud_tests/testcases/modules/ssh_keys_generate.py
944@@ -9,11 +9,6 @@ class TestSshKeysGenerate(base.CloudTestCase):
945
946 # TODO: Check cloud-init-output for the correct keys being generated
947
948- def test_ubuntu_authorized_keys(self):
949- """Test passed in key is not in list for ubuntu."""
950- out = self.get_data_file('auth_keys_ubuntu')
951- self.assertEqual('', out)
952-
953 def test_dsa_public(self):
954 """Test dsa public key not generated."""
955 out = self.get_data_file('dsa_public')
956diff --git a/tests/cloud_tests/testcases/modules/ssh_keys_generate.yaml b/tests/cloud_tests/testcases/modules/ssh_keys_generate.yaml
957index 659fd93..0a7adf6 100644
958--- a/tests/cloud_tests/testcases/modules/ssh_keys_generate.yaml
959+++ b/tests/cloud_tests/testcases/modules/ssh_keys_generate.yaml
960@@ -10,12 +10,6 @@ cloud_config: |
961 - ed25519
962 authkey_hash: sha512
963 collect_scripts:
964- auth_keys_root: |
965- #!/bin/bash
966- cat /root/.ssh/authorized_keys
967- auth_keys_ubuntu: |
968- #!/bin/bash
969- cat /home/ubuntu/ssh/authorized_keys
970 dsa_public: |
971 #!/bin/bash
972 cat /etc/ssh/ssh_host_dsa_key.pub
973diff --git a/tests/cloud_tests/testcases/modules/ssh_keys_provided.py b/tests/cloud_tests/testcases/modules/ssh_keys_provided.py
974index 544649d..add3f46 100644
975--- a/tests/cloud_tests/testcases/modules/ssh_keys_provided.py
976+++ b/tests/cloud_tests/testcases/modules/ssh_keys_provided.py
977@@ -7,17 +7,6 @@ from tests.cloud_tests.testcases import base
978 class TestSshKeysProvided(base.CloudTestCase):
979 """Test ssh keys module."""
980
981- def test_ubuntu_authorized_keys(self):
982- """Test passed in key is not in list for ubuntu."""
983- out = self.get_data_file('auth_keys_ubuntu')
984- self.assertEqual('', out)
985-
986- def test_root_authorized_keys(self):
987- """Test passed in key is in authorized list for root."""
988- out = self.get_data_file('auth_keys_root')
989- self.assertIn('lzrkPqONphoZx0LDV86w7RUz1ksDzAdcm0tvmNRFMN1a0frDs50'
990- '6oA3aWK0oDk4Nmvk8sXGTYYw3iQSkOvDUUlIsqdaO+w==', out)
991-
992 def test_dsa_public(self):
993 """Test dsa public key passed in."""
994 out = self.get_data_file('dsa_public')
995diff --git a/tests/cloud_tests/testcases/modules/ssh_keys_provided.yaml b/tests/cloud_tests/testcases/modules/ssh_keys_provided.yaml
996index 5ceb362..41f6355 100644
997--- a/tests/cloud_tests/testcases/modules/ssh_keys_provided.yaml
998+++ b/tests/cloud_tests/testcases/modules/ssh_keys_provided.yaml
999@@ -71,12 +71,6 @@ cloud_config: |
1000 -----END EC PRIVATE KEY-----
1001 ecdsa_public: ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFsS5Tvky/IC/dXhE/afxxUG6kdQOvdQJCYGZN42OZqWasYF+L3IG+3/wrV7jOrNrL3AyagHl6+lpPDiSXDMcpQ= root@xenial-lxd
1002 collect_scripts:
1003- auth_keys_root: |
1004- #!/bin/bash
1005- cat /root/.ssh/authorized_keys
1006- auth_keys_ubuntu: |
1007- #!/bin/bash
1008- cat /home/ubuntu/ssh/authorized_keys
1009 dsa_public: |
1010 #!/bin/bash
1011 cat /etc/ssh/ssh_host_dsa_key.pub
1012diff --git a/tests/cloud_tests/util.py b/tests/cloud_tests/util.py
1013index 4357fbb..92c31c3 100644
1014--- a/tests/cloud_tests/util.py
1015+++ b/tests/cloud_tests/util.py
1016@@ -7,6 +7,7 @@ import copy
1017 import glob
1018 import os
1019 import random
1020+import shlex
1021 import shutil
1022 import string
1023 import subprocess
1024@@ -285,20 +286,165 @@ def shell_pack(cmd):
1025 return 'eval set -- "$(echo %s | base64 --decode)" && exec "$@"' % b64
1026
1027
1028+def shell_quote(cmd):
1029+ if isinstance(cmd, (tuple, list)):
1030+ return ' '.join([shlex.quote(x) for x in cmd])
1031+ return shlex.quote(cmd)
1032+
1033+
1034+class TargetBase(object):
1035+ _tmp_count = 0
1036+
1037+ def execute(self, command, stdin=None, env=None,
1038+ rcs=None, description=None):
1039+ """Execute command in instance, recording output, error and exit code.
1040+
1041+ Assumes functional networking and execution as root with the
1042+ target filesystem being available at /.
1043+
1044+ @param command: the command to execute as root inside the image
1045+ if command is a string, then it will be executed as:
1046+ ['sh', '-c', command]
1047+ @param stdin: bytes content for standard in
1048+ @param env: environment variables
1049+ @param rcs: return codes.
1050+ None (default): non-zero exit code will raise exception.
1051+ False: any is allowed (No execption raised).
1052+ list of int: any rc not in the list will raise exception.
1053+ @param description: purpose of command
1054+ @return_value: tuple containing stdout data, stderr data, exit code
1055+ """
1056+ if isinstance(command, str):
1057+ command = ['sh', '-c', command]
1058+
1059+ if rcs is None:
1060+ rcs = (0,)
1061+
1062+ if description:
1063+ LOG.debug('Executing "%s"', description)
1064+ else:
1065+ LOG.debug("Executing command: %s", shell_quote(command))
1066+
1067+ out, err, rc = self._execute(command=command, stdin=stdin, env=env)
1068+
1069+ # False means accept anything.
1070+ if (rcs is False or rc in rcs):
1071+ return out, err, rc
1072+
1073+ raise InTargetExecuteError(out, err, rc, command, description)
1074+
1075+ def _execute(self, command, stdin=None, env=None):
1076+ """Execute command in inside, return stdout, stderr and exit code.
1077+
1078+ Assumes functional networking and execution as root with the
1079+ target filesystem being available at /.
1080+
1081+ @param stdin: bytes content for standard in
1082+ @param env: environment variables
1083+ @return_value: tuple containing stdout data, stderr data, exit code
1084+
1085+ This is intended to be implemented by the Image or Instance.
1086+ Many callers will use the higher level 'execute'."""
1087+ raise NotImplementedError
1088+
1089+ def read_data(self, remote_path, decode=False):
1090+ """Read data from instance filesystem.
1091+
1092+ @param remote_path: path in instance
1093+ @param decode: decode data before returning.
1094+ @return_value: content of remote_path as bytes if 'decode' is False,
1095+ and as string if 'decode' is True.
1096+ """
1097+ # when sh is invoked with '-c', then the first argument is "$0"
1098+ # which is commonly understood as the "program name".
1099+ # 'read_data' is the program name, and 'remote_path' is '$1'
1100+ stdout, stderr, rc = self._execute(
1101+ ["sh", "-c", 'exec cat "$1"', 'read_data', remote_path])
1102+ if rc != 0:
1103+ raise RuntimeError("Failed to read file '%s'" % remote_path)
1104+
1105+ if decode:
1106+ return stdout.decode()
1107+ return stdout
1108+
1109+ def write_data(self, remote_path, data):
1110+ """Write data to instance filesystem.
1111+
1112+ @param remote_path: path in instance
1113+ @param data: data to write in bytes
1114+ """
1115+ # when sh is invoked with '-c', then the first argument is "$0"
1116+ # which is commonly understood as the "program name".
1117+ # 'write_data' is the program name, and 'remote_path' is '$1'
1118+ _, _, rc = self._execute(
1119+ ["sh", "-c", 'exec cat >"$1"', 'write_data', remote_path],
1120+ stdin=data)
1121+
1122+ if rc != 0:
1123+ raise RuntimeError("Failed to write to '%s'" % remote_path)
1124+ return
1125+
1126+ def pull_file(self, remote_path, local_path):
1127+ """Copy file at 'remote_path', from instance to 'local_path'.
1128+
1129+ @param remote_path: path on remote instance
1130+ @param local_path: path on local instance
1131+ """
1132+ with open(local_path, 'wb') as fp:
1133+ fp.write(self.read_data(remote_path))
1134+
1135+ def push_file(self, local_path, remote_path):
1136+ """Copy file at 'local_path' to instance at 'remote_path'.
1137+
1138+ @param local_path: path on local instance
1139+ @param remote_path: path on remote instance"""
1140+ with open(local_path, "rb") as fp:
1141+ self.write_data(remote_path, data=fp.read())
1142+
1143+ def run_script(self, script, rcs=None, description=None):
1144+ """Run script in target and return stdout.
1145+
1146+ @param script: script contents
1147+ @param rcs: allowed return codes from script
1148+ @param description: purpose of script
1149+ @return_value: stdout from script
1150+ """
1151+ # Just write to a file, add execute, run it, then remove it.
1152+ shblob = '; '.join((
1153+ 'set -e',
1154+ 's="$1"',
1155+ 'shift',
1156+ 'cat > "$s"',
1157+ 'trap "rm -f $s" EXIT',
1158+ 'chmod +x "$s"',
1159+ '"$s" "$@"'))
1160+ return self.execute(
1161+ ['sh', '-c', shblob, 'runscript', self.tmpfile()],
1162+ stdin=script, description=description, rcs=rcs)
1163+
1164+ def tmpfile(self):
1165+ """Get a tmp file in the target.
1166+
1167+ @return_value: path to new file in target
1168+ """
1169+ path = "/tmp/%s-%04d" % (type(self).__name__, self._tmp_count)
1170+ self._tmp_count += 1
1171+ return path
1172+
1173+
1174 class InTargetExecuteError(c_util.ProcessExecutionError):
1175 """Error type for in target commands that fail."""
1176
1177- default_desc = 'Unexpected error while running command in target instance'
1178+ default_desc = 'Unexpected error while running command.'
1179
1180- def __init__(self, stdout, stderr, exit_code, cmd, instance,
1181- description=None):
1182+ def __init__(self, stdout, stderr, exit_code, cmd, description=None,
1183+ reason=None):
1184 """Init error and parent error class."""
1185- if isinstance(cmd, (tuple, list)):
1186- cmd = ' '.join(cmd)
1187 super(InTargetExecuteError, self).__init__(
1188- stdout=stdout, stderr=stderr, exit_code=exit_code, cmd=cmd,
1189- reason="Instance: {}".format(instance),
1190- description=description if description else self.default_desc)
1191+ stdout=stdout, stderr=stderr, exit_code=exit_code,
1192+ cmd=shell_quote(cmd),
1193+ description=description if description else self.default_desc,
1194+ reason=reason)
1195
1196
1197 class TempDir(object):

Subscribers

People subscribed via source and target branches

to all changes: