Merge "XenAPI: Support daemon mode for rootwrap"

This commit is contained in:
Jenkins 2017-01-21 02:18:20 +00:00 committed by Gerrit Code Review
commit 86d47bad2d
11 changed files with 297 additions and 10 deletions

View File

@ -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

View File

@ -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 = [

View File

@ -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

View 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

View 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)

View File

@ -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)
]

View File

@ -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)

View File

@ -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:

View File

@ -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, ""]

View 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)

View File

@ -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 =