Revision bc14ba88 snf-tools/test_suite.py

b/snf-tools/test_suite.py
42 42
import paramiko
43 43
import subprocess
44 44
import socket
45
import struct
45 46
import sys
46 47
import time
47 48

  
48
from random import choice
49 49
from IPy import IP
50
from random import choice
50 51
from kamaki.client import Client, ClientError
52
from vncauthproxy.d3des import generate_response as d3des_generate_response
51 53

  
52 54
# Use backported unittest functionality if Python < 2.7
53 55
try:
54 56
    import unittest2 as unittest
55 57
except ImportError:
58
    if sys.version_info < (2, 7):
59
        raise Exception("The unittest2 package is required for Python < 2.7")
56 60
    import unittest
57 61

  
58 62

  
......
187 191
    @classmethod
188 192
    def setUpClass(cls):
189 193
        """Initialize a kamaki instance"""
190
        log.info("Spawning server for image `%s'", self.imagename)
194
        log.info("Spawning server for image `%s'", cls.imagename)
191 195
        cls.client = Client(API, TOKEN)
192 196

  
193 197
    def _get_ipv4(self, server):
198
        """Get the public IPv4 of a server from the detailed server info"""
194 199
        public_addrs = filter(lambda x: x["id"] == "public",
195 200
                              server["addresses"]["values"])
196 201
        self.assertEqual(len(public_addrs), 1)
......
200 205
        return ipv4_addrs[0]["addr"]
201 206

  
202 207
    def _get_ipv6(self, server):
208
        """Get the public IPv6 of a server from the detailed server info"""
203 209
        public_addrs = filter(lambda x: x["id"] == "public",
204 210
                              server["addresses"]["values"])
205 211
        self.assertEqual(len(public_addrs), 1)
......
208 214
        self.assertEqual(len(ipv6_addrs), 1)
209 215
        return ipv6_addrs[0]["addr"]
210 216

  
211
    def _get_tcp_connection(family, host, port):
212
        tmout = time.time() + self.action_timeout
213
        while True:
214
            self.assertLess(time.time(), tmout,
215
                "Timed out trying to to %s:%s" % (host, port))
216
            sock = None
217
            for res in \
218
                socket.getaddrinfo(host, port, family, socket.SOCK_STREAM, 0,
219
                                   socket.AI_PASSIVE):
220
                af, socktype, proto, canonname, sa = res
221
                try:
222
                    sock = socket.socket(af, socktype, proto)
223
                except socket.error as msg:
224
                    sock = None
225
                    continue
226
                try:
227
                    sock.connect(sa)
228
                except socket.error as msg:
229
                    sock.close()
230
                    sock = None
231
                    continue
232
                self.assertIsNotNone(sock)
233
                return sock
234
            time.sleep(self.query_interval)
217
    def _connect_loginname(self, os):
218
        """Return the login name for connections based on the server OS"""
219
        if os in ('ubuntu', 'kubuntu', 'fedora'):
220
            return 'user'
221
        elif os == 'windows':
222
            return 'Administrator'
223
        else:
224
            return 'root'
225

  
226
    def _verify_server_status(self, current_status, new_status):
227
        """Verify a server has switched to a specified status"""
228
        log.info("Getting status for server %d, Image %s",
229
                 self.serverid, self.imagename)
230
        server = self.client.get_server_details(self.serverid)
231
        self.assertIn(server["status"], (current_status, new_status))
232
        self.assertEquals(server["status"], new_status)
233

  
234
    def _get_connected_tcp_socket(self, family, host, port):
235
        """Get a connected socket from the specified family to host:port"""
236
        sock = None
237
        for res in \
238
            socket.getaddrinfo(host, port, family, socket.SOCK_STREAM, 0,
239
                               socket.AI_PASSIVE):
240
            af, socktype, proto, canonname, sa = res
241
            try:
242
                sock = socket.socket(af, socktype, proto)
243
            except socket.error as msg:
244
                sock = None
245
                continue
246
            try:
247
                sock.connect(sa)
248
            except socket.error as msg:
249
                sock.close()
250
                sock = None
251
                continue
252
        self.assertIsNotNone(sock)
253
        return sock
254

  
255
    def _ping_once(self, ipv6, ip):
256
        """Test server responds to a single IPv4 or IPv6 ping"""
257
        log.info("PING IPv%s to %s", "6" if ipv6 else "4", ip)
258
        cmd = "ping%s -c 2 -w 3 %s" % ("6" if ipv6 else "", ip)
259
        ping = subprocess.Popen(cmd, shell=True,
260
                                stdout=subprocess.PIPE, stderr=subprocess.PIPE)
261
        (stdout, stderr) = ping.communicate()
262
        ret = ping.wait()
263
        self.assertEquals(ret, 0)
235 264

  
236
    def _wait_for_status_transition(self, current_status, new_status,
237
                                    fail_timeout, warn_timeout=None):
238
        if warn_timeout is None:
265
    def _get_hostname_over_ssh(self, hostip, username, password):
266
        ssh = paramiko.SSHClient()
267
        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
268
        try:
269
            ssh.connect(hostip, username=username, password=password)
270
        except socket.error:
271
            raise AssertionError
272
        stdin, stdout, stderr = ssh.exec_command("hostname")
273
        lines = stdout.readlines()
274
        self.assertEqual(len(lines), 1)
275
        return lines
276

  
277
    def _try_until_timeout_expires(self, warn_timeout, fail_timeout,
278
                                   opmsg, callable, *args, **kwargs):
279
        if warn_timeout == fail_timeout:
239 280
            warn_timeout = fail_timeout + 1
240 281
        warn_tmout = time.time() + warn_timeout
241 282
        fail_tmout = time.time() + fail_timeout
242 283
        while True:
243 284
            if time.time() > warn_tmout:
244
                log.warning("Server %d: %s still not %s",
245
                            self.serverid, self.servername, new_status)
246
            self.assertLess(time.time(), fail_tmout)
247
            log.info("Getting status for server %d, Image %s",
248
                     self.serverid, self.imagename)
249
            server = self.client.get_server_details(self.serverid)
250
            self.assertIn(server["status"], (current_status, new_status))
251
            if server["status"] == new_status:
252
                break
285
                log.warning("Server %d: %s operation '%s' not done yet",
286
                            opmsg, self.serverid, self.servername)
287
            try:
288
                return callable(*args, **kwargs)
289
            except AssertionError:
290
                pass
291

  
292
            self.assertLess(time.time(), fail_tmout,
293
                            "operation '%s' timed out" % opmsg)
253 294
            time.sleep(self.query_interval)
254 295

  
255
    def _get_hostname_over_ssh(self, hostip, username, password):
256
        ssh = paramiko.SSHClient()
257
        ssh.set_missing_host_key_policy(paramiko.AutoAdPolicy())
258
        ssh.connect(hostip, username=username, password=password)
259
        stdin, stdout, stderr = ssh.exec_command("hostname")
260
        lines = stdout.readlines()
261
        self.assertEqual(len(lines), 1)
262
        self.assertEqual(lines[0], "notsnf-%s" % self.serverid)
296
    def _insist_on_tcp_connection(self, family, host, port):
297
        familystr = {socket.AF_INET: 'IPv4', socket.AF_INET6: 'IPv6'}
298
        msg = "connect over %s to %s:%s" % \
299
              (familystr.get(family, "Unknown"), host, port)
300
        sock = self._try_until_timeout_expires(
301
                self.action_timeout, self.action_timeout,
302
                msg, self._get_connected_tcp_socket,
303
                family, host, port)
304
        return sock
305

  
306
    def _insist_on_status_transition(self, current_status, new_status,
307
                                    fail_timeout, warn_timeout=None):
308
        msg = "status transition %s -> %s" % (current_status, new_status)
309
        if warn_timeout is None:
310
            warn_timeout = fail_timeout
311
        self._try_until_timeout_expires(warn_timeout, fail_timeout,
312
                                        msg, self._verify_server_status,
313
                                        current_status, new_status)
314

  
315
    def _insist_on_ssh_hostname(self, hostip, username, password):
316
        msg = "ssh to %s, as %s/%s" % (hostip, username, password)
317
        hostname = self._try_until_timeout_expires(
318
                self.action_timeout, self.action_timeout,
319
                msg, self._get_hostname_over_ssh,
320
                hostip, username, password)
321

  
322
        # The hostname must be of the form 'prefix-id'
323
        self.assertTrue(hostname.endswith("-%d\n" % self.serverid))
263 324

  
264 325
    def _skipIf(self, condition, msg):
265 326
        if condition:
......
277 338
        # Update class attributes to reflect data on building server
278 339
        cls = type(self)
279 340
        cls.serverid = server["id"]
341
        cls.username = None
280 342
        cls.passwd = server["adminPass"]
281 343

  
282 344
    def test_002a_server_is_building_in_list(self):
......
300 362

  
301 363
    def test_002c_set_server_metadata(self):
302 364
        image = self.client.get_image_details(self.imageid)
303
        self.client.update_server_metadata(
304
            self.serverid, OS=image["metadata"]["values"]["OS"])
365
        os = image["metadata"]["values"]["OS"]
366
        loginname = image["metadata"]["values"].get("loginname", None)
367
        self.client.update_server_metadata(self.serverid, OS=os)
368

  
369
        # Determine the username to use for future connections
370
        # to this host
371
        cls = type(self)
372
        cls.username = loginname
373
        if not cls.username:
374
            cls.username = self._connect_loginname(os)
375
        self.assertIsNotNone(cls.username)
305 376

  
306 377
    def test_002d_verify_server_metadata(self):
307 378
        """Test server metadata keys are set based on image metadata"""
......
311 382

  
312 383
    def test_003_server_becomes_active(self):
313 384
        """Test server becomes ACTIVE"""
314
        self._wait_for_status_transition("BUILD", "ACTIVE",
385
        self._insist_on_status_transition("BUILD", "ACTIVE",
315 386
                                         self.build_fail, self.build_warning)
316 387

  
317 388
    def test_003a_get_server_oob_console(self):
318
        """Test getting OOB server console over VNC"""
389
        """Test getting OOB server console over VNC
390

  
391
        Implementation of RFB protocol follows
392
        http://www.realvnc.com/docs/rfbproto.pdf.
393

  
394
        """
319 395
        console = self.client.get_server_console(self.serverid)
320 396
        self.assertEquals(console['type'], "vnc")
321
        sock = self._get_tcp_connection(socket.AF_UNSPEC,
397
        sock = self._insist_on_tcp_connection(socket.AF_UNSPEC,
322 398
                                        console["host"], console["port"])
399

  
400
        # Step 1. ProtocolVersion message (par. 6.1.1)
323 401
        version = sock.recv(1024)
324
        self.assertTrue(version.startswith("RFB "))
402
        self.assertEquals(version, 'RFB 003.008\n')
403
        sock.send(version)
404

  
405
        # Step 2. Security (par 6.1.2): Only VNC Authentication supported
406
        sec = sock.recv(1024)
407
        self.assertEquals(list(sec), ['\x01', '\x02'])
408

  
409
        # Step 3. Request VNC Authentication (par 6.1.2)
410
        sock.send('\x02')
411

  
412
        # Step 4. Receive Challenge (par 6.2.2)
413
        challenge = sock.recv(1024)
414
        self.assertEquals(len(challenge), 16)
415

  
416
        # Step 5. DES-Encrypt challenge, use password as key (par 6.2.2)
417
        response = d3des_generate_response(
418
            (console["password"] + '\0' * 8)[:8], challenge)
419
        sock.send(response)
420

  
421
        # Step 6. SecurityResult (par 6.1.3)
422
        result = sock.recv(4)
423
        self.assertEquals(list(result), ['\x00', '\x00', '\x00', '\x00'])
325 424
        sock.close()
326 425

  
327 426
    def test_004_server_has_ipv4(self):
......
339 438
    def test_006_server_responds_to_ping_IPv4(self):
340 439
        """Test server responds to ping on IPv4 address"""
341 440
        server = self.client.get_server_details(self.serverid)
342
        ping = subprocess.Popen("ping -c 4 -w 4 %s" % self._get_ipv4(server),
343
                                shell=True, stdout=subprocess.PIPE,
344
                                stderr=subprocess.PIPE)
345
        (stdout, stderr) = ping.communicate()
346
        ret = ping.wait()
347
        self.assertEquals(ret, 0,
348
                          "ping IPv4 %s failed.\nStdout:\n%s\nStderr:\n%s" %
349
                          (self._get_ipv4(server), stdout, stderr))
441
        ip = self._get_ipv4(server)
442
        self._try_until_timeout_expires(self.action_timeout,
443
                                        self.action_timeout,
444
                                        "PING IPv4", self._ping_once,
445
                                        False, ip)
350 446

  
351 447
    def test_007_server_responds_to_ping_IPv6(self):
352 448
        """Test server responds to ping on IPv6 address"""
353 449
        server = self.client.get_server_details(self.serverid)
354
        ping6 = subprocess.Popen("ping6 -c 4 -w 4 %s" % self._get_ipv6(server),
355
                                 shell=True, stdout=subprocess.PIPE,
356
                                 stderr=subprocess.PIPE)
357
        (stdout, stderr) = ping6.communicate()
358
        ret = ping6.wait()
359
        self.assertEquals(ret, 0,
360
                          "ping IPv6 %s failed.\nStdout:\n%s\nStderr:\n%s" %
361
                          (self._get_ipv6(server), stdout, stderr))
450
        ip = self._get_ipv6(server)
451
        self._try_until_timeout_expires(self.action_timeout,
452
                                        self.action_timeout,
453
                                        "PING IPv6", self._ping_once,
454
                                        True, ip)
362 455

  
363 456
    def test_008_submit_shutdown_request(self):
364 457
        """Test submit request to shutdown server"""
......
366 459

  
367 460
    def test_009_server_becomes_stopped(self):
368 461
        """Test server becomes STOPPED"""
369
        self._wait_for_status_transition("ACTIVE", "STOPPED",
462
        self._insist_on_status_transition("ACTIVE", "STOPPED",
463
                                         self.action_timeout,
370 464
                                         self.action_timeout)
371 465

  
372 466
    def test_010_submit_start_request(self):
......
375 469

  
376 470
    def test_011_server_becomes_active(self):
377 471
        """Test server becomes ACTIVE again"""
378
        self._wait_for_status_transition("STOPPED", "ACTIVE",
472
        self._insist_on_status_transition("STOPPED", "ACTIVE",
473
                                         self.action_timeout,
379 474
                                         self.action_timeout)
380 475

  
381 476
    def test_011a_server_responds_to_ping_IPv4(self):
......
384 479

  
385 480
    def test_012_ssh_to_server_IPv4(self):
386 481
        """Test SSH to server public IPv4 works, verify hostname"""
387
        self._skipIf(self.is_windows, "only for Linux servers")
482
        self._skipIf(self.is_windows, "only valid for Linux servers")
388 483
        server = self.client.get_server_details(self.serverid)
389
        hostname = self._get_hostname_over_ssh(self._get_ipv4(server),
390
                                               "root", self.passwd)
391
        self.assertEqual(hostname, "notsnf-%s" % self.serverid)
484
        self._insist_on_ssh_hostname(self._get_ipv4(server),
485
                                     self.username, self.passwd)
392 486

  
393 487
    def test_013_ssh_to_server_IPv6(self):
394 488
        """Test SSH to server public IPv6 works, verify hostname"""
395
        self._skipIf(self.is_windows, "only for Linux servers")
489
        self._skipIf(self.is_windows, "only valid for Linux servers")
396 490
        server = self.client.get_server_details(self.serverid)
397
        hostname = self._get_hostname_over_ssh(self._get_ipv6(server),
398
                                               "root", self.passwd)
399
        self.assertEqual(hostname, "notsnf-%s" % self.serverid)
491
        self._insist_on_ssh_hostname(self._get_ipv6(server),
492
                                     self.username, self.passwd)
400 493

  
401 494
    def test_014_rdp_to_server_IPv4(self):
402 495
        "Test RDP connection to server public IPv4 works"""
403
        self._skipIf(not self.is_windows, "only for Windows servers")
496
        self._skipIf(not self.is_windows, "only valid for Windows servers")
404 497
        server = self.client.get_server_details(self.serverid)
405 498
        ipv4 = self._get_ipv4(server)
406
        sock = _get_tcp_connection(socket.AF_INET, ipv4, 3389)
499
        sock = _insist_on_tcp_connection(socket.AF_INET, ipv4, 3389)
407 500

  
408 501
        # No actual RDP processing done. We assume the RDP server is there
409 502
        # if the connection to the RDP port is successful.
......
411 504

  
412 505
    def test_015_rdp_to_server_IPv6(self):
413 506
        "Test RDP connection to server public IPv6 works"""
414
        self._skipIf(not self.is_windows, "only for Windows servers")
507
        self._skipIf(not self.is_windows, "only valid for Windows servers")
415 508
        server = self.client.get_server_details(self.serverid)
416 509
        ipv6 = self._get_ipv6(server)
417 510
        sock = _get_tcp_connection(socket.AF_INET6, ipv6, 3389)
......
422 515

  
423 516
    def test_016_personality_is_enforced(self):
424 517
        """Test file injection for personality enforcement"""
425
        self._skipIf(self.is_windows, "only for Linux servers")
518
        self._skipIf(self.is_windows, "only implemented for Linux servers")
426 519
        self.assertTrue(False, "test not implemented, will fail")
427 520

  
428 521

  
......
470 563

  
471 564
    parser = OptionParser(**kw)
472 565
    parser.disable_interspersed_args()
566
    parser.add_option("--failfast",
567
                      action="store_true", dest="failfast",
568
                      help="Fail immediately if one of the tests fails",
569
                      default=False)
473 570
    parser.add_option("--action-timeout",
474 571
                      action="store", type="int", dest="action_timeout",
475 572
                      metavar="TIMEOUT",
......
587 684
    #
588 685
    suites = map(unittest.TestLoader().loadTestsFromTestCase, cases)
589 686
    alltests = unittest.TestSuite(suites)
590
    unittest.TextTestRunner(verbosity=2).run(alltests)
687
    unittest.TextTestRunner(verbosity=2, failfast=opts.failfast).run(alltests)
591 688

  
592 689

  
593 690
if __name__ == "__main__":

Also available in: Unified diff