Skip to content
This repository was archived by the owner on Aug 5, 2022. It is now read-only.

Commit 55a3734

Browse files
committed
systemupdate: refactor code
Much of the code relied on a running code in certain contexts. So far, that was solved via callbacks, which made reusing some subset of the functionality or parameterizing tests harder. Now the flow is broken up into individual steps, with context managers used whenever cleanup operations are needed. Signed-off-by: Patrick Ohly <patrick.ohly@intel.com>
1 parent 443a91b commit 55a3734

2 files changed

Lines changed: 181 additions & 124 deletions

File tree

Lines changed: 126 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,101 @@
11
from oeqa.selftest.systemupdate.systemupdatebase import SystemUpdateBase
22
from oeqa.utils.commands import runqemu, get_bb_vars, bitbake
33

4+
import contextlib
45
import http.server
56
import os
67
import stat
78
import errno
89
import tempfile
10+
import traceback
911
import threading
1012

13+
class HTTPServer(object):
14+
"""
15+
Dynamically finds an available port and serves a certain directory there.
16+
To be used in a "with HTTPServer(dir) as httpd" construct.
17+
"""
18+
def __init__(self, root, logger):
19+
self.root = root
20+
self.logger = logger
21+
self.server = None
22+
self.http_log = []
23+
self.stop_at = None
24+
25+
def __enter__(self):
26+
try:
27+
class HTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
28+
parent = self
29+
request_counter = 0
30+
def log_message(self, format, *args):
31+
msg = format % args
32+
self.parent.logger.info(msg)
33+
self.parent.http_log.append(msg)
34+
35+
def translate_path(self, path):
36+
"""
37+
Return absolute path based on document root instead of current directory.
38+
"""
39+
40+
# The original implementation returns an absolute path rooted in the
41+
# current directory. We need to serve a different
42+
# directory without being able to chdir(), because
43+
# doing that would cause commands like bitbake to
44+
# run there, which is undesirable because for
45+
# example bitbake creates a bitbake-cookerdaemon.log
46+
# in the current directory.
47+
path = super().translate_path(path)
48+
relpath = os.path.relpath(path)
49+
path = os.path.join(self.parent.root, relpath)
50+
return path
51+
52+
def do_GET(self):
53+
"""
54+
Inject errors.
55+
"""
56+
counter = HTTPRequestHandler.request_counter
57+
HTTPRequestHandler.request_counter += 1
58+
if self.parent.stop_at is not None and counter >= self.parent.stop_at:
59+
self.send_error(500, 'test server is intentionally down')
60+
else:
61+
super().do_GET()
62+
63+
handler = HTTPRequestHandler
64+
65+
def create_httpd():
66+
for port in range(9999, 10000):
67+
try:
68+
server = http.server.HTTPServer(('localhost', port), handler)
69+
return server
70+
except OSError as ex:
71+
if ex.errno != errno.EADDRINUSE:
72+
raise
73+
self.fail('no port available for HTTP server')
74+
75+
self.server = create_httpd()
76+
self.port = self.server.server_port
77+
self.logger.info('serving repo %s on port %d' % (self.root, self.port))
78+
helper = threading.Thread(name='HTTPD', target=self.server.serve_forever)
79+
helper.start()
80+
81+
# Now let caller do its work while the server runs.
82+
return self
83+
except:
84+
self._stop()
85+
raise
86+
87+
def __exit__(self, exc_type, exc_val, exc_tb):
88+
self._stop()
89+
90+
def _stop(self):
91+
# We have to stop a running server under all circumstances,
92+
# otherwise the helper thread will keep running and we end up
93+
# with thread locking issues.
94+
if self.server:
95+
self.server.shutdown()
96+
self.server.server_close()
97+
self.server = None
98+
1199
class HTTPUpdate(SystemUpdateBase):
12100
"""
13101
System update tests for image update mechanisms which depend on
@@ -51,7 +139,8 @@ def track_for_cleanup(self, name):
51139
if 'NO_CLEANUP' not in os.environ:
52140
super().track_for_cleanup(name)
53141

54-
def boot_image(self, overrides):
142+
@contextlib.contextmanager
143+
def boot_image(self, overrides = {}):
55144
# We don't know the final port yet, so instead we create a placeholder script
56145
# for qemu to use and rewrite that script once we are ready. The kernel refuses
57146
# to execute a shell script while we have it open, so here we close it
@@ -60,106 +149,55 @@ def boot_image(self, overrides):
60149
# The helper script also keeps command line handling a bit simpler (no whitespace
61150
# in -netdev parameter), which may or may not be relevant.
62151
self.httpd_netcat = tempfile.NamedTemporaryFile(mode='w', prefix='httpd-netcat-', dir=os.getcwd(), delete=False)
63-
self.httpd_netcat.close()
64-
os.chmod(self.httpd_netcat.name, stat.S_IRUSR|stat.S_IWUSR|stat.S_IXUSR)
65-
self.track_for_cleanup(self.httpd_netcat.name)
66-
67-
qemuboot_conf = os.path.join(self.image_dir_test,
68-
'%s-%s.qemuboot.conf' % (self.IMAGE_PN, self.BB_VARS['MACHINE']))
69-
with open(qemuboot_conf) as f:
70-
conf = f.read()
71-
with open(qemuboot_conf, 'w') as f:
72-
f.write('\n'.join([x for x in conf.splitlines() if not x.startswith('qb_slirp_opt')]))
73-
f.write('\nqb_slirp_opt = -netdev user,id=net0,guestfwd=tcp:%s-cmd:%s\n' % \
74-
(self.HTTPD_SERVER, self.httpd_netcat.name))
75-
return runqemu(self.IMAGE_PN,
76-
discard_writes=False, ssh=False,
77-
overrides=overrides,
78-
runqemuparams='ovmf slirp nographic',
79-
image_fstype='wic')
80-
81-
def update_image(self, qemu):
82-
# We need to bring up some simple HTTP server for the
83-
# update repo.
84-
server = None
85-
self.http_log = []
86-
http_log = self.http_log
87152
try:
88-
class HTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
89-
parent = self
90-
request_counter = 0
91-
def log_message(self, format, *args):
92-
msg = format % args
93-
self.parent.logger.info(msg)
94-
self.parent.http_log.append(msg)
95-
96-
def translate_path(self, path):
97-
"""
98-
Return absolute path based on document root instead of current directory.
99-
"""
100-
101-
# The original implementation returns an absolute path rooted in the
102-
# current directory. We need to serve a different
103-
# directory without being able to chdir(), because
104-
# doing that would cause commands like bitbake to
105-
# run there, which is undesirable because for
106-
# example bitbake creates a bitbake-cookerdaemon.log
107-
# in the current directory.
108-
path = super().translate_path(path)
109-
relpath = os.path.relpath(path)
110-
path = os.path.join(self.parent.REPO_DIR, relpath)
111-
return path
112-
113-
def do_GET(self):
114-
"""
115-
Inject errors.
116-
"""
117-
counter = HTTPRequestHandler.request_counter
118-
HTTPRequestHandler.request_counter += 1
119-
stop_at = getattr(self.parent, 'stop_serving_http_at', None)
120-
if stop_at is not None and counter >= stop_at:
121-
self.send_error(500, 'test server is intentionally down')
122-
else:
123-
super().do_GET()
153+
self.httpd_netcat.close()
154+
os.chmod(self.httpd_netcat.name, stat.S_IRUSR|stat.S_IWUSR|stat.S_IXUSR)
155+
qemuboot_conf = os.path.join(self.image_dir_test,
156+
'%s-%s.qemuboot.conf' % (self.IMAGE_PN, self.BB_VARS['MACHINE']))
157+
with open(qemuboot_conf) as f:
158+
conf = f.read()
159+
with open(qemuboot_conf, 'w') as f:
160+
f.write('\n'.join([x for x in conf.splitlines() if not x.startswith('qb_slirp_opt')]))
161+
f.write('\nqb_slirp_opt = -netdev user,id=net0,guestfwd=tcp:%s-cmd:%s\n' % \
162+
(self.HTTPD_SERVER, self.httpd_netcat.name))
163+
with super().boot_image(ssh=False,
164+
runqemuparams='ovmf slirp nographic',
165+
image_fstype='wic') as qemu:
166+
yield qemu
167+
finally:
168+
os.unlink(self.httpd_netcat.name)
124169

125-
handler = HTTPRequestHandler
170+
@contextlib.contextmanager
171+
def start_httpd(self):
172+
"""
173+
Bring up the HTTP server when entering the context and shut it down when done.
174+
"""
126175

127-
def create_httpd():
128-
for port in range(9999, 10000):
129-
try:
130-
server = http.server.HTTPServer(('localhost', port), handler)
131-
return server
132-
except OSError as ex:
133-
if ex.errno != errno.EADDRINUSE:
134-
raise
135-
self.fail('no port available for HTTP server')
176+
# netcat can't be assumed to be present. Build and use socat instead.
177+
# It's a bit more complicated but has the advantage that it is in OE-core.
178+
socat = os.path.join(self.BB_VARS['RECIPE_SYSROOT_NATIVE'], 'usr', 'bin', 'socat')
179+
if not os.path.exists(socat):
180+
bitbake('socat-native:do_addto_recipe_sysroot', output_log=self.logger)
181+
self.assertExists(socat, 'socat-native was not built as expected')
136182

137-
server = create_httpd()
138-
port = server.server_port
139-
self.logger.info('serving repo %s on port %d' % (self.REPO_DIR, port))
140-
helper = threading.Thread(name='HTTPD', target=server.serve_forever)
141-
helper.start()
142-
# netcat can't be assumed to be present. Build and use socat instead.
143-
# It's a bit more complicated but has the advantage that it is in OE-core.
144-
socat = os.path.join(self.BB_VARS['RECIPE_SYSROOT_NATIVE'], 'usr', 'bin', 'socat')
145-
if not os.path.exists(socat):
146-
bitbake('socat-native:do_addto_recipe_sysroot', output_log=self.logger)
147-
self.assertExists(socat, 'socat-native was not built as expected')
183+
with HTTPServer(self.REPO_DIR, self.logger) as httpd:
148184
with open(self.httpd_netcat.name, 'w') as f:
149185
f.write('''#!/bin/sh
150186
exec %s 2>/tmp/httpd.log -D -v -d -d -d -d STDIO TCP:localhost:%d
151-
''' % (socat, port))
187+
''' % (socat, httpd.port))
188+
yield httpd
152189

190+
191+
def update_image(self, qemu):
192+
# We need to bring up some simple HTTP server for the
193+
# update repo.
194+
with self.start_httpd() as self.httpd:
153195
# Now run the real update command inside the virtual machine.
154196
return self.update_image_via_http(qemu)
155197

156-
finally:
157-
if server:
158-
server.shutdown()
159-
server.server_close()
160-
161198
def update_image_via_http(self, qemu):
162199
"""
163200
Called by update_image() with the HTTPD server running.
164201
"""
165202
return False
203+

meta-refkit-core/lib/oeqa/selftest/systemupdate/systemupdatebase.py

Lines changed: 55 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import oe.path
88

99
import base64
10+
import contextlib
1011
import fnmatch
1112
import pathlib
1213
import pickle
@@ -210,13 +211,20 @@ class SystemUpdateBase(OESelftestTestCase):
210211
# Expected to be replaced by derived class.
211212
IMAGE_MODIFY = SystemUpdateModify()
212213

213-
def boot_image(self, overrides):
214+
def boot_image(self, overrides = {}, **kwargs):
214215
"""
215216
Calls runqemu() such that commands can be started via run_serial().
216217
Derived classes need to replace with something that adds whatever
217218
other parameters are needed or useful.
218219
"""
219-
return runqemu(self.IMAGE_PN, discard_writes=False, overrides=overrides)
220+
# Change DEPLOY_DIR_IMAGE so that we use our copy of the
221+
# images from before the update. Further customizations for booting can
222+
# be done by rewriting self.image_dir_test/IMAGE_PN-MACHINE.qemuboot.conf
223+
# (read, close, write, not just appending as that would also change
224+
# the file copy under image_dir).
225+
overrides = overrides.copy()
226+
overrides['DEPLOY_DIR_IMAGE'] = self.image_dir_test
227+
return runqemu(self.IMAGE_PN, discard_writes=False, overrides=overrides, **kwargs)
220228

221229
def update_image(self, qemu):
222230
"""
@@ -241,28 +249,18 @@ def modify_image_build(self, testname, updates, is_update):
241249
bbappend.append('APPEND_append = " modify_kernel_test=%s"' % ('updated' if is_update else 'original'))
242250
return '\n'.join(bbappend)
243251

244-
def do_update(self, testname, updates):
252+
def create_image_bbappend(self, testname, updates, is_update):
245253
"""
246-
Builds the image, makes a copy of the result, rebuilds to produce
247-
an update with configurable changes, boots the original image, updates it,
248-
reboots and then checks the updated image.
249-
250-
'update' is a list of modify_* function names which make the actual changes
251-
(adding, removing, modifying files or kernel) that are part of the tests.
254+
Creates an IMAGE_BBAPPEND which contains the pickled modification code.
255+
A .bbappend is used because it can contain code and is guaranteed to be
256+
applied also to image variants.
252257
"""
253258

254-
def create_image_bbappend(is_update):
255-
"""
256-
Creates an IMAGE_BBAPPEND which contains the pickled modification code.
257-
A .bbappend is used because it can contain code and is guaranteed to be
258-
applied also to image variants.
259-
"""
260-
261-
bbappend = self.IMAGE_BBAPPEND_UPDATE if is_update else self.IMAGE_BBAPPEND
262-
self.append_config('BBFILES_append = " %s"' % os.path.abspath(bbappend))
263-
self.track_for_cleanup(bbappend)
264-
with open(bbappend, 'w') as f:
265-
f.write('''
259+
bbappend = self.IMAGE_BBAPPEND_UPDATE if is_update else self.IMAGE_BBAPPEND
260+
self.append_config('BBFILES_append = " %s"' % os.path.abspath(bbappend))
261+
self.track_for_cleanup(bbappend)
262+
with open(bbappend, 'w') as f:
263+
f.write('''
266264
python system_update_test_modify () {
267265
import base64
268266
import pickle
@@ -283,9 +281,14 @@ def create_image_bbappend(is_update):
283281
self.IMAGE_CONFIG,
284282
self.modify_image_build(testname, updates, is_update)))
285283

284+
def prepare_image(self, testname, updates):
285+
"""
286+
Builds the initial image and prepares it for booting.
287+
"""
288+
286289
# Creating a .bbappend for the image will trigger a rebuild.
287290
# To avoid this, use separate image recipes.
288-
create_image_bbappend(False)
291+
self.create_image_bbappend(testname, updates, False)
289292
self.logger.info('Building base image')
290293
result = bitbake(self.IMAGE_PN, output_log=self.logger)
291294

@@ -303,28 +306,44 @@ def create_image_bbappend(is_update):
303306
# when the image recipes are different.
304307
self.clone_files(self.image_dir, ('ovmf*', vars['IMAGE_LINK_NAME'] + '*'))
305308

306-
# Change DEPLOY_DIR_IMAGE so that we use our copy of the
307-
# images from before the update. Further customizations for booting can
308-
# be done by rewriting self.image_dir_test/IMAGE_PN-MACHINE.qemuboot.conf
309-
# (read, close, write, not just appending as that would also change
310-
# the file copy under image_dir).
311-
overrides = { 'DEPLOY_DIR_IMAGE': self.image_dir_test }
312-
309+
@contextlib.contextmanager
310+
def boot_and_verify_image(self, testname, updates):
311+
"""
312+
Boots the initial image and verifies its content.
313+
To be used as:
314+
with boot_initial_image() as qemu:
315+
... run additional checks in qemu ...
316+
"""
313317
# Boot image, verify before and after update.
314-
with self.boot_image(overrides) as qemu:
318+
with self.boot_image() as qemu:
315319
self.verify_image(testname, False, qemu, updates)
320+
yield qemu
316321

317-
# Now we change our .bbappend so that the updated state is generated
318-
# during the next rebuild.
319-
create_image_bbappend(True)
320-
self.logger.info('Building updated image')
321-
bitbake(self.IMAGE_PN_UPDATE, output_log=self.logger)
322+
def prepare_update(self, testname, updates):
323+
"""
324+
Build the update image.
325+
"""
326+
self.create_image_bbappend(testname, updates, True)
327+
self.logger.info('Building updated image')
328+
bitbake(self.IMAGE_PN_UPDATE, output_log=self.logger)
322329

330+
def do_update(self, testname, updates):
331+
"""
332+
Builds the image, makes a copy of the result, rebuilds to produce
333+
an update with configurable changes, boots the original image, updates it,
334+
reboots and then checks the updated image.
335+
336+
'update' is a list of modify_* function names which make the actual changes
337+
(adding, removing, modifying files or kernel) that are part of the tests.
338+
"""
339+
self.prepare_image(testname, updates)
340+
with self.boot_and_verify_image(testname, updates) as qemu:
341+
self.prepare_update(testname, updates)
323342
reboot = self.update_image(qemu)
324343
if not reboot:
325344
self.verify_image(testname, True, qemu, updates)
326345
if reboot:
327-
with self.boot_image(overrides) as qemu:
346+
with self.boot_image() as qemu:
328347
self.verify_image(testname, True, qemu, updates)
329348

330349
def clone_files(self, dirname, file_patterns):

0 commit comments

Comments
 (0)