├── .gitignore ├── .pylintrc ├── .travis.yml ├── AUTHORS ├── CHANGES.txt ├── CODE-OF-CONDUCT.md ├── ChangeLog ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── SECURITY.md ├── examples ├── eg_attach.py ├── eg_containers_by_image.py ├── eg_image_list.py ├── eg_inspect_fedora.py ├── eg_latest_containers.py ├── eg_new_image.py ├── eg_rm.py └── run_example.sh ├── podman ├── __init__.py ├── client.py └── libs │ ├── __init__.py │ ├── _containers_attach.py │ ├── _containers_start.py │ ├── containers.py │ ├── errors.py │ ├── images.py │ ├── pods.py │ ├── system.py │ ├── tunnel.py │ └── volumes.py ├── requirements.txt ├── setup.cfg ├── setup.py ├── test-requirements.txt ├── test ├── __init__.py ├── podman_testcase.py ├── retry_decorator.py ├── test_client.py ├── test_containers.py ├── test_images.py ├── test_libs.py ├── test_pods_ctnrs.py ├── test_pods_no_ctnrs.py ├── test_runner.sh ├── test_system.py └── test_tunnel.py ├── tests └── libs │ ├── __init__.py │ └── test_pod.py ├── tools └── synchronize.sh └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | wheels/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | MANIFEST 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *.cover 46 | .hypothesis/ 47 | .pytest_cache/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | db.sqlite3 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # SageMath parsed files 81 | *.sage.py 82 | 83 | # Environments 84 | .env 85 | .venv 86 | env/ 87 | venv/ 88 | ENV/ 89 | env.bak/ 90 | venv.bak/ 91 | 92 | # Spyder project settings 93 | .spyderproject 94 | .spyproject 95 | 96 | # Rope project settings 97 | .ropeproject 98 | 99 | # mkdocs documentation 100 | /site 101 | 102 | # mypy 103 | .mypy_cache/ 104 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # A comma-separated list of package or module names from where C extensions may 4 | # be loaded. Extensions are loading into the active Python interpreter and may 5 | # run arbitrary code. 6 | extension-pkg-whitelist= 7 | 8 | # Add files or directories to the blacklist. They should be base names, not 9 | # paths. 10 | ignore=CVS 11 | 12 | # Add files or directories matching the regex patterns to the blacklist. The 13 | # regex matches against base names, not paths. 14 | ignore-patterns= 15 | 16 | # Python code to execute, usually for sys.path manipulation such as 17 | # pygtk.require(). 18 | #init-hook= 19 | 20 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 21 | # number of processors available to use. 22 | jobs=0 23 | 24 | # Control the amount of potential inferred values when inferring a single 25 | # object. This can help the performance when dealing with large functions or 26 | # complex, nested conditions. 27 | limit-inference-results=100 28 | 29 | # List of plugins (as comma separated values of python modules names) to load, 30 | # usually to register additional checkers. 31 | load-plugins= 32 | 33 | # Pickle collected data for later comparisons. 34 | persistent=yes 35 | 36 | # Specify a configuration file. 37 | #rcfile= 38 | 39 | # When enabled, pylint would attempt to guess common misconfiguration and emit 40 | # user-friendly hints instead of false-positive error messages. 41 | suggestion-mode=yes 42 | 43 | # Allow loading of arbitrary C extensions. Extensions are imported into the 44 | # active Python interpreter and may run arbitrary code. 45 | unsafe-load-any-extension=no 46 | 47 | 48 | [MESSAGES CONTROL] 49 | 50 | # Only show warnings with the listed confidence levels. Leave empty to show 51 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. 52 | confidence= 53 | 54 | # Disable the message, report, category or checker with the given id(s). You 55 | # can either give multiple identifiers separated by comma (,) or put this 56 | # option multiple times (only on the command line, not in the configuration 57 | # file where it should appear only once). You can also use "--disable=all" to 58 | # disable everything first and then reenable specific checks. For example, if 59 | # you want to run only the similarities checker, you can use "--disable=all 60 | # --enable=similarities". If you want to run only the classes checker, but have 61 | # no Warning level messages displayed, use "--disable=all --enable=classes 62 | # --disable=W". 63 | disable=print-statement, 64 | parameter-unpacking, 65 | unpacking-in-except, 66 | old-raise-syntax, 67 | backtick, 68 | long-suffix, 69 | old-ne-operator, 70 | old-octal-literal, 71 | import-star-module-level, 72 | non-ascii-bytes-literal, 73 | raw-checker-failed, 74 | bad-inline-option, 75 | locally-disabled, 76 | locally-enabled, 77 | file-ignored, 78 | suppressed-message, 79 | useless-suppression, 80 | deprecated-pragma, 81 | use-symbolic-message-instead, 82 | apply-builtin, 83 | basestring-builtin, 84 | buffer-builtin, 85 | cmp-builtin, 86 | coerce-builtin, 87 | execfile-builtin, 88 | file-builtin, 89 | long-builtin, 90 | raw_input-builtin, 91 | reduce-builtin, 92 | standarderror-builtin, 93 | unicode-builtin, 94 | xrange-builtin, 95 | coerce-method, 96 | delslice-method, 97 | getslice-method, 98 | setslice-method, 99 | no-absolute-import, 100 | old-division, 101 | dict-iter-method, 102 | dict-view-method, 103 | next-method-called, 104 | metaclass-assignment, 105 | indexing-exception, 106 | raising-string, 107 | reload-builtin, 108 | oct-method, 109 | hex-method, 110 | nonzero-method, 111 | cmp-method, 112 | input-builtin, 113 | round-builtin, 114 | intern-builtin, 115 | unichr-builtin, 116 | map-builtin-not-iterating, 117 | zip-builtin-not-iterating, 118 | range-builtin-not-iterating, 119 | filter-builtin-not-iterating, 120 | using-cmp-argument, 121 | eq-without-hash, 122 | div-method, 123 | idiv-method, 124 | rdiv-method, 125 | exception-message-attribute, 126 | invalid-str-codec, 127 | sys-max-int, 128 | bad-python3-import, 129 | deprecated-string-function, 130 | deprecated-str-translate-call, 131 | deprecated-itertools-function, 132 | deprecated-types-field, 133 | next-method-defined, 134 | dict-items-not-iterating, 135 | dict-keys-not-iterating, 136 | dict-values-not-iterating, 137 | deprecated-operator-function, 138 | deprecated-urllib-function, 139 | xreadlines-attribute, 140 | deprecated-sys-function, 141 | exception-escape, 142 | comprehension-escape 143 | 144 | # Enable the message, report, category or checker with the given id(s). You can 145 | # either give multiple identifier separated by comma (,) or put this option 146 | # multiple time (only on the command line, not in the configuration file where 147 | # it should appear only once). See also the "--disable" option for examples. 148 | enable=c-extension-no-member 149 | 150 | 151 | [REPORTS] 152 | 153 | # Python expression which should return a note less than 10 (10 is the highest 154 | # note). You have access to the variables errors warning, statement which 155 | # respectively contain the number of errors / warnings messages and the total 156 | # number of statements analyzed. This is used by the global evaluation report 157 | # (RP0004). 158 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 159 | 160 | # Template used to display messages. This is a python new-style format string 161 | # used to format the message information. See doc for all details. 162 | #msg-template= 163 | 164 | # Set the output format. Available formats are text, parseable, colorized, json 165 | # and msvs (visual studio). You can also give a reporter class, e.g. 166 | # mypackage.mymodule.MyReporterClass. 167 | output-format=text 168 | 169 | # Tells whether to display a full report or only the messages. 170 | reports=no 171 | 172 | # Activate the evaluation score. 173 | score=yes 174 | 175 | 176 | [REFACTORING] 177 | 178 | # Maximum number of nested blocks for function / method body 179 | max-nested-blocks=5 180 | 181 | # Complete name of functions that never returns. When checking for 182 | # inconsistent-return-statements if a never returning function is called then 183 | # it will be considered as an explicit return statement and no message will be 184 | # printed. 185 | never-returning-functions=sys.exit 186 | 187 | 188 | [TYPECHECK] 189 | 190 | # List of decorators that produce context managers, such as 191 | # contextlib.contextmanager. Add to this list to register other decorators that 192 | # produce valid context managers. 193 | contextmanager-decorators=contextlib.contextmanager 194 | 195 | # List of members which are set dynamically and missed by pylint inference 196 | # system, and so shouldn't trigger E1101 when accessed. Python regular 197 | # expressions are accepted. 198 | generated-members= 199 | 200 | # Tells whether missing members accessed in mixin class should be ignored. A 201 | # mixin class is detected if its name ends with "mixin" (case insensitive). 202 | ignore-mixin-members=yes 203 | 204 | # Tells whether to warn about missing members when the owner of the attribute 205 | # is inferred to be None. 206 | ignore-none=yes 207 | 208 | # This flag controls whether pylint should warn about no-member and similar 209 | # checks whenever an opaque object is returned when inferring. The inference 210 | # can return multiple potential results while evaluating a Python object, but 211 | # some branches might not be evaluated, which results in partial inference. In 212 | # that case, it might be useful to still emit no-member and other checks for 213 | # the rest of the inferred objects. 214 | ignore-on-opaque-inference=yes 215 | 216 | # List of class names for which member attributes should not be checked (useful 217 | # for classes with dynamically set attributes). This supports the use of 218 | # qualified names. 219 | ignored-classes=optparse.Values,thread._local,_thread._local 220 | 221 | # List of module names for which member attributes should not be checked 222 | # (useful for modules/projects where namespaces are manipulated during runtime 223 | # and thus existing member attributes cannot be deduced by static analysis. It 224 | # supports qualified module names, as well as Unix pattern matching. 225 | ignored-modules= 226 | 227 | # Show a hint with possible names when a member name was not found. The aspect 228 | # of finding the hint is based on edit distance. 229 | missing-member-hint=yes 230 | 231 | # The minimum edit distance a name should have in order to be considered a 232 | # similar match for a missing member name. 233 | missing-member-hint-distance=1 234 | 235 | # The total number of similar names that should be taken in consideration when 236 | # showing a hint for a missing member. 237 | missing-member-max-choices=1 238 | 239 | 240 | [SPELLING] 241 | 242 | # Limits count of emitted suggestions for spelling mistakes. 243 | max-spelling-suggestions=4 244 | 245 | # Spelling dictionary name. Available dictionaries: none. To make it working 246 | # install python-enchant package.. 247 | spelling-dict= 248 | 249 | # List of comma separated words that should not be checked. 250 | spelling-ignore-words= 251 | 252 | # A path to a file that contains private dictionary; one word per line. 253 | spelling-private-dict-file= 254 | 255 | # Tells whether to store unknown words to indicated private dictionary in 256 | # --spelling-private-dict-file option instead of raising a message. 257 | spelling-store-unknown-words=no 258 | 259 | 260 | [MISCELLANEOUS] 261 | 262 | # List of note tags to take in consideration, separated by a comma. 263 | notes=FIXME, 264 | XXX, 265 | TODO 266 | 267 | 268 | [FORMAT] 269 | 270 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 271 | expected-line-ending-format= 272 | 273 | # Regexp for a line that is allowed to be longer than the limit. 274 | ignore-long-lines=^\s*(# )??$ 275 | 276 | # Number of spaces of indent required inside a hanging or continued line. 277 | indent-after-paren=4 278 | 279 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 280 | # tab). 281 | indent-string=' ' 282 | 283 | # Maximum number of characters on a single line. 284 | max-line-length=100 285 | 286 | # Maximum number of lines in a module. 287 | max-module-lines=1000 288 | 289 | # List of optional constructs for which whitespace checking is disabled. `dict- 290 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 291 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 292 | # `empty-line` allows space-only lines. 293 | no-space-check=trailing-comma, 294 | dict-separator 295 | 296 | # Allow the body of a class to be on the same line as the declaration if body 297 | # contains single statement. 298 | single-line-class-stmt=no 299 | 300 | # Allow the body of an if to be on the same line as the test if there is no 301 | # else. 302 | single-line-if-stmt=no 303 | 304 | 305 | [BASIC] 306 | 307 | # Naming style matching correct argument names. 308 | #argument-naming-style=snake_case 309 | 310 | # Regular expression matching correct argument names. Overrides argument- 311 | # naming-style. 312 | argument-rgx=[a-z_][a-z0-9_]{1,30}$ 313 | argument-name-hint=[a-z_][a-z0-9_]{1,30}$ 314 | 315 | # Naming style matching correct attribute names. 316 | attr-naming-style=snake_case 317 | 318 | # Regular expression matching correct attribute names. Overrides attr-naming- 319 | # style. 320 | #attr-rgx= 321 | 322 | # Bad variable names which should always be refused, separated by a comma. 323 | bad-names=foo, 324 | bar, 325 | baz, 326 | toto, 327 | tutu, 328 | tata 329 | 330 | # Naming style matching correct class attribute names. 331 | class-attribute-naming-style=any 332 | 333 | # Regular expression matching correct class attribute names. Overrides class- 334 | # attribute-naming-style. 335 | #class-attribute-rgx= 336 | 337 | # Naming style matching correct class names. 338 | class-naming-style=PascalCase 339 | 340 | # Regular expression matching correct class names. Overrides class-naming- 341 | # style. 342 | #class-rgx= 343 | 344 | # Naming style matching correct constant names. 345 | const-naming-style=UPPER_CASE 346 | 347 | # Regular expression matching correct constant names. Overrides const-naming- 348 | # style. 349 | #const-rgx= 350 | 351 | # Minimum line length for functions/classes that require docstrings, shorter 352 | # ones are exempt. 353 | docstring-min-length=-1 354 | 355 | # Naming style matching correct function names. 356 | function-naming-style=snake_case 357 | 358 | # Regular expression matching correct function names. Overrides function- 359 | # naming-style. 360 | #function-rgx= 361 | 362 | # Good variable names which should always be accepted, separated by a comma. 363 | good-names=c, 364 | e, 365 | i, 366 | j, 367 | k, 368 | r, 369 | v, 370 | ex, 371 | Run, 372 | _ 373 | 374 | # Include a hint for the correct naming format with invalid-name. 375 | include-naming-hint=no 376 | 377 | # Naming style matching correct inline iteration names. 378 | inlinevar-naming-style=any 379 | 380 | # Regular expression matching correct inline iteration names. Overrides 381 | # inlinevar-naming-style. 382 | #inlinevar-rgx= 383 | 384 | # Naming style matching correct method names. 385 | method-naming-style=snake_case 386 | 387 | # Regular expression matching correct method names. Overrides method-naming- 388 | # style. 389 | #method-rgx= 390 | 391 | # Naming style matching correct module names. 392 | module-naming-style=snake_case 393 | 394 | # Regular expression matching correct module names. Overrides module-naming- 395 | # style. 396 | #module-rgx= 397 | 398 | # Colon-delimited sets of names that determine each other's naming style when 399 | # the name regexes allow several styles. 400 | name-group= 401 | 402 | # Regular expression which should only match function or class names that do 403 | # not require a docstring. 404 | no-docstring-rgx=^_ 405 | 406 | # List of decorators that produce properties, such as abc.abstractproperty. Add 407 | # to this list to register other decorators that produce valid properties. 408 | # These decorators are taken in consideration only for invalid-name. 409 | property-classes=abc.abstractproperty 410 | 411 | # Naming style matching correct variable names. 412 | #variable-naming-style=snake_case 413 | 414 | # Regular expression matching correct variable names. Overrides variable- 415 | # naming-style. 416 | variable-rgx=[a-z_][a-z0-9_]{2,30}$ 417 | variable-name-hint=[a-z_][a-z0-9_]{2,30}$ 418 | 419 | [SIMILARITIES] 420 | 421 | # Ignore comments when computing similarities. 422 | ignore-comments=yes 423 | 424 | # Ignore docstrings when computing similarities. 425 | ignore-docstrings=yes 426 | 427 | # Ignore imports when computing similarities. 428 | ignore-imports=no 429 | 430 | # Minimum lines number of a similarity. 431 | min-similarity-lines=4 432 | 433 | 434 | [VARIABLES] 435 | 436 | # List of additional names supposed to be defined in builtins. Remember that 437 | # you should avoid to define new builtins when possible. 438 | additional-builtins= 439 | 440 | # Tells whether unused global variables should be treated as a violation. 441 | allow-global-unused-variables=yes 442 | 443 | # List of strings which can identify a callback function by name. A callback 444 | # name must start or end with one of those strings. 445 | callbacks=cb_, 446 | _cb 447 | 448 | # A regular expression matching the name of dummy variables (i.e. expected to 449 | # not be used). 450 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 451 | 452 | # Argument names that match this expression will be ignored. Default to name 453 | # with leading underscore. 454 | ignored-argument-names=_.*|^ignored_|^unused_ 455 | 456 | # Tells whether we should check for unused import in __init__ files. 457 | init-import=no 458 | 459 | # List of qualified module names which can have objects that can redefine 460 | # builtins. 461 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 462 | 463 | 464 | [LOGGING] 465 | 466 | # Logging modules to check that the string format arguments are in logging 467 | # function parameter format. 468 | logging-modules=logging 469 | 470 | 471 | [IMPORTS] 472 | 473 | # Allow wildcard imports from modules that define __all__. 474 | allow-wildcard-with-all=no 475 | 476 | # Analyse import fallback blocks. This can be used to support both Python 2 and 477 | # 3 compatible code, which means that the block might have code that exists 478 | # only in one or another interpreter, leading to false positives when analysed. 479 | analyse-fallback-blocks=no 480 | 481 | # Deprecated modules which should not be used, separated by a comma. 482 | deprecated-modules=optparse,tkinter.tix 483 | 484 | # Create a graph of external dependencies in the given file (report RP0402 must 485 | # not be disabled). 486 | ext-import-graph= 487 | 488 | # Create a graph of every (i.e. internal and external) dependencies in the 489 | # given file (report RP0402 must not be disabled). 490 | import-graph= 491 | 492 | # Create a graph of internal dependencies in the given file (report RP0402 must 493 | # not be disabled). 494 | int-import-graph= 495 | 496 | # Force import order to recognize a module as part of the standard 497 | # compatibility libraries. 498 | known-standard-library= 499 | 500 | # Force import order to recognize a module as part of a third party library. 501 | known-third-party=enchant 502 | 503 | 504 | [DESIGN] 505 | 506 | # Support argparse.Action constructor API 507 | # Maximum number of arguments for function / method. 508 | max-args=12 509 | 510 | # Maximum number of attributes for a class (see R0902). 511 | max-attributes=7 512 | 513 | # Maximum number of boolean expressions in an if statement. 514 | max-bool-expr=5 515 | 516 | # Maximum number of branch for function / method body. 517 | max-branches=12 518 | 519 | # Maximum number of locals for function / method body. 520 | max-locals=15 521 | 522 | # Maximum number of parents for a class (see R0901). 523 | max-parents=10 524 | 525 | # Maximum number of public methods for a class (see R0904). 526 | max-public-methods=20 527 | 528 | # Maximum number of return / yield for function / method body. 529 | max-returns=6 530 | 531 | # Maximum number of statements in function / method body. 532 | max-statements=50 533 | 534 | # Minimum number of public methods for a class (see R0903). 535 | min-public-methods=2 536 | 537 | 538 | [CLASSES] 539 | 540 | # List of method names used to declare (i.e. assign) instance attributes. 541 | defining-attr-methods=__init__, 542 | __new__, 543 | setUp 544 | 545 | # List of member names, which should be excluded from the protected access 546 | # warning. 547 | exclude-protected=_asdict, 548 | _fields, 549 | _replace, 550 | _source, 551 | _make 552 | 553 | # List of valid names for the first argument in a class method. 554 | valid-classmethod-first-arg=cls 555 | 556 | # List of valid names for the first argument in a metaclass class method. 557 | valid-metaclass-classmethod-first-arg=cls 558 | 559 | 560 | [EXCEPTIONS] 561 | 562 | # Exceptions that will emit a warning when being caught. Defaults to 563 | # "Exception". 564 | overgeneral-exceptions=Exception 565 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | language: python 3 | cache: 4 | - pip 5 | python: 6 | - '3.7' 7 | - '3.6' 8 | - '3.5' 9 | before_install: 10 | - pip install -U pip 11 | - pip install -U setuptools 12 | - pip install -U wheel 13 | - pip install -U twine 14 | install: 15 | - pip install tox-travis .[devel] 16 | script: 17 | # Check packaging (README format etc...) 18 | - pip install -U setuptools 19 | - pip install -U wheel 20 | - pip install -U twine 21 | - pip list 22 | - rm -rf dist 23 | - python setup.py sdist 24 | - python setup.py bdist_wheel 25 | - twine check dist/* 26 | # Execture tests 27 | - tox 28 | # While many deltas exists between python-podman version and libpod version 29 | # we ask to synchronize.sh to don't exit "1" to avoid to get lot of 30 | # well know issues, in other word we will only display errors in CI job log 31 | # but we will consider that everything work fine with that. 32 | - ./tools/synchronize.sh --ignore 33 | deploy: 34 | - provider: pypi 35 | user: containers-libpod 36 | password: 37 | secure: "C88DmalsYjwtUrjWWyEeeKtLa9Q0VXlCCoCDguNOhZFmroW3DsOxZZmHBEEK3fN2QCfE5asR4zMNMpszqg324ubY6gmfy+PHVOnsLVZf4B2ZV96vhGrElk99YfgblYidwqB0L84AxK6n54iX5KFQj/8WWfkTqnPrGcA6pGX5C332uKpQoW40k/OCSbdD0C/FzaTLvgH+rUxAhamu2I2k/qJFUr6uQ5flgIJyQJcgVCvN0dP4Yyx9eSRpuWbApn0NkHOSmXJD3I2YWEf5LCSjTiyDGuQlja2rOgLynoSgBa1t8ZFXSb/nenSunAAy2y1KOcrn7qCfc56V7NwcQ+RIXT7LlSU/e2GgwHlXXsRCpZOrBcKJu3tTlVsrxpk5aXg1+boKwl2T4NEjJERskmENT2TJyZFJMr3/XAdHtnuKZcqydO/CPIqIbfRfDSN2+/8J++I1zQa5+lDXjiA/Qs7gzCW2DLZIzvzT3bD1zQPWDAP2pmixLDOY6e29jMW5pqnJazU+mrJSwOKN6e5pe5dAKx436XFByK7HZhptmkFbP5LL0HnDO6VjK4QyF1KYnBV/S4Y58FyO2jOgXA7feYaybzzbzjRbooKiqG+GC0J76GSu7dd/pqTRWVYTEn37B+No7v5HEzTP1dnQWPPynppwyBMrtT1tqsRGyEmJnm7Ml/8=" 38 | on: 39 | tags: true 40 | distributions: sdist bdist_wheel 41 | skip_existing: true 42 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Anders F Björklund 2 | Brent Baude 3 | Daniel J Walsh 4 | Dhanisha Phadate 5 | Hervé Beraud 6 | Jhon Honce 7 | Jhon Honce 8 | -------------------------------------------------------------------------------- /CHANGES.txt: -------------------------------------------------------------------------------- 1 | v0.1.0, 2018-05-11 -- Initial release. 2 | -------------------------------------------------------------------------------- /CODE-OF-CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## The python-podman Project Community Code of Conduct 2 | 3 | The python-podman project follows the [Containers Community Code of Conduct](https://github.com/containers/common/blob/master/CODE-OF-CONDUCT.md). 4 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | CHANGES 2 | ======= 3 | 4 | * Fix pypi deployment by using documentation at markdown format 5 | 6 | v0.0.2 7 | ------ 8 | 9 | * Fix up pushing to pypi 10 | 11 | v0.0.1 12 | ------ 13 | 14 | * Pull image function throws KeyError for id 15 | * Secure Travis 16 | * Introduce travis-ci and autodeployments on tags 17 | * Improve packaging by using PBR 18 | * Add base requirements to README.md 19 | * Remove pypodman to python-pypodman repo 20 | * Update module to align with varlink API changes 21 | * Use GetVersion instead of Ping, as recommended 22 | * Improve README 23 | * pypodman: add options to handle ssh host keys 24 | * Update README.md 25 | * add missing bits 26 | * Initial copy from containers/libpod 27 | * Initial commit 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | prune test/ 2 | include README.md 3 | include requirements.txt 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PYTHON ?= $(shell command -v python3 2>/dev/null || command -v python) 2 | DESTDIR ?= / 3 | PODMAN_VERSION ?= '0.12' 4 | 5 | .PHONY: python-podman 6 | python-podman: 7 | PODMAN_VERSION=$(PODMAN_VERSION) \ 8 | $(PYTHON) setup.py sdist bdist 9 | 10 | .PHONY: lint 11 | lint: 12 | $(PYTHON) -m pylint podman 13 | 14 | .PHONY: integration 15 | integration: 16 | test/test_runner.sh -v 17 | 18 | .PHONY: install 19 | install: 20 | PODMAN_VERSION=$(PODMAN_VERSION) \ 21 | $(PYTHON) setup.py install --root ${DESTDIR} 22 | 23 | .PHONY: upload 24 | upload: clean 25 | PODMAN_VERSION=$(PODMAN_VERSION) $(PYTHON) setup.py sdist bdist_wheel 26 | twine check dist/* 27 | twine upload --verbose dist/* 28 | twine upload --verbose dist/* 29 | 30 | .PHONY: clobber 31 | clobber: uninstall clean 32 | 33 | .PHONY: uninstall 34 | uninstall: 35 | $(PYTHON) -m pip uninstall --yes podman ||: 36 | 37 | .PHONY: clean 38 | clean: 39 | rm -rf podman.egg-info dist 40 | find . -depth -name __pycache__ -exec rm -rf {} \; 41 | find . -depth -name \*.pyc -exec rm -f {} \; 42 | $(PYTHON) ./setup.py clean --all 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # podman - pythonic library for working with varlink interface to Podman 2 | 3 | [![Build Status](https://travis-ci.org/containers/python-podman.svg?branch=master)](https://travis-ci.org/containers/python-podman) 4 | ![PyPI](https://img.shields.io/pypi/v/podman.svg) 5 | ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/podman.svg) 6 | ![PyPI - Status](https://img.shields.io/pypi/status/podman.svg) 7 | 8 | ## Status: Deprecated 9 | 10 | See [podman-py](https://github.com/containers/podman-py) 11 | 12 | ## Overview 13 | 14 | Python podman library. 15 | 16 | Provide a stable API to call into. 17 | 18 | **Notice:** The varlink interface to Podman is currently deprecated and in maintenance mode. Podman version 2.0 was released in June 2020, including a fully supported REST API that replaces the varlink interface. The varlink interface is being removed from Podman at the 3.0 release. Python support for the 2.0 REST API is in the [python-py](https://github.com/containers/python-py) repository. The documentation for the REST API resides [here](http://docs.podman.io/en/latest/_static/api.html#operation/changesContainer). 19 | 20 | ## Releases 21 | 22 | ### Requirements 23 | 24 | * Python 3.5+ 25 | * See [How to install Python 3 on Red Hat Enterprise Linux](https://developers.redhat.com/blog/2018/08/13/install-python3-rhel/) if your installed version of Python is too old. 26 | * OpenSSH 6.7+ 27 | * Python dependencies in requirements.txt 28 | 29 | ### Install 30 | 31 | #### From pypi 32 | 33 | Install `python-podman` to the standard location for third-party 34 | Python modules: 35 | 36 | ```sh 37 | python3 -m pip install podman 38 | ``` 39 | 40 | To use this method on Unix/Linux system you need to have permission to write 41 | to the standard third-party module directory. 42 | 43 | Else, you can install the latest version of python-podman published on 44 | pypi to the Python user install directory for your platform. 45 | Typically ~/.local/. ([See the Python documentation for site.USER_BASE for full 46 | details.](https://pip.pypa.io/en/stable/user_guide/#user-installs)) 47 | You can install like this by using the `--user` option: 48 | 49 | ```sh 50 | python3 -m pip install --user podman 51 | ``` 52 | 53 | This method can be useful in many situations, for example, 54 | on a Unix system you might not have permission to write to the 55 | standard third-party module directory. Or you might wish to try out a module 56 | before making it a standard part of your local Python installation. 57 | This is especially true when upgrading a distribution already present: you want 58 | to make sure your existing base of scripts still works with the new version 59 | before actually upgrading. 60 | 61 | For further reading about how python installation works [you can read 62 | this documentation](https://docs.python.org/3/install/index.html#how-installation-works). 63 | 64 | #### By building from source 65 | 66 | To build the podman egg and install as user: 67 | 68 | ```sh 69 | cd ~/python-podman 70 | python3 setup.py clean -a && python3 setup.py sdist bdist 71 | python3 setup.py install --user 72 | ``` 73 | 74 | ## Code snippets/examples: 75 | 76 | ### Show images in storage 77 | 78 | ```python 79 | import podman 80 | 81 | with podman.Client() as client: 82 | list(map(print, client.images.list())) 83 | ``` 84 | 85 | ### Show containers created since midnight 86 | 87 | ```python 88 | from datetime import datetime, time, timezone 89 | 90 | import podman 91 | 92 | midnight = datetime.combine(datetime.today(), time.min, tzinfo=timezone.utc) 93 | 94 | with podman.Client() as client: 95 | for c in client.containers.list(): 96 | created_at = podman.datetime_parse(c.createdat) 97 | 98 | if created_at > midnight: 99 | print('Container {}: image: {} created at: {}'.format( 100 | c.id[:12], c.image[:32], podman.datetime_format(created_at))) 101 | ``` 102 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Security and Disclosure Information Policy for the python-podman Project 2 | 3 | The python-podman Project follows the [Security and Disclosure Information Policy](https://github.com/containers/common/blob/master/SECURITY.md) for the Containers Projects. 4 | -------------------------------------------------------------------------------- /examples/eg_attach.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Example: Run top on Alpine container.""" 3 | 4 | import podman 5 | 6 | print('{}\n'.format(__doc__)) 7 | 8 | with podman.Client() as client: 9 | id = client.images.pull('alpine:latest') 10 | img = client.images.get(id) 11 | cntr = img.create(detach=True, tty=True, command=['/usr/bin/top']) 12 | cntr.attach(eot=4) 13 | 14 | try: 15 | cntr.start() 16 | print() 17 | except (BrokenPipeError, KeyboardInterrupt): 18 | print('\nContainer disconnected.') 19 | -------------------------------------------------------------------------------- /examples/eg_containers_by_image.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Example: Show containers grouped by image id.""" 3 | 4 | from itertools import groupby 5 | 6 | import podman 7 | 8 | print('{}\n'.format(__doc__)) 9 | 10 | with podman.Client() as client: 11 | ctnrs = sorted(client.containers.list(), key=lambda k: k.imageid) 12 | for key, grp in groupby(ctnrs, key=lambda k: k.imageid): 13 | print('Image: {}'.format(key)) 14 | for c in grp: 15 | print(' : container: {} created at: {}'.format( 16 | c.id[:12], podman.datetime_format(c.createdat))) 17 | -------------------------------------------------------------------------------- /examples/eg_image_list.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Example: Show all images on system.""" 3 | 4 | import podman 5 | 6 | print('{}\n'.format(__doc__)) 7 | 8 | with podman.Client() as client: 9 | for img in client.images.list(): 10 | print(img) 11 | -------------------------------------------------------------------------------- /examples/eg_inspect_fedora.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Example: Pull Fedora and inspect image and container.""" 3 | 4 | import podman 5 | 6 | print('{}\n'.format(__doc__)) 7 | 8 | with podman.Client() as client: 9 | id = client.images.pull('registry.fedoraproject.org/fedora:28') 10 | img = client.images.get(id) 11 | print(img.inspect()) 12 | 13 | cntr = img.create() 14 | print(cntr.inspect()) 15 | 16 | cntr.remove() 17 | -------------------------------------------------------------------------------- /examples/eg_latest_containers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Example: Show all containers created since midnight.""" 3 | 4 | from datetime import datetime, time, timezone 5 | 6 | import podman 7 | 8 | print('{}\n'.format(__doc__)) 9 | 10 | 11 | midnight = datetime.combine(datetime.today(), time.min, tzinfo=timezone.utc) 12 | 13 | with podman.Client() as client: 14 | for c in client.containers.list(): 15 | created_at = podman.datetime_parse(c.createdat) 16 | 17 | if created_at > midnight: 18 | print('{}: image: {} createdAt: {}'.format( 19 | c.id[:12], c.image[:32], podman.datetime_format(created_at))) 20 | -------------------------------------------------------------------------------- /examples/eg_new_image.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Example: Create new image from container.""" 3 | 4 | import sys 5 | 6 | import podman 7 | 8 | 9 | def print_history(details): 10 | """Format history data from an image, in a table.""" 11 | for i, r in enumerate(details): 12 | print( 13 | '{}: {} {} {}'.format(i, r.id[:12], 14 | podman.datetime_format(r.created), r.tags), 15 | sep='\n') 16 | print("-" * 25) 17 | 18 | 19 | print('{}\n'.format(__doc__)) 20 | 21 | with podman.Client() as client: 22 | ctnr = next( 23 | (c for c in client.containers.list() if 'alpine' in c['image']), None) 24 | 25 | if ctnr: 26 | print_history(client.images.get(ctnr.imageid).history()) 27 | 28 | # Make changes as we save the container to a new image 29 | id = ctnr.commit('alpine-ash', changes=['CMD=/bin/ash']) 30 | print_history(client.images.get(id).history()) 31 | else: 32 | print('Unable to find "alpine" container.', file=sys.stderr) 33 | -------------------------------------------------------------------------------- /examples/eg_rm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Example: Remove containers by name.""" 3 | 4 | import podman 5 | 6 | print('{}\n'.format(__doc__)) 7 | 8 | with podman.Client() as client: 9 | image = client.image.get('alpine:latest') 10 | for _ in range(1000): 11 | ctnr = image.container() 12 | ctnr.start() 13 | ctnrs = client.containers.list() 14 | ctnr.remove() 15 | ctnrs = client.containers.list() 16 | ctnr.remove(force=True) 17 | ctnrs = client.containers.list() 18 | -------------------------------------------------------------------------------- /examples/run_example.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export PYTHONPATH=.. 4 | 5 | function examples { 6 | for file in $@; do 7 | python3 -c "import ast; f=open('"${file}"'); t=ast.parse(f.read()); print(ast.get_docstring(t) + ' -- "${file}"')" 8 | done 9 | } 10 | 11 | while getopts "lh" arg; do 12 | case $arg in 13 | l ) examples $(ls eg_*.py); exit 0 ;; 14 | h ) echo 1>&2 $0 [-l] [-h] filename ; exit 2 ;; 15 | esac 16 | done 17 | shift $((OPTIND-1)) 18 | 19 | # podman needs to play some games with resources 20 | if [[ $(id -u) != 0 ]]; then 21 | echo 1>&2 $0 must be run as root. 22 | exit 2 23 | fi 24 | 25 | if ! systemctl --quiet is-active io.podman.socket; then 26 | echo 1>&2 'podman is not running. systemctl enable --now io.podman.socket' 27 | exit 1 28 | fi 29 | 30 | function cleanup { 31 | podman rm $1 >/dev/null 2>&1 32 | } 33 | 34 | # Setup storage with an image and container 35 | podman pull alpine:latest >/tmp/podman.output 2>&1 36 | CTNR=$(podman create alpine) 37 | trap "cleanup $CTNR" EXIT 38 | 39 | if [[ -f $1 ]]; then 40 | python3 $1 41 | else 42 | python3 $1.py 43 | fi 44 | -------------------------------------------------------------------------------- /podman/__init__.py: -------------------------------------------------------------------------------- 1 | """A client for communicating with a Podman server.""" 2 | 3 | from pbr.version import VersionInfo 4 | 5 | from .client import Client 6 | from .libs import FoldedString, datetime_format, datetime_parse 7 | from .libs.errors import (ContainerNotFound, ErrorOccurred, ImageNotFound, 8 | InvalidState, NoContainerRunning, NoContainersInPod, 9 | PodContainerError, PodmanError, PodNotFound) 10 | 11 | assert FoldedString 12 | 13 | try: 14 | __version__ = VersionInfo("podman") 15 | except Exception: # pylint: disable=broad-except 16 | __version__ = '0.0.0' 17 | 18 | __all__ = [ 19 | 'Client', 20 | 'ContainerNotFound', 21 | 'datetime_format', 22 | 'datetime_parse', 23 | 'ErrorOccurred', 24 | 'ImageNotFound', 25 | 'InvalidState', 26 | 'NoContainerRunning', 27 | 'NoContainersInPod', 28 | 'PodContainerError', 29 | 'PodmanError', 30 | 'PodNotFound', 31 | ] 32 | -------------------------------------------------------------------------------- /podman/client.py: -------------------------------------------------------------------------------- 1 | """A client for communicating with a Podman varlink service.""" 2 | import errno 3 | import logging 4 | import os 5 | from urllib.parse import urlparse 6 | 7 | from varlink import Client as VarlinkClient 8 | from varlink import VarlinkError 9 | 10 | from .libs import cached_property 11 | from .libs.containers import Containers 12 | from .libs.errors import error_factory 13 | from .libs.images import Images 14 | from .libs.pods import Pods 15 | from .libs.system import System 16 | from .libs.tunnel import Context, Portal, Tunnel 17 | 18 | 19 | class BaseClient: 20 | """Context manager for API workers to access varlink.""" 21 | 22 | def __init__(self, context): 23 | """Construct Client.""" 24 | self._client = None 25 | self._iface = None 26 | self._context = context 27 | 28 | def __call__(self): 29 | """Support being called for old API.""" 30 | return self 31 | 32 | @classmethod 33 | def factory(cls, uri=None, interface="io.podman", **kwargs): 34 | """Construct a Client based on input.""" 35 | log_level = os.environ.get("PODMAN_LOG_LEVEL") 36 | if log_level is not None: 37 | logging.basicConfig(level=logging.getLevelName(log_level.upper())) 38 | logging.debug( 39 | "Logging level set to %s", 40 | logging.getLevelName(logging.getLogger().getEffectiveLevel()), 41 | ) 42 | 43 | if uri is None: 44 | raise ValueError("uri is required and cannot be None") 45 | if interface is None: 46 | raise ValueError("interface is required and cannot be None") 47 | 48 | unsupported = set(kwargs.keys()).difference( 49 | ( 50 | "uri", 51 | "interface", 52 | "remote_uri", 53 | "identity_file", 54 | "ignore_hosts", 55 | "known_hosts", 56 | ) 57 | ) 58 | if unsupported: 59 | raise ValueError( 60 | "Unknown keyword arguments: {}".format(", ".join(unsupported)) 61 | ) 62 | 63 | local_path = urlparse(uri).path 64 | if not local_path: 65 | raise ValueError( 66 | "path is required for uri," 67 | ' expected format "unix://path_to_socket"' 68 | ) 69 | 70 | if kwargs.get("remote_uri") is None: 71 | return LocalClient(Context(uri, interface)) 72 | 73 | required = ( 74 | "{} is required, expected format" 75 | ' "ssh://user@hostname[:port]/path_to_socket".' 76 | ) 77 | 78 | # Remote access requires the full tuple of information 79 | if kwargs.get("remote_uri") is None: 80 | raise ValueError(required.format("remote_uri")) 81 | 82 | remote = urlparse(kwargs["remote_uri"]) 83 | if remote.username is None: 84 | raise ValueError(required.format("username")) 85 | if remote.path == "": 86 | raise ValueError(required.format("path")) 87 | if remote.hostname is None: 88 | raise ValueError(required.format("hostname")) 89 | 90 | return RemoteClient( 91 | Context( 92 | uri, 93 | interface, 94 | local_path, 95 | remote.path, 96 | remote.username, 97 | remote.hostname, 98 | remote.port, 99 | kwargs.get("identity_file"), 100 | kwargs.get("ignore_hosts"), 101 | kwargs.get("known_hosts"), 102 | ) 103 | ) 104 | 105 | def open(self): 106 | """Open connection to podman service.""" 107 | self._client = VarlinkClient(address=self._context.uri) 108 | self._iface = self._client.open(self._context.interface) 109 | logging.debug( 110 | "%s opened varlink connection %s", 111 | type(self).__name__, 112 | str(self._iface), 113 | ) 114 | return self._iface 115 | 116 | def close(self): 117 | """Close connection to podman service.""" 118 | if hasattr(self._client, "close"): 119 | self._client.close() # pylint: disable=no-member 120 | self._iface.close() 121 | logging.debug( 122 | "%s closed varlink connection %s", 123 | type(self).__name__, 124 | str(self._iface), 125 | ) 126 | 127 | 128 | class LocalClient(BaseClient): 129 | """Context manager for API workers to access varlink.""" 130 | 131 | def __enter__(self): 132 | """Enter context for LocalClient.""" 133 | return self.open() 134 | 135 | def __exit__(self, e_type, e, e_traceback): 136 | """Cleanup context for LocalClient.""" 137 | self.close() 138 | if isinstance(e, VarlinkError): 139 | raise error_factory(e) 140 | 141 | 142 | class RemoteClient(BaseClient): 143 | """Context manager for API workers to access remote varlink.""" 144 | 145 | def __init__(self, context): 146 | """Construct RemoteCLient.""" 147 | super().__init__(context) 148 | self._portal = Portal() 149 | 150 | def __enter__(self): 151 | """Context manager for API workers to access varlink.""" 152 | tunnel = self._portal.get(self._context.uri) 153 | if tunnel is None: 154 | tunnel = Tunnel(self._context).bore() 155 | self._portal[self._context.uri] = tunnel 156 | 157 | try: 158 | return self.open() 159 | except Exception: 160 | tunnel.close() 161 | raise 162 | 163 | def __exit__(self, e_type, e, e_traceback): 164 | """Cleanup context for RemoteClient.""" 165 | self.close() 166 | # set timer to shutdown ssh tunnel 167 | # self._portal.get(self._context.uri).close() 168 | if isinstance(e, VarlinkError): 169 | raise error_factory(e) 170 | 171 | 172 | class Client: 173 | """A client for communicating with a Podman varlink service. 174 | 175 | Example: 176 | 177 | >>> import podman 178 | >>> c = podman.Client() 179 | >>> c.system.versions 180 | 181 | Example remote podman: 182 | 183 | >>> import podman 184 | >>> c = podman.Client(uri='unix:/tmp/podman.sock', 185 | remote_uri='ssh://user@host/run/podman/io.podman', 186 | identity_file='~/.ssh/id_rsa') 187 | """ 188 | 189 | def __init__( 190 | self, uri="unix:/run/podman/io.podman", interface="io.podman", **kwargs 191 | ): 192 | """Construct a podman varlink Client. 193 | 194 | uri from default systemd unit file. 195 | interface from io.podman.varlink, do not change unless 196 | you are a varlink guru. 197 | """ 198 | self._client = BaseClient.factory(uri, interface, **kwargs) 199 | 200 | address = "{}-{}".format(uri, interface) 201 | # Quick validation of connection data provided 202 | try: 203 | if not System(self._client).ping(): 204 | raise ConnectionRefusedError( 205 | errno.ECONNREFUSED, 206 | ('Failed varlink connection "{}"').format(address), 207 | ) 208 | except FileNotFoundError: 209 | raise ConnectionError( 210 | errno.ECONNREFUSED, 211 | ( 212 | 'Failed varlink connection "{}".' 213 | " Is podman socket or service running?" 214 | ).format(address), 215 | ) 216 | 217 | def __enter__(self): 218 | """Return `self` upon entering the runtime context.""" 219 | return self 220 | 221 | def __exit__(self, exc_type, exc_value, traceback): 222 | """Raise any exception triggered within the runtime context.""" 223 | return 224 | 225 | @cached_property 226 | def system(self): 227 | """Manage system model for podman.""" 228 | return System(self._client) 229 | 230 | @cached_property 231 | def images(self): 232 | """Manage images model for libpod.""" 233 | return Images(self._client) 234 | 235 | @cached_property 236 | def containers(self): 237 | """Manage containers model for libpod.""" 238 | return Containers(self._client) 239 | 240 | @cached_property 241 | def pods(self): 242 | """Manage pods model for libpod.""" 243 | return Pods(self._client) 244 | -------------------------------------------------------------------------------- /podman/libs/__init__.py: -------------------------------------------------------------------------------- 1 | """Support files for podman API implementation.""" 2 | import collections 3 | import datetime 4 | import functools 5 | 6 | from dateutil.parser import parse as dateutil_parse 7 | 8 | __all__ = [ 9 | 'cached_property', 10 | 'datetime_format', 11 | 'datetime_parse', 12 | 'flatten', 13 | 'fold_keys', 14 | ] 15 | 16 | 17 | def cached_property(fn): 18 | """Decorate property to cache return value.""" 19 | return property(functools.lru_cache(maxsize=8)(fn)) 20 | 21 | 22 | class ConfigDict(collections.UserDict): 23 | """Silently ignore None values, only take key once.""" 24 | 25 | def __init__(self, **kwargs): 26 | """Construct dictionary.""" 27 | super().__init__(kwargs) 28 | 29 | def __setitem__(self, key, value): 30 | """Store unique, not None values.""" 31 | if value is None: 32 | return 33 | 34 | if super().__contains__(key): 35 | return 36 | 37 | super().__setitem__(key, value) 38 | 39 | 40 | class FoldedString(collections.UserString): 41 | """Foldcase sequences value.""" 42 | 43 | def __init__(self, seq): 44 | super().__init__(seq) 45 | self.data.casefold() 46 | 47 | 48 | def fold_keys(): # noqa: D202 49 | """Fold case of dictionary keys.""" 50 | 51 | @functools.wraps(fold_keys) 52 | def wrapped(mapping): 53 | """Fold case of dictionary keys.""" 54 | return {k.casefold(): v for (k, v) in mapping.items()} 55 | 56 | return wrapped 57 | 58 | 59 | def datetime_parse(string): 60 | """Convert timestamps to datetime. 61 | 62 | tzinfo aware, if provided. 63 | """ 64 | return dateutil_parse(string.upper(), fuzzy=True) 65 | 66 | 67 | def datetime_format(dt): 68 | """Format datetime in consistent style.""" 69 | if isinstance(dt, str): 70 | return datetime_parse(dt).isoformat() 71 | 72 | if isinstance(dt, datetime.datetime): 73 | return dt.isoformat() 74 | 75 | raise ValueError('Unable to format {}. Type {} not supported.'.format( 76 | dt, type(dt))) 77 | 78 | 79 | def flatten(list_, ltypes=(list, tuple)): 80 | """Flatten lists of list into a list.""" 81 | ltype = type(list_) 82 | list_ = list(list_) 83 | i = 0 84 | while i < len(list_): 85 | while isinstance(list_[i], ltypes): 86 | if not list_[i]: 87 | list_.pop(i) 88 | i -= 1 89 | break 90 | else: 91 | list_[i:i + 1] = list_[i] 92 | i += 1 93 | return ltype(list_) 94 | -------------------------------------------------------------------------------- /podman/libs/_containers_attach.py: -------------------------------------------------------------------------------- 1 | """Exported method Container.attach().""" 2 | 3 | import collections 4 | import fcntl 5 | import logging 6 | import struct 7 | import sys 8 | import termios 9 | 10 | 11 | class Mixin: 12 | """Publish attach() for inclusion in Container class.""" 13 | 14 | def attach(self, eot=4, stdin=None, stdout=None): 15 | """Attach to container's PID1 stdin and stdout. 16 | 17 | stderr is ignored. 18 | PseudoTTY work is done in start(). 19 | """ 20 | if stdin is None: 21 | stdin = sys.stdin.fileno() 22 | elif hasattr(stdin, 'fileno'): 23 | stdin = stdin.fileno() 24 | 25 | if stdout is None: 26 | stdout = sys.stdout.fileno() 27 | elif hasattr(stdout, 'fileno'): 28 | stdout = stdout.fileno() 29 | 30 | with self._client() as podman: 31 | attach = podman.GetAttachSockets(self._id) 32 | 33 | # This is the UDS where all the IO goes 34 | io_socket = attach['sockets']['io_socket'] 35 | assert len(io_socket) <= 107,\ 36 | 'Path length for sockets too long. {} > 107'.format( 37 | len(io_socket) 38 | ) 39 | 40 | # This is the control socket where resizing events are sent to conmon 41 | # attach['sockets']['control_socket'] 42 | self.pseudo_tty = collections.namedtuple( 43 | 'PseudoTTY', 44 | ['stdin', 'stdout', 'io_socket', 'control_socket', 'eot'])( 45 | stdin, 46 | stdout, 47 | attach['sockets']['io_socket'], 48 | attach['sockets']['control_socket'], 49 | eot, 50 | ) 51 | 52 | @property 53 | def resize_handler(self): 54 | """Send the new window size to conmon.""" 55 | 56 | def wrapped(signum, frame): # pylint: disable=unused-argument 57 | packed = fcntl.ioctl(self.pseudo_tty.stdout, termios.TIOCGWINSZ, 58 | struct.pack('HHHH', 0, 0, 0, 0)) 59 | rows, cols, _, _ = struct.unpack('HHHH', packed) 60 | logging.debug('Resize window(%dx%d) using %s', rows, cols, 61 | self.pseudo_tty.control_socket) 62 | 63 | # TODO: Need some kind of timeout in case pipe is blocked 64 | with open(self.pseudo_tty.control_socket, 'w') as skt: 65 | # send conmon window resize message 66 | skt.write('1 {} {}\n'.format(rows, cols)) 67 | 68 | return wrapped 69 | 70 | @property 71 | def log_handler(self): 72 | """Send command to reopen log to conmon.""" 73 | 74 | def wrapped(signum, frame): # pylint: disable=unused-argument 75 | with open(self.pseudo_tty.control_socket, 'w') as skt: 76 | # send conmon reopen log message 77 | skt.write('2\n') 78 | 79 | return wrapped 80 | -------------------------------------------------------------------------------- /podman/libs/_containers_start.py: -------------------------------------------------------------------------------- 1 | """Exported method Container.start().""" 2 | import logging 3 | import os 4 | import select 5 | import signal 6 | import socket 7 | import sys 8 | import termios 9 | import tty 10 | 11 | CONMON_BUFSZ = 8192 12 | 13 | 14 | class Mixin: 15 | """Publish start() for inclusion in Container class.""" 16 | 17 | def start(self): 18 | """Start container, return container on success. 19 | 20 | Will block if container has been detached. 21 | """ 22 | with self._client() as podman: 23 | logging.debug('Starting Container "%s"', self._id) 24 | results = podman.StartContainer(self._id) 25 | logging.debug('Started Container "%s"', results['container']) 26 | 27 | if not hasattr(self, 'pseudo_tty') or self.pseudo_tty is None: 28 | return self._refresh(podman) 29 | 30 | logging.debug('Setting up PseudoTTY for Container "%s"', 31 | results['container']) 32 | 33 | try: 34 | # save off the old settings for terminal 35 | tcoldattr = termios.tcgetattr(self.pseudo_tty.stdin) 36 | tty.setraw(self.pseudo_tty.stdin) 37 | 38 | # initialize container's window size 39 | self.resize_handler(None, sys._getframe(0)) 40 | 41 | # catch any resizing events and send the resize info 42 | # to the control fifo "socket" 43 | signal.signal(signal.SIGWINCH, self.resize_handler) 44 | 45 | except termios.error: 46 | tcoldattr = None 47 | 48 | try: 49 | # TODO: Is socket.SOCK_SEQPACKET supported in Windows? 50 | with socket.socket(socket.AF_UNIX, 51 | socket.SOCK_SEQPACKET) as skt: 52 | # Prepare socket for use with conmon/container 53 | skt.connect(self.pseudo_tty.io_socket) 54 | 55 | sources = [skt, self.pseudo_tty.stdin] 56 | while sources: 57 | logging.debug('Waiting on sources: %s', sources) 58 | readable, _, _ = select.select(sources, [], []) 59 | 60 | if skt in readable: 61 | data = skt.recv(CONMON_BUFSZ) 62 | if data: 63 | # Remove source marker when writing 64 | os.write(self.pseudo_tty.stdout, data[1:]) 65 | else: 66 | sources.remove(skt) 67 | 68 | if self.pseudo_tty.stdin in readable: 69 | data = os.read(self.pseudo_tty.stdin, CONMON_BUFSZ) 70 | if data: 71 | skt.sendall(data) 72 | 73 | if self.pseudo_tty.eot in data: 74 | sources.clear() 75 | else: 76 | sources.remove(self.pseudo_tty.stdin) 77 | finally: 78 | if tcoldattr: 79 | termios.tcsetattr(self.pseudo_tty.stdin, termios.TCSADRAIN, 80 | tcoldattr) 81 | signal.signal(signal.SIGWINCH, signal.SIG_DFL) 82 | return self._refresh(podman) 83 | -------------------------------------------------------------------------------- /podman/libs/containers.py: -------------------------------------------------------------------------------- 1 | """Models for manipulating containers and storage.""" 2 | import collections 3 | import getpass 4 | import json 5 | import logging 6 | import signal 7 | import time 8 | 9 | from . import fold_keys 10 | from ._containers_attach import Mixin as AttachMixin 11 | from ._containers_start import Mixin as StartMixin 12 | 13 | 14 | class Container(AttachMixin, StartMixin, collections.UserDict): 15 | """Model for a container.""" 16 | 17 | def __init__(self, client, ident, data, refresh=True): 18 | """Construct Container Model.""" 19 | super(Container, self).__init__(data) 20 | self._client = client 21 | self._id = ident 22 | 23 | if refresh: 24 | with client() as podman: 25 | self._refresh(podman) 26 | else: 27 | for k, v in self.data.items(): 28 | setattr(self, k, v) 29 | if 'containerrunning' in self.data: 30 | setattr(self, 'running', self.data['containerrunning']) 31 | self.data['running'] = self.data['containerrunning'] 32 | 33 | assert self._id == data['id'],\ 34 | 'Requested container id({}) does not match store id({})'.format( 35 | self._id, data['id'] 36 | ) 37 | 38 | def _refresh(self, podman, tries=1): 39 | try: 40 | ctnr = podman.GetContainer(self._id) 41 | except BrokenPipeError: 42 | logging.debug('Failed GetContainer(%s) try %d/3', self._id, tries) 43 | if tries > 3: 44 | raise 45 | else: 46 | with self._client() as pman: 47 | self._refresh(pman, tries + 1) 48 | else: 49 | super().update(ctnr['container']) 50 | 51 | for k, v in self.data.items(): 52 | setattr(self, k, v) 53 | if 'containerrunning' in self.data: 54 | setattr(self, 'running', self.data['containerrunning']) 55 | self.data['running'] = self.data['containerrunning'] 56 | 57 | return self 58 | 59 | def refresh(self): 60 | """Refresh status fields for this container.""" 61 | with self._client() as podman: 62 | return self._refresh(podman) 63 | 64 | def processes(self): 65 | """Show processes running in container.""" 66 | with self._client() as podman: 67 | results = podman.ListContainerProcesses(self._id) 68 | yield from results['container'] 69 | 70 | def changes(self): 71 | """Retrieve container changes.""" 72 | with self._client() as podman: 73 | results = podman.ListContainerChanges(self._id) 74 | return results['container'] 75 | 76 | def kill(self, sig=signal.SIGTERM, wait=25): 77 | """Send signal to container. 78 | 79 | default signal is signal.SIGTERM. 80 | wait n of seconds, 0 waits forever. 81 | """ 82 | with self._client() as podman: 83 | podman.KillContainer(self._id, sig) 84 | timeout = time.time() + wait 85 | while True: 86 | self._refresh(podman) 87 | if self.status != 'running': # pylint: disable=no-member 88 | return self 89 | 90 | if wait and timeout < time.time(): 91 | raise TimeoutError() 92 | 93 | time.sleep(0.5) 94 | 95 | def inspect(self): 96 | """Retrieve details about containers.""" 97 | with self._client() as podman: 98 | results = podman.InspectContainer(self._id) 99 | obj = json.loads(results['container'], object_hook=fold_keys()) 100 | return collections.namedtuple('ContainerInspect', obj.keys())(**obj) 101 | 102 | def export(self, target): 103 | """Export container from store to tarball. 104 | 105 | TODO: should there be a compress option, like images? 106 | """ 107 | with self._client() as podman: 108 | results = podman.ExportContainer(self._id, target) 109 | return results['tarfile'] 110 | 111 | def commit(self, image_name, **kwargs): 112 | """Create image from container. 113 | 114 | Keyword arguments: 115 | author -- change image's author 116 | message -- change image's message, docker format only. 117 | pause -- pause container during commit 118 | change -- Additional properties to change 119 | 120 | Change examples: 121 | CMD=/usr/bin/zsh 122 | ENTRYPOINT=/bin/sh date 123 | ENV=TEST=test_containers.TestContainers.test_commit 124 | EXPOSE=8888/tcp 125 | LABEL=unittest=test_commit 126 | USER=bozo:circus 127 | VOLUME=/data 128 | WORKDIR=/data/application 129 | 130 | All changes overwrite existing values. 131 | See inspect() to obtain current settings. 132 | """ 133 | author = kwargs.get('author', None) or getpass.getuser() 134 | change = kwargs.get('change', None) or [] 135 | message = kwargs.get('message', None) or '' 136 | pause = kwargs.get('pause', None) or True 137 | 138 | for c in change: 139 | if c.startswith('LABEL=') and c.count('=') < 2: 140 | raise ValueError( 141 | 'LABEL should have the format: LABEL=label=value, not {}'. 142 | format(c)) 143 | 144 | with self._client() as podman: 145 | results = podman.Commit(self._id, image_name, change, author, 146 | message, pause) 147 | return results['reply']['id'] 148 | 149 | def stop(self, timeout=25): 150 | """Stop container, return id on success.""" 151 | with self._client() as podman: 152 | podman.StopContainer(self._id, timeout) 153 | return self._refresh(podman) 154 | 155 | def remove(self, force=False): 156 | """Remove container, return id on success. 157 | 158 | force=True, stop running container. 159 | """ 160 | with self._client() as podman: 161 | results = podman.RemoveContainer(self._id, force) 162 | return results['container'] 163 | 164 | def restart(self, timeout=25): 165 | """Restart container with timeout, return id on success.""" 166 | with self._client() as podman: 167 | podman.RestartContainer(self._id, timeout) 168 | return self._refresh(podman) 169 | 170 | def pause(self): 171 | """Pause container, return id on success.""" 172 | with self._client() as podman: 173 | podman.PauseContainer(self._id) 174 | return self._refresh(podman) 175 | 176 | def unpause(self): 177 | """Unpause container, return id on success.""" 178 | with self._client() as podman: 179 | podman.UnpauseContainer(self._id) 180 | return self._refresh(podman) 181 | 182 | def update_container(self, *args, **kwargs): \ 183 | # pylint: disable=unused-argument 184 | """TODO: Update container..., return id on success.""" 185 | with self._client() as podman: 186 | podman.UpdateContainer() 187 | return self._refresh(podman) 188 | 189 | def wait(self): 190 | """Wait for container to finish, return 'returncode'.""" 191 | with self._client() as podman: 192 | results = podman.WaitContainer(self._id) 193 | return int(results['exitcode']) 194 | 195 | def stats(self): 196 | """Retrieve resource stats from the container.""" 197 | with self._client() as podman: 198 | results = podman.GetContainerStats(self._id) 199 | obj = results['container'] 200 | return collections.namedtuple('StatDetail', obj.keys())(**obj) 201 | 202 | def logs(self, *args, **kwargs): # pylint: disable=unused-argument 203 | """Retrieve container logs.""" 204 | with self._client() as podman: 205 | results = podman.GetContainerLogs(self._id) 206 | yield from results['container'] 207 | 208 | def health_check_run(self): 209 | """Executes defined container's healthcheck command 210 | and returns the container's health status..""" 211 | with self._client() as podman: 212 | result = podman.HealthCheckRun(self._id) 213 | yield result['healthCheckStatus'] 214 | 215 | def get_stats_with_history(self, previous_stats): 216 | """Takes a previous set of container statistics and uses 217 | libpod functions to calculate the containers statistics based on 218 | current and previous measurements.""" 219 | with self._client() as podman: 220 | results = podman.GetContainerStatsWithHistory(previous_stats) 221 | return results['container'] 222 | 223 | def init(self): 224 | """Initializes the container.""" 225 | with self._client() as podman: 226 | results = podman.InitContainer(self._id) 227 | return results['container'] 228 | 229 | def attach_control(self): 230 | """Sets up the ability to remotely attach to the container console.""" 231 | with self._client() as podman: 232 | podman.AttachControl(self._id) 233 | 234 | def checkpoint(self, keep=True, leaveRunning=True, tcpEstablished=True): 235 | """performs a checkpopint on the container.""" 236 | with self._client() as podman: 237 | results = podman.ContainerCheckpoint( 238 | self._id, 239 | keep, 240 | leaveRunning, 241 | tcpEstablished) 242 | return results['id'] 243 | 244 | def restore(self, keep=True, tcpEstablished=True): 245 | """Restores a container that has been checkpointed.""" 246 | with self._client() as podman: 247 | results = podman.ContainerRestore( 248 | self._id, 249 | keep, 250 | tcpEstablished) 251 | return results['id'] 252 | 253 | def run_label(self, runlabel): 254 | """Executes a command as described by a given container image label.""" 255 | with self._client() as podman: 256 | podman.ContainerRunlabel(runlabel) 257 | 258 | def exec(self, opts): 259 | """Executes a command in the container.""" 260 | with self._client() as podman: 261 | podman.ExecContainer(opts) 262 | 263 | def mount(self): 264 | """Mounts the container.""" 265 | with self._client() as podman: 266 | results = podman.MountContainer(self._id) 267 | return results['path'] 268 | 269 | def umount(self, force=False): 270 | """Mounts the container.""" 271 | with self._client() as podman: 272 | podman.UnmountContainer(self._id, force) 273 | 274 | def config(self): 275 | """Returns container's config in string form.""" 276 | with self._client() as podman: 277 | results = podman.ContainerConfig(self._id) 278 | return results['config'] 279 | 280 | def artifacts(self, artifactName): 281 | """Returns the container's artifacts in string form.""" 282 | with self._client() as podman: 283 | results = podman.ContainerArtifacts(self._id, artifactName) 284 | return results['config'] 285 | 286 | def inspect_data(self, size=True): 287 | """Returns the container's inspect data in string form.""" 288 | with self._client() as podman: 289 | results = podman.ContainerInspectData(self._id, size) 290 | return results['config'] 291 | 292 | def state_data(self): 293 | """Returns the container's state config in string form.""" 294 | with self._client() as podman: 295 | results = podman.ContainerStateData(self._id) 296 | return results['config'] 297 | 298 | 299 | class Containers(): 300 | """Model for Containers collection.""" 301 | 302 | def __init__(self, client): 303 | """Construct model for Containers collection.""" 304 | self._client = client 305 | 306 | def list(self): 307 | """List of containers in the container store.""" 308 | with self._client() as podman: 309 | results = podman.ListContainers() 310 | for cntr in results['containers']: 311 | yield Container(self._client, cntr['id'], cntr, refresh=False) 312 | 313 | def delete_stopped(self): 314 | """Delete all stopped containers.""" 315 | with self._client() as podman: 316 | results = podman.DeleteStoppedContainers() 317 | return results['containers'] 318 | 319 | def get(self, id_): 320 | """Retrieve container details from store.""" 321 | with self._client() as podman: 322 | cntr = podman.GetContainer(id_) 323 | return Container(self._client, cntr['container']['id'], 324 | cntr['container']) 325 | 326 | def get_by_status(self, status): 327 | """Get containers by status""" 328 | with self._client() as podman: 329 | results = podman.GetContainersByStatus(status) 330 | for cntr in results['containers']: 331 | yield Container(self._client, cntr['id'], cntr, refresh=False) 332 | 333 | def get_by_context(self, all=True, latest=False, args=[]): 334 | """Get containers ids or names depending on all, latest, or a list of 335 | container names""" 336 | with self._client() as podman: 337 | results = podman.GetContainersByContext(all, latest, args) 338 | for cntr in results['containers']: 339 | yield Container(self._client, cntr['id'], cntr, refresh=False) 340 | 341 | def logs(self, 342 | names, 343 | follow=True, 344 | latest=True, 345 | since="", 346 | tail=None, 347 | timestamps=True): 348 | """Get containers ids or names and returns the logs 349 | of these containers""" 350 | with self._client() as podman: 351 | results = podman.GetContainersLogs( 352 | names, 353 | follow, 354 | latest, 355 | since, 356 | tail, 357 | timestamps 358 | ) 359 | return results['log'] 360 | 361 | def exists(self, id_): 362 | """Returns a bool as to whether the container exists in 363 | local storage.""" 364 | with self._client() as podman: 365 | exist = podman.ContainerExists(id_) 366 | if exist['exists'] == 0: 367 | return True 368 | return False 369 | 370 | def list_mounts(self): 371 | """gathers all the mounted container mount points and returns 372 | them as an array of strings.""" 373 | with self._client() as podman: 374 | results = podman.ListContainerMounts() 375 | return results['mounts'] 376 | -------------------------------------------------------------------------------- /podman/libs/errors.py: -------------------------------------------------------------------------------- 1 | """Error classes and wrappers for VarlinkError.""" 2 | from varlink import VarlinkError 3 | 4 | 5 | class VarlinkErrorProxy(VarlinkError): 6 | """Class to Proxy VarlinkError methods.""" 7 | 8 | def __init__(self, message, namespaced=False): 9 | """Construct proxy from Exception.""" 10 | super().__init__(message.as_dict(), namespaced) 11 | self._message = message 12 | self.__module__ = 'libpod' 13 | 14 | def __getattr__(self, method): 15 | """Return attribute from proxied Exception.""" 16 | if hasattr(self._message, method): 17 | return getattr(self._message, method) 18 | 19 | try: 20 | return self._message.parameters()[method] 21 | except KeyError: 22 | raise AttributeError('No such attribute: {}'.format(method)) 23 | 24 | 25 | class ContainerNotFound(VarlinkErrorProxy): 26 | """Raised when Client cannot find requested container.""" 27 | 28 | 29 | class ImageNotFound(VarlinkErrorProxy): 30 | """Raised when Client cannot find requested image.""" 31 | 32 | 33 | class PodNotFound(VarlinkErrorProxy): 34 | """Raised when Client cannot find requested image.""" 35 | 36 | 37 | class PodContainerError(VarlinkErrorProxy): 38 | """Raised when a container fails requested pod operation.""" 39 | 40 | 41 | class NoContainerRunning(VarlinkErrorProxy): 42 | """Raised when no container is running in pod.""" 43 | 44 | 45 | class NoContainersInPod(VarlinkErrorProxy): 46 | """Raised when Client fails to connect to runtime.""" 47 | 48 | 49 | class ErrorOccurred(VarlinkErrorProxy): 50 | """Raised when an error occurs during the execution. 51 | 52 | See error() to see actual error text. 53 | """ 54 | 55 | 56 | class PodmanError(VarlinkErrorProxy): 57 | """Raised when Client fails to connect to runtime.""" 58 | 59 | 60 | class InvalidState(VarlinkErrorProxy): 61 | """Raised when container is in invalid state for operation.""" 62 | 63 | 64 | ERROR_MAP = { 65 | 'io.podman.ContainerNotFound': ContainerNotFound, 66 | 'io.podman.ErrorOccurred': ErrorOccurred, 67 | 'io.podman.ImageNotFound': ImageNotFound, 68 | 'io.podman.InvalidState': InvalidState, 69 | 'io.podman.NoContainerRunning': NoContainerRunning, 70 | 'io.podman.NoContainersInPod': NoContainersInPod, 71 | 'io.podman.PodContainerError': PodContainerError, 72 | 'io.podman.PodNotFound': PodNotFound, 73 | 'io.podman.RuntimeError': PodmanError, 74 | } 75 | 76 | 77 | def error_factory(exception): 78 | """Map Exceptions to a discrete type.""" 79 | try: 80 | return ERROR_MAP[exception.error()](exception) 81 | except KeyError: 82 | return exception 83 | -------------------------------------------------------------------------------- /podman/libs/images.py: -------------------------------------------------------------------------------- 1 | """Models for manipulating images in/to/from storage.""" 2 | import collections 3 | import copy 4 | import io 5 | import json 6 | import logging 7 | import os 8 | import tarfile 9 | import tempfile 10 | 11 | from . import ConfigDict, flatten, fold_keys 12 | from .containers import Container 13 | 14 | 15 | class Image(collections.UserDict): 16 | """Model for an Image.""" 17 | 18 | def __init__(self, client, id_, data): 19 | """Construct Image Model.""" 20 | super().__init__(data) 21 | for k, v in data.items(): 22 | setattr(self, k, v) 23 | 24 | self._id = id_ 25 | self._client = client 26 | 27 | assert ( 28 | self._id == data["id"] 29 | ), "Requested image id({}) does not match store id({})".format( 30 | self._id, data["id"] 31 | ) 32 | 33 | @staticmethod 34 | def _split_token(values=None, sep="="): 35 | if not values: 36 | return {} 37 | return {k: v1 for k, v1 in (v0.split(sep, 1) for v0 in values)} 38 | 39 | def create(self, **kwargs): 40 | """Create container from image. 41 | 42 | Pulls defaults from image.inspect() 43 | """ 44 | details = self.inspect() 45 | 46 | config = ConfigDict(image_id=self._id, **kwargs) 47 | config["command"] = details.config.get("cmd") 48 | config["env"] = self._split_token(details.config.get("env")) 49 | config["image"] = copy.deepcopy(details.repotags[0]) 50 | config["labels"] = copy.deepcopy(details.labels) 51 | # TODO: Are these settings still required? 52 | config["net_mode"] = "bridge" 53 | config["network"] = "bridge" 54 | 55 | try: 56 | config['args'] = flatten([config['image'], config['command']]) 57 | except KeyError: 58 | config['args'] = flatten([config['image']]) 59 | 60 | logging.debug("Image %s: create config: %s", self._id, config) 61 | with self._client() as podman: 62 | id_ = podman.CreateContainer(config)["container"] 63 | cntr = podman.GetContainer(id_) 64 | return Container(self._client, id_, cntr["container"]) 65 | 66 | container = create 67 | 68 | def export(self, dest, compressed=False): 69 | """Write image to dest, return id on success.""" 70 | with self._client() as podman: 71 | results = podman.ExportImage(self._id, dest, compressed) 72 | return results["image"] 73 | 74 | def history(self): 75 | """Retrieve image history.""" 76 | with self._client() as podman: 77 | for r in podman.HistoryImage(self._id)["history"]: 78 | yield collections.namedtuple("HistoryDetail", r.keys())(**r) 79 | 80 | def inspect(self): 81 | """Retrieve details about image.""" 82 | with self._client() as podman: 83 | results = podman.InspectImage(self._id) 84 | obj = json.loads(results["image"], object_hook=fold_keys()) 85 | return collections.namedtuple("ImageInspect", obj.keys())(**obj) 86 | 87 | def push( 88 | self, 89 | target, 90 | compress=False, 91 | manifest_format="", 92 | remove_signatures=False, 93 | sign_by="", 94 | ): 95 | """Copy image to target, return id on success.""" 96 | with self._client() as podman: 97 | results = podman.PushImage( 98 | self._id, 99 | target, 100 | compress, 101 | manifest_format, 102 | remove_signatures, 103 | sign_by, 104 | ) 105 | return results["reply"]["id"] 106 | 107 | def remove(self, force=False): 108 | """Delete image, return id on success. 109 | 110 | force=True, stop any running containers using image. 111 | """ 112 | with self._client() as podman: 113 | results = podman.RemoveImage(self._id, force) 114 | return results["image"] 115 | 116 | def tag(self, tag): 117 | """Tag image.""" 118 | with self._client() as podman: 119 | results = podman.TagImage(self._id, tag) 120 | return results["image"] 121 | 122 | def save(self, output, format_type="oci-archive"): 123 | """Save the image from the local image storage to a tarball.""" 124 | options = {'Name': self._id, 'Output': output, 'Format': format_type} 125 | with self._client() as podman: 126 | results = podman.ImageSave(options) 127 | return results["reply"] 128 | 129 | 130 | class Images: 131 | """Model for Images collection.""" 132 | 133 | def __init__(self, client): 134 | """Construct model for Images collection.""" 135 | self._client = client 136 | 137 | def list(self): 138 | """List all images in the libpod image store.""" 139 | with self._client() as podman: 140 | results = podman.ListImages() 141 | for img in results["images"]: 142 | yield Image(self._client, img["id"], img) 143 | 144 | def build( 145 | self, context_directory=None, containerfiles=None, tags=None, **kwargs 146 | ): 147 | """Build container from image. 148 | 149 | See podman-build.1.md for kwargs details. 150 | """ 151 | if not (containerfiles or context_directory): 152 | raise ValueError( 153 | 'Either "containerfiles" or "context_directory"' 154 | " is a required argument." 155 | ) 156 | 157 | if context_directory: 158 | if not os.path.isdir(context_directory): 159 | raise ValueError('"context_directory" must be a directory.') 160 | context_directory = os.path.abspath(context_directory) 161 | else: 162 | context_directory = os.getcwd() 163 | 164 | if not containerfiles: 165 | containerfiles = [] 166 | for entry in os.walk(context_directory): 167 | containerfiles.append(entry) 168 | 169 | if containerfiles and not isinstance(containerfiles, (list, tuple)): 170 | raise ValueError( 171 | '"containerfiles" is required to be a list or tuple.' 172 | ) 173 | 174 | if not tags: 175 | raise ValueError('"tags" is a required argument.') 176 | if not isinstance(tags, (list, tuple)): 177 | raise ValueError('"tags" is required to be a list or tuple.') 178 | 179 | config = ConfigDict( 180 | dockerfiles=containerfiles, tags=tags[1:], output=tags[0], **kwargs 181 | ) 182 | 183 | with io.BytesIO() as stream: 184 | # Compile build context in memory tar file 185 | with tarfile.open(mode="w:gz", fileobj=stream) as tar: 186 | for name in containerfiles: 187 | tar.addfile(tar.gettarinfo(fileobj=open(name))) 188 | 189 | if logging.getLogger().getEffectiveLevel() == logging.DEBUG: 190 | # If debugging save a copy of the tar file we're going 191 | # to send to service 192 | tar = os.path.join(tempfile.gettempdir(), "buildContext.tgz") 193 | with open(tar, "wb") as file: 194 | file.write(stream.getvalue()) 195 | 196 | with self._client() as podman: 197 | length = stream.seek(0, io.SEEK_END) 198 | remote_location = podman.SendFile("", length, _upgrade=True) 199 | 200 | logging.debug( 201 | "Build Tarball sent to host %s: %d", podman, length 202 | ) 203 | # TODO: When available use the convenience routines 204 | # pylint: disable=protected-access 205 | podman._connection.send(stream.getvalue()) 206 | 207 | config["contextDir"] = remote_location["file_handle"] 208 | clnt = self._client().open() 209 | output = clnt.BuildImage(build=config, _more=True) 210 | 211 | def wrapper(): 212 | v = None 213 | for v in output: 214 | if not v["image"]["logs"]: 215 | break 216 | yield v["image"]["logs"], None 217 | if v: 218 | yield None, self.get(v["image"]["id"]) 219 | clnt.close() 220 | 221 | return wrapper 222 | 223 | def delete_unused(self): 224 | """Delete Images not associated with a container.""" 225 | with self._client() as podman: 226 | results = podman.DeleteUnusedImages() 227 | return results["images"] 228 | 229 | def import_image(self, source, reference, message="", changes=None): 230 | """Read image tarball from source and save in image store.""" 231 | with self._client() as podman: 232 | results = podman.ImportImage(source, reference, message, changes) 233 | return results["image"] 234 | 235 | def pull(self, source): 236 | """Copy image from registry to image store.""" 237 | with self._client() as podman: 238 | results = podman.PullImage(source) 239 | return results["reply"]["id"] 240 | 241 | def search( 242 | self, 243 | id_, 244 | limit=25, 245 | is_official=None, 246 | is_automated=None, 247 | star_count=None, 248 | ): 249 | """Search registries for id.""" 250 | constraints = {} 251 | 252 | if is_official is not None: 253 | constraints["is_official"] = is_official 254 | if is_automated is not None: 255 | constraints["is_automated"] = is_automated 256 | if star_count is not None: 257 | constraints["star_count"] = star_count 258 | 259 | with self._client() as podman: 260 | results = podman.SearchImages(id_, limit, constraints) 261 | for img in results["results"]: 262 | yield collections.namedtuple("ImageSearch", img.keys())(**img) 263 | 264 | def get(self, id_): 265 | """Get Image from id.""" 266 | with self._client() as podman: 267 | result = podman.GetImage(id_) 268 | return Image(self._client, result["image"]["id"], result["image"]) 269 | 270 | def exists(self, id_): 271 | """Returns a bool as to whether the image exists in local storage.""" 272 | with self._client() as podman: 273 | exist = podman.ImageExists(id_) 274 | if exist['exists'] == 0: 275 | return True 276 | return False 277 | 278 | def prune(self, all=True, filter=[]): 279 | """Removes all unused images from the local store.""" 280 | with self._client() as podman: 281 | results = podman.ImagesPrune(all, filter) 282 | return results['pruned'] 283 | 284 | def load(self, id_, inputFile, quiet, deleteFile): 285 | """Load an image into local storage from a tarball.""" 286 | with self._client() as podman: 287 | results = podman.LoadImage(id_, inputFile, quiet, deleteFile) 288 | return results['reply'] 289 | -------------------------------------------------------------------------------- /podman/libs/pods.py: -------------------------------------------------------------------------------- 1 | """Model for accessing details of Pods from podman service.""" 2 | import collections 3 | import json 4 | import signal 5 | import time 6 | 7 | from . import ConfigDict, FoldedString, fold_keys 8 | 9 | 10 | class Pod(collections.UserDict): 11 | """Model for a Pod.""" 12 | 13 | def __init__(self, client, ident, data): 14 | """Construct Pod model.""" 15 | super().__init__(data) 16 | 17 | self._ident = ident 18 | self._client = client 19 | 20 | with client() as podman: 21 | self._refresh(podman) 22 | 23 | def _refresh(self, podman): 24 | pod = podman.GetPod(self._ident) 25 | super().update(pod['pod']) 26 | 27 | for k, v in self.data.items(): 28 | setattr(self, k, v) 29 | return self 30 | 31 | def inspect(self): 32 | """Retrieve details about pod.""" 33 | with self._client() as podman: 34 | results = podman.InspectPod(self._ident) 35 | obj = json.loads(results['pod'], object_hook=fold_keys()) 36 | obj['id'] = obj['config']['id'] 37 | return collections.namedtuple('PodInspect', obj.keys())(**obj) 38 | 39 | def kill(self, signal_=signal.SIGTERM, wait=25): 40 | """Send signal to all containers in pod. 41 | 42 | default signal is signal.SIGTERM. 43 | wait n of seconds, 0 waits forever. 44 | """ 45 | with self._client() as podman: 46 | podman.KillPod(self._ident, signal_) 47 | timeout = time.time() + wait 48 | while True: 49 | # pylint: disable=maybe-no-member 50 | self._refresh(podman) 51 | running = FoldedString(self.status) 52 | if running != 'running': 53 | break 54 | 55 | if wait and timeout < time.time(): 56 | raise TimeoutError() 57 | 58 | time.sleep(0.5) 59 | return self 60 | 61 | def pause(self): 62 | """Pause all containers in the pod.""" 63 | with self._client() as podman: 64 | podman.PausePod(self._ident) 65 | return self._refresh(podman) 66 | 67 | def refresh(self): 68 | """Refresh status fields for this pod.""" 69 | with self._client() as podman: 70 | return self._refresh(podman) 71 | 72 | def remove(self, force=False): 73 | """Remove pod and its containers returning pod ident. 74 | 75 | force=True, stop any running container. 76 | """ 77 | with self._client() as podman: 78 | results = podman.RemovePod(self._ident, force) 79 | return results['pod'] 80 | 81 | def restart(self): 82 | """Restart all containers in the pod.""" 83 | with self._client() as podman: 84 | podman.RestartPod(self._ident) 85 | return self._refresh(podman) 86 | 87 | def stats(self): 88 | """Stats on all containers in the pod.""" 89 | with self._client() as podman: 90 | results = podman.GetPodStats(self._ident) 91 | for obj in results['containers']: 92 | yield collections.namedtuple('ContainerStats', obj.keys())(**obj) 93 | 94 | def start(self): 95 | """Start all containers in the pod.""" 96 | with self._client() as podman: 97 | podman.StartPod(self._ident) 98 | return self._refresh(podman) 99 | 100 | def stop(self): 101 | """Stop all containers in the pod.""" 102 | with self._client() as podman: 103 | podman.StopPod(self._ident) 104 | return self._refresh(podman) 105 | 106 | def top(self): 107 | """Display stats for all containers.""" 108 | with self._client() as podman: 109 | results = podman.TopPod(self._ident) 110 | return results['pod'] 111 | 112 | def unpause(self): 113 | """Unpause all containers in the pod.""" 114 | with self._client() as podman: 115 | podman.UnpausePod(self._ident) 116 | return self._refresh(podman) 117 | 118 | def generate_kub(self, service=True): 119 | """Generates a Kubernetes v1 Pod description of a Podman container""" 120 | with self._client() as podman: 121 | results = podman.GenerateKube(self._ident, service) 122 | return results['pod'] 123 | 124 | def state_data(self): 125 | """Returns the container's state config in string form.""" 126 | with self._client() as podman: 127 | results = podman.PodStateData(self._id) 128 | return results['config'] 129 | 130 | 131 | class Pods(): 132 | """Model for accessing pods.""" 133 | 134 | def __init__(self, client): 135 | """Construct pod model.""" 136 | self._client = client 137 | 138 | def create(self, 139 | ident=None, 140 | cgroupparent=None, 141 | labels=None, 142 | share=None, 143 | infra=False): 144 | """Create a new empty pod.""" 145 | config = ConfigDict( 146 | name=ident, 147 | cgroupParent=cgroupparent, 148 | labels=labels, 149 | share=share, 150 | infra=infra, 151 | ) 152 | 153 | with self._client() as podman: 154 | result = podman.CreatePod(config) 155 | details = podman.GetPod(result['pod']) 156 | return Pod(self._client, result['pod'], details['pod']) 157 | 158 | def get(self, ident): 159 | """Get Pod from ident.""" 160 | with self._client() as podman: 161 | result = podman.GetPod(ident) 162 | return Pod(self._client, result['pod']['id'], result['pod']) 163 | 164 | def list(self): 165 | """List all pods.""" 166 | with self._client() as podman: 167 | results = podman.ListPods() 168 | for pod in results['pods']: 169 | yield Pod(self._client, pod['id'], pod) 170 | 171 | def get_by_status(self, statuses): 172 | """Get pods by statuses""" 173 | with self._client() as podman: 174 | results = podman.GetPodsByStatus(statuses) 175 | return results['containers'] 176 | 177 | def get_by_context(self, all=True, latest=False, args=[]): 178 | """Get pods ids or names depending on all, latest, or a list of 179 | pods names""" 180 | with self._client() as podman: 181 | results = podman.GetPodsByContext(all, latest, args) 182 | for pod in results['pods']: 183 | yield Pod(self._client, pod['id'], pod) 184 | -------------------------------------------------------------------------------- /podman/libs/system.py: -------------------------------------------------------------------------------- 1 | """Models for accessing details from varlink server.""" 2 | import collections 3 | 4 | import pkg_resources 5 | 6 | from . import cached_property 7 | 8 | 9 | class System(): 10 | """Model for accessing system resources.""" 11 | 12 | def __init__(self, client): 13 | """Construct system model.""" 14 | self._client = client 15 | 16 | @cached_property 17 | def versions(self): 18 | """Access versions.""" 19 | with self._client() as podman: 20 | vers = podman.GetVersion() 21 | 22 | client = '0.0.0' 23 | try: 24 | client = pkg_resources.get_distribution('podman').version 25 | except Exception: # pylint: disable=broad-except 26 | pass 27 | vers['client_version'] = client 28 | return collections.namedtuple('Version', vers.keys())(**vers) 29 | 30 | def info(self): 31 | """Return podman info.""" 32 | with self._client() as podman: 33 | info = podman.GetInfo()['info'] 34 | return collections.namedtuple('Info', info.keys())(**info) 35 | 36 | def ping(self): 37 | """Return True if server awake.""" 38 | with self._client() as podman: 39 | response = podman.GetVersion() 40 | return 'version' in response 41 | 42 | def receive_file(self, path, delete=True): 43 | """Allows the host to send a remote client a file.""" 44 | with self._client() as podman: 45 | response = podman.ReceiveFile(path, delete) 46 | return response['len'] 47 | 48 | def get_events(self, filters=[], since=None, until=None): 49 | """Returns known libpod events filtered by the options provided.""" 50 | with self._client() as podman: 51 | response = podman.GetEvent(filters, since, until) 52 | return response['events'] 53 | -------------------------------------------------------------------------------- /podman/libs/tunnel.py: -------------------------------------------------------------------------------- 1 | """Cache for SSH tunnels.""" 2 | import collections 3 | import getpass 4 | import logging 5 | import os 6 | import subprocess 7 | import threading 8 | import time 9 | import weakref 10 | from contextlib import suppress 11 | 12 | import psutil 13 | 14 | Context = collections.namedtuple('Context', ( 15 | 'uri', 16 | 'interface', 17 | 'local_socket', 18 | 'remote_socket', 19 | 'username', 20 | 'hostname', 21 | 'port', 22 | 'identity_file', 23 | 'ignore_hosts', 24 | 'known_hosts', 25 | )) 26 | Context.__new__.__defaults__ = (None, ) * len(Context._fields) 27 | 28 | 29 | class Portal(collections.MutableMapping): 30 | """Expiring container for tunnels.""" 31 | 32 | def __init__(self, sweap=25): 33 | """Construct portal, reap tunnels every sweap seconds.""" 34 | self.data = collections.OrderedDict() 35 | self.sweap = sweap 36 | self.ttl = sweap * 2 37 | self.lock = threading.RLock() 38 | self._schedule_reaper() 39 | 40 | def __getitem__(self, key): 41 | """Given uri return tunnel and update TTL.""" 42 | with self.lock: 43 | value, _ = self.data[key] 44 | self.data[key] = (value, time.time() + self.ttl) 45 | self.data.move_to_end(key) 46 | return value 47 | 48 | def __setitem__(self, key, value): 49 | """Store given tunnel keyed with uri.""" 50 | if not isinstance(value, Tunnel): 51 | raise ValueError('Portals only support Tunnels.') 52 | 53 | with self.lock: 54 | self.data[key] = (value, time.time() + self.ttl) 55 | self.data.move_to_end(key) 56 | 57 | def __delitem__(self, key): 58 | """Remove and close tunnel from portal.""" 59 | with self.lock: 60 | value, _ = self.data[key] 61 | del self.data[key] 62 | value.close() 63 | del value 64 | 65 | def __iter__(self): 66 | """Iterate tunnels.""" 67 | with self.lock: 68 | values = self.data.values() 69 | 70 | for tunnel, _ in values: 71 | yield tunnel 72 | 73 | def __len__(self): 74 | """Return number of tunnels in portal.""" 75 | with self.lock: 76 | return len(self.data) 77 | 78 | def _schedule_reaper(self): 79 | timer = threading.Timer(interval=self.sweap, function=self.reap) 80 | timer.setName('PortalReaper') 81 | timer.setDaemon(True) 82 | timer.start() 83 | 84 | def reap(self): 85 | """Remove tunnels who's TTL has expired.""" 86 | now = time.time() 87 | with self.lock: 88 | reaped_data = self.data.copy() 89 | for entry in reaped_data.items(): 90 | if entry[1][1] < now: 91 | del self.data[entry[0]] 92 | else: 93 | # StopIteration as soon as possible 94 | break 95 | self._schedule_reaper() 96 | 97 | 98 | class Tunnel(): 99 | """SSH tunnel.""" 100 | 101 | def __init__(self, context): 102 | """Construct Tunnel.""" 103 | self.context = context 104 | self._closed = True 105 | 106 | @property 107 | def closed(self): 108 | """Is tunnel closed.""" 109 | return self._closed 110 | 111 | def bore(self): 112 | """Create SSH tunnel from given context.""" 113 | cmd = ['ssh', '-fNT'] 114 | 115 | if logging.getLogger().getEffectiveLevel() == logging.DEBUG: 116 | cmd.append('-v') 117 | else: 118 | cmd.append('-q') 119 | 120 | if self.context.port: 121 | cmd.extend(('-p', str(self.context.port))) 122 | 123 | cmd.extend(('-L', '{}:{}'.format(self.context.local_socket, 124 | self.context.remote_socket))) 125 | 126 | if self.context.ignore_hosts: 127 | cmd.extend(('-o', 'StrictHostKeyChecking=no', 128 | '-o', 'UserKnownHostsFile=/dev/null')) 129 | elif self.context.known_hosts: 130 | cmd.extend(('-o', 'UserKnownHostsFile={known_hosts}'.format( 131 | known_hosts=self.context.known_hosts))) 132 | 133 | if self.context.identity_file: 134 | cmd.extend(('-i', self.context.identity_file)) 135 | 136 | cmd.append('{}@{}'.format(self.context.username, 137 | self.context.hostname)) 138 | 139 | logging.debug('Opening tunnel "%s", cmd "%s"', self.context.uri, 140 | ' '.join(cmd)) 141 | 142 | tunnel = subprocess.Popen(cmd, close_fds=True) 143 | # The return value of Popen() has no long term value as that process 144 | # has already exited by the time control is returned here. This is a 145 | # side effect of the -f option. wait() will be called to clean up 146 | # resources. 147 | for _ in range(300): 148 | # TODO: Make timeout configurable 149 | if os.path.exists(self.context.local_socket) \ 150 | or tunnel.returncode is not None: 151 | break 152 | with suppress(subprocess.TimeoutExpired): 153 | # waiting for either socket to be created 154 | # or first child to exit 155 | tunnel.wait(0.5) 156 | else: 157 | raise TimeoutError( 158 | 'Failed to create tunnel "{}", using: "{}"'.format( 159 | self.context.uri, ' '.join(cmd))) 160 | if tunnel.returncode is not None and tunnel.returncode != 0: 161 | raise subprocess.CalledProcessError(tunnel.returncode, 162 | ' '.join(cmd)) 163 | tunnel.wait() 164 | 165 | self._closed = False 166 | weakref.finalize(self, self.close) 167 | return self 168 | 169 | def close(self): 170 | """Close SSH tunnel.""" 171 | logging.debug('Closing tunnel "%s"', self.context.uri) 172 | 173 | if self._closed: 174 | return 175 | 176 | # Find all ssh instances for user with uri tunnel the hard way... 177 | targets = [ 178 | p 179 | for p in psutil.process_iter(attrs=['name', 'username', 'cmdline']) 180 | if p.info['username'] == getpass.getuser() 181 | and p.info['name'] == 'ssh' 182 | and self.context.local_socket in ' '.join(p.info['cmdline']) 183 | ] # yapf: disable 184 | 185 | # ask nicely for ssh to quit, reap results 186 | for proc in targets: 187 | proc.terminate() 188 | _, alive = psutil.wait_procs(targets, timeout=300) 189 | 190 | # kill off the uncooperative, then report any stragglers 191 | for proc in alive: 192 | proc.kill() 193 | _, alive = psutil.wait_procs(targets, timeout=300) 194 | 195 | for proc in alive: 196 | logging.info('process %d survived SIGKILL, giving up.', proc.pid) 197 | 198 | with suppress(OSError): 199 | os.remove(self.context.local_socket) 200 | self._closed = True 201 | -------------------------------------------------------------------------------- /podman/libs/volumes.py: -------------------------------------------------------------------------------- 1 | """Models for manipulating containers and storage.""" 2 | 3 | 4 | class Volumes(): 5 | """Model for Volumes collection.""" 6 | 7 | def __init__(self, client): 8 | """Construct model for Volume collection.""" 9 | self._client = client 10 | 11 | def create(self, options): 12 | """Creates a volume on a remote host.""" 13 | with self._client() as podman: 14 | results = podman.VolumeCreate(options) 15 | return results['volumeName'] 16 | 17 | def remove(self, options): 18 | """Remove a volume on a remote host.""" 19 | with self._client() as podman: 20 | results = podman.VolumeRemove(options) 21 | return results['successes'], results['failures'] 22 | 23 | def get(self, args, all=True): 24 | """Gets slice of the volumes on a remote host.""" 25 | with self._client() as podman: 26 | results = podman.GetVolumes(args, all) 27 | return results['volumes'] 28 | 29 | def prunes(self): 30 | """Removes unused volumes on the host.""" 31 | with self._client() as podman: 32 | results = podman.VolumesPrune() 33 | return results['prunedNames'], results['prunedErrors'] 34 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | psutil 2 | python-dateutil 3 | setuptools>=39 4 | varlink 5 | pbr 6 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = podman 3 | home-page = https://github.com/containers/python-podman 4 | summary = A library to interact with a Podman server 5 | description-file = 6 | README.md 7 | ChangeLog 8 | AUTHORS 9 | author = Jhon Honce 10 | long_description_content_type = text/markdown 11 | author_email = jhonce@redhat.com 12 | license = Apache Software License 13 | project_urls = 14 | Bug Tracker = https://github.com/containers/python-podman/issues 15 | Source Code = https://github.com/containers/python-podman 16 | python-requires = >=3.5 17 | classifier = 18 | Development Status :: 3 - Alpha 19 | Intended Audience :: Developers 20 | License :: OSI Approved :: Apache Software License 21 | Programming Language :: Python :: 3.5 22 | Programming Language :: Python :: 3.6 23 | Programming Language :: Python :: 3.7 24 | Programming Language :: Python :: 3 :: Only 25 | Programming Language :: Python :: Implementation :: CPython 26 | Topic :: Software Development 27 | keywords = varlink, libpod, podman 28 | requires-dist = 29 | setuptools 30 | wheel 31 | 32 | [files] 33 | packages = 34 | podman 35 | 36 | [extras] 37 | devel= 38 | fixtures 39 | pbr 40 | tox 41 | bandit 42 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup 4 | 5 | setup( 6 | setup_requires=['pbr'], 7 | pbr=True, 8 | ) 9 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | # Put your test dependencies here. 2 | # The order of packages is significant, 3 | # because pip processes them in the order 4 | # of appearance. Changing the order has 5 | # an impact on the overall integration 6 | # process, which may cause wedges in the CI later. 7 | flake8 8 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/containers/python-podman/838727e32e3582b03d3cb9ea314096c39d2da6da/test/__init__.py -------------------------------------------------------------------------------- /test/podman_testcase.py: -------------------------------------------------------------------------------- 1 | """Base for podman tests.""" 2 | import sys 3 | import contextlib 4 | import functools 5 | import itertools 6 | import logging 7 | import os 8 | import subprocess 9 | import time 10 | import unittest 11 | from varlink import VarlinkError 12 | 13 | 14 | MethodNotImplemented = "org.varlink.service.MethodNotImplemented" # pylint: disable=invalid-name 15 | 16 | 17 | class LogTestCase(type): 18 | """LogTestCase wires in a logger handler to handle logging during tests.""" 19 | 20 | def __new__(cls, name, bases, dct): 21 | def wrapped_setup(self): 22 | self.hdlr = logging.StreamHandler(sys.stdout) 23 | self.logger.addHandler(self.hdlr) 24 | 25 | dct["setUp"] = wrapped_setup 26 | 27 | teardown = dct["tearDown"] if "tearDown" in dct else lambda self: None 28 | 29 | def wrapped_teardown(self): 30 | teardown(self) 31 | self.logger.removeHandler(self.hdlr) 32 | 33 | dct["tearDown"] = wrapped_teardown 34 | 35 | return type.__new__(cls, name, bases, dct) 36 | 37 | 38 | class PodmanTestCase(unittest.TestCase): 39 | """Hide the sausage making of initializing storage.""" 40 | 41 | __metaclass__ = LogTestCase 42 | logger = logging.getLogger("unittestLogger") 43 | level = os.environ.get("PODMAN_LOG_LEVEL") 44 | if level is not None: 45 | logger.setLevel(logging.getLevelName(level.upper())) 46 | 47 | @classmethod 48 | def setUpClass(cls): 49 | """Fixture to setup podman test case.""" 50 | if hasattr(PodmanTestCase, "alpine_process"): 51 | PodmanTestCase.tearDownClass() 52 | 53 | def run_cmd(*args): 54 | cmd = list(itertools.chain(*args)) 55 | try: 56 | pid = subprocess.Popen( 57 | cmd, 58 | close_fds=True, 59 | stdout=subprocess.PIPE, 60 | stderr=subprocess.PIPE, 61 | ) 62 | out, _ = pid.communicate() 63 | except OSError as e: 64 | print("{}: {}({})".format(cmd, e.strerror, e.errno)) 65 | except ValueError as e: 66 | print("{}: {}".format(cmd, e)) 67 | raise 68 | else: 69 | return out.strip() 70 | 71 | tmpdir = os.environ.get("TMPDIR", "/tmp") 72 | podman_args = [ 73 | "--storage-driver=vfs", 74 | "--cgroup-manager=cgroupfs", 75 | "--root={}/crio".format(tmpdir), 76 | "--runroot={}/crio-run".format(tmpdir), 77 | "--cni-config-dir={}/cni/net.d".format(tmpdir), 78 | ] 79 | 80 | run_podman = functools.partial(run_cmd, ["podman"], podman_args) 81 | 82 | id_ = run_podman(["pull", "alpine"]) 83 | setattr(PodmanTestCase, "alpine_id", id_) 84 | 85 | run_podman(["pull", "busybox"]) 86 | run_podman(["images"]) 87 | 88 | run_cmd(["rm", "-f", "{}/alpine_gold.tar".format(tmpdir)]) 89 | run_podman( 90 | ["save", "--output", "{}/alpine_gold.tar".format(tmpdir), "alpine"] 91 | ) 92 | 93 | PodmanTestCase.alpine_log = open( 94 | os.path.join("/tmp/", "alpine.log"), "w" 95 | ) 96 | 97 | cmd = ["podman"] 98 | cmd.extend(podman_args) 99 | # cmd.extend(['run', '-d', 'alpine', 'sleep', '500']) 100 | cmd.extend(["run", "-dt", "alpine", "/bin/sh"]) 101 | PodmanTestCase.alpine_process = subprocess.Popen( 102 | cmd, stdout=PodmanTestCase.alpine_log, stderr=subprocess.STDOUT 103 | ) 104 | 105 | PodmanTestCase.busybox_log = open( 106 | os.path.join("/tmp/", "busybox.log"), "w" 107 | ) 108 | 109 | cmd = ["podman"] 110 | cmd.extend(podman_args) 111 | cmd.extend(["create", "busybox"]) 112 | PodmanTestCase.busybox_process = subprocess.Popen( 113 | cmd, stdout=PodmanTestCase.busybox_log, stderr=subprocess.STDOUT 114 | ) 115 | # give podman time to start ctnr 116 | time.sleep(2) 117 | 118 | # Close our handle of file 119 | PodmanTestCase.alpine_log.close() 120 | PodmanTestCase.busybox_log.close() 121 | 122 | @classmethod 123 | def tearDownClass(cls): 124 | """Fixture to clean up after podman unittest.""" 125 | try: 126 | PodmanTestCase.alpine_process.kill() 127 | assert PodmanTestCase.alpine_process.wait(500) == 0 128 | delattr(PodmanTestCase, "alpine_process") 129 | 130 | PodmanTestCase.busybox_process.kill() 131 | assert PodmanTestCase.busybox_process.wait(500) == 0 132 | except Exception as e: 133 | print("Exception: {}".format(e)) 134 | raise 135 | 136 | @contextlib.contextmanager 137 | def assertRaisesNotImplemented(self): # pylint: disable=invalid-name 138 | """Sugar for unimplemented varlink methods.""" 139 | with self.assertRaisesRegex(VarlinkError, MethodNotImplemented): 140 | yield 141 | -------------------------------------------------------------------------------- /test/retry_decorator.py: -------------------------------------------------------------------------------- 1 | """Decorator to retry failed method.""" 2 | import functools 3 | import time 4 | 5 | 6 | def retry(ExceptionToCheck, tries=4, delay=3, backoff=2, print_=None): 7 | """Retry calling the decorated function using an exponential backoff. 8 | 9 | Specialized for our unittests 10 | from: 11 | http://www.saltycrane.com/blog/2009/11/trying-out-retry-decorator-python/ 12 | original from: http://wiki.python.org/moin/PythonDecoratorLibrary#Retry 13 | 14 | :param ExceptionToCheck: the exception to check. may be a tuple of 15 | exceptions to check 16 | :type ExceptionToCheck: Exception or tuple 17 | :param tries: number of times to try (not retry) before giving up 18 | :type tries: int 19 | :param delay: initial delay between retries in seconds 20 | :type delay: int 21 | :param backoff: backoff multiplier e.g. value of 2 will double the delay 22 | each retry 23 | :type backoff: int 24 | """ 25 | def deco_retry(f): 26 | @functools.wraps(f) 27 | def f_retry(*args, **kwargs): 28 | mtries, mdelay = tries, delay 29 | while mtries > 1: 30 | try: 31 | return f(*args, **kwargs) 32 | except ExceptionToCheck as e: 33 | if print_: 34 | print_('{}, Retrying in {} seconds...'.format( 35 | str(e), mdelay)) 36 | time.sleep(mdelay) 37 | mtries -= 1 38 | mdelay *= backoff 39 | return f(*args, **kwargs) 40 | 41 | return f_retry # true decorator 42 | 43 | return deco_retry 44 | -------------------------------------------------------------------------------- /test/test_client.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import unittest 4 | from unittest.mock import patch 5 | 6 | from podman.client import BaseClient, Client, LocalClient 7 | 8 | 9 | class TestClient(unittest.TestCase): 10 | def setUp(self): 11 | pass 12 | 13 | @patch('podman.libs.system.System.ping', return_value=True) 14 | def test_local(self, mock_ping): 15 | p = Client( 16 | uri='unix:/run/podman', 17 | interface='io.podman', 18 | ) 19 | 20 | self.assertIsInstance(p._client, LocalClient) 21 | self.assertIsInstance(p._client, BaseClient) 22 | 23 | mock_ping.assert_called_once_with() 24 | 25 | @patch('podman.libs.system.System.ping', return_value=True) 26 | def test_remote(self, mock_ping): 27 | p = Client( 28 | uri='unix:/run/podman', 29 | interface='io.podman', 30 | remote_uri='ssh://user@hostname/run/podman/podman', 31 | identity_file='~/.ssh/id_rsa') 32 | 33 | self.assertIsInstance(p._client, BaseClient) 34 | mock_ping.assert_called_once_with() 35 | -------------------------------------------------------------------------------- /test/test_containers.py: -------------------------------------------------------------------------------- 1 | import os 2 | import signal 3 | import unittest 4 | from pathlib import Path 5 | 6 | import podman 7 | from test.podman_testcase import PodmanTestCase 8 | from test.retry_decorator import retry 9 | 10 | 11 | class TestContainers(PodmanTestCase): 12 | @classmethod 13 | def setUpClass(cls): 14 | super().setUpClass() 15 | 16 | @classmethod 17 | def tearDownClass(cls): 18 | super().tearDownClass() 19 | 20 | def setUp(self): 21 | self.tmpdir = os.environ['TMPDIR'] 22 | self.host = os.environ['PODMAN_HOST'] 23 | 24 | self.pclient = podman.Client(self.host) 25 | self.loadCache() 26 | 27 | def tearDown(self): 28 | pass 29 | 30 | def loadCache(self): 31 | self.containers = list(self.pclient.containers.list()) 32 | 33 | self.alpine_ctnr = next( 34 | iter([c for c in self.containers if 'alpine' in c['image']] or []), 35 | None) 36 | 37 | if self.alpine_ctnr and self.alpine_ctnr.status != 'running': 38 | self.alpine_ctnr.start() 39 | 40 | def test_list(self): 41 | self.assertGreaterEqual(len(self.containers), 2) 42 | self.assertIsNotNone(self.alpine_ctnr) 43 | self.assertIn('alpine', self.alpine_ctnr.image) 44 | 45 | def test_delete_stopped(self): 46 | before = len(self.containers) 47 | 48 | self.alpine_ctnr.stop() 49 | target = self.alpine_ctnr.id 50 | actual = self.pclient.containers.delete_stopped() 51 | self.assertIn(target, actual) 52 | 53 | self.loadCache() 54 | after = len(self.containers) 55 | 56 | self.assertLess(after, before) 57 | TestContainers.setUpClass() 58 | 59 | def test_get(self): 60 | actual = self.pclient.containers.get(self.alpine_ctnr.id) 61 | for k in ['id', 'status', 'ports']: 62 | self.assertEqual(actual[k], self.alpine_ctnr[k]) 63 | 64 | with self.assertRaises(podman.ContainerNotFound): 65 | self.pclient.containers.get("bozo") 66 | 67 | @unittest.skip('Update to match new attach API') 68 | def test_attach(self): 69 | # StringIO does not support fileno() so we had to go old school 70 | _input = os.path.join(self.tmpdir, 'test_attach.stdin') 71 | output = os.path.join(self.tmpdir, 'test_attach.stdout') 72 | 73 | with open(_input, 'w+') as mock_in, open(output, 'w+') as mock_out: 74 | # double quote is indeed in the expected place 75 | mock_in.write('echo H"ello, World"; exit\n') 76 | mock_in.seek(0, 0) 77 | 78 | ctnr = self.pclient.images.get(self.alpine_ctnr.image).container( 79 | detach=True, tty=True) 80 | ctnr.attach(stdin=mock_in.fileno(), stdout=mock_out.fileno()) 81 | ctnr.start() 82 | 83 | mock_out.flush() 84 | mock_out.seek(0, 0) 85 | output = mock_out.read() 86 | self.assertIn('Hello', output) 87 | 88 | ctnr.remove(force=True) 89 | 90 | def test_processes(self): 91 | actual = list(self.alpine_ctnr.processes()) 92 | self.assertGreaterEqual(len(actual), 2) 93 | 94 | def test_start_stop_wait(self): 95 | ctnr = self.alpine_ctnr.stop() 96 | self.assertFalse(ctnr['running']) 97 | 98 | ctnr.start() 99 | self.assertTrue(ctnr.running) 100 | 101 | ctnr.stop() 102 | self.assertFalse(ctnr['containerrunning']) 103 | 104 | actual = ctnr.wait() 105 | self.assertGreaterEqual(actual, 0) 106 | 107 | def test_changes(self): 108 | actual = self.alpine_ctnr.changes() 109 | 110 | self.assertListEqual( 111 | sorted(['changed', 'added', 'deleted']), sorted( 112 | list(actual.keys()))) 113 | 114 | # TODO: brittle, depends on knowing history of ctnr 115 | self.assertGreaterEqual(len(actual['changed']), 0) 116 | self.assertGreaterEqual(len(actual['added']), 0) 117 | self.assertEqual(len(actual['deleted']), 0) 118 | 119 | def test_kill(self): 120 | self.assertTrue(self.alpine_ctnr.running) 121 | ctnr = self.alpine_ctnr.kill(signal.SIGKILL) 122 | self.assertFalse(ctnr.running) 123 | 124 | def test_inspect(self): 125 | actual = self.alpine_ctnr.inspect() 126 | self.assertEqual(actual.id, self.alpine_ctnr.id) 127 | # TODO: Datetime values from inspect missing offset in CI instance 128 | # self.assertEqual( 129 | # datetime_parse(actual.created), 130 | # datetime_parse(self.alpine_ctnr.createdat)) 131 | 132 | def test_export(self): 133 | target = os.path.join(self.tmpdir, 'alpine_export_ctnr.tar') 134 | 135 | actual = self.alpine_ctnr.export(target) 136 | self.assertEqual(actual, target) 137 | self.assertTrue(os.path.isfile(target)) 138 | self.assertGreater(os.path.getsize(target), 0) 139 | 140 | def test_commit(self): 141 | # TODO: Test for STOPSIGNAL when supported by OCI 142 | # TODO: Test for message when supported by OCI 143 | details = self.pclient.images.get(self.alpine_ctnr.image).inspect() 144 | changes = ['ENV=' + i for i in details.config['env']] 145 | changes.append('CMD=/usr/bin/zsh') 146 | changes.append('ENTRYPOINT=/bin/sh date') 147 | changes.append('ENV=TEST=test_containers.TestContainers.test_commit') 148 | changes.append('EXPOSE=80') 149 | changes.append('EXPOSE=8888') 150 | changes.append('LABEL=unittest=test_commit') 151 | changes.append('USER=bozo:circus') 152 | changes.append('VOLUME=/data') 153 | changes.append('WORKDIR=/data/application') 154 | 155 | id = self.alpine_ctnr.commit( 156 | 'alpine3', author='Bozo the clown', change=changes, pause=True) 157 | img = self.pclient.images.get(id) 158 | self.assertIsNotNone(img) 159 | 160 | details = img.inspect() 161 | self.assertEqual(details.author, 'Bozo the clown') 162 | self.assertListEqual(['/bin/sh', '-c', '/usr/bin/zsh'], 163 | details.config['cmd']) 164 | self.assertListEqual(['/bin/sh', '-c', '/bin/sh date'], 165 | details.config['entrypoint']) 166 | self.assertIn('TEST=test_containers.TestContainers.test_commit', 167 | details.config['env']) 168 | self.assertTrue( 169 | [e for e in details.config['env'] if 'PATH=' in e]) 170 | self.assertDictEqual({ 171 | '80': {}, 172 | '8888': {}, 173 | }, details.config['exposedports']) 174 | self.assertDictEqual({'unittest': 'test_commit'}, details.labels) 175 | self.assertEqual('bozo:circus', details.config['user']) 176 | self.assertEqual({'/data': {}}, details.config['volumes']) 177 | self.assertEqual('/data/application', 178 | details.config['workingdir']) 179 | 180 | def test_remove(self): 181 | before = len(self.containers) 182 | 183 | with self.assertRaises(podman.InvalidState): 184 | self.alpine_ctnr.remove() 185 | 186 | self.assertEqual( 187 | self.alpine_ctnr.id, self.alpine_ctnr.remove(force=True)) 188 | self.loadCache() 189 | after = len(self.containers) 190 | 191 | self.assertLess(after, before) 192 | TestContainers.setUpClass() 193 | 194 | def test_restart(self): 195 | self.assertTrue(self.alpine_ctnr.running) 196 | _ = self.alpine_ctnr.runningfor 197 | 198 | ctnr = self.alpine_ctnr.restart() 199 | self.assertTrue(ctnr.running) 200 | 201 | _ = self.alpine_ctnr.runningfor 202 | 203 | # TODO: restore check when restart zeros counter 204 | # self.assertLess(after, before) 205 | 206 | def test_pause_unpause(self): 207 | self.assertTrue(self.alpine_ctnr.running) 208 | 209 | ctnr = self.alpine_ctnr.pause() 210 | self.assertEqual(ctnr.status, 'paused') 211 | 212 | ctnr = self.alpine_ctnr.unpause() 213 | self.assertTrue(ctnr.running) 214 | self.assertTrue(ctnr.status, 'running') 215 | 216 | # creating cgoups can be flakey 217 | @retry(podman.libs.errors.ErrorOccurred, tries=4, delay=2, print_=print) 218 | def test_stats(self): 219 | try: 220 | self.assertTrue(self.alpine_ctnr.running) 221 | 222 | actual = self.alpine_ctnr.stats() 223 | self.assertEqual(self.alpine_ctnr.id, actual.id) 224 | self.assertEqual(self.alpine_ctnr.names, actual.name) 225 | except Exception: 226 | info = Path('/proc/self/mountinfo') 227 | with info.open() as fd: 228 | print('{} {}'.format(self.alpine_ctnr.id, info)) 229 | print(fd.read()) 230 | 231 | def test_logs(self): 232 | self.assertTrue(self.alpine_ctnr.running) 233 | actual = list(self.alpine_ctnr.logs()) 234 | self.assertIsNotNone(actual) 235 | 236 | 237 | if __name__ == '__main__': 238 | unittest.main() 239 | -------------------------------------------------------------------------------- /test/test_images.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import os 3 | import unittest 4 | from collections import Counter 5 | from datetime import datetime, timezone 6 | from test.podman_testcase import PodmanTestCase 7 | 8 | import podman 9 | from podman import FoldedString 10 | 11 | 12 | class TestImages(PodmanTestCase): 13 | @classmethod 14 | def setUpClass(cls): 15 | super().setUpClass() 16 | 17 | @classmethod 18 | def tearDownClass(cls): 19 | super().tearDownClass() 20 | 21 | def setUp(self): 22 | self.tmpdir = os.environ["TMPDIR"] 23 | self.host = os.environ["PODMAN_HOST"] 24 | 25 | self.pclient = podman.Client(self.host) 26 | self.images = self.loadCache() 27 | 28 | def tearDown(self): 29 | pass 30 | 31 | def loadCache(self): 32 | with podman.Client(self.host) as pclient: 33 | self.images = list(pclient.images.list()) 34 | 35 | self.alpine_image = next( 36 | iter( 37 | [ 38 | i 39 | for i in self.images 40 | if "docker.io/library/alpine:latest" in i["repoTags"] 41 | ] 42 | or [] 43 | ), 44 | None, 45 | ) 46 | 47 | return self.images 48 | 49 | def test_list(self): 50 | actual = self.loadCache() 51 | self.assertGreaterEqual(len(actual), 2) 52 | self.assertIsNotNone(self.alpine_image) 53 | 54 | def test_build(self): 55 | path = os.path.join(self.tmpdir, "ctnr", "Dockerfile") 56 | os.makedirs(os.path.dirname(path), exist_ok=True) 57 | with open(path, "w") as stream: 58 | stream.write("FROM alpine") 59 | 60 | builder = self.pclient.images.build( 61 | containerfiles=[path], tags=["alpine-unittest"] 62 | ) 63 | self.assertIsNotNone(builder) 64 | 65 | *_, last_element = builder() # drain the builder generator 66 | # Each element from builder is a tuple (line, image) 67 | img = last_element[1] 68 | 69 | self.assertIsNotNone(img) 70 | self.assertIn("localhost/alpine-unittest:latest", img.repoTags) 71 | self.assertLess( 72 | podman.datetime_parse(img.created), datetime.now(timezone.utc) 73 | ) 74 | 75 | def test_build_failures(self): 76 | with self.assertRaises(ValueError): 77 | self.pclient.images.build() 78 | 79 | with self.assertRaises(ValueError): 80 | self.pclient.images.build(tags=["busted"]) 81 | 82 | def test_create(self): 83 | img_details = self.alpine_image.inspect() 84 | 85 | actual = self.alpine_image.container(command=["sleep", "1h"]) 86 | self.assertIsNotNone(actual) 87 | self.assertEqual(FoldedString(actual.status), "configured") 88 | 89 | ctnr = actual.start() 90 | self.assertEqual(FoldedString(ctnr.status), "running") 91 | 92 | ctnr_details = ctnr.inspect() 93 | for e in img_details.config["env"]: 94 | self.assertIn(e, ctnr_details.config["env"]) 95 | 96 | def test_export(self): 97 | path = os.path.join(self.tmpdir, "alpine_export.tar") 98 | target = "oci-archive:{}:latest".format(path) 99 | 100 | actual = self.alpine_image.export(target, False) 101 | self.assertTrue(actual) 102 | self.assertTrue(os.path.isfile(path)) 103 | 104 | def test_get(self): 105 | actual = self.pclient.images.get(self.alpine_image.id) 106 | self.assertEqual(actual.id, self.alpine_image.id) 107 | 108 | def test_history(self): 109 | records = [] 110 | bucket = Counter() 111 | for record in self.alpine_image.history(): 112 | self.assertIn(record.id, (self.alpine_image.id, "")) 113 | bucket[record.id] += 1 114 | records.append(record) 115 | 116 | self.assertGreater(bucket[self.alpine_image.id], 0) 117 | self.assertEqual(sum(bucket.values()), len(records)) 118 | 119 | def test_inspect(self): 120 | actual = self.alpine_image.inspect() 121 | self.assertEqual(actual.id, self.alpine_image.id) 122 | 123 | def test_push(self): 124 | path = os.path.join(self.tmpdir, "alpine_push") 125 | target = "dir:" + path 126 | self.alpine_image.push(target) 127 | 128 | self.assertTrue(os.path.isfile(os.path.join(path, "manifest.json"))) 129 | self.assertTrue(os.path.isfile(os.path.join(path, "version"))) 130 | 131 | def test_tag(self): 132 | self.assertEqual( 133 | self.alpine_image.id, self.alpine_image.tag("alpine:fubar") 134 | ) 135 | self.loadCache() 136 | self.assertIn("localhost/alpine:fubar", self.alpine_image.repoTags) 137 | 138 | def test_remove(self): 139 | before = self.loadCache() 140 | 141 | # assertRaises doesn't follow the import name :( 142 | with self.assertRaises(podman.ErrorOccurred): 143 | self.alpine_image.remove() 144 | 145 | # Work around for issue in service 146 | for ctnr in self.pclient.containers.list(): 147 | if ctnr.running: 148 | ctnr.stop() 149 | 150 | actual = self.alpine_image.remove(force=True) 151 | self.assertEqual(self.alpine_image.id, actual) 152 | after = self.loadCache() 153 | 154 | self.assertLess(len(after), len(before)) 155 | TestImages.setUpClass() 156 | self.loadCache() 157 | 158 | def test_import_delete_unused(self): 159 | before = self.loadCache() 160 | # create unused image, so we have something to delete 161 | source = os.path.join(self.tmpdir, "alpine_gold.tar") 162 | new_img = self.pclient.images.import_image( 163 | source, "alpine2:latest", "unittest.test_import", 164 | ) 165 | after = self.loadCache() 166 | 167 | self.assertEqual(len(before) + 1, len(after)) 168 | self.assertIsNotNone( 169 | next(iter([i for i in after if new_img in i["id"]] or []), None) 170 | ) 171 | 172 | actual = self.pclient.images.delete_unused() 173 | self.assertIn(new_img, actual) 174 | 175 | after = self.loadCache() 176 | self.assertGreaterEqual(len(before), len(after)) 177 | 178 | TestImages.setUpClass() 179 | self.loadCache() 180 | 181 | def test_pull(self): 182 | before = self.loadCache() 183 | actual = self.pclient.images.pull("prom/busybox:latest") 184 | after = self.loadCache() 185 | 186 | self.assertEqual(len(before) + 1, len(after)) 187 | self.assertIsNotNone( 188 | next(iter([i for i in after if actual in i["id"]] or []), None) 189 | ) 190 | 191 | def test_search(self): 192 | actual = self.pclient.images.search("alpine", 25) 193 | names, length = itertools.tee(actual) 194 | 195 | for img in names: 196 | self.assertIn("alpine", img.name) 197 | self.assertTrue(0 < len(list(length)) <= 25) 198 | 199 | 200 | if __name__ == "__main__": 201 | unittest.main() 202 | -------------------------------------------------------------------------------- /test/test_libs.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import unittest 3 | 4 | import podman 5 | 6 | 7 | class TestLibs(unittest.TestCase): 8 | def setUp(self): 9 | pass 10 | 11 | def tearDown(self): 12 | pass 13 | 14 | def test_parse(self): 15 | expected = datetime.datetime.strptime( 16 | '2018-05-08T14:12:53.797795-0700', '%Y-%m-%dT%H:%M:%S.%f%z') 17 | for v in [ 18 | '2018-05-08T14:12:53.797795191-07:00', 19 | '2018-05-08T14:12:53.797795-07:00', 20 | '2018-05-08T14:12:53.797795-0700', 21 | '2018-05-08 14:12:53.797795191 -0700 MST', 22 | ]: 23 | actual = podman.datetime_parse(v) 24 | self.assertEqual(actual, expected) 25 | 26 | expected = datetime.datetime.strptime( 27 | '2018-05-08T14:12:53.797795-0000', '%Y-%m-%dT%H:%M:%S.%f%z') 28 | for v in [ 29 | '2018-05-08T14:12:53.797795191Z', 30 | '2018-05-08T14:12:53.797795191z', 31 | ]: 32 | actual = podman.datetime_parse(v) 33 | self.assertEqual(actual, expected) 34 | 35 | actual = podman.datetime_parse(datetime.datetime.now().isoformat()) 36 | self.assertIsNotNone(actual) 37 | 38 | def test_parse_fail(self): 39 | for v in [ 40 | 'There is no time here.', 41 | ]: 42 | with self.assertRaises(ValueError): 43 | podman.datetime_parse(v) 44 | 45 | def test_format(self): 46 | expected = '2018-05-08T18:24:52.753227-07:00' 47 | dt = podman.datetime_parse(expected) 48 | actual = podman.datetime_format(dt) 49 | self.assertEqual(actual, expected) 50 | 51 | 52 | if __name__ == '__main__': 53 | unittest.main() 54 | -------------------------------------------------------------------------------- /test/test_pods_ctnrs.py: -------------------------------------------------------------------------------- 1 | import os 2 | from test.podman_testcase import PodmanTestCase 3 | 4 | import podman 5 | from podman import FoldedString 6 | 7 | pod = None 8 | 9 | 10 | class TestPodsCtnrs(PodmanTestCase): 11 | @classmethod 12 | def setUpClass(cls): 13 | # Populate storage 14 | super().setUpClass() 15 | 16 | @classmethod 17 | def tearDownClass(cls): 18 | super().tearDownClass() 19 | 20 | def setUp(self): 21 | self.tmpdir = os.environ['TMPDIR'] 22 | self.host = os.environ['PODMAN_HOST'] 23 | 24 | self.pclient = podman.Client(self.host) 25 | 26 | def test_010_populate(self): 27 | global pod 28 | 29 | pod = self.pclient.pods.create('pod1') 30 | self.assertEqual('pod1', pod.name) 31 | 32 | img = self.pclient.images.get('docker.io/library/alpine:latest') 33 | ctnr = img.container(pod=pod.id) 34 | 35 | pod.refresh() 36 | self.assertEqual('1', pod.numberofcontainers) 37 | self.assertEqual(ctnr.id, pod.containersinfo[0]['id']) 38 | 39 | def test_015_one_shot(self): 40 | global pod 41 | 42 | details = pod.inspect() 43 | state = FoldedString(details.containers[0]['state']) 44 | self.assertEqual(state, 'configured') 45 | 46 | pod = pod.start() 47 | status = FoldedString(pod.containersinfo[0]['status']) 48 | # Race on whether container is still running or finished 49 | self.assertIn(status, ('stopped', 'exited', 'running')) 50 | 51 | pod = pod.restart() 52 | status = FoldedString(pod.containersinfo[0]['status']) 53 | self.assertIn(status, ('stopped', 'exited', 'running')) 54 | 55 | # Pod kill is broken, so use stop for now 56 | killed = pod.stop() 57 | self.assertEqual(pod, killed) 58 | 59 | def test_999_remove(self): 60 | global pod 61 | 62 | ident = pod.remove(force=True) 63 | self.assertEqual(ident, pod.id) 64 | 65 | with self.assertRaises(StopIteration): 66 | next(self.pclient.pods.list()) 67 | -------------------------------------------------------------------------------- /test/test_pods_no_ctnrs.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | import podman 5 | 6 | ident = None 7 | pod = None 8 | 9 | 10 | class TestPodsNoCtnrs(unittest.TestCase): 11 | def setUp(self): 12 | self.tmpdir = os.environ['TMPDIR'] 13 | self.host = os.environ['PODMAN_HOST'] 14 | 15 | self.pclient = podman.Client(self.host) 16 | 17 | def test_010_create(self): 18 | global ident 19 | 20 | actual = self.pclient.pods.create('pod0') 21 | self.assertIsNotNone(actual) 22 | ident = actual.id 23 | 24 | def test_015_list(self): 25 | global ident, pod 26 | 27 | actual = next(self.pclient.pods.list()) 28 | self.assertEqual('pod0', actual.name) 29 | self.assertEqual(ident, actual.id) 30 | self.assertEqual('Created', actual.status) 31 | self.assertEqual('0', actual.numberofcontainers) 32 | self.assertFalse(actual.containersinfo) 33 | pod = actual 34 | 35 | def test_020_get(self): 36 | global ident, pod 37 | 38 | actual = self.pclient.pods.get(pod.id) 39 | self.assertEqual('pod0', actual.name) 40 | self.assertEqual(ident, actual.id) 41 | self.assertEqual('Created', actual.status) 42 | self.assertEqual('0', actual.numberofcontainers) 43 | self.assertFalse(actual.containersinfo) 44 | 45 | def test_025_inspect(self): 46 | global ident, pod 47 | 48 | details = pod.inspect() 49 | self.assertEqual(ident, details.id) 50 | self.assertEqual('pod0', details.config['name']) 51 | self.assertIsNone(details.containers) 52 | 53 | def test_030_ident_no_ctnrs(self): 54 | global ident, pod 55 | 56 | actual = pod.kill() 57 | self.assertEqual(pod, actual) 58 | 59 | actual = pod.pause() 60 | self.assertEqual(pod, actual) 61 | 62 | actual = pod.unpause() 63 | self.assertEqual(pod, actual) 64 | 65 | actual = pod.stop() 66 | self.assertEqual(pod, actual) 67 | 68 | def test_045_raises_no_ctnrs(self): 69 | global ident, pod 70 | 71 | with self.assertRaises(podman.NoContainersInPod): 72 | pod.start() 73 | 74 | with self.assertRaises(podman.NoContainersInPod): 75 | pod.restart() 76 | 77 | with self.assertRaises(podman.NoContainerRunning): 78 | next(pod.stats()) 79 | 80 | with self.assertRaises(podman.ErrorOccurred): 81 | pod.top() 82 | 83 | def test_999_remove(self): 84 | global ident, pod 85 | 86 | actual = pod.remove() 87 | self.assertEqual(ident, actual) 88 | 89 | with self.assertRaises(StopIteration): 90 | next(self.pclient.pods.list()) 91 | -------------------------------------------------------------------------------- /test/test_runner.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # podman needs to play some games with resources 4 | if [[ $(id -u) != 0 ]]; then 5 | echo >&2 $0 must be run as root. 6 | exit 2 7 | fi 8 | 9 | function usage() { 10 | echo 1>&2 $0 '[-v] [-h] [test.|test..]' 11 | } 12 | 13 | while getopts "vh" arg; do 14 | case $arg in 15 | v) 16 | VERBOSE='-v' 17 | export PODMAN_LOG_LEVEL=debug 18 | ;; 19 | h) 20 | usage 21 | exit 0 22 | ;; 23 | \?) 24 | usage 25 | exit 2 26 | ;; 27 | esac 28 | done 29 | shift $((OPTIND - 1)) 30 | 31 | function cleanup() { 32 | set +xeuo pipefail 33 | # aggressive cleanup as tests may crash leaving crap around 34 | umount '^(shm|nsfs)' 35 | umount '\/run\/netns' 36 | if [[ $RETURNCODE -eq 0 ]]; then 37 | rm -r "$1" 38 | fi 39 | } 40 | 41 | # Create temporary directory for storage 42 | export TMPDIR=$(mktemp -d /tmp/podman.XXXXXXXXXX) 43 | trap "cleanup $TMPDIR" EXIT 44 | 45 | function umount() { 46 | set +xeuo pipefail 47 | # xargs -r always ran once, so write any mount points to file first 48 | mount | awk "/$1/"' { print $3 }' >${TMPDIR}/mounts 49 | if [[ -s ${TMPDIR}/mounts ]]; then 50 | xargs <${TMPDIR}/mounts -t umount 51 | fi 52 | } 53 | 54 | function showlog() { 55 | [[ -s $1 ]] && cat <<-EOT 56 | $1 ===== 57 | $(cat "$1") 58 | 59 | EOT 60 | } 61 | 62 | # Need locations to store stuff 63 | mkdir -p ${TMPDIR}/{podman,crio,crio-run,cni/net.d,ctnr,tunnel} 64 | 65 | # Cannot be done in python unittest fixtures. EnvVar not picked up. 66 | export REGISTRIES_CONFIG_PATH=${TMPDIR}/registry.conf 67 | cat >$REGISTRIES_CONFIG_PATH <<-EOT 68 | [registries.search] 69 | registries = ['docker.io'] 70 | [registries.insecure] 71 | registries = [] 72 | [registries.block] 73 | registries = [] 74 | EOT 75 | 76 | export CNI_CONFIG_PATH=${TMPDIR}/cni/net.d 77 | cat >$CNI_CONFIG_PATH/87-podman-bridge.conflist <<-EOT 78 | { 79 | "cniVersion": "0.3.0", 80 | "name": "podman", 81 | "plugins": [{ 82 | "type": "bridge", 83 | "bridge": "cni0", 84 | "isGateway": true, 85 | "ipMasq": true, 86 | "ipam": { 87 | "type": "host-local", 88 | "subnet": "10.88.0.0/16", 89 | "routes": [{ 90 | "dst": "0.0.0.0/0" 91 | }] 92 | } 93 | }, 94 | { 95 | "type": "portmap", 96 | "capabilities": { 97 | "portMappings": true 98 | } 99 | } 100 | ] 101 | } 102 | EOT 103 | 104 | export PODMAN_HOST="unix:${TMPDIR}/podman/io.podman" 105 | PODMAN_ARGS="--storage-driver=vfs \ 106 | --root=${TMPDIR}/crio \ 107 | --runroot=${TMPDIR}/crio-run \ 108 | --cni-config-dir=$CNI_CONFIG_PATH \ 109 | --cgroup-manager=cgroupfs \ 110 | " 111 | if [[ -n $VERBOSE ]]; then 112 | PODMAN_ARGS="$PODMAN_ARGS --log-level=$PODMAN_LOG_LEVEL" 113 | fi 114 | PODMAN="podman $PODMAN_ARGS" 115 | 116 | cat <<-EOT | tee /tmp/test_podman.output 117 | $($PODMAN --version) 118 | $PODMAN varlink --timeout=0 ${PODMAN_HOST} 119 | ========================================== 120 | EOT 121 | 122 | # Run podman in background without systemd for test purposes 123 | $PODMAN varlink --timeout=0 ${PODMAN_HOST} >>/tmp/test_podman.output 2>&1 & 124 | if [[ $? != 0 ]]; then 125 | echo 1>&2 Failed to start podman 126 | showlog /tmp/test_podman.output 127 | fi 128 | 129 | if [[ -z $1 ]]; then 130 | export PYTHONPATH=. 131 | python3 -m unittest discover -s . $VERBOSE 132 | RETURNCODE=$? 133 | else 134 | export PYTHONPATH=.:./test 135 | python3 -m unittest $1 $VERBOSE 136 | RETURNCODE=$? 137 | fi 138 | 139 | pkill -9 podman 140 | pkill -9 conmon 141 | 142 | showlog /tmp/test_podman.output 143 | showlog /tmp/alpine.log 144 | showlog /tmp/busybox.log 145 | 146 | exit $RETURNCODE 147 | -------------------------------------------------------------------------------- /test/test_system.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | from urllib.parse import urlparse 4 | 5 | import podman 6 | import varlink 7 | 8 | 9 | class TestSystem(unittest.TestCase): 10 | def setUp(self): 11 | self.host = os.environ['PODMAN_HOST'] 12 | self.tmpdir = os.environ['TMPDIR'] 13 | 14 | def tearDown(self): 15 | pass 16 | 17 | def test_bad_address(self): 18 | with self.assertRaisesRegex(varlink.client.ConnectionError, 19 | "Invalid address 'bad address'"): 20 | podman.Client('bad address') 21 | 22 | def test_ping(self): 23 | with podman.Client(self.host) as pclient: 24 | self.assertTrue(pclient.system.ping()) 25 | 26 | @unittest.skip("TODO: Need to setup ssh under Travis") 27 | def test_remote_ping(self): 28 | host = urlparse(self.host) 29 | remote_uri = 'ssh://root@localhost{}'.format(host.path) 30 | 31 | local_uri = 'unix:{}/tunnel/podman.sock'.format(self.tmpdir) 32 | with podman.Client( 33 | uri=local_uri, 34 | remote_uri=remote_uri, 35 | identity_file=os.path.expanduser('~/.ssh/id_rsa'), 36 | ) as remote_client: 37 | self.assertTrue(remote_client.system.ping()) 38 | 39 | def test_versions(self): 40 | with podman.Client(self.host) as pclient: 41 | # Values change with each build so we cannot test too much 42 | self.assertListEqual( 43 | sorted([ 44 | 'built', 'client_version', 'git_commit', 'go_version', 45 | 'os_arch', 'version', 'remote_api_version' 46 | ]), sorted(list(pclient.system.versions._fields))) 47 | pclient.system.versions 48 | self.assertIsNot(podman.__version__, '0.0.0') 49 | 50 | def test_info(self): 51 | with podman.Client(self.host) as pclient: 52 | actual = pclient.system.info() 53 | # Values change too much to do exhaustive testing 54 | self.assertIsNotNone(actual.podman['go_version']) 55 | self.assertListEqual( 56 | sorted([ 57 | 'host', 'insecure_registries', 'podman', 'registries', 58 | 'store' 59 | ]), sorted(list(actual._fields))) 60 | 61 | def test_swap_total(self): 62 | with podman.Client(self.host) as pclient: 63 | actual = pclient.system.info() 64 | self.assertNotEqual(actual.host['swap_total'], 0) 65 | 66 | 67 | if __name__ == '__main__': 68 | unittest.main() 69 | -------------------------------------------------------------------------------- /test/test_tunnel.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import logging 4 | import time 5 | import unittest 6 | from unittest.mock import MagicMock, patch 7 | 8 | from podman.libs.tunnel import Context, Portal, Tunnel 9 | 10 | 11 | class TestTunnel(unittest.TestCase): 12 | def setUp(self): 13 | self.tunnel_01 = MagicMock(spec=Tunnel) 14 | self.tunnel_02 = MagicMock(spec=Tunnel) 15 | 16 | def test_portal_ops(self): 17 | portal = Portal(sweap=500) 18 | portal['unix:/01'] = self.tunnel_01 19 | portal['unix:/02'] = self.tunnel_02 20 | 21 | self.assertEqual(portal.get('unix:/01'), self.tunnel_01) 22 | self.assertEqual(portal.get('unix:/02'), self.tunnel_02) 23 | 24 | del portal['unix:/02'] 25 | with self.assertRaises(KeyError): 26 | portal['unix:/02'] 27 | self.assertEqual(len(portal), 1) 28 | 29 | def test_portal_reaping(self): 30 | portal = Portal(sweap=0.5) 31 | portal['unix:/01'] = self.tunnel_01 32 | portal['unix:/02'] = self.tunnel_02 33 | 34 | self.assertEqual(len(portal), 2) 35 | for entry in portal: 36 | self.assertIn(entry, (self.tunnel_01, self.tunnel_02)) 37 | 38 | time.sleep(1) 39 | portal.reap() 40 | self.assertEqual(len(portal), 0) 41 | 42 | def test_portal_no_reaping(self): 43 | portal = Portal(sweap=500) 44 | portal['unix:/01'] = self.tunnel_01 45 | portal['unix:/02'] = self.tunnel_02 46 | 47 | portal.reap() 48 | self.assertEqual(len(portal), 2) 49 | for entry in portal: 50 | self.assertIn(entry, (self.tunnel_01, self.tunnel_02)) 51 | 52 | @patch('subprocess.Popen') 53 | @patch('os.path.exists', return_value=True) 54 | @patch('weakref.finalize') 55 | def test_tunnel(self, mock_finalize, mock_exists, mock_Popen): 56 | mock_Popen.return_value.returncode = 0 57 | 58 | context = Context( 59 | 'unix:/01', 60 | 'io.podman', 61 | '/tmp/user/socket', 62 | '/run/podman/socket', 63 | 'user', 64 | 'hostname', 65 | None, 66 | '~/.ssh/id_rsa', 67 | ) 68 | tunnel = Tunnel(context).bore() 69 | 70 | cmd = ['ssh', '-fNT'] 71 | if logging.getLogger().getEffectiveLevel() == logging.DEBUG: 72 | cmd.append('-v') 73 | else: 74 | cmd.append('-q') 75 | 76 | cmd.extend(( 77 | '-L', 78 | '{}:{}'.format(context.local_socket, context.remote_socket), 79 | '-i', 80 | context.identity_file, 81 | '{}@{}'.format(context.username, context.hostname), 82 | )) 83 | 84 | mock_finalize.assert_called_once_with(tunnel, tunnel.close) 85 | mock_exists.assert_called_once_with(context.local_socket) 86 | mock_Popen.assert_called_once_with(cmd, close_fds=True) 87 | -------------------------------------------------------------------------------- /tests/libs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/containers/python-podman/838727e32e3582b03d3cb9ea314096c39d2da6da/tests/libs/__init__.py -------------------------------------------------------------------------------- /tests/libs/test_pod.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from varlink import mock 3 | import varlink 4 | 5 | import podman 6 | from podman.libs.pods import Pod 7 | 8 | 9 | pod_id_1 = "135d71b9495f7c3967f536edad57750bfdb569336cd107d8aabab45565ffcfb6" 10 | short_pod_id_1 = "135d71b9495f" 11 | pod_id_2 = "49a5cce72093a5ca47c6de86f10ad7bb36391e2d89cef765f807e460865a0ec6" 12 | short_pod_id_2 = "49a5cce72093" 13 | pods = { 14 | short_pod_id_1: pod_id_1, 15 | short_pod_id_2: pod_id_2, 16 | } 17 | 18 | types = """ 19 | type ListPodData ( 20 | id: string, 21 | name: string, 22 | createdat: string, 23 | cgroup: string, 24 | status: string, 25 | labels: [string]string, 26 | numberofcontainers: string, 27 | containersinfo: []ListPodContainerInfo 28 | ) 29 | 30 | type ListPodContainerInfo ( 31 | name: string, 32 | id: string, 33 | status: string 34 | ) 35 | """ 36 | 37 | 38 | class ServicePod(): 39 | 40 | def StartPod(self, name: str) -> str: 41 | """return pod""" 42 | return { 43 | "pod": "135d71b9495f7c3967f536edad57750bfdb569336cd107d8aabab45565ffcfb6" 44 | } 45 | 46 | def GetPod(self, name: str) -> str: 47 | """return pod: ListPodData""" 48 | return { 49 | "pod": { 50 | "cgroup": "machine.slice", 51 | "containersinfo": [ 52 | { 53 | "id": "1840835294cf076a822e4e12ba4152411f131bd869e7f6a4e8b16df9b0ea5c7f", 54 | "name": "1840835294cf-infra", 55 | "status": "running" 56 | }, 57 | { 58 | "id": "49a5cce72093a5ca47c6de86f10ad7bb36391e2d89cef765f807e460865a0ec6", 59 | "name": "upbeat_murdock", 60 | "status": "running" 61 | } 62 | ], 63 | "createdat": "2018-12-07 13:10:15.014139258 -0600 CST", 64 | "id": "135d71b9495f7c3967f536edad57750bfdb569336cd107d8aabab45565ffcfb6", 65 | "name": "foobar", 66 | "numberofcontainers": "2", 67 | "status": "Running" 68 | } 69 | } 70 | 71 | def GetVersion(self) -> str: 72 | """return version""" 73 | return {"version": "testing"} 74 | 75 | 76 | class TestPod(unittest.TestCase): 77 | 78 | @mock.mockedservice( 79 | fake_service=ServicePod, 80 | fake_types=types, 81 | name='io.podman', 82 | address='unix:@podmantests' 83 | ) 84 | def test_start(self): 85 | client = podman.Client(uri="unix:@podmantests") 86 | pod = Pod(client._client, short_pod_id_1, {"foo": "bar"}) 87 | self.assertEqual(pod.start()["numberofcontainers"], "2") 88 | -------------------------------------------------------------------------------- /tools/synchronize.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | function help { 5 | # Display helping message 6 | cat <] 8 | 9 | Compare python-podman implemention of the libpod varlink interface 10 | 11 | Arguments: 12 | -b, --branch The libpod branch or commit ID to compare with (default: master) 13 | -d, --debug Turn on the debug mode 14 | examples: 15 | $0 --branch=master 16 | EOF 17 | } 18 | 19 | function clean { 20 | if [ -f ${LOCAL_INTERFACE_FILE} ]; then 21 | rm ${LOCAL_INTERFACE_FILE} 22 | fi 23 | } 24 | 25 | function download { 26 | echo "Downloading the libpod's defined interface" 27 | url=${LIBPOD_RAW_URL}/${BRANCH}/${INTERFACE} 28 | echo ${url} 29 | curl ${url} -o ${LOCAL_INTERFACE_FILE} 30 | } 31 | 32 | function extract { 33 | cat ${LOCAL_INTERFACE_FILE} | \ 34 | grep "method" | \ 35 | grep -v "^#" | \ 36 | awk '{print $2}' | \ 37 | awk -F "(" '{print $1}' 38 | } 39 | 40 | function compare { 41 | for method in $(extract) 42 | do 43 | grep -ri podman -e "${method}" &>/dev/null; 44 | if [ $? != 0 ]; then 45 | echo -e "Method ${method} seems not yet implemented" 46 | fi 47 | done 48 | } 49 | 50 | BRANCH="master" 51 | IGNORE_ERRORS=false 52 | # Parse command line user inputs 53 | for i in "$@" 54 | do 55 | case $i in 56 | # The host to use 57 | -b=*|--branch=*) 58 | BRANCH="${i#*=}" 59 | shift 1 60 | ;; 61 | # Ignore error 62 | -i|--ignore) 63 | IGNORE_ERRORS=true 64 | shift 1 65 | ;; 66 | # Turn on the debug mode 67 | -d|--debug) 68 | set -x 69 | shift 1 70 | ;; 71 | # Display the helping message 72 | -h|--help) 73 | help 74 | exit 0 75 | ;; 76 | esac 77 | done 78 | 79 | EXIT_CODE=0 80 | LIBPOD_REPO=containers/libpod 81 | INTERFACE=cmd/podman/varlink/io.podman.varlink 82 | LOCAL_INTERFACE_FILE=/tmp/$(basename ${INTERFACE}) 83 | LIBPOD_RAW_URL=https://raw.githubusercontent.com/${LIBPOD_REPO} 84 | 85 | clean 86 | download 87 | result=$(compare) 88 | echo -e "${result}" 89 | if [ ! -z "${result}" ]; then 90 | echo "$(echo -e "${result}" | wc -l) error(s) found" 91 | if [ ${IGNORE_ERRORS} = true ]; then 92 | echo "You asked to ignore errors so This script will return status 0" 93 | else 94 | EXIT_CODE=1 95 | fi 96 | else 97 | echo "libpod and python-podman seems properly synchronized" 98 | fi 99 | clean 100 | 101 | echo "Comparison finished...bye!" 102 | exit ${EXIT_CODE} 103 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{35,36,37},pep8 3 | skipdist = True 4 | 5 | [testenv] 6 | usedevelop = True 7 | install_command = pip install {opts} {packages} 8 | deps = 9 | -r{toxinidir}/test-requirements.txt 10 | -r{toxinidir}/requirements.txt 11 | whitelist_externals = bash 12 | commands=python -m unittest discover tests/ 13 | 14 | [testenv:pep8] 15 | basepython = python3 16 | commands = flake8 {posargs} 17 | --------------------------------------------------------------------------------