Merge ~bladernr/plainbox-provider-checkbox:1628264-virt-timeout into plainbox-provider-checkbox:master

Proposed by Jeff Lane 
Status: Merged
Approved by: Rod Smith
Approved revision: 6709eb2fbcb1fe279c89beb0668c1fdc5af2cb65
Merged at revision: 71725310d95a76f942dda7642611ef68dc8e9d70
Proposed branch: ~bladernr/plainbox-provider-checkbox:1628264-virt-timeout
Merge into: plainbox-provider-checkbox:master
Diff against target: 167 lines (+45/-32)
1 file modified
bin/virtualization (+45/-32)
Reviewer Review Type Date Requested Status
Rod Smith Approve
Review via email: mp+326038@code.launchpad.net

Description of the change

KVM test now properly polls console output for status message (before, regardless of successful boot, it would just sleep the full TIMEOUT before checking).
Also, quiet urllib3 built in logger to squash unnecessary INFO level messages.

To post a comment you must log in.
Revision history for this message
Rod Smith (rodsmith) wrote :

The Good
--------

I've run the modified script on a couple of systems, one of which supports virtualization and the other of which does not, and I got expected results both times:

* https://certification.canonical.com/hardware/201701-25363/submission/120327/test/380/result/8905819/
* https://certification.canonical.com/hardware/201409-15506/submission/120330/test/380/result/8906798/

I have not tested every possible failure mode, though.

The Bad
-------

Both "pylint" and "flake8" produce large numbers of errors and warnings on this script. Most of these are for existing code, so it's unfair to ask you to fix them on this submission; however, there were a few complaints about code submitted here. From the pylint output:

C:443, 0: Exactly one space required after comma
    def log_check(self,stream):
                      ^ (bad-whitespace)
C:499, 0: Exactly one space required around assignment
                self.elapsed_time=0
                                 ^ (bad-whitespace)
W:529,30: Use % formatting in logging functions and pass the % parameters as arguments (logging-format-interpolation)
W:499,16: Attribute 'elapsed_time' defined outside __init__ (attribute-defined-outside-init)

If you prefer to file a bug report about the large number of complaints by these tools and leave these as-is, I'm willing to let this slide, since they seem to be trivial things that don't affect the program's ability to work, and they're a drop in the bucket amidst the many similar complaints by pylint and flake8.

The Question:
-------------

Finally, concerning the while/else clause on lines 501-524: It seems that a lot of code is duplicated from the main while loop in the else clause. The main difference seems to be that, in the else clause, the output of debug_file.read() is saved to the variable stream so that it can be logged if there's an error. Wouldn't it make more sense to simply store output in the stream variable in the main while loop and then log it only if there's an error? If there's some reason this doesn't work, then I'll pass it as-is; but if I'm not overlooking something, it appears that the code could be made cleaner by revising this logic.

review: Needs Information
Revision history for this message
Jeff Lane  (bladernr) wrote :
Download full text (4.1 KiB)

On Wed, Jun 21, 2017 at 12:29 PM, Rod Smith <email address hidden> wrote:
> The Bad
> -------
>
> Both "pylint" and "flake8" produce large numbers of errors and warnings on this script. Most of these are for existing code, so it's unfair to ask you to fix them on this submission; however, there were a few complaints about code
> submitted here. From the pylint output:

There's a reason I don't use pylint and flake8. Pylint, for example,
also tells me this:

C:742, 0: Unnecessary parens after 'print' keyword (superfluous-parens)
C:745, 0: Unnecessary parens after 'print' keyword (superfluous-parens)

which is incorrect, python3 treats print as a function and the parens
are passing arguments to that function... they're both a bit crazy
and require a good bit of configuration and work to produce useful
output, IMO. IN any case, I always check to the level of pep8, (apt
install pep8) which catches most of the PEP8 violations (the important
ones at least).

> C:443, 0: Exactly one space required after comma
> def log_check(self,stream):
> ^ (bad-whitespace)
> C:499, 0: Exactly one space required around assignment
> self.elapsed_time=0
> ^ (bad-whitespace)
> W:529,30: Use % formatting in logging functions and pass the % parameters as arguments (logging-format-interpolation)
> W:499,16: Attribute 'elapsed_time' defined outside __init__ (attribute-defined-outside-init)
>
> If you prefer to file a bug report about the large number of complaints by these tools and leave these as-is, I'm willing to let this slide, since they seem to be trivial things that don't affect the program's ability to work, and they're a drop in the bucket amidst the many similar complaints by pylint and flake8.

I did a few, just to see how bit a task it would be, and it would be
huge. If it really bugs you, we can revisit it later to pylint/flake8
it, but once you go down that road, it'll need to be done for every
python script in there ;) pep8 was the recommended tool way back
when, and afaik, still is, and most, or all, the scripts at this point
will pass a pep8 check.

> The Question:
> -------------
>
> Finally, concerning the while/else clause on lines 501-524: It seems that a lot of code is duplicated from the main while loop in the else clause. The main difference seems to be that, in the else clause, the output of debug_file.read() is
> saved to the variable stream so that it can be logged if there's an error. Wouldn't it make more sense to simply store output in the stream variable in the main while loop and then log it only if there's an error? If there's some reason this
> doesn't work, then I'll pass it as-is; but if I'm not overlooking something, it appears that the code could be made cleaner by revising this logic.

That's a good question, and I did spend a fair bit of time wrestling
with how to do what I wanted to do. In the end, this was the least
complicated... The file reads have to happen within the while loop,
since we read the contents and close the filehandle on each pass. I
didn't want to open a second filehandle (since KVM is already writing
to that file) that was cont...

Read more...

Revision history for this message
Rod Smith (rodsmith) wrote :

OK. I see you've fixed the couple of pep8 complaints. (I agree that these Python code-checking tools are overly-anal, but I wanted you to be aware of the fact that they ARE complaining a lot when fed this script.) Your explanation for why duplicating code in the "while" loop's "else" clause is necessary makes sense. Thus, I'm accepting this version.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/bin/virtualization b/bin/virtualization
2index 46cf13e..b67a5fa 100755
3--- a/bin/virtualization
4+++ b/bin/virtualization
5@@ -229,14 +229,14 @@ class KVMTest(object):
6 def _construct_filename(alt_pattern=None, initial_url=None):
7 if self.qemu_config['cloudimg_type'] == CLOUD_IMAGE_TYPE_TAR:
8 cloud_iso = "%s-server-cloudimg-%s.tar.gz" % (
9- self.release, self.qemu_config['cloudimg_arch'])
10+ self.release, self.qemu_config['cloudimg_arch'])
11 elif alt_pattern is "modern":
12 # LP 1635345 - yakkety and beyond have a new naming scheme
13 cloud_iso = "%s-server-cloudimg-%s.img" % (
14- self.release, self.qemu_config['cloudimg_arch'])
15+ self.release, self.qemu_config['cloudimg_arch'])
16 elif self.qemu_config['cloudimg_type'] == CLOUD_IMAGE_TYPE_DISK:
17 cloud_iso = "%s-server-cloudimg-%s-disk1.img" % (
18- self.release, self.qemu_config['cloudimg_arch'])
19+ self.release, self.qemu_config['cloudimg_arch'])
20 elif initial_url:
21 # LP 1662580 - if we pass a full URL, assume the last piece is
22 # the filname and return that.
23@@ -388,7 +388,7 @@ class KVMTest(object):
24 logging.debug("Using params:{}".format(" ".join(params)))
25
26 logging.info("Storing VM console output in {}".format(
27- os.path.realpath(self.debug_file)))
28+ os.path.realpath(self.debug_file)))
29 # Open VM STDERR/STDOUT log file for writing
30 try:
31 file = open(self.debug_file, 'w')
32@@ -440,6 +440,12 @@ final_message: CERTIFICATION BOOT COMPLETE
33 except CalledProcessError as exception:
34 logging.exception("Cloud data disk creation failed")
35
36+ def log_check(self, stream):
37+ if "CERTIFICATION BOOT COMPLETE" in stream:
38+ return 0
39+ else:
40+ return 1
41+
42 def start(self):
43 if self.arch == 'arm64':
44 # lp:1548539 - For arm64, we need to make sure we're using qemu
45@@ -485,36 +491,42 @@ final_message: CERTIFICATION BOOT COMPLETE
46 # Boot Virtual Machine
47 instance = self.boot_image(self.image)
48
49- time.sleep(self.timeout)
50 # If running in console, reset console window to regain
51 # control from VM Serial I/0
52 if sys.stdout.isatty():
53 call('reset')
54 # Check to be sure VM boot was successful
55- with open(self.debug_file, 'r') as debug_file:
56- file_contents = debug_file.read()
57- if "CERTIFICATION BOOT COMPLETE" in file_contents:
58- if "END SSH HOST KEY KEYS" in file_contents:
59- print("Booted successfully", file=sys.stderr)
60+ self.elapsed_time = 0
61+ status = 1
62+ while self.elapsed_time <= self.timeout:
63+ # Check log every 30 seconds to see if the VM boots
64+ with open(self.debug_file, 'r') as debug_file:
65+ status = self.log_check(debug_file.read())
66+ if status == 0:
67+ logging.info("Booted successfully.")
68+ break
69 else:
70- print("Booted successfully (Previously "
71- "initalized VM)", file=sys.stderr)
72- status = 0
73- else:
74- print("E: KVM instance failed to boot",
75- file=sys.stderr)
76- print("Console output".center(72, "="),
77- file=sys.stderr)
78- with open(self.debug_file, 'r') as console_log:
79- print(console_log.read(), file=sys.stderr)
80- print("E: KVM instance failed to boot",
81- file=sys.stderr)
82+ # Sleep 30 seconds and log check again
83+ time.sleep(30)
84+ self.elapsed_time += 30
85+ else:
86+ # Finally, if we didn't get the Success message by now try
87+ # one more time and return 1 if we still haven't booted
88+ if status != 0:
89+ with open(self.debug_file, 'r') as debug_file:
90+ stream = debug_file.read()
91+ status = self.log_check(stream)
92+ if status == 0:
93+ logging.info("Booted successfully.")
94+ else:
95+ logging.error("KVM instance failed to boot.")
96+ logging.error("Console output".center(72, "="))
97+ logging.error(stream)
98 self.process.terminate()
99 elif not self.image:
100- print("Could not find downloaded image")
101+ logging.error("Could not find downloaded image")
102 else:
103- print("Could not find: {}".format(self.image),
104- file=sys.stderr)
105+ logging.error("Could not find: {}".format(self.image))
106
107 return status
108
109@@ -560,7 +572,7 @@ class LXDTest(object):
110 task = RunCommand(cmd)
111 if task.returncode != 0:
112 logging.error('Command {} returnd a code of {}'.format(
113- task.cmd, task.returncode))
114+ task.cmd, task.returncode))
115 logging.error(' STDOUT: {}'.format(task.stdout))
116 logging.error(' STDERR: {}'.format(task.stderr))
117 return False
118@@ -611,7 +623,7 @@ class LXDTest(object):
119 filename)
120 if not self.rootfs_tarball:
121 logging.error("Unable to download {} from{}".format(
122- self.rootfs_tarball, self.rootfs_url))
123+ self.rootfs_tarball, self.rootfs_url))
124 logging.error("Aborting")
125 result = False
126 else:
127@@ -620,11 +632,11 @@ class LXDTest(object):
128 self.rootfs_tarball = filename
129
130 # Insert images
131- if self.template_url is not None and self.rootfs_url is not None:
132+ if self.template_url is not None and self.rootfs_url is not None:
133 logging.debug("Importing images into LXD")
134 cmd = 'lxc image import {} rootfs {} --alias {}'.format(
135- self.template_tarball, self.rootfs_tarball,
136- self.image_alias)
137+ self.template_tarball, self.rootfs_tarball,
138+ self.image_alias)
139 if not self.run_command(cmd):
140 logging.error('Error encountered while attempting to '
141 'import images into LXD')
142@@ -633,7 +645,7 @@ class LXDTest(object):
143 logging.debug("No local image available, attempting to "
144 "import from default remote.")
145 cmd = 'lxc image copy {}{} local: --alias {}'.format(
146- self.default_remote, self.os_version, self.image_alias)
147+ self.default_remote, self.os_version, self.image_alias)
148 if not self.run_command(cmd):
149 logging.error('Error encountered while attempting to '
150 'import images from default remote.')
151@@ -735,7 +747,7 @@ def test_lxd(args):
152
153
154 def test_kvm(args):
155- print("Executing KVM Test", file=sys.stderr)
156+ logging.debug("Executing KVM Test")
157
158 image = ""
159 timeout = ""
160@@ -809,6 +821,7 @@ def main():
161
162 # silence normal output from requests module
163 logging.getLogger("requests").setLevel(logging.WARNING)
164+ logging.getLogger("urllib3").setLevel(logging.WARNING)
165
166 # Verify args
167 try:

Subscribers

People subscribed via source and target branches