From ccd2d839ebb7e511849a62f4addd8de64f64f077 Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Mon, 17 Apr 2017 19:39:29 +0200 Subject: [PATCH] Retire repo This repo was created by accident, use deb-python-oslo.rootwrap instead. Needed-By: I1ac1a06931c8b6dd7c2e73620a0302c29e605f03 Change-Id: I81894aea69b9d09b0977039623c26781093a397a --- .gitignore | 17 - .gitreview | 4 - .testr.conf | 4 - CONTRIBUTING.rst | 16 - LICENSE | 204 ------ README.rst | 19 - README.txt | 13 + benchmark/benchmark.py | 109 --- benchmark/filters.d/ip.filters | 4 - benchmark/rootwrap.conf | 4 - doc/source/conf.py | 75 --- doc/source/contributing.rst | 5 - doc/source/history.rst | 1 - doc/source/index.rst | 28 - doc/source/installation.rst | 12 - doc/source/usage.rst | 338 ---------- etc/rootwrap.conf.sample | 27 - oslo_rootwrap/__init__.py | 30 - oslo_rootwrap/client.py | 137 ---- oslo_rootwrap/cmd.py | 127 ---- oslo_rootwrap/daemon.py | 155 ----- oslo_rootwrap/filters.py | 393 ----------- oslo_rootwrap/jsonrpc.py | 195 ------ oslo_rootwrap/tests/__init__.py | 0 oslo_rootwrap/tests/run_daemon.py | 57 -- oslo_rootwrap/tests/test_functional.py | 267 -------- .../tests/test_functional_eventlet.py | 27 - oslo_rootwrap/tests/test_rootwrap.py | 636 ------------------ oslo_rootwrap/wrapper.py | 210 ------ requirements.txt | 5 - setup.cfg | 43 -- setup.py | 29 - test-requirements.txt | 24 - tox.ini | 44 -- 34 files changed, 13 insertions(+), 3246 deletions(-) delete mode 100644 .gitignore delete mode 100644 .gitreview delete mode 100644 .testr.conf delete mode 100644 CONTRIBUTING.rst delete mode 100644 LICENSE delete mode 100644 README.rst create mode 100644 README.txt delete mode 100644 benchmark/benchmark.py delete mode 100644 benchmark/filters.d/ip.filters delete mode 100644 benchmark/rootwrap.conf delete mode 100755 doc/source/conf.py delete mode 100644 doc/source/contributing.rst delete mode 100644 doc/source/history.rst delete mode 100644 doc/source/index.rst delete mode 100644 doc/source/installation.rst delete mode 100644 doc/source/usage.rst delete mode 100644 etc/rootwrap.conf.sample delete mode 100644 oslo_rootwrap/__init__.py delete mode 100644 oslo_rootwrap/client.py delete mode 100644 oslo_rootwrap/cmd.py delete mode 100644 oslo_rootwrap/daemon.py delete mode 100644 oslo_rootwrap/filters.py delete mode 100644 oslo_rootwrap/jsonrpc.py delete mode 100644 oslo_rootwrap/tests/__init__.py delete mode 100644 oslo_rootwrap/tests/run_daemon.py delete mode 100644 oslo_rootwrap/tests/test_functional.py delete mode 100644 oslo_rootwrap/tests/test_functional_eventlet.py delete mode 100644 oslo_rootwrap/tests/test_rootwrap.py delete mode 100644 oslo_rootwrap/wrapper.py delete mode 100644 requirements.txt delete mode 100644 setup.cfg delete mode 100644 setup.py delete mode 100644 test-requirements.txt delete mode 100644 tox.ini diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 96e344f..0000000 --- a/.gitignore +++ /dev/null @@ -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 diff --git a/.gitreview b/.gitreview deleted file mode 100644 index 32872e1..0000000 --- a/.gitreview +++ /dev/null @@ -1,4 +0,0 @@ -[gerrit] -host=review.openstack.org -port=29418 -project=openstack/oslo.rootwrap.git diff --git a/.testr.conf b/.testr.conf deleted file mode 100644 index 1641f86..0000000 --- a/.testr.conf +++ /dev/null @@ -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 diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst deleted file mode 100644 index 1a948e6..0000000 --- a/CONTRIBUTING.rst +++ /dev/null @@ -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 diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 4143aac..0000000 --- a/LICENSE +++ /dev/null @@ -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. diff --git a/README.rst b/README.rst deleted file mode 100644 index b70b7ff..0000000 --- a/README.rst +++ /dev/null @@ -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 diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..ef31e63 --- /dev/null +++ b/README.txt @@ -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. diff --git a/benchmark/benchmark.py b/benchmark/benchmark.py deleted file mode 100644 index ef7417c..0000000 --- a/benchmark/benchmark.py +++ /dev/null @@ -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() diff --git a/benchmark/filters.d/ip.filters b/benchmark/filters.d/ip.filters deleted file mode 100644 index 8942d6d..0000000 --- a/benchmark/filters.d/ip.filters +++ /dev/null @@ -1,4 +0,0 @@ -[Filters] - -ip: IpFilter, ip, root -ip_exec: IpNetnsExecFilter, ip, root diff --git a/benchmark/rootwrap.conf b/benchmark/rootwrap.conf deleted file mode 100644 index 73240df..0000000 --- a/benchmark/rootwrap.conf +++ /dev/null @@ -1,4 +0,0 @@ -[DEFAULT] -filters_path=filters.d -exec_dirs=/sbin,/usr/sbin,/bin,/usr/bin -use_syslog=False diff --git a/doc/source/conf.py b/doc/source/conf.py deleted file mode 100755 index c387e21..0000000 --- a/doc/source/conf.py +++ /dev/null @@ -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} \ No newline at end of file diff --git a/doc/source/contributing.rst b/doc/source/contributing.rst deleted file mode 100644 index 72b4bb2..0000000 --- a/doc/source/contributing.rst +++ /dev/null @@ -1,5 +0,0 @@ -============= -Contributing -============= - -.. include:: ../../CONTRIBUTING.rst diff --git a/doc/source/history.rst b/doc/source/history.rst deleted file mode 100644 index 69ed4fe..0000000 --- a/doc/source/history.rst +++ /dev/null @@ -1 +0,0 @@ -.. include:: ../../ChangeLog diff --git a/doc/source/index.rst b/doc/source/index.rst deleted file mode 100644 index 4897568..0000000 --- a/doc/source/index.rst +++ /dev/null @@ -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` diff --git a/doc/source/installation.rst b/doc/source/installation.rst deleted file mode 100644 index 574bfcb..0000000 --- a/doc/source/installation.rst +++ /dev/null @@ -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 \ No newline at end of file diff --git a/doc/source/usage.rst b/doc/source/usage.rst deleted file mode 100644 index 56f4bcb..0000000 --- a/doc/source/usage.rst +++ /dev/null @@ -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 `` as long as -```` 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. diff --git a/etc/rootwrap.conf.sample b/etc/rootwrap.conf.sample deleted file mode 100644 index a29f501..0000000 --- a/etc/rootwrap.conf.sample +++ /dev/null @@ -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 diff --git a/oslo_rootwrap/__init__.py b/oslo_rootwrap/__init__.py deleted file mode 100644 index 483f60c..0000000 --- a/oslo_rootwrap/__init__.py +++ /dev/null @@ -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 diff --git a/oslo_rootwrap/client.py b/oslo_rootwrap/client.py deleted file mode 100644 index dec161e..0000000 --- a/oslo_rootwrap/client.py +++ /dev/null @@ -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 diff --git a/oslo_rootwrap/cmd.py b/oslo_rootwrap/cmd.py deleted file mode 100644 index 0036fa5..0000000 --- a/oslo_rootwrap/cmd.py +++ /dev/null @@ -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) diff --git a/oslo_rootwrap/daemon.py b/oslo_rootwrap/daemon.py deleted file mode 100644 index 5409dfd..0000000 --- a/oslo_rootwrap/daemon.py +++ /dev/null @@ -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 diff --git a/oslo_rootwrap/filters.py b/oslo_rootwrap/filters.py deleted file mode 100644 index 119e304..0000000 --- a/oslo_rootwrap/filters.py +++ /dev/null @@ -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 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 diff --git a/oslo_rootwrap/jsonrpc.py b/oslo_rootwrap/jsonrpc.py deleted file mode 100644 index 9dd0641..0000000 --- a/oslo_rootwrap/jsonrpc.py +++ /dev/null @@ -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) diff --git a/oslo_rootwrap/tests/__init__.py b/oslo_rootwrap/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/oslo_rootwrap/tests/run_daemon.py b/oslo_rootwrap/tests/run_daemon.py deleted file mode 100644 index 8483ff1..0000000 --- a/oslo_rootwrap/tests/run_daemon.py +++ /dev/null @@ -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() diff --git a/oslo_rootwrap/tests/test_functional.py b/oslo_rootwrap/tests/test_functional.py deleted file mode 100644 index a23d9f1..0000000 --- a/oslo_rootwrap/tests/test_functional.py +++ /dev/null @@ -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) diff --git a/oslo_rootwrap/tests/test_functional_eventlet.py b/oslo_rootwrap/tests/test_functional_eventlet.py deleted file mode 100644 index eafef8e..0000000 --- a/oslo_rootwrap/tests/test_functional_eventlet.py +++ /dev/null @@ -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 diff --git a/oslo_rootwrap/tests/test_rootwrap.py b/oslo_rootwrap/tests/test_rootwrap.py deleted file mode 100644 index dc251a3..0000000 --- a/oslo_rootwrap/tests/test_rootwrap.py +++ /dev/null @@ -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) diff --git a/oslo_rootwrap/wrapper.py b/oslo_rootwrap/wrapper.py deleted file mode 100644 index cd7a253..0000000 --- a/oslo_rootwrap/wrapper.py +++ /dev/null @@ -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 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index a676725..0000000 --- a/requirements.txt +++ /dev/null @@ -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 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 175c136..0000000 --- a/setup.cfg +++ /dev/null @@ -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 diff --git a/setup.py b/setup.py deleted file mode 100644 index 782bb21..0000000 --- a/setup.py +++ /dev/null @@ -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) diff --git a/test-requirements.txt b/test-requirements.txt deleted file mode 100644 index 65bf3d1..0000000 --- a/test-requirements.txt +++ /dev/null @@ -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 diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 7d47491..0000000 --- a/tox.ini +++ /dev/null @@ -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