diff --git a/swift/cli/recon.py b/swift/cli/recon.py index b47bee9489..676973c410 100755 --- a/swift/cli/recon.py +++ b/swift/cli/recon.py @@ -109,6 +109,34 @@ class Scout(object): url, content, status = self.scout_host(base_url, self.recon_type) return url, content, status + def scout_server_type(self, host): + """ + Obtain Server header by calling OPTIONS. + + :param host: host to check + :returns: Server type, status + """ + try: + url = "http://%s:%s/" % (host[0], host[1]) + req = urllib2.Request(url) + req.get_method = lambda: 'OPTIONS' + conn = urllib2.urlopen(req) + header = conn.info().getheader('Server') + server_header = header.split('/') + content = server_header[0] + status = 200 + except urllib2.HTTPError as err: + if not self.suppress_errors or self.verbose: + print("-> %s: %s" % (url, err)) + content = err + status = err.code + except urllib2.URLError as err: + if not self.suppress_errors or self.verbose: + print("-> %s: %s" % (url, err)) + content = err + status = -1 + return url, content, status + class SwiftRecon(object): """ @@ -334,6 +362,29 @@ class SwiftRecon(object): print("Device errors: %s on %s" % (entry, node)) print("=" * 79) + def server_type_check(self, hosts): + """ + Check for server types on the ring + + :param hosts: set of hosts to check. in the format of: + set([('127.0.0.1', 6020), ('127.0.0.2', 6030)]) + """ + errors = {} + recon = Scout("server_type_check", self.verbose, self.suppress_errors, + self.timeout) + print("[%s] Validating server type '%s' on %s hosts..." % + (self._ptime(), self.server_type, len(hosts))) + for url, response, status in self.pool.imap( + recon.scout_server_type, hosts): + if status == 200: + if response != self.server_type + '-server': + errors[url] = response + print("%s/%s hosts ok, %s error[s] while checking hosts." % ( + len(hosts) - len(errors), len(hosts), len(errors))) + for host in errors: + print("Invalid: %s is %s" % (host, errors[host])) + print("=" * 79) + def expirer_check(self, hosts): """ Obtain and print expirer statistics @@ -872,6 +923,8 @@ class SwiftRecon(object): help="Get cluster load average stats") args.add_option('--quarantined', '-q', action="store_true", help="Get cluster quarantine stats") + args.add_option('--validate-servers', action="store_true", + help="Validate servers on the ring") args.add_option('--md5', action="store_true", help="Get md5sum of servers ring and compare to " "local copy") @@ -938,6 +991,7 @@ class SwiftRecon(object): self.get_ringmd5(hosts, swift_dir) self.quarantine_check(hosts) self.socket_usage(hosts) + self.server_type_check(hosts) else: if options.async: if self.server_type == 'object': @@ -966,6 +1020,8 @@ class SwiftRecon(object): self.expirer_check(hosts) else: print("Error: Can't check expired on non object servers.") + if options.validate_servers: + self.server_type_check(hosts) if options.loadstats: self.load_check(hosts) if options.diskusage: diff --git a/test/unit/cli/test_recon.py b/test/unit/cli/test_recon.py index 4f836e7d51..e9ad45d2c8 100644 --- a/test/unit/cli/test_recon.py +++ b/test/unit/cli/test_recon.py @@ -56,6 +56,7 @@ class TestScout(unittest.TestCase): def setUp(self, *_args, **_kwargs): self.scout_instance = recon.Scout("type", suppress_errors=True) self.url = 'http://127.0.0.1:8080/recon/type' + self.server_type_url = 'http://127.0.0.1:8080/' @mock.patch('eventlet.green.urllib2.urlopen') def test_scout_ok(self, mock_urlopen): @@ -85,6 +86,37 @@ class TestScout(unittest.TestCase): self.assertTrue(isinstance(content, urllib2.HTTPError)) self.assertEqual(status, 404) + @mock.patch('eventlet.green.urllib2.urlopen') + def test_scout_server_type_ok(self, mock_urlopen): + def getheader(name): + d = {'Server': 'server-type'} + return d.get(name) + mock_urlopen.return_value.info.return_value.getheader = getheader + url, content, status = self.scout_instance.scout_server_type( + ("127.0.0.1", "8080")) + self.assertEqual(url, self.server_type_url) + self.assertEqual(content, 'server-type') + self.assertEqual(status, 200) + + @mock.patch('eventlet.green.urllib2.urlopen') + def test_scout_server_type_url_error(self, mock_urlopen): + mock_urlopen.side_effect = urllib2.URLError("") + url, content, status = self.scout_instance.scout_server_type( + ("127.0.0.1", "8080")) + self.assertTrue(isinstance(content, urllib2.URLError)) + self.assertEqual(url, self.server_type_url) + self.assertEqual(status, -1) + + @mock.patch('eventlet.green.urllib2.urlopen') + def test_scout_server_type_http_error(self, mock_urlopen): + mock_urlopen.side_effect = urllib2.HTTPError( + self.server_type_url, 404, "Internal error", None, None) + url, content, status = self.scout_instance.scout_server_type( + ("127.0.0.1", "8080")) + self.assertEqual(url, self.server_type_url) + self.assertTrue(isinstance(content, urllib2.HTTPError)) + self.assertEqual(status, 404) + class TestRecon(unittest.TestCase): def setUp(self, *_args, **_kwargs): @@ -289,6 +321,86 @@ class TestReconCommands(unittest.TestCase): return mock.patch('eventlet.green.urllib2.urlopen', fake_urlopen) + def test_server_type_check(self): + hosts = [('127.0.0.1', 6010), ('127.0.0.1', 6011), + ('127.0.0.1', 6012)] + + # sample json response from http://<host>:<port>/ + responses = {6010: 'object-server', 6011: 'container-server', + 6012: 'account-server'} + + def mock_scout_server_type(app, host): + url = 'http://%s:%s/' % (host[0], host[1]) + response = responses[host[1]] + status = 200 + return url, response, status + + stdout = StringIO() + patches = [ + mock.patch('swift.cli.recon.Scout.scout_server_type', + mock_scout_server_type), + mock.patch('sys.stdout', new=stdout), + ] + + res_object = 'Invalid: http://127.0.0.1:6010/ is object-server' + res_container = 'Invalid: http://127.0.0.1:6011/ is container-server' + res_account = 'Invalid: http://127.0.0.1:6012/ is account-server' + valid = "1/1 hosts ok, 0 error[s] while checking hosts." + + #Test for object server type - default + with nested(*patches): + self.recon.server_type_check(hosts) + + output = stdout.getvalue() + self.assertTrue(res_container in output.splitlines()) + self.assertTrue(res_account in output.splitlines()) + stdout.truncate(0) + + #Test ok for object server type - default + with nested(*patches): + self.recon.server_type_check([hosts[0]]) + + output = stdout.getvalue() + self.assertTrue(valid in output.splitlines()) + stdout.truncate(0) + + #Test for account server type + with nested(*patches): + self.recon.server_type = 'account' + self.recon.server_type_check(hosts) + + output = stdout.getvalue() + self.assertTrue(res_container in output.splitlines()) + self.assertTrue(res_object in output.splitlines()) + stdout.truncate(0) + + #Test ok for account server type + with nested(*patches): + self.recon.server_type = 'account' + self.recon.server_type_check([hosts[2]]) + + output = stdout.getvalue() + self.assertTrue(valid in output.splitlines()) + stdout.truncate(0) + + #Test for container server type + with nested(*patches): + self.recon.server_type = 'container' + self.recon.server_type_check(hosts) + + output = stdout.getvalue() + self.assertTrue(res_account in output.splitlines()) + self.assertTrue(res_object in output.splitlines()) + stdout.truncate(0) + + #Test ok for container server type + with nested(*patches): + self.recon.server_type = 'container' + self.recon.server_type_check([hosts[1]]) + + output = stdout.getvalue() + self.assertTrue(valid in output.splitlines()) + def test_get_swiftconfmd5(self): hosts = set([('10.1.1.1', 10000), ('10.2.2.2', 10000)])