XenAPI: Support daemon mode for rootwrap
For Neutron's compute agent in a XenServer's compute node, the commands actually need run in Dom0. Currently XenServer only supports rootwrap for that purpose by invoking a script which invokes XenAPI to execute commands in dom0. There are much performance overhead due to it requires parsing on the script and the configuration file every time running commands. This change is to support daemon mode with which each agent service will call XenAPI directly to execute commands in dom0. And it will keep the single XenAPI session. DocImpact: Need update the following configuration. file: /etc/neutron/plugins/ml2/openvswitch_agent.ini [agent] root_helper_daemon = xenapi_root_helper [xenapi] connection_url = http://169.254.0.1 connection_username = root connection_password = xenroot Closes-Bug: #1585510 Change-Id: I684034359fe0571bc92dbcf342a9821553b1da35
This commit is contained in:
parent
3987159db8
commit
8047da17db
@ -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
|
||||
|
@ -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 = [
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
120
neutron/agent/linux/xenapi_root_helper.py
Normal file
120
neutron/agent/linux/xenapi_root_helper.py
Normal file
@ -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
|
36
neutron/conf/agent/xenapi_conf.py
Normal file
36
neutron/conf/agent/xenapi_conf.py
Normal file
@ -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)
|
@ -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)
|
||||
]
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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, ""]
|
||||
|
93
neutron/tests/unit/agent/linux/test_xenapi_root_helper.py
Normal file
93
neutron/tests/unit/agent/linux/test_xenapi_root_helper.py
Normal file
@ -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)
|
@ -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 =
|
||||
|
Loading…
Reference in New Issue
Block a user