472 lines
17 KiB
Python
472 lines
17 KiB
Python
#!/usr/bin/python -u
|
|
# Copyright (c) 2010-2012 OpenStack Foundation
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
# implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
import unittest
|
|
|
|
from contextlib import contextmanager
|
|
import eventlet
|
|
import json
|
|
import os
|
|
import random
|
|
import shutil
|
|
import time
|
|
from uuid import uuid4
|
|
|
|
from six.moves import http_client as httplib
|
|
from six.moves.urllib.parse import urlparse
|
|
|
|
from swift.common.ring import Ring
|
|
from swift.common.manager import Manager
|
|
|
|
from test.probe import PROXY_BASE_URL
|
|
from test.probe.common import resetswift, ReplProbeTest, client
|
|
|
|
|
|
def putrequest(conn, method, path, headers):
|
|
|
|
conn.putrequest(method, path, skip_host=(headers and 'Host' in headers))
|
|
if headers:
|
|
for header, value in headers.items():
|
|
conn.putheader(header, str(value))
|
|
conn.endheaders()
|
|
|
|
|
|
def get_server_and_worker_pids(manager, old_workers=None):
|
|
# Gets all the server parent pids, as well as the set of all worker PIDs
|
|
# (i.e. any PID whose PPID is in the set of parent pids).
|
|
server_pid_set = {pid for server in manager.servers
|
|
for (_, pid) in server.iter_pid_files()}
|
|
children_pid_set = set()
|
|
old_worker_pid_set = set(old_workers or [])
|
|
all_pids = [int(f) for f in os.listdir('/proc') if f.isdigit()]
|
|
for pid in all_pids:
|
|
try:
|
|
with open('/proc/%d/status' % pid, 'r') as fh:
|
|
for line in fh:
|
|
if line.startswith('PPid:\t'):
|
|
ppid = int(line[6:])
|
|
if ppid in server_pid_set or pid in old_worker_pid_set:
|
|
children_pid_set.add(pid)
|
|
break
|
|
except Exception:
|
|
# No big deal, a process could have exited since we listed /proc,
|
|
# so we just ignore errors
|
|
pass
|
|
return {'server': server_pid_set, 'worker': children_pid_set}
|
|
|
|
|
|
def wait_for_pids(manager, callback, timeout=15, old_workers=None):
|
|
# Waits up to `timeout` seconds for the supplied callback to return True
|
|
# when passed in the manager's pid set.
|
|
start_time = time.time()
|
|
|
|
pid_sets = get_server_and_worker_pids(manager, old_workers=old_workers)
|
|
got = callback(pid_sets)
|
|
while not got and time.time() - start_time < timeout:
|
|
time.sleep(0.1)
|
|
pid_sets = get_server_and_worker_pids(manager, old_workers=old_workers)
|
|
got = callback(pid_sets)
|
|
if time.time() - start_time >= timeout:
|
|
raise AssertionError('timed out waiting for PID state; got %r' % (
|
|
pid_sets))
|
|
return pid_sets
|
|
|
|
|
|
class TestWSGIServerProcessHandling(ReplProbeTest):
|
|
# Subclasses need to define SERVER_NAME
|
|
HAS_INFO = False
|
|
PID_TIMEOUT = 25
|
|
|
|
def setUp(self):
|
|
super(TestWSGIServerProcessHandling, self).setUp()
|
|
self.container = 'container-%s' % uuid4()
|
|
client.put_container(self.url, self.token, self.container,
|
|
headers={'X-Storage-Policy':
|
|
self.policy.name})
|
|
self.manager = Manager([self.SERVER_NAME])
|
|
for server in self.manager.servers:
|
|
self.assertTrue(server.get_running_pids,
|
|
'No running PIDs for %s' % server.cmd)
|
|
self.starting_pids = get_server_and_worker_pids(self.manager)
|
|
|
|
def assert4xx(self, resp):
|
|
self.assertEqual(resp.status // 100, 4)
|
|
got_body = resp.read()
|
|
try:
|
|
self.assertIn('resource could not be found', got_body)
|
|
except AssertionError:
|
|
self.assertIn('Invalid path: blah', got_body)
|
|
|
|
def get_conn(self):
|
|
scheme, ip, port = self.get_scheme_ip_port()
|
|
if scheme == 'https':
|
|
return httplib.HTTPSConnection('%s:%s' % (ip, port))
|
|
return httplib.HTTPConnection('%s:%s' % (ip, port))
|
|
|
|
def _check_reload(self):
|
|
conn = self.get_conn()
|
|
self.addCleanup(conn.close)
|
|
|
|
# sanity request
|
|
self.start_write_req(conn, 'sanity')
|
|
resp = self.finish_write_req(conn)
|
|
self.check_write_resp(resp)
|
|
|
|
if self.HAS_INFO:
|
|
self.check_info_value(8192)
|
|
|
|
# Start another write request before reloading...
|
|
self.start_write_req(conn, 'across-reload')
|
|
|
|
if self.HAS_INFO:
|
|
self.swap_configs() # new server's max_header_size == 8191
|
|
|
|
self.do_reload()
|
|
|
|
wait_for_pids(self.manager, self.make_post_reload_pid_cb(),
|
|
old_workers=self.starting_pids['worker'],
|
|
timeout=self.PID_TIMEOUT)
|
|
|
|
# ... and make sure we can finish what we were doing
|
|
resp = self.finish_write_req(conn)
|
|
self.check_write_resp(resp)
|
|
|
|
# After this, we're in a funny spot. With eventlet 0.22.0, the
|
|
# connection's now closed, but with prior versions we could keep
|
|
# going indefinitely. See https://bugs.launchpad.net/swift/+bug/1792615
|
|
|
|
# Close our connections, to make sure old eventlet shuts down
|
|
conn.close()
|
|
|
|
# sanity
|
|
wait_for_pids(self.manager, self.make_post_close_pid_cb(),
|
|
old_workers=self.starting_pids['worker'],
|
|
timeout=self.PID_TIMEOUT)
|
|
|
|
if self.HAS_INFO:
|
|
self.check_info_value(8191)
|
|
|
|
|
|
class OldReloadMixin(object):
|
|
def make_post_reload_pid_cb(self):
|
|
def _cb(post_reload_pids):
|
|
# We expect all old server PIDs to be gone, a new server present,
|
|
# and for there to be exactly 1 old worker PID plus additional new
|
|
# worker PIDs.
|
|
old_servers_dead = not (self.starting_pids['server'] &
|
|
post_reload_pids['server'])
|
|
one_old_worker = 1 == len(self.starting_pids['worker'] &
|
|
post_reload_pids['worker'])
|
|
new_workers_present = (post_reload_pids['worker'] -
|
|
self.starting_pids['worker'])
|
|
return (post_reload_pids['server'] and old_servers_dead and
|
|
one_old_worker and new_workers_present)
|
|
return _cb
|
|
|
|
def make_post_close_pid_cb(self):
|
|
def _cb(post_close_pids):
|
|
# We expect all old server PIDs to be gone, a new server present,
|
|
# no old worker PIDs, and additional new worker PIDs.
|
|
old_servers_dead = not (self.starting_pids['server'] &
|
|
post_close_pids['server'])
|
|
old_workers_dead = not (self.starting_pids['worker'] &
|
|
post_close_pids['worker'])
|
|
new_workers_present = (post_close_pids['worker'] -
|
|
self.starting_pids['worker'])
|
|
return (post_close_pids['server'] and old_servers_dead and
|
|
old_workers_dead and new_workers_present)
|
|
return _cb
|
|
|
|
def do_reload(self):
|
|
self.manager.reload()
|
|
|
|
|
|
class SeamlessReloadMixin(object):
|
|
def make_post_reload_pid_cb(self):
|
|
def _cb(post_reload_pids):
|
|
# We expect all orig server PIDs to STILL BE PRESENT, no new server
|
|
# present, and for there to be exactly 1 old worker PID plus
|
|
# additional new worker PIDs.
|
|
same_servers = (self.starting_pids['server'] ==
|
|
post_reload_pids['server'])
|
|
one_old_worker = 1 == len(self.starting_pids['worker'] &
|
|
post_reload_pids['worker'])
|
|
new_workers_present = (post_reload_pids['worker'] -
|
|
self.starting_pids['worker'])
|
|
return (post_reload_pids['server'] and same_servers and
|
|
one_old_worker and new_workers_present)
|
|
return _cb
|
|
|
|
def make_post_close_pid_cb(self):
|
|
def _cb(post_close_pids):
|
|
# We expect all orig server PIDs to STILL BE PRESENT, no new server
|
|
# present, no old worker PIDs, and additional new worker PIDs.
|
|
same_servers = (self.starting_pids['server'] ==
|
|
post_close_pids['server'])
|
|
old_workers_dead = not (self.starting_pids['worker'] &
|
|
post_close_pids['worker'])
|
|
new_workers_present = (post_close_pids['worker'] -
|
|
self.starting_pids['worker'])
|
|
return (post_close_pids['server'] and same_servers and
|
|
old_workers_dead and new_workers_present)
|
|
return _cb
|
|
|
|
def do_reload(self):
|
|
self.manager.reload_seamless()
|
|
|
|
|
|
class ChildReloadMixin(object):
|
|
def make_post_reload_pid_cb(self):
|
|
def _cb(post_reload_pids):
|
|
# We expect all orig server PIDs to STILL BE PRESENT, no new server
|
|
# present, and for there to be exactly 1 old worker PID plus
|
|
# all but one additional new worker PIDs.
|
|
num_workers = len(self.starting_pids['worker'])
|
|
same_servers = (self.starting_pids['server'] ==
|
|
post_reload_pids['server'])
|
|
one_old_worker = 1 == len(self.starting_pids['worker'] &
|
|
post_reload_pids['worker'])
|
|
new_workers_present = (post_reload_pids['worker'] -
|
|
self.starting_pids['worker'])
|
|
return (post_reload_pids['server'] and same_servers and
|
|
one_old_worker and
|
|
len(new_workers_present) == num_workers - 1)
|
|
return _cb
|
|
|
|
def make_post_close_pid_cb(self):
|
|
def _cb(post_close_pids):
|
|
# We expect all orig server PIDs to STILL BE PRESENT, no new server
|
|
# present, no old worker PIDs, and all new worker PIDs.
|
|
same_servers = (self.starting_pids['server'] ==
|
|
post_close_pids['server'])
|
|
old_workers_dead = not (self.starting_pids['worker'] &
|
|
post_close_pids['worker'])
|
|
new_workers_present = (post_close_pids['worker'] -
|
|
self.starting_pids['worker'])
|
|
return (post_close_pids['server'] and same_servers and
|
|
old_workers_dead and new_workers_present)
|
|
return _cb
|
|
|
|
def do_reload(self):
|
|
self.manager.kill_child_pids(seamless=True)
|
|
|
|
|
|
class TestObjectServerReloadBase(TestWSGIServerProcessHandling):
|
|
SERVER_NAME = 'object'
|
|
PID_TIMEOUT = 35
|
|
|
|
def get_scheme_ip_port(self):
|
|
self.policy.load_ring('/etc/swift')
|
|
self.ring_node = random.choice(
|
|
self.policy.object_ring.get_part_nodes(1))
|
|
return 'http', self.ring_node['ip'], self.ring_node['port']
|
|
|
|
def start_write_req(self, conn, suffix):
|
|
putrequest(conn, 'PUT', '/%s/123/%s/%s/blah-%s' % (
|
|
self.ring_node['device'], self.account, self.container, suffix),
|
|
headers={'X-Timestamp': str(time.time()),
|
|
'Content-Type': 'application/octet-string',
|
|
'Content-Length': len(self.BODY),
|
|
'X-Backend-Storage-Policy-Index': str(self.policy.idx)})
|
|
|
|
def finish_write_req(self, conn):
|
|
conn.send(self.BODY)
|
|
return conn.getresponse()
|
|
|
|
def check_write_resp(self, resp):
|
|
got_body = resp.read()
|
|
self.assertEqual(resp.status // 100, 2, 'Got status %d; %r' %
|
|
(resp.status, got_body))
|
|
self.assertEqual(b'', got_body)
|
|
return resp
|
|
|
|
|
|
class TestObjectServerReload(OldReloadMixin, TestObjectServerReloadBase):
|
|
BODY = b'test-object' * 10
|
|
|
|
def test_object_reload(self):
|
|
self._check_reload()
|
|
|
|
|
|
class TestObjectServerReloadSeamless(SeamlessReloadMixin,
|
|
TestObjectServerReloadBase):
|
|
BODY = b'test-object' * 10
|
|
|
|
def test_object_reload_seamless(self):
|
|
self._check_reload()
|
|
|
|
|
|
class TestObjectServerReloadChild(ChildReloadMixin,
|
|
TestObjectServerReloadBase):
|
|
BODY = b'test-object' * 10
|
|
|
|
def test_object_reload_child(self):
|
|
self._check_reload()
|
|
|
|
|
|
class TestProxyServerReloadBase(TestWSGIServerProcessHandling):
|
|
SERVER_NAME = 'proxy-server'
|
|
HAS_INFO = True
|
|
|
|
def setUp(self):
|
|
super(TestProxyServerReloadBase, self).setUp()
|
|
self.swift_conf_path = '/etc/swift/swift.conf'
|
|
self.new_swift_conf_path = self.swift_conf_path + '.new'
|
|
self.saved_swift_conf_path = self.swift_conf_path + '.orig'
|
|
shutil.copy(self.swift_conf_path, self.saved_swift_conf_path)
|
|
with open(self.swift_conf_path, 'r') as rfh:
|
|
config = rfh.read()
|
|
section_header = '\n[swift-constraints]\n'
|
|
if section_header in config:
|
|
config = config.replace(
|
|
section_header,
|
|
section_header + 'max_header_size = 8191\n',
|
|
1)
|
|
else:
|
|
config += section_header + 'max_header_size = 8191\n'
|
|
with open(self.new_swift_conf_path, 'w') as wfh:
|
|
wfh.write(config)
|
|
wfh.flush()
|
|
|
|
def tearDown(self):
|
|
shutil.move(self.saved_swift_conf_path, self.swift_conf_path)
|
|
try:
|
|
os.unlink(self.new_swift_conf_path)
|
|
except OSError:
|
|
pass
|
|
super(TestProxyServerReloadBase, self).tearDown()
|
|
|
|
def swap_configs(self):
|
|
shutil.copy(self.new_swift_conf_path, self.swift_conf_path)
|
|
|
|
def get_scheme_ip_port(self):
|
|
parsed = urlparse(PROXY_BASE_URL)
|
|
host, port = parsed.netloc.partition(':')[::2]
|
|
if not port:
|
|
port = '443' if parsed.scheme == 'https' else '80'
|
|
return parsed.scheme, host, int(port)
|
|
|
|
def assertMaxHeaderSize(self, resp, exp_max_header_size):
|
|
self.assertEqual(resp.status // 100, 2)
|
|
info_dict = json.loads(resp.read())
|
|
self.assertEqual(exp_max_header_size,
|
|
info_dict['swift']['max_header_size'])
|
|
|
|
def check_info_value(self, expected_value):
|
|
# show that we're talking to the original server with the default
|
|
# max_header_size == 8192
|
|
conn2 = self.get_conn()
|
|
putrequest(conn2, 'GET', '/info',
|
|
headers={'Content-Length': '0',
|
|
'Accept': 'application/json'})
|
|
conn2.send('')
|
|
resp = conn2.getresponse()
|
|
self.assertMaxHeaderSize(resp, expected_value)
|
|
conn2.close()
|
|
|
|
def start_write_req(self, conn, suffix):
|
|
putrequest(conn, 'PUT', '/v1/%s/%s/blah-%s' % (
|
|
self.account, self.container, suffix),
|
|
headers={'X-Auth-Token': self.token,
|
|
'Content-Length': len(self.BODY)})
|
|
|
|
def finish_write_req(self, conn):
|
|
conn.send(self.BODY)
|
|
return conn.getresponse()
|
|
|
|
def check_write_resp(self, resp):
|
|
got_body = resp.read()
|
|
self.assertEqual(resp.status // 100, 2, 'Got status %d; %r' %
|
|
(resp.status, got_body))
|
|
self.assertEqual(b'', got_body)
|
|
return resp
|
|
|
|
|
|
class TestProxyServerReload(OldReloadMixin, TestProxyServerReloadBase):
|
|
BODY = b'proxy' * 10
|
|
|
|
def test_proxy_reload(self):
|
|
self._check_reload()
|
|
|
|
|
|
class TestProxyServerReloadSeamless(SeamlessReloadMixin,
|
|
TestProxyServerReloadBase):
|
|
BODY = b'proxy-seamless' * 10
|
|
|
|
def test_proxy_reload_seamless(self):
|
|
self._check_reload()
|
|
|
|
|
|
class TestProxyServerReloadChild(ChildReloadMixin,
|
|
TestProxyServerReloadBase):
|
|
BODY = b'proxy-seamless' * 10
|
|
# A bit of a lie, but the respawned child won't pick up the updated config
|
|
HAS_INFO = False
|
|
|
|
def test_proxy_reload_child(self):
|
|
self._check_reload()
|
|
|
|
|
|
@contextmanager
|
|
def spawn_services(ip_ports, timeout=10):
|
|
q = eventlet.Queue()
|
|
|
|
def service(sock):
|
|
try:
|
|
conn, address = sock.accept()
|
|
q.put(address)
|
|
eventlet.sleep(timeout)
|
|
conn.close()
|
|
finally:
|
|
sock.close()
|
|
|
|
pool = eventlet.GreenPool()
|
|
for ip, port in ip_ports:
|
|
sock = eventlet.listen((ip, port))
|
|
pool.spawn(service, sock)
|
|
|
|
try:
|
|
yield q
|
|
finally:
|
|
for gt in list(pool.coroutines_running):
|
|
gt.kill()
|
|
|
|
|
|
class TestHungDaemon(unittest.TestCase):
|
|
|
|
def setUp(self):
|
|
resetswift()
|
|
self.ip_ports = [
|
|
(dev['ip'], dev['port'])
|
|
for dev in Ring('/etc/swift', ring_name='account').devs
|
|
if dev
|
|
]
|
|
|
|
def test_main(self):
|
|
reconciler = Manager(['container-reconciler'])
|
|
with spawn_services(self.ip_ports) as q:
|
|
reconciler.start()
|
|
# wait for the reconciler to connect
|
|
q.get()
|
|
# once it's hung in our connection - send it sig term
|
|
print('Attempting to stop reconciler!')
|
|
reconciler.stop()
|
|
self.assertEqual(1, reconciler.status())
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|