diff --git a/neutron/agent/ovsdb/native/connection.py b/neutron/agent/ovsdb/native/connection.py index ca2ab4ede32..7865f031611 100644 --- a/neutron/agent/ovsdb/native/connection.py +++ b/neutron/agent/ovsdb/native/connection.py @@ -12,13 +12,17 @@ # License for the specific language governing permissions and limitations # under the License. +import os + from debtcollector import moves from oslo_config import cfg from ovs.db import idl +from ovs.stream import Stream from ovsdbapp.backend.ovs_idl import connection as _connection from ovsdbapp.backend.ovs_idl import idlutils import tenacity +from neutron.agent.ovsdb.native import exceptions as ovsdb_exc from neutron.agent.ovsdb.native import helpers TransactionQueue = moves.moved_class(_connection.TransactionQueue, @@ -26,9 +30,31 @@ TransactionQueue = moves.moved_class(_connection.TransactionQueue, Connection = moves.moved_class(_connection.Connection, 'Connection', __name__) +def configure_ssl_conn(): + """ + Configures required settings for an SSL based OVSDB client connection + :return: None + """ + + req_ssl_opts = {'ssl_key_file': cfg.CONF.OVS.ssl_key_file, + 'ssl_cert_file': cfg.CONF.OVS.ssl_cert_file, + 'ssl_ca_cert_file': cfg.CONF.OVS.ssl_ca_cert_file} + for ssl_opt, ssl_file in req_ssl_opts.items(): + if not ssl_file: + raise ovsdb_exc.OvsdbSslRequiredOptError(ssl_opt=ssl_opt) + elif not os.path.exists(ssl_file): + raise ovsdb_exc.OvsdbSslConfigNotFound(ssl_file=ssl_file) + # TODO(ihrachys): move to ovsdbapp + Stream.ssl_set_private_key_file(req_ssl_opts['ssl_key_file']) + Stream.ssl_set_certificate_file(req_ssl_opts['ssl_cert_file']) + Stream.ssl_set_ca_cert_file(req_ssl_opts['ssl_ca_cert_file']) + + def idl_factory(): conn = cfg.CONF.OVS.ovsdb_connection schema_name = 'Open_vSwitch' + if conn.startswith('ssl:'): + configure_ssl_conn() try: helper = idlutils.get_schema_helper(conn, schema_name) except Exception: diff --git a/neutron/agent/ovsdb/native/exceptions.py b/neutron/agent/ovsdb/native/exceptions.py new file mode 100644 index 00000000000..46db629304d --- /dev/null +++ b/neutron/agent/ovsdb/native/exceptions.py @@ -0,0 +1,28 @@ +# Copyright 2018 Red Hat, Inc. +# All Rights Reserved. +# +# 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. + +from neutron_lib import exceptions as e + +from neutron._i18n import _ + + +class OvsdbSslConfigNotFound(e.NeutronException): + message = _("Specified SSL file %(ssl_file)s could not be found") + + +class OvsdbSslRequiredOptError(e.NeutronException): + message = _("Required 'ovs' group option %(ssl_opt)s not set. SSL " + "configuration options are required when using SSL " + "ovsdb_connection URI") diff --git a/neutron/conf/agent/ovsdb_api.py b/neutron/conf/agent/ovsdb_api.py index 1f84000a8ab..7c13a85465f 100644 --- a/neutron/conf/agent/ovsdb_api.py +++ b/neutron/conf/agent/ovsdb_api.py @@ -35,7 +35,22 @@ API_OPTS = [ 'Will be used by ovsdb-client when monitoring and ' 'used for the all ovsdb commands when native ' 'ovsdb_interface is enabled' - )) + )), + cfg.StrOpt('ssl_key_file', + help=_('The SSL private key file to use when interacting with ' + 'OVSDB. Required when using an "ssl:" prefixed ' + 'ovsdb_connection' + )), + cfg.StrOpt('ssl_cert_file', + help=_('The SSL certificate file to use when interacting ' + 'with OVSDB. Required when using an "ssl:" prefixed ' + 'ovsdb_connection' + )), + cfg.StrOpt('ssl_ca_cert_file', + help=_('The Certificate Authority (CA) certificate to use ' + 'when interacting with OVSDB. Required when using an ' + '"ssl:" prefixed ovsdb_connection' + )), ] diff --git a/neutron/tests/unit/agent/ovsdb/native/test_connection.py b/neutron/tests/unit/agent/ovsdb/native/test_connection.py index 090911c3dc2..3b016067f52 100644 --- a/neutron/tests/unit/agent/ovsdb/native/test_connection.py +++ b/neutron/tests/unit/agent/ovsdb/native/test_connection.py @@ -18,9 +18,14 @@ from ovsdbapp.backend.ovs_idl import connection from ovsdbapp.backend.ovs_idl import idlutils from neutron.agent.ovsdb.native import connection as native_conn +from neutron.agent.ovsdb.native import exceptions as ovsdb_exc from neutron.agent.ovsdb.native import helpers from neutron.tests import base +SSL_KEY_FILE = '/tmp/dummy.pem' +SSL_CERT_FILE = '/tmp/dummy.crt' +SSL_CA_FILE = '/tmp/ca.crt' + class TestOVSNativeConnection(base.BaseTestCase): @mock.patch.object(connection, 'threading') @@ -46,3 +51,59 @@ class TestOVSNativeConnection(base.BaseTestCase): conn.start() self.assertEqual(3, len(mock_get_schema_helper.mock_calls)) mock_helper.register_all.assert_called_once_with() + + @mock.patch.object(native_conn, 'Stream') + @mock.patch.object(connection, 'threading') + @mock.patch.object(native_conn, 'idl') + @mock.patch.object(idlutils, 'get_schema_helper') + @mock.patch.object(native_conn, 'os') + @mock.patch.object(native_conn, 'cfg') + def test_ssl_connection(self, mock_cfg, mock_os, mock_get_schema_helper, + mock_idl, mock_threading, mock_stream): + mock_os.path.isfile.return_value = True + mock_cfg.CONF.OVS.ovsdb_connection = 'ssl:127.0.0.1:6640' + mock_cfg.CONF.OVS.ssl_key_file = SSL_KEY_FILE + mock_cfg.CONF.OVS.ssl_cert_file = SSL_CERT_FILE + mock_cfg.CONF.OVS.ssl_ca_cert_file = SSL_CA_FILE + + conn = connection.Connection(idl=native_conn.idl_factory(), + timeout=1) + conn.start() + mock_stream.ssl_set_private_key_file.assert_called_once_with( + SSL_KEY_FILE + ) + mock_stream.ssl_set_certificate_file.assert_called_once_with( + SSL_CERT_FILE + ) + mock_stream.ssl_set_ca_cert_file.assert_called_once_with( + SSL_CA_FILE + ) + + @mock.patch.object(native_conn, 'Stream') + @mock.patch.object(connection, 'threading') + @mock.patch.object(native_conn, 'idl') + @mock.patch.object(idlutils, 'get_schema_helper') + @mock.patch.object(native_conn, 'cfg') + def test_ssl_conn_file_missing(self, mock_cfg, mock_get_schema_helper, + mock_idl, mock_threading, mock_stream): + mock_cfg.CONF.OVS.ovsdb_connection = 'ssl:127.0.0.1:6640' + mock_cfg.CONF.OVS.ssl_key_file = SSL_KEY_FILE + mock_cfg.CONF.OVS.ssl_cert_file = SSL_CERT_FILE + mock_cfg.CONF.OVS.ssl_ca_cert_file = SSL_CA_FILE + + self.assertRaises(ovsdb_exc.OvsdbSslConfigNotFound, + native_conn.idl_factory) + + @mock.patch.object(native_conn, 'Stream') + @mock.patch.object(connection, 'threading') + @mock.patch.object(native_conn, 'idl') + @mock.patch.object(idlutils, 'get_schema_helper') + @mock.patch.object(native_conn, 'cfg') + def test_ssl_conn_cfg_missing(self, mock_cfg, mock_get_schema_helper, + mock_idl, mock_threading, mock_stream): + mock_cfg.CONF.OVS.ovsdb_connection = 'ssl:127.0.0.1:6640' + mock_cfg.CONF.OVS.ssl_key_file = None + mock_cfg.CONF.OVS.ssl_cert_file = None + mock_cfg.CONF.OVS.ssl_ca_cert_file = None + self.assertRaises(ovsdb_exc.OvsdbSslRequiredOptError, + native_conn.idl_factory) diff --git a/releasenotes/notes/fix-ovsdb-ssl-connection-4058caf4fdcb33ab.yaml b/releasenotes/notes/fix-ovsdb-ssl-connection-4058caf4fdcb33ab.yaml new file mode 100644 index 00000000000..6be8e71baed --- /dev/null +++ b/releasenotes/notes/fix-ovsdb-ssl-connection-4058caf4fdcb33ab.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Neutron agents now support SSL connections to OVSDB server. + To enable an SSL based connection, use an ``ssl`` prefixed URI for the + ``ovsdb_connection`` setting. When using SSL it is also required to set + new ``ovs`` group options which include ``ssl_key_file``, ``ssl_cert_file``, and + ``ssl_ca_cert_file``.