Add error handling and logging to CNI daemon
This adds error handling and improves logging in CNI daemon. Besides that unit tests for new codepaths are added. Implements: blueprint cni-split-exec-daemon Change-Id: Ieff94a5c0b85e8d958421fc4db2f09a411f4e667
This commit is contained in:
parent
d688e6f6ec
commit
7b4b68e16e
|
@ -147,12 +147,20 @@ class CNIDaemonizedRunner(CNIRunner):
|
|||
|
||||
def _add(self, params):
|
||||
resp = self._make_request('addNetwork', params)
|
||||
if resp.status != 201:
|
||||
LOG.error('CNI daemon returned error "%(status)d %(reason)s".',
|
||||
{'status': resp.status, 'reason': resp.reason})
|
||||
raise k_exc.CNIError('Got invalid status code from CNI daemon.')
|
||||
vif_dict = jsonutils.loads(resp.read())
|
||||
vif = base.VersionedObject.obj_from_primitive(vif_dict)
|
||||
return self._vif_data(vif)
|
||||
|
||||
def _delete(self, params):
|
||||
self._make_request('delNetwork', params)
|
||||
resp = self._make_request('delNetwork', params)
|
||||
if resp.status != 204:
|
||||
LOG.error('CNI daemon returned error "%(status)d %(reason)s".',
|
||||
{'status': resp.status, 'reason': resp.reason})
|
||||
raise k_exc.CNIError('Got invalid status code from CNI daemon.')
|
||||
|
||||
def prepare_env(self, env, stdin):
|
||||
cni_envs = {}
|
||||
|
@ -162,13 +170,6 @@ class CNIDaemonizedRunner(CNIRunner):
|
|||
return cni_envs
|
||||
|
||||
def _make_request(self, base, cni_envs):
|
||||
"""Make request
|
||||
|
||||
:param method
|
||||
:param path
|
||||
:param kwargs (body, headers)
|
||||
:return response
|
||||
"""
|
||||
method = 'POST'
|
||||
body = jsonutils.dumps(cni_envs)
|
||||
headers = {
|
||||
|
@ -179,7 +180,18 @@ class CNIDaemonizedRunner(CNIRunner):
|
|||
# (janonymous) make base configurable
|
||||
|
||||
socket_path = config.CONF.kubernetes.sock_file
|
||||
conn = UnixDomainHttpConnection(socket_path)
|
||||
conn.request(method, path, body=body, headers=headers)
|
||||
resp = conn.getresponse()
|
||||
try:
|
||||
conn = UnixDomainHttpConnection(socket_path)
|
||||
LOG.debug('Making request to CNI Daemon. Method: %(method)s,'
|
||||
'path: %(path)s, headers: %(headers)s, body %(body)s',
|
||||
{'method': method, 'path': path, 'body': body,
|
||||
'headers': headers})
|
||||
conn.request(method, path, body=body, headers=headers)
|
||||
resp = conn.getresponse()
|
||||
except (socket.error, OSError):
|
||||
LOG.exception('Looks like the socket file is not present in %s. Is'
|
||||
'kuryr-daemon running?', socket_path)
|
||||
raise
|
||||
LOG.debug('CNI Daemon returned "%(status)d %(reason)s".',
|
||||
{'status': resp.status, 'reason': resp.reason})
|
||||
return resp
|
||||
|
|
|
@ -90,20 +90,41 @@ class RequestHandler(BaseHTTPRequestHandler):
|
|||
def do_POST(self):
|
||||
# Delegate to add/del network based on path
|
||||
# and write on wfile
|
||||
content_length = int(self.headers.get('Content-Length', 0))
|
||||
body = self.rfile.read(content_length)
|
||||
self._params = utils.CNIParameters(dict(jsonutils.loads(body)))
|
||||
try:
|
||||
content_length = int(self.headers.get('Content-Length', 0))
|
||||
body = self.rfile.read(content_length)
|
||||
self._params = utils.CNIParameters(dict(jsonutils.loads(body)))
|
||||
except Exception:
|
||||
LOG.warning('Malformed request.')
|
||||
self.send_error(400, 'Bad Request')
|
||||
return
|
||||
|
||||
if self.path.endswith('/addNetwork'):
|
||||
resp = self.addNetwork()
|
||||
try:
|
||||
resp = self.addNetwork()
|
||||
except Exception:
|
||||
LOG.exception('Error when processing addNetwork request. '
|
||||
'CNI Params: %s', self._params)
|
||||
self.send_error(500, 'Internal Server Error')
|
||||
return
|
||||
|
||||
self.send_response(201, "Created")
|
||||
self.send_header("Content-type", 'application/json')
|
||||
self.end_headers()
|
||||
self.wfile.write(resp.encode())
|
||||
elif self.path.endswith('/delNetwork'):
|
||||
self.delNetwork()
|
||||
try:
|
||||
self.delNetwork()
|
||||
except Exception:
|
||||
LOG.exception('Error when processing delNetwork request. '
|
||||
'CNI Params: %s', self._params)
|
||||
self.send_error(500, 'Internal Server Error')
|
||||
return
|
||||
|
||||
self.send_response(204, "No Content")
|
||||
else:
|
||||
self.send_error(405, "Method Not Allowed")
|
||||
LOG.warning('Unexpected path %s in the call.' % self.path)
|
||||
self.send_error(404, "Not Found")
|
||||
|
||||
def addNetwork(self):
|
||||
vif = self._plugin.add(self._params)
|
||||
|
@ -114,20 +135,16 @@ class RequestHandler(BaseHTTPRequestHandler):
|
|||
self._plugin.delete(self._params)
|
||||
|
||||
def log_message(self, format, *args):
|
||||
"""Log an arbitrary message.
|
||||
|
||||
The client ip address and current date/time are prefixed to every
|
||||
message.
|
||||
"""
|
||||
# Default log_message assumes client_address to log which is not
|
||||
# passed in unix sockets.
|
||||
sys.stderr.write("%s - - [%s] %s\n" %
|
||||
(socket.gethostname(),
|
||||
self.log_date_time_string(),
|
||||
format % args))
|
||||
LOG.debug("%(hostname)s - - [%(time)s] %(msg)s\n" %
|
||||
{'hostname': socket.gethostname(),
|
||||
'time': self.log_date_time_string(),
|
||||
'msg': format % args})
|
||||
|
||||
|
||||
def run_daemon():
|
||||
config.init(sys.argv[1:])
|
||||
config.setup_logging()
|
||||
|
||||
# TODO(vikasc): Should be done using dynamically loadable OVO types plugin.
|
||||
objects.register_locally_defined_vifs()
|
||||
|
||||
|
@ -138,11 +155,15 @@ def run_daemon():
|
|||
except OSError:
|
||||
if os.path.exists(server_address):
|
||||
raise
|
||||
httpd = None
|
||||
try:
|
||||
LOG.info('Starting daemon HTTP server on socket %s.', server_address)
|
||||
httpd = UnixDomainHttpServer(server_address, RequestHandler)
|
||||
httpd.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
except Exception:
|
||||
LOG.exception('Failed to start kuryr-daemon.')
|
||||
httpd.socket.close()
|
||||
finally:
|
||||
if httpd:
|
||||
httpd.socket.close()
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
|
||||
import mock
|
||||
from six import StringIO
|
||||
import socket
|
||||
|
||||
from oslo_serialization import jsonutils
|
||||
|
||||
|
@ -117,8 +118,28 @@ class TestCNIDaemonizedRunner(test_base.TestCase, TestCNIRunnerMixin):
|
|||
m_socket)
|
||||
self.assertEqual(0, result)
|
||||
|
||||
def test_run_add_invalid(self, m_getresponse, m_request, m_socket):
|
||||
m_response = mock.Mock(status=400)
|
||||
m_response.read = mock.Mock()
|
||||
result = self._test_run('ADD', 'addNetwork', m_getresponse, m_request,
|
||||
m_socket)
|
||||
self.assertEqual(1, result)
|
||||
m_response.read.assert_not_called()
|
||||
|
||||
def test_run_del(self, m_getresponse, m_request, m_socket):
|
||||
m_getresponse.return_value = mock.Mock(status=204)
|
||||
result = self._test_run('DEL', 'delNetwork', m_getresponse, m_request,
|
||||
m_socket)
|
||||
self.assertEqual(0, result)
|
||||
|
||||
def test_run_del_invalid(self, m_getresponse, m_request, m_socket):
|
||||
m_getresponse.return_value = mock.Mock(status=400)
|
||||
result = self._test_run('DEL', 'delNetwork', m_getresponse, m_request,
|
||||
m_socket)
|
||||
self.assertEqual(1, result)
|
||||
|
||||
def test_run_socket_error(self, m_getresponse, m_request, m_socket):
|
||||
m_request.side_effect = socket.error
|
||||
result = self._test_run('DEL', 'delNetwork', m_getresponse, m_request,
|
||||
m_socket)
|
||||
self.assertEqual(1, result)
|
||||
|
|
|
@ -95,21 +95,41 @@ class TestRequestHandler(test_base.TestCase):
|
|||
mock_request.sendall = mock.MagicMock()
|
||||
handler_class(mock_request, ip_port, self)
|
||||
|
||||
def _test_path(self, path, mock_method):
|
||||
def _test_path(self, path, mock_method, expect_run=True):
|
||||
mock_method.return_value = fake._fake_vif()
|
||||
|
||||
TestRequestHandler.path = path
|
||||
|
||||
TestRequestHandler.MockServer(('0.0.0.0', 8888),
|
||||
cni_daemon.RequestHandler)
|
||||
self.assertTrue(mock_method.called)
|
||||
self.assertEqual(TestRequestHandler.params['CNI_ARGS'],
|
||||
mock_method.call_args[0][0].CNI_ARGS)
|
||||
self.assertEqual(expect_run, mock_method.called)
|
||||
if expect_run:
|
||||
self.assertEqual(TestRequestHandler.params['CNI_ARGS'],
|
||||
mock_method.call_args[0][0].CNI_ARGS)
|
||||
|
||||
@mock.patch('kuryr_kubernetes.cni.cni_daemon.K8sCNIPlugin.add')
|
||||
def test_addNetwork(self, m_add):
|
||||
self._test_path('addNetwork', m_add)
|
||||
|
||||
@mock.patch('kuryr_kubernetes.cni.cni_daemon.RequestHandler.send_error')
|
||||
@mock.patch('kuryr_kubernetes.cni.cni_daemon.K8sCNIPlugin.add')
|
||||
def test_addNetwork_exception(self, m_add, m_send_error):
|
||||
m_add.side_effect = Exception
|
||||
self._test_path('addNetwork', m_add)
|
||||
m_send_error.assert_called_with(500, 'Internal Server Error')
|
||||
|
||||
@mock.patch('kuryr_kubernetes.cni.cni_daemon.K8sCNIPlugin.delete')
|
||||
def test_delNetwork(self, m_delete):
|
||||
self._test_path('delNetwork', m_delete)
|
||||
|
||||
@mock.patch('kuryr_kubernetes.cni.cni_daemon.RequestHandler.send_error')
|
||||
@mock.patch('kuryr_kubernetes.cni.cni_daemon.K8sCNIPlugin.delete')
|
||||
def test_delNetwork_exception(self, m_delete, m_send_error):
|
||||
m_delete.side_effect = Exception
|
||||
self._test_path('delNetwork', m_delete)
|
||||
m_send_error.assert_called_with(500, 'Internal Server Error')
|
||||
|
||||
@mock.patch('kuryr_kubernetes.cni.cni_daemon.RequestHandler.send_error')
|
||||
def test_invalid_path(self, m_send_error):
|
||||
self._test_path('invalid', mock.Mock(), False)
|
||||
m_send_error.assert_called_with(404, 'Not Found')
|
||||
|
|
Loading…
Reference in New Issue