summaryrefslogtreecommitdiffstats
path: root/lib/oeqa/selftest/updater.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/oeqa/selftest/updater.py')
-rw-r--r--lib/oeqa/selftest/updater.py261
1 files changed, 192 insertions, 69 deletions
diff --git a/lib/oeqa/selftest/updater.py b/lib/oeqa/selftest/updater.py
index 8fbc857..c114456 100644
--- a/lib/oeqa/selftest/updater.py
+++ b/lib/oeqa/selftest/updater.py
@@ -15,6 +15,7 @@ class SotaToolsTests(oeSelfTest):
15 15
16 @classmethod 16 @classmethod
17 def setUpClass(cls): 17 def setUpClass(cls):
18 super(SotaToolsTests, cls).setUpClass()
18 logger = logging.getLogger("selftest") 19 logger = logging.getLogger("selftest")
19 logger.info('Running bitbake to build aktualizr-native tools') 20 logger.info('Running bitbake to build aktualizr-native tools')
20 bitbake('aktualizr-native') 21 bitbake('aktualizr-native')
@@ -63,7 +64,6 @@ class GeneralTests(oeSelfTest):
63 "Java not found. Do you have a JDK installed on your host machine?") 64 "Java not found. Do you have a JDK installed on your host machine?")
64 65
65 def test_add_package(self): 66 def test_add_package(self):
66 print('')
67 deploydir = get_bb_var('DEPLOY_DIR_IMAGE') 67 deploydir = get_bb_var('DEPLOY_DIR_IMAGE')
68 imagename = get_bb_var('IMAGE_LINK_NAME', 'core-image-minimal') 68 imagename = get_bb_var('IMAGE_LINK_NAME', 'core-image-minimal')
69 image_path = deploydir + '/' + imagename + '.otaimg' 69 image_path = deploydir + '/' + imagename + '.otaimg'
@@ -97,6 +97,7 @@ class AktualizrToolsTests(oeSelfTest):
97 97
98 @classmethod 98 @classmethod
99 def setUpClass(cls): 99 def setUpClass(cls):
100 super(AktualizrToolsTests, cls).setUpClass()
100 logger = logging.getLogger("selftest") 101 logger = logging.getLogger("selftest")
101 logger.info('Running bitbake to build aktualizr-native tools') 102 logger.info('Running bitbake to build aktualizr-native tools')
102 bitbake('aktualizr-native') 103 bitbake('aktualizr-native')
@@ -132,20 +133,34 @@ class AktualizrToolsTests(oeSelfTest):
132 self.assertTrue(os.path.getsize(ca_path) > 0, "Client certificate at %s is empty." % ca_path) 133 self.assertTrue(os.path.getsize(ca_path) > 0, "Client certificate at %s is empty." % ca_path)
133 134
134 135
135class QemuTests(oeSelfTest): 136class AutoProvTests(oeSelfTest):
136 137
137 @classmethod 138 def setUpLocal(self):
138 def setUpClass(cls): 139 layer = "meta-updater-qemux86-64"
139 cls.qemu, cls.s = qemu_launch(machine='qemux86-64') 140 result = runCmd('bitbake-layers show-layers')
141 if re.search(layer, result.output) is None:
142 # Assume the directory layout for finding other layers. We could also
143 # make assumptions by using 'show-layers', but either way, if the
144 # layers we need aren't where we expect them, we are out of like.
145 path = os.path.abspath(os.path.dirname(__file__))
146 metadir = path + "/../../../../"
147 self.meta_qemu = metadir + layer
148 runCmd('bitbake-layers add-layer "%s"' % self.meta_qemu)
149 else:
150 self.meta_qemu = None
151 self.append_config('MACHINE = "qemux86-64"')
152 self.append_config('SOTA_CLIENT_PROV = " aktualizr-auto-prov "')
153 self.qemu, self.s = qemu_launch(machine='qemux86-64')
140 154
141 @classmethod 155 def tearDownLocal(self):
142 def tearDownClass(cls): 156 qemu_terminate(self.s)
143 qemu_terminate(cls.s) 157 if self.meta_qemu:
158 runCmd('bitbake-layers remove-layer "%s"' % self.meta_qemu, ignore_status=True)
144 159
145 def qemu_command(self, command): 160 def qemu_command(self, command):
146 return qemu_send_command(self.qemu.ssh_port, command) 161 return qemu_send_command(self.qemu.ssh_port, command)
147 162
148 def test_qemu(self): 163 def test_provisioning(self):
149 print('Checking machine name (hostname) of device:') 164 print('Checking machine name (hostname) of device:')
150 stdout, stderr, retcode = self.qemu_command('hostname') 165 stdout, stderr, retcode = self.qemu_command('hostname')
151 self.assertEqual(retcode, 0, "Unable to check hostname. " + 166 self.assertEqual(retcode, 0, "Unable to check hostname. " +
@@ -153,10 +168,10 @@ class QemuTests(oeSelfTest):
153 machine = get_bb_var('MACHINE', 'core-image-minimal') 168 machine = get_bb_var('MACHINE', 'core-image-minimal')
154 self.assertEqual(stderr, b'', 'Error: ' + stderr.decode()) 169 self.assertEqual(stderr, b'', 'Error: ' + stderr.decode())
155 # Strip off line ending. 170 # Strip off line ending.
156 value_str = stdout.decode()[:-1] 171 value = stdout.decode()[:-1]
157 self.assertEqual(value_str, machine, 172 self.assertEqual(value, machine,
158 'MACHINE does not match hostname: ' + machine + ', ' + value_str) 173 'MACHINE does not match hostname: ' + machine + ', ' + value)
159 print(value_str) 174 print(value)
160 print('Checking output of aktualizr-info:') 175 print('Checking output of aktualizr-info:')
161 ran_ok = False 176 ran_ok = False
162 for delay in [0, 1, 2, 5, 10, 15]: 177 for delay in [0, 1, 2, 5, 10, 15]:
@@ -167,31 +182,122 @@ class QemuTests(oeSelfTest):
167 break 182 break
168 self.assertTrue(ran_ok, 'aktualizr-info failed: ' + stderr.decode() + stdout.decode()) 183 self.assertTrue(ran_ok, 'aktualizr-info failed: ' + stderr.decode() + stdout.decode())
169 184
185 verifyProvisioned(self, machine)
186
187
188class RpiTests(oeSelfTest):
189
190 def setUpLocal(self):
191 # Add layers before changing the machine type, otherwise the sanity
192 # checker complains loudly.
193 layer_python = "meta-openembedded/meta-python"
194 layer_rpi = "meta-raspberrypi"
195 layer_upd_rpi = "meta-updater-raspberrypi"
196 result = runCmd('bitbake-layers show-layers')
197 # Assume the directory layout for finding other layers. We could also
198 # make assumptions by using 'show-layers', but either way, if the
199 # layers we need aren't where we expect them, we are out of like.
200 path = os.path.abspath(os.path.dirname(__file__))
201 metadir = path + "/../../../../"
202 if re.search(layer_python, result.output) is None:
203 self.meta_python = metadir + layer_python
204 runCmd('bitbake-layers add-layer "%s"' % self.meta_python)
205 else:
206 self.meta_python = None
207 if re.search(layer_rpi, result.output) is None:
208 self.meta_rpi = metadir + layer_rpi
209 runCmd('bitbake-layers add-layer "%s"' % self.meta_rpi)
210 else:
211 self.meta_rpi = None
212 if re.search(layer_upd_rpi, result.output) is None:
213 self.meta_upd_rpi = metadir + layer_upd_rpi
214 runCmd('bitbake-layers add-layer "%s"' % self.meta_upd_rpi)
215 else:
216 self.meta_upd_rpi = None
217
218 # This is trickier that I would've thought. The fundamental problem is
219 # that the qemu layer changes the u-boot file extension to .rom, but
220 # raspberrypi still expects .bin. To prevent this, the qemu layer must
221 # be temporarily removed if it is present. It has to be removed by name
222 # without the complete path, but to add it back when we are done, we
223 # need the full path.
224 p = re.compile(r'meta-updater-qemux86-64\s*(\S*meta-updater-qemux86-64)\s')
225 m = p.search(result.output)
226 if m and m.lastindex > 0:
227 self.meta_qemu = m.group(1)
228 runCmd('bitbake-layers remove-layer meta-updater-qemux86-64')
229 else:
230 self.meta_qemu = None
231
232 self.append_config('MACHINE = "raspberrypi3"')
233 self.append_config('SOTA_CLIENT_PROV = " aktualizr-auto-prov "')
234
235 def tearDownLocal(self):
236 if self.meta_qemu:
237 runCmd('bitbake-layers add-layer "%s"' % self.meta_qemu, ignore_status=True)
238 if self.meta_upd_rpi:
239 runCmd('bitbake-layers remove-layer "%s"' % self.meta_upd_rpi, ignore_status=True)
240 if self.meta_rpi:
241 runCmd('bitbake-layers remove-layer "%s"' % self.meta_rpi, ignore_status=True)
242 if self.meta_python:
243 runCmd('bitbake-layers remove-layer "%s"' % self.meta_python, ignore_status=True)
244
245 def test_rpi(self):
246 logger = logging.getLogger("selftest")
247 logger.info('Running bitbake to build rpi-basic-image')
248 self.append_config('SOTA_CLIENT_PROV = "aktualizr-auto-prov"')
249 bitbake('rpi-basic-image')
250 credentials = get_bb_var('SOTA_PACKED_CREDENTIALS')
251 # Skip the test if the variable SOTA_PACKED_CREDENTIALS is not set.
252 if credentials is None:
253 raise unittest.SkipTest("Variable 'SOTA_PACKED_CREDENTIALS' not set.")
254 # Check if the file exists.
255 self.assertTrue(os.path.isfile(credentials), "File %s does not exist" % credentials)
256 deploydir = get_bb_var('DEPLOY_DIR_IMAGE')
257 imagename = get_bb_var('IMAGE_LINK_NAME', 'rpi-basic-image')
258 # Check if the credentials are included in the output image.
259 result = runCmd('tar -jtvf %s/%s.tar.bz2 | grep sota_provisioning_credentials.zip' %
260 (deploydir, imagename), ignore_status=True)
261 self.assertEqual(result.status, 0, "Status not equal to 0. output: %s" % result.output)
262
170 263
171class GrubTests(oeSelfTest): 264class GrubTests(oeSelfTest):
172 265
173 def setUpLocal(self): 266 def setUpLocal(self):
174 # This is a bit of a hack but I can't see a better option. 267 layer_intel = "meta-intel"
268 layer_minnow = "meta-updater-minnowboard"
269 result = runCmd('bitbake-layers show-layers')
270 # Assume the directory layout for finding other layers. We could also
271 # make assumptions by using 'show-layers', but either way, if the
272 # layers we need aren't where we expect them, we are out of like.
175 path = os.path.abspath(os.path.dirname(__file__)) 273 path = os.path.abspath(os.path.dirname(__file__))
176 metadir = path + "/../../../../" 274 metadir = path + "/../../../../"
177 grub_config = 'OSTREE_BOOTLOADER = "grub"\nMACHINE = "intel-corei7-64"' 275 if re.search(layer_intel, result.output) is None:
178 self.append_config(grub_config) 276 self.meta_intel = metadir + layer_intel
179 self.meta_intel = metadir + "meta-intel" 277 runCmd('bitbake-layers add-layer "%s"' % self.meta_intel)
180 self.meta_minnow = metadir + "meta-updater-minnowboard" 278 else:
181 runCmd('bitbake-layers add-layer "%s"' % self.meta_intel) 279 self.meta_intel = None
182 runCmd('bitbake-layers add-layer "%s"' % self.meta_minnow) 280 if re.search(layer_minnow, result.output) is None:
281 self.meta_minnow = metadir + layer_minnow
282 runCmd('bitbake-layers add-layer "%s"' % self.meta_minnow)
283 else:
284 self.meta_minnow = None
285 self.append_config('MACHINE = "intel-corei7-64"')
286 self.append_config('OSTREE_BOOTLOADER = "grub"')
287 self.append_config('SOTA_CLIENT_PROV = " aktualizr-auto-prov "')
183 self.qemu, self.s = qemu_launch(efi=True, machine='intel-corei7-64') 288 self.qemu, self.s = qemu_launch(efi=True, machine='intel-corei7-64')
184 289
185 def tearDownLocal(self): 290 def tearDownLocal(self):
186 qemu_terminate(self.s) 291 qemu_terminate(self.s)
187 runCmd('bitbake-layers remove-layer "%s"' % self.meta_intel, ignore_status=True) 292 if self.meta_intel:
188 runCmd('bitbake-layers remove-layer "%s"' % self.meta_minnow, ignore_status=True) 293 runCmd('bitbake-layers remove-layer "%s"' % self.meta_intel, ignore_status=True)
294 if self.meta_minnow:
295 runCmd('bitbake-layers remove-layer "%s"' % self.meta_minnow, ignore_status=True)
189 296
190 def qemu_command(self, command): 297 def qemu_command(self, command):
191 return qemu_send_command(self.qemu.ssh_port, command) 298 return qemu_send_command(self.qemu.ssh_port, command)
192 299
193 def test_grub(self): 300 def test_grub(self):
194 print('')
195 print('Checking machine name (hostname) of device:') 301 print('Checking machine name (hostname) of device:')
196 stdout, stderr, retcode = self.qemu_command('hostname') 302 stdout, stderr, retcode = self.qemu_command('hostname')
197 self.assertEqual(retcode, 0, "Unable to check hostname. " + 303 self.assertEqual(retcode, 0, "Unable to check hostname. " +
@@ -214,16 +320,32 @@ class GrubTests(oeSelfTest):
214 break 320 break
215 self.assertTrue(ran_ok, 'aktualizr-info failed: ' + stderr.decode() + stdout.decode()) 321 self.assertTrue(ran_ok, 'aktualizr-info failed: ' + stderr.decode() + stdout.decode())
216 322
323 verifyProvisioned(self, machine)
324
217 325
218class ImplProvTests(oeSelfTest): 326class ImplProvTests(oeSelfTest):
219 327
220 def setUpLocal(self): 328 def setUpLocal(self):
329 layer = "meta-updater-qemux86-64"
330 result = runCmd('bitbake-layers show-layers')
331 if re.search(layer, result.output) is None:
332 # Assume the directory layout for finding other layers. We could also
333 # make assumptions by using 'show-layers', but either way, if the
334 # layers we need aren't where we expect them, we are out of like.
335 path = os.path.abspath(os.path.dirname(__file__))
336 metadir = path + "/../../../../"
337 self.meta_qemu = metadir + layer
338 runCmd('bitbake-layers add-layer "%s"' % self.meta_qemu)
339 else:
340 self.meta_qemu = None
341 self.append_config('MACHINE = "qemux86-64"')
221 self.append_config('SOTA_CLIENT_PROV = " aktualizr-implicit-prov "') 342 self.append_config('SOTA_CLIENT_PROV = " aktualizr-implicit-prov "')
222 # note: this will build aktualizr-native as a side-effect
223 self.qemu, self.s = qemu_launch(machine='qemux86-64') 343 self.qemu, self.s = qemu_launch(machine='qemux86-64')
224 344
225 def tearDownLocal(self): 345 def tearDownLocal(self):
226 qemu_terminate(self.s) 346 qemu_terminate(self.s)
347 if self.meta_qemu:
348 runCmd('bitbake-layers remove-layer "%s"' % self.meta_qemu, ignore_status=True)
227 349
228 def qemu_command(self, command): 350 def qemu_command(self, command):
229 return qemu_send_command(self.qemu.ssh_port, command) 351 return qemu_send_command(self.qemu.ssh_port, command)
@@ -236,10 +358,10 @@ class ImplProvTests(oeSelfTest):
236 machine = get_bb_var('MACHINE', 'core-image-minimal') 358 machine = get_bb_var('MACHINE', 'core-image-minimal')
237 self.assertEqual(stderr, b'', 'Error: ' + stderr.decode()) 359 self.assertEqual(stderr, b'', 'Error: ' + stderr.decode())
238 # Strip off line ending. 360 # Strip off line ending.
239 value_str = stdout.decode()[:-1] 361 value = stdout.decode()[:-1]
240 self.assertEqual(value_str, machine, 362 self.assertEqual(value, machine,
241 'MACHINE does not match hostname: ' + machine + ', ' + value_str) 363 'MACHINE does not match hostname: ' + machine + ', ' + value)
242 print(value_str) 364 print(value)
243 print('Checking output of aktualizr-info:') 365 print('Checking output of aktualizr-info:')
244 ran_ok = False 366 ran_ok = False
245 for delay in [0, 1, 2, 5, 10, 15]: 367 for delay in [0, 1, 2, 5, 10, 15]:
@@ -267,36 +389,33 @@ class ImplProvTests(oeSelfTest):
267 akt_native_run(self, 'aktualizr_cert_provider -c {creds} -t root@localhost -p {port} -s -g {config}' 389 akt_native_run(self, 'aktualizr_cert_provider -c {creds} -t root@localhost -p {port} -s -g {config}'
268 .format(creds=creds, port=self.qemu.ssh_port, config=config)) 390 .format(creds=creds, port=self.qemu.ssh_port, config=config))
269 391
270 # Verify that device HAS provisioned. 392 verifyProvisioned(self, machine)
271 ran_ok = False
272 for delay in [5, 5, 5, 5, 10]:
273 sleep(delay)
274 stdout, stderr, retcode = self.qemu_command('aktualizr-info')
275 if retcode == 0 and stderr == b'' and stdout.decode().find('Fetched metadata: yes') >= 0:
276 ran_ok = True
277 break
278 self.assertIn(b'Device ID: ', stdout, 'Provisioning failed: ' + stderr.decode() + stdout.decode())
279 self.assertIn(b'Primary ecu hardware ID: qemux86-64', stdout,
280 'Provisioning failed: ' + stderr.decode() + stdout.decode())
281 self.assertIn(b'Fetched metadata: yes', stdout, 'Provisioning failed: ' + stderr.decode() + stdout.decode())
282 p = re.compile(r'Device ID: ([a-z0-9-]*)\n')
283 m = p.search(stdout.decode())
284 self.assertTrue(m, 'Device ID could not be read: ' + stderr.decode() + stdout.decode())
285 self.assertGreater(m.lastindex, 0, 'Device ID could not be read: ' + stderr.decode() + stdout.decode())
286 logger = logging.getLogger("selftest")
287 logger.info('Device successfully provisioned with ID: ' + m.group(1))
288 393
289 394
290class HsmTests(oeSelfTest): 395class HsmTests(oeSelfTest):
291 396
292 def setUpLocal(self): 397 def setUpLocal(self):
398 layer = "meta-updater-qemux86-64"
399 result = runCmd('bitbake-layers show-layers')
400 if re.search(layer, result.output) is None:
401 # Assume the directory layout for finding other layers. We could also
402 # make assumptions by using 'show-layers', but either way, if the
403 # layers we need aren't where we expect them, we are out of like.
404 path = os.path.abspath(os.path.dirname(__file__))
405 metadir = path + "/../../../../"
406 self.meta_qemu = metadir + layer
407 runCmd('bitbake-layers add-layer "%s"' % self.meta_qemu)
408 else:
409 self.meta_qemu = None
410 self.append_config('MACHINE = "qemux86-64"')
293 self.append_config('SOTA_CLIENT_PROV = "aktualizr-hsm-prov"') 411 self.append_config('SOTA_CLIENT_PROV = "aktualizr-hsm-prov"')
294 self.append_config('SOTA_CLIENT_FEATURES = "hsm"') 412 self.append_config('SOTA_CLIENT_FEATURES = "hsm"')
295 # note: this will build aktualizr-native as a side-effect
296 self.qemu, self.s = qemu_launch(machine='qemux86-64') 413 self.qemu, self.s = qemu_launch(machine='qemux86-64')
297 414
298 def tearDownLocal(self): 415 def tearDownLocal(self):
299 qemu_terminate(self.s) 416 qemu_terminate(self.s)
417 if self.meta_qemu:
418 runCmd('bitbake-layers remove-layer "%s"' % self.meta_qemu, ignore_status=True)
300 419
301 def qemu_command(self, command): 420 def qemu_command(self, command):
302 return qemu_send_command(self.qemu.ssh_port, command) 421 return qemu_send_command(self.qemu.ssh_port, command)
@@ -309,10 +428,11 @@ class HsmTests(oeSelfTest):
309 machine = get_bb_var('MACHINE', 'core-image-minimal') 428 machine = get_bb_var('MACHINE', 'core-image-minimal')
310 self.assertEqual(stderr, b'', 'Error: ' + stderr.decode()) 429 self.assertEqual(stderr, b'', 'Error: ' + stderr.decode())
311 # Strip off line ending. 430 # Strip off line ending.
312 value_str = stdout.decode()[:-1] 431 value = stdout.decode()[:-1]
313 self.assertEqual(value_str, machine, 432 self.assertEqual(value, machine,
314 'MACHINE does not match hostname: ' + machine + ', ' + value_str) 433 'MACHINE does not match hostname: ' + machine + ', ' + value +
315 print(value_str) 434 '\nIs tianocore ovmf installed?')
435 print(value)
316 print('Checking output of aktualizr-info:') 436 print('Checking output of aktualizr-info:')
317 ran_ok = False 437 ran_ok = False
318 for delay in [0, 1, 2, 5, 10, 15]: 438 for delay in [0, 1, 2, 5, 10, 15]:
@@ -382,24 +502,7 @@ class HsmTests(oeSelfTest):
382 self.assertEqual(p11_m.group(1), hsm_m.group(1), 'Slot number does not match: ' + 502 self.assertEqual(p11_m.group(1), hsm_m.group(1), 'Slot number does not match: ' +
383 p11_err.decode() + p11_out.decode() + hsm_err.decode() + hsm_out.decode()) 503 p11_err.decode() + p11_out.decode() + hsm_err.decode() + hsm_out.decode())
384 504
385 # Verify that device HAS provisioned. 505 verifyProvisioned(self, machine)
386 ran_ok = False
387 for delay in [5, 5, 5, 5, 10]:
388 sleep(delay)
389 stdout, stderr, retcode = self.qemu_command('aktualizr-info')
390 if retcode == 0 and stderr == b'' and stdout.decode().find('Fetched metadata: yes') >= 0:
391 ran_ok = True
392 break
393 self.assertIn(b'Device ID: ', stdout, 'Provisioning failed: ' + stderr.decode() + stdout.decode())
394 self.assertIn(b'Primary ecu hardware ID: qemux86-64', stdout,
395 'Provisioning failed: ' + stderr.decode() + stdout.decode())
396 self.assertIn(b'Fetched metadata: yes', stdout, 'Provisioning failed: ' + stderr.decode() + stdout.decode())
397 p = re.compile(r'Device ID: ([a-z0-9-]*)\n')
398 m = p.search(stdout.decode())
399 self.assertTrue(m, 'Device ID could not be read: ' + stderr.decode() + stdout.decode())
400 self.assertGreater(m.lastindex, 0, 'Device ID could not be read: ' + stderr.decode() + stdout.decode())
401 logger = logging.getLogger("selftest")
402 logger.info('Device successfully provisioned with ID: ' + m.group(1))
403 506
404 507
405def qemu_launch(efi=False, machine=None): 508def qemu_launch(efi=False, machine=None):
@@ -466,5 +569,25 @@ def akt_native_run(testInst, cmd, **kwargs):
466 testInst.assertEqual(result.status, 0, "Status not equal to 0. output: %s" % result.output) 569 testInst.assertEqual(result.status, 0, "Status not equal to 0. output: %s" % result.output)
467 570
468 571
572def verifyProvisioned(testInst, machine):
573 # Verify that device HAS provisioned.
574 ran_ok = False
575 for delay in [5, 5, 5, 5, 10]:
576 sleep(delay)
577 stdout, stderr, retcode = testInst.qemu_command('aktualizr-info')
578 if retcode == 0 and stderr == b'' and stdout.decode().find('Fetched metadata: yes') >= 0:
579 ran_ok = True
580 break
581 testInst.assertIn(b'Device ID: ', stdout, 'Provisioning failed: ' + stderr.decode() + stdout.decode())
582 testInst.assertIn(b'Primary ecu hardware ID: ' + machine.encode(), stdout,
583 'Provisioning failed: ' + stderr.decode() + stdout.decode())
584 testInst.assertIn(b'Fetched metadata: yes', stdout, 'Provisioning failed: ' + stderr.decode() + stdout.decode())
585 p = re.compile(r'Device ID: ([a-z0-9-]*)\n')
586 m = p.search(stdout.decode())
587 testInst.assertTrue(m, 'Device ID could not be read: ' + stderr.decode() + stdout.decode())
588 testInst.assertGreater(m.lastindex, 0, 'Device ID could not be read: ' + stderr.decode() + stdout.decode())
589 logger = logging.getLogger("selftest")
590 logger.info('Device successfully provisioned with ID: ' + m.group(1))
591
469 592
470# vim:set ts=4 sw=4 sts=4 expandtab: 593# vim:set ts=4 sw=4 sts=4 expandtab: