# Copyright (c) 2015 OpenStack Foundation. # 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. import typing as ty import configparser as ConfigParser import errno import os import sys from eventlet.green import subprocess from neutron.common import config from neutron.common import utils from neutron_lib.utils import helpers from oslo_config import cfg from oslo_log import log as logging from oslo_rootwrap import wrapper from neutron_vpnaas._i18n import _ LOG = logging.getLogger(__name__) def setup_conf() -> cfg.ConfigOpts: cli_opts = [ cfg.DictOpt('mount_paths', required=True, help=_('Dict of paths to bind-mount (source:target) ' 'prior to launch subprocess.')), cfg.ListOpt( 'cmd', required=True, help=_('Command line to execute as a subprocess ' 'provided as comma-separated list of arguments.')), cfg.StrOpt('rootwrap_config', default='/etc/neutron/rootwrap.conf', help=_('Rootwrap configuration file.')), ] conf = cfg.CONF conf.register_cli_opts(cli_opts) return conf def execute(cmd) -> ty.Optional[int]: if not cmd: return None cmd = list(map(str, cmd)) LOG.debug("Running command: %s", cmd) env = os.environ.copy() obj = utils.subprocess_popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env) _stdout, _stderr = obj.communicate() _stdout = helpers.safe_decode_utf8(_stdout) _stderr = helpers.safe_decode_utf8(_stderr) msg = ('Command: %(cmd)s Exit code: %(returncode)s ' 'Stdout: %(stdout)s Stderr: %(stderr)s' % {'cmd': cmd, 'returncode': obj.returncode, 'stdout': _stdout, 'stderr': _stderr}) LOG.debug(msg) obj.stdin.close() # Pass the output to calling process sys.stdout.write(msg) sys.stdout.flush() return obj.returncode def filter_command(command, rootwrap_config): # Load rootwrap configuration try: rawconfig = ConfigParser.RawConfigParser() rawconfig.read(rootwrap_config) rw_config = wrapper.RootwrapConfig(rawconfig) except ValueError as exc: LOG.error('Incorrect value in %(config)s: %(exc)s', {'config': rootwrap_config, 'exc': exc}) sys.exit(errno.EINVAL) except ConfigParser.Error: LOG.error('Incorrect configuration file: %(config)s', {'config': rootwrap_config}) sys.exit(errno.EINVAL) # Check if command matches any of the loaded filters filters = wrapper.load_filters(rw_config.filters_path) try: wrapper.match_filter(filters, command, exec_dirs=rw_config.exec_dirs) except wrapper.FilterMatchNotExecutable as exc: LOG.error('Command %(command)s is not executable: ' '%(path)s (filter match = %(name)s)', {'command': command, 'path': exc.match.exec_path, 'name': exc.match.name}) sys.exit(errno.EINVAL) except wrapper.NoFilterMatched: LOG.error("Unauthorized command: %(cmd)s (no filter matched)", {"cmd": command}) sys.exit(errno.EPERM) def execute_with_mount() -> ty.Optional[int]: config.register_common_config_options() conf = setup_conf() conf() config.setup_logging() if not conf.cmd: LOG.error('No command provided, exiting') return errno.EINVAL if not conf.mount_paths: LOG.error('No mount path provided, exiting') return errno.EINVAL # Both sudoers and rootwrap.conf will not exist in the directory /etc # after bind-mount, so we can't use utils.execute(conf.cmd, # run_as_root=True). That's why we have to check here if cmd matches # CommandFilter filter_command(conf.cmd, conf.rootwrap_config) # Make sure the process is running in net namespace invoked by ip # netns exec(/proc/[pid]/ns/net) which is since Linux 3.0, # as we can't check mount namespace(/proc/[pid]/ns/mnt) # which is since Linux 3.8. For more detail please refer the link # http://man7.org/linux/man-pages/man7/namespaces.7.html if os.path.samefile(os.path.join('/proc/1/ns/net'), os.path.join('/proc', str(os.getpid()), 'ns/net')): LOG.error('Cannot run without netns, exiting') return errno.EINVAL for path, new_path in conf.mount_paths.items(): if not os.path.isdir(new_path): # Sometimes all directories are not ready LOG.debug('%s is not directory', new_path) continue if os.path.isdir(path) and os.path.isabs(path): return_code = execute(['mount', '--bind', new_path, path]) if return_code == 0: LOG.info('%(new_path)s has been ' 'bind-mounted in %(path)s', {'new_path': new_path, 'path': path}) else: LOG.error('Failed to bind-mount ' '%(new_path)s in %(path)s', {'new_path': new_path, 'path': path}) return execute(conf.cmd) def main(): sys.exit(execute_with_mount()) if __name__ == "__main__": main()