Retire repo
This repo was created by accident, use deb-python-oslo.rootwrap instead. Needed-By: I1ac1a06931c8b6dd7c2e73620a0302c29e605f03 Change-Id: I81894aea69b9d09b0977039623c26781093a397a
This commit is contained in:
parent
a46b731c8a
commit
ccd2d839eb
17
.gitignore
vendored
17
.gitignore
vendored
@ -1,17 +0,0 @@
|
||||
AUTHORS
|
||||
ChangeLog
|
||||
*~
|
||||
*.swp
|
||||
*.pyc
|
||||
*.log
|
||||
.tox
|
||||
.eggs*
|
||||
.coverage
|
||||
oslo.rootwrap.egg-info/
|
||||
build/
|
||||
doc/build/
|
||||
doc/source/api/
|
||||
dist/
|
||||
.testrepository/
|
||||
.project
|
||||
.pydevproject
|
@ -1,4 +0,0 @@
|
||||
[gerrit]
|
||||
host=review.openstack.org
|
||||
port=29418
|
||||
project=openstack/oslo.rootwrap.git
|
@ -1,4 +0,0 @@
|
||||
[DEFAULT]
|
||||
test_command=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 OS_TEST_TIMEOUT=60 ${PYTHON:-python} -m subunit.run discover -t ./ . $LISTOPT $IDOPTION
|
||||
test_id_option=--load-list $IDFILE
|
||||
test_list_option=--list
|
@ -1,16 +0,0 @@
|
||||
If you would like to contribute to the development of OpenStack,
|
||||
you must follow the steps in this page:
|
||||
|
||||
http://docs.openstack.org/infra/manual/developers.html
|
||||
|
||||
Once those steps have been completed, changes to OpenStack
|
||||
should be submitted for review via the Gerrit tool, following
|
||||
the workflow documented at:
|
||||
|
||||
http://docs.openstack.org/infra/manual/developers.html#development-workflow
|
||||
|
||||
Pull requests submitted through GitHub will be ignored.
|
||||
|
||||
Bugs should be filed on Launchpad, not GitHub:
|
||||
|
||||
https://bugs.launchpad.net/oslo.rootwrap
|
204
LICENSE
204
LICENSE
@ -1,204 +0,0 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
--- License for python-keystoneclient versions prior to 2.1 ---
|
||||
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of this project nor the names of its contributors may
|
||||
be used to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
19
README.rst
19
README.rst
@ -1,19 +0,0 @@
|
||||
===============================================
|
||||
oslo.rootwrap -- Escalated Permission Control
|
||||
===============================================
|
||||
|
||||
.. image:: https://img.shields.io/pypi/v/oslo.rootwrap.svg
|
||||
:target: https://pypi.python.org/pypi/oslo.rootwrap/
|
||||
:alt: Latest Version
|
||||
|
||||
.. image:: https://img.shields.io/pypi/dm/oslo.rootwrap.svg
|
||||
:target: https://pypi.python.org/pypi/oslo.rootwrap/
|
||||
:alt: Downloads
|
||||
|
||||
oslo.rootwrap allows fine-grained filtering of shell commands to run
|
||||
as `root` from OpenStack services.
|
||||
|
||||
* License: Apache License, Version 2.0
|
||||
* Documentation: http://docs.openstack.org/developer/oslo.rootwrap
|
||||
* Source: http://git.openstack.org/cgit/openstack/oslo.rootwrap
|
||||
* Bugs: http://bugs.launchpad.net/oslo.rootwrap
|
13
README.txt
Normal file
13
README.txt
Normal file
@ -0,0 +1,13 @@
|
||||
This project is no longer maintained.
|
||||
|
||||
The contents of this repository are still available in the Git
|
||||
source code management system. To see the contents of this
|
||||
repository before it reached its end of life, please check out the
|
||||
previous commit with "git checkout HEAD^1".
|
||||
|
||||
Use instead the project deb-python-oslo.rootwrap at
|
||||
http://git.openstack.org/cgit/openstack/deb-python-oslo.rootwrap .
|
||||
|
||||
For any further questions, please email
|
||||
openstack-dev@lists.openstack.org or join #openstack-dev on
|
||||
Freenode.
|
@ -1,109 +0,0 @@
|
||||
# Copyright (c) 2014 Mirantis Inc.
|
||||
# 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 __future__ import print_function
|
||||
|
||||
import atexit
|
||||
import math
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import timeit
|
||||
|
||||
from oslo_rootwrap import client
|
||||
|
||||
config_path = "rootwrap.conf"
|
||||
num_iterations = 100
|
||||
|
||||
|
||||
def run_plain(cmd):
|
||||
obj = subprocess.Popen(cmd,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
out, err = obj.communicate()
|
||||
return obj.returncode, out, err
|
||||
|
||||
|
||||
def run_sudo(cmd):
|
||||
return run_plain(["sudo"] + cmd)
|
||||
|
||||
|
||||
def run_rootwrap(cmd):
|
||||
return run_plain([
|
||||
"sudo", sys.executable, "-c",
|
||||
"from oslo_rootwrap import cmd; cmd.main()", config_path] + cmd)
|
||||
|
||||
|
||||
run_daemon = client.Client([
|
||||
"sudo", sys.executable, "-c",
|
||||
"from oslo_rootwrap import cmd; cmd.daemon()", config_path]).execute
|
||||
|
||||
|
||||
def run_one(runner, cmd):
|
||||
def __inner():
|
||||
code, out, err = runner(cmd)
|
||||
assert err == "", "Stderr not empty:\n" + err
|
||||
assert code == 0, "Command failed"
|
||||
return __inner
|
||||
|
||||
runners = [
|
||||
("{0}", run_plain),
|
||||
("sudo {0}", run_sudo),
|
||||
("sudo rootwrap conf {0}", run_rootwrap),
|
||||
("daemon.run('{0}')", run_daemon),
|
||||
]
|
||||
|
||||
|
||||
def get_time_string(sec):
|
||||
if sec > 0.9:
|
||||
return "{0:7.3f}s ".format(sec)
|
||||
elif sec > 0.0009:
|
||||
return "{0:7.3f}ms".format(sec * 1000.0)
|
||||
else:
|
||||
return "{0:7.3f}us".format(sec * 1000000.0)
|
||||
|
||||
|
||||
def run_bench(cmd, runners):
|
||||
strcmd = ' '.join(cmd)
|
||||
max_name_len = max(len(name) for name, _ in runners) + len(strcmd) - 3
|
||||
print("Running '{0}':".format(strcmd))
|
||||
print("{0:^{1}} :".format("method", max_name_len),
|
||||
"".join(map("{0:^10}".format, ["min", "avg", "max", "dev"])))
|
||||
for name, runner in runners:
|
||||
results = timeit.repeat(run_one(runner, cmd), repeat=num_iterations,
|
||||
number=1)
|
||||
avg = sum(results) / num_iterations
|
||||
min_ = min(results)
|
||||
max_ = max(results)
|
||||
dev = math.sqrt(sum((r - avg) ** 2 for r in results) / num_iterations)
|
||||
print("{0:>{1}} :".format(name.format(strcmd), max_name_len),
|
||||
" ".join(map(get_time_string, [min_, avg, max_, dev])))
|
||||
|
||||
|
||||
def main():
|
||||
os.chdir(os.path.dirname(__file__))
|
||||
code, _, _ = run_sudo(["-vn"])
|
||||
if code:
|
||||
print("We need you to authorize with sudo to run this benchmark")
|
||||
run_sudo(["-v"])
|
||||
|
||||
run_bench(["ip", "a"], runners)
|
||||
run_sudo(["ip", "netns", "add", "bench_ns"])
|
||||
atexit.register(run_sudo, ["ip", "netns", "delete", "bench_ns"])
|
||||
run_bench('ip netns exec bench_ns ip a'.split(), runners[1:])
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@ -1,4 +0,0 @@
|
||||
[Filters]
|
||||
|
||||
ip: IpFilter, ip, root
|
||||
ip_exec: IpNetnsExecFilter, ip, root
|
@ -1,4 +0,0 @@
|
||||
[DEFAULT]
|
||||
filters_path=filters.d
|
||||
exec_dirs=/sbin,/usr/sbin,/bin,/usr/bin
|
||||
use_syslog=False
|
@ -1,75 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# 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 os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.abspath('../..'))
|
||||
# -- General configuration ----------------------------------------------------
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
#'sphinx.ext.intersphinx',
|
||||
'oslosphinx'
|
||||
]
|
||||
|
||||
# autodoc generation is a bit aggressive and a nuisance when doing heavy
|
||||
# text edit cycles.
|
||||
# execute "export SPHINX_DEBUG=1" in your terminal to disable
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'oslo.rootwrap'
|
||||
copyright = u'2014, OpenStack Foundation'
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
add_module_names = True
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# -- Options for HTML output --------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. Major themes that come with
|
||||
# Sphinx are currently 'default' and 'sphinxdoc'.
|
||||
# html_theme_path = ["."]
|
||||
# html_theme = '_theme'
|
||||
# html_static_path = ['static']
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = '%sdoc' % project
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title, author, documentclass
|
||||
# [howto/manual]).
|
||||
latex_documents = [
|
||||
('index',
|
||||
'%s.tex' % project,
|
||||
u'%s Documentation' % project,
|
||||
u'OpenStack Foundation', 'manual'),
|
||||
]
|
||||
|
||||
# Example configuration for intersphinx: refer to the Python standard library.
|
||||
#intersphinx_mapping = {'http://docs.python.org/': None}
|
@ -1,5 +0,0 @@
|
||||
=============
|
||||
Contributing
|
||||
=============
|
||||
|
||||
.. include:: ../../CONTRIBUTING.rst
|
@ -1 +0,0 @@
|
||||
.. include:: ../../ChangeLog
|
@ -1,28 +0,0 @@
|
||||
===============================================
|
||||
oslo.rootwrap -- Escalated Permission Control
|
||||
===============================================
|
||||
|
||||
oslo.rootwrap allows fine-grained filtering of shell commands to run
|
||||
as `root` from OpenStack services.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
installation
|
||||
usage
|
||||
contributing
|
||||
|
||||
Release Notes
|
||||
=============
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
history
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
@ -1,12 +0,0 @@
|
||||
============
|
||||
Installation
|
||||
============
|
||||
|
||||
At the command line::
|
||||
|
||||
$ pip install oslo.rootwrap
|
||||
|
||||
Or, if you have virtualenvwrapper installed::
|
||||
|
||||
$ mkvirtualenv oslo.rootwrap
|
||||
$ pip install oslo.rootwrap
|
@ -1,338 +0,0 @@
|
||||
=====
|
||||
Usage
|
||||
=====
|
||||
|
||||
Rootwrap should be used as a separate Python process calling the
|
||||
``oslo_rootwrap.cmd:main`` function. You can set up a specific console_script
|
||||
calling into ``oslo_rootwrap.cmd:main``, called for example ``nova-rootwrap``.
|
||||
To keep things simple, this document will consider that your console_script
|
||||
is called ``/usr/bin/nova-rootwrap``.
|
||||
|
||||
The rootwrap command line should be called under `sudo`. It's first parameter
|
||||
is the configuration file to use, and the remainder of the parameters are the
|
||||
command line to execute:
|
||||
|
||||
::
|
||||
|
||||
sudo nova-rootwrap ROOTWRAP_CONFIG COMMAND_LINE
|
||||
|
||||
|
||||
How rootwrap works
|
||||
==================
|
||||
|
||||
OpenStack services generally run under a specific, unprivileged user. However,
|
||||
sometimes they need to run a command as ``root``. Instead of just calling
|
||||
``sudo make me a sandwich`` and have a blanket ``sudoers`` permission to always
|
||||
escalate rights from their unprivileged users to ``root``, those services can
|
||||
call ``sudo nova-rootwrap /etc/nova/rootwrap.conf make me a sandwich``.
|
||||
|
||||
A sudoers entry lets the unprivileged user run ``nova-rootwrap`` as ``root``.
|
||||
``nova-rootwrap`` looks for filter definition directories in its configuration
|
||||
file, and loads command filters from them. Then it checks if the command
|
||||
requested by the OpenStack service matches one of those filters, in which
|
||||
case it executes the command (as ``root``). If no filter matches, it denies
|
||||
the request. This allows for complex filtering of allowed commands, as well
|
||||
as shipping filter definitions together with the OpenStack code that needs
|
||||
them.
|
||||
|
||||
Security model
|
||||
==============
|
||||
|
||||
The escalation path is fully controlled by the ``root`` user. A ``sudoers`` entry
|
||||
(owned by ``root``) allows the unprivileged user to run (as ``root``) a specific
|
||||
rootwrap executable, and only with a specific configuration file (which should
|
||||
be owned by ``root``) as its first parameter.
|
||||
|
||||
``nova-rootwrap`` imports the Python modules it needs from a cleaned (and
|
||||
system-default) ``PYTHONPATH``. The configuration file points to root-owned
|
||||
filter definition directories, which contain root-owned filters definition
|
||||
files. This chain ensures that the unprivileged user itself is never in
|
||||
control of the configuration or modules used by the ``nova-rootwrap`` executable.
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
All nodes wishing to run ``nova-rootwrap`` should contain a ``sudoers`` entry that
|
||||
lets the unprivileged user run ``nova-rootwrap`` as ``root``, pointing to the
|
||||
root-owned ``rootwrap.conf`` configuration file and allowing any parameter
|
||||
after that. For example, Nova nodes should have this line in their ``sudoers``
|
||||
file, to allow the ``nova`` user to call ``sudo nova-rootwrap``::
|
||||
|
||||
nova ALL = (root) NOPASSWD: /usr/bin/nova-rootwrap /etc/nova/rootwrap.conf *
|
||||
|
||||
Then the node also should ship the filter definitions corresponding to its
|
||||
usage of ``nova-rootwrap``. You should not install any other filters file on
|
||||
that node, otherwise you would allow extra unneeded commands to be run as
|
||||
``root``.
|
||||
|
||||
The filter file(s) corresponding to the node must be installed in one of the
|
||||
filters_path directories. For example, on Nova compute nodes, you should only
|
||||
have ``compute.filters`` installed. The file should be owned and writeable only
|
||||
by the ``root`` user.
|
||||
|
||||
Rootwrap configuration
|
||||
======================
|
||||
|
||||
The ``rootwrap.conf`` file is used to influence how ``nova-rootwrap`` works. Since
|
||||
it's in the trusted security path, it needs to be owned and writeable only by
|
||||
the ``root`` user. Its location is specified in the ``sudoers`` entry, and must be
|
||||
provided on ``nova-rootwrap`` command line as its first argument.
|
||||
|
||||
``rootwrap.conf`` uses an *INI* file format with the following sections and
|
||||
parameters:
|
||||
|
||||
[DEFAULT] section
|
||||
-----------------
|
||||
|
||||
filters_path
|
||||
Comma-separated list of directories containing filter definition files.
|
||||
All directories listed must be owned and only writeable by ``root``.
|
||||
This is the only mandatory parameter.
|
||||
Example:
|
||||
``filters_path=/etc/nova/rootwrap.d,/usr/share/nova/rootwrap``
|
||||
|
||||
exec_dirs
|
||||
Comma-separated list of directories to search executables in, in case
|
||||
filters do not explicitly specify a full path. If not specified, defaults
|
||||
to the system ``PATH`` environment variable. All directories listed must be
|
||||
owned and only writeable by ``root``. Example:
|
||||
``exec_dirs=/sbin,/usr/sbin,/bin,/usr/bin``
|
||||
|
||||
use_syslog
|
||||
Enable logging to syslog. Default value is False. Example:
|
||||
``use_syslog=True``
|
||||
|
||||
syslog_log_facility
|
||||
Which syslog facility to use for syslog logging. Valid values include
|
||||
``auth``, ``authpriv``, ``syslog``, ``user0``, ``user1``...
|
||||
Default value is ``syslog``. Example:
|
||||
``syslog_log_facility=syslog``
|
||||
|
||||
syslog_log_level
|
||||
Which messages to log. ``INFO`` means log all usage, ``ERROR`` means only log
|
||||
unsuccessful attempts. Example:
|
||||
``syslog_log_level=ERROR``
|
||||
|
||||
.filters files
|
||||
==============
|
||||
|
||||
Filters definition files contain lists of filters that ``nova-rootwrap`` will
|
||||
use to allow or deny a specific command. They are generally suffixed by
|
||||
``.filters``. Since they are in the trusted security path, they need to be
|
||||
owned and writeable only by the ``root`` user. Their location is specified
|
||||
in the ``rootwrap.conf`` file.
|
||||
|
||||
It uses an *INI* file format with a ``[Filters]`` section and several lines,
|
||||
each with a unique parameter name (different for each filter you define):
|
||||
|
||||
[Filters] section
|
||||
-----------------
|
||||
|
||||
filter_name (different for each filter)
|
||||
Comma-separated list containing first the Filter class to use, followed
|
||||
by that Filter arguments (which vary depending on the Filter class
|
||||
selected). Example:
|
||||
``kpartx: CommandFilter, /sbin/kpartx, root``
|
||||
|
||||
|
||||
Available filter classes
|
||||
========================
|
||||
|
||||
CommandFilter
|
||||
-------------
|
||||
|
||||
Basic filter that only checks the executable called. Parameters are:
|
||||
|
||||
1. Executable allowed
|
||||
2. User to run the command under
|
||||
|
||||
Example: allow to run kpartx as the root user, with any parameters::
|
||||
|
||||
kpartx: CommandFilter, kpartx, root
|
||||
|
||||
RegExpFilter
|
||||
------------
|
||||
|
||||
Generic filter that checks the executable called, then uses a list of regular
|
||||
expressions to check all subsequent arguments. Parameters are:
|
||||
|
||||
1. Executable allowed
|
||||
2. User to run the command under
|
||||
3. (and following) Regular expressions to use to match first (and subsequent)
|
||||
command arguments
|
||||
|
||||
Example: allow to run ``/usr/sbin/tunctl``, but only with three parameters with
|
||||
the first two being -b and -t::
|
||||
|
||||
tunctl: RegExpFilter, /usr/sbin/tunctl, root, tunctl, -b, -t, .*
|
||||
|
||||
PathFilter
|
||||
----------
|
||||
|
||||
Generic filter that lets you check that paths provided as parameters fall
|
||||
under a given directory. Parameters are:
|
||||
|
||||
1. Executable allowed
|
||||
2. User to run the command under
|
||||
3. (and following) Command arguments.
|
||||
|
||||
There are three types of command arguments: ``pass`` will accept any parameter
|
||||
value, a string will only accept the corresponding string as a parameter,
|
||||
except if the string starts with '/' in which case it will accept any path
|
||||
that resolves under the corresponding directory.
|
||||
|
||||
Example: allow to chown to the 'nova' user any file under /var/lib/images::
|
||||
|
||||
chown: PathFilter, /bin/chown, root, nova, /var/lib/images
|
||||
|
||||
EnvFilter
|
||||
---------
|
||||
|
||||
Filter allowing extra environment variables to be set by the calling code.
|
||||
Parameters are:
|
||||
|
||||
1. ``env``
|
||||
2. User to run the command under
|
||||
3. (and following) name of the environment variables that can be set,
|
||||
suffixed by ``=``
|
||||
4. Executable allowed
|
||||
|
||||
Example: allow to run ``CONFIG_FILE=foo NETWORK_ID=bar dnsmasq ...`` as root::
|
||||
|
||||
dnsmasq: EnvFilter, env, root, CONFIG_FILE=, NETWORK_ID=, dnsmasq
|
||||
|
||||
ReadFileFilter
|
||||
--------------
|
||||
|
||||
Specific filter that lets you read files as ``root`` using ``cat``.
|
||||
Parameters are:
|
||||
|
||||
1. Path to the file that you want to read as the ``root`` user.
|
||||
|
||||
Example: allow to run ``cat /etc/iscsi/initiatorname.iscsi`` as ``root``::
|
||||
|
||||
read_initiator: ReadFileFilter, /etc/iscsi/initiatorname.iscsi
|
||||
|
||||
KillFilter
|
||||
----------
|
||||
|
||||
Kill-specific filter that checks the affected process and the signal sent
|
||||
before allowing the command. Parameters are:
|
||||
|
||||
1. User to run ``kill`` under
|
||||
2. Only affect processes running that executable
|
||||
3. (and following) Signals you're allowed to send
|
||||
|
||||
Example: allow to send ``-9`` or ``-HUP`` signals to
|
||||
``/usr/sbin/dnsmasq`` processes::
|
||||
|
||||
kill_dnsmasq: KillFilter, root, /usr/sbin/dnsmasq, -9, -HUP
|
||||
|
||||
IpFilter
|
||||
--------
|
||||
|
||||
ip-specific filter that allows to run any ``ip`` command, except for ``ip netns``
|
||||
(in which case it only allows the list, add and delete subcommands).
|
||||
Parameters are:
|
||||
|
||||
1. ``ip``
|
||||
2. User to run ``ip`` under
|
||||
|
||||
Example: allow to run any ``ip`` command except ``ip netns exec`` and
|
||||
``ip netns monitor``::
|
||||
|
||||
ip: IpFilter, ip, root
|
||||
|
||||
IpNetnsExecFilter
|
||||
-----------------
|
||||
|
||||
ip-specific filter that allows to run any otherwise-allowed command under
|
||||
``ip netns exec``. The command specified to ``ip netns exec`` must match another
|
||||
filter for this filter to accept it. Parameters are:
|
||||
|
||||
1. ``ip``
|
||||
2. User to run ``ip`` under
|
||||
|
||||
Example: allow to run ``ip netns exec <namespace> <command>`` as long as
|
||||
``<command>`` matches another filter::
|
||||
|
||||
ip: IpNetnsExecFilter, ip, root
|
||||
|
||||
ChainingRegExpFilter
|
||||
--------------------
|
||||
|
||||
Filter that allows to run the prefix command, if the beginning of its arguments
|
||||
match to a list of regular expressions, and if remaining arguments are any
|
||||
otherwise-allowed command. Parameters are:
|
||||
|
||||
1. Executable allowed
|
||||
2. User to run the command under
|
||||
3. (and following) Regular expressions to use to match first (and subsequent)
|
||||
command arguments.
|
||||
|
||||
This filter regards the length of the regular expressions list as the number of
|
||||
arguments to be checked, and remaining parts are checked by other filters.
|
||||
|
||||
Example: allow to run ``/usr/bin/nice``, but only with first two parameters being
|
||||
-n and integer, and followed by any allowed command by the other filters::
|
||||
|
||||
nice: ChainingRegExpFilter, /usr/bin/nice, root, nice, -n, -?\d+
|
||||
|
||||
Note: this filter can't be used to impose that the subcommand is always run
|
||||
under the prefix command. In particular, it can't enforce that a particular
|
||||
command is only run under "nice", since the subcommand can explicitly be
|
||||
called directly.
|
||||
|
||||
|
||||
Calling rootwrap from OpenStack services
|
||||
========================================
|
||||
|
||||
Standalone mode (``sudo`` way)
|
||||
------------------------------
|
||||
|
||||
The ``oslo.processutils`` library ships with a convenience ``execute()`` function
|
||||
that can be used to call shell commands as ``root``, if you call it with the
|
||||
following parameters::
|
||||
|
||||
run_as_root=True
|
||||
|
||||
root_helper='sudo nova-rootwrap /etc/nova/rootwrap.conf
|
||||
|
||||
NB: Some services ship with a ``utils.execute()`` convenience function that
|
||||
automatically sets ``root_helper`` based on the value of a ``rootwrap_config``
|
||||
parameter, so only ``run_as_root=True`` needs to be set.
|
||||
|
||||
If you want to call as ``root`` a previously-unauthorized command, you will also
|
||||
need to modify the filters (generally shipped in the source tree under
|
||||
``etc/rootwrap.d`` so that the command you want to run as ``root`` will actually
|
||||
be allowed by ``nova-rootwrap``.
|
||||
|
||||
Daemon mode
|
||||
-----------
|
||||
|
||||
Since 1.3.0 version ``oslo.rootwrap`` supports "daemon mode". In this mode
|
||||
rootwrap would start, read config file and wait for commands to be run with
|
||||
root privileges. All communications with the daemon should go through
|
||||
``Client`` class that resides in ``oslo_rootwrap.client`` module.
|
||||
|
||||
Its constructor expects one argument - a list that can be passed to ``Popen``
|
||||
to create rootwrap daemon process. For ``root_helper`` above it will be
|
||||
``["sudo", "nova-rootwrap-daemon", "/etc/neutron/rootwrap.conf"]``,
|
||||
for example. Note that it uses a separate script that points to
|
||||
``oslo_rootwrap.cmd:daemon`` endpoint (instead of ``:main``).
|
||||
|
||||
The class provides one method ``execute`` with following arguments:
|
||||
|
||||
* ``userargs`` - list of command line arguments that are to be used to run the
|
||||
command;
|
||||
* ``stdin`` - string to be passed to standard input of child process.
|
||||
|
||||
The method returns 3-tuple containing:
|
||||
|
||||
* return code of child process;
|
||||
* string containing everything captured from its stdout stream;
|
||||
* string containing everything captured from its stderr stream.
|
||||
|
||||
The class lazily creates an instance of the daemon, connects to it and passes
|
||||
arguments. This daemon can die or be killed, ``Client`` will respawn it and/or
|
||||
reconnect to it as necessary.
|
@ -1,27 +0,0 @@
|
||||
# Configuration for rootwrap
|
||||
# This file should be owned by (and only-writeable by) the root user
|
||||
|
||||
[DEFAULT]
|
||||
# List of directories to load filter definitions from (separated by ',').
|
||||
# These directories MUST all be only writeable by root !
|
||||
filters_path=/etc/oslo-rootwrap/filters.d,/usr/share/oslo-rootwrap
|
||||
|
||||
# List of directories to search executables in, in case filters do not
|
||||
# explicitly specify a full path (separated by ',')
|
||||
# If not specified, defaults to system PATH environment variable.
|
||||
# These directories MUST all be only writeable by root !
|
||||
exec_dirs=/sbin,/usr/sbin,/bin,/usr/bin
|
||||
|
||||
# Enable logging to syslog
|
||||
# Default value is False
|
||||
use_syslog=False
|
||||
|
||||
# Which syslog facility to use.
|
||||
# Valid values include auth, authpriv, syslog, user0, user1...
|
||||
# Default value is 'syslog'
|
||||
syslog_log_facility=syslog
|
||||
|
||||
# Which messages to log.
|
||||
# INFO means log all usage
|
||||
# ERROR means only log unsuccessful attempts
|
||||
syslog_log_level=ERROR
|
@ -1,30 +0,0 @@
|
||||
# Copyright (c) 2015 Red Hat.
|
||||
# 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 os
|
||||
|
||||
try:
|
||||
import eventlet.patcher
|
||||
except ImportError:
|
||||
_patched_socket = False
|
||||
else:
|
||||
# In tests patching happens later, so we'll rely on environment variable
|
||||
_patched_socket = (eventlet.patcher.is_monkey_patched('socket') or
|
||||
os.environ.get('TEST_EVENTLET', False))
|
||||
|
||||
if not _patched_socket:
|
||||
import subprocess
|
||||
else:
|
||||
from eventlet.green import subprocess # noqa
|
@ -1,137 +0,0 @@
|
||||
# Copyright (c) 2014 Mirantis Inc.
|
||||
# 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 logging
|
||||
from multiprocessing import managers
|
||||
from multiprocessing import util as mp_util
|
||||
import threading
|
||||
import weakref
|
||||
|
||||
import oslo_rootwrap
|
||||
from oslo_rootwrap import daemon
|
||||
from oslo_rootwrap import jsonrpc
|
||||
from oslo_rootwrap import subprocess
|
||||
|
||||
if oslo_rootwrap._patched_socket:
|
||||
# We have to use slow version of recvall with eventlet because of a bug in
|
||||
# GreenSocket.recv_into:
|
||||
# https://bitbucket.org/eventlet/eventlet/pull-request/41
|
||||
# This check happens here instead of jsonrpc to avoid importing eventlet
|
||||
# from daemon code that is run with root privileges.
|
||||
jsonrpc.JsonConnection.recvall = jsonrpc.JsonConnection._recvall_slow
|
||||
|
||||
try:
|
||||
finalize = weakref.finalize
|
||||
except AttributeError:
|
||||
def finalize(obj, func, *args, **kwargs):
|
||||
return mp_util.Finalize(obj, func, args=args, kwargs=kwargs,
|
||||
exitpriority=0)
|
||||
|
||||
ClientManager = daemon.get_manager_class()
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Client(object):
|
||||
def __init__(self, rootwrap_daemon_cmd):
|
||||
self._start_command = rootwrap_daemon_cmd
|
||||
self._initialized = False
|
||||
self._mutex = threading.Lock()
|
||||
self._manager = None
|
||||
self._proxy = None
|
||||
self._process = None
|
||||
self._finalize = None
|
||||
|
||||
def _initialize(self):
|
||||
if self._process is not None and self._process.poll() is not None:
|
||||
LOG.warning("Leaving behind already spawned process with pid %d, "
|
||||
"root should kill it if it's still there (I can't)",
|
||||
self._process.pid)
|
||||
|
||||
process_obj = subprocess.Popen(self._start_command,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
LOG.debug("Popen for %s command has been instantiated",
|
||||
self._start_command)
|
||||
|
||||
self._process = process_obj
|
||||
socket_path = process_obj.stdout.readline()[:-1]
|
||||
# For Python 3 we need to convert bytes to str here
|
||||
if not isinstance(socket_path, str):
|
||||
socket_path = socket_path.decode('utf-8')
|
||||
authkey = process_obj.stdout.read(32)
|
||||
if process_obj.poll() is not None:
|
||||
stderr = process_obj.stderr.read()
|
||||
# NOTE(yorik-sar): don't expose stdout here
|
||||
raise Exception("Failed to spawn rootwrap process.\nstderr:\n%s" %
|
||||
(stderr,))
|
||||
LOG.info("Spawned new rootwrap daemon process with pid=%d",
|
||||
process_obj.pid)
|
||||
self._manager = ClientManager(socket_path, authkey)
|
||||
self._manager.connect()
|
||||
self._proxy = self._manager.rootwrap()
|
||||
self._finalize = finalize(self, self._shutdown, self._process,
|
||||
self._manager)
|
||||
self._initialized = True
|
||||
|
||||
@staticmethod
|
||||
def _shutdown(process, manager, JsonClient=jsonrpc.JsonClient):
|
||||
# Storing JsonClient in arguments because globals are set to None
|
||||
# before executing atexit routines in Python 2.x
|
||||
if process.poll() is None:
|
||||
LOG.info('Stopping rootwrap daemon process with pid=%s',
|
||||
process.pid)
|
||||
try:
|
||||
manager.rootwrap().shutdown()
|
||||
except (EOFError, IOError):
|
||||
pass # assume it is dead already
|
||||
# We might want to wait for process to exit or kill it, but we
|
||||
# can't provide sane timeout on 2.x and we most likely don't have
|
||||
# permisions to do so
|
||||
# Invalidate manager's state so that proxy won't try to do decref
|
||||
manager._state.value = managers.State.SHUTDOWN
|
||||
|
||||
def _ensure_initialized(self):
|
||||
with self._mutex:
|
||||
if not self._initialized:
|
||||
self._initialize()
|
||||
|
||||
def _restart(self, proxy):
|
||||
with self._mutex:
|
||||
assert self._initialized
|
||||
# Verify if someone has already restarted this.
|
||||
if self._proxy is proxy:
|
||||
self._finalize()
|
||||
self._manager = None
|
||||
self._proxy = None
|
||||
self._initialized = False
|
||||
self._initialize()
|
||||
return self._proxy
|
||||
|
||||
def execute(self, cmd, stdin=None):
|
||||
self._ensure_initialized()
|
||||
proxy = self._proxy
|
||||
retry = False
|
||||
try:
|
||||
res = proxy.run_one_command(cmd, stdin)
|
||||
except (EOFError, IOError):
|
||||
retry = True
|
||||
# res can be None if we received final None sent by dying server thread
|
||||
# instead of response to our request. Process is most likely to be dead
|
||||
# at this point.
|
||||
if retry or res is None:
|
||||
proxy = self._restart(proxy)
|
||||
res = proxy.run_one_command(cmd, stdin)
|
||||
return res
|
@ -1,127 +0,0 @@
|
||||
# Copyright (c) 2011 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.
|
||||
|
||||
"""Root wrapper for OpenStack services
|
||||
|
||||
Filters which commands a service is allowed to run as another user.
|
||||
|
||||
To use this with oslo, you should set the following in
|
||||
oslo.conf:
|
||||
rootwrap_config=/etc/oslo/rootwrap.conf
|
||||
|
||||
You also need to let the oslo user run oslo-rootwrap
|
||||
as root in sudoers:
|
||||
oslo ALL = (root) NOPASSWD: /usr/bin/oslo-rootwrap
|
||||
/etc/oslo/rootwrap.conf *
|
||||
|
||||
Service packaging should deploy .filters files only on nodes where
|
||||
they are needed, to avoid allowing more than is necessary.
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from six import moves
|
||||
|
||||
from oslo_rootwrap import wrapper
|
||||
|
||||
RC_UNAUTHORIZED = 99
|
||||
RC_NOCOMMAND = 98
|
||||
RC_BADCONFIG = 97
|
||||
RC_NOEXECFOUND = 96
|
||||
SIGNAL_BASE = 128
|
||||
|
||||
|
||||
def _exit_error(execname, message, errorcode, log=True):
|
||||
print("%s: %s" % (execname, message), file=sys.stderr)
|
||||
if log:
|
||||
logging.error(message)
|
||||
sys.exit(errorcode)
|
||||
|
||||
|
||||
def daemon():
|
||||
return main(run_daemon=True)
|
||||
|
||||
|
||||
def main(run_daemon=False):
|
||||
# Split arguments, require at least a command
|
||||
execname = sys.argv.pop(0)
|
||||
if run_daemon:
|
||||
if len(sys.argv) != 1:
|
||||
_exit_error(execname, "Extra arguments to daemon", RC_NOCOMMAND,
|
||||
log=False)
|
||||
else:
|
||||
if len(sys.argv) < 2:
|
||||
_exit_error(execname, "No command specified", RC_NOCOMMAND,
|
||||
log=False)
|
||||
|
||||
configfile = sys.argv.pop(0)
|
||||
|
||||
# Load configuration
|
||||
try:
|
||||
rawconfig = moves.configparser.RawConfigParser()
|
||||
rawconfig.read(configfile)
|
||||
config = wrapper.RootwrapConfig(rawconfig)
|
||||
except ValueError as exc:
|
||||
msg = "Incorrect value in %s: %s" % (configfile, exc.args[0])
|
||||
_exit_error(execname, msg, RC_BADCONFIG, log=False)
|
||||
except moves.configparser.Error:
|
||||
_exit_error(execname, "Incorrect configuration file: %s" % configfile,
|
||||
RC_BADCONFIG, log=False)
|
||||
|
||||
if config.use_syslog:
|
||||
wrapper.setup_syslog(execname,
|
||||
config.syslog_log_facility,
|
||||
config.syslog_log_level)
|
||||
|
||||
filters = wrapper.load_filters(config.filters_path)
|
||||
|
||||
if run_daemon:
|
||||
# NOTE(dims): When not running as daemon, this import
|
||||
# slows us down just a bit. So moving it here so we have
|
||||
# it only when we need it.
|
||||
from oslo_rootwrap import daemon as daemon_mod
|
||||
daemon_mod.daemon_start(config, filters)
|
||||
else:
|
||||
run_one_command(execname, config, filters, sys.argv)
|
||||
|
||||
|
||||
def run_one_command(execname, config, filters, userargs):
|
||||
# Execute command if it matches any of the loaded filters
|
||||
try:
|
||||
obj = wrapper.start_subprocess(
|
||||
filters, userargs,
|
||||
exec_dirs=config.exec_dirs,
|
||||
log=config.use_syslog,
|
||||
stdin=sys.stdin,
|
||||
stdout=sys.stdout,
|
||||
stderr=sys.stderr)
|
||||
returncode = obj.wait()
|
||||
# Fix returncode of Popen
|
||||
if returncode < 0:
|
||||
returncode = SIGNAL_BASE - returncode
|
||||
sys.exit(returncode)
|
||||
|
||||
except wrapper.FilterMatchNotExecutable as exc:
|
||||
msg = ("Executable not found: %s (filter match = %s)"
|
||||
% (exc.match.exec_path, exc.match.name))
|
||||
_exit_error(execname, msg, RC_NOEXECFOUND, log=config.use_syslog)
|
||||
|
||||
except wrapper.NoFilterMatched:
|
||||
msg = ("Unauthorized command: %s (no filter matched)"
|
||||
% ' '.join(userargs))
|
||||
_exit_error(execname, msg, RC_UNAUTHORIZED, log=config.use_syslog)
|
@ -1,155 +0,0 @@
|
||||
# Copyright (c) 2014 Mirantis Inc.
|
||||
# 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 __future__ import print_function
|
||||
|
||||
import functools
|
||||
import logging
|
||||
from multiprocessing import managers
|
||||
import os
|
||||
import shutil
|
||||
import signal
|
||||
import six
|
||||
import stat
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
|
||||
from oslo_rootwrap import jsonrpc
|
||||
from oslo_rootwrap import subprocess
|
||||
from oslo_rootwrap import wrapper
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
# Since multiprocessing supports only pickle and xmlrpclib for serialization of
|
||||
# RPC requests and responses, we declare another 'jsonrpc' serializer
|
||||
|
||||
managers.listener_client['jsonrpc'] = jsonrpc.JsonListener, jsonrpc.JsonClient
|
||||
|
||||
|
||||
class RootwrapClass(object):
|
||||
def __init__(self, config, filters):
|
||||
self.config = config
|
||||
self.filters = filters
|
||||
|
||||
def run_one_command(self, userargs, stdin=None):
|
||||
obj = wrapper.start_subprocess(
|
||||
self.filters, userargs,
|
||||
exec_dirs=self.config.exec_dirs,
|
||||
log=self.config.use_syslog,
|
||||
close_fds=True,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
if six.PY3 and stdin is not None:
|
||||
stdin = os.fsencode(stdin)
|
||||
out, err = obj.communicate(stdin)
|
||||
if six.PY3:
|
||||
out = os.fsdecode(out)
|
||||
err = os.fsdecode(err)
|
||||
return obj.returncode, out, err
|
||||
|
||||
def shutdown(self):
|
||||
# Suicide to force break of the main thread
|
||||
os.kill(os.getpid(), signal.SIGINT)
|
||||
|
||||
|
||||
def get_manager_class(config=None, filters=None):
|
||||
class RootwrapManager(managers.BaseManager):
|
||||
def __init__(self, address=None, authkey=None):
|
||||
# Force jsonrpc because neither pickle nor xmlrpclib is secure
|
||||
super(RootwrapManager, self).__init__(address, authkey,
|
||||
serializer='jsonrpc')
|
||||
|
||||
if config is not None:
|
||||
partial_class = functools.partial(RootwrapClass, config, filters)
|
||||
RootwrapManager.register('rootwrap', partial_class)
|
||||
else:
|
||||
RootwrapManager.register('rootwrap')
|
||||
|
||||
return RootwrapManager
|
||||
|
||||
|
||||
def daemon_start(config, filters):
|
||||
temp_dir = tempfile.mkdtemp(prefix='rootwrap-')
|
||||
LOG.debug("Created temporary directory %s", temp_dir)
|
||||
try:
|
||||
# allow everybody to find the socket
|
||||
rwxr_xr_x = (stat.S_IRWXU |
|
||||
stat.S_IRGRP | stat.S_IXGRP |
|
||||
stat.S_IROTH | stat.S_IXOTH)
|
||||
os.chmod(temp_dir, rwxr_xr_x)
|
||||
socket_path = os.path.join(temp_dir, "rootwrap.sock")
|
||||
LOG.debug("Will listen on socket %s", socket_path)
|
||||
manager_cls = get_manager_class(config, filters)
|
||||
manager = manager_cls(address=socket_path)
|
||||
server = manager.get_server()
|
||||
try:
|
||||
# allow everybody to connect to the socket
|
||||
rw_rw_rw_ = (stat.S_IRUSR | stat.S_IWUSR |
|
||||
stat.S_IRGRP | stat.S_IWGRP |
|
||||
stat.S_IROTH | stat.S_IWOTH)
|
||||
os.chmod(socket_path, rw_rw_rw_)
|
||||
try:
|
||||
# In Python 3 we have to use buffer to push in bytes directly
|
||||
stdout = sys.stdout.buffer
|
||||
except AttributeError:
|
||||
stdout = sys.stdout
|
||||
stdout.write(socket_path.encode('utf-8'))
|
||||
stdout.write(b'\n')
|
||||
stdout.write(bytes(server.authkey))
|
||||
sys.stdin.close()
|
||||
sys.stdout.close()
|
||||
sys.stderr.close()
|
||||
# Gracefully shutdown on INT or TERM signals
|
||||
stop = functools.partial(daemon_stop, server)
|
||||
signal.signal(signal.SIGTERM, stop)
|
||||
signal.signal(signal.SIGINT, stop)
|
||||
LOG.info("Starting rootwrap daemon main loop")
|
||||
server.serve_forever()
|
||||
finally:
|
||||
conn = server.listener
|
||||
# This will break accept() loop with EOFError if it was not in the
|
||||
# main thread (as in Python 3.x)
|
||||
conn.close()
|
||||
# Closing all currently connected client sockets for reading to
|
||||
# break worker threads blocked on recv()
|
||||
for cl_conn in conn.get_accepted():
|
||||
try:
|
||||
cl_conn.half_close()
|
||||
except Exception:
|
||||
# Most likely the socket have already been closed
|
||||
LOG.debug("Failed to close connection")
|
||||
LOG.info("Waiting for all client threads to finish.")
|
||||
for thread in threading.enumerate():
|
||||
if thread.daemon:
|
||||
LOG.debug("Joining thread %s", thread)
|
||||
thread.join()
|
||||
finally:
|
||||
LOG.debug("Removing temporary directory %s", temp_dir)
|
||||
shutil.rmtree(temp_dir)
|
||||
|
||||
|
||||
def daemon_stop(server, signal, frame):
|
||||
LOG.info("Got signal %s. Shutting down server", signal)
|
||||
# Signals are caught in the main thread which means this handler will run
|
||||
# in the middle of serve_forever() loop. It will catch this exception and
|
||||
# properly return. Since all threads created by server_forever are
|
||||
# daemonic, we need to join them afterwards. In Python 3 we can just hit
|
||||
# stop_event instead.
|
||||
try:
|
||||
server.stop_event.set()
|
||||
except AttributeError:
|
||||
raise KeyboardInterrupt
|
@ -1,393 +0,0 @@
|
||||
# Copyright (c) 2011 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 os
|
||||
import pwd
|
||||
import re
|
||||
|
||||
|
||||
def _getuid(user):
|
||||
"""Return uid for user."""
|
||||
return pwd.getpwnam(user).pw_uid
|
||||
|
||||
|
||||
class CommandFilter(object):
|
||||
"""Command filter only checking that the 1st argument matches exec_path."""
|
||||
|
||||
def __init__(self, exec_path, run_as, *args):
|
||||
self.name = ''
|
||||
self.exec_path = exec_path
|
||||
self.run_as = run_as
|
||||
self.args = args
|
||||
self.real_exec = None
|
||||
|
||||
def get_exec(self, exec_dirs=None):
|
||||
"""Returns existing executable, or empty string if none found."""
|
||||
exec_dirs = exec_dirs or []
|
||||
if self.real_exec is not None:
|
||||
return self.real_exec
|
||||
self.real_exec = ""
|
||||
if os.path.isabs(self.exec_path):
|
||||
if os.access(self.exec_path, os.X_OK):
|
||||
self.real_exec = self.exec_path
|
||||
else:
|
||||
for binary_path in exec_dirs:
|
||||
expanded_path = os.path.join(binary_path, self.exec_path)
|
||||
if os.access(expanded_path, os.X_OK):
|
||||
self.real_exec = expanded_path
|
||||
break
|
||||
return self.real_exec
|
||||
|
||||
def match(self, userargs):
|
||||
"""Only check that the first argument (command) matches exec_path."""
|
||||
return userargs and os.path.basename(self.exec_path) == userargs[0]
|
||||
|
||||
def preexec(self):
|
||||
"""Setuid in subprocess right before command is invoked."""
|
||||
if self.run_as != 'root':
|
||||
os.setuid(_getuid(self.run_as))
|
||||
|
||||
def get_command(self, userargs, exec_dirs=None):
|
||||
"""Returns command to execute."""
|
||||
exec_dirs = exec_dirs or []
|
||||
to_exec = self.get_exec(exec_dirs=exec_dirs) or self.exec_path
|
||||
return [to_exec] + userargs[1:]
|
||||
|
||||
def get_environment(self, userargs):
|
||||
"""Returns specific environment to set, None if none."""
|
||||
return None
|
||||
|
||||
|
||||
class RegExpFilter(CommandFilter):
|
||||
"""Command filter doing regexp matching for every argument."""
|
||||
|
||||
def match(self, userargs):
|
||||
# Early skip if command or number of args don't match
|
||||
if (not userargs or len(self.args) != len(userargs)):
|
||||
# DENY: argument numbers don't match
|
||||
return False
|
||||
# Compare each arg (anchoring pattern explicitly at end of string)
|
||||
for (pattern, arg) in zip(self.args, userargs):
|
||||
try:
|
||||
if not re.match(pattern + '$', arg):
|
||||
# DENY: Some arguments did not match
|
||||
return False
|
||||
except re.error:
|
||||
# DENY: Badly-formed filter
|
||||
return False
|
||||
# ALLOW: All arguments matched
|
||||
return True
|
||||
|
||||
|
||||
class PathFilter(CommandFilter):
|
||||
"""Command filter checking that path arguments are within given dirs
|
||||
|
||||
One can specify the following constraints for command arguments:
|
||||
1) pass - pass an argument as is to the resulting command
|
||||
2) some_str - check if an argument is equal to the given string
|
||||
3) abs path - check if a path argument is within the given base dir
|
||||
|
||||
A typical rootwrapper filter entry looks like this:
|
||||
# cmdname: filter name, raw command, user, arg_i_constraint [, ...]
|
||||
chown: PathFilter, /bin/chown, root, nova, /var/lib/images
|
||||
|
||||
"""
|
||||
|
||||
def match(self, userargs):
|
||||
if not userargs or len(userargs) < 2:
|
||||
return False
|
||||
|
||||
arguments = userargs[1:]
|
||||
|
||||
equal_args_num = len(self.args) == len(arguments)
|
||||
exec_is_valid = super(PathFilter, self).match(userargs)
|
||||
args_equal_or_pass = all(
|
||||
arg == 'pass' or arg == value
|
||||
for arg, value in zip(self.args, arguments)
|
||||
if not os.path.isabs(arg) # arguments not specifying abs paths
|
||||
)
|
||||
paths_are_within_base_dirs = all(
|
||||
os.path.commonprefix([arg, os.path.realpath(value)]) == arg
|
||||
for arg, value in zip(self.args, arguments)
|
||||
if os.path.isabs(arg) # arguments specifying abs paths
|
||||
)
|
||||
|
||||
return (equal_args_num and
|
||||
exec_is_valid and
|
||||
args_equal_or_pass and
|
||||
paths_are_within_base_dirs)
|
||||
|
||||
def get_command(self, userargs, exec_dirs=None):
|
||||
exec_dirs = exec_dirs or []
|
||||
command, arguments = userargs[0], userargs[1:]
|
||||
|
||||
# convert path values to canonical ones; copy other args as is
|
||||
args = [os.path.realpath(value) if os.path.isabs(arg) else value
|
||||
for arg, value in zip(self.args, arguments)]
|
||||
|
||||
return super(PathFilter, self).get_command([command] + args,
|
||||
exec_dirs)
|
||||
|
||||
|
||||
class KillFilter(CommandFilter):
|
||||
"""Specific filter for the kill calls.
|
||||
|
||||
1st argument is the user to run /bin/kill under
|
||||
2nd argument is the location of the affected executable
|
||||
if the argument is not absolute, it is checked against $PATH
|
||||
Subsequent arguments list the accepted signals (if any)
|
||||
|
||||
This filter relies on /proc to accurately determine affected
|
||||
executable, so it will only work on procfs-capable systems (not OSX).
|
||||
"""
|
||||
|
||||
def __init__(self, *args):
|
||||
super(KillFilter, self).__init__("/bin/kill", *args)
|
||||
|
||||
def _program_path(self, command):
|
||||
"""Determine the full path for command"""
|
||||
if os.path.isabs(command):
|
||||
return command
|
||||
else:
|
||||
for path in os.environ.get('PATH', '').split(os.pathsep):
|
||||
program = os.path.join(path, command)
|
||||
if os.path.isfile(program):
|
||||
return program
|
||||
return command
|
||||
|
||||
def _program(self, pid):
|
||||
"""Determine the program associated with pid"""
|
||||
|
||||
try:
|
||||
command = os.readlink("/proc/%d/exe" % int(pid))
|
||||
except (ValueError, EnvironmentError):
|
||||
# Incorrect PID
|
||||
return None
|
||||
|
||||
# NOTE(yufang521247): /proc/PID/exe may have '\0' on the
|
||||
# end, because python doesn't stop at '\0' when read the
|
||||
# target path.
|
||||
command = command.partition('\0')[0]
|
||||
|
||||
# NOTE(dprince): /proc/PID/exe may have ' (deleted)' on
|
||||
# the end if an executable is updated or deleted
|
||||
if command.endswith(" (deleted)"):
|
||||
command = command[:-len(" (deleted)")]
|
||||
|
||||
# /proc/PID/exe may have been renamed with
|
||||
# a ';......' or '.#prelink#......' suffix etc.
|
||||
# So defer to /proc/PID/cmdline in that case.
|
||||
if not os.path.isfile(command):
|
||||
try:
|
||||
with open("/proc/%d/cmdline" % int(pid)) as pfile:
|
||||
cmdline = pfile.read().partition('\0')[0]
|
||||
cmdline = self._program_path(cmdline)
|
||||
if os.path.isfile(cmdline):
|
||||
command = cmdline
|
||||
# Note we don't return None if cmdline doesn't exist
|
||||
# as that will allow killing a process where the exe
|
||||
# has been removed from the system rather than updated.
|
||||
except EnvironmentError:
|
||||
return None
|
||||
|
||||
return command
|
||||
|
||||
def match(self, userargs):
|
||||
if not userargs or userargs[0] != "kill":
|
||||
return False
|
||||
args = list(userargs)
|
||||
if len(args) == 3:
|
||||
# A specific signal is requested
|
||||
signal = args.pop(1)
|
||||
if signal not in self.args[1:]:
|
||||
# Requested signal not in accepted list
|
||||
return False
|
||||
else:
|
||||
if len(args) != 2:
|
||||
# Incorrect number of arguments
|
||||
return False
|
||||
if len(self.args) > 1:
|
||||
# No signal requested, but filter requires specific signal
|
||||
return False
|
||||
|
||||
command = self._program(args[1])
|
||||
if not command:
|
||||
return False
|
||||
|
||||
kill_command = self.args[0]
|
||||
|
||||
if os.path.isabs(kill_command):
|
||||
return kill_command == command
|
||||
|
||||
return (os.path.isabs(command) and
|
||||
kill_command == os.path.basename(command) and
|
||||
os.path.dirname(command) in os.environ.get('PATH', ''
|
||||
).split(':'))
|
||||
|
||||
|
||||
class ReadFileFilter(CommandFilter):
|
||||
"""Specific filter for the utils.read_file_as_root call."""
|
||||
|
||||
def __init__(self, file_path, *args):
|
||||
self.file_path = file_path
|
||||
super(ReadFileFilter, self).__init__("/bin/cat", "root", *args)
|
||||
|
||||
def match(self, userargs):
|
||||
return (userargs == ['cat', self.file_path])
|
||||
|
||||
|
||||
class IpFilter(CommandFilter):
|
||||
"""Specific filter for the ip utility to that does not match exec."""
|
||||
|
||||
def match(self, userargs):
|
||||
if userargs[0] == 'ip':
|
||||
# Avoid the 'netns exec' command here
|
||||
for a, b in zip(userargs[1:], userargs[2:]):
|
||||
if a == 'netns':
|
||||
return (b != 'exec')
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
class EnvFilter(CommandFilter):
|
||||
"""Specific filter for the env utility.
|
||||
|
||||
Behaves like CommandFilter, except that it handles
|
||||
leading env A=B.. strings appropriately.
|
||||
"""
|
||||
|
||||
def _extract_env(self, arglist):
|
||||
"""Extract all leading NAME=VALUE arguments from arglist."""
|
||||
|
||||
envs = set()
|
||||
for arg in arglist:
|
||||
if '=' not in arg:
|
||||
break
|
||||
envs.add(arg.partition('=')[0])
|
||||
return envs
|
||||
|
||||
def __init__(self, exec_path, run_as, *args):
|
||||
super(EnvFilter, self).__init__(exec_path, run_as, *args)
|
||||
|
||||
env_list = self._extract_env(self.args)
|
||||
# Set exec_path to X when args are in the form of
|
||||
# env A=a B=b C=c X Y Z
|
||||
if "env" in exec_path and len(env_list) < len(self.args):
|
||||
self.exec_path = self.args[len(env_list)]
|
||||
|
||||
def match(self, userargs):
|
||||
# ignore leading 'env'
|
||||
if userargs[0] == 'env':
|
||||
userargs.pop(0)
|
||||
|
||||
# require one additional argument after configured ones
|
||||
if len(userargs) < len(self.args):
|
||||
return False
|
||||
|
||||
# extract all env args
|
||||
user_envs = self._extract_env(userargs)
|
||||
filter_envs = self._extract_env(self.args)
|
||||
user_command = userargs[len(user_envs):len(user_envs) + 1]
|
||||
|
||||
# match first non-env argument with CommandFilter
|
||||
return (super(EnvFilter, self).match(user_command)
|
||||
and len(filter_envs) and user_envs == filter_envs)
|
||||
|
||||
def exec_args(self, userargs):
|
||||
args = userargs[:]
|
||||
|
||||
# ignore leading 'env'
|
||||
if args[0] == 'env':
|
||||
args.pop(0)
|
||||
|
||||
# Throw away leading NAME=VALUE arguments
|
||||
while args and '=' in args[0]:
|
||||
args.pop(0)
|
||||
|
||||
return args
|
||||
|
||||
def get_command(self, userargs, exec_dirs=[]):
|
||||
to_exec = self.get_exec(exec_dirs=exec_dirs) or self.exec_path
|
||||
return [to_exec] + self.exec_args(userargs)[1:]
|
||||
|
||||
def get_environment(self, userargs):
|
||||
env = os.environ.copy()
|
||||
|
||||
# ignore leading 'env'
|
||||
if userargs[0] == 'env':
|
||||
userargs.pop(0)
|
||||
|
||||
# Handle leading NAME=VALUE pairs
|
||||
for a in userargs:
|
||||
env_name, equals, env_value = a.partition('=')
|
||||
if not equals:
|
||||
break
|
||||
if env_name and env_value:
|
||||
env[env_name] = env_value
|
||||
|
||||
return env
|
||||
|
||||
|
||||
class ChainingFilter(CommandFilter):
|
||||
def exec_args(self, userargs):
|
||||
return []
|
||||
|
||||
|
||||
class IpNetnsExecFilter(ChainingFilter):
|
||||
"""Specific filter for the ip utility to that does match exec."""
|
||||
|
||||
def match(self, userargs):
|
||||
# Network namespaces currently require root
|
||||
# require <ns> argument
|
||||
if self.run_as != "root" or len(userargs) < 4:
|
||||
return False
|
||||
|
||||
return (userargs[:3] == ['ip', 'netns', 'exec'])
|
||||
|
||||
def exec_args(self, userargs):
|
||||
args = userargs[4:]
|
||||
if args:
|
||||
args[0] = os.path.basename(args[0])
|
||||
return args
|
||||
|
||||
|
||||
class ChainingRegExpFilter(ChainingFilter):
|
||||
"""Command filter doing regexp matching for prefix commands.
|
||||
|
||||
Remaining arguments are filtered again. This means that the command
|
||||
specified as the arguments must be also allowed to execute directly.
|
||||
"""
|
||||
|
||||
def match(self, userargs):
|
||||
# Early skip if number of args is smaller than the filter
|
||||
if (not userargs or len(self.args) > len(userargs)):
|
||||
return False
|
||||
# Compare each arg (anchoring pattern explicitly at end of string)
|
||||
for (pattern, arg) in zip(self.args, userargs):
|
||||
try:
|
||||
if not re.match(pattern + '$', arg):
|
||||
# DENY: Some arguments did not match
|
||||
return False
|
||||
except re.error:
|
||||
# DENY: Badly-formed filter
|
||||
return False
|
||||
# ALLOW: All arguments matched
|
||||
return True
|
||||
|
||||
def exec_args(self, userargs):
|
||||
args = userargs[len(self.args):]
|
||||
if args:
|
||||
args[0] = os.path.basename(args[0])
|
||||
return args
|
@ -1,195 +0,0 @@
|
||||
# Copyright (c) 2014 Mirantis Inc.
|
||||
# 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 base64
|
||||
import errno
|
||||
import json
|
||||
from multiprocessing import connection
|
||||
from multiprocessing import managers
|
||||
import socket
|
||||
import struct
|
||||
import weakref
|
||||
|
||||
from oslo_rootwrap import wrapper
|
||||
|
||||
|
||||
class RpcJSONEncoder(json.JSONEncoder):
|
||||
def default(self, o):
|
||||
# We need to pass bytes unchanged as they are expected in arguments for
|
||||
# and are result of Popen.communicate()
|
||||
if isinstance(o, bytes):
|
||||
return {"__bytes__": base64.b64encode(o).decode('ascii')}
|
||||
# Handle two exception types relevant to command execution
|
||||
if isinstance(o, wrapper.NoFilterMatched):
|
||||
return {"__exception__": "NoFilterMatched"}
|
||||
elif isinstance(o, wrapper.FilterMatchNotExecutable):
|
||||
return {"__exception__": "FilterMatchNotExecutable",
|
||||
"match": o.match}
|
||||
# Other errors will fail to pass JSON encoding and will be visible on
|
||||
# client side
|
||||
else:
|
||||
return super(RpcJSONEncoder, self).default(o)
|
||||
|
||||
|
||||
# Parse whatever RpcJSONEncoder supplied us with
|
||||
def rpc_object_hook(obj):
|
||||
if "__exception__" in obj:
|
||||
type_name = obj.pop("__exception__")
|
||||
if type_name not in ("NoFilterMatched", "FilterMatchNotExecutable"):
|
||||
return obj
|
||||
exc_type = getattr(wrapper, type_name)
|
||||
return exc_type(**obj)
|
||||
elif "__bytes__" in obj:
|
||||
return base64.b64decode(obj["__bytes__"].encode('ascii'))
|
||||
else:
|
||||
return obj
|
||||
|
||||
|
||||
class JsonListener(object):
|
||||
def __init__(self, address, backlog=1):
|
||||
self.address = address
|
||||
self._socket = socket.socket(socket.AF_UNIX)
|
||||
try:
|
||||
self._socket.setblocking(True)
|
||||
self._socket.bind(address)
|
||||
self._socket.listen(backlog)
|
||||
except socket.error:
|
||||
self._socket.close()
|
||||
raise
|
||||
self.closed = False
|
||||
self._accepted = weakref.WeakSet()
|
||||
|
||||
def accept(self):
|
||||
while True:
|
||||
try:
|
||||
s, _ = self._socket.accept()
|
||||
except socket.error as e:
|
||||
if e.errno in (errno.EINVAL, errno.EBADF):
|
||||
raise EOFError
|
||||
elif e.errno != errno.EINTR:
|
||||
raise
|
||||
else:
|
||||
break
|
||||
s.setblocking(True)
|
||||
conn = JsonConnection(s)
|
||||
self._accepted.add(conn)
|
||||
return conn
|
||||
|
||||
def close(self):
|
||||
if not self.closed:
|
||||
self._socket.shutdown(socket.SHUT_RDWR)
|
||||
self._socket.close()
|
||||
self.closed = True
|
||||
|
||||
def get_accepted(self):
|
||||
return self._accepted
|
||||
|
||||
if hasattr(managers.Server, 'accepter'):
|
||||
# In Python 3 accepter() thread has infinite loop. We break it with
|
||||
# EOFError, so we should silence this error here.
|
||||
def silent_accepter(self):
|
||||
try:
|
||||
old_accepter(self)
|
||||
except EOFError:
|
||||
pass
|
||||
old_accepter = managers.Server.accepter
|
||||
managers.Server.accepter = silent_accepter
|
||||
|
||||
|
||||
class JsonConnection(object):
|
||||
def __init__(self, sock):
|
||||
sock.setblocking(True)
|
||||
self._socket = sock
|
||||
|
||||
def send_bytes(self, s):
|
||||
self._socket.sendall(struct.pack('!Q', len(s)))
|
||||
self._socket.sendall(s)
|
||||
|
||||
def recv_bytes(self, maxsize=None):
|
||||
l = struct.unpack('!Q', self.recvall(8))[0]
|
||||
if maxsize is not None and l > maxsize:
|
||||
raise RuntimeError("Too big message received")
|
||||
s = self.recvall(l)
|
||||
return s
|
||||
|
||||
def send(self, obj):
|
||||
s = self.dumps(obj)
|
||||
self.send_bytes(s)
|
||||
|
||||
def recv(self):
|
||||
s = self.recv_bytes()
|
||||
return self.loads(s)
|
||||
|
||||
def close(self):
|
||||
self._socket.close()
|
||||
|
||||
def half_close(self):
|
||||
self._socket.shutdown(socket.SHUT_RD)
|
||||
|
||||
# We have to use slow version of recvall with eventlet because of a bug in
|
||||
# GreenSocket.recv_into:
|
||||
# https://bitbucket.org/eventlet/eventlet/pull-request/41
|
||||
def _recvall_slow(self, size):
|
||||
remaining = size
|
||||
res = []
|
||||
while remaining:
|
||||
piece = self._socket.recv(remaining)
|
||||
if not piece:
|
||||
raise EOFError
|
||||
res.append(piece)
|
||||
remaining -= len(piece)
|
||||
return b''.join(res)
|
||||
|
||||
def recvall(self, size):
|
||||
buf = bytearray(size)
|
||||
mem = memoryview(buf)
|
||||
got = 0
|
||||
while got < size:
|
||||
piece_size = self._socket.recv_into(mem[got:])
|
||||
if not piece_size:
|
||||
raise EOFError
|
||||
got += piece_size
|
||||
# bytearray is mostly compatible with bytes and we could avoid copying
|
||||
# data here, but hmac doesn't like it in Python 3.3 (not in 2.7 or 3.4)
|
||||
return bytes(buf)
|
||||
|
||||
@staticmethod
|
||||
def dumps(obj):
|
||||
return json.dumps(obj, cls=RpcJSONEncoder).encode('utf-8')
|
||||
|
||||
@staticmethod
|
||||
def loads(s):
|
||||
res = json.loads(s.decode('utf-8'), object_hook=rpc_object_hook)
|
||||
try:
|
||||
kind = res[0]
|
||||
except (IndexError, TypeError):
|
||||
pass
|
||||
else:
|
||||
# In Python 2 json returns unicode while multiprocessing needs str
|
||||
if (kind in ("#TRACEBACK", "#UNSERIALIZABLE") and
|
||||
not isinstance(res[1], str)):
|
||||
res[1] = res[1].encode('utf-8', 'replace')
|
||||
return res
|
||||
|
||||
|
||||
class JsonClient(JsonConnection):
|
||||
def __init__(self, address, authkey=None):
|
||||
sock = socket.socket(socket.AF_UNIX)
|
||||
sock.setblocking(True)
|
||||
sock.connect(address)
|
||||
super(JsonClient, self).__init__(sock)
|
||||
if authkey is not None:
|
||||
connection.answer_challenge(self, authkey)
|
||||
connection.deliver_challenge(self, authkey)
|
@ -1,57 +0,0 @@
|
||||
# Copyright (c) 2014 Mirantis Inc.
|
||||
# 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 logging
|
||||
import sys
|
||||
import threading
|
||||
|
||||
from oslo_rootwrap import cmd
|
||||
from oslo_rootwrap import subprocess
|
||||
|
||||
|
||||
def forward_stream(fr, to):
|
||||
while True:
|
||||
line = fr.readline()
|
||||
if not line:
|
||||
break
|
||||
to.write(line)
|
||||
|
||||
|
||||
def forwarding_popen(f, old_popen=subprocess.Popen):
|
||||
def popen(*args, **kwargs):
|
||||
p = old_popen(*args, **kwargs)
|
||||
t = threading.Thread(target=forward_stream, args=(p.stderr, f))
|
||||
t.daemon = True
|
||||
t.start()
|
||||
return p
|
||||
return popen
|
||||
|
||||
|
||||
class nonclosing(object):
|
||||
def __init__(self, f):
|
||||
self._f = f
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._f, name)
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
log_format = ("%(asctime)s | [%(process)5s]+%(levelname)5s | "
|
||||
"%(message)s")
|
||||
if __name__ == '__main__':
|
||||
logging.basicConfig(level=logging.DEBUG, format=log_format)
|
||||
sys.stderr = nonclosing(sys.stderr)
|
||||
cmd.daemon()
|
@ -1,267 +0,0 @@
|
||||
# Copyright (c) 2014 Mirantis Inc.
|
||||
# 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 contextlib
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import pwd
|
||||
import signal
|
||||
import sys
|
||||
import threading
|
||||
|
||||
try:
|
||||
import eventlet
|
||||
except ImportError:
|
||||
eventlet = None
|
||||
|
||||
import fixtures
|
||||
import mock
|
||||
import six
|
||||
import testtools
|
||||
from testtools import content
|
||||
|
||||
from oslo_rootwrap import client
|
||||
from oslo_rootwrap import subprocess
|
||||
from oslo_rootwrap.tests import run_daemon
|
||||
from oslo_rootwrap import wrapper
|
||||
|
||||
|
||||
class _FunctionalBase(object):
|
||||
def setUp(self):
|
||||
super(_FunctionalBase, self).setUp()
|
||||
tmpdir = self.useFixture(fixtures.TempDir()).path
|
||||
self.config_file = os.path.join(tmpdir, 'rootwrap.conf')
|
||||
filters_dir = os.path.join(tmpdir, 'filters.d')
|
||||
filters_file = os.path.join(tmpdir, 'filters.d', 'test.filters')
|
||||
os.mkdir(filters_dir)
|
||||
with open(self.config_file, 'w') as f:
|
||||
f.write("""[DEFAULT]
|
||||
filters_path=%s
|
||||
exec_dirs=/bin""" % (filters_dir,))
|
||||
with open(filters_file, 'w') as f:
|
||||
f.write("""[Filters]
|
||||
echo: CommandFilter, /bin/echo, root
|
||||
cat: CommandFilter, /bin/cat, root
|
||||
sh: CommandFilter, /bin/sh, root
|
||||
id: CommandFilter, /usr/bin/id, nobody
|
||||
""")
|
||||
|
||||
def _test_run_once(self, expect_byte=True):
|
||||
code, out, err = self.execute(['echo', 'teststr'])
|
||||
self.assertEqual(0, code)
|
||||
if expect_byte:
|
||||
expect_out = b'teststr\n'
|
||||
expect_err = b''
|
||||
else:
|
||||
expect_out = 'teststr\n'
|
||||
expect_err = ''
|
||||
self.assertEqual(expect_out, out)
|
||||
self.assertEqual(expect_err, err)
|
||||
|
||||
def _test_run_with_stdin(self, expect_byte=True):
|
||||
code, out, err = self.execute(['cat'], stdin=b'teststr')
|
||||
self.assertEqual(0, code)
|
||||
if expect_byte:
|
||||
expect_out = b'teststr'
|
||||
expect_err = b''
|
||||
else:
|
||||
expect_out = 'teststr'
|
||||
expect_err = ''
|
||||
self.assertEqual(expect_out, out)
|
||||
self.assertEqual(expect_err, err)
|
||||
|
||||
def test_run_as(self):
|
||||
if os.getuid() != 0:
|
||||
self.skip('Test requires root (for setuid)')
|
||||
|
||||
# Should run as 'nobody'
|
||||
code, out, err = self.execute(['id', '-u'])
|
||||
self.assertEqual('%s\n' % pwd.getpwnam('nobody').pw_uid, out)
|
||||
|
||||
# Should run as 'root'
|
||||
code, out, err = self.execute(['sh', '-c', 'id -u'])
|
||||
self.assertEqual('0\n', out)
|
||||
|
||||
|
||||
class RootwrapTest(_FunctionalBase, testtools.TestCase):
|
||||
def setUp(self):
|
||||
super(RootwrapTest, self).setUp()
|
||||
self.cmd = [
|
||||
sys.executable, '-c',
|
||||
'from oslo_rootwrap import cmd; cmd.main()',
|
||||
self.config_file]
|
||||
|
||||
def execute(self, cmd, stdin=None):
|
||||
proc = subprocess.Popen(
|
||||
self.cmd + cmd,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
out, err = proc.communicate(stdin)
|
||||
self.addDetail('stdout',
|
||||
content.text_content(out.decode('utf-8', 'replace')))
|
||||
self.addDetail('stderr',
|
||||
content.text_content(err.decode('utf-8', 'replace')))
|
||||
return proc.returncode, out, err
|
||||
|
||||
def test_run_once(self):
|
||||
self._test_run_once(expect_byte=True)
|
||||
|
||||
def test_run_with_stdin(self):
|
||||
self._test_run_with_stdin(expect_byte=True)
|
||||
|
||||
|
||||
class RootwrapDaemonTest(_FunctionalBase, testtools.TestCase):
|
||||
def assert_unpatched(self):
|
||||
# We need to verify that these tests are run without eventlet patching
|
||||
if eventlet and eventlet.patcher.is_monkey_patched('socket'):
|
||||
self.fail("Standard library should not be patched by eventlet"
|
||||
" for this test")
|
||||
|
||||
def setUp(self):
|
||||
self.assert_unpatched()
|
||||
|
||||
super(RootwrapDaemonTest, self).setUp()
|
||||
|
||||
# Collect daemon logs
|
||||
daemon_log = io.BytesIO()
|
||||
p = mock.patch('oslo_rootwrap.subprocess.Popen',
|
||||
run_daemon.forwarding_popen(daemon_log))
|
||||
p.start()
|
||||
self.addCleanup(p.stop)
|
||||
|
||||
# Collect client logs
|
||||
client_log = six.StringIO()
|
||||
handler = logging.StreamHandler(client_log)
|
||||
log_format = run_daemon.log_format.replace('+', ' ')
|
||||
handler.setFormatter(logging.Formatter(log_format))
|
||||
logger = logging.getLogger('oslo_rootwrap')
|
||||
logger.addHandler(handler)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
self.addCleanup(logger.removeHandler, handler)
|
||||
|
||||
# Add all logs as details
|
||||
@self.addCleanup
|
||||
def add_logs():
|
||||
self.addDetail('daemon_log', content.Content(
|
||||
content.UTF8_TEXT,
|
||||
lambda: [daemon_log.getvalue()]))
|
||||
self.addDetail('client_log', content.Content(
|
||||
content.UTF8_TEXT,
|
||||
lambda: [client_log.getvalue().encode('utf-8')]))
|
||||
|
||||
# Create client
|
||||
self.client = client.Client([
|
||||
sys.executable, run_daemon.__file__,
|
||||
self.config_file])
|
||||
|
||||
# _finalize is set during Client.execute()
|
||||
@self.addCleanup
|
||||
def finalize_client():
|
||||
if self.client._initialized:
|
||||
self.client._finalize()
|
||||
|
||||
self.execute = self.client.execute
|
||||
|
||||
def test_run_once(self):
|
||||
self._test_run_once(expect_byte=False)
|
||||
|
||||
def test_run_with_stdin(self):
|
||||
self._test_run_with_stdin(expect_byte=False)
|
||||
|
||||
def test_error_propagation(self):
|
||||
self.assertRaises(wrapper.NoFilterMatched, self.execute, ['other'])
|
||||
|
||||
def test_daemon_ressurection(self):
|
||||
# Let the client start a daemon
|
||||
self.execute(['cat'])
|
||||
# Make daemon go away
|
||||
os.kill(self.client._process.pid, signal.SIGTERM)
|
||||
# Expect client to successfully restart daemon and run simple request
|
||||
self.test_run_once()
|
||||
|
||||
def _exec_thread(self, fifo_path):
|
||||
try:
|
||||
# Run a shell script that signals calling process through FIFO and
|
||||
# then hangs around for 1 sec
|
||||
self._thread_res = self.execute([
|
||||
'sh', '-c', 'echo > "%s"; sleep 1; echo OK' % fifo_path])
|
||||
except Exception as e:
|
||||
self._thread_res = e
|
||||
|
||||
def test_graceful_death(self):
|
||||
# Create a fifo in a temporary dir
|
||||
tmpdir = self.useFixture(fixtures.TempDir()).path
|
||||
fifo_path = os.path.join(tmpdir, 'fifo')
|
||||
os.mkfifo(fifo_path)
|
||||
# Start daemon
|
||||
self.execute(['cat'])
|
||||
# Begin executing shell script
|
||||
t = threading.Thread(target=self._exec_thread, args=(fifo_path,))
|
||||
t.start()
|
||||
# Wait for shell script to actually start
|
||||
with open(fifo_path) as f:
|
||||
f.readline()
|
||||
# Gracefully kill daemon process
|
||||
os.kill(self.client._process.pid, signal.SIGTERM)
|
||||
# Expect daemon to wait for our request to finish
|
||||
t.join()
|
||||
if isinstance(self._thread_res, Exception):
|
||||
raise self._thread_res # Python 3 will even provide nice traceback
|
||||
code, out, err = self._thread_res
|
||||
self.assertEqual(0, code)
|
||||
self.assertEqual('OK\n', out)
|
||||
self.assertEqual('', err)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _test_daemon_cleanup(self):
|
||||
# Start a daemon
|
||||
self.execute(['cat'])
|
||||
socket_path = self.client._manager._address
|
||||
# Stop it one way or another
|
||||
yield
|
||||
process = self.client._process
|
||||
stop = threading.Event()
|
||||
|
||||
# Start background thread that would kill process in 1 second if it
|
||||
# doesn't die by then
|
||||
def sleep_kill():
|
||||
stop.wait(1)
|
||||
if not stop.is_set():
|
||||
os.kill(process.pid, signal.SIGKILL)
|
||||
threading.Thread(target=sleep_kill).start()
|
||||
# Wait for process to finish one way or another
|
||||
self.client._process.wait()
|
||||
# Notify background thread that process is dead (no need to kill it)
|
||||
stop.set()
|
||||
# Fail if the process got killed by the background thread
|
||||
self.assertNotEqual(-signal.SIGKILL, process.returncode,
|
||||
"Server haven't stopped in one second")
|
||||
# Verify that socket is deleted
|
||||
self.assertFalse(os.path.exists(socket_path),
|
||||
"Server didn't remove its temporary directory")
|
||||
|
||||
def test_daemon_cleanup_client(self):
|
||||
# Run _test_daemon_cleanup stopping daemon as Client instance would
|
||||
# normally do
|
||||
with self._test_daemon_cleanup():
|
||||
self.client._finalize()
|
||||
|
||||
def test_daemon_cleanup_signal(self):
|
||||
# Run _test_daemon_cleanup stopping daemon with SIGTERM signal
|
||||
with self._test_daemon_cleanup():
|
||||
os.kill(self.client._process.pid, signal.SIGTERM)
|
@ -1,27 +0,0 @@
|
||||
# Copyright (c) 2014 Mirantis Inc.
|
||||
# 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 os
|
||||
|
||||
if os.environ.get('TEST_EVENTLET', False):
|
||||
import eventlet
|
||||
eventlet.monkey_patch()
|
||||
|
||||
from oslo_rootwrap.tests import test_functional
|
||||
|
||||
class RootwrapDaemonTest(test_functional.RootwrapDaemonTest):
|
||||
def assert_unpatched(self):
|
||||
# This test case is specifically for eventlet testing
|
||||
pass
|
@ -1,636 +0,0 @@
|
||||
# Copyright 2011 OpenStack Foundation
|
||||
#
|
||||
# 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 logging
|
||||
import logging.handlers
|
||||
import os
|
||||
import uuid
|
||||
|
||||
import fixtures
|
||||
import mock
|
||||
from six import moves
|
||||
import testtools
|
||||
|
||||
from oslo_rootwrap import cmd
|
||||
from oslo_rootwrap import daemon
|
||||
from oslo_rootwrap import filters
|
||||
from oslo_rootwrap import subprocess
|
||||
from oslo_rootwrap import wrapper
|
||||
|
||||
|
||||
class RootwrapLoaderTestCase(testtools.TestCase):
|
||||
|
||||
def test_privsep_in_loader(self):
|
||||
privsep = ["privsep-helper", "--context", "foo"]
|
||||
filterlist = wrapper.load_filters([])
|
||||
|
||||
# mock out get_exec because
|
||||
with mock.patch.object(filters.CommandFilter, 'get_exec') as ge:
|
||||
ge.return_value = "/fake/privsep-helper"
|
||||
filtermatch = wrapper.match_filter(filterlist, privsep)
|
||||
|
||||
self.assertIsNotNone(filtermatch)
|
||||
self.assertEqual(["/fake/privsep-helper", "--context", "foo"],
|
||||
filtermatch.get_command(privsep))
|
||||
|
||||
|
||||
class RootwrapTestCase(testtools.TestCase):
|
||||
if os.path.exists('/sbin/ip'):
|
||||
_ip = '/sbin/ip'
|
||||
else:
|
||||
_ip = '/bin/ip'
|
||||
|
||||
def setUp(self):
|
||||
super(RootwrapTestCase, self).setUp()
|
||||
self.filters = [
|
||||
filters.RegExpFilter("/bin/ls", "root", 'ls', '/[a-z]+'),
|
||||
filters.CommandFilter("/usr/bin/foo_bar_not_exist", "root"),
|
||||
filters.RegExpFilter("/bin/cat", "root", 'cat', '/[a-z]+'),
|
||||
filters.CommandFilter("/nonexistent/cat", "root"),
|
||||
filters.CommandFilter("/bin/cat", "root") # Keep this one last
|
||||
]
|
||||
|
||||
def test_CommandFilter(self):
|
||||
f = filters.CommandFilter("sleep", 'root', '10')
|
||||
self.assertFalse(f.match(["sleep2"]))
|
||||
|
||||
# verify that any arguments are accepted
|
||||
self.assertTrue(f.match(["sleep"]))
|
||||
self.assertTrue(f.match(["sleep", "anything"]))
|
||||
self.assertTrue(f.match(["sleep", "10"]))
|
||||
f = filters.CommandFilter("sleep", 'root')
|
||||
self.assertTrue(f.match(["sleep", "10"]))
|
||||
|
||||
def test_empty_commandfilter(self):
|
||||
f = filters.CommandFilter("sleep", "root")
|
||||
self.assertFalse(f.match([]))
|
||||
self.assertFalse(f.match(None))
|
||||
|
||||
def test_empty_regexpfilter(self):
|
||||
f = filters.RegExpFilter("sleep", "root", "sleep")
|
||||
self.assertFalse(f.match([]))
|
||||
self.assertFalse(f.match(None))
|
||||
|
||||
def test_empty_invalid_regexpfilter(self):
|
||||
f = filters.RegExpFilter("sleep", "root")
|
||||
self.assertFalse(f.match(["anything"]))
|
||||
self.assertFalse(f.match([]))
|
||||
|
||||
def test_RegExpFilter_match(self):
|
||||
usercmd = ["ls", "/root"]
|
||||
filtermatch = wrapper.match_filter(self.filters, usercmd)
|
||||
self.assertFalse(filtermatch is None)
|
||||
self.assertEqual(["/bin/ls", "/root"],
|
||||
filtermatch.get_command(usercmd))
|
||||
|
||||
def test_RegExpFilter_reject(self):
|
||||
usercmd = ["ls", "root"]
|
||||
self.assertRaises(wrapper.NoFilterMatched,
|
||||
wrapper.match_filter, self.filters, usercmd)
|
||||
|
||||
def test_missing_command(self):
|
||||
valid_but_missing = ["foo_bar_not_exist"]
|
||||
invalid = ["foo_bar_not_exist_and_not_matched"]
|
||||
self.assertRaises(wrapper.FilterMatchNotExecutable,
|
||||
wrapper.match_filter,
|
||||
self.filters, valid_but_missing)
|
||||
self.assertRaises(wrapper.NoFilterMatched,
|
||||
wrapper.match_filter, self.filters, invalid)
|
||||
|
||||
def _test_EnvFilter_as_DnsMasq(self, config_file_arg):
|
||||
usercmd = ['env', config_file_arg + '=A', 'NETWORK_ID=foobar',
|
||||
'dnsmasq', 'foo']
|
||||
f = filters.EnvFilter("env", "root", config_file_arg + '=A',
|
||||
'NETWORK_ID=', "/usr/bin/dnsmasq")
|
||||
self.assertTrue(f.match(usercmd))
|
||||
self.assertEqual(['/usr/bin/dnsmasq', 'foo'], f.get_command(usercmd))
|
||||
env = f.get_environment(usercmd)
|
||||
self.assertEqual('A', env.get(config_file_arg))
|
||||
self.assertEqual('foobar', env.get('NETWORK_ID'))
|
||||
|
||||
def test_EnvFilter(self):
|
||||
envset = ['A=/some/thing', 'B=somethingelse']
|
||||
envcmd = ['env'] + envset
|
||||
realcmd = ['sleep', '10']
|
||||
usercmd = envcmd + realcmd
|
||||
|
||||
f = filters.EnvFilter("env", "root", "A=", "B=ignored", "sleep")
|
||||
# accept with leading env
|
||||
self.assertTrue(f.match(envcmd + ["sleep"]))
|
||||
# accept without leading env
|
||||
self.assertTrue(f.match(envset + ["sleep"]))
|
||||
|
||||
# any other command does not match
|
||||
self.assertFalse(f.match(envcmd + ["sleep2"]))
|
||||
self.assertFalse(f.match(envset + ["sleep2"]))
|
||||
|
||||
# accept any trailing arguments
|
||||
self.assertTrue(f.match(usercmd))
|
||||
|
||||
# require given environment variables to match
|
||||
self.assertFalse(f.match([envcmd, 'C=ELSE']))
|
||||
self.assertFalse(f.match(['env', 'C=xx']))
|
||||
self.assertFalse(f.match(['env', 'A=xx']))
|
||||
|
||||
# require env command to be given
|
||||
# (otherwise CommandFilters should match
|
||||
self.assertFalse(f.match(realcmd))
|
||||
# require command to match
|
||||
self.assertFalse(f.match(envcmd))
|
||||
self.assertFalse(f.match(envcmd[1:]))
|
||||
|
||||
# ensure that the env command is stripped when executing
|
||||
self.assertEqual(realcmd, f.exec_args(usercmd))
|
||||
env = f.get_environment(usercmd)
|
||||
# check that environment variables are set
|
||||
self.assertEqual('/some/thing', env.get('A'))
|
||||
self.assertEqual('somethingelse', env.get('B'))
|
||||
self.assertFalse('sleep' in env.keys())
|
||||
|
||||
def test_EnvFilter_without_leading_env(self):
|
||||
envset = ['A=/some/thing', 'B=somethingelse']
|
||||
envcmd = ['env'] + envset
|
||||
realcmd = ['sleep', '10']
|
||||
|
||||
f = filters.EnvFilter("sleep", "root", "A=", "B=ignored")
|
||||
|
||||
# accept without leading env
|
||||
self.assertTrue(f.match(envset + ["sleep"]))
|
||||
|
||||
self.assertEqual(realcmd, f.get_command(envcmd + realcmd))
|
||||
self.assertEqual(realcmd, f.get_command(envset + realcmd))
|
||||
|
||||
env = f.get_environment(envset + realcmd)
|
||||
# check that environment variables are set
|
||||
self.assertEqual('/some/thing', env.get('A'))
|
||||
self.assertEqual('somethingelse', env.get('B'))
|
||||
self.assertFalse('sleep' in env.keys())
|
||||
|
||||
def test_KillFilter(self):
|
||||
if not os.path.exists("/proc/%d" % os.getpid()):
|
||||
self.skipTest("Test requires /proc filesystem (procfs)")
|
||||
p = subprocess.Popen(["cat"], stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT)
|
||||
try:
|
||||
f = filters.KillFilter("root", "/bin/cat", "-9", "-HUP")
|
||||
f2 = filters.KillFilter("root", "/usr/bin/cat", "-9", "-HUP")
|
||||
usercmd = ['kill', '-ALRM', p.pid]
|
||||
# Incorrect signal should fail
|
||||
self.assertFalse(f.match(usercmd) or f2.match(usercmd))
|
||||
usercmd = ['kill', p.pid]
|
||||
# Providing no signal should fail
|
||||
self.assertFalse(f.match(usercmd) or f2.match(usercmd))
|
||||
# Providing matching signal should be allowed
|
||||
usercmd = ['kill', '-9', p.pid]
|
||||
self.assertTrue(f.match(usercmd) or f2.match(usercmd))
|
||||
|
||||
f = filters.KillFilter("root", "/bin/cat")
|
||||
f2 = filters.KillFilter("root", "/usr/bin/cat")
|
||||
usercmd = ['kill', os.getpid()]
|
||||
# Our own PID does not match /bin/sleep, so it should fail
|
||||
self.assertFalse(f.match(usercmd) or f2.match(usercmd))
|
||||
usercmd = ['kill', 999999]
|
||||
# Nonexistent PID should fail
|
||||
self.assertFalse(f.match(usercmd) or f2.match(usercmd))
|
||||
usercmd = ['kill', p.pid]
|
||||
# Providing no signal should work
|
||||
self.assertTrue(f.match(usercmd) or f2.match(usercmd))
|
||||
|
||||
# verify that relative paths are matched against $PATH
|
||||
f = filters.KillFilter("root", "cat")
|
||||
# Our own PID does not match so it should fail
|
||||
usercmd = ['kill', os.getpid()]
|
||||
self.assertFalse(f.match(usercmd))
|
||||
# Filter should find cat in /bin or /usr/bin
|
||||
usercmd = ['kill', p.pid]
|
||||
self.assertTrue(f.match(usercmd))
|
||||
# Filter shouldn't be able to find binary in $PATH, so fail
|
||||
with fixtures.EnvironmentVariable("PATH", "/foo:/bar"):
|
||||
self.assertFalse(f.match(usercmd))
|
||||
# ensure that unset $PATH is not causing an exception
|
||||
with fixtures.EnvironmentVariable("PATH"):
|
||||
self.assertFalse(f.match(usercmd))
|
||||
finally:
|
||||
# Terminate the "cat" process and wait for it to finish
|
||||
p.terminate()
|
||||
p.wait()
|
||||
|
||||
def test_KillFilter_no_raise(self):
|
||||
"""Makes sure ValueError from bug 926412 is gone."""
|
||||
f = filters.KillFilter("root", "")
|
||||
# Providing anything other than kill should be False
|
||||
usercmd = ['notkill', 999999]
|
||||
self.assertFalse(f.match(usercmd))
|
||||
# Providing something that is not a pid should be False
|
||||
usercmd = ['kill', 'notapid']
|
||||
self.assertFalse(f.match(usercmd))
|
||||
# no arguments should also be fine
|
||||
self.assertFalse(f.match([]))
|
||||
self.assertFalse(f.match(None))
|
||||
|
||||
def test_KillFilter_deleted_exe(self):
|
||||
"""Makes sure deleted exe's are killed correctly."""
|
||||
command = "/bin/commandddddd"
|
||||
f = filters.KillFilter("root", command)
|
||||
usercmd = ['kill', 1234]
|
||||
# Providing no signal should work
|
||||
with mock.patch('os.readlink') as readlink:
|
||||
readlink.return_value = command + ' (deleted)'
|
||||
with mock.patch('os.path.isfile') as exists:
|
||||
def fake_exists(path):
|
||||
return path == command
|
||||
exists.side_effect = fake_exists
|
||||
self.assertTrue(f.match(usercmd))
|
||||
|
||||
def test_KillFilter_upgraded_exe(self):
|
||||
"""Makes sure upgraded exe's are killed correctly."""
|
||||
f = filters.KillFilter("root", "/bin/commandddddd")
|
||||
command = "/bin/commandddddd"
|
||||
usercmd = ['kill', 1234]
|
||||
with mock.patch('os.readlink') as readlink:
|
||||
readlink.return_value = command + '\0\05190bfb2 (deleted)'
|
||||
with mock.patch('os.path.isfile') as exists:
|
||||
def fake_exists(path):
|
||||
return path == command
|
||||
exists.side_effect = fake_exists
|
||||
self.assertTrue(f.match(usercmd))
|
||||
|
||||
def test_KillFilter_renamed_exe(self):
|
||||
"""Makes sure renamed exe's are killed correctly."""
|
||||
command = "/bin/commandddddd"
|
||||
f = filters.KillFilter("root", command)
|
||||
usercmd = ['kill', 1234]
|
||||
with mock.patch('os.readlink') as readlink:
|
||||
readlink.return_value = command + ';90bfb2 (deleted)'
|
||||
m = mock.mock_open(read_data=command)
|
||||
with mock.patch("six.moves.builtins.open", m, create=True):
|
||||
with mock.patch('os.path.isfile') as exists:
|
||||
def fake_exists(path):
|
||||
return path == command
|
||||
exists.side_effect = fake_exists
|
||||
self.assertTrue(f.match(usercmd))
|
||||
|
||||
def test_ReadFileFilter(self):
|
||||
goodfn = '/good/file.name'
|
||||
f = filters.ReadFileFilter(goodfn)
|
||||
usercmd = ['cat', '/bad/file']
|
||||
self.assertFalse(f.match(['cat', '/bad/file']))
|
||||
usercmd = ['cat', goodfn]
|
||||
self.assertEqual(['/bin/cat', goodfn], f.get_command(usercmd))
|
||||
self.assertTrue(f.match(usercmd))
|
||||
|
||||
def test_IpFilter_non_netns(self):
|
||||
f = filters.IpFilter(self._ip, 'root')
|
||||
self.assertTrue(f.match(['ip', 'link', 'list']))
|
||||
self.assertTrue(f.match(['ip', '-s', 'link', 'list']))
|
||||
self.assertTrue(f.match(['ip', '-s', '-v', 'netns', 'add']))
|
||||
self.assertTrue(f.match(['ip', 'link', 'set', 'interface',
|
||||
'netns', 'somens']))
|
||||
|
||||
def test_IpFilter_netns(self):
|
||||
f = filters.IpFilter(self._ip, 'root')
|
||||
self.assertFalse(f.match(['ip', 'netns', 'exec', 'foo']))
|
||||
self.assertFalse(f.match(['ip', 'netns', 'exec']))
|
||||
self.assertFalse(f.match(['ip', '-s', 'netns', 'exec']))
|
||||
self.assertFalse(f.match(['ip', '-l', '42', 'netns', 'exec']))
|
||||
|
||||
def _test_IpFilter_netns_helper(self, action):
|
||||
f = filters.IpFilter(self._ip, 'root')
|
||||
self.assertTrue(f.match(['ip', 'link', action]))
|
||||
|
||||
def test_IpFilter_netns_add(self):
|
||||
self._test_IpFilter_netns_helper('add')
|
||||
|
||||
def test_IpFilter_netns_delete(self):
|
||||
self._test_IpFilter_netns_helper('delete')
|
||||
|
||||
def test_IpFilter_netns_list(self):
|
||||
self._test_IpFilter_netns_helper('list')
|
||||
|
||||
def test_IpNetnsExecFilter_match(self):
|
||||
f = filters.IpNetnsExecFilter(self._ip, 'root')
|
||||
self.assertTrue(
|
||||
f.match(['ip', 'netns', 'exec', 'foo', 'ip', 'link', 'list']))
|
||||
|
||||
def test_IpNetnsExecFilter_nomatch(self):
|
||||
f = filters.IpNetnsExecFilter(self._ip, 'root')
|
||||
self.assertFalse(f.match(['ip', 'link', 'list']))
|
||||
|
||||
# verify that at least a NS is given
|
||||
self.assertFalse(f.match(['ip', 'netns', 'exec']))
|
||||
|
||||
def test_IpNetnsExecFilter_nomatch_nonroot(self):
|
||||
f = filters.IpNetnsExecFilter(self._ip, 'user')
|
||||
self.assertFalse(
|
||||
f.match(['ip', 'netns', 'exec', 'foo', 'ip', 'link', 'list']))
|
||||
|
||||
def test_match_filter_recurses_exec_command_filter_matches(self):
|
||||
filter_list = [filters.IpNetnsExecFilter(self._ip, 'root'),
|
||||
filters.IpFilter(self._ip, 'root')]
|
||||
args = ['ip', 'netns', 'exec', 'foo', 'ip', 'link', 'list']
|
||||
|
||||
self.assertIsNotNone(wrapper.match_filter(filter_list, args))
|
||||
|
||||
def test_match_filter_recurses_exec_command_matches_user(self):
|
||||
filter_list = [filters.IpNetnsExecFilter(self._ip, 'root'),
|
||||
filters.IpFilter(self._ip, 'user')]
|
||||
args = ['ip', 'netns', 'exec', 'foo', 'ip', 'link', 'list']
|
||||
|
||||
# Currently ip netns exec requires root, so verify that
|
||||
# no non-root filter is matched, as that would escalate privileges
|
||||
self.assertRaises(wrapper.NoFilterMatched,
|
||||
wrapper.match_filter, filter_list, args)
|
||||
|
||||
def test_match_filter_recurses_exec_command_filter_does_not_match(self):
|
||||
filter_list = [filters.IpNetnsExecFilter(self._ip, 'root'),
|
||||
filters.IpFilter(self._ip, 'root')]
|
||||
args = ['ip', 'netns', 'exec', 'foo', 'ip', 'netns', 'exec', 'bar',
|
||||
'ip', 'link', 'list']
|
||||
|
||||
self.assertRaises(wrapper.NoFilterMatched,
|
||||
wrapper.match_filter, filter_list, args)
|
||||
|
||||
def test_ChainingRegExpFilter_match(self):
|
||||
filter_list = [filters.ChainingRegExpFilter('nice', 'root',
|
||||
'nice', '-?\d+'),
|
||||
filters.CommandFilter('cat', 'root')]
|
||||
args = ['nice', '5', 'cat', '/a']
|
||||
dirs = ['/bin', '/usr/bin']
|
||||
|
||||
self.assertIsNotNone(wrapper.match_filter(filter_list, args, dirs))
|
||||
|
||||
def test_ChainingRegExpFilter_not_match(self):
|
||||
filter_list = [filters.ChainingRegExpFilter('nice', 'root',
|
||||
'nice', '-?\d+'),
|
||||
filters.CommandFilter('cat', 'root')]
|
||||
args_invalid = (['nice', '5', 'ls', '/a'],
|
||||
['nice', '--5', 'cat', '/a'],
|
||||
['nice2', '5', 'cat', '/a'],
|
||||
['nice', 'cat', '/a'],
|
||||
['nice', '5'])
|
||||
dirs = ['/bin', '/usr/bin']
|
||||
|
||||
for args in args_invalid:
|
||||
self.assertRaises(wrapper.NoFilterMatched,
|
||||
wrapper.match_filter, filter_list, args, dirs)
|
||||
|
||||
def test_ChainingRegExpFilter_multiple(self):
|
||||
filter_list = [filters.ChainingRegExpFilter('ionice', 'root', 'ionice',
|
||||
'-c[0-3]'),
|
||||
filters.ChainingRegExpFilter('ionice', 'root', 'ionice',
|
||||
'-c[0-3]', '-n[0-7]'),
|
||||
filters.CommandFilter('cat', 'root')]
|
||||
# both filters match to ['ionice', '-c2'], but only the second accepts
|
||||
args = ['ionice', '-c2', '-n7', 'cat', '/a']
|
||||
dirs = ['/bin', '/usr/bin']
|
||||
|
||||
self.assertIsNotNone(wrapper.match_filter(filter_list, args, dirs))
|
||||
|
||||
def test_ReadFileFilter_empty_args(self):
|
||||
goodfn = '/good/file.name'
|
||||
f = filters.ReadFileFilter(goodfn)
|
||||
self.assertFalse(f.match([]))
|
||||
self.assertFalse(f.match(None))
|
||||
|
||||
def test_exec_dirs_search(self):
|
||||
# This test supposes you have /bin/cat or /usr/bin/cat locally
|
||||
f = filters.CommandFilter("cat", "root")
|
||||
usercmd = ['cat', '/f']
|
||||
self.assertTrue(f.match(usercmd))
|
||||
self.assertTrue(f.get_command(usercmd,
|
||||
exec_dirs=['/bin', '/usr/bin'])
|
||||
in (['/bin/cat', '/f'], ['/usr/bin/cat', '/f']))
|
||||
|
||||
def test_skips(self):
|
||||
# Check that all filters are skipped and that the last matches
|
||||
usercmd = ["cat", "/"]
|
||||
filtermatch = wrapper.match_filter(self.filters, usercmd)
|
||||
self.assertTrue(filtermatch is self.filters[-1])
|
||||
|
||||
def test_RootwrapConfig(self):
|
||||
raw = moves.configparser.RawConfigParser()
|
||||
|
||||
# Empty config should raise configparser.Error
|
||||
self.assertRaises(moves.configparser.Error,
|
||||
wrapper.RootwrapConfig, raw)
|
||||
|
||||
# Check default values
|
||||
raw.set('DEFAULT', 'filters_path', '/a,/b')
|
||||
config = wrapper.RootwrapConfig(raw)
|
||||
self.assertEqual(['/a', '/b'], config.filters_path)
|
||||
self.assertEqual(os.environ["PATH"].split(':'), config.exec_dirs)
|
||||
|
||||
with fixtures.EnvironmentVariable("PATH"):
|
||||
c = wrapper.RootwrapConfig(raw)
|
||||
self.assertEqual([], c.exec_dirs)
|
||||
|
||||
self.assertFalse(config.use_syslog)
|
||||
self.assertEqual(logging.handlers.SysLogHandler.LOG_SYSLOG,
|
||||
config.syslog_log_facility)
|
||||
self.assertEqual(logging.ERROR, config.syslog_log_level)
|
||||
|
||||
# Check general values
|
||||
raw.set('DEFAULT', 'exec_dirs', '/a,/x')
|
||||
config = wrapper.RootwrapConfig(raw)
|
||||
self.assertEqual(['/a', '/x'], config.exec_dirs)
|
||||
|
||||
raw.set('DEFAULT', 'use_syslog', 'oui')
|
||||
self.assertRaises(ValueError, wrapper.RootwrapConfig, raw)
|
||||
raw.set('DEFAULT', 'use_syslog', 'true')
|
||||
config = wrapper.RootwrapConfig(raw)
|
||||
self.assertTrue(config.use_syslog)
|
||||
|
||||
raw.set('DEFAULT', 'syslog_log_facility', 'moo')
|
||||
self.assertRaises(ValueError, wrapper.RootwrapConfig, raw)
|
||||
raw.set('DEFAULT', 'syslog_log_facility', 'local0')
|
||||
config = wrapper.RootwrapConfig(raw)
|
||||
self.assertEqual(logging.handlers.SysLogHandler.LOG_LOCAL0,
|
||||
config.syslog_log_facility)
|
||||
raw.set('DEFAULT', 'syslog_log_facility', 'LOG_AUTH')
|
||||
config = wrapper.RootwrapConfig(raw)
|
||||
self.assertEqual(logging.handlers.SysLogHandler.LOG_AUTH,
|
||||
config.syslog_log_facility)
|
||||
|
||||
raw.set('DEFAULT', 'syslog_log_level', 'bar')
|
||||
self.assertRaises(ValueError, wrapper.RootwrapConfig, raw)
|
||||
raw.set('DEFAULT', 'syslog_log_level', 'INFO')
|
||||
config = wrapper.RootwrapConfig(raw)
|
||||
self.assertEqual(logging.INFO, config.syslog_log_level)
|
||||
|
||||
def test_getlogin(self):
|
||||
with mock.patch('os.getlogin') as os_getlogin:
|
||||
os_getlogin.return_value = 'foo'
|
||||
self.assertEqual('foo', wrapper._getlogin())
|
||||
|
||||
def test_getlogin_bad(self):
|
||||
with mock.patch('os.getenv') as os_getenv:
|
||||
with mock.patch('os.getlogin') as os_getlogin:
|
||||
os_getenv.side_effect = [None, None, 'bar']
|
||||
os_getlogin.side_effect = OSError(
|
||||
'[Errno 22] Invalid argument')
|
||||
self.assertEqual('bar', wrapper._getlogin())
|
||||
os_getlogin.assert_called_once_with()
|
||||
self.assertEqual(3, os_getenv.call_count)
|
||||
|
||||
|
||||
class PathFilterTestCase(testtools.TestCase):
|
||||
def setUp(self):
|
||||
super(PathFilterTestCase, self).setUp()
|
||||
|
||||
tmpdir = fixtures.TempDir('/tmp')
|
||||
self.useFixture(tmpdir)
|
||||
|
||||
self.f = filters.PathFilter('/bin/chown', 'root', 'nova', tmpdir.path)
|
||||
|
||||
gen_name = lambda: str(uuid.uuid4())
|
||||
|
||||
self.SIMPLE_FILE_WITHIN_DIR = os.path.join(tmpdir.path, 'some')
|
||||
self.SIMPLE_FILE_OUTSIDE_DIR = os.path.join('/tmp', 'some')
|
||||
self.TRAVERSAL_WITHIN_DIR = os.path.join(tmpdir.path, 'a', '..',
|
||||
'some')
|
||||
self.TRAVERSAL_OUTSIDE_DIR = os.path.join(tmpdir.path, '..', 'some')
|
||||
|
||||
self.TRAVERSAL_SYMLINK_WITHIN_DIR = os.path.join(tmpdir.path,
|
||||
gen_name())
|
||||
os.symlink(os.path.join(tmpdir.path, 'a', '..', 'a'),
|
||||
self.TRAVERSAL_SYMLINK_WITHIN_DIR)
|
||||
|
||||
self.TRAVERSAL_SYMLINK_OUTSIDE_DIR = os.path.join(tmpdir.path,
|
||||
gen_name())
|
||||
os.symlink(os.path.join(tmpdir.path, 'a', '..', '..', '..', 'etc'),
|
||||
self.TRAVERSAL_SYMLINK_OUTSIDE_DIR)
|
||||
|
||||
self.SYMLINK_WITHIN_DIR = os.path.join(tmpdir.path, gen_name())
|
||||
os.symlink(os.path.join(tmpdir.path, 'a'), self.SYMLINK_WITHIN_DIR)
|
||||
|
||||
self.SYMLINK_OUTSIDE_DIR = os.path.join(tmpdir.path, gen_name())
|
||||
os.symlink(os.path.join('/tmp', 'some_file'), self.SYMLINK_OUTSIDE_DIR)
|
||||
|
||||
def test_empty_args(self):
|
||||
self.assertFalse(self.f.match([]))
|
||||
self.assertFalse(self.f.match(None))
|
||||
|
||||
def test_argument_pass_constraint(self):
|
||||
f = filters.PathFilter('/bin/chown', 'root', 'pass', 'pass')
|
||||
|
||||
args = ['chown', 'something', self.SIMPLE_FILE_OUTSIDE_DIR]
|
||||
self.assertTrue(f.match(args))
|
||||
|
||||
def test_argument_equality_constraint(self):
|
||||
f = filters.PathFilter('/bin/chown', 'root', 'nova', '/tmp/spam/eggs')
|
||||
|
||||
args = ['chown', 'nova', '/tmp/spam/eggs']
|
||||
self.assertTrue(f.match(args))
|
||||
|
||||
args = ['chown', 'quantum', '/tmp/spam/eggs']
|
||||
self.assertFalse(f.match(args))
|
||||
|
||||
def test_wrong_arguments_number(self):
|
||||
args = ['chown', '-c', 'nova', self.SIMPLE_FILE_WITHIN_DIR]
|
||||
self.assertFalse(self.f.match(args))
|
||||
|
||||
def test_wrong_exec_command(self):
|
||||
args = ['wrong_exec', self.SIMPLE_FILE_WITHIN_DIR]
|
||||
self.assertFalse(self.f.match(args))
|
||||
|
||||
def test_match(self):
|
||||
args = ['chown', 'nova', self.SIMPLE_FILE_WITHIN_DIR]
|
||||
self.assertTrue(self.f.match(args))
|
||||
|
||||
def test_match_traversal(self):
|
||||
args = ['chown', 'nova', self.TRAVERSAL_WITHIN_DIR]
|
||||
self.assertTrue(self.f.match(args))
|
||||
|
||||
def test_match_symlink(self):
|
||||
args = ['chown', 'nova', self.SYMLINK_WITHIN_DIR]
|
||||
self.assertTrue(self.f.match(args))
|
||||
|
||||
def test_match_traversal_symlink(self):
|
||||
args = ['chown', 'nova', self.TRAVERSAL_SYMLINK_WITHIN_DIR]
|
||||
self.assertTrue(self.f.match(args))
|
||||
|
||||
def test_reject(self):
|
||||
args = ['chown', 'nova', self.SIMPLE_FILE_OUTSIDE_DIR]
|
||||
self.assertFalse(self.f.match(args))
|
||||
|
||||
def test_reject_traversal(self):
|
||||
args = ['chown', 'nova', self.TRAVERSAL_OUTSIDE_DIR]
|
||||
self.assertFalse(self.f.match(args))
|
||||
|
||||
def test_reject_symlink(self):
|
||||
args = ['chown', 'nova', self.SYMLINK_OUTSIDE_DIR]
|
||||
self.assertFalse(self.f.match(args))
|
||||
|
||||
def test_reject_traversal_symlink(self):
|
||||
args = ['chown', 'nova', self.TRAVERSAL_SYMLINK_OUTSIDE_DIR]
|
||||
self.assertFalse(self.f.match(args))
|
||||
|
||||
def test_get_command(self):
|
||||
args = ['chown', 'nova', self.SIMPLE_FILE_WITHIN_DIR]
|
||||
expected = ['/bin/chown', 'nova', self.SIMPLE_FILE_WITHIN_DIR]
|
||||
|
||||
self.assertEqual(expected, self.f.get_command(args))
|
||||
|
||||
def test_get_command_traversal(self):
|
||||
args = ['chown', 'nova', self.TRAVERSAL_WITHIN_DIR]
|
||||
expected = ['/bin/chown', 'nova',
|
||||
os.path.realpath(self.TRAVERSAL_WITHIN_DIR)]
|
||||
|
||||
self.assertEqual(expected, self.f.get_command(args))
|
||||
|
||||
def test_get_command_symlink(self):
|
||||
args = ['chown', 'nova', self.SYMLINK_WITHIN_DIR]
|
||||
expected = ['/bin/chown', 'nova',
|
||||
os.path.realpath(self.SYMLINK_WITHIN_DIR)]
|
||||
|
||||
self.assertEqual(expected, self.f.get_command(args))
|
||||
|
||||
def test_get_command_traversal_symlink(self):
|
||||
args = ['chown', 'nova', self.TRAVERSAL_SYMLINK_WITHIN_DIR]
|
||||
expected = ['/bin/chown', 'nova',
|
||||
os.path.realpath(self.TRAVERSAL_SYMLINK_WITHIN_DIR)]
|
||||
|
||||
self.assertEqual(expected, self.f.get_command(args))
|
||||
|
||||
|
||||
class RunOneCommandTestCase(testtools.TestCase):
|
||||
def _test_returncode_helper(self, returncode, expected):
|
||||
with mock.patch.object(wrapper, 'start_subprocess') as mock_start:
|
||||
with mock.patch('sys.exit') as mock_exit:
|
||||
mock_start.return_value.wait.return_value = returncode
|
||||
cmd.run_one_command(None, mock.Mock(), None, None)
|
||||
mock_exit.assert_called_once_with(expected)
|
||||
|
||||
def test_positive_returncode(self):
|
||||
self._test_returncode_helper(1, 1)
|
||||
|
||||
def test_negative_returncode(self):
|
||||
self._test_returncode_helper(-1, 129)
|
||||
|
||||
|
||||
class DaemonCleanupException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class DaemonCleanupTestCase(testtools.TestCase):
|
||||
|
||||
@mock.patch('os.chmod')
|
||||
@mock.patch('shutil.rmtree')
|
||||
@mock.patch('tempfile.mkdtemp')
|
||||
@mock.patch('multiprocessing.managers.BaseManager.get_server',
|
||||
side_effect=DaemonCleanupException)
|
||||
def test_daemon_no_cleanup_for_uninitialized_server(self, gs, *args):
|
||||
self.assertRaises(DaemonCleanupException, daemon.daemon_start,
|
||||
config=None, filters=None)
|
@ -1,210 +0,0 @@
|
||||
# Copyright (c) 2011 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 logging
|
||||
import logging.handlers
|
||||
import os
|
||||
import pwd
|
||||
import signal
|
||||
|
||||
from six import moves
|
||||
|
||||
from oslo_rootwrap import filters
|
||||
from oslo_rootwrap import subprocess
|
||||
|
||||
|
||||
class NoFilterMatched(Exception):
|
||||
"""This exception is raised when no filter matched."""
|
||||
pass
|
||||
|
||||
|
||||
class FilterMatchNotExecutable(Exception):
|
||||
"""Raised when a filter matched but no executable was found."""
|
||||
def __init__(self, match=None, **kwargs):
|
||||
self.match = match
|
||||
|
||||
|
||||
class RootwrapConfig(object):
|
||||
|
||||
def __init__(self, config):
|
||||
# filters_path
|
||||
self.filters_path = config.get("DEFAULT", "filters_path").split(",")
|
||||
|
||||
# exec_dirs
|
||||
if config.has_option("DEFAULT", "exec_dirs"):
|
||||
self.exec_dirs = config.get("DEFAULT", "exec_dirs").split(",")
|
||||
else:
|
||||
self.exec_dirs = []
|
||||
# Use system PATH if exec_dirs is not specified
|
||||
if "PATH" in os.environ:
|
||||
self.exec_dirs = os.environ['PATH'].split(':')
|
||||
|
||||
# syslog_log_facility
|
||||
if config.has_option("DEFAULT", "syslog_log_facility"):
|
||||
v = config.get("DEFAULT", "syslog_log_facility")
|
||||
facility_names = logging.handlers.SysLogHandler.facility_names
|
||||
self.syslog_log_facility = getattr(logging.handlers.SysLogHandler,
|
||||
v, None)
|
||||
if self.syslog_log_facility is None and v in facility_names:
|
||||
self.syslog_log_facility = facility_names.get(v)
|
||||
if self.syslog_log_facility is None:
|
||||
raise ValueError('Unexpected syslog_log_facility: %s' % v)
|
||||
else:
|
||||
default_facility = logging.handlers.SysLogHandler.LOG_SYSLOG
|
||||
self.syslog_log_facility = default_facility
|
||||
|
||||
# syslog_log_level
|
||||
if config.has_option("DEFAULT", "syslog_log_level"):
|
||||
v = config.get("DEFAULT", "syslog_log_level")
|
||||
level = v.upper()
|
||||
if (hasattr(logging, '_nameToLevel')
|
||||
and level in logging._nameToLevel):
|
||||
# Workaround a regression of Python 3.4.0 bug fixed in 3.4.2:
|
||||
# http://bugs.python.org/issue22386
|
||||
self.syslog_log_level = logging._nameToLevel[level]
|
||||
else:
|
||||
self.syslog_log_level = logging.getLevelName(level)
|
||||
if (self.syslog_log_level == "Level %s" % level):
|
||||
raise ValueError('Unexpected syslog_log_level: %r' % v)
|
||||
else:
|
||||
self.syslog_log_level = logging.ERROR
|
||||
|
||||
# use_syslog
|
||||
if config.has_option("DEFAULT", "use_syslog"):
|
||||
self.use_syslog = config.getboolean("DEFAULT", "use_syslog")
|
||||
else:
|
||||
self.use_syslog = False
|
||||
|
||||
|
||||
def setup_syslog(execname, facility, level):
|
||||
rootwrap_logger = logging.getLogger()
|
||||
rootwrap_logger.setLevel(level)
|
||||
handler = logging.handlers.SysLogHandler(address='/dev/log',
|
||||
facility=facility)
|
||||
handler.setFormatter(logging.Formatter(
|
||||
os.path.basename(execname) + ': %(message)s'))
|
||||
rootwrap_logger.addHandler(handler)
|
||||
|
||||
|
||||
def build_filter(class_name, *args):
|
||||
"""Returns a filter object of class class_name."""
|
||||
if not hasattr(filters, class_name):
|
||||
logging.warning("Skipping unknown filter class (%s) specified "
|
||||
"in filter definitions" % class_name)
|
||||
return None
|
||||
filterclass = getattr(filters, class_name)
|
||||
return filterclass(*args)
|
||||
|
||||
|
||||
def load_filters(filters_path):
|
||||
"""Load filters from a list of directories."""
|
||||
filterlist = []
|
||||
for filterdir in filters_path:
|
||||
if not os.path.isdir(filterdir):
|
||||
continue
|
||||
for filterfile in filter(lambda f: not f.startswith('.'),
|
||||
os.listdir(filterdir)):
|
||||
filterconfig = moves.configparser.RawConfigParser()
|
||||
filterconfig.read(os.path.join(filterdir, filterfile))
|
||||
for (name, value) in filterconfig.items("Filters"):
|
||||
filterdefinition = [s.strip() for s in value.split(',')]
|
||||
newfilter = build_filter(*filterdefinition)
|
||||
if newfilter is None:
|
||||
continue
|
||||
newfilter.name = name
|
||||
filterlist.append(newfilter)
|
||||
# And always include privsep-helper
|
||||
privsep = build_filter("CommandFilter", "privsep-helper", "root")
|
||||
privsep.name = "privsep-helper"
|
||||
filterlist.append(privsep)
|
||||
return filterlist
|
||||
|
||||
|
||||
def match_filter(filter_list, userargs, exec_dirs=None):
|
||||
"""Checks user command and arguments through command filters.
|
||||
|
||||
Returns the first matching filter.
|
||||
|
||||
Raises NoFilterMatched if no filter matched.
|
||||
Raises FilterMatchNotExecutable if no executable was found for the
|
||||
best filter match.
|
||||
"""
|
||||
first_not_executable_filter = None
|
||||
exec_dirs = exec_dirs or []
|
||||
|
||||
for f in filter_list:
|
||||
if f.match(userargs):
|
||||
if isinstance(f, filters.ChainingFilter):
|
||||
# This command calls exec verify that remaining args
|
||||
# matches another filter.
|
||||
def non_chain_filter(fltr):
|
||||
return (fltr.run_as == f.run_as
|
||||
and not isinstance(fltr, filters.ChainingFilter))
|
||||
|
||||
leaf_filters = [fltr for fltr in filter_list
|
||||
if non_chain_filter(fltr)]
|
||||
args = f.exec_args(userargs)
|
||||
if not args:
|
||||
continue
|
||||
try:
|
||||
match_filter(leaf_filters, args, exec_dirs=exec_dirs)
|
||||
except (NoFilterMatched, FilterMatchNotExecutable):
|
||||
continue
|
||||
|
||||
# Try other filters if executable is absent
|
||||
if not f.get_exec(exec_dirs=exec_dirs):
|
||||
if not first_not_executable_filter:
|
||||
first_not_executable_filter = f
|
||||
continue
|
||||
# Otherwise return matching filter for execution
|
||||
return f
|
||||
|
||||
if first_not_executable_filter:
|
||||
# A filter matched, but no executable was found for it
|
||||
raise FilterMatchNotExecutable(match=first_not_executable_filter)
|
||||
|
||||
# No filter matched
|
||||
raise NoFilterMatched()
|
||||
|
||||
|
||||
def _getlogin():
|
||||
try:
|
||||
return os.getlogin()
|
||||
except OSError:
|
||||
return (os.getenv('USER') or
|
||||
os.getenv('USERNAME') or
|
||||
os.getenv('LOGNAME'))
|
||||
|
||||
|
||||
def start_subprocess(filter_list, userargs, exec_dirs=[], log=False, **kwargs):
|
||||
filtermatch = match_filter(filter_list, userargs, exec_dirs)
|
||||
|
||||
command = filtermatch.get_command(userargs, exec_dirs)
|
||||
if log:
|
||||
logging.info("(%s > %s) Executing %s (filter match = %s)" % (
|
||||
_getlogin(), pwd.getpwuid(os.getuid())[0],
|
||||
command, filtermatch.name))
|
||||
|
||||
def preexec():
|
||||
# Python installs a SIGPIPE handler by default. This is
|
||||
# usually not what non-Python subprocesses expect.
|
||||
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
||||
filtermatch.preexec()
|
||||
|
||||
obj = subprocess.Popen(command,
|
||||
preexec_fn=preexec,
|
||||
env=filtermatch.get_environment(userargs),
|
||||
**kwargs)
|
||||
return obj
|
@ -1,5 +0,0 @@
|
||||
# The order of packages is significant, because pip processes them in the order
|
||||
# of appearance. Changing the order has an impact on the overall integration
|
||||
# process, which may cause wedges in the gate later.
|
||||
|
||||
six>=1.9.0 # MIT
|
43
setup.cfg
43
setup.cfg
@ -1,43 +0,0 @@
|
||||
[metadata]
|
||||
name = oslo.rootwrap
|
||||
author = OpenStack
|
||||
author-email = openstack-dev@lists.openstack.org
|
||||
summary = Oslo Rootwrap
|
||||
description-file =
|
||||
README.rst
|
||||
home-page = https://launchpad.net/oslo
|
||||
classifier =
|
||||
Development Status :: 4 - Beta
|
||||
Environment :: OpenStack
|
||||
Intended Audience :: Developers
|
||||
Intended Audience :: Information Technology
|
||||
License :: OSI Approved :: Apache Software License
|
||||
Operating System :: OS Independent
|
||||
Programming Language :: Python
|
||||
Programming Language :: Python :: 2.7
|
||||
Programming Language :: Python :: 3
|
||||
Programming Language :: Python :: 3.4
|
||||
Programming Language :: Python :: 3.5
|
||||
|
||||
[files]
|
||||
packages =
|
||||
oslo_rootwrap
|
||||
|
||||
[entry_points]
|
||||
console_scripts =
|
||||
oslo-rootwrap = oslo_rootwrap.cmd:main
|
||||
oslo-rootwrap-daemon = oslo_rootwrap.cmd:daemon
|
||||
|
||||
[build_sphinx]
|
||||
source-dir = doc/source
|
||||
build-dir = doc/build
|
||||
all_files = 1
|
||||
|
||||
[upload_sphinx]
|
||||
upload-dir = doc/build/html
|
||||
|
||||
[pbr]
|
||||
warnerrors = True
|
||||
|
||||
[wheel]
|
||||
universal = 1
|
29
setup.py
29
setup.py
@ -1,29 +0,0 @@
|
||||
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# 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.
|
||||
|
||||
# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT
|
||||
import setuptools
|
||||
|
||||
# In python < 2.7.4, a lazy loading of package `pbr` will break
|
||||
# setuptools if some other modules registered functions in `atexit`.
|
||||
# solution from: http://bugs.python.org/issue15881#msg170215
|
||||
try:
|
||||
import multiprocessing # noqa
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
setuptools.setup(
|
||||
setup_requires=['pbr>=1.8'],
|
||||
pbr=True)
|
@ -1,24 +0,0 @@
|
||||
# The order of packages is significant, because pip processes them in the order
|
||||
# of appearance. Changing the order has an impact on the overall integration
|
||||
# process, which may cause wedges in the gate later.
|
||||
|
||||
hacking<0.11,>=0.10.0
|
||||
|
||||
discover # BSD
|
||||
fixtures>=3.0.0 # Apache-2.0/BSD
|
||||
python-subunit>=0.0.18 # Apache-2.0/BSD
|
||||
testrepository>=0.0.18 # Apache-2.0/BSD
|
||||
testscenarios>=0.4 # Apache-2.0/BSD
|
||||
testtools>=1.4.0 # MIT
|
||||
|
||||
# this is required for the docs build jobs
|
||||
sphinx!=1.3b1,<1.3,>=1.2.1 # BSD
|
||||
oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0
|
||||
oslotest>=1.10.0 # Apache-2.0
|
||||
|
||||
|
||||
# mocking framework
|
||||
mock>=2.0 # BSD
|
||||
|
||||
# rootwrap daemon's client should be verified to run in eventlet
|
||||
eventlet!=0.18.3,>=0.18.2 # MIT
|
44
tox.ini
44
tox.ini
@ -1,44 +0,0 @@
|
||||
[tox]
|
||||
minversion = 1.6
|
||||
envlist = py35,py34,py27,pep8
|
||||
|
||||
[testenv]
|
||||
deps = -r{toxinidir}/test-requirements.txt
|
||||
# Functional tests with Eventlet involve monkeypatching, so force them to be
|
||||
# run in a separate process
|
||||
whitelist_externals = env
|
||||
commands =
|
||||
python setup.py testr --slowest --testr-args='(?!tests.test_functional_eventlet)tests {posargs}'
|
||||
env TEST_EVENTLET=1 python setup.py testr --slowest --testr-args='tests.test_functional_eventlet'
|
||||
|
||||
[testenv:pep8]
|
||||
commands = flake8
|
||||
|
||||
[testenv:cover]
|
||||
deps = {[testenv]deps}
|
||||
coverage
|
||||
setenv = VIRTUAL_ENV={envdir}
|
||||
commands =
|
||||
python setup.py testr --coverage
|
||||
|
||||
|
||||
[testenv:venv]
|
||||
commands = {posargs}
|
||||
|
||||
[testenv:docs]
|
||||
commands = python setup.py build_sphinx
|
||||
|
||||
[flake8]
|
||||
show-source = True
|
||||
exclude = .tox,dist,doc,*.egg,build
|
||||
|
||||
[testenv:benchmark]
|
||||
commands = python benchmark/benchmark.py
|
||||
|
||||
[testenv:pip-missing-reqs]
|
||||
# do not install test-requirements as that will pollute the virtualenv for
|
||||
# determining missing packages
|
||||
# this also means that pip-missing-reqs must be installed separately, outside
|
||||
# of the requirements.txt files
|
||||
deps = pip_missing_reqs
|
||||
commands = pip-missing-reqs -d --ignore-module=oslo_rootwrap* --ignore-module=pkg_resources --ignore-file=oslo_rootwrap/test.py --ignore-file=oslo_rootwrap/tests/* oslo_rootwrap
|
Loading…
Reference in New Issue
Block a user