pax_global_header00006660000000000000000000000064147554054120014521gustar00rootroot0000000000000052 comment=2a29132efab4a183580a30d9c18b9bdb61b5ab49 podman-py-5.4.0.1/000077500000000000000000000000001475540541200135725ustar00rootroot00000000000000podman-py-5.4.0.1/.cirrus.yml000066400000000000000000000065371475540541200157150ustar00rootroot00000000000000--- env: DEST_BRANCH: "main" GOPATH: "/var/tmp/go" GOBIN: "${GOPATH}/bin" GOCACHE: "${GOPATH}/cache" GOSRC: "${GOPATH}/src/github.com/containers/podman" CIRRUS_WORKING_DIR: "${GOPATH}/src/github.com/containers/podman-py" SCRIPT_BASE: "./contrib/cirrus" CIRRUS_SHELL: "/bin/bash" HOME: "/root" # not set by default #### #### Cache-image names to test with (double-quotes around names are critical) #### # Google-cloud VM Images IMAGE_SUFFIX: "c20250131t121915z-f41f40d13" FEDORA_CACHE_IMAGE_NAME: "fedora-podman-py-${IMAGE_SUFFIX}" gcp_credentials: ENCRYPTED[0c639039cdd3a9a93fac7746ea1bf366d432e5ff3303bf293e64a7ff38dee85fd445f71625fa5626dc438be2b8efe939] # Default VM to use unless set or modified by task gce_instance: image_project: "libpod-218412" zone: "us-central1-c" # Required by Cirrus for the time being cpu: 2 memory: "4Gb" disk: 200 # Required for performance reasons image_name: "${FEDORA_CACHE_IMAGE_NAME}" gating_task: name: "Gating test" alias: gating # Only run this on PRs, never during post-merge testing. This is also required # for proper setting of EPOCH_TEST_COMMIT value, required by validation tools. only_if: $CIRRUS_PR != "" timeout_in: 20m env: PATH: ${PATH}:${GOPATH}/bin script: - make - make lint test_task: name: "Test on Fedora" alias: test depends_on: - gating script: - ${SCRIPT_BASE}/enable_ssh.sh - ${SCRIPT_BASE}/build_podman.sh - ${SCRIPT_BASE}/enable_podman.sh - ${SCRIPT_BASE}/test.sh latest_task: name: "Test Podman main on Fedora" alias: latest allow_failures: true depends_on: - gating env: PATH: ${PATH}:${GOPATH}/bin script: - ${SCRIPT_BASE}/enable_ssh.sh - ${SCRIPT_BASE}/build_podman.sh - ${SCRIPT_BASE}/enable_podman.sh - ${SCRIPT_BASE}/test.sh # This task is critical. It updates the "last-used by" timestamp stored # in metadata for all VM images. This mechanism functions in tandem with # an out-of-band pruning operation to remove disused VM images. meta_task: alias: meta name: "VM img. keepalive" container: &smallcontainer image: "quay.io/libpod/imgts:latest" cpu: 1 memory: 1 env: IMGNAMES: ${FEDORA_CACHE_IMAGE_NAME} BUILDID: "${CIRRUS_BUILD_ID}" REPOREF: "${CIRRUS_REPO_NAME}" GCPJSON: ENCRYPTED[e8a53772eff6e86bf6b99107b6e6ee3216e2ca00c36252ae3bd8cb29d9b903ffb2e1a1322ea810ca251b04f833b8f8d9] GCPNAME: ENCRYPTED[fb878daf188d35c2ed356dc777267d99b59863ff3abf0c41199d562fca50ba0668fdb0d87e109c9eaa2a635d2825feed] GCPPROJECT: "libpod-218412" clone_script: &noop mkdir -p $CIRRUS_WORKING_DIR script: /usr/local/bin/entrypoint.sh # Status aggregator for all tests. This task simply ensures a defined # set of tasks all passed, and allows confirming that based on the status # of this task. success_task: name: "Total Success" alias: success # N/B: ALL tasks must be listed here, minus their '_task' suffix. depends_on: - meta - gating - test - latest container: image: quay.io/libpod/alpine:latest cpu: 1 memory: 1 env: CIRRUS_SHELL: "/bin/sh" clone_script: *noop script: *noop podman-py-5.4.0.1/.fmf/000077500000000000000000000000001475540541200144205ustar00rootroot00000000000000podman-py-5.4.0.1/.fmf/version000066400000000000000000000000021475540541200160200ustar00rootroot000000000000001 podman-py-5.4.0.1/.github/000077500000000000000000000000001475540541200151325ustar00rootroot00000000000000podman-py-5.4.0.1/.github/renovate.json5000066400000000000000000000036101475540541200177350ustar00rootroot00000000000000/* Renovate is a service similar to GitHub Dependabot, but with (fantastically) more configuration options. So many options in fact, if you're new I recommend glossing over this cheat-sheet prior to the official documentation: https://www.augmentedmind.de/2021/07/25/renovate-bot-cheat-sheet Configuration Update/Change Procedure: 1. Make changes 2. Manually validate changes (from repo-root): podman run -it \ -v ./.github/renovate.json5:/usr/src/app/renovate.json5:z \ docker.io/renovate/renovate:latest \ renovate-config-validator 3. Commit. Configuration Reference: https://docs.renovatebot.com/configuration-options/ Monitoring Dashboard: https://app.renovatebot.com/dashboard#github/containers Note: The Renovate bot will create/manage it's business on branches named 'renovate/*'. Otherwise, and by default, the only the copy of this file that matters is the one on the `main` branch. No other branches will be monitored or touched in any way. */ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", /************************************************* ****** Global/general configuration options ***** *************************************************/ // Re-use predefined sets of configuration options to DRY "extends": [ // https://github.com/containers/automation/blob/main/renovate/defaults.json5 "github>containers/automation//renovate/defaults.json5" ], // Permit automatic rebasing when base-branch changes by more than // one commit. "rebaseWhen": "behind-base-branch", /************************************************* *** Repository-specific configuration options *** *************************************************/ // Don't leave dep. update. PRs "hanging", assign them to people. "assignees": ["inknos"], } podman-py-5.4.0.1/.github/workflows/000077500000000000000000000000001475540541200171675ustar00rootroot00000000000000podman-py-5.4.0.1/.github/workflows/pr.yml000066400000000000000000000011411475540541200203300ustar00rootroot00000000000000name: validate on: pull_request: jobs: commit: runs-on: ubuntu-24.04 # Only check commits on pull requests. if: github.event_name == 'pull_request' steps: - name: get pr commits id: 'get-pr-commits' uses: tim-actions/get-pr-commits@v1.3.1 with: token: ${{ secrets.GITHUB_TOKEN }} - name: check subject line length uses: tim-actions/commit-message-checker-with-regex@v0.3.2 with: commits: ${{ steps.get-pr-commits.outputs.commits }} pattern: '^.{0,72}(\n.*)*$' error: 'Subject too long (max 72)' podman-py-5.4.0.1/.github/workflows/pre-commit.yml000066400000000000000000000005201475540541200217630ustar00rootroot00000000000000name: pre-commit on: pull_request: push: branches: [main] jobs: pre-commit: runs-on: ubuntu-latest env: SKIP: no-commit-to-branch steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: | 3.9 3.x - uses: pre-commit/action@v3.0.1 podman-py-5.4.0.1/.github/workflows/publish-to-test-pypi.yml000066400000000000000000000070761475540541200237460ustar00rootroot00000000000000name: Publish Python 🐍 distribution 📦 to PyPI and TestPyPI on: push jobs: build: name: Build distribution 📦 # ensure the workflow is never executed on forked branches # it would fail anyway, so we just avoid to see an error if: ${{ github.repository == 'containers/podman-py' }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.x" - name: Install pypa/build run: >- python3 -m pip install build --user - name: Build a binary wheel and a source tarball run: python3 -m build - name: Store the distribution packages uses: actions/upload-artifact@v4 with: name: python-package-distributions path: dist/ publish-to-pypi: name: >- Publish Python 🐍 distribution 📦 to PyPI if: startsWith(github.ref, 'refs/tags/') && github.repository == 'containers/podman-py' needs: - build runs-on: ubuntu-latest environment: name: pypi url: https://pypi.org/p/podman permissions: id-token: write # IMPORTANT: mandatory for trusted publishing steps: - name: Download all the dists uses: actions/download-artifact@v4 with: name: python-package-distributions path: dist/ - name: Publish distribution 📦 to PyPI uses: pypa/gh-action-pypi-publish@release/v1 github-release: name: >- Sign the Python 🐍 distribution 📦 with Sigstore and upload them to GitHub Release if: github.repository == 'containers/podman-py' needs: - publish-to-pypi runs-on: ubuntu-latest permissions: contents: write # IMPORTANT: mandatory for making GitHub Releases id-token: write # IMPORTANT: mandatory for sigstore steps: - name: Download all the dists uses: actions/download-artifact@v4 with: name: python-package-distributions path: dist/ - name: Sign the dists with Sigstore uses: sigstore/gh-action-sigstore-python@v3.0.0 with: inputs: >- ./dist/*.tar.gz ./dist/*.whl - name: Create GitHub Release env: GITHUB_TOKEN: ${{ github.token }} run: >- gh release create '${{ github.ref_name }}' --repo '${{ github.repository }}' --generate-notes - name: Upload artifact signatures to GitHub Release env: GITHUB_TOKEN: ${{ github.token }} # Upload to GitHub Release using the `gh` CLI. # `dist/` contains the built packages, and the # sigstore-produced signatures and certificates. run: >- gh release upload '${{ github.ref_name }}' dist/** --repo '${{ github.repository }}' publish-to-testpypi: name: Publish Python 🐍 distribution 📦 to TestPyPI if: github.repository == 'containers/podman-py' needs: - build runs-on: ubuntu-latest environment: name: testpypi url: https://test.pypi.org/p/podman permissions: id-token: write # IMPORTANT: mandatory for trusted publishing steps: - name: Download all the dists uses: actions/download-artifact@v4 with: name: python-package-distributions path: dist/ - name: Publish distribution 📦 to TestPyPI uses: pypa/gh-action-pypi-publish@release/v1 with: repository-url: https://test.pypi.org/legacy/ podman-py-5.4.0.1/.gitignore000066400000000000000000000023111475540541200155570ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints #PyCharm ./idea/ .idea* .vscode* # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ podman-py-5.4.0.1/.packit.yaml000066400000000000000000000054221475540541200160120ustar00rootroot00000000000000--- # See the documentation for more information: # https://packit.dev/docs/configuration/ upstream_tag_template: v{version} packages: python-podman-fedora: pkg_tool: fedpkg downstream_package_name: python-podman specfile_path: rpm/python-podman.spec python-podman-centos: pkg_tool: centpkg downstream_package_name: python-podman specfile_path: rpm/python-podman.spec python-podman-rhel: specfile_path: rpm/python-podman.spec srpm_build_deps: - make jobs: # Copr builds for Fedora - job: copr_build trigger: pull_request identifier: pr-fedora packages: [python-podman-fedora] targets: - fedora-all # Copr builds for CentOS Stream - job: copr_build trigger: pull_request identifier: pr-centos packages: [python-podman-centos] targets: - centos-stream-10 - centos-stream-9 # Copr builds for RHEL - job: copr_build trigger: pull_request identifier: pr-rhel packages: [python-podman-rhel] targets: - epel-9 # Run on commit to main branch - job: copr_build trigger: commit identifier: commit-fedora packages: [python-podman-fedora] branch: main owner: rhcontainerbot project: podman-next # Downstream sync for Fedora - job: propose_downstream trigger: release packages: [python-podman-fedora] dist_git_branches: - fedora-all # Downstream sync for CentOS Stream # TODO: c9s enablement being tracked in https://issues.redhat.com/browse/RUN-2123 - job: propose_downstream trigger: release packages: [python-podman-centos] dist_git_branches: - c10s - c9s - job: koji_build trigger: commit packages: [python-podman-fedora] dist_git_branches: - fedora-all - job: bodhi_update trigger: commit packages: [python-podman-fedora] dist_git_branches: - fedora-branched # rawhide updates are created automatically # Test linting on the codebase # This test might break based on the OS and lint used, so we follow fedora-latest as a reference - job: tests trigger: pull_request identifier: upstream-sanity tmt_plan: /upstream/sanity packages: [python-podman-fedora] targets: - fedora-latest-stable skip_build: true - job: tests trigger: pull_request identifier: upstream-all-fedora tmt_plan: /upstream/all packages: [python-podman-fedora] targets: - fedora-all - job: tests trigger: pull_request identifier: upstream-base-centos tmt_plan: /upstream/base packages: [python-podman-centos] targets: - centos-stream-9 - centos-stream-10 - job: tests trigger: pull_request identifier: upstream-base-rhel tmt_plan: /upstream/base packages: [python-podman-rhel] targets: - epel-9 podman-py-5.4.0.1/.pre-commit-config.yaml000066400000000000000000000007151475540541200200560ustar00rootroot00000000000000repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v2.3.0 hooks: - id: check-yaml - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. rev: v0.8.1 hooks: # Run the linter. - id: ruff args: [ --fix ] # Run the formatter. - id: ruff-format - repo: https://github.com/teemtee/tmt.git rev: 1.39.0 hooks: - id: tmt-lint podman-py-5.4.0.1/.pylintrc000066400000000000000000000432241475540541200154440ustar00rootroot00000000000000[MASTER] # A comma-separated list of package or module names from where C extensions may # be loaded. Extensions are loading into the active Python interpreter and may # run arbitrary code. extension-pkg-whitelist= # Add files or directories to the blacklist. They should be base names, not # paths. ignore=CVS,docs # Add files or directories matching the regex patterns to the blacklist. The # regex matches against base names, not paths. # ignore-patterns=test_.* ignore-paths=^podman/tests/.*$ # Python code to execute, usually for sys.path manipulation such as # pygtk.require(). #init-hook= # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the # number of processors available to use. jobs=0 # Control the amount of potential inferred values when inferring a single # object. This can help the performance when dealing with large functions or # complex, nested conditions. limit-inference-results=100 # List of plugins (as comma separated values of python modules names) to load, # usually to register additional checkers. load-plugins= # Pickle collected data for later comparisons. persistent=yes # Specify a configuration file. #rcfile= # When enabled, pylint would attempt to guess common misconfiguration and emit # user-friendly hints instead of false-positive error messages. suggestion-mode=yes # Allow loading of arbitrary C extensions. Extensions are imported into the # active Python interpreter and may run arbitrary code. unsafe-load-any-extension=no [MESSAGES CONTROL] # Only show warnings with the listed confidence levels. Leave empty to show # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. confidence= # Disable the message, report, category or checker with the given id(s). You # can either give multiple identifiers separated by comma (,) or put this # option multiple times (only on the command line, not in the configuration # file where it should appear only once). You can also use "--disable=all" to # disable everything first and then reenable specific checks. For example, if # you want to run only the similarities checker, you can use "--disable=all # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use "--disable=all --enable=classes # --disable=W". disable=print-statement, parameter-unpacking, unpacking-in-except, old-raise-syntax, backtick, long-suffix, old-ne-operator, old-octal-literal, import-star-module-level, non-ascii-bytes-literal, raw-checker-failed, bad-inline-option, locally-disabled, locally-enabled, file-ignored, suppressed-message, useless-suppression, deprecated-pragma, use-symbolic-message-instead, apply-builtin, basestring-builtin, buffer-builtin, cmp-builtin, coerce-builtin, execfile-builtin, file-builtin, long-builtin, raw_input-builtin, reduce-builtin, standarderror-builtin, unicode-builtin, xrange-builtin, coerce-method, delslice-method, getslice-method, setslice-method, no-absolute-import, old-division, dict-iter-method, dict-view-method, next-method-called, metaclass-assignment, indexing-exception, raising-string, reload-builtin, oct-method, hex-method, nonzero-method, cmp-method, input-builtin, round-builtin, intern-builtin, unichr-builtin, map-builtin-not-iterating, zip-builtin-not-iterating, range-builtin-not-iterating, filter-builtin-not-iterating, using-cmp-argument, eq-without-hash, div-method, idiv-method, rdiv-method, exception-message-attribute, invalid-str-codec, sys-max-int, bad-python3-import, deprecated-string-function, deprecated-str-translate-call, deprecated-itertools-function, deprecated-types-field, next-method-defined, dict-items-not-iterating, dict-keys-not-iterating, dict-values-not-iterating, deprecated-operator-function, deprecated-urllib-function, xreadlines-attribute, deprecated-sys-function, exception-escape, comprehension-escape, no-self-use, no-member, # PyCQA/pylint#3157 too-many-public-methods, # tests can have many members fixme, # don't warn on fixme stuff duplicate-code, # current CI version doesn't honor the minimum for some reason bad-option-value # needed until pylint version is increased to a version with it # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option # multiple time (only on the command line, not in the configuration file where # it should appear only once). See also the "--disable" option for examples. enable=c-extension-no-member [REPORTS] # Python expression which should return a note less than 10 (10 is the highest # note). You have access to the variables errors warning, statement which # respectively contain the number of errors / warnings messages and the total # number of statements analyzed. This is used by the global evaluation report # (RP0004). evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) # Template used to display messages. This is a python new-style format string # used to format the message information. See doc for all details. #msg-template= # Set the output format. Available formats are text, parseable, colorized, json # and msvs (visual studio). You can also give a reporter class, e.g. # mypackage.mymodule.MyReporterClass. output-format=text # Tells whether to display a full report or only the messages. reports=no # Activate the evaluation score. score=yes [REFACTORING] # Maximum number of nested blocks for function / method body max-nested-blocks=5 # Complete name of functions that never returns. When checking for # inconsistent-return-statements if a never returning function is called then # it will be considered as an explicit return statement and no message will be # printed. never-returning-functions=sys.exit [TYPECHECK] # List of decorators that produce context managers, such as # contextlib.contextmanager. Add to this list to register other decorators that # produce valid context managers. contextmanager-decorators=contextlib.contextmanager # List of members which are set dynamically and missed by pylint inference # system, and so shouldn't trigger E1101 when accessed. Python regular # expressions are accepted. generated-members= # Tells whether missing members accessed in mixin class should be ignored. A # mixin class is detected if its name ends with "mixin" (case insensitive). ignore-mixin-members=yes # Tells whether to warn about missing members when the owner of the attribute # is inferred to be None. ignore-none=yes # This flag controls whether pylint should warn about no-member and similar # checks whenever an opaque object is returned when inferring. The inference # can return multiple potential results while evaluating a Python object, but # some branches might not be evaluated, which results in partial inference. In # that case, it might be useful to still emit no-member and other checks for # the rest of the inferred objects. ignore-on-opaque-inference=yes # List of class names for which member attributes should not be checked (useful # for classes with dynamically set attributes). This supports the use of # qualified names. ignored-classes=optparse.Values,thread._local,_thread._local, http.HTTPStatus.NOT_FOUND, http.HTTPStatus.INTERNAL_SERVER_ERROR # List of module names for which member attributes should not be checked # (useful for modules/projects where namespaces are manipulated during runtime # and thus existing member attributes cannot be deduced by static analysis. It # supports qualified module names, as well as Unix pattern matching. ignored-modules= # Show a hint with possible names when a member name was not found. The aspect # of finding the hint is based on edit distance. missing-member-hint=yes # The minimum edit distance a name should have in order to be considered a # similar match for a missing member name. missing-member-hint-distance=1 # The total number of similar names that should be taken in consideration when # showing a hint for a missing member. missing-member-max-choices=1 [SPELLING] # Limits count of emitted suggestions for spelling mistakes. max-spelling-suggestions=4 # Spelling dictionary name. Available dictionaries: none. To make it working # install python-enchant package.. # jhonce: _NEVER_ turn on spelling, it does not differentiate between human comments and strings # for arguments. :-( spelling-dict= # List of comma separated words that should not be checked. spelling-ignore-words=podman # A path to a file that contains private dictionary; one word per line. spelling-private-dict-file= # Tells whether to store unknown words to indicated private dictionary in # --spelling-private-dict-file option instead of raising a message. spelling-store-unknown-words=no [MISCELLANEOUS] # List of note tags to take in consideration, separated by a comma. notes=FIXME, XXX, TODO [STRING] # This flag controls whether inconsistent-quotes generates a warning when the # character used as a quote delimiter is used inconsistently within a module. check-quote-consistency=no [FORMAT] # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. expected-line-ending-format= # Regexp for a line that is allowed to be longer than the limit. #ignore-long-lines=^\s*(# )??$ ignore-long-lines=(?x)( ^\s*(\#\ )??$| ^\s*(from\s+\S+\s+)?import\s+.+$) # Number of spaces of indent required inside a hanging or continued line. indent-after-paren=4 # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 # tab). indent-string=' ' # Maximum number of characters on a single line. max-line-length=100 # Maximum number of lines in a module. max-module-lines=1000 # Allow the body of a class to be on the same line as the declaration if body # contains single statement. single-line-class-stmt=no # Allow the body of an if to be on the same line as the test if there is no # else. single-line-if-stmt=no [BASIC] # Naming style matching correct argument names. #argument-naming-style=snake_case # Regular expression matching correct argument names. Overrides argument- # naming-style. argument-rgx=[a-z_][a-z0-9_]{1,30}$ # Naming style matching correct attribute names. attr-naming-style=snake_case # Regular expression matching correct attribute names. Overrides attr-naming- # style. #attr-rgx= # Bad variable names which should always be refused, separated by a comma. bad-names=foo, bar, baz, toto, tutu, tata # Naming style matching correct class attribute names. class-attribute-naming-style=any # Regular expression matching correct class attribute names. Overrides class- # attribute-naming-style. #class-attribute-rgx= class-attribute-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$ # Naming style matching correct class names. class-naming-style=PascalCase # Regular expression matching correct class names. Overrides class-naming- # style. #class-rgx= # Naming style matching correct constant names. const-naming-style=UPPER_CASE # Regular expression matching correct constant names. Overrides const-naming- # style. #const-rgx= # Minimum line length for functions/classes that require docstrings, shorter # ones are exempt. docstring-min-length=12 # Naming style matching correct function names. function-naming-style=snake_case # Regular expression matching correct function names. Overrides function- # naming-style. #function-rgx= # Good variable names which should always be accepted, separated by a comma. good-names=c, e, i, ip, j, k, r, v, ex, Run, _ # Include a hint for the correct naming format with invalid-name. include-naming-hint=no # Naming style matching correct inline iteration names. inlinevar-naming-style=any # Regular expression matching correct inline iteration names. Overrides # inlinevar-naming-style. #inlinevar-rgx= inlinevar-rgx=^[a-z][a-z0-9_]*$ # Naming style matching correct method names. method-naming-style=snake_case # Regular expression matching correct method names. Overrides method-naming- # style. #method-rgx= # Naming style matching correct module names. module-naming-style=snake_case # Regular expression matching correct module names. Overrides module-naming- # style. #module-rgx= # Colon-delimited sets of names that determine each other's naming style when # the name regexes allow several styles. name-group= # Regular expression which should only match function or class names that do # not require a docstring. no-docstring-rgx=^_ # List of decorators that produce properties, such as abc.abstractproperty. Add # to this list to register other decorators that produce valid properties. # These decorators are taken in consideration only for invalid-name. property-classes=abc.abstractproperty # Naming style matching correct variable names. #variable-naming-style=snake_case # Regular expression matching correct variable names. Overrides variable- # naming-style. variable-rgx=[a-z_][a-z0-9_]{2,30}$ [SIMILARITIES] # Ignore comments when computing similarities. ignore-comments=yes # Ignore docstrings when computing similarities. ignore-docstrings=yes # Ignore imports when computing similarities. ignore-imports=no # Minimum lines number of a similarity. min-similarity-lines=10 [VARIABLES] # List of additional names supposed to be defined in builtins. Remember that # you should avoid to define new builtins when possible. additional-builtins= # Tells whether unused global variables should be treated as a violation. allow-global-unused-variables=yes # List of strings which can identify a callback function by name. A callback # name must start or end with one of those strings. callbacks=cb_, _cb # A regular expression matching the name of dummy variables (i.e. expected to # not be used). dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ # Argument names that match this expression will be ignored. Default to name # with leading underscore. ignored-argument-names=_.*|^ignored_|^unused_ # Tells whether we should check for unused import in __init__ files. init-import=no # List of qualified module names which can have objects that can redefine # builtins. redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io [LOGGING] # Logging modules to check that the string format arguments are in logging # function parameter format. logging-modules=logging [IMPORTS] # Allow wildcard imports from modules that define __all__. allow-wildcard-with-all=no # Analyse import fallback blocks. This can be used to support both Python 2 and # 3 compatible code, which means that the block might have code that exists # only in one or another interpreter, leading to false positives when analysed. analyse-fallback-blocks=no # Deprecated modules which should not be used, separated by a comma. deprecated-modules=optparse,tkinter.tix # Create a graph of external dependencies in the given file (report RP0402 must # not be disabled). ext-import-graph= # Create a graph of every (i.e. internal and external) dependencies in the # given file (report RP0402 must not be disabled). import-graph= # Create a graph of internal dependencies in the given file (report RP0402 must # not be disabled). int-import-graph= # Force import order to recognize a module as part of the standard # compatibility libraries. known-standard-library= # Force import order to recognize a module as part of a third party library. known-third-party=enchant,requests [DESIGN] # Support argparse.Action constructor API # Maximum number of arguments for function / method. max-args=12 # Maximum number of attributes for a class (see R0902). max-attributes=7 # Maximum number of boolean expressions in an if statement. max-bool-expr=5 # Maximum number of branch for function / method body. max-branches=12 # Maximum number of locals for function / method body. max-locals=15 # Maximum number of parents for a class (see R0901). max-parents=10 # Maximum number of public methods for a class (see R0904). max-public-methods=20 # Maximum number of return / yield for function / method body. max-returns=6 # Maximum number of statements in function / method body. max-statements=50 # Minimum number of public methods for a class (see R0903). min-public-methods=2 [CLASSES] # List of method names used to declare (i.e. assign) instance attributes. defining-attr-methods=__init__, __new__, setUp # List of member names, which should be excluded from the protected access # warning. exclude-protected=_asdict, _fields, _replace, _source, _make # List of valid names for the first argument in a class method. valid-classmethod-first-arg=cls, class_ # List of valid names for the first argument in a metaclass class method. valid-metaclass-classmethod-first-arg=mcs [EXCEPTIONS] # Exceptions that will emit a warning when being caught. Defaults to # "Exception". overgeneral-exceptions=builtins.StandardError, builtins.Exception, builtins.BaseException podman-py-5.4.0.1/.readthedocs.yaml000066400000000000000000000013661475540541200170270ustar00rootroot00000000000000# Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 formats: - pdf - epub # Set the version of Python and other tools you might need build: os: ubuntu-22.04 tools: python: "3.11" jobs: pre_build: - sphinx-apidoc --separate --no-toc --force --templatedir docs/source/_templates/apidoc -o docs/source/ podman podman/tests # We recommend specifying your dependencies to enable reproducible builds: # https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html python: install: - requirements: requirements.txt # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/source/conf.py fail_on_warning: false podman-py-5.4.0.1/CODE-OF-CONDUCT.md000066400000000000000000000003011475540541200162170ustar00rootroot00000000000000## The podman-py Project Community Code of Conduct The podman-py project follows the [Containers Community Code of Conduct](https://github.com/containers/common/blob/main/CODE-OF-CONDUCT.md). podman-py-5.4.0.1/CONTRIBUTING.md000066400000000000000000000074221475540541200160300ustar00rootroot00000000000000# How to contribute Thank you in your interest in the podman-py project. We need your help to make it successful. You may also want to look at: - [podman](https://github.com/containers/podman) - [podman Reference](https://podman.readthedocs.io/en/latest/Reference.html) ## Reporting Issues Before reporting an issue, check our backlog of open issues to see if someone else has already reported it. If so, feel free to add your scenario, or additional information, to the discussion. Or simply "subscribe" to it to be notified when it is updated. If you find a new issue with the project we'd love to hear about it! The most important aspect of a bug report is that it includes enough information for us to reproduce it. So, please include as much detail as possible and try to remove the extra stuff that doesn't really relate to the issue itself. The easier it is for us to reproduce it, the faster it'll be fixed! Please don't include any private/sensitive information in your issue! ## Tools we use - Python >= 3.9 - [pre-commit](https://pre-commit.com/) - [ruff](https://docs.astral.sh/ruff/) - [tox](https://tox.readthedocs.io/en/latest/) - You may need to use [virtualenv](https://virtualenv.pypa.io/en/latest/) to support Python 3.6 ## Testing Depending on the size of your PR we will expect at a minimum unit tests. Code will not be merged if unit test coverage drops below 85%. Integration tests would be required for large changes (TBD). Run unit tests and get coverage report: ``` pip install tox tox -e coverage ``` ## Submitting changes - Create a github pull request (PR) - We expect a short summary followed by a longer description of why you are making these change(s). - Include the header `Signed-off-by: Git Hub User ` in your PR description/commit message with your name. - Setting `user.name` and `user.email` in your git configs allows you to then use `git commit -s`. Let git do the work of signing your commits. ## Where to find other contributors - For general questions and discussion, please use the IRC #podman channel on irc.libera.chat. - For discussions around issues/bugs and features, you can use the GitHub [issues](https://github.com/containers/podman-py/issues) and [PRs](https://github.com/containers/podman-py/pulls) tracking system. ## Coding conventions - Formatting and linting are incorporated using [ruff](https://docs.astral.sh/ruff/). - If you use [pre-commit](https://pre-commit.com/) the checks will run automatically when you commit some changes - If you prefer to run the ckecks with pre-commit, use `pre-commit run -a` to run the pre-commit checks for you. - If you'd like to see what's happening with the checks you can run the [linter](https://docs.astral.sh/ruff/linter/) and [formatter](https://docs.astral.sh/ruff/formatter/) separately with `ruff check --diff` and `ruff format --diff` - Checks need to pass pylint - exceptions are possible, but you will need to make a good argument - Use spaces not tabs for indentation - This is open source software. Consider the people who will read your code, and make it look nice for them. It's sort of like driving a car: Perhaps you love doing donuts when you're alone, but with passengers the goal is to make the ride as smooth as possible. - Use Google style python [docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) - A general exception is made for kwargs where we use the Sphinx extension of adding a section "Keyword Arguments" and documenting the accepted keyword arguments, their type and usage. Example: kwarg1 (int): Description of kwarg1 Again, thank you for your interest and participation. Jhon Honce `` Thanks to Carl Tashian, Participatory Politics Foundation for his fine CONTRIBUTING.md example. podman-py-5.4.0.1/LICENSE000066400000000000000000000261351475540541200146060ustar00rootroot00000000000000 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. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} 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. podman-py-5.4.0.1/Makefile000066400000000000000000000070161475540541200152360ustar00rootroot00000000000000export GO111MODULE=off export GOPROXY=https://proxy.golang.org PYTHON ?= $(shell command -v python3 2>/dev/null || command -v python || which python3) DESTDIR ?= / DESTDIR ?= EPOCH_TEST_COMMIT ?= $(shell git merge-base $${DEST_BRANCH:-main} HEAD) HEAD ?= HEAD export PODMAN_VERSION ?= "5.4.0" .PHONY: podman podman: rm dist/* || : $(PYTHON) -m pip install -q build PODMAN_VERSION=$(PODMAN_VERSION) \ $(PYTHON) -m build .PHONY: lint lint: tox $(PYTHON) -m tox -e format,lint .PHONY: tests tests: tox # see tox.ini for environment variable settings $(PYTHON) -m tox -e coverage,py39,py310,py311,py312,py313 .PHONY: tests-ci-base-python tests-ci-base-python: $(PYTHON) -m tox -e coverage,py .PHONY: tests-ci-all-python tests-ci-all-python: $(PYTHON) -m tox -e coverage,py39,py310,py311,py312,py313 .PHONY: unittest unittest: coverage run -m unittest discover -s podman/tests/unit coverage report -m --skip-covered --fail-under=80 --omit=./podman/tests/* --omit=.tox/* --omit=/usr/lib/* .PHONY: integration integration: coverage run -m unittest discover -s podman/tests/integration coverage report -m --skip-covered --fail-under=80 --omit=./podman/tests/* --omit=.tox/* --omit=/usr/lib/* .PHONY: tox tox: ifeq (, $(shell which dnf)) brew install python@3.9 python@3.10 python@3.11 python@3.12 python@3.13 else -dnf install -y python3 python3.9 python3.10 python3.11 python3.12 python3.13 endif # ensure tox is available. It will take care of other testing requirements $(PYTHON) -m pip install --user tox .PHONY: test-release test-release: SOURCE = $(shell find dist -regex '.*/podman-[0-9][0-9\.]*.tar.gz' -print) test-release: twine upload --verbose -r testpypi dist/*whl $(SOURCE) # pip install -i https://test.pypi.org/simple/ podman .PHONY: release release: SOURCE = $(shell find dist -regex '.*/podman-[0-9][0-9\.]*.tar.gz' -print) release: twine upload --verbose dist/*whl $(SOURCE) # pip install podman .PHONY: docs docs: sphinx-apidoc --separate --no-toc --force --templatedir docs/source/_templates/apidoc \ -o docs/source/ podman podman/tests # HARD CODED COMMAND from readthedocs! We must conform! # -T : traceback # -E : do not use saved environment, always read all files # -W : warnings reported as errors then --keep-going when getting warnings # -b html : build html # -d : path for cached environment and doctree files # -D language=en : define language as en # . : source directory # _build/html : target cd docs/source && python3 -m sphinx -T -E -W --keep-going -b html -d _build/doctrees -D language=en . _build/html .PHONY: rpm rpm: ## Build rpm packages rpkg local # .PHONY: install HEAD ?= HEAD # install: # $(PYTHON) setup.py install --root ${DESTDIR} # .PHONY: upload # upload: clean # PODMAN_VERSION=$(PODMAN_VERSION) $(PYTHON) setup.py sdist bdist_wheel # twine check dist/* # twine upload --verbose dist/* # twine upload --verbose dist/* .PHONY: clobber clobber: uninstall clean .PHONY: uninstall uninstall: $(PYTHON) -m pip uninstall --yes podman ||: .PHONY: clean clean: rm -rf podman_py.egg-info dist build/* find . -depth -name __pycache__ -exec rm -rf {} \; find . -depth -name \*.pyc -exec rm -f {} \; $(PYTHON) ./setup.py clean --all .PHONY: validate validate: .gitvalidation lint .PHONY: .gitvalidation .gitvalidation: # I have no great ideas on how to install/check for git-validation @echo "Validating vs commit '$(call err_if_empty,EPOCH_TEST_COMMIT)'" GIT_CHECK_EXCLUDE="./vendor:docs/make.bat" git-validation -run DCO,short-subject,dangling-whitespace -range $(EPOCH_TEST_COMMIT)..$(HEAD) podman-py-5.4.0.1/OWNERS000066400000000000000000000003311475540541200145270ustar00rootroot00000000000000approvers: - edsantiago - giuseppe - jwhonce - lsm5 - Luap99 - mheon - mwhahaha - umohnani8 - vrothberg - inknos reviewers: - ashley-cui - baude - rhatdan - TomSweeneyRedHat - Edward5hen podman-py-5.4.0.1/README.md000066400000000000000000000045561475540541200150630ustar00rootroot00000000000000# podman-py [![Build Status](https://api.cirrus-ci.com/github/containers/podman-py.svg)](https://cirrus-ci.com/github/containers/podman-py/main) This python package is a library of bindings to use the RESTful API of [Podman](https://github.com/containers/podman). It is currently under development and contributors are welcome! ## Installation
```console pip install podman ```
--- **Documentation**: https://podman-py.readthedocs.io/en/latest/ **Source Code**: https://github.com/containers/podman-py --- ## Dependencies * For runtime dependencies, see \[dependencies\] in [pyproject.toml](https://github.com/containers/podman-py/blob/main/pyproject.toml) * For testing and development dependencies, see \[project.optional.dependencies\] in [pyproject.toml](https://github.com/containers/podman-py/blob/main/pyproject.toml) * The package is split in \[progress\_bar\], \[docs\], and \[test\] ## Example usage ```python """Demonstrate PodmanClient.""" import json from podman import PodmanClient # Provide a URI path for the libpod service. In libpod, the URI can be a unix # domain socket(UDS) or TCP. The TCP connection has not been implemented in this # package yet. uri = "unix:///run/user/1000/podman/podman.sock" with PodmanClient(base_url=uri) as client: version = client.version() print("Release: ", version["Version"]) print("Compatible API: ", version["ApiVersion"]) print("Podman API: ", version["Components"][0]["Details"]["APIVersion"], "\n") # get all images for image in client.images.list(): print(image, image.id, "\n") # find all containers for container in client.containers.list(): # After a list call you would probably want to reload the container # to get the information about the variables such as status. # Note that list() ignores the sparse option and assumes True by default. container.reload() print(container, container.id, "\n") print(container, container.status, "\n") # available fields print(sorted(container.attrs.keys())) print(json.dumps(client.df(), indent=4)) ``` ## Contributing See [CONTRIBUTING.md](https://github.com/containers/podman-py/blob/main/CONTRIBUTING.md) podman-py-5.4.0.1/SECURITY.md000066400000000000000000000003611475540541200153630ustar00rootroot00000000000000## Security and Disclosure Information Policy for the podman-py Project The podman-py Project follows the [Security and Disclosure Information Policy](https://github.com/containers/common/blob/main/SECURITY.md) for the Containers Projects. podman-py-5.4.0.1/contrib/000077500000000000000000000000001475540541200152325ustar00rootroot00000000000000podman-py-5.4.0.1/contrib/cirrus/000077500000000000000000000000001475540541200165415ustar00rootroot00000000000000podman-py-5.4.0.1/contrib/cirrus/build_podman.sh000077500000000000000000000002341475540541200215340ustar00rootroot00000000000000#!/bin/bash set -xeo pipefail systemctl stop podman.socket || : dnf remove podman -y dnf copr enable rhcontainerbot/podman-next -y dnf install podman -y podman-py-5.4.0.1/contrib/cirrus/enable_podman.sh000077500000000000000000000003661475540541200216710ustar00rootroot00000000000000#!/bin/bash set -eo pipefail systemctl enable podman.socket podman.service systemctl start podman.socket systemctl status podman.socket ||: # log which version of podman we just enabled echo "Locate podman: $(type -P podman)" podman --version podman-py-5.4.0.1/contrib/cirrus/enable_ssh.sh000077500000000000000000000004201475540541200211770ustar00rootroot00000000000000#!/bin/bash set -eo pipefail systemctl enable sshd systemctl start sshd systemctl status sshd ||: ssh-keygen -t ecdsa -b 521 -f /root/.ssh/id_ecdsa -P "" cp /root/.ssh/authorized_keys /root/.ssh/authorized_keys% cat /root/.ssh/id_ecdsa.pub >>/root/.ssh/authorized_keys podman-py-5.4.0.1/contrib/cirrus/test.sh000077500000000000000000000000521475540541200200540ustar00rootroot00000000000000#!/bin/bash set -eo pipefail make tests podman-py-5.4.0.1/contrib/examples/000077500000000000000000000000001475540541200170505ustar00rootroot00000000000000podman-py-5.4.0.1/contrib/examples/demo.py000077500000000000000000000024051475540541200203520ustar00rootroot00000000000000#!/usr/bin/env python3 import podman with podman.PodmanClient() as client: print("** Check Service Available") if client.ping(): print("Service active") else: print(f"No service found @ {client.base_url}") print("\n** Print out some versions") version_report = client.version() print("Service Version: ", version_report["Version"]) print("Service API: ", version_report["Components"][0]["Details"]["APIVersion"]) print("Minimal API: ", version_report["Components"][0]["Details"]["MinAPIVersion"]) print("\n** Pull latest alpine Image") image = client.images.pull("quay.io/libpod/alpine", tag="latest") print(image, image.id) image = client.images.pull("quay.io/libpod/alpine:latest") print("\n** Create Pod") pod = client.pods.create("demo_pod") print(pod, pod.name) print("\n** Create Container in Pod") container = client.containers.create(image, pod=pod) print(container, container.name, "Pod Id:", container.attrs["Pod"][:17]) print("\n** Remove Pod and Container") pod.remove(force=True) print("\n** Remove Image and report existing Images") client.images.remove(image, force=True) for image in client.images.list(): print("Image: ", ", ".join(image.tags)) podman-py-5.4.0.1/docs/000077500000000000000000000000001475540541200145225ustar00rootroot00000000000000podman-py-5.4.0.1/docs/source/000077500000000000000000000000001475540541200160225ustar00rootroot00000000000000podman-py-5.4.0.1/docs/source/_static/000077500000000000000000000000001475540541200174505ustar00rootroot00000000000000podman-py-5.4.0.1/docs/source/_static/podman-logo.ico000066400000000000000000000353561475540541200223740ustar00rootroot0000000000000000 %6  % h6(0` $~ /$`$y~51^DŽTڠ,'}~ <3b٬ӝVߡ-+} !C6gݲբZ//| "K9mק]14~#S=t٪a28~$\@zۺسسسشش«۰d4 =~&'eDƁݴh6"B)6ǂîۺٵٵٶڶٶڷڶd-o~ ZֱܼڸdzͽIJ׻εȩã̮ջò?x F|ɷܻ­İğmũ|qpv|z{|ävpYܗ~*v֣ڸį~AA`ffw}xBs:ǶӶѯŠyϬҴNNNsssӝ(p}RپlYȶfb_|ys`}}|gg\]]lllX7g8pooo|{|{qpp”eN&FRQjiiϔ/!4Plkkܱ:%UgrqqeedIқ!+*{ǃtttfff^#H 4բihhllkv'k{ BxxxwwwΓ/":U۰:%]mIћ *-ƀ\#G'-677778888899999:7*[$[9_?_>^>cBqOqOqOqNuT{[yY`??????( @ fmdGc(;]qūDozg ,C`ԠƁӬFu{: l 0KeקɇׯL{"Q,p5SkڭܶȆۮL&b=:[pݳʸ̺˹̍ްO$)J~ۯͻȵï۾׸ֻxӛ ${[ rǴç̱Ĭ~zy}̱ʳզ7Q|"ϓï¯̼VVgP1Iܱιzo̸ǰabalkGKz{}qɽyOxXpˋّ w[3hsh¼ڬ)=uɈȿ{ŲCk':٪}|{ȹ_Biutt}ɇj^Ӟ,m~޻9V}#ȈxxxʵF|mPig:]Y|lAcťʈ۠+4 vcšYzkϰԡ4T'{EykF-EϕYo >i۰n|fQɆڝ$4xhԠ4S ,ƀEwgB5LƁ֣բբ֣֣֣֥֥֥צћWp }$--.../000 1 0??(  rnBxT8 z sJڮҭƁb7. {Rݴֵ̋hGyUȸԽƆϘȱƽöѪ| g ݷվO%K#·xKwKЖ{ȉsӣɥ d4{ŀ^{XϖąmJQ(Β~تϪ&~צͥʮN#U̐աˊأ͊أ΋֢̋ϗĊh6?podman-py-5.4.0.1/docs/source/_static/podman-logo.png000066400000000000000000001121471475540541200224000ustar00rootroot00000000000000PNG  IHDR7bAsBIT|d pHYs+tEXtSoftwarewww.inkscape.org< IDATxwxTU3%!t vQ !eŵl I-S)!4QIJ$ j@AzIHǤ$!u&<ə{}' 3s{gdrhE |\/`ڻ_tjGsbQEQEQI|B?Bpc9Bȱqٱt ((pgUbrb<((4DIO΀JIs'x:EQEQE4GrxP-8&6}R^ҋلv¥}ed]on:;1$E?,|ѡ)WK!]((L0|+`Cs-= 9ܲ ul'ry($EQEQEW\s#B iĦ }slSoTL((R6ܤg<)6VǼHuj y3)((JDr#W蜜/<٣@sx29,v(((h#2ɱMH*.7fb:*8 Jaark 8֜\m^FLts?x*.EQEQ&92m$m^FyCc`(#t tٓӯ_?uF@@t҅Ν;dBqa=ʱc8|0cǎڵ(V~n2X&)!y6X`(DL_ 5A~2gsn(rFhr3/|Iwj՜Ba?8h .R." @5qCm6{[mۆ5Yt1@V) 2ۄŕVOť(/-nNb윘\(ʙc͢LK?.shB;cZ([z뭌3ݻP(u;z(_}k֬a֭>< n;0˱MJ^N5?졐EQZ(JA(~ XCb3h ^|E֬YCtttIlt-BFF˖-cȑwb/<\k-(0`c<aPH((Y#MRxz&ӾChˇ?1 <3\/!<Nk넆:>t{7IJ*&>?,|D((Z44???ϟO?bEd2qtRu\dY 9Zsk2<((rj&sBLV}޸-Stg`_cc{5xtKEFFb <xC! )_|(z#)((gKnv\ bcKNyeJ~|ٖץKx *g<}Cc@>~bREQEQ<-$ogx7_J`5`gѢEt͗///ϟϵ^pBbbDINچRג/PX((MRDZDjvfҡۀ `,\Mi(3<å^ؼ{55x8b8<EysAZ=) HQEQE9#%IHY TkP$=9ޤ|}UW]=Iu7~RJ>iӦ96=ijH cݱ]J^KL4)((JD4Nrd`tFC&8bpyxcǎdeeZucOtԉs=K/kp??dÆ ޽SNCHHsr s9rHT/q+|GX:˘3g]v ''SVTI|w\䈴[p[ַ*T\(EQrٴ4dH Hl(OlM7||Arss{0 ]\\޲ϤO9 NKC!)((mKk)F&LWs`ذa\veNfeeYM7Dff&SL!882 !::{1cT?tPpF , /… i׮]e#xgHMM{:uGy#GЧOnFǮnRC":eI:= +)OEQEQsBA G4EoH޽ԩ7x#O=~~~ח{YfCwy\quoQYPaСOWšn /4xCIJ×{*.EQEQhpĂȌv ;4[ܘu @Yzu8pT}Qglbcc?ۗ=_uaq1l**$񇙅LQTAEQjȍD ]؀[0QcǞݻ3wܳ*8:ujѬj&u|΅$x/PH((J&%(;sc_ivT  `08QvУGoa/.*嘟Fcp,OŤ((~MJn#-AT/߻Et1O h˷]q 786=J !tXz19,J((zNn2pX嘀Dr]h rKQWg6P(5$N,ib\sCHHHŷ~Ok-;Cs& kG,:};EQEQTs[:9I!K6˥Q55OI;s,\]2v}pCߟ1#:~='dsB־B˻[/vjwTq9O-OU~>KBPEQEQ#7勹cۄg]UT՜wyu/!7}yFb`-16뎺&R*+ino'vHl e^HNE5rb2ۤ-x($EQEQjMRQVO1Z <52 J?<Ďw`6оyn"Ы;z}n.i l|N ,}W_tk ֓ ؓy÷[P~va/ ]'H!e4_Q8px(9)"}kBv'FZHHXI6#e+5m _\itm O 6oQt0fI. mWIB|-ns`4 M: )9 ^ Xܖk4iҤ>>F?U`4j'ʴ"/nUc1c &Fg!i'KK#YYY ~1ch3ޠl6= M&[ }y01 >F>m6 NRѨZ%X,>n9o =]c=[yM0!Ɔ3rf^&xgIIIf7qA~Y!t=ON6-ѭt^LM6Cvv6&͋Q]Fsm}v q_qطo_ŷ? 5sB5th>*򘤈lΔ0?@/Ffj@v` r؋Hqtl9r0L `zh&>b3`bwBnZ_ٳgn]徫1@F-HZcмEӴQHD^|NLhcZ4bXuF5?4X'%YE}|6| OϷ:Y?tZ1|&nv5ZE#x_I1I 9p%_BĬ57ס$711 XVسf̘бk\q5-ߤj+Vnu] 3y&/U׹J+8[U{k垺rw=&uy\@k lRߔ*f;AnNʆ$5DEEL9FJ:6'jK)Jgedf뜖v@^*pb TweM0rFD<,c7xt|(:\ʁ$N fݻǏRXXf߿ݺu'$$vږD2FbC=DBB|jnr}Zف:&7Ύ49"j#u]fHj Kf~D/ %! A/G-RLȝ*G*sBaϧnI&4QFIB.V4mtZW/a@龗 `ꮭ"_ū85ᇙ'uԩ:JKTMտC1HHɜ)ˊ]|Mnw'  yAd˯L`Q Z 鎟Wm0\9xl!uͶ0NfA<rV^!Ľvt&%2CJYmSNC=KN#vÛN!jѯ_?rsFE|uXr{̞={(+Ƴ 8HLc+>RFˀ/,,+ 'VHA5UK[ur~pgUG6*pqI/L'^lׂ#afr}~BfMo:j%>][ ->]RAN | s "3¥~'lP!勔x'&G--O}VIM5Xy.n7\ Jyz,U.Ռ?l666]pɟ ,OR5\e"VbZU_6xyUK^b~OGՒbc\sRrQKbIPok576vr\ZL@/$ e\(% 5o2rgcݤxCFnwg:\Z2)%:'?o#s?k׾ڵc%6Z)6v&L0x˛1cBG9.;T!%ƉFlZd6{N^#=>R<^ =Ƌɼ%uMvh.4mCcW?C"+hMI{Bc-;:tȩh>,"dÆyݷNٓ뮻{O䩧婧("#U5RSXX_'̥'?7m6K,~9{紺&i6m/MRnI#uL | m ~!i7꺞>71,n@U%D-92MmB$K/yr!%޿p^iL,Y8`ww$`QB`JKO}јP2eJS߂HMi0!0Jɓy}Ր$-"6vBWyҋ^@Bz9v ڵol.[sD)ӱTW al6Oj lfшE&#ƷW %L%zQ^-ߟ cMG78pٰa<@󼴪ץ5>b4=_d޼qX IDAT.."@hh_BCɵ^CW[>|U޽yӧSXXXVZZ76nX9Ds0x~cLANk[4G;ap*eh@QAXOr~9O~ӚGq2~ ,aZ8tD=W=i>ᖿ !VM.nmBbb&_iQfzԨ&%fs WSN #5kz}EGZ\_ل 11wb@\ԙ 9LZ6|T2.[n!>>[nK/Ɣ. j]f<kػwOe۹qJV_ϧm)))]5Ӈ˗3l0~f͚Exx8ZSusP][[wS!~3)Dc!eچ[WKX#aK \XKH hsӼ eOx_ުΏXϷ؀}-`}V{Pt[Cay<#DWywu# ?:b<)/.x1&f^;@|+OLrA:ܣO|_W_e۶ڋ;1}&~fL}ÚD<$!;% ͱM q<Rq.Jgr;8-A"EJ%YJ B&2>9<6n] !:u:&QYz5ӧOg̘1L8y摓S >/UnBO3tW>7x>}DkskV }slSoTLmARXp!hS 䰴fmf\i>)oII'pfwJ䲿U6m䅡Ku\!ȤiPxE*bb wiI[91f3e%&uAxGI&+mv6&fcB{=G# )KTפ0hU7Eέ6pm'yXߗړMhDEۏ>*H:lTUoK.s:fƌ1nݺ1i$.]'|BVV?8ӇcDztR;#q=uH[!_̝g`OhBcl$wnu,\r;XT/0TN齄0 x99⑩JɃS}ۋρfJC&%"쉋' I]umXhGEm&B̘1\ae ztN2;Qr]9,_}Om&OGyq47kf*[TpVWM RIacZb/]k.hk#3;t-ldOL^6Q'&fe z" uSSQ})X`0@}f]N7vr&kٳ'&SvO㗌ْ_ӳr?V>vZ㜬ʯov8@dd$s z-8ѣGO֪ !>m~ˮdФ C|w\7B lp݋`vQoFHv|R5j+dAMJK$:B)4?t/0X`w5as'BJBkHY}__*^BoϏX9Swxӕ>/h m  "芤f/fmlbYWoJ٪J5foy kos@nlM3ZeَʲM0ۻhZ')e /^4G׸ot%w>U<=|Crkbh NT9pS~=E2EA~tّ΃+Ue˫wˎ;س^LJaÆi4Ν;3x֭sJnL=8pOJKKطtq'osxs>{(+ajoįcTQjn*jӄt_{*V9+cgmUsGZ|NZ2qqoc?_ _f,y+S}76@2$WྪM5pKo+ҭk$B'uG}dN4\Hy3p)M+66 KKK|' /)K{IcyԬQK)lbyRA|ءñ/_}_udeeEo>z4mڄ.=)j+9N;vi94Di.:"'ؘŕgBF)ˍY0X9!3}/Ӥ$ VlR2u̦ )^-hTfSZrX9=x ),#+!7.h %Eٹ1k4џ{eeZV$Bj-[`ڴI!63@M{hkx\24M[ ʭZp 2ePQGJbq6 x }WʲL~@Sw"zDd_]x[l ^E#B0&,NnaH4V/[iӦl2 ݸߧbʔ)S/_~J%z~9Zw|Aٳ[w@PPOzM^%c-KQQQN ˆ#Z|g&cɒ%۷oyEԥKjhЁAA՛^ hlv M47|;'ޅYmqVʆpq}DK'Eߌl8 3"qܘuq91w]صB5sB<!vU$PDX\]W%6{<.7f9Wuv ` XVޟVwbs:,Uyyww4xʤ|֗؜NK[ѨR.E#t)tk)YyOsJCV>Ħb)XVU{+*v4OJ9L}- LljHM]cHjGJN8N*G5ZԴЊ/vF f!~~t //{:LU+KC{yyvZњcX*G~t]g,['Ogbɒ%]U*q,HPZZCG/8vC UɌ/ݺQȠ!MޅӐNwrZ'$Mȉ}%Fp˙~(g8!x iKc pɾ/]+|}|8g9SBSԆsaZ-h\G9govy k)`-UY,$ߋgHK[(+}AV eՓ+Wu\c-]@zRWYA;C] xXyy,IXV,qAW:8=}bRN/^\J)wEɍڂk{Q֨rBs:fqINXw"LUOy;vaO69W-BйKGL&L n+Df]Vsg#`tB#)uJ$Q6?#4zMit-YOރj ؔKMwO!HtƔ)ai=]R㚮] WВm#6my{3k7L,*zC -i^j?B+goXd|Sol cVt6[m6m )!+&&EZʭ\8TJyϩS%fXVmn'++&fm mqJJ)[V6i<|w;oll2x饗X~=UWY`ݕ#6vUm'ѥs;u wO.d#FChh#ܹ76}8_9RFB|bk Ӕ4YmR'rr\nds̩Hm,f}>V}д$9qA_M;7 \qw$W3e Vw,q2˪-u˗uy'>-!,ew_BZʭG?!>YYY%YYYX,dK|]Z +,տl~~Qf3$~qU4}GnjZhFd((8CGؕ;;w߶q!V+>> l.5#Ρ_P#fqD>#EV\;wV%dbMy'NC=TTdo_]ٹs'RJn cǑR"?!!qF$lzCYYǎ|޻~#9j?[7T-r#]8Ф> x>!7COQ(7o./]їlVa7W!Hωmuuyx"!x\sG=4dCy7pUO'|ODs*KK_5#c:pRVkbY!`qE_Ra&Lpj2}-5`ЮsTʺO}n@ S [nD+*ZECή{P`[n 2. /<ҵkW*cֲ2N'o}[S^k^Oj]aa#n^f{ hԾMh||hVAMkU7+X,+2\ӛ JnB\⚋L(BCGnN|hΏ] E`{ѽ{7jAqQ~m;f]P956?3{s^|EΌ3;f>XV~m;?'k/e/A=ѿoI@N?i^ K# rj[&$f0#[֜ͱb 2 *e5WlĦ 5KY&^(F\ 6|V%a9Wz:+4LwCKZZ!.2&%V]ؿo}=`4ZrYxq!Ф.~rmz %Mgi)EƯDۀc]xɰ–z1zAԱ^^5-'ORZZuܭ?Kii^&Aݻһg4xh屫W$..o-''%KYs7W石xb>ʒҽ{&337gOFs{ {WFl?Rr=vҹ{[nt NmlhJTmwr_g.6)U !laEܖzt'=fޡ|DOs).M.2es]dx-[K׍\ݧ&WXR.v'Fn4q) IDATB]7> >NJ\QͳWer3k{ICRT]usV|=n>t|t]jc.:A LoofuB+3x뭷7o ^`֭;k3zʑ Bnf6l؀d䭷VU>ַ`NV&+۷#$]:idg<8t %%%4E=uj;kj[oX/54v{rd` Bu ifn5'{NXDG`$f_J6s.m{:zsDӼ^t.X 54=}u.0 wA8g!Ӝ5Mw]n"ht%MӛR2{.㦂Ǐs1V["vvhxۦ88sCsuܹs5k| BuF!Cj3f cƌlܸ#G@xx8C̛TYS0S AztCit܉>rSA`7A^7#OU<~ǭ>rSF!zmUO&30xlcѬNZA/lfIg-#wMy,!ERu!_]lֺ4@DG5򌌌/bmq 4)}]M['(hNEh)n}JVOHJiyoUD~'W+*@)*=n*y0V}]Mp ztlL&#]rCT}\Mz2|||k?ϟK~~>`˳~(<==ЮNT *񖑉ڰsMKwT!ˇw,u΃j]Y8I(ō*Ečh2UPk.\fEj X~Ϟ8i#5|=ne]=9sj5pE?Rr05uoU LׅRScփs UES2!0:Ix,%>8U|| w*tܸ+RJrGJ(T#j6F| o/xѿnfbcw 77_W_}AQQծ?Ç_Or v8y.p*C*B/PXTLII)!!AzҩYJڵ z5Tﳦ x5t(W6v,jK~s`R[U` ҚCT_pd_Q_P[Ý}tn$Et)iLZ:k57YUZ8-N$n8{!AkwMnQ ý?ͅ؎ES8pVEښSz)U":#Mjwq>;)*fßڕ=4R 4Z˧܂'A1|55dggq r)))d2ၧ'AAAmATTzLaQ1&b{ݝJsI#3 \E!&&ݻmf%G Թ͍!?%5tjKv@zTn.WtHACe$oVfGHqDߓ.a _ Poخ GdKVq=B-*=zILJ^)%ӒsnTuO,Ըz5%>wjč}צK.5:utN>.!SPXd&7/А`kM%p*k̽k͙{׾&B &^ 4dzb-RTm׊P$nҊ\Q |̩N[:b4[MAЄd{q7H;!Z/j<CF&VA##""x'o6v3ߡ( Ce# ,TkI!MX(O&'7<5! su7#V (78ҖRFP[uRH-;BVboUZo*hB4.B-n aCii4iD)[ == Zōj|]B{c&7wZ (< ldwxGE![Ww!ќh c)+ζs M +쎊XVw_k,s4) 6N%$%!|Lȋ9%rTPWxsN(1zbbb;v,X?UU)))al߾d<=e@Hp9ydf۬pA:ĉ'ԩ`*5(RTUaaaNtt4DFFb0˪haF"8&$;Aߪѫ cViU VTz9)Z$HR+yÅ:))q#(TMRmeKPw^ 'FJi]F+i +aY.B{Gll,O<qqq2Ǝ 5k8}4?#^}UkCLL ]vs΄*lEEEH)1呕ENNgΜ!==#GpZy;Dr4I𳻧$^kUqN !Dq]HuÐͱG4չtI1y՚ @e:: @ғK O\DbxM- B[! P,Rܼ6Ӏ'x#F4p ;ӓ'|)%6mgGm1?SO=֭!!!|\yzqF~W{f1\̮ SMտ5ԋw6!eGCB XbvjwOJ5]ma @heF˴Ǣ!U{ZdĈddd>aÆq/O?W1бcG&L#j;Pnfnfz!V^_k$Y6tW{\ {h) H^/]4[aV3}UU^IgWMP"Ѱ}# 77qDSR<|cqM:QieF qyttZ5S4/t/\2bbZ}iM2T"a 32Q\\̑#G0ʹmۖ(_7fti׮L2t֬Y_}\6~h5 6mZ:0uTƎ+‡~4t7g_5ۄ,"* *9pٸ{LqZGhtg&)J_z dƏ(of5[)KlS(mD[H&5M4#"=bԩeeZVAi\6):'>:867!;yhжm[̱囧on:,ר(Əwt8VC9v-~&Av=z43gd۶mdffūc=撱.\țoiީS'RRR֭Ko 3q_]^&V!lQƢrpBL .꽱tu s3d~z)mгgOyf۹9sPRb+ [n 0P{={l2 baѢE^nsε?+y7RO|t҅%Kcw>z4΍KoBg͢BWT=T<t#-UyPJYx¦SteCc= ,Z R<z#yc::-EZӖ,Yђ 45 G*W/p@'௕OxgQU[y޽{zj/_믿_ͽދ(!5jK/.ILL ˗/gʕ|W̚5^og߾}t~_͙3g5ٳgpO?t40^}Uڶmk?|M4YG⮀T=^H)/g낮5ݓv#9qyfZj*AI;bqJҫ]O 砣Hvpzʝ :zōWFzlVqK\Uz"`ۮM||<ҩS'^}U:w d$''3sLƌ㢩 ^[n7ߤG1cxB0efΜoTU/n!'EEEK/㚋j"ڶm /P}'rL*ᰊ/?mrfSy3x~| YW?NO~ <6SKTk@/ݔVÆȮI;E>גC,-Q::-!F AE܃M&z΍7cƯcJǧ!a`1!hO 'vΝ;Jvvr- ȑ#ݻ7](Fo6]~= c>aF[:([C!q]."8~_`D1~B!i.G%K~xh{RGsC\ї*'9O{fk]vOkpɒ2HXo>wRvcΔOBΑoB$US?M@r*O!Jq7JapUWHAAgvvtKY1?[bHBB`yg8}bFPP'Ofܸq&߿}yxxpmqn:~Rߴjdhԕ䐬( 2[^e$Hp!U?&SyM [eﬣ]0 O)Ļ.]bX$QH8YnwNZ: "qZLbJiEtt".--bE{ҤCS?NrQ~ɗ.uUJ1$\oY+ 6>~&S >>o<ȑ/2SLR2gI~}t?GJN~ 'w:5TU2ӦMÃ?_~<,,c1uTк9u>(&=bȑ̙3ӧӿ{COsUGYҵtnBj 򹄂ZJFK㖸=-%a.N*|fuk.\4)đ^쵺%KHRJU/$i KÚk|bm. KzxRƟ*=^JùBE)wᔸ箻3wW-iPe[F}OqL "zW|̖y1evclCr;LI9ì"r}kr:66믿UU裪+?z;Sj*&N@BBS>C̜9!Ѽ</aĉ,Zv0`{Pqo^l}AoFdU_ŋⓔ%ߴiH_,$C7$<ׇ4 !pG2e^ `> ŤyK Kz::C ., e3i G6xZM8];Bυd(s#IEvѮZ"N|C@/c߾Np9 ״B+{ROoftY +H} ߔKR|a˅2,Aڶ#3`5^|asСNBV_F|ij (Mk.Zs~RRRr\Pӧ5\&+: rSM7DAA#..QFrJ_=H9=x"Ū3fJs U>Eů#Ť[;.I./{hL0`vW sy KG ȍD>=g4 +6l??n+R9@>4`y=H[:)?XGCk P9z/W^mu՜!9yQR W.M8-n)X퐖K*TZ6ӝ˦$?SfӳgOҋro19(>i/J{9p`?'Obq 7 oI׮+8|JKVA^dI0ҡ*4W79/ce{-+yN/8܄!)KT[pRQG]᜴]ף*/)K0>K噽"l\(%\lVr``ܝtvS W2?>56%~^C6:1F/ OJL)5܅dI!޽[~߭[7rdK~d+VgNXRa:36 k׎=zѳg/ڷ`\\\ķ~͏?~jж?b檱UkO\[|#R." ӜTPñ):fgM=96^ 78^Skb*q{pz ButԿpɒ`><-dg$bɬ_aWbP@`S\k0?.uhJ\R?@"ޞ+(-e 6 Ey>YGw/+EKDϏO}MB]կNkeѢE% jCUY8^(k }#\ @J΍pK^ d @Z#Sܰ4CxrNswpױsNz*o5nuֲcGeq`` 7£Y|cl|Wǵ sKNG+= ѷq9NSJs%Fke/)FUr&YU%(H+ M|/ӌx֞RIAN(raK!, ))"Ba$0NX 3K)A!W1( z&v@ٱc+P=yZPV`uvMݻ7ar }#U8Yќl9LFiJJJ0Lx{{GDD;^%$9mUs%c'D^jٳguqXA#98AQ=.kNڄG[c1*'eLo6*P$I2%_p[C?#{I-]-ʟ{ЭZ[o< ѹQT о oOPY+vQ#c[q4 716t^q{:Ҧ$F?}ġEѣb0)*Я_?Nƍǀ lg!;n^ơi7 t&g2\S lqj>Qq@qOOOY5[ȊuĜ]EXLk{ye:r)rss8}\キÇ7V2Vtñǘ1c SP`7s?X@ 6B qdfTM^ԬJ;ǬlL+1yv5טnBJ!fޕ9vWUȑ8ٴfQ|.siۅR&ѩ'S Xp@iphhdd ^ }w WF_|ن!)ݡ2@l>f bԨQՋݻ3bj'fv[N.kg[`0[L*736mc- ם6jwTTTTT $""FӶ߾ܣ5^۷oeg9kQ-Sg}_|JVVfs#nW*'ⲚUU"-/+v9qv9;t3&yЬE ic\S?.( Xc7!gIϻcv&*W hXANc>wODGXt s<4d)[EJ]^bf SS!P+Pt/(//CdV wEi0 6о}{vǏU**kt֝3glDw$4шhR^zs)&?l`? ,CJ?ƍUQnʆZJ)(z_RЦM[{VV&;vm[#Z.#GގjYۼy{~ԛoI~m 2ΉϏbUꎠ_a?B;]]I>*ri2+mmOK[u(Y,a )z0gwb\H !r]I{"::Mqygw@~,UBKs 1[=n/%ǭmt胟Ǩ4$RH~8]`*~RbZZUL&#f[ݥS0Y1pXPAA F>cBNEbPZAQEn{oM(}|U"efee?U[AD:䚤z_uad燐̔pdA!vޑم믿?i$ULDDkn\~O曯RRPg}LЪ<]_dx޽+WVLUů*gȧMXvQy/7鳬xmih?:uȮI͋K"o5g !Ż >eZq1e9q~\ Ԭۺ،b04<3i܄R֕'&bo{"::`ե'N]Qk}!%4A&hP]4X?.+%.;srOCƼOHFv PL|]m@yMk2Sa\{>9b*T@(4[P ׍oX*?r-wzӘժ?Aoڋzyysӧ%߳f222keذay8z`2r<+o?0+n[!c9;79'1p[S^IK\6?.5] [!kR'Rqo*r$5[>xT>cL ytCK( Ȇ4{gcsQlٲccx#-/w];qxM4N 68>m%EIi)N:ڦ a(D{<owgz\1\;H#͆ իWӫW/ywȰEedd|rٳ'> }@Ql[y/qHO4~9[ۏϡM9WrN^,F RϷi"f%n0rw@<{g6ՙ6yOi`'Pm E E+䴖%l*I>dsT\R{HWel=MGX:w<}w}7Fc7čTwsbʫʋ-;̃vƫ=wFMO6M\s}^ba̙;< , ?0eeeӗk)$=aLMfjl nP7_:YI"%6 @SK;_.P}#ĜҰ΃fݓg?E K}縑R2iw$8Ü]{HKO-SQ.e/t=;-..D u七"OM]PrZ}WKܸF%[`J(%CV^e8׆U"R 8 Si^^^~h0Aw), ᴹևvd<<<56o̱cgҮ];2228x  [Hddݭ."."4ޟsSLYX Ʒii;qqq sN{?]taЪ͵"""̴[aرaÆG||[nAJI6<73哽Sv9Qɇ˃[BTί `5>QT{|u1I'{-jPDvts@ R=kmOk1GwN>YuJ-x` ]sUEJZc7 Ll۽-(hfA`֮[::Αb0hҤ w 4'AT^.X%E(踈F)"W$#++8GԠKgl< t).#ٜP*ªe.Ui-礥ѩS' Z ٲej߾p.]ٺuCð" J,r ___BCCfz̩ P- ifI:L_u%>ֿ*R*\3U>歏'` S[0/nY'E%rpPJ6NHw ?4m$PluHzϲUކ@^ x|0A\֣i;0$s#mp`^T`$?"S>RUߏ-j[Tm1tju-!čk.ՅKt:8u|lvyyyɩ{ d2UW]E=طoV?ÇsWO?9nݺq5`2%/ڷRS^^NNvS;tP)n*F v;1_,L^/TÐ8! أ SQ~)h6R>Y/XKHA 8iq!{Wc‰&#;&@<IDAT+K} z*&l4E)6X_s/ǫDi*iL!.%q|\L{!ZnQY!D72QqVWO:գt !/q) ;ea)٤(bBӶիW7(`RFVMb/hhU9/!J8:{פh-xࡇbm BUU #77"ݳG׿N`͚Xz5Çg„ _r(y.$99+WvZ&NW_ŊJ xzzLhH0!!<{=k' RSU[\R\6;mΞt6Rk|MY]xBg(W#@Bb2(JHzB=p5K}Ϊ4#e/EJUJUZKQ2c)=,~ʧPED*T&̋j(rdב$]LGG)'moB:, %BP&R(rSgLwn|ME[JM؋)}G]l $3;7&2K.%?! LQ|} g3طoǎcٲe5kCpB}ݼ|G,_WU($8@JKM$3狗x{՚CtuFhPuz^i)>zn fږiwBB:7IM< )vViȷ~p У7tfDŽj ]DW>KܘɱY! 88А<=>}3Ui۷ox{{`JKKYtI&ꫯb4i׮fپ}7>ۇҊ\).. !^_5Ɗ8xdsK2h@ a7-F7ucY= E%xѶmBBB0m]23{#7/ 0dggSRRbo.c͜9saT[/T+wf<==  5DEur1KJ1̔qv±x'HZGGGGGGGGh76TOHgݴFϏZyyyҮm8>>F  TU;vUUBL\\nK]wp>L>s}}}ɓ3$L&39LfhwUOOOZsͥu+?*i߾=z'Ϛ5~ݻwB///@#/<  &8]ʹH6" luq*9]wߖMGGGGGGGGG[΍ TTPZ/KMso!m{W枯QeaV)s}3򊼳*f^B:kstkcǎr; v]mºukՄtwjQD?j[ *◙é,bGq-'wMmKԲkhFx+_1xW-1)wJmtttttttttEBnRUueHE [%p7wmdl2ĢF0xXTkGu Ǔ6MibODD;{MFΖ׫Z#a!-x[}{t]ACƱcA0{0z@/ީS?wn,l~vpk>z:7 N+H`!#&kx`RB7mZVTTDQwwQgnDR') vNqH[&qC HQ|TbNEM;LcP ,4c,H[(s^mcلIy~wI=g.x~= goGk܀QZθp M̞sQm.T*Pwg0x熈u)>Q SᗥI>}2 ^{41Aku-N]uRB16tiSp[m͙F*v} 28L><‘ov[oM H&"""=7P? 2fFBV&aKR!6Dy&BՒ'.mi$]Z=Ή1$5L,B:y==:DDDDDŤH+7 bqXyw{5O{MDDDD& 0 |ϟQkӨQUw=6DDDD4 1sf^~H2:SEecр΍?)"Ax\u6޲XE' roŠ̺[zt>Dy, q}L<lnJ֤fǜG[ȯd1rC9F( YZN@cef\f ux{RкPkl3fo[F@V僿|;8#f [wm{B}&Kt,Cph7PPgic>6c o2䨓(qĴ܄]:@`kc5n5f6 oR_S7'aLA} nX .Nq)7a-v ݏ$j@ٰ$;Yۨ`޾xضTbیuWY(aŵ܄9M_ "k"f =x?@HBXI#45ۆ:C46޲z3cYB+㒊|DDDDbz̪R]`^#l=!f8ɭY fQ-+}`6d&[?_Ȏ0v\ [[/Z;'ڝF6Rr).gfIUx 冈oC^n*5UB@ R=U-d>V{a>xl>yM\rpy',7DDDD}6&|fW 9ՅG9oUiYZ:ugၜh8c!"""۰+7aek2DǞ.؍.3k?\ѣܻ6>/8!"[ؠ3Xn6lMXe\c3HƺUE-N䚷l !ۜXk0ZX|nX'<9ek2ܦ1m7-i/ >n,3 þ܄)Z tR;ȱx晿P/]nu+x8Sgz9DDDDD3bMқ-hSl,\xHrsh0vHD7@4DDDDD3Mېօ"X( Pn*om!ߍ0!Q,rHJ`fFU o&+:b+Ӿ?)&Q̌r OM4옳E{"6c m2-:_GDDDD)7= pS?R~ p͘`%:{xDDDDD1&lHUkrK[wRKy}Q<%t +Ϫ5}fs@OrcCy_:/w]-fbZ닏}d>""""x&,kTC~ fQ-+}\mQwm-8(r5=/'Ө*7aޚx"ٌ} ` wp/n=%8kl.؍.3k5""""lTLXϝN@ן'սv{`V=d>"""""'rcdasX #$+3>{妇icb KdmڛTQ p힪r2PbCe`3"6j$/yz ITNYx -y#)"Fbԙi漞cj olc5?@Er3@YuZgvHT ߹'4صʽE,xEDDDD(XnPv? r{ ,?ߏ0֤MKDDDDD4ұ ҥG sܹvת̵̪ׄ_QS}ܷѥ'""""J<,7Q*<̀Y fgQ:~CiDX‘C F:U˲H Pm(Il_aY^%+n\-~gֳMKޏC<""""r'Uޗw3n^KuCܠ M cҤ-߂ 0׏y Fה<$ˍCކ.I2knUpQPg#""""J2goIENDB`podman-py-5.4.0.1/docs/source/_templates/000077500000000000000000000000001475540541200201575ustar00rootroot00000000000000podman-py-5.4.0.1/docs/source/_templates/apidoc/000077500000000000000000000000001475540541200214165ustar00rootroot00000000000000podman-py-5.4.0.1/docs/source/_templates/apidoc/module.rst_t000066400000000000000000000006631475540541200237650ustar00rootroot00000000000000{%- if show_headings %} {%- if "podman.errors" in basename %} {{- basename | replace("podman.errors.", "") | e | heading }} {% elif "podman.client" in basename -%} {{- basename | replace("podman.client", "client") | e | heading }} {% else -%} {{- basename | replace("podman.domain.", "") | e | heading }} {% endif -%} {% endif -%} .. automodule:: {{ qualname }} {%- for option in automodule_options %} :{{ option }}: {%- endfor %} podman-py-5.4.0.1/docs/source/_templates/apidoc/package.rst_t000066400000000000000000000021241475540541200240650ustar00rootroot00000000000000{%- macro automodule(modname, options) -%} .. automodule:: {{ modname }} {%- for option in options %} :{{ option }}: {%- endfor %} {%- endmacro %} {%- macro toctree(docnames) -%} .. toctree:: :maxdepth: {{ maxdepth }} {% for docname in docnames %} {{ docname }} {%- endfor %} {%- endmacro %} {%- if is_namespace %} {{- [pkgname, "namespace"] | join(" ") | e | heading }} {% else %} {{- [pkgname, "package"] | join(" ") | e | heading }} {% endif %} {%- if modulefirst and not is_namespace %} {{ automodule(pkgname, automodule_options) }} {% endif %} {%- if subpackages %} Subpackages ----------- {{ toctree(subpackages) }} {% endif %} {%- if submodules %} Submodules ---------- {% if separatemodules %} {{ toctree(submodules) }} {% else %} {%- for submodule in submodules %} {% if show_headings %} {{- [submodule, "module"] | join(" ") | e | heading(2) }} {% endif %} {{ automodule(submodule, automodule_options) }} {% endfor %} {%- endif %} {%- endif %} {%- if not modulefirst and not is_namespace %} Module contents --------------- {{ automodule(pkgname, automodule_options) }} {% endif %} podman-py-5.4.0.1/docs/source/_templates/apidoc/toc.rst_t000066400000000000000000000001771475540541200232650ustar00rootroot00000000000000{{ header | heading }} .. toctree:: :maxdepth: {{ maxdepth }} {% for docname in docnames %} {{ docname }} {%- endfor %} podman-py-5.4.0.1/docs/source/conf.py000066400000000000000000000107431475540541200173260ustar00rootroot00000000000000# Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # import os import sys from sphinx.domains.python import PythonDomain sys.path.insert(0, os.path.abspath('../..')) # -- Project information ----------------------------------------------------- project = 'Podman Python SDK' copyright = '2021, Red Hat Inc' author = 'Red Hat Inc' # The full version, including alpha/beta/rc tags version = '3.2.1.0' release = version add_module_names = False # -- 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. # sphinx.ext.autodoc: Include documentation from docstrings # sphinx.ext.napoleon: Support for NumPy and Google style docstrings # sphinx.ext.viewcode: Add links to highlighted source code # isort: unique-list extensions = ['sphinx.ext.napoleon', 'sphinx.ext.autodoc', 'sphinx.ext.viewcode'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. # isort: unique-list exclude_patterns = [ 'podman.api.*rst', 'podman.rst', 'podman.version.rst', 'podman.tlsconfig.rst', 'podman.errors.rst', 'podman.domain.rst', ] # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = 'alabaster' html_favicon = '_static/podman-logo.ico' html_theme_options = { 'description': 'Develop scripted Podman operations', 'fixed_sidebar': False, 'github_banner': True, 'github_repo': 'podman-py', 'github_user': 'containers', 'logo': "podman-logo.png", 'logo_name': True, 'show_powered_by': False, 'extra_nav_links': { 'Report PodmanPy Issue': 'https://github.com/containers/podman-py/issues', 'Podman Reference': 'https://docs.podman.io', 'Podman on github': 'https://github.com/containers/podman', }, } html_sidebars = { '**': [ 'about.html', 'navigation.html', 'relations.html', 'searchbox.html', ] } # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # -- autoclass settings ------------------------------------------------------ s autodoc_member_order = "groupwise" autodoc_default_options = { 'members': True, 'inherited-members': True, 'show-inheritance': True, } autoclass_content = "both" # -- Napoleon settings ------------------------------------------------------ s napoleon_google_docstring = True napoleon_numpy_docstring = False napoleon_include_init_with_doc = False napoleon_include_private_with_doc = False napoleon_include_special_with_doc = False napoleon_use_admonition_for_examples = True napoleon_use_admonition_for_notes = True napoleon_use_admonition_for_references = False napoleon_use_ivar = False napoleon_use_param = True napoleon_use_rtype = True napoleon_preprocess_types = False napoleon_type_aliases = None napoleon_attr_annotations = True class PatchedPythonDomain(PythonDomain): def resolve_xref(self, env, fromdocname, builder, typ, target, node, contnode): if 'refspecific' in node: del node['refspecific'] return super().resolve_xref(env, fromdocname, builder, typ, target, node, contnode) def skip(app, what, name, obj, would_skip, options): # isort: unique-list cls = ['ApiConnection', 'DockerClient', 'DockerException'] if name in cls: return True return None def setup(app): app.connect("autodoc-skip-member", skip) app.add_domain(PatchedPythonDomain, override=True) podman-py-5.4.0.1/docs/source/index.rst000066400000000000000000000037251475540541200176720ustar00rootroot00000000000000Podman: Python scripting for Podman services ============================================== .. image:: https://img.shields.io/pypi/l/podman.svg :target: https://pypi.org/project/podman/ .. image:: https://img.shields.io/pypi/wheel/podman.svg :target: https://pypi.org/project/podman/ .. image:: https://img.shields.io/pypi/pyversions/podman.svg :target: https://pypi.org/project/podman/ PodmanPy is a Python3 module that allows you to write Python scripts that access resources maintained by a Podman service. It leverages the Podman service RESTful API. Podman services are addressed using a URL where the scheme signals to the client how to connect to service. Supported schemes are: ``http+ssh``, ``http+unix`` or ``tcp``. Formats are the following styles: - ``http+ssh://[@][:]/`` - ``http+ssh://alice@api.example:22/run/user/1000/podman/podman.sock`` - The scheme ``ssh`` is excepted as an alias - ``http+unix://`` - ``http+unix:///run/podman/podman.sock`` - The scheme ``unix`` is excepted as an alias - ``tcp://:`` - ``tcp://api.example:8888`` Example ------- .. code-block:: python :linenos: import podman with podman.PodmanClient() as client: if client.ping(): images = client.images.list() for image in images: print(image.id) .. toctree:: :caption: Podman Client :hidden: podman.client .. toctree:: :caption: Podman Entities :glob: :hidden: podman.domain.config podman.domain.containers* podman.domain.images* podman.domain.ipam podman.domain.events podman.domain.manager podman.domain.manifests podman.domain.networks* podman.domain.pods* podman.domain.registry_data podman.domain.secrets podman.domain.system podman.domain.volumes podman.errors.exceptions Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` podman-py-5.4.0.1/hack/000077500000000000000000000000001475540541200145005ustar00rootroot00000000000000podman-py-5.4.0.1/hack/get_ci_vm.sh000077500000000000000000000036611475540541200170010ustar00rootroot00000000000000#!/usr/bin/env bash # # For help and usage information, simply execute the script w/o any arguments. # # This script is intended to be run by Red Hat podman-py developers who need # to debug problems specifically related to Cirrus-CI automated testing. # It requires that you have been granted prior access to create VMs in # google-cloud. For non-Red Hat contributors, VMs are available as-needed, # with supervision upon request. set -e SCRIPT_FILEPATH=$(realpath "${BASH_SOURCE[0]}") SCRIPT_DIRPATH=$(dirname "$SCRIPT_FILEPATH") REPO_DIRPATH=$(realpath "$SCRIPT_DIRPATH/../") # Help detect if we were called by get_ci_vm container GET_CI_VM="${GET_CI_VM:-0}" in_get_ci_vm() { if ((GET_CI_VM==0)); then echo "Error: $1 is not intended for use in this context" exit 2 fi } # get_ci_vm APIv1 container entrypoint calls into this script # to obtain required repo. specific configuration options. if [[ "$1" == "--config" ]]; then in_get_ci_vm "$1" cat < /dev/stderr echo 'PATH=$PATH:$GOPATH/bin' > /etc/ci_environment make else # Create and access VM for specified Cirrus-CI task mkdir -p $HOME/.config/gcloud/ssh podman run -it --rm \ --tz=local \ -e NAME="$USER" \ -e SRCDIR=/src \ -e GCLOUD_ZONE="$GCLOUD_ZONE" \ -e DEBUG="${DEBUG:-0}" \ -v $REPO_DIRPATH:/src:O \ -v $HOME/.config/gcloud:/root/.config/gcloud:z \ -v $HOME/.config/gcloud/ssh:/root/.ssh:z \ quay.io/libpod/get_ci_vm:latest "$@" fi podman-py-5.4.0.1/mkdocs.yml000066400000000000000000000000231475540541200155700ustar00rootroot00000000000000site_name: My Docs podman-py-5.4.0.1/plans/000077500000000000000000000000001475540541200147075ustar00rootroot00000000000000podman-py-5.4.0.1/plans/main.fmf000066400000000000000000000031201475540541200163210ustar00rootroot00000000000000summary: Run Python Podman Tests discover: how: fmf execute: how: tmt prepare: - name: pkg dependencies how: install package: - make - podman - python3-pip - name: pip dependencies how: shell script: - pip3 install .[test] - name: ssh configuration how: shell script: - ssh-keygen -t ecdsa -b 521 -f /root/.ssh/id_ecdsa -P "" - cp /root/.ssh/authorized_keys /root/.ssh/authorized_keys% - cat /root/.ssh/id_ecdsa.pub >>/root/.ssh/authorized_keys /upstream: /sanity: summary: Run Sanity and Coverage checks on Python Podman discover+: # we want to change this to tag:stable once all the coverage tests are fixed filter: tag:lint /base: summary: Run Python Podman Tests on Upstream PRs for base Python discover+: filter: tag:base /all: summary: Run Python Podman Tests on Upstream PRs for all Python versions prepare+: - name: install all python versions how: install package: - python3.9 - python3.10 - python3.11 - python3.12 - python3.13 discover+: filter: tag:matrix adjust+: enabled: false when: initiator is not defined or initiator != packit /downstream: /all: summary: Run Python Podman Tests on bodhi / errata and dist-git PRs discover+: filter: tag:matrix adjust+: enabled: false when: initiator == packit podman-py-5.4.0.1/podman.svg000066400000000000000000002445111475540541200156000ustar00rootroot00000000000000 G podman podman podman_api podman.api podman_client podman.client podman_api->podman_client podman_domain_config podman. domain. config podman_api->podman_domain_config podman_domain_containers podman. domain. containers podman_api->podman_domain_containers podman_domain_containers_create podman. domain. containers_create podman_api->podman_domain_containers_create podman_domain_containers_manager podman. domain. containers_manager podman_api->podman_domain_containers_manager podman_domain_events podman. domain. events podman_api->podman_domain_events podman_domain_images podman. domain. images podman_api->podman_domain_images podman_domain_images_build podman. domain. images_build podman_api->podman_domain_images_build podman_domain_images_manager podman. domain. images_manager podman_api->podman_domain_images_manager podman_domain_manager podman. domain. manager podman_api->podman_domain_manager podman_domain_manifests podman. domain. manifests podman_api->podman_domain_manifests podman_domain_networks_manager podman. domain. networks_manager podman_api->podman_domain_networks_manager podman_domain_pods_manager podman. domain. pods_manager podman_api->podman_domain_pods_manager podman_domain_registry_data podman. domain. registry_data podman_api->podman_domain_registry_data podman_domain_secrets podman. domain. secrets podman_api->podman_domain_secrets podman_domain_system podman. domain. system podman_api->podman_domain_system podman_domain_volumes podman. domain. volumes podman_api->podman_domain_volumes podman_errors_exceptions podman. errors. exceptions podman_api->podman_errors_exceptions podman_api_adapter_utils podman. api. adapter_utils podman_api_ssh podman.api.ssh podman_api_adapter_utils->podman_api_ssh podman_api_uds podman.api.uds podman_api_adapter_utils->podman_api_uds podman_api_cached_property podman. api. cached_property podman_api_cached_property->podman_api podman_api_cached_property->podman_client podman_api_cached_property->podman_domain_config podman_api_client podman. api. client podman_api_client->podman_api podman_api_client->podman_client podman_api_client->podman_domain_events podman_api_client->podman_domain_manager podman_api_client->podman_domain_system podman_api_client->podman_errors_exceptions podman_api_http_utils podman. api. http_utils podman_api_http_utils->podman_api podman_api_http_utils->podman_domain_networks_manager podman_api_parse_utils podman. api. parse_utils podman_api_parse_utils->podman_api podman_api_ssh->podman_api_client podman_api_tar_utils podman. api. tar_utils podman_api_tar_utils->podman_api podman_api_uds->podman_api_client podman_api_connection podman. api_connection podman_api_connection->podman podman_client->podman podman_containers podman. containers podman_containers->podman_api_connection podman_domain podman.domain podman_domain->podman_client podman_domain->podman_errors_exceptions podman_domain_config->podman_client podman_domain_containers->podman_domain_containers_create podman_domain_containers->podman_domain_containers_manager podman_domain_containers_run podman. domain. containers_run podman_domain_containers->podman_domain_containers_run podman_domain_networks podman. domain. networks podman_domain_containers->podman_domain_networks podman_domain_containers->podman_errors_exceptions podman_domain_containers_create->podman_domain_containers_manager podman_domain_containers_manager->podman_client podman_domain_containers_manager->podman_domain_networks podman_domain_containers_run->podman_domain_containers_manager podman_domain_events->podman_client podman_domain_images->podman_domain_containers podman_domain_images->podman_domain_containers_create podman_domain_images->podman_domain_containers_run podman_domain_images->podman_domain_images_build podman_domain_images->podman_domain_images_manager podman_domain_images->podman_domain_manifests podman_domain_images->podman_domain_registry_data podman_domain_images_build->podman_domain_images_manager podman_domain_images_manager->podman_client podman_domain_images_manager->podman_domain_containers podman_domain_manager->podman_domain_containers podman_domain_manager->podman_domain_containers_manager podman_domain_manager->podman_domain_images podman_domain_manager->podman_domain_images_manager podman_domain_manager->podman_domain_manifests podman_domain_manager->podman_domain_networks podman_domain_manager->podman_domain_networks_manager podman_domain_pods podman. domain. pods podman_domain_manager->podman_domain_pods podman_domain_manager->podman_domain_pods_manager podman_domain_manager->podman_domain_registry_data podman_domain_manager->podman_domain_secrets podman_domain_manager->podman_domain_volumes podman_domain_manifests->podman_client podman_domain_networks->podman_domain_networks_manager podman_domain_networks_manager->podman_client podman_domain_pods->podman_domain_containers_create podman_domain_pods->podman_domain_pods_manager podman_domain_pods_manager->podman_client podman_domain_registry_data->podman_domain_images_manager podman_domain_secrets->podman_client podman_domain_system->podman_client podman_domain_volumes->podman_client podman_errors podman.errors podman_errors->podman_api_client podman_errors->podman_api_uds podman_errors->podman_api_connection podman_errors->podman_containers podman_errors->podman_domain_containers podman_errors->podman_domain_containers_create podman_errors->podman_domain_containers_manager podman_errors->podman_domain_containers_run podman_errors->podman_domain_images podman_errors->podman_domain_images_build podman_errors->podman_domain_images_manager podman_errors->podman_domain_manifests podman_errors->podman_domain_networks_manager podman_errors->podman_domain_pods_manager podman_errors->podman_domain_registry_data podman_errors->podman_domain_volumes podman_images podman.images podman_errors->podman_images podman_manifests podman. manifests podman_errors->podman_manifests podman_networks podman. networks podman_errors->podman_networks podman_pods podman.pods podman_errors->podman_pods podman_system podman.system podman_errors->podman_system podman_errors_exceptions->podman_errors podman_images->podman_api_connection podman_system->podman_api_connection podman_tlsconfig podman. tlsconfig podman_tlsconfig->podman_api_client podman_version podman.version podman_version->podman podman_version->podman_api podman_version->podman_api_client requests requests requests->podman_api_client requests->podman_api_parse_utils requests->podman_api_ssh requests->podman_api_uds requests->podman_domain_containers requests->podman_domain_images_manager requests->podman_domain_volumes requests->podman_errors_exceptions toml toml toml->podman_domain_config typingextensions typingextensions typingextensions->podman_api urllib3 urllib3 urllib3->podman_api_ssh urllib3->podman_api_uds urllib3->requests xdg xdg xdg->podman_api_ssh xdg->podman_client xdg->podman_domain_config podman-py-5.4.0.1/podman/000077500000000000000000000000001475540541200150505ustar00rootroot00000000000000podman-py-5.4.0.1/podman/__init__.py000066400000000000000000000003011475540541200171530ustar00rootroot00000000000000"""Podman client module.""" from podman.client import PodmanClient, from_env from podman.version import __version__ # isort: unique-list __all__ = ['PodmanClient', '__version__', 'from_env'] podman-py-5.4.0.1/podman/api/000077500000000000000000000000001475540541200156215ustar00rootroot00000000000000podman-py-5.4.0.1/podman/api/__init__.py000066400000000000000000000017301475540541200177330ustar00rootroot00000000000000"""Tools for connecting to a Podman service.""" from podman.api.cached_property import cached_property from podman.api.client import APIClient from podman.api.api_versions import VERSION, COMPATIBLE_VERSION from podman.api.http_utils import prepare_body, prepare_filters from podman.api.parse_utils import ( decode_header, frames, parse_repository, prepare_cidr, prepare_timestamp, stream_frames, stream_helper, ) from podman.api.tar_utils import create_tar, prepare_containerfile, prepare_containerignore DEFAULT_CHUNK_SIZE = 2 * 1024 * 1024 # isort: unique-list __all__ = [ 'APIClient', 'COMPATIBLE_VERSION', 'DEFAULT_CHUNK_SIZE', 'VERSION', 'cached_property', 'create_tar', 'decode_header', 'frames', 'parse_repository', 'prepare_body', 'prepare_cidr', 'prepare_containerfile', 'prepare_containerignore', 'prepare_filters', 'prepare_timestamp', 'stream_frames', 'stream_helper', ] podman-py-5.4.0.1/podman/api/adapter_utils.py000066400000000000000000000037301475540541200210360ustar00rootroot00000000000000"""Utility functions for working with Adapters.""" from typing import NamedTuple from collections.abc import Mapping def _key_normalizer(key_class: NamedTuple, request_context: Mapping) -> Mapping: """Create a pool key out of a request context dictionary. According to RFC 3986, both the scheme and host are case-insensitive. Therefore, this function normalizes both before constructing the pool key for an HTTPS request. If you wish to change this behaviour, provide alternate callables to ``key_fn_by_scheme``. Copied from urllib3.poolmanager._default_key_normalizer. Args: key_class: The class to use when constructing the key. This should be a namedtuple with the scheme and host keys at a minimum. request_context: An object that contain the context for a request. Returns: A namedtuple that can be used as a connection pool key. """ # Since we mutate the dictionary, make a copy first context = request_context.copy() context["scheme"] = context["scheme"].lower() context["host"] = context["host"].lower() # These are both dictionaries and need to be transformed into frozensets for key in ("headers", "_proxy_headers", "_socks_options"): if key in context and context[key] is not None: context[key] = frozenset(context[key].items()) # The socket_options key may be a list and needs to be transformed into a # tuple. socket_opts = context.get("socket_options") if socket_opts is not None: context["socket_options"] = tuple(socket_opts) # Map the kwargs to the names in the namedtuple - this is necessary since # namedtuples can't have fields starting with '_'. for key in list(context.keys()): context["key_" + key] = context.pop(key) # Default to ``None`` for keys missing from the context for field in key_class._fields: if field not in context: context[field] = None return key_class(**context) podman-py-5.4.0.1/podman/api/api_versions.py000066400000000000000000000010401475540541200206670ustar00rootroot00000000000000"""Constants API versions""" import re from .. import version def _api_version(release: str, significant: int = 3) -> str: """Return API version removing any additional identifiers from the release version. This is a simple lexicographical parsing, no semantics are applied, e.g. semver checking. """ items = re.split(r"\.|-|\+", release) parts = items[0:significant] return ".".join(parts) VERSION: str = _api_version(version.__version__) COMPATIBLE_VERSION: str = _api_version(version.__compatible_version__, 2) podman-py-5.4.0.1/podman/api/cached_property.py000066400000000000000000000004011475540541200213410ustar00rootroot00000000000000"""Provide cached_property for Python <=3.8 programs.""" import functools try: from functools import cached_property # pylint: disable=unused-import except ImportError: def cached_property(fn): return property(functools.lru_cache()(fn)) podman-py-5.4.0.1/podman/api/client.py000066400000000000000000000364671475540541200174710ustar00rootroot00000000000000"""APIClient for connecting to Podman service.""" import json import warnings import urllib.parse from typing import ( Any, ClassVar, IO, Optional, Union, ) from collections.abc import Iterable, Mapping import requests from requests.adapters import HTTPAdapter from podman.api.api_versions import VERSION, COMPATIBLE_VERSION from podman.api.ssh import SSHAdapter from podman.api.uds import UDSAdapter from podman.errors import APIError, NotFound from podman.tlsconfig import TLSConfig from podman.version import __version__ _Data = Union[ None, str, bytes, Mapping[str, Any], Iterable[tuple[str, Optional[str]]], IO, ] """Type alias for request data parameter.""" _Timeout = Union[None, float, tuple[float, float], tuple[float, None]] """Type alias for request timeout parameter.""" class ParameterDeprecationWarning(DeprecationWarning): """ Custom DeprecationWarning for deprecated parameters. """ # Make the ParameterDeprecationWarning visible for user. warnings.simplefilter('always', ParameterDeprecationWarning) class APIResponse: """APIResponse proxy requests.Response objects. Override raise_for_status() to implement Podman API binding errors. All other methods and attributes forwarded to original Response. """ def __init__(self, response: requests.Response): """Initialize APIResponse. Args: response: the requests.Response to provide implementation """ self._response = response def __getattr__(self, item: str): """Forward any query for an attribute not defined in this proxy class to wrapped class.""" return getattr(self._response, item) def raise_for_status(self, not_found: type[APIError] = NotFound) -> None: """Raises exception when Podman service reports one.""" if self.status_code < 400: return try: body = self.json() cause = body["cause"] message = body["message"] except (json.decoder.JSONDecodeError, KeyError): cause = message = self.text if self.status_code == requests.codes.not_found: raise not_found(cause, response=self._response, explanation=message) raise APIError(cause, response=self._response, explanation=message) class APIClient(requests.Session): """Client for Podman service API.""" # Abstract methods (delete,get,head,post) are specialized and pylint cannot walk hierarchy. # pylint: disable=too-many-instance-attributes,arguments-differ,arguments-renamed supported_schemes: ClassVar[list[str]] = ( "unix", "http+unix", "ssh", "http+ssh", "tcp", "http", ) def __init__( self, base_url: str = None, version: Optional[str] = None, timeout: Optional[float] = None, tls: Union[TLSConfig, bool] = False, user_agent: Optional[str] = None, num_pools: Optional[int] = None, credstore_env: Optional[Mapping[str, str]] = None, use_ssh_client=True, max_pool_size=None, **kwargs, ): # pylint: disable=unused-argument,too-many-positional-arguments """Instantiate APIClient object. Args: base_url: Address to use for connecting to Podman service. version: Override version prefix for Podman resource URLs. timeout: Time in seconds to allow for Podman service operation. tls: Configuration for TLS connections. user_agent: Override User-Agent HTTP header. num_pools: The number of connection pools to cache. credstore_env: Environment for storing credentials. use_ssh_client: Use system ssh agent rather than ssh module. Always, True. max_pool_size: Override number of connections pools to maintain. Default: requests.adapters.DEFAULT_POOLSIZE Keyword Args: compatible_version (str): Override version prefix for compatible resource URLs. identity (str): Provide SSH key to authenticate SSH connection. Raises: ValueError: when a parameter is incorrect """ super().__init__() self.base_url = self._normalize_url(base_url) adapter_kwargs = kwargs.copy() # The HTTPAdapter doesn't handle the "**kwargs", so it needs special structure # where the parameters are set specifically. http_adapter_kwargs = {} if num_pools is not None: adapter_kwargs["pool_connections"] = num_pools http_adapter_kwargs["pool_connections"] = num_pools if max_pool_size is not None: adapter_kwargs["pool_maxsize"] = max_pool_size http_adapter_kwargs["pool_maxsize"] = max_pool_size if timeout is not None: adapter_kwargs["timeout"] = timeout if self.base_url.scheme == "http+unix": self.mount("http://", UDSAdapter(self.base_url.geturl(), **adapter_kwargs)) self.mount("https://", UDSAdapter(self.base_url.geturl(), **adapter_kwargs)) # ignore proxies from the env vars self.trust_env = False elif self.base_url.scheme == "http+ssh": self.mount("http://", SSHAdapter(self.base_url.geturl(), **adapter_kwargs)) self.mount("https://", SSHAdapter(self.base_url.geturl(), **adapter_kwargs)) elif self.base_url.scheme == "http": self.mount("http://", HTTPAdapter(**http_adapter_kwargs)) self.mount("https://", HTTPAdapter(**http_adapter_kwargs)) else: raise PodmanError("APIClient.supported_schemes changed without adding a branch here.") self.version = version or VERSION self.path_prefix = f"/v{self.version}/libpod/" self.compatible_version = kwargs.get("compatible_version", COMPATIBLE_VERSION) self.compatible_prefix = f"/v{self.compatible_version}/" self.timeout = timeout self.pool_maxsize = num_pools or requests.adapters.DEFAULT_POOLSIZE self.credstore_env = credstore_env or {} self.user_agent = user_agent or ( f"PodmanPy/{__version__} (API v{self.version}; Compatible v{self.compatible_version})" ) self.headers.update({"User-Agent": self.user_agent}) @staticmethod def _normalize_url(base_url: str) -> urllib.parse.ParseResult: uri = urllib.parse.urlparse(base_url) if uri.scheme not in APIClient.supported_schemes: raise ValueError( f"The scheme '{uri.scheme}' must be one of {APIClient.supported_schemes}" ) # Normalize URL scheme, needs to match up with adapter mounts if uri.scheme == "unix": uri = uri._replace(scheme="http+unix") elif uri.scheme == "ssh": uri = uri._replace(scheme="http+ssh") elif uri.scheme == "tcp": uri = uri._replace(scheme="http") # Normalize URL netloc, needs to match up with transport adapters expectations if uri.netloc == "": uri = uri._replace(netloc=uri.path)._replace(path="") if "/" in uri.netloc: uri = uri._replace(netloc=urllib.parse.quote_plus(uri.netloc)) return uri def delete( self, path: Union[str, bytes], *, params: Union[None, bytes, Mapping[str, str]] = None, headers: Optional[Mapping[str, str]] = None, timeout: _Timeout = None, stream: Optional[bool] = False, **kwargs, ) -> APIResponse: """HTTP DELETE operation against configured Podman service. Args: path: Relative path to RESTful resource. params: Optional parameters to include with URL. headers: Optional headers to include in request. timeout: Number of seconds to wait on request, or (connect timeout, read timeout) tuple stream: Return iterator for content vs reading all content into memory Keyword Args: compatible: Will override the default path prefix with compatible prefix Raises: APIError: when service returns an error """ return self._request( "DELETE", path=path, params=params, headers=headers, timeout=timeout, stream=stream, **kwargs, ) def get( self, path: Union[str, bytes], *, params: Union[None, bytes, Mapping[str, list[str]]] = None, headers: Optional[Mapping[str, str]] = None, timeout: _Timeout = None, stream: Optional[bool] = False, **kwargs, ) -> APIResponse: """HTTP GET operation against configured Podman service. Args: path: Relative path to RESTful resource. params: Optional parameters to include with URL. headers: Optional headers to include in request. timeout: Number of seconds to wait on request, or (connect timeout, read timeout) tuple stream: Return iterator for content vs reading all content into memory Keyword Args: compatible: Will override the default path prefix with compatible prefix Raises: APIError: when service returns an error """ return self._request( "GET", path=path, params=params, headers=headers, timeout=timeout, stream=stream, **kwargs, ) def head( self, path: Union[str, bytes], *, params: Union[None, bytes, Mapping[str, str]] = None, headers: Optional[Mapping[str, str]] = None, timeout: _Timeout = None, stream: Optional[bool] = False, **kwargs, ) -> APIResponse: """HTTP HEAD operation against configured Podman service. Args: path: Relative path to RESTful resource. params: Optional parameters to include with URL. headers: Optional headers to include in request. timeout: Number of seconds to wait on request, or (connect timeout, read timeout) tuple stream: Return iterator for content vs reading all content into memory Keyword Args: compatible: Will override the default path prefix with compatible prefix Raises: APIError: when service returns an error """ return self._request( "HEAD", path=path, params=params, headers=headers, timeout=timeout, stream=stream, **kwargs, ) def post( self, path: Union[str, bytes], *, params: Union[None, bytes, Mapping[str, str]] = None, data: _Data = None, headers: Optional[Mapping[str, str]] = None, timeout: _Timeout = None, stream: Optional[bool] = False, **kwargs, ) -> APIResponse: """HTTP POST operation against configured Podman service. Args: path: Relative path to RESTful resource. data: HTTP body for operation params: Optional parameters to include with URL. headers: Optional headers to include in request. timeout: Number of seconds to wait on request, or (connect timeout, read timeout) tuple stream: Return iterator for content vs reading all content into memory Keyword Args: compatible: Will override the default path prefix with compatible prefix verify: Whether to verify TLS certificates. Raises: APIError: when service returns an error """ return self._request( "POST", path=path, params=params, data=data, headers=headers, timeout=timeout, stream=stream, **kwargs, ) def put( self, path: Union[str, bytes], *, params: Union[None, bytes, Mapping[str, str]] = None, data: _Data = None, headers: Optional[Mapping[str, str]] = None, timeout: _Timeout = None, stream: Optional[bool] = False, **kwargs, ) -> APIResponse: """HTTP PUT operation against configured Podman service. Args: path: Relative path to RESTful resource. data: HTTP body for operation params: Optional parameters to include with URL. headers: Optional headers to include in request. timeout: Number of seconds to wait on request, or (connect timeout, read timeout) tuple stream: Return iterator for content vs reading all content into memory Keyword Args: compatible: Will override the default path prefix with compatible prefix Raises: APIError: when service returns an error """ return self._request( "PUT", path=path, params=params, data=data, headers=headers, timeout=timeout, stream=stream, **kwargs, ) def _request( self, method: str, path: Union[str, bytes], *, data: _Data = None, params: Union[None, bytes, Mapping[str, str]] = None, headers: Optional[Mapping[str, str]] = None, timeout: _Timeout = None, stream: Optional[bool] = None, **kwargs, ) -> APIResponse: """HTTP operation against configured Podman service. Args: method: HTTP method to use for request path: Relative path to RESTful resource. params: Optional parameters to include with URL. headers: Optional headers to include in request. timeout: Number of seconds to wait on request, or (connect timeout, read timeout) tuple Keyword Args: compatible: Will override the default path prefix with compatible prefix verify: Whether to verify TLS certificates. Raises: APIError: when service returns an error """ # Only set timeout if one is given, lower level APIs will not override None timeout_kw = {} timeout = timeout or self.timeout if timeout_kw is not None: timeout_kw["timeout"] = timeout compatible = kwargs.get("compatible", False) path_prefix = self.compatible_prefix if compatible else self.path_prefix path = path.lstrip("/") # leading / makes urljoin crazy... scheme = "https" if kwargs.get("verify", None) else "http" # Build URL for operation from base_url uri = urllib.parse.ParseResult( scheme, self.base_url.netloc, urllib.parse.urljoin(path_prefix, path), self.base_url.params, self.base_url.query, self.base_url.fragment, ) try: return APIResponse( self.request( method.upper(), uri.geturl(), params=params, data=data, headers=(headers or {}), stream=stream, verify=kwargs.get("verify", None), **timeout_kw, ) ) except OSError as e: raise APIError(uri.geturl(), explanation=f"{method.upper()} operation failed") from e podman-py-5.4.0.1/podman/api/http_utils.py000066400000000000000000000056071475540541200204020ustar00rootroot00000000000000"""Utility functions for working with URLs.""" import base64 import collections.abc import json from typing import Optional, Union, Any from collections.abc import Mapping def prepare_filters(filters: Union[str, list[str], Mapping[str, str]]) -> Optional[str]: """Return filters as an URL quoted JSON dict[str, list[Any]].""" if filters is None or len(filters) == 0: return None criteria: dict[str, list[str]] = {} if isinstance(filters, str): _format_string(filters, criteria) elif isinstance(filters, collections.abc.Mapping): _format_dict(filters, criteria) else: _format_list(filters, criteria) if len(criteria) == 0: return None return json.dumps(criteria, sort_keys=True) def _format_list(filters, criteria): for item in filters: if item is None: continue key, value = item.split("=", 1) if key in criteria: criteria[key].append(value) else: criteria[key] = [value] def _format_dict(filters, criteria): for key, value in filters.items(): if value is None: continue str_value = str(value) if key in criteria: criteria[key].append(str_value) else: criteria[key] = [str_value] def _format_string(filters, criteria): key, value = filters.split("=", 1) criteria[key] = [value] def prepare_body(body: Mapping[str, Any]) -> str: """Returns JSON payload to be uploaded to server. Values of None and empty Iterables are removed, False and zero-values are retained. """ if body is None: return "" body = _filter_values(body) return json.dumps(body, sort_keys=True) def _filter_values(mapping: Mapping[str, Any], recursion=False) -> dict[str, Any]: """Returns a canonical dictionary with values == None or empty Iterables removed. Dictionary is walked using recursion. """ canonical = {} for key, value in mapping.items(): # quick filter if possible... if ( value is None or (isinstance(value, collections.abc.Sized) and len(value) <= 0) and not recursion ): continue # depending on type we need details... if isinstance(value, collections.abc.Mapping): proposal = _filter_values(value, recursion=True) elif isinstance(value, collections.abc.Iterable) and not isinstance(value, str): proposal = [i for i in value if i is not None] else: proposal = value if not recursion and proposal not in (None, "", [], {}): canonical[key] = proposal elif recursion and proposal not in (None, [], {}): canonical[key] = proposal return canonical def encode_auth_header(auth_config: dict[str, str]) -> str: return base64.urlsafe_b64encode(json.dumps(auth_config).encode('utf-8')) podman-py-5.4.0.1/podman/api/output_utils.py000066400000000000000000000030731475540541200207560ustar00rootroot00000000000000"""Utility functions for dealing with stdout and stderr.""" HEADER_SIZE = 8 STDOUT = 1 STDERR = 2 # pylint: disable=line-too-long def demux_output(data_bytes): """Demuxes the output of a container stream into stdout and stderr streams. Stream data is expected to be in the following format: - 1 byte: stream type (1=stdout, 2=stderr) - 3 bytes: padding - 4 bytes: payload size (big-endian) - N bytes: payload data ref: https://docs.podman.io/en/latest/_static/api.html?version=v5.0#tag/containers/operation/ContainerAttachLibpod Args: data_bytes: Bytes object containing the combined stream data. Returns: A tuple containing two bytes objects: (stdout, stderr). """ stdout = b"" stderr = b"" while len(data_bytes) >= HEADER_SIZE: # Extract header information header, data_bytes = data_bytes[:HEADER_SIZE], data_bytes[HEADER_SIZE:] stream_type = header[0] payload_size = int.from_bytes(header[4:HEADER_SIZE], "big") # Check if data is sufficient for payload if len(data_bytes) < payload_size: break # Incomplete frame, wait for more data # Extract and process payload payload = data_bytes[:payload_size] if stream_type == STDOUT: stdout += payload elif stream_type == STDERR: stderr += payload else: # todo: Handle unexpected stream types pass # Update data for next frame data_bytes = data_bytes[payload_size:] return stdout or None, stderr or None podman-py-5.4.0.1/podman/api/parse_utils.py000066400000000000000000000067751475540541200205440ustar00rootroot00000000000000"""Helper functions for parsing strings.""" import base64 import ipaddress import json import struct from datetime import datetime from typing import Any, Optional, Union from collections.abc import Iterator from requests import Response from .output_utils import demux_output def parse_repository(name: str) -> tuple[str, Optional[str]]: """Parse repository image name from tag or digest Returns: item 1: repository name item 2: Either digest and tag, tag, or None """ # split image name and digest elements = name.split("@", 1) if len(elements) == 2: return elements[0], elements[1] # split repository and image name from tag # tags need to be split from the right since # a port number might increase the split list len by 1 elements = name.rsplit(":", 1) if len(elements) == 2 and "/" not in elements[1]: return elements[0], elements[1] return name, None def decode_header(value: Optional[str]) -> dict[str, Any]: """Decode a base64 JSON header value.""" if value is None: return {} value = base64.b64decode(value) text = value.decode("utf-8") return json.loads(text) def prepare_timestamp(value: Union[datetime, int, None]) -> Optional[int]: """Returns a UTC UNIX timestamp from given input.""" if value is None: return None if isinstance(value, int): return value if isinstance(value, datetime): delta = value - datetime.utcfromtimestamp(0) return delta.seconds + delta.days * 24 * 3600 raise ValueError(f"Type '{type(value)}' is not supported by prepare_timestamp()") def prepare_cidr(value: Union[ipaddress.IPv4Network, ipaddress.IPv6Network]) -> (str, str): """Returns network address and Base64 encoded netmask from CIDR. The return values are dictated by the Go JSON decoder. """ return str(value.network_address), base64.b64encode(value.netmask.packed).decode("utf-8") def frames(response: Response) -> Iterator[bytes]: """Returns each frame from multiplexed payload, all results are expected in the payload. The stdout and stderr frames are undifferentiated as they are returned. """ length = len(response.content) index = 0 while length - index > 8: header = response.content[index : index + 8] _, frame_length = struct.unpack_from(">BxxxL", header) frame_begin = index + 8 frame_end = frame_begin + frame_length index = frame_end yield response.content[frame_begin:frame_end] def stream_frames( response: Response, demux: bool = False ) -> Iterator[Union[bytes, tuple[bytes, bytes]]]: """Returns each frame from multiplexed streamed payload. If ``demux`` then output will be tuples where the first position is ``STDOUT`` and the second is ``STDERR``. """ while True: header = response.raw.read(8) if not header: return _, frame_length = struct.unpack_from(">BxxxL", header) if not frame_length: continue data = response.raw.read(frame_length) if demux: data = demux_output(header + data) if not data: return yield data def stream_helper( response: Response, decode_to_json: bool = False ) -> Union[Iterator[bytes], Iterator[dict[str, Any]]]: """Helper to stream results and optionally decode to json""" for value in response.iter_lines(): if decode_to_json: yield json.loads(value) else: yield value podman-py-5.4.0.1/podman/api/path_utils.py000066400000000000000000000033221475540541200203470ustar00rootroot00000000000000"""Helper functions for managing paths""" import errno import getpass import os import stat def get_runtime_dir() -> str: """Returns the runtime directory for the current user The value in XDG_RUNTIME_DIR is preferred, but that is not always set, for example, on headless servers. /run/user/$UID is defined in the XDG documentation. """ try: return os.environ['XDG_RUNTIME_DIR'] except KeyError: user = getpass.getuser() run_user = f'/run/user/{os.getuid()}' if os.path.isdir(run_user): return run_user fallback = f'/tmp/podmanpy-runtime-dir-fallback-{user}' try: # This must be a real directory, not a symlink, so attackers can't # point it elsewhere. So we use lstat to check it. fallback_st = os.lstat(fallback) except OSError as e: if e.errno == errno.ENOENT: os.mkdir(fallback, 0o700) else: raise else: # The fallback must be a directory if not stat.S_ISDIR(fallback_st.st_mode): os.unlink(fallback) os.mkdir(fallback, 0o700) # Must be owned by the user and not accessible by anyone else elif (fallback_st.st_uid != os.getuid()) or ( fallback_st.st_mode & (stat.S_IRWXG | stat.S_IRWXO) ): os.rmdir(fallback) os.mkdir(fallback, 0o700) return fallback def get_xdg_config_home() -> str: """Returns the XDG_CONFIG_HOME directory for the current user""" try: return os.environ["XDG_CONFIG_HOME"] except KeyError: return os.path.join(os.path.expanduser("~"), ".config") podman-py-5.4.0.1/podman/api/ssh.py000066400000000000000000000234121475540541200167720ustar00rootroot00000000000000"""Specialized Transport Adapter for remote Podman access via ssh tunnel. See Podman go bindings for more details. """ import collections import functools import logging import pathlib import random import socket import subprocess import urllib.parse from contextlib import suppress from typing import Optional, Union import time import urllib3 import urllib3.connection from requests.adapters import DEFAULT_POOLBLOCK, DEFAULT_RETRIES, HTTPAdapter from podman.api.path_utils import get_runtime_dir from .adapter_utils import _key_normalizer logger = logging.getLogger("podman.ssh_adapter") class SSHSocket(socket.socket): """Specialization of socket.socket to forward a UNIX domain socket via SSH.""" def __init__(self, uri: str, identity: Optional[str] = None): """Initialize SSHSocket. Args: uri: Full address of a Podman service including path to remote socket. identity: path to file containing SSH key for authorization Examples: SSHSocket("http+ssh://alice@api.example:2222/run/user/1000/podman/podman.sock", "~alice/.ssh/api_ed25519") """ super().__init__(socket.AF_UNIX, socket.SOCK_STREAM) self.uri = uri self.identity = identity self._proc: Optional[subprocess.Popen] = None runtime_dir = pathlib.Path(get_runtime_dir()) / "podman" runtime_dir.mkdir(mode=0o700, parents=True, exist_ok=True) self.local_sock = runtime_dir / f"podman-forward-{random.getrandbits(80):x}.sock" def connect(self, **kwargs): # pylint: disable=unused-argument """Returns socket for SSH tunneled UNIX domain socket. Raises: subprocess.TimeoutExpired: when SSH client fails to create local socket """ uri = urllib.parse.urlparse(self.uri) command = [ "ssh", "-N", "-o", "StrictHostKeyChecking no", "-L", f"{self.local_sock}:{uri.path}", ] if self.identity is not None: path = pathlib.Path(self.identity).expanduser() command += ["-i", str(path)] command += [f"ssh://{uri.netloc}"] self._proc = subprocess.Popen( # pylint: disable=consider-using-with command, shell=False, stdout=subprocess.PIPE, stdin=subprocess.PIPE, ) expiration = time.monotonic() + 300 while not self.local_sock.exists(): if time.monotonic() > expiration: cmd = " ".join(command) raise subprocess.TimeoutExpired(cmd, expiration) logger.debug("Waiting on %s", self.local_sock) time.sleep(0.2) super().connect(str(self.local_sock)) def send(self, data: bytes, flags=None) -> int: # pylint: disable=unused-argument """Write data to SSH forwarded UNIX domain socket. Args: data: Data to write. flags: Ignored. Returns: The number of bytes written. Raises: RuntimeError: When socket has not been connected. """ if not self._proc or self._proc.stdin.closed: raise RuntimeError(f"SSHSocket({self.uri}) not connected.") count = self._proc.stdin.write(data) self._proc.stdin.flush() return count def recv(self, buffersize, flags=None) -> bytes: # pylint: disable=unused-argument """Read data from SSH forwarded UNIX domain socket. Args: buffersize: Maximum number of bytes to read. flags: Ignored. Raises: RuntimeError: When socket has not been connected. """ if not self._proc: raise RuntimeError(f"SSHSocket({self.uri}) not connected.") return self._proc.stdout.read(buffersize) def close(self): """Release resources held by SSHSocket. The SSH client is first sent SIGTERM, then a SIGKILL 20 seconds later if needed. """ if not self._proc or self._proc.stdin.closed: return with suppress(BrokenPipeError): self._proc.stdin.close() self._proc.stdout.close() self._proc.terminate() try: self._proc.wait(timeout=20) except subprocess.TimeoutExpired: logger.debug("SIGKILL required to stop SSH client.") self._proc.kill() self.local_sock.unlink() self._proc = None super().close() class SSHConnection(urllib3.connection.HTTPConnection): """Specialization of HTTPConnection to use a SSH forwarded socket.""" def __init__( self, host: str, port: int, timeout: Union[float, urllib3.Timeout, None] = None, strict=False, **kwargs, # pylint: disable=unused-argument ) -> None: """Initialize connection to SSHSocket for HTTP client. Args: host: Ignored. port: Ignored. timeout: Time to allow for operation. strict: Ignored. Keyword Args: uri: Full address of a Podman service including path to remote socket. Required. identity: path to file containing SSH key for authorization. """ self.sock: Optional[socket.socket] = None connection_kwargs = kwargs.copy() connection_kwargs["port"] = port if timeout is not None: if isinstance(timeout, urllib3.Timeout): try: connection_kwargs["timeout"] = float(timeout.total) except TypeError: pass connection_kwargs["timeout"] = timeout self.uri = connection_kwargs.pop("uri") self.identity = connection_kwargs.pop("identity", None) super().__init__(host, **connection_kwargs) if logger.getEffectiveLevel() == logging.DEBUG: self.set_debuglevel(1) def connect(self) -> None: """Connect to Podman service via SSHSocket.""" sock = SSHSocket(self.uri, self.identity) sock.settimeout(self.timeout) sock.connect() self.sock = sock class SSHConnectionPool(urllib3.HTTPConnectionPool): """Specialized HTTPConnectionPool for holding SSH connections.""" ConnectionCls = SSHConnection # pylint: disable=invalid-name class SSHPoolManager(urllib3.PoolManager): """Specialized PoolManager for tracking SSH connections.""" # pylint's special handling for namedtuple does not cover this usage # pylint: disable=invalid-name _PoolKey = collections.namedtuple( "_PoolKey", urllib3.poolmanager.PoolKey._fields + ("key_uri", "key_identity") ) # Map supported schemes to Pool Classes _pool_classes_by_scheme = { "http": SSHConnectionPool, "http+ssh": SSHConnectionPool, } # Map supported schemes to Pool Key index generator _key_fn_by_scheme = { "http": functools.partial(_key_normalizer, _PoolKey), "http+ssh": functools.partial(_key_normalizer, _PoolKey), } def __init__(self, num_pools=10, headers=None, **kwargs): """Initialize SSHPoolManager. Args: num_pools: Number of SSH Connection pools to maintain. headers: Additional headers to add to operations. """ super().__init__(num_pools, headers, **kwargs) self.pool_classes_by_scheme = SSHPoolManager._pool_classes_by_scheme self.key_fn_by_scheme = SSHPoolManager._key_fn_by_scheme class SSHAdapter(HTTPAdapter): """Specialization of requests transport adapter for SSH forwarded UNIX domain sockets.""" def __init__( self, uri: str, pool_connections: int = 9, pool_maxsize: int = 10, max_retries: int = DEFAULT_RETRIES, pool_block: int = DEFAULT_POOLBLOCK, **kwargs, ): # pylint: disable=too-many-positional-arguments """Initialize SSHAdapter. Args: uri: Full address of a Podman service including path to remote socket. Format, ssh://@[:port]/run/podman/podman.sock?secure=True pool_connections: The number of connection pools to cache. Should be at least one less than pool_maxsize. pool_maxsize: The maximum number of connections to save in the pool. OpenSSH default is 10. max_retries: The maximum number of retries each connection should attempt. pool_block: Whether the connection pool should block for connections. Keyword Args: timeout (float): identity (str): Optional path to ssh identity key """ self.poolmanager: Optional[SSHPoolManager] = None # Parsed for fail-fast side effects _ = urllib.parse.urlparse(uri) self._pool_kwargs = {"uri": uri} if "identity" in kwargs: path = pathlib.Path(kwargs.get("identity")) if not path.exists(): raise FileNotFoundError(f"Identity file '{path}' does not exist.") self._pool_kwargs["identity"] = str(path) if "timeout" in kwargs: self._pool_kwargs["timeout"] = kwargs.get("timeout") super().__init__(pool_connections, pool_maxsize, max_retries, pool_block) def init_poolmanager(self, connections, maxsize, block=DEFAULT_POOLBLOCK, **kwargs): """Initialize SSHPoolManager to be used by SSHAdapter. Args: connections: The number of urllib3 connection pools to cache. maxsize: The maximum number of connections to save in the pool. block: Block when no free connections are available. """ pool_kwargs = kwargs.copy() pool_kwargs.update(self._pool_kwargs) self.poolmanager = SSHPoolManager( num_pools=connections, maxsize=maxsize, block=block, **pool_kwargs ) podman-py-5.4.0.1/podman/api/tar_utils.py000066400000000000000000000074001475540541200202020ustar00rootroot00000000000000"""Utility functions for working with tarballs.""" import pathlib import random import shutil import tarfile import tempfile from fnmatch import fnmatch from typing import BinaryIO, Optional import sys def prepare_containerignore(anchor: str) -> list[str]: """Return the list of patterns for filenames to exclude. .containerignore takes precedence over .dockerignore. """ for filename in (".containerignore", ".dockerignore"): ignore = pathlib.Path(anchor) / filename if not ignore.exists(): continue with ignore.open(encoding='utf-8') as file: return list( filter( lambda L: L and not L.startswith("#"), (line.strip() for line in file.readlines()), ) ) return [] def prepare_containerfile(anchor: str, dockerfile: str) -> str: """Ensure that Containerfile, or a proxy Containerfile is in context_dir. Args: anchor: Build context directory dockerfile: Path to Dockerfile/Containerfile Returns: path to Dockerfile/Containerfile in root of context directory """ anchor_path = pathlib.Path(anchor) dockerfile_path = pathlib.Path(dockerfile) if dockerfile_path.parent.samefile(anchor_path): return dockerfile_path.name proxy_path = anchor_path / f".containerfile.{random.getrandbits(160):x}" shutil.copy2(dockerfile_path, proxy_path, follow_symlinks=False) return proxy_path.name def create_tar( anchor: str, name: str = None, exclude: list[str] = None, gzip: bool = False ) -> BinaryIO: """Create a tarfile from context_dir to send to Podman service. Args: anchor: Directory to use as root of tar file. name: Name of tar file. exclude: List of patterns for files to exclude from tar file. gzip: When True, gzip compress tar file. """ def add_filter(info: tarfile.TarInfo) -> Optional[tarfile.TarInfo]: """Filter files targeted to be added to tarfile. Args: info: Information on the file targeted to be added Returns: None: if file is not to be added TarInfo: when file is to be added. Modified as needed. Notes: exclude is captured from parent """ if not (info.isfile() or info.isdir() or info.issym()): return None if _exclude_matcher(info.name, exclude): return None # Workaround https://bugs.python.org/issue32713. Fixed in Python 3.7 if info.mtime < 0 or info.mtime > 8**11 - 1: info.mtime = int(info.mtime) # do not leak client information to service info.uid = 0 info.uname = info.gname = "root" if sys.platform == "win32": info.mode = info.mode & 0o755 | 0o111 return info if name is None: # pylint: disable=consider-using-with name = tempfile.NamedTemporaryFile(prefix="podman_context", suffix=".tar") else: name = pathlib.Path(name) if exclude is None: exclude = [] else: exclude = exclude.copy() # FIXME caller needs to add this... # exclude.append(".dockerignore") exclude.append(name.name) mode = "w:gz" if gzip else "w" with tarfile.open(name.name, mode) as tar: tar.add(anchor, arcname="", recursive=True, filter=add_filter) return open(name.name, "rb") # pylint: disable=consider-using-with def _exclude_matcher(path: str, exclude: list[str]) -> bool: """Returns True if path matches an entry in exclude. Note: FIXME Not compatible, support !, **, etc """ if not exclude: return False for pattern in exclude: if fnmatch(path, pattern): return True return False podman-py-5.4.0.1/podman/api/uds.py000066400000000000000000000135601475540541200167730ustar00rootroot00000000000000"""Specialized Transport Adapter for UNIX domain sockets.""" import collections import functools import logging import socket from typing import Optional, Union from urllib.parse import unquote, urlparse import urllib3 import urllib3.connection from requests.adapters import DEFAULT_POOLBLOCK, DEFAULT_POOLSIZE, DEFAULT_RETRIES, HTTPAdapter from ..errors import APIError from .adapter_utils import _key_normalizer logger = logging.getLogger("podman.uds_adapter") class UDSSocket(socket.socket): """Specialization of socket.socket for a UNIX domain socket.""" def __init__(self, uds: str): """Initialize UDSSocket. Args: uds: Full address of a Podman service UNIX domain socket. Examples: UDSSocket("http+unix:///run/podman/podman.sock") """ super().__init__(socket.AF_UNIX, socket.SOCK_STREAM) self.uds = uds def connect(self, **kwargs): # pylint: disable=unused-argument """Returns socket for UNIX domain socket.""" netloc = unquote(urlparse(self.uds).netloc) try: super().connect(netloc) except Exception as e: raise APIError(f"Unable to make connection to UDS '{netloc}'") from e class UDSConnection(urllib3.connection.HTTPConnection): """Specialization of HTTPConnection to use a UNIX domain sockets.""" def __init__( self, host: str, port: int, timeout: Union[float, urllib3.Timeout, None] = None, strict=False, **kwargs, # pylint: disable=unused-argument ): """Initialize connection to UNIX domain socket for HTTP client. Args: host: Ignored. port: Ignored. timeout: Time to allow for operation. strict: Ignored. Keyword Args: uds: Full address of a Podman service UNIX domain socket. Required. """ connection_kwargs = kwargs.copy() self.sock: Optional[socket.socket] = None if timeout is not None: if isinstance(timeout, urllib3.Timeout): try: connection_kwargs["timeout"] = float(timeout.total) except TypeError: pass connection_kwargs["timeout"] = timeout self.uds = connection_kwargs.pop("uds") super().__init__(host, **connection_kwargs) def connect(self) -> None: """Connect to Podman service via UNIX domain socket.""" sock = UDSSocket(self.uds) sock.settimeout(self.timeout) sock.connect() self.sock = sock class UDSConnectionPool(urllib3.HTTPConnectionPool): """Specialization of HTTPConnectionPool for holding UNIX domain sockets.""" ConnectionCls = UDSConnection # pylint: disable=invalid-name class UDSPoolManager(urllib3.PoolManager): """Specialized PoolManager for tracking UNIX domain socket connections.""" # pylint's special handling for namedtuple does not cover this usage # pylint: disable=invalid-name _PoolKey = collections.namedtuple( "_PoolKey", urllib3.poolmanager.PoolKey._fields + ("key_uds",) ) # Map supported schemes to Pool Classes _pool_classes_by_scheme = { "http": UDSConnectionPool, "http+ssh": UDSConnectionPool, } # Map supported schemes to Pool Key index generator _key_fn_by_scheme = { "http": functools.partial(_key_normalizer, _PoolKey), "http+ssh": functools.partial(_key_normalizer, _PoolKey), } def __init__(self, num_pools=10, headers=None, **kwargs): """Initialize UDSPoolManager. Args: num_pools: Number of UDS Connection pools to maintain. headers: Additional headers to add to operations. """ super().__init__(num_pools, headers, **kwargs) self.pool_classes_by_scheme = UDSPoolManager._pool_classes_by_scheme self.key_fn_by_scheme = UDSPoolManager._key_fn_by_scheme class UDSAdapter(HTTPAdapter): """Specialization of requests transport adapter for UNIX domain sockets.""" def __init__( self, uds: str, pool_connections=DEFAULT_POOLSIZE, pool_maxsize=DEFAULT_POOLSIZE, max_retries=DEFAULT_RETRIES, pool_block=DEFAULT_POOLBLOCK, **kwargs, ): # pylint: disable=too-many-positional-arguments """Initialize UDSAdapter. Args: uds: Full address of a Podman service UNIX domain socket. Format, http+unix:///run/podman/podman.sock max_retries: The maximum number of retries each connection should attempt. pool_block: Whether the connection pool should block for connections. pool_connections: The number of connection pools to cache. pool_maxsize: The maximum number of connections to save in the pool. Keyword Args: timeout (float): Time in seconds to wait for response Examples: requests.Session.mount( "http://", UDSAdapter("http+unix:///run/user/1000/podman/podman.sock")) """ self.poolmanager: Optional[UDSPoolManager] = None self._pool_kwargs = {"uds": uds} if "timeout" in kwargs: self._pool_kwargs["timeout"] = kwargs.get("timeout") super().__init__(pool_connections, pool_maxsize, max_retries, pool_block) def init_poolmanager(self, connections, maxsize, block=DEFAULT_POOLBLOCK, **kwargs): """Initialize UDS Pool Manager. Args: connections: The number of urllib3 connection pools to cache. maxsize: The maximum number of connections to save in the pool. block: Block when no free connections are available. """ pool_kwargs = kwargs.copy() pool_kwargs.update(self._pool_kwargs) self.poolmanager = UDSPoolManager( num_pools=connections, maxsize=maxsize, block=block, **pool_kwargs ) podman-py-5.4.0.1/podman/client.py000066400000000000000000000206151475540541200167040ustar00rootroot00000000000000"""Client for connecting to Podman service.""" import logging import os from contextlib import AbstractContextManager from pathlib import Path from typing import Any, Optional from podman.api import cached_property from podman.api.client import APIClient from podman.api.path_utils import get_runtime_dir from podman.domain.config import PodmanConfig from podman.domain.containers_manager import ContainersManager from podman.domain.events import EventsManager from podman.domain.images_manager import ImagesManager from podman.domain.manifests import ManifestsManager from podman.domain.networks_manager import NetworksManager from podman.domain.pods_manager import PodsManager from podman.domain.secrets import SecretsManager from podman.domain.system import SystemManager from podman.domain.volumes import VolumesManager logger = logging.getLogger("podman") class PodmanClient(AbstractContextManager): """Client to connect to a Podman service. Examples: with PodmanClient(base_url="ssh://root@api.example:22/run/podman/podman.sock?secure=True", identity="~alice/.ssh/api_ed25519") """ def __init__(self, **kwargs) -> None: """Initialize PodmanClient. Keyword Args: base_url (str): Full URL to Podman service. See examples. version (str): API version to use. Default: auto, use version from server timeout (int): Timeout for API calls, in seconds. Default: socket._GLOBAL_DEFAULT_TIMEOUT. tls: Ignored. SSH connection configuration delegated to SSH Host configuration. user_agent (str): User agent for service connections. Default: PodmanPy/ credstore_env (Mapping[str, str]): Dict containing environment for credential store use_ssh_client (True): Always shell out to SSH client for SSH Podman service connections. max_pool_size (int): Number of connections to save in pool connection (str): Identifier of connection to use from XDG_CONFIG_HOME/containers/containers.conf identity (str): Provide SSH key to authenticate SSH connection. Examples: base_url: - http+ssh://@[:port][?secure=True] - http+unix:// - tcp://[:] """ super().__init__() config = PodmanConfig() api_kwargs = kwargs.copy() if "connection" in api_kwargs: connection = config.services[api_kwargs.get("connection")] api_kwargs["base_url"] = connection.url.geturl() # Override configured identity, if provided in arguments api_kwargs["identity"] = kwargs.get("identity", str(connection.identity)) elif "base_url" not in api_kwargs: path = str(Path(get_runtime_dir()) / "podman" / "podman.sock") api_kwargs["base_url"] = "http+unix://" + path self.api = APIClient(**api_kwargs) def __enter__(self) -> "PodmanClient": return self def __exit__(self, exc_type, exc_value, traceback) -> None: self.close() @classmethod def from_env( cls, *, version: str = "auto", timeout: Optional[int] = None, max_pool_size: Optional[int] = None, ssl_version: Optional[int] = None, # pylint: disable=unused-argument assert_hostname: bool = False, # pylint: disable=unused-argument environment: Optional[dict[str, str]] = None, credstore_env: Optional[dict[str, str]] = None, use_ssh_client: bool = True, # pylint: disable=unused-argument ) -> "PodmanClient": """Returns connection to service using environment variables and parameters. Environment variables: - DOCKER_HOST, CONTAINER_HOST: URL to Podman service - DOCKER_TLS_VERIFY, CONTAINER_TLS_VERIFY: Verify host against CA certificate - DOCKER_CERT_PATH, CONTAINER_CERT_PATH: Path to TLS certificates for host connection Args: version: API version to use. Default: auto, use version from server timeout: Timeout for API calls, in seconds. max_pool_size: Number of connections to save in pool. ssl_version: SSH configuration delegated to SSH client configuration. Ignored. assert_hostname: Ignored. environment: Dict containing input environment. Default: os.environ credstore_env: Dict containing environment for credential store use_ssh_client: Use system ssh client rather than ssh module. Always, True. Returns: Client used to communicate with a Podman service. Raises: ValueError when required environment variable is not set """ environment = environment or os.environ credstore_env = credstore_env or {} if version == "auto": version = None kwargs = { 'version': version, 'timeout': timeout, 'tls': False, 'credstore_env': credstore_env, 'max_pool_size': max_pool_size, } host = environment.get("CONTAINER_HOST") or environment.get("DOCKER_HOST") or None if host is not None: kwargs['base_url'] = host return PodmanClient(**kwargs) @cached_property def containers(self) -> ContainersManager: """Returns Manager for operations on containers stored by a Podman service.""" return ContainersManager(client=self.api, podman_client=self) @cached_property def images(self) -> ImagesManager: """Returns Manager for operations on images stored by a Podman service.""" return ImagesManager(client=self.api) @cached_property def manifests(self) -> ManifestsManager: """Returns Manager for operations on manifests maintained by a Podman service.""" return ManifestsManager(client=self.api) @cached_property def networks(self) -> NetworksManager: """Returns Manager for operations on networks maintained by a Podman service.""" return NetworksManager(client=self.api) @cached_property def volumes(self) -> VolumesManager: """Returns Manager for operations on volumes maintained by a Podman service.""" return VolumesManager(client=self.api) @cached_property def pods(self) -> PodsManager: """Returns Manager for operations on pods maintained by a Podman service.""" return PodsManager(client=self.api) @cached_property def secrets(self): """Returns Manager for operations on secrets maintained by a Podman service.""" return SecretsManager(client=self.api) @cached_property def system(self): return SystemManager(client=self.api) def df(self) -> dict[str, Any]: # pylint: disable=missing-function-docstring,invalid-name return self.system.df() df.__doc__ = SystemManager.df.__doc__ def events(self, *args, **kwargs): # pylint: disable=missing-function-docstring return EventsManager(client=self.api).list(*args, **kwargs) events.__doc__ = EventsManager.list.__doc__ def info(self, *args, **kwargs): # pylint: disable=missing-function-docstring return self.system.info(*args, **kwargs) info.__doc__ = SystemManager.info.__doc__ def login(self, *args, **kwargs): # pylint: disable=missing-function-docstring return self.system.login(*args, **kwargs) login.__doc__ = SystemManager.login.__doc__ def ping(self) -> bool: # pylint: disable=missing-function-docstring return self.system.ping() ping.__doc__ = SystemManager.ping.__doc__ def version(self, *args, **kwargs): # pylint: disable=missing-function-docstring _ = args return self.system.version(**kwargs) version.__doc__ = SystemManager.version.__doc__ def close(self): """Release PodmanClient Resources.""" return self.api.close() @property def swarm(self): """Swarm not supported. Raises: NotImplemented: Swarm not supported by Podman service """ raise NotImplementedError("Swarm operations are not supported by Podman service.") # Aliases to cover all swarm methods services = swarm configs = swarm nodes = swarm # Aliases to minimize effort to port to PodmanPy DockerClient = PodmanClient from_env = PodmanClient.from_env podman-py-5.4.0.1/podman/domain/000077500000000000000000000000001475540541200163175ustar00rootroot00000000000000podman-py-5.4.0.1/podman/domain/__init__.py000066400000000000000000000000001475540541200204160ustar00rootroot00000000000000podman-py-5.4.0.1/podman/domain/config.py000066400000000000000000000137711475540541200201470ustar00rootroot00000000000000"""Read containers.conf file.""" import sys import urllib from pathlib import Path from typing import Optional import json from podman.api import cached_property from podman.api.path_utils import get_xdg_config_home if sys.version_info >= (3, 11): from tomllib import loads as toml_loads else: try: from tomli import loads as toml_loads except ImportError: try: from toml import loads as toml_loads except ImportError: from pytoml import loads as toml_loads class ServiceConnection: """ServiceConnection defines a connection to the Podman service.""" def __init__(self, name: str, attrs: dict[str, str]): """Create a Podman ServiceConnection.""" self.name = name self.attrs = attrs def __repr__(self) -> str: return f"""<{self.__class__.__name__}: '{self.id}'>""" def __hash__(self) -> int: return hash(tuple(self.name)) def __eq__(self, other) -> bool: if isinstance(other, ServiceConnection): return self.id == other.id and self.attrs == other.attrs return False @property def id(self): # pylint: disable=invalid-name """str: Returns identifier for service connection.""" return self.name @cached_property def url(self): """urllib.parse.ParseResult: Returns URL for service connection.""" if self.attrs.get("uri"): return urllib.parse.urlparse(self.attrs.get("uri")) return urllib.parse.urlparse(self.attrs.get("URI")) @cached_property def identity(self): """Path: Returns Path to identity file for service connection.""" if self.attrs.get("identity"): return Path(self.attrs.get("identity")) return Path(self.attrs.get("Identity")) class PodmanConfig: """PodmanConfig provides a representation of the containers.conf file.""" def __init__(self, path: Optional[str] = None): """Read Podman configuration from users XDG_CONFIG_HOME.""" self.is_default = False if path is None: home = Path(get_xdg_config_home()) self.path = home / "containers" / "podman-connections.json" old_toml_file = home / "containers" / "containers.conf" self.is_default = True # this elif is only for testing purposes elif "@@is_test@@" in path: test_path = path.replace("@@is_test@@", '') self.path = Path(test_path) / "podman-connections.json" old_toml_file = Path(test_path) / "containers.conf" self.is_default = True else: self.path = Path(path) old_toml_file = None self.attrs = {} if self.path.exists(): try: with open(self.path, encoding='utf-8') as file: self.attrs = json.load(file) except Exception: # if the user specifies a path, it can either be a JSON file # or a TOML file - so try TOML next try: with self.path.open(encoding='utf-8') as file: buffer = file.read() loaded_toml = toml_loads(buffer) self.attrs.update(loaded_toml) except Exception as e: raise AttributeError( "The path given is neither a JSON nor a TOML connections file" ) from e # Read the old toml file configuration if self.is_default and old_toml_file.exists(): with old_toml_file.open(encoding='utf-8') as file: buffer = file.read() loaded_toml = toml_loads(buffer) self.attrs.update(loaded_toml) def __hash__(self) -> int: return hash(tuple(self.path.name)) def __eq__(self, other) -> bool: if isinstance(other, PodmanConfig): return self.id == other.id and self.attrs == other.attrs return False @property def id(self): # pylint: disable=invalid-name """Path: Returns Path() of container.conf.""" return self.path @cached_property def services(self): """dict[str, ServiceConnection]: Returns list of service connections. Examples: podman_config = PodmanConfig() address = podman_config.services["testing"] print(f"Testing service address {address}") """ services: dict[str, ServiceConnection] = {} # read the keys of the toml file first engine = self.attrs.get("engine") if engine: destinations = engine.get("service_destinations") for key in destinations: connection = ServiceConnection(key, attrs=destinations[key]) services[key] = connection # read the keys of the json file next # this will ensure that if the new json file and the old toml file # has a connection with the same name defined, we always pick the # json one connection = self.attrs.get("Connection") if connection: destinations = connection.get("Connections") for key in destinations: connection = ServiceConnection(key, attrs=destinations[key]) services[key] = connection return services @cached_property def active_service(self): """Optional[ServiceConnection]: Returns active connection.""" # read the new json file format connection = self.attrs.get("Connection") if connection: active = connection.get("Default") destinations = connection.get("Connections") return ServiceConnection(active, attrs=destinations[active]) # if we are here, that means there was no default in the new json file engine = self.attrs.get("engine") if engine: active = engine.get("active_service") destinations = engine.get("service_destinations") return ServiceConnection(active, attrs=destinations[active]) return None podman-py-5.4.0.1/podman/domain/containers.py000066400000000000000000000470161475540541200210460ustar00rootroot00000000000000"""Model and Manager for Container resources.""" import json import logging import shlex from contextlib import suppress from typing import Any, Optional, Union from collections.abc import Iterable, Iterator, Mapping import requests from podman import api from podman.api.output_utils import demux_output from podman.domain.images import Image from podman.domain.images_manager import ImagesManager from podman.domain.manager import PodmanResource from podman.errors import APIError logger = logging.getLogger("podman.containers") class Container(PodmanResource): """Details and configuration for a container managed by the Podman service.""" @property def name(self): """str: Returns container's name.""" with suppress(KeyError): if 'Name' in self.attrs: return self.attrs["Name"].lstrip("/") return self.attrs["Names"][0].lstrip("/") return None @property def image(self): """podman.domain.images.Image: Returns Image object used to create Container.""" if "Image" in self.attrs: image_id = self.attrs["Image"] return ImagesManager(client=self.client).get(image_id) return Image() @property def labels(self): """dict[str, str]: Returns labels associated with container.""" labels = None with suppress(KeyError): # Container created from ``list()`` operation if "Labels" in self.attrs: labels = self.attrs["Labels"] # Container created from ``get()`` operation else: labels = self.attrs["Config"].get("Labels", {}) return labels or {} @property def status(self): """Literal["created", "initialized", "running", "stopped", "exited", "unknown"]: Returns status of container.""" with suppress(KeyError): return self.attrs["State"]["Status"] return "unknown" @property def ports(self): """dict[str, int]: Return ports exposed by container.""" with suppress(KeyError): return self.attrs["NetworkSettings"]["Ports"] return {} def attach(self, **kwargs) -> Union[str, Iterator[str]]: """Attach to container's tty. Keyword Args: stdout (bool): Include stdout. Default: True stderr (bool): Include stderr. Default: True stream (bool): Return iterator of string(s) vs single string. Default: False logs (bool): Include previous container output. Default: False Raises: NotImplementedError: method not implemented. """ raise NotImplementedError() def attach_socket(self, **kwargs): """Not Implemented. Raises: NotImplementedError: method not implemented. """ raise NotImplementedError() def commit(self, repository: str = None, tag: str = None, **kwargs) -> Image: """Save container to given repository. Args: repository: Where to save Image tag: Tag to push with Image Keyword Args: author (str): Name of commit author changes (list[str]): Instructions to apply during commit comment (str): Commit message to include with Image, overrides keyword message conf (dict[str, Any]): Ignored. format (str): Format of the image manifest and metadata message (str): Commit message to include with Image pause (bool): Pause the container before committing it """ params = { "author": kwargs.get("author"), "changes": kwargs.get("changes"), "comment": kwargs.get("comment", kwargs.get("message")), "container": self.id, "format": kwargs.get("format"), "pause": kwargs.get("pause"), "repo": repository, "tag": tag, } response = self.client.post("/commit", params=params) response.raise_for_status() body = response.json() return ImagesManager(client=self.client).get(body["Id"]) def diff(self) -> list[dict[str, int]]: """Report changes of a container's filesystem. Raises: APIError: when service reports an error """ response = self.client.get(f"/containers/{self.id}/changes") response.raise_for_status() return response.json() # pylint: disable=too-many-arguments def exec_run( self, cmd: Union[str, list[str]], *, stdout: bool = True, stderr: bool = True, stdin: bool = False, tty: bool = False, privileged: bool = False, user=None, detach: bool = False, stream: bool = False, socket: bool = False, # pylint: disable=unused-argument environment: Union[Mapping[str, str], list[str]] = None, workdir: str = None, demux: bool = False, ) -> tuple[ Optional[int], Union[Iterator[Union[bytes, tuple[bytes, bytes]]], Any, tuple[bytes, bytes]], ]: """Run given command inside container and return results. Args: cmd: Command to be executed stdout: Attach to stdout. Default: True stderr: Attach to stderr. Default: True stdin: Attach to stdin. Default: False tty: Allocate a pseudo-TTY. Default: False privileged: Run as privileged. user: User to execute command as. detach: If true, detach from the exec command. Default: False stream: Stream response data. Ignored if ``detach`` is ``True``. Default: False socket: Return the connection socket to allow custom read/write operations. Default: False environment: A dictionary or a list[str] in the following format ["PASSWORD=xxx"] or {"PASSWORD": "xxx"}. workdir: Path to working directory for this exec session demux: Return stdout and stderr separately Returns: A tuple of (``response_code``, ``output``). ``response_code``: The exit code of the provided command. ``None`` if ``stream``. ``output``: If ``stream``, then a generator yielding response chunks. If ``demux``, then a tuple of (``stdout``, ``stderr``). Else the response content. Raises: NotImplementedError: method not implemented. APIError: when service reports error """ # pylint: disable-msg=too-many-locals if isinstance(environment, dict): environment = [f"{k}={v}" for k, v in environment.items()] data = { "AttachStderr": stderr, "AttachStdin": stdin, "AttachStdout": stdout, "Cmd": cmd if isinstance(cmd, list) else shlex.split(cmd), # "DetachKeys": detach, # This is something else "Env": environment, "Privileged": privileged, "Tty": tty, "WorkingDir": workdir, } if user: data["User"] = user stream = stream and not detach # create the exec instance response = self.client.post(f"/containers/{self.name}/exec", data=json.dumps(data)) response.raise_for_status() exec_id = response.json()['Id'] # start the exec instance, this will store command output start_resp = self.client.post( f"/exec/{exec_id}/start", data=json.dumps({"Detach": detach, "Tty": tty}), stream=stream ) start_resp.raise_for_status() if stream: return None, api.stream_frames(start_resp, demux=demux) # get and return exec information response = self.client.get(f"/exec/{exec_id}/json") response.raise_for_status() if demux: stdout_data, stderr_data = demux_output(start_resp.content) return response.json().get('ExitCode'), (stdout_data, stderr_data) return response.json().get('ExitCode'), start_resp.content def export(self, chunk_size: int = api.DEFAULT_CHUNK_SIZE) -> Iterator[bytes]: """Download container's filesystem contents as a tar archive. Args: chunk_size: <= number of bytes to return for each iteration of the generator. Yields: tarball in size/chunk_size chunks Raises: NotFound: when container has been removed from service APIError: when service reports an error """ response = self.client.get(f"/containers/{self.id}/export", stream=True) response.raise_for_status() yield from response.iter_content(chunk_size=chunk_size) def get_archive( self, path: str, chunk_size: int = api.DEFAULT_CHUNK_SIZE ) -> tuple[Iterable, dict[str, Any]]: """Download a file or folder from the container's filesystem. Args: path: Path to file or folder. chunk_size: <= number of bytes to return for each iteration of the generator. Returns: First item is a raw tar data stream. Second item is a dict containing os.stat() information on the specified path. """ response = self.client.get(f"/containers/{self.id}/archive", params={"path": [path]}) response.raise_for_status() stat = response.headers.get("x-docker-container-path-stat", None) stat = api.decode_header(stat) return response.iter_content(chunk_size=chunk_size), stat def init(self) -> None: """Initialize the container.""" response = self.client.post(f"/containers/{self.id}/init") response.raise_for_status() def inspect(self) -> dict: """Inspect a container. Raises: APIError: when service reports an error """ response = self.client.get(f"/containers/{self.id}/json") response.raise_for_status() return response.json() def kill(self, signal: Union[str, int, None] = None) -> None: """Send signal to container. Raises: APIError: when service reports an error """ response = self.client.post(f"/containers/{self.id}/kill", params={"signal": signal}) response.raise_for_status() def logs(self, **kwargs) -> Union[bytes, Iterator[bytes]]: """Get logs from the container. Keyword Args: stdout (bool): Include stdout. Default: True stderr (bool): Include stderr. Default: True stream (bool): Return generator of strings as the response. Default: False timestamps (bool): Show timestamps in output. Default: False tail (Union[str, int]): Output specified number of lines at the end of logs. Integer representing the number of lines to display, or the string all. Default: all since (Union[datetime, int]): Show logs since a given datetime or integer epoch (in seconds) follow (bool): Follow log output. Default: False until (Union[datetime, int]): Show logs that occurred before the given datetime or integer epoch (in seconds) """ stream = bool(kwargs.get("stream", False)) params = { "follow": kwargs.get("follow", kwargs.get("stream", None)), "since": api.prepare_timestamp(kwargs.get("since")), "stderr": kwargs.get("stderr", True), "stdout": kwargs.get("stdout", True), "tail": kwargs.get("tail"), "timestamps": kwargs.get("timestamps"), "until": api.prepare_timestamp(kwargs.get("until")), } response = self.client.get(f"/containers/{self.id}/logs", stream=stream, params=params) response.raise_for_status() if stream: return api.stream_frames(response) return api.frames(response) def pause(self) -> None: """Pause processes within the container.""" response = self.client.post(f"/containers/{self.id}/pause") response.raise_for_status() def put_archive(self, path: str, data: bytes = None) -> bool: """Upload tar archive containing a file or folder to be written into container. Args: path: File to write data into data: Contents to write to file, when None path will be read on client to build tarfile. Returns: True when successful Raises: APIError: when server reports error """ if path is None: raise ValueError("'path' is a required argument.") if data is None: data = api.create_tar("/", path) response = self.client.put( f"/containers/{self.id}/archive", params={"path": path}, data=data ) return response.ok def remove(self, **kwargs) -> None: """Delete container. Keyword Args: v (bool): Delete associated volumes as well. link (bool): Ignored. force (bool): Kill a running container before deleting. """ self.manager.remove(self.id, **kwargs) def rename(self, name: str) -> None: """Rename container. Container updated in-situ to avoid reload(). Args: name: New name for container. """ if not name: raise ValueError("'name' is a required argument.") response = self.client.post(f"/containers/{self.id}/rename", params={"name": name}) response.raise_for_status() self.attrs["Name"] = name # shortcut to avoid needing reload() def resize(self, height: int = None, width: int = None) -> None: """Resize the tty session. Args: height: New height of tty session. width: New width of tty session. """ params = { "h": height, "w": width, } response = self.client.post(f"/containers/{self.id}/resize", params=params) response.raise_for_status() def restart(self, **kwargs) -> None: """Restart processes in container. Keyword Args: timeout (int): Seconds to wait for container to stop before killing container. """ params = {"timeout": kwargs.get("timeout")} post_kwargs = {} if kwargs.get("timeout"): post_kwargs["timeout"] = float(params["timeout"]) * 1.5 response = self.client.post(f"/containers/{self.id}/restart", params=params, **post_kwargs) response.raise_for_status() def start(self, **kwargs) -> None: """Start processes in container. Keyword Args: detach_keys: Override the key sequence for detaching a container (Podman only) """ response = self.client.post( f"/containers/{self.id}/start", params={"detachKeys": kwargs.get("detach_keys")} ) response.raise_for_status() def stats( self, **kwargs ) -> Union[bytes, dict[str, Any], Iterator[bytes], Iterator[dict[str, Any]]]: """Return statistics for container. Keyword Args: decode (bool): If True and stream is True, stream will be decoded into dict's. Default: False. stream (bool): Stream statistics until cancelled. Default: True. Raises: APIError: when service reports an error """ # FIXME Errors in stream are not handled, need content and json to read Errors. stream = kwargs.get("stream", True) decode = kwargs.get("decode", False) params = { "containers": self.id, "stream": stream, } response = self.client.get("/containers/stats", params=params, stream=stream) response.raise_for_status() if stream: return api.stream_helper(response, decode_to_json=decode) return json.loads(response.content) if decode else response.content def stop(self, **kwargs) -> None: """Stop container. Keyword Args: all (bool): When True, stop all containers. Default: False (Podman only) ignore (bool): When True, ignore error if container already stopped (Podman only) timeout (int): Number of seconds to wait on container to stop before killing it. """ params = {"all": kwargs.get("all"), "timeout": kwargs.get("timeout")} post_kwargs = {} if kwargs.get("timeout"): post_kwargs["timeout"] = float(params["timeout"]) * 1.5 response = self.client.post(f"/containers/{self.id}/stop", params=params, **post_kwargs) response.raise_for_status() if response.status_code == requests.codes.no_content: return if response.status_code == requests.codes.not_modified: if kwargs.get("ignore", False): return body = response.json() raise APIError(body["cause"], response=response, explanation=body["message"]) def top(self, **kwargs) -> Union[Iterator[dict[str, Any]], dict[str, Any]]: """Report on running processes in the container. Keyword Args: ps_args (str): When given, arguments will be passed to ps stream (bool): When True, repeatedly return results. Default: False Raises: NotFound: when the container no longer exists APIError: when the service reports an error """ stream = kwargs.get("stream", False) params = { "stream": stream, "ps_args": kwargs.get("ps_args"), } response = self.client.get(f"/containers/{self.id}/top", params=params, stream=stream) response.raise_for_status() if stream: return api.stream_helper(response, decode_to_json=True) return response.json() def unpause(self) -> None: """Unpause processes in container.""" response = self.client.post(f"/containers/{self.id}/unpause") response.raise_for_status() def update(self, **kwargs): """Update resource configuration of the containers. Raises: NotImplementedError: Podman service unsupported operation. """ raise NotImplementedError("Container.update() is not supported by Podman service.") def wait(self, **kwargs) -> int: """Block until the container enters given state. Keyword Args: condition (Union[str, list[str]]): Container state on which to release. One or more of: "configured", "created", "running", "stopped", "paused", "exited", "removing", "stopping". interval (int): Time interval to wait before polling for completion. Returns: "Error" key has a dictionary value with the key "Message". Raises: NotFound: when Container not found ReadTimeoutError: when timeout is exceeded APIError: when service returns an error """ condition = kwargs.get("condition") if isinstance(condition, str): condition = [condition] interval = kwargs.get("interval") params = {} if condition != []: params["condition"] = condition if interval != "": params["interval"] = interval # This API endpoint responds with a JSON encoded integer. # See: # https://docs.podman.io/en/latest/_static/api.html#tag/containers/operation/ContainerWaitLibpod response = self.client.post(f"/containers/{self.id}/wait", params=params) response.raise_for_status() return response.json() podman-py-5.4.0.1/podman/domain/containers_create.py000066400000000000000000001055041475540541200223660ustar00rootroot00000000000000"""Mixin to provide Container create() method.""" # pylint: disable=line-too-long import copy import logging import re from contextlib import suppress from typing import Any, Union from collections.abc import MutableMapping from podman import api from podman.domain.containers import Container from podman.domain.images import Image from podman.domain.pods import Pod from podman.domain.secrets import Secret from podman.errors import ImageNotFound logger = logging.getLogger("podman.containers") NAMED_VOLUME_PATTERN = re.compile(r'[a-zA-Z0-9][a-zA-Z0-9_.-]*') class CreateMixin: # pylint: disable=too-few-public-methods """Class providing create method for ContainersManager.""" def create( self, image: Union[Image, str], command: Union[str, list[str], None] = None, **kwargs, ) -> Container: """Create a container. Args: image: Image to run. command: Command to run in the container. Keyword Args: auto_remove (bool): Enable auto-removal of the container on daemon side when the container's process exits. blkio_weight_device (dict[str, Any]): Block IO weight (relative device weight) in the form of: [{"Path": "device_path", "Weight": weight}]. blkio_weight (int): Block IO weight (relative weight), accepts a weight value between 10 and 1000. cap_add (list[str]): Add kernel capabilities. For example: ["SYS_ADMIN", "MKNOD"] cap_drop (list[str]): Drop kernel capabilities. cgroup_parent (str): Override the default parent cgroup. cpu_count (int): Number of usable CPUs (Windows only). cpu_percent (int): Usable percentage of the available CPUs (Windows only). cpu_period (int): The length of a CPU period in microseconds. cpu_quota (int): Microseconds of CPU time that the container can get in a CPU period. cpu_rt_period (int): Limit CPU real-time period in microseconds. cpu_rt_runtime (int): Limit CPU real-time runtime in microseconds. cpu_shares (int): CPU shares (relative weight). cpuset_cpus (str): CPUs in which to allow execution (0-3, 0,1). cpuset_mems (str): Memory nodes (MEMs) in which to allow execution (0-3, 0,1). Only effective on NUMA systems. detach (bool): Run container in the background and return a Container object. device_cgroup_rules (list[str]): A list of cgroup rules to apply to the container. device_read_bps: Limit read rate (bytes per second) from a device in the form of: `[{"Path": "device_path", "Rate": rate}]` device_read_iops: Limit read rate (IO per second) from a device. device_write_bps: Limit write rate (bytes per second) from a device. device_write_iops: Limit write rate (IO per second) from a device. devices (list[str]): Expose host devices to the container, as a list[str] in the form ::. For example: /dev/sda:/dev/xvda:rwm allows the container to have read-write access to the host's /dev/sda via a node named /dev/xvda inside the container. dns (list[str]): Set custom DNS servers. dns_opt (list[str]): Additional options to be added to the container's resolv.conf file. dns_search (list[str]): DNS search domains. domainname (Union[str, list[str]]): Set custom DNS search domains. entrypoint (Union[str, list[str]]): The entrypoint for the container. environment (Union[dict[str, str], list[str]): Environment variables to set inside the container, as a dictionary or a list[str] in the format ["SOMEVARIABLE=xxx", "SOMEOTHERVARIABLE=xyz"]. extra_hosts (dict[str, str]): Additional hostnames to resolve inside the container, as a mapping of hostname to IP address. group_add (list[str]): List of additional group names and/or IDs that the container process will run as. healthcheck (dict[str,Any]): Specify a test to perform to check that the container is healthy. health_check_on_failure_action (int): Specify an action if a healthcheck fails. hostname (str): Optional hostname for the container. init (bool): Run an init inside the container that forwards signals and reaps processes init_path (str): Path to the docker-init binary ipc_mode (str): Set the IPC mode for the container. isolation (str): Isolation technology to use. Default: `None`. kernel_memory (int or str): Kernel memory limit labels (Union[dict[str, str], list[str]): A dictionary of name-value labels (e.g. {"label1": "value1", "label2": "value2"}) or a list of names of labels to set with empty values (e.g. ["label1", "label2"]) links (Optional[dict[str, str]]): Mapping of links using the {'container': 'alias'} format. The alias is optional. Containers declared in this dict will be linked to the new container using the provided alias. Default: None. log_config (LogConfig): Logging configuration. lxc_config (dict[str, str]): LXC config. mac_address (str): MAC address to assign to the container. mem_limit (Union[int, str]): Memory limit. Accepts float values (which represent the memory limit of the created container in bytes) or a string with a units identification char (100000b, 1000k, 128m, 1g). If a string is specified without a units character, bytes are assumed as an intended unit. mem_reservation (Union[int, str]): Memory soft limit. mem_swappiness (int): Tune a container's memory swappiness behavior. Accepts number between 0 and 100. memswap_limit (Union[int, str]): Maximum amount of memory + swap a container is allowed to consume. mounts (list[Mount]): Specification for mounts to be added to the container. More powerful alternative to volumes. Each item in the list is expected to be a Mount object. For example: [ { "type": "bind", "source": "/a/b/c1", "target" "/d1", "read_only": True, "relabel": "Z" }, { "type": "tmpfs", # If this was not passed, the regular directory # would be created rather than tmpfs mount !!! # as this will cause to have invalid entry # in /proc/self/mountinfo "source": "tmpfs", "target" "/d2", "size": "100k", "chown": True } ] name (str): The name for this container. nano_cpus (int): CPU quota in units of 1e-9 CPUs. networks (dict[str, dict[str, Union[str, list[str]]): Networks which will be connected to container during container creation Values of the network configuration can be : - string - list of strings (e.g. Aliases) network_disabled (bool): Disable networking. network_mode (str): One of: - bridge: Create a new network stack for the container on the bridge network. - none: No networking for this container. - container:: Reuse another container's network stack. - host: Use the host network stack. - ns:: User defined netns path. Incompatible with network. oom_kill_disable (bool): Whether to disable OOM killer. oom_score_adj (int): An integer value containing the score given to the container in order to tune OOM killer preferences. pid_mode (str): If set to host, use the host PID namespace inside the container. pids_limit (int): Tune a container's pids limit. Set -1 for unlimited. platform (str): Platform in the format os[/arch[/variant]]. Only used if the method needs to pull the requested image. ports ( dict[ Union[int, str], Union[ int, Tuple[str, int], list[int], dict[ str, Union[ int, Tuple[str, int], list[int] ] ] ] ]): Ports to bind inside the container. The keys of the dictionary are the ports to bind inside the container, either as an integer or a string in the form port/protocol, where the protocol is either tcp, udp, or sctp. The values of the dictionary are the corresponding ports to open on the host, which can be either: - The port number, as an integer. For example: {'2222/tcp': 3333} will expose port 2222 inside the container as port 3333 on the host. - None, to assign a random host port. For example: {'2222/tcp': None}. - A tuple of (address, port) if you want to specify the host interface. For example: {'1111/tcp': ('127.0.0.1', 1111)}. - A list of integers or tuples of (address, port), if you want to bind multiple host ports to a single container port. For example: {'1111/tcp': [1234, ("127.0.0.1", 4567)]}. For example: {'9090': 7878, '10932/tcp': '8781', "8989/tcp": ("127.0.0.1", 9091)} - A dictionary of the options mentioned above except for random host port. The dictionary has additional option "range", which allows binding range of ports. For example: - {'2222/tcp': {"port": 3333, "range": 4}} - {'1111/tcp': {"port": ('127.0.0.1', 1111), "range": 4}} - {'1111/tcp': [ {"port": 1234, "range": 4}, {"ip": "127.0.0.1", "port": 4567} ] } privileged (bool): Give extended privileges to this container. publish_all_ports (bool): Publish all ports to the host. read_only (bool): Mount the container's root filesystem as read only. read_write_tmpfs (bool): Mount temporary file systems as read write, in case of read_only options set to True. Default: False remove (bool): Remove the container when it has finished running. Default: False. restart_policy (dict[str, Union[str, int]]): Restart the container when it exits. Configured as a dictionary with keys: - Name: One of on-failure, or always. - MaximumRetryCount: Number of times to restart the container on failure. For example: {"Name": "on-failure", "MaximumRetryCount": 5} runtime (str): Runtime to use with this container. secrets (list[Union[str, Secret, dict[str, Union[str, int]]]]): Secrets to mount to this container. For example: - As list of strings, each string representing a secret's ID or name: ['my_secret', 'my_secret2'] - As list of Secret objects the corresponding IDs are read from: [Secret, Secret] - As list of dictionaries: [ { "source": "my_secret", # A string representing the ID or name of # a secret "target": "/my_secret", # An optional target to mount source to, # falls back to /run/secrets/source "uid": 1000, # An optional UID that falls back to 0 # if not given "gid": 1000, # An optional GID that falls back to 0 # if not given "mode": 0o400, # An optional mode to apply to the target, # use an 0o prefix for octal integers }, ] secret_env (dict[str, str]): Secrets to add as environment variables available in the container. For example: {"VARIABLE1": "NameOfSecret", "VARIABLE2": "NameOfAnotherSecret"} security_opt (list[str]): A list[str]ing values to customize labels for MLS systems, such as SELinux. shm_size (Union[str, int]): Size of /dev/shm (e.g. 1G). stdin_open (bool): Keep STDIN open even if not attached. stdout (bool): Return logs from STDOUT when detach=False. Default: True. stderr (bool): Return logs from STDERR when detach=False. Default: False. stop_signal (str): The stop signal to use to stop the container (e.g. SIGINT). storage_opt (dict[str, str]): Storage driver options per container as a key-value mapping. stream (bool): If true and detach is false, return a log generator instead of a string. Ignored if detach is true. Default: False. sysctls (dict[str, str]): Kernel parameters to set in the container. tmpfs (dict[str, str]): Temporary filesystems to mount, as a dictionary mapping a path inside the container to options for that path. For example: {'/mnt/vol2': '', '/mnt/vol1': 'size=3G,uid=1000'} tty (bool): Allocate a pseudo-TTY. ulimits (list[Ulimit]): Ulimits to set inside the container. use_config_proxy (bool): If True, and if the docker client configuration file (~/.config/containers/config.json by default) contains a proxy configuration, the corresponding environment variables will be set in the container being built. user (Union[str, int]): Username or UID to run commands as inside the container. userns_mode (str): Sets the user namespace mode for the container when user namespace remapping option is enabled. Supported values documented `here `_ uts_mode (str): Sets the UTS namespace mode for the container. `These `_ are the supported values. version (str): The version of the API to use. Set to auto to automatically detect the server's version. Default: 3.0.0 volume_driver (str): The name of a volume driver/plugin. volumes (dict[str, dict[str, Union[str, list]]]): A dictionary to configure volumes mounted inside the container. The key is either the host path or a volume name, and the value is a dictionary with the keys: - bind: The path to mount the volume inside the container - mode: Either rw to mount the volume read/write, or ro to mount it read-only. Kept for docker-py compatibility - extended_mode: List of options passed to volume mount. For example: { 'test_bind_1': {'bind': '/mnt/vol1', 'mode': 'rw'}, 'test_bind_2': {'bind': '/mnt/vol2', 'extended_mode': ['ro', 'noexec']}, 'test_bind_3': {'bind': '/mnt/vol3', 'extended_mode': ['noexec'], 'mode': 'rw'} } volumes_from (list[str]): List of container names or IDs to get volumes from. working_dir (str): Path to the working directory. workdir (str): Alias of working_dir - Path to the working directory. Returns: A Container object. Raises: ImageNotFound: when Image not found by Podman service APIError: when Podman service reports an error """ if isinstance(image, Image): image = image.id if isinstance(command, str): command = [command] payload = {"image": image, "command": command} payload.update(kwargs) payload = self._render_payload(payload) payload = api.prepare_body(payload) response = self.client.post( "/containers/create", headers={"content-type": "application/json"}, data=payload ) response.raise_for_status(not_found=ImageNotFound) container_id = response.json()["Id"] return self.get(container_id) # pylint: disable=too-many-locals,too-many-statements,too-many-branches @staticmethod def _render_payload(kwargs: MutableMapping[str, Any]) -> dict[str, Any]: """Map create/run kwargs into body parameters.""" args = copy.copy(kwargs) if "links" in args: if len(args["links"]) > 0: raise ValueError("'links' are not supported by Podman service.") del args["links"] # Ignore these keywords for key in ( "cpu_count", "cpu_percent", "nano_cpus", "platform", # used by caller "remove", # used by caller "stderr", # used by caller "stdout", # used by caller "stream", # used by caller "detach", # used by caller "volume_driver", ): with suppress(KeyError): del args[key] # These keywords are not supported for various reasons. unsupported_keys = set(args.keys()).intersection( ( "blkio_weight", "blkio_weight_device", # FIXME In addition to device Major/Minor include path "device_cgroup_rules", # FIXME Where to map for Podman API? "device_read_bps", # FIXME In addition to device Major/Minor include path "device_read_iops", # FIXME In addition to device Major/Minor include path "device_requests", # FIXME In addition to device Major/Minor include path "device_write_bps", # FIXME In addition to device Major/Minor include path "device_write_iops", # FIXME In addition to device Major/Minor include path "domainname", "network_disabled", # FIXME Where to map for Podman API? "storage_opt", # FIXME Where to map for Podman API? "tmpfs", # FIXME Where to map for Podman API? ) ) if len(unsupported_keys) > 0: raise TypeError( f"""Keyword(s) '{" ,".join(unsupported_keys)}' are""" f""" currently not supported by Podman API.""" ) def pop(k): return args.pop(k, None) def to_bytes(size: Union[int, str, None]) -> Union[int, None]: """ Converts str or int to bytes. Input can be in the following forms : 0) None - e.g. None -> returns None 1) int - e.g. 100 == 100 bytes 2) str - e.g. '100' == 100 bytes 3) str with suffix - available suffixes: b | B - bytes k | K = kilobytes m | M = megabytes g | G = gigabytes e.g. '100m' == 104857600 bytes """ size_type = type(size) if size is None: return size if size_type is int: return size if size_type is str: try: return int(size) except ValueError as bad_size: mapping = {'b': 0, 'k': 1, 'm': 2, 'g': 3} mapping_regex = ''.join(mapping.keys()) search = re.search(rf'^(\d+)([{mapping_regex}])$', size.lower()) if search: return int(search.group(1)) * (1024 ** mapping[search.group(2)]) raise TypeError( f"Passed string size {size} should be in format\\d+[bBkKmMgG] (e.g. '100m')" ) from bad_size else: raise TypeError( f"Passed size {size} should be a type of unicode, str " f"or int (found : {size_type})" ) # Transform keywords into parameters params = { "annotations": pop("annotations"), # TODO document, podman only "apparmor_profile": pop("apparmor_profile"), # TODO document, podman only "cap_add": pop("cap_add"), "cap_drop": pop("cap_drop"), "cgroup_parent": pop("cgroup_parent"), "cgroups_mode": pop("cgroups_mode"), # TODO document, podman only "cni_networks": [pop("network")], "command": args.pop("command", args.pop("cmd", None)), "conmon_pid_file": pop("conmon_pid_file"), # TODO document, podman only "containerCreateCommand": pop("containerCreateCommand"), # TODO document, podman only "devices": [], "dns_option": pop("dns_opt"), "dns_search": pop("dns_search"), "dns_server": pop("dns"), "entrypoint": pop("entrypoint"), "env": pop("environment"), "env_host": pop("env_host"), # TODO document, podman only "expose": {}, "groups": pop("group_add"), "healthconfig": pop("healthcheck"), "health_check_on_failure_action": pop("health_check_on_failure_action"), "hostadd": [], "hostname": pop("hostname"), "httpproxy": pop("use_config_proxy"), "idmappings": pop("idmappings"), # TODO document, podman only "image": pop("image"), "image_volume_mode": pop("image_volume_mode"), # TODO document, podman only "image_volumes": pop("image_volumes"), # TODO document, podman only "init": pop("init"), "init_path": pop("init_path"), "isolation": pop("isolation"), "labels": pop("labels"), "log_configuration": {}, "lxc_config": pop("lxc_config"), "mask": pop("masked_paths"), "mounts": [], "name": pop("name"), "namespace": pop("namespace"), # TODO What is this for? "network_options": pop("network_options"), # TODO document, podman only "networks": pop("networks"), "no_new_privileges": pop("no_new_privileges"), # TODO document, podman only "oci_runtime": pop("runtime"), "oom_score_adj": pop("oom_score_adj"), "overlay_volumes": pop("overlay_volumes"), # TODO document, podman only "portmappings": [], "privileged": pop("privileged"), "procfs_opts": pop("procfs_opts"), # TODO document, podman only "publish_image_ports": pop("publish_all_ports"), "r_limits": [], "raw_image_name": pop("raw_image_name"), # TODO document, podman only "read_only_filesystem": pop("read_only"), "read_write_tmpfs": pop("read_write_tmpfs"), "remove": args.pop("remove", args.pop("auto_remove", None)), "resource_limits": {}, "rootfs": pop("rootfs"), "rootfs_propagation": pop("rootfs_propagation"), "sdnotifyMode": pop("sdnotifyMode"), # TODO document, podman only "seccomp_policy": pop("seccomp_policy"), # TODO document, podman only "seccomp_profile_path": pop("seccomp_profile_path"), # TODO document, podman only "secrets": [], # TODO document, podman only "selinux_opts": pop("security_opt"), "shm_size": to_bytes(pop("shm_size")), "static_mac": pop("mac_address"), "stdin": pop("stdin_open"), "stop_signal": pop("stop_signal"), "stop_timeout": pop("stop_timeout"), # TODO document, podman only "sysctl": pop("sysctls"), "systemd": pop("systemd"), # TODO document, podman only "terminal": pop("tty"), "timezone": pop("timezone"), "umask": pop("umask"), # TODO document, podman only "unified": pop("unified"), # TODO document, podman only "unmask": pop("unmasked_paths"), # TODO document, podman only "use_image_hosts": pop("use_image_hosts"), # TODO document, podman only "use_image_resolve_conf": pop("use_image_resolve_conf"), # TODO document, podman only "user": pop("user"), "version": pop("version"), "volumes": [], "volumes_from": pop("volumes_from"), "work_dir": pop("workdir") or pop("working_dir"), } for device in args.pop("devices", []): params["devices"].append({"path": device}) for item in args.pop("exposed_ports", []): port, protocol = item.split("/") params["expose"][int(port)] = protocol for hostname, ip in args.pop("extra_hosts", {}).items(): params["hostadd"].append(f"{hostname}:{ip}") if "log_config" in args: params["log_configuration"]["driver"] = args["log_config"].get("Type") if "Config" in args["log_config"]: params["log_configuration"]["path"] = args["log_config"]["Config"].get("path") params["log_configuration"]["size"] = args["log_config"]["Config"].get("size") params["log_configuration"]["options"] = args["log_config"]["Config"].get("options") args.pop("log_config") for item in args.pop("mounts", []): normalized_item = {key.lower(): value for key, value in item.items()} mount_point = { "destination": normalized_item.get("target"), "options": [], "source": normalized_item.get("source"), "type": normalized_item.get("type"), } # some names are different for podman-py vs REST API due to compatibility with docker # some (e.g. chown) despite listed in podman-run documentation fails with error names_dict = {"read_only": "ro", "chown": "U"} options = [] simple_options = ["propagation", "relabel"] bool_options = ["read_only", "U", "chown"] regular_options = ["consistency", "mode", "size"] for k, v in item.items(): _k = k.lower() option_name = names_dict.get(_k, _k) if _k in bool_options and v is True: options.append(option_name) elif _k in regular_options: options.append(f'{option_name}={v}') elif _k in simple_options: options.append(v) mount_point["options"] = options params["mounts"].append(mount_point) if "pod" in args: pod = args.pop("pod") if isinstance(pod, Pod): pod = pod.id params["pod"] = pod # TODO document, podman only def parse_host_port(_container_port, _protocol, _host): result = [] port_map = {"container_port": int(_container_port), "protocol": _protocol} if _host is None: result.append(port_map) elif isinstance(_host, int) or isinstance(_host, str) and _host.isdigit(): port_map["host_port"] = int(_host) result.append(port_map) elif isinstance(_host, tuple): port_map["host_ip"] = _host[0] port_map["host_port"] = int(_host[1]) result.append(port_map) elif isinstance(_host, list): for host_list in _host: host_list_result = parse_host_port(_container_port, _protocol, host_list) result.extend(host_list_result) elif isinstance(_host, dict): _host_port = _host.get("port") if _host_port is not None: if ( isinstance(_host_port, int) or isinstance(_host_port, str) and _host_port.isdigit() ): port_map["host_port"] = int(_host_port) elif isinstance(_host_port, tuple): port_map["host_ip"] = _host_port[0] port_map["host_port"] = int(_host_port[1]) if _host.get("range"): port_map["range"] = _host.get("range") if _host.get("ip"): port_map["host_ip"] = _host.get("ip") result.append(port_map) return result for container, host in args.pop("ports", {}).items(): # avoid redefinition of the loop variable, then ensure it's a string str_container = container if isinstance(str_container, int): str_container = str(str_container) if "/" in str_container: container_port, protocol = str_container.split("/") else: container_port, protocol = str_container, "tcp" port_map_list = parse_host_port(container_port, protocol, host) params["portmappings"].extend(port_map_list) if "restart_policy" in args: params["restart_policy"] = args["restart_policy"].get("Name") params["restart_tries"] = args["restart_policy"].get("MaximumRetryCount") args.pop("restart_policy") params["resource_limits"]["pids"] = {"limit": args.pop("pids_limit", None)} params["resource_limits"]["cpu"] = { "cpus": args.pop("cpuset_cpus", None), "mems": args.pop("cpuset_mems", None), "period": args.pop("cpu_period", None), "quota": args.pop("cpu_quota", None), "realtimePeriod": args.pop("cpu_rt_period", None), "realtimeRuntime": args.pop("cpu_rt_runtime", None), "shares": args.pop("cpu_shares", None), } params["resource_limits"]["memory"] = { "disableOOMKiller": args.pop("oom_kill_disable", None), "kernel": to_bytes(args.pop("kernel_memory", None)), "kernelTCP": args.pop("kernel_memory_tcp", None), "limit": to_bytes(args.pop("mem_limit", None)), "reservation": to_bytes(args.pop("mem_reservation", None)), "swap": to_bytes(args.pop("memswap_limit", None)), "swappiness": args.pop("mem_swappiness", None), "useHierarchy": args.pop("mem_use_hierarchy", None), } for item in args.pop("ulimits", []): params["r_limits"].append( { "type": item["Name"], "hard": item["Hard"], "soft": item["Soft"], } ) for item in args.pop("volumes", {}).items(): key, value = item extended_mode = value.get('extended_mode', []) if not isinstance(extended_mode, list): raise ValueError("'extended_mode' value should be a list") options = extended_mode mode = value.get('mode') if mode is not None: if not isinstance(mode, str): raise ValueError("'mode' value should be a str") options.append(mode) # The Podman API only supports named volumes through the ``volume`` parameter. Directory # mounting needs to happen through the ``mounts`` parameter. Luckily the translation # isn't too complicated so we can just do it for the user if we suspect that the key # isn't a named volume. if NAMED_VOLUME_PATTERN.match(key): volume = {"Name": key, "Dest": value["bind"], "Options": options} params["volumes"].append(volume) else: mount_point = { "destination": value['bind'], "options": options, "source": key, "type": 'bind', } params["mounts"].append(mount_point) for item in args.pop("secrets", []): if isinstance(item, Secret): params["secrets"].append({"source": item.id}) elif isinstance(item, str): params["secrets"].append({"source": item}) elif isinstance(item, dict): secret = {} secret_opts = ["source", "target", "uid", "gid", "mode"] for k, v in item.items(): if k in secret_opts: secret.update({k: v}) params["secrets"].append(secret) if "secret_env" in args: params["secret_env"] = args.pop("secret_env", {}) if "cgroupns" in args: params["cgroupns"] = {"nsmode": args.pop("cgroupns")} if "ipc_mode" in args: params["ipcns"] = {"nsmode": args.pop("ipc_mode")} if "network_mode" in args: network_mode = args.pop("network_mode") details = network_mode.split(":") if len(details) == 2 and details[0] == "ns": params["netns"] = {"nsmode": "path", "value": details[1]} else: params["netns"] = {"nsmode": network_mode} if "pid_mode" in args: params["pidns"] = {"nsmode": args.pop("pid_mode")} if "userns_mode" in args: params["userns"] = {"nsmode": args.pop("userns_mode")} if "uts_mode" in args: params["utsns"] = {"nsmode": args.pop("uts_mode")} if len(args) > 0: raise TypeError( "Unknown keyword argument(s): " + " ,".join(f"'{k}'" for k in args.keys()) ) return params podman-py-5.4.0.1/podman/domain/containers_manager.py000066400000000000000000000127441475540541200225400ustar00rootroot00000000000000"""PodmanResource manager subclassed for Containers.""" import logging import urllib from typing import Any, Union from collections.abc import Mapping from podman import api from podman.domain.containers import Container from podman.domain.containers_create import CreateMixin from podman.domain.containers_run import RunMixin from podman.domain.manager import Manager from podman.errors import APIError logger = logging.getLogger("podman.containers") class ContainersManager(RunMixin, CreateMixin, Manager): """Specialized Manager for Container resources.""" @property def resource(self): """Type[Container]: prepare_model() will create Container classes.""" return Container def exists(self, key: str) -> bool: response = self.client.get(f"/containers/{key}/exists") return response.ok def get(self, key: str) -> Container: """Get container by name or id. Args: key: Container name or id. Returns: A `Container` object corresponding to `key`. Raises: NotFound: when Container does not exist APIError: when an error return by service """ container_id = urllib.parse.quote_plus(key) response = self.client.get(f"/containers/{container_id}/json") response.raise_for_status() return self.prepare_model(attrs=response.json()) def list(self, **kwargs) -> list[Container]: """Report on containers. Keyword Args: all: If False, only show running containers. Default: False. since: Show containers created after container name or id given. before: Show containers created before container name or id given. limit: Show last N created containers. filters: Filter container reported. Available filters: - exited (int): Only containers with specified exit code - status (str): One of restarting, running, paused, exited - label (Union[str, list[str]]): Format either "key", "key=value" or a list of such. - id (str): The id of the container. - name (str): The name of the container. - ancestor (str): Filter by container ancestor. Format of [:tag], , or . - before (str): Only containers created before a particular container. Give the container name or id. - since (str): Only containers created after a particular container. Give container name or id. sparse: Ignored ignore_removed: If True, ignore failures due to missing containers. Raises: APIError: when service returns an error """ params = { "all": kwargs.get("all"), "filters": kwargs.get("filters", {}), "limit": kwargs.get("limit"), } if "before" in kwargs: params["filters"]["before"] = kwargs.get("before") if "since" in kwargs: params["filters"]["since"] = kwargs.get("since") # filters formatted last because some kwargs may need to be mapped into filters params["filters"] = api.prepare_filters(params["filters"]) response = self.client.get("/containers/json", params=params) response.raise_for_status() return [self.prepare_model(attrs=i) for i in response.json()] def prune(self, filters: Mapping[str, str] = None) -> dict[str, Any]: """Delete stopped containers. Args: filters: Criteria for determining containers to remove. Available keys are: - until (str): Delete containers before this time - label (list[str]): Labels associated with containers Returns: Keys: - ContainersDeleted (list[str]): Identifiers of deleted containers. - SpaceReclaimed (int): Amount of disk space reclaimed in bytes. Raises: APIError: when service reports an error """ params = {"filters": api.prepare_filters(filters)} response = self.client.post("/containers/prune", params=params) response.raise_for_status() results = {"ContainersDeleted": [], "SpaceReclaimed": 0} for entry in response.json(): if entry.get("Err") is not None: raise APIError( entry["Err"], response=response, explanation=f"""Failed to prune container '{entry["Id"]}'""", ) results["ContainersDeleted"].append(entry["Id"]) results["SpaceReclaimed"] += entry["Size"] return results def remove(self, container_id: Union[Container, str], **kwargs): """Delete container. Podman only Args: container_id: identifier of Container to delete. Keyword Args: v (bool): Delete associated volumes as well. link (bool): Ignored. force (bool): Kill a running container before deleting. """ if isinstance(container_id, Container): container_id = container_id.id # v is used for the compat endpoint while volumes is used for the libpod endpoint params = {"v": kwargs.get("v"), "force": kwargs.get("force"), "volumes": kwargs.get("v")} response = self.client.delete(f"/containers/{container_id}", params=params) response.raise_for_status() podman-py-5.4.0.1/podman/domain/containers_run.py000066400000000000000000000100531475540541200217210ustar00rootroot00000000000000"""Mixin to provide Container run() method.""" import logging import threading from contextlib import suppress from typing import Union from collections.abc import Generator, Iterator from podman.domain.containers import Container from podman.domain.images import Image from podman.errors import ContainerError, ImageNotFound logger = logging.getLogger("podman.containers") class RunMixin: # pylint: disable=too-few-public-methods """Class providing run() method for ContainersManager.""" def run( self, image: Union[str, Image], command: Union[str, list[str], None] = None, *, stdout=True, stderr=False, remove: bool = False, **kwargs, ) -> Union[Container, Union[Generator[str, None, None], Iterator[str]]]: """Run a container. By default, run() will wait for the container to finish and return its logs. If detach=True, run() will start the container and return a Container object rather than logs. In this case, if remove=True, run() will monitor and remove the container after it finishes running; the logs will be lost in this case. Args: image: Image to run. command: Command to run in the container. stdout: Include stdout. Default: True. stderr: Include stderr. Default: False. remove: Delete container on the client side when the container's processes exit. The `auto_remove` flag is also available to manage the removal on the daemon side. Default: False. Keyword Args: - See the create() method for keyword arguments. Returns: - When detach is True, return a Container - If stdout is True, include stdout from container in output - If stderr is True, include stderr from container in output - When stream is True, output from container is returned as a generator - Otherwise, an iterator is returned after container has finished Raises: ContainerError: when Container exists with a non-zero code ImageNotFound: when Image not found by Podman service APIError: when Podman service reports an error """ if isinstance(image, Image): image = image.id if isinstance(command, str): command = [command] try: container = self.create(image=image, command=command, **kwargs) except ImageNotFound: self.podman_client.images.pull(image, platform=kwargs.get("platform")) container = self.create(image=image, command=command, **kwargs) container.start() container.reload() def remove_container(container_object: Container) -> None: """ Wait the container to finish and remove it. Args: container_object: Container object """ container_object.wait() # Wait for the container to finish container_object.remove() # Remove the container if kwargs.get("detach", False): if remove: # Start a background thread to remove the container after finishing threading.Thread(target=remove_container, args=(container,)).start() return container with suppress(KeyError): log_type = container.attrs["HostConfig"]["LogConfig"]["Type"] log_iter = None if log_type in ("json-file", "journald"): log_iter = container.logs(stdout=stdout, stderr=stderr, stream=True, follow=True) exit_status = container.wait() if exit_status != 0: log_iter = None if not kwargs.get("auto_remove", False): log_iter = container.logs(stdout=False, stderr=True) if remove: container.remove() if exit_status != 0: raise ContainerError(container, exit_status, command, image, log_iter) return log_iter if kwargs.get("stream", False) or log_iter is None else b"".join(log_iter) podman-py-5.4.0.1/podman/domain/events.py000066400000000000000000000033221475540541200201750ustar00rootroot00000000000000"""Model and Manager for Event resources.""" import json import logging from datetime import datetime from typing import Any, Optional, Union from collections.abc import Iterator from podman import api from podman.api.client import APIClient logger = logging.getLogger("podman.events") class EventsManager: # pylint: disable=too-few-public-methods """Specialized Manager for Event resources.""" def __init__(self, client: APIClient) -> None: """Initialize EventManager object. Args: client: Connection to Podman service. """ self.client = client def list( self, since: Union[datetime, int, None] = None, until: Union[datetime, int, None] = None, filters: Optional[dict[str, Any]] = None, decode: bool = False, ) -> Iterator[Union[str, dict[str, Any]]]: """Report on networks. Args: decode: When True, decode stream into dict's. Default: False filters: Criteria for including events. since: Get events newer than this time. until: Get events older than this time. Yields: When decode is True, Iterator[dict[str, Any]] When decode is False, Iterator[str] """ params = { "filters": api.prepare_filters(filters), "since": api.prepare_timestamp(since), "stream": True, "until": api.prepare_timestamp(until), } response = self.client.get("/events", params=params, stream=True) response.raise_for_status() for item in response.iter_lines(): if decode: yield json.loads(item) else: yield item podman-py-5.4.0.1/podman/domain/images.py000066400000000000000000000107311475540541200201400ustar00rootroot00000000000000"""Model and Manager for Image resources.""" import logging from typing import Any, Optional, Literal, Union from collections.abc import Iterator import urllib.parse from podman.api import DEFAULT_CHUNK_SIZE from podman.domain.manager import PodmanResource from podman.errors import ImageNotFound, InvalidArgument logger = logging.getLogger("podman.images") class Image(PodmanResource): """Details and configuration for an Image managed by the Podman service.""" def __repr__(self) -> str: return f"""<{self.__class__.__name__}: '{"', '".join(self.tags)}'>""" @property def labels(self): """dict[str, str]: Return labels associated with Image.""" image_labels = self.attrs.get("Labels") if image_labels is None or len(image_labels) == 0: return {} return image_labels @property def tags(self): """list[str]: Return tags from Image.""" repo_tags = self.attrs.get("RepoTags") if repo_tags is None or len(repo_tags) == 0: return [] return [tag for tag in repo_tags if tag != ":"] def history(self) -> list[dict[str, Any]]: """Returns history of the Image. Raises: APIError: when service returns an error """ response = self.client.get(f"/images/{self.id}/history") response.raise_for_status(not_found=ImageNotFound) return response.json() def remove( self, **kwargs ) -> list[dict[Literal["Deleted", "Untagged", "Errors", "ExitCode"], Union[str, int]]]: """Delete image from Podman service. Podman only Keyword Args: force: Delete Image even if in use noprune: Ignored. Returns: Report on which images were deleted and untagged, including any reported errors. Raises: ImageNotFound: when image does not exist APIError: when service returns an error """ return self.manager.remove(self.id, **kwargs) def save( self, chunk_size: Optional[int] = DEFAULT_CHUNK_SIZE, named: Union[str, bool] = False, ) -> Iterator[bytes]: """Returns Image as tarball. Format is set to docker-archive, this allows load() to import this tarball. Args: chunk_size: If None, data will be streamed in received buffer size. If not None, data will be returned in sized buffers. Default: 2MB named (str or bool): If ``False`` (default), the tarball will not retain repository and tag information for this image. If set to ``True``, the first tag in the :py:attr:`~tags` list will be used to identify the image. Alternatively, any element of the :py:attr:`~tags` list can be used as an argument to use that specific tag as the saved identifier. Raises: APIError: When service returns an error InvalidArgument: When the provided Tag name is not valid for the image. """ img = self.id if named: img = urllib.parse.quote(self.tags[0] if self.tags else img) if isinstance(named, str): if named not in self.tags: raise InvalidArgument(f"'{named}' is not a valid tag for this image") img = urllib.parse.quote(named) response = self.client.get( f"/images/{img}/get", params={"format": ["docker-archive"]}, stream=True ) response.raise_for_status(not_found=ImageNotFound) return response.iter_content(chunk_size=chunk_size) def tag( self, repository: str, tag: Optional[str], force: bool = False, # pylint: disable=unused-argument ) -> bool: """Tag Image into repository. Args: repository: The repository for tagging Image. tag: optional tag name. force: Ignore client errors Returns: True, when operational succeeds. Raises: ImageNotFound: when service cannot find image APIError: when service returns an error """ params = {"repo": repository, "tag": tag} response = self.client.post(f"/images/{self.id}/tag", params=params) if response.ok: return True if force and response.status_code <= 500: return False response.raise_for_status(not_found=ImageNotFound) return False podman-py-5.4.0.1/podman/domain/images_build.py000066400000000000000000000206611475540541200213220ustar00rootroot00000000000000"""Mixin for Image build support.""" import json import logging import pathlib import random import re import shutil import tempfile from typing import Any from collections.abc import Iterator import itertools from podman import api from podman.domain.images import Image from podman.errors import BuildError, PodmanError, ImageNotFound logger = logging.getLogger("podman.images") class BuildMixin: """Class providing build method for ImagesManager.""" # pylint: disable=too-many-locals,too-many-branches,too-few-public-methods,too-many-statements def build(self, **kwargs) -> tuple[Image, Iterator[bytes]]: """Returns built image. Keyword Args: path (str) – Path to the directory containing the Dockerfile fileobj – A file object to use as the Dockerfile. (Or an IO object) tag (str) – A tag to add to the final image quiet (bool) – Whether to return the status nocache (bool) – Don’t use the cache when set to True rm (bool) – Remove intermediate containers. Default True timeout (int) – HTTP timeout custom_context (bool) – Optional if using fileobj (ignored) encoding (str) – The encoding for a stream. Set to gzip for compressing (ignored) pull (bool) – Downloads any updates to the FROM image in Dockerfile forcerm (bool) – Always remove intermediate containers, even after unsuccessful builds dockerfile (str) – full path to the Dockerfile / Containerfile buildargs (Mapping[str,str) – A dictionary of build arguments container_limits (dict[str, Union[int,str]]) – A dictionary of limits applied to each container created by the build process. Valid keys: - memory (int): set memory limit for build - memswap (int): Total memory (memory + swap), -1 to disable swap - cpushares (int): CPU shares (relative weight) - cpusetcpus (str): CPUs in which to allow execution, For example, "0-3", "0,1" - cpuperiod (int): CPU CFS (Completely Fair Scheduler) period (Podman only) - cpuquota (int): CPU CFS (Completely Fair Scheduler) quota (Podman only) shmsize (int) – Size of /dev/shm in bytes. The size must be greater than 0. If omitted the system uses 64MB labels (Mapping[str,str]) – A dictionary of labels to set on the image cache_from (list[str]) – A list of image's identifier used for build cache resolution target (str) – Name of the build-stage to build in a multi-stage Dockerfile network_mode (str) – networking mode for the run commands during build squash (bool) – Squash the resulting images layers into a single layer. extra_hosts (dict[str,str]) – Extra hosts to add to /etc/hosts in building containers, as a mapping of hostname to IP address. platform (str) – Platform in the format os[/arch[/variant]]. isolation (str) – Isolation technology used during build. (ignored) use_config_proxy (bool) – (ignored) http_proxy (bool) - Inject http proxy environment variables into container (Podman only) layers (bool) - Cache intermediate layers during build. output (str) - specifies if any custom build output is selected for following build. outputformat (str) - The format of the output image's manifest and configuration data. Returns: first item is the podman.domain.images.Image built second item is the build logs Raises: BuildError: when there is an error during the build APIError: when service returns an error TypeError: when neither path nor fileobj is not specified """ params = self._render_params(kwargs) body = None path = None if "fileobj" in kwargs: path = tempfile.TemporaryDirectory() # pylint: disable=consider-using-with filename = pathlib.Path(path.name) / params["dockerfile"] with open(filename, "w", encoding='utf-8') as file: shutil.copyfileobj(kwargs["fileobj"], file) body = api.create_tar(anchor=path.name, gzip=kwargs.get("gzip", False)) elif "path" in kwargs: filename = pathlib.Path(kwargs["path"]) / params["dockerfile"] # The Dockerfile will be copied into the context_dir if needed params["dockerfile"] = api.prepare_containerfile(kwargs["path"], str(filename)) excludes = api.prepare_containerignore(kwargs["path"]) body = api.create_tar( anchor=kwargs["path"], exclude=excludes, gzip=kwargs.get("gzip", False) ) post_kwargs = {} if kwargs.get("timeout"): post_kwargs["timeout"] = float(kwargs.get("timeout")) response = self.client.post( "/build", params=params, data=body, headers={ "Content-type": "application/x-tar", # "X-Registry-Config": "TODO", }, stream=True, **post_kwargs, ) if hasattr(body, "close"): body.close() if hasattr(path, "cleanup"): path.cleanup() response.raise_for_status(not_found=ImageNotFound) image_id = unknown = None marker = re.compile(r"(^[0-9a-f]+)\n$") report_stream, stream = itertools.tee(response.iter_lines()) for line in stream: result = json.loads(line) if "error" in result: raise BuildError(result["error"], report_stream) if "stream" in result: match = marker.match(result["stream"]) if match: image_id = match.group(1) unknown = line if image_id: return self.get(image_id), report_stream raise BuildError(unknown or "Unknown", report_stream) @staticmethod def _render_params(kwargs) -> dict[str, list[Any]]: """Map kwargs to query parameters. All unsupported kwargs are silently ignored. """ if "path" not in kwargs and "fileobj" not in kwargs: raise TypeError("Either path or fileobj must be provided.") if "gzip" in kwargs and "encoding" in kwargs: raise PodmanError("Custom encoding not supported when gzip enabled.") params = { "dockerfile": kwargs.get("dockerfile"), "forcerm": kwargs.get("forcerm"), "httpproxy": kwargs.get("http_proxy"), "networkmode": kwargs.get("network_mode"), "nocache": kwargs.get("nocache"), "platform": kwargs.get("platform"), "pull": kwargs.get("pull"), "q": kwargs.get("quiet"), "remote": kwargs.get("remote"), "rm": kwargs.get("rm"), "shmsize": kwargs.get("shmsize"), "squash": kwargs.get("squash"), "t": kwargs.get("tag"), "target": kwargs.get("target"), "layers": kwargs.get("layers"), "output": kwargs.get("output"), "outputformat": kwargs.get("outputformat"), } if "buildargs" in kwargs: params["buildargs"] = json.dumps(kwargs.get("buildargs")) if "cache_from" in kwargs: params["cachefrom"] = json.dumps(kwargs.get("cache_from")) if "container_limits" in kwargs: params["cpuperiod"] = kwargs["container_limits"].get("cpuperiod") params["cpuquota"] = kwargs["container_limits"].get("cpuquota") params["cpusetcpus"] = kwargs["container_limits"].get("cpusetcpus") params["cpushares"] = kwargs["container_limits"].get("cpushares") params["memory"] = kwargs["container_limits"].get("memory") params["memswap"] = kwargs["container_limits"].get("memswap") if "extra_hosts" in kwargs: params["extrahosts"] = json.dumps(kwargs.get("extra_hosts")) if "labels" in kwargs: params["labels"] = json.dumps(kwargs.get("labels")) if params["dockerfile"] is None: params["dockerfile"] = f".containerfile.{random.getrandbits(160):x}" # Remove any unset parameters return dict(filter(lambda i: i[1] is not None, params.items())) podman-py-5.4.0.1/podman/domain/images_manager.py000066400000000000000000000515631475540541200216420ustar00rootroot00000000000000"""PodmanResource manager subclassed for Images.""" import builtins import io import json import logging import os import urllib.parse from typing import Any, Literal, Optional, Union from collections.abc import Iterator, Mapping, Generator from pathlib import Path import requests from podman import api from podman.api.parse_utils import parse_repository from podman.api.http_utils import encode_auth_header from podman.domain.images import Image from podman.domain.images_build import BuildMixin from podman.domain.json_stream import json_stream from podman.domain.manager import Manager from podman.domain.registry_data import RegistryData from podman.errors import APIError, ImageNotFound, PodmanError try: from rich.progress import ( Progress, TextColumn, BarColumn, TaskProgressColumn, TimeRemainingColumn, ) except (ImportError, ModuleNotFoundError): Progress = None logger = logging.getLogger("podman.images") class ImagesManager(BuildMixin, Manager): """Specialized Manager for Image resources.""" @property def resource(self): """Type[podman.domain.images.Image]: prepare_model() will create Image classes.""" return Image def exists(self, key: str) -> bool: """Return true when image exists.""" key = urllib.parse.quote_plus(key) response = self.client.get(f"/images/{key}/exists") return response.ok def list(self, **kwargs) -> builtins.list[Image]: """Report on images. Keyword Args: name (str) – Only show images belonging to the repository name all (bool) – Show intermediate image layers. By default, these are filtered out. filters (Mapping[str, Union[str, list[str]]) – Filters to be used on the image list. Available filters: - dangling (bool) - label (Union[str, list[str]]): format either "key" or "key=value" Raises: APIError: when service returns an error """ filters = kwargs.get("filters", {}).copy() if name := kwargs.get("name"): filters["reference"] = name params = { "all": kwargs.get("all"), "filters": api.prepare_filters(filters=filters), } response = self.client.get("/images/json", params=params) if response.status_code == requests.codes.not_found: return [] response.raise_for_status() return [self.prepare_model(attrs=i) for i in response.json()] # pylint is flagging 'name' here vs. 'key' parameter in super.get() def get(self, name: str) -> Image: # pylint: disable=arguments-differ,arguments-renamed """Returns an image by name or id. Args: name: Image id or name for which to search Raises: ImageNotFound: when image does not exist APIError: when service returns an error """ name = urllib.parse.quote_plus(name) response = self.client.get(f"/images/{name}/json") response.raise_for_status(not_found=ImageNotFound) return self.prepare_model(response.json()) def get_registry_data( self, name: str, auth_config=Mapping[str, str], # pylint: disable=unused-argument ) -> RegistryData: """Returns registry data for an image. Provided for compatibility Args: name: Image name auth_config: Override configured credentials. Keys username and password are required. Raises: APIError: when service returns an error """ # FIXME populate attrs using auth_config image = self.get(name) return RegistryData( image_name=name, attrs=image.attrs, client=self.client, collection=self, ) def load( self, data: Optional[bytes] = None, file_path: Optional[os.PathLike] = None ) -> Generator[bytes, None, None]: """Restore an image previously saved. Args: data: Image to be loaded in tarball format. file_path: Path of the Tarball. It works with both str and Path-like objects Raises: APIError: When service returns an error. PodmanError: When the arguments are not set correctly. """ # TODO fix podman swagger cannot use this header! # headers = {"Content-type": "application/x-www-form-urlencoded"} # Check that exactly one of the data or file_path is provided if not data and not file_path: raise PodmanError("The 'data' or 'file_path' parameter should be set.") if data and file_path: raise PodmanError( "Only one parameter should be set from 'data' and 'file_path' parameters." ) post_data = data if file_path: # Convert to Path if file_path is a string file_path_object = Path(file_path) post_data = file_path_object.read_bytes() # Read the tarball file as bytes # Make the client request before entering the generator response = self.client.post( "/images/load", data=post_data, headers={"Content-type": "application/x-tar"} ) response.raise_for_status() # Catch any errors before proceeding def _generator(body: dict) -> Generator[bytes, None, None]: # Iterate and yield images from response body for item in body["Names"]: yield self.get(item) # Pass the response body to the generator return _generator(response.json()) def prune( self, all: Optional[bool] = False, # pylint: disable=redefined-builtin external: Optional[bool] = False, filters: Optional[Mapping[str, Any]] = None, ) -> dict[Literal["ImagesDeleted", "SpaceReclaimed"], Any]: """Delete unused images. The Untagged keys will always be "". Args: all: Remove all images not in use by containers, not just dangling ones. external: Remove images even when they are used by external containers (e.g, by build containers). filters: Qualify Images to prune. Available filters: - dangling (bool): when true, only delete unused and untagged images. - label: (dict): filter by label. Examples: filters={"label": {"key": "value"}} filters={"label!": {"key": "value"}} - until (str): Delete images older than this timestamp. Raises: APIError: when service returns an error """ params = { "all": all, "external": external, "filters": api.prepare_filters(filters), } response = self.client.post("/images/prune", params=params) response.raise_for_status() deleted: builtins.list[dict[str, str]] = [] error: builtins.list[str] = [] reclaimed: int = 0 # If the prune doesn't remove images, the API returns "null" # and it's interpreted as None (NoneType) # so the for loop throws "TypeError: 'NoneType' object is not iterable". # The below if condition fixes this issue. if response.json() is not None: for element in response.json(): if "Err" in element and element["Err"] is not None: error.append(element["Err"]) else: reclaimed += element["Size"] deleted.append( { "Deleted": element["Id"], "Untagged": "", } ) if len(error) > 0: raise APIError(response.url, response=response, explanation="; ".join(error)) return { "ImagesDeleted": deleted, "SpaceReclaimed": reclaimed, } def prune_builds(self) -> dict[Literal["CachesDeleted", "SpaceReclaimed"], Any]: """Delete builder cache. Method included to complete API, the operation always returns empty CacheDeleted and zero SpaceReclaimed. """ return {"CachesDeleted": [], "SpaceReclaimed": 0} def push( self, repository: str, tag: Optional[str] = None, **kwargs ) -> Union[str, Iterator[Union[str, dict[str, Any]]]]: """Push Image or repository to the registry. Args: repository: Target repository for push tag: Tag to push, if given Keyword Args: auth_config (Mapping[str, str]: Override configured credentials. Must include username and password keys. decode (bool): return data from server as dict[str, Any]. Ignored unless stream=True. destination (str): alternate destination for image. (Podman only) stream (bool): return output as blocking generator. Default: False. tlsVerify (bool): Require TLS verification. format (str): Manifest type (oci, v2s1, or v2s2) to use when pushing an image. Default is manifest type of source, with fallbacks. Raises: APIError: when service returns an error """ auth_config: Optional[dict[str, str]] = kwargs.get("auth_config") headers = { # A base64url-encoded auth configuration "X-Registry-Auth": encode_auth_header(auth_config) if auth_config else "" } params = { "destination": kwargs.get("destination"), "tlsVerify": kwargs.get("tlsVerify"), "format": kwargs.get("format"), } name = f'{repository}:{tag}' if tag else repository name = urllib.parse.quote_plus(name) response = self.client.post(f"/images/{name}/push", params=params, headers=headers) response.raise_for_status(not_found=ImageNotFound) tag_count = 0 if tag is None else 1 body = [ { "status": f"Pushing repository {repository} ({tag_count} tags)", }, { "status": "Pushing", "progressDetail": {}, "id": repository, }, ] stream = kwargs.get("stream", False) decode = kwargs.get("decode", False) if stream: return self._push_helper(decode, body) with io.StringIO() as buffer: for entry in body: buffer.write(json.dumps(entry) + "\n") return buffer.getvalue() @staticmethod def _push_helper( decode: bool, body: builtins.list[dict[str, Any]] ) -> Iterator[Union[str, dict[str, Any]]]: """Helper needed to allow push() to return either a generator or a str.""" for entry in body: if decode: yield entry else: yield json.dumps(entry) # pylint: disable=too-many-locals,too-many-branches def pull( self, repository: str, tag: Optional[str] = None, all_tags: bool = False, **kwargs, ) -> Union[Image, builtins.list[Image], Iterator[str]]: """Request Podman service to pull image(s) from repository. Args: repository: Repository to pull from tag: Image tag to pull. Default: "latest". all_tags: pull all image tags from repository. Keyword Args: auth_config (Mapping[str, str]) – Override the credentials that are found in the config for this request. auth_config should contain the username and password keys to be valid. compatMode (bool) – Return the same JSON payload as the Docker-compat endpoint. Default: True. decode (bool) – Decode the JSON data from the server into dicts. Only applies with ``stream=True`` platform (str) – Platform in the format os[/arch[/variant]] progress_bar (bool) - Display a progress bar with the image pull progress (uses the compat endpoint). Default: False tls_verify (bool) - Require TLS verification. Default: True. stream (bool) - When True, the pull progress will be published as received. Default: False. Returns: When stream is True, return a generator publishing the service pull progress. If all_tags is True, return list of Image's rather than Image pulled. Raises: APIError: when service returns an error """ if tag is None or len(tag) == 0: repository, parsed_tag = parse_repository(repository) if parsed_tag is not None: tag = parsed_tag else: tag = "latest" auth_config: Optional[dict[str, str]] = kwargs.get("auth_config") headers = { # A base64url-encoded auth configuration "X-Registry-Auth": encode_auth_header(auth_config) if auth_config else "" } params = { "reference": repository, "tlsVerify": kwargs.get("tls_verify", True), "compatMode": kwargs.get("compatMode", True), } if all_tags: params["allTags"] = True else: params["reference"] = f"{repository}:{tag}" # Check if "platform" in kwargs AND it has value. if "platform" in kwargs and kwargs["platform"]: tokens = kwargs.get("platform").split("/") if 1 < len(tokens) > 3: raise ValueError(f'\'{kwargs.get("platform")}\' is not a legal platform.') params["OS"] = tokens[0] if len(tokens) > 1: params["Arch"] = tokens[1] if len(tokens) > 2: params["Variant"] = tokens[2] stream = kwargs.get("stream", False) # if the user wants a progress bar, we need to use the compat endpoint # so set that to true as well as stream so we can parse that output for the # progress bar progress_bar = kwargs.get("progress_bar", False) if progress_bar: if Progress is None: raise ModuleNotFoundError('progress_bar requires \'rich.progress\' module') params["compatMode"] = True stream = True response = self.client.post("/images/pull", params=params, stream=stream, headers=headers) response.raise_for_status(not_found=ImageNotFound) if progress_bar: tasks = {} print("Pulling", params["reference"]) progress = Progress( TextColumn("[progress.description]{task.description}"), BarColumn(complete_style="default", finished_style="green"), TaskProgressColumn(), TimeRemainingColumn(), ) with progress: for line in response.iter_lines(): decoded_line = json.loads(line.decode('utf-8')) self.__show_progress_bar(decoded_line, progress, tasks) return None if stream: return self._stream_helper(response, decode=kwargs.get("decode")) for item in reversed(list(response.iter_lines())): obj = json.loads(item) if all_tags and "images" in obj: images: builtins.list[Image] = [] for name in obj["images"]: images.append(self.get(name)) return images if "id" in obj: return self.get(obj["id"]) return self.resource() def __show_progress_bar(self, line, progress, tasks): completed = False if line['status'] == 'Download complete': description = f'[green][Download complete {line["id"]}]' completed = True elif line['status'] == 'Downloading': description = f'[bold][Downloading {line["id"]}]' else: # skip other statuses return task_id = line["id"] if task_id not in tasks.keys(): if completed: # some layers are really small that they download immediately without showing # anything as Downloading in the stream. # For that case, show a completed progress bar tasks[task_id] = progress.add_task(description, total=100, completed=100) else: tasks[task_id] = progress.add_task( description, total=line['progressDetail']['total'] ) else: if completed: # due to the stream, the Download complete output can happen before the Downloading # bar outputs the 100%. So when we detect that the download is in fact complete, # update the progress bar to show 100% progress.update(tasks[task_id], description=description, total=100, completed=100) else: progress.update(tasks[task_id], completed=line['progressDetail']['current']) def remove( self, image: Union[Image, str], force: Optional[bool] = None, noprune: bool = False, # pylint: disable=unused-argument ) -> builtins.list[dict[Literal["Deleted", "Untagged", "Errors", "ExitCode"], Union[str, int]]]: """Delete image from Podman service. Args: image: Name or Id of Image to remove force: Delete Image even if in use noprune: Ignored. Raises: ImageNotFound: when image does not exist APIError: when service returns an error """ if isinstance(image, Image): image = image.id response = self.client.delete(f"/images/{image}", params={"force": force}) response.raise_for_status(not_found=ImageNotFound) body = response.json() results: builtins.list[dict[str, Union[int, str]]] = [] for key in ("Deleted", "Untagged", "Errors"): if key in body: for element in body[key]: results.append({key: element}) results.append({"ExitCode": body["ExitCode"]}) return results def search(self, term: str, **kwargs) -> builtins.list[dict[str, Any]]: """Search Images on registries. Args: term: Used to target Image results. Keyword Args: filters (Mapping[str, list[str]): Refine results of search. Available filters: - is-automated (bool): Image build is automated. - is-official (bool): Image build is owned by product provider. - stars (int): Image has at least this number of stars. noTrunc (bool): Do not truncate any result string. Default: True. limit (int): Maximum number of results. listTags (bool): list the available tags in the repository. Default: False Raises: APIError: when service returns an error """ params = { "filters": api.prepare_filters(kwargs.get("filters")), "limit": kwargs.get("limit"), "noTrunc": True, "term": [term], } if "listTags" in kwargs: params["listTags"] = kwargs.get("listTags") response = self.client.get("/images/search", params=params) response.raise_for_status(not_found=ImageNotFound) return response.json() def scp( self, source: str, dest: Optional[str] = None, quiet: Optional[bool] = False, ) -> str: """Securely copy images between hosts. Args: source: source connection/image dest: destination connection/image quiet: do not print save/load output, only the image Returns: A string containing the loaded image Raises: APIError: when service returns an error """ params = {"quiet": quiet} if dest is not None: params["destination"] = dest response = self.client.post(f"/images/scp/{source}", params=params) response.raise_for_status() return response.json() def _stream_helper(self, response, decode=False): """Generator for data coming from a chunked-encoded HTTP response.""" if response.raw._fp.chunked: if decode: yield from json_stream(self._stream_helper(response, False)) else: reader = response.raw while not reader.closed: # this read call will block until we get a chunk data = reader.read(1) if not data: break if reader._fp.chunk_left: data += reader.read(reader._fp.chunk_left) yield data else: # Response isn't chunked, meaning we probably # encountered an error immediately yield self._result(response, json=decode) podman-py-5.4.0.1/podman/domain/ipam.py000066400000000000000000000032721475540541200176230ustar00rootroot00000000000000"""Classes to support Internet Protocol Address Management. Provided for compatibility """ from typing import Any, Optional from collections.abc import Mapping class IPAMPool(dict): """Collect IP Network configuration.""" def __init__( self, subnet: Optional[str] = None, iprange: Optional[str] = None, gateway: Optional[str] = None, aux_addresses: Optional[Mapping[str, str]] = None, ): """Create IPAMPool. Args: subnet: IP subnet in CIDR format for this network. iprange: IP range in CIDR format for endpoints on this network. gateway: IP gateway address for this network. aux_addresses: Ignored. """ super().__init__() self.update( { "AuxiliaryAddresses": aux_addresses, "Gateway": gateway, "IPRange": iprange, "Subnet": subnet, } ) class IPAMConfig(dict): """Collect IP Address configuration.""" def __init__( self, driver: Optional[str] = "host-local", pool_configs: Optional[list[IPAMPool]] = None, options: Optional[Mapping[str, Any]] = None, ): """Create IPAMConfig. Args: driver: Network driver to use with this network. pool_configs: Network and endpoint information. Podman only supports one pool. options: Options to provide to the Network driver. """ super().__init__() self.update( { "Config": pool_configs or [], "Driver": driver, "Options": options or {}, } ) podman-py-5.4.0.1/podman/domain/json_stream.py000066400000000000000000000042471475540541200212240ustar00rootroot00000000000000import json import json.decoder from podman.errors import StreamParseError json_decoder = json.JSONDecoder() def stream_as_text(stream): """ Given a stream of bytes or text, if any of the items in the stream are bytes convert them to text. This function can be removed once we return text streams instead of byte streams. """ for data in stream: _data = data if not isinstance(data, str): _data = data.decode('utf-8', 'replace') yield _data def json_splitter(buffer): """Attempt to parse a json object from a buffer. If there is at least one object, return it and the rest of the buffer, otherwise return None. """ buffer = buffer.strip() try: obj, index = json_decoder.raw_decode(buffer) rest = buffer[json.decoder.WHITESPACE.match(buffer, index).end() :] return obj, rest except ValueError: return None def json_stream(stream): """Given a stream of text, return a stream of json objects. This handles streams which are inconsistently buffered (some entries may be newline delimited, and others are not). """ return split_buffer(stream, json_splitter, json_decoder.decode) def line_splitter(buffer, separator='\n'): index = buffer.find(str(separator)) if index == -1: return None return buffer[: index + 1], buffer[index + 1 :] def split_buffer(stream, splitter=None, decoder=lambda a: a): """Given a generator which yields strings and a splitter function, joins all input, splits on the separator and yields each chunk. Unlike string.split(), each chunk includes the trailing separator, except for the last one if none was found on the end of the input. """ splitter = splitter or line_splitter buffered = '' for data in stream_as_text(stream): buffered += data while True: buffer_split = splitter(buffered) if buffer_split is None: break item, buffered = buffer_split yield item if buffered: try: yield decoder(buffered) except Exception as e: raise StreamParseError(e) from e podman-py-5.4.0.1/podman/domain/manager.py000066400000000000000000000106231475540541200203050ustar00rootroot00000000000000"""Base classes for PodmanResources and Manager's.""" from abc import ABC, abstractmethod from collections import abc from typing import Any, Optional, TypeVar, Union from collections.abc import Mapping from podman.api.client import APIClient # Methods use this Type when a subclass of PodmanResource is expected. PodmanResourceType: TypeVar = TypeVar("PodmanResourceType", bound="PodmanResource") class PodmanResource(ABC): # noqa: B024 """Base class for representing resource of a Podman service. Attributes: attrs: Mapping of attributes for resource from Podman service """ def __init__( self, attrs: Optional[Mapping[str, Any]] = None, client: Optional[APIClient] = None, collection: Optional["Manager"] = None, podman_client: Optional["PodmanClient"] = None, ): """Initialize base class for PodmanResource's. Args: attrs: Mapping of attributes for resource from Podman service. client: Configured connection to a Podman service. collection: Manager of this category of resource, named `collection` for compatibility podman_client: PodmanClient() configured to connect to Podman object. """ super().__init__() self.client = client self.manager = collection self.podman_client = podman_client self.attrs = {} if attrs is not None: self.attrs.update(attrs) def __repr__(self): return f"<{self.__class__.__name__}: {self.short_id}>" def __eq__(self, other): return isinstance(other, self.__class__) and self.id == other.id def __hash__(self): return hash(f"{self.__class__.__name__}:{self.id}") @property def id(self): # pylint: disable=invalid-name """str: Returns the identifier for the object.""" return self.attrs.get("Id") @property def short_id(self): """str: Returns truncated identifier. 'sha256' preserved when included in the id. No attempt is made to ensure the returned value is semantically meaningful for all resources. """ if self.id.startswith("sha256:"): return self.id[:17] return self.id[:10] def reload(self) -> None: """Refresh this object's data from the service.""" latest = self.manager.get(self.id) self.attrs = latest.attrs class Manager(ABC): """Base class for representing a Manager of resources for a Podman service.""" @property @abstractmethod def resource(self): """Type[PodmanResource]: Class which the factory method prepare_model() will use.""" def __init__( self, client: Optional[APIClient] = None, podman_client: Optional["PodmanClient"] = None ) -> None: """Initialize Manager() object. Args: client: APIClient() configured to connect to Podman service. podman_client: PodmanClient() configured to connect to Podman object. """ super().__init__() self.client = client self.podman_client = podman_client @abstractmethod def exists(self, key: str) -> bool: """Returns True if resource exists. Podman only. Notes: This method does _not_ provide any mutex mechanism. """ @abstractmethod def get(self, key: str) -> PodmanResourceType: """Returns representation of resource.""" @abstractmethod def list(self, **kwargs) -> list[PodmanResourceType]: """Returns list of resources.""" def prepare_model(self, attrs: Union[PodmanResource, Mapping[str, Any]]) -> PodmanResourceType: """Create a model from a set of attributes.""" # Refresh existing PodmanResource. if isinstance(attrs, PodmanResource): attrs.client = self.client attrs.podman_client = self.podman_client attrs.collection = self return attrs # Instantiate new PodmanResource from Mapping[str, Any] if isinstance(attrs, abc.Mapping): # TODO Determine why pylint is reporting typing.Type not callable # pylint: disable=not-callable return self.resource( attrs=attrs, client=self.client, podman_client=self.podman_client, collection=self ) # pylint: disable=broad-exception-raised raise Exception(f"Can't create {self.resource.__name__} from {attrs}") podman-py-5.4.0.1/podman/domain/manifests.py000066400000000000000000000170641475540541200206720ustar00rootroot00000000000000"""Model and Manager for Manifest resources.""" import logging import urllib.parse from contextlib import suppress from typing import Any, Optional, Union from podman import api from podman.domain.images import Image from podman.domain.manager import Manager, PodmanResource from podman.errors import ImageNotFound logger = logging.getLogger("podman.manifests") class Manifest(PodmanResource): """Details and configuration for a manifest managed by the Podman service.""" @property def id(self): """str: Returns the identifier of the manifest list.""" with suppress(KeyError, TypeError, IndexError): digest = self.attrs["manifests"][0]["digest"] if digest.startswith("sha256:"): return digest[7:] return digest return self.name @property def name(self): """str: Returns the human-formatted identifier of the manifest list.""" return self.attrs.get("names") @property def quoted_name(self): """str: name quoted as path parameter.""" return urllib.parse.quote_plus(self.name) @property def names(self): """list[str]: Returns the identifier of the manifest.""" return self.name @property def media_type(self): """Optional[str]: Returns the Media/MIME type for this manifest.""" return self.attrs.get("mediaType") @property def version(self): """int: Returns the schema version type for this manifest.""" return self.attrs.get("schemaVersion") def add(self, images: list[Union[Image, str]], **kwargs) -> None: """Add Image to manifest list. Args: images: List of Images to be added to manifest. Keyword Args: all (bool): annotation (dict[str, str]): arch (str): features (list[str]): os (str): os_version (str): variant (str): Raises: ImageNotFound: when Image(s) could not be found APIError: when service reports an error """ data = { "all": kwargs.get("all"), "annotation": kwargs.get("annotation"), "arch": kwargs.get("arch"), "features": kwargs.get("features"), "images": [], "os": kwargs.get("os"), "os_version": kwargs.get("os_version"), "variant": kwargs.get("variant"), "operation": "update", } for item in images: # avoid redefinition of the loop variable, then ensure it's an image img_item = item if isinstance(img_item, Image): img_item = img_item.attrs["RepoTags"][0] data["images"].append(img_item) data = api.prepare_body(data) response = self.client.put(f"/manifests/{self.quoted_name}", data=data) response.raise_for_status(not_found=ImageNotFound) return self.reload() def push( self, destination: str, all: Optional[bool] = None, # pylint: disable=redefined-builtin ) -> None: """Push a manifest list or image index to a registry. Args: destination: Target for push. all: Push all images. Raises: NotFound: when the Manifest could not be found APIError: when service reports an error """ params = { "all": all, "destination": destination, } response = self.client.post(f"/manifests/{self.quoted_name}/push", params=params) response.raise_for_status() def remove(self, digest: str) -> None: """Remove Image digest from manifest list. Args: digest: Image digest to be removed. Should a full Image reference be provided, the digest will be parsed out. Raises: ImageNotFound: when the Image could not be found APIError: when service reports an error """ if "@" in digest: digest = digest.split("@", maxsplit=2)[1] data = {"operation": "remove", "images": [digest]} data = api.prepare_body(data) response = self.client.put(f"/manifests/{self.quoted_name}", data=data) response.raise_for_status(not_found=ImageNotFound) return self.reload() def reload(self) -> None: """Refresh this object's data from the service.""" latest = self.manager.get(self.name) self.attrs = latest.attrs class ManifestsManager(Manager): """Specialized Manager for Manifest resources.""" @property def resource(self): """Type[Manifest]: prepare_model() will create Manifest classes.""" return Manifest def create( self, name: str, images: Optional[list[Union[Image, str]]] = None, all: Optional[bool] = None, # pylint: disable=redefined-builtin ) -> Manifest: """Create a Manifest. Args: name: Name of manifest list. images: Images or Image identifiers to be included in the manifest. all: When True, add all contents from images given. Raises: ValueError: when no names are provided NotFoundImage: when a given image does not exist """ params: dict[str, Any] = {} if images is not None: params["images"] = [] for item in images: # avoid redefinition of the loop variable, then ensure it's an image img_item = item if isinstance(img_item, Image): img_item = img_item.attrs["RepoTags"][0] params["images"].append(img_item) if all is not None: params["all"] = all name_quoted = urllib.parse.quote_plus(name) response = self.client.post(f"/manifests/{name_quoted}", params=params) response.raise_for_status(not_found=ImageNotFound) body = response.json() manifest = self.get(body["Id"]) manifest.attrs["names"] = name if manifest.attrs["manifests"] is None: manifest.attrs["manifests"] = [] return manifest def exists(self, key: str) -> bool: key = urllib.parse.quote_plus(key) response = self.client.get(f"/manifests/{key}/exists") return response.ok def get(self, key: str) -> Manifest: """Returns the manifest by name. To have Manifest conform with other PodmanResource's, we use the key that retrieved the Manifest be its name. Args: key: Manifest name for which to search Raises: NotFound: when manifest could not be found APIError: when service reports an error """ quoted_key = urllib.parse.quote_plus(key) response = self.client.get(f"/manifests/{quoted_key}/json") response.raise_for_status() body = response.json() if "names" not in body: body["names"] = key return self.prepare_model(attrs=body) def list(self, **kwargs) -> list[Manifest]: """Not Implemented.""" raise NotImplementedError("Podman service currently does not support listing manifests.") def remove(self, name: Union[Manifest, str]) -> dict[str, Any]: """Delete the manifest list from the Podman service.""" if isinstance(name, Manifest): name = name.name response = self.client.delete(f"/manifests/{name}") response.raise_for_status(not_found=ImageNotFound) body = response.json() body["ExitCode"] = response.status_code return body podman-py-5.4.0.1/podman/domain/networks.py000066400000000000000000000114511475540541200205470ustar00rootroot00000000000000"""Model for Network resources. Example: with PodmanClient(base_url="unix:///run/user/1000/podman/podman.sock") as client: net = client.networks.get("db_network") print(net.name, "\n") """ import hashlib import json import logging from contextlib import suppress from typing import Optional, Union from podman.domain.containers import Container from podman.domain.containers_manager import ContainersManager from podman.domain.manager import PodmanResource logger = logging.getLogger("podman.networks") class Network(PodmanResource): """Details and configuration for a networks managed by the Podman service. Attributes: attrs (dict[str, Any]): Attributes of Network reported from Podman service """ @property def id(self): # pylint: disable=invalid-name """str: Returns the identifier of the network.""" with suppress(KeyError): return self.attrs["Id"] with suppress(KeyError): sha256 = hashlib.sha256(self.attrs["name"].encode("ascii")) return sha256.hexdigest() return None @property def containers(self): """list[Container]: Returns list of Containers connected to network.""" with suppress(KeyError): container_manager = ContainersManager(client=self.client) return [container_manager.get(ident) for ident in self.attrs["Containers"].keys()] return [] @property def name(self): """str: Returns the name of the network.""" if "Name" in self.attrs: return self.attrs["Name"] if "name" in self.attrs: return self.attrs["name"] raise KeyError("Neither 'name' or 'Name' attribute found.") def reload(self): """Refresh this object's data from the service.""" latest = self.manager.get(self.name) self.attrs = latest.attrs def connect(self, container: Union[str, Container], *_, **kwargs) -> None: """Connect given container to this network. Args: container: To add to this Network Keyword Args: aliases (list[str]): Aliases to add for this endpoint driver_opt (dict[str, Any]): Options to provide to network driver ipv4_address (str): IPv4 address for given Container on this network ipv6_address (str): IPv6 address for given Container on this network link_local_ips (list[str]): list of link-local addresses links (list[Union[str, Containers]]): Ignored Raises: APIError: when Podman service reports an error """ if isinstance(container, Container): container = container.id # TODO Talk with baude on which IPAddress field is needed... ipam = { "IPv4Address": kwargs.get('ipv4_address'), "IPv6Address": kwargs.get('ipv6_address'), "Links": kwargs.get("link_local_ips"), } ipam = {k: v for (k, v) in ipam.items() if not (v is None or len(v) == 0)} endpoint_config = { "Aliases": kwargs.get("aliases"), "DriverOpts": kwargs.get("driver_opt"), "IPAddress": kwargs.get("ipv4_address", kwargs.get("ipv6_address")), "IPAMConfig": ipam, "Links": kwargs.get("link_local_ips"), "NetworkID": self.id, } endpoint_config = { k: v for (k, v) in endpoint_config.items() if not (v is None or len(v) == 0) } data = {"Container": container, "EndpointConfig": endpoint_config} data = {k: v for (k, v) in data.items() if not (v is None or len(v) == 0)} response = self.client.post( f"/networks/{self.name}/connect", data=json.dumps(data), headers={"Content-type": "application/json"}, **kwargs, ) response.raise_for_status() def disconnect(self, container: Union[str, Container], **kwargs) -> None: """Disconnect given container from this network. Args: container: To remove from this Network Keyword Args: force (bool): Force operation Raises: APIError: when Podman service reports an error """ if isinstance(container, Container): container = container.id data = {"Container": container, "Force": kwargs.get("force")} response = self.client.post(f"/networks/{self.name}/disconnect", data=json.dumps(data)) response.raise_for_status() def remove(self, force: Optional[bool] = None, **kwargs) -> None: """Remove this network. Args: force: Remove network and any associated containers Raises: APIError: when Podman service reports an error """ self.manager.remove(self.name, force=force, **kwargs) podman-py-5.4.0.1/podman/domain/networks_manager.py000066400000000000000000000154721475540541200222500ustar00rootroot00000000000000"""PodmanResource manager subclassed for Network resources. Classes and methods for manipulating network resources via Podman API service. Example: with PodmanClient(base_url="unix:///run/user/1000/podman/podman.sock") as client: for net in client.networks.list(): print(net.id, "\n") """ import ipaddress import logging from contextlib import suppress from typing import Any, Optional, Literal from podman.api import http_utils, prepare_filters from podman.domain.manager import Manager from podman.domain.networks import Network from podman.errors import APIError logger = logging.getLogger("podman.networks") class NetworksManager(Manager): """Specialized Manager for Network resources.""" @property def resource(self): """Type[Network]: prepare_model() will create Network classes.""" return Network def create(self, name: str, **kwargs) -> Network: """Create a Network resource. Args: name: Name of network to be created Keyword Args: attachable (bool): Ignored, always False. check_duplicate (bool): Ignored, always False. dns_enabled (bool): When True, do not provision DNS for this network. driver (str): Which network driver to use when creating network. enable_ipv6 (bool): Enable IPv6 on the network. ingress (bool): Ignored, always False. internal (bool): Restrict external access to the network. ipam (IPAMConfig): Optional custom IP scheme for the network. labels (dict[str, str]): Map of labels to set on the network. options (dict[str, Any]): Driver options. scope (str): Ignored, always "local". Raises: APIError: when Podman service reports an error """ data = { "name": name, "driver": kwargs.get("driver"), "dns_enabled": kwargs.get("dns_enabled"), "subnets": kwargs.get("subnets"), "ipv6_enabled": kwargs.get("enable_ipv6"), "internal": kwargs.get("internal"), "labels": kwargs.get("labels"), "options": kwargs.get("options"), } with suppress(KeyError): self._prepare_ipam(data, kwargs["ipam"]) response = self.client.post( "/networks/create", data=http_utils.prepare_body(data), headers={"Content-Type": "application/json"}, ) response.raise_for_status() return self.prepare_model(attrs=response.json()) def _prepare_ipam(self, data: dict[str, Any], ipam: dict[str, Any]): if "Driver" in ipam: data["ipam_options"] = {"driver": ipam["Driver"]} if "Config" not in ipam: return data["subnets"] = [] for cfg in ipam["Config"]: subnet = { "gateway": cfg.get("Gateway"), "subnet": cfg.get("Subnet"), } with suppress(KeyError): net = ipaddress.ip_network(cfg["IPRange"]) subnet["lease_range"] = { "start_ip": str(net[1]), "end_ip": str(net[-2]), } data["subnets"].append(subnet) def exists(self, key: str) -> bool: response = self.client.get(f"/networks/{key}/exists") return response.ok def get(self, key: str) -> Network: """Return information for the network_id. Args: key: Network name or id. Raises: NotFound: when Network does not exist APIError: when error returned by service """ response = self.client.get(f"/networks/{key}") response.raise_for_status() return self.prepare_model(attrs=response.json()) def list(self, **kwargs) -> list[Network]: """Report on networks. Keyword Args: names (list[str]): List of names to filter by. ids (list[str]): List of identifiers to filter by. filters (Mapping[str,str]): Criteria for listing networks. Available filters: - driver="bridge": Matches a network's driver. Only "bridge" is supported. - label=(Union[str, list[str]]): format either "key", "key=value" or a list of such. - type=(str): Filters networks by type, legal values are: - "custom" - "builtin" - plugin=(list[str]]): Matches CNI plugins included in a network, legal values are (Podman only): - bridge - portmap - firewall - tuning - dnsname - macvlan greedy (bool): Fetch more details for each network individually. You might want this to get the containers attached to them. Ignored. Raises: APIError: when error returned by service """ filters = kwargs.get("filters", {}) filters["name"] = kwargs.get("names") filters["id"] = kwargs.get("ids") filters = prepare_filters(filters) params = {"filters": filters} response = self.client.get("/networks/json", params=params) response.raise_for_status() return [self.prepare_model(i) for i in response.json()] def prune( self, filters: Optional[dict[str, Any]] = None ) -> dict[Literal["NetworksDeleted", "SpaceReclaimed"], Any]: """Delete unused Networks. SpaceReclaimed always reported as 0 Args: filters: Criteria for selecting volumes to delete. Ignored. Raises: APIError: when service reports error """ params = {"filters": prepare_filters(filters)} response = self.client.post("/networks/prune", params=params) response.raise_for_status() deleted: list[str] = [] for item in response.json(): if item["Error"] is not None: raise APIError( item["Error"], response=response, explanation=f"""Failed to prune network '{item["Name"]}'""", ) deleted.append(item["Name"]) return {"NetworksDeleted": deleted, "SpaceReclaimed": 0} def remove(self, name: [Network, str], force: Optional[bool] = None) -> None: """Remove Network resource. Args: name: Identifier of Network to delete. force: Remove network and any associated containers Raises: APIError: when Podman service reports an error """ if isinstance(name, Network): name = name.name response = self.client.delete(f"/networks/{name}", params={"force": force}) response.raise_for_status() podman-py-5.4.0.1/podman/domain/pods.py000066400000000000000000000065571475540541200176530ustar00rootroot00000000000000"""Model and Manager for Pod resources.""" import logging from typing import Any, Optional, Union from podman.domain.manager import PodmanResource _Timeout = Union[None, float, tuple[float, float], tuple[float, None]] logger = logging.getLogger("podman.pods") class Pod(PodmanResource): """Details and configuration for a pod managed by the Podman service.""" @property def id(self): # pylint: disable=invalid-name return self.attrs.get("ID", self.attrs.get("Id")) @property def name(self): """str: Returns name of pod.""" return self.attrs.get("Name") def kill(self, signal: Union[str, int, None] = None) -> None: """Send signal to pod. Args: signal: To be sent to pod. Raises: NotFound: when pod not found APIError: when service reports an error """ response = self.client.post(f"/pods/{self.id}/kill", params={"signal": signal}) response.raise_for_status() def pause(self) -> None: """Pause pod. Raises: NotFound: when pod not found APIError: when service reports an error """ response = self.client.post(f"/pods/{self.id}/pause") response.raise_for_status() def remove(self, force: Optional[bool] = None) -> None: """Delete pod. Args: force: When True, stop and delete all containers in pod before deleting pod. Raises: NotFound: when pod not found APIError: when service reports an error """ self.manager.remove(self.id, force=force) def restart(self) -> None: """Restart pod. Raises: NotFound: when pod not found APIError: when service reports an error """ response = self.client.post(f"/pods/{self.id}/restart") response.raise_for_status() def start(self) -> None: """Start pod. Raises: NotFound: when pod not found APIError: when service reports an error """ response = self.client.post(f"/pods/{self.id}/start") response.raise_for_status() def stop(self, timeout: _Timeout = None) -> None: """Stop pod. Raises: NotFound: when pod not found APIError: when service reports an error """ params = {"t": timeout} response = self.client.post(f"/pods/{self.id}/stop", params=params) response.raise_for_status() def top(self, **kwargs) -> dict[str, Any]: """Report on running processes in pod. Keyword Args: ps_args (str): Optional arguments passed to ps. Raises: NotFound: when pod not found APIError: when service reports an error """ params = { "ps_args": kwargs.get("ps_args"), "stream": False, } response = self.client.get(f"/pods/{self.id}/top", params=params) response.raise_for_status() if len(response.text) == 0: return {"Processes": [], "Titles": []} return response.json() def unpause(self) -> None: """Unpause pod. Raises: NotFound: when pod not found APIError: when service reports an error """ response = self.client.post(f"/pods/{self.id}/unpause") response.raise_for_status() podman-py-5.4.0.1/podman/domain/pods_manager.py000066400000000000000000000141051475540541200213310ustar00rootroot00000000000000"""PodmanResource manager subclassed for Networks.""" import builtins import json import logging from typing import Any, Optional, Union from collections.abc import Iterator from podman import api from podman.domain.manager import Manager from podman.domain.pods import Pod from podman.errors import APIError logger = logging.getLogger("podman.pods") class PodsManager(Manager): """Specialized Manager for Pod resources.""" @property def resource(self): """Type[Pod]: prepare_model() will create Pod classes.""" return Pod def create(self, name: str, **kwargs) -> Pod: """Create a Pod. Keyword Args: See (API documentation)[ https://docs.podman.io/en/latest/_static/api.html#operation/CreatePod] for complete list of keywords. """ data = {} if kwargs is None else kwargs.copy() data["name"] = name response = self.client.post("/pods/create", data=json.dumps(data)) response.raise_for_status() body = response.json() return self.get(body["Id"]) def exists(self, key: str) -> bool: """Returns True, when pod exists.""" response = self.client.get(f"/pods/{key}/exists") return response.ok # pylint is flagging 'pod_id' here vs. 'key' parameter in super.get() def get(self, pod_id: str) -> Pod: # pylint: disable=arguments-differ,arguments-renamed """Return information for Pod by name or id. Args: pod_id: Pod name or id. Raises: NotFound: when network does not exist APIError: when error returned by service """ response = self.client.get(f"/pods/{pod_id}/json") response.raise_for_status() return self.prepare_model(attrs=response.json()) def list(self, **kwargs) -> builtins.list[Pod]: """Report on pods. Keyword Args: filters (Mapping[str, str]): Criteria for listing pods. Available filters: - ctr-ids (list[str]): list of container ids to filter by. - ctr-names (list[str]): list of container names to filter by. - ctr-number (list[int]): list pods with given number of containers. - ctr-status (list[str]): list pods with containers in given state. Legal values are: "created", "running", "paused", "stopped", "exited", or "unknown" - id (str) - List pod with this id. - name (str) - List pod with this name. - status (list[str]): List pods in given state. Legal values are: "created", "running", "paused", "stopped", "exited", or "unknown" - label (list[str]): List pods with given labels. - network (list[str]): List pods associated with given Network Ids (not Names). Raises: APIError: when an error returned by service """ params = {"filters": api.prepare_filters(kwargs.get("filters"))} response = self.client.get("/pods/json", params=params) response.raise_for_status() return [self.prepare_model(attrs=i) for i in response.json()] def prune(self, filters: Optional[dict[str, str]] = None) -> dict[str, Any]: """Delete unused Pods. Returns: Dictionary Keys: - PodsDeleted (list[str]): List of pod ids deleted. - SpaceReclaimed (int): Always zero. Raises: APIError: when service reports error """ response = self.client.post("/pods/prune", params={"filters": api.prepare_filters(filters)}) response.raise_for_status() deleted: builtins.list[str] = [] for item in response.json(): if item["Err"] is not None: raise APIError( item["Err"], response=response, explanation=f"""Failed to prune pod '{item["Id"]}'""", ) deleted.append(item["Id"]) return {"PodsDeleted": deleted, "SpaceReclaimed": 0} def remove(self, pod_id: Union[Pod, str], force: Optional[bool] = None) -> None: """Delete pod. Args: pod_id: Identifier of Pod to delete. force: When True, stop and delete all containers in pod before deleting pod. Raises: NotFound: when pod not found APIError: when service reports an error Notes: Podman only. """ if isinstance(pod_id, Pod): pod_id = pod_id.id response = self.client.delete(f"/pods/{pod_id}", params={"force": force}) response.raise_for_status() def stats( self, **kwargs ) -> Union[builtins.list[dict[str, Any]], Iterator[builtins.list[dict[str, Any]]]]: """Resource usage statistics for the containers in pods. Keyword Args: all (bool): Provide statistics for all running pods. name (Union[str, list[str]]): Pods to include in report. stream (bool): Stream statistics until cancelled. Default: False. decode (bool): If True, response will be decoded into dict. Default: False. Raises: NotFound: when pod not found APIError: when service reports an error """ if "all" in kwargs and "name" in kwargs: raise ValueError("Keywords 'all' and 'name' are mutually exclusive.") # Keeping the default for stream as False to not break existing users # Should probably be changed in a newer major version to match behavior of container.stats stream = kwargs.get("stream", False) decode = kwargs.get("decode", False) params = { "all": kwargs.get("all"), "namesOrIDs": kwargs.get("name"), "stream": stream, } response = self.client.get("/pods/stats", params=params, stream=stream) response.raise_for_status() if stream: return api.stream_helper(response, decode_to_json=decode) return json.loads(response.content) if decode else response.content podman-py-5.4.0.1/podman/domain/registry_data.py000066400000000000000000000056241475540541200215410ustar00rootroot00000000000000"""Module for tracking registry metadata.""" import logging from typing import Any, Optional, Union from collections.abc import Mapping from podman import api from podman.domain.images import Image from podman.domain.manager import PodmanResource from podman.errors import InvalidArgument logger = logging.getLogger("podman.images") class RegistryData(PodmanResource): """Registry metadata about Image.""" def __init__(self, image_name: str, *args, **kwargs) -> None: """Initialize RegistryData object. Args: image_name: Name of Image. Keyword Args: client (APIClient): Configured connection to a Podman service. collection (Manager): Manager of this category of resource, named `collection` for compatibility """ super().__init__(*args, **kwargs) self.image_name = image_name self.attrs = kwargs.get("attrs") if self.attrs is None: self.attrs = self.manager.get(image_name).attrs def pull(self, platform: Optional[str] = None) -> Image: """Returns Image pulled by identifier. Args: platform: Platform for which to pull Image. Default: None (all platforms.) """ repository = api.parse_repository(self.image_name) return self.manager.pull(repository, tag=self.id, platform=platform) def has_platform(self, platform: Union[str, Mapping[str, Any]]) -> bool: """Returns True if platform is available for Image. Podman API does not support "variant" therefore it is ignored. Args: platform: Name as os[/arch[/variant]] or Mapping[str,Any] Returns: True if platform is available Raises: InvalidArgument: when platform value is not valid APIError: when service reports an error """ invalid_platform = InvalidArgument(f"'{platform}' is not a valid platform descriptor.") if platform is None: platform = {} if isinstance(platform, dict): if not {"os", "architecture"} <= platform.keys(): version = self.client.version() platform["os"] = platform.get("os", version["Os"]) platform["architecture"] = platform.get("architecture", version["Arch"]) elif isinstance(platform, str): elements = platform.split("/") if 1 < len(elements) > 3: raise invalid_platform platform = {"os": elements[0]} if len(elements) > 2: platform["variant"] = elements[2] if len(elements) > 1: platform["architecture"] = elements[1] else: raise invalid_platform return ( # Variant not carried in libpod attrs platform["os"] == self.attrs["Os"] and platform["architecture"] == self.attrs["Architecture"] ) podman-py-5.4.0.1/podman/domain/secrets.py000066400000000000000000000077201475540541200203470ustar00rootroot00000000000000"""Model and Manager for Secrets resources.""" from contextlib import suppress from typing import Any, Optional, Union from collections.abc import Mapping from podman.api import APIClient from podman.domain.manager import Manager, PodmanResource class Secret(PodmanResource): """Details and configuration for a secret registered with the Podman service.""" def __repr__(self): return f"<{self.__class__.__name__}: {self.name}>" @property def id(self): # pylint: disable=invalid-name return self.attrs.get("ID") @property def name(self): """str: name of the secret.""" with suppress(KeyError): return self.attrs['Spec']['Name'] return "" def remove( self, all: Optional[bool] = None, # pylint: disable=redefined-builtin ): """Delete secret. Args: all: When True, delete all secrets. Raises: NotFound: when Secret does not exist APIError: when error returned by service """ self.manager.remove(self.id, all=all) class SecretsManager(Manager): """Specialized Manager for Secret resources.""" @property def resource(self): """Type[Secret]: prepare_model() will create Secret classes.""" return Secret def __init__(self, client: APIClient): """Initialize SecretsManager object. Args: client: Connection to Podman service. """ super().__init__(client) def exists(self, key: str) -> bool: response = self.client.get(f"/secrets/{key}/json") return response.ok # pylint is flagging 'secret_id' here vs. 'key' parameter in super.get() def get(self, secret_id: str) -> Secret: # pylint: disable=arguments-differ,arguments-renamed """Return information for Secret by name or id. Args: secret_id: Secret name or id. Raises: NotFound: when Secret does not exist APIError: when error returned by service """ response = self.client.get(f"/secrets/{secret_id}/json") response.raise_for_status() return self.prepare_model(attrs=response.json()) def list(self, **kwargs) -> list[Secret]: """Report on Secrets. Keyword Args: filters (dict[str, Any]): Ignored. Raises: APIError: when error returned by service """ response = self.client.get("/secrets/json") response.raise_for_status() return [self.prepare_model(attrs=item) for item in response.json()] def create( self, name: str, data: bytes, labels: Optional[Mapping[str, Any]] = None, # pylint: disable=unused-argument driver: Optional[str] = None, ) -> Secret: """Create a Secret. Args: name: User-defined name of the secret. data: Secret to be registered with Podman service. labels: Ignored. driver: Secret driver. Raises: APIError: when service returns an error """ params = { "name": name, "driver": driver, } response = self.client.post("/secrets/create", params=params, data=data) response.raise_for_status() body = response.json() return self.get(body["ID"]) def remove( self, secret_id: Union[Secret, str], all: Optional[bool] = None, # pylint: disable=redefined-builtin ): """Delete secret. Podman only Args: secret_id: Identifier of Secret to delete. all: When True, delete all secrets. Raises: NotFound: when Secret does not exist APIError: when an error returned by service """ if isinstance(secret_id, Secret): secret_id = secret_id.id response = self.client.delete(f"/secrets/{secret_id}", params={"all": all}) response.raise_for_status() podman-py-5.4.0.1/podman/domain/system.py000066400000000000000000000071251475540541200202220ustar00rootroot00000000000000"""SystemManager to provide system level information from Podman service.""" import logging from typing import Any, Optional, Union from podman.api.client import APIClient from podman import api logger = logging.getLogger("podman.system") class SystemManager: """SystemManager to provide system level information from Podman service.""" def __init__(self, client: APIClient) -> None: """Initialize SystemManager object. Args: client: Connection to Podman service. """ self.client = client def df(self) -> dict[str, Any]: # pylint: disable=invalid-name """Disk usage by Podman resources. Returns: dict: Keyed by resource categories and their data usage. """ response = self.client.get("/system/df") response.raise_for_status() return response.json() def info(self, *_, **__) -> dict[str, Any]: """Returns information on Podman service.""" response = self.client.get("/info") response.raise_for_status() return response.json() def login( # pylint: disable=too-many-arguments,too-many-positional-arguments,unused-argument self, username: str, password: Optional[str] = None, email: Optional[str] = None, registry: Optional[str] = None, reauth: Optional[bool] = False, dockercfg_path: Optional[str] = None, auth: Optional[str] = None, identitytoken: Optional[str] = None, registrytoken: Optional[str] = None, tls_verify: Optional[Union[bool, str]] = None, ) -> dict[str, Any]: """Log into Podman service. Args: username: Registry username password: Registry plaintext password email: Registry account email address registry: URL for registry access. For example, reauth: Ignored: If True, refresh existing authentication. Default: False dockercfg_path: Ignored: Path to custom configuration file. https://quay.io/v2 auth: TODO: Add description based on the source code of Podman. identitytoken: IdentityToken is used to authenticate the user and get an access token for the registry. registrytoken: RegistryToken is a bearer token to be sent to a registry tls_verify: Whether to verify TLS certificates. """ payload = { "username": username, "password": password, "email": email, "serveraddress": registry, "auth": auth, "identitytoken": identitytoken, "registrytoken": registrytoken, } payload = api.prepare_body(payload) response = self.client.post( path="/auth", headers={"Content-type": "application/json"}, data=payload, compatible=True, verify=tls_verify, # Pass tls_verify to the client ) response.raise_for_status() return response.json() def ping(self) -> bool: """Returns True if service responded with OK.""" response = self.client.head("/_ping") return response.ok def version(self, **kwargs) -> dict[str, Any]: """Get version information from service. Keyword Args: api_version (bool): When True include API version """ response = self.client.get("/version") response.raise_for_status() body = response.json() if not kwargs.get("api_version", True): del body["APIVersion"] return body podman-py-5.4.0.1/podman/domain/volumes.py000066400000000000000000000115211475540541200203630ustar00rootroot00000000000000"""Model and Manager for Volume resources.""" import logging from typing import Any, Literal, Optional, Union import requests from podman import api from podman.domain.manager import Manager, PodmanResource from podman.errors import APIError logger = logging.getLogger("podman.volumes") class Volume(PodmanResource): """Details and configuration for an image managed by the Podman service.""" @property def id(self): return self.name @property def name(self): """str: Returns the name of the volume.""" return self.attrs.get("Name") def remove(self, force: Optional[bool] = None) -> None: """Delete this volume. Args: force: When true, force deletion of in-use volume Raises: APIError: when service reports an error """ self.manager.remove(self.name, force=force) class VolumesManager(Manager): """Specialized Manager for Volume resources.""" @property def resource(self): """Type[Volume]: prepare_model() will create Volume classes.""" return Volume def create(self, name: Optional[str] = None, **kwargs) -> Volume: """Create a Volume. Args: name: Name given to new volume Keyword Args: driver (str): Volume driver to use driver_opts (dict[str, str]): Options to use with driver labels (dict[str, str]): Labels to apply to volume Raises: APIError: when service reports error """ data = { "Driver": kwargs.get("driver"), "Labels": kwargs.get("labels"), "Name": name, "Options": kwargs.get("driver_opts"), } response = self.client.post( "/volumes/create", data=api.prepare_body(data), headers={"Content-Type": "application/json"}, ) response.raise_for_status() return self.prepare_model(attrs=response.json()) def exists(self, key: str) -> bool: response = self.client.get(f"/volumes/{key}/exists") return response.ok # pylint is flagging 'volume_id' here vs. 'key' parameter in super.get() def get(self, volume_id: str) -> Volume: # pylint: disable=arguments-differ,arguments-renamed """Returns and volume by name or id. Args: volume_id: Volume id or name for which to search Raises: NotFound: when volume could not be found APIError: when service reports an error """ response = self.client.get(f"/volumes/{volume_id}/json") response.raise_for_status() return self.prepare_model(attrs=response.json()) def list(self, *_, **kwargs) -> list[Volume]: """Report on volumes. Keyword Args: filters (dict[str, str]): criteria to filter Volume list - driver (str): filter volumes by their driver - label (dict[str, str]): filter by label and/or value - name (str): filter by volume's name """ filters = api.prepare_filters(kwargs.get("filters")) response = self.client.get("/volumes/json", params={"filters": filters}) if response.status_code == requests.codes.not_found: return [] response.raise_for_status() return [self.prepare_model(i) for i in response.json()] def prune( self, filters: Optional[dict[str, str]] = None, # pylint: disable=unused-argument ) -> dict[Literal["VolumesDeleted", "SpaceReclaimed"], Any]: """Delete unused volumes. Args: filters: Criteria for selecting volumes to delete. Ignored. Raises: APIError: when service reports error """ response = self.client.post("/volumes/prune") data = response.json() response.raise_for_status() volumes: list[str] = [] space_reclaimed = 0 for item in data: if "Err" in item: raise APIError( item["Err"], response=response, explanation=f"""Failed to prune volume '{item.get("Id")}'""", ) volumes.append(item.get("Id")) space_reclaimed += item["Size"] return {"VolumesDeleted": volumes, "SpaceReclaimed": space_reclaimed} def remove(self, name: Union[Volume, str], force: Optional[bool] = None) -> None: """Delete a volume. Podman only. Args: name: Identifier for Volume to be deleted. force: When true, force deletion of in-use volume Raises: APIError: when service reports an error """ if isinstance(name, Volume): name = name.name response = self.client.delete(f"/volumes/{name}", params={"force": force}) response.raise_for_status() podman-py-5.4.0.1/podman/errors/000077500000000000000000000000001475540541200163645ustar00rootroot00000000000000podman-py-5.4.0.1/podman/errors/__init__.py000066400000000000000000000054651475540541200205070ustar00rootroot00000000000000"""Podman API errors Package. Import exceptions from 'importlib' are used to differentiate between APIConnection and PodmanClient errors. Therefore, installing both APIConnection and PodmanClient is not supported. PodmanClient related errors take precedence over APIConnection ones. ApiConnection and associated classes have been deprecated. """ import warnings from http.client import HTTPException # isort: unique-list __all__ = [ 'APIError', 'BuildError', 'ContainerError', 'DockerException', 'ImageNotFound', 'InvalidArgument', 'NotFound', 'NotFoundError', 'PodmanError', 'StreamParseError', ] try: from .exceptions import ( APIError, BuildError, ContainerError, DockerException, InvalidArgument, NotFound, PodmanError, StreamParseError, ) except ImportError: pass class NotFoundError(HTTPException): """HTTP request returned a http.HTTPStatus.NOT_FOUND. Deprecated. """ def __init__(self, message, response=None): super().__init__(message) self.response = response warnings.warn( "APIConnection() and supporting classes.", PendingDeprecationWarning, stacklevel=2 ) # If found, use new ImageNotFound otherwise old class try: from .exceptions import ImageNotFound except ImportError: class ImageNotFound(NotFoundError): """HTTP request returned a http.HTTPStatus.NOT_FOUND. Specialized for Image not found. Deprecated. """ class NetworkNotFound(NotFoundError): """Network request returned a http.HTTPStatus.NOT_FOUND. Deprecated. """ class ContainerNotFound(NotFoundError): """HTTP request returned a http.HTTPStatus.NOT_FOUND. Specialized for Container not found. Deprecated. """ class PodNotFound(NotFoundError): """HTTP request returned a http.HTTPStatus.NOT_FOUND. Specialized for Pod not found. Deprecated. """ class ManifestNotFound(NotFoundError): """HTTP request returned a http.HTTPStatus.NOT_FOUND. Specialized for Manifest not found. Deprecated. """ class RequestError(HTTPException): """Podman service reported issue with the request. Deprecated. """ def __init__(self, message, response=None): super().__init__(message) self.response = response warnings.warn( "APIConnection() and supporting classes.", PendingDeprecationWarning, stacklevel=2 ) class InternalServerError(HTTPException): """Podman service reported an internal error. Deprecated. """ def __init__(self, message, response=None): super().__init__(message) self.response = response warnings.warn( "APIConnection() and supporting classes.", PendingDeprecationWarning, stacklevel=2 ) podman-py-5.4.0.1/podman/errors/exceptions.py000066400000000000000000000101601475540541200211150ustar00rootroot00000000000000"""Podman API Errors.""" from typing import Optional, Union, TYPE_CHECKING from collections.abc import Iterable from requests import Response from requests.exceptions import HTTPError # Break circular import if TYPE_CHECKING: from podman.domain.containers import Container from podman.api.client import APIResponse class APIError(HTTPError): """Wraps HTTP errors for processing by the API and clients.""" def __init__( self, message: str, response: Union[Response, "APIResponse", None] = None, explanation: Optional[str] = None, ): """Initialize APIError. Args: message: Message from service. Default: response.text, may be enhanced or wrapped by bindings response: HTTP Response from service. explanation: An enhanced or wrapped version of message with additional context. """ super().__init__(message, response=response) self.explanation = explanation def __str__(self): msg = super().__str__() if self.response is not None: msg = self.response.reason if self.is_client_error(): msg = f"{self.status_code} Client Error: {msg}" elif self.is_server_error(): msg = f"{self.status_code} Server Error: {msg}" if self.explanation: msg = f"{msg} ({self.explanation})" return msg @property def status_code(self): """Optional[int]: HTTP status code from response.""" if self.response is not None: return self.response.status_code return None def is_error(self) -> bool: """Returns True when HTTP operation resulted in an error.""" return self.is_client_error() or self.is_server_error() def is_client_error(self) -> bool: """Returns True when request is incorrect.""" return 400 <= (self.status_code or 0) < 500 def is_server_error(self) -> bool: """Returns True when error occurred in service.""" return 500 <= (self.status_code or 0) < 600 class NotFound(APIError): """Resource not found on Podman service. Named for compatibility. """ class ImageNotFound(APIError): """Image not found on Podman service.""" class DockerException(Exception): """Base class for exception hierarchy. Provided for compatibility. """ class PodmanError(DockerException): """Base class for PodmanPy exceptions.""" class BuildError(PodmanError): """Error occurred during build operation.""" def __init__(self, reason: str, build_log: Iterable[str]) -> None: """Initialize BuildError. Args: reason: describes the error build_log: build log output """ super().__init__(reason) self.msg = reason self.build_log = build_log class ContainerError(PodmanError): """Represents a container that has exited with a non-zero exit code.""" def __init__( self, container: "Container", exit_status: int, command: Union[str, list[str]], image: str, stderr: Optional[Iterable[str]] = None, ): # pylint: disable=too-many-positional-arguments """Initialize ContainerError. Args: container: Container that reported error. exit_status: Non-zero status code from Container exit. command: Command passed to container when created. image: Name of image that was used to create container. stderr: Errors reported by Container. """ err = f": {stderr}" if stderr is not None else "" msg = ( f"Command '{command}' in image '{image}' returned non-zero exit " f"status {exit_status}{err}" ) super().__init__(msg) self.container = container self.exit_status: int = exit_status self.command = command self.image = image self.stderr = stderr class InvalidArgument(PodmanError): """Parameter to method/function was not valid.""" class StreamParseError(RuntimeError): def __init__(self, reason): self.msg = reason podman-py-5.4.0.1/podman/py.typed000066400000000000000000000000001475540541200165350ustar00rootroot00000000000000podman-py-5.4.0.1/podman/tests/000077500000000000000000000000001475540541200162125ustar00rootroot00000000000000podman-py-5.4.0.1/podman/tests/README.md000066400000000000000000000003141475540541200174670ustar00rootroot00000000000000# Podman Testing ## Unit Test Framework `unittest` included in python standard library ## Coverage Reporting Framework `coverage.py` see https://coverage.readthedocs.io/en/coverage-5.0.3/#quick-start podman-py-5.4.0.1/podman/tests/__init__.py000066400000000000000000000004331475540541200203230ustar00rootroot00000000000000"""PodmanPy Tests.""" # Do not auto-update these from version.py, # as test code should be changed to reflect changes in Podman API versions BASE_SOCK = "unix:///run/api.sock" LIBPOD_URL = "http://%2Frun%2Fapi.sock/v5.4.0/libpod" COMPATIBLE_URL = "http://%2Frun%2Fapi.sock/v1.40" podman-py-5.4.0.1/podman/tests/errors.py000066400000000000000000000013141475540541200200770ustar00rootroot00000000000000# Copyright 2020 Red Hat, Inc. # # 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. # """test error exceptions""" class PodmanNotInstalled(Exception): """Exception when podman is not available""" podman-py-5.4.0.1/podman/tests/integration/000077500000000000000000000000001475540541200205355ustar00rootroot00000000000000podman-py-5.4.0.1/podman/tests/integration/__init__.py000066400000000000000000000000001475540541200226340ustar00rootroot00000000000000podman-py-5.4.0.1/podman/tests/integration/base.py000066400000000000000000000043551475540541200220300ustar00rootroot00000000000000# Copyright 2020 Red Hat, Inc. # # 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. # """Base integration test code""" import logging import os import shutil import uuid import fixtures from podman.tests.integration import utils class IntegrationTest(fixtures.TestWithFixtures): """Base Integration test case. Notes: - Logging for the Podman service configured here for later capture, this configuration is inherited by other libraries like unittest and requests. - Logging output will start with stdout from the Podman service events, followed by results and logging captured by the unittest module test runner. """ podman: str = None @classmethod def setUpClass(cls) -> None: super(fixtures.TestWithFixtures, cls).setUpClass() command = os.environ.get("PODMAN_BINARY", "podman") if shutil.which(command) is None: raise AssertionError(f"'{command}' not found.") IntegrationTest.podman = command # This log_level is for our python code log_level = os.environ.get("PODMAN_LOG_LEVEL", "INFO") log_level = logging.getLevelName(log_level) logging.basicConfig(level=log_level) def setUp(self): super().setUp() self.log_level = os.environ.get("PODMAN_LOG_LEVEL", "INFO") self.test_dir = self.useFixture(fixtures.TempDir()).path self.socket_file = os.path.join(self.test_dir, uuid.uuid4().hex) self.socket_uri = f'unix://{self.socket_file}' self.service_launcher = utils.PodmanLauncher( self.socket_uri, podman_path=IntegrationTest.podman, log_level=self.log_level ) self.service_launcher.start() self.addCleanup(self.service_launcher.stop) podman-py-5.4.0.1/podman/tests/integration/test_adapters.py000066400000000000000000000027411475540541200237550ustar00rootroot00000000000000import getpass import unittest import time from podman import PodmanClient from podman.tests.integration import base, utils class AdapterIntegrationTest(base.IntegrationTest): def setUp(self): super().setUp() def test_ssh_ping(self): with PodmanClient( base_url=f"http+ssh://{getpass.getuser()}@localhost:22{self.socket_file}" ) as client: self.assertTrue(client.ping()) with PodmanClient( base_url=f"ssh://{getpass.getuser()}@localhost:22{self.socket_file}" ) as client: self.assertTrue(client.ping()) def test_unix_ping(self): with PodmanClient(base_url=f"unix://{self.socket_file}") as client: self.assertTrue(client.ping()) with PodmanClient(base_url=f"http+unix://{self.socket_file}") as client: self.assertTrue(client.ping()) def test_tcp_ping(self): podman = utils.PodmanLauncher( "tcp:localhost:8889", podman_path=base.IntegrationTest.podman, log_level=self.log_level, ) try: podman.start(check_socket=False) time.sleep(0.5) with PodmanClient(base_url="tcp:localhost:8889") as client: self.assertTrue(client.ping()) with PodmanClient(base_url="http://localhost:8889") as client: self.assertTrue(client.ping()) finally: podman.stop() if __name__ == '__main__': unittest.main() podman-py-5.4.0.1/podman/tests/integration/test_container_create.py000066400000000000000000000372641475540541200254670ustar00rootroot00000000000000import unittest import re import podman.tests.integration.base as base from podman import PodmanClient # @unittest.skipIf(os.geteuid() != 0, 'Skipping, not running as root') class ContainersIntegrationTest(base.IntegrationTest): """Containers Integration tests.""" def setUp(self): super().setUp() self.client = PodmanClient(base_url=self.socket_uri) self.addCleanup(self.client.close) self.alpine_image = self.client.images.pull("quay.io/libpod/alpine", tag="latest") self.containers = [] def tearUp(self): for container in self.containers: container.remove(force=True) def test_container_named_volume_mount(self): with self.subTest("Check volume mount"): volumes = { 'test_bind_1': {'bind': '/mnt/vol1', 'mode': 'rw'}, 'test_bind_2': {'bind': '/mnt/vol2', 'extended_mode': ['ro', 'noexec']}, 'test_bind_3': {'bind': '/mnt/vol3', 'extended_mode': ['noexec'], 'mode': 'rw'}, } container = self.client.containers.create(self.alpine_image, volumes=volumes) container_mounts = container.attrs.get('Mounts', {}) self.assertEqual(len(container_mounts), len(volumes)) for mount in container_mounts: name = mount.get('Name') self.assertIn(name, volumes) test_mount = volumes.get(name) test_mode = test_mount.get('mode', '') test_extended_mode = test_mount.get('extended_mode', []) # check RO/RW if 'ro' in test_mode or 'ro' in test_extended_mode: self.assertEqual(mount.get('RW'), False) if 'rw' in test_mode or 'rw' in test_extended_mode: self.assertEqual(mount.get('RW'), True) other_options = [o for o in test_extended_mode if o not in ['ro', 'rw']] for o in other_options: self.assertIn(o, mount.get('Options')) def test_container_directory_volume_mount(self): """Test that directories can be mounted with the ``volume`` parameter.""" with self.subTest("Check bind mount"): volumes = { "/etc/hosts": dict(bind="/test_ro", mode='ro'), "/etc/hosts": dict(bind="/test_rw", mode='rw'), # noqa: F601 } container = self.client.containers.create( self.alpine_image, command=["cat", "/test_ro", "/test_rw"], volumes=volumes ) container_mounts = container.attrs.get('Mounts', {}) self.assertEqual(len(container_mounts), len(volumes)) self.containers.append(container) for directory, mount_spec in volumes.items(): self.assertIn( f"{directory}:{mount_spec['bind']}:{mount_spec['mode']},rprivate,rbind", container.attrs.get('HostConfig', {}).get('Binds', list()), ) # check if container can be started and exits with EC == 0 container.start() container.wait() self.assertEqual(container.attrs.get('State', dict()).get('ExitCode', 256), 0) def test_container_extra_hosts(self): """Test Container Extra hosts""" extra_hosts = {"host1 host3": "127.0.0.2", "host2": "127.0.0.3"} with self.subTest("Check extra hosts in container object"): proper_container = self.client.containers.create( self.alpine_image, command=["cat", "/etc/hosts"], extra_hosts=extra_hosts ) self.containers.append(proper_container) formatted_hosts = [f"{hosts}:{ip}" for hosts, ip in extra_hosts.items()] self.assertEqual( proper_container.attrs.get('HostConfig', dict()).get('ExtraHosts', list()), formatted_hosts, ) with self.subTest("Check extra hosts in running container"): proper_container.start() proper_container.wait() logs = b"\n".join(proper_container.logs()).decode() formatted_hosts = [f"{ip}\t{hosts}" for hosts, ip in extra_hosts.items()] for hosts_entry in formatted_hosts: self.assertIn(hosts_entry, logs) def _test_memory_limit(self, parameter_name, host_config_name, set_mem_limit=False): """Base for tests which checks memory limits""" memory_limit_tests = [ {'value': 1000, 'expected_value': 1000}, {'value': '1000', 'expected_value': 1000}, {'value': '1234b', 'expected_value': 1234}, {'value': '123k', 'expected_value': 123 * 1024}, {'value': '44m', 'expected_value': 44 * 1024 * 1024}, {'value': '2g', 'expected_value': 2 * 1024 * 1024 * 1024}, ] for test in memory_limit_tests: parameters = {parameter_name: test['value']} if set_mem_limit: parameters['mem_limit'] = test['expected_value'] - 100 container = self.client.containers.create(self.alpine_image, **parameters) self.containers.append(container) self.assertEqual( container.attrs.get('HostConfig', dict()).get(host_config_name), test['expected_value'], ) def test_container_ports(self): """Test ports binding""" port_tests = [ { 'input': {'97/tcp': '43'}, 'expected_output': {'97/tcp': [{'HostIp': '', 'HostPort': '43'}]}, }, { 'input': {'2/udp': ('127.0.0.1', '939')}, 'expected_output': {'2/udp': [{'HostIp': '127.0.0.1', 'HostPort': '939'}]}, }, { 'input': { '11123/tcp': [('127.0.0.1', '11123'), ('127.0.0.1', '112'), '1123', '159'] }, 'expected_output': { '11123/tcp': [ {'HostIp': '127.0.0.1', 'HostPort': '11123'}, {'HostIp': '', 'HostPort': '112'}, {'HostIp': '', 'HostPort': '1123'}, {'HostIp': '', 'HostPort': '159'}, ] }, }, { 'input': {'1111/tcp': {"port": ('127.0.0.1', 1111), "range": 3}}, 'expected_output': { '1111/tcp': [{'HostIp': '127.0.0.1', 'HostPort': '1111'}], '1112/tcp': [{'HostIp': '127.0.0.1', 'HostPort': '1112'}], '1113/tcp': [{'HostIp': '127.0.0.1', 'HostPort': '1113'}], }, }, { 'input': { '1222/tcp': [{"port": 1234, "range": 2}, {"ip": "127.0.0.1", "port": 4567}] }, 'expected_output': { '1222/tcp': [ {'HostIp': '', 'HostPort': '1234'}, {'HostIp': '127.0.0.1', 'HostPort': '4567'}, ], '1223/tcp': [{'HostIp': '', 'HostPort': '1235'}], }, }, { 'input': { 2244: 3344, }, 'expected_output': { '2244/tcp': [ {'HostIp': '', 'HostPort': '3344'}, ], }, }, ] for port_test in port_tests: container = self.client.containers.create(self.alpine_image, ports=port_test['input']) self.containers.append(container) self.assertTrue( all( [ x in port_test['expected_output'] for x in container.attrs.get('HostConfig', {}).get('PortBindings') ] ) ) def test_container_dns_option(self): expected_dns_opt = ['edns0'] container = self.client.containers.create( self.alpine_image, command=["cat", "/etc/resolv.conf"], dns_opt=expected_dns_opt ) self.containers.append(container) with self.subTest("Check HostConfig"): self.assertEqual( container.attrs.get('HostConfig', {}).get('DnsOptions'), expected_dns_opt ) with self.subTest("Check content of /etc/resolv.conf"): container.start() container.wait() self.assertTrue( all([opt in b"\n".join(container.logs()).decode() for opt in expected_dns_opt]) ) def test_container_healthchecks(self): """Test passing various healthcheck options""" parameters = { 'healthcheck': {'Test': ['CMD-SHELL curl http://localhost || exit']}, 'health_check_on_failure_action': 1, } container = self.client.containers.create(self.alpine_image, **parameters) self.containers.append(container) def test_container_mem_limit(self): """Test passing memory limit""" self._test_memory_limit('mem_limit', 'Memory') def test_container_memswap_limit(self): """Test passing memory swap limit""" self._test_memory_limit('memswap_limit', 'MemorySwap', set_mem_limit=True) def test_container_mem_reservation(self): """Test passing memory reservation""" self._test_memory_limit('mem_reservation', 'MemoryReservation') def test_container_shm_size(self): """Test passing shared memory size""" self._test_memory_limit('shm_size', 'ShmSize') def test_container_mounts(self): """Test passing mounts""" with self.subTest("Check bind mount"): mount = { "type": "bind", "source": "/etc/hosts", "target": "/test", "read_only": True, "relabel": "Z", } container = self.client.containers.create( self.alpine_image, command=["cat", "/test"], mounts=[mount] ) self.containers.append(container) self.assertIn( f"{mount['source']}:{mount['target']}:ro,Z,rprivate,rbind", container.attrs.get('HostConfig', {}).get('Binds', list()), ) # check if container can be started and exits with EC == 0 container.start() container.wait() self.assertEqual(container.attrs.get('State', dict()).get('ExitCode', 256), 0) with self.subTest("Check tmpfs mount"): mount = {"type": "tmpfs", "source": "tmpfs", "target": "/test", "size": "456k"} container = self.client.containers.create( self.alpine_image, command=["df", "-h"], mounts=[mount] ) self.containers.append(container) self.assertEqual( container.attrs.get('HostConfig', {}).get('Tmpfs', {}).get(mount['target']), f"size={mount['size']},rw,rprivate,nosuid,nodev,tmpcopyup", ) container.start() container.wait() logs = b"\n".join(container.logs()).decode() self.assertTrue( re.search( rf"{mount['size'].replace('k', '.0K')}.*?{mount['target']}", logs, flags=re.MULTILINE, ) ) with self.subTest("Check uppercase mount option attributes"): mount = { "TypE": "bind", "SouRce": "/etc/hosts", "TarGet": "/test", "Read_Only": True, "ReLabel": "Z", } container = self.client.containers.create( self.alpine_image, command=["cat", "/test"], mounts=[mount] ) self.containers.append(container) self.assertIn( f"{mount['SouRce']}:{mount['TarGet']}:ro,Z,rprivate,rbind", container.attrs.get('HostConfig', {}).get('Binds', list()), ) # check if container can be started and exits with EC == 0 container.start() container.wait() self.assertEqual(container.attrs.get('State', dict()).get('ExitCode', 256), 0) def test_container_devices(self): devices = ["/dev/null:/dev/foo", "/dev/zero:/dev/bar"] container = self.client.containers.create( self.alpine_image, devices=devices, command=["ls", "-l", "/dev/"] ) self.containers.append(container) container_devices = container.attrs.get('HostConfig', {}).get('Devices', []) with self.subTest("Check devices in container object"): for device in devices: path_on_host, path_in_container = device.split(':', 1) self.assertTrue( any( [ c.get('PathOnHost') == path_on_host and c.get('PathInContainer') == path_in_container for c in container_devices ] ) ) with self.subTest("Check devices in running container object"): container.start() container.wait() logs = b"\n".join(container.logs()).decode() device_regex = r'(\d+, *?\d+).*?{}\n' for device in devices: # check whether device exists source_device, destination_device = device.split(':', 1) source_match = re.search( device_regex.format(source_device.rsplit("/", 1)[-1]), logs ) destination_match = re.search( device_regex.format(destination_device.rsplit("/", 1)[-1]), logs ) self.assertIsNotNone(source_match) self.assertIsNotNone(destination_match) # validate if proper device was added (by major/minor numbers) self.assertEqual(source_match.group(1), destination_match.group(1)) def test_read_write_tmpfs(self): test_cases = [ {"read_write_tmpfs": True, "failed_container": False}, { "read_write_tmpfs": False, "failed_container": True, "expected_output": "Read-only file system", }, { "read_write_tmpfs": None, "failed_container": True, "expected_output": "Read-only file system", }, ] for test in test_cases: read_write_tmpfs = test.get('read_write_tmpfs') with self.subTest(f"Check read_write_tmpfs set to {read_write_tmpfs}"): kwargs = ( {"read_write_tmpfs": read_write_tmpfs} if read_write_tmpfs is not None else {} ) container = self.client.containers.create( self.alpine_image, read_only=True, command=["/bin/touch", "/tmp/test_file"], **kwargs, ) self.containers.append(container) container.start() container.wait() inspect = container.inspect() logs = b"\n".join(container.logs(stderr=True)).decode() if test.get("failed_container") is True: self.assertNotEqual(inspect.get("State", {}).get("ExitCode", -1), 0) else: self.assertEqual(inspect.get("State", {}).get("ExitCode", -1), 0) expected_output = test.get("expected_output") if expected_output: print(inspect) self.assertIn(expected_output, logs) if __name__ == '__main__': unittest.main() podman-py-5.4.0.1/podman/tests/integration/test_container_exec.py000066400000000000000000000106051475540541200251360ustar00rootroot00000000000000import podman.tests.integration.base as base from podman import PodmanClient # @unittest.skipIf(os.geteuid() != 0, 'Skipping, not running as root') class ContainersExecIntegrationTests(base.IntegrationTest): """Containers integration tests for exec""" def setUp(self): super().setUp() self.client = PodmanClient(base_url=self.socket_uri) self.addCleanup(self.client.close) self.alpine_image = self.client.images.pull("quay.io/libpod/alpine", tag="latest") self.containers = [] def tearDown(self): for container in self.containers: container.remove(force=True) def test_container_exec_run(self): """Test any command that will return code 0 and no output""" container = self.client.containers.create(self.alpine_image, command=["top"], detach=True) container.start() error_code, stdout = container.exec_run("echo hello") self.assertEqual(error_code, 0) self.assertEqual(stdout, b'\x01\x00\x00\x00\x00\x00\x00\x06hello\n') def test_container_exec_run_errorcode(self): """Test a failing command with stdout and stderr in a single bytestring""" container = self.client.containers.create(self.alpine_image, command=["top"], detach=True) container.start() error_code, output = container.exec_run("ls nonexistent") self.assertEqual(error_code, 1) self.assertEqual( output, b"\x02\x00\x00\x00\x00\x00\x00+ls: nonexistent: No such file or directory\n" ) def test_container_exec_run_demux(self): """Test a failing command with stdout and stderr in a bytestring tuple""" container = self.client.containers.create(self.alpine_image, command=["top"], detach=True) container.start() error_code, output = container.exec_run("ls nonexistent", demux=True) self.assertEqual(error_code, 1) self.assertEqual(output[0], None) self.assertEqual(output[1], b"ls: nonexistent: No such file or directory\n") def test_container_exec_run_stream(self): """Test streaming the output from a long running command.""" container = self.client.containers.create(self.alpine_image, command=["top"], detach=True) container.start() command = [ '/bin/sh', '-c', 'echo 0 ; sleep .1 ; echo 1 ; sleep .1 ; echo 2 ; sleep .1 ;', ] error_code, output = container.exec_run(command, stream=True) self.assertEqual(error_code, None) self.assertEqual( list(output), [ b'0\n', b'1\n', b'2\n', ], ) def test_container_exec_run_stream_demux(self): """Test streaming the output from a long running command with demux enabled.""" container = self.client.containers.create(self.alpine_image, command=["top"], detach=True) container.start() command = [ '/bin/sh', '-c', 'echo 0 ; >&2 echo 1 ; sleep .1 ; ' + 'echo 2 ; >&2 echo 3 ; sleep .1 ; ' + 'echo 4 ; >&2 echo 5 ; sleep .1 ;', ] error_code, output = container.exec_run(command, stream=True, demux=True) self.assertEqual(error_code, None) self.assertEqual( list(output), [ (b'0\n', None), (None, b'1\n'), (b'2\n', None), (None, b'3\n'), (b'4\n', None), (None, b'5\n'), ], ) def test_container_exec_run_stream_detach(self): """Test streaming the output from a long running command with detach enabled.""" container = self.client.containers.create(self.alpine_image, command=["top"], detach=True) container.start() command = [ '/bin/sh', '-c', 'echo 0 ; sleep .1 ; echo 1 ; sleep .1 ; echo 2 ; sleep .1 ;', ] error_code, output = container.exec_run(command, stream=True, detach=True) # Detach should make the ``exec_run`` ignore the ``stream`` flag so we will # assert against the standard, non-streaming behavior. self.assertEqual(error_code, 0) # The endpoint should return immediately, before we are able to actually # get any of the output. self.assertEqual( output, b'\n', ) podman-py-5.4.0.1/podman/tests/integration/test_containers.py000066400000000000000000000217701475540541200243220ustar00rootroot00000000000000import io import random import tarfile import unittest import tempfile try: # Python >= 3.10 from collections.abc import Iterator except ImportError: # Python < 3.10 from collections.abc import Iterator import podman.tests.integration.base as base from podman import PodmanClient from podman.domain.containers import Container from podman.domain.images import Image from podman.errors import NotFound # @unittest.skipIf(os.geteuid() != 0, 'Skipping, not running as root') class ContainersIntegrationTest(base.IntegrationTest): """Containers Integration tests.""" def setUp(self): super().setUp() self.client = PodmanClient(base_url=self.socket_uri) self.addCleanup(self.client.close) self.alpine_image = self.client.images.pull("quay.io/libpod/alpine", tag="latest") # TODO should this use podman binary instead? for container in self.client.containers.list(): container.remove(force=True) def test_container_crud(self): """Test Container CRUD.""" random_string = f"{random.getrandbits(160):x}" with self.subTest("Create from Alpine Image"): container = self.client.containers.create( self.alpine_image, command=["echo", random_string], ports={'2222/tcp': 3333, 2244: 3344}, ) self.assertIsInstance(container, Container) self.assertGreater(len(container.attrs), 0) self.assertIsNotNone(container.id) self.assertIsNotNone(container.name) self.assertIsInstance(container.image, Image) self.assertTrue(self.client.containers.exists(container.id)) self.assertIn("quay.io/libpod/alpine:latest", container.image.tags) with self.subTest("Inspect Container"): actual = self.client.containers.get(container.id) self.assertIsInstance(actual, Container) self.assertEqual(actual.id, container.id) self.assertIn("2222/tcp", container.attrs["NetworkSettings"]["Ports"]) self.assertEqual( "3333", container.attrs["NetworkSettings"]["Ports"]["2222/tcp"][0]["HostPort"] ) self.assertIn("2244/tcp", container.attrs["NetworkSettings"]["Ports"]) self.assertEqual( "3344", container.attrs["NetworkSettings"]["Ports"]["2244/tcp"][0]["HostPort"] ) file_contents = b"This is an integration test for archive." file_buffer = io.BytesIO(file_contents) tar_buffer = io.BytesIO() with tarfile.open(fileobj=tar_buffer, mode="w") as tar: info = tarfile.TarInfo("/") info.type = tarfile.DIRTYPE tar.addfile(info) info = tarfile.TarInfo("unittest") info.size = len(file_buffer.getbuffer()) tar.addfile(info, file_buffer) tarball = tar_buffer.getvalue() with self.subTest("Archive /root/unittest"): self.assertTrue(container.put_archive("/root", data=tarball)) actual, stats = container.get_archive("/root") with io.BytesIO() as fd: for chunk in actual: fd.write(chunk) fd.seek(0, 0) with tarfile.open(fileobj=fd, mode="r") as tar: contents = tar.extractfile("root/unittest").read() self.assertEqual(contents, file_contents) with self.subTest("List containers --all"): containers = self.client.containers.list(all=True) self.assertGreater(len(containers), 0) ids = [i.id for i in containers] self.assertIn(container.id, ids) with self.subTest("Get container's logs"): container.start() container.wait(condition="exited") logs_iter = container.logs(stream=False) self.assertIsInstance(logs_iter, Iterator) logs = list(logs_iter) self.assertIn((random_string + "\n").encode("utf-8"), logs) with self.subTest("Delete Container"): container.remove() with self.assertRaises(NotFound): self.client.containers.get(container.id) with self.subTest("Run Container"): top_ctnr = self.client.containers.run( self.alpine_image, "/usr/bin/top", name="TestRunPs", detach=True ) self.assertEqual(top_ctnr.status, "running") top_ctnr.pause() top_ctnr.reload() self.assertEqual(top_ctnr.status, "paused") top_ctnr.unpause() top_ctnr.reload() self.assertEqual(top_ctnr.status, "running") report = top_ctnr.top() # See https://github.com/containers/podman/pull/9892 for the service # side fix requires podman >= 3.2 # processes = [i.strip() for i in report["Processes"][0]] self.assertIn("/usr/bin/top", report["Processes"][0][-1]) top_ctnr.stop() top_ctnr.reload() self.assertIn(top_ctnr.status, ("exited", "stopped")) with self.subTest("Create-Init-Start Container"): top_ctnr = self.client.containers.create( self.alpine_image, ["/usr/bin/top"], name="TestInitPs", detach=True ) self.assertEqual(top_ctnr.status, "created") top_ctnr.init() top_ctnr.reload() self.assertEqual(top_ctnr.status, "initialized") top_ctnr.start() top_ctnr.reload() self.assertEqual(top_ctnr.status, "running") top_ctnr.stop() top_ctnr.reload() self.assertIn(top_ctnr.status, ("exited", "stopped")) with self.subTest("Prune Containers"): report = self.client.containers.prune() self.assertIn(top_ctnr.id, report["ContainersDeleted"]) # SpaceReclaimed is the size of the content created during the running of the container # TODO: This should probably check if the podman version is >= 4.6 (guess) self.assertGreater(report["SpaceReclaimed"], 0) with self.assertRaises(NotFound): self.client.containers.get(top_ctnr.id) def test_container_commit(self): """Commit new image.""" busybox = self.client.images.pull("quay.io/libpod/busybox", tag="latest") container = self.client.containers.create( busybox, command=["echo", f"{random.getrandbits(160):x}"] ) image = container.commit(repository="busybox.local", tag="unittest") self.assertIn("localhost/busybox.local:unittest", image.attrs["RepoTags"]) busybox.remove(force=True) def test_container_rm_anonymous_volume(self): with self.subTest("Check anonymous volume is removed"): container_file = """ FROM alpine VOLUME myvol ENV foo=bar """ tmp_file = tempfile.mktemp() file = open(tmp_file, 'w') file.write(container_file) file.close() self.client.images.build(dockerfile=tmp_file, tag="test-img", path=".") # get existing number of containers and volumes existing_containers = self.client.containers.list(all=True) existing_volumes = self.client.volumes.list() container = self.client.containers.create("test-img") container_list = self.client.containers.list(all=True) self.assertEqual(len(container_list), len(existing_containers) + 1) volume_list = self.client.volumes.list() self.assertEqual(len(volume_list), len(existing_volumes) + 1) # remove the container with v=True container.remove(v=True) container_list = self.client.containers.list(all=True) self.assertEqual(len(container_list), len(existing_containers)) volume_list = self.client.volumes.list() self.assertEqual(len(volume_list), len(existing_volumes)) def test_container_labels(self): labels = {'label1': 'value1', 'label2': 'value2'} labeled_container = self.client.containers.create(self.alpine_image, labels=labels) unlabeled_container = self.client.containers.create( self.alpine_image, ) # inspect and list have 2 different schemas so we need to verify that we can # successfully retrieve the labels on both try: # inspect schema self.assertEqual(labeled_container.labels, labels) self.assertEqual(unlabeled_container.labels, {}) # list schema for container in self.client.containers.list(all=True): if container.id == labeled_container.id: self.assertEqual(container.labels, labels) elif container.id == unlabeled_container.id: self.assertEqual(container.labels, {}) finally: labeled_container.remove(v=True) unlabeled_container.remove(v=True) if __name__ == '__main__': unittest.main() podman-py-5.4.0.1/podman/tests/integration/test_images.py000066400000000000000000000150321475540541200234140ustar00rootroot00000000000000# Copyright 2020 Red Hat, Inc. # # 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. # """Images integration tests.""" import io import tarfile import types import unittest import podman.tests.integration.base as base from podman import PodmanClient from podman.domain.images import Image from podman.errors import APIError, ImageNotFound # @unittest.skipIf(os.geteuid() != 0, 'Skipping, not running as root') class ImagesIntegrationTest(base.IntegrationTest): """images call integration test""" def setUp(self): super().setUp() self.client = PodmanClient(base_url=self.socket_uri) self.addCleanup(self.client.close) def test_image_crud(self): """Test Image CRUD. Notes: Written to maximize reuse of pulled image. """ with self.subTest("Pull Alpine Image"): image = self.client.images.pull("quay.io/libpod/alpine", tag="latest") self.assertIsInstance(image, Image) self.assertIn("quay.io/libpod/alpine:latest", image.tags) self.assertTrue(self.client.images.exists(image.id)) with self.subTest("Inspect Alpine Image"): image = self.client.images.get("quay.io/libpod/alpine") self.assertIsInstance(image, Image) self.assertIn("quay.io/libpod/alpine:latest", image.tags) with self.subTest("Retrieve Image history"): ids = [i["Id"] for i in image.history()] self.assertIn(image.id, ids) with self.subTest("Export Image to tarball (in memory)"): buffer = io.BytesIO() for chunk in image.save(): buffer.write(chunk) buffer.seek(0, 0) with tarfile.open(fileobj=buffer, mode="r") as tar: items = tar.getnames() self.assertIn("manifest.json", items) self.assertIn("repositories", items) with self.subTest("List images"): image_list = self.client.images.list() self.assertTrue( any([i for i in image_list if "quay.io/libpod/alpine:latest" in i.tags]), f"pulled image 'quay.io/libpod/alpine:latest' not listed, {image_list}.", ) with self.subTest("Tag and reload() Image"): self.assertTrue(image.tag("localhost/alpine", "unittest")) image.reload() self.assertIn("localhost/alpine:unittest", image.tags) with self.subTest("Delete Image"): actual = self.client.images.remove(image.id, force=True) deleted = [d["Deleted"] for d in actual if "Deleted" in d] self.assertIn(image.id, deleted) untagged = [d["Untagged"] for d in actual if "Untagged" in d] self.assertIn(image.tags[0], untagged) exit_code = [d["ExitCode"] for d in actual if "ExitCode" in d] self.assertIn(0, exit_code) with self.assertRaises(ImageNotFound): # verify image deleted before loading below self.client.images.get(image.id) with self.subTest("Load Image previously deleted"): buffer.seek(0, 0) new_image = next(iter(self.client.images.load(buffer.getvalue()))) self.assertEqual(image.id, new_image.id) with self.subTest("Deleted unused Images"): actual = self.client.images.prune() deleted = [d.get("Deleted") for d in actual["ImagesDeleted"]] self.assertIn(image.id, deleted) self.assertGreater(actual["SpaceReclaimed"], 0) with self.subTest("Export Image to tarball (in memory) with named mode"): alpine_image = self.client.images.pull("quay.io/libpod/alpine", tag="latest") image_buffer = io.BytesIO() for chunk in alpine_image.save(named=True): image_buffer.write(chunk) image_buffer.seek(0, 0) with tarfile.open(fileobj=image_buffer, mode="r") as tar: items_in_tar = tar.getnames() # Check if repositories file is available in the tarball self.assertIn("repositories", items_in_tar) # Extract the 'repositories' file repositories_file = tar.extractfile("repositories") if repositories_file is not None: # Check the content of the "repositories" file. repositories_content = repositories_file.read().decode("utf-8") # Check if "repositories" file contains the name of the Image (named). self.assertTrue("alpine" in str(repositories_content)) def test_search(self): # N/B: This is an infrequently used feature, that tends to flake a lot. # Just check that it doesn't throw an exception and move on. self.client.images.search("alpine") @unittest.skip("Needs Podman 3.1.0") def test_corrupt_load(self): with self.assertRaises(APIError) as e: next(self.client.images.load(b"This is a corrupt tarball")) self.assertIn("payload does not match", e.exception.explanation) def test_build(self): buffer = io.StringIO("""FROM quay.io/libpod/alpine_labels:latest""") image, stream = self.client.images.build(fileobj=buffer) self.assertIsNotNone(image) self.assertIsNotNone(image.id) def test_pull_stream(self): generator = self.client.images.pull("ubi8", tag="latest", stream=True) self.assertIsInstance(generator, types.GeneratorType) def test_pull_stream_decode(self): generator = self.client.images.pull("ubi8", tag="latest", stream=True, decode=True) self.assertIsInstance(generator, types.GeneratorType) def test_scp(self): with self.assertRaises(APIError) as e: next( self.client.images.scp( source="randuser@fake.ip.addr:22::quay.io/libpod/alpine", quiet=False ) ) self.assertRegex( e.exception.explanation, r"failed to connect: dial tcp: lookup fake\.ip\.addr.+no such host", ) podman-py-5.4.0.1/podman/tests/integration/test_manifests.py000066400000000000000000000054761475540541200241530ustar00rootroot00000000000000import unittest from contextlib import suppress from podman import PodmanClient from podman.errors import APIError, ImageNotFound from podman.tests.integration import base # @unittest.skipIf(os.geteuid() != 0, 'Skipping, not running as root') class ManifestsIntegrationTest(base.IntegrationTest): def setUp(self): super().setUp() self.client = PodmanClient(base_url=self.socket_uri) self.addCleanup(self.client.close) self.alpine_image = self.client.images.pull("quay.io/libpod/alpine", tag="latest") self.invalid_manifest_name = "InvalidManifestName" def tearDown(self) -> None: if self.client.images.exists(self.invalid_manifest_name): self.client.images.remove(self.invalid_manifest_name, force=True) self.client.images.remove(self.alpine_image, force=True) with suppress(ImageNotFound): self.client.images.remove("localhost/unittest/alpine", force=True) def test_manifest_crud(self): """Test Manifest CRUD.""" self.assertFalse( self.client.manifests.exists("localhost/unittest/alpine"), "Image store is corrupt from previous run", ) with self.subTest("Create"): manifest = self.client.manifests.create( "localhost/unittest/alpine", ["quay.io/libpod/alpine:latest"] ) self.assertEqual(len(manifest.attrs["manifests"]), 1, manifest.attrs) self.assertTrue(self.client.manifests.exists(manifest.names), manifest.id) with self.assertRaises(APIError): self.client.manifests.create("123456!@#$%^") with self.subTest("Add"): manifest.add([self.alpine_image]) self.assertIsNotNone(manifest.attrs["manifests"]) self.assertIsInstance(manifest.attrs["manifests"], list) self.assertTrue( any([d for d in self.alpine_image.attrs["RepoDigests"] if manifest.id in d]), f'{manifest.id} not in any {self.alpine_image.attrs["RepoDigests"]}', ) with self.subTest("Inspect"): actual = self.client.manifests.get("quay.io/libpod/alpine:latest") self.assertEqual(actual.id, manifest.id) actual = self.client.manifests.get(manifest.name) self.assertEqual(actual.id, manifest.id) self.assertEqual(actual.version, 2) with self.subTest("Remove digest"): manifest.remove(self.alpine_image.attrs["RepoDigests"][0]) self.assertEqual(manifest.attrs["manifests"], []) def test_create_409(self): """Test that invalid Image names are caught and not corrupt storage.""" with self.assertRaises(APIError): self.client.manifests.create(self.invalid_manifest_name) if __name__ == '__main__': unittest.main() podman-py-5.4.0.1/podman/tests/integration/test_networks.py000066400000000000000000000101741475540541200240250ustar00rootroot00000000000000# Copyright 2020 Red Hat, Inc. # # 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. # """Network integration tests.""" import random import unittest from contextlib import suppress import podman.tests.integration.base as base from podman import PodmanClient from podman.domain.ipam import IPAMConfig, IPAMPool from podman.errors import NotFound class NetworksIntegrationTest(base.IntegrationTest): """networks call integration test""" pool = IPAMPool(subnet="10.11.13.0/24", iprange="10.11.13.0/26", gateway="10.11.13.1") ipam = IPAMConfig(pool_configs=[pool]) def setUp(self): super().setUp() self.client = PodmanClient(base_url=self.socket_uri) self.addCleanup(self.client.close) def tearDown(self): with suppress(NotFound): self.client.networks.get("integration_test").remove(force=True) super().tearDown() def test_network_crud(self): """integration: networks create and remove calls""" with self.subTest("Create Network"): network = self.client.networks.create( "integration_test", dns_enabled=False, ipam=NetworksIntegrationTest.ipam, ) self.assertEqual(network.name, "integration_test") self.assertTrue(self.client.networks.exists(network.name)) with self.subTest("Inspect Network"): network = self.client.networks.get("integration_test") self.assertEqual(network.name, "integration_test") with self.subTest("List Networks"): nets = self.client.networks.list() names = [i.name for i in nets] self.assertIn("integration_test", names) with self.subTest("Delete network"): network = self.client.networks.get("integration_test") network.remove(force=True) with self.assertRaises(NotFound): self.client.networks.get("integration_test") @unittest.skip("Skipping, libpod endpoint does not report container count") def test_network_connect(self): self.alpine_image = self.client.images.pull("quay.io/libpod/alpine", tag="latest") random_string = f"{random.getrandbits(160):x}" container = self.client.containers.create( self.alpine_image, command=["echo", random_string] ) with self.subTest("Create Network"): network = self.client.networks.create( "integration_test", disabled_dns=True, enable_ipv6=False, ipam=NetworksIntegrationTest.ipam, ) self.assertEqual(network.name, "integration_test") with self.subTest("Connect container to Network"): pre_count = network.containers network.connect(container=container) network.reload() post_count = network.containers self.assertLess(pre_count, post_count) with self.subTest("Disconnect container from Network"): pre_count = network.containers network.disconnect(container=container) network.reload() post_count = network.containers self.assertGreater(pre_count, post_count) @unittest.skip("Requires Podman 3.1") def test_network_prune(self): network = self.client.networks.create( "integration_test", disabled_dns=True, enable_ipv6=False, ipam=NetworksIntegrationTest.ipam, ) self.assertEqual(network.name, "integration_test") report = self.client.networks.prune() self.assertIn(network.name, report["NetworksDeleted"]) podman-py-5.4.0.1/podman/tests/integration/test_pods.py000066400000000000000000000115061475540541200231160ustar00rootroot00000000000000import random import unittest from podman import PodmanClient from podman.errors import NotFound from podman.tests.integration import base class PodsIntegrationTest(base.IntegrationTest): """Pods Integration tests.""" def setUp(self): super().setUp() self.client = PodmanClient(base_url=self.socket_uri) self.addCleanup(self.client.close) self.alpine_image = self.client.images.pull("quay.io/libpod/alpine", tag="latest") self.pod_name = f"pod_{random.getrandbits(160):x}" for container in self.client.containers.list(): container.remove(force=True) def tearDown(self): if self.client.pods.exists(self.pod_name): self.client.pods.remove(self.pod_name) super().tearDown() def test_pod_crud(self): """Test Pod CRUD.""" with self.subTest("Create no_infra"): pod = self.client.pods.create( self.pod_name, labels={ "unittest": "true", }, no_infra=True, ) self.assertEqual(self.pod_name, pod.name) self.assertTrue(self.client.pods.exists(pod.id)) with self.subTest("Inspect"): actual = self.client.pods.get(pod.id) self.assertEqual(actual.name, pod.name) self.assertNotIn("Containers", actual.attrs) with self.subTest("Exists"): self.assertTrue(self.client.pods.exists(self.pod_name)) # TODO Need method for deterministic prune... # with self.subTest("Prune"): # report = self.client.pods.prune() # self.assertIn("PodsDeleted", report) # self.assertIn(actual.id, report["PodsDeleted"]) # # with self.assertRaises(NotFound): # pod.reload() # # For now, delete pod explicitly with self.subTest("Delete"): pod.remove(force=True) with self.assertRaises(NotFound): pod.reload() def test_pod_crud_infra(self): """Test Pod CRUD with infra container.""" with self.subTest("Create with infra"): pod = self.client.pods.create( self.pod_name, labels={ "unittest": "true", }, ) self.assertEqual(self.pod_name, pod.name) with self.subTest("Inspect"): actual = self.client.pods.get(pod.id) self.assertEqual(actual.name, pod.name) self.assertIn("Containers", actual.attrs) self.assertEqual(actual.attrs["State"], "Created") with self.subTest("Add container"): container = self.client.containers.create(self.alpine_image, command=["ls"], pod=actual) actual = self.client.pods.get(pod.id) ids = {c["Id"] for c in actual.attrs["Containers"]} self.assertIn(container.id, ids) with self.subTest("List"): pods = self.client.pods.list() self.assertGreaterEqual(len(pods), 1) ids = {p.id for p in pods} self.assertIn(actual.id, ids) with self.subTest("Delete"): pod.remove(force=True) with self.assertRaises(NotFound): pod.reload() def test_ps(self): pod = self.client.pods.create( self.pod_name, labels={ "unittest": "true", }, no_infra=True, ) self.assertTrue(self.client.pods.exists(pod.id)) self.client.containers.create( self.alpine_image, command=["top"], detach=True, tty=True, pod=pod ) pod.start() pod.reload() with self.subTest("top"): # this is the API top call not the # top command running in the container procs = pod.top() self.assertGreater(len(procs["Processes"]), 0) self.assertGreater(len(procs["Titles"]), 0) with self.subTest("stats"): report = self.client.pods.stats(all=True) self.assertGreaterEqual(len(report), 1) with self.subTest("Stop/Start"): pod.stop() pod.reload() self.assertIn(pod.attrs["State"], ("Stopped", "Exited")) pod.start() pod.reload() self.assertEqual(pod.attrs["State"], "Running") with self.subTest("Restart"): pod.stop() pod.restart() pod.reload() self.assertEqual(pod.attrs["State"], "Running") with self.subTest("Pause/Unpause"): pod.pause() pod.reload() self.assertEqual(pod.attrs["State"], "Paused") pod.unpause() pod.reload() self.assertEqual(pod.attrs["State"], "Running") pod.stop() if __name__ == '__main__': unittest.main() podman-py-5.4.0.1/podman/tests/integration/test_secrets.py000066400000000000000000000027161475540541200236240ustar00rootroot00000000000000"""Secrets integration tests.""" import random import unittest import uuid from podman import PodmanClient from podman.errors import NotFound from podman.tests.integration import base class SecretsIntegrationTest(base.IntegrationTest): """Secrets call integration test""" def setUp(self): super().setUp() self.client = PodmanClient(base_url=self.socket_uri) self.addCleanup(self.client.close) def test_secret_crud(self): """Test Secret CRUD.""" random_string = f"{random.getrandbits(160):x}" secret_payload = uuid.uuid4().bytes with self.subTest("Create"): secret = self.client.secrets.create(f"secret_{random_string}", secret_payload) self.assertGreater(len(secret.id), 0) self.assertGreater(len(secret.name), 0) self.assertTrue(self.client.secrets.exists(secret.id)) with self.subTest("Inspect"): actual = self.client.secrets.get(secret.id) self.assertEqual(secret.id, actual.id) self.assertTrue(self.client.secrets.exists(secret.name)) with self.subTest("List"): report = self.client.secrets.list() ids = [i.id for i in report] self.assertIn(secret.id, ids) with self.subTest("Delete"): actual.remove() with self.assertRaises(NotFound): self.client.secrets.get(secret.id) if __name__ == '__main__': unittest.main() podman-py-5.4.0.1/podman/tests/integration/test_system.py000066400000000000000000000044131475540541200234740ustar00rootroot00000000000000# Copyright 2020 Red Hat, Inc. # # 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. # """system call integration tests""" import podman.tests.integration.base as base from podman import PodmanClient from podman.errors import APIError class SystemIntegrationTest(base.IntegrationTest): """system call integration test""" def setUp(self): super().setUp() self.client = PodmanClient(base_url=self.socket_uri) self.addCleanup(self.client.close) def test_info(self): """integration: system info call""" output = self.client.info() self.assertTrue('host' in output) def test_version(self): """integration: system version call""" output = self.client.version() self.assertTrue('Platform' in output) self.assertTrue('Version' in output) self.assertTrue('ApiVersion' in output) def test_show_disk_usage(self): """integration: system disk usage call""" output = self.client.df() self.assertTrue('Images' in output) self.assertTrue('Containers' in output) self.assertTrue('Volumes' in output) def test_login(self): """integration: system login call""" # here, we just test the sanity of the endpoint # confirming that we get through to podman, and get tcp rejected. with self.assertRaises(APIError) as e: next( self.client.login( "fake_user", "fake_password", "fake_email@fake_domain.test", "fake_registry" ) ) self.assertRegex( e.exception.explanation, r"lookup fake_registry.+no such host", ) def test_from_env(self): """integration: from_env() no error""" PodmanClient.from_env() podman-py-5.4.0.1/podman/tests/integration/test_volumes.py000066400000000000000000000027671475540541200236540ustar00rootroot00000000000000import random import unittest from podman import PodmanClient from podman.errors import NotFound from podman.tests.integration import base class VolumesIntegrationTest(base.IntegrationTest): def setUp(self): super().setUp() self.client = PodmanClient(base_url=self.socket_uri) self.addCleanup(self.client.close) def test_volume_crud(self): """Test Volume CRUD.""" volume_name = f"volume_{random.getrandbits(160):x}" self.assertFalse( self.client.volumes.exists(volume_name), "Storage is corrupt from previous run" ) with self.subTest("Create"): volume = self.client.volumes.create(volume_name) self.assertEqual(volume.name, volume_name) with self.subTest("Get"): actual = self.client.volumes.get(volume_name) self.assertDictEqual(actual.attrs, volume.attrs) self.assertTrue(self.client.volumes.exists(volume_name)) with self.subTest("List"): report = self.client.volumes.list() names = [i.name for i in report] self.assertIn(volume_name, names) with self.subTest("Remove"): self.client.volumes.remove(volume_name, force=True) with self.assertRaises(NotFound): self.client.volumes.get(volume_name) def test_inspect_404(self): with self.assertRaises(NotFound): self.client.volumes.get("NoSuchVolume") if __name__ == '__main__': unittest.main() podman-py-5.4.0.1/podman/tests/integration/utils.py000066400000000000000000000076321475540541200222570ustar00rootroot00000000000000# Copyright 2020 Red Hat, Inc. # # 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. # """Integration Test Utils""" import logging import os import shutil import subprocess import threading from contextlib import suppress from typing import Optional import time from podman.tests import errors logger = logging.getLogger("podman.service") class PodmanLauncher: """Podman service launcher""" def __init__( self, socket_uri: str, podman_path: Optional[str] = None, timeout: int = 0, privileged: bool = False, log_level: str = "WARNING", ) -> None: """create a launcher and build podman command""" podman_exe: str = podman_path if not podman_exe: podman_exe = shutil.which('podman') if podman_exe is None: raise errors.PodmanNotInstalled() self.socket_file: str = socket_uri.replace('unix://', '') self.log_level = log_level self.proc = None self.reference_id = hash(time.monotonic()) self.cmd: list[str] = [] if privileged: self.cmd.append('sudo') self.cmd.append(podman_exe) logger.setLevel(logging.getLevelName(log_level)) # Map from python to go logging levels, FYI trace level breaks cirrus logging self.cmd.append(f"--log-level={log_level.lower()}") if os.environ.get("container") == "oci": self.cmd.append("--storage-driver=vfs") self.cmd.extend( [ "system", "service", f"--time={timeout}", socket_uri, ] ) process = subprocess.run( [podman_exe, "--version"], check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT ) self.version = str(process.stdout.decode("utf-8")).strip().split()[2] def start(self, check_socket=True) -> None: """start podman service""" logger.info( "Launching(%s) %s refid=%s", self.version, ' '.join(self.cmd), self.reference_id, ) def consume_lines(pipe, consume_fn): with pipe: for line in iter(pipe.readline, b""): consume_fn(line.decode("utf-8")) def consume(line: str): logger.debug(line.strip("\n") + f" refid={self.reference_id}") self.proc = subprocess.Popen(self.cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) # pylint: disable=consider-using-with threading.Thread(target=consume_lines, args=[self.proc.stdout, consume]).start() if not check_socket: return # wait for socket to be created timeout = time.monotonic() + 30 while not os.path.exists(self.socket_file): if time.monotonic() > timeout: raise subprocess.TimeoutExpired("podman service ", timeout) time.sleep(0.2) def stop(self) -> None: """stop podman service""" if not self.proc: return self.proc.terminate() try: return_code = self.proc.wait(timeout=15) except subprocess.TimeoutExpired: self.proc.kill() return_code = self.proc.wait() self.proc = None with suppress(FileNotFoundError): os.remove(self.socket_file) logger.info("Command return Code: %d refid=%s", return_code, self.reference_id) podman-py-5.4.0.1/podman/tests/unit/000077500000000000000000000000001475540541200171715ustar00rootroot00000000000000podman-py-5.4.0.1/podman/tests/unit/__init__.py000066400000000000000000000000001475540541200212700ustar00rootroot00000000000000podman-py-5.4.0.1/podman/tests/unit/test_api_utils.py000066400000000000000000000126461475540541200226040ustar00rootroot00000000000000import json import pathlib import unittest from typing import Any, Optional from unittest import mock from unittest.mock import mock_open, patch from dataclasses import dataclass from podman import api class TestUtilsCase(unittest.TestCase): def test_format_filters(self): @dataclass class TestCase: name: str input: Any expected: Optional[str] cases = [ TestCase(name="empty str", input="", expected=None), TestCase(name="str", input="reference=fedora", expected='{"reference": ["fedora"]}'), TestCase( name="list[str]", input=["reference=fedora"], expected='{"reference": ["fedora"]}' ), TestCase( name="dict[str,str]", input={"reference": "fedora"}, expected='{"reference": ["fedora"]}', ), ] for case in cases: actual = api.prepare_filters(case.input) self.assertEqual( case.expected, actual, f"failed test {case.name} expected {case.expected}, actual {actual}", ) if actual is not None: self.assertIsInstance(actual, str) def test_containerignore_404(self): actual = api.prepare_containerignore("/does/not/exists") self.assertListEqual([], actual) @patch.object(pathlib.Path, "exists", return_value=True) def test_containerignore_read(self, patch_exists): data = r"""# unittest #Ignore the logs directory logs/ #Ignoring the password file passwords.txt #Ignoring git and cache folders .git .cache #Ignoring all the markdown and class files *.md **/*.class """ with mock.patch("pathlib.Path.open", mock_open(read_data=data)): actual = api.prepare_containerignore(".") self.assertListEqual( actual, ["logs/", "passwords.txt", ".git", ".cache", "*.md", "**/*.class"] ) patch_exists.assert_called_once_with() @patch.object(pathlib.Path, "exists", return_value=True) def test_containerignore_empty(self, patch_exists): data = r"""# unittest """ patch_exists.return_value = True with mock.patch("pathlib.Path.open", mock_open(read_data=data)): actual = api.prepare_containerignore(".") self.assertListEqual(actual, []) patch_exists.assert_called_once_with() @mock.patch("pathlib.Path.parent", autospec=True) def test_containerfile_1(self, mock_parent): mock_parent.samefile.return_value = True actual = api.prepare_containerfile("/work", "/work/Dockerfile") self.assertEqual(actual, "Dockerfile") mock_parent.samefile.assert_called() @mock.patch("pathlib.Path.parent", autospec=True) def test_containerfile_2(self, mock_parent): mock_parent.samefile.return_value = True actual = api.prepare_containerfile(".", "Dockerfile") self.assertEqual(actual, "Dockerfile") mock_parent.samefile.assert_called() @mock.patch("shutil.copy2") def test_containerfile_copy(self, mock_copy): mock_copy.return_value = None with mock.patch.object(pathlib.Path, "parent") as mock_parent: mock_parent.samefile.return_value = False actual = api.prepare_containerfile("/work", "/home/Dockerfile") self.assertRegex(actual, r"\.containerfile\..*") def test_prepare_body_all_types(self): payload = { "String": "string", "Integer": 42, "Boolean": True, "Dictionary": {"key": "value"}, "Tuple": (1, 2), "List": [1, 2], } actual = api.prepare_body(payload) self.assertEqual(actual, json.dumps(payload, sort_keys=True)) def test_prepare_body_none(self): payload = { "String": "", "Integer": None, "Boolean": False, "Dictionary": dict(), "Tuple": tuple(), "List": list(), } actual = api.prepare_body(payload) self.assertEqual(actual, '{"Boolean": false}') def test_prepare_body_embedded(self): payload = { "String": "", "Integer": None, "Boolean": False, "Dictionary": {"key": "value"}, "Dictionary2": {"key": {"key2": None}}, "Tuple": tuple(), "List": [None], "Set1": {"item1", "item2"}, "Set2": {None}, } actual = api.prepare_body(payload) actual_dict = json.loads(actual) # Because of the sets above we have to do some type dances to test results self.assertListEqual([*actual_dict], ["Boolean", "Dictionary", "Set1"]) self.assertEqual(actual_dict["Boolean"], payload["Boolean"]) self.assertDictEqual(actual_dict["Dictionary"], payload["Dictionary"]) self.assertEqual(set(actual_dict["Set1"]), {"item1", "item2"}) def test_prepare_body_dict_empty_string(self): payload = {"Dictionary": {"key1": "", "key2": {"key3": ""}, "key4": [], "key5": {}}} actual = api.prepare_body(payload) actual_dict = json.loads(actual) payload["Dictionary"].pop("key4") payload["Dictionary"].pop("key5") self.assertDictEqual(payload, actual_dict) if __name__ == '__main__': unittest.main() podman-py-5.4.0.1/podman/tests/unit/test_build.py000066400000000000000000000116231475540541200217040ustar00rootroot00000000000000import io import json import unittest try: # Python >= 3.10 from collections.abc import Iterable except ImportError: # Python < 3.10 from collections.abc import Iterable from unittest.mock import patch import requests_mock from podman import PodmanClient, api, tests from podman.domain.images import Image from podman.errors import BuildError, DockerException class TestBuildCase(unittest.TestCase): """Test ImagesManager build(). Note: Mock responses need to be coded for libpod returns. The python bindings are responsible for mapping to compatible output. """ def setUp(self) -> None: super().setUp() self.client = PodmanClient(base_url=tests.BASE_SOCK) def tearDown(self) -> None: super().tearDown() self.client.close() @patch.object(api, "create_tar") @patch.object(api, "prepare_containerfile") def test_build(self, mock_prepare_containerfile, mock_create_tar): mock_prepare_containerfile.return_value = "Containerfile" mock_create_tar.return_value = b"This is a mocked tarball." stream = [ {"stream": " ---\u003e a9eb17255234"}, {"stream": "Step 1 : VOLUME /data"}, {"stream": " ---\u003e Running in abdc1e6896c6"}, {"stream": " ---\u003e 713bca62012e"}, {"stream": "Removing intermediate container abdc1e6896c6"}, {"stream": "Step 2 : CMD [\"/bin/sh\"]"}, {"stream": " ---\u003e Running in dba30f2a1a7e"}, {"stream": " ---\u003e 032b8b2855fc"}, {"stream": "Removing intermediate container dba30f2a1a7e"}, {"stream": "032b8b2855fc\n"}, ] buffer = io.StringIO() for entry in stream: buffer.write(json.JSONEncoder().encode(entry)) buffer.write("\n") with requests_mock.Mocker() as mock: mock.post( tests.LIBPOD_URL + "/build" "?t=latest" "&buildargs=%7B%22BUILD_DATE%22%3A+%22January+1%2C+1970%22%7D" "&cpuperiod=10" "&extrahosts=%7B%22database%22%3A+%22127.0.0.1%22%7D" "&labels=%7B%22Unittest%22%3A+%22true%22%7D", text=buffer.getvalue(), ) mock.get( tests.LIBPOD_URL + "/images/032b8b2855fc/json", json={ "Id": "032b8b2855fc", "ParentId": "", "RepoTags": ["fedora:latest", "fedora:33", ":"], "RepoDigests": [ "fedora@sha256:9598a10fa72b402db876ccd4b3d240a4061c7d1e442745f1896ba37e1bf38664" ], "Created": 1614033320, "Size": 23855104, "VirtualSize": 23855104, "SharedSize": 0, "Labels": {}, "Containers": 2, }, ) image, logs = self.client.images.build( path="/tmp/context_dir", tag="latest", buildargs={ "BUILD_DATE": "January 1, 1970", }, container_limits={ "cpuperiod": 10, }, extra_hosts={"database": "127.0.0.1"}, labels={"Unittest": "true"}, ) self.assertIsInstance(image, Image) self.assertEqual(image.id, "032b8b2855fc") self.assertIsInstance(logs, Iterable) @patch.object(api, "create_tar") @patch.object(api, "prepare_containerfile") def test_build_logged_error(self, mock_prepare_containerfile, mock_create_tar): mock_prepare_containerfile.return_value = "Containerfile" mock_create_tar.return_value = b"This is a mocked tarball." stream = [ {"error": "We do not need any stinking badges."}, ] buffer = io.StringIO() for entry in stream: buffer.write(json.JSONEncoder().encode(entry)) buffer.write("\n") with requests_mock.Mocker() as mock: mock.post( tests.LIBPOD_URL + "/build", text=buffer.getvalue(), ) with self.assertRaises(BuildError) as e: self.client.images.build(path="/tmp/context_dir") self.assertEqual(e.exception.msg, "We do not need any stinking badges.") @requests_mock.Mocker() def test_build_no_context(self, mock): mock.post(tests.LIBPOD_URL + "/images/build") with self.assertRaises(TypeError): self.client.images.build() @requests_mock.Mocker() def test_build_encoding(self, mock): mock.post(tests.LIBPOD_URL + "/images/build") with self.assertRaises(DockerException): self.client.images.build(path="/root", gzip=True, encoding="utf-8") if __name__ == '__main__': unittest.main() podman-py-5.4.0.1/podman/tests/unit/test_config.py000066400000000000000000000116601475540541200220530ustar00rootroot00000000000000import unittest import urllib.parse import json import os import tempfile from pathlib import Path from unittest import mock from unittest.mock import MagicMock from podman.domain.config import PodmanConfig class PodmanConfigTestCaseDefault(unittest.TestCase): def setUp(self): self.temp_dir = tempfile.mkdtemp() # Data to be written to the JSON file self.data_json = """ { "Connection": { "Default": "testing_json", "Connections": { "testing_json": { "URI": "ssh://qe@localhost:2222/run/podman/podman.sock", "Identity": "/home/qe/.ssh/id_rsa" }, "production": { "URI": "ssh://root@localhost:22/run/podman/podman.sock", "Identity": "/home/root/.ssh/id_rsajson" } } }, "Farm": {} } """ # Data to be written to the TOML file self.data_toml = """ [containers] log_size_max = -1 pids_limit = 2048 userns_size = 65536 [engine] num_locks = 2048 active_service = "testing" stop_timeout = 10 [engine.service_destinations] [engine.service_destinations.production] uri = "ssh://root@localhost:22/run/podman/podman.sock" identity = "/home/root/.ssh/id_rsa" [engine.service_destinations.testing] uri = "ssh://qe@localhost:2222/run/podman/podman.sock" identity = "/home/qe/.ssh/id_rsa" [network] """ # Define the file path self.path_json = os.path.join(self.temp_dir, 'podman-connections.json') self.path_toml = os.path.join(self.temp_dir, 'containers.conf') # Write data to the JSON file j_data = json.loads(self.data_json) with open(self.path_json, 'w+') as file_json: json.dump(j_data, file_json) # Write data to the TOML file with open(self.path_toml, 'w+') as file_toml: # toml.dump(self.data_toml, file_toml) file_toml.write(self.data_toml) def test_connections(self): config = PodmanConfig("@@is_test@@" + self.temp_dir) self.assertEqual(config.active_service.id, "testing_json") expected = urllib.parse.urlparse("ssh://qe@localhost:2222/run/podman/podman.sock") self.assertEqual(config.active_service.url, expected) self.assertEqual(config.services["production"].identity, Path("/home/root/.ssh/id_rsajson")) class PodmanConfigTestCaseTOML(unittest.TestCase): opener = mock.mock_open( read_data=""" [containers] log_size_max = -1 pids_limit = 2048 userns_size = 65536 [engine] num_locks = 2048 active_service = "testing" stop_timeout = 10 [engine.service_destinations] [engine.service_destinations.production] uri = "ssh://root@localhost:22/run/podman/podman.sock" identity = "/home/root/.ssh/id_rsa" [engine.service_destinations.testing] uri = "ssh://qe@localhost:2222/run/podman/podman.sock" identity = "/home/qe/.ssh/id_rsa" [network] """ ) def setUp(self) -> None: super().setUp() def mocked_open(self, *args, **kwargs): return PodmanConfigTestCaseTOML.opener(self, *args, **kwargs) self.mocked_open = mocked_open def test_connections(self): with mock.patch.multiple(Path, open=self.mocked_open, exists=MagicMock(return_value=True)): config = PodmanConfig("/home/developer/containers.conf") self.assertEqual(config.active_service.id, "testing") expected = urllib.parse.urlparse("ssh://qe@localhost:2222/run/podman/podman.sock") self.assertEqual(config.active_service.url, expected) self.assertEqual(config.services["production"].identity, Path("/home/root/.ssh/id_rsa")) PodmanConfigTestCaseTOML.opener.assert_called_with( Path("/home/developer/containers.conf"), encoding='utf-8' ) class PodmanConfigTestCaseJSON(unittest.TestCase): def setUp(self) -> None: super().setUp() self.temp_dir = tempfile.mkdtemp() self.data = """ { "Connection": { "Default": "testing", "Connections": { "testing": { "URI": "ssh://qe@localhost:2222/run/podman/podman.sock", "Identity": "/home/qe/.ssh/id_rsa" }, "production": { "URI": "ssh://root@localhost:22/run/podman/podman.sock", "Identity": "/home/root/.ssh/id_rsa" } } }, "Farm": {} } """ self.path = os.path.join(self.temp_dir, 'podman-connections.json') # Write data to the JSON file data = json.loads(self.data) with open(self.path, 'w+') as file: json.dump(data, file) def test_connections(self): config = PodmanConfig(self.path) self.assertEqual(config.active_service.id, "testing") expected = urllib.parse.urlparse("ssh://qe@localhost:2222/run/podman/podman.sock") self.assertEqual(config.active_service.url, expected) self.assertEqual(config.services["production"].identity, Path("/home/root/.ssh/id_rsa")) if __name__ == '__main__': unittest.main() podman-py-5.4.0.1/podman/tests/unit/test_container.py000066400000000000000000000403571475540541200225750ustar00rootroot00000000000000import base64 import io import json import unittest try: # Python >= 3.10 from collections.abc import Iterable except ImportError: # Python < 3.10 from collections.abc import Iterable import requests_mock from podman import PodmanClient, tests from podman.domain.containers import Container from podman.domain.containers_manager import ContainersManager from podman.errors import APIError, NotFound FIRST_CONTAINER = { "Id": "87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd", "Image": "quay.io/fedora:latest", "Name": "evil_ptolemy", } class ContainersTestCase(unittest.TestCase): def setUp(self) -> None: super().setUp() self.client = PodmanClient(base_url=tests.BASE_SOCK) def tearDown(self) -> None: super().tearDown() self.client.close() @requests_mock.Mocker() def test_remove(self, mock): adapter = mock.delete( tests.LIBPOD_URL + "/containers/" "87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd?v=True&force=True", status_code=204, ) manager = ContainersManager(self.client.api) container = manager.prepare_model(attrs=FIRST_CONTAINER) container.remove(v=True, force=True) self.assertTrue(adapter.called_once) @requests_mock.Mocker() def test_rename(self, mock): adapter = mock.post( tests.LIBPOD_URL + "/containers/87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd/rename", status_code=204, ) container = Container(attrs=FIRST_CONTAINER, client=self.client.api) container.rename("good_galileo") self.assertEqual(container.attrs["Name"], "good_galileo") self.assertTrue(adapter.called_once) @requests_mock.Mocker() def test_rename_type_error(self, mock): container = Container( attrs={"ID": "87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd"} ) with self.assertRaises(TypeError): container.rename() @requests_mock.Mocker() def test_restart(self, mock): adapter = mock.post( tests.LIBPOD_URL + "/containers/" "87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd/restart?timeout=10", status_code=204, ) container = Container(attrs=FIRST_CONTAINER, client=self.client.api) container.restart(timeout=10) self.assertTrue(adapter.called_once) @requests_mock.Mocker() def test_start_dkeys(self, mock): adapter = mock.post( tests.LIBPOD_URL + "/containers/" "87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd/start" "?detachKeys=%5Ef%5Eu", status_code=204, ) container = Container(attrs=FIRST_CONTAINER, client=self.client.api) container.start(detach_keys="^f^u") self.assertTrue(adapter.called_once) @requests_mock.Mocker() def test_start(self, mock): adapter = mock.post( tests.LIBPOD_URL + "/containers/87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd/start", status_code=204, ) container = Container(attrs=FIRST_CONTAINER, client=self.client.api) container.start() self.assertTrue(adapter.called_once) @requests_mock.Mocker() def test_init(self, mock): adapter = mock.post( tests.LIBPOD_URL + "/containers/87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd/init", status_code=204, ) container = Container(attrs=FIRST_CONTAINER, client=self.client.api) container.init() self.assertTrue(adapter.called_once) @requests_mock.Mocker() def test_stats(self, mock): stream = [ { "Error": None, "Stats": [ { "ContainerId": ( "87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd" ), "Name": "evil_ptolemy", "CPU": 1000.0, } ], } ] buffer = io.StringIO() for entry in stream: buffer.write(json.JSONEncoder().encode(entry)) buffer.write("\n") adapter = mock.get( tests.LIBPOD_URL + "/containers/stats" "?containers=87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd" "&stream=True", text=buffer.getvalue(), ) container = Container(attrs=FIRST_CONTAINER, client=self.client.api) stats = container.stats(decode=True) self.assertIsInstance(stats, Iterable) for entry in stats: self.assertIsInstance(entry, dict) for stat in entry["Stats"]: self.assertEqual( stat["ContainerId"], "87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd", ) self.assertTrue(adapter.called_once) @requests_mock.Mocker() def test_stop(self, mock): adapter = mock.post( tests.LIBPOD_URL + "/containers/" "87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd/stop" "?all=True&timeout=10.0", status_code=204, ) container = Container(attrs=FIRST_CONTAINER, client=self.client.api) container.stop(all=True, timeout=10.0) self.assertTrue(adapter.called_once) @requests_mock.Mocker() def test_stop_304(self, mock): adapter = mock.post( tests.LIBPOD_URL + "/containers/87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd/stop", json={ "cause": "container already stopped", "message": "container already stopped", "response": 304, }, status_code=304, ) container = Container(attrs=FIRST_CONTAINER, client=self.client.api) with self.assertRaises(APIError): container.stop() self.assertTrue(adapter.called_once) @requests_mock.Mocker() def test_unpause(self, mock): adapter = mock.post( tests.LIBPOD_URL + "/containers/" "87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd/unpause", status_code=204, ) container = Container(attrs=FIRST_CONTAINER, client=self.client.api) container.unpause() self.assertTrue(adapter.called_once) @requests_mock.Mocker() def test_pause(self, mock): adapter = mock.post( tests.LIBPOD_URL + "/containers/87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd/pause", status_code=204, ) container = Container(attrs=FIRST_CONTAINER, client=self.client.api) container.pause() self.assertTrue(adapter.called_once) @requests_mock.Mocker() def test_wait(self, mock): adapter = mock.post( tests.LIBPOD_URL + "/containers/87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd/wait", status_code=200, json={"StatusCode": 0}, ) container = Container(attrs=FIRST_CONTAINER, client=self.client.api) container.wait() self.assertTrue(adapter.called_once) @requests_mock.Mocker() def test_wait_condition_interval(self, mock): adapter = mock.post( tests.LIBPOD_URL + "/containers/87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd/wait", status_code=200, json={"StatusCode": 0}, ) container = Container(attrs=FIRST_CONTAINER, client=self.client.api) container.wait(condition="exited", interval=1) self.assertTrue(adapter.called_once) @requests_mock.Mocker() def test_diff(self, mock): payload = [ {"Path": "modified", "Kind": 0}, {"Path": "added", "Kind": 1}, {"Path": "deleted", "Kind": 2}, ] adapter = mock.get( tests.LIBPOD_URL + "/containers/" "87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd/changes", json=payload, ) container = Container(attrs=FIRST_CONTAINER, client=self.client.api) actual = container.diff() self.assertListEqual(actual, payload) self.assertTrue(adapter.called_once) @requests_mock.Mocker() def test_diff_404(self, mock): adapter = mock.get( tests.LIBPOD_URL + "/containers/" "87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd/changes", json={ "cause": "Container not found.", "message": "Container not found.", "response": 404, }, status_code=404, ) container = Container(attrs=FIRST_CONTAINER, client=self.client.api) with self.assertRaises(NotFound): container.diff() self.assertTrue(adapter.called_once) @requests_mock.Mocker() def test_export(self, mock): tarball = b'Yet another weird tarball...' body = io.BytesIO(tarball) adapter = mock.get( tests.LIBPOD_URL + "/containers/87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd/export", body=body, ) container = Container(attrs=FIRST_CONTAINER, client=self.client.api) with io.BytesIO() as fd: for chunk in container.export(): fd.write(chunk) self.assertEqual(fd.getbuffer(), tarball) self.assertTrue(adapter.called_once) @requests_mock.Mocker() def test_get_archive(self, mock): tarball = b'Yet another weird tarball...' body = io.BytesIO(tarball) header_value = { "name": "/etc/motd", "size": len(tarball), "mode": 0o444, "mtime": "20210309T12:49:0205:00", } encoded_value = base64.urlsafe_b64encode(json.dumps(header_value).encode("utf8")) adapter = mock.get( tests.LIBPOD_URL + "/containers/" "87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd/archive" "?path=/etc/motd", body=body, headers={"x-docker-container-path-stat": encoded_value.decode("utf8")}, ) container = Container(attrs=FIRST_CONTAINER, client=self.client.api) actual = container.get_archive("/etc/motd") self.assertEqual(len(actual), 2) self.assertEqual(actual[1]["name"], "/etc/motd") with io.BytesIO() as fd: for chunk in actual[0]: fd.write(chunk) self.assertEqual(fd.getbuffer(), tarball) self.assertTrue(adapter.called_once) @requests_mock.Mocker() def test_commit(self, mock): post_adapter = mock.post( tests.LIBPOD_URL + "/commit" "?author=redhat&changes=ADD+%2fetc%2fmod&comment=This+is+a+unittest" "&container=87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd&format=docker" "&pause=True&repo=quay.local&tag=unittest", status_code=201, json={"Id": "d2459aad75354ddc9b5b23f863786e279637125af6ba4d4a83f881866b3c903f"}, ) get_adapter = mock.get( tests.LIBPOD_URL + "/images/d2459aad75354ddc9b5b23f863786e279637125af6ba4d4a83f881866b3c903f/json", json={"Id": "d2459aad75354ddc9b5b23f863786e279637125af6ba4d4a83f881866b3c903f"}, ) container = Container(attrs=FIRST_CONTAINER, client=self.client.api) image = container.commit( repository="quay.local", tag="unittest", author="redhat", changes=["ADD /etc/mod"], comment="This is a unittest", format="docker", message="This is a unittest", pause=True, ) self.assertEqual( image.id, "d2459aad75354ddc9b5b23f863786e279637125af6ba4d4a83f881866b3c903f" ) self.assertTrue(post_adapter.called_once) self.assertTrue(get_adapter.called_once) @requests_mock.Mocker() def test_put_archive(self, mock): adapter = mock.put( tests.LIBPOD_URL + "/containers/" "87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd/archive" "?path=%2fetc%2fmotd", status_code=200, ) container = Container(attrs=FIRST_CONTAINER, client=self.client.api) tarball = b'Yet another weird tarball...' body = io.BytesIO(tarball) actual = container.put_archive(path="/etc/motd", data=body.getvalue()) self.assertTrue(actual) self.assertTrue(adapter.called_once) @requests_mock.Mocker() def test_put_archive_404(self, mock): adapter = mock.put( tests.LIBPOD_URL + "/containers/" "87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd/archive" "?path=deadbeef", status_code=404, json={ "cause": "Container not found.", "message": "Container not found.", "response": 404, }, ) container = Container(attrs=FIRST_CONTAINER, client=self.client.api) tarball = b'Yet another weird tarball...' body = io.BytesIO(tarball) actual = container.put_archive(path="deadbeef", data=body.getvalue()) self.assertFalse(actual) self.assertTrue(adapter.called_once) @requests_mock.Mocker() def test_top(self, mock): body = { "Processes": [ [ 'jhonce', '2417', '2274', '0', 'Mar01', '?', '00:00:01', '/usr/bin/ssh-agent /bin/sh -c exec -l /bin/bash -c "/usr/bin/gnome-session"', ], ['jhonce', '5544', '3522', '0', 'Mar01', 'pts/1', '00:00:02', '-bash'], ['jhonce', '6140', '3522', '0', 'Mar01', 'pts/2', '00:00:00', '-bash'], ], "Titles": ["UID", "PID", "PPID", "C", "STIME", "TTY", "TIME CMD"], } adapter = mock.get( tests.LIBPOD_URL + "/containers/87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd/top", json=body, ) container = Container(attrs=FIRST_CONTAINER, client=self.client.api) actual = container.top() self.assertDictEqual(actual, body) self.assertTrue(adapter.called_once) @requests_mock.Mocker() def test_top_with_streaming(self, mock): stream = [ { "Processes": [ [ 'jhonce', '2417', '2274', '0', 'Mar01', '?', '00:00:01', '/usr/bin/ssh-agent /bin/sh -c exec -l /bin/bash' + '-c "/usr/bin/gnome-session"', ], ['jhonce', '5544', '3522', '0', 'Mar01', 'pts/1', '00:00:02', '-bash'], ['jhonce', '6140', '3522', '0', 'Mar01', 'pts/2', '00:00:00', '-bash'], ], "Titles": ["UID", "PID", "PPID", "C", "STIME", "TTY", "TIME CMD"], } ] buffer = io.StringIO() for entry in stream: buffer.write(json.JSONEncoder().encode(entry)) buffer.write("\n") adapter = mock.get( tests.LIBPOD_URL + "/containers/87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd/top" "?stream=True", text=buffer.getvalue(), ) container = Container(attrs=FIRST_CONTAINER, client=self.client.api) top_stats = container.top(stream=True) self.assertIsInstance(top_stats, Iterable) for response, actual in zip(top_stats, stream): self.assertIsInstance(response, dict) self.assertDictEqual(response, actual) self.assertTrue(adapter.called_once) if __name__ == '__main__': unittest.main() podman-py-5.4.0.1/podman/tests/unit/test_containersmanager.py000066400000000000000000000272531475540541200243130ustar00rootroot00000000000000import json import unittest try: # Python >= 3.10 from collections.abc import Iterator except ImportError: # Python < 3.10 from collections.abc import Iterator from unittest.mock import DEFAULT, patch, MagicMock import requests_mock from podman import PodmanClient, tests from podman.domain.containers import Container from podman.domain.containers_manager import ContainersManager from podman.errors import ImageNotFound, NotFound FIRST_CONTAINER = { "Id": "87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd", "Image": "quay.io/fedora:latest", "Name": "evil_ptolemy", "HostConfig": {"LogConfig": {"Type": "json-file"}}, } SECOND_CONTAINER = { "Id": "6dc84cc0a46747da94e4c1571efcc01a756b4017261440b4b8985d37203c3c03", "Image": "quay.io/fedora:rc", "Name": "good_galileo", } class ContainersManagerTestCase(unittest.TestCase): """Test ContainersManager area of concern. Note: Mock responses need to be coded for libpod returns. The python bindings are responsible for mapping to compatible output. """ def setUp(self) -> None: super().setUp() self.client = PodmanClient(base_url=tests.BASE_SOCK) def tearDown(self) -> None: super().tearDown() self.client.close() def test_podmanclient(self): manager = self.client.containers self.assertIsInstance(manager, ContainersManager) @requests_mock.Mocker() def test_get(self, mock): mock.get( tests.LIBPOD_URL + "/containers/87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd/json", json=FIRST_CONTAINER, ) actual = self.client.containers.get( "87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd" ) self.assertEqual( actual.id, "87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd" ) @requests_mock.Mocker() def test_get_404(self, mock): mock.get( tests.LIBPOD_URL + "/containers/87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd/json", json={ "cause": "Container not found.", "message": "Container not found.", "response": 404, }, status_code=404, ) with self.assertRaises(NotFound): self.client.containers.get( "87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd" ) @requests_mock.Mocker() def test_list_empty(self, mock): mock.get( tests.LIBPOD_URL + "/containers/json", text="[]", ) actual = self.client.containers.list() self.assertListEqual(actual, []) @requests_mock.Mocker() def test_list(self, mock): mock.get( tests.LIBPOD_URL + "/containers/json", json=[FIRST_CONTAINER, SECOND_CONTAINER], ) actual = self.client.containers.list() self.assertIsInstance(actual, list) self.assertEqual( actual[0].id, "87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd" ) self.assertEqual( actual[1].id, "6dc84cc0a46747da94e4c1571efcc01a756b4017261440b4b8985d37203c3c03" ) @requests_mock.Mocker() def test_list_filtered(self, mock): mock.get( tests.LIBPOD_URL + "/containers/json?" "all=True" "&filters=%7B" "%22before%22%3A" "+%5B%226dc84cc0a46747da94e4c1571efcc01a756b4017261440b4b8985d37203c3c03%22%5D%2C" "+%22since%22%3A" "+%5B%2287e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd%22%5D%2C" "+%22status%22%3A+%5B%22running%22%5D%7D", json=[FIRST_CONTAINER, SECOND_CONTAINER], ) actual = self.client.containers.list( all=True, filters={"status": "running"}, since="87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd", before="6dc84cc0a46747da94e4c1571efcc01a756b4017261440b4b8985d37203c3c03", ) self.assertIsInstance(actual, list) self.assertEqual( actual[0].id, "87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd" ) self.assertEqual( actual[1].id, "6dc84cc0a46747da94e4c1571efcc01a756b4017261440b4b8985d37203c3c03" ) @requests_mock.Mocker() def test_list_no_filters(self, mock): mock.get( tests.LIBPOD_URL + "/containers/json", json=[FIRST_CONTAINER, SECOND_CONTAINER], ) actual = self.client.containers.list() self.assertIsInstance(actual, list) self.assertEqual( actual[0].id, "87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd" ) self.assertEqual( actual[1].id, "6dc84cc0a46747da94e4c1571efcc01a756b4017261440b4b8985d37203c3c03" ) @requests_mock.Mocker() def test_prune(self, mock): mock.post( tests.LIBPOD_URL + "/containers/prune", json=[ { "Id": "87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd", "Size": 1024, }, { "Id": "6dc84cc0a46747da94e4c1571efcc01a756b4017261440b4b8985d37203c3c03", "Size": 1024, }, ], ) actual = self.client.containers.prune() self.assertDictEqual( actual, { "ContainersDeleted": [ "87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd", "6dc84cc0a46747da94e4c1571efcc01a756b4017261440b4b8985d37203c3c03", ], "SpaceReclaimed": 2048, }, ) @requests_mock.Mocker() def test_create(self, mock): mock.post( tests.LIBPOD_URL + "/containers/create", status_code=201, json={ "Id": "87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd", "Warnings": [], }, ) mock.get( tests.LIBPOD_URL + "/containers/87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd/json", json=FIRST_CONTAINER, ) actual = self.client.containers.create("fedora", "/usr/bin/ls", cpu_count=9999) self.assertIsInstance(actual, Container) @requests_mock.Mocker() def test_create_404(self, mock): mock.post( tests.LIBPOD_URL + "/containers/create", status_code=404, json={ "cause": "Image not found", "message": "Image not found", "response": 404, }, ) with self.assertRaises(ImageNotFound): self.client.containers.create("fedora", "/usr/bin/ls", cpu_count=9999) @requests_mock.Mocker() def test_create_parse_host_port(self, mock): mock_response = MagicMock() mock_response.json = lambda: { "Id": "87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd", "Size": 1024, } self.client.containers.client.post = MagicMock(return_value=mock_response) mock.get( tests.LIBPOD_URL + "/containers/87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd/json", json=FIRST_CONTAINER, ) port_str = {'2233': 3333} port_str_protocol = {'2244/tcp': 3344} port_int = {2255: 3355} ports = {**port_str, **port_str_protocol, **port_int} self.client.containers.create("fedora", "/usr/bin/ls", ports=ports) self.client.containers.client.post.assert_called() expected_ports = [ { 'container_port': 2233, 'host_port': 3333, 'protocol': 'tcp', }, { 'container_port': 2244, 'host_port': 3344, 'protocol': 'tcp', }, { 'container_port': 2255, 'host_port': 3355, 'protocol': 'tcp', }, ] actual_ports = json.loads(self.client.containers.client.post.call_args[1]['data'])[ 'portmappings' ] self.assertEqual(expected_ports, actual_ports) def test_create_unsupported_key(self): with self.assertRaises(TypeError): self.client.containers.create("fedora", "/usr/bin/ls", blkio_weight=100.0) def test_create_unknown_key(self): with self.assertRaises(TypeError): self.client.containers.create("fedora", "/usr/bin/ls", unknown_key=100.0) @requests_mock.Mocker() def test_run_detached(self, mock): mock.post( tests.LIBPOD_URL + "/containers/create", status_code=201, json={ "Id": "87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd", "Warnings": [], }, ) mock.post( tests.LIBPOD_URL + "/containers/87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd/start", status_code=204, ) mock.get( tests.LIBPOD_URL + "/containers/87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd/json", json=FIRST_CONTAINER, ) with patch.multiple(Container, logs=DEFAULT, wait=DEFAULT, autospec=True) as mock_container: mock_container["logs"].return_value = [] mock_container["wait"].return_value = {"StatusCode": 0} actual = self.client.containers.run("fedora", "/usr/bin/ls", detach=True) self.assertIsInstance(actual, Container) @requests_mock.Mocker() def test_run(self, mock): mock.post( tests.LIBPOD_URL + "/containers/create", status_code=201, json={ "Id": "87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd", "Warnings": [], }, ) mock.post( tests.LIBPOD_URL + "/containers/87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd/start", status_code=204, ) mock.get( tests.LIBPOD_URL + "/containers/87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd/json", json=FIRST_CONTAINER, ) mock_logs = ( b"This is a unittest - line 1", b"This is a unittest - line 2", ) with patch.multiple(Container, logs=DEFAULT, wait=DEFAULT, autospec=True) as mock_container: mock_container["wait"].return_value = 0 with self.subTest("Results not streamed"): mock_container["logs"].return_value = iter(mock_logs) actual = self.client.containers.run("fedora", "/usr/bin/ls") self.assertIsInstance(actual, bytes) self.assertEqual(actual, b'This is a unittest - line 1This is a unittest - line 2') # iter() cannot be reset so subtests used to create new instance with self.subTest("Stream results"): mock_container["logs"].return_value = iter(mock_logs) actual = self.client.containers.run("fedora", "/usr/bin/ls", stream=True) self.assertNotIsInstance(actual, bytes) self.assertIsInstance(actual, Iterator) self.assertEqual(next(actual), b"This is a unittest - line 1") self.assertEqual(next(actual), b"This is a unittest - line 2") if __name__ == '__main__': unittest.main() podman-py-5.4.0.1/podman/tests/unit/test_events.py000066400000000000000000000034301475540541200221060ustar00rootroot00000000000000import io import json import unittest from types import GeneratorType import requests_mock from podman import PodmanClient, tests from podman.domain.events import EventsManager class EventsManagerTestCase(unittest.TestCase): def setUp(self) -> None: super().setUp() self.client = PodmanClient(base_url=tests.BASE_SOCK) def tearDown(self) -> None: super().tearDown() self.client.close() @requests_mock.Mocker() def test_list(self, mock): stream = [ { "Type": "pod", "Action": "create", "Actor": { "ID": "", "Attributes": { "image": "", "name": "", "containerExitCode": 0, }, }, "Scope": "local", "Time": 1615845480, "TimeNano": 1615845480, } ] buffer = io.StringIO() for item in stream: buffer.write(json.JSONEncoder().encode(item)) buffer.write("\n") adapter = mock.get(tests.LIBPOD_URL + "/events", text=buffer.getvalue()) # noqa: F841 manager = EventsManager(client=self.client.api) actual = manager.list(decode=True) self.assertIsInstance(actual, GeneratorType) for item in actual: self.assertIsInstance(item, dict) self.assertEqual(item["Type"], "pod") actual = manager.list(decode=False) self.assertIsInstance(actual, GeneratorType) for item in actual: self.assertIsInstance(item, bytes) event = json.loads(item) self.assertEqual(event["Type"], "pod") if __name__ == '__main__': unittest.main() podman-py-5.4.0.1/podman/tests/unit/test_image.py000066400000000000000000000063741475540541200216760ustar00rootroot00000000000000import io import unittest import requests_mock from podman import PodmanClient, tests from podman.domain.images import Image FIRST_IMAGE = { "Id": "326dd9d7add24646a325e8eaa82125294027db2332e49c5828d96312c5d773ab", "ParentId": "", "RepoTags": ["fedora:latest", "fedora:33", ":"], "RepoDigests": [ "fedora@sha256:9598a10fa72b402db876ccd4b3d240a4061c7d1e442745f1896ba37e1bf38664" ], "Created": 1614033320, "Size": 23855104, "VirtualSize": 23855104, "SharedSize": 0, "Labels": {}, "Containers": 2, } SECOND_IMAGE = { "Id": "c4b16966ecd94ffa910eab4e630e24f259bf34a87e924cd4b1434f267b0e354e", "ParentId": "", "RepoDigests": [ "fedora@sha256:4a877de302c6463cb624ddfe146ad850413724462ec24847832aa6eb1e957746" ], "Created": 1614033320, "Size": 23855104, "VirtualSize": 23855104, "SharedSize": 0, "Containers": 0, } class ImageTestCase(unittest.TestCase): def setUp(self) -> None: super().setUp() self.client = PodmanClient(base_url=tests.BASE_SOCK) def tearDown(self) -> None: super().tearDown() self.client.close() @requests_mock.Mocker() def test_history(self, mock): adapter = mock.get( tests.LIBPOD_URL + "/images/326dd9d7add24646a325e8eaa82125294027db2332e49c5828d96312c5d773ab/history", json=[ { "Id": "326dd9d7add24646a325e8eaa82125294027db2332e49c5828d96312c5d773ab", "Comment": "", "Created": 1614208404, "CreatedBy": "2021-02-24T23:13:24+00:00", "Tags": ["latest"], "Size": 1024, } ], ) image = Image(attrs=FIRST_IMAGE, client=self.client.api) history = image.history() self.assertEqual(history[0]["Id"], image.id) self.assertTrue(adapter.called_once) @requests_mock.Mocker() def test_reload(self, mock): update = FIRST_IMAGE.copy() update["Containers"] = 0 adapter = mock.get( tests.LIBPOD_URL + "/images/326dd9d7add24646a325e8eaa82125294027db2332e49c5828d96312c5d773ab/json", [ {"json": FIRST_IMAGE}, {"json": update}, ], ) image = self.client.images.get( "326dd9d7add24646a325e8eaa82125294027db2332e49c5828d96312c5d773ab" ) self.assertEqual(image.attrs["Containers"], 2) image.reload() self.assertEqual(image.attrs["Containers"], 0) self.assertTrue(adapter.call_count, 2) @requests_mock.Mocker() def test_save(self, mock): tarball = b'Yet another weird tarball...' body = io.BytesIO(tarball) adapter = mock.get( tests.LIBPOD_URL + "/images/326dd9d7add24646a325e8eaa82125294027db2332e49c5828d96312c5d773ab/get", body=body, ) image = Image(attrs=FIRST_IMAGE, client=self.client.api) with io.BytesIO() as fd: for chunk in image.save(): fd.write(chunk) self.assertEqual(fd.getbuffer(), tarball) self.assertTrue(adapter.called_once) if __name__ == '__main__': unittest.main() podman-py-5.4.0.1/podman/tests/unit/test_imagesmanager.py000066400000000000000000000611741475540541200234130ustar00rootroot00000000000000import types import unittest from unittest.mock import patch try: # Python >= 3.10 from collections.abc import Iterable except ImportError: # Python < 3.10 from collections.abc import Iterable import requests_mock from podman import PodmanClient, tests from podman.domain.images import Image from podman.domain.images_manager import ImagesManager from podman.errors import APIError, ImageNotFound, PodmanError FIRST_IMAGE = { "Id": "sha256:326dd9d7add24646a325e8eaa82125294027db2332e49c5828d96312c5d773ab", "ParentId": "", "RepoTags": ["fedora:latest", "fedora:33", ":"], "RepoDigests": [ "fedora@sha256:9598a10fa72b402db876ccd4b3d240a4061c7d1e442745f1896ba37e1bf38664" ], "Created": 1614033320, "Size": 23855104, "VirtualSize": 23855104, "SharedSize": 0, "Labels": {"license": " Apache-2.0"}, "Containers": 2, } SECOND_IMAGE = { "Id": "c4b16966ecd94ffa910eab4e630e24f259bf34a87e924cd4b1434f267b0e354e", "ParentId": "", "RepoDigests": [ "fedora@sha256:4a877de302c6463cb624ddfe146ad850413724462ec24847832aa6eb1e957746" ], "Created": 1614033320, "Size": 23855104, "VirtualSize": 23855104, "SharedSize": 0, "Containers": 0, } class ImagesManagerTestCase(unittest.TestCase): """Test ImagesManager area of concern. Note: Mock responses need to be coded for libpod returns. The python bindings are responsible for mapping to compatible output. """ def setUp(self) -> None: super().setUp() self.client = PodmanClient(base_url=tests.BASE_SOCK) def tearDown(self) -> None: super().tearDown() self.client.close() def test_podmanclient(self): manager = self.client.images self.assertIsInstance(manager, ImagesManager) @requests_mock.Mocker() def test_list_empty(self, mock): """Unit test Images list().""" mock.get( tests.LIBPOD_URL + "/images/json", text="[]", ) images = self.client.images.list() self.assertEqual(len(images), 0) @requests_mock.Mocker() def test_list_1(self, mock): """Unit test Images list().""" mock.get( tests.LIBPOD_URL + "/images/json", json=[FIRST_IMAGE], ) images = self.client.images.list() self.assertEqual(len(images), 1) self.assertIsInstance(images[0], Image) self.assertEqual(str(images[0]), "") self.assertEqual( images[0].id, "sha256:326dd9d7add24646a325e8eaa82125294027db2332e49c5828d96312c5d773ab" ) self.assertIsInstance(images[0].labels, dict) self.assertEqual(len(images[0].labels), 1) self.assertEqual(images[0].short_id, "sha256:326dd9d7ad") self.assertIsInstance(images[0].tags, list) self.assertEqual(len(images[0].tags), 2) @requests_mock.Mocker() def test_list_2(self, mock): """Unit test Images list().""" mock.get( tests.LIBPOD_URL + "/images/json", json=[FIRST_IMAGE, SECOND_IMAGE], ) images = self.client.images.list() self.assertEqual(len(images), 2) self.assertIsInstance(images[0], Image) self.assertIsInstance(images[1], Image) self.assertEqual(images[1].short_id, "c4b16966ec") self.assertIsInstance(images[1].labels, dict) self.assertEqual(len(images[1].labels), 0) self.assertIsInstance(images[1].tags, list) self.assertEqual(len(images[1].tags), 0) @requests_mock.Mocker() def test_list_filters(self, mock): """Unit test filters param for Images list().""" mock.get( tests.LIBPOD_URL + "/images/json?filters=%7B%22dangling%22%3A+%5B%22True%22%5D%7D", json=[FIRST_IMAGE], ) images = self.client.images.list(filters={"dangling": True}) self.assertEqual( images[0].id, "sha256:326dd9d7add24646a325e8eaa82125294027db2332e49c5828d96312c5d773ab" ) @requests_mock.Mocker() def test_list_all(self, mock): """Unit test filters param for Images list().""" mock.get( tests.LIBPOD_URL + "/images/json?all=true", json=[FIRST_IMAGE], ) images = self.client.images.list(all=True) self.assertEqual( images[0].id, "sha256:326dd9d7add24646a325e8eaa82125294027db2332e49c5828d96312c5d773ab" ) @requests_mock.Mocker() def test_prune(self, mock): """Unit test Images prune().""" mock.post( tests.LIBPOD_URL + "/images/prune", json=[ { "Id": "326dd9d7add24646a325e8eaa82125294027db2332e49c5828d96312c5d773ab", "Err": None, "Size": 1024, } ], ) results = self.client.images.prune() self.assertIn("ImagesDeleted", results) self.assertIn("SpaceReclaimed", results) self.assertEqual( results["ImagesDeleted"][0]["Deleted"], "326dd9d7add24646a325e8eaa82125294027db2332e49c5828d96312c5d773ab", ) self.assertEqual(results["SpaceReclaimed"], 1024) @requests_mock.Mocker() def test_prune_filters(self, mock): """Unit test filters param for Images prune().""" mock.post( tests.LIBPOD_URL + "/images/prune?filters=%7B%22dangling%22%3A+%5B%22True%22%5D%7D", json=[ { "Id": "326dd9d7add24646a325e8eaa82125294027db2332e49c5828d96312c5d773ab", "Size": 1024, }, { "Id": "c4b16966ecd94ffa910eab4e630e24f259bf34a87e924cd4b1434f267b0e354e", "Size": 1024, }, ], ) report = self.client.images.prune(filters={"dangling": True}) self.assertIn("ImagesDeleted", report) self.assertIn("SpaceReclaimed", report) self.assertEqual(report["SpaceReclaimed"], 2048) deleted = [r["Deleted"] for r in report["ImagesDeleted"] if "Deleted" in r] self.assertEqual(len(deleted), 2) self.assertIn("326dd9d7add24646a325e8eaa82125294027db2332e49c5828d96312c5d773ab", deleted) self.assertGreater(len("".join(deleted)), 0) untagged = [r["Untagged"] for r in report["ImagesDeleted"] if "Untagged" in r] self.assertEqual(len(untagged), 2) self.assertEqual(len("".join(untagged)), 0) @requests_mock.Mocker() def test_prune_filters_label(self, mock): """Unit test filters param label for Images prune().""" mock.post( tests.LIBPOD_URL + "/images/prune?filters=%7B%22label%22%3A+%5B%22%7B%27license%27%3A+" + "%27Apache-2.0%27%7D%22%5D%7D", json=[ { "Id": "326dd9d7add24646a325e8eaa82125294027db2332e49c5828d96312c5d773ab", "Size": 1024, }, ], ) report = self.client.images.prune(filters={"label": {"license": "Apache-2.0"}}) self.assertIn("ImagesDeleted", report) self.assertIn("SpaceReclaimed", report) self.assertEqual(report["SpaceReclaimed"], 1024) deleted = [r["Deleted"] for r in report["ImagesDeleted"] if "Deleted" in r] self.assertEqual(len(deleted), 1) self.assertIn("326dd9d7add24646a325e8eaa82125294027db2332e49c5828d96312c5d773ab", deleted) self.assertGreater(len("".join(deleted)), 0) untagged = [r["Untagged"] for r in report["ImagesDeleted"] if "Untagged" in r] self.assertEqual(len(untagged), 1) self.assertEqual(len("".join(untagged)), 0) @requests_mock.Mocker() def test_prune_filters_not_label(self, mock): """Unit test filters param NOT-label for Images prune().""" mock.post( tests.LIBPOD_URL + "/images/prune?filters=%7B%22label%21%22%3A+%5B%22%7B%27license%27%3A+" + "%27Apache-2.0%27%7D%22%5D%7D", json=[ { "Id": "c4b16966ecd94ffa910eab4e630e24f259bf34a87e924cd4b1434f267b0e354e", "Size": 1024, }, ], ) report = self.client.images.prune(filters={"label!": {"license": "Apache-2.0"}}) self.assertIn("ImagesDeleted", report) self.assertIn("SpaceReclaimed", report) self.assertEqual(report["SpaceReclaimed"], 1024) deleted = [r["Deleted"] for r in report["ImagesDeleted"] if "Deleted" in r] self.assertEqual(len(deleted), 1) self.assertIn("c4b16966ecd94ffa910eab4e630e24f259bf34a87e924cd4b1434f267b0e354e", deleted) self.assertGreater(len("".join(deleted)), 0) untagged = [r["Untagged"] for r in report["ImagesDeleted"] if "Untagged" in r] self.assertEqual(len(untagged), 1) self.assertEqual(len("".join(untagged)), 0) @requests_mock.Mocker() def test_prune_failure(self, mock): """Unit test to report error carried in response body.""" mock.post( tests.LIBPOD_URL + "/images/prune", json=[ { "Err": "Test prune failure in response body.", } ], ) with self.assertRaises(APIError) as e: self.client.images.prune() self.assertEqual(e.exception.explanation, "Test prune failure in response body.") @requests_mock.Mocker() def test_prune_empty(self, mock): """Unit test if prune API responses null (None).""" mock.post(tests.LIBPOD_URL + "/images/prune", text="null") report = self.client.images.prune() self.assertEqual(report["ImagesDeleted"], []) self.assertEqual(report["SpaceReclaimed"], 0) @requests_mock.Mocker() def test_get(self, mock): mock.get( tests.LIBPOD_URL + "/images/fedora%3Alatest/json", json=FIRST_IMAGE, ) image = self.client.images.get("fedora:latest") self.assertIsInstance(image, Image) self.assertDictEqual(FIRST_IMAGE["Labels"], image.attrs["Labels"]) @requests_mock.Mocker() def test_get_oserror(self, mock): mock.get( tests.LIBPOD_URL + "/images/bad_image/json", exc=OSError, ) with self.assertRaises(APIError) as e: _ = self.client.images.get("bad_image") self.assertEqual( str(e.exception), tests.LIBPOD_URL + "/images/bad_image/json (GET operation failed)", ) @requests_mock.Mocker() def test_get_404(self, mock): mock.get( tests.LIBPOD_URL + "/images/bad_image/json", status_code=404, json={ "cause": "Image not found", "message": "Image not found", "response": 404, }, ) with self.assertRaises(ImageNotFound): _ = self.client.images.get("bad_image") @requests_mock.Mocker() def test_get_500(self, mock): mock.get( tests.LIBPOD_URL + "/images/bad_image/json", status_code=500, json={ "cause": "Server error", "message": "Server error", "response": 500, }, ) with self.assertRaises(APIError): _ = self.client.images.get("bad_image") @requests_mock.Mocker() def test_remove(self, mock): mock.delete( tests.LIBPOD_URL + "/images/fedora:latest", json={ "Untagged": ["326dd9d7add24646a325e8eaa82125294027db2332e49c5828d96312c5d773ab"], "Deleted": [ "326dd9d7add24646a325e8eaa82125294027db2332e49c5828d96312c5d773ab", "c4b16966ecd94ffa910eab4e630e24f259bf34a87e924cd4b1434f267b0e354e", ], "Errors": [], "ExitCode": 0, }, ) report = self.client.images.remove("fedora:latest") self.assertEqual(len(report), 4) deleted = [r["Deleted"] for r in report if "Deleted" in r] self.assertEqual(len(deleted), 2) untagged = [r["Untagged"] for r in report if "Untagged" in r] self.assertEqual(len(untagged), 1) errors = [r["Errors"] for r in report if "Errors" in r] self.assertEqual(len(errors), 0) codes = [r["ExitCode"] for r in report if "ExitCode" in r] self.assertEqual(len(codes), 1) self.assertEqual(codes[0], 0) @requests_mock.Mocker() def test_load(self, mock): with self.assertRaises(PodmanError): self.client.images.load() with self.assertRaises(PodmanError): self.client.images.load(b'data', b'file_path') with self.assertRaises(PodmanError): self.client.images.load(data=b'data', file_path=b'file_path') # Patch Path.read_bytes to mock the file reading behavior with patch("pathlib.Path.read_bytes", return_value=b"mock tarball data"): mock.post( tests.LIBPOD_URL + "/images/load", json={"Names": ["quay.io/fedora:latest"]}, ) mock.get( tests.LIBPOD_URL + "/images/quay.io%2ffedora%3Alatest/json", json=FIRST_IMAGE, ) # 3a. Test the case where only 'file_path' is provided gntr = self.client.images.load(file_path="mock_file.tar") self.assertIsInstance(gntr, types.GeneratorType) report = list(gntr) self.assertEqual(len(report), 1) self.assertEqual( report[0].id, "sha256:326dd9d7add24646a325e8eaa82125294027db2332e49c5828d96312c5d773ab", ) mock.post( tests.LIBPOD_URL + "/images/load", json={"Names": ["quay.io/fedora:latest"]}, ) mock.get( tests.LIBPOD_URL + "/images/quay.io%2ffedora%3Alatest/json", json=FIRST_IMAGE, ) gntr = self.client.images.load(b'This is a weird tarball...') self.assertIsInstance(gntr, types.GeneratorType) report = list(gntr) self.assertEqual(len(report), 1) self.assertEqual( report[0].id, "sha256:326dd9d7add24646a325e8eaa82125294027db2332e49c5828d96312c5d773ab" ) @requests_mock.Mocker() def test_search(self, mock): mock.get( tests.LIBPOD_URL + "/images/search?term=fedora&noTrunc=true", json=[ { "description": "mock term=fedora search", "is_official": False, "is_automated": False, "name": "quay.io/libpod/fedora", "star_count": 0, }, ], ) report = self.client.images.search("fedora") self.assertEqual(len(report), 1) self.assertEqual(report[0]["name"], "quay.io/libpod/fedora") @requests_mock.Mocker() def test_search_oserror(self, mock): mock.get( tests.LIBPOD_URL + "/images/search?term=fedora&noTrunc=true", exc=OSError, ) with self.assertRaises(OSError): self.client.images.search("fedora") @requests_mock.Mocker() def test_search_500(self, mock): mock.get( tests.LIBPOD_URL + "/images/search?term=fedora&noTrunc=true", status_code=500, json={ "cause": "Server error", "message": "Server error", "response": 500, }, ) with self.assertRaises(OSError): self.client.images.search("fedora") @requests_mock.Mocker() def test_search_limit(self, mock): mock.get( tests.LIBPOD_URL + "/images/search?term=fedora&noTrunc=true&limit=5", json=[ { "description": "mock term=fedora search", "is_official": False, "is_automated": False, "name": "quay.io/libpod/fedora", "star_count": 0, }, ], ) report = self.client.images.search("fedora", limit=5) self.assertEqual(len(report), 1) self.assertEqual(report[0]["name"], "quay.io/libpod/fedora") @requests_mock.Mocker() def test_search_filters(self, mock): mock.get( tests.LIBPOD_URL + "/images/search?filters=%7B%22stars%22%3A+%5B%225%22%5D%7D&noTrunc=True&term=fedora", json=[ { "description": "mock term=fedora search", "is_official": False, "is_automated": False, "name": "quay.io/libpod/fedora", "star_count": 0, }, ], ) report = self.client.images.search("fedora", filters={"stars": 5}) self.assertEqual(len(report), 1) self.assertEqual(report[0]["name"], "quay.io/libpod/fedora") @requests_mock.Mocker() def test_search_listTags(self, mock): mock.get( tests.LIBPOD_URL + "/images/search?term=fedora&noTrunc=true&listTags=true", json=[ { "description": "mock term=fedora search", "is_official": False, "is_automated": False, "name": "quay.io/libpod/fedora", "star_count": 0, "tag": "1.0.0", }, ], ) report = self.client.images.search("fedora", listTags=True) self.assertEqual(len(report), 1) self.assertEqual(report[0]["name"], "quay.io/libpod/fedora") self.assertEqual(report[0]["tag"], "1.0.0") @requests_mock.Mocker() def test_push(self, mock): mock.post(tests.LIBPOD_URL + "/images/quay.io%2Ffedora%3Alatest/push") report = self.client.images.push("quay.io/fedora", "latest") expected = r"""{"status": "Pushing repository quay.io/fedora (1 tags)"} {"status": "Pushing", "progressDetail": {}, "id": "quay.io/fedora"} """ self.assertEqual(report, expected) @requests_mock.Mocker() def test_pull(self, mock): image_id = "sha256:326dd9d7add24646a325e8eaa82125294027db2332e49c5828d96312c5d773ab" mock.post( tests.LIBPOD_URL + "/images/pull?reference=quay.io%2ffedora%3Alatest", json={ "error": "", "id": image_id, "images": [image_id], "stream": "", }, ) mock.get( tests.LIBPOD_URL + "/images" "/sha256%3A326dd9d7add24646a325e8eaa82125294027db2332e49c5828d96312c5d773ab/json", json=FIRST_IMAGE, ) image = self.client.images.pull("quay.io/fedora", "latest") self.assertEqual(image.id, image_id) @requests_mock.Mocker() def test_pull_enhanced(self, mock): image_id = "sha256:326dd9d7add24646a325e8eaa82125294027db2332e49c5828d96312c5d773ab" mock.post( tests.LIBPOD_URL + "/images/pull?reference=quay.io%2ffedora%3Alatest", json={ "error": "", "id": image_id, "images": [image_id], "stream": "", }, ) mock.get( tests.LIBPOD_URL + "/images" "/sha256%3A326dd9d7add24646a325e8eaa82125294027db2332e49c5828d96312c5d773ab/json", json=FIRST_IMAGE, ) image = self.client.images.pull("quay.io/fedora:latest") self.assertEqual(image.id, image_id) @requests_mock.Mocker() def test_pull_platform(self, mock): image_id = "sha256:326dd9d7add24646a325e8eaa82125294027db2332e49c5828d96312c5d773ab" adapter = mock.post( tests.LIBPOD_URL + "/images/pull?reference=quay.io%2ffedora%3Alatest&OS=linux", json={ "error": "", "id": image_id, "images": [image_id], "stream": "", }, ) mock.get( tests.LIBPOD_URL + "/images" "/sha256%3A326dd9d7add24646a325e8eaa82125294027db2332e49c5828d96312c5d773ab/json", json=FIRST_IMAGE, ) image = self.client.images.pull("quay.io/fedora:latest", platform="linux") self.assertEqual(image.id, image_id) self.assertTrue(adapter.called_once) @requests_mock.Mocker() def test_pull_2x(self, mock): image_id = "sha256:326dd9d7add24646a325e8eaa82125294027db2332e49c5828d96312c5d773ab" mock.post( tests.LIBPOD_URL + "/images/pull?reference=quay.io%2ffedora&allTags=True", json={ "error": "", "id": image_id, "images": [ image_id, "c4b16966ecd94ffa910eab4e630e24f259bf34a87e924cd4b1434f267b0e354e", ], "stream": "", }, ) mock.get( tests.LIBPOD_URL + "/images" "/sha256%3A326dd9d7add24646a325e8eaa82125294027db2332e49c5828d96312c5d773ab/json", json=FIRST_IMAGE, ) mock.get( tests.LIBPOD_URL + "/images/c4b16966ecd94ffa910eab4e630e24f259bf34a87e924cd4b1434f267b0e354e/json", json=SECOND_IMAGE, ) images = self.client.images.pull("quay.io/fedora", "latest", all_tags=True) self.assertIsInstance(images, Iterable) self.assertIsInstance(images[0], Image) self.assertIsInstance(images[1], Image) self.assertEqual(images[0].id, image_id) self.assertEqual( images[1].id, "c4b16966ecd94ffa910eab4e630e24f259bf34a87e924cd4b1434f267b0e354e" ) @requests_mock.Mocker() def test_list_with_name_parameter(self, mock): """Test that name parameter is correctly converted to a reference filter""" mock.get( tests.LIBPOD_URL + "/images/json?filters=%7B%22reference%22%3A+%5B%22fedora%22%5D%7D", json=[FIRST_IMAGE], ) images = self.client.images.list(name="fedora") self.assertEqual(len(images), 1) self.assertIsInstance(images[0], Image) self.assertEqual(images[0].tags, ["fedora:latest", "fedora:33"]) @requests_mock.Mocker() def test_list_with_name_and_existing_filters(self, mock): """Test that name parameter works alongside other filters""" mock.get( tests.LIBPOD_URL + ( "/images/json?filters=%7B%22dangling%22%3A+%5B%22True%22%5D%2C+" "%22reference%22%3A+%5B%22fedora%22%5D%7D" ), json=[FIRST_IMAGE], ) images = self.client.images.list(name="fedora", filters={"dangling": True}) self.assertEqual(len(images), 1) self.assertIsInstance(images[0], Image) @requests_mock.Mocker() def test_list_with_name_overrides_reference_filter(self, mock): """Test that name parameter takes precedence over existing reference filter""" mock.get( tests.LIBPOD_URL + "/images/json?filters=%7B%22reference%22%3A+%5B%22fedora%22%5D%7D", json=[FIRST_IMAGE], ) # The name parameter should override the reference filter images = self.client.images.list( name="fedora", filters={"reference": "ubuntu"}, # This should be overridden ) self.assertEqual(len(images), 1) self.assertIsInstance(images[0], Image) @requests_mock.Mocker() def test_list_with_all_and_name(self, mock): """Test that all parameter works alongside name filter""" mock.get( tests.LIBPOD_URL + "/images/json?all=true&filters=%7B%22reference%22%3A+%5B%22fedora%22%5D%7D", json=[FIRST_IMAGE], ) images = self.client.images.list(all=True, name="fedora") self.assertEqual(len(images), 1) self.assertIsInstance(images[0], Image) @requests_mock.Mocker() def test_list_with_empty_name(self, mock): """Test that empty name parameter doesn't add a reference filter""" mock.get(tests.LIBPOD_URL + "/images/json", json=[FIRST_IMAGE]) images = self.client.images.list(name="") self.assertEqual(len(images), 1) self.assertIsInstance(images[0], Image) @requests_mock.Mocker() def test_list_with_none_name(self, mock): """Test that None name parameter doesn't add a reference filter""" mock.get(tests.LIBPOD_URL + "/images/json", json=[FIRST_IMAGE]) images = self.client.images.list(name=None) self.assertEqual(len(images), 1) self.assertIsInstance(images[0], Image) if __name__ == '__main__': unittest.main() podman-py-5.4.0.1/podman/tests/unit/test_manager.py000066400000000000000000000005351475540541200222170ustar00rootroot00000000000000import unittest from podman.domain.pods_manager import PodsManager class ManagerTestCase(unittest.TestCase): def test_prepare_model(self): with self.assertRaisesRegex(Exception, "^Can't create Pod from .*$"): PodsManager().prepare_model(attrs=("Sets", "Not", "supported")) if __name__ == '__main__': unittest.main() podman-py-5.4.0.1/podman/tests/unit/test_manifests.py000066400000000000000000000013171475540541200225750ustar00rootroot00000000000000import unittest from podman import PodmanClient, tests from podman.domain.manifests import Manifest, ManifestsManager class ManifestTestCase(unittest.TestCase): def setUp(self) -> None: super().setUp() self.client = PodmanClient(base_url=tests.BASE_SOCK) self.addCleanup(self.client.close) def test_podmanclient(self): manager = self.client.manifests self.assertIsInstance(manager, ManifestsManager) def test_list(self): with self.assertRaises(NotImplementedError): self.client.manifests.list() def test_name(self): manifest = Manifest() self.assertIsNone(manifest.name) if __name__ == '__main__': unittest.main() podman-py-5.4.0.1/podman/tests/unit/test_network.py000066400000000000000000000076731475540541200223100ustar00rootroot00000000000000import unittest import requests_mock from podman import PodmanClient, tests from podman.domain.networks import Network from podman.domain.networks_manager import NetworksManager FIRST_NETWORK = { "Name": "podman", "Id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9", "Created": "2021-03-01T09:18:37.491308364-07:00", "Scope": "local", "Driver": "bridge", "EnableIPv6": False, "IPAM": { "Driver": "default", "Options": {}, "Config": [{"Subnet": "10.88.0.0/16", "Gateway": "10.88.0.1"}], }, "Internal": False, "Attachable": False, "Ingress": False, "ConfigFrom": {"Network": ""}, "ConfigOnly": False, "Containers": {}, "Options": {}, "Labels": {}, } FIRST_NETWORK_LIBPOD = { "name": "podman", "id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9", "driver": "bridge", "network_interface": "libpod_veth0", "created": "2022-01-28T09:18:37.491308364-07:00", "subnets": [ { "subnet": "10.11.12.0/24", "gateway": "10.11.12.1", "lease_range": { "start_ip": "10.11.12.1", "end_ip": "10.11.12.63", }, } ], "ipv6_enabled": False, "internal": False, "dns_enabled": False, "labels": {}, "options": {}, "ipam_options": {}, } class NetworkTestCase(unittest.TestCase): def setUp(self) -> None: super().setUp() self.client = PodmanClient(base_url=tests.BASE_SOCK) self.addCleanup(self.client.close) def test_id(self): expected = {"Id": "1cf06390-709d-4ffa-a054-c3083abe367c"} actual = Network(attrs=expected) self.assertEqual(actual.id, expected["Id"]) actual = Network(attrs={"name": "database"}) self.assertEqual( actual.id, "3549b0028b75d981cdda2e573e9cb49dedc200185876df299f912b79f69dabd8" ) def test_name(self): actual = Network(attrs={"Name": "database"}) self.assertEqual(actual.name, "database") actual = Network({"name": "database"}) self.assertEqual(actual.name, "database") @requests_mock.Mocker() def test_remove(self, mock): adapter = mock.delete( tests.LIBPOD_URL + "/networks/podman?force=True", status_code=204, json={"Name": "podman", "Err": None}, ) net_manager = NetworksManager(client=self.client.api) net = net_manager.prepare_model(attrs=FIRST_NETWORK) net.remove(force=True) self.assertTrue(adapter.called_once) @requests_mock.Mocker() def test_connect(self, mock): adapter = mock.post(tests.LIBPOD_URL + "/networks/podman/connect") net = Network(attrs=FIRST_NETWORK, client=self.client.api) net.connect( "podman_ctnr", aliases=["production"], ipv4_address="172.16.0.1", ) self.assertEqual(adapter.call_count, 1) self.assertDictEqual( adapter.last_request.json(), { 'Container': 'podman_ctnr', "EndpointConfig": { 'Aliases': ['production'], 'IPAMConfig': {'IPv4Address': '172.16.0.1'}, 'IPAddress': '172.16.0.1', 'NetworkID': '2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9', }, }, ) @requests_mock.Mocker() def test_disconnect(self, mock): adapter = mock.post(tests.LIBPOD_URL + "/networks/podman/disconnect") net = Network(attrs=FIRST_NETWORK, client=self.client.api) net.disconnect("podman_ctnr", force=True) self.assertEqual(adapter.call_count, 1) self.assertDictEqual( adapter.last_request.json(), { 'Container': 'podman_ctnr', "Force": True, }, ) if __name__ == '__main__': unittest.main() podman-py-5.4.0.1/podman/tests/unit/test_networksmanager.py000066400000000000000000000131651475540541200240170ustar00rootroot00000000000000import unittest import requests_mock from podman import PodmanClient, tests from podman.domain.networks import Network from podman.domain.networks_manager import NetworksManager FIRST_NETWORK = { "Name": "podman", "Id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9", "Created": "2021-03-01T09:18:37.491308364-07:00", "Scope": "local", "Driver": "bridge", "EnableIPv6": False, "IPAM": { "Driver": "default", "Options": {}, "Config": [{"Subnet": "10.88.0.0/16", "Gateway": "10.88.0.1"}], }, "Internal": False, "Attachable": False, "Ingress": False, "ConfigFrom": {"Network": ""}, "ConfigOnly": False, "Containers": {}, "Options": {}, "Labels": {}, } SECOND_NETWORK = { "Name": "database", "Id": "3549b0028b75d981cdda2e573e9cb49dedc200185876df299f912b79f69dabd8", "Created": "2021-03-01T09:18:37.491308364-07:00", "Scope": "local", "Driver": "bridge", "EnableIPv6": False, "IPAM": { "Driver": "default", "Options": {}, "Config": [{"Subnet": "10.88.0.0/16", "Gateway": "10.88.0.1"}], }, "Internal": False, "Attachable": False, "Ingress": False, "ConfigFrom": {"Network": ""}, "ConfigOnly": False, "Containers": {}, "Options": {}, "Labels": {}, } FIRST_NETWORK_LIBPOD = { "name": "podman", "id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9", "driver": "bridge", "network_interface": "libpod_veth0", "created": "2022-01-28T09:18:37.491308364-07:00", "subnets": [ { "subnet": "10.11.12.0/24", "gateway": "10.11.12.1", "lease_range": { "start_ip": "10.11.12.1", "end_ip": "10.11.12.63", }, } ], "ipv6_enabled": False, "internal": False, "dns_enabled": False, "labels": {}, "options": {}, "ipam_options": {}, } SECOND_NETWORK_LIBPOD = { "name": "database", "id": "3549b0028b75d981cdda2e573e9cb49dedc200185876df299f912b79f69dabd8", "created": "2021-03-01T09:18:37.491308364-07:00", "driver": "bridge", "network_interface": "libpod_veth1", "subnets": [ { "subnet": "10.11.12.0/24", "gateway": "10.11.12.1", "lease_range": { "start_ip": "10.11.12.1", "end_ip": "10.11.12.63", }, } ], "ipv6_enabled": False, "internal": False, "dns_enabled": False, "labels": {}, "options": {}, "ipam_options": {}, } class NetworksManagerTestCase(unittest.TestCase): """Test NetworksManager area of concern. Note: Mock responses need to be coded for libpod returns. The python bindings are responsible for mapping to compatible output. """ def setUp(self) -> None: super().setUp() self.client = PodmanClient(base_url=tests.BASE_SOCK) self.addCleanup(self.client.close) def test_podmanclient(self): manager = self.client.networks self.assertIsInstance(manager, NetworksManager) @requests_mock.Mocker() def test_get(self, mock): mock.get(tests.LIBPOD_URL + "/networks/podman", json=FIRST_NETWORK) actual = self.client.networks.get("podman") self.assertIsInstance(actual, Network) self.assertEqual( actual.id, "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9" ) @requests_mock.Mocker() def test_list_libpod(self, mock): mock.get( tests.LIBPOD_URL + "/networks/json", json=[FIRST_NETWORK_LIBPOD, SECOND_NETWORK_LIBPOD], ) actual = self.client.networks.list() self.assertEqual(len(actual), 2) self.assertIsInstance(actual[0], Network) self.assertEqual( actual[0].id, "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9" ) self.assertEqual(actual[0].attrs["name"], "podman") self.assertIsInstance(actual[1], Network) self.assertEqual( actual[1].id, "3549b0028b75d981cdda2e573e9cb49dedc200185876df299f912b79f69dabd8" ) self.assertEqual(actual[1].name, "database") @requests_mock.Mocker() def test_create_libpod(self, mock): adapter = mock.post(tests.LIBPOD_URL + "/networks/create", json=FIRST_NETWORK_LIBPOD) network = self.client.networks.create("podman", dns_enabled=True, enable_ipv6=True) self.assertIsInstance(network, Network) self.assertEqual(adapter.call_count, 1) self.assertDictEqual( adapter.last_request.json(), { "name": "podman", "ipv6_enabled": True, "dns_enabled": True, }, ) @requests_mock.Mocker() def test_create_defaults(self, mock): adapter = mock.post(tests.LIBPOD_URL + "/networks/create", json=FIRST_NETWORK_LIBPOD) network = self.client.networks.create("podman") self.assertIsInstance(network, Network) self.assertEqual(adapter.call_count, 1) self.assertDictEqual( adapter.last_request.json(), {"name": "podman"}, ) @requests_mock.Mocker() def test_prune_libpod(self, mock): mock.post( tests.LIBPOD_URL + "/networks/prune", json=[ {"Name": "podman", "Error": None}, {"Name": "database", "Error": None}, ], ) actual = self.client.networks.prune() self.assertListEqual(actual["NetworksDeleted"], ["podman", "database"]) if __name__ == '__main__': unittest.main() podman-py-5.4.0.1/podman/tests/unit/test_parse_utils.py000066400000000000000000000074251475540541200231440ustar00rootroot00000000000000import datetime import ipaddress import json import unittest from dataclasses import dataclass from typing import Any, Optional from collections.abc import Iterable from unittest import mock from requests import Response from podman import api class ParseUtilsTestCase(unittest.TestCase): def test_parse_repository(self): @dataclass class TestCase: name: str input: Any expected: tuple[str, Optional[str]] cases = [ TestCase(name="empty str", input="", expected=("", None)), TestCase( name="name", input="quay.io/libpod/testimage", expected=("quay.io/libpod/testimage", None), ), TestCase( name="@digest", input="quay.io/libpod/testimage@71f1b47263fc", expected=("quay.io/libpod/testimage", "71f1b47263fc"), ), TestCase( name=":tag", input="quay.io/libpod/testimage:latest", expected=("quay.io/libpod/testimage", "latest"), ), TestCase( name=":port", input="quay.io:5000/libpod/testimage", expected=("quay.io:5000/libpod/testimage", None), ), TestCase( name=":port@digest", input="quay.io:5000/libpod/testimage@71f1b47263fc", expected=("quay.io:5000/libpod/testimage", "71f1b47263fc"), ), TestCase( name=":port:tag", input="quay.io:5000/libpod/testimage:latest", expected=("quay.io:5000/libpod/testimage", "latest"), ), ] for case in cases: actual = api.parse_repository(case.input) self.assertEqual( case.expected, actual, f"failed test {case.name} expected {case.expected}, actual {actual}", ) def test_decode_header(self): actual = api.decode_header("eyJIZWFkZXIiOiJ1bml0dGVzdCJ9") self.assertDictEqual(actual, {"Header": "unittest"}) self.assertDictEqual(api.decode_header(None), {}) def test_prepare_timestamp(self): time = datetime.datetime(2022, 1, 24, 12, 0, 0) self.assertEqual(api.prepare_timestamp(time), 1643025600) self.assertEqual(api.prepare_timestamp(2), 2) self.assertEqual(api.prepare_timestamp(None), None) with self.assertRaises(ValueError): api.prepare_timestamp("bad input") # type: ignore def test_prepare_cidr(self): net = ipaddress.IPv4Network("127.0.0.0/24") self.assertEqual(api.prepare_cidr(net), ("127.0.0.0", "////AA==")) def test_stream_helper(self): streamed_results = [b'{"test":"val1"}', b'{"test":"val2"}'] mock_response = mock.Mock(spec=Response) mock_response.iter_lines.return_value = iter(streamed_results) streamable = api.stream_helper(mock_response) self.assertIsInstance(streamable, Iterable) for expected, actual in zip(streamed_results, streamable): self.assertIsInstance(actual, bytes) self.assertEqual(expected, actual) def test_stream_helper_with_decode(self): streamed_results = [b'{"test":"val1"}', b'{"test":"val2"}'] mock_response = mock.Mock(spec=Response) mock_response.iter_lines.return_value = iter(streamed_results) streamable = api.stream_helper(mock_response, decode_to_json=True) self.assertIsInstance(streamable, Iterable) for expected, actual in zip(streamed_results, streamable): self.assertIsInstance(actual, dict) self.assertDictEqual(json.loads(expected), actual) if __name__ == '__main__': unittest.main() podman-py-5.4.0.1/podman/tests/unit/test_path_utils.py000066400000000000000000000026041475540541200227600ustar00rootroot00000000000000import os import unittest import tempfile from unittest import mock from podman import api class PathUtilsTestCase(unittest.TestCase): def setUp(self): self.xdg_runtime_dir = os.getenv('XDG_RUNTIME_DIR') @mock.patch.dict(os.environ, clear=True) def test_get_runtime_dir_env_var_set(self): with tempfile.TemporaryDirectory() as tmpdir: os.environ['XDG_RUNTIME_DIR'] = str(tmpdir) self.assertEqual(str(tmpdir), api.path_utils.get_runtime_dir()) @mock.patch.dict(os.environ, clear=True) def test_get_runtime_dir_env_var_not_set(self): if not self.xdg_runtime_dir: self.skipTest('XDG_RUNTIME_DIR must be set for this test.') if self.xdg_runtime_dir.startswith('/run/user/'): self.skipTest("XDG_RUNTIME_DIR in /run/user/, can't check") self.assertNotEqual(self.xdg_runtime_dir, api.path_utils.get_runtime_dir()) @mock.patch('os.path.isdir', lambda d: False) @mock.patch.dict(os.environ, clear=True) def test_get_runtime_dir_env_var_not_set_and_no_run(self): """Fake that XDG_RUNTIME_DIR is not set and /run/user/ does not exist.""" if not self.xdg_runtime_dir: self.skipTest('XDG_RUNTIME_DIR must be set to fetch a working dir.') self.assertNotEqual(self.xdg_runtime_dir, api.path_utils.get_runtime_dir()) if __name__ == '__main__': unittest.main() podman-py-5.4.0.1/podman/tests/unit/test_pod.py000066400000000000000000000153671475540541200214000ustar00rootroot00000000000000import unittest import requests_mock from podman import PodmanClient, tests from podman.domain.pods import Pod from podman.domain.pods_manager import PodsManager from podman.errors import NotFound FIRST_POD = { "ID": "c8b9f5b17dc1406194010c752fc6dcb330192032e27648db9b14060447ecf3b8", "Name": "redis-ngnix", } class PodTestCase(unittest.TestCase): def setUp(self) -> None: super().setUp() self.client = PodmanClient(base_url=tests.BASE_SOCK) def tearDown(self) -> None: super().tearDown() self.client.close() def test_id(self): expected = {"Id": "c8b9f5b17dc1406194010c752fc6dcb330192032e27648db9b14060447ecf3b8"} actual = Pod(attrs=expected) self.assertEqual(actual.id, expected["Id"]) expected = {"Name": "redis-ngnix"} actual = Pod(attrs=expected) self.assertEqual(actual.name, expected["Name"]) @requests_mock.Mocker() def test_kill(self, mock): adapter = mock.post( tests.LIBPOD_URL + "/pods/c8b9f5b17dc1406194010c752fc6dcb330192032e27648db9b14060447ecf3b8/kill", json={ "Errs": [], "Id": "c8b9f5b17dc1406194010c752fc6dcb330192032e27648db9b14060447ecf3b8", }, ) pod = Pod(attrs=FIRST_POD, client=self.client.api) pod.kill() self.assertTrue(adapter.called_once) @requests_mock.Mocker() def test_kill_404(self, mock): adapter = mock.post( tests.LIBPOD_URL + "/pods/c8b9f5b17dc1406194010c752fc6dcb330192032e27648db9b14060447ecf3b8/kill", status_code=404, json={ "cause": "no such pod", "message": "no pod with name or ID xyz found: no such pod", "response": 404, }, ) pod = Pod(attrs=FIRST_POD, client=self.client.api) with self.assertRaises(NotFound): pod.kill() self.assertTrue(adapter.called_once) @requests_mock.Mocker() def test_pause(self, mock): adapter = mock.post( tests.LIBPOD_URL + "/pods/c8b9f5b17dc1406194010c752fc6dcb330192032e27648db9b14060447ecf3b8/pause", json={ "Errs": [], "Id": "c8b9f5b17dc1406194010c752fc6dcb330192032e27648db9b14060447ecf3b8", }, ) pod = Pod(attrs=FIRST_POD, client=self.client.api) pod.pause() self.assertTrue(adapter.called_once) @requests_mock.Mocker() def test_pause_404(self, mock): adapter = mock.post( tests.LIBPOD_URL + "/pods/c8b9f5b17dc1406194010c752fc6dcb330192032e27648db9b14060447ecf3b8/pause", status_code=404, json={ "cause": "no such pod", "message": "no pod with name or ID xyz found: no such pod", "response": 404, }, ) pod = Pod(attrs=FIRST_POD, client=self.client.api) with self.assertRaises(NotFound): pod.pause() self.assertTrue(adapter.called_once) @requests_mock.Mocker() def test_remove(self, mock): adapter = mock.delete( tests.LIBPOD_URL + "/pods/c8b9f5b17dc1406194010c752fc6dcb330192032e27648db9b14060447ecf3b8?force=True", json={ "Errs": [], "Id": "c8b9f5b17dc1406194010c752fc6dcb330192032e27648db9b14060447ecf3b8", }, ) pod_manager = PodsManager(client=self.client.api) pod = pod_manager.prepare_model(attrs=FIRST_POD) pod.remove(force=True) self.assertTrue(adapter.called_once) @requests_mock.Mocker() def test_restart(self, mock): adapter = mock.post( tests.LIBPOD_URL + "/pods/c8b9f5b17dc1406194010c752fc6dcb330192032e27648db9b14060447ecf3b8/restart", json={ "Errs": [], "Id": "c8b9f5b17dc1406194010c752fc6dcb330192032e27648db9b14060447ecf3b8", }, ) pod = Pod(attrs=FIRST_POD, client=self.client.api) pod.restart() self.assertTrue(adapter.called_once) @requests_mock.Mocker() def test_start(self, mock): adapter = mock.post( tests.LIBPOD_URL + "/pods/c8b9f5b17dc1406194010c752fc6dcb330192032e27648db9b14060447ecf3b8/start", json={ "Errs": [], "Id": "c8b9f5b17dc1406194010c752fc6dcb330192032e27648db9b14060447ecf3b8", }, ) pod = Pod(attrs=FIRST_POD, client=self.client.api) pod.start() self.assertTrue(adapter.called_once) @requests_mock.Mocker() def test_stop(self, mock): adapter = mock.post( tests.LIBPOD_URL + "/pods/c8b9f5b17dc1406194010c752fc6dcb330192032e27648db9b14060447ecf3b8/stop?t=70.0", json={ "Errs": [], "Id": "c8b9f5b17dc1406194010c752fc6dcb330192032e27648db9b14060447ecf3b8", }, ) pod = Pod(attrs=FIRST_POD, client=self.client.api) pod.stop(timeout=70.0) self.assertTrue(adapter.called_once) @requests_mock.Mocker() def test_top(self, mock): body = { "Processes": [ [ 'jhonce', '2417', '2274', '0', 'Mar01', '?', '00:00:01', '/usr/bin/ssh-agent /bin/sh -c exec -l /bin/bash -c "/usr/bin/gnome-session"', ], ['jhonce', '5544', '3522', '0', 'Mar01', 'pts/1', '00:00:02', '-bash'], ['jhonce', '6140', '3522', '0', 'Mar01', 'pts/2', '00:00:00', '-bash'], ], "Titles": ["UID", "PID", "PPID", "C", "STIME", "TTY", "TIME CMD"], } adapter = mock.get( tests.LIBPOD_URL + "/pods" "/c8b9f5b17dc1406194010c752fc6dcb330192032e27648db9b14060447ecf3b8/top" "?ps_args=aux&stream=False", json=body, ) pod = Pod(attrs=FIRST_POD, client=self.client.api) actual = pod.top(ps_args="aux") self.assertDictEqual(actual, body) self.assertTrue(adapter.called_once) @requests_mock.Mocker() def test_unpause(self, mock): adapter = mock.post( tests.LIBPOD_URL + "/pods/c8b9f5b17dc1406194010c752fc6dcb330192032e27648db9b14060447ecf3b8/unpause", json={ "Errs": [], "Id": "c8b9f5b17dc1406194010c752fc6dcb330192032e27648db9b14060447ecf3b8", }, ) pod = Pod(attrs=FIRST_POD, client=self.client.api) pod.unpause() self.assertTrue(adapter.called_once) if __name__ == '__main__': unittest.main() podman-py-5.4.0.1/podman/tests/unit/test_podmanclient.py000066400000000000000000000073661475540541200232730ustar00rootroot00000000000000import unittest import urllib.parse from pathlib import Path from unittest import mock from unittest.mock import MagicMock import requests_mock from podman import PodmanClient, tests from podman.api.path_utils import get_runtime_dir, get_xdg_config_home class PodmanClientTestCase(unittest.TestCase): """Test the PodmanClient() object.""" opener = mock.mock_open( read_data=""" [containers] log_size_max = -1 pids_limit = 2048 userns_size = 65536 [engine] num_locks = 2048 active_service = "testing" stop_timeout = 10 [engine.service_destinations] [engine.service_destinations.production] uri = "ssh://root@localhost:22/run/podman/podman.sock" identity = "/home/root/.ssh/id_rsa" [engine.service_destinations.testing] uri = "ssh://qe@localhost:2222/run/podman/podman.sock" identity = "/home/qe/.ssh/id_rsa" [network] """ ) def setUp(self) -> None: super().setUp() self.client = PodmanClient(base_url=tests.BASE_SOCK) def mocked_open(self, *args, **kwargs): return PodmanClientTestCase.opener(self, *args, **kwargs) self.mocked_open = mocked_open @mock.patch('requests.Session.close') def test_close(self, mock_close): self.client.close() mock_close.assert_called_once_with() @requests_mock.Mocker() def test_contextmanager(self, mock): body = { "host": { "arch": "amd65", "os": "linux", } } adapter = mock.get(tests.LIBPOD_URL + "/info", json=body) # noqa: F841 with PodmanClient(base_url=tests.BASE_SOCK) as client: actual = client.info() self.assertDictEqual(actual, body) self.assertIn("User-Agent", mock.last_request.headers) self.assertIn( "PodmanPy/", mock.last_request.headers["User-Agent"], mock.last_request.headers ) def test_swarm(self): with PodmanClient(base_url=tests.BASE_SOCK) as client: with self.assertRaises(NotImplementedError): # concrete property _ = client.swarm with self.assertRaises(NotImplementedError): # aliased property _ = client.nodes def test_connect(self): with mock.patch.multiple(Path, open=self.mocked_open, exists=MagicMock(return_value=True)): with PodmanClient(connection="testing") as client: self.assertEqual( client.api.base_url.geturl(), "http+ssh://qe@localhost:2222/run/podman/podman.sock", ) # Build path to support tests running as root or a user expected = Path(get_xdg_config_home()) / "containers" / "containers.conf" PodmanClientTestCase.opener.assert_called_with(expected, encoding="utf-8") def test_connect_404(self): with mock.patch.multiple(Path, open=self.mocked_open, exists=MagicMock(return_value=True)): with self.assertRaises(KeyError): _ = PodmanClient(connection="not defined") def test_connect_default(self): with mock.patch.multiple(Path, open=self.mocked_open, exists=MagicMock(return_value=True)): with PodmanClient() as client: expected = "http+unix://" + urllib.parse.quote_plus( str(Path(get_runtime_dir()) / "podman" / "podman.sock") ) self.assertEqual(client.api.base_url.geturl(), expected) # Build path to support tests running as root or a user expected = Path(get_xdg_config_home()) / "containers" / "containers.conf" PodmanClientTestCase.opener.assert_called_with(expected, encoding="utf-8") if __name__ == '__main__': unittest.main() podman-py-5.4.0.1/podman/tests/unit/test_podsmanager.py000066400000000000000000000200761475540541200231070ustar00rootroot00000000000000import io import json import unittest from collections.abc import Iterable import requests_mock from podman import PodmanClient, tests from podman.domain.pods import Pod from podman.domain.pods_manager import PodsManager from podman.errors import NotFound FIRST_POD = { "ID": "c8b9f5b17dc1406194010c752fc6dcb330192032e27648db9b14060447ecf3b8", "Name": "blog-nginx", } SECOND_POD = { "ID": "c847d00ed0474835a2e246f00e90346fe98d388f98064f4494953c5fb921b8bc", "Name": "podman", } class PodsManagerTestCase(unittest.TestCase): def setUp(self) -> None: super().setUp() self.client = PodmanClient(base_url=tests.BASE_SOCK) def tearDown(self) -> None: super().tearDown() self.client.close() def test_podmanclient(self): manager = self.client.pods self.assertIsInstance(manager, PodsManager) @requests_mock.Mocker() def test_create(self, mock): adapter = mock.post( tests.LIBPOD_URL + "/pods/create", json={"Id": "c8b9f5b17dc1406194010c752fc6dcb330192032e27648db9b14060447ecf3b8"}, status_code=201, ) mock.get( tests.LIBPOD_URL + "/pods/c8b9f5b17dc1406194010c752fc6dcb330192032e27648db9b14060447ecf3b8/json", json=FIRST_POD, ) actual = self.client.pods.create(name="database") self.assertIsInstance(actual, Pod) self.assertTrue(adapter.called_once) self.assertDictEqual(adapter.last_request.json(), {"name": "database"}) @requests_mock.Mocker() def test_get(self, mock): mock.get( tests.LIBPOD_URL + "/pods/c8b9f5b17dc1406194010c752fc6dcb330192032e27648db9b14060447ecf3b8/json", json=FIRST_POD, ) actual = self.client.pods.get( "c8b9f5b17dc1406194010c752fc6dcb330192032e27648db9b14060447ecf3b8" ) self.assertEqual( actual.id, "c8b9f5b17dc1406194010c752fc6dcb330192032e27648db9b14060447ecf3b8" ) @requests_mock.Mocker() def test_get404(self, mock): mock.get( tests.LIBPOD_URL + "/pods/c8b9f5b17dc1406194010c752fc6dcb330192032e27648db9b14060447ecf3b8/json", status_code=404, json={ "cause": "no such pod", "message": ( "no pod with name or ID " "c8b9f5b17dc1406194010c752fc6dcb330192032e27648db9b14060447ecf3b8" " found: no such pod" ), "response": 404, }, ) with self.assertRaises(NotFound): self.client.pods.get("c8b9f5b17dc1406194010c752fc6dcb330192032e27648db9b14060447ecf3b8") @requests_mock.Mocker() def test_list(self, mock): mock.get(tests.LIBPOD_URL + "/pods/json", json=[FIRST_POD, SECOND_POD]) actual = self.client.pods.list() self.assertEqual( actual[0].id, "c8b9f5b17dc1406194010c752fc6dcb330192032e27648db9b14060447ecf3b8" ) self.assertEqual( actual[1].id, "c847d00ed0474835a2e246f00e90346fe98d388f98064f4494953c5fb921b8bc" ) @requests_mock.Mocker() def test_prune(self, mock): adapter = mock.post( tests.LIBPOD_URL + "/pods/prune", json=[ { "Err": None, "Id": "c8b9f5b17dc1406194010c752fc6dcb330192032e27648db9b14060447ecf3b8", }, { "Err": None, "Id": "c847d00ed0474835a2e246f00e90346fe98d388f98064f4494953c5fb921b8bc", }, ], ) actual = self.client.pods.prune() self.assertTrue(adapter.called_once) self.assertListEqual( actual["PodsDeleted"], [ "c8b9f5b17dc1406194010c752fc6dcb330192032e27648db9b14060447ecf3b8", "c847d00ed0474835a2e246f00e90346fe98d388f98064f4494953c5fb921b8bc", ], ) self.assertEqual(actual["SpaceReclaimed"], 0) @requests_mock.Mocker() def test_stats(self, mock): body = { "Processes": [ [ 'jhonce', '2417', '2274', '0', 'Mar01', '?', '00:00:01', '/usr/bin/ssh-agent /bin/sh -c exec -l /bin/bash -c "/usr/bin/gnome-session"', ], ['jhonce', '5544', '3522', '0', 'Mar01', 'pts/1', '00:00:02', '-bash'], ['jhonce', '6140', '3522', '0', 'Mar01', 'pts/2', '00:00:00', '-bash'], ], "Titles": ["UID", "PID", "PPID", "C", "STIME", "TTY", "TIME CMD"], } mock.get( tests.LIBPOD_URL + "/pods/stats" "?namesOrIDs=c8b9f5b17dc1406194010c752fc6dcb330192032e27648db9b14060447ecf3b8", json=body, ) actual = self.client.pods.stats( name="c8b9f5b17dc1406194010c752fc6dcb330192032e27648db9b14060447ecf3b8", ) self.assertEqual(actual, json.dumps(body).encode()) @requests_mock.Mocker() def test_stats_without_decode(self, mock): body = { "Processes": [ [ 'jhonce', '2417', '2274', '0', 'Mar01', '?', '00:00:01', '/usr/bin/ssh-agent /bin/sh -c exec -l /bin/bash -c "/usr/bin/gnome-session"', ], ['jhonce', '5544', '3522', '0', 'Mar01', 'pts/1', '00:00:02', '-bash'], ['jhonce', '6140', '3522', '0', 'Mar01', 'pts/2', '00:00:00', '-bash'], ], "Titles": ["UID", "PID", "PPID", "C", "STIME", "TTY", "TIME CMD"], } mock.get( tests.LIBPOD_URL + "/pods/stats" "?namesOrIDs=c8b9f5b17dc1406194010c752fc6dcb330192032e27648db9b14060447ecf3b8", json=body, ) actual = self.client.pods.stats( name="c8b9f5b17dc1406194010c752fc6dcb330192032e27648db9b14060447ecf3b8", decode=True ) self.assertDictEqual(actual, body) @requests_mock.Mocker() def test_top_with_streaming(self, mock): stream = [ [ { 'CPU': '2.53%', 'MemUsage': '49.15kB / 16.71GB', 'MemUsageBytes': '48KiB / 15.57GiB', 'Mem': '0.00%', 'NetIO': '7.638kB / 430B', 'BlockIO': '-- / --', 'PIDS': '1', 'Pod': '1c948ab42339', 'CID': 'd999c49a7b6c', 'Name': '1c948ab42339-infra', } ], [ { 'CPU': '1.46%', 'MemUsage': '57.23B / 16.71GB', 'MemUsageBytes': '48KiB / 15.57GiB', 'Mem': '0.00%', 'NetIO': '7.638kB / 430B', 'BlockIO': '-- / --', 'PIDS': '1', 'Pod': '1c948ab42339', 'CID': 'd999c49a7b6c', 'Name': '1c948ab42339-infra', } ], ] buffer = io.StringIO() for entry in stream: buffer.write(json.JSONEncoder().encode(entry)) buffer.write("\n") adapter = mock.get( tests.LIBPOD_URL + "/pods/stats?stream=True", text=buffer.getvalue(), ) stream_results = self.client.pods.stats(stream=True, decode=True) self.assertIsInstance(stream_results, Iterable) for response, actual in zip(stream_results, stream): self.assertIsInstance(response, list) self.assertListEqual(response, actual) self.assertTrue(adapter.called_once) def test_stats_400(self): with self.assertRaises(ValueError): self.client.pods.stats(all=True, name="container") if __name__ == '__main__': unittest.main() podman-py-5.4.0.1/podman/tests/unit/test_registrydata.py000066400000000000000000000065531475540541200233150ustar00rootroot00000000000000import unittest import requests_mock from podman import PodmanClient, tests from podman.domain.images_manager import ImagesManager from podman.domain.registry_data import RegistryData from podman.errors import InvalidArgument FIRST_IMAGE = { "Id": "326dd9d7add24646a325e8eaa82125294027db2332e49c5828d96312c5d773ab", "ParentId": "", "RepoTags": ["fedora:latest", "fedora:33", ":"], "RepoDigests": [ "fedora@sha256:9598a10fa72b402db876ccd4b3d240a4061c7d1e442745f1896ba37e1bf38664" ], "Created": 1614033320, "Size": 23855104, "VirtualSize": 23855104, "SharedSize": 0, "Labels": {}, "Containers": 2, "Os": "linux", "Architecture": "amd64", } class RegistryDataTestCase(unittest.TestCase): """Test RegistryData. Note: Mock responses need to be coded for libpod returns. The python bindings are responsible for mapping to compatible output. """ def setUp(self) -> None: super().setUp() self.client = PodmanClient(base_url=tests.BASE_SOCK) def tearDown(self) -> None: super().tearDown() self.client.close() @requests_mock.Mocker() def test_init(self, mock): mock.get( tests.LIBPOD_URL + "/images/326dd9d7add24646a325e8eaa82125294027db2332e49c5828d96312c5d773ab/json", json=FIRST_IMAGE, ) actual = RegistryData( "326dd9d7add24646a325e8eaa82125294027db2332e49c5828d96312c5d773ab", client=self.client.api, collection=ImagesManager(client=self.client.api), ) self.assertEqual( actual.id, "326dd9d7add24646a325e8eaa82125294027db2332e49c5828d96312c5d773ab" ) def test_platform(self): rd = RegistryData( "326dd9d7add24646a325e8eaa82125294027db2332e49c5828d96312c5d773ab", attrs=FIRST_IMAGE, collection=ImagesManager(client=self.client.api), ) self.assertTrue(rd.has_platform("linux/amd64/fedora")) def test_platform_dict(self): rd = RegistryData( "326dd9d7add24646a325e8eaa82125294027db2332e49c5828d96312c5d773ab", attrs=FIRST_IMAGE, collection=ImagesManager(client=self.client.api), ) self.assertTrue(rd.has_platform({"os": "linux", "architecture": "amd64"})) def test_platform_404(self): rd = RegistryData( "326dd9d7add24646a325e8eaa82125294027db2332e49c5828d96312c5d773ab", attrs=FIRST_IMAGE, collection=ImagesManager(client=self.client.api), ) self.assertFalse(rd.has_platform({"os": "COS", "architecture": "X-MP"})) def test_platform_409(self): rd = RegistryData( "326dd9d7add24646a325e8eaa82125294027db2332e49c5828d96312c5d773ab", attrs=FIRST_IMAGE, collection=ImagesManager(client=self.client.api), ) with self.assertRaises(InvalidArgument): rd.has_platform(list()) def test_platform_500(self): rd = RegistryData( "326dd9d7add24646a325e8eaa82125294027db2332e49c5828d96312c5d773ab", attrs=FIRST_IMAGE, collection=ImagesManager(client=self.client.api), ) with self.assertRaises(InvalidArgument): rd.has_platform("This/is/not/a/legal/image/name") if __name__ == '__main__': unittest.main() podman-py-5.4.0.1/podman/tests/unit/test_secrets.py000066400000000000000000000010211475540541200222440ustar00rootroot00000000000000import unittest from podman import PodmanClient, tests from podman.domain.secrets import SecretsManager class SecretsTestCase(unittest.TestCase): def setUp(self) -> None: super().setUp() self.client = PodmanClient(base_url=tests.BASE_SOCK) def tearDown(self) -> None: super().tearDown() self.client.close() def test_podmanclient(self): manager = self.client.secrets self.assertIsInstance(manager, SecretsManager) if __name__ == '__main__': unittest.main() podman-py-5.4.0.1/podman/tests/unit/test_system.py000066400000000000000000000034721475540541200221340ustar00rootroot00000000000000import unittest import requests_mock from podman import PodmanClient, tests class SystemTestCase(unittest.TestCase): def setUp(self) -> None: super().setUp() self.client = PodmanClient(base_url=tests.BASE_SOCK) def tearDown(self) -> None: super().tearDown() self.client.close() @requests_mock.Mocker() def test_df(self, mock): body = { "Containers": [ {"ContainerID": "f1fb3564c202"}, {"ContainerID": "779afab684c7"}, ], "Images": [ {"ImageID": "118cc2c68ef5"}, {"ImageID": "a6b4a8255f9e"}, ], "Volumes": [ {"VolumeName": "27df59163be8"}, {"VolumeName": "77a83a10f86e"}, ], } mock.get( tests.LIBPOD_URL + "/system/df", json=body, ) actual = self.client.df() self.assertDictEqual(actual, body) @requests_mock.Mocker() def test_info(self, mock): body = { "host": { "arch": "amd65", "os": "linux", } } mock.get(tests.LIBPOD_URL + "/info", json=body) actual = self.client.info() self.assertDictEqual(actual, body) @requests_mock.Mocker() def test_ping(self, mock): mock.head(tests.LIBPOD_URL + "/_ping") self.assertTrue(self.client.ping()) @requests_mock.Mocker() def test_version(self, mock): body = { "APIVersion": "3.0.0", "MinAPIVersion": "3.0.0", "Arch": "amd64", "Os": "linux", } mock.get(tests.LIBPOD_URL + "/version", json=body) self.assertDictEqual(self.client.version(), body) if __name__ == '__main__': unittest.main() podman-py-5.4.0.1/podman/tests/unit/test_volume.py000066400000000000000000000021261475540541200221120ustar00rootroot00000000000000import unittest import requests_mock from podman import PodmanClient, tests from podman.domain.volumes import Volume, VolumesManager FIRST_VOLUME = { "CreatedAt": "1985-04-12T23:20:50.52Z", "Driver": "default", "Labels": {"BackupRequired": True}, "Mountpoint": "/var/database", "Name": "dbase", "Scope": "local", } class VolumeTestCase(unittest.TestCase): def setUp(self) -> None: super().setUp() self.client = PodmanClient(base_url=tests.BASE_SOCK) def tearDown(self) -> None: super().tearDown() self.client.close() def test_id(self): actual = Volume(attrs={"Name": "dbase"}) self.assertEqual(actual.id, "dbase") @requests_mock.Mocker() def test_remove(self, mock): adapter = mock.delete(tests.LIBPOD_URL + "/volumes/dbase?force=True", status_code=204) vol_manager = VolumesManager(self.client.api) volume = vol_manager.prepare_model(attrs=FIRST_VOLUME) volume.remove(force=True) self.assertTrue(adapter.called_once) if __name__ == '__main__': unittest.main() podman-py-5.4.0.1/podman/tests/unit/test_volumesmanager.py000066400000000000000000000076341475540541200236410ustar00rootroot00000000000000import unittest import requests import requests_mock from podman import PodmanClient, tests from podman.domain.volumes import Volume, VolumesManager from podman.errors import NotFound FIRST_VOLUME = { "CreatedAt": "1985-04-12T23:20:50.52Z", "Driver": "default", "Labels": {"BackupRequired": True}, "Mountpoint": "/var/database", "Name": "dbase", "Scope": "local", } SECOND_VOLUME = { "CreatedAt": "1996-12-19T16:39:57-08:00", "Driver": "default", "Labels": {"BackupRequired": False}, "Mountpoint": "/var/source", "Name": "source", "Scope": "local", } class VolumesManagerTestCase(unittest.TestCase): """Test VolumesManager area of concern. Note: Mock responses need to be coded for libpod returns. The python bindings are responsible for mapping to compatible output. """ def setUp(self) -> None: super().setUp() self.client = PodmanClient(base_url=tests.BASE_SOCK) self.addCleanup(self.client.close) def test_podmanclient(self): manager = self.client.volumes self.assertIsInstance(manager, VolumesManager) @requests_mock.Mocker() def test_create(self, mock): adapter = mock.post( tests.LIBPOD_URL + "/volumes/create", json=FIRST_VOLUME, status_code=requests.codes.created, ) actual = self.client.volumes.create( "dbase", labels={ "BackupRequired": True, }, ) self.assertIsInstance(actual, Volume) self.assertTrue(adapter.called_once) self.assertDictEqual( adapter.last_request.json(), { "Name": "dbase", "Labels": { "BackupRequired": True, }, }, ) self.assertEqual(actual.id, "dbase") self.assertDictEqual( actual.attrs["Labels"], { "BackupRequired": True, }, ) @requests_mock.Mocker() def test_get(self, mock): mock.get( tests.LIBPOD_URL + "/volumes/dbase/json", json=FIRST_VOLUME, ) actual = self.client.volumes.get("dbase") self.assertIsInstance(actual, Volume) self.assertDictEqual(actual.attrs, FIRST_VOLUME) self.assertEqual(actual.id, actual.name) @requests_mock.Mocker() def test_get_404(self, mock): adapter = mock.get( tests.LIBPOD_URL + "/volumes/dbase/json", text="Not Found", status_code=404, ) with self.assertRaises(NotFound): self.client.volumes.get("dbase") self.assertTrue(adapter.called_once) @requests_mock.Mocker() def test_list(self, mock): mock.get(tests.LIBPOD_URL + "/volumes/json", json=[FIRST_VOLUME, SECOND_VOLUME]) actual = self.client.volumes.list(filters={"driver": "local"}) self.assertEqual(len(actual), 2) self.assertIsInstance(actual[0], Volume) self.assertEqual(actual[0].name, "dbase") self.assertIsInstance(actual[1], Volume) self.assertEqual(actual[1].id, "source") @requests_mock.Mocker() def test_list_404(self, mock): mock.get(tests.LIBPOD_URL + "/volumes/json", text="Not Found", status_code=404) actual = self.client.volumes.list() self.assertIsInstance(actual, list) self.assertEqual(len(actual), 0) @requests_mock.Mocker() def test_prune(self, mock): mock.post( tests.LIBPOD_URL + "/volumes/prune", json=[ {"Id": "dbase", "Size": 1024}, {"Id": "source", "Size": 1024}, ], ) actual = self.client.volumes.prune() self.assertDictEqual( actual, {"VolumesDeleted": ["dbase", "source"], "SpaceReclaimed": 2048} ) if __name__ == '__main__': unittest.main() podman-py-5.4.0.1/podman/tlsconfig.py000066400000000000000000000015761475540541200174230ustar00rootroot00000000000000"""Holds TLS configuration.""" class TLSConfig: """TLS configuration. Provided for compatibility, currently ignored. """ # pylint: disable=too-few-public-methods def __init__(self, *args, **kwargs): """Initialize TLSConfig. Keywords may be delegated to the SSH client configuration. Keyword Args: client_cert (tuple of str): Path to client cert, path to client key. ca_cert (str): Path to CA cert file. verify (bool or str): This can be False, or a path to a CA cert file. ssl_version (int): Ignored. assert_hostname (bool): Verify the hostname of the server. """ @staticmethod def configure_client(client) -> None: """Add TLS configuration to the client.""" # TODO Somehow work this into SSHAdapter(), if/when someone complains. podman-py-5.4.0.1/podman/version.py000066400000000000000000000001221475540541200171020ustar00rootroot00000000000000"""Version of PodmanPy.""" __version__ = "5.4.0" __compatible_version__ = "1.40" podman-py-5.4.0.1/pyproject.toml000066400000000000000000000067611475540541200165200ustar00rootroot00000000000000[build-system] requires = ["setuptools>=46.4"] build-backend = "setuptools.build_meta" [project] name = "podman" # TODO: remove the line version = ... on podman-py > 5.4.0 releases # dynamic = ["version"] version = "5.4.0.1" description = "Bindings for Podman RESTful API" readme = "README.md" license = {file = "LICENSE"} requires-python = ">=3.9" authors = [ { name = "Brent Baude" }, { name = "Jhon Honce", email = "jhonce@redhat.com" }, { name = "Urvashi Mohnani" }, { name = "Nicola Sella", email = "nsella@redhat.com" }, ] keywords = [ "libpod", "podman", ] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Software Development :: Libraries :: Python Modules", ] # compatible releases # ~= with version numbers dependencies = [ "requests >=2.24", "tomli>=1.2.3; python_version<'3.11'", "urllib3", ] [project.optional-dependencies] progress_bar = [ "rich >= 12.5.1", ] docs = [ "sphinx" ] test = [ "coverage", "fixtures", "pytest", "requests-mock", "tox", ] [project.urls] "Bug Tracker" = "https://github.com/containers/podman-py/issues" Homepage = "https://github.com/containers/podman-py" "Libpod API" = "https://docs.podman.io/en/latest/_static/api.html" [tool.pytest.ini_options] log_cli = true log_cli_level = "DEBUG" log_cli_format = "%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)" log_cli_date_format = "%Y-%m-%d %H:%M:%S" [tool.setuptools.packages.find] where = ["."] include = ["podman*"] # TODO: remove the line version = ... on podman-py > 5.4.0 releases # [tool.setuptools.dynamic] # version = {attr = "podman.version.__version__"} [tool.ruff] line-length = 100 src = ["podman"] # This is the section where Black is mostly replaced with Ruff [tool.ruff.format] exclude = [ ".git", ".history", ".tox", ".venv", "build", "dist", "docs", "hack", ] quote-style = "preserve" [tool.ruff.lint] select = [ # More stuff here https://docs.astral.sh/ruff/rules/ "F", # Pyflakes "E", # Pycodestyle Error "W", # Pycodestyle Warning "N", # PEP8 Naming # TODO "UP", # Pyupgrade # TODO "ANN", # TODO "S", # Bandit # "B", # Bugbear "A", # flake-8-builtins "YTT", # flake-8-2020 "PLC", # Pylint Convention "PLE", # Pylint Error "PLW", # Pylint Warning ] # Some checks should be enabled for code sanity disabled now # to avoid changing too many lines ignore = [ "F821", # TODO Undefined name "F541", # TODO f-string is missing placeholders "F401", # TODO Module imported but unused "F841", # TODO Local variable is assigned to but never used "E402", # TODO Module level import not at top of file "E741", # TODO ambiguous variable name "E722", # TODO do not use bare 'except' "E501", # TODO line too long "N818", # TODO Error Suffix in exception name "N80", # TODO Invalid Name "ANN10", # Missing type annotation "PLW2901", # TODO Redefined Loop Name ] [tool.ruff.lint.flake8-builtins] builtins-ignorelist = ["copyright", "all"] [tool.ruff.lint.per-file-ignores] "podman/tests/*.py" = ["S"] podman-py-5.4.0.1/rpm/000077500000000000000000000000001475540541200143705ustar00rootroot00000000000000podman-py-5.4.0.1/rpm/python-podman.spec000066400000000000000000000054451475540541200200510ustar00rootroot00000000000000# RHEL 8 envs has slightly different python deps # and also doesn't support dynamic (build)requires. %if %{defined rhel} && 0%{?rhel} == 8 %define rhel8_py 1 %endif %global pypi_name podman %global desc %{pypi_name} is a library of bindings to use the RESTful API for Podman. %global pypi_dist 4 Name: python-%{pypi_name} %if %{defined copr_username} Epoch: 102 %else Epoch: 3 %endif # DO NOT TOUCH the Version string! # The TRUE source of this specfile is: # https://github.com/containers/podman/blob/main/rpm/python-podman.spec # If that's what you're reading, Version must be 0, and will be updated by Packit for # copr and koji builds. # If you're reading this on dist-git, the version is automatically filled in by Packit. Version: 0 License: Apache-2.0 Release: %autorelease Summary: RESTful API for Podman URL: https://github.com/containers/%{pypi_name}-py # Tarball fetched from upstream Source0: %{url}/archive/v%{version}.tar.gz BuildArch: noarch %description %desc %package -n python%{python3_pkgversion}-%{pypi_name} BuildRequires: git-core BuildRequires: python%{python3_pkgversion}-devel %if %{defined rhel8_py} BuildRequires: python%{python3_pkgversion}-rpm-macros BuildRequires: python%{python3_pkgversion}-pytoml BuildRequires: python%{python3_pkgversion}-requests Requires: python%{python3_pkgversion}-pytoml Requires: python%{python3_pkgversion}-requests %else BuildRequires: pyproject-rpm-macros %endif Provides: %{pypi_name}-py = %{epoch}:%{version}-%{release} Provides: python%{python3_pkgversion}dist(%{pypi_name}) = %{pypi_dist} Provides: python%{python3_version}dist(%{pypi_name}) = %{pypi_dist} Obsoletes: python%{python3_pkgversion}-%{pypi_name}-api <= 0.0.0-1 Provides: python%{python3_pkgversion}-%{pypi_name}-api = %{epoch}:%{version}-%{release} Summary: %{summary} %{?python_provide:%python_provide python%{python3_pkgversion}-%{pypi_name}} %description -n python%{python3_pkgversion}-%{pypi_name} %desc %prep %autosetup -Sgit -n %{pypi_name}-py-%{version} %if !%{defined rhel8_py} %generate_buildrequires %pyproject_buildrequires %{?with_tests:-t} %endif %build export PBR_VERSION="0.0.0" %if %{defined rhel8_py} %py3_build %else %pyproject_wheel %endif %install export PBR_VERSION="0.0.0" %if %{defined rhel8_py} %py3_install %else %pyproject_install %pyproject_save_files %{pypi_name} %endif %check %if %{defined rhel8_py} %files -n python%{python3_pkgversion}-%{pypi_name} %dir %{python3_sitelib}/%{pypi_name}-*-py%{python3_version}.egg-info %{python3_sitelib}/%{pypi_name}-*-py%{python3_version}.egg-info/* %dir %{python3_sitelib}/%{pypi_name} %{python3_sitelib}/%{pypi_name}/* %else %pyproject_extras_subpkg -n python%{python3_pkgversion}-%{pypi_name} progress_bar %files -n python%{python3_pkgversion}-%{pypi_name} -f %{pyproject_files} %endif %license LICENSE %doc README.md %changelog %autochangelog podman-py-5.4.0.1/rpm/update-spec-version.sh000066400000000000000000000007331475540541200206240ustar00rootroot00000000000000#!/usr/bin/env bash # This script will update the Version field in the spec which is set to 0 by # default. Useful for local manual rpm builds where the Version needs to be set # correctly. Run from the git root directory. set -eo pipefail # Script is run from git root directory SPEC_FILE=rpm/podman.spec LATEST_TAG=$(git tag --sort=creatordate | tail -1) LATEST_VERSION=$(echo $LATEST_TAG | sed -e 's/^v//') sed -i "s/^Version:.*/Version: $LATEST_VERSION/" $SPEC_FILE podman-py-5.4.0.1/setup.cfg000066400000000000000000000027611475540541200154210ustar00rootroot00000000000000[metadata] name = podman version = 5.4.0.1 author = Brent Baude, Jhon Honce, Urvashi Mohnani, Nicola Sella author_email = jhonce@redhat.com description = Bindings for Podman RESTful API long_description = file: README.md long_description_content_type = text/markdown url = https://github.com/containers/podman-py license = Apache-2.0 license_files = LICENSE platforms = any project_urls = Bug Tracker = https://github.com/containers/podman-py/issues Libpod API = https://docs.podman.io/en/latest/_static/api.html classifiers = Development Status :: 5 - Production/Stable Intended Audience :: Developers License :: OSI Approved :: Apache Software License Operating System :: OS Independent Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 Programming Language :: Python :: 3.13 Topic :: Software Development :: Libraries :: Python Modules keywords = podman, libpod [options] include_package_data = True python_requires = >=3.9 test_suite = # Any changes should be copied into pyproject.toml install_requires = requests >=2.24 tomli>=1.2.3; python_version<'3.11' urllib3 [options.extras_require] progress_bar = rich >= 12.5.1 # typing_extensions are included for RHEL 8.5 # typing_extensions;python_version<'3.8' [bdist_wheel] # python < 3.6 not supported universal = false [sdist] formats = gztar podman-py-5.4.0.1/setup.py000066400000000000000000000011431475540541200153030ustar00rootroot00000000000000import fnmatch import setuptools from setuptools import find_packages from setuptools.command.build_py import build_py as build_py_orig excluded = [ "podman/tests/*", ] class build_py(build_py_orig): def find_package_modules(self, package, package_dir): modules = super().find_package_modules(package, package_dir) return [ (pkg, mod, file) for (pkg, mod, file) in modules if not any(fnmatch.fnmatchcase(file, pat=pattern) for pattern in excluded) ] setuptools.setup( packages=find_packages(), cmdclass={"build_py": build_py}, ) podman-py-5.4.0.1/tests/000077500000000000000000000000001475540541200147345ustar00rootroot00000000000000podman-py-5.4.0.1/tests/main.fmf000066400000000000000000000014011475540541200163460ustar00rootroot00000000000000require: - make - python3-pip /lint: tag: [ stable, lint ] summary: Run linting on the whole codebase test: cd .. && make lint /coverage_integration: tag: [ stable, coverage ] summary: Run integration tests coverage check test: cd .. && make integration /coverage_unittest: tag: [ stable, coverage ] summary: Run unit tests coverage check test: cd .. && make unittest /tests: /base_python: tag: [ base ] summary: Run all tests on the base python version test: cd .. && make tests-ci-base-python duration: 10m /all_python: tag: [ matrix] summary: Run all tests for all ptyhon versions available test: cd .. && make tests-ci-all-python duration: 10m podman-py-5.4.0.1/tox.ini000066400000000000000000000015701475540541200151100ustar00rootroot00000000000000[tox] minversion = 3.2.0 envlist = coverage,py39,py310,py311,py312,py313 ignore_basepython_conflict = true [testenv] basepython = python3 usedevelop = True deps = .[test] commands = pytest {posargs} setenv = PODMAN_LOG_LEVEL = {env:PODMAN_LOG_LEVEL:INFO} PODMAN_BINARY = {env:PODMAN_BINARY:podman} DEBUG = {env:DEBUG:0} [testenv:venv] commands = {posargs} [testenv:lint] deps = ruff==0.8.1 allowlist_externals = ruff commands = ruff check --diff # TODO: add pylint as alias of lint for compatibility [testenv:coverage] commands = coverage run -m pytest coverage report -m --skip-covered --fail-under=80 --omit=podman/tests/* --omit=.tox/* [testenv:format] deps = ruff==0.8.1 allowlist_externals = ruff commands = ruff format --diff # TODO: add black as alias of format for compatibility [testenv:black-format] deps = black commands = black {posargs} .