File pepper-0.7.6+git44.996fc29.obscpio of Package pepper
07070100000000000041ED00000000000000000000000267D9C77000000000000000000000000000000000000000000000002300000000pepper-0.7.6+git44.996fc29/.github07070100000001000041ED00000000000000000000000267D9C77000000000000000000000000000000000000000000000002D00000000pepper-0.7.6+git44.996fc29/.github/workflows07070100000002000081A400000000000000000000000167D9C77000000202000000000000000000000000000000000000003900000000pepper-0.7.6+git44.996fc29/.github/workflows/flake8.yamlname: flake8 lint
on:
push:
pull_request:
jobs:
flake8-lint:
runs-on: ubuntu-24.04
name: flake8 lint
steps:
- name: Setup python for flake8
uses: actions/setup-python@v4
with:
python-version: "3.10"
- uses: actions/checkout@v3
- name: Install nox
run: python -m pip install nox==2024.10.09
- name: Setup flake8
run: nox --force-color -e flake8 --install-only
- name: Run flake8
run: nox --force-color -e flake8 -vv
07070100000003000081A400000000000000000000000167D9C7700000093B000000000000000000000000000000000000003700000000pepper-0.7.6+git44.996fc29/.github/workflows/test.yamlname: test
on:
workflow_dispatch:
push:
pull_request:
schedule:
- cron: "0 8 * * *"
jobs:
pre-commit:
name: pre-commit
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.10"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pre-commit
pre-commit install
- name: Run pre-commit
run: pre-commit run --all-files
test:
name: test ${{ matrix.py }} - ${{ matrix.netapi }} - ${{ matrix.salt }}
runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
py:
- "3.8"
- "3.9"
- "3.10"
- "3.11"
netapi:
- "cherrypy"
- "tornado"
salt:
- "v3006.9"
- "v3007.1"
- "master"
exclude:
- py: "3.8"
salt: "v3007.1"
- py: "3.9"
salt: "v3007.1"
- py: "3.8"
salt: "master"
- py: "3.9"
salt: "master"
env:
SALT_REQUIREMENT: ${{ matrix.salt == 'master' && 'salt@git+https://github.com/saltstack/salt.git@master' || format('salt=={0}', matrix.salt) }}
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.py }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.py }}
- name: Install setuptools_scm
run: python -m pip install setuptools_scm
- name: Install dependencies
run: sudo apt update && sudo apt install -y libc6-dev libffi-dev gcc git openssh-server libzmq3-dev
env:
DEBIAN_FRONTEND: noninteractive
- name: Install Nox
run: |
python -m pip install --upgrade pip
pip install nox
- name: Install Test Requirements
env:
PYTHON_VERSIONS: ${{ matrix.py }}
API_BACKEND: ${{ matrix.netapi }}
run: |
nox --force-color -e tests --install-only
- name: Test
env:
PYTHON_VERSIONS: ${{ matrix.py }}
API_BACKEND: ${{ matrix.netapi }}
SKIP_REQUIREMENTS_INSTALL: YES
run: |
nox --force-color -e tests -- -vv tests/
07070100000004000081A400000000000000000000000167D9C77000000058000000000000000000000000000000000000002600000000pepper-0.7.6+git44.996fc29/.gitignore/build
*.py[co]
*.swp
*.DS_Store
MANIFEST
dist/
salt_pepper.egg-info/
.nox/
artifacts/
07070100000005000081A400000000000000000000000167D9C77000000827000000000000000000000000000000000000003300000000pepper-0.7.6+git44.996fc29/.pre-commit-config.yaml---
minimum_pre_commit_version: 3.5.0
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: check-merge-conflict # Check for files that contain merge conflict strings.
- id: trailing-whitespace # Trims trailing whitespace.
args: [--markdown-linebreak-ext=md]
- id: mixed-line-ending # Replaces or checks mixed line ending.
args: [--fix=lf]
- id: end-of-file-fixer # Makes sure files end in a newline and only a newline.
- id: check-merge-conflict # Check for files that contain merge conflict strings.
- id: check-ast # Simply check whether files parse as valid python.
# ----- Formatting ---------------------------------------------------------------------------->
- repo: https://github.com/myint/autoflake
rev: v2.2.1
hooks:
- id: autoflake
name: Remove unused variables and imports
language: python
args: ["--in-place", "--remove-all-unused-imports", "--remove-unused-variables", "--expand-star-imports"]
files: \.py$
- repo: https://github.com/saltstack/pre-commit-remove-import-headers
rev: 1.1.0
hooks:
- id: remove-import-headers
- repo: https://github.com/saltstack/salt-rewrite
# Automatically rewrite code with known rules
rev: 2.5.2
hooks:
- id: salt-rewrite
alias: rewrite-tests
name: Rewrite the test suite
files: ^tests/.*\.py$
args: [--silent, -E, fix_docstrings]
- repo: https://github.com/asottile/pyupgrade
rev: v3.15.0
hooks:
- id: pyupgrade
name: Rewrite Code to be Py3+
args: [
--py3-plus
]
- repo: https://github.com/asottile/reorder_python_imports
rev: v3.12.0
hooks:
- id: reorder-python-imports
args: [
--py3-plus,
]
- repo: https://github.com/psf/black
rev: 23.12.1
hooks:
- id: black
args: [-l 100]
# <---- Formatting -----------------------------------------------------------------------------
07070100000006000081A400000000000000000000000167D9C77000000309000000000000000000000000000000000000002700000000pepper-0.7.6+git44.996fc29/.travis.ymldist: xenial
sudo: false
services:
- docker
language: python
services:
- docker
install:
- pip install tox
python:
- '2.7'
- '3.4'
- '3.5'
- '3.6'
- '3.7'
- '3.8-dev'
env:
- SALT=v2018.3 BACKEND=cherrypy
- SALT=v2018.3 BACKEND=tornado
- SALT=v2019.2 BACKEND=cherrypy
- SALT=v2019.2 BACKEND=tornado
- SALT=develop BACKEND=cherrypy
- SALT=develop BACKEND=tornado
matrix:
allow_failures:
- python: '3.8-dev'
- env: SALT=develop BACKEND=tornado
- env: SALT=develop BACKEND=cherrypy
script:
- PYTHON="${TRAVIS_PYTHON_VERSION/-dev/-rc}"
- TOX_VERSION="py${PYTHON//./}"
- docker run -v $PWD:/pepper -ti --rm "python:$PYTHON" make -C /pepper test PYVERSION="${TOX_VERSION%-rc}" SALT="${SALT}" BACKEND="${BACKEND}"
after_success:
- sudo chown $USER .tox/
- tox -e codecov
07070100000007000081A400000000000000000000000167D9C77000000271000000000000000000000000000000000000002300000000pepper-0.7.6+git44.996fc29/LICENSE Salt - Remote execution system
Copyright 2014-2016 SaltStack Team
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.
07070100000008000081A400000000000000000000000167D9C77000000023000000000000000000000000000000000000002700000000pepper-0.7.6+git44.996fc29/MANIFEST.ininclude LICENSE
include README.rst
07070100000009000081A400000000000000000000000167D9C770000000FE000000000000000000000000000000000000002400000000pepper-0.7.6+git44.996fc29/MakefilePYVERSION?=py37
SALT?=v2019.2
BACKEND?=cherrypy
DEBIAN_FRONTEND=noninteractive
install:
apt update && apt install -y libc6-dev libffi-dev gcc git openssh-server libzmq3-dev
pip install tox
test: install
tox -e flake8,$(PYVERSION)-$(BACKEND)-$(SALT)
0707010000000A000081A400000000000000000000000167D9C77000000B09000000000000000000000000000000000000002600000000pepper-0.7.6+git44.996fc29/README.rst======
Pepper
======
.. image:: https://img.shields.io/pypi/v/salt-pepper.svg
:target: https://pypi.org/project/salt-pepper
.. image:: https://travis-ci.com/saltstack/pepper.svg?branch=develop
:target: https://travis-ci.com/saltstack/pepper
.. image:: https://img.shields.io/pypi/pyversions/salt-pepper.svg
:target: https://pypi.org/project/salt-pepper
.. image:: https://img.shields.io/badge/license-Apache2-blue.svg?maxAge=3600
:target: https://pypi.org/project/salt-pepper
.. image:: https://codecov.io/gh/saltstack/pepper/branch/develop/graph/badge.svg
:target: https://codecov.io/gh/saltstack/pepper/branch/develop
Pepper contains a Python library and CLI scripts for accessing a remote
`salt-api`__ instance.
``pepperlib`` abstracts the HTTP calls to ``salt-api`` so existing Python
projects can easily integrate with a remote Salt installation just by
instantiating a class.
The ``pepper`` CLI script allows users to execute Salt commands from computers
that are external to computers running the ``salt-master`` or ``salt-minion``
daemons as though they were running Salt locally. The long-term goal is to add
additional CLI scripts maintain the same interface as Salt's own CLI scripts
(``salt``, ``salt-run``, ``salt-key``, etc).
It does not require any additional dependencies and runs on Python 2.5+ and
Python 3. (Python 3 support is new, please file an issue if you encounter
trouble.)
.. __: https://github.com/saltstack/salt-api
Installation
------------
.. code-block:: bash
pip install salt-pepper
Usage
-----
Basic usage is in heavy flux. You can run pepper using the script in %PYTHONHOME%/scripts/pepper (a pepper.cmd wrapper is provided for convenience to Windows users).
.. code-block:: bash
export SALTAPI_USER=saltdev SALTAPI_PASS=saltdev SALTAPI_EAUTH=pam
pepper '*' test.ping
pepper '*' test.kwarg hello=dolly
Examples leveraging the runner client.
.. code-block:: bash
pepper --client runner reactor.list
pepper --client runner reactor.add event='test/provision/*' reactors='/srv/salt/state/reactor/test-provision.sls'
Configuration
-------------
You can configure pepper through the command line, using environment variables
or in a configuration file ``$HOME/.pepperrc`` with the following syntax :
.. code-block::
[main]
SALTAPI_URL=https://localhost:8000/
SALTAPI_USER=saltdev
SALTAPI_PASS=saltdev
SALTAPI_EAUTH=pam
Contributing
------------
Please feel free to get involved by sending pull requests or join us on the
Salt mailing list or on IRC in #salt or #salt-devel.
This repo follows the same `contributing guidelines`__ as Salt and uses
separate develop and master branches for in-progress additions and bug-fix
changes respectively.
.. __: https://docs.saltstack.com/en/latest/topics/development/contributing.html
0707010000000B000081A400000000000000000000000167D9C77000000690000000000000000000000000000000000000002F00000000pepper-0.7.6+git44.996fc29/azure-pipelines.ymltrigger:
- develop
variables:
python: '["py2.7", "py3.4", "py3.5", "py3.6", "py3.7", "py3.8"]'
salt: '["v2018.3", "v2019.2", "develop"]'
backends: '["cherrypy", "tornado"]'
jobs:
- job: build_matrix
pool:
vmImage: 'Ubuntu-16.04'
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.7'
architecture: x64
- task: PythonScript@0
name: matrix
inputs:
scriptSource: Inline
script: |
import json
matrix = {}
for pyver in $(python):
for saltver in $(salt):
for backend in $(backends):
matrix['{0}-{1}-{2}'.format(pyver, backend, saltver)] = {
'python.version': pyver[2:],
'version': pyver.replace('.', ''),
'salt': saltver,
'backend': backend,
}
print('##vso[task.setvariable variable=matrix;isOutput=true]{0}'.format(json.dumps(matrix)))
- job: test_pepper
dependsOn: build_matrix
pool:
vmImage: 'Ubuntu-16.04'
strategy:
matrix: $[ dependencies.build_matrix.outputs['matrix.matrix'] ]
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '$(python.version)'
architecture: x64
- script: |
pip install tox
displayName: Install dependencies
- script: |
docker run -v $PWD:/pepper --rm "python:$(python.version)" make -C /pepper test PYTHON_VERSION=$(version) SALT=$(salt) BACKEND=$(backend)
displayName: pytest
- script: |
sudo chown $USER .tox/
tox -e codecov
displayName: codecov
env:
CODECOV_TOKEN: '16c2a232-4329-438c-b163-ccbfeeab47aa'
0707010000000C000081A400000000000000000000000167D9C77000000175000000000000000000000000000000000000002700000000pepper-0.7.6+git44.996fc29/codecov.ymlcodecov:
notify:
require_ci_to_pass: yes
branch: develop
coverage:
precision: 2
round: down
range: "50...100"
status:
project: yes
patch: yes
changes: no
parsers:
gcov:
branch_detection:
conditional: yes
loop: yes
method: no
macro: no
comment:
layout: "header, diff"
behavior: default
require_changes: no
0707010000000D000081A400000000000000000000000167D9C770000017BC000000000000000000000000000000000000002600000000pepper-0.7.6+git44.996fc29/noxfile.pyimport datetime
import os
import pathlib
import shutil
import nox
from nox.command import CommandFailed
API_BACKEND = [os.environ.get("API_BACKEND")] or ["cherrypy", "tornado"]
PYTHON_VERSIONS = [os.environ.get("PYTHON_VERSIONS")] or ["3.8", "3.9", "3.10", "3.11"]
# Nox options
# Reuse existing virtualenvs
nox.options.reuse_existing_virtualenvs = True
# Don't fail on missing interpreters
nox.options.error_on_missing_interpreters = False
# Be verbose when running under a CI context
CI_RUN = os.environ.get("CI")
PIP_INSTALL_SILENT = CI_RUN is False
SKIP_REQUIREMENTS_INSTALL = "SKIP_REQUIREMENTS_INSTALL" in os.environ
EXTRA_REQUIREMENTS_INSTALL = os.environ.get("EXTRA_REQUIREMENTS_INSTALL")
SALT_REQUIREMENT = os.environ.get("SALT_REQUIREMENT") or "salt"
COVERAGE_VERSION_REQUIREMENT = "coverage==5.5"
# Prevent Python from writing bytecode
os.environ["PYTHONDONTWRITEBYTECODE"] = "1"
# Global Path Definitions
REPO_ROOT = pathlib.Path(__file__).resolve().parent
# Change current directory to REPO_ROOT
os.chdir(str(REPO_ROOT))
ARTIFACTS_DIR = REPO_ROOT / "artifacts"
# Make sure the artifacts directory exists
ARTIFACTS_DIR.mkdir(parents=True, exist_ok=True)
RUNTESTS_LOGFILE = ARTIFACTS_DIR / "runtests-{}.log".format(
datetime.datetime.now().strftime("%Y%m%d%H%M%S.%f")
)
COVERAGE_REPORT_DB = REPO_ROOT / ".coverage"
COVERAGE_REPORT_PROJECT = ARTIFACTS_DIR.relative_to(REPO_ROOT) / "coverage-project.xml"
COVERAGE_REPORT_TESTS = ARTIFACTS_DIR.relative_to(REPO_ROOT) / "coverage-tests.xml"
JUNIT_REPORT = ARTIFACTS_DIR.relative_to(REPO_ROOT) / "junit-report.xml"
def _install_requirements(
session,
*passed_requirements, # pylint: disable=unused-argument
install_coverage_requirements=True,
install_test_requirements=True,
install_source=True,
install_salt=True,
install_extras=None,
):
install_extras = install_extras or []
if SKIP_REQUIREMENTS_INSTALL is False:
# Always have the wheel package installed
session.install("--progress-bar=off", "wheel", silent=PIP_INSTALL_SILENT)
if install_coverage_requirements:
session.install(
"--progress-bar=off", COVERAGE_VERSION_REQUIREMENT, silent=PIP_INSTALL_SILENT
)
# Install the latest salt package
if install_salt:
session.install("--progress-bar=off", SALT_REQUIREMENT, silent=PIP_INSTALL_SILENT)
if install_test_requirements:
requirements_file = REPO_ROOT / "tests" / "requirements.txt"
install_command = [
"--progress-bar=off",
"-r",
str(requirements_file.relative_to(REPO_ROOT)),
]
session.install(*install_command, silent=PIP_INSTALL_SILENT)
if EXTRA_REQUIREMENTS_INSTALL:
session.log(
"Installing the following extra requirements because the "
"EXTRA_REQUIREMENTS_INSTALL environment variable was set: "
"EXTRA_REQUIREMENTS_INSTALL='%s'",
EXTRA_REQUIREMENTS_INSTALL,
)
install_command = ["--progress-bar=off"]
install_command += [req.strip() for req in EXTRA_REQUIREMENTS_INSTALL.split()]
session.install(*install_command, silent=PIP_INSTALL_SILENT)
if install_source:
pkg = "."
if install_extras:
pkg += f"[{','.join(install_extras)}]"
session.install("-e", pkg, silent=PIP_INSTALL_SILENT)
elif install_extras:
pkg = f".[{','.join(install_extras)}]"
session.install(pkg, silent=PIP_INSTALL_SILENT)
"""
Nox session to run tests for corresponding python versions and salt versions
"""
@nox.session(python=PYTHON_VERSIONS)
@nox.parametrize("api_backend", API_BACKEND)
def tests(session, api_backend):
_install_requirements(session)
args = [
"--rootdir",
str(REPO_ROOT),
f"--log-file={RUNTESTS_LOGFILE.relative_to(REPO_ROOT)}",
"--log-file-level=debug",
"--show-capture=no",
f"--junitxml={JUNIT_REPORT}",
"--showlocals",
"-ra",
"-s",
]
if session._runner.global_config.forcecolor:
args.append("--color=yes")
if not session.posargs:
args.append("tests/")
else:
for arg in session.posargs:
if arg.startswith("--color") and args[0].startswith("--color"):
args.pop(0)
args.append(arg)
for arg in session.posargs:
if arg.startswith("-"):
continue
if arg.startswith(f"tests{os.sep}"):
break
try:
pathlib.Path(arg).resolve().relative_to(REPO_ROOT / "tests")
break
except ValueError:
continue
else:
args.append("tests/")
try:
session.run(
"coverage", "run", "-m", "pytest", f"--salt-api-backend=rest_{api_backend}", *args
)
finally:
# Always combine and generate the XML coverage report
try:
session.run("coverage", "combine")
except CommandFailed:
# Sometimes some of the coverage files are corrupt which would
# trigger a CommandFailed exception
pass
# Generate report for salt code coverage
session.run(
"coverage",
"xml",
"-o",
str(COVERAGE_REPORT_PROJECT),
)
try:
session.run("coverage", "report", "--show-missing", "--include=pepper/*")
finally:
# Move the coverage DB to artifacts/coverage in order for it to be archived by CI
if COVERAGE_REPORT_DB.exists():
shutil.move(str(COVERAGE_REPORT_DB), str(ARTIFACTS_DIR / COVERAGE_REPORT_DB.name))
@nox.session(python="3.10")
def flake8(session):
_install_requirements(session)
# Install flake8
session.install("flake8")
# Run flake8
session.run("flake8", "tests/", "pepper/", "scripts/pepper", "setup.py")
0707010000000E000041ED00000000000000000000000267D9C77000000000000000000000000000000000000000000000002200000000pepper-0.7.6+git44.996fc29/pepper0707010000000F000081A400000000000000000000000167D9C770000001D5000000000000000000000000000000000000002E00000000pepper-0.7.6+git44.996fc29/pepper/__init__.py"""
Pepper is a CLI front-end to salt-api
"""
from importlib.metadata import PackageNotFoundError
from importlib.metadata import version
from pepper.libpepper import Pepper
from pepper.libpepper import PepperException
__all__ = ("__version__", "Pepper", "PepperException")
try:
__version__ = version("salt_pepper")
except PackageNotFoundError:
# package is not installed
__version__ = None
# For backwards compatibility
version = __version__
sha = None
07070100000010000081A400000000000000000000000167D9C77000006396000000000000000000000000000000000000002900000000pepper-0.7.6+git44.996fc29/pepper/cli.py"""
A CLI interface to a remote salt-api instance
"""
import getpass
import json
import logging
import optparse
import os
import sys
import textwrap
import time
import pepper
from pepper.exceptions import PepperArgumentsException
from pepper.exceptions import PepperAuthException
from pepper.exceptions import PepperException
# Import Pepper Libraries
try:
# Python 3
from configparser import ConfigParser, RawConfigParser
except ImportError:
# Python 2
from ConfigParser import ConfigParser, RawConfigParser
try:
# Python 3
JSONDecodeError = json.decode.JSONDecodeError
except AttributeError:
# Python 2
JSONDecodeError = ValueError
try:
input = raw_input
except NameError:
pass
if sys.version_info[0] == 2:
FileNotFoundError = IOError
logger = logging.getLogger(__name__)
class PepperCli:
def __init__(self, seconds_to_wait=3):
self.seconds_to_wait = seconds_to_wait
self.parser = self.get_parser()
self.parser.option_groups.extend(
[
self.add_globalopts(),
self.add_tgtopts(),
self.add_authopts(),
self.add_retcodeopts(),
]
)
self.parse()
def get_parser(self):
return optparse.OptionParser(
description=__doc__, usage="%prog [opts]", version=pepper.__version__
)
def parse(self):
"""
Parse all args
"""
self.parser.add_option(
"-c",
dest="config",
default=os.environ.get("PEPPERRC", os.path.join(os.path.expanduser("~"), ".pepperrc")),
help=textwrap.dedent(
"""
Configuration file location. Default is a file path in the
"PEPPERRC" environment variable or ~/.pepperrc.
"""
),
)
self.parser.add_option(
"-p",
dest="profile",
default=os.environ.get("PEPPERPROFILE", "main"),
help=textwrap.dedent(
"""
Profile in config file to use. Default is "PEPPERPROFILE" environment
variable or 'main'
"""
),
)
self.parser.add_option(
"-m",
dest="master",
default=os.environ.get(
"MASTER_CONFIG",
os.path.join(os.path.expanduser("~"), ".config", "pepper", "master"),
),
help=textwrap.dedent(
"""
Master Configuration file location for configuring outputters.
default: ~/.config/pepper/master
"""
),
)
self.parser.add_option(
"-o",
"--out",
dest="output",
default=None,
help=textwrap.dedent(
"""
Salt outputter to use for printing out returns.
"""
),
)
self.parser.add_option(
"--output-file",
dest="output_file",
default=None,
help=textwrap.dedent(
"""
File to put command output in
"""
),
)
self.parser.add_option(
"-v",
dest="verbose",
default=0,
action="count",
help=textwrap.dedent(
"""
Increment output verbosity; may be specified multiple times
"""
),
)
self.parser.add_option(
"-H",
"--debug-http",
dest="debug_http",
default=False,
action="store_true",
help=textwrap.dedent(
"""
Output the HTTP request/response headers on stderr
"""
),
)
self.parser.add_option(
"--ignore-ssl-errors",
action="store_true",
dest="ignore_ssl_certificate_errors",
default=False,
help=textwrap.dedent(
"""
Ignore any SSL certificate that may be encountered. Note that it is
recommended to resolve certificate errors for production.
"""
),
)
self.options, self.args = self.parser.parse_args()
option_names = ["fail_any", "fail_any_none", "fail_all", "fail_all_none"]
toggled_options = [name for name in option_names if getattr(self.options, name)]
if len(toggled_options) > 1:
s = repr(toggled_options).strip("[]")
self.parser.error("Options %s are mutually exclusive" % s)
def add_globalopts(self):
"""
Misc global options
"""
optgroup = optparse.OptionGroup(
self.parser, "Pepper ``salt`` Options", "Mimic the ``salt`` CLI"
)
optgroup.add_option(
"-t",
"--timeout",
dest="timeout",
type="int",
default=60,
help=textwrap.dedent(
"""
Specify wait time (in seconds) before returning control to the shell
"""
),
)
optgroup.add_option(
"--client",
dest="client",
default="local",
help=textwrap.dedent(
"""
specify the salt-api client to use (local, local_async,
runner, etc)
"""
),
)
optgroup.add_option(
"--json",
dest="json_input",
help=textwrap.dedent(
"""
Enter JSON at the CLI instead of positional (text) arguments. This
is useful for arguments that need complex data structures.
Specifying this argument will cause positional arguments to be
ignored.
"""
),
)
optgroup.add_option(
"--json-file",
dest="json_file",
help=textwrap.dedent(
"""
Specify file containing the JSON to be used by pepper
"""
),
)
optgroup.add_option(
"--fail-if-incomplete",
action="store_true",
dest="fail_if_minions_dont_respond",
default=False,
help=textwrap.dedent(
"""
Return a failure exit code if not all minions respond. This option
requires the authenticated user have access to run the
`jobs.list_jobs` runner function.
"""
),
)
return optgroup
def add_tgtopts(self):
"""
Targeting
"""
optgroup = optparse.OptionGroup(
self.parser, "Targeting Options", "Target which minions to run commands on"
)
optgroup.defaults.update({"expr_form": "glob"})
optgroup.add_option(
"-E",
"--pcre",
dest="expr_form",
action="store_const",
const="pcre",
help="Target hostnames using PCRE regular expressions",
)
optgroup.add_option(
"-L",
"--list",
dest="expr_form",
action="store_const",
const="list",
help="Specify a comma delimited list of hostnames",
)
optgroup.add_option(
"-G",
"--grain",
dest="expr_form",
action="store_const",
const="grain",
help="Target based on system properties",
)
optgroup.add_option(
"--grain-pcre",
dest="expr_form",
action="store_const",
const="grain_pcre",
help="Target based on PCRE matches on system properties",
)
optgroup.add_option(
"-I",
"--pillar",
dest="expr_form",
action="store_const",
const="pillar",
help="Target based on pillar values",
)
optgroup.add_option(
"--pillar-pcre",
dest="expr_form",
action="store_const",
const="pillar_pcre",
help="Target based on PCRE matches on pillar values",
)
optgroup.add_option(
"-R",
"--range",
dest="expr_form",
action="store_const",
const="range",
help="Target based on range expression",
)
optgroup.add_option(
"-C",
"--compound",
dest="expr_form",
action="store_const",
const="compound",
help="Target based on compound expression",
)
optgroup.add_option(
"-N",
"--nodegroup",
dest="expr_form",
action="store_const",
const="nodegroup",
help="Target based on a named nodegroup",
)
optgroup.add_option("--batch", dest="batch", default=None)
return optgroup
def add_authopts(self):
"""
Authentication options
"""
optgroup = optparse.OptionGroup(
self.parser,
"Authentication Options",
textwrap.dedent(
"""
Authentication credentials can optionally be supplied via the
environment variables:
SALTAPI_URL, SALTAPI_USER, SALTAPI_PASS, SALTAPI_EAUTH.
"""
),
)
optgroup.add_option(
"-u",
"--saltapi-url",
dest="saltapiurl",
help="Specify the host url. Defaults to https://localhost:8080",
)
optgroup.add_option(
"-a",
"--auth",
"--eauth",
"--extended-auth",
dest="eauth",
help=textwrap.dedent(
"""
Specify the external_auth backend to authenticate against and
interactively prompt for credentials
"""
),
)
optgroup.add_option(
"--username",
dest="username",
help=textwrap.dedent(
"""
Optional, defaults to user name. will be prompt if empty unless --non-interactive
"""
),
)
optgroup.add_option(
"--password",
dest="password",
help=textwrap.dedent(
"""
Optional, but will be prompted unless --non-interactive
"""
),
)
optgroup.add_option(
"--token-expire",
dest="token_expire",
help=textwrap.dedent(
"""
Set eauth token expiry in seconds. Must be allowed per
user. See the `token_expire_user_override` Master setting
for more info.
"""
),
)
optgroup.add_option(
"--non-interactive",
action="store_false",
dest="interactive",
default=True,
help=textwrap.dedent(
"""
Optional, fail rather than waiting for input
"""
),
)
optgroup.add_option(
"-T",
"--make-token",
default=False,
dest="mktoken",
action="store_true",
help=textwrap.dedent(
"""
Generate and save an authentication token for re-use. The token is
generated and made available for the period defined in the Salt
Master.
"""
),
)
optgroup.add_option(
"-r",
"--run-uri",
default=False,
dest="userun",
action="store_true",
help=textwrap.dedent(
"""
Use an eauth token from /token and send commands through the
/run URL instead of the traditional session token
approach.
"""
),
)
optgroup.add_option(
"-x",
dest="cache",
default=os.environ.get(
"PEPPERCACHE", os.path.join(os.path.expanduser("~"), ".peppercache")
),
help=textwrap.dedent(
"""
Cache file location. Default is a file path in the
"PEPPERCACHE" environment variable or ~/.peppercache.
"""
),
)
return optgroup
def add_retcodeopts(self):
"""
ret code validation options
"""
optgroup = optparse.OptionGroup(
self.parser, "retcode Field Validation Options", "Validate return.HOST.retcode fields"
)
optgroup.add_option(
"--fail-any",
dest="fail_any",
action="store_true",
help="Fail if any of retcode field is non zero.",
)
optgroup.add_option(
"--fail-any-none",
dest="fail_any_none",
action="store_true",
help="Fail if any of retcode field is non zero or there is no retcode at all.",
)
optgroup.add_option(
"--fail-all",
dest="fail_all",
action="store_true",
help="Fail if all retcode fields are non zero.",
)
optgroup.add_option(
"--fail-all-none",
dest="fail_all_none",
action="store_true",
help="Fail if all retcode fields are non zero or there is no retcode at all.",
)
return optgroup
def get_login_details(self):
"""
This parses the config file, environment variables and command line options
and returns the config values
Order of parsing:
command line options, ~/.pepperrc, environment, defaults
"""
# setting default values
results = {
"SALTAPI_USER": None,
"SALTAPI_PASS": None,
"SALTAPI_EAUTH": "auto",
}
try:
config = ConfigParser(interpolation=None)
except TypeError:
config = RawConfigParser()
config.read(self.options.config)
# read file
profile = self.options.profile
if config.has_section(profile):
for key, value in list(results.items()):
if config.has_option(profile, key):
results[key] = config.get(profile, key)
# get environment values
for key, value in list(results.items()):
results[key] = os.environ.get(key, results[key])
if results["SALTAPI_EAUTH"] == "kerberos":
results["SALTAPI_PASS"] = None
if self.options.eauth:
results["SALTAPI_EAUTH"] = self.options.eauth
if self.options.token_expire:
results["SALTAPI_TOKEN_EXPIRE"] = self.options.token_expire
if self.options.username is None and results["SALTAPI_USER"] is None:
if self.options.interactive:
results["SALTAPI_USER"] = input("Username: ")
else:
raise PepperAuthException("SALTAPI_USER required")
else:
if self.options.username is not None:
results["SALTAPI_USER"] = self.options.username
if (
self.options.password is None
and results["SALTAPI_PASS"] is None
and results["SALTAPI_EAUTH"] != "kerberos"
):
if self.options.interactive:
results["SALTAPI_PASS"] = getpass.getpass(prompt="Password: ")
else:
raise PepperAuthException("SALTAPI_PASS required")
else:
if self.options.password is not None:
results["SALTAPI_PASS"] = self.options.password
return results
def parse_url(self):
"""
Determine api url
"""
url = "https://localhost:8000/"
try:
config = ConfigParser(interpolation=None)
except TypeError:
config = RawConfigParser()
config.read(self.options.config)
# read file
profile = self.options.profile
if config.has_section(profile):
if config.has_option(profile, "SALTAPI_URL"):
url = config.get(profile, "SALTAPI_URL")
# get environment values
url = os.environ.get("SALTAPI_URL", url)
# get eauth prompt options
if self.options.saltapiurl:
url = self.options.saltapiurl
return url
def parse_login(self):
"""
Extract the authentication credentials
"""
login_details = self.get_login_details()
# Auth values placeholder; grab interactively at CLI or from config
username = login_details["SALTAPI_USER"]
password = login_details["SALTAPI_PASS"]
eauth = login_details["SALTAPI_EAUTH"]
ret = dict(username=username, password=password, eauth=eauth)
token_expire = login_details.get("SALTAPI_TOKEN_EXPIRE", None)
if token_expire:
ret["token_expire"] = int(token_expire)
return ret
def parse_cmd(self, api):
"""
Extract the low data for a command from the passed CLI params
"""
# Short-circuit if JSON was given.
if self.options.json_input:
try:
return json.loads(self.options.json_input)
except JSONDecodeError:
raise PepperArgumentsException("Invalid JSON given.")
if self.options.json_file:
try:
with open(self.options.json_file) as json_content:
try:
return json.load(json_content)
except JSONDecodeError:
raise PepperArgumentsException("Invalid JSON given.")
except FileNotFoundError:
raise PepperArgumentsException("Cannot open file: %s", self.options.json_file)
args = list(self.args)
client = self.options.client if not self.options.batch else "local_batch"
low = {"client": client}
if client.startswith("local"):
if len(args) < 2:
self.parser.error("Command or target not specified")
low["tgt_type"] = self.options.expr_form
low["tgt"] = args.pop(0)
low["fun"] = args.pop(0)
low["batch"] = self.options.batch
low["arg"] = args
elif client.startswith("runner"):
low["fun"] = args.pop(0)
# post https://github.com/saltstack/salt/pull/50124, kwargs can be
# passed as is in foo=bar form, splitting and deserializing will
# happen in salt-api. additionally, the presence of salt-version header
# means we are neon or newer, so don't need a finer grained check
if api.salt_version:
low["arg"] = args
else:
for arg in args:
if "=" in arg:
key, value = arg.split("=", 1)
try:
low[key] = json.loads(value)
except JSONDecodeError:
low[key] = value
else:
low.setdefault("arg", []).append(arg)
elif client.startswith("wheel"):
low["fun"] = args.pop(0)
# see above comment in runner arg handling
if api.salt_version:
low["arg"] = args
else:
for arg in args:
if "=" in arg:
key, value = arg.split("=", 1)
try:
low[key] = json.loads(value)
except JSONDecodeError:
low[key] = value
else:
low.setdefault("arg", []).append(arg)
elif client.startswith("ssh"):
if len(args) < 2:
self.parser.error("Command or target not specified")
low["tgt_type"] = self.options.expr_form
low["tgt"] = args.pop(0)
low["fun"] = args.pop(0)
low["batch"] = self.options.batch
low["arg"] = args
else:
raise PepperException("Client not implemented: {}".format(client))
return [low]
def poll_for_returns(self, api, load):
"""
Run a command with the local_async client and periodically poll the job
cache for returns for the job.
"""
load[0]["client"] = "local_async"
async_ret = self.low(api, load)
jid = async_ret["return"][0]["jid"]
nodes = async_ret["return"][0]["minions"]
ret_nodes = []
exit_code = 1
# keep trying until all expected nodes return
total_time = 0
start_time = time.time()
exit_code = 0
while True:
total_time = time.time() - start_time
if total_time > self.options.timeout:
exit_code = 1
break
jid_ret = self.low(
api,
[
{
"client": "runner",
"fun": "jobs.lookup_jid",
"kwarg": {
"jid": jid,
},
}
],
)
inner_ret = jid_ret["return"][0]
# sometimes ret is nested in data
if "data" in inner_ret:
inner_ret = inner_ret["data"]
responded = set(inner_ret.keys()) ^ set(ret_nodes)
for node in responded:
yield None, [{node: inner_ret[node]}]
ret_nodes = list(inner_ret.keys())
if set(ret_nodes) == set(nodes):
exit_code = 0
break
else:
time.sleep(self.seconds_to_wait)
exit_code = exit_code if self.options.fail_if_minions_dont_respond else 0
failed = list(set(ret_nodes) ^ set(nodes))
if failed:
yield exit_code, [{"Failed": failed}]
def login(self, api):
login = api.token if self.options.userun else api.login
if self.options.mktoken:
token_file = self.options.cache
try:
with open(token_file) as f:
auth = json.load(f)
if auth["expire"] < time.time() + 30:
logger.error("Login token expired")
raise Exception("Login token expired")
except Exception as e:
if e.args[0] != 2:
logger.error("Unable to load login token from {} {}".format(token_file, str(e)))
if os.path.isfile(token_file):
os.remove(token_file)
auth = login(**self.parse_login())
try:
oldumask = os.umask(0)
fdsc = os.open(token_file, os.O_WRONLY | os.O_CREAT, 0o600)
with os.fdopen(fdsc, "wt") as f:
json.dump(auth, f)
except Exception as e:
logger.error("Unable to save token to {} {}".format(token_file, str(e)))
finally:
os.umask(oldumask)
else:
auth = login(**self.parse_login())
api.auth = auth
self.auth = auth
return auth
def low(self, api, load):
path = "/run" if self.options.userun else "/"
if self.options.userun:
for i in load:
i["token"] = self.auth["token"]
# having a defined salt_version means changes from https://github.com/saltstack/salt/pull/51979
# are available if backend is tornado, so safe to supply timeout
if self.options.timeout and api.salt_version:
for i in load:
if not i.get("client", "").startswith("wheel"):
i["timeout"] = self.options.timeout
return api.low(load, path=path)
def run(self):
"""
Parse all arguments and call salt-api
"""
# set up logging
rootLogger = logging.getLogger(name=None)
rootLogger.addHandler(logging.StreamHandler())
rootLogger.setLevel(max(logging.ERROR - (self.options.verbose * 10), 1))
api = pepper.Pepper(
self.parse_url(),
debug_http=self.options.debug_http,
ignore_ssl_errors=self.options.ignore_ssl_certificate_errors,
)
self.login(api)
load = self.parse_cmd(api)
for entry in load:
if not entry.get("client", "").startswith("wheel"):
entry["full_return"] = True
if self.options.fail_if_minions_dont_respond:
for exit_code, ret in self.poll_for_returns(api, load): # pragma: no cover
yield exit_code, json.dumps(ret, sort_keys=True, indent=4)
else:
ret = self.low(api, load)
exit_code = 0
yield exit_code, json.dumps(ret, sort_keys=True, indent=4)
07070100000011000081A400000000000000000000000167D9C77000000092000000000000000000000000000000000000003000000000pepper-0.7.6+git44.996fc29/pepper/exceptions.pyclass PepperAuthException(Exception):
pass
class PepperArgumentsException(Exception):
pass
class PepperException(Exception):
pass
07070100000012000081A400000000000000000000000167D9C77000003AB8000000000000000000000000000000000000002F00000000pepper-0.7.6+git44.996fc29/pepper/libpepper.py"""
A Python library for working with Salt's REST API
(Specifically the rest_cherrypy netapi module.)
"""
import json
import logging
import re
import ssl
from pepper.exceptions import PepperException
try:
ssl._create_default_https_context = ssl._create_stdlib_context
except Exception:
pass
try:
from urllib.request import (
HTTPHandler,
HTTPSHandler,
Request,
urlopen,
install_opener,
build_opener,
)
from urllib.error import HTTPError, URLError
import urllib.parse as urlparse
except ImportError:
from urllib2 import (
HTTPHandler,
HTTPSHandler,
Request,
urlopen,
install_opener,
build_opener,
HTTPError,
URLError,
)
import urlparse
logger = logging.getLogger(__name__)
class Pepper:
"""
A thin wrapper for making HTTP calls to the salt-api rest_cherrpy REST
interface
>>> api = Pepper('https://localhost:8000')
>>> api.login('saltdev', 'saltdev', 'pam')
{"return": [
{
"eauth": "pam",
"expire": 1370434219.714091,
"perms": [
"test.*"
],
"start": 1370391019.71409,
"token": "c02a6f4397b5496ba06b70ae5fd1f2ab75de9237",
"user": "saltdev"
}
]
}
>>> api.low([{'client': 'local', 'tgt': '*', 'fun': 'test.ping'}])
{u'return': [{u'ms-0': True,
u'ms-1': True,
u'ms-2': True,
u'ms-3': True,
u'ms-4': True}]}
"""
def __init__(self, api_url="https://localhost:8000", debug_http=False, ignore_ssl_errors=False):
"""
Initialize the class with the URL of the API
:param api_url: Host or IP address of the salt-api URL;
include the port number
:param debug_http: Add a flag to urllib2 to output the HTTP exchange
:param ignore_ssl_errors: Add a flag to urllib2 to ignore invalid SSL certificates
:raises PepperException: if the api_url is misformed
"""
split = urlparse.urlsplit(api_url)
if split.scheme not in ["http", "https"]:
raise PepperException("salt-api URL missing HTTP(s) protocol: {}".format(api_url))
self.api_url = api_url
self.debug_http = int(debug_http)
self._ssl_verify = not ignore_ssl_errors
self.auth = {}
self.salt_version = None
def req_stream(self, path):
"""
A thin wrapper to get a response from saltstack api.
The body of the response will not be downloaded immediately.
Make sure to close the connection after use.
api = Pepper('http://ipaddress/api/')
print(api.login('salt','salt','pam'))
response = api.req_stream('/events')
:param path: The path to the salt api resource
:return: :class:`Response <Response>` object
:rtype: requests.Response
"""
import requests
headers = {
"Accept": "application/json",
"Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest",
}
if self.auth and "token" in self.auth and self.auth["token"]:
headers.setdefault("X-Auth-Token", self.auth["token"])
else:
raise PepperException("Authentication required")
return
params = {
"url": self._construct_url(path),
"headers": headers,
"verify": self._ssl_verify is True,
"stream": True,
}
try:
resp = requests.get(**params)
if resp.status_code == 401:
raise PepperException(str(resp.status_code) + ":Authentication denied")
return
if resp.status_code == 500:
raise PepperException(str(resp.status_code) + ":Server error.")
return
if resp.status_code == 404:
raise PepperException(str(resp.status_code) + " :This request returns nothing.")
return
except PepperException as e:
print(e)
return
return resp
def req_get(self, path):
"""
A thin wrapper from get http method of saltstack api
api = Pepper('http://ipaddress/api/')
print(api.login('salt','salt','pam'))
print(api.req_get('/keys'))
"""
import requests
headers = {
"Accept": "application/json",
"Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest",
}
if self.auth and "token" in self.auth and self.auth["token"]:
headers.setdefault("X-Auth-Token", self.auth["token"])
else:
raise PepperException("Authentication required")
return
params = {
"url": self._construct_url(path),
"headers": headers,
"verify": self._ssl_verify is True,
}
try:
resp = requests.get(**params)
if resp.status_code == 401:
raise PepperException(str(resp.status_code) + ":Authentication denied")
return
if resp.status_code == 500:
raise PepperException(str(resp.status_code) + ":Server error.")
return
if resp.status_code == 404:
raise PepperException(str(resp.status_code) + " :This request returns nothing.")
return
except PepperException as e:
print(e)
return
return resp.json()
def req(self, path, data=None):
"""
A thin wrapper around urllib2 to send requests and return the response
If the current instance contains an authentication token it will be
attached to the request as a custom header.
:rtype: dictionary
"""
if (hasattr(data, "get") and data.get("eauth") == "kerberos") or self.auth.get(
"eauth"
) == "kerberos":
return self.req_requests(path, data)
headers = {
"Accept": "application/json",
"Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest",
}
opener = build_opener()
for handler in opener.handlers:
if isinstance(handler, HTTPHandler):
handler.set_http_debuglevel(self.debug_http)
if isinstance(handler, HTTPSHandler):
handler.set_http_debuglevel(self.debug_http)
install_opener(opener)
# Build POST data
if data is not None:
postdata = json.dumps(data).encode()
clen = len(postdata)
else:
postdata = None
# Create request object
url = self._construct_url(path)
req = Request(url, postdata, headers)
# Add POST data to request
if data is not None:
req.add_header("Content-Length", clen)
# Add auth header to request
if path != "/run" and self.auth and "token" in self.auth and self.auth["token"]:
req.add_header("X-Auth-Token", self.auth["token"])
# Send request
try:
if not (self._ssl_verify):
con = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
f = urlopen(req, context=con)
else:
f = urlopen(req)
content = f.read().decode("utf-8")
if self.debug_http:
logger.debug("Response: %s", content)
ret = json.loads(content)
if not self.salt_version and "x-salt-version" in f.headers:
self._parse_salt_version(f.headers["x-salt-version"])
except (HTTPError, URLError) as exc:
logger.debug("Error with request", exc_info=True)
status = getattr(exc, "code", None)
if status == 401:
raise PepperException("Authentication denied")
if status == 500:
raise PepperException("Server error.")
logger.error("Error with request: {}".format(exc))
raise
except AttributeError:
logger.debug("Error converting response from JSON", exc_info=True)
raise PepperException("Unable to parse the server response.")
return ret
def req_requests(self, path, data=None):
"""
A thin wrapper around request and request_kerberos to send
requests and return the response
If the current instance contains an authentication token it will be
attached to the request as a custom header.
:rtype: dictionary
"""
import requests
from requests_gssapi import HTTPSPNEGOAuth, OPTIONAL
auth = HTTPSPNEGOAuth(mutual_authentication=OPTIONAL)
headers = {
"Accept": "application/json",
"Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest",
}
if self.auth and "token" in self.auth and self.auth["token"]:
headers.setdefault("X-Auth-Token", self.auth["token"])
# Optionally toggle SSL verification
params = {
"url": self._construct_url(path),
"headers": headers,
"verify": self._ssl_verify is True,
"auth": auth,
"data": json.dumps(data),
}
logger.debug("postdata {}".format(params))
resp = requests.post(**params)
if resp.status_code == 401:
# TODO should be resp.raise_from_status
raise PepperException("Authentication denied")
if resp.status_code == 500:
# TODO should be resp.raise_from_status
raise PepperException("Server error.")
if not self.salt_version and "x-salt-version" in resp.headers:
self._parse_salt_version(resp.headers["x-salt-version"])
return resp.json()
def low(self, lowstate, path="/"):
"""
Execute a command through salt-api and return the response
:param string path: URL path to be joined with the API hostname
:param list lowstate: a list of lowstate dictionaries
"""
return self.req(path, lowstate)
def local(self, tgt, fun, arg=None, kwarg=None, expr_form="glob", timeout=None, ret=None):
"""
Run a single command using the ``local`` client
Wraps :meth:`low`.
"""
low = {
"client": "local",
"tgt": tgt,
"fun": fun,
}
if arg:
low["arg"] = arg
if kwarg:
low["kwarg"] = kwarg
if expr_form:
low["expr_form"] = expr_form
if timeout:
low["timeout"] = timeout
if ret:
low["ret"] = ret
return self.low([low])
def local_async(self, tgt, fun, arg=None, kwarg=None, expr_form="glob", timeout=None, ret=None):
"""
Run a single command using the ``local_async`` client
Wraps :meth:`low`.
"""
low = {
"client": "local_async",
"tgt": tgt,
"fun": fun,
}
if arg:
low["arg"] = arg
if kwarg:
low["kwarg"] = kwarg
if expr_form:
low["expr_form"] = expr_form
if timeout:
low["timeout"] = timeout
if ret:
low["ret"] = ret
return self.low([low])
def local_batch(self, tgt, fun, arg=None, kwarg=None, expr_form="glob", batch="50%", ret=None):
"""
Run a single command using the ``local_batch`` client
Wraps :meth:`low`.
"""
low = {
"client": "local_batch",
"tgt": tgt,
"fun": fun,
}
if arg:
low["arg"] = arg
if kwarg:
low["kwarg"] = kwarg
if expr_form:
low["expr_form"] = expr_form
if batch:
low["batch"] = batch
if ret:
low["ret"] = ret
return self.low([low])
def lookup_jid(self, jid):
"""
Get job results
Wraps :meth:`runner`.
"""
return self.runner("jobs.lookup_jid", jid="{}".format(jid))
def runner(self, fun, arg=None, **kwargs):
"""
Run a single command using the ``runner`` client
Usage::
runner('jobs.lookup_jid', jid=12345)
"""
low = {
"client": "runner",
"fun": fun,
}
if arg:
low["arg"] = arg
low.update(kwargs)
return self.low([low])
def wheel(self, fun, arg=None, kwarg=None, **kwargs):
"""
Run a single command using the ``wheel`` client
Usage::
wheel('key.accept', match='myminion')
"""
low = {
"client": "wheel",
"fun": fun,
}
if arg:
low["arg"] = arg
if kwarg:
low["kwarg"] = kwarg
low.update(kwargs)
return self.low([low])
def _send_auth(self, path, **kwargs):
return self.req(path, kwargs)
def login(self, username=None, password=None, eauth=None, **kwargs):
"""
Authenticate with salt-api and return the user permissions and
authentication token or an empty dict
"""
local = locals()
kwargs.update(
{
key: local[key]
for key in ("username", "password", "eauth")
if local.get(key, None) is not None
}
)
self.auth = self._send_auth("/login", **kwargs).get("return", [{}])[0]
return self.auth
def token(self, **kwargs):
"""
Get an eauth token from Salt for use with the /run URL
"""
self.auth = self._send_auth("/token", **kwargs)[0]
return self.auth
def _construct_url(self, path):
"""
Construct the url to salt-api for the given path
Args:
path: the path to the salt-api resource
>>> api = Pepper('https://localhost:8000/salt-api/')
>>> api._construct_url('/login')
'https://localhost:8000/salt-api/login'
"""
relative_path = path.lstrip("/")
return urlparse.urljoin(self.api_url, relative_path)
def _parse_salt_version(self, version):
# borrow from salt.version
git_describe_regex = re.compile(
r"(?:[^\d]+)?(?P<major>[\d]{1,4})"
r"\.(?P<minor>[\d]{1,2})"
r"(?:\.(?P<bugfix>[\d]{0,2}))?"
r"(?:\.(?P<mbugfix>[\d]{0,2}))?"
r"(?:(?P<pre_type>rc|a|b|alpha|beta|nb)(?P<pre_num>[\d]{1}))?"
r"(?:(?:.*)-(?P<noc>(?:[\d]+|n/a))-(?P<sha>[a-z0-9]{8}))?"
)
match = git_describe_regex.match(version)
if match:
self.salt_version = match.groups()
07070100000013000081A400000000000000000000000167D9C77000000FD7000000000000000000000000000000000000002D00000000pepper-0.7.6+git44.996fc29/pepper/retcode.py"""
A retcode validator
"""
class PepperRetcode:
"""
Validation container
"""
def validate(self, options, result):
"""
Validate result dictionary retcode values.
:param options: optparse options
:param result: dictionary from Saltstack master
:return: exit code
"""
if options.fail_any:
return self.validate_fail_any(result)
if options.fail_any_none:
return self.validate_fail_any_none(result)
if options.fail_all:
return self.validate_fail_all(result)
if options.fail_all_none:
return self.validate_fail_all_none(result)
return 0
@staticmethod
def validate_fail_any(result):
"""
Validate result dictionary retcode values.
Returns 0 if no retcode keys.
Returns first non zero retcode if any of recodes is non zero.
:param result: dictionary from Saltstack master
:return: exit code
"""
if isinstance(result, list):
if isinstance(result[0], dict):
minion = result[0]
retcodes = list(
minion[name].get("retcode")
for name in minion
if isinstance(minion[name], dict) and minion[name].get("retcode") is not None
)
return next((r for r in retcodes if r != 0), 0)
return 0
@staticmethod
def validate_fail_any_none(result):
"""
Validate result dictionary retcode values.
Returns -1 if no retcode keys.
Returns first non zero retcode if any of recodes is non zero.
:param result: dictionary from Saltstack master
:return: exit code
"""
if isinstance(result, list):
if isinstance(result[0], dict):
minion = result[0]
retcodes = list(
minion[name].get("retcode")
for name in minion
if isinstance(minion[name], dict) and minion[name].get("retcode") is not None
)
if not retcodes:
return -1 # there are no retcodes
return next((r for r in retcodes if r != 0), 0)
return -1
@staticmethod
def validate_fail_all(result):
"""
Validate result dictionary retcode values.
Returns 0 if no retcode keys.
Returns first non zero retcode if all recodes are non zero.
:param result: dictionary from Saltstack master
:return: exit code
"""
if isinstance(result, list):
if isinstance(result[0], dict):
minion = result[0]
retcodes = list(
minion[name].get("retcode")
for name in minion
if isinstance(minion[name], dict) and minion[name].get("retcode") is not None
)
if all(r != 0 for r in retcodes):
return next((r for r in retcodes if r != 0), 0)
return 0
@staticmethod
def validate_fail_all_none(result):
"""
Validate result dictionary retcode values.
Returns -1 if no retcode keys.
Returns first non zero retcode if all recodes are non zero.
:param result: dictionary from Saltstack master
:return: exit code
"""
if isinstance(result, list):
if isinstance(result[0], dict):
minion = result[0]
retcodes = list(
minion[name].get("retcode")
for name in minion
if isinstance(minion[name], dict) and minion[name].get("retcode") is not None
)
if not retcodes:
return -1 # there are no retcodes
if all(r != 0 for r in retcodes):
return next((r for r in retcodes if r != 0), 0)
else:
return 0
return -1
07070100000014000081ED00000000000000000000000167D9C770000016C0000000000000000000000000000000000000002C00000000pepper-0.7.6+git44.996fc29/pepper/script.py#!/usr/bin/env python
"""
A CLI interface to a remote salt-api instance
"""
import json
import logging
import sys
from pepper.cli import PepperCli
from pepper.exceptions import PepperArgumentsException
from pepper.exceptions import PepperAuthException
from pepper.exceptions import PepperException
from pepper.retcode import PepperRetcode
try:
import salt.loader
import salt.config
import salt.output
HAS_SALT = True
except ImportError:
HAS_SALT = False
logger = logging.getLogger(__name__)
class Pepper:
def __init__(self):
self.cli = PepperCli()
if HAS_SALT:
self.opts = salt.config.client_config(self.cli.options.master)
else:
self.opts = {}
if self.cli.options.output_file is not None:
self.opts["output_file"] = self.cli.options.output_file
@property
def output(self):
if not hasattr(self, "modules"):
self.modules = salt.loader.minion_mods(self.opts)
try:
oput = self.modules[self.cli.args[1]].__outputter__
except (KeyError, AttributeError, TypeError):
oput = "nested"
return oput
def __call__(self):
try:
for exit_code, result in self.cli.run():
if HAS_SALT and self.opts:
logger.debug("Use Salt outputters")
result = json.loads(result)
# unwrap ret in some cases
if "return" in result:
result = result["return"]
for ret in result:
if isinstance(ret, dict):
if self.cli.options.client.startswith("local"):
for minionid, minionret in ret.items():
# rest_tornado doesnt return full_return directly
# it will always be from get_event, so the output differs slightly
if isinstance(minionret, dict) and "return" in minionret:
# version >= 2017.7
salt.output.display_output(
{minionid: minionret["return"]},
self.cli.options.output
or minionret.get("out", None)
or "nested",
opts=self.opts,
)
# cherrypy returns with ret via full_return
elif isinstance(minionret, dict) and "ret" in minionret:
# version >= 2017.7
salt.output.display_output(
{minionid: minionret["ret"]},
self.cli.options.output
or minionret.get("out", None)
or "nested",
opts=self.opts,
)
else:
salt.output.display_output(
{minionid: minionret},
self.cli.options.output or self.output,
opts=self.opts,
)
elif "data" in ret:
# unfold runners
outputter = ret.get("outputter", "nested")
if isinstance(ret["data"], dict) and "return" in ret["data"]:
ret = ret["data"]["return"]
salt.output.display_output(
ret, self.cli.options.output or outputter, opts=self.opts
)
else:
salt.output.display_output(
{self.cli.options.client: ret},
self.cli.options.output or ret.get("outputter", "nested"),
opts=self.opts,
)
else:
salt.output.display_output(
{self.cli.options.client: ret},
self.cli.options.output or "nested",
opts=self.opts,
)
else:
if self.cli.options.output_file is not None:
with open(self.cli.options.output_file, "a") as ofile:
print(result, file=ofile)
else:
print(result)
if exit_code is not None:
if exit_code == 0:
return PepperRetcode().validate(self.cli.options, result)
return exit_code
except (PepperException, PepperAuthException, PepperArgumentsException) as exc:
print("Pepper error: {}".format(exc), file=sys.stderr)
return 1
except KeyboardInterrupt:
# TODO: mimic CLI and output JID on ctrl-c
return 0
except Exception as e:
print(e)
print(
"Uncaught Pepper error (increase verbosity for the full traceback).",
file=sys.stderr,
)
logger.debug("Uncaught traceback:", exc_info=True)
return 1
07070100000015000081A400000000000000000000000167D9C770000000E8000000000000000000000000000000000000002A00000000pepper-0.7.6+git44.996fc29/pyproject.toml[tool.pytest.ini_options]
addopts = "--showlocals --log-file /tmp/pepper-runtests.log --show-capture=no -ra"
testpaths = ["tests"]
norecursedirs = [".git", ".nox"]
usefixtures = ["pepperconfig"]
[tool.flake8]
max-line-length = 119
07070100000016000041ED00000000000000000000000267D9C77000000000000000000000000000000000000000000000002300000000pepper-0.7.6+git44.996fc29/scripts07070100000017000081A400000000000000000000000167D9C77000000228000000000000000000000000000000000000002A00000000pepper-0.7.6+git44.996fc29/scripts/pepper#!/usr/bin/env python
# Import Python Libraries
import logging
# Import Pepper Libraries
import pepper.script
try:
from logging import NullHandler
except ImportError: # Python < 2.7
class NullHandler(logging.Handler):
def emit(self, record): pass
logging.basicConfig(format='%(levelname)s %(asctime)s %(module)s: %(message)s')
logger = logging.getLogger('pepper')
logger.addHandler(NullHandler())
if __name__ == '__main__':
exit_code = pepper.script.Pepper()()
raise SystemExit(exit_code if exit_code is not None else 0)
07070100000018000081A400000000000000000000000167D9C77000000060000000000000000000000000000000000000002E00000000pepper-0.7.6+git44.996fc29/scripts/pepper.cmd@echo off
set _SCRIPTDIR=%~d0
set _SCRIPTPATH=%~p0
python "%_SCRIPTDIR%%_SCRIPTPATH%pepper" %*
07070100000019000081A400000000000000000000000167D9C770000005DD000000000000000000000000000000000000002400000000pepper-0.7.6+git44.996fc29/setup.py#!/usr/bin/env python
"""
A CLI front-end to a running salt-api system
"""
import setuptools
with open("README.rst") as fh:
long_description = fh.read()
setup_kwargs = {
"name": "salt-pepper",
"description": __doc__.strip(),
"author": "Seth House",
"author_email": "shouse@saltstack.com",
"url": "http://saltstack.com",
"long_description": long_description,
"long_description_content_type": "text/x-rst",
"use_scm_version": True,
"setup_requires": ["setuptools_scm"],
"classifiers": [
"Programming Language :: Python",
"Programming Language :: Cython",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Development Status :: 5 - Production/Stable",
"Environment :: Console",
"Intended Audience :: Developers",
"Intended Audience :: Information Technology",
"Intended Audience :: System Administrators",
"License :: OSI Approved :: Apache Software License",
"Operating System :: POSIX :: Linux",
"Topic :: System :: Clustering",
"Topic :: System :: Distributed Computing",
],
"packages": [
"pepper",
],
"extras_require": {
"kerberos": ["requests-gssapi>=1.1.0"],
},
"scripts": [
"scripts/pepper",
],
}
if __name__ == "__main__":
setuptools.setup(**setup_kwargs)
0707010000001A000041ED00000000000000000000000267D9C77000000000000000000000000000000000000000000000002100000000pepper-0.7.6+git44.996fc29/tests0707010000001B000081A400000000000000000000000167D9C77000000000000000000000000000000000000000000000002D00000000pepper-0.7.6+git44.996fc29/tests/__init__.py0707010000001C000081A400000000000000000000000167D9C77000002306000000000000000000000000000000000000002D00000000pepper-0.7.6+git44.996fc29/tests/conftest.py# Import python libraries
import logging
import os.path
import shutil
import sys
import tempfile
import textwrap
import pytest
import salt.utils.yaml as yaml
from pytestskipmarkers.utils import ports
from saltfactories.utils import random_string
from saltfactories.utils import running_username
import pepper.script
# Import pytest libraries
# Import Salt Libraries
log = logging.getLogger(__name__)
@pytest.fixture(scope="session")
def sshd_config_dir(salt_factories):
config_dir = salt_factories.get_root_dir_for_daemon("sshd")
yield config_dir
shutil.rmtree(str(config_dir), ignore_errors=True)
@pytest.fixture(scope="session")
def session_sshd_server(salt_factories, sshd_config_dir, session_master):
sshd_config_dict = {
"Protocol": "2",
# Turn strict modes off so that we can operate in /tmp
"StrictModes": "no",
# Logging
"SyslogFacility": "AUTH",
"LogLevel": "INFO",
# Authentication:
"LoginGraceTime": "120",
"PermitRootLogin": "without-password",
"PubkeyAuthentication": "yes",
# Don't read the user's ~/.rhosts and ~/.shosts files
"IgnoreRhosts": "yes",
"HostbasedAuthentication": "no",
# To enable empty passwords, change to yes (NOT RECOMMENDED)
"PermitEmptyPasswords": "no",
# Change to yes to enable challenge-response passwords (beware issues with
# some PAM modules and threads)
"ChallengeResponseAuthentication": "no",
# Change to no to disable tunnelled clear text passwords
"PasswordAuthentication": "no",
"X11Forwarding": "no",
"X11DisplayOffset": "10",
"PrintMotd": "no",
"PrintLastLog": "yes",
"TCPKeepAlive": "yes",
"AcceptEnv": "LANG LC_*",
"UsePAM": "yes",
}
factory = salt_factories.get_sshd_daemon(
sshd_config_dict=sshd_config_dict,
config_dir=sshd_config_dir,
)
with factory.started():
yield factory
@pytest.fixture(scope="session")
def session_ssh_roster_config(session_sshd_server, session_master):
roster_contents = """
localhost:
host: 127.0.0.1
port: {}
user: {}
priv: {}
mine_functions:
test.arg: ['itworked']
""".format(
session_sshd_server.listen_port, running_username(), session_sshd_server.client_key
)
with pytest.helpers.temp_file(
"roster", roster_contents, session_master.config_dir
) as roster_file:
yield roster_file
@pytest.fixture(scope="session")
def salt_api_port():
"""
Returns an unused localhost port for the api port
"""
return ports.get_unused_localhost_port()
@pytest.fixture(scope="session")
def pepperconfig(salt_api_port):
config = textwrap.dedent(
"""
[main]
SALTAPI_URL=http://localhost:{0}/
SALTAPI_USER=pepper
SALTAPI_PASS=pepper
SALTAPI_EAUTH=sharedsecret
[pepper]
SALTAPI_URL=http://localhost:{0}/
SALTAPI_USER=pepper
SALTAPI_PASS=pepper
SALTAPI_EAUTH=sharedsecret
[baduser]
SALTAPI_URL=http://localhost:{0}/
SALTAPI_USER=saltdev
SALTAPI_PASS=saltdev
SALTAPI_EAUTH=pam
[badapi]
SALTAPI_URL=git://localhost:{0}/
[noapi]
SALTAPI_USER=pepper
SALTAPI_PASS=pepper
SALTAPI_EAUTH=sharedsecret
[noopts]
SALTAPI_URL=http://localhost:{0}/
""".format(
salt_api_port
)
)
with open("tests/.pepperrc", "w") as pepper_file:
print(config, file=pepper_file)
yield
os.remove("tests/.pepperrc")
@pytest.fixture
def pepper_client(session_salt_api, salt_api_port):
client = pepper.Pepper("http://localhost:{}".format(salt_api_port))
client.login("pepper", "pepper", "sharedsecret")
return client
@pytest.fixture
def tokfile():
tokdir = tempfile.mkdtemp()
yield os.path.join(tokdir, "peppertok.json")
shutil.rmtree(tokdir)
@pytest.fixture
def output_file():
"""
Returns the path to the salt master configuration file
"""
out_dir = tempfile.mkdtemp()
yield os.path.join(out_dir, "output")
shutil.rmtree(out_dir)
@pytest.fixture(params=["/run", "/login"])
def pepper_cli(request, session_salt_api, salt_api_port, output_file, session_sshd_server):
"""
Wrapper to invoke Pepper with common params and inside an empty env
"""
if request.config.getoption("--salt-api-backend") == "rest_tornado" and request.param == "/run":
pytest.xfail("rest_tornado does not support /run endpoint until next release")
def_args = [
"--out=json",
"--output-file={}".format(output_file),
"-c",
"tests/.pepperrc",
]
if request.param == "/run":
def_args = ["--run-uri"] + def_args
def _run_pepper_cli(*args, **kwargs):
sys.argv = ["pepper", "-p", kwargs.pop("profile", "main")] + def_args + list(args)
exitcode = pepper.script.Pepper()()
try:
with open(output_file) as result:
try:
return yaml.load(result)
except yaml.parser.ParserError:
result.seek(0)
return [
yaml.load("{0}}}".format(ret).strip('"'))
for ret in result.read().split('}"\n')
if ret
]
except Exception as exc:
log.error("ExitCode %s: %s", exitcode, exc)
return exitcode
return _run_pepper_cli
@pytest.fixture(scope="session")
def session_master_factory(request, salt_factories, session_master_config_overrides):
return salt_factories.salt_master_daemon(
random_string("master-"), overrides=session_master_config_overrides
)
@pytest.fixture(scope="session")
def session_master(session_master_factory):
with session_master_factory.started():
yield session_master_factory
@pytest.fixture(scope="session")
def session_master_config_overrides(request, salt_api_port, salt_api_backend):
return {
salt_api_backend: {
"port": salt_api_port,
"disable_ssl": True,
},
"external_auth": {
"sharedsecret": {
"pepper": [
".*",
"@jobs",
"@wheel",
"@runner",
],
},
},
"sharedsecret": "pepper",
"token_expire": 94670856,
"ignore_host_keys": True,
"ssh_wipe": True,
"netapi_enable_clients": [
"local",
"local_async",
"local_subset",
"ssh",
"runner",
"runner_async",
"wheel",
"wheel_async",
"run",
],
}
@pytest.helpers.register
def remove_stale_minion_key(master, minion_id):
"""Helper to remove a stale minion key."""
key_path = os.path.join(master.config["pki_dir"], "minions", minion_id)
if os.path.exists(key_path):
os.unlink(key_path)
else:
log.debug("The minion(id=%r) key was not found at %s", minion_id, key_path)
@pytest.fixture(scope="session")
def session_minion_factory(session_master_factory):
"""Return a factory for a randomly named minion connected to master."""
minion_factory = session_master_factory.salt_minion_daemon(random_string("minion-"))
minion_factory.after_terminate(
pytest.helpers.remove_stale_minion_key, session_master_factory, minion_factory.id
)
return minion_factory
@pytest.fixture(scope="session")
def session_minion(session_master, session_minion_factory): # noqa
assert session_master.is_running()
with session_minion_factory.started():
yield session_minion_factory
@pytest.fixture(scope="session")
def session_minion_id(session_minion):
return session_minion.id
@pytest.fixture(scope="session")
def salt_api_backend(request):
"""
Return the salt-api backend (cherrypy or tornado)
"""
backend = request.config.getoption("--salt-api-backend")
if backend is not None:
return backend
backend = request.config.getini("salt_api_backend")
if backend is not None:
return backend
return "rest_cherrypy"
@pytest.fixture(scope="session")
def session_salt_api_factory(session_master_factory):
return session_master_factory.salt_api_daemon()
@pytest.fixture(scope="session")
def session_salt_api(session_master, session_salt_api_factory):
assert session_master.is_running()
with session_salt_api_factory.started():
yield session_salt_api_factory
def pytest_addoption(parser):
parser.addoption(
"--salt-api-backend",
action="store",
default="rest_cherrypy",
help="which backend to use for salt-api, must be one of rest_cherrypy or rest_tornado",
)
0707010000001D000041ED00000000000000000000000267D9C77000000000000000000000000000000000000000000000002D00000000pepper-0.7.6+git44.996fc29/tests/integration0707010000001E000081A400000000000000000000000167D9C77000000000000000000000000000000000000000000000003900000000pepper-0.7.6+git44.996fc29/tests/integration/__init__.py0707010000001F000081A400000000000000000000000167D9C7700000089B000000000000000000000000000000000000003D00000000pepper-0.7.6+git44.996fc29/tests/integration/test_clients.pyimport json
import pathlib
import pytest
def test_local_bad_opts(pepper_cli):
with pytest.raises(SystemExit):
pepper_cli("*")
with pytest.raises(SystemExit):
pepper_cli("test.ping")
with pytest.raises(SystemExit):
pepper_cli("--client=ssh", "test.ping")
with pytest.raises(SystemExit):
pepper_cli("--client=ssh", "*")
@pytest.mark.xfail(
'config.getoption("--salt-api-backend") == "rest_tornado"',
reason="timeout kwarg isnt popped until next version of salt/tornado",
)
def test_runner_client(pepper_cli):
ret = pepper_cli(
"--timeout=123",
"--client=runner",
"test.arg",
"one",
"two=what",
"three={}".format(json.dumps({"hello": "world"})),
)
assert ret == {"args": ["one"], "kwargs": {"three": {"hello": "world"}, "two": "what"}}
@pytest.mark.xfail(
'config.getoption("--salt-api-backend") == "rest_tornado"',
reason="wheelClient unimplemented for now on tornado",
)
def test_wheel_client_arg(pepper_cli, session_minion):
ret = pepper_cli("--client=wheel", "minions.connected")
assert ret == [session_minion.id]
@pytest.mark.xfail(
'config.getoption("--salt-api-backend") == "rest_tornado"',
reason="wheelClient unimplemented for now on tornado",
)
def test_wheel_client_kwargs(pepper_cli, session_master):
ret = pepper_cli(
"--client=wheel",
"config.update_config",
"file_name=pepper",
"yaml_contents={}".format(json.dumps({"timeout": 5})),
)
assert ret == "Wrote pepper.conf"
default_include_dir = pathlib.Path(session_master.config["default_include"]).parent
pepper_config = pathlib.Path(session_master.config_dir) / default_include_dir / "pepper.conf"
assert pepper_config.exists
@pytest.mark.xfail(
'config.getoption("--salt-api-backend") == "rest_tornado"',
reason="sshClient unimplemented for now on tornado",
)
def test_ssh_client(pepper_cli, session_ssh_roster_config):
ret = pepper_cli("--client=ssh", "*", "test.ping")
assert ret["ssh"]["localhost"]["return"] is True
def test_bad_client(pepper_cli):
ret = pepper_cli("--client=whatever")
assert ret == 1
07070100000020000081A400000000000000000000000167D9C770000004B2000000000000000000000000000000000000003A00000000pepper-0.7.6+git44.996fc29/tests/integration/test_json.pyimport os
import shutil
import tempfile
def test_local_json(pepper_cli, session_minion_id):
json = '[{"tgt": "*", "fun": "test.ping", "client": "local"}]'
ret = pepper_cli("--json", json)
assert ret[session_minion_id] is True
def test_local_json_bad(pepper_cli):
json = "{what}"
ret = pepper_cli("--json", json)
assert ret == 1
def test_local_json_file(pepper_cli, session_minion_id):
tmpjson = os.path.join(tempfile.mkdtemp(), "json")
with open(tmpjson, "w") as tmpfile:
print(
'[{"client": "local", "tgt": "*", "fun": "test.ping"}]',
file=tmpfile,
)
ret = pepper_cli("--json-file", tmpjson)
shutil.rmtree(os.path.dirname(tmpjson))
assert ret[session_minion_id] is True
def test_local_json_file_bad(pepper_cli):
tmpjson = os.path.join(tempfile.mkdtemp(), "json")
with open(tmpjson, "w") as tmpfile:
print(
"{what}",
file=tmpfile,
)
ret = pepper_cli("--json-file", tmpjson)
shutil.rmtree(os.path.dirname(tmpjson))
assert ret == 1
def test_local_json_no_file(pepper_cli):
ret = pepper_cli("--json-file", "/tmp/wahteverfile")
assert ret == 1
07070100000021000081A400000000000000000000000167D9C7700000008B000000000000000000000000000000000000003B00000000pepper-0.7.6+git44.996fc29/tests/integration/test_local.pydef test_local(pepper_client, session_minion_id):
assert pepper_client.local("*", "test.ping")["return"][0][session_minion_id] is True
07070100000022000081A400000000000000000000000167D9C77000000612000000000000000000000000000000000000004000000000pepper-0.7.6+git44.996fc29/tests/integration/test_login_opts.pydef test_cli_opts(pepper_cli, session_minion_id, salt_api_port):
"""Test the using a profile"""
ret = pepper_cli(
"--saltapi-url=http://localhost:{}/".format(salt_api_port),
"--eauth=sharedsecret",
"--username=pepper",
"--password=pepper",
"*",
"test.ping",
profile="noprofile",
)
assert ret[session_minion_id] is True
def test_cli_opts_not_in_profile(pepper_cli, session_minion_id, salt_api_port):
"""Test the using a profile"""
ret = pepper_cli(
"--eauth=sharedsecret",
"--username=pepper",
"--password=pepper",
"*",
"test.ping",
profile="noopts",
)
assert ret[session_minion_id] is True
def test_cli_api_not_in_profile(pepper_cli, session_minion_id, salt_api_port):
"""Test the using a profile"""
ret = pepper_cli(
"--saltapi-url=http://localhost:{}/".format(salt_api_port),
"*",
"test.ping",
profile="noapi",
)
assert ret[session_minion_id] is True
def test_no_username(pepper_cli, session_minion_id, salt_api_port):
"""Test the using a profile"""
ret = pepper_cli(
"--non-interactive",
"*",
"test.ping",
profile="noopts",
)
assert ret == 1
def test_no_password(pepper_cli, session_minion_id, salt_api_port):
"""Test the using a profile"""
ret = pepper_cli(
"--username=pepper",
"--non-interactive",
"*",
"test.ping",
profile="noopts",
)
assert ret == 1
07070100000023000081A400000000000000000000000167D9C770000002DB000000000000000000000000000000000000003C00000000pepper-0.7.6+git44.996fc29/tests/integration/test_poller.pydef test_local_poll(pepper_cli, session_minion_id):
"""Test the returns poller for localclient"""
ret = pepper_cli("--fail-if-incomplete", "*", "test.sleep", "1")
assert ret[session_minion_id] is True
assert len(ret) == 1
def test_local_poll_long(pepper_cli, session_minion_id):
"""Test the returns poller for localclient"""
ret = pepper_cli("--fail-if-incomplete", "*", "test.sleep", "30")
assert ret[session_minion_id] is True
assert len(ret) == 1
def test_local_poll_timeout(pepper_cli, session_minion_id):
"""Test the returns poller for localclient"""
ret = pepper_cli("--timeout=5", "--fail-if-incomplete", "*", "test.sleep", "30")
assert ret == {"Failed": [session_minion_id]}
07070100000024000081A400000000000000000000000167D9C770000000BE000000000000000000000000000000000000003D00000000pepper-0.7.6+git44.996fc29/tests/integration/test_profile.pydef test_config_profile(pepper_cli, session_minion_id):
"""Test the using a profile"""
ret = pepper_cli("*", "test.ping", profile="pepper")
assert ret[session_minion_id] is True
07070100000025000081A400000000000000000000000167D9C7700000047D000000000000000000000000000000000000003B00000000pepper-0.7.6+git44.996fc29/tests/integration/test_token.pyimport json
import time
def test_local_token(tokfile, pepper_cli, session_minion_id):
"""Test local execution with token file"""
ret = pepper_cli("-x", tokfile, "--make-token", "*", "test.ping")
assert ret[session_minion_id] is True
def test_runner_token(tokfile, pepper_cli):
"""Test runner execution with token file"""
ret = pepper_cli("-x", tokfile, "--make-token", "--client", "runner", "test.metasyntactic")
exps = [
"foo",
"bar",
"baz",
"qux",
"quux",
"quuz",
"corge",
"grault",
"garply",
"waldo",
"fred",
"plugh",
"xyzzy",
"thud",
]
assert all(exp in ret for exp in exps)
def test_token_expire(tokfile, pepper_cli):
"""Test token override param"""
now = time.time()
pepper_cli("-x", tokfile, "--make-token", "--token-expire", "94670856", "*", "test.ping")
with open(tokfile) as tfile:
token = json.load(tfile)
diff = (now + float(94670856)) - token["expire"]
# Allow for 10-second window between request and master-side auth.
assert diff < 10
07070100000026000081A400000000000000000000000167D9C77000000255000000000000000000000000000000000000003D00000000pepper-0.7.6+git44.996fc29/tests/integration/test_vanilla.pyimport pytest
def test_local(pepper_cli, session_minion_id):
"""Sanity-check: Has at least one minion - /run - /login query type is parameterized"""
ret = pepper_cli("*", "test.ping")
assert ret[session_minion_id] is True
@pytest.mark.xfail(
'config.getoption("--salt-api-backend") == "rest_tornado"',
reason="this is broken in rest_tornado until future release",
)
def test_long_local(pepper_cli, session_minion_id):
"""Test a long call blocks until the return"""
ret = pepper_cli("--timeout=60", "*", "test.sleep", "30")
assert ret[session_minion_id] is True
07070100000027000081A400000000000000000000000167D9C77000000147000000000000000000000000000000000000003200000000pepper-0.7.6+git44.996fc29/tests/requirements.txtmock
pytest>=6.0.0
flake8-pyproject
pytest-rerunfailures
pytest-cov
pytest-salt-factories==1.0.4
CherryPy
setuptools_scm==7.1.0
importlib-metadata<5.0.0
pyzmq>=25.1.2 ; python_version < '3.13'
pyzmq>=26.2.0 ; python_version >= '3.13'
cryptography>=42.0.0; python_version < '3.13'
cryptography==42.0.2; python_version >= '3.13'
07070100000028000041ED00000000000000000000000267D9C77000000000000000000000000000000000000000000000002600000000pepper-0.7.6+git44.996fc29/tests/unit07070100000029000081A400000000000000000000000167D9C77000000000000000000000000000000000000000000000003200000000pepper-0.7.6+git44.996fc29/tests/unit/__init__.py0707010000002A000081A400000000000000000000000167D9C77000000271000000000000000000000000000000000000003300000000pepper-0.7.6+git44.996fc29/tests/unit/test_init.py# Import Python Libraries
import imp
import os
import shutil
import setuptools_scm
import pepper
# Import Pepper Libraries
def test_no_setup():
setuppath = os.path.join(os.path.dirname(pepper.__file__), os.pardir, "setup.py")
shutil.move(setuppath, setuppath + ".bak")
ptest = imp.load_source("ptest", os.path.join(os.path.dirname(pepper.__file__), "__init__.py"))
shutil.move(setuppath + ".bak", setuppath)
assert ptest.version == setuptools_scm.get_version()
assert ptest.sha is None
def test_version():
assert pepper.version == setuptools_scm.get_version()
assert pepper.sha is None
0707010000002B000081A400000000000000000000000167D9C77000000216000000000000000000000000000000000000003C00000000pepper-0.7.6+git44.996fc29/tests/unit/test_login_details.pyimport sys
from unittest.mock import MagicMock
from unittest.mock import patch
import pepper.cli
# Import Testing libraries
def test_interactive_logins():
sys.argv = ["pepper", "-c", "tests/.pepperrc", "-p", "noopts"]
with patch("pepper.cli.input", MagicMock(return_value="pepper")), patch(
"pepper.cli.getpass.getpass", MagicMock(return_value="pepper")
):
result = pepper.cli.PepperCli().get_login_details()
assert result["SALTAPI_USER"] == "pepper"
assert result["SALTAPI_PASS"] == "pepper"
0707010000002C000081A400000000000000000000000167D9C77000000123000000000000000000000000000000000000003D00000000pepper-0.7.6+git44.996fc29/tests/unit/test_retcodes_error.py# Import Python Libraries
import sys
import pytest
import pepper
# Import Pepper Libraries
# Import Testing Libraries
def test_fail_any():
sys.argv = ["pepper", "--fail-all", "--fail-any", "minion_id", "request"]
with pytest.raises(SystemExit):
pepper.script.Pepper()()
0707010000002D000081A400000000000000000000000167D9C7700000086D000000000000000000000000000000000000004100000000pepper-0.7.6+git44.996fc29/tests/unit/test_retcodes_fail_fail.py# Import Python Libraries
import sys
from unittest.mock import MagicMock
from unittest.mock import patch
import pepper
# Import Pepper Libraries
PAYLOAD = {
"return": [
{
"ezh.msk.ru": {
"jid": "20180414193904158892",
"ret": "/bin/sh: 123: command not found",
"retcode": 127,
},
"saltstack.ezh.msk.ru": {
"jid": "20180414193904158892",
"ret": "/bin/sh: 1: 123: not found",
"retcode": 127,
},
}
]
}
@patch("pepper.cli.PepperCli.login", MagicMock(side_effect=lambda arg: None))
@patch("pepper.cli.PepperCli.low", MagicMock(side_effect=lambda api, load: PAYLOAD))
def test_default():
sys.argv = ["pepper", "minion_id", "request"]
ret_code = pepper.script.Pepper()()
assert ret_code == 0
@patch("pepper.cli.PepperCli.login", MagicMock(side_effect=lambda arg: None))
@patch("pepper.cli.PepperCli.low", MagicMock(side_effect=lambda api, load: PAYLOAD))
def test_fail_any():
sys.argv = ["pepper", "--fail-any", "minion_id", "request"]
ret_code = pepper.script.Pepper()()
assert ret_code == 127
@patch("pepper.cli.PepperCli.login", MagicMock(side_effect=lambda arg: None))
@patch("pepper.cli.PepperCli.low", MagicMock(side_effect=lambda api, load: PAYLOAD))
def test_fail_any_none():
sys.argv = ["pepper", "--fail-any-none", "minion_id", "request"]
ret_code = pepper.script.Pepper()()
assert ret_code == 127
@patch("pepper.cli.PepperCli.login", MagicMock(side_effect=lambda arg: None))
@patch("pepper.cli.PepperCli.low", MagicMock(side_effect=lambda api, load: PAYLOAD))
def test_fail_all():
sys.argv = ["pepper", "--fail-all", "minion_id", "request"]
ret_code = pepper.script.Pepper()()
assert ret_code == 127
@patch("pepper.cli.PepperCli.login", MagicMock(side_effect=lambda arg: None))
@patch("pepper.cli.PepperCli.low", MagicMock(side_effect=lambda api, load: PAYLOAD))
def test_fail_all_none():
sys.argv = ["pepper", "--fail-all-none", "minion_id", "request"]
ret_code = pepper.script.Pepper()()
assert ret_code == 127
0707010000002E000081A400000000000000000000000167D9C77000000847000000000000000000000000000000000000004100000000pepper-0.7.6+git44.996fc29/tests/unit/test_retcodes_fail_none.py# Import Python Libraries
import sys
from unittest.mock import MagicMock
from unittest.mock import patch
import pepper
# Import Pepper Libraries
PAYLOAD = {
"return": [
{
"saltstack.ezh.msk.ru": {
"jid": "20180414193904158892",
"ret": "/bin/sh: 123: command not found",
"retcode": 127,
},
"ezh.msk.ru": {
"jid": "20180414193904158892",
"ret": "Hello from SaltStack",
},
}
]
}
@patch("pepper.cli.PepperCli.login", MagicMock(side_effect=lambda arg: None))
@patch("pepper.cli.PepperCli.low", MagicMock(side_effect=lambda api, load: PAYLOAD))
def test_default():
sys.argv = ["pepper", "minion_id", "request"]
ret_code = pepper.script.Pepper()()
assert ret_code == 0
@patch("pepper.cli.PepperCli.login", MagicMock(side_effect=lambda arg: None))
@patch("pepper.cli.PepperCli.low", MagicMock(side_effect=lambda api, load: PAYLOAD))
def test_fail_any():
sys.argv = ["pepper", "--fail-any", "minion_id", "request"]
ret_code = pepper.script.Pepper()()
assert ret_code == 127
@patch("pepper.cli.PepperCli.login", MagicMock(side_effect=lambda arg: None))
@patch("pepper.cli.PepperCli.low", MagicMock(side_effect=lambda api, load: PAYLOAD))
def test_fail_any_none():
sys.argv = ["pepper", "--fail-any-none", "minion_id", "request"]
ret_code = pepper.script.Pepper()()
assert ret_code == 127
@patch("pepper.cli.PepperCli.login", MagicMock(side_effect=lambda arg: None))
@patch("pepper.cli.PepperCli.low", MagicMock(side_effect=lambda api, load: PAYLOAD))
def test_fail_all():
sys.argv = ["pepper", "--fail-all", "minion_id", "request"]
ret_code = pepper.script.Pepper()()
assert ret_code == 127
@patch("pepper.cli.PepperCli.login", MagicMock(side_effect=lambda arg: None))
@patch("pepper.cli.PepperCli.low", MagicMock(side_effect=lambda api, load: PAYLOAD))
def test_fail_all_none():
sys.argv = ["pepper", "--fail-all-none", "minion_id", "request"]
ret_code = pepper.script.Pepper()()
assert ret_code == 127
0707010000002F000081A400000000000000000000000167D9C77000000812000000000000000000000000000000000000004100000000pepper-0.7.6+git44.996fc29/tests/unit/test_retcodes_fail_pass.py# Import Python Libraries
import sys
from unittest.mock import MagicMock
from unittest.mock import patch
import pepper
# Import Pepper Libraries
PAYLOAD = {
"return": [
{
"ezh.msk.ru": {
"jid": "20180414193904158892",
"ret": "/bin/sh: 123: command not found",
"retcode": 127,
},
"saltstack.ezh.msk.ru": {"jid": "20180414193904158892", "ret": "pass", "retcode": 0},
}
]
}
@patch("pepper.cli.PepperCli.login", MagicMock(side_effect=lambda arg: None))
@patch("pepper.cli.PepperCli.low", MagicMock(side_effect=lambda api, load: PAYLOAD))
def test_default():
sys.argv = ["pepper", "minion_id", "request"]
ret_code = pepper.script.Pepper()()
assert ret_code == 0
@patch("pepper.cli.PepperCli.login", MagicMock(side_effect=lambda arg: None))
@patch("pepper.cli.PepperCli.low", MagicMock(side_effect=lambda api, load: PAYLOAD))
def test_fail_any():
sys.argv = ["pepper", "--fail-any", "minion_id", "request"]
ret_code = pepper.script.Pepper()()
assert ret_code == 127
@patch("pepper.cli.PepperCli.login", MagicMock(side_effect=lambda arg: None))
@patch("pepper.cli.PepperCli.low", MagicMock(side_effect=lambda api, load: PAYLOAD))
def test_fail_any_none():
sys.argv = ["pepper", "--fail-any-none", "minion_id", "request"]
ret_code = pepper.script.Pepper()()
assert ret_code == 127
@patch("pepper.cli.PepperCli.login", MagicMock(side_effect=lambda arg: None))
@patch("pepper.cli.PepperCli.low", MagicMock(side_effect=lambda api, load: PAYLOAD))
def test_fail_all():
sys.argv = ["pepper", "--fail-all", "minion_id", "request"]
ret_code = pepper.script.Pepper()()
assert ret_code == 0
@patch("pepper.cli.PepperCli.login", MagicMock(side_effect=lambda arg: None))
@patch("pepper.cli.PepperCli.low", MagicMock(side_effect=lambda api, load: PAYLOAD))
def test_fail_all_none():
sys.argv = ["pepper", "--fail-all-none", "minion_id", "request"]
ret_code = pepper.script.Pepper()()
assert ret_code == 0
07070100000030000081A400000000000000000000000167D9C77000000816000000000000000000000000000000000000004100000000pepper-0.7.6+git44.996fc29/tests/unit/test_retcodes_none_none.py# Import Python Libraries
import sys
from unittest.mock import MagicMock
from unittest.mock import patch
import pepper
# Import Pepper Libraries
PAYLOAD = {
"return": [
{
"saltstack.ezh.msk.ru": {
"jid": "20180414193904158892",
"ret": "Hello from SaltStack",
},
"ezh.msk.ru": {
"jid": "20180414193904158892",
"ret": "Hello from SaltStack",
},
}
]
}
@patch("pepper.cli.PepperCli.login", MagicMock(side_effect=lambda arg: None))
@patch("pepper.cli.PepperCli.low", MagicMock(side_effect=lambda api, load: PAYLOAD))
def test_default():
sys.argv = ["pepper", "minion_id", "request"]
ret_code = pepper.script.Pepper()()
assert ret_code == 0
@patch("pepper.cli.PepperCli.login", MagicMock(side_effect=lambda arg: None))
@patch("pepper.cli.PepperCli.low", MagicMock(side_effect=lambda api, load: PAYLOAD))
def test_fail_any():
sys.argv = ["pepper", "--fail-any", "minion_id", "request"]
ret_code = pepper.script.Pepper()()
assert ret_code == 0
@patch("pepper.cli.PepperCli.login", MagicMock(side_effect=lambda arg: None))
@patch("pepper.cli.PepperCli.low", MagicMock(side_effect=lambda api, load: PAYLOAD))
def test_fail_any_none():
sys.argv = ["pepper", "--fail-any-none", "minion_id", "request"]
ret_code = pepper.script.Pepper()()
assert ret_code == -1
@patch("pepper.cli.PepperCli.login", MagicMock(side_effect=lambda arg: None))
@patch("pepper.cli.PepperCli.low", MagicMock(side_effect=lambda api, load: PAYLOAD))
def test_fail_all():
sys.argv = ["pepper", "--fail-all", "minion_id", "request"]
ret_code = pepper.script.Pepper()()
assert ret_code == 0
@patch("pepper.cli.PepperCli.login", MagicMock(side_effect=lambda arg: None))
@patch("pepper.cli.PepperCli.low", MagicMock(side_effect=lambda api, load: PAYLOAD))
def test_fail_all_none():
sys.argv = ["pepper", "--fail-all-none", "minion_id", "request"]
ret_code = pepper.script.Pepper()()
assert ret_code == -1
07070100000031000081A400000000000000000000000167D9C77000000812000000000000000000000000000000000000004100000000pepper-0.7.6+git44.996fc29/tests/unit/test_retcodes_pass_fail.py# Import Python Libraries
import sys
from unittest.mock import MagicMock
from unittest.mock import patch
import pepper
# Import Pepper Libraries
PAYLOAD = {
"return": [
{
"saltstack.ezh.msk.ru": {"jid": "20180414193904158892", "ret": "pass", "retcode": 0},
"ezh.msk.ru": {
"jid": "20180414193904158892",
"ret": "/bin/sh: 123: command not found",
"retcode": 127,
},
}
]
}
@patch("pepper.cli.PepperCli.login", MagicMock(side_effect=lambda arg: None))
@patch("pepper.cli.PepperCli.low", MagicMock(side_effect=lambda api, load: PAYLOAD))
def test_default():
sys.argv = ["pepper", "minion_id", "request"]
ret_code = pepper.script.Pepper()()
assert ret_code == 0
@patch("pepper.cli.PepperCli.login", MagicMock(side_effect=lambda arg: None))
@patch("pepper.cli.PepperCli.low", MagicMock(side_effect=lambda api, load: PAYLOAD))
def test_fail_any():
sys.argv = ["pepper", "--fail-any", "minion_id", "request"]
ret_code = pepper.script.Pepper()()
assert ret_code == 127
@patch("pepper.cli.PepperCli.login", MagicMock(side_effect=lambda arg: None))
@patch("pepper.cli.PepperCli.low", MagicMock(side_effect=lambda api, load: PAYLOAD))
def test_fail_any_none():
sys.argv = ["pepper", "--fail-any-none", "minion_id", "request"]
ret_code = pepper.script.Pepper()()
assert ret_code == 127
@patch("pepper.cli.PepperCli.login", MagicMock(side_effect=lambda arg: None))
@patch("pepper.cli.PepperCli.low", MagicMock(side_effect=lambda api, load: PAYLOAD))
def test_fail_all():
sys.argv = ["pepper", "--fail-all", "minion_id", "request"]
ret_code = pepper.script.Pepper()()
assert ret_code == 0
@patch("pepper.cli.PepperCli.login", MagicMock(side_effect=lambda arg: None))
@patch("pepper.cli.PepperCli.low", MagicMock(side_effect=lambda api, load: PAYLOAD))
def test_fail_all_none():
sys.argv = ["pepper", "--fail-all-none", "minion_id", "request"]
ret_code = pepper.script.Pepper()()
assert ret_code == 0
07070100000032000081A400000000000000000000000167D9C770000007E3000000000000000000000000000000000000004100000000pepper-0.7.6+git44.996fc29/tests/unit/test_retcodes_pass_none.py# Import Python Libraries
import sys
from unittest.mock import MagicMock
from unittest.mock import patch
import pepper
# Import Pepper Libraries
PAYLOAD = {
"return": [
{
"saltstack.ezh.msk.ru": {"jid": "20180414193904158892", "ret": "pass", "retcode": 0},
"ezh.msk.ru": {
"jid": "20180414193904158892",
"ret": "Hello from SaltStack",
},
}
]
}
@patch("pepper.cli.PepperCli.login", MagicMock(side_effect=lambda arg: None))
@patch("pepper.cli.PepperCli.low", MagicMock(side_effect=lambda api, load: PAYLOAD))
def test_default():
sys.argv = ["pepper", "minion_id", "request"]
ret_code = pepper.script.Pepper()()
assert ret_code == 0
@patch("pepper.cli.PepperCli.login", MagicMock(side_effect=lambda arg: None))
@patch("pepper.cli.PepperCli.low", MagicMock(side_effect=lambda api, load: PAYLOAD))
def test_fail_any():
sys.argv = ["pepper", "--fail-any", "minion_id", "request"]
ret_code = pepper.script.Pepper()()
assert ret_code == 0
@patch("pepper.cli.PepperCli.login", MagicMock(side_effect=lambda arg: None))
@patch("pepper.cli.PepperCli.low", MagicMock(side_effect=lambda api, load: PAYLOAD))
def test_fail_any_none():
sys.argv = ["pepper", "--fail-any-none", "minion_id", "request"]
ret_code = pepper.script.Pepper()()
assert ret_code == 0
@patch("pepper.cli.PepperCli.login", MagicMock(side_effect=lambda arg: None))
@patch("pepper.cli.PepperCli.low", MagicMock(side_effect=lambda api, load: PAYLOAD))
def test_fail_all():
sys.argv = ["pepper", "--fail-all", "minion_id", "request"]
ret_code = pepper.script.Pepper()()
assert ret_code == 0
@patch("pepper.cli.PepperCli.login", MagicMock(side_effect=lambda arg: None))
@patch("pepper.cli.PepperCli.low", MagicMock(side_effect=lambda api, load: PAYLOAD))
def test_fail_all_none():
sys.argv = ["pepper", "--fail-all-none", "minion_id", "request"]
ret_code = pepper.script.Pepper()()
assert ret_code == 0
07070100000033000081A400000000000000000000000167D9C770000007B2000000000000000000000000000000000000004100000000pepper-0.7.6+git44.996fc29/tests/unit/test_retcodes_pass_pass.py# Import Python Libraries
import sys
from unittest.mock import MagicMock
from unittest.mock import patch
import pepper
# Import Pepper Libraries
PAYLOAD = {
"return": [
{
"ezh.msk.ru": {"jid": "20180414193904158892", "ret": "pass", "retcode": 0},
"saltstack.ezh.msk.ru": {"jid": "20180414193904158892", "ret": "pass", "retcode": 0},
}
]
}
@patch("pepper.cli.PepperCli.login", MagicMock(side_effect=lambda arg: None))
@patch("pepper.cli.PepperCli.low", MagicMock(side_effect=lambda api, load: PAYLOAD))
def test_default():
sys.argv = ["pepper", "minion_id", "request"]
ret_code = pepper.script.Pepper()()
assert ret_code == 0
@patch("pepper.cli.PepperCli.login", MagicMock(side_effect=lambda arg: None))
@patch("pepper.cli.PepperCli.low", MagicMock(side_effect=lambda api, load: PAYLOAD))
def test_fail_any():
sys.argv = ["pepper", "--fail-any", "minion_id", "request"]
ret_code = pepper.script.Pepper()()
assert ret_code == 0
@patch("pepper.cli.PepperCli.login", MagicMock(side_effect=lambda arg: None))
@patch("pepper.cli.PepperCli.low", MagicMock(side_effect=lambda api, load: PAYLOAD))
def test_fail_any_none():
sys.argv = ["pepper", "--fail-any-none", "minion_id", "request"]
ret_code = pepper.script.Pepper()()
assert ret_code == 0
@patch("pepper.cli.PepperCli.login", MagicMock(side_effect=lambda arg: None))
@patch("pepper.cli.PepperCli.low", MagicMock(side_effect=lambda api, load: PAYLOAD))
def test_fail_all():
sys.argv = ["pepper", "--fail-all", "minion_id", "request"]
ret_code = pepper.script.Pepper()()
assert ret_code == 0
@patch("pepper.cli.PepperCli.login", MagicMock(side_effect=lambda arg: None))
@patch("pepper.cli.PepperCli.low", MagicMock(side_effect=lambda api, load: PAYLOAD))
def test_fail_all_none():
sys.argv = ["pepper", "--fail-all-none", "minion_id", "request"]
ret_code = pepper.script.Pepper()()
assert ret_code == 0
07070100000034000081A400000000000000000000000167D9C770000005B7000000000000000000000000000000000000003400000000pepper-0.7.6+git44.996fc29/tests/unit/test_token.pyimport json
import sys
from unittest.mock import MagicMock
from unittest.mock import mock_open
from unittest.mock import patch
import pepper.cli
# Import Pepper Libraries
# Import Testing Libraries
def test_token():
sys.argv = ["pepper", "*", "test.ping"]
client = pepper.cli.PepperCli()
client.options.mktoken = True
mock_data = (
'{"perms": [".*", "@runner", "@wheel", "@jobs"], "start": 1529967752.516165, '
'"token": "7130faa1e17f935d5f2702465cafdc73212d64d0", "expire": 1529968905.1131861, '
'"user": "pepper", "eauth": "pam"}\n'
)
mock_api = MagicMock()
mock_api.login = MagicMock(return_value=mock_data)
with patch("pepper.cli.open", mock_open(read_data=mock_data)), patch(
"pepper.cli.PepperCli.get_login_details", MagicMock(return_value=mock_data)
), patch("pepper.cli.PepperCli.parse_login", MagicMock(return_value={})), patch(
"os.remove", MagicMock(return_value=None)
), patch(
"json.dump", MagicMock(side_effect=Exception("Test Error"))
):
ret1 = client.login(mock_api)
with patch("os.path.isfile", MagicMock(return_value=False)):
ret2 = client.login(mock_api)
with patch("time.time", MagicMock(return_value=1529968044.133632)):
ret3 = client.login(mock_api)
assert json.loads(ret1) == json.loads(mock_data)
assert json.loads(ret2) == json.loads(mock_data)
assert ret3 == json.loads(mock_data)
07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!224 blocks