Merge "XenAPI: Support daemon mode for rootwrap"
This commit is contained in:
commit
86d47bad2d
@ -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