diff --git a/etc/oslo-config-generator/openvswitch_agent.ini b/etc/oslo-config-generator/openvswitch_agent.ini index 3fab70d3921..79478d39957 100644 --- a/etc/oslo-config-generator/openvswitch_agent.ini +++ b/etc/oslo-config-generator/openvswitch_agent.ini @@ -3,4 +3,5 @@ output_file = etc/neutron/plugins/ml2/openvswitch_agent.ini.sample wrap_width = 79 namespace = neutron.ml2.ovs.agent +namespace = neutron.ml2.xenapi namespace = oslo.log diff --git a/neutron/agent/common/config.py b/neutron/agent/common/config.py index 7da9801ae41..54f606168f5 100644 --- a/neutron/agent/common/config.py +++ b/neutron/agent/common/config.py @@ -42,7 +42,11 @@ ROOT_HELPER_OPTS = [ # Having a bool use_rootwrap_daemon option precludes specifying the # rootwrap daemon command, which may be necessary for Xen? cfg.StrOpt('root_helper_daemon', - help=_('Root helper daemon application to use when possible.')), + help=_("Root helper daemon application to use when possible. " + "For the agent which needs to execute commands in Dom0 " + "in the hypervisor of XenServer, this item should be " + "set to 'xenapi_root_helper', so that it will keep a " + "XenAPI session to pass commands to Dom0.")), ] AGENT_STATE_OPTS = [ diff --git a/neutron/agent/linux/utils.py b/neutron/agent/linux/utils.py index c85a27c3eb0..5aac540caef 100644 --- a/neutron/agent/linux/utils.py +++ b/neutron/agent/linux/utils.py @@ -40,6 +40,7 @@ from six.moves import http_client as httplib from neutron._i18n import _, _LE from neutron.agent.common import config +from neutron.agent.linux import xenapi_root_helper from neutron.common import utils from neutron import wsgi @@ -65,8 +66,12 @@ class RootwrapDaemonHelper(object): def get_client(cls): with cls.__lock: if cls.__client is None: - cls.__client = client.Client( - shlex.split(cfg.CONF.AGENT.root_helper_daemon)) + if xenapi_root_helper.ROOT_HELPER_DAEMON_TOKEN == \ + cfg.CONF.AGENT.root_helper_daemon: + cls.__client = xenapi_root_helper.XenAPIClient() + else: + cls.__client = client.Client( + shlex.split(cfg.CONF.AGENT.root_helper_daemon)) return cls.__client diff --git a/neutron/agent/linux/xenapi_root_helper.py b/neutron/agent/linux/xenapi_root_helper.py new file mode 100644 index 00000000000..17d9e95e301 --- /dev/null +++ b/neutron/agent/linux/xenapi_root_helper.py @@ -0,0 +1,120 @@ +# Copyright (c) 2016 Citrix System. +# 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. + +"""xenapi root helper + +For xenapi, we may need to run some commands in dom0 with additional privilege. +This xenapi root helper contains the class of XenAPIClient to support it: +XenAPIClient will keep a XenAPI session to dom0 and allow to run commands +in dom0 via calling XenAPI plugin. The XenAPI plugin is responsible to +determine whether a command is safe to execute. +""" + +from oslo_config import cfg +from oslo_log import log as logging +from oslo_rootwrap import cmd as oslo_rootwrap_cmd +from oslo_serialization import jsonutils + +from neutron._i18n import _LE +from neutron.conf.agent import xenapi_conf + + +ROOT_HELPER_DAEMON_TOKEN = 'xenapi_root_helper' + +RC_UNKNOWN_XENAPI_ERROR = 80 +MSG_UNAUTHORIZED = "Unauthorized command" +MSG_NOT_FOUND = "Executable not found" +XENAPI_PLUGIN_FAILURE_ID = "XENAPI_PLUGIN_FAILURE" + +LOG = logging.getLogger(__name__) +xenapi_conf.register_xenapi_opts(cfg.CONF) + + +class XenAPIClient(object): + def __init__(self): + self._session = None + self._host = None + self._XenAPI = None + + def _call_plugin(self, plugin, fn, args): + host = self._this_host() + return self.get_session().xenapi.host.call_plugin( + host, plugin, fn, args) + + def _create_session(self, url, username, password): + session = self._get_XenAPI().Session(url) + session.login_with_password(username, password) + return session + + def _get_return_code(self, failure_details): + # The details will be as: + # [XENAPI_PLUGIN_FAILURE_ID, methodname, except_class_name, message] + # We can distinguish the error type by checking the message string. + if (len(failure_details) == 4 and + XENAPI_PLUGIN_FAILURE_ID == failure_details[0]): + if (MSG_UNAUTHORIZED == failure_details[3]): + return oslo_rootwrap_cmd.RC_UNAUTHORIZED + elif (MSG_NOT_FOUND == failure_details[3]): + return oslo_rootwrap_cmd.RC_NOEXECFOUND + # otherwise we get unexpected exception. + return RC_UNKNOWN_XENAPI_ERROR + + def _get_XenAPI(self): + # Delay importing XenAPI as this module may not exist + # for non-XenServer hypervisors. + if self._XenAPI is None: + import XenAPI + self._XenAPI = XenAPI + return self._XenAPI + + def _this_host(self): + if not self._host: + session = self.get_session() + self._host = session.xenapi.session.get_this_host(session.handle) + return self._host + + def execute(self, cmd, stdin=None): + out = "" + err = "" + if cmd is None or len(cmd) == 0: + err = "No command specified." + return oslo_rootwrap_cmd.RC_NOCOMMAND, out, err + try: + result_raw = self._call_plugin( + 'netwrap', 'run_command', + {'cmd': jsonutils.dumps(cmd), + 'cmd_input': jsonutils.dumps(stdin)}) + result = jsonutils.loads(result_raw) + returncode = result['returncode'] + out = result['out'] + err = result['err'] + return returncode, out, err + except self._get_XenAPI().Failure as failure: + LOG.exception(_LE('Failed to execute command: %s'), cmd) + returncode = self._get_return_code(failure.details) + return returncode, out, err + + def get_session(self): + if self._session is None: + url = cfg.CONF.xenapi.connection_url + username = cfg.CONF.xenapi.connection_username + password = cfg.CONF.xenapi.connection_password + try: + self._session = self._create_session(url, username, password) + except Exception: + # Shouldn't reach here, otherwise it's a fatal error. + LOG.exception(_LE("Failed to initiate XenAPI session")) + raise SystemExit(1) + return self._session diff --git a/neutron/conf/agent/xenapi_conf.py b/neutron/conf/agent/xenapi_conf.py new file mode 100644 index 00000000000..ce9d095a7fa --- /dev/null +++ b/neutron/conf/agent/xenapi_conf.py @@ -0,0 +1,36 @@ +# Copyright 2016 Citrix Systems. +# 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 oslo_config import cfg + +from neutron._i18n import _ + +XENAPI_CONF_SECTION = 'xenapi' + +XENAPI_OPTS = [ + cfg.StrOpt('connection_url', + help=_("URL for connection to XenServer/Xen Cloud Platform.")), + cfg.StrOpt('connection_username', + help=_("Username for connection to XenServer/Xen Cloud " + "Platform.")), + cfg.StrOpt('connection_password', + help=_("Password for connection to XenServer/Xen Cloud " + "Platform."), + secret=True) +] + + +def register_xenapi_opts(cfg=cfg.CONF): + cfg.register_opts(XENAPI_OPTS, group=XENAPI_CONF_SECTION) diff --git a/neutron/opts.py b/neutron/opts.py index 6eba3054f7d..6ea48134d55 100644 --- a/neutron/opts.py +++ b/neutron/opts.py @@ -30,6 +30,7 @@ import neutron.conf.agent.l3.config import neutron.conf.agent.l3.ha import neutron.conf.agent.metadata.config as meta_conf import neutron.conf.agent.ovs_conf +import neutron.conf.agent.xenapi_conf import neutron.conf.cache_utils import neutron.conf.common import neutron.conf.extensions.allowedaddresspairs @@ -274,7 +275,7 @@ def list_ovs_opts(): AGENT_EXT_MANAGER_OPTS) ), ('securitygroup', - neutron.conf.agent.securitygroups_rpc.security_group_opts) + neutron.conf.agent.securitygroups_rpc.security_group_opts), ] @@ -300,3 +301,10 @@ def list_auth_opts(): opt_list.append(plugin_option) opt_list.sort(key=operator.attrgetter('name')) return [(NOVA_GROUP, opt_list)] + + +def list_xenapi_opts(): + return [ + ('xenapi', + neutron.conf.agent.xenapi_conf.XENAPI_OPTS) + ] diff --git a/neutron/plugins/ml2/drivers/openvswitch/agent/ovs_neutron_agent.py b/neutron/plugins/ml2/drivers/openvswitch/agent/ovs_neutron_agent.py index 8f90be7c251..9a967615a1f 100644 --- a/neutron/plugins/ml2/drivers/openvswitch/agent/ovs_neutron_agent.py +++ b/neutron/plugins/ml2/drivers/openvswitch/agent/ovs_neutron_agent.py @@ -40,6 +40,7 @@ from neutron.agent.common import ovs_lib from neutron.agent.common import polling from neutron.agent.common import utils from neutron.agent.l2 import l2_agent_extensions_manager as ext_manager +from neutron.agent.linux import xenapi_root_helper from neutron.agent import rpc as agent_rpc from neutron.agent import securitygroups_rpc as agent_sg_rpc from neutron.api.rpc.callbacks import resources @@ -50,6 +51,7 @@ from neutron.callbacks import registry from neutron.common import config from neutron.common import constants as c_const from neutron.common import topics +from neutron.conf.agent import xenapi_conf from neutron import context from neutron.extensions import portbindings from neutron.plugins.common import constants as p_const @@ -2141,8 +2143,11 @@ def validate_tunnel_config(tunnel_types, local_ip): def prepare_xen_compute(): - is_xen_compute_host = 'rootwrap-xen-dom0' in cfg.CONF.AGENT.root_helper + is_xen_compute_host = 'rootwrap-xen-dom0' in cfg.CONF.AGENT.root_helper \ + or xenapi_root_helper.ROOT_HELPER_DAEMON_TOKEN == \ + cfg.CONF.AGENT.root_helper_daemon if is_xen_compute_host: + xenapi_conf.register_xenapi_opts() # Force ip_lib to always use the root helper to ensure that ip # commands target xen dom0 rather than domU. cfg.CONF.register_opts(ip_lib.OPTS) diff --git a/neutron/plugins/ml2/drivers/openvswitch/agent/xenapi/etc/xapi.d/plugins/netwrap b/neutron/plugins/ml2/drivers/openvswitch/agent/xenapi/etc/xapi.d/plugins/netwrap index 33204cf2f2a..895d40f62ab 100644 --- a/neutron/plugins/ml2/drivers/openvswitch/agent/xenapi/etc/xapi.d/plugins/netwrap +++ b/neutron/plugins/ml2/drivers/openvswitch/agent/xenapi/etc/xapi.d/plugins/netwrap @@ -21,6 +21,7 @@ # XenAPI plugin for executing network commands (ovs, iptables, etc) on dom0 # +import errno import gettext gettext.install('neutron', unicode=1) try: @@ -32,6 +33,9 @@ import subprocess import XenAPIPlugin +MSG_UNAUTHORIZED = "Unauthorized command" +MSG_NOT_FOUND = "Executable not found" + ALLOWED_CMDS = [ 'ip', 'ipset', @@ -58,9 +62,13 @@ def _run_command(cmd, cmd_input): returns anything in stderr, a PluginError is raised with that information. Otherwise, the output from stdout is returned. """ - pipe = subprocess.PIPE - proc = subprocess.Popen(cmd, shell=False, stdin=pipe, stdout=pipe, - stderr=pipe, close_fds=True) + try: + pipe = subprocess.PIPE + proc = subprocess.Popen(cmd, shell=False, stdin=pipe, stdout=pipe, + stderr=pipe, close_fds=True) + except OSError, e: + if e.errno == errno.ENOENT: + raise PluginError(MSG_NOT_FOUND) (out, err) = proc.communicate(cmd_input) return proc.returncode, out, err @@ -68,8 +76,7 @@ def _run_command(cmd, cmd_input): def run_command(session, args): cmd = json.loads(args.get('cmd')) if cmd and cmd[0] not in ALLOWED_CMDS: - msg = _("Dom0 execution of '%s' is not permitted") % cmd[0] - raise PluginError(msg) + raise PluginError(MSG_UNAUTHORIZED) returncode, out, err = _run_command( cmd, json.loads(args.get('cmd_input', 'null'))) if not err: diff --git a/neutron/tests/unit/agent/linux/test_utils.py b/neutron/tests/unit/agent/linux/test_utils.py index 61c8a2b6a7a..8037c7b4f9f 100644 --- a/neutron/tests/unit/agent/linux/test_utils.py +++ b/neutron/tests/unit/agent/linux/test_utils.py @@ -38,6 +38,13 @@ class AgentUtilsExecuteTest(base.BaseTestCase): self.process.return_value.returncode = 0 self.mock_popen = self.process.return_value.communicate + def test_xenapi_root_helper(self): + token = utils.xenapi_root_helper.ROOT_HELPER_DAEMON_TOKEN + self.config(group='AGENT', root_helper_daemon=token) + cmd_client = utils.RootwrapDaemonHelper.get_client() + self.assertIsInstance(cmd_client, + utils.xenapi_root_helper.XenAPIClient) + def test_without_helper(self): expected = "%s\n" % self.test_file self.mock_popen.return_value = [expected, ""] diff --git a/neutron/tests/unit/agent/linux/test_xenapi_root_helper.py b/neutron/tests/unit/agent/linux/test_xenapi_root_helper.py new file mode 100644 index 00000000000..9a073af0efc --- /dev/null +++ b/neutron/tests/unit/agent/linux/test_xenapi_root_helper.py @@ -0,0 +1,93 @@ +# Copyright 2016 Citrix System. +# +# 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 mock +from oslo_config import cfg +from oslo_rootwrap import cmd as oslo_rootwrap_cmd + +from neutron.agent.linux import xenapi_root_helper as helper +from neutron.conf.agent import xenapi_conf +from neutron.tests import base + + +class TestXenapiRootHelper(base.BaseTestCase): + def _get_fake_xenapi_client(self): + class FakeXenapiClient(helper.XenAPIClient): + def __init__(self): + super(FakeXenapiClient, self).__init__() + # Mock XenAPI which may not exist in the unit test env. + self.XenAPI = mock.MagicMock() + + return FakeXenapiClient() + + def setUp(self): + super(TestXenapiRootHelper, self).setUp() + conf = cfg.CONF + xenapi_conf.register_xenapi_opts(conf) + + def test_get_return_code_unauthourized(self): + failure_details = [helper.XENAPI_PLUGIN_FAILURE_ID, + 'run_command', + 'PluginError', + helper.MSG_UNAUTHORIZED] + xenapi_client = self._get_fake_xenapi_client() + rc = xenapi_client._get_return_code(failure_details) + self.assertEqual(oslo_rootwrap_cmd.RC_UNAUTHORIZED, rc) + + def test_get_return_code_noexecfound(self): + failure_details = [helper.XENAPI_PLUGIN_FAILURE_ID, + 'run_command', + 'PluginError', + helper.MSG_NOT_FOUND] + xenapi_client = self._get_fake_xenapi_client() + rc = xenapi_client._get_return_code(failure_details) + self.assertEqual(oslo_rootwrap_cmd.RC_NOEXECFOUND, rc) + + def test_get_return_code_unknown_error(self): + failure_details = [helper.XENAPI_PLUGIN_FAILURE_ID, + 'run_command', + 'PluginError', + 'Any unknown error'] + xenapi_client = self._get_fake_xenapi_client() + rc = xenapi_client._get_return_code(failure_details) + self.assertEqual(helper.RC_UNKNOWN_XENAPI_ERROR, rc) + + def test_execute(self): + cmd = ["ovs-vsctl", "list-ports", "xapi2"] + expect_cmd_args = {'cmd': '["ovs-vsctl", "list-ports", "xapi2"]', + 'cmd_input': 'null'} + raw_result = '{"returncode": 0, "err": "", "out": "vif158.2"}' + + with mock.patch.object(helper.XenAPIClient, "_call_plugin", + return_value=raw_result) as mock_call_plugin: + xenapi_client = self._get_fake_xenapi_client() + rc, out, err = xenapi_client.execute(cmd) + + mock_call_plugin.assert_called_once_with( + 'netwrap', 'run_command', expect_cmd_args) + self.assertEqual(0, rc) + self.assertEqual("vif158.2", out) + self.assertEqual("", err) + + def test_execute_nocommand(self): + cmd = [] + xenapi_client = self._get_fake_xenapi_client() + rc, out, err = xenapi_client.execute(cmd) + self.assertEqual(oslo_rootwrap_cmd.RC_NOCOMMAND, rc) + + def test_get_session_except(self): + xenapi_client = self._get_fake_xenapi_client() + with mock.patch.object(helper.XenAPIClient, "_create_session", + side_effect=Exception()): + self.assertRaises(SystemExit, xenapi_client.get_session) diff --git a/setup.cfg b/setup.cfg index a210ff4810e..6c187ede217 100644 --- a/setup.cfg +++ b/setup.cfg @@ -135,6 +135,7 @@ oslo.config.opts = neutron.ml2.macvtap.agent = neutron.opts:list_macvtap_opts neutron.ml2.ovs.agent = neutron.opts:list_ovs_opts neutron.ml2.sriov.agent = neutron.opts:list_sriov_agent_opts + neutron.ml2.xenapi = neutron.opts:list_xenapi_opts neutron.qos = neutron.opts:list_qos_opts nova.auth = neutron.opts:list_auth_opts oslo.config.opts.defaults =