././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1740145268.2890563 oslo_utils-8.2.0/0000775000175000017500000000000000000000000013740 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/.coveragerc0000664000175000017500000000015700000000000016064 0ustar00zuulzuul00000000000000[run] branch = True source = oslo_utils omit = oslo_utils/tests/* [report] ignore_errors = True precision = 2 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/.mailmap0000664000175000017500000000013000000000000015353 0ustar00zuulzuul00000000000000# Format is: # # ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/.pre-commit-config.yaml0000664000175000017500000000211400000000000020217 0ustar00zuulzuul00000000000000repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: - id: trailing-whitespace # Replaces or checks mixed line ending - id: mixed-line-ending args: ['--fix', 'lf'] exclude: '.*\.(svg)$' # Forbid files which have a UTF-8 byte-order marker - id: check-byte-order-marker # Checks that non-binary executables have a proper shebang - id: check-executables-have-shebangs # Check for files that contain merge conflict strings. - id: check-merge-conflict # Check for debugger imports and py37+ breakpoint() # calls in python source - id: debug-statements - id: check-yaml files: .*\.(yaml|yml)$ - repo: https://opendev.org/openstack/hacking rev: 7.0.0 hooks: - id: hacking additional_dependencies: [] - repo: https://github.com/PyCQA/bandit rev: 1.7.10 hooks: - id: bandit args: ['-x', 'tests'] - repo: https://github.com/asottile/pyupgrade rev: v3.18.0 hooks: - id: pyupgrade args: [--py3-only] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/.stestr.conf0000664000175000017500000000006300000000000016210 0ustar00zuulzuul00000000000000[DEFAULT] test_path=./oslo_utils/tests top_path=./ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/.zuul.yaml0000664000175000017500000000037000000000000015701 0ustar00zuulzuul00000000000000- project: templates: - check-requirements - lib-forward-testing-python3 - openstack-cover-jobs - openstack-python3-jobs - periodic-stable-jobs - publish-openstack-docs-pti - release-notes-jobs-python3 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145268.0 oslo_utils-8.2.0/AUTHORS0000664000175000017500000001657000000000000015021 0ustar00zuulzuul00000000000000Abhishek Chanda Abhishek Kekane Adam Harwell Adam Rozman Ade Lee Akihiro Motoki Akihiro Motoki Albert White Alessio Ababilov Alex Gaynor Alexander Gorodnev Alexey Stupnikov Alexis Lee Alfredo Moralejo Alvaro Lopez Garcia Amaury Medeiros Amrith Kumar Amrith Kumar Andreas Jaeger Andreas Jaeger Angus Lees Ann Kamyshnikova Balazs Gibizer Ben Nemec Ben Nemec Bence Romsics Bin Zhou Brant Knudson Brian Haley Brian Rosmaita BubaVV Cedric Brandily Chang Bo Guo ChangBo Guo(gcb) Christian Berendt Chuck Short Chuck Short Corey Bryant Costin Galan Cyril Roelandt Cyril Roelandt Cédric Jeanneret Dan Prince Dan Smith Dan Smith Daniel Bengtsson Dariusz Smigiel Davanum Srinivas Davanum Srinivas Davanum Srinivas David Stanek Derek Higgins Dina Belova Dirk Mueller Dmitriy Rabotyagov Dmitry Mescheryakov Dolph Mathews Doug Hellmann Doug Hellmann Dougal Matthews Drew Varner Elena Ezhova Elvira García Eoghan Glynn Eric Brown Eric Fried Eugene Kirpichov Fabian Wiesel Flaper Fesp Flavio Percoco Gary Kotton Ghanshyam Ghanshyam Mann Ghe Rivero Guang Yee Hanxi Liu Hervé Beraud Ian Wienand Ihar Hrachyshka Ildiko Ivan Kolodyazhny Jakub Libosvar James Carey Javeme Jay Faulkner Jay Pipes Jay S. Bryant Jeremy Stanley Jim Rollenhagen Joe Gordon Joe Gordon Johannes Erdfelt John Eckersberg John L. Villalovos John L. Villalovos Jordan Pittier Joshua Harlow Joshua Harlow Joshua Harlow Julien Danjou Kenneth Giusti Kevin Houdebert Lee Yarwood Lucas Alvares Gomes Mark Mielke Matthew Booth Mehdi Abaakouk Michael Wilson Moisés Guimarães de Medeiros Monty Taylor Morgan Fainberg Nguyen Hung Phuong Noorul Islam K M Oleg Bondarev Oleksii Chuprykov Oleksii Zamiatin Omar Shykhkerimov OpenStack Release Bot Paul Belanger Petr Vaněk Pierre Riteau Radomir Dopieralski Rahul Nair Rajath Agasthya Raymond Pekowski Rick Harris Rodolfo Alonso Hernandez Ronald Bradford RongzeZhu Ruby Loo Russell Bryant SandyWalsh Sean Dague Sean McGinnis Sean McGinnis Sergey Kraynev Sergey Lukjanov Sirisha Devineni Stanislav Kudriashev Stephen Finucane Stephen Finucane Steve Martinelli Steven Hardy Suraj Deshmukh Swapnil Kulkarni (coolsvap) Takashi Kajinami Takashi Kajinami Timur Sufiev Tobias Urdin Tony Breeds Tovin Seven Valeriy Ponomaryov Victor Sergeyev Victor Stinner Victor Stinner Vladyslav Drok Vu Cong Tuan Wen Zhi Yu Yaguang Tang Young Yunhong, Jiang Zane Bitter ZhiQiang Fan ZhijunWei ZhongShengping Zhongyue Luo Zhongyue Luo avnish bhagyashris caoyuan changxun damani42 dengzhaosen dharmendra ekudryashova gecong1973 haixin hnyang howardlee jacky06 karolinku kgriffs lin-hua-cheng lingyongxu lvdongbing malei melissaml paul-carlton2 pengyuesheng pran1990 ricolin songwenping sridhargaddam sunyandi wangzihao whoami-rajat yatinkarel yenai zhangboye zhangsong zhengyao1 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/CONTRIBUTING.rst0000664000175000017500000000134300000000000016402 0ustar00zuulzuul00000000000000If you would like to contribute to the development of oslo's libraries, first you must take a look to this page: https://specs.openstack.org/openstack/oslo-specs/specs/policy/contributing.html If you would like to contribute to the development of OpenStack, you must follow the steps in this page: http://docs.openstack.org/infra/manual/developers.html Once those steps have been completed, changes to OpenStack should be submitted for review via the Gerrit tool, following the workflow documented at: http://docs.openstack.org/infra/manual/developers.html#development-workflow Pull requests submitted through GitHub will be ignored. Bugs should be filed on Launchpad, not GitHub: https://bugs.launchpad.net/oslo.utils ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145268.0 oslo_utils-8.2.0/ChangeLog0000664000175000017500000007300700000000000015521 0ustar00zuulzuul00000000000000CHANGES ======= 8.2.0 ----- * Skip installation to speed up pep8 * Run pyupgrade to clean up Python 2 syntaxes 8.1.0 ----- * Adjust warning message for eventlet support deprecation * Fix wrong warning category * deprecate the eventletutils module * Remove unused private constants * Deprecate redundant exception\_to\_unicode function * Drop compatibility code for Python 3.8 * Add image checker to imageutils 8.0.0 ----- * Adjust the version in descriptions * Add a release note about the new crypt utilities * Fix get\_my\_ipv4 in absence of IPv4 * Fix get\_my\_ipv6 in absence of IPv6 * Add utility to replace crypt.mksalt * reno: Update master for unmaintained/2023.1 * Add utility to replace crypt.crypt * is\_valid\_ipv4: Enable strict check by default * Add note about requirements lower bounds 7.4.0 ----- * Vendor VersionPredicate * Declare Python 3.12 support * Allow multiple format hits if explicitly handled * Abort the stream early if no match * Fix compatibility with netaddr 1.1.0 * Drop dependency on netifaces * Use context manager for socket * Clarify GPT structure offset * Deprecate redundant constant\_time\_compare function * Deprecate redundant md5 method * Remove Python 3.8 support * Squelch irrelevant format complaints * Add qemu-img to bindep * Avoid detecting FAT VBR as an MBR * Update master for stable/2024.2 * Add LUKSv1 inspector 7.3.0 ----- * Add a function to trim scope out of ipv6 addr * Add release note for format\_inspector * Add stream-based detection * Add GPT/MBR inspector * Fix qcow2 feature flag checks (for the future) * Refactor some things for oslo * Support VMDK sparse with footer * Make VMDKInspector support non-sparse formats * Add region-complete signaling for inspectors * Reinstate VMDK safety check coverage * Modularize image safety checks * Imported Translations from Zanata * Make FileInspector an ABC * Test cover target in CI * Import format\_inspector from nova * Make imageutils a directory * Fix coverage target 7.2.0 ----- * reno: Update master for unmaintained/zed * Remove old excludes * Update master for stable/2024.1 * reno: Update master for unmaintained/xena * reno: Update master for unmaintained/wallaby * reno: Update master for unmaintained/victoria 7.1.0 ----- * add spec DSL operator * netutils: Explicitly require INET\_ATON * reno: Update master for unmaintained/yoga * pre-commit: Integrate bandit * pre-commit: Bump versions * Bump hacking 7.0.0 ----- * Update python classifier in setup.cfg * Python-3.12: do not use datetime.datetime.utcnow() * Require pytz only in Python < 3.9 6.3.0 ----- * versionutil: Remove trailing alpha/beta/rc suffix * Fix missing PyYAML dependency * Mask chapsecret * Imported Translations from Zanata * Update master for stable/2023.2 6.2.1 ----- * Imported Translations from Zanata 6.2.0 ----- * Replace deprecated assertAlmostEquals method * strutils: update string\_to\_bytes * Add netutils.get\_my\_ipv6() * Bump bandit * Fix compatibility with Python 3.8 * Remove reference to monotonic on PyPI * Revert "Moves supported python runtimes from version 3.8 to 3.10" * Moves supported python runtimes from version 3.8 to 3.10 * Update units for current SI prefixes * Implement zoneinfo support to drop dependency to pytz * Use the new openstack-python3-jobs template * Remove strict from is\_same\_callback() * Update master for stable/2023.1 6.1.0 ----- * [imageutils] Fix \_\_str\_\_ for QemuImgInfo * Imported Translations from Zanata * Add Python3 antelope unit tests * Update master for stable/zed 6.0.1 ----- * Imported Translations from Zanata * Imported Translations from Zanata 6.0.0 ----- 5.0.0 ----- * bindep: Use Python 3 devel packages * Remove deprecated helpers from oslo\_utils.timeutils * Remove oslo\_utils.fnmatch * requirements: Remove explicit pbr dependency * strutils: Defer import of pyparsing * Drop python3.6/3.7 support in testing runtime * Remove unnecessary unicode prefixes 4.13.0 ------ * fix strutils password regex * Add Python3 zed unit tests * Update master for stable/yoga 4.12.2 ------ * Fix formatting of release list 4.12.1 ------ * Fix regex used to mask password * Update python testing classifier * Use LOG.warning instead of deprecated LOG.warn 4.12.0 ------ * Add backing file format to the output 4.11.0 ------ * Add Python3 yoga unit tests * Update master for stable/xena * QemuImgInfo: Fix inconsistent value format of encrypted * setup.cfg: Replace dashes with underscores * Changed minversion in tox to 3.18.0 4.10.0 ------ * Modify UUID sentinel to support keystone-like UUIDs 4.9.2 ----- * QemuImgInfo: Skip deprecation warning when output is not passed * Drop warnings enable filter 4.9.1 ----- * Remove references to 'sys.version\_info' * Deprecate the fnmatch module * Deprecate the human format on QemuImgInfo * Ussuri+ is python3 only and update python to python3 * Dropping lower constraints testing 4.9.0 ----- * Drop lower-constraints * Move flake8 as a pre-commit local target * Add Python3 xena unit tests * Update master for stable/wallaby 4.8.0 ----- * Add a \`\`strict\`\` flag allowing users to restrict validation of IPv4 format * Address nits from six removal patch * Switch to collections.abc.\* * Use TOX\_CONSTRAINTS\_FILE * Dropping lower constraints testing * strutils: Stop masking encryption\_key\_id * Use TOX\_CONSTRAINTS\_FILE * Use py3 as the default runtime for tox * Remove all usage of six library 4.7.0 ----- * Add function to encapsule md5 for FIPS systems * Add Python3 wallaby unit tests * Update master for stable/victoria * Adding pre-commit 4.6.0 ----- * [goal] Migrate testing to ubuntu focal * Add Python3 victoria unit tests * Fix is\_same\_callback() testing for python3.8 4.5.0 ----- * Add util methods for checking json and yaml formatted file * Bump bandit version 4.4.0 ----- 4.3.0 ----- * versionutils: switch from pkg\_resources to packaging * New method in netutils: get\_mac\_addr\_by\_ipv6 4.2.2 ----- * Fix uuidsentinel to follow getattr protocol 4.2.1 ----- * Release greenthread when computing checksum * Fix pygments style 4.2.0 ----- * Stop to use the \_\_future\_\_ module * Fix hacking min version to 3.0.1 * Switch to newer openstackdocstheme and reno versions * Remove the unused coding style modules * Remove translation sections from setup.cfg * Remove monotonic usage * Align contributing doc with oslo's policy * Imported Translations from Zanata * Add release notes links to doc index * Update master for stable/ussuri 4.1.1 ----- * Update hacking for Python3 * Use unittest.mock instead of third party mock * tox: Use upper-constraints for docs jobs 4.1.0 ----- * Add test to check scientific notation on disk virtual size * tests: Convert remaining tests to mock * Flatten test case * Fix regex to correctly recognize scientific notation with QemuImgInfo * imageutils: Report format specific details when using JSON output format 4.0.1 ----- * remove outdated header * Remove universal wheel configuration * reword releasenote for py27 support dropping 4.0.0 ----- * [ussuri][goal] Drop python 2.7 support and testing * tox: Trivial cleanup * Ignore releasenote cache within git untracked files * trivial: Move setup code into setUp helper 3.42.1 ------ * Verify the sanitize keys are lowered * Fix invalid escapes in regular expression strings * Ignore the .eggs directory * Make mask\_dict\_password case insensitive and add new patterns * Bump the openstackdocstheme extension to 1.20 3.42.0 ------ * Make mask\_password case insensitive, and add new patterns * tox: Keeping going with docs * Switch to Ussuri jobs * Update the constraints url * Support "qemu-img info" virtual size in QEMU 4.1 and later * Update master for stable/train 3.41.1 ------ * Add digestmod when using hmac * Add Python 3 Train unit tests * Cap Bandit below 1.6.0 and update Sphinx requirement * Replace git.openstack.org URLs with opendev.org URLs 3.41.0 ------ * OpenDev Migration Patch * Dropping the py35 testing * Update master for stable/stein 3.40.3 ------ * add python 3.7 unit test job * Update hacking version * Mask encryption\_key\_id * eventletutils: Optimise EventletEvent.clear() 3.40.2 ------ * Avoid double-setting event 3.40.1 ------ 3.40.0 ------ 3.39.1 ------ * Avoid calling eventlet.event.Event.reset() * Use template for lower-constraints 3.39.0 ------ * Fix race condition in eventletutils Event * Don't use monotonic on Python >=3.3 * Update mailinglist from dev to discuss * Support non-dict mappings in mask\_dict\_password 3.38.0 ------ * Expose eventlet Event wrapper class * Clean up .gitignore references to personal tools 3.37.1 ------ * Fix exception raise at rpdb session 3.37.0 ------ * Fix docstring formatting nit in uuidsentinel * UUID sentinel * Remove moxstubout usage * add lib-forward-testing-python3 test job * add python 3.6 unit test job * import zuul job settings from project-config * Update reno for stable/rocky * Remove extra copy.deepcopy 3.36.4 ------ * Handle non-string keys appropriately * Switch to stestr * Add release notes link to README 3.36.3 ------ * fix tox python3 overrides * Fix exception with secretutils 3.36.2 ------ * Add private\_key to the list of sanitized keys * Remove stale pip-missing-reqs tox test * Capitalize Oslo 3.36.1 ------ * Trivial: Update pypi url to new url * set default python to python3 * eventletutils: Fix behavior discrepency when reusing Events * Fix project name in user docs * add lower-constraints job * Clean old output before new doc builds * Remove sphinx settings from setup.cfg * Add bindep.txt file to prevent fallback to generic list * Updated from global requirements 3.36.0 ------ * Add -W for document build * Imported Translations from Zanata * Update links in README * Imported Translations from Zanata * Fix breaking unit tests due to iso8601 changes * Document specs\_matcher.py functions * Clean imports in code * Update reno for stable/queens * Updated from global requirements * Replace 'assertFalse(a in b)' with 'assertNotIn(a, b)' * Updated from global requirements * Updated from global requirements 3.35.0 ------ 3.34.0 ------ * Updated from global requirements * Cleanup test-requirements * improve docstring for last\_bytes() * Add method last\_bytes in fileutils * Follow the new PTI for document build * Add missing information in docstring of validate\_integer 3.33.0 ------ 3.32.0 ------ * Avoid tox\_install.sh for constraints support * Updated from global requirements * Remove setting of version/release from releasenotes * Updated from global requirements 3.31.0 ------ * Add method to compute a file's checksum to fileutils * Imported Translations from Zanata * Add method validate\_integer 3.30.0 ------ * Updated from global requirements * Use six.binary\_type to point to the right type * Add a mixed mode parser to string\_to\_bytes * Updated from global requirements * Fix some reST field lists in docstrings 3.29.0 ------ * Imported Translations from Zanata * Add method to escape ipv6 ip addresses * Updated from global requirements * Update reno for stable/pike * Updated from global requirements * Prevent deprecation error messages from pkg\_resources 3.28.0 ------ * Update URLs in documents according to document migration 3.27.0 ------ * rearrange existing documentation to fit the new standard layout * switch from oslosphinx to openstackdocstheme * Updated from global requirements * Updated from global requirements 3.26.0 ------ * Updated from global requirements * Updated from global requirements * Updated from global requirements * Updated from global requirements 3.25.1 ------ * Updated from global requirements * Remove split conversion to tuple * Updated from global requirements 3.25.0 ------ * Remove log translations 3.24.0 ------ * Use Sphinx 1.5 warning-is-error * Updated from global requirements * Adding a check of string type for hmacs 3.23.0 ------ * Updated from global requirements * [Fix gate]Update test requirement * Add missing documentation for secretutils * Updated from global requirements * Updated from global requirements * Update reno for stable/ocata 3.22.0 ------ * Remove references to Python 3.4 * Added the token 'encrypted\_key' to mask\_password list * Add Constraints support 3.21.0 ------ * Allow 'get\_all\_class\_names' to pass kwargs 3.20.0 ------ * Add toggle 'dashed' to 'generate\_uuid' function * Show team and repo badges on README 3.19.0 ------ * Updated from global requirements * Improve eventlet check when selecting Event backend * Updated from global requirements 3.18.0 ------ * Add option to not truncate built-ins * Create dictutils and add 'flatten\_dict\_to\_keypairs' * Updated from global requirements * Add reno for release notes management * Allow scoped ipv6 addresses * Trivial fixes to the usage doc * Add threading<->eventlet compatible Event * Updated from global requirements * [TrivialFix] Replace 'assertEqual(None, ...)' with 'assertIsNone(...)' 3.17.0 ------ * Add method is\_valid\_mac * Add \_\_ne\_\_ built-in function * Make method import\_versioned\_module work * Change assertTrue(isinstance()) by optimal assert * doc: Fix docstring of method bool\_from\_string * Change assertTrue(isinstance()) by optimal assert * Add method is\_valid\_boolstr * Add method is\_valid\_ipv6\_cidr * Updated from global requirements * Updated from global requirements * Add missing specs\_matcher documentation * Update homepage with developer documentation page * Updated from global requirements * Updated from global requirements * Add utils for validating and splitting quotes * Updated from global requirements * Extend specs matcher to support ">" and "<" * Remove discover from test-requirements 3.16.0 ------ * Fix mask\_dict\_password for non string/dict type key in dict * Restore operator * More unit tests for specs matcher * Imported Translations from Zanata * Add Python 3.5 classifier and venv * Use an actual well defined parser for spec matching * Remove unused LOG to keep code clean * Updated from global requirements 3.15.0 ------ * Add basic docstrings to stopwatch has\_started/stopped methods * Make mask\_dict\_password consistent with mask\_password * Updated from global requirements * improve tests for mask\_password and mask\_dict\_password * Simplify boolean expression in executils.py 3.14.0 ------ * Support json format output from qemu command * Fix flake8 issues * Use is\_valid\_ipv4 in get\_ipv6\_addr\_by\_EUI64 * Imported Translations from Zanata 3.13.0 ------ * Allow assigning "0" to port 3.12.0 ------ * Updated from global requirements * Fix method split\_path's docstring 'versionadded' * Updated from global requirements * Updated from global requirements * Avoid catching generic exception * Remove method total\_seconds in timeuitls * Fix is\_valid\_cidr raises TypeError 3.11.0 ------ * Trivial: ignore openstack/common in flake8 exclude list * Move method split\_path into oslo.utils 3.10.0 ------ * Imported Translations from Zanata * Updated from global requirements * Move nova extra\_specs\_ops to oslo.utils 3.9.0 ----- * Imported Translations from Zanata * Provide single step check if eventlet is monkey\_patched * Add method is\_valid\_cidr to netutils * Updated from global requirements * Updated from global requirements * Add importutils.import\_any method * Add excutils.exception\_filter * Explicitly exclude tests from bandit scan * Add CHAPPASSWORD to list of sanitize keys * Enable bandit in gate * Updated from global requirements 3.7.0 ----- * Add method check\_string\_length * Add missing versionchanged for configdrive 3.6.0 ----- 3.5.0 ----- * Updated from global requirements * Imported Translations from Zanata * Remove bandit.yaml in favor of defaults * Updated from global requirements * Narrow mock for getfilesystemencoding * Update translation setup * Revert "Use assertTrue/False instead of assertEqual(T/F)" * Updated from global requirements * Updated from global requirements * Add excutils.save\_and\_reraise\_exception force\_reraise + capture * Add encodeutils.to\_utf8() function * Create secretutils and include 'constant\_time\_compare' function * Fix coverage * Imported Translations from Zanata * Updated from global requirements 3.4.0 ----- * Updated from global requirements * Use assertTrue/False instead of assertEqual(T/F) * Add a mechanism to mask passwords in dictionaries * Add "configdrive" to the list of keys used by mask\_password() * assertIsNone(val) instead of assertEqual(None,val) 3.3.0 ----- * Fix DeprecationWarning when call method delta\_seconds * fix fnmatch.filter in non-posix system * fix fileutils ut code random failure * Add missing doc index for imageutils and fnmatch * re-implement thread safe fnmatch * Fix the bug of can't get the desired image info 3.2.0 ----- * Revert "Move netifaces to extras" * Remove Babel from requirements 3.1.0 ----- * Remove duplicated profiles section from bandit.yaml * Allow get\_class\_name to accept bound method and class method * deprecate timeutils.total\_seconds() * Move imageutils from oslo-incubator to oslo.utils * add comment explaining why we don't want extra values passed to mask\_password * networkutils: drop python 2.6 support * Remove 'MANIFEST.in' * Move netifaces to extras 3.0.0 ----- * Add a bandit target to tox.ini * Updated from global requirements * Remove python 2.6 classifier * Fix wrong bug tracking link * Remove python 2.6 and cleanup tox.ini * Refactor Port number validation * Add useful 'time\_it' decorator 2.8.0 ----- * Fix get\_class\_name() on Python 3 * Added ICMP 'type' and 'code' checking capability to 'netutils' module * Updated from global requirements * Imported Translations from Zanata * comment in write\_to\_tempfile * Use versionadded and versionchanged in doc 2.7.0 ----- * Expose function signature fetching function * Allow 'forever\_retry\_uncaught\_exceptions' to take in different defaults * Write document for each unit of oslo\_utils.utils * Fix usage of "deprecated" markup in docstrings * Just use 'exception\_to\_unicode' to handle exception to string * Add 'secret' to sensitive keys 2.6.0 ----- * Fix coverage configuration and execution * Use a stopwatch in 'forever\_retry\_uncaught\_exceptions' * No need for Oslo Incubator Sync * Make forever\_retry\_uncaught\_exceptions handle its own failures * Ensure stopwatch \_\_enter\_\_, \_\_exit\_\_ are in docs * Add some timeutils stop watch examples * Imported Translations from Zanata * Move 'history' -> release notes section * Fix bad acting classes and 'is\_bound\_method' check * Change ignore-errors to ignore\_errors * Updated from global requirements * If 'bool\_from\_string' provided a boolean just return it * Imported Translations from Zanata * only capture the ImportError when importing * Add 'token' to list of fields to be santized by mask\_password 2.5.0 ----- * Updated from global requirements * Imported Translations from Transifex * Updated from global requirements * Updated from global requirements 2.4.0 ----- 2.3.0 ----- * Updated from global requirements * Update docstring on stop watch to reflect monotonic lib. usage * Updated from global requirements * flake8 - remove unused rules * Bump monotonic to 0.3 to remove exception catching on import * Provide a common exception caused by base class * Imported Translations from Transifex * Allow access to reflection 'get\_members' * Updated from global requirements 2.2.0 ----- * Imported Translations from Transifex * Updated from global requirements 2.1.0 ----- * Imported Translations from Transifex * Updated from global requirements * Adding checking around the monotonic import 2.0.0 ----- * Updated from global requirements * Updated from global requirements * Add oslo.config to test requirements * Remove oslo namespace package * Updated from global requirements 1.9.0 ----- * Updated from global requirements * versionutils: add version convert helper methods * Imported Translations from Transifex * Add write\_to\_tempfile back to fileutils * Use monotonic library to avoid finding monotonic time function * Fix exception\_to\_unicode() for oslo\_i18n Message 1.8.0 ----- * Add fileutils to oslo\_utils * Updated from global requirements * Add tox target to find missing requirements 1.7.0 ----- * Updated from global requirements * Updated from global requirements * Switch badges from 'pypip.in' to 'shields.io' * timeutils: fix newer/older comparison with TZ aware datetime * Replace parse\_strtime with parse\_isotime in older/newer 1.6.0 ----- * Add pytz to requirements * Deprecate strtime * Imported Translations from Transifex * timeutils: utcnow() can return a value with a timezone * Add 'raise\_with\_cause' chaining helper to excutils * timeutils: deprecate isotime() * timeutils: make marshall timezone aware * Advertise support for Python3.4 / Remove support for Python 3.3 * Updated from global requirements * Add exception\_to\_unicode() function * Remove run\_cross\_tests.sh * Imported Translations from Transifex * Move versionutils into place and remove deprecation tools * Denote monotonic import ordering + usage 1.5.0 ----- * Add liberty release name to versionutils * Expose opts entry point for version\_utils * Switch from oslo.config to oslo\_config * Remove oslo.log code and clean up versionutils API * Remove code that moved to oslo.i18n * Enhance versionutils.deprecated to work with classes * Add Kilo release name to versionutils * Allow deprecated decorator to specify no plan for removal * Uncap library requirements for liberty * Add JUNO as a target to versionutils module * Add missing reflection + uuidutils docs * timeutils: avoid passing leap second to datetime * Add pypi download + version badges * Cleanup README.rst and setup.cfg * pep8: fixed multiple violations * Use oslotest instead of common test module * Use hacking import\_exceptions for gettextutils.\_ * fixed typos * Fix violations of H302:import only modules * Adds decorator to deprecate functions and methods * Remove vim header * Add \`versionutils\` for version compatibility checks * Update hacking setting * Updated from global requirements * Imported Translations from Transifex * Clean up TestIsIPv6Enabled * Fix test\_netutils: stop patches * Add a new string to the list of masked patterns * Provide common \`fetch\_current\_thread\_functor\` function * Imported Translations from Transifex 1.4.0 ----- * Add a stopwatch + split for duration(s) * Allow providing a logger to save\_and\_reraise\_exception * Updated from global requirements * Utility API to generate EUI-64 IPv6 address 1.3.0 ----- * Add a eventlet utils helper module * Add microsecond support to iso8601\_from\_timestamp * add dependency warning to requirements.txt * Updated from global requirements * Update Oslo imports to remove namespace package * Add TimeFixture * Add microsecond support to timeutils.utcnow\_ts() * Make setup.cfg packages include oslo.utils 1.2.1 ----- * Return LOCALHOST if no default interface * fix link to bug tracker in README 1.2.0 ----- * Improve performance of strutils.mask\_password * Move files out of the namespace package * Add method is\_valid\_port in netutils * Support non-lowercase uuids in is\_uuid\_like * Add 'secret\_uuid' in \_SANITIZE\_KEYS for strutils * Imported Translations from Transifex * Workflow documentation is now in infra-manual 1.1.0 ----- * Improve error reporting in \_get\_my\_ipv4\_address() * Add get\_my\_ip() * Updated from global requirements * Add 'auth\_password' in \_SANITIZE\_KEYS for strutils * Updated from global requirements * Activate pep8 check that \_ is imported * Add uuidutils to oslo.utils * Add pbr to installation requirements * Updated from global requirements * Add is\_int\_like() function * Hide auth\_token and new\_pass * Imported Translations from Transifex * Add history/changelog to docs * Imported Translations from Transifex * Support building wheels (PEP-427) * Imported Translations from Transifex * Improve docstrings for IP verification functions * Imported Translations from Transifex * Add ip address validation * Fix how it appears we need to use mock\_anything to avoid 'self' errors * Updated from global requirements * Move over a reflection module that taskflow uses * Make safe\_encode func case-insensitive * Enable mask\_password to handle byte code strings * Updated from global requirements 1.0.0 ----- * Imported Translations from Transifex * Add the ability to extract the query params from a urlsplit * Work toward Python 3.4 support and testing * warn against sorting requirements * Remove unused dependency on oslo.config * Updated from global requirements * Just use int(BOOL) to convert to 1 or 0 * Re-enable \_import\* hidden methods in import\_utils 0.2.0 ----- * Make strutils.mask\_password more secure * New public API for mask\_password ported from incubator * Imported Translations from Transifex 0.1.1 ----- * Make return type from urlsplit private * Add API docs and clean up other docs * Make the i18n integration module private * Cleaning up index.rst file * export only try\_import in \_\_all\_\_ * Switch to oslo.i18n and remove any dependency on oslo-incubator * Move units into oslo.utils * Switch to standard python logging * Setup for translation * Split strutils into 2 different modules * Rename network\_utils into netutils * get pep8 working * Get the tox tests working * exported from oslo-incubator by graduate.sh * Fixed a new pep8 error and a small typo * Set pbr 'warnerrors' option for doc build * fixed typos found by RETF rules * Use moxstubout and mockpatch from oslotest * Remove ValueError when accessing sys.modules * Enable configuring tcp keepalive * Avoid raising index error when no host * Remove str() from LOG.\* and exceptions * Remove import workaround of SplitResult * Use oslotest instead of common test module * Partial fix of test\_strutils.py on Python 3 * Fix safe\_encode(): return bytes on Python 3 * urlsplit issues with IPv6 addresses in python26 * save\_and\_reraise\_exception: make logging respect the reraise parameter * strutils: Allow safe\_{encode,decode} to take bytes as input * Fix import order in test\_excutils * Update oslo log messages with translation domains * Implements SI/IEC unit system conversion to bytes * Add basic Python 3 tests * py3kcompat: remove * Deleted duplicated method in cliutils * strutils bool\_from\_string, allow specified default * Utilizes assertIsNone and assertIsNotNone * Fix spelling errors in comments * Use hacking import\_exceptions for gettextutils.\_ * Correct invalid docstrings * Fix a bug in safe\_encode where it returns a bytes object in py3 * Fix typo in parameter documentation (timeutils) * Avoid TypeError in is\_older\_than, is\_newer\_than * Remove vim header * Use py3kcompat urlutils functions instead of urlparse * Add helper method total\_seconds in timeutils.py * Do not name variables as builtins * Use six.text\_type instead of unicode function in tests * Fix typos in oslo * Adjust import order according to PEP8 imports rule * python3: use six.text\_types for unicode() * Don't shadow str * Fix timeutils.set\_override\_time not defaulting to current wall time * Fix misused assertTrue in unit tests * Optimize timeutils.utcnow\_ts() * excutils: replace unicode by six.u * excutils: use six.reraise to re-raise * Replace using tests.utils part2 * Bump hacking to 0.7.0 * Replace using tests.utils with openstack.common.test * BaseException.message is deprecated since Python 2.6 * Enable H302 hacking check * Add conditional exception reraise * python3: Add python3 compatibility * Make AMQP based RPC consumer threads more robust * Add network\_utils.urlsplit * Remove useless logging in networks\_utils * python3: Fix traceback while running python3 * Refactors to\_bytes * Add slugify to strutils * Enable hacking H404 test * Added common code into fileutils and strutils * Enable hacking H402 test * Enable hacking H403 test * Enable hacking H201 test * Add 't', 'y', and \`strict\` to \`bool\_from\_string\` * Handle ints passed to \`boolean\_from\_string\` * Removes leading zeros on integers in test\_timeutils * Convert unicode strings for python3 portability * Do not import openstack.common.log in strutils * Improve Python 3.x compatibility * Replaces standard logging with common logging * Removes unused imports in the tests module * Fix Copyright Headers - Rename LLC to Foundation * support ISO 8601 micro-second precision * Decode / Encode string utils for openstack * timeutils: considers that now is soon * Replace direct use of testtools BaseTestCase * Use testtools as test base class * Import timeutils.is\_soon from keystoneclient * UTC ISO8601 from timestamp * Implement importutils.try\_import * Use basestring instead of str for type check * Make time comparison functions accept strings * Fix timezone handling in timeutils tests * Rename utils.py to strutils.py * Provide i18n to those messages without \_() * Make project pyflakes clean * Account for tasks duration in LoopingCall delay * Convenience wrapper for datetime.timedelta.total\_seconds() * Added is\_newer\_than function * Extracted parse\_host\_port into network\_utils * Normalize\_time() always return naive object * Use pep8 v1.3.3 * Don't trap then re-raise ImportError * Fix spelling typos * Support for marshalling datetime while preserving microseconds * Remove unused imports * fix bug lp:1019348,update openstack-common to support pep8 1.3 * Use strtime() in to\_primitive() for datetime objs * Improve exception from importutils.import\_class() * Update common code to support pep 1.3. bug 1014216 * add import\_object\_ns function * add more realistic unit tests for importutils * Fix utcnow\_ts to return UTC timestamp * Add nova time util functions to timeutils * Replace datetime.utcnow with timeutils.utcnow * Remove common.exception from common.importutils * Add save\_and\_reraise\_exception() * Update exception from importutils.import\_class() * Change behavior in utils.import\_object() * Create openstack.common.timeutils * Initial skeleton project ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/HACKING.rst0000664000175000017500000000021600000000000015535 0ustar00zuulzuul00000000000000oslo.utils Style Commandments ============================= Read the OpenStack Style Commandments https://docs.openstack.org/hacking/latest/ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/LICENSE0000664000175000017500000002363600000000000014757 0ustar00zuulzuul00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1740145268.2890563 oslo_utils-8.2.0/PKG-INFO0000644000175000017500000000424400000000000015037 0ustar00zuulzuul00000000000000Metadata-Version: 2.1 Name: oslo.utils Version: 8.2.0 Summary: Oslo Utility library Home-page: https://docs.openstack.org/oslo.utils/latest/ Author: OpenStack Author-email: openstack-discuss@lists.openstack.org Classifier: Environment :: OpenStack Classifier: Intended Audience :: Information Technology Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: Implementation :: CPython Requires-Python: >=3.9 License-File: LICENSE Requires-Dist: iso8601>=0.1.11 Requires-Dist: oslo.i18n>=3.15.3 Requires-Dist: netaddr>=0.10.0 Requires-Dist: debtcollector>=1.2.0 Requires-Dist: pyparsing>=2.1.0 Requires-Dist: packaging>=20.4 Requires-Dist: tzdata>=2022.4 Requires-Dist: PyYAML>=3.13 Requires-Dist: psutil>=3.2.2 Requires-Dist: pbr>=6.1.0 ======================== Team and repository tags ======================== .. image:: https://governance.openstack.org/tc/badges/oslo.utils.svg :target: https://governance.openstack.org/tc/reference/tags/index.html .. Change things from this point on ========== oslo.utils ========== .. image:: https://img.shields.io/pypi/v/oslo.utils.svg :target: https://pypi.org/project/oslo.utils/ :alt: Latest Version .. image:: https://img.shields.io/pypi/dm/oslo.utils.svg :target: https://pypi.org/project/oslo.utils/ :alt: Downloads The oslo.utils library provides support for common utility type functions, such as encoding, exception handling, string manipulation, and time handling. * Free software: Apache license * Documentation: https://docs.openstack.org/oslo.utils/latest/ * Source: https://opendev.org/openstack/oslo.utils * Bugs: https://bugs.launchpad.net/oslo.utils * Release notes: https://docs.openstack.org/releasenotes/oslo.utils/ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/README.rst0000664000175000017500000000170700000000000015434 0ustar00zuulzuul00000000000000======================== Team and repository tags ======================== .. image:: https://governance.openstack.org/tc/badges/oslo.utils.svg :target: https://governance.openstack.org/tc/reference/tags/index.html .. Change things from this point on ========== oslo.utils ========== .. image:: https://img.shields.io/pypi/v/oslo.utils.svg :target: https://pypi.org/project/oslo.utils/ :alt: Latest Version .. image:: https://img.shields.io/pypi/dm/oslo.utils.svg :target: https://pypi.org/project/oslo.utils/ :alt: Downloads The oslo.utils library provides support for common utility type functions, such as encoding, exception handling, string manipulation, and time handling. * Free software: Apache license * Documentation: https://docs.openstack.org/oslo.utils/latest/ * Source: https://opendev.org/openstack/oslo.utils * Bugs: https://bugs.launchpad.net/oslo.utils * Release notes: https://docs.openstack.org/releasenotes/oslo.utils/ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/bindep.txt0000664000175000017500000000076400000000000015751 0ustar00zuulzuul00000000000000# This is a cross-platform list tracking distribution packages needed by tests; # see http://docs.openstack.org/infra/bindep/ for additional information. locales [platform:debian] python3-all-dev [platform:ubuntu !platform:ubuntu-precise] python3-dev [platform:dpkg] python3-devel [platform:rpm] qemu-img [platform:redhat test] qemu-tools [platform:suse test] qemu-utils [platform:dpkg test] libxcrypt-compat [platform:redhat crypt] libcrypt1 [platform:suse crypt] libcrypt1 [platform:dpkg crypt] ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1740145268.2690525 oslo_utils-8.2.0/doc/0000775000175000017500000000000000000000000014505 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/doc/requirements.txt0000664000175000017500000000024000000000000017765 0ustar00zuulzuul00000000000000# this is required for the docs build jobs sphinx>=2.0.0 # BSD openstackdocstheme>=2.2.0 # Apache-2.0 reno>=3.1.0 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1740145268.2690525 oslo_utils-8.2.0/doc/source/0000775000175000017500000000000000000000000016005 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/doc/source/conf.py0000664000175000017500000000466300000000000017315 0ustar00zuulzuul00000000000000# Copyright (C) 2020 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import os import sys sys.path.insert(0, os.path.abspath('../..')) # -- General configuration ---------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ 'sphinx.ext.autodoc', 'openstackdocstheme' ] # openstackdocstheme options openstackdocs_repo_name = 'openstack/oslo.utils' openstackdocs_bug_project = 'oslo.utils' openstackdocs_bug_tag = '' # autodoc generation is a bit aggressive and a nuisance when doing heavy # text edit cycles. # execute "export SPHINX_DEBUG=1" in your terminal to disable # The suffix of source filenames. source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General information about the project. project = 'oslo.utils' copyright = '2014, OpenStack Foundation' # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). add_module_names = True # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'native' # -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. # html_theme_path = ["."] # html_theme = '_theme' # html_static_path = ['static'] html_theme = 'openstackdocs' # Output file base name for HTML help builder. htmlhelp_basename = '%sdoc' % project # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass # [howto/manual]). latex_documents = [ ('index', '%s.tex' % project, '%s Documentation' % project, 'OpenStack Foundation', 'manual'), ] ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1740145268.2690525 oslo_utils-8.2.0/doc/source/contributor/0000775000175000017500000000000000000000000020357 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/doc/source/contributor/index.rst0000664000175000017500000000011700000000000022217 0ustar00zuulzuul00000000000000============ Contributing ============ .. include:: ../../../CONTRIBUTING.rst ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/doc/source/index.rst0000664000175000017500000000122700000000000017650 0ustar00zuulzuul00000000000000====================================== Welcome to oslo.utils's documentation! ====================================== The `Oslo`_ utils library provides support for common utility type functions, such as encoding, exception handling, string manipulation, and time handling. Contents ======== .. toctree:: :maxdepth: 1 install/index user/index reference/index contributor/index Release Notes ============= Read also the `oslo.utils Release Notes `_. Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` .. _Oslo: https://wiki.openstack.org/wiki/Oslo ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1740145268.2690525 oslo_utils-8.2.0/doc/source/install/0000775000175000017500000000000000000000000017453 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/doc/source/install/index.rst0000664000175000017500000000030600000000000021313 0ustar00zuulzuul00000000000000============ Installation ============ At the command line:: $ pip install oslo.utils Or, if you have virtualenvwrapper installed:: $ mkvirtualenv oslo.utils $ pip install oslo.utils././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1740145268.2730532 oslo_utils-8.2.0/doc/source/reference/0000775000175000017500000000000000000000000017743 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/doc/source/reference/dictutils.rst0000664000175000017500000000012600000000000022500 0ustar00zuulzuul00000000000000=========== dictutils =========== .. automodule:: oslo_utils.dictutils :members: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/doc/source/reference/encodeutils.rst0000664000175000017500000000013600000000000023013 0ustar00zuulzuul00000000000000============= encodeutils ============= .. automodule:: oslo_utils.encodeutils :members: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/doc/source/reference/eventletutils.rst0000664000175000017500000000014600000000000023405 0ustar00zuulzuul00000000000000=============== eventletutils =============== .. automodule:: oslo_utils.eventletutils :members: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/doc/source/reference/excutils.rst0000664000175000017500000000012200000000000022330 0ustar00zuulzuul00000000000000========== excutils ========== .. automodule:: oslo_utils.excutils :members: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/doc/source/reference/fileutils.rst0000664000175000017500000000013300000000000022472 0ustar00zuulzuul00000000000000============= fileutils ============= .. automodule:: oslo_utils.fileutils :members: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/doc/source/reference/fixture.rst0000664000175000017500000000012600000000000022162 0ustar00zuulzuul00000000000000============= fixture ============= .. automodule:: oslo_utils.fixture :members: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/doc/source/reference/importutils.rst0000664000175000017500000000013600000000000023070 0ustar00zuulzuul00000000000000============= importutils ============= .. automodule:: oslo_utils.importutils :members: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/doc/source/reference/index.rst0000664000175000017500000000044200000000000021604 0ustar00zuulzuul00000000000000============= API Reference ============= .. toctree:: :maxdepth: 2 dictutils encodeutils eventletutils excutils fileutils fixture importutils netutils reflection secretutils specs_matcher strutils timeutils units uuidutils versionutils ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/doc/source/reference/netutils.rst0000664000175000017500000000012200000000000022337 0ustar00zuulzuul00000000000000========== netutils ========== .. automodule:: oslo_utils.netutils :members: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/doc/source/reference/reflection.rst0000664000175000017500000000013200000000000022623 0ustar00zuulzuul00000000000000============ reflection ============ .. automodule:: oslo_utils.reflection :members: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/doc/source/reference/secretutils.rst0000664000175000017500000000016400000000000023044 0ustar00zuulzuul00000000000000============= secretutils ============= .. automodule:: oslo_utils.secretutils :members: constant_time_compare ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/doc/source/reference/specs_matcher.rst0000664000175000017500000000014400000000000023314 0ustar00zuulzuul00000000000000============== specs_matcher ============== .. automodule:: oslo_utils.specs_matcher :members: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/doc/source/reference/strutils.rst0000664000175000017500000000012200000000000022361 0ustar00zuulzuul00000000000000========== strutils ========== .. automodule:: oslo_utils.strutils :members: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/doc/source/reference/timeutils.rst0000664000175000017500000000017700000000000022521 0ustar00zuulzuul00000000000000=========== timeutils =========== .. automodule:: oslo_utils.timeutils :members: :special-members: __enter__, __exit__ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/doc/source/reference/units.rst0000664000175000017500000000010600000000000021634 0ustar00zuulzuul00000000000000======= units ======= .. automodule:: oslo_utils.units :members: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/doc/source/reference/uuidutils.rst0000664000175000017500000000012600000000000022523 0ustar00zuulzuul00000000000000=========== uuidutils =========== .. automodule:: oslo_utils.uuidutils :members: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/doc/source/reference/versionutils.rst0000664000175000017500000000014200000000000023240 0ustar00zuulzuul00000000000000============== versionutils ============== .. automodule:: oslo_utils.versionutils :members: ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1740145268.2730532 oslo_utils-8.2.0/doc/source/user/0000775000175000017500000000000000000000000016763 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/doc/source/user/history.rst0000664000175000017500000000004000000000000021210 0ustar00zuulzuul00000000000000.. include:: ../../../ChangeLog ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/doc/source/user/index.rst0000664000175000017500000000033200000000000020622 0ustar00zuulzuul00000000000000================ Using oslo.utils ================ .. toctree:: :maxdepth: 2 usage timeutils .. history contains a lot of sections, toctree with maxdepth 1 is used. .. toctree:: :maxdepth: 1 history ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/doc/source/user/timeutils.rst0000664000175000017500000000467000000000000021543 0ustar00zuulzuul00000000000000=========== timeutils =========== Using a stopwatch (as a context manager) ---------------------------------------- :: >>> from oslo_utils import timeutils >>> import time >>> >>> def slow_routine(delay): ... def i_am_slow(): ... time.sleep(delay) ... return i_am_slow ... >>> >>> half_sec_func = slow_routine(0.5) >>> with timeutils.StopWatch() as w: ... half_sec_func() ... >>> print(w.elapsed()) 0.500243999995 Manually using a stopwatch -------------------------- :: >>> from oslo_utils import timeutils >>> import time >>> w = timeutils.StopWatch() >>> w.start() >>> time.sleep(0.1) >>> time.sleep(0.1) >>> time.sleep(0.1) >>> time.sleep(0.1) >>> w.stop() >>> w.elapsed() 13.96467600017786 Tracking durations with a stopwatch ----------------------------------- :: >>> from oslo_utils import timeutils >>> w = timeutils.StopWatch(duration=10) >>> w.start() >>> w.elapsed() 2.023942000232637 >>> w.leftover() 4.648160999640822 >>> w.leftover() 3.5522090001031756 >>> w.leftover() 3.0481000002473593 >>> w.leftover() 2.1918740002438426 >>> w.leftover() 1.6966530000790954 >>> w.leftover() 1.1202940000221133 >>> w.leftover() 0.0 >>> w.expired() True Tracking and splitting with a stopwatch --------------------------------------- :: >>> from oslo_utils import timeutils >>> w = timeutils.StopWatch() >>> w.start() >>> w.split() Split(elapsed=3.02423300035, length=3.02423300035) >>> w.split() Split(elapsed=6.44820600003, length=3.42397299968) >>> w.split() Split(elapsed=7.9678720003, length=1.51966600027) >>> w.splits (Split(elapsed=3.02423300035, length=3.02423300035), Split(elapsed=6.44820600003, length=3.42397299968), Split(elapsed=7.9678720003, length=1.51966600027)) >>> w.stop() >>> w.elapsed() 16.799759999848902 >>> w.splits (Split(elapsed=3.02423300035, length=3.02423300035), Split(elapsed=6.44820600003, length=3.42397299968), Split(elapsed=7.9678720003, length=1.51966600027)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/doc/source/user/usage.rst0000664000175000017500000000027600000000000020626 0ustar00zuulzuul00000000000000======= Usage ======= To use oslo.utils in a project, import the individual module you need. For example:: from oslo_utils import strutils slug = strutils.to_slug('input value') ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1740145268.2890563 oslo_utils-8.2.0/oslo.utils.egg-info/0000775000175000017500000000000000000000000017545 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145268.0 oslo_utils-8.2.0/oslo.utils.egg-info/PKG-INFO0000644000175000017500000000424400000000000020644 0ustar00zuulzuul00000000000000Metadata-Version: 2.1 Name: oslo.utils Version: 8.2.0 Summary: Oslo Utility library Home-page: https://docs.openstack.org/oslo.utils/latest/ Author: OpenStack Author-email: openstack-discuss@lists.openstack.org Classifier: Environment :: OpenStack Classifier: Intended Audience :: Information Technology Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: Implementation :: CPython Requires-Python: >=3.9 License-File: LICENSE Requires-Dist: iso8601>=0.1.11 Requires-Dist: oslo.i18n>=3.15.3 Requires-Dist: netaddr>=0.10.0 Requires-Dist: debtcollector>=1.2.0 Requires-Dist: pyparsing>=2.1.0 Requires-Dist: packaging>=20.4 Requires-Dist: tzdata>=2022.4 Requires-Dist: PyYAML>=3.13 Requires-Dist: psutil>=3.2.2 Requires-Dist: pbr>=6.1.0 ======================== Team and repository tags ======================== .. image:: https://governance.openstack.org/tc/badges/oslo.utils.svg :target: https://governance.openstack.org/tc/reference/tags/index.html .. Change things from this point on ========== oslo.utils ========== .. image:: https://img.shields.io/pypi/v/oslo.utils.svg :target: https://pypi.org/project/oslo.utils/ :alt: Latest Version .. image:: https://img.shields.io/pypi/dm/oslo.utils.svg :target: https://pypi.org/project/oslo.utils/ :alt: Downloads The oslo.utils library provides support for common utility type functions, such as encoding, exception handling, string manipulation, and time handling. * Free software: Apache license * Documentation: https://docs.openstack.org/oslo.utils/latest/ * Source: https://opendev.org/openstack/oslo.utils * Bugs: https://bugs.launchpad.net/oslo.utils * Release notes: https://docs.openstack.org/releasenotes/oslo.utils/ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145268.0 oslo_utils-8.2.0/oslo.utils.egg-info/SOURCES.txt0000664000175000017500000001334400000000000021436 0ustar00zuulzuul00000000000000.coveragerc .mailmap .pre-commit-config.yaml .stestr.conf .zuul.yaml AUTHORS CONTRIBUTING.rst ChangeLog HACKING.rst LICENSE README.rst bindep.txt requirements.txt setup.cfg setup.py test-requirements.txt tox.ini doc/requirements.txt doc/source/conf.py doc/source/index.rst doc/source/contributor/index.rst doc/source/install/index.rst doc/source/reference/dictutils.rst doc/source/reference/encodeutils.rst doc/source/reference/eventletutils.rst doc/source/reference/excutils.rst doc/source/reference/fileutils.rst doc/source/reference/fixture.rst doc/source/reference/importutils.rst doc/source/reference/index.rst doc/source/reference/netutils.rst doc/source/reference/reflection.rst doc/source/reference/secretutils.rst doc/source/reference/specs_matcher.rst doc/source/reference/strutils.rst doc/source/reference/timeutils.rst doc/source/reference/units.rst doc/source/reference/uuidutils.rst doc/source/reference/versionutils.rst doc/source/user/history.rst doc/source/user/index.rst doc/source/user/timeutils.rst doc/source/user/usage.rst oslo.utils.egg-info/PKG-INFO oslo.utils.egg-info/SOURCES.txt oslo.utils.egg-info/dependency_links.txt oslo.utils.egg-info/not-zip-safe oslo.utils.egg-info/pbr.json oslo.utils.egg-info/requires.txt oslo.utils.egg-info/top_level.txt oslo_utils/__init__.py oslo_utils/_i18n.py oslo_utils/dictutils.py oslo_utils/encodeutils.py oslo_utils/eventletutils.py oslo_utils/excutils.py oslo_utils/fileutils.py oslo_utils/fixture.py oslo_utils/importutils.py oslo_utils/netutils.py oslo_utils/reflection.py oslo_utils/secretutils.py oslo_utils/specs_matcher.py oslo_utils/strutils.py oslo_utils/timeutils.py oslo_utils/units.py oslo_utils/uuidutils.py oslo_utils/version.py oslo_utils/versionutils.py oslo_utils/imageutils/__init__.py oslo_utils/imageutils/__main__.py oslo_utils/imageutils/cli.py oslo_utils/imageutils/format_inspector.py oslo_utils/imageutils/qemu.py oslo_utils/locale/de/LC_MESSAGES/oslo_utils.po oslo_utils/locale/en_GB/LC_MESSAGES/oslo_utils.po oslo_utils/locale/fr/LC_MESSAGES/oslo_utils.po oslo_utils/locale/ka_GE/LC_MESSAGES/oslo_utils.po oslo_utils/tests/__init__.py oslo_utils/tests/base.py oslo_utils/tests/test_dictutils.py oslo_utils/tests/test_eventletutils.py oslo_utils/tests/test_excutils.py oslo_utils/tests/test_fileutils.py oslo_utils/tests/test_fixture.py oslo_utils/tests/test_importutils.py oslo_utils/tests/test_netutils.py oslo_utils/tests/test_reflection.py oslo_utils/tests/test_secretutils.py oslo_utils/tests/test_specs_matcher.py oslo_utils/tests/test_strutils.py oslo_utils/tests/test_timeutils.py oslo_utils/tests/test_uuidutils.py oslo_utils/tests/test_versionutils.py oslo_utils/tests/tests_encodeutils.py oslo_utils/tests/fake/__init__.py oslo_utils/tests/fake/v2/__init__.py oslo_utils/tests/fake/v2/dummpy.py oslo_utils/tests/imageutils/__init__.py oslo_utils/tests/imageutils/test_format_inspector.py oslo_utils/tests/imageutils/test_qemu.py releasenotes/notes/add-image-format-inspector-2ad45f623838a8f8.yaml releasenotes/notes/add-md5-wrapper-7bf81c2464a7a224.yaml releasenotes/notes/add-methods-for-json-yaml-file-check-746dca0a11c2f9c9.yaml releasenotes/notes/add-reno-350f5f34f794fb5e.yaml releasenotes/notes/allow-to-convert-ipv4-address-from-text-to-binary-8c46ad2d9989e8c5.yaml releasenotes/notes/bug-1942682-ea95d54b2587b32f.yaml releasenotes/notes/bug-2073894-2e11ca85984b7bb7.yaml releasenotes/notes/bump-up-port-range-f774a16336158339.yaml releasenotes/notes/crypt-utils-de46bd8fe835dc98.yaml releasenotes/notes/deprecate-constant_time_compare-53669f464c9811c1.yaml releasenotes/notes/deprecate-eventletutils-f8a96c2c42cd9a15.yaml releasenotes/notes/deprecate-exception_to_unicode-cb4da633bc1bfcc9.yaml releasenotes/notes/deprecate-fnmatch-057a092d434a0c53.yaml releasenotes/notes/deprecate-md5-cc365c25c2a51a8c.yaml releasenotes/notes/drop-imageutils-human-format-support-a89101a36c4dd3cb.yaml releasenotes/notes/drop-python27-support-f97f680651693b47.yaml releasenotes/notes/fix_mask_password_regex-c0661f95a23369a4.yaml releasenotes/notes/image-utils-handle-scientific-notation-6f65d46e9c8c8f8c.yaml releasenotes/notes/imageutils-cli-dd0d1cecbc607725.yaml releasenotes/notes/implement-zoneinfo-to-remove-pytz-fba6f70db09ecdb8.yaml releasenotes/notes/introduce-keystoneidsentinel-16bf3e7f2ae7e9f3.yaml releasenotes/notes/is_valid_ipv4-strict-3da92c0452aaf947.yaml releasenotes/notes/mask-dict-passwords-99357ffb7972fb0b.yaml releasenotes/notes/mask-password-pattern-c8c880098743de3e.yaml releasenotes/notes/mask-password-patterns-f41524069b8ae488.yaml releasenotes/notes/netutils-get_mac_addr_by_ipv6-c3ce6a35a69f7c2b.yaml releasenotes/notes/netutils-get_my_ipv6-c9f124374655326b.yaml releasenotes/notes/new_spec_dsl_operator-21c80a46f67c56df.yaml releasenotes/notes/remove-fnmatch-f227b54f237a02c2.yaml releasenotes/notes/remove-py38-a22bb6c463f92868.yaml releasenotes/notes/remove-strict-from-is_same_callback-cfbff2ada778987e.yaml releasenotes/notes/remove-timeutils-deprecated-helpers-5de68c21dd281529.yaml releasenotes/notes/version-predicate-42f38f7b7e9187e1.yaml releasenotes/source/2023.1.rst releasenotes/source/2023.2.rst releasenotes/source/2024.1.rst releasenotes/source/2024.2.rst releasenotes/source/conf.py releasenotes/source/index.rst releasenotes/source/newton.rst releasenotes/source/ocata.rst releasenotes/source/pike.rst releasenotes/source/queens.rst releasenotes/source/rocky.rst releasenotes/source/stein.rst releasenotes/source/train.rst releasenotes/source/unreleased.rst releasenotes/source/ussuri.rst releasenotes/source/victoria.rst releasenotes/source/wallaby.rst releasenotes/source/xena.rst releasenotes/source/yoga.rst releasenotes/source/zed.rst releasenotes/source/_static/.placeholder releasenotes/source/_templates/.placeholder releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po tools/perf_test_mask_password.py././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145268.0 oslo_utils-8.2.0/oslo.utils.egg-info/dependency_links.txt0000664000175000017500000000000100000000000023613 0ustar00zuulzuul00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145268.0 oslo_utils-8.2.0/oslo.utils.egg-info/not-zip-safe0000664000175000017500000000000100000000000021773 0ustar00zuulzuul00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145268.0 oslo_utils-8.2.0/oslo.utils.egg-info/pbr.json0000664000175000017500000000005600000000000021224 0ustar00zuulzuul00000000000000{"git_version": "cb68378", "is_release": true}././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145268.0 oslo_utils-8.2.0/oslo.utils.egg-info/requires.txt0000664000175000017500000000023500000000000022145 0ustar00zuulzuul00000000000000iso8601>=0.1.11 oslo.i18n>=3.15.3 netaddr>=0.10.0 debtcollector>=1.2.0 pyparsing>=2.1.0 packaging>=20.4 tzdata>=2022.4 PyYAML>=3.13 psutil>=3.2.2 pbr>=6.1.0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145268.0 oslo_utils-8.2.0/oslo.utils.egg-info/top_level.txt0000664000175000017500000000001300000000000022271 0ustar00zuulzuul00000000000000oslo_utils ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1740145268.277054 oslo_utils-8.2.0/oslo_utils/0000775000175000017500000000000000000000000016134 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/oslo_utils/__init__.py0000664000175000017500000000000000000000000020233 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/oslo_utils/_i18n.py0000664000175000017500000000152700000000000017431 0ustar00zuulzuul00000000000000# Copyright 2014 IBM Corp. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """oslo.i18n integration module. See https://docs.openstack.org/oslo.i18n/latest/user/index.html . """ import oslo_i18n _translators = oslo_i18n.TranslatorFactory(domain='oslo_utils') # The primary translation function using the well-known name "_" _ = _translators.primary ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/oslo_utils/dictutils.py0000664000175000017500000000225600000000000020517 0ustar00zuulzuul00000000000000# Copyright (c) 2016 EasyStack Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. def flatten_dict_to_keypairs(d, separator=':'): """Generator that produces sequence of keypairs for nested dictionaries. :param d: dictionaries which may be nested :param separator: symbol between names """ for name, value in sorted(d.items()): if isinstance(value, dict): for subname, subvalue in flatten_dict_to_keypairs(value, separator): yield '{}{}{}'.format(name, separator, subname), subvalue else: yield name, value ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/oslo_utils/encodeutils.py0000664000175000017500000001041100000000000021021 0ustar00zuulzuul00000000000000# Copyright 2014 Red Hat, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import sys import debtcollector.removals def safe_decode(text, incoming=None, errors='strict'): """Decodes incoming text/bytes string using `incoming` if they're not already unicode. :param incoming: Text's current encoding :param errors: Errors handling policy. See here for valid values http://docs.python.org/2/library/codecs.html :returns: text or a unicode `incoming` encoded representation of it. :raises TypeError: If text is not an instance of str """ if not isinstance(text, (str, bytes)): raise TypeError("%s can't be decoded" % type(text)) if isinstance(text, str): return text if not incoming: incoming = (getattr(sys.stdin, 'encoding', None) or sys.getdefaultencoding()) try: return text.decode(incoming, errors) except UnicodeDecodeError: # Note(flaper87) If we get here, it means that # sys.stdin.encoding / sys.getdefaultencoding # didn't return a suitable encoding to decode # text. This happens mostly when global LANG # var is not set correctly and there's no # default encoding. In this case, most likely # python will use ASCII or ANSI encoders as # default encodings but they won't be capable # of decoding non-ASCII characters. # # Also, UTF-8 is being used since it's an ASCII # extension. return text.decode('utf-8', errors) def safe_encode(text, incoming=None, encoding='utf-8', errors='strict'): """Encodes incoming text/bytes string using `encoding`. If incoming is not specified, text is expected to be encoded with current python's default encoding. (`sys.getdefaultencoding`) :param incoming: Text's current encoding :param encoding: Expected encoding for text (Default UTF-8) :param errors: Errors handling policy. See here for valid values http://docs.python.org/2/library/codecs.html :returns: text or a bytestring `encoding` encoded representation of it. :raises TypeError: If text is not an instance of str See also to_utf8() function which is simpler and don't depend on the locale encoding. """ if not isinstance(text, (str, bytes)): raise TypeError("%s can't be encoded" % type(text)) if not incoming: incoming = (getattr(sys.stdin, 'encoding', None) or sys.getdefaultencoding()) # Avoid case issues in comparisons if hasattr(incoming, 'lower'): incoming = incoming.lower() if hasattr(encoding, 'lower'): encoding = encoding.lower() if isinstance(text, str): return text.encode(encoding, errors) elif text and encoding != incoming: # Decode text before encoding it with `encoding` text = safe_decode(text, incoming, errors) return text.encode(encoding, errors) else: return text def to_utf8(text): """Encode Unicode to UTF-8, return bytes unchanged. Raise TypeError if text is not a bytes string or a Unicode string. .. versionadded:: 3.5 """ if isinstance(text, bytes): return text elif isinstance(text, str): return text.encode('utf-8') else: raise TypeError("bytes or Unicode expected, got %s" % type(text).__name__) @debtcollector.removals.remove(message='Use str(exc) instead', category=DeprecationWarning) def exception_to_unicode(exc): """Get the message of an exception as a Unicode string. On Python 3, the exception message is always a Unicode string. .. versionadded:: 1.6 """ return str(exc) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/oslo_utils/eventletutils.py0000664000175000017500000001575500000000000021432 0ustar00zuulzuul00000000000000# Copyright (C) 2015 Yahoo! Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ Eventlet utils helper module. .. versionadded:: 1.3 """ import threading import warnings import debtcollector from oslo_utils import importutils from oslo_utils import timeutils debtcollector.deprecate( "eventuletutils module is deprecated and will be removed.") # These may or may not exist; so carefully import them if we can... _eventlet = importutils.try_import('eventlet') _patcher = importutils.try_import('eventlet.patcher') # Attribute that can be used by others to see if eventlet is even currently # useable (can be used in unittests to skip test cases or test classes that # require eventlet to work). EVENTLET_AVAILABLE = all((_eventlet, _patcher)) # Taken from eventlet.py (v0.16.1) patcher code (it's not a accessible set # for some reason...) _ALL_PATCH = frozenset(['__builtin__', 'MySQLdb', 'os', 'psycopg', 'select', 'socket', 'thread', 'time']) def fetch_current_thread_functor(): """Get the current thread. If eventlet is used to monkey-patch the threading module, return the current eventlet greenthread. Otherwise, return the current Python thread. .. versionadded:: 1.5 """ # Until https://github.com/eventlet/eventlet/issues/172 is resolved # or addressed we have to use complicated workaround to get a object # that will not be recycled; the usage of threading.current_thread() # doesn't appear to currently be monkey patched and therefore isn't # reliable to use (and breaks badly when used as all threads share # the same current_thread() object)... if not EVENTLET_AVAILABLE: return threading.current_thread else: green_threaded = _patcher.is_monkey_patched('thread') if green_threaded: return _eventlet.getcurrent else: return threading.current_thread def warn_eventlet_not_patched(expected_patched_modules=None, what='this library'): """Warns if eventlet is being used without patching provided modules. :param expected_patched_modules: list of modules to check to ensure that they are patched (and to warn if they are not); these names should correspond to the names passed into the eventlet monkey_patch() routine. If not provided then *all* the modules that could be patched are checked. The currently valid selection is one or multiple of ['MySQLdb', '__builtin__', 'all', 'os', 'psycopg', 'select', 'socket', 'thread', 'time'] (where 'all' has an inherent special meaning). :type expected_patched_modules: list/tuple/iterable :param what: string to merge into the warnings message to identify what is being checked (used in forming the emitted warnings message). :type what: string """ if not expected_patched_modules: expanded_patched_modules = _ALL_PATCH.copy() else: expanded_patched_modules = set() for m in expected_patched_modules: if m == 'all': expanded_patched_modules.update(_ALL_PATCH) else: if m not in _ALL_PATCH: raise ValueError("Unknown module '%s' requested to check" " if patched" % m) else: expanded_patched_modules.add(m) if EVENTLET_AVAILABLE: try: # The patcher code stores a dictionary here of all modules # names -> whether it was patched... # # Example: # # >>> _patcher.monkey_patch(os=True) # >>> print(_patcher.already_patched) # {'os': True} maybe_patched = bool(_patcher.already_patched) except AttributeError: # Assume it is patched (the attribute used here doesn't appear # to be a public documented API so we will assume that everything # is patched when that attribute isn't there to be safe...) maybe_patched = True if maybe_patched: not_patched = [] for m in sorted(expanded_patched_modules): if not _patcher.is_monkey_patched(m): not_patched.append(m) if not_patched: warnings.warn("It is highly recommended that when eventlet" " is used that the %s modules are monkey" " patched when using %s (to avoid" " spurious or unexpected lock-ups" " and/or hangs)" % (not_patched, what), RuntimeWarning, stacklevel=3) def is_monkey_patched(module): """Determines safely is eventlet patching for module enabled or not :param module: String, module name :return Bool: True if module is patched, False otherwise """ if _patcher is None: return False return _patcher.is_monkey_patched(module) class EventletEvent: """A class that provides consistent eventlet/threading Event API. This wraps the eventlet.event.Event class to have the same API as the standard threading.Event object. """ def __init__(self, *args, **kwargs): super().__init__() self.clear() def clear(self): if getattr(self, '_set', True): self._set = False self._event = _eventlet.event.Event() def is_set(self): return self._set isSet = is_set def set(self): if not self._set: self._set = True self._event.send(True) def wait(self, timeout=None): with timeutils.StopWatch(timeout) as sw: while True: event = self._event with _eventlet.timeout.Timeout(sw.leftover(return_none=True), False): event.wait() if event is not self._event: continue return self.is_set() def Event(): if is_monkey_patched("thread"): return EventletEvent() else: return threading.Event() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/oslo_utils/excutils.py0000664000175000017500000003427300000000000020357 0ustar00zuulzuul00000000000000# Copyright 2011 OpenStack Foundation. # Copyright 2012, Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ Exception related utilities. """ import functools import io import logging import os import sys import time import traceback from oslo_utils import encodeutils from oslo_utils import reflection from oslo_utils import timeutils class CausedByException(Exception): """Base class for exceptions which have associated causes. NOTE(harlowja): in later versions of python we can likely remove the need to have a ``cause`` here as PY3+ have implemented :pep:`3134` which handles chaining in a much more elegant manner. :param message: the exception message, typically some string that is useful for consumers to view when debugging or analyzing failures. :param cause: the cause of the exception being raised, when provided this should itself be an exception instance, this is useful for creating a chain of exceptions for versions of python where this is not yet implemented/supported natively. .. versionadded:: 2.4 """ def __init__(self, message, cause=None): super().__init__(message) self.cause = cause def __bytes__(self): return self.pformat().encode("utf8") def __str__(self): return self.pformat() def _get_message(self): # We must *not* call into the ``__str__`` method as that will # reactivate the pformat method, which will end up badly (and doesn't # look pretty at all); so be careful... return self.args[0] def pformat(self, indent=2, indent_text=" ", show_root_class=False): """Pretty formats a caused exception + any connected causes.""" if indent < 0: raise ValueError("Provided 'indent' must be greater than" " or equal to zero instead of %s" % indent) buf = io.StringIO() if show_root_class: buf.write(reflection.get_class_name(self, fully_qualified=False)) buf.write(": ") buf.write(self._get_message()) active_indent = indent next_up = self.cause seen = [] while next_up is not None and next_up not in seen: seen.append(next_up) buf.write(os.linesep) if isinstance(next_up, CausedByException): buf.write(indent_text * active_indent) buf.write(reflection.get_class_name(next_up, fully_qualified=False)) buf.write(": ") buf.write(next_up._get_message()) else: lines = traceback.format_exception_only(type(next_up), next_up) for i, line in enumerate(lines): buf.write(indent_text * active_indent) if line.endswith("\n"): # We'll add our own newlines on... line = line[0:-1] buf.write(line) if i + 1 != len(lines): buf.write(os.linesep) # Don't go deeper into non-caused-by exceptions... as we # don't know if there exception 'cause' attributes are even # useable objects... break active_indent += indent next_up = getattr(next_up, 'cause', None) return buf.getvalue() def raise_with_cause(exc_cls, message, *args, **kwargs): """Helper to raise + chain exceptions (when able) and associate a *cause*. NOTE(harlowja): Since in py3.x exceptions can be chained (due to :pep:`3134`) we should try to raise the desired exception with the given *cause* (or extract a *cause* from the current stack if able) so that the exception formats nicely in old and new versions of python. Since py2.x does **not** support exception chaining (or formatting) the exception class provided should take a ``cause`` keyword argument (which it may discard if it wants) to its constructor which can then be inspected/retained on py2.x to get *similar* information as would be automatically included/obtainable in py3.x. :param exc_cls: the exception class to raise (typically one derived from :py:class:`.CausedByException` or equivalent). :param message: the text/str message that will be passed to the exceptions constructor as its first positional argument. :param args: any additional positional arguments to pass to the exceptions constructor. :param kwargs: any additional keyword arguments to pass to the exceptions constructor. .. versionadded:: 1.6 """ if 'cause' not in kwargs: exc_type, exc, exc_tb = sys.exc_info() try: if exc is not None: kwargs['cause'] = exc finally: # Leave no references around (especially with regards to # tracebacks and any variables that it retains internally). del (exc_type, exc, exc_tb) raise exc_cls(message, *args, **kwargs) from kwargs.get('cause') class save_and_reraise_exception: """Save current exception, run some code and then re-raise. In some cases the exception context can be cleared, resulting in None being attempted to be re-raised after an exception handler is run. This can happen when eventlet switches greenthreads or when running an exception handler, code raises and catches an exception. In both cases the exception context will be cleared. To work around this, we save the exception state, run handler code, and then re-raise the original exception. If another exception occurs, the saved exception is logged and the new exception is re-raised. In some cases the caller may not want to re-raise the exception, and for those circumstances this context provides a reraise flag that can be used to suppress the exception. For example:: except Exception: with save_and_reraise_exception() as ctxt: decide_if_need_reraise() if not should_be_reraised: ctxt.reraise = False If another exception occurs and reraise flag is False, the saved exception will not be logged. If the caller wants to raise new exception during exception handling he/she sets reraise to False initially with an ability to set it back to True if needed:: except Exception: with save_and_reraise_exception(reraise=False) as ctxt: [if statements to determine whether to raise a new exception] # Not raising a new exception, so reraise ctxt.reraise = True .. versionchanged:: 1.4 Added *logger* optional parameter. """ def __init__(self, reraise=True, logger=None): self.reraise = reraise if logger is None: logger = logging.getLogger() self.logger = logger self.type_, self.value, self.tb = (None, None, None) def force_reraise(self): if self.type_ is None and self.value is None: raise RuntimeError("There is no (currently) captured exception" " to force the reraising of") try: if self.value is None: self.value = self.type_() if self.value.__traceback__ is not self.tb: raise self.value.with_traceback(self.tb) raise self.value finally: self.value = None self.tb = None def capture(self, check=True): (type_, value, tb) = sys.exc_info() if check and type_ is None and value is None: raise RuntimeError("There is no active exception to capture") self.type_, self.value, self.tb = (type_, value, tb) return self def __enter__(self): # TODO(harlowja): perhaps someday in the future turn check here # to true, because that is likely the desired intention, and doing # so ensures that people are actually using this correctly. return self.capture(check=False) def __exit__(self, exc_type, exc_val, exc_tb): if exc_type is not None: if self.reraise: self.logger.error('Original exception being dropped: %s', traceback.format_exception(self.type_, self.value, self.tb)) return False if self.reraise: self.force_reraise() def forever_retry_uncaught_exceptions(*args, **kwargs): """Decorates provided function with infinite retry behavior. The function retry delay is **always** one second unless keyword argument ``retry_delay`` is passed that defines a value different than 1.0 (less than zero values are automatically changed to be 0.0). If repeated exceptions with the same message occur, logging will only output/get triggered for those equivalent messages every 60.0 seconds, this can be altered by keyword argument ``same_log_delay`` to be a value different than 60.0 seconds (exceptions that change the message are always logged no matter what this delay is set to). As in the ``retry_delay`` case if this is less than zero, it will be automatically changed to be 0.0. """ def decorator(infunc): retry_delay = max(0.0, float(kwargs.get('retry_delay', 1.0))) same_log_delay = max(0.0, float(kwargs.get('same_log_delay', 60.0))) @functools.wraps(infunc) def wrapper(*args, **kwargs): last_exc_message = None same_failure_count = 0 watch = timeutils.StopWatch(duration=same_log_delay) while True: try: return infunc(*args, **kwargs) except Exception as exc: this_exc_message = encodeutils.exception_to_unicode(exc) if this_exc_message == last_exc_message: same_failure_count += 1 else: same_failure_count = 1 if this_exc_message != last_exc_message or watch.expired(): # The watch has expired or the exception message # changed, so time to log it again... logging.exception( 'Unexpected exception occurred %d time(s)... ' 'retrying.' % same_failure_count) if not watch.has_started(): watch.start() else: watch.restart() same_failure_count = 0 last_exc_message = this_exc_message time.sleep(retry_delay) return wrapper # This is needed to handle when the decorator has args or the decorator # doesn't have args, python is rather weird here... if kwargs or not args: return decorator else: if len(args) == 1: return decorator(args[0]) else: return decorator class exception_filter: """A context manager that prevents some exceptions from being raised. Use this class as a decorator for a function that returns whether a given exception should be ignored, in cases where complex logic beyond subclass matching is required. e.g. >>> @exception_filter >>> def ignore_test_assertions(ex): ... return isinstance(ex, AssertionError) and 'test' in str(ex) The filter matching function can then be used as a context manager: >>> with ignore_test_assertions: ... assert False, 'This is a test' or called directly: >>> try: ... assert False, 'This is a test' ... except Exception as ex: ... ignore_test_assertions(ex) Any non-matching exception will be re-raised. When the filter is used as a context manager, the traceback for re-raised exceptions is always preserved. When the filter is called as a function, the traceback is preserved provided that no other exceptions have been raised in the intervening time. The context manager method is preferred for this reason except in cases where the ignored exception affects control flow. """ def __init__(self, should_ignore_ex): self._should_ignore_ex = should_ignore_ex if all(hasattr(should_ignore_ex, a) for a in functools.WRAPPER_ASSIGNMENTS): functools.update_wrapper(self, should_ignore_ex) def __get__(self, obj, owner): return type(self)(self._should_ignore_ex.__get__(obj, owner)) def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): if exc_val is not None: return self._should_ignore_ex(exc_val) def __call__(self, ex): """Re-raise any exception value not being filtered out. If the exception was the last to be raised, it will be re-raised with its original traceback. """ exc_type, exc_val, traceback = sys.exc_info() try: if not self._should_ignore_ex(ex): if exc_val is ex: try: if exc_val is None: exc_val = exc_type() if exc_val.__traceback__ is not traceback: raise exc_val.with_traceback(traceback) raise exc_val finally: exc_val = None traceback = None else: raise ex finally: del exc_type, exc_val, traceback ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/oslo_utils/fileutils.py0000664000175000017500000001371000000000000020510 0ustar00zuulzuul00000000000000# Copyright 2011 OpenStack Foundation. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ File utilities. .. versionadded:: 1.8 """ import contextlib import errno import hashlib import json import os import stat import tempfile import time import yaml from oslo_utils import excutils _DEFAULT_MODE = stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO def ensure_tree(path, mode=_DEFAULT_MODE): """Create a directory (and any ancestor directories required) :param path: Directory to create :param mode: Directory creation permissions """ try: os.makedirs(path, mode) except OSError as exc: if exc.errno == errno.EEXIST: if not os.path.isdir(path): raise else: raise def delete_if_exists(path, remove=os.unlink): """Delete a file, but ignore file not found error. :param path: File to delete :param remove: Optional function to remove passed path """ try: remove(path) except OSError as e: if e.errno != errno.ENOENT: raise @contextlib.contextmanager def remove_path_on_error(path, remove=delete_if_exists): """Protect code that wants to operate on PATH atomically. Any exception will cause PATH to be removed. :param path: File to work with :param remove: Optional function to remove passed path """ try: yield except Exception: with excutils.save_and_reraise_exception(): remove(path) def write_to_tempfile(content, path=None, suffix='', prefix='tmp'): """Create a temporary file containing data. Create a temporary file containing specified content, with a specified filename suffix and prefix. The tempfile will be created in a default location, or in the directory `path`, if it is not None. `path` and its parent directories will be created if they don't exist. :param content: bytestring to write to the file :param path: same as parameter 'dir' for mkstemp :param suffix: same as parameter 'suffix' for mkstemp :param prefix: same as parameter 'prefix' for mkstemp For example: it can be used in database tests for creating configuration files. .. versionadded:: 1.9 """ if path: ensure_tree(path) (fd, path) = tempfile.mkstemp(suffix=suffix, dir=path, prefix=prefix) try: os.write(fd, content) finally: os.close(fd) return path def compute_file_checksum(path, read_chunksize=65536, algorithm='sha256'): """Compute checksum of a file's contents. :param path: Path to the file :param read_chunksize: Maximum number of bytes to be read from the file at once. Default is 65536 bytes or 64KB :param algorithm: The hash algorithm name to use. For example, 'md5', 'sha256', 'sha512' and so on. Default is 'sha256'. Refer to hashlib.algorithms_available for available algorithms :return: Hex digest string of the checksum .. versionadded:: 3.31.0 """ checksum = hashlib.new(algorithm) # Raises appropriate exceptions. with open(path, 'rb') as f: for chunk in iter(lambda: f.read(read_chunksize), b''): checksum.update(chunk) # Release greenthread, if greenthreads are not used it is a noop. time.sleep(0) return checksum.hexdigest() def last_bytes(path, num): """Return num bytes from the end of the file and unread byte count. Returns a tuple containing some content from the file and the number of bytes that appear in the file before the point at which reading started. The content will be at most ``num`` bytes, taken from the end of the file. If the file is smaller than ``num`` bytes the entire content of the file is returned. :param path: The file path to read :param num: The number of bytes to return :returns: (data, unread_bytes) """ with open(path, 'rb') as fp: try: fp.seek(-num, os.SEEK_END) except OSError as e: # seek() fails with EINVAL when trying to go before the start of # the file. It means that num is larger than the file size, so # just go to the start. if e.errno == errno.EINVAL: fp.seek(0, os.SEEK_SET) else: raise unread_bytes = fp.tell() return (fp.read(), unread_bytes) def is_json(file_path): """Check if file is of json type or not. This function try to load the input file using json.loads() and return False if ValueError otherwise True. :param file_path: The file path to check :returns: bool """ with open(file_path) as fh: data = fh.read() try: json.loads(data) return True except ValueError: return False def is_yaml(file_path): """Check if file is of yaml type or not. This function try to load the input file using yaml.safe_load() and return True if loadable. Because every json file can be loadable in yaml, so this function return False if file is loadable using json.loads() means it is json file. :param file_path: The file path to check :returns: bool """ with open(file_path) as fh: data = fh.read() is_yaml = False try: json.loads(data) except ValueError: try: yaml.safe_load(data) is_yaml = True except yaml.scanner.ScannerError: pass return is_yaml ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/oslo_utils/fixture.py0000664000175000017500000000646100000000000020203 0ustar00zuulzuul00000000000000# Copyright 2015 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ Test fixtures. .. versionadded:: 1.3 """ import threading import fixtures from oslo_utils import timeutils from oslo_utils import uuidutils class TimeFixture(fixtures.Fixture): """A fixture for overriding the time returned by timeutils.utcnow(). :param override_time: datetime instance or list thereof. If not given, defaults to the current UTC time. """ def __init__(self, override_time=None): super().__init__() self._override_time = override_time def setUp(self): super().setUp() timeutils.set_time_override(self._override_time) self.addCleanup(timeutils.clear_time_override) def advance_time_delta(self, timedelta): """Advance overridden time using a datetime.timedelta.""" timeutils.advance_time_delta(timedelta) def advance_time_seconds(self, seconds): """Advance overridden time by seconds.""" timeutils.advance_time_seconds(seconds) class _UUIDSentinels: """Registry of dynamically created, named, random UUID strings in regular (with hyphens) and similar to some keystone IDs (without hyphens) formats. An instance of this class will dynamically generate attributes as they are referenced, associating a random UUID string with each. Thereafter, referring to the same attribute will give the same UUID for the life of the instance. Plan accordingly. Usage:: from oslo_utils.fixture import uuidsentinel as uuids from oslo_utils.fixture import keystoneidsentinel as keystids ... foo = uuids.foo do_a_thing(foo) # Referencing the same sentinel again gives the same value assert foo == uuids.foo # But a different one will be different assert foo != uuids.bar # Same approach is valid for keystoneidsentinel: data = create_some_data_structure(keystids.bar, var1, var2, var3) assert extract_bar(data) == keystids.bar """ def __init__(self, is_dashed=True): self._sentinels = {} self._lock = threading.Lock() self.is_dashed = is_dashed def __getattr__(self, name): if name.startswith('_'): raise AttributeError('Sentinels must not start with _') with self._lock: if name not in self._sentinels: self._sentinels[name] = uuidutils.generate_uuid( dashed=self.is_dashed) return self._sentinels[name] # Singleton sentinel instance. Caveat emptor: using this multiple times in the # same process (including across multiple modules) will result in the same # values uuidsentinel = _UUIDSentinels() keystoneidsentinel = _UUIDSentinels(is_dashed=False) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1740145268.277054 oslo_utils-8.2.0/oslo_utils/imageutils/0000775000175000017500000000000000000000000020277 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/oslo_utils/imageutils/__init__.py0000664000175000017500000000003600000000000022407 0ustar00zuulzuul00000000000000from .qemu import QemuImgInfo ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/oslo_utils/imageutils/__main__.py0000664000175000017500000000124700000000000022375 0ustar00zuulzuul00000000000000# All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from oslo_utils.imageutils.cli import main if __name__ == "__main__": main() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/oslo_utils/imageutils/cli.py0000664000175000017500000000706500000000000021430 0ustar00zuulzuul00000000000000# All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import argparse import logging import os import sys import textwrap from oslo_utils.imageutils import format_inspector from oslo_utils.version import version_info def main(): """Run image security checks and give feedback. Runs the image format detector and related security checks against a provided image. Usage: python -m oslo_utils.imageutils -i /path/to/image [-v|--verbose] Default behavior is to communicate status via exit code: - Exit code of 0 indicates a safe image. - Exit code of 1 indicates an unsafe or missing image. If verbose mode is enabled, KEY=VALUE is output for several useful pieces of information about the image and imageutils: - SAFETY_CHECK_PASSED (bool) - VIRTUAL_SIZE (virtual disk size) - ACTUAL_SIZE (actual size of image) - IMAGE_FORMAT (format of image) - OSLO_UTILS_VERSION (version of oslo_utils currently in use) - FAILURE_REASONS (reasons for failure, if failed safety check) """ logging.basicConfig(level=logging.CRITICAL) oslo_utils_version = str(version_info) parser = argparse.ArgumentParser( prog='oslo.utils.imageutils', formatter_class=argparse.RawDescriptionHelpFormatter, description=(textwrap.dedent('''\ oslo.utils.imageutils image checking program. * Exit code of 0 indicates image passes safety check * Exit code of 1 indicates image fails safety check ''')), epilog=f"Testing using oslo.utils version {oslo_utils_version}") parser.add_argument('-v', '--verbose', action='store_true', help=("Print detailed information about the image in " "KEY=VALUE format. Defaults to no output.")) parser.add_argument('-i', '--image', action='store', required=True, metavar="IMG", help="Path to an image you wish to inspect.") args = parser.parse_args() image = args.image verbose = args.verbose if not os.path.exists(image) or not os.path.isfile(image): print('Image path %s provided does not exist' % image, file=sys.stderr) sys.exit(1) inspector = format_inspector.detect_file_format(image) safe = True try: inspector.safety_check() except format_inspector.SafetyCheckFailed as e: safe = False failure_reasons = [] for exc in e.failures.items(): failure_reasons.append("{}: {}".format(exc[0], exc[1])) virtual_size = inspector.virtual_size actual_size = inspector.actual_size fmt = str(inspector) if verbose: print(f"SAFETY_CHECK_PASSED={safe}") print(f"VIRTUAL_SIZE={virtual_size}") print(f"ACTUAL_SIZE={actual_size}") print(f"IMAGE_FORMAT=\"{fmt}\"") print(f"OSLO_UTILS_VERSION=\"{oslo_utils_version}\"") if safe: sys.exit(0) if verbose: print('FAILURE_REASONS=\'%s\'' % ','.join(failure_reasons)) sys.exit(1) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/oslo_utils/imageutils/format_inspector.py0000664000175000017500000016607100000000000024242 0ustar00zuulzuul00000000000000# Copyright 2020 Red Hat, Inc # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ This is a python implementation of virtual disk format inspection routines gathered from various public specification documents, as well as qemu disk driver code. It attempts to store and parse the minimum amount of data required, and in a streaming-friendly manner to collect metadata about complex-format images. """ import abc import struct import logging from oslo_utils._i18n import _ from oslo_utils import units LOG = logging.getLogger(__name__) def _chunked_reader(fileobj, chunk_size=512): while True: chunk = fileobj.read(chunk_size) if not chunk: break yield chunk class CaptureRegion: """Represents a region of a file we want to capture. A region of a file we want to capture requires a byte offset into the file and a length. This is expected to be used by a data processing loop, calling capture() with the most recently-read chunk. This class handles the task of grabbing the desired region of data across potentially multiple fractional and unaligned reads. :param offset: Byte offset into the file starting the region :param length: The length of the region :param min_length: Consider this region complete if it has captured at least this much data. This should generally NOT be used but may be required for certain formats with highly variable data structures. """ def __init__(self, offset, length, min_length=None): self.offset = offset self.length = length self.data = b'' self.min_length = min_length @property def complete(self): """Returns True when we have captured the desired data.""" if self.min_length is not None: return self.min_length <= len(self.data) else: return self.length == len(self.data) def capture(self, chunk, current_position): """Process a chunk of data. This should be called for each chunk in the read loop, at least until complete returns True. :param chunk: A chunk of bytes in the file :param current_position: The position of the file processed by the read loop so far. Note that this will be the position in the file *after* the chunk being presented. """ read_start = current_position - len(chunk) if (read_start <= self.offset <= current_position or self.offset <= read_start <= (self.offset + self.length)): if read_start < self.offset: lead_gap = self.offset - read_start else: lead_gap = 0 self.data += chunk[lead_gap:] self.data = self.data[:self.length] class EndCaptureRegion(CaptureRegion): """Represents a region that captures the last N bytes of a stream. This can only capture the last N bytes of a stream and not an arbitrary region referenced from the end of the file since in most cases we do not know how much data we will read. :param offset: Byte offset from the end of the stream to capture (which will also be the region length) """ def __init__(self, offset): super().__init__(offset, offset) # We don't want to indicate completeness until we have the data we # want *and* have reached EOF self._complete = False def capture(self, chunk, current_position): self.data += chunk self.data = self.data[0 - self.length:] self.offset = current_position - len(self.data) @property def complete(self): return super().complete and self._complete def finish(self): """Indicate that the entire stream has been read.""" self._complete = True class SafetyCheck: """Represents a named safety check on an inspector""" def __init__(self, name, target_fn, description=None): """A safety check, it's meta info, and result. @name should be a short name of the check (ideally no spaces) @target_fn is the implementation we run (no args) which returns either None if the check passes, or a string reason why it failed. @description is a optional longer-format human-readable string that describes the check. """ self.name = name self.target_fn = target_fn self.description = description def __call__(self): """Executes the target check function, records the result. Returns True if the check succeeded (i.e. no failure reason) or False if it did not. """ try: self.target_fn() except SafetyViolation: raise except Exception as e: LOG.error('Failed to run safety check %s on %s inspector: %s', self.name, self, e) raise SafetyViolation(_('Unexpected error')) @classmethod def null(cls): """The "null" safety check always returns True. This should only be used if there is no meaningful checks that can be done for a given format. """ return cls('null', lambda: None, _('This file format has no meaningful safety check')) @classmethod def banned(cls): """The "banned" safety check always returns False. This should be used for formats we want to identify but never allow, generally because they are unsupported by any of our users and/or we are unable to check for safety. """ def fail(): raise SafetyViolation(_('This file format is not allowed')) return cls('banned', fail, _('This file format is not allowed')) class ImageFormatError(Exception): """An unrecoverable image format error that aborts the process.""" pass class SafetyViolation(Exception): """Indicates a failure of a single safety violation.""" pass class SafetyCheckFailed(Exception): """Indictes that one or more of a series of safety checks failed.""" def __init__(self, failures): super().__init__(_('Safety checks failed: %s') % ','.join( failures.keys())) self.failures = failures class FileInspector(abc.ABC): """A stream-based disk image inspector. This base class works on raw images and is subclassed for more complex types. It is to be presented with the file to be examined one chunk at a time, during read processing and will only store as much data as necessary to determine required attributes of the file. """ # This should match what qemu-img thinks this format is NAME = '' def __init__(self, tracing=False): self._total_count = 0 # NOTE(danms): The logging in here is extremely verbose for a reason, # but should never really be enabled at that level at runtime. To # retain all that work and assist in future debug, we have a separate # debug flag that can be passed from a manual tool to turn it on. self._tracing = tracing self._capture_regions = {} self._safety_checks = {} self._finished = False self._initialize() if not self._safety_checks: # Make sure we actively declare some safety check, even if it # is a no-op. raise RuntimeError( 'All inspectors must define at least one safety check') def _trace(self, *args, **kwargs): if self._tracing: LOG.debug(*args, **kwargs) @abc.abstractmethod def _initialize(self): """Set up inspector before we start processing data. This should add the initial set of capture regions and safety checks. """ def finish(self): """Indicate that the entire stream has been read. This should be called when the entire stream has been completely read, which will mark any EndCaptureRegion objects as complete. """ self._finished = True for region in self._capture_regions.values(): if isinstance(region, EndCaptureRegion): region.finish() def _capture(self, chunk, only=None): if self._finished: raise RuntimeError('Inspector has been marked finished, ' 'no more data processing allowed') for name, region in self._capture_regions.items(): if only and name not in only: continue if isinstance(region, EndCaptureRegion) or not region.complete: region.capture(chunk, self._total_count) def eat_chunk(self, chunk): """Call this to present chunks of the file to the inspector.""" pre_regions = set(self._capture_regions.values()) pre_complete = {region for region in self._capture_regions.values() if region.complete} # Increment our position-in-file counter self._total_count += len(chunk) # Run through the regions we know of to see if they want this # data self._capture(chunk) # Let the format do some post-read processing of the stream self.post_process() # Check to see if the post-read processing added new regions # which may require the current chunk. new_regions = set(self._capture_regions.values()) - pre_regions if new_regions: self._capture(chunk, only=[self.region_name(r) for r in new_regions]) post_complete = {region for region in self._capture_regions.values() if region.complete} # Call the handler for any regions that are newly complete for region in post_complete - pre_complete: self.region_complete(self.region_name(region)) def post_process(self): """Post-read hook to process what has been read so far. This will be called after each chunk is read and potentially captured by the defined regions. If any regions are defined by this call, those regions will be presented with the current chunk in case it is within one of the new regions. """ pass def region(self, name): """Get a CaptureRegion by name.""" return self._capture_regions[name] def region_name(self, region): """Return the region name for a region object.""" for name in self._capture_regions: if self._capture_regions[name] is region: return name raise ValueError('No such region') def new_region(self, name, region): """Add a new CaptureRegion by name.""" if self.has_region(name): # This is a bug, we tried to add the same region twice raise ImageFormatError('Inspector re-added region %s' % name) self._capture_regions[name] = region def has_region(self, name): """Returns True if named region has been defined.""" return name in self._capture_regions def delete_region(self, name): """Remove a capture region by name. This will raise KeyError if the region does not exist. """ del self._capture_regions[name] def region_complete(self, region_name): """Called when a region becomes complete. Subclasses may implement this if they need to do one-time processing of a region's data. """ pass def add_safety_check(self, check): if not isinstance(check, SafetyCheck): raise RuntimeError(_('Unable to add safety check of type %s') % ( type(check).__name__)) if check.name in self._safety_checks: raise RuntimeError(_('Duplicate check of name %s') % check.name) self._safety_checks[check.name] = check @property @abc.abstractmethod def format_match(self): """Returns True if the file appears to be the expected format.""" @property def virtual_size(self): """Returns the virtual size of the disk image, or zero if unknown.""" return self._total_count @property def actual_size(self): """Returns the total size of the file, usually smaller than virtual_size. NOTE: this will only be accurate if the entire file is read and processed. """ return self._total_count @property def complete(self): """Returns True if we have all the information needed.""" return all(r.complete for r in self._capture_regions.values()) def __str__(self): """The string name of this file format.""" return self.NAME @property def context_info(self): """Return info on amount of data held in memory for auditing. This is a dict of region:sizeinbytes items that the inspector uses to examine the file. """ return {name: len(region.data) for name, region in self._capture_regions.items()} @classmethod def from_file(cls, filename): """Read as much of a file as necessary to complete inspection. NOTE: Because we only read as much of the file as necessary, the actual_size property will not reflect the size of the file, but the amount of data we read before we satisfied the inspector. Raises ImageFormatError if we cannot parse the file. """ inspector = cls() with open(filename, 'rb') as f: for chunk in _chunked_reader(f): inspector.eat_chunk(chunk) if inspector.complete: # No need to eat any more data break inspector.finish() if not inspector.complete or not inspector.format_match: raise ImageFormatError('File is not in requested format') return inspector def safety_check(self): """Perform all checks to determine if this file is safe. Returns if safe, raises otherwise. It may raise ImageFormatError if safety cannot be guaranteed because of parsing or other errors. It will raise SafetyCheckFailed if one or more checks fails. """ if not self.complete: raise ImageFormatError( _('Incomplete file cannot be safety checked')) if not self.format_match: raise ImageFormatError( _('Unable to safety check format %s ' 'because content does not match') % self) failures = {} for check in self._safety_checks.values(): try: result = check() if result is not None: raise RuntimeError('check returned result') except SafetyViolation as exc: exc.check = check failures[check.name] = exc LOG.warning('Safety check %s on %s failed because %s', check.name, self, exc) if failures: raise SafetyCheckFailed(failures) class RawFileInspector(FileInspector): NAME = 'raw' def _initialize(self): """Raw files have nothing to capture and no safety checks.""" self.add_safety_check(SafetyCheck.null()) @property def format_match(self): # By definition, raw files are unformatted and thus we always match return True # The qcow2 format consists of a big-endian 72-byte header, of which # only a small portion has information we care about: # # Dec Hex Name # 0 0x00 Magic 4-bytes 'QFI\xfb' # 4 0x04 Version (uint32_t, should always be 2 for modern files) # . . . # 8 0x08 Backing file offset (uint64_t) # 24 0x18 Size in bytes (unint64_t) # . . . # 72 0x48 Incompatible features bitfield (6 bytes) # # https://gitlab.com/qemu-project/qemu/-/blob/master/docs/interop/qcow2.txt class QcowInspector(FileInspector): """QEMU QCOW Format This should only require about 32 bytes of the beginning of the file to determine the virtual size, and 104 bytes to perform the safety check. This recognizes the (very) old v1 format but will raise a SafetyViolation for it, as it should definitely not be in production use at this point. """ NAME = 'qcow2' BF_OFFSET = 0x08 BF_OFFSET_LEN = 8 I_FEATURES = 0x48 I_FEATURES_LEN = 8 I_FEATURES_DATAFILE_BIT = 3 I_FEATURES_MAX_BIT = 4 def _initialize(self): self.qemu_header_info = {} self.new_region('header', CaptureRegion(0, 512)) self.add_safety_check( SafetyCheck('backing_file', self.check_backing_file)) self.add_safety_check( SafetyCheck('data_file', self.check_data_file)) self.add_safety_check( SafetyCheck('unknown_features', self.check_unknown_features)) def region_complete(self, region): self.qemu_header_info = dict(zip( ('magic', 'version', 'bf_offset', 'bf_sz', 'cluster_bits', 'size'), struct.unpack('>4sIQIIQ', self.region('header').data[:32]))) if not self.format_match: self.qemu_header_info = {} @property def virtual_size(self): return self.qemu_header_info.get('size', 0) @property def format_match(self): if not self.region('header').complete: return False return self.qemu_header_info.get('magic') == b'QFI\xFB' def check_backing_file(self): bf_offset_bytes = self.region('header').data[ self.BF_OFFSET:self.BF_OFFSET + self.BF_OFFSET_LEN] # nonzero means "has a backing file" bf_offset, = struct.unpack('>Q', bf_offset_bytes) if bf_offset != 0: raise SafetyViolation('Image has a backing file') def check_unknown_features(self): ver = self.qemu_header_info.get('version') if ver == 2: # Version 2 did not have the feature flag array, so no need to # check it here. return elif ver != 3: raise SafetyViolation('Unsupported qcow2 version') i_features = self.region('header').data[ self.I_FEATURES:self.I_FEATURES + self.I_FEATURES_LEN] # This is the maximum byte number we should expect any bits to be set max_byte = self.I_FEATURES_MAX_BIT // 8 # The flag bytes are in big-endian ordering, so if we process # them in index-order, they're reversed for i, byte_num in enumerate(reversed(range(self.I_FEATURES_LEN))): if byte_num == max_byte: # If we're in the max-allowed byte, allow any bits less than # the maximum-known feature flag bit to be set allow_mask = ((1 << (self.I_FEATURES_MAX_BIT % 8)) - 1) elif byte_num > max_byte: # If we're above the byte with the maximum known feature flag # bit, then we expect all zeroes allow_mask = 0x0 else: # Any earlier-than-the-maximum byte can have any of the flag # bits set allow_mask = 0xFF if i_features[i] & ~allow_mask: LOG.warning('Found unknown feature bit in byte %i: %s/%s', byte_num, bin(i_features[byte_num] & ~allow_mask), bin(allow_mask)) raise SafetyViolation('Unknown QCOW2 features found') def check_data_file(self): i_features = self.region('header').data[ self.I_FEATURES:self.I_FEATURES + self.I_FEATURES_LEN] # First byte of bitfield, which is i_features[7] byte = self.I_FEATURES_LEN - 1 - self.I_FEATURES_DATAFILE_BIT // 8 # Third bit of bitfield, which is 0x04 bit = 1 << (self.I_FEATURES_DATAFILE_BIT - 1 % 8) if bool(i_features[byte] & bit): raise SafetyViolation('Image has data_file set') class QEDInspector(FileInspector): NAME = 'qed' def _initialize(self): self.new_region('header', CaptureRegion(0, 512)) # QED format is not supported by anyone, but we want to detect it # and mark it as just always unsafe. self.add_safety_check(SafetyCheck.banned()) @property def format_match(self): if not self.region('header').complete: return False return self.region('header').data.startswith(b'QED\x00') # The VHD (or VPC as QEMU calls it) format consists of a big-endian # 512-byte "footer" at the beginning of the file with various # information, most of which does not matter to us: # # Dec Hex Name # 0 0x00 Magic string (8-bytes, always 'conectix') # 40 0x28 Disk size (uint64_t) # # https://github.com/qemu/qemu/blob/master/block/vpc.c class VHDInspector(FileInspector): """Connectix/MS VPC VHD Format This should only require about 512 bytes of the beginning of the file to determine the virtual size. """ NAME = 'vhd' def _initialize(self): self.new_region('header', CaptureRegion(0, 512)) self.add_safety_check(SafetyCheck.null()) @property def format_match(self): return self.region('header').data.startswith(b'conectix') @property def virtual_size(self): if not self.region('header').complete: return 0 if not self.format_match: return 0 return struct.unpack('>Q', self.region('header').data[40:48])[0] # The VHDX format consists of a complex dynamic little-endian # structure with multiple regions of metadata and data, linked by # offsets with in the file (and within regions), identified by MSFT # GUID strings. The header is a 320KiB structure, only a few pieces of # which we actually need to capture and interpret: # # Dec Hex Name # 0 0x00000 Identity (Technically 9-bytes, padded to 64KiB, the first # 8 bytes of which are 'vhdxfile') # 196608 0x30000 The Region table (64KiB of a 32-byte header, followed # by up to 2047 36-byte region table entry structures) # # The region table header includes two items we need to read and parse, # which are: # # 196608 0x30000 4-byte signature ('regi') # 196616 0x30008 Entry count (uint32-t) # # The region table entries follow the region table header immediately # and are identified by a 16-byte GUID, and provide an offset of the # start of that region. We care about the "metadata region", identified # by the METAREGION class variable. The region table entry is (offsets # from the beginning of the entry, since it could be in multiple places): # # 0 0x00000 16-byte MSFT GUID # 16 0x00010 Offset of the actual metadata region (uint64_t) # # When we find the METAREGION table entry, we need to grab that offset # and start examining the region structure at that point. That # consists of a metadata table of structures, which point to places in # the data in an unstructured space that follows. The header is # (offsets relative to the region start): # # 0 0x00000 8-byte signature ('metadata') # . . . # 16 0x00010 2-byte entry count (up to 2047 entries max) # # This header is followed by the specified number of metadata entry # structures, identified by GUID: # # 0 0x00000 16-byte MSFT GUID # 16 0x00010 4-byte offset (uint32_t, relative to the beginning of # the metadata region) # # We need to find the "Virtual Disk Size" metadata item, identified by # the GUID in the VIRTUAL_DISK_SIZE class variable, grab the offset, # add it to the offset of the metadata region, and examine that 8-byte # chunk of data that follows. # # The "Virtual Disk Size" is a naked uint64_t which contains the size # of the virtual disk, and is our ultimate target here. # # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-vhdx/83e061f8-f6e2-4de1-91bd-5d518a43d477 class VHDXInspector(FileInspector): """MS VHDX Format This requires some complex parsing of the stream. The first 256KiB of the image is stored to get the header and region information, and then we capture the first metadata region to read those records, find the location of the virtual size data and parse it. This needs to store the metadata table entries up until the VDS record, which may consist of up to 2047 32-byte entries at max. Finally, it must store a chunk of data at the offset of the actual VDS uint64. """ NAME = 'vhdx' METAREGION = '8B7CA206-4790-4B9A-B8FE-575F050F886E' VIRTUAL_DISK_SIZE = '2FA54224-CD1B-4876-B211-5DBED83BF4B8' VHDX_METADATA_TABLE_MAX_SIZE = 32 * 2048 # From qemu def _initialize(self): self.new_region('ident', CaptureRegion(0, 32)) self.new_region('header', CaptureRegion(192 * 1024, 64 * 1024)) self.add_safety_check(SafetyCheck.null()) def post_process(self): # After reading a chunk, we may have the following conditions: # # 1. We may have just completed the header region, and if so, # we need to immediately read and calculate the location of # the metadata region, as it may be starting in the same # read we just did. # 2. We may have just completed the metadata region, and if so, # we need to immediately calculate the location of the # "virtual disk size" record, as it may be starting in the # same read we just did. if self.region('header').complete and not self.has_region('metadata'): region = self._find_meta_region() if region: self.new_region('metadata', region) elif self.has_region('metadata') and not self.has_region('vds'): region = self._find_meta_entry(self.VIRTUAL_DISK_SIZE) if region: self.new_region('vds', region) @property def format_match(self): return self.region('ident').data.startswith(b'vhdxfile') @staticmethod def _guid(buf): """Format a MSFT GUID from the 16-byte input buffer.""" guid_format = '= 2048: raise ImageFormatError('Region count is %i (limit 2047)' % count) # Process the regions until we find the metadata one; grab the # offset and return self._trace('Region entry first is %x', region_entry_first) self._trace('Region entries %i', count) meta_offset = 0 for i in range(0, count): entry_start = region_entry_first + (i * 32) entry_end = entry_start + 32 entry = self.region('header').data[entry_start:entry_end] self._trace('Entry offset is %x', entry_start) # GUID is the first 16 bytes guid = self._guid(entry[:16]) if guid == self.METAREGION: # This entry is the metadata region entry meta_offset, meta_len, meta_req = struct.unpack( '= 2048: raise ImageFormatError( 'Metadata item count is %i (limit 2047)' % count) for i in range(0, count): entry_offset = 32 + (i * 32) guid = self._guid(meta_buffer[entry_offset:entry_offset + 16]) if guid == desired_guid: # Found the item we are looking for by id. # Stop our region from capturing item_offset, item_length, _reserved = struct.unpack( '6sh32s32s32sI', self.region('header').data[:108]) names = ['magic', 'version', 'cipher_alg', 'cipher_mode', 'hash', 'payload_offset'] return dict(zip(names, fields)) def check_version(self): header = self.header_items if header['version'] != 1: raise SafetyViolation( 'LUKS version %i is not supported' % header['version']) @property def virtual_size(self): # NOTE(danms): This will not be correct until/unless the whole stream # has been read, since all we have is (effectively the size of the # header. This is similar to how RawFileInspector works. return super().virtual_size - self.header_items['payload_offset'] * 512 class InspectWrapper: """A file-like object that wraps another and detects the format. This passes chunks to a group of format inspectors (default: all) while reading. After the stream is finished (or enough has been read to make a confident decision), the format attribute will provide the inspector object that matched. :param source: The file-like input stream to wrap :param expected_format: The format name anticipated to match, if any. If set to a format name, reading of the stream will be interrupted if the matching inspector raises an error (indicting a mismatch or any other problem). This allows the caller to abort before all data is processed. :param allowed_formats: A list of format names that limits the inspector objects that will be used. This may be a security hole if used improperly, but may be used to limit the detected formats to some smaller scope. """ def __init__(self, source, expected_format=None, allowed_formats=None): self._source = source self._expected_format = expected_format self._errored_inspectors = set() self._inspectors = {v() for k, v in ALL_FORMATS.items() if not allowed_formats or k in allowed_formats} self._finished = False def __iter__(self): return self def _process_chunk(self, chunk): for inspector in [i for i in self._inspectors if i not in self._errored_inspectors]: try: inspector.eat_chunk(chunk) except Exception as e: if inspector.NAME == self._expected_format: # If our desired inspector has failed, we cannot continue raise # Absolutely do not allow the format inspector to break # our streaming of the image for non-expected formats. If we # failed, just stop trying, log and keep going. if not self._expected_format: # If we are expecting to parse a specific format, we do # not need to log scary messages about the other formats # failing to parse the data as expected. LOG.debug('Format inspector for %s does not match, ' 'excluding from consideration (%s)', inspector.NAME, e) self._errored_inspectors.add(inspector) else: # If we are expecting a format, have read enough data to # satisfy that format's inspector, and no match is detected, # abort the stream immediately to save having to read the # entire thing before we signal the mismatch. if (inspector.NAME == self._expected_format and inspector.complete and not inspector.format_match): raise ImageFormatError( 'Content does not match expected format %r' % ( inspector.NAME)) def __next__(self): try: chunk = next(self._source) except StopIteration: self._finish() raise self._process_chunk(chunk) return chunk def read(self, size): chunk = self._source.read(size) self._process_chunk(chunk) return chunk def _finish(self): for inspector in self._inspectors: inspector.finish() self._finished = True def close(self): if hasattr(self._source, 'close'): self._source.close() self._finish() @property def formats(self): """The formats (potentially multiple) determined from the content. This is just like format, but returns a list of formats that matched, which may be more than one if appropriate. This should generally not be used as it is safer to allow one and only one format. However, there are situations where multiple formats could be detected legitimately (i.e. bootable ISOs) where we need to expose the case where we have found more than one. If no specific matches are made, this will return a list with just the Raw inspector, but will never include Raw in combination with others. This will be None if a decision has not been reached. """ non_raw = {i for i in self._inspectors if i.NAME != 'raw'} complete = all([i.complete for i in non_raw]) matches = [i for i in non_raw if i.format_match] if not complete and not self._finished: # We do not know what our format is if we're still in progress # of reading the stream and have incomplete inspectors. However, # if EOF has been signaled, then we can assume the incomplete ones # are not matches. return None if not matches: try: # If nothing *specific* matched, we return the raw format to # indicate that we do not recognize this content at all. return [x for x in self._inspectors if str(x) == 'raw'] except IndexError: raise ImageFormatError( 'Content does not match any allowed format') return matches @property def format(self): """The format determined from the content. If this is None, a decision has not been reached. Otherwise, it is a FileInspector that matches (which may be RawFileInspector if no other formats matched and enough of the stream has been read to make that determination). If more than one format matched, then ImageFormatError is raised. If the allowed_formats was constrained and raw was not included, then this will raise ImageFormatError to indicate that no suitable match was found. """ matches = self.formats if matches is None: return matches elif len(matches) > 1: # Multiple format matches mean that not only can we not return a # decision here, but also means that there may be something # nefarious going on (i.e. hiding one header in another). raise ImageFormatError('Multiple formats detected: %s' % ','.join( str(i) for i in matches)) else: try: # The expected outcome of this is a single match of something # specific return matches[0] except IndexError: raise ImageFormatError( 'Content does not match any allowed format') ALL_FORMATS = { 'raw': RawFileInspector, 'qcow2': QcowInspector, 'vhd': VHDInspector, 'vhdx': VHDXInspector, 'vmdk': VMDKInspector, 'vdi': VDIInspector, 'qed': QEDInspector, 'iso': ISOInspector, 'gpt': GPTInspector, 'luks': LUKSInspector, } def get_inspector(format_name): """Returns a FormatInspector class based on the given name. :param format_name: The name of the disk_format (raw, qcow2, etc). :returns: A FormatInspector or None if unsupported. """ return ALL_FORMATS.get(format_name) def detect_file_format(filename): """Attempts to detect the format of a file. This runs through a file one time, running all the known inspectors in parallel. It stops reading the file once all of them matches or all of them are sure they don't match. :param filename: The path to the file to inspect. :returns: A FormatInspector instance matching the file. :raises: ImageFormatError if multiple formats are detected. """ with open(filename, 'rb') as f: wrapper = InspectWrapper(f) try: for _chunk in _chunked_reader(wrapper, 4096): if wrapper.format: return wrapper.format finally: wrapper.close() return wrapper.format ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/oslo_utils/imageutils/qemu.py0000664000175000017500000002077100000000000021627 0ustar00zuulzuul00000000000000# Copyright 2010 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. # All Rights Reserved. # Copyright (c) 2010 Citrix Systems, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ Helper methods to deal with images. .. versionadded:: 3.1 .. versionchanged:: 3.14.0 add paramter format. """ import json import re import debtcollector from oslo_utils._i18n import _ from oslo_utils import strutils class QemuImgInfo: """Parse Qemu image information from command `qemu-img info`'s output. The instance of :class:`QemuImgInfo` has properties: `image`, `backing_file`, `file_format`, `virtual_size`, `cluster_size`, `disk_size`, `snapshots` and `encrypted`. The parameter format can be set to 'json' or 'human'. With 'json' format output, qemu image information will be parsed more easily and readable. However 'human' format support will be dropped in next cycle and only 'json' format will be supported. Prefer to use 'json' instead of 'human'. """ BACKING_FILE_RE = re.compile((r"^(.*?)\s*\(actual\s+path\s*:" r"\s+(.*?)\)\s*$"), re.I) TOP_LEVEL_RE = re.compile(r"^([\w\d\s\_\-]+):(.*)$") SIZE_RE = re.compile(r"([0-9]+[eE][-+][0-9]+|\d*\.?\d+)" r"\s*(\w+)?(\s*\(\s*(\d+)\s+bytes\s*\))?", re.I) def __init__(self, cmd_output=None, format='human'): if format == 'json': details = json.loads(cmd_output or '{}') self.image = details.get('filename') self.backing_file = details.get('backing-filename') self.backing_file_format = details.get('backing-filename-format') self.file_format = details.get('format') self.virtual_size = details.get('virtual-size') self.cluster_size = details.get('cluster-size') self.disk_size = details.get('actual-size') self.snapshots = details.get('snapshots', []) self.encrypted = 'yes' if details.get('encrypted') else None self.format_specific = details.get('format-specific') else: if cmd_output is not None: debtcollector.deprecate( 'The human format is deprecated and the format parameter ' 'will be removed. Use explicitly json instead', version="xena", category=FutureWarning) details = self._parse(cmd_output or '') self.image = details.get('image') self.backing_file = details.get('backing_file') self.backing_file_format = details.get('backing_file_format') self.file_format = details.get('file_format') self.virtual_size = details.get('virtual_size') self.cluster_size = details.get('cluster_size') self.disk_size = details.get('disk_size') self.snapshots = details.get('snapshot_list', []) self.encrypted = details.get('encrypted') self.format_specific = None def __str__(self): lines = [ 'image: %s' % self.image, 'file_format: %s' % self.file_format, 'virtual_size: %s' % self.virtual_size, 'disk_size: %s' % self.disk_size, 'cluster_size: %s' % self.cluster_size, 'backing_file: %s' % self.backing_file, 'backing_file_format: %s' % self.backing_file_format, ] if self.snapshots: lines.append("snapshots: %s" % self.snapshots) if self.encrypted: lines.append("encrypted: %s" % self.encrypted) if self.format_specific: lines.append("format_specific: %s" % self.format_specific) return "\n".join(lines) def _canonicalize(self, field): # Standardize on underscores/lc/no dash and no spaces # since qemu seems to have mixed outputs here... and # this format allows for better integration with python # - i.e. for usage in kwargs and such... field = field.lower().strip() for c in (" ", "-"): field = field.replace(c, '_') return field def _extract_bytes(self, details): # Replace it with the byte amount real_size = self.SIZE_RE.search(details) if not real_size: raise ValueError(_('Invalid input value "%s".') % details) magnitude = real_size.group(1) if "e" in magnitude.lower(): magnitude = format(float(real_size.group(1)), '.0f') unit_of_measure = real_size.group(2) bytes_info = real_size.group(3) if bytes_info: return int(real_size.group(4)) elif not unit_of_measure: return int(magnitude) # Allow abbreviated unit such as K to mean KB for compatibility. if len(unit_of_measure) == 1 and unit_of_measure != 'B': unit_of_measure += 'B' return strutils.string_to_bytes( '{}{}'.format(magnitude, unit_of_measure), return_int=True) def _extract_details(self, root_cmd, root_details, lines_after): real_details = root_details if root_cmd == 'backing_file': # Replace it with the real backing file backing_match = self.BACKING_FILE_RE.match(root_details) if backing_match: real_details = backing_match.group(2).strip() elif root_cmd in ['virtual_size', 'cluster_size', 'disk_size']: # Replace it with the byte amount (if we can convert it) if root_details in ('None', 'unavailable'): real_details = 0 else: real_details = self._extract_bytes(root_details) elif root_cmd == 'file_format': real_details = real_details.strip().lower() elif root_cmd == 'snapshot_list': # Next line should be a header, starting with 'ID' if not lines_after or not lines_after.pop(0).startswith("ID"): msg = _("Snapshot list encountered but no header found!") raise ValueError(msg) real_details = [] # This is the sprintf pattern we will try to match # "%-10s%-20s%7s%20s%15s" # ID TAG VM SIZE DATE VM CLOCK (current header) while lines_after: line = lines_after[0] line_pieces = line.split() if len(line_pieces) != 6: break # Check against this pattern in the final position # "%02d:%02d:%02d.%03d" date_pieces = line_pieces[5].split(":") if len(date_pieces) != 3: break lines_after.pop(0) real_details.append({ 'id': line_pieces[0], 'tag': line_pieces[1], 'vm_size': line_pieces[2], 'date': line_pieces[3], 'vm_clock': line_pieces[4] + " " + line_pieces[5], }) return real_details def _parse(self, cmd_output): # Analysis done of qemu-img.c to figure out what is going on here # Find all points start with some chars and then a ':' then a newline # and then handle the results of those 'top level' items in a separate # function. # # TODO(harlowja): newer versions might have a json output format # we should switch to that whenever possible. # see: http://bit.ly/XLJXDX contents = {} lines = [x for x in cmd_output.splitlines() if x.strip()] while lines: line = lines.pop(0) top_level = self.TOP_LEVEL_RE.match(line) if top_level: root = self._canonicalize(top_level.group(1)) if not root: continue root_details = top_level.group(2).strip() details = self._extract_details(root, root_details, lines) contents[root] = details return contents ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/oslo_utils/importutils.py0000664000175000017500000000724700000000000021113 0ustar00zuulzuul00000000000000# Copyright 2011 OpenStack Foundation. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ Import related utilities and helper functions. """ import sys import traceback def import_class(import_str): """Returns a class from a string including module and class. .. versionadded:: 0.3 """ mod_str, _sep, class_str = import_str.rpartition('.') __import__(mod_str) try: return getattr(sys.modules[mod_str], class_str) except AttributeError: raise ImportError('Class %s cannot be found (%s)' % (class_str, traceback.format_exception(*sys.exc_info()))) def import_object(import_str, *args, **kwargs): """Import a class and return an instance of it. .. versionadded:: 0.3 """ return import_class(import_str)(*args, **kwargs) def import_object_ns(name_space, import_str, *args, **kwargs): """Tries to import object from default namespace. Imports a class and return an instance of it, first by trying to find the class in a default namespace, then failing back to a full path if not found in the default namespace. .. versionadded:: 0.3 .. versionchanged:: 2.6 Don't capture :exc:`ImportError` when instanciating the object, only when importing the object class. """ import_value = "{}.{}".format(name_space, import_str) try: cls = import_class(import_value) except ImportError: cls = import_class(import_str) return cls(*args, **kwargs) def import_module(import_str): """Import a module. .. versionadded:: 0.3 """ __import__(import_str) return sys.modules[import_str] def import_versioned_module(module, version, submodule=None): """Import a versioned module in format {module}.v{version][.{submodule}]. :param module: the module name. :param version: the version number. :param submodule: the submodule name. :raises ValueError: For any invalid input. .. versionadded:: 0.3 .. versionchanged:: 3.17 Added *module* parameter. """ # NOTE(gcb) Disallow parameter version include character '.' if '.' in '%s' % version: raise ValueError("Parameter version shouldn't include character '.'.") module_str = '{}.v{}'.format(module, version) if submodule: module_str = '.'.join((module_str, submodule)) return import_module(module_str) def try_import(import_str, default=None): """Try to import a module and if it fails return default.""" try: return import_module(import_str) except ImportError: return default def import_any(module, *modules): """Try to import a module from a list of modules. :param modules: A list of modules to try and import :returns: The first module found that can be imported :raises ImportError: If no modules can be imported from list .. versionadded:: 3.8 """ for module_name in (module,) + modules: imported_module = try_import(module_name) if imported_module: return imported_module raise ImportError('Unable to import any modules from the list %s' % str(modules)) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1740145268.2650516 oslo_utils-8.2.0/oslo_utils/locale/0000775000175000017500000000000000000000000017373 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1740145268.2650516 oslo_utils-8.2.0/oslo_utils/locale/de/0000775000175000017500000000000000000000000017763 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1740145268.277054 oslo_utils-8.2.0/oslo_utils/locale/de/LC_MESSAGES/0000775000175000017500000000000000000000000021550 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/oslo_utils/locale/de/LC_MESSAGES/oslo_utils.po0000664000175000017500000000420700000000000024307 0ustar00zuulzuul00000000000000# Translations template for oslo.utils. # Copyright (C) 2015 ORGANIZATION # This file is distributed under the same license as the oslo.utils project. # # Translators: # Andreas Jaeger , 2014 # Ettore Atalan , 2014 # Andreas Jaeger , 2016. #zanata msgid "" msgstr "" "Project-Id-Version: oslo.utils 3.7.1.dev13\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" "POT-Creation-Date: 2016-05-02 20:03+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2016-01-31 08:10+0000\n" "Last-Translator: Andreas Jaeger \n" "Language: de\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "Generated-By: Babel 2.0\n" "X-Generator: Zanata 3.7.3\n" "Language-Team: German\n" #, python-format msgid "%s is not a string or unicode" msgstr "%s ist keine Zeichenkette oder Unicode" #, python-format msgid "" "Bad prefix or mac format for generating IPv6 address by EUI-64: %(prefix)s, " "%(mac)s:" msgstr "" "Falsches Präfix- oder MAC-Format für das Generieren der IPv6-Adresse durch " "EUI-64: %(prefix)s, %(mac)s:" #, python-format msgid "Bad prefix type for generating IPv6 address by EUI-64: %s" msgstr "" "Falscher Präfixtyp für das Generieren der IPv6-Adresse durch EUI-64: %s" #, python-format msgid "Invalid input value \"%s\"." msgstr "Ungültiger Eingabewert \"%s\"." #, python-format msgid "Invalid string format: %s" msgstr "Ungültiges Stringformat: %s" #, python-format msgid "Invalid unit system: \"%s\"" msgstr "Ungültiges Einheitensystem: \"%s\"" msgid "Snapshot list encountered but no header found!" msgstr "Momentaufnahmenliste gefunden, aber kein Header gefunden!" msgid "Unable to generate IP address by EUI64 for IPv4 prefix" msgstr "" "IP-Adresse kann nicht mithilfe von EUI64 mit dem IPv4-Präfix generiert werden" #, python-format msgid "Unrecognized value '%(val)s', acceptable values are: %(acceptable)s" msgstr "Nicht erkannter Wert '%(val)s', zulässige Werte are: %(acceptable)s" #, python-format msgid "Version %s is invalid." msgstr "Version %s ist ungültig." ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1740145268.2650516 oslo_utils-8.2.0/oslo_utils/locale/en_GB/0000775000175000017500000000000000000000000020345 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1740145268.277054 oslo_utils-8.2.0/oslo_utils/locale/en_GB/LC_MESSAGES/0000775000175000017500000000000000000000000022132 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/oslo_utils/locale/en_GB/LC_MESSAGES/oslo_utils.po0000664000175000017500000000572100000000000024673 0ustar00zuulzuul00000000000000# Translations template for oslo.utils. # Copyright (C) 2015 ORGANIZATION # This file is distributed under the same license as the oslo.utils project. # # Translators: # Andi Chandler , 2014-2015 # OpenStack Infra , 2015. #zanata # Andi Chandler , 2016. #zanata # Andreas Jaeger , 2016. #zanata # Andi Chandler , 2018. #zanata msgid "" msgstr "" "Project-Id-Version: oslo.utils VERSION\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" "POT-Creation-Date: 2018-02-09 13:01+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2018-02-06 11:26+0000\n" "Last-Translator: Andi Chandler \n" "Language: en_GB\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "Generated-By: Babel 2.0\n" "X-Generator: Zanata 4.3.3\n" "Language-Team: English (United Kingdom)\n" #, python-format msgid "%(name)s has %(length)s characters, less than %(min_length)s." msgstr "%(name)s has %(length)s characters, less than %(min_length)s." #, python-format msgid "%(name)s has %(length)s characters, more than %(max_length)s." msgstr "%(name)s has %(length)s characters, more than %(max_length)s." #, python-format msgid "%(value_name)s must be <= %(max_value)d" msgstr "%(value_name)s must be <= %(max_value)d" #, python-format msgid "%(value_name)s must be >= %(min_value)d" msgstr "%(value_name)s must be >= %(min_value)d" #, python-format msgid "%(value_name)s must be an integer" msgstr "%(value_name)s must be an integer" #, python-format msgid "%s is not a string or unicode" msgstr "%s is not a string or unicode" #, python-format msgid "" "Bad prefix or mac format for generating IPv6 address by EUI-64: %(prefix)s, " "%(mac)s:" msgstr "" "Bad prefix or mac format for generating IPv6 address by EUI-64: %(prefix)s, " "%(mac)s:" #, python-format msgid "Bad prefix type for generating IPv6 address by EUI-64: %s" msgstr "Bad prefix type for generating IPv6 address by EUI-64: %s" #, python-format msgid "Invalid input value \"%s\"." msgstr "Invalid input value \"%s\"." #, python-format msgid "Invalid path: %s" msgstr "Invalid path: %s" #, python-format msgid "Invalid string format: %s" msgstr "Invalid string format: %s" #, python-format msgid "Invalid unit system: \"%s\"" msgstr "Invalid unit system: \"%s\"" msgid "Snapshot list encountered but no header found!" msgstr "Snapshot list encountered but no header found!" msgid "Unable to generate IP address by EUI64 for IPv4 prefix" msgstr "Unable to generate IP address by EUI64 for IPv4 prefix" #, python-format msgid "Unrecognized value '%(val)s', acceptable values are: %(acceptable)s" msgstr "Unrecognised value '%(val)s', acceptable values are: %(acceptable)s" #, python-format msgid "Version %s is invalid." msgstr "Version %s is invalid." #, python-format msgid "minsegs > maxsegs: %(min)d > %(max)d)" msgstr "minsegs > maxsegs: %(min)d > %(max)d)" ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1740145268.2650516 oslo_utils-8.2.0/oslo_utils/locale/fr/0000775000175000017500000000000000000000000020002 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1740145268.277054 oslo_utils-8.2.0/oslo_utils/locale/fr/LC_MESSAGES/0000775000175000017500000000000000000000000021567 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/oslo_utils/locale/fr/LC_MESSAGES/oslo_utils.po0000664000175000017500000000434500000000000024331 0ustar00zuulzuul00000000000000# Translations template for oslo.utils. # Copyright (C) 2015 ORGANIZATION # This file is distributed under the same license as the oslo.utils project. # # Translators: # Maxime COQUEREL , 2014-2015 # OpenStack Infra , 2015. #zanata # Tom Cocozzello , 2015. #zanata # Andreas Jaeger , 2016. #zanata msgid "" msgstr "" "Project-Id-Version: oslo.utils 3.7.1.dev13\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" "POT-Creation-Date: 2016-05-02 20:03+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2015-07-27 10:55+0000\n" "Last-Translator: Maxime COQUEREL \n" "Language: fr\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" "Generated-By: Babel 2.0\n" "X-Generator: Zanata 3.7.3\n" "Language-Team: French\n" #, python-format msgid "%s is not a string or unicode" msgstr "%s n'est pas une chaîne ou unicode" #, python-format msgid "" "Bad prefix or mac format for generating IPv6 address by EUI-64: %(prefix)s, " "%(mac)s:" msgstr "" "Mauvais type de préfixe ou mauvais format d'adresse mac pour générer une " "adresse IPv6 par EUI-64: %(prefix)s, %(mac)s:" #, python-format msgid "Bad prefix type for generating IPv6 address by EUI-64: %s" msgstr "Mauvais type de préfixe pour générer adresse IPv6 par EUI-64: %s" #, python-format msgid "Invalid input value \"%s\"." msgstr "Valeur en entrée \"%s\" non valide." #, python-format msgid "Invalid string format: %s" msgstr "Format de chaine de caractère non valide: %s" #, python-format msgid "Invalid unit system: \"%s\"" msgstr "Unit système non valide: \"%s\"" msgid "Snapshot list encountered but no header found!" msgstr "Liste d'instantanés trouvée mais aucun en-tête trouvé !" msgid "Unable to generate IP address by EUI64 for IPv4 prefix" msgstr "Impossible de générer l'adresse IP par EUI64 pour le préfixe IPv4" #, python-format msgid "Unrecognized value '%(val)s', acceptable values are: %(acceptable)s" msgstr "" "Valeur non reconnue '%(val)s', les valeurs acceptables sont: %(acceptable)s" #, python-format msgid "Version %s is invalid." msgstr "La version %s est invalide." ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1740145268.2650516 oslo_utils-8.2.0/oslo_utils/locale/ka_GE/0000775000175000017500000000000000000000000020341 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1740145268.277054 oslo_utils-8.2.0/oslo_utils/locale/ka_GE/LC_MESSAGES/0000775000175000017500000000000000000000000022126 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/oslo_utils/locale/ka_GE/LC_MESSAGES/oslo_utils.po0000664000175000017500000000706600000000000024673 0ustar00zuulzuul00000000000000# Temuri Doghonadze , 2024. #zanata msgid "" msgstr "" "Project-Id-Version: oslo.utils VERSION\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" "POT-Creation-Date: 2024-07-23 18:02+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2024-06-16 06:17+0000\n" "Last-Translator: Copied by Zanata \n" "Language-Team: Georgian (Georgia)\n" "Language: ka_GE\n" "Plural-Forms: nplurals=1; plural=0;\n" "X-Generator: Zanata 4.3.3\n" #, python-format msgid "%(name)s has %(length)s characters, less than %(min_length)s." msgstr "" "%(name)s-ის სიგრძეა %(length)s სიმბოლო, რაც %(min_length)s-ზე ნაკლებია." #, python-format msgid "%(name)s has %(length)s characters, more than %(max_length)s." msgstr "%(name)s-ის სიგრძეა %(length)s სიმბოლო, რაც %(max_length)s-ზე მეტია." #, python-format msgid "%(value_name)s must be <= %(max_value)d" msgstr "%(value_name)s უნდა იყოს <= %(max_value)d" #, python-format msgid "%(value_name)s must be >= %(min_value)d" msgstr "%(value_name)s უნდა იყოს >= %(min_value)d" #, python-format msgid "%(value_name)s must be an integer" msgstr "%(value_name)s მთელი რიცხვი უნდა იყოს" #, python-format msgid "%s is not a string or unicode" msgstr "%s არც სტრიქონია, არც უნიკოდი" #, python-format msgid "" "Bad prefix or mac format for generating IPv6 address by EUI-64: %(prefix)s, " "%(mac)s:" msgstr "" "არასწორი პრეფიქსი ან MAC-ის ფორმატი IPv6 მისამართის გენერაციისთვის EUI-64-ის " "მიერ: %(prefix)s, %(mac)s:" #, python-format msgid "Bad prefix type for generating IPv6 address by EUI-64: %s" msgstr "არასწორი პრეფიქსი EUI-64-ის მიერ IPv6 მისამართის გენერაციისთვის: %s" #, python-format msgid "Invalid input value \"%s\"." msgstr "არასწორი შეყვანილი მნიშვნელობა \"%s\"." #, python-format msgid "Invalid path: %s" msgstr "არასწორი ბილიკი: %s" #, python-format msgid "Invalid string format: %s" msgstr "არასწორი სტრიქონის ფორმატი: %s" #, python-format msgid "Invalid unit system: \"%s\"" msgstr "არასწორი ერთეულების სისტემა: \"%s\"" msgid "Snapshot list encountered but no header found!" msgstr "სწრაფი ასლების სია აღმოჩენილია, მაგრამ თავსართი არა!" msgid "Unable to generate IP address by EUI64 for IPv4 prefix" msgstr "EUI64-დან IPv4 პრეფიქსისთვის IP მისამართის გენერაცია შეუძლებელია" #, python-format msgid "Unrecognized value '%(val)s', acceptable values are: %(acceptable)s" msgstr "უცნობი მნიშვნელობა '%(val)s'. მისაღებია მნიშვნელობები: %(acceptable)s" #, python-format msgid "Version %s is invalid." msgstr "ვერსია %s არასწორია." #, python-format msgid "minsegs > maxsegs: %(min)d > %(max)d)" msgstr "minsegs > maxsegs: %(min)d > %(max)d)" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/oslo_utils/netutils.py0000664000175000017500000004526200000000000020366 0ustar00zuulzuul00000000000000# Copyright 2012 OpenStack Foundation. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ Network-related utilities and helper functions. """ import ipaddress import logging import os import re import socket from urllib import parse import netaddr from netaddr.core import INET_ATON from netaddr.core import INET_PTON import psutil from oslo_utils._i18n import _ LOG = logging.getLogger(__name__) _IS_IPV6_ENABLED = None def parse_host_port(address, default_port=None): """Interpret a string as a host:port pair. An IPv6 address MUST be escaped if accompanied by a port, because otherwise ambiguity ensues: 2001:db8:85a3::8a2e:370:7334 means both [2001:db8:85a3::8a2e:370:7334] and [2001:db8:85a3::8a2e:370]:7334. >>> parse_host_port('server01:80') ('server01', 80) >>> parse_host_port('server01') ('server01', None) >>> parse_host_port('server01', default_port=1234) ('server01', 1234) >>> parse_host_port('[::1]:80') ('::1', 80) >>> parse_host_port('[::1]') ('::1', None) >>> parse_host_port('[::1]', default_port=1234) ('::1', 1234) >>> parse_host_port('2001:db8:85a3::8a2e:370:7334', default_port=1234) ('2001:db8:85a3::8a2e:370:7334', 1234) >>> parse_host_port(None) (None, None) """ if not address: return (None, None) if address[0] == '[': # Escaped ipv6 _host, _port = address[1:].split(']') host = _host if ':' in _port: port = _port.split(':')[1] else: port = default_port else: if address.count(':') == 1: host, port = address.split(':') else: # 0 means ipv4, >1 means ipv6. # We prohibit unescaped ipv6 addresses with port. host = address port = default_port return (host, None if port is None else int(port)) def is_valid_ipv4(address, strict=True): """Verify that address represents a valid IPv4 address. :param address: Value to verify :type address: string :param strict: flag allowing users to restrict validation to IP addresses in presentation format (``a.b.c.d``) as opposed to address format (``a.b.c.d``, ``a.b.c``, ``a.b``, ``a``). :type flags: bool :returns: bool .. versionadded:: 1.1 .. versionchanged:: 4.8.0 Allow to restrict validation to IP addresses in presentation format (``a.b.c.d``) as opposed to address format (``a.b.c.d``, ``a.b.c``, ``a.b``, ``a``). """ if not address: return False flag = INET_PTON if strict else INET_ATON try: return netaddr.valid_ipv4(address, flags=flag) except netaddr.AddrFormatError: return False def is_valid_ipv6(address): """Verify that address represents a valid IPv6 address. :param address: Value to verify :type address: string :returns: bool .. versionadded:: 1.1 """ if not address: return False parts = address.rsplit("%", 1) address = parts[0] scope = parts[1] if len(parts) > 1 else None if scope is not None and (len(scope) < 1 or len(scope) > 15): return False try: return netaddr.valid_ipv6(address, netaddr.core.INET_PTON) except netaddr.AddrFormatError: return False def get_noscope_ipv6(address): """Take an IPv6 address and trim scope out if present. :param address: Value to change :type address: string :returns: string .. versionadded: 7.3.0: """ _ipv6 = ipaddress.IPv6Address(address) if _ipv6.scope_id: address = address.removesuffix('%' + _ipv6.scope_id) return address def is_valid_cidr(address): """Verify that address represents a valid CIDR address. :param address: Value to verify :type address: string :returns: bool .. versionadded:: 3.8 """ try: # Validate the correct CIDR Address netaddr.IPNetwork(address) except (TypeError, netaddr.AddrFormatError): return False # Prior validation partially verify /xx part # Verify it here ip_segment = address.split('/') if (len(ip_segment) <= 1 or ip_segment[1] == ''): return False return True def is_valid_ipv6_cidr(address): """Verify that address represents a valid IPv6 CIDR address. :param address: address to verify :type address: string :returns: true if address is valid, false otherwise .. versionadded:: 3.17 """ try: netaddr.IPNetwork(address, version=6).cidr return True except (TypeError, netaddr.AddrFormatError): return False def get_ipv6_addr_by_EUI64(prefix, mac): """Calculate IPv6 address using EUI-64 specification. This method calculates the IPv6 address using the EUI-64 addressing scheme as explained in rfc2373. :param prefix: IPv6 prefix. :param mac: IEEE 802 48-bit MAC address. :returns: IPv6 address on success. :raises ValueError, TypeError: For any invalid input. .. versionadded:: 1.4 """ if not isinstance(prefix, str): msg = _("Prefix must be a string") raise TypeError(msg) # Check if the prefix is an IPv4 address if is_valid_ipv4(prefix, False): msg = _("Unable to generate IP address by EUI64 for IPv4 prefix") raise ValueError(msg) try: eui64 = int(netaddr.EUI(mac).eui64()) prefix = netaddr.IPNetwork(prefix) return netaddr.IPAddress(prefix.first + eui64 ^ (1 << 57)) except (ValueError, netaddr.AddrFormatError): raise ValueError(_('Bad prefix or mac format for generating IPv6 ' 'address by EUI-64: %(prefix)s, %(mac)s:') % {'prefix': prefix, 'mac': mac}) except TypeError: raise TypeError(_('Bad prefix type for generating IPv6 address by ' 'EUI-64: %s') % prefix) def get_mac_addr_by_ipv6(ipv6, dialect=netaddr.mac_unix_expanded): """Extract MAC address from interface identifier based IPv6 address. For example from link-local addresses (fe80::/10) generated from MAC. :param ipv6: An interface identifier (i.e. mostly MAC) based IPv6 address as a netaddr.IPAddress() object. :param dialect: The netaddr dialect of the the object returned. Defaults to netaddr.mac_unix_expanded. :returns: A MAC address as a netaddr.EUI() object. See also: * https://tools.ietf.org/html/rfc4291#appendix-A * https://tools.ietf.org/html/rfc4291#section-2.5.6 .. versionadded:: 4.3.0 """ return netaddr.EUI(int( # out of the lowest 8 bytes (byte positions 8-1) # delete the middle 2 bytes (5-4, 0xff_fe) # by shifting the highest 3 bytes to the right by 2 bytes (8-6 -> 6-4) (((ipv6 & 0xff_ff_ff_00_00_00_00_00) >> 16) + # adding the lowest 3 bytes as they are (3-1) (ipv6 & 0xff_ff_ff)) ^ # then invert the universal/local bit 0x02_00_00_00_00_00), dialect=dialect) def is_ipv6_enabled(): """Check if IPv6 support is enabled on the platform. This api will look into the proc entries of the platform to figure out the status of IPv6 support on the platform. :returns: True if the platform has IPv6 support, False otherwise. .. versionadded:: 1.4 """ global _IS_IPV6_ENABLED if _IS_IPV6_ENABLED is None: disabled_ipv6_path = "/proc/sys/net/ipv6/conf/default/disable_ipv6" if os.path.exists(disabled_ipv6_path): with open(disabled_ipv6_path) as f: disabled = f.read().strip() _IS_IPV6_ENABLED = disabled == "0" else: _IS_IPV6_ENABLED = False return _IS_IPV6_ENABLED def escape_ipv6(address): """Escape an IP address in square brackets if IPv6 :param address: address to optionaly escape :type address: string :returns: string .. versionadded:: 3.29.0 """ if is_valid_ipv6(address): return "[%s]" % address return address def is_valid_ip(address): """Verify that address represents a valid IP address. :param address: Value to verify :type address: string :returns: bool .. versionadded:: 1.1 """ return is_valid_ipv4(address, False) or is_valid_ipv6(address) def is_valid_mac(address): """Verify the format of a MAC address. Check if a MAC address is valid and contains six octets. Accepts colon-separated format only. :param address: MAC address to be validated. :returns: True if valid. False if not. .. versionadded:: 3.17 """ m = "[0-9a-f]{2}(:[0-9a-f]{2}){5}$" return isinstance(address, str) and re.match(m, address.lower()) def _is_int_in_range(value, start, end): """Try to convert value to int and check if it lies within range 'start' to 'end'. :param value: value to verify :param start: start number of range :param end: end number of range :returns: bool """ try: val = int(value) except (ValueError, TypeError): return False return (start <= val <= end) def is_valid_port(port): """Verify that port represents a valid port number. Port can be valid integer having a value of 0 up to and including 65535. .. versionadded:: 1.1.1 """ return _is_int_in_range(port, 0, 65535) def is_valid_icmp_type(type): """Verify if ICMP type is valid. :param type: ICMP *type* field can only be a valid integer :returns: bool ICMP *type* field can be valid integer having a value of 0 up to and including 255. """ return _is_int_in_range(type, 0, 255) def is_valid_icmp_code(code): """Verify if ICMP code is valid. :param code: ICMP *code* field can be valid integer or None :returns: bool ICMP *code* field can be either None or valid integer having a value of 0 up to and including 255. """ if code is None: return True return _is_int_in_range(code, 0, 255) def get_my_ipv4(): """Returns the actual ipv4 of the local machine. This code figures out what source address would be used if some traffic were to be sent out to some well known address on the Internet. In this case, IP from RFC5737 is used, but the specific address does not matter much. No traffic is actually sent. .. versionadded:: 1.1 .. versionchanged:: 1.2.1 Return ``'127.0.0.1'`` if there is no default interface. """ try: with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as csock: csock.connect(('192.0.2.0', 80)) return csock.getsockname()[0] except OSError: return _get_my_ipv4_address() def _get_my_ipv4_address(): """Figure out the best ipv4 """ LOCALHOST = '127.0.0.1' interface = None try: with open('/proc/net/route') as routes: for route in routes: route_attrs = route.strip().split('\t') if route_attrs[1] == '00000000': interface = route_attrs[0] break else: LOG.info('Could not determine default network interface, ' 'using %s for IPv4 address', LOCALHOST) return LOCALHOST except FileNotFoundError: LOG.info('IPv4 route table not found, ' 'using %s for IPv4 address', LOCALHOST) return LOCALHOST try: addrs = psutil.net_if_addrs()[interface] v4addrs = [addr for addr in addrs if addr.family == socket.AF_INET] return v4addrs[0].address except (KeyError, IndexError): LOG.info('Could not determine IPv4 address for interface %s, ' 'using 127.0.0.1', interface) except Exception as e: LOG.info('Could not determine IPv4 address for ' 'interface %(interface)s: %(error)s', {'interface': interface, 'error': e}) return LOCALHOST def get_my_ipv6(): """Returns the actual IPv6 address of the local machine. This code figures out what source address would be used if some traffic were to be sent out to some well known address on the Internet. In this case, IPv6 from RFC3849 is used, but the specific address does not matter much. No traffic is actually sent. .. versionadded:: 6.1 Return ``'::1'`` if there is no default interface. """ try: with socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) as csock: csock.connect(('2001:db8::1', 80)) return csock.getsockname()[0] except OSError: return _get_my_ipv6_address() def _get_my_ipv6_address(): """Figure out the best IPv6 address """ LOCALHOST = '::1' interface = None ZERO_ADDRESS = '00000000000000000000000000000000' ZERO_PAIR = (ZERO_ADDRESS, '00') try: with open('/proc/net/ipv6_route') as routes: for route in routes: route_attrs = route.strip().split(' ') if ((route_attrs[0], route_attrs[1]) == ZERO_PAIR and (route_attrs[2], route_attrs[3]) == ZERO_PAIR): interface = route_attrs[-1] break else: LOG.info('Could not determine default network interface, ' 'using %s for IPv6 address', LOCALHOST) return LOCALHOST except FileNotFoundError: LOG.info('IPv6 route table not found, ' 'using %s for IPv6 address', LOCALHOST) return LOCALHOST try: addrs = psutil.net_if_addrs()[interface] v6addrs = [addr for addr in addrs if addr.family == socket.AF_INET6] return v6addrs[0].address except (KeyError, IndexError): LOG.info('Could not determine IPv6 address for interface ' '%(interface)s, using %(address)s', {'interface': interface, 'address': LOCALHOST}) except Exception as e: LOG.info('Could not determine IPv6 address for ' 'interface %(interface)s: %(error)s', {'interface': interface, 'error': e}) return LOCALHOST class _ModifiedSplitResult(parse.SplitResult): """Split results class for urlsplit.""" def params(self, collapse=True): """Extracts the query parameters from the split urls components. This method will provide back as a dictionary the query parameter names and values that were provided in the url. :param collapse: Boolean, turn on or off collapsing of query values with the same name. Since a url can contain the same query parameter name with different values it may or may not be useful for users to care that this has happened. This parameter when True uses the last value that was given for a given name, while if False it will retain all values provided by associating the query parameter name with a list of values instead of a single (non-list) value. """ if self.query: if collapse: return dict(parse.parse_qsl(self.query)) else: params = {} for (key, value) in parse.parse_qsl(self.query): if key in params: if isinstance(params[key], list): params[key].append(value) else: params[key] = [params[key], value] else: params[key] = value return params else: return {} def urlsplit(url, scheme='', allow_fragments=True): """Parse a URL using urlparse.urlsplit(), splitting query and fragments. This function papers over Python issue9374_ when needed. .. _issue9374: http://bugs.python.org/issue9374 The parameters are the same as urlparse.urlsplit. """ scheme, netloc, path, query, fragment = parse.urlsplit( url, scheme, allow_fragments) if allow_fragments and '#' in path: path, fragment = path.split('#', 1) if '?' in path: path, query = path.split('?', 1) return _ModifiedSplitResult(scheme, netloc, path, query, fragment) def set_tcp_keepalive(sock, tcp_keepalive=True, tcp_keepidle=None, tcp_keepalive_interval=None, tcp_keepalive_count=None): """Set values for tcp keepalive parameters This function configures tcp keepalive parameters if users wish to do so. :param tcp_keepalive: Boolean, turn on or off tcp_keepalive. If users are not sure, this should be True, and default values will be used. :param tcp_keepidle: time to wait before starting to send keepalive probes :param tcp_keepalive_interval: time between successive probes, once the initial wait time is over :param tcp_keepalive_count: number of probes to send before the connection is killed """ # NOTE(praneshp): Despite keepalive being a tcp concept, the level is # still SOL_SOCKET. This is a quirk. if isinstance(tcp_keepalive, bool): sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, tcp_keepalive) else: raise TypeError("tcp_keepalive must be a boolean") if not tcp_keepalive: return # These options aren't available in the OS X version of eventlet, # Idle + Count * Interval effectively gives you the total timeout. if tcp_keepidle is not None: if hasattr(socket, 'TCP_KEEPIDLE'): sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, tcp_keepidle) else: LOG.warning('tcp_keepidle not available on your system') if tcp_keepalive_interval is not None: if hasattr(socket, 'TCP_KEEPINTVL'): sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, tcp_keepalive_interval) else: LOG.warning('tcp_keepintvl not available on your system') if tcp_keepalive_count is not None: if hasattr(socket, 'TCP_KEEPCNT'): sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, tcp_keepalive_count) else: LOG.warning('tcp_keepcnt not available on your system') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/oslo_utils/reflection.py0000664000175000017500000001567300000000000020654 0ustar00zuulzuul00000000000000# Copyright (C) 2012-2013 Yahoo! Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ Reflection module. .. versionadded:: 1.1 """ import inspect import logging import operator import types try: _TYPE_TYPE = types.TypeType except AttributeError: _TYPE_TYPE = type # See: https://docs.python.org/2/library/__builtin__.html#module-__builtin__ # and see https://docs.python.org/2/reference/executionmodel.html (and likely # others)... _BUILTIN_MODULES = ('builtins', '__builtin__', '__builtins__', 'exceptions') LOG = logging.getLogger(__name__) Parameter = inspect.Parameter Signature = inspect.Signature get_signature = inspect.signature def get_members(obj, exclude_hidden=True): """Yields the members of an object, filtering by hidden/not hidden. .. versionadded:: 2.3 """ for (name, value) in inspect.getmembers(obj): if name.startswith("_") and exclude_hidden: continue yield (name, value) def get_member_names(obj, exclude_hidden=True): """Get all the member names for a object.""" return [name for (name, _obj) in get_members(obj, exclude_hidden=exclude_hidden)] def get_class_name(obj, fully_qualified=True, truncate_builtins=True): """Get class name for object. If object is a type, returns name of the type. If object is a bound method or a class method, returns its ``self`` object's class name. If object is an instance of class, returns instance's class name. Else, name of the type of the object is returned. If fully_qualified is True, returns fully qualified name of the type. For builtin types, just name is returned. TypeError is raised if can't get class name from object. """ if inspect.isfunction(obj): raise TypeError("Can't get class name.") if inspect.ismethod(obj): obj = get_method_self(obj) if not isinstance(obj, type): obj = type(obj) if truncate_builtins: try: built_in = obj.__module__ in _BUILTIN_MODULES except AttributeError: # nosec pass else: if built_in: return obj.__name__ if fully_qualified and hasattr(obj, '__module__'): return '{}.{}'.format(obj.__module__, obj.__name__) else: return obj.__name__ def get_all_class_names(obj, up_to=object, fully_qualified=True, truncate_builtins=True): """Get class names of object parent classes. Iterate over all class names object is instance or subclass of, in order of method resolution (mro). If up_to parameter is provided, only name of classes that are sublcasses to that class are returned. """ if not isinstance(obj, type): obj = type(obj) for cls in obj.mro(): if issubclass(cls, up_to): yield get_class_name(cls, fully_qualified=fully_qualified, truncate_builtins=truncate_builtins) def get_callable_name(function): """Generate a name from callable. Tries to do the best to guess fully qualified callable name. """ method_self = get_method_self(function) if method_self is not None: # This is a bound method. if isinstance(method_self, type): # This is a bound class method. im_class = method_self else: im_class = type(method_self) try: parts = (im_class.__module__, function.__qualname__) except AttributeError: parts = (im_class.__module__, im_class.__name__, function.__name__) elif inspect.ismethod(function) or inspect.isfunction(function): # This could be a function, a static method, a unbound method... try: parts = (function.__module__, function.__qualname__) except AttributeError: if hasattr(function, 'im_class'): # This is a unbound method, which exists only in python 2.x im_class = function.im_class parts = (im_class.__module__, im_class.__name__, function.__name__) else: parts = (function.__module__, function.__name__) else: im_class = type(function) if im_class is _TYPE_TYPE: im_class = function try: parts = (im_class.__module__, im_class.__qualname__) except AttributeError: parts = (im_class.__module__, im_class.__name__) return '.'.join(parts) def get_method_self(method): """Gets the ``self`` object attached to this method (or none).""" if not inspect.ismethod(method): return None try: return operator.attrgetter("__self__")(method) except AttributeError: return None def is_same_callback(callback1, callback2): """Returns if the two callbacks are the same.""" if callback1 is callback2: # This happens when plain methods are given (or static/non-bound # methods). return True if callback1 == callback2: # NOTE(gmann): python3.8 onward, comparison of bound methods is # changed. It no longer decide the bound method's equality based # on their bounded objects equality instead it checks the identity # of their '__self__'. return True return False def is_bound_method(method): """Returns if the given method is bound to an object.""" return get_method_self(method) is not None def is_subclass(obj, cls): """Returns if the object is class and it is subclass of a given class.""" return inspect.isclass(obj) and issubclass(obj, cls) def get_callable_args(function, required_only=False): """Get names of callable arguments. Special arguments (like ``*args`` and ``**kwargs``) are not included into output. If required_only is True, optional arguments (with default values) are not included into output. """ sig = get_signature(function) function_args = list(sig.parameters.keys()) for param_name, p in sig.parameters.items(): if (p.kind in (Parameter.VAR_POSITIONAL, Parameter.VAR_KEYWORD) or (required_only and p.default is not Parameter.empty)): function_args.remove(param_name) return function_args def accepts_kwargs(function): """Returns ``True`` if function accepts kwargs otherwise ``False``.""" sig = get_signature(function) return any( p.kind == Parameter.VAR_KEYWORD for p in sig.parameters.values() ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/oslo_utils/secretutils.py0000664000175000017500000000553100000000000021060 0ustar00zuulzuul00000000000000# All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ Secret utilities. .. versionadded:: 3.5 """ import ctypes import ctypes.util import hashlib import hmac import secrets import string as _string import debtcollector.removals @debtcollector.removals.remove(message='Use hmac.compare_digest instead', category=DeprecationWarning) def constant_time_compare(*args, **kwargs): return hmac.compare_digest(*args, **kwargs) @debtcollector.removals.remove(message='Use hashlib.md5 instead', category=DeprecationWarning) def md5(string=b'', usedforsecurity=True): """Return an md5 hashlib object using usedforsecurity parameter For python distributions that support the usedforsecurity keyword parameter, this passes the parameter through as expected. See https://bugs.python.org/issue9216 """ return hashlib.md5(string, usedforsecurity=usedforsecurity) # nosec if ctypes.util.find_library("crypt"): _libcrypt = ctypes.CDLL(ctypes.util.find_library("crypt"), use_errno=True) _crypt = _libcrypt.crypt _crypt.argtypes = (ctypes.c_char_p, ctypes.c_char_p) _crypt.restype = ctypes.c_char_p else: _crypt = None def crypt_mksalt(method): """Make salt to encrypt password string This is provided as a replacement of crypt.mksalt method because crypt module was removed in Python 3.13. .. versionadded:: 8.0 """ # NOTE(tkajinam): The mksalt method in crypto module used to support MD5 # and DES. However these are considered unsafe so we do not support these # to engourage more secure methods. methods = {'SHA-512': '$6$', 'SHA-256': '$5$'} if method not in methods: raise ValueError('Unsupported method: %s' % method) salt_set = _string.ascii_letters + _string.digits + './' return ''.join( [methods[method]] + [secrets.choice(salt_set) for c in range(16)]) def crypt_password(key, salt): """Encrtpt password string and generate the value in /etc/shadow format This is provided as a replacement of crypt.crypt method because crypt module was removed in Python 3.13. .. versionadded:: 8.0 """ if _crypt is None: raise RuntimeError('libcrypt is not available') return _crypt(key.encode('utf-8'), salt.encode('utf-8')).decode('utf-8') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/oslo_utils/specs_matcher.py0000664000175000017500000001673600000000000021343 0ustar00zuulzuul00000000000000# Copyright (c) 2011 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import ast import operator import pyparsing def _all_in(x, *y): x = ast.literal_eval(x) if not isinstance(x, list): raise TypeError(" must compare with a list literal" " string, EG \"%s\"" % (['aes', 'mmx'],)) return all(val in x for val in y) def _range_in(x, *y): x = ast.literal_eval(x) if len(y) != 4: raise TypeError(" operator has to be followed by 2 " "space separated numeric value surrounded by " "brackets \"range_in [ 10 20 ] \"") num_x = float(x) num_y = float(y[1]) num_z = float(y[2]) if num_y > num_z: raise TypeError(" operator's first argument has to be " "smaller or equal to the second argument EG" "\"range_in ( 10 20 ] \"") if y[0] == '[': lower = num_x >= num_y elif y[0] == '(': lower = num_x > num_y else: raise TypeError("The first element should be an opening bracket " "(\"(\" or \"[\")") if y[3] == ']': upper = num_x <= num_z elif y[3] == ')': upper = num_x < num_z else: raise TypeError("The last element should be a closing bracket " "(\")\" or \"]\")") return lower and upper op_methods = { # This one is special/odd, # TODO(harlowja): fix it so that it's not greater than or # equal, see here for the original @ https://review.openstack.org/#/c/8089/ '=': lambda x, y: float(x) >= float(y), # More sane ops/methods # Numerical methods '!=': lambda x, y: float(x) != float(y), '<=': lambda x, y: float(x) <= float(y), '<': lambda x, y: float(x) < float(y), '==': lambda x, y: float(x) == float(y), '>=': lambda x, y: float(x) >= float(y), '>': lambda x, y: float(x) > float(y), # String methods 's!=': operator.ne, 's<': operator.lt, 's<=': operator.le, 's==': operator.eq, 's>': operator.gt, 's>=': operator.ge, # Other '': _all_in, '': lambda x, y: y in x, '': lambda x, *y: any(x == a for a in y), '': _range_in, } def make_grammar(): """Creates the grammar to be used by a spec matcher. The grammar created supports the following operations. Numerical values: * ``= :`` equal to or greater than. This is equivalent to ``>=`` and is supported for `legacy reasons `_ * ``!= :`` Float/integer value not equal * ``<= :`` Float/integer value less than or equal * ``< :`` Float/integer value less than * ``== :`` Float/integer value equal * ``>= :`` Float/integer value greater than or equal * ``> :`` Float/integer value greater String operations: * ``s!= :`` Not equal * ``s< :`` Less than * ``s<= :`` Less than or equal * ``s== :`` Equal * ``s> :`` Greater than * ``s>= :`` Greater than or equal Other operations: * `` :`` All items 'in' value * `` :`` Item 'in' value, like a substring in a string. * `` :`` Logical 'or' * ``:`` Range tester with customizable boundary conditions, tests whether value is in the range, boundary condition could be inclusve \'[\' or exclusive \'(\'. If no operator is specified the default is ``s==`` (string equality comparison) Example operations: * ``">= 60"`` Is the numerical value greater than or equal to 60 * ``" spam eggs"`` Does the value contain ``spam`` or ``eggs`` * ``"s== 2.1.0"`` Is the string value equal to ``2.1.0`` * ``" gcc"`` Is the string ``gcc`` contained in the value string * ``" aes mmx"`` Are both ``aes`` and ``mmx`` in the value * ``" [ 10 20 ]"`` float(value) >= 10 and float(value) <= 20 * ``" ( 10 20 ]"`` float(value) > 10 and float(value) <= 20 * ``" ( 10 20 )"`` float(value) > 10 and float(value) < 20 :returns: A pyparsing.MatchFirst object. See https://pythonhosted.org/pyparsing/ for details on pyparsing. """ # This is apparently how pyparsing recommends to be used, # as http://pyparsing.wikispaces.com/share/view/644825 states that # it is not thread-safe to use a parser across threads. unary_ops = ( # Order matters here (so that '=' doesn't match before '==') pyparsing.Literal("==") | pyparsing.Literal("=") | pyparsing.Literal("!=") | pyparsing.Literal("") | pyparsing.Literal(">=") | pyparsing.Literal("<=") | pyparsing.Literal(">") | pyparsing.Literal("<") | pyparsing.Literal("s==") | pyparsing.Literal("s!=") | # Order matters here (so that '<' doesn't match before '<=') pyparsing.Literal("s<=") | pyparsing.Literal("s<") | # Order matters here (so that '>' doesn't match before '>=') pyparsing.Literal("s>=") | pyparsing.Literal("s>")) all_in_nary_op = pyparsing.Literal("") or_ = pyparsing.Literal("") range_in_binary_op = pyparsing.Literal("") # An atom is anything not an keyword followed by anything but whitespace atom = ~(unary_ops | all_in_nary_op | or_ | range_in_binary_op) + \ pyparsing.Regex(r"\S+") unary = unary_ops + atom range_op = range_in_binary_op + atom + atom + atom + atom nary = all_in_nary_op + pyparsing.OneOrMore(atom) disjunction = pyparsing.OneOrMore(or_ + atom) # Even-numbered tokens will be '', so we drop them disjunction.setParseAction(lambda _s, _l, t: [""] + t[1::2]) expr = disjunction | nary | range_op | unary | atom return expr def match(cmp_value, spec): """Match a given value to a given spec DSL. This uses the grammar defined by make_grammar() :param cmp_value: Value to be checked for match. :param spec: The comparison specification string, for example ``">= 70"`` or ``"s== string_value"``. See ``make_grammar()`` for examples of a specification string. :returns: True if cmp_value is a match for spec. False otherwise. """ expr = make_grammar() try: # As of 2018-01-29 documentation on parseString() # https://pythonhosted.org/pyparsing/pyparsing.ParserElement-class.html#parseString # # parseString() will take our specification string, for example "< 6" # and convert it into a list of ['<', "6"] tree = expr.parseString(spec) except pyparsing.ParseException: # If an exception then we will just check if the value matches the spec tree = [spec] if len(tree) == 1: return tree[0] == cmp_value # tree[0] will contain a string representation of a comparison operation # such as '>=', we then convert that string to a comparison function compare_func = op_methods[tree[0]] return compare_func(cmp_value, *tree[1:]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/oslo_utils/strutils.py0000664000175000017500000005270300000000000020406 0ustar00zuulzuul00000000000000# Copyright 2011 OpenStack Foundation. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ System-level utilities and helper functions. """ import collections.abc import math import re import unicodedata import urllib from oslo_utils._i18n import _ from oslo_utils import encodeutils UNIT_PREFIX_EXPONENT = { 'k': 1, 'K': 1, 'Ki': 1, 'M': 2, 'Mi': 2, 'G': 3, 'Gi': 3, 'T': 4, 'Ti': 4, 'P': 5, 'Pi': 5, 'E': 6, 'Ei': 6, 'Z': 7, 'Zi': 7, 'Y': 8, 'Yi': 8, 'R': 9, 'Ri': 9, 'Q': 10, 'Qi': 10, } UNIT_SYSTEM_INFO = { 'IEC': (1024, re.compile(r'(^[-+]?\d*\.?\d+)([KMGTPEZYRQ]i?)?(b|bit|B)$')), 'SI': (1000, re.compile(r'(^[-+]?\d*\.?\d+)([kMGTPEZYRQ])?(b|bit|B)$')), 'mixed': (None, re.compile( r'(^[-+]?\d*\.?\d+)([kKMGTPEZYRQ]i?)?(b|bit|B)$')), } TRUE_STRINGS = ('1', 't', 'true', 'on', 'y', 'yes') FALSE_STRINGS = ('0', 'f', 'false', 'off', 'n', 'no') SLUGIFY_STRIP_RE = re.compile(r"[^\w\s-]") SLUGIFY_HYPHENATE_RE = re.compile(r"[-\s]+") # NOTE(flaper87): The following globals are used by `mask_password` and # `mask_dict_password`. They must all be lowercase. _SANITIZE_KEYS = ['adminpass', 'admin_pass', 'password', 'admin_password', 'auth_token', 'new_pass', 'auth_password', 'secret_uuid', 'secret', 'sys_pswd', 'token', 'configdrive', 'chappassword', 'encrypted_key', 'private_key', 'fernetkey', 'sslkey', 'passphrase', 'cephclusterfsid', 'octaviaheartbeatkey', 'rabbitcookie', 'cephmanilaclientkey', 'pacemakerremoteauthkey', 'designaterndckey', 'cephadminkey', 'heatauthencryptionkey', 'cephclientkey', 'keystonecredential', 'barbicansimplecryptokek', 'cephrgwkey', 'swifthashsuffix', 'migrationsshkey', 'cephmdskey', 'cephmonkey', 'chapsecret'] # NOTE(ldbragst): Let's build a list of regex objects using the list of # _SANITIZE_KEYS we already have. This way, we only have to add the new key # to the list of _SANITIZE_KEYS and we can generate regular expressions # for XML and JSON automatically. _SANITIZE_PATTERNS_2 = {} _SANITIZE_PATTERNS_1 = {} _SANITIZE_PATTERNS_WILDCARD = {} # NOTE(amrith): Some regular expressions have only one parameter, some # have two parameters. Use different lists of patterns here. _FORMAT_PATTERNS_1 = [r'(%(key)s[0-9]*\s*[=]\s*)[^\s^\'^\"]+'] _FORMAT_PATTERNS_2 = [r'(%(key)s[0-9]*\s*[=]\s*[\"\'])[^\"\']*([\"\'])', r'(%(key)s[0-9]*\s*[=]\s*[\"])[^\"]*([\"])', r'(%(key)s[0-9]*\s*[=]\s*[\'])[^\']*([\'])', r'(%(key)s[0-9]*\s+[\"\'])[^\"\']*([\"\'])', r'([-]{2}%(key)s[0-9]*\s+)[^\'^\"^=^\s]+([\s]*)', r'(<%(key)s[0-9]*>)[^<]*()', r'([\"\']%(key)s[0-9]*[\"\']\s*:\s*[\"\'])[^\"\']*' r'([\"\'])', r'([\'"][^"\']*%(key)s[0-9]*[\'"]\s*:\s*u?[\'"])[^\"\']*' r'([\'"])', r'([\'"][^\'"]*%(key)s[0-9]*[\'"]\s*,\s*\'--?[A-z]+' r'\'\s*,\s*u?[\'"])[^\"\']*([\'"])', r'(%(key)s[0-9]*\s*--?[A-z]+\s*)\S+(\s*)'] _FORMAT_PATTERNS_WILDCARD = [r'([\'\"][^\"\']*%(key)s[0-9]*[\'\"]\s*:\s*u?[\'\"].*[\'\"])[^\"\']*([\'\"])'] # noqa: E501 # NOTE(dhellmann): Keep a separate list of patterns by key so we only # need to apply the substitutions for keys we find using a quick "in" # test. for key in _SANITIZE_KEYS: _SANITIZE_PATTERNS_1[key] = [] _SANITIZE_PATTERNS_2[key] = [] _SANITIZE_PATTERNS_WILDCARD[key] = [] for pattern in _FORMAT_PATTERNS_2: reg_ex = re.compile(pattern % {'key': key}, re.DOTALL | re.IGNORECASE) _SANITIZE_PATTERNS_2[key].append(reg_ex) for pattern in _FORMAT_PATTERNS_1: reg_ex = re.compile(pattern % {'key': key}, re.DOTALL | re.IGNORECASE) _SANITIZE_PATTERNS_1[key].append(reg_ex) for pattern in _FORMAT_PATTERNS_WILDCARD: reg_ex = re.compile(pattern % {'key': key}, re.DOTALL | re.IGNORECASE) _SANITIZE_PATTERNS_WILDCARD[key].append(reg_ex) def int_from_bool_as_string(subject): """Interpret a string as a boolean and return either 1 or 0. Any string value in: ('True', 'true', 'On', 'on', '1') is interpreted as a boolean True. Useful for JSON-decoded stuff and config file parsing """ return int(bool_from_string(subject)) def bool_from_string(subject, strict=False, default=False): """Interpret a subject as a boolean. A subject can be a boolean, a string or an integer. Boolean type value will be returned directly, otherwise the subject will be converted to a string. A case-insensitive match is performed such that strings matching 't','true', 'on', 'y', 'yes', or '1' are considered True and, when `strict=False`, anything else returns the value specified by 'default'. Useful for JSON-decoded stuff and config file parsing. If `strict=True`, unrecognized values, including None, will raise a ValueError which is useful when parsing values passed in from an API call. Strings yielding False are 'f', 'false', 'off', 'n', 'no', or '0'. """ if isinstance(subject, bool): return subject if not isinstance(subject, str): subject = str(subject) lowered = subject.strip().lower() if lowered in TRUE_STRINGS: return True elif lowered in FALSE_STRINGS: return False elif strict: acceptable = ', '.join( "'%s'" % s for s in sorted(TRUE_STRINGS + FALSE_STRINGS)) msg = _("Unrecognized value '%(val)s', acceptable values are:" " %(acceptable)s") % {'val': subject, 'acceptable': acceptable} raise ValueError(msg) else: return default def is_valid_boolstr(value): """Check if the provided string is a valid bool string or not. :param value: value to verify :type value: string :returns: true if value is boolean string, false otherwise .. versionadded:: 3.17 """ boolstrs = TRUE_STRINGS + FALSE_STRINGS return str(value).lower() in boolstrs def string_to_bytes(text, unit_system='IEC', return_int=False): """Converts a string into an float representation of bytes. The units supported for IEC / mixed:: Kb(it), Kib(it), Mb(it), Mib(it), Gb(it), Gib(it), Tb(it), Tib(it), Pb(it), Pib(it), Eb(it), Eib(it), Zb(it), Zib(it), Yb(it), Yib(it), Rb(it), Rib(it), Qb(it), Qib(it) KB, KiB, MB, MiB, GB, GiB, TB, TiB, PB, PiB, EB, EiB, ZB, ZiB, YB, YiB, RB, RiB, QB, QiB The units supported for SI :: kb(it), Mb(it), Gb(it), Tb(it), Pb(it), Eb(it), Zb(it), Yb(it), Rb(it), Qb(it) kB, MB, GB, TB, PB, EB, ZB, YB, RB, QB SI units are interpreted as power-of-ten (e.g. 1kb = 1000b). Note that the SI unit system does not support capital letter 'K' IEC units are interpreted as power-of-two (e.g. 1MiB = 1MB = 1024b) Mixed units interpret the "i" to mean IEC, and no "i" to mean SI (e.g. 1kb = 1000b, 1kib == 1024b). Additionaly, mixed units interpret 'K' as power-of-ten. This mode is not particuarly useful for new code, but can help with compatability for parsers such as GNU parted. :param text: String input for bytes size conversion. :param unit_system: Unit system for byte size conversion. :param return_int: If True, returns integer representation of text in bytes. (default: decimal) :returns: Numerical representation of text in bytes. :raises ValueError: If text has an invalid value. """ try: base, reg_ex = UNIT_SYSTEM_INFO[unit_system] except KeyError: msg = _('Invalid unit system: "%s"') % unit_system raise ValueError(msg) match = reg_ex.match(text) if match: magnitude = float(match.group(1)) unit_prefix = match.group(2) if match.group(3) in ['b', 'bit']: magnitude /= 8 # In the mixed matcher, IEC units (with a trailing 'i') are # interpreted as power-of-two, others as power-of-ten if unit_system == 'mixed': if unit_prefix and not unit_prefix.endswith('i'): # For maximum compatability in mixed mode, we understand # "K" (which is not strict SI) as "k" if unit_prefix.startswith == 'K': unit_prefix = 'k' base = 1000 else: base = 1024 else: msg = _('Invalid string format: %s') % text raise ValueError(msg) if not unit_prefix: res = magnitude else: res = magnitude * pow(base, UNIT_PREFIX_EXPONENT[unit_prefix]) if return_int: return int(math.ceil(res)) return res def to_slug(value, incoming=None, errors="strict"): """Normalize string. Convert to lowercase, remove non-word characters, and convert spaces to hyphens. Inspired by Django's `slugify` filter. :param value: Text to slugify :param incoming: Text's current encoding :param errors: Errors handling policy. See here for valid values http://docs.python.org/2/library/codecs.html :returns: slugified unicode representation of `value` :raises TypeError: If text is not an instance of str """ value = encodeutils.safe_decode(value, incoming, errors) # NOTE(aababilov): no need to use safe_(encode|decode) here: # encodings are always "ascii", error handling is always "ignore" # and types are always known (first: unicode; second: str) value = unicodedata.normalize("NFKD", value).encode( "ascii", "ignore").decode("ascii") value = SLUGIFY_STRIP_RE.sub("", value).strip().lower() return SLUGIFY_HYPHENATE_RE.sub("-", value) # NOTE(dhellmann): Before submitting a patch to add a new argument to # this function to allow the caller to pass in "extra" or "additional" # or "replacement" patterns to be masked out, please note that we have # discussed that feature many times and always rejected it based on # the desire to have Oslo functions behave consistently across all # projects and *especially* to have security features work the same # way no matter where they are used. If every project adopted its own # set patterns for secret values, it would be very difficult to audit # the logging to ensure that everything is properly masked. So, please # either add your pattern to the module-level variables at the top of # this file or, even better, pick an existing pattern or key to use in # your application to ensure that the value is masked by this # function. def mask_password(message, secret="***"): # nosec """Replace password with *secret* in message. :param message: The string which includes security information. :param secret: value with which to replace passwords. :returns: The unicode value of message with the password fields masked. For example: >>> mask_password("'adminPass' : 'aaaaa'") "'adminPass' : '***'" >>> mask_password("'admin_pass' : 'aaaaa'") "'admin_pass' : '***'" >>> mask_password('"password" : "aaaaa"') '"password" : "***"' >>> mask_password("'original_password' : 'aaaaa'") "'original_password' : '***'" .. versionadded:: 0.2 .. versionchanged:: 1.1 Replace also ``'auth_token'``, ``'new_pass'`` and ``'auth_password'`` keys. .. versionchanged:: 1.1.1 Replace also ``'secret_uuid'`` key. .. versionchanged:: 1.5 Replace also ``'sys_pswd'`` key. .. versionchanged:: 2.6 Replace also ``'token'`` key. .. versionchanged:: 2.7 Replace also ``'secret'`` key. .. versionchanged:: 3.4 Replace also ``'configdrive'`` key. .. versionchanged:: 3.8 Replace also ``'CHAPPASSWORD'`` key. """ try: message = str(message) except UnicodeDecodeError: # nosec # NOTE(jecarey): Temporary fix to handle cases where message is a # byte string. A better solution will be provided in Kilo. pass substitute1 = r'\g<1>' + secret substitute2 = r'\g<1>' + secret + r'\g<2>' substitute_wildcard = r'\g<1>' # NOTE(ldbragst): Check to see if anything in message contains any key # specified in _SANITIZE_KEYS, if not then just return the message since # we don't have to mask any passwords. for key in _SANITIZE_KEYS: if key in message.lower(): for pattern in _SANITIZE_PATTERNS_2[key]: message = re.sub(pattern, substitute2, message) for pattern in _SANITIZE_PATTERNS_1[key]: message = re.sub(pattern, substitute1, message) # NOTE(hberaud): Those case are poorly handled by previous # patterns. They are passwords with quotes or double quotes. # They also needs a different way to substitute group this is why # they aren't fix in the pattern 1 or 2. for pattern in _SANITIZE_PATTERNS_WILDCARD[key]: message = re.sub(pattern, substitute_wildcard, message) return message def mask_dict_password(dictionary, secret="***"): # nosec """Replace password with *secret* in a dictionary recursively. :param dictionary: The dictionary which includes secret information. :param secret: value with which to replace secret information. :returns: The dictionary with string substitutions. A dictionary (which may contain nested dictionaries) contains information (such as passwords) which should not be revealed, and this function helps detect and replace those with the 'secret' provided (or `***` if none is provided). Substitution is performed in one of three situations: If the key is something that is considered to be indicative of a secret, then the corresponding value is replaced with the secret provided (or `***` if none is provided). If a value in the dictionary is a string, then it is masked using the ``mask_password()`` function. Finally, if a value is a dictionary, this function will recursively mask that dictionary as well. For example: >>> mask_dict_password({'password': 'd81juxmEW_', >>> 'user': 'admin', >>> 'home-dir': '/home/admin'}, >>> '???') {'password': '???', 'user': 'admin', 'home-dir': '/home/admin'} For example (the value is masked using mask_password()) >>> mask_dict_password({'password': '--password d81juxmEW_', >>> 'user': 'admin', >>> 'home-dir': '/home/admin'}, >>> '???') {'password': '--password ???', 'user': 'admin', 'home-dir': '/home/admin'} For example (a nested dictionary is masked): >>> mask_dict_password({"nested": {'password': 'd81juxmEW_', >>> 'user': 'admin', >>> 'home': '/home/admin'}}, >>> '???') {"nested": {'password': '???', 'user': 'admin', 'home': '/home/admin'}} .. versionadded:: 3.4 """ if not isinstance(dictionary, collections.abc.Mapping): raise TypeError("Expected a Mapping, got %s instead." % type(dictionary)) out = {} for k, v in dictionary.items(): if isinstance(v, collections.abc.Mapping): out[k] = mask_dict_password(v, secret=secret) continue # NOTE(jlvillal): Check to see if anything in the dictionary 'key' # contains any key specified in _SANITIZE_KEYS. k_matched = False if isinstance(k, str): for sani_key in _SANITIZE_KEYS: if sani_key in k.lower(): out[k] = secret k_matched = True break if not k_matched: # We did not find a match for the key name in the # _SANITIZE_KEYS, so we fall through to here if isinstance(v, str): out[k] = mask_password(v, secret=secret) else: # Just leave it alone. out[k] = v return out def is_int_like(val): """Check if a value looks like an integer with base 10. :param val: Value to verify :type val: string :returns: bool .. versionadded:: 1.1 """ try: return str(int(val)) == str(val) except (TypeError, ValueError): return False def check_string_length(value, name=None, min_length=0, max_length=None): """Check the length of specified string. :param value: the value of the string :param name: the name of the string :param min_length: the min_length of the string :param max_length: the max_length of the string :raises TypeError, ValueError: For any invalid input. .. versionadded:: 3.7 """ if name is None: name = value if not isinstance(value, str): msg = _("%s is not a string or unicode") % name raise TypeError(msg) length = len(value) if length < min_length: msg = _("%(name)s has %(length)s characters, less than " "%(min_length)s.") % {'name': name, 'length': length, 'min_length': min_length} raise ValueError(msg) if max_length and length > max_length: msg = _("%(name)s has %(length)s characters, more than " "%(max_length)s.") % {'name': name, 'length': length, 'max_length': max_length} raise ValueError(msg) def validate_integer(value, name, min_value=None, max_value=None): """Make sure that value is a valid integer, potentially within range. :param value: value of the integer :param name: name of the integer :param min_value: min_value of the integer :param max_value: max_value of the integer :returns: integer :raises: ValueError if value is an invalid integer .. versionadded:: 3.33 """ try: value = int(str(value)) except (ValueError, UnicodeEncodeError): msg = _('%(value_name)s must be an integer' ) % {'value_name': name} raise ValueError(msg) if min_value is not None and value < min_value: msg = _('%(value_name)s must be >= %(min_value)d' ) % {'value_name': name, 'min_value': min_value} raise ValueError(msg) if max_value is not None and value > max_value: msg = _('%(value_name)s must be <= %(max_value)d' ) % {'value_name': name, 'max_value': max_value} raise ValueError(msg) return value def split_path(path, minsegs=1, maxsegs=None, rest_with_last=False): """Validate and split the given HTTP request path. **Examples**:: ['a'] = _split_path('/a') ['a', None] = _split_path('/a', 1, 2) ['a', 'c'] = _split_path('/a/c', 1, 2) ['a', 'c', 'o/r'] = _split_path('/a/c/o/r', 1, 3, True) :param path: HTTP Request path to be split :param minsegs: Minimum number of segments to be extracted :param maxsegs: Maximum number of segments to be extracted :param rest_with_last: If True, trailing data will be returned as part of last segment. If False, and there is trailing data, raises ValueError. :returns: list of segments with a length of maxsegs (non-existent segments will return as None) :raises: ValueError if given an invalid path .. versionadded:: 3.11 """ if not maxsegs: maxsegs = minsegs if minsegs > maxsegs: raise ValueError(_('minsegs > maxsegs: %(min)d > %(max)d)') % {'min': minsegs, 'max': maxsegs}) if rest_with_last: segs = path.split('/', maxsegs) minsegs += 1 maxsegs += 1 count = len(segs) if (segs[0] or count < minsegs or count > maxsegs or '' in segs[1:minsegs]): raise ValueError(_('Invalid path: %s') % urllib.parse.quote(path)) else: minsegs += 1 maxsegs += 1 segs = path.split('/', maxsegs) count = len(segs) if (segs[0] or count < minsegs or count > maxsegs + 1 or '' in segs[1:minsegs] or (count == maxsegs + 1 and segs[maxsegs])): raise ValueError(_('Invalid path: %s') % urllib.parse.quote(path)) segs = segs[1:maxsegs] segs.extend([None] * (maxsegs - 1 - len(segs))) return segs def split_by_commas(value): """Split values by commas and quotes according to api-wg :param value: value to be split .. versionadded:: 3.17 """ # pyparsing is a slow import; defer loading until we need it import pyparsing as pp word = ( pp.QuotedString(quoteChar='"', escChar='\\') | pp.Word(pp.printables, excludeChars='",') ) grammar = pp.stringStart + pp.delimitedList(word) + pp.stringEnd try: return list(grammar.parseString(value)) except pp.ParseException: raise ValueError("Invalid value: %s" % value) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1740145268.2810547 oslo_utils-8.2.0/oslo_utils/tests/0000775000175000017500000000000000000000000017276 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/oslo_utils/tests/__init__.py0000664000175000017500000000104100000000000021403 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/oslo_utils/tests/base.py0000664000175000017500000000362100000000000020564 0ustar00zuulzuul00000000000000# Copyright 2010-2011 OpenStack Foundation # Copyright (c) 2013 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import os import fixtures import testtools _TRUE_VALUES = ('true', '1', 'yes') # FIXME(dhellmann) Update this to use oslo.test library class TestCase(testtools.TestCase): """Test case base class for all unit tests.""" def setUp(self): """Run before each test method to initialize test environment.""" super().setUp() test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0) try: test_timeout = int(test_timeout) except ValueError: # If timeout value is invalid do not set a timeout. test_timeout = 0 if test_timeout > 0: self.useFixture(fixtures.Timeout(test_timeout, gentle=True)) self.useFixture(fixtures.NestedTempfile()) self.useFixture(fixtures.TempHomeDir()) if os.environ.get('OS_STDOUT_CAPTURE') in _TRUE_VALUES: stdout = self.useFixture(fixtures.StringStream('stdout')).stream self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout)) if os.environ.get('OS_STDERR_CAPTURE') in _TRUE_VALUES: stderr = self.useFixture(fixtures.StringStream('stderr')).stream self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr)) self.log_fixture = self.useFixture(fixtures.FakeLogger()) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1740145268.2810547 oslo_utils-8.2.0/oslo_utils/tests/fake/0000775000175000017500000000000000000000000020204 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/oslo_utils/tests/fake/__init__.py0000664000175000017500000000160200000000000022314 0ustar00zuulzuul00000000000000# Copyright 2012 IBM Corp. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. class FakeDriver(): def __init__(self, first_arg=True): self.first_arg = first_arg class FakeDriver2(): def __init__(self, first_arg): self.first_arg = first_arg class FakeDriver3(): def __init__(self): raise ImportError("ImportError occurs in __init__") ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1740145268.2810547 oslo_utils-8.2.0/oslo_utils/tests/fake/v2/0000775000175000017500000000000000000000000020533 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/oslo_utils/tests/fake/v2/__init__.py0000664000175000017500000000000000000000000022632 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/oslo_utils/tests/fake/v2/dummpy.py0000664000175000017500000000161100000000000022417 0ustar00zuulzuul00000000000000# Copyright 2016, EasyStack, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. class V2FakeDriver: def __init__(self, first_arg=True): self.first_arg = first_arg class V2FakeDriver2: def __init__(self, first_arg): self.first_arg = first_arg class V2FakeDriver3: def __init__(self): raise ImportError("ImportError occurs in __init__") ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1740145268.2810547 oslo_utils-8.2.0/oslo_utils/tests/imageutils/0000775000175000017500000000000000000000000021441 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/oslo_utils/tests/imageutils/__init__.py0000664000175000017500000000000000000000000023540 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/oslo_utils/tests/imageutils/test_format_inspector.py0000664000175000017500000013517300000000000026442 0ustar00zuulzuul00000000000000# Copyright 2020 Red Hat, Inc # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import io import os import struct import subprocess import tempfile from unittest import mock import ddt from oslo_utils import units from oslo_utils.imageutils import format_inspector from oslo_utils.imageutils import QemuImgInfo from oslotest import base as test_base TEST_IMAGE_PREFIX = 'oslo-unittest-formatinspector-' def get_size_format_from_qemu_img(filename): output = subprocess.check_output( 'qemu-img info --output=json "%s"' % filename, shell=True) info = QemuImgInfo(output, format='json') return info.virtual_size, info.file_format @ddt.ddt class TestFormatInspectors(test_base.BaseTestCase): def setUp(self): super().setUp() self._created_files = [] def tearDown(self): super().tearDown() for fn in self._created_files: try: os.remove(fn) except Exception: pass def _create_iso(self, image_size, subformat='9660'): """Create an ISO file of the given size. :param image_size: The size of the image to create in bytes :param subformat: The subformat to use, if any """ # these tests depend on mkisofs # being installed and in the path, # if it is not installed, skip try: subprocess.check_output('mkisofs --version', shell=True) except Exception: self.skipTest('mkisofs not installed') size = image_size // units.Mi base_cmd = "mkisofs" if subformat == 'udf': # depending on the distribution mkisofs may not support udf # and may be provided by genisoimage instead. As a result we # need to check if the command supports udf via help # instead of checking the installed version. # mkisofs --help outputs to stderr so we need to # redirect it to stdout to use grep. try: subprocess.check_output( 'mkisofs --help 2>&1 | grep udf', shell=True) except Exception: self.skipTest('mkisofs does not support udf format') base_cmd += " -udf" prefix = TEST_IMAGE_PREFIX prefix += '-%s-' % subformat fn = tempfile.mktemp(prefix=prefix, suffix='.iso') self._created_files.append(fn) subprocess.check_output( 'dd if=/dev/zero of=%s bs=1M count=%i' % (fn, size), shell=True) # We need to use different file as input and output as the behavior # of mkisofs is version dependent if both the input and the output # are the same and can cause test failures out_fn = "%s.iso" % fn subprocess.check_output( '{} -V "TEST" -o {} {}'.format(base_cmd, out_fn, fn), shell=True) self._created_files.append(out_fn) return out_fn def _create_gpt(self, image_size, subformat='gpt'): data = bytearray(b'\x00' * 512 * 10) # The last two bytes of the first sector is the little-endian signature # value 0xAA55 data[510:512] = b'\x55\xAA' # This is one EFI Protective MBR partition in the first PTE slot, # which is 16 bytes starting at offset 446. data[446:446 + 16] = struct.pack('', 'a', '', 'b')) self.assertEqual(expected_name, name) def test_inner_class(self): obj = self.InnerCallableClass() name = reflection.get_callable_name(obj) expected_name = '.'.join((__name__, 'GetCallableNameTestExtended', 'InnerCallableClass')) self.assertEqual(expected_name, name) class GetCallableArgsTest(test_base.BaseTestCase): def test_mere_function(self): result = reflection.get_callable_args(mere_function) self.assertEqual(['a', 'b'], result) def test_function_with_defaults(self): result = reflection.get_callable_args(function_with_defs) self.assertEqual(['a', 'b', 'optional'], result) def test_required_only(self): result = reflection.get_callable_args(function_with_defs, required_only=True) self.assertEqual(['a', 'b'], result) def test_method(self): result = reflection.get_callable_args(Class.method) self.assertEqual(['self', 'c', 'd'], result) def test_instance_method(self): result = reflection.get_callable_args(Class().method) self.assertEqual(['c', 'd'], result) def test_class_method(self): result = reflection.get_callable_args(Class.class_method) self.assertEqual(['g', 'h'], result) def test_class_constructor(self): result = reflection.get_callable_args(ClassWithInit) self.assertEqual(['k', 'lll'], result) def test_class_with_call(self): result = reflection.get_callable_args(CallableClass()) self.assertEqual(['i', 'j'], result) def test_decorators_work(self): @dummy_decorator def special_fun(x, y): pass result = reflection.get_callable_args(special_fun) self.assertEqual(['x', 'y'], result) class AcceptsKwargsTest(test_base.BaseTestCase): def test_no_kwargs(self): self.assertEqual(False, reflection.accepts_kwargs(mere_function)) def test_with_kwargs(self): self.assertEqual(True, reflection.accepts_kwargs(function_with_kwargs)) class GetClassNameTest(test_base.BaseTestCase): def test_std_exception(self): name = reflection.get_class_name(RuntimeError) self.assertEqual('RuntimeError', name) def test_class(self): name = reflection.get_class_name(Class) self.assertEqual('.'.join((__name__, 'Class')), name) def test_qualified_class(self): class QualifiedClass: pass name = reflection.get_class_name(QualifiedClass) self.assertEqual('.'.join((__name__, 'QualifiedClass')), name) def test_instance(self): name = reflection.get_class_name(Class()) self.assertEqual('.'.join((__name__, 'Class')), name) def test_int(self): name = reflection.get_class_name(42) self.assertEqual('int', name) def test_class_method(self): name = reflection.get_class_name(Class.class_method) self.assertEqual('%s.Class' % __name__, name) # test with fully_qualified=False name = reflection.get_class_name(Class.class_method, fully_qualified=False) self.assertEqual('Class', name) def test_static_method(self): self.assertRaises(TypeError, reflection.get_class_name, Class.static_method) def test_unbound_method(self): self.assertRaises(TypeError, reflection.get_class_name, mere_function) def test_bound_method(self): c = Class() name = reflection.get_class_name(c.method) self.assertEqual('%s.Class' % __name__, name) # test with fully_qualified=False name = reflection.get_class_name(c.method, fully_qualified=False) self.assertEqual('Class', name) class GetAllClassNamesTest(test_base.BaseTestCase): def test_std_class(self): names = list(reflection.get_all_class_names(RuntimeError)) self.assertEqual(RUNTIME_ERROR_CLASSES, names) def test_std_class_up_to(self): names = list(reflection.get_all_class_names(RuntimeError, up_to=Exception)) self.assertEqual(RUNTIME_ERROR_CLASSES[:-2], names) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/oslo_utils/tests/test_secretutils.py0000664000175000017500000000721700000000000023264 0ustar00zuulzuul00000000000000# All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import hashlib import hmac from oslotest import base as test_base import testscenarios from oslo_utils import secretutils class SecretUtilsTest(testscenarios.TestWithScenarios, test_base.BaseTestCase): _gen_digest = lambda text: hmac.new(b'foo', text.encode('utf-8'), digestmod=hashlib.sha1).digest() scenarios = [ ('binary', {'converter': _gen_digest}), ('unicode', {'converter': lambda text: text}), ] _test_data = b"Openstack forever" _md5_digest = hashlib.md5(_test_data).digest() def test_md5_with_data(self): digest = secretutils.md5(self._test_data).digest() self.assertEqual(digest, self._md5_digest) digest = secretutils.md5(self._test_data, usedforsecurity=True).digest() self.assertEqual(digest, self._md5_digest) digest = secretutils.md5(self._test_data, usedforsecurity=False).digest() self.assertEqual(digest, self._md5_digest) def test_md5_without_data(self): md5 = secretutils.md5() md5.update(self._test_data) digest = md5.digest() self.assertEqual(digest, self._md5_digest) md5 = secretutils.md5(usedforsecurity=True) md5.update(self._test_data) digest = md5.digest() self.assertEqual(digest, self._md5_digest) md5 = secretutils.md5(usedforsecurity=False) md5.update(self._test_data) digest = md5.digest() self.assertEqual(digest, self._md5_digest) def test_string_data_raises_type_error(self): self.assertRaises(TypeError, hashlib.md5, 'foo') self.assertRaises(TypeError, secretutils.md5, 'foo') self.assertRaises( TypeError, secretutils.md5, 'foo', usedforsecurity=True) self.assertRaises( TypeError, secretutils.md5, 'foo', usedforsecurity=False) def test_none_data_raises_type_error(self): self.assertRaises(TypeError, hashlib.md5, None) self.assertRaises(TypeError, secretutils.md5, None) self.assertRaises( TypeError, secretutils.md5, None, usedforsecurity=True) self.assertRaises( TypeError, secretutils.md5, None, usedforsecurity=False) def test_password_mksalt(self): self.assertRaises(ValueError, secretutils.crypt_mksalt, 'MD5') salt = secretutils.crypt_mksalt('SHA-256') self.assertEqual(3 + 16, len(salt)) self.assertTrue(salt.startswith('$5$')) salt = secretutils.crypt_mksalt('SHA-512') self.assertEqual(3 + 16, len(salt)) self.assertTrue(salt.startswith('$6$')) def test_password_crypt(self): self.assertEqual( '$5$mysalt$fcnMdhaFpUmeWtGOgVuImueZGL1v0Q1kUVbV2NbFOX4', secretutils.crypt_password('mytopsecret', '$5$mysalt$')) self.assertEqual( '$6$mysalt$jTEJ24XtvcWmav/sTQb1tYqmk1kBQD/sxcMIxEPUcie' 'J8L9AuCTWxYlxGz.XtIQYWspWkUXQz9zPIFTSKubP6.', secretutils.crypt_password('mytopsecret', '$6$mysalt$')) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/oslo_utils/tests/test_specs_matcher.py0000664000175000017500000004434500000000000023541 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from oslotest import base as test_base from oslo_utils import specs_matcher class SpecsMatcherTestCase(test_base.BaseTestCase): def _do_specs_matcher_test(self, value, req, matches): assertion = self.assertTrue if matches else self.assertFalse assertion(specs_matcher.match(value, req)) def test_specs_matches_simple(self): self._do_specs_matcher_test( value='1', req='1', matches=True) def test_specs_fails_string_vs_int(self): # With no operator specified it is a string comparison test, therefore # '1' does not equal '01' self._do_specs_matcher_test( value='01', req='1', matches=False) def test_specs_match_int_leading_zero(self): # Show that numerical comparison works with leading zero self._do_specs_matcher_test( value='01', req='== 1', matches=True) def test_specs_fails_simple(self): self._do_specs_matcher_test( value='', req='1', matches=False) def test_specs_fails_simple2(self): self._do_specs_matcher_test( value='3', req='1', matches=False) def test_specs_fails_simple3(self): self._do_specs_matcher_test( value='222', req='2', matches=False) def test_specs_fails_with_bogus_ops(self): self._do_specs_matcher_test( value='4', req='! 2', matches=False) def test_specs_matches_with_op_eq(self): self._do_specs_matcher_test( value='123', req='= 123', matches=True) def test_specs_matches_with_op_eq2(self): self._do_specs_matcher_test( value='124', req='= 123', matches=True) def test_specs_fails_with_op_eq(self): self._do_specs_matcher_test( value='34', req='= 234', matches=False) def test_specs_fails_with_op_eq3(self): self._do_specs_matcher_test( value='34', req='=', matches=False) def test_specs_matches_with_op_seq(self): self._do_specs_matcher_test( value='123', req='s== 123', matches=True) def test_specs_fails_with_op_seq(self): self._do_specs_matcher_test( value='1234', req='s== 123', matches=False) def test_specs_matches_with_op_sneq(self): self._do_specs_matcher_test( value='1234', req='s!= 123', matches=True) def test_specs_fails_with_op_sneq(self): self._do_specs_matcher_test( value='123', req='s!= 123', matches=False) def test_specs_matches_with_op_sge(self): self._do_specs_matcher_test( value='234', req='s>= 1000', matches=True) def test_specs_matches_with_op_sge2(self): self._do_specs_matcher_test( value='234', req='s>= 234', matches=True) def test_specs_fails_with_op_sge(self): self._do_specs_matcher_test( value='1000', req='s>= 234', matches=False) def test_specs_matches_with_op_sle(self): self._do_specs_matcher_test( value='1000', req='s<= 1234', matches=True) def test_specs_matches_with_op_sle2(self): self._do_specs_matcher_test( value='1234', req='s<= 1234', matches=True) def test_specs_fails_with_op_sle(self): self._do_specs_matcher_test( value='1234', req='s<= 1000', matches=False) def test_specs_matches_with_op_sl(self): self._do_specs_matcher_test( value='12', req='s< 2', matches=True) def test_specs_fails_with_op_sl(self): self._do_specs_matcher_test( value='2', req='s< 12', matches=False) def test_specs_fails_with_op_sl2(self): self._do_specs_matcher_test( value='12', req='s< 12', matches=False) def test_specs_matches_with_op_sg(self): self._do_specs_matcher_test( value='2', req='s> 12', matches=True) def test_specs_fails_with_op_sg(self): self._do_specs_matcher_test( value='12', req='s> 2', matches=False) def test_specs_fails_with_op_sg2(self): self._do_specs_matcher_test( value='12', req='s> 12', matches=False) def test_specs_matches_with_op_in(self): self._do_specs_matcher_test( value='12311321', req=' 11', matches=True) def test_specs_matches_with_op_in2(self): self._do_specs_matcher_test( value='12311321', req=' 12311321', matches=True) def test_specs_matches_with_op_in3(self): self._do_specs_matcher_test( value='12311321', req=' 12311321 ', matches=True) def test_specs_fails_with_op_in(self): self._do_specs_matcher_test( value='12310321', req=' 11', matches=False) def test_specs_fails_with_op_in2(self): self._do_specs_matcher_test( value='12310321', req=' 11 ', matches=False) def test_specs_matches_with_op_or(self): self._do_specs_matcher_test( value='12', req=' 11 12', matches=True) def test_specs_matches_with_op_or2(self): self._do_specs_matcher_test( value='12', req=' 11 12 ', matches=True) def test_specs_matches_with_op_or3(self): self._do_specs_matcher_test( value='12', req=' 12', matches=True) def test_specs_fails_with_op_or(self): self._do_specs_matcher_test( value='13', req=' 11 12', matches=False) def test_specs_fails_with_op_or2(self): self._do_specs_matcher_test( value='13', req=' 11 12 ', matches=False) def test_specs_fails_with_op_or3(self): self._do_specs_matcher_test( value='13', req=' 11', matches=False) def test_specs_matches_with_op_le(self): self._do_specs_matcher_test( value='2', req='<= 10', matches=True) def test_specs_matches_with_op_le2(self): self._do_specs_matcher_test( value='10', req='<= 10', matches=True) def test_specs_fails_with_op_le(self): self._do_specs_matcher_test( value='3', req='<= 2', matches=False) def test_specs_matches_with_op_ge(self): self._do_specs_matcher_test( value='3', req='>= 1', matches=True) def test_specs_matches_with_op_ge2(self): self._do_specs_matcher_test( value='3.0', req='>= 3', matches=True) def test_specs_matches_with_op_g(self): self._do_specs_matcher_test( value='3', req='> 1', matches=True) def test_specs_matches_with_op_g2(self): self._do_specs_matcher_test( value='3', req='> 3', matches=False) def test_specs_matches_with_op_g3(self): self._do_specs_matcher_test( value='3.0', req='> 2', matches=True) def test_specs_matches_with_op_l(self): self._do_specs_matcher_test( value='3', req='< 5', matches=True) def test_specs_matches_with_op_l2(self): self._do_specs_matcher_test( value='3', req='< 3', matches=False) def test_specs_matches_with_op_l3(self): self._do_specs_matcher_test( value='1.0', req='< 6', matches=True) def test_specs_fails_with_op_ge(self): self._do_specs_matcher_test( value='2', req='>= 3', matches=False) def test_specs_matches_with_op_ne(self): self._do_specs_matcher_test( value='3.2', req='!= 3.1', matches=True) def test_specs_fails_with_op_ne(self): self._do_specs_matcher_test( value='3.2', req='!= 3.2', matches=False) def test_specs_matches_with_op_eqeq(self): self._do_specs_matcher_test( value='3', req='== 3', matches=True) def test_specs_matches_with_op_eqeq2(self): self._do_specs_matcher_test( value='3.0', req='== 3', matches=True) def test_specs_fails_with_op_eqeq(self): self._do_specs_matcher_test( value='3.0', req='== 3.1', matches=False) def test_specs_matches_all_with_op_allin(self): self._do_specs_matcher_test( value=str(['aes', 'mmx', 'aux']), req=' aes mmx', matches=True) def test_specs_matches_one_with_op_allin(self): self._do_specs_matcher_test( value=str(['aes', 'mmx', 'aux']), req=' mmx', matches=True) def test_specs_fails_with_op_allin(self): self._do_specs_matcher_test( value=str(['aes', 'mmx', 'aux']), req=' txt', matches=False) def test_specs_fails_all_with_op_allin(self): self._do_specs_matcher_test( value=str(['aes', 'mmx', 'aux']), req=' txt 3dnow', matches=False) def test_specs_fails_match_one_with_op_allin(self): self._do_specs_matcher_test( value=str(['aes', 'mmx', 'aux']), req=' txt aes', matches=False) def test_specs_fails_match_substr_single(self): self._do_specs_matcher_test( value=str(['X_X']), req=' _', matches=False) def test_specs_fails_match_substr(self): self._do_specs_matcher_test( value=str(['X___X']), req=' ___', matches=False) def test_specs_fails_match_substr_reversed(self): self._do_specs_matcher_test( value=str(['aes', 'mmx', 'aux']), req=' XaesX', matches=False) def test_specs_fails_onechar_with_op_allin(self): self.assertRaises( TypeError, specs_matcher.match, value=str(['aes', 'mmx', 'aux']), req=' e') def test_specs_errors_list_with_op_allin(self): self.assertRaises( TypeError, specs_matcher.match, value=['aes', 'mmx', 'aux'], req=' aes') def test_specs_errors_str_with_op_allin(self): self.assertRaises( TypeError, specs_matcher.match, value='aes', req=' aes') def test_specs_errors_dict_literal_with_op_allin(self): self.assertRaises( TypeError, specs_matcher.match, value=str({'aes': 1}), req=' aes') def test_specs_errors_bad_literal_with_op_allin(self): self.assertRaises( TypeError, specs_matcher.match, value="^&*($", req=' aes') def test_specs_fails_not_enough_args_with_op_rangein(self): self.assertRaises( TypeError, specs_matcher.match, value="23", req=' [ 10 ]') def test_specs_fails_no_brackets_with_op_rangein(self): self.assertRaises( TypeError, specs_matcher.match, value="23", req=' 10 20') def test_specs_fails_no_opening_bracket_with_op_rangein(self): self.assertRaises( TypeError, specs_matcher.match, value="23", req=' 10 20 ]') def test_specs_fails_no_closing_bracket_with_op_rangein(self): self.assertRaises( TypeError, specs_matcher.match, value="23", req=' [ 10 20') def test_specs_fails_invalid_brackets_with_op_rangein(self): self.assertRaises( TypeError, specs_matcher.match, value="23", req=' { 10 20 }') def test_specs_fails_not_opening_brackets_with_op_rangein(self): self.assertRaises( TypeError, specs_matcher.match, value="23", req=' ) 10 20 )') def test_specs_fails_not_closing_brackets_with_op_rangein(self): self.assertRaises( TypeError, specs_matcher.match, value="23", req=' ( 10 20 (') def test_specs_fails_reverse_brackets_with_op_rangein(self): self.assertRaises( TypeError, specs_matcher.match, value="23", req=' ) 10 20 (') def test_specs_fails_too_many_args_with_op_rangein(self): self.assertRaises( TypeError, specs_matcher.match, value="23", req=' [ 10 20 30 ]') def test_specs_fails_bad_limits_with_op_rangein(self): self.assertRaises( TypeError, specs_matcher.match, value="23", req=' [ 20 10 ]') def test_specs_fails_match_beyond_scope_with_op_rangein_le(self): self._do_specs_matcher_test( matches=False, value="23", req=' [ 10 20 ]') def test_specs_fails_match_beyond_scope_with_op_rangein_lt(self): self._do_specs_matcher_test( matches=False, value="23", req=' [ 10 20 )') def test_specs_fails_match_under_scope_with_op_rangein_ge(self): self._do_specs_matcher_test( matches=False, value="5", req=' [ 10 20 ]') def test_specs_fails_match_under_scope_with_op_rangein_gt(self): self._do_specs_matcher_test( matches=False, value="5", req=' ( 10 20 ]') def test_specs_fails_match_float_beyond_scope_with_op_rangein_le(self): self._do_specs_matcher_test( matches=False, value="20.3", req=' [ 10.1 20.2 ]') def test_specs_fails_match_float_beyond_scope_with_op_rangein_lt(self): self._do_specs_matcher_test( matches=False, value="20.3", req=' [ 10.1 20.2 )') def test_specs_fails_match_float_under_scope_with_op_rangein_ge(self): self._do_specs_matcher_test( matches=False, value="5.0", req=' [ 5.1 20.2 ]') def test_specs_fails_match_float_under_scope_with_op_rangein_gt(self): self._do_specs_matcher_test( matches=False, value="5.0", req=' ( 5.1 20.2 ]') def test_specs_matches_int_lower_int_range_with_op_rangein_ge(self): self._do_specs_matcher_test( matches=True, value="10", req=' [ 10 20 ]') def test_specs_fails_matchesint_lower_int_range_with_op_rangein_gt(self): self._do_specs_matcher_test( matches=False, value="10", req=' ( 10 20 ]') def test_specs_matches_float_lower_float_range_with_op_rangein_ge(self): self._do_specs_matcher_test( matches=True, value="10.1", req=' [ 10.1 20 ]') def test_specs_fails_matche_float_lower_float_range_with_op_rangein_gt( self): self._do_specs_matcher_test( matches=False, value="10.1", req=' ( 10.1 20 ]') def test_specs_matches_int_with_int_range_with_op_rangein(self): self._do_specs_matcher_test( matches=True, value="15", req=' [ 10 20 ]') def test_specs_matches_float_with_int_limit_with_op_rangein(self): self._do_specs_matcher_test( matches=True, value="15.5", req=' [ 10 20 ]') def test_specs_matches_int_upper_int_range_with_op_rangein(self): self._do_specs_matcher_test( matches=True, value="20", req=' [ 10 20 ]') def test_specs_fails_matche_int_upper_int_range_with_op_rangein_lt(self): self._do_specs_matcher_test( matches=False, value="20", req=' [ 10 20 )') def test_specs_matches_float_upper_mixed_range_with_op_rangein(self): self._do_specs_matcher_test( matches=True, value="20.5", req=' [ 10 20.5 ]') def test_specs_fails_matche_float_upper_mixed_range_with_op_rangein_lt( self): self._do_specs_matcher_test( matches=False, value="20.5", req=' [ 10 20.5 )') def test_specs_matches_float_with_float_limit_with_op_rangein(self): self._do_specs_matcher_test( matches=True, value="12.5", req=' [ 10.1 20.1 ]') def test_specs_matches_only_one_with_op_rangein(self): self._do_specs_matcher_test( matches=True, value="10.1", req=' [ 10.1 10.1 ]') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/oslo_utils/tests/test_strutils.py0000664000175000017500000012441200000000000022604 0ustar00zuulzuul00000000000000# Copyright 2011 OpenStack Foundation. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import collections.abc import copy import math from unittest import mock import ddt from oslotest import base as test_base import testscenarios from oslo_utils import strutils from oslo_utils import units load_tests = testscenarios.load_tests_apply_scenarios class StrUtilsTest(test_base.BaseTestCase): def test_bool_bool_from_string(self): self.assertTrue(strutils.bool_from_string(True)) self.assertFalse(strutils.bool_from_string(False)) def test_bool_bool_from_string_default(self): self.assertTrue(strutils.bool_from_string('', default=True)) self.assertFalse(strutils.bool_from_string('wibble', default=False)) def _test_bool_from_string(self, c): self.assertTrue(strutils.bool_from_string(c('true'))) self.assertTrue(strutils.bool_from_string(c('TRUE'))) self.assertTrue(strutils.bool_from_string(c('on'))) self.assertTrue(strutils.bool_from_string(c('On'))) self.assertTrue(strutils.bool_from_string(c('yes'))) self.assertTrue(strutils.bool_from_string(c('YES'))) self.assertTrue(strutils.bool_from_string(c('yEs'))) self.assertTrue(strutils.bool_from_string(c('1'))) self.assertTrue(strutils.bool_from_string(c('T'))) self.assertTrue(strutils.bool_from_string(c('t'))) self.assertTrue(strutils.bool_from_string(c('Y'))) self.assertTrue(strutils.bool_from_string(c('y'))) self.assertFalse(strutils.bool_from_string(c('false'))) self.assertFalse(strutils.bool_from_string(c('FALSE'))) self.assertFalse(strutils.bool_from_string(c('off'))) self.assertFalse(strutils.bool_from_string(c('OFF'))) self.assertFalse(strutils.bool_from_string(c('no'))) self.assertFalse(strutils.bool_from_string(c('0'))) self.assertFalse(strutils.bool_from_string(c('42'))) self.assertFalse(strutils.bool_from_string(c( 'This should not be True'))) self.assertFalse(strutils.bool_from_string(c('F'))) self.assertFalse(strutils.bool_from_string(c('f'))) self.assertFalse(strutils.bool_from_string(c('N'))) self.assertFalse(strutils.bool_from_string(c('n'))) # Whitespace should be stripped self.assertTrue(strutils.bool_from_string(c(' 1 '))) self.assertTrue(strutils.bool_from_string(c(' true '))) self.assertFalse(strutils.bool_from_string(c(' 0 '))) self.assertFalse(strutils.bool_from_string(c(' false '))) def test_bool_from_string(self): self._test_bool_from_string(lambda s: s) def test_unicode_bool_from_string(self): self._test_bool_from_string(str) self.assertFalse(strutils.bool_from_string('使用', strict=False)) exc = self.assertRaises(ValueError, strutils.bool_from_string, '使用', strict=True) expected_msg = ("Unrecognized value '使用', acceptable values are:" " '0', '1', 'f', 'false', 'n', 'no', 'off', 'on'," " 't', 'true', 'y', 'yes'") self.assertEqual(expected_msg, str(exc)) def test_other_bool_from_string(self): self.assertFalse(strutils.bool_from_string(None)) self.assertFalse(strutils.bool_from_string(mock.Mock())) def test_int_bool_from_string(self): self.assertTrue(strutils.bool_from_string(1)) self.assertFalse(strutils.bool_from_string(-1)) self.assertFalse(strutils.bool_from_string(0)) self.assertFalse(strutils.bool_from_string(2)) def test_strict_bool_from_string(self): # None isn't allowed in strict mode exc = self.assertRaises(ValueError, strutils.bool_from_string, None, strict=True) expected_msg = ("Unrecognized value 'None', acceptable values are:" " '0', '1', 'f', 'false', 'n', 'no', 'off', 'on'," " 't', 'true', 'y', 'yes'") self.assertEqual(expected_msg, str(exc)) # Unrecognized strings aren't allowed self.assertFalse(strutils.bool_from_string('Other', strict=False)) exc = self.assertRaises(ValueError, strutils.bool_from_string, 'Other', strict=True) expected_msg = ("Unrecognized value 'Other', acceptable values are:" " '0', '1', 'f', 'false', 'n', 'no', 'off', 'on'," " 't', 'true', 'y', 'yes'") self.assertEqual(expected_msg, str(exc)) # Unrecognized numbers aren't allowed exc = self.assertRaises(ValueError, strutils.bool_from_string, 2, strict=True) expected_msg = ("Unrecognized value '2', acceptable values are:" " '0', '1', 'f', 'false', 'n', 'no', 'off', 'on'," " 't', 'true', 'y', 'yes'") self.assertEqual(expected_msg, str(exc)) # False-like values are allowed self.assertFalse(strutils.bool_from_string('f', strict=True)) self.assertFalse(strutils.bool_from_string('false', strict=True)) self.assertFalse(strutils.bool_from_string('off', strict=True)) self.assertFalse(strutils.bool_from_string('n', strict=True)) self.assertFalse(strutils.bool_from_string('no', strict=True)) self.assertFalse(strutils.bool_from_string('0', strict=True)) self.assertTrue(strutils.bool_from_string('1', strict=True)) # Avoid font-similarity issues (one looks like lowercase-el, zero like # oh, etc...) for char in ('O', 'o', 'L', 'l', 'I', 'i'): self.assertRaises(ValueError, strutils.bool_from_string, char, strict=True) def test_int_from_bool_as_string(self): self.assertEqual(1, strutils.int_from_bool_as_string(True)) self.assertEqual(0, strutils.int_from_bool_as_string(False)) def test_is_valid_boolstr(self): self.assertTrue(strutils.is_valid_boolstr('true')) self.assertTrue(strutils.is_valid_boolstr('false')) self.assertTrue(strutils.is_valid_boolstr('yes')) self.assertTrue(strutils.is_valid_boolstr('no')) self.assertTrue(strutils.is_valid_boolstr('y')) self.assertTrue(strutils.is_valid_boolstr('n')) self.assertTrue(strutils.is_valid_boolstr('1')) self.assertTrue(strutils.is_valid_boolstr('0')) self.assertTrue(strutils.is_valid_boolstr(1)) self.assertTrue(strutils.is_valid_boolstr(0)) self.assertFalse(strutils.is_valid_boolstr('maybe')) self.assertFalse(strutils.is_valid_boolstr('only on tuesdays')) def test_slugify(self): to_slug = strutils.to_slug self.assertRaises(TypeError, to_slug, True) self.assertEqual("hello", to_slug("hello")) self.assertEqual("two-words", to_slug("Two Words")) self.assertEqual("ma-any-spa-ce-es", to_slug("Ma-any\t spa--ce- es")) self.assertEqual("excamation", to_slug("exc!amation!")) self.assertEqual("ampserand", to_slug("&ser$and")) self.assertEqual("ju5tnum8er", to_slug("ju5tnum8er")) self.assertEqual("strip-", to_slug(" strip - ")) self.assertEqual("perche", to_slug(b"perch\xc3\xa9")) self.assertEqual("strange", to_slug("\x80strange", errors="ignore")) class StringToBytesTest(test_base.BaseTestCase): _unit_system = [ ('si', dict(unit_system='SI')), ('iec', dict(unit_system='IEC')), ('mixed', dict(unit_system='mixed')), ('invalid_unit_system', dict(unit_system='KKK', assert_error=True)), ] _sign = [ ('no_sign', dict(sign='')), ('positive', dict(sign='+')), ('negative', dict(sign='-')), ('invalid_sign', dict(sign='~', assert_error=True)), ] _magnitude = [ ('integer', dict(magnitude='79')), ('decimal', dict(magnitude='7.9')), ('decimal_point_start', dict(magnitude='.9')), ('decimal_point_end', dict(magnitude='79.', assert_error=True)), ('invalid_literal', dict(magnitude='7.9.9', assert_error=True)), ('garbage_value', dict(magnitude='asdf', assert_error=True)), ] _unit_prefix = [ ('no_unit_prefix', dict(unit_prefix='')), ('k', dict(unit_prefix='k')), ('K', dict(unit_prefix='K')), ('M', dict(unit_prefix='M')), ('G', dict(unit_prefix='G')), ('T', dict(unit_prefix='T')), ('P', dict(unit_prefix='P')), ('E', dict(unit_prefix='E')), ('Z', dict(unit_prefix='Z')), ('Y', dict(unit_prefix='Y')), ('R', dict(unit_prefix='R')), ('Q', dict(unit_prefix='Q')), ('Ki', dict(unit_prefix='Ki')), ('Mi', dict(unit_prefix='Mi')), ('Gi', dict(unit_prefix='Gi')), ('Ti', dict(unit_prefix='Ti')), ('Pi', dict(unit_prefix='Pi')), ('Ei', dict(unit_prefix='Ei')), ('Zi', dict(unit_prefix='Zi')), ('Yi', dict(unit_prefix='Yi')), ('Ri', dict(unit_prefix='Ri')), ('Qi', dict(unit_prefix='Qi')), ('invalid_unit_prefix', dict(unit_prefix='B', assert_error=True)), ] _unit_suffix = [ ('b', dict(unit_suffix='b')), ('bit', dict(unit_suffix='bit')), ('B', dict(unit_suffix='B')), ('invalid_unit_suffix', dict(unit_suffix='Kg', assert_error=True)), ] _return_int = [ ('return_dec', dict(return_int=False)), ('return_int', dict(return_int=True)), ] @classmethod def generate_scenarios(cls): cls.scenarios = testscenarios.multiply_scenarios(cls._unit_system, cls._sign, cls._magnitude, cls._unit_prefix, cls._unit_suffix, cls._return_int) def test_string_to_bytes(self): def _get_quantity(sign, magnitude, unit_suffix): res = float('{}{}'.format(sign, magnitude)) if unit_suffix in ['b', 'bit']: res /= 8 return res def _get_constant(unit_prefix, unit_system): if not unit_prefix: return 1 elif unit_system == 'SI': res = getattr(units, unit_prefix) elif unit_system == 'IEC': if unit_prefix.endswith('i'): res = getattr(units, unit_prefix) else: res = getattr(units, '%si' % unit_prefix) elif unit_system == 'mixed': # Note: this will return 'i' units as power-of-two, # and other units as power-of-ten. Additionally, for # compatability a "K" is interpreted as "k" in mixed # mode if unit_prefix == 'K': unit_prefix = 'k' res = getattr(units, unit_prefix) return res text = ''.join([self.sign, self.magnitude, self.unit_prefix, self.unit_suffix]) err_si = self.unit_system == 'SI' and (self.unit_prefix == 'K' or self.unit_prefix.endswith('i')) err_iec = self.unit_system == 'IEC' and self.unit_prefix == 'k' if getattr(self, 'assert_error', False) or err_si or err_iec: self.assertRaises(ValueError, strutils.string_to_bytes, text, unit_system=self.unit_system, return_int=self.return_int) return quantity = _get_quantity(self.sign, self.magnitude, self.unit_suffix) constant = _get_constant(self.unit_prefix, self.unit_system) expected = quantity * constant actual = strutils.string_to_bytes(text, unit_system=self.unit_system, return_int=self.return_int) if self.return_int: self.assertEqual(actual, int(math.ceil(expected))) else: self.assertAlmostEqual(actual, expected) StringToBytesTest.generate_scenarios() class MaskPasswordTestCase(test_base.BaseTestCase): def test_namespace_objects(self): payload = """ Namespace(passcode='', username='', password='my"password', profile='', verify=None, token='') """ expected = """ Namespace(passcode='', username='', password='***', profile='', verify=None, token='***') """ self.assertEqual(expected, strutils.mask_password(payload)) def test_sanitize_keys(self): lowered = [k.lower() for k in strutils._SANITIZE_KEYS] message = "The _SANITIZE_KEYS must all be lowercase." self.assertEqual(strutils._SANITIZE_KEYS, lowered, message) def test_json(self): # Test 'adminPass' w/o spaces payload = """{'adminPass':'TL0EfN33'}""" expected = """{'adminPass':'***'}""" self.assertEqual(expected, strutils.mask_password(payload)) # Test 'adminPass' with spaces payload = """{ 'adminPass' : 'TL0EfN33' }""" expected = """{ 'adminPass' : '***' }""" self.assertEqual(expected, strutils.mask_password(payload)) # Test 'admin_pass' w/o spaces payload = """{'admin_pass':'TL0EfN33'}""" expected = """{'admin_pass':'***'}""" self.assertEqual(expected, strutils.mask_password(payload)) # Test 'admin_pass' with spaces payload = """{ 'admin_pass' : 'TL0EfN33' }""" expected = """{ 'admin_pass' : '***' }""" self.assertEqual(expected, strutils.mask_password(payload)) # Test 'admin_password' w/o spaces payload = """{'admin_password':'TL0EfN33'}""" expected = """{'admin_password':'***'}""" self.assertEqual(expected, strutils.mask_password(payload)) # Test 'admin_password' with spaces payload = """{ 'admin_password' : 'TL0EfN33' }""" expected = """{ 'admin_password' : '***' }""" self.assertEqual(expected, strutils.mask_password(payload)) # Test 'password' w/o spaces payload = """{'password':'TL0EfN33'}""" expected = """{'password':'***'}""" self.assertEqual(expected, strutils.mask_password(payload)) # Test 'password' with spaces payload = """{ 'password' : 'TL0EfN33' }""" expected = """{ 'password' : '***' }""" self.assertEqual(expected, strutils.mask_password(payload)) # Test 'auth_password' w/o spaces payload = """{'auth_password':'TL0EfN33'}""" expected = """{'auth_password':'***'}""" self.assertEqual(expected, strutils.mask_password(payload)) # Test 'auth_password' with spaces payload = """{ 'auth_password' : 'TL0EfN33' }""" expected = """{ 'auth_password' : '***' }""" self.assertEqual(expected, strutils.mask_password(payload)) # Test 'secret_uuid' w/o spaces payload = """{'secret_uuid':'myuuid'}""" expected = """{'secret_uuid':'***'}""" self.assertEqual(expected, strutils.mask_password(payload)) # Test 'secret_uuid' with spaces payload = """{ 'secret_uuid' : 'myuuid' }""" expected = """{ 'secret_uuid' : '***' }""" self.assertEqual(expected, strutils.mask_password(payload)) # Test 'token' w/o spaces payload = """{'token':'token'}""" expected = """{'token':'***'}""" self.assertEqual(expected, strutils.mask_password(payload)) # Test 'token' with spaces payload = """{ 'token' : 'token' }""" expected = """{ 'token' : '***' }""" self.assertEqual(expected, strutils.mask_password(payload)) # Test 'fernetkey' payload = """{ 'fernetkey' : 'token' }""" expected = """{ 'fernetkey' : '***' }""" self.assertEqual(expected, strutils.mask_password(payload)) # Test 'FernetKey' payload = """{ 'FernetKey' : 'token' }""" expected = """{ 'FernetKey' : '***' }""" self.assertEqual(expected, strutils.mask_password(payload)) # Test 'sslkey' payload = """{ 'sslkey' : 'token' }""" expected = """{ 'sslkey' : '***' }""" self.assertEqual(expected, strutils.mask_password(payload)) # Test 'SslKey' payload = """{ 'SslKey' : 'token' }""" expected = """{ 'SslKey' : '***' }""" self.assertEqual(expected, strutils.mask_password(payload)) # Test 'passphrase' payload = """{ 'passphrase' : 'token' }""" expected = """{ 'passphrase' : '***' }""" self.assertEqual(expected, strutils.mask_password(payload)) # Test 'PassPhrase' payload = """{ 'PassPhrase' : 'token' }""" expected = """{ 'PassPhrase' : '***' }""" self.assertEqual(expected, strutils.mask_password(payload)) # Some real-life cases # Test 'KeystoneFernetKey1' payload = """{ 'KeystoneFernetKey1' : 'token' }""" expected = """{ 'KeystoneFernetKey1' : '***' }""" self.assertEqual(expected, strutils.mask_password(payload)) # Test 'OctaviaCaKeyPassword' payload = """{ 'OctaviaCaKeyPassword' : 'token' }""" expected = """{ 'OctaviaCaKeyPassword' : '***' }""" self.assertEqual(expected, strutils.mask_password(payload)) # Test 'OctaviaCaKeyPassphrase' payload = """{ 'OctaviaCaKeyPassphrase' : 'token' }""" expected = """{ 'OctaviaCaKeyPassphrase' : '***' }""" self.assertEqual(expected, strutils.mask_password(payload)) def test_xml(self): # Test 'adminPass' w/o spaces payload = """TL0EfN33""" expected = """***""" self.assertEqual(expected, strutils.mask_password(payload)) # Test 'adminPass' with spaces payload = """ TL0EfN33 """ expected = """***""" self.assertEqual(expected, strutils.mask_password(payload)) # Test 'admin_pass' w/o spaces payload = """TL0EfN33""" expected = """***""" self.assertEqual(expected, strutils.mask_password(payload)) # Test 'admin_pass' with spaces payload = """ TL0EfN33 """ expected = """***""" self.assertEqual(expected, strutils.mask_password(payload)) # Test 'admin_password' w/o spaces payload = """TL0EfN33""" expected = """***""" self.assertEqual(expected, strutils.mask_password(payload)) # Test 'admin_password' with spaces payload = """ TL0EfN33 """ expected = """***""" self.assertEqual(expected, strutils.mask_password(payload)) # Test 'password' w/o spaces payload = """TL0EfN33""" expected = """***""" self.assertEqual(expected, strutils.mask_password(payload)) # Test 'password' with spaces payload = """ TL0EfN33 """ expected = """***""" self.assertEqual(expected, strutils.mask_password(payload)) # Test 'Password1' - case-insensitive + number payload = """TL0EfN33""" expected = """***""" self.assertEqual(expected, strutils.mask_password(payload)) def test_xml_attribute(self): # Test 'adminPass' w/o spaces payload = """adminPass='TL0EfN33'""" expected = """adminPass='***'""" self.assertEqual(expected, strutils.mask_password(payload)) # Test 'adminPass' with spaces payload = """adminPass = 'TL0EfN33'""" expected = """adminPass = '***'""" self.assertEqual(expected, strutils.mask_password(payload)) # Test 'adminPass' with double quotes payload = """adminPass = "TL0EfN33\"""" expected = """adminPass = "***\"""" self.assertEqual(expected, strutils.mask_password(payload)) # Test 'admin_pass' w/o spaces payload = """admin_pass='TL0EfN33'""" expected = """admin_pass='***'""" self.assertEqual(expected, strutils.mask_password(payload)) # Test 'admin_pass' with spaces payload = """admin_pass = 'TL0EfN33'""" expected = """admin_pass = '***'""" self.assertEqual(expected, strutils.mask_password(payload)) # Test 'admin_pass' with double quotes payload = """admin_pass = "TL0EfN33\"""" expected = """admin_pass = "***\"""" self.assertEqual(expected, strutils.mask_password(payload)) # Test 'admin_password' w/o spaces payload = """admin_password='TL0EfN33'""" expected = """admin_password='***'""" self.assertEqual(expected, strutils.mask_password(payload)) # Test 'admin_password' with spaces payload = """admin_password = 'TL0EfN33'""" expected = """admin_password = '***'""" self.assertEqual(expected, strutils.mask_password(payload)) # Test 'admin_password' with double quotes payload = """admin_password = "TL0EfN33\"""" expected = """admin_password = "***\"""" self.assertEqual(expected, strutils.mask_password(payload)) # Test 'password' w/o spaces payload = """password='TL0EfN33'""" expected = """password='***'""" self.assertEqual(expected, strutils.mask_password(payload)) # Test 'password' with spaces payload = """password = 'TL0EfN33'""" expected = """password = '***'""" self.assertEqual(expected, strutils.mask_password(payload)) # Test 'password' with double quotes payload = """password = "TL0EfN33\"""" expected = """password = "***\"""" self.assertEqual(expected, strutils.mask_password(payload)) def test_json_message(self): payload = """body: {"changePassword": {"adminPass": "1234567"}}""" expected = """body: {"changePassword": {"adminPass": "***"}}""" self.assertEqual(expected, strutils.mask_password(payload)) payload = """body: {"rescue": {"admin_pass": "1234567"}}""" expected = """body: {"rescue": {"admin_pass": "***"}}""" self.assertEqual(expected, strutils.mask_password(payload)) payload = """body: {"rescue": {"admin_password": "1234567"}}""" expected = """body: {"rescue": {"admin_password": "***"}}""" self.assertEqual(expected, strutils.mask_password(payload)) payload = """body: {"rescue": {"password": "1234567"}}""" expected = """body: {"rescue": {"password": "***"}}""" self.assertEqual(expected, strutils.mask_password(payload)) def test_xml_message(self): payload = """ Apache1 """ expected = """ Apache1 """ self.assertEqual(expected, strutils.mask_password(payload)) payload = """ """ expected = """ """ self.assertEqual(expected, strutils.mask_password(payload)) payload = """ """ expected = """ """ self.assertEqual(expected, strutils.mask_password(payload)) payload = """ """ expected = """ """ self.assertEqual(expected, strutils.mask_password(payload)) def test_mask_password(self): payload = "test = 'password' : 'aaaaaa'" expected = "test = 'password' : '111'" self.assertEqual(expected, strutils.mask_password(payload, secret='111')) payload = 'mysqld --password "aaaaaa"' expected = 'mysqld --password "****"' self.assertEqual(expected, strutils.mask_password(payload, secret='****')) payload = 'mysqld --password aaaaaa' expected = 'mysqld --password ???' self.assertEqual(expected, strutils.mask_password(payload, secret='???')) payload = 'mysqld --password = "aaaaaa"' expected = 'mysqld --password = "****"' self.assertEqual(expected, strutils.mask_password(payload, secret='****')) payload = "mysqld --password = 'aaaaaa'" expected = "mysqld --password = '****'" self.assertEqual(expected, strutils.mask_password(payload, secret='****')) payload = "mysqld --password = aaaaaa" expected = "mysqld --password = ****" self.assertEqual(expected, strutils.mask_password(payload, secret='****')) payload = "test = password = aaaaaa" expected = "test = password = 111" self.assertEqual(expected, strutils.mask_password(payload, secret='111')) payload = "test = password= aaaaaa" expected = "test = password= 111" self.assertEqual(expected, strutils.mask_password(payload, secret='111')) payload = "test = password =aaaaaa" expected = "test = password =111" self.assertEqual(expected, strutils.mask_password(payload, secret='111')) payload = "test = password=aaaaaa" expected = "test = password=111" self.assertEqual(expected, strutils.mask_password(payload, secret='111')) payload = 'test = "original_password" : "aaaaaaaaa"' expected = 'test = "original_password" : "***"' self.assertEqual(expected, strutils.mask_password(payload)) payload = 'test = "param1" : "value"' expected = 'test = "param1" : "value"' self.assertEqual(expected, strutils.mask_password(payload)) payload = 'test = "original_password" : "aaaaa"aaaa"' expected = 'test = "original_password" : "***"' self.assertEqual(expected, strutils.mask_password(payload)) payload = """{'adminPass':'TL0EfN33'}""" payload = str(payload) expected = """{'adminPass':'***'}""" self.assertEqual(expected, strutils.mask_password(payload)) payload = """{'adminPass':'TL0E'fN33'}""" payload = str(payload) expected = """{'adminPass':'***'}""" self.assertEqual(expected, strutils.mask_password(payload)) payload = """{'token':'mytoken'}""" payload = str(payload) expected = """{'token':'***'}""" self.assertEqual(expected, strutils.mask_password(payload)) payload = ("test = 'node.session.auth.password','-v','TL0EfN33'," "'nomask'") expected = ("test = 'node.session.auth.password','-v','***'," "'nomask'") self.assertEqual(expected, strutils.mask_password(payload)) payload = ("test = 'node.session.auth.password', '--password', " "'TL0EfN33', 'nomask'") expected = ("test = 'node.session.auth.password', '--password', " "'***', 'nomask'") self.assertEqual(expected, strutils.mask_password(payload)) payload = ("test = 'node.session.auth.password', '--password', " "'TL0EfN33'") expected = ("test = 'node.session.auth.password', '--password', " "'***'") self.assertEqual(expected, strutils.mask_password(payload)) payload = "test = node.session.auth.password -v TL0EfN33 nomask" expected = "test = node.session.auth.password -v *** nomask" self.assertEqual(expected, strutils.mask_password(payload)) payload = ("test = node.session.auth.password --password TL0EfN33 " "nomask") expected = ("test = node.session.auth.password --password *** " "nomask") self.assertEqual(expected, strutils.mask_password(payload)) payload = ("test = node.session.auth.password --password TL0EfN33") expected = ("test = node.session.auth.password --password ***") self.assertEqual(expected, strutils.mask_password(payload)) payload = "test = cmd --password my\xe9\x80\x80pass" expected = ("test = cmd --password ***") self.assertEqual(expected, strutils.mask_password(payload)) class TestMapping(collections.abc.Mapping): """Test class for non-dict mappings""" def __init__(self): super().__init__() self.data = {'password': 'shhh', 'foo': 'bar', } def __getitem__(self, key): return self.data[key] def __iter__(self): return self.data.__iter__() def __len__(self): return len(self.data) class NestedMapping(TestMapping): """Test class that contains an instance of TestMapping""" def __init__(self): super().__init__() self.data = {'nested': TestMapping()} class MaskDictionaryPasswordTestCase(test_base.BaseTestCase): def test_dictionary(self): payload = {'password': 'TL0EfN33'} expected = {'password': '***'} self.assertEqual(expected, strutils.mask_dict_password(payload)) payload = {'password': 'TL0Ef"N33'} expected = {'password': '***'} self.assertEqual(expected, strutils.mask_dict_password(payload)) payload = {'user': 'admin', 'password': 'TL0EfN33'} expected = {'user': 'admin', 'password': '***'} self.assertEqual(expected, strutils.mask_dict_password(payload)) payload = {'strval': 'somestring', 'dictval': {'user': 'admin', 'password': 'TL0EfN33'}} expected = {'strval': 'somestring', 'dictval': {'user': 'admin', 'password': '***'}} self.assertEqual(expected, strutils.mask_dict_password(payload)) payload = {'strval': '--password abc', 'dont_change': 'this is fine', 'dictval': {'user': 'admin', 'password': b'TL0EfN33'}} expected = {'strval': '--password ***', 'dont_change': 'this is fine', 'dictval': {'user': 'admin', 'password': '***'}} self.assertEqual(expected, strutils.mask_dict_password(payload)) payload = {'ipmi_password': 'KeDrahishvowphyecMornEm0or('} expected = {'ipmi_password': '***'} self.assertEqual(expected, strutils.mask_dict_password(payload)) payload = {'passwords': {'KeystoneFernetKey1': 'c5FijjS'}} expected = {'passwords': {'KeystoneFernetKey1': '***'}} self.assertEqual(expected, strutils.mask_dict_password(payload)) payload = {'passwords': {'keystonecredential0': 'c5FijjS'}} expected = {'passwords': {'keystonecredential0': '***'}} self.assertEqual(expected, strutils.mask_dict_password(payload)) def test_do_no_harm(self): payload = {} expected = {} self.assertEqual(expected, strutils.mask_dict_password(payload)) payload = {'somekey': 'somevalue', 'anotherkey': 'anothervalue'} expected = {'somekey': 'somevalue', 'anotherkey': 'anothervalue'} self.assertEqual(expected, strutils.mask_dict_password(payload)) def test_do_an_int(self): payload = {} payload[1] = 2 expected = payload.copy() self.assertEqual(expected, strutils.mask_dict_password(payload)) def test_mask_values(self): payload = {'somekey': 'test = cmd --password my\xe9\x80\x80pass'} expected = {'somekey': 'test = cmd --password ***'} self.assertEqual(expected, strutils.mask_dict_password(payload)) def test_other_non_str_values(self): payload = {'password': 'DK0PK1AK3', 'bool': True, 'dict': {'cat': 'meow', 'password': "*aa38skdjf"}, 'float': 0.1, 'int': 123, 'list': [1, 2], 'none': None, 'str': 'foo'} expected = {'password': '***', 'bool': True, 'dict': {'cat': 'meow', 'password': '***'}, 'float': 0.1, 'int': 123, 'list': [1, 2], 'none': None, 'str': 'foo'} self.assertEqual(expected, strutils.mask_dict_password(payload)) def test_argument_untouched(self): """Make sure that the argument passed in is not modified""" payload = {'password': 'DK0PK1AK3', 'bool': True, 'dict': {'cat': 'meow', 'password': "*aa38skdjf"}, 'float': 0.1, 'int': 123, 'list': [1, 2], 'none': None, 'str': 'foo'} pristine = copy.deepcopy(payload) # Send the payload into the function, to see if it gets modified strutils.mask_dict_password(payload) self.assertEqual(pristine, payload) def test_non_dict(self): expected = {'password': '***', 'foo': 'bar', } payload = TestMapping() self.assertEqual(expected, strutils.mask_dict_password(payload)) def test_nested_non_dict(self): expected = {'nested': {'password': '***', 'foo': 'bar', } } payload = NestedMapping() self.assertEqual(expected, strutils.mask_dict_password(payload)) class IsIntLikeTestCase(test_base.BaseTestCase): def test_is_int_like_true(self): self.assertTrue(strutils.is_int_like(1)) self.assertTrue(strutils.is_int_like("1")) self.assertTrue(strutils.is_int_like("514")) self.assertTrue(strutils.is_int_like("0")) def test_is_int_like_false(self): self.assertFalse(strutils.is_int_like(1.1)) self.assertFalse(strutils.is_int_like("1.1")) self.assertFalse(strutils.is_int_like("1.1.1")) self.assertFalse(strutils.is_int_like(None)) self.assertFalse(strutils.is_int_like("0.")) self.assertFalse(strutils.is_int_like("aaaaaa")) self.assertFalse(strutils.is_int_like("....")) self.assertFalse(strutils.is_int_like("1g")) self.assertFalse( strutils.is_int_like("0cc3346e-9fef-4445-abe6-5d2b2690ec64")) self.assertFalse(strutils.is_int_like("a1")) # NOTE(viktors): 12e3 - is a float number self.assertFalse(strutils.is_int_like("12e3")) # NOTE(viktors): Check integer numbers with base not 10 self.assertFalse(strutils.is_int_like("0o51")) self.assertFalse(strutils.is_int_like("0xDEADBEEF")) class StringLengthTestCase(test_base.BaseTestCase): def test_check_string_length(self): self.assertIsNone(strutils.check_string_length( 'test', 'name', max_length=255)) self.assertRaises(ValueError, strutils.check_string_length, '', 'name', min_length=1) self.assertRaises(ValueError, strutils.check_string_length, 'a' * 256, 'name', max_length=255) self.assertRaises(TypeError, strutils.check_string_length, 11, 'name', max_length=255) self.assertRaises(TypeError, strutils.check_string_length, dict(), 'name', max_length=255) def test_check_string_length_noname(self): self.assertIsNone(strutils.check_string_length( 'test', max_length=255)) self.assertRaises(ValueError, strutils.check_string_length, '', min_length=1) self.assertRaises(ValueError, strutils.check_string_length, 'a' * 256, max_length=255) self.assertRaises(TypeError, strutils.check_string_length, 11, max_length=255) self.assertRaises(TypeError, strutils.check_string_length, dict(), max_length=255) class SplitPathTestCase(test_base.BaseTestCase): def test_split_path_failed(self): self.assertRaises(ValueError, strutils.split_path, '') self.assertRaises(ValueError, strutils.split_path, '/') self.assertRaises(ValueError, strutils.split_path, '//') self.assertRaises(ValueError, strutils.split_path, '//a') self.assertRaises(ValueError, strutils.split_path, '/a/c') self.assertRaises(ValueError, strutils.split_path, '//c') self.assertRaises(ValueError, strutils.split_path, '/a/c/') self.assertRaises(ValueError, strutils.split_path, '/a//') self.assertRaises(ValueError, strutils.split_path, '/a', 2) self.assertRaises(ValueError, strutils.split_path, '/a', 2, 3) self.assertRaises(ValueError, strutils.split_path, '/a', 2, 3, True) self.assertRaises(ValueError, strutils.split_path, '/a/c/o/r', 3, 3) self.assertRaises(ValueError, strutils.split_path, '/a', 5, 4) def test_split_path_success(self): self.assertEqual(strutils.split_path('/a'), ['a']) self.assertEqual(strutils.split_path('/a/'), ['a']) self.assertEqual(strutils.split_path('/a/c', 2), ['a', 'c']) self.assertEqual(strutils.split_path('/a/c/o', 3), ['a', 'c', 'o']) self.assertEqual(strutils.split_path('/a/c/o/r', 3, 3, True), ['a', 'c', 'o/r']) self.assertEqual(strutils.split_path('/a/c', 2, 3, True), ['a', 'c', None]) self.assertEqual(strutils.split_path('/a/c/', 2), ['a', 'c']) self.assertEqual(strutils.split_path('/a/c/', 2, 3), ['a', 'c', '']) def test_split_path_invalid_path(self): try: strutils.split_path('o\nn e', 2) except ValueError as err: self.assertEqual(str(err), 'Invalid path: o%0An%20e') try: strutils.split_path('o\nn e', 2, 3, True) except ValueError as err: self.assertEqual(str(err), 'Invalid path: o%0An%20e') class SplitByCommas(test_base.BaseTestCase): def test_not_closed_quotes(self): self.assertRaises(ValueError, strutils.split_by_commas, '"ab","b""') def test_no_comma_before_opening_quotes(self): self.assertRaises(ValueError, strutils.split_by_commas, '"ab""b"') def test_quote_inside_unquoted(self): self.assertRaises(ValueError, strutils.split_by_commas, 'a"b,cd') def check(self, expect, input): self.assertEqual(expect, strutils.split_by_commas(input)) def test_plain(self): self.check(["a,b", "ac"], '"a,b",ac') def test_with_backslash_inside_quoted(self): self.check(['abc"', 'de', 'fg,h', 'klm\\', '"nop'], r'"abc\"","de","fg,h","klm\\","\"nop"') def test_with_backslash_inside_unquoted(self): self.check([r'a\bc', 'de'], r'a\bc,de') def test_with_escaped_quotes_in_row_inside_quoted(self): self.check(['a"b""c', 'd'], r'"a\"b\"\"c",d') @ddt.ddt class ValidateIntegerTestCase(test_base.BaseTestCase): @ddt.unpack @ddt.data({"value": 42, "name": "answer", "output": 42}, {"value": "42", "name": "answer", "output": 42}, {"value": "7", "name": "lucky", "output": 7, "min_value": 7, "max_value": 8}, {"value": 7, "name": "lucky", "output": 7, "min_value": 6, "max_value": 7}, {"value": 300, "name": "Spartaaa!!!", "output": 300, "min_value": 300}, {"value": "300", "name": "Spartaaa!!!", "output": 300, "max_value": 300}) def test_valid_inputs(self, output, value, name, **kwargs): self.assertEqual(strutils.validate_integer(value, name, **kwargs), output) @ddt.unpack @ddt.data({"value": "im-not-an-int", "name": ''}, {"value": 3.14, "name": "Pie"}, {"value": "299", "name": "Sparta no-show", "min_value": 300, "max_value": 300}, {"value": 55, "name": "doing 55 in a 54", "max_value": 54}, {"value": chr(129), "name": "UnicodeError", "max_value": 1000}) def test_invalid_inputs(self, value, name, **kwargs): self.assertRaises(ValueError, strutils.validate_integer, value, name, **kwargs) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/oslo_utils/tests/test_timeutils.py0000664000175000017500000005161700000000000022740 0ustar00zuulzuul00000000000000# Copyright 2011 OpenStack Foundation. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import datetime import logging import time from unittest import mock import iso8601 from oslotest import base as test_base from testtools import matchers from oslo_utils import timeutils def monotonic_iter(start=0, incr=0.05): while True: yield start start += incr class TimeUtilsTest(test_base.BaseTestCase): def setUp(self): super().setUp() self.skynet_self_aware_time_str = '1997-08-29T06:14:00Z' self.skynet_self_aware_time_ms_str = '1997-08-29T06:14:00.000123Z' self.skynet_self_aware_time = datetime.datetime(1997, 8, 29, 6, 14, 0) self.skynet_self_aware_ms_time = datetime.datetime(1997, 8, 29, 6, 14, 0, 123) self.one_minute_before = datetime.datetime(1997, 8, 29, 6, 13, 0) self.one_minute_after = datetime.datetime(1997, 8, 29, 6, 15, 0) self.skynet_self_aware_time_perfect_str = '1997-08-29T06:14:00.000000' self.skynet_self_aware_time_perfect = datetime.datetime(1997, 8, 29, 6, 14, 0) self.addCleanup(timeutils.clear_time_override) def test_parse_isotime(self): expect = timeutils.parse_isotime(self.skynet_self_aware_time_str) skynet_self_aware_time_utc = self.skynet_self_aware_time.replace( tzinfo=iso8601.iso8601.UTC) self.assertEqual(skynet_self_aware_time_utc, expect) def test_parse_isotime_micro_second_precision(self): expect = timeutils.parse_isotime(self.skynet_self_aware_time_ms_str) skynet_self_aware_time_ms_utc = self.skynet_self_aware_ms_time.replace( tzinfo=iso8601.iso8601.UTC) self.assertEqual(skynet_self_aware_time_ms_utc, expect) def test_parse_strtime(self): perfect_time_format = self.skynet_self_aware_time_perfect_str expect = timeutils.parse_strtime(perfect_time_format) self.assertEqual(self.skynet_self_aware_time_perfect, expect) @mock.patch('datetime.datetime', wraps=datetime.datetime) def _test_is_older_than(self, fn, datetime_mock): datetime_mock.now.return_value = self.skynet_self_aware_time expect_true = timeutils.is_older_than(fn(self.one_minute_before), 59) self.assertTrue(expect_true) expect_false = timeutils.is_older_than(fn(self.one_minute_before), 60) self.assertFalse(expect_false) expect_false = timeutils.is_older_than(fn(self.one_minute_before), 61) self.assertFalse(expect_false) def test_is_older_than_datetime(self): self._test_is_older_than(lambda x: x) def test_is_older_than_aware(self): """Tests sending is_older_than an 'aware' datetime.""" self._test_is_older_than(lambda x: x.replace( tzinfo=iso8601.iso8601.UTC)) def test_is_older_than_aware_no_utc(self): self._test_is_older_than(lambda x: x.replace( tzinfo=iso8601.iso8601.FixedOffset(1, 0, 'foo')).replace( hour=7)) @mock.patch('datetime.datetime', wraps=datetime.datetime) def _test_is_newer_than(self, fn, datetime_mock): datetime_mock.now.return_value = self.skynet_self_aware_time expect_true = timeutils.is_newer_than(fn(self.one_minute_after), 59) self.assertTrue(expect_true) expect_false = timeutils.is_newer_than(fn(self.one_minute_after), 60) self.assertFalse(expect_false) expect_false = timeutils.is_newer_than(fn(self.one_minute_after), 61) self.assertFalse(expect_false) def test_is_newer_than_datetime(self): self._test_is_newer_than(lambda x: x) def test_is_newer_than_aware(self): """Tests sending is_newer_than an 'aware' datetime.""" self._test_is_newer_than(lambda x: x.replace( tzinfo=iso8601.iso8601.UTC)) def test_is_newer_than_aware_no_utc(self): self._test_is_newer_than(lambda x: x.replace( tzinfo=iso8601.iso8601.FixedOffset(1, 0, 'foo')).replace( hour=7)) def test_set_time_override_using_default(self): now = timeutils.utcnow_ts() # NOTE(kgriffs): Normally it's bad form to sleep in a unit test, # but this is the only way to test that set_time_override defaults # to setting the override to the current time. time.sleep(1) timeutils.set_time_override() overriden_now = timeutils.utcnow_ts() self.assertThat(now, matchers.LessThan(overriden_now)) def test_utcnow_ts(self): skynet_self_aware_ts = 872835240 skynet_dt = datetime.datetime.utcfromtimestamp(skynet_self_aware_ts) self.assertEqual(self.skynet_self_aware_time, skynet_dt) # NOTE(kgriffs): timeutils.utcnow_ts() uses time.time() # IFF time override is not set. with mock.patch('time.time') as time_mock: time_mock.return_value = skynet_self_aware_ts ts = timeutils.utcnow_ts() self.assertEqual(ts, skynet_self_aware_ts) timeutils.set_time_override(skynet_dt) ts = timeutils.utcnow_ts() self.assertEqual(ts, skynet_self_aware_ts) def test_utcnow(self): timeutils.set_time_override(mock.sentinel.utcnow) self.assertEqual(timeutils.utcnow(), mock.sentinel.utcnow) timeutils.clear_time_override() self.assertFalse(timeutils.utcnow() == mock.sentinel.utcnow) self.assertTrue(timeutils.utcnow()) def test_advance_time_delta(self): timeutils.set_time_override(self.one_minute_before) timeutils.advance_time_delta(datetime.timedelta(seconds=60)) self.assertEqual(timeutils.utcnow(), self.skynet_self_aware_time) def test_advance_time_seconds(self): timeutils.set_time_override(self.one_minute_before) timeutils.advance_time_seconds(60) self.assertEqual(timeutils.utcnow(), self.skynet_self_aware_time) def test_marshall_time(self): now = timeutils.utcnow() binary = timeutils.marshall_now(now) backagain = timeutils.unmarshall_time(binary) self.assertEqual(now, backagain) def test_marshall_time_with_tz(self): now = timeutils.utcnow() now = now.replace(tzinfo=iso8601.iso8601.UTC) binary = timeutils.marshall_now(now) self.assertEqual("UTC", binary['tzname']) backagain = timeutils.unmarshall_time(binary) self.assertEqual(now, backagain) self.assertIsNotNone(backagain.tzinfo) self.assertEqual(now.utcoffset(), backagain.utcoffset()) def test_unmarshall_time_leap_second(self): leap_dict = dict(day=30, month=6, year=2015, hour=23, minute=59, second=timeutils._MAX_DATETIME_SEC + 1, microsecond=0) leap_time = timeutils.unmarshall_time(leap_dict) leap_dict.update(second=timeutils._MAX_DATETIME_SEC) expected = timeutils.unmarshall_time(leap_dict) self.assertEqual(expected, leap_time) def test_delta_seconds(self): before = timeutils.utcnow() after = before + datetime.timedelta(days=7, seconds=59, microseconds=123456) self.assertAlmostEqual(604859.123456, timeutils.delta_seconds(before, after)) def test_is_soon(self): expires = timeutils.utcnow() + datetime.timedelta(minutes=5) self.assertFalse(timeutils.is_soon(expires, 120)) self.assertTrue(timeutils.is_soon(expires, 300)) self.assertTrue(timeutils.is_soon(expires, 600)) with mock.patch('datetime.datetime') as datetime_mock: datetime_mock.now.return_value = self.skynet_self_aware_time expires = timeutils.utcnow() self.assertTrue(timeutils.is_soon(expires, 0)) class TestIso8601Time(test_base.BaseTestCase): def _instaneous(self, timestamp, yr, mon, day, hr, minute, sec, micro): self.assertEqual(timestamp.year, yr) self.assertEqual(timestamp.month, mon) self.assertEqual(timestamp.day, day) self.assertEqual(timestamp.hour, hr) self.assertEqual(timestamp.minute, minute) self.assertEqual(timestamp.second, sec) self.assertEqual(timestamp.microsecond, micro) def _do_test(self, time_str, yr, mon, day, hr, minute, sec, micro, shift): DAY_SECONDS = 24 * 60 * 60 timestamp = timeutils.parse_isotime(time_str) self._instaneous(timestamp, yr, mon, day, hr, minute, sec, micro) offset = timestamp.tzinfo.utcoffset(None) self.assertEqual(offset.seconds + offset.days * DAY_SECONDS, shift) def test_zulu(self): time_str = '2012-02-14T20:53:07Z' self._do_test(time_str, 2012, 2, 14, 20, 53, 7, 0, 0) def test_zulu_micros(self): time_str = '2012-02-14T20:53:07.123Z' self._do_test(time_str, 2012, 2, 14, 20, 53, 7, 123000, 0) def test_offset_east(self): time_str = '2012-02-14T20:53:07+04:30' offset = 4.5 * 60 * 60 self._do_test(time_str, 2012, 2, 14, 20, 53, 7, 0, offset) def test_offset_east_micros(self): time_str = '2012-02-14T20:53:07.42+04:30' offset = 4.5 * 60 * 60 self._do_test(time_str, 2012, 2, 14, 20, 53, 7, 420000, offset) def test_offset_west(self): time_str = '2012-02-14T20:53:07-05:30' offset = -5.5 * 60 * 60 self._do_test(time_str, 2012, 2, 14, 20, 53, 7, 0, offset) def test_offset_west_micros(self): time_str = '2012-02-14T20:53:07.654321-05:30' offset = -5.5 * 60 * 60 self._do_test(time_str, 2012, 2, 14, 20, 53, 7, 654321, offset) def test_compare(self): zulu = timeutils.parse_isotime('2012-02-14T20:53:07') east = timeutils.parse_isotime('2012-02-14T20:53:07-01:00') west = timeutils.parse_isotime('2012-02-14T20:53:07+01:00') self.assertTrue(east > west) self.assertTrue(east > zulu) self.assertTrue(zulu > west) def test_compare_micros(self): zulu = timeutils.parse_isotime('2012-02-14T20:53:07.6544') east = timeutils.parse_isotime('2012-02-14T19:53:07.654321-01:00') west = timeutils.parse_isotime('2012-02-14T21:53:07.655+01:00') self.assertTrue(east < west) self.assertTrue(east < zulu) self.assertTrue(zulu < west) def test_zulu_normalize(self): time_str = '2012-02-14T20:53:07Z' zulu = timeutils.parse_isotime(time_str) normed = timeutils.normalize_time(zulu) self._instaneous(normed, 2012, 2, 14, 20, 53, 7, 0) def test_east_normalize(self): time_str = '2012-02-14T20:53:07-07:00' east = timeutils.parse_isotime(time_str) normed = timeutils.normalize_time(east) self._instaneous(normed, 2012, 2, 15, 3, 53, 7, 0) def test_west_normalize(self): time_str = '2012-02-14T20:53:07+21:00' west = timeutils.parse_isotime(time_str) normed = timeutils.normalize_time(west) self._instaneous(normed, 2012, 2, 13, 23, 53, 7, 0) def test_normalize_aware_to_naive(self): dt = datetime.datetime(2011, 2, 14, 20, 53, 7) time_str = '2011-02-14T20:53:07+21:00' aware = timeutils.parse_isotime(time_str) naive = timeutils.normalize_time(aware) self.assertTrue(naive < dt) def test_normalize_zulu_aware_to_naive(self): dt = datetime.datetime(2011, 2, 14, 20, 53, 7) time_str = '2011-02-14T19:53:07Z' aware = timeutils.parse_isotime(time_str) naive = timeutils.normalize_time(aware) self.assertTrue(naive < dt) def test_normalize_naive(self): dt = datetime.datetime(2011, 2, 14, 20, 53, 7) dtn = datetime.datetime(2011, 2, 14, 19, 53, 7) naive = timeutils.normalize_time(dtn) self.assertTrue(naive < dt) class TimeItTest(test_base.BaseTestCase): @mock.patch('time.sleep') @mock.patch('oslo_utils.timeutils.now') def test_timed(self, mock_now, mock_sleep): mock_now.side_effect = monotonic_iter(incr=0.1) fake_logger = mock.MagicMock(logging.getLogger(), autospec=True) @timeutils.time_it(fake_logger) def slow_function(): time.sleep(0.1) slow_function() self.assertTrue(mock_now.called) self.assertTrue(mock_sleep.called) self.assertTrue(fake_logger.log.called) fake_logger.log.assert_called_with(logging.DEBUG, mock.ANY, mock.ANY) @mock.patch('time.sleep') @mock.patch('oslo_utils.timeutils.now') def test_no_timed_disabled(self, mock_now, mock_sleep): mock_now.side_effect = monotonic_iter(incr=0.1) fake_logger = mock.MagicMock(logging.getLogger(), autospec=True) @timeutils.time_it(fake_logger, enabled=False) def slow_function(): time.sleep(0.1) slow_function() self.assertFalse(mock_now.called) self.assertFalse(fake_logger.log.called) @mock.patch('time.sleep') @mock.patch('oslo_utils.timeutils.now') def test_no_timed_to_fast(self, mock_now, mock_sleep): mock_now.side_effect = monotonic_iter(incr=0.1) fake_logger = mock.MagicMock(logging.getLogger(), autospec=True) @timeutils.time_it(fake_logger, min_duration=10) def fast_function(): pass fast_function() self.assertFalse(fake_logger.log.called) @mock.patch('time.sleep') @mock.patch('oslo_utils.timeutils.now') def test_no_timed_exception(self, mock_now, mock_sleep): mock_now.side_effect = monotonic_iter(incr=0.1) fake_logger = mock.MagicMock(logging.getLogger(), autospec=True) @timeutils.time_it(fake_logger) def broken_function(): raise OSError("Broken") self.assertRaises(IOError, broken_function) self.assertFalse(fake_logger.log.called) @mock.patch('time.sleep') @mock.patch('oslo_utils.timeutils.now') def test_timed_custom_message(self, mock_now, mock_sleep): mock_now.side_effect = monotonic_iter(incr=0.1) fake_logger = mock.MagicMock(logging.getLogger(), autospec=True) @timeutils.time_it(fake_logger, message="That took a long time") def slow_function(): time.sleep(0.1) slow_function() self.assertTrue(mock_now.called) self.assertTrue(mock_sleep.called) self.assertTrue(fake_logger.log.called) fake_logger.log.assert_called_with(logging.DEBUG, "That took a long time", mock.ANY) @mock.patch('time.sleep') @mock.patch('oslo_utils.timeutils.now') def test_timed_custom_level(self, mock_now, mock_sleep): mock_now.side_effect = monotonic_iter(incr=0.1) fake_logger = mock.MagicMock(logging.getLogger(), autospec=True) @timeutils.time_it(fake_logger, log_level=logging.INFO) def slow_function(): time.sleep(0.1) slow_function() self.assertTrue(mock_now.called) self.assertTrue(mock_sleep.called) self.assertTrue(fake_logger.log.called) fake_logger.log.assert_called_with(logging.INFO, mock.ANY, mock.ANY) class StopWatchTest(test_base.BaseTestCase): def test_leftover_no_duration(self): watch = timeutils.StopWatch() watch.start() self.assertRaises(RuntimeError, watch.leftover) self.assertRaises(RuntimeError, watch.leftover, return_none=False) self.assertIsNone(watch.leftover(return_none=True)) def test_no_states(self): watch = timeutils.StopWatch() self.assertRaises(RuntimeError, watch.stop) self.assertRaises(RuntimeError, watch.resume) def test_bad_expiry(self): self.assertRaises(ValueError, timeutils.StopWatch, -1) @mock.patch('oslo_utils.timeutils.now') def test_backwards(self, mock_now): mock_now.side_effect = [0, 0.5, -1.0, -1.0] watch = timeutils.StopWatch(0.1) watch.start() self.assertTrue(watch.expired()) self.assertFalse(watch.expired()) self.assertEqual(0.0, watch.elapsed()) @mock.patch('oslo_utils.timeutils.now') def test_expiry(self, mock_now): mock_now.side_effect = monotonic_iter(incr=0.2) watch = timeutils.StopWatch(0.1) watch.start() self.assertTrue(watch.expired()) @mock.patch('oslo_utils.timeutils.now') def test_not_expired(self, mock_now): mock_now.side_effect = monotonic_iter() watch = timeutils.StopWatch(0.1) watch.start() self.assertFalse(watch.expired()) def test_has_started_stopped(self): watch = timeutils.StopWatch() self.assertFalse(watch.has_started()) self.assertFalse(watch.has_stopped()) watch.start() self.assertTrue(watch.has_started()) self.assertFalse(watch.has_stopped()) watch.stop() self.assertTrue(watch.has_stopped()) self.assertFalse(watch.has_started()) def test_no_expiry(self): watch = timeutils.StopWatch(0.1) self.assertRaises(RuntimeError, watch.expired) @mock.patch('oslo_utils.timeutils.now') def test_elapsed(self, mock_now): mock_now.side_effect = monotonic_iter(incr=0.2) watch = timeutils.StopWatch() watch.start() matcher = matchers.GreaterThan(0.19) self.assertThat(watch.elapsed(), matcher) def test_no_elapsed(self): watch = timeutils.StopWatch() self.assertRaises(RuntimeError, watch.elapsed) def test_no_leftover(self): watch = timeutils.StopWatch() self.assertRaises(RuntimeError, watch.leftover) watch = timeutils.StopWatch(1) self.assertRaises(RuntimeError, watch.leftover) @mock.patch('oslo_utils.timeutils.now') def test_pause_resume(self, mock_now): mock_now.side_effect = monotonic_iter() watch = timeutils.StopWatch() watch.start() watch.stop() elapsed = watch.elapsed() self.assertAlmostEqual(elapsed, watch.elapsed()) watch.resume() self.assertNotEqual(elapsed, watch.elapsed()) @mock.patch('oslo_utils.timeutils.now') def test_context_manager(self, mock_now): mock_now.side_effect = monotonic_iter() with timeutils.StopWatch() as watch: pass matcher = matchers.GreaterThan(0.04) self.assertThat(watch.elapsed(), matcher) @mock.patch('oslo_utils.timeutils.now') def test_context_manager_splits(self, mock_now): mock_now.side_effect = monotonic_iter() with timeutils.StopWatch() as watch: time.sleep(0.01) watch.split() self.assertRaises(RuntimeError, watch.split) self.assertEqual(1, len(watch.splits)) def test_splits_stopped(self): watch = timeutils.StopWatch() watch.start() watch.split() watch.stop() self.assertRaises(RuntimeError, watch.split) def test_splits_never_started(self): watch = timeutils.StopWatch() self.assertRaises(RuntimeError, watch.split) @mock.patch('oslo_utils.timeutils.now') def test_splits(self, mock_now): mock_now.side_effect = monotonic_iter() watch = timeutils.StopWatch() watch.start() self.assertEqual(0, len(watch.splits)) watch.split() self.assertEqual(1, len(watch.splits)) self.assertEqual(watch.splits[0].elapsed, watch.splits[0].length) watch.split() splits = watch.splits self.assertEqual(2, len(splits)) self.assertNotEqual(splits[0].elapsed, splits[1].elapsed) self.assertEqual(splits[1].length, splits[1].elapsed - splits[0].elapsed) watch.stop() self.assertEqual(2, len(watch.splits)) watch.start() self.assertEqual(0, len(watch.splits)) @mock.patch('oslo_utils.timeutils.now') def test_elapsed_maximum(self, mock_now): mock_now.side_effect = [0, 1] + ([11] * 4) watch = timeutils.StopWatch() watch.start() self.assertEqual(1, watch.elapsed()) self.assertEqual(11, watch.elapsed()) self.assertEqual(1, watch.elapsed(maximum=1)) watch.stop() self.assertEqual(11, watch.elapsed()) self.assertEqual(11, watch.elapsed()) self.assertEqual(0, watch.elapsed(maximum=-1)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/oslo_utils/tests/test_uuidutils.py0000664000175000017500000000447700000000000022752 0ustar00zuulzuul00000000000000# Copyright (c) 2012 Intel Corporation. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import uuid from oslotest import base as test_base from oslo_utils import uuidutils class UUIDUtilsTest(test_base.BaseTestCase): def test_generate_uuid(self): uuid_string = uuidutils.generate_uuid() self.assertIsInstance(uuid_string, str) self.assertEqual(len(uuid_string), 36) # make sure there are 4 dashes self.assertEqual(len(uuid_string.replace('-', '')), 32) def test_generate_uuid_dashed_false(self): uuid_string = uuidutils.generate_uuid(dashed=False) self.assertIsInstance(uuid_string, str) self.assertEqual(len(uuid_string), 32) self.assertNotIn('-', uuid_string) def test_is_uuid_like(self): self.assertTrue(uuidutils.is_uuid_like(str(uuid.uuid4()))) self.assertTrue(uuidutils.is_uuid_like( '{12345678-1234-5678-1234-567812345678}')) self.assertTrue(uuidutils.is_uuid_like( '12345678123456781234567812345678')) self.assertTrue(uuidutils.is_uuid_like( 'urn:uuid:12345678-1234-5678-1234-567812345678')) self.assertTrue(uuidutils.is_uuid_like( 'urn:bbbaaaaa-aaaa-aaaa-aabb-bbbbbbbbbbbb')) self.assertTrue(uuidutils.is_uuid_like( 'uuid:bbbaaaaa-aaaa-aaaa-aabb-bbbbbbbbbbbb')) self.assertTrue(uuidutils.is_uuid_like( '{}---bbb---aaa--aaa--aaa-----aaa---aaa--bbb-bbb---bbb-bbb-bb-{}')) def test_is_uuid_like_insensitive(self): self.assertTrue(uuidutils.is_uuid_like(str(uuid.uuid4()).upper())) def test_id_is_uuid_like(self): self.assertFalse(uuidutils.is_uuid_like(1234567)) def test_name_is_uuid_like(self): self.assertFalse(uuidutils.is_uuid_like('zhongyueluo')) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/oslo_utils/tests/test_versionutils.py0000664000175000017500000001434700000000000023466 0ustar00zuulzuul00000000000000# Copyright (c) 2013 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from oslotest import base as test_base from oslo_utils import versionutils class IsCompatibleTestCase(test_base.BaseTestCase): def test_same_version(self): self.assertTrue(versionutils.is_compatible('1', '1')) self.assertTrue(versionutils.is_compatible('1.0', '1.0')) self.assertTrue(versionutils.is_compatible('1.0.0', '1.0.0')) def test_requested_minor_greater(self): self.assertFalse(versionutils.is_compatible('1.1', '1.0')) def test_requested_minor_less_than(self): self.assertTrue(versionutils.is_compatible('1.0', '1.1')) def test_requested_patch_greater(self): self.assertFalse(versionutils.is_compatible('1.0.1', '1.0.0')) def test_requested_patch_less_than(self): self.assertTrue(versionutils.is_compatible('1.0.0', '1.0.1')) def test_requested_patch_not_present_same(self): self.assertTrue(versionutils.is_compatible('1.0', '1.0.0')) def test_requested_patch_not_present_less_than(self): self.assertTrue(versionutils.is_compatible('1.0', '1.0.1')) def test_current_patch_not_present_same(self): self.assertTrue(versionutils.is_compatible('1.0.0', '1.0')) def test_current_patch_not_present_less_than(self): self.assertFalse(versionutils.is_compatible('1.0.1', '1.0')) def test_same_major_true(self): """Even though the current version is 2.0, since `same_major` defaults to `True`, 1.0 is deemed incompatible. """ self.assertFalse(versionutils.is_compatible('2.0', '1.0')) self.assertTrue(versionutils.is_compatible('1.0', '1.0')) self.assertFalse(versionutils.is_compatible('1.0', '2.0')) def test_same_major_false(self): """With `same_major` set to False, then major version compatibiity rule is not enforced, so a current version of 2.0 is deemed to satisfy a requirement of 1.0. """ self.assertFalse(versionutils.is_compatible('2.0', '1.0', same_major=False)) self.assertTrue(versionutils.is_compatible('1.0', '1.0', same_major=False)) self.assertTrue(versionutils.is_compatible('1.0', '2.0', same_major=False)) def test_convert_version_to_int(self): self.assertEqual(6002000, versionutils.convert_version_to_int('6.2.0')) self.assertEqual(6004003, versionutils.convert_version_to_int((6, 4, 3))) self.assertEqual(5, versionutils.convert_version_to_int((5, ))) self.assertRaises(ValueError, versionutils.convert_version_to_int, '5a.6b') def test_convert_version_to_string(self): self.assertEqual('6.7.0', versionutils.convert_version_to_str(6007000)) self.assertEqual('4', versionutils.convert_version_to_str(4)) def test_convert_version_to_tuple(self): self.assertEqual((6, 7, 0), versionutils.convert_version_to_tuple('6.7.0')) self.assertEqual((6, 7, 0), versionutils.convert_version_to_tuple('6.7.0a1')) self.assertEqual((6, 7, 0), versionutils.convert_version_to_tuple('6.7.0alpha1')) self.assertEqual((6, 7, 0), versionutils.convert_version_to_tuple('6.7.0b1')) self.assertEqual((6, 7, 0), versionutils.convert_version_to_tuple('6.7.0beta1')) self.assertEqual((6, 7, 0), versionutils.convert_version_to_tuple('6.7.0rc1')) class VersionPredicateTest(test_base.BaseTestCase): def test_version_predicate_valid(self): pred = versionutils.VersionPredicate('<2.0.0') self.assertTrue(pred.satisfied_by('1.10.0')) self.assertFalse(pred.satisfied_by('2.0.0')) pred = versionutils.VersionPredicate('<=2.0.0') self.assertTrue(pred.satisfied_by('2.0.0')) self.assertFalse(pred.satisfied_by('2.1.0')) pred = versionutils.VersionPredicate('>2.0.0') self.assertFalse(pred.satisfied_by('1.10.0')) self.assertTrue(pred.satisfied_by('2.1.0')) pred = versionutils.VersionPredicate('>=2.0.0') self.assertFalse(pred.satisfied_by('1.9.0')) self.assertTrue(pred.satisfied_by('2.0.0')) pred = versionutils.VersionPredicate('== 2.0.0') self.assertFalse(pred.satisfied_by('1.9.9')) self.assertTrue(pred.satisfied_by('2.0.0')) self.assertFalse(pred.satisfied_by('2.0.1')) pred = versionutils.VersionPredicate('!= 2.0.0') self.assertTrue(pred.satisfied_by('1.9.9')) self.assertFalse(pred.satisfied_by('2.0.0')) self.assertTrue(pred.satisfied_by('2.0.1')) def test_version_predicate_valid_multi(self): pred = versionutils.VersionPredicate('<3.0.0,>=2.1.0') self.assertTrue(pred.satisfied_by('2.1.0')) self.assertTrue(pred.satisfied_by('2.11.0')) self.assertFalse(pred.satisfied_by('2.0.0')) self.assertFalse(pred.satisfied_by('3.0.0')) pred = versionutils.VersionPredicate(' < 2.0.0, >= 1.0.0 ') self.assertTrue(pred.satisfied_by('1.0.0')) self.assertTrue(pred.satisfied_by('1.10.0')) self.assertFalse(pred.satisfied_by('0.9.0')) self.assertFalse(pred.satisfied_by('2.0.0')) def test_version_predicate_valid_invalid(self): for invalid_str in ['3.0.0', 'foo', '<> 3.0.0', '>=1.0.0;<2.0.0', '>abc', '>=1.0.0,', '>=1.0.0,2.0.0']: self.assertRaises( ValueError, versionutils.VersionPredicate, invalid_str) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/oslo_utils/tests/tests_encodeutils.py0000664000175000017500000001027000000000000023410 0ustar00zuulzuul00000000000000# Copyright 2014 Red Hat, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from oslo_i18n import fixture as oslo_i18n_fixture from oslotest import base as test_base from oslo_utils import encodeutils class EncodeUtilsTest(test_base.BaseTestCase): def test_safe_decode(self): safe_decode = encodeutils.safe_decode self.assertRaises(TypeError, safe_decode, True) self.assertEqual('ni\xf1o', safe_decode(b"ni\xc3\xb1o", incoming="utf-8")) self.assertEqual("strange", safe_decode(b'\x80strange', errors='ignore')) self.assertEqual('\xc0', safe_decode(b'\xc0', incoming='iso-8859-1')) # Forcing incoming to ascii so it falls back to utf-8 self.assertEqual('ni\xf1o', safe_decode(b'ni\xc3\xb1o', incoming='ascii')) self.assertEqual('foo', safe_decode(b'foo')) def test_safe_encode_none_instead_of_text(self): self.assertRaises(TypeError, encodeutils.safe_encode, None) def test_safe_encode_bool_instead_of_text(self): self.assertRaises(TypeError, encodeutils.safe_encode, True) def test_safe_encode_int_instead_of_text(self): self.assertRaises(TypeError, encodeutils.safe_encode, 1) def test_safe_encode_list_instead_of_text(self): self.assertRaises(TypeError, encodeutils.safe_encode, []) def test_safe_encode_dict_instead_of_text(self): self.assertRaises(TypeError, encodeutils.safe_encode, {}) def test_safe_encode_tuple_instead_of_text(self): self.assertRaises(TypeError, encodeutils.safe_encode, ('foo', 'bar', )) def test_safe_encode_force_incoming_utf8_to_ascii(self): # Forcing incoming to ascii so it falls back to utf-8 self.assertEqual( b'ni\xc3\xb1o', encodeutils.safe_encode(b'ni\xc3\xb1o', incoming='ascii'), ) def test_safe_encode_same_encoding_different_cases(self): with mock.patch.object(encodeutils, 'safe_decode', mock.Mock()): utf8 = encodeutils.safe_encode( 'foo\xf1bar', encoding='utf-8') self.assertEqual( encodeutils.safe_encode(utf8, 'UTF-8', 'utf-8'), encodeutils.safe_encode(utf8, 'utf-8', 'UTF-8'), ) self.assertEqual( encodeutils.safe_encode(utf8, 'UTF-8', 'utf-8'), encodeutils.safe_encode(utf8, 'utf-8', 'utf-8'), ) encodeutils.safe_decode.assert_has_calls([]) def test_safe_encode_different_encodings(self): text = 'foo\xc3\xb1bar' result = encodeutils.safe_encode( text=text, incoming='utf-8', encoding='iso-8859-1') self.assertNotEqual(text, result) self.assertNotEqual(b"foo\xf1bar", result) def test_to_utf8(self): self.assertEqual(encodeutils.to_utf8(b'a\xe9\xff'), # bytes b'a\xe9\xff') self.assertEqual(encodeutils.to_utf8('a\xe9\xff\u20ac'), # Unicode b'a\xc3\xa9\xc3\xbf\xe2\x82\xac') self.assertRaises(TypeError, encodeutils.to_utf8, 123) # invalid # oslo.i18n Message objects should also be accepted for convenience. # It works because Message is a subclass of str. Use the # lazy translation to get a Message instance of oslo_i18n. msg = oslo_i18n_fixture.Translation().lazy("test") self.assertEqual(encodeutils.to_utf8(msg), b'test') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/oslo_utils/timeutils.py0000664000175000017500000003620300000000000020531 0ustar00zuulzuul00000000000000# Copyright 2011 OpenStack Foundation. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ Time related utilities and helper functions. """ import calendar import datetime import functools import logging import time import zoneinfo import iso8601 from oslo_utils import reflection # ISO 8601 extended time format with microseconds PERFECT_TIME_FORMAT = '%Y-%m-%dT%H:%M:%S.%f' _MAX_DATETIME_SEC = 59 now = time.monotonic def parse_isotime(timestr): """Parse time from ISO 8601 format.""" try: return iso8601.parse_date(timestr) except iso8601.ParseError as e: raise ValueError(str(e)) except TypeError as e: raise ValueError(str(e)) def parse_strtime(timestr, fmt=PERFECT_TIME_FORMAT): """Turn a formatted time back into a datetime.""" return datetime.datetime.strptime(timestr, fmt) def normalize_time(timestamp): """Normalize time in arbitrary timezone to UTC naive object.""" offset = timestamp.utcoffset() if offset is None: return timestamp return timestamp.replace(tzinfo=None) - offset def is_older_than(before, seconds): """Return True if before is older than seconds. .. versionchanged:: 1.7 Accept datetime string with timezone information. Fix comparison with timezone aware datetime. """ if isinstance(before, str): before = parse_isotime(before) before = normalize_time(before) return utcnow() - before > datetime.timedelta(seconds=seconds) def is_newer_than(after, seconds): """Return True if after is newer than seconds. .. versionchanged:: 1.7 Accept datetime string with timezone information. Fix comparison with timezone aware datetime. """ if isinstance(after, str): after = parse_isotime(after) after = normalize_time(after) return after - utcnow() > datetime.timedelta(seconds=seconds) def utcnow_ts(microsecond=False): """Timestamp version of our utcnow function. See :py:class:`oslo_utils.fixture.TimeFixture`. .. versionchanged:: 1.3 Added optional *microsecond* parameter. """ if utcnow.override_time is None: # NOTE(kgriffs): This is several times faster # than going through calendar.timegm(...) timestamp = time.time() if not microsecond: timestamp = int(timestamp) return timestamp now = utcnow() timestamp = calendar.timegm(now.timetuple()) if microsecond: timestamp += float(now.microsecond) / 1000000 return timestamp def utcnow(with_timezone=False): """Overridable version of utils.utcnow that can return a TZ-aware datetime. See :py:class:`oslo_utils.fixture.TimeFixture`. .. versionchanged:: 1.6 Added *with_timezone* parameter. """ if utcnow.override_time: try: return utcnow.override_time.pop(0) except AttributeError: return utcnow.override_time if with_timezone: return datetime.datetime.now(tz=iso8601.iso8601.UTC) return datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) utcnow.override_time = None def set_time_override(override_time=None): """Overrides utils.utcnow. Make it return a constant time or a list thereof, one at a time. See :py:class:`oslo_utils.fixture.TimeFixture`. :param override_time: datetime instance or list thereof. If not given, defaults to the current UTC time. """ utcnow.override_time = ( override_time or datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None)) def advance_time_delta(timedelta): """Advance overridden time using a datetime.timedelta. See :py:class:`oslo_utils.fixture.TimeFixture`. """ assert utcnow.override_time is not None # nosec try: for dt in utcnow.override_time: dt += timedelta except TypeError: utcnow.override_time += timedelta def advance_time_seconds(seconds): """Advance overridden time by seconds. See :py:class:`oslo_utils.fixture.TimeFixture`. """ advance_time_delta(datetime.timedelta(0, seconds)) def clear_time_override(): """Remove the overridden time. See :py:class:`oslo_utils.fixture.TimeFixture`. """ utcnow.override_time = None def marshall_now(now=None): """Make an rpc-safe datetime with microseconds. .. versionchanged:: 1.6 Timezone information is now serialized instead of being stripped. """ if not now: now = utcnow() d = dict(day=now.day, month=now.month, year=now.year, hour=now.hour, minute=now.minute, second=now.second, microsecond=now.microsecond) if now.tzinfo: # Need to handle either iso8601 or python UTC format tzname = now.tzinfo.tzname(None) d['tzname'] = 'UTC' if tzname == 'UTC+00:00' else tzname return d def unmarshall_time(tyme): """Unmarshall a datetime dict. .. versionchanged:: 1.5 Drop leap second. .. versionchanged:: 1.6 Added support for timezone information. """ # NOTE(ihrachys): datetime does not support leap seconds, # so the best thing we can do for now is dropping them # http://bugs.python.org/issue23574 second = min(tyme['second'], _MAX_DATETIME_SEC) dt = datetime.datetime(day=tyme['day'], month=tyme['month'], year=tyme['year'], hour=tyme['hour'], minute=tyme['minute'], second=second, microsecond=tyme['microsecond']) tzname = tyme.get('tzname') if tzname: # Need to handle either iso8601 or python UTC format tzname = 'UTC' if tzname == 'UTC+00:00' else tzname tzinfo = zoneinfo.ZoneInfo(tzname) dt = dt.replace(tzinfo=tzinfo) return dt def delta_seconds(before, after): """Return the difference between two timing objects. Compute the difference in seconds between two date, time, or datetime objects (as a float, to microsecond resolution). """ delta = after - before return delta.total_seconds() def is_soon(dt, window): """Determines if time is going to happen in the next window seconds. :param dt: the time :param window: minimum seconds to remain to consider the time not soon :return: True if expiration is within the given duration """ soon = (utcnow() + datetime.timedelta(seconds=window)) return normalize_time(dt) <= soon class Split: """A *immutable* stopwatch split. See: http://en.wikipedia.org/wiki/Stopwatch for what this is/represents. .. versionadded:: 1.4 """ __slots__ = ['_elapsed', '_length'] def __init__(self, elapsed, length): self._elapsed = elapsed self._length = length @property def elapsed(self): """Duration from stopwatch start.""" return self._elapsed @property def length(self): """Seconds from last split (or the elapsed time if no prior split).""" return self._length def __repr__(self): r = reflection.get_class_name(self, fully_qualified=False) r += "(elapsed={}, length={})".format(self._elapsed, self._length) return r def time_it(logger, log_level=logging.DEBUG, message="It took %(seconds).02f seconds to" " run function '%(func_name)s'", enabled=True, min_duration=0.01): """Decorator that will log how long its decorated function takes to run. This does **not** output a log if the decorated function fails with an exception. :param logger: logger instance to use when logging elapsed time :param log_level: logger logging level to use when logging elapsed time :param message: customized message to use when logging elapsed time, the message may use automatically provide values ``%(seconds)`` and ``%(func_name)`` if it finds those values useful to record :param enabled: whether to enable or disable this decorator (useful to decorate a function with this decorator, and then easily be able to switch that decoration off by some config or other value) :param min_duration: argument that determines if logging is triggered or not, it is by default set to 0.01 seconds to avoid logging when durations and/or elapsed function call times are less than 0.01 seconds, to disable any ``min_duration`` checks this value should be set to less than or equal to zero or set to none """ def decorator(func): if not enabled: return func @functools.wraps(func) def wrapper(*args, **kwargs): with StopWatch() as w: result = func(*args, **kwargs) time_taken = w.elapsed() if min_duration is None or time_taken >= min_duration: logger.log(log_level, message, {'seconds': time_taken, 'func_name': reflection.get_callable_name(func)}) return result return wrapper return decorator class StopWatch: """A simple timer/stopwatch helper class. Inspired by: apache-commons-lang java stopwatch. Not thread-safe (when a single watch is mutated by multiple threads at the same time). Thread-safe when used by a single thread (not shared) or when operations are performed in a thread-safe manner on these objects by wrapping those operations with locks. .. versionadded:: 1.4 """ _STARTED = 'STARTED' _STOPPED = 'STOPPED' def __init__(self, duration=None): if duration is not None and duration < 0: raise ValueError("Duration must be greater or equal to" " zero and not %s" % duration) self._duration = duration self._started_at = None self._stopped_at = None self._state = None self._splits = () def start(self): """Starts the watch (if not already started). NOTE(harlowja): resets any splits previously captured (if any). """ if self._state == self._STARTED: return self self._started_at = now() self._stopped_at = None self._state = self._STARTED self._splits = () return self @property def splits(self): """Accessor to all/any splits that have been captured.""" return self._splits def split(self): """Captures a split/elapsed since start time (and doesn't stop).""" if self._state == self._STARTED: elapsed = self.elapsed() if self._splits: length = self._delta_seconds(self._splits[-1].elapsed, elapsed) else: length = elapsed self._splits = self._splits + (Split(elapsed, length),) return self._splits[-1] else: raise RuntimeError("Can not create a split time of a stopwatch" " if it has not been started or if it has been" " stopped") def restart(self): """Restarts the watch from a started/stopped state.""" if self._state == self._STARTED: self.stop() self.start() return self @staticmethod def _delta_seconds(earlier, later): # Uses max to avoid the delta/time going backwards (and thus negative). return max(0.0, later - earlier) def elapsed(self, maximum=None): """Returns how many seconds have elapsed.""" if self._state not in (self._STARTED, self._STOPPED): raise RuntimeError("Can not get the elapsed time of a stopwatch" " if it has not been started/stopped") if self._state == self._STOPPED: elapsed = self._delta_seconds(self._started_at, self._stopped_at) else: elapsed = self._delta_seconds(self._started_at, now()) if maximum is not None and elapsed > maximum: elapsed = max(0.0, maximum) return elapsed def __enter__(self): """Starts the watch.""" self.start() return self def __exit__(self, type, value, traceback): """Stops the watch (ignoring errors if stop fails).""" try: self.stop() except RuntimeError: # nosec: errors are meant to be ignored pass def leftover(self, return_none=False): """Returns how many seconds are left until the watch expires. :param return_none: when ``True`` instead of raising a ``RuntimeError`` when no duration has been set this call will return ``None`` instead. :type return_none: boolean """ if self._state != self._STARTED: raise RuntimeError("Can not get the leftover time of a stopwatch" " that has not been started") if self._duration is None: if not return_none: raise RuntimeError("Can not get the leftover time of a watch" " that has no duration") return None return max(0.0, self._duration - self.elapsed()) def expired(self): """Returns if the watch has expired (ie, duration provided elapsed).""" if self._state not in (self._STARTED, self._STOPPED): raise RuntimeError("Can not check if a stopwatch has expired" " if it has not been started/stopped") if self._duration is None: return False return self.elapsed() > self._duration def has_started(self): """Returns True if the watch is in a started state.""" return self._state == self._STARTED def has_stopped(self): """Returns True if the watch is in a stopped state.""" return self._state == self._STOPPED def resume(self): """Resumes the watch from a stopped state.""" if self._state == self._STOPPED: self._state = self._STARTED return self else: raise RuntimeError("Can not resume a stopwatch that has not been" " stopped") def stop(self): """Stops the watch.""" if self._state == self._STOPPED: return self if self._state != self._STARTED: raise RuntimeError("Can not stop a stopwatch that has not been" " started") self._stopped_at = now() self._state = self._STOPPED return self ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/oslo_utils/units.py0000664000175000017500000000254400000000000017655 0ustar00zuulzuul00000000000000# Copyright 2013 IBM Corp # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ Unit constants """ # Binary unit constants. Ki = 1024 "Binary kilo unit" Mi = 1024 ** 2 "Binary mega unit" Gi = 1024 ** 3 "Binary giga unit" Ti = 1024 ** 4 "Binary tera unit" Pi = 1024 ** 5 "Binary peta unit" Ei = 1024 ** 6 "Binary exa unit" Zi = 1024 ** 7 "Binary zetta unit" Yi = 1024 ** 8 "Binary yotta unit" Ri = 1024 ** 9 "Binary ronna unit" Qi = 1024 ** 10 "Binary quetta unit" # Decimal unit constants. k = 1000 "Decimal kilo unit" M = 1000 ** 2 "Decimal mega unit" G = 1000 ** 3 "Decimal giga unit" T = 1000 ** 4 "Decimal tera unit" P = 1000 ** 5 "Decimal peta unit" E = 1000 ** 6 "Decimal exa unit" Z = 1000 ** 7 "Decimal zetta unit" Y = 1000 ** 8 "Decimal yotta unit" R = 1000 ** 9 "Decimal ronna unit" Q = 1000 ** 10 "Decimal quetta unit" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/oslo_utils/uuidutils.py0000664000175000017500000000304400000000000020536 0ustar00zuulzuul00000000000000# Copyright (c) 2012 Intel Corporation. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ UUID related utilities and helper functions. .. versionadded:: 1.1 """ import uuid def generate_uuid(dashed=True): """Creates a random uuid string. :param dashed: Generate uuid with dashes or not :type dashed: bool :returns: string """ if dashed: return str(uuid.uuid4()) return uuid.uuid4().hex def _format_uuid_string(string): return (string.replace('urn:', '') .replace('uuid:', '') .strip('{}') .replace('-', '') .lower()) def is_uuid_like(val): """Returns validation of a value as a UUID. :param val: Value to verify :type val: string :returns: bool .. versionchanged:: 1.1.1 Support non-lowercase UUIDs. """ try: return str(uuid.UUID(val)).replace('-', '') == _format_uuid_string(val) except (TypeError, ValueError, AttributeError): return False ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/oslo_utils/version.py0000664000175000017500000000126200000000000020174 0ustar00zuulzuul00000000000000# Copyright 2016 OpenStack Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import pbr.version version_info = pbr.version.VersionInfo('oslo.utils') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/oslo_utils/versionutils.py0000664000175000017500000000773100000000000021264 0ustar00zuulzuul00000000000000# Copyright (c) 2013 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ Helpers for comparing version strings. .. versionadded:: 1.6 """ import functools import operator import re import packaging.version from oslo_utils._i18n import _ def is_compatible(requested_version, current_version, same_major=True): """Determine whether `requested_version` is satisfied by `current_version`; in other words, `current_version` is >= `requested_version`. :param requested_version: version to check for compatibility :param current_version: version to check against :param same_major: if True, the major version must be identical between `requested_version` and `current_version`. This is used when a major-version difference indicates incompatibility between the two versions. Since this is the common-case in practice, the default is True. :returns: True if compatible, False if not """ requested = packaging.version.Version(requested_version) current = packaging.version.Version(current_version) if same_major: if requested.major != current.major: return False return current >= requested def convert_version_to_int(version): """Convert a version to an integer. *version* must be a string with dots or a tuple of integers. .. versionadded:: 2.0 """ try: if isinstance(version, str): version = convert_version_to_tuple(version) if isinstance(version, tuple): return functools.reduce(lambda x, y: (x * 1000) + y, version) except Exception as ex: msg = _("Version %s is invalid.") % version raise ValueError(msg) from ex def convert_version_to_str(version_int): """Convert a version integer to a string with dots. .. versionadded:: 2.0 """ version_numbers = [] factor = 1000 while version_int != 0: version_number = version_int - (version_int // factor * factor) version_numbers.insert(0, str(version_number)) version_int = version_int // factor return '.'.join(map(str, version_numbers)) def convert_version_to_tuple(version_str): """Convert a version string with dots to a tuple. .. versionadded:: 2.0 """ version_str = re.sub(r'(\d+)(a|alpha|b|beta|rc)\d+$', '\\1', version_str) return tuple(int(part) for part in version_str.split('.')) class VersionPredicate: """Parse version predicate and check version requirements This is based on the implementation of distutils.VersionPredicate .. versionadded:: 7.4 """ _PREDICATE_MATCH = re.compile(r"^\s*(<=|>=|<|>|!=|==)\s*([^\s]+)\s*$") _COMP_MAP = { "<": operator.lt, "<=": operator.le, "==": operator.eq, ">": operator.gt, ">=": operator.ge, "!=": operator.ne } def __init__(self, predicate_str): self.pred = [self._parse_predicate(pred) for pred in predicate_str.split(',')] def _parse_predicate(self, pred): res = self._PREDICATE_MATCH.match(pred) if not res: raise ValueError("bad package restriction syntax: %s" % pred) cond, ver_str = res.groups() return (cond, packaging.version.Version(ver_str)) def satisfied_by(self, version_str): version = packaging.version.Version(version_str) for cond, ver in self.pred: if not self._COMP_MAP[cond](version, ver): return False return True ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1740145268.2650516 oslo_utils-8.2.0/releasenotes/0000775000175000017500000000000000000000000016431 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1740145268.2850556 oslo_utils-8.2.0/releasenotes/notes/0000775000175000017500000000000000000000000017561 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/notes/add-image-format-inspector-2ad45f623838a8f8.yaml0000664000175000017500000000236400000000000027724 0ustar00zuulzuul00000000000000--- features: - | The format_inspector module has been imported from the projects that were using it. They had effectively maintained in-tree forks of the original code from glance. This code has been imported (from Nova) into oslo.utils under imageutils for common use. A refactoring of how the safety check works was done to facilitate the ability to surface fine-grained information about individual checks, as needed by some projects, as well as a few other generalizing aspects, which are detailed in the following. - | A gpt/mbr format inspector was added, which should reduce the number of disk images that are detected as "raw". This furthers the goal of trying to avoid using "raw" for both "anything we don't recognize" and "exact image of a physical disk". - | The glance-centric InfoWrapper from the imported code has been replaced with InspectWrapper, which natively runs multiple (default: all) inspector classes in parallel. This is identical to what detect_file_format() did, but in a stream-friendly way, and detect_file_format() now uses this internally. Users of the glance-based code moving to the oslo implemenation should switch to using the InspectWrapper going forward. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/notes/add-md5-wrapper-7bf81c2464a7a224.yaml0000664000175000017500000000140500000000000025467 0ustar00zuulzuul00000000000000--- features: - | A wrapper for hashlib.md5() has been added to allow OpenStack to run on systems where FIPS is enabled. Under FIPS, md5 is disabled and calls to hashlib.md5() will fail. In most cases in OpenStack, though, md5 is not used within a security context. In https://bugs.python.org/issue9216, a proposal has been made to allow the addition of a keyword parameter usedforsecurity, which can be used to designate non-security context uses. In this case, md5() operations would be permitted. This feature is expected to be delivered in python 3.9. Downstream python already supports this option, though. This wrapper simply allows for this option to be supported where the underlying python version supports it. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/notes/add-methods-for-json-yaml-file-check-746dca0a11c2f9c9.yaml0000664000175000017500000000022500000000000031611 0ustar00zuulzuul00000000000000--- features: - | New method ``is_json`` ``is_yaml`` added in fileutils. These can be used to check if file is JSON or YAML formatted. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/notes/add-reno-350f5f34f794fb5e.yaml0000664000175000017500000000007200000000000024366 0ustar00zuulzuul00000000000000--- other: - Introduce reno for deployer release notes. ././@PaxHeader0000000000000000000000000000021300000000000011451 xustar0000000000000000117 path=oslo_utils-8.2.0/releasenotes/notes/allow-to-convert-ipv4-address-from-text-to-binary-8c46ad2d9989e8c5.yaml 22 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/notes/allow-to-convert-ipv4-address-from-text-to-binary-8c46ad2d9989e80000664000175000017500000000037100000000000033066 0ustar00zuulzuul00000000000000--- features: - | Add a ``strict`` flag to ``netutils.is_valid_ipv4`` to allowing users to restrict validation to IP addresses in presentation format (``a.b.c.d``) as opposed to address format (``a.b.c.d``, ``a.b.c``, ``a.b``, ``a``). ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/notes/bug-1942682-ea95d54b2587b32f.yaml0000664000175000017500000000036600000000000024307 0ustar00zuulzuul00000000000000--- fixes: - | `bug #1942682 `_: Fix inconsistent value of `QemuImgInfo.encrypted`. Now the attribute is always `'yes'` or `None` regardless of the format(`human` or `json`) used. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/notes/bug-2073894-2e11ca85984b7bb7.yaml0000664000175000017500000000041200000000000024275 0ustar00zuulzuul00000000000000--- features: - | `Bug #2073894 `_: Added the netutils.get_noscope_ipv6() function in order to fix errors related to IPv6 address formatting that can happen when using an IPv6 defined with scope. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/notes/bump-up-port-range-f774a16336158339.yaml0000664000175000017500000000011400000000000026030 0ustar00zuulzuul00000000000000--- fixes: - Expanded range of allowed ports by adding 0 to valid number. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/notes/crypt-utils-de46bd8fe835dc98.yaml0000664000175000017500000000041000000000000025340 0ustar00zuulzuul00000000000000--- features: - | The following utility functions have been added to the ``oslo_utils.secretutils`` module. These can be used to replace the built-in ``crypt`` module which was removed in Python 3.13. - ``crypt_password`` - ``crypt_mksalt`` ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/notes/deprecate-constant_time_compare-53669f464c9811c1.yaml0000664000175000017500000000027600000000000030771 0ustar00zuulzuul00000000000000--- deprecations: - | The ``oslo_utils.secretutils.constant_time_compare`` function has been deprecated. Use the ``compare_digest`` function from the built-in ``hmac`` module. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/notes/deprecate-eventletutils-f8a96c2c42cd9a15.yaml0000664000175000017500000000054200000000000027607 0ustar00zuulzuul00000000000000--- deprecations: - | Eventlet usages are deprecated and the removal of Eventlet from OpenStack `is planned `_, for this reason the ``eventletutils`` module is deprecated. Please start considering removing your internal Eventlet usages and migrating your stack. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/notes/deprecate-exception_to_unicode-cb4da633bc1bfcc9.yaml0000664000175000017500000000024000000000000031305 0ustar00zuulzuul00000000000000--- deprecations: - | The ``exception_to_unicode`` function from the ``oslo_utils.encodeutils`` module has been deprecated. Use ``str(exc)`` instead. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/notes/deprecate-fnmatch-057a092d434a0c53.yaml0000664000175000017500000000023400000000000026056 0ustar00zuulzuul00000000000000--- deprecations: - | Oslo.utils's ``fnmatch`` module is deprecated, please use the stdlib ``fnmatch`` module which is thread safe for python 3+. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/notes/deprecate-md5-cc365c25c2a51a8c.yaml0000664000175000017500000000030000000000000025340 0ustar00zuulzuul00000000000000--- deprecations: - | The ``md5`` method from ``oslo_utils.secretutils`` module has been deprecated because ``hashlib.md5`` can be used instead in all supported python versions. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/notes/drop-imageutils-human-format-support-a89101a36c4dd3cb.yaml0000664000175000017500000000020600000000000032140 0ustar00zuulzuul00000000000000--- deprecations: - | Support for parsing the ``human`` format has been deprecated and will be removed in a future release. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/notes/drop-python27-support-f97f680651693b47.yaml0000664000175000017500000000017700000000000026642 0ustar00zuulzuul00000000000000--- upgrade: - | Support for Python 2.7 has been dropped. The minimum version of Python now supported is Python 3.6. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/notes/fix_mask_password_regex-c0661f95a23369a4.yaml0000664000175000017500000000036200000000000027441 0ustar00zuulzuul00000000000000--- fixes: - | Fix regex used to mask password. The ``strutils.mask_password`` function will now correctly handle passwords that contain single or double quotes. Previously, only the characters before the quote were masked. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/notes/image-utils-handle-scientific-notation-6f65d46e9c8c8f8c.yaml0000664000175000017500000000024000000000000032410 0ustar00zuulzuul00000000000000--- fixes: - | qemu 4.1.0 output shifts to scientific notation at 1000mb, breaking oslo.utils. ``QemuImgInfo`` is now fixed to support this notation. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/notes/imageutils-cli-dd0d1cecbc607725.yaml0000664000175000017500000000037300000000000025725 0ustar00zuulzuul00000000000000features: - | Adds a utility CLI module for checking images against the format inspector in oslo_utils.imageutils. After installing oslo.utils, simply run ``python -m oslo_utils.imageutils -h`` for full usage for the new CLI helper. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/notes/implement-zoneinfo-to-remove-pytz-fba6f70db09ecdb8.yaml0000664000175000017500000000015500000000000031722 0ustar00zuulzuul00000000000000--- other: - | Implement zoneinfo to allow us to remove pytz's dependency for Python 3.9 and 3.10. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/notes/introduce-keystoneidsentinel-16bf3e7f2ae7e9f3.yaml0000664000175000017500000000027100000000000030746 0ustar00zuulzuul00000000000000--- features: - | keystoneidsentinel singleton was introduced to generate random keystone-like UUIDs. New sentinel could be used in the same way as existing uuidsentinel. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/notes/is_valid_ipv4-strict-3da92c0452aaf947.yaml0000664000175000017500000000027300000000000026723 0ustar00zuulzuul00000000000000--- upgrade: - | The ``netutils.is_valid_ipv4`` method now enables the strict check by default. To preserve the previous behavior, set the ``strict`` argument to ``False``. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/notes/mask-dict-passwords-99357ffb7972fb0b.yaml0000664000175000017500000000050500000000000026603 0ustar00zuulzuul00000000000000--- security: - | This patch ensures that we mask sensitive data when masking dicts, even if the case doesn't match. This means the behaviour of mask_password and mask_dict_password is now the same. - | Additional password names were included from real world logs that contained sensitive information.././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/notes/mask-password-pattern-c8c880098743de3e.yaml0000664000175000017500000000022400000000000027064 0ustar00zuulzuul00000000000000--- security: - | This patch ensures that we mask sensitive data when masking password, even if double quotes are used as password value. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/notes/mask-password-patterns-f41524069b8ae488.yaml0000664000175000017500000000047100000000000027166 0ustar00zuulzuul00000000000000--- security: - This patch ensures we actually mask sensitive data, even if case doesn't match the static entry we have in the patterns. - It also ensures that some fancy names with a common base, but added number are actually taken care of. fixes: - https://bugs.launchpad.net/tripleo/+bug/1850843 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/notes/netutils-get_mac_addr_by_ipv6-c3ce6a35a69f7c2b.yaml0000664000175000017500000000023200000000000030714 0ustar00zuulzuul00000000000000--- features: - | New method ``netutils.get_mac_addr_by_ipv6(ipv6, dialect)`` extracts the MAC address from IPv6 addresses generated from MACs. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/notes/netutils-get_my_ipv6-c9f124374655326b.yaml0000664000175000017500000000016100000000000026541 0ustar00zuulzuul00000000000000--- features: - | New method ``netutils.get_my_ipv6()`` returns the IPv6 address of the local machine. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/notes/new_spec_dsl_operator-21c80a46f67c56df.yaml0000664000175000017500000000131200000000000027243 0ustar00zuulzuul00000000000000--- features: - | Introducing a new spec DSL operator called ```` that allows users to match a numeric value against a range of numbers that are delimited with lower and upper limits. The new operator is a binary operator that accepts 4 arguments. - The first one and the last one are brackets. ``[`` and ``]`` defines inclusive limits while ``(`` and ``)`` defines exclusive limits. - The second one is the lower limit while the third one is the upper limit. Example: " [ 10.4 20 )" will match a value against an range such as the lower limit of the range is 10.4 and the upper limit is 20. Note that 10.4 is included while 20 is excluded. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/notes/remove-fnmatch-f227b54f237a02c2.yaml0000664000175000017500000000021200000000000025476 0ustar00zuulzuul00000000000000--- upgrade: - | The ``oslo_utils.fnmatch`` module has been removed. The stdlib ``fnmatch`` module is thread safe in Python 3+. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/notes/remove-py38-a22bb6c463f92868.yaml0000664000175000017500000000016600000000000024711 0ustar00zuulzuul00000000000000--- upgrade: - | Support for Python 3.8 has been removed. Now the minimum python version supported is 3.9 . ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/notes/remove-strict-from-is_same_callback-cfbff2ada778987e.yaml0000664000175000017500000000023200000000000032117 0ustar00zuulzuul00000000000000--- upgrade: - | The ``strict`` argument has been removed from the ``is_same_callback`` function. The argument has had no effect in Python 3.8. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/notes/remove-timeutils-deprecated-helpers-5de68c21dd281529.yaml0000664000175000017500000000031000000000000031651 0ustar00zuulzuul00000000000000--- upgrade: - | The ``isotime``, ``strtime`` and ``iso8601_from_timestamp`` helpers have been removed from ``oslo_utils.timeutils``. These are all available in the stdlib in Python 3. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/notes/version-predicate-42f38f7b7e9187e1.yaml0000664000175000017500000000032200000000000026243 0ustar00zuulzuul00000000000000--- features: - | The new ``VersionPredicate`` class has been added to the ``versionutils`` module, which parses version predicate and check if the given version meets the described requirements. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1740145268.2850556 oslo_utils-8.2.0/releasenotes/source/0000775000175000017500000000000000000000000017731 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/source/2023.1.rst0000664000175000017500000000021000000000000021201 0ustar00zuulzuul00000000000000=========================== 2023.1 Series Release Notes =========================== .. release-notes:: :branch: unmaintained/2023.1 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/source/2023.2.rst0000664000175000017500000000020200000000000021203 0ustar00zuulzuul00000000000000=========================== 2023.2 Series Release Notes =========================== .. release-notes:: :branch: stable/2023.2 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/source/2024.1.rst0000664000175000017500000000020200000000000021203 0ustar00zuulzuul00000000000000=========================== 2024.1 Series Release Notes =========================== .. release-notes:: :branch: stable/2024.1 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/source/2024.2.rst0000664000175000017500000000020200000000000021204 0ustar00zuulzuul00000000000000=========================== 2024.2 Series Release Notes =========================== .. release-notes:: :branch: stable/2024.2 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1740145268.2850556 oslo_utils-8.2.0/releasenotes/source/_static/0000775000175000017500000000000000000000000021357 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/source/_static/.placeholder0000664000175000017500000000000000000000000023630 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1740145268.2850556 oslo_utils-8.2.0/releasenotes/source/_templates/0000775000175000017500000000000000000000000022066 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/source/_templates/.placeholder0000664000175000017500000000000000000000000024337 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/source/conf.py0000664000175000017500000002061300000000000021232 0ustar00zuulzuul00000000000000# Copyright (C) 2020 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'openstackdocstheme', 'reno.sphinxext', ] # openstackdocstheme options openstackdocs_repo_name = 'openstack/oslo.utils' openstackdocs_bug_project = 'oslo.utils' openstackdocs_bug_tag = '' # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. copyright = '2016, oslo.utils Developers' # Release notes do not need a version in the title, they span # multiple versions. # The full version, including alpha/beta/rc tags. release = '' # The short X.Y version. version = '' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all # documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'native' # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'openstackdocs' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # html_extra_path = [] # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'oslo.utilsReleaseNotesDoc' # -- Options for LaTeX output --------------------------------------------- # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ('index', 'oslo.utilsReleaseNotes.tex', 'oslo.utils Release Notes Documentation', 'oslo.utils Developers', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'oslo.utilsReleaseNotes', 'oslo.utils Release Notes Documentation', ['oslo.utils Developers'], 1) ] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'oslo.utilsReleaseNotes', 'oslo.utils Release Notes Documentation', 'oslo.utils Developers', 'oslo.utilsReleaseNotes', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # texinfo_no_detailmenu = False # -- Options for Internationalization output ------------------------------ locale_dirs = ['locale/'] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/source/index.rst0000664000175000017500000000043700000000000021576 0ustar00zuulzuul00000000000000=========================== oslo.utils Release Notes =========================== .. toctree:: :maxdepth: 1 unreleased 2024.2 2024.1 2023.2 2023.1 zed yoga xena wallaby victoria ussuri train stein rocky queens pike ocata newton ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1740145268.2690525 oslo_utils-8.2.0/releasenotes/source/locale/0000775000175000017500000000000000000000000021170 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1740145268.2690525 oslo_utils-8.2.0/releasenotes/source/locale/en_GB/0000775000175000017500000000000000000000000022142 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1740145268.2850556 oslo_utils-8.2.0/releasenotes/source/locale/en_GB/LC_MESSAGES/0000775000175000017500000000000000000000000023727 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po0000664000175000017500000002501700000000000026765 0ustar00zuulzuul00000000000000# Andi Chandler , 2017. #zanata # Andi Chandler , 2018. #zanata # Andi Chandler , 2020. #zanata # Andi Chandler , 2022. #zanata # Andi Chandler , 2023. #zanata msgid "" msgstr "" "Project-Id-Version: oslo.utils\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2024-07-23 18:02+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2023-07-28 12:47+0000\n" "Last-Translator: Andi Chandler \n" "Language-Team: English (United Kingdom)\n" "Language: en_GB\n" "X-Generator: Zanata 4.3.3\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" msgid "2023.1 Series Release Notes" msgstr "2023.1 Series Release Notes" msgid "3.13.0" msgstr "3.13.0" msgid "3.18.0" msgstr "3.18.0" msgid "3.35.1-4" msgstr "3.35.1-4" msgid "3.36.5" msgstr "3.36.5" msgid "3.40.5" msgstr "3.40.5" msgid "3.40.6" msgstr "3.40.6" msgid "3.41.3" msgstr "3.41.3" msgid "3.41.4" msgstr "3.41.4" msgid "3.41.5" msgstr "3.41.5" msgid "3.41.6-4" msgstr "3.41.6-4" msgid "3.42.1" msgstr "3.42.1" msgid "4.0.0" msgstr "4.0.0" msgid "4.1.0" msgstr "4.1.0" msgid "4.1.2-4" msgstr "4.1.2-4" msgid "4.10.0" msgstr "4.10.0" msgid "4.10.1" msgstr "4.10.1" msgid "4.10.2" msgstr "4.10.2" msgid "4.11.0" msgstr "4.11.0" msgid "4.12.1" msgstr "4.12.1" msgid "4.12.3" msgstr "4.12.3" msgid "4.13.0" msgstr "4.13.0" msgid "4.4.0" msgstr "4.4.0" msgid "4.5.0" msgstr "4.5.0" msgid "4.6.1" msgstr "4.6.1" msgid "4.7.0" msgstr "4.7.0" msgid "4.8.0" msgstr "4.8.0" msgid "4.8.1" msgstr "4.8.1" msgid "4.8.2" msgstr "4.8.2" msgid "4.9.1" msgstr "4.9.1" msgid "6.0.0" msgstr "6.0.0" msgid "6.2.0" msgstr "6.2.0" msgid "" "A wrapper for hashlib.md5() has been added to allow OpenStack to run on " "systems where FIPS is enabled. Under FIPS, md5 is disabled and calls to " "hashlib.md5() will fail. In most cases in OpenStack, though, md5 is not " "used within a security context." msgstr "" "A wrapper for hashlib.md5() has been added to allow OpenStack to run on " "systems where FIPS is enabled. Under FIPS, md5 is disabled and calls to " "hashlib.md5() will fail. In most cases in OpenStack, though, MD5 is not " "used within a security context." msgid "" "Add a ``strict`` flag to ``netutils.is_valid_ipv4`` to allowing users to " "restrict validation to IP addresses in presentation format (``a.b.c.d``) as " "opposed to address format (``a.b.c.d``, ``a.b.c``, ``a.b``, ``a``)." msgstr "" "Add a ``strict`` flag to ``netutils.is_valid_ipv4`` to allowing users to " "restrict validation to IP addresses in presentation format (``a.b.c.d``) as " "opposed to address format (``a.b.c.d``, ``a.b.c``, ``a.b``, ``a``)." msgid "" "Additional password names were included from real world logs that contained " "sensitive information." msgstr "" "Additional password names were included from real world logs that contained " "sensitive information." msgid "Bug Fixes" msgstr "Bug Fixes" msgid "Deprecation Notes" msgstr "Deprecation Notes" msgid "" "Downstream python already supports this option, though. This wrapper simply " "allows for this option to be supported where the underlying python version " "supports it." msgstr "" "Downstream Python already supports this option, though. This wrapper simply " "allows for this option to be supported where the underlying Python version " "supports it." msgid "Expanded range of allowed ports by adding 0 to valid number." msgstr "Expanded range of allowed ports by adding 0 to valid number." msgid "" "Fix regex used to mask password. The ``strutils.mask_password`` function " "will now correctly handle passwords that contain single or double quotes. " "Previously, only the characters before the quote were masked." msgstr "" "Fix regex used to mask the password. The ``strutils.mask_password`` function " "will now correctly handle passwords that contain single or double quotes. " "Previously, only the characters before the quote were masked." msgid "" "Implement zoneinfo to allow us to remove pytz's dependency for Python 3.9 " "and 3.10." msgstr "" "Implement zoneinfo to allow us to remove pytz's dependency for Python 3.9 " "and 3.10." msgid "" "In https://bugs.python.org/issue9216, a proposal has been made to allow the " "addition of a keyword parameter usedforsecurity, which can be used to " "designate non-security context uses. In this case, md5() operations would " "be permitted. This feature is expected to be delivered in python 3.9." msgstr "" "In https://bugs.python.org/issue9216, a proposal has been made to allow the " "addition of a keyword parameter usedforsecurity, which can be used to " "designate non-security context uses. In this case, md5() operations would " "be permitted. This feature is expected to be delivered in python 3.9." msgid "Introduce reno for deployer release notes." msgstr "Introduce Reno for developer release notes." msgid "" "It also ensures that some fancy names with a common base, but added number " "are actually taken care of." msgstr "" "It also ensures that some fancy names with a common base, but added number " "are actually taken care of." msgid "New Features" msgstr "New Features" msgid "" "New method ``is_json`` ``is_yaml`` added in fileutils. These can be used to " "check if file is JSON or YAML formatted." msgstr "" "New method ``is_json`` ``is_yaml`` added in fileutils. These can be used to " "check if the file is JSON or YAML formatted." msgid "" "New method ``netutils.get_mac_addr_by_ipv6(ipv6, dialect)`` extracts the MAC " "address from IPv6 addresses generated from MACs." msgstr "" "New method ``netutils.get_mac_addr_by_ipv6(ipv6, dialect)`` extracts the MAC " "address from IPv6 addresses generated from MACs." msgid "" "New method ``netutils.get_my_ipv6()`` returns the IPv6 address of the local " "machine." msgstr "" "New method ``netutils.get_my_ipv6()`` returns the IPv6 address of the local " "machine." msgid "Newton Series Release Notes" msgstr "Newton Series Release Notes" msgid "Ocata Series Release Notes" msgstr "Ocata Series Release Notes" msgid "" "Oslo.utils's ``fnmatch`` module is deprecated, please use the stdlib " "``fnmatch`` module which is thread safe for python 3+." msgstr "" "Oslo.utils's ``fnmatch`` module is deprecated, please use the stdlib " "``fnmatch`` module which is thread safe for python 3+." msgid "Other Notes" msgstr "Other Notes" msgid "Pike Series Release Notes" msgstr "Pike Series Release Notes" msgid "Queens Series Release Notes" msgstr "Queens Series Release Notes" msgid "Rocky Series Release Notes" msgstr "Rocky Series Release Notes" msgid "Security Issues" msgstr "Security Issues" msgid "Stein Series Release Notes" msgstr "Stein Series Release Notes" msgid "" "Support for Python 2.7 has been dropped. The minimum version of Python now " "supported is Python 3.6." msgstr "" "Support for Python 2.7 has been dropped. The minimum version of Python now " "supported is Python 3.6." msgid "" "Support for parsing the ``human`` format has been deprecated and will be " "removed in a future release." msgstr "" "Support for parsing the ``human`` format has been deprecated and will be " "removed in a future release." msgid "" "The ``isotime``, ``strtime`` and ``iso8601_from_timestamp`` helpers have " "been removed from ``oslo_utils.timeutils``. These are all available in the " "stdlib in Python 3." msgstr "" "The ``isotime``, ``strtime`` and ``iso8601_from_timestamp`` helpers have " "been removed from ``oslo_utils.timeutils``. These are all available in the " "stdlib in Python 3." msgid "" "The ``oslo_utils.fnmatch`` module has been removed. The stdlib ``fnmatch`` " "module is thread safe in Python 3+." msgstr "" "The ``oslo_utils.fnmatch`` module has been removed. The stdlib ``fnmatch`` " "module is thread safe in Python 3+." msgid "" "This patch ensures that we mask sensitive data when masking dicts, even if " "the case doesn't match. This means the behaviour of mask_password and " "mask_dict_password is now the same." msgstr "" "This patch ensures that we mask sensitive data when masking dicts, even if " "the case doesn't match. This means the behaviour of mask_password and " "mask_dict_password is now the same." msgid "" "This patch ensures that we mask sensitive data when masking password, even " "if double quotes are used as password value." msgstr "" "This patch ensures that we mask sensitive data when masking passwords, even " "if double quotes are used as password values." msgid "" "This patch ensures we actually mask sensitive data, even if case doesn't " "match the static entry we have in the patterns." msgstr "" "This patch ensures we actually mask sensitive data, even if case doesn't " "match the static entry we have in the patterns." msgid "Train Series Release Notes" msgstr "Train Series Release Notes" msgid "Unreleased Release Notes" msgstr "Unreleased Release Notes" msgid "Upgrade Notes" msgstr "Upgrade Notes" msgid "Ussuri Series Release Notes" msgstr "Ussuri Series Release Notes" msgid "Victoria Series Release Notes" msgstr "Victoria Series Release Notes" msgid "Wallaby Series Release Notes" msgstr "Wallaby Series Release Notes" msgid "Xena Series Release Notes" msgstr "Xena Series Release Notes" msgid "Yoga Series Release Notes" msgstr "Yoga Series Release Notes" msgid "Zed Series Release Notes" msgstr "Zed Series Release Notes" msgid "" "`bug #1942682 `_: Fix " "inconsistent value of `QemuImgInfo.encrypted`. Now the attribute is always " "`'yes'` or `None` regardless of the format(`human` or `json`) used." msgstr "" "`bug #1942682 `_: Fix " "inconsistent value of `QemuImgInfo.encrypted`. Now the attribute is always " "`'yes'` or `None` regardless of the format(`human` or `json`) used." msgid "https://bugs.launchpad.net/tripleo/+bug/1850843" msgstr "https://bugs.launchpad.net/tripleo/+bug/1850843" msgid "" "keystoneidsentinel singleton was introduced to generate random keystone-like " "UUIDs. New sentinel could be used in the same way as existing uuidsentinel." msgstr "" "keystoneidsentinel singleton was introduced to generate random keystone-like " "UUIDs. New Sentinel could be used in the same way as existing uuidsentinel." msgid "oslo.utils Release Notes" msgstr "oslo.utils Release Notes" msgid "" "qemu 4.1.0 output shifts to scientific notation at 1000mb, breaking oslo." "utils. ``QemuImgInfo`` is now fixed to support this notation." msgstr "" "QEMU 4.1.0 output shifts to scientific notation at 1000MB, breaking oslo." "utils. ``QemuImgInfo`` is now fixed to support this notation." ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/source/newton.rst0000664000175000017500000000021600000000000021774 0ustar00zuulzuul00000000000000============================= Newton Series Release Notes ============================= .. release-notes:: :branch: origin/stable/newton ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/source/ocata.rst0000664000175000017500000000023000000000000021545 0ustar00zuulzuul00000000000000=================================== Ocata Series Release Notes =================================== .. release-notes:: :branch: origin/stable/ocata ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/source/pike.rst0000664000175000017500000000021700000000000021413 0ustar00zuulzuul00000000000000=================================== Pike Series Release Notes =================================== .. release-notes:: :branch: stable/pike ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/source/queens.rst0000664000175000017500000000022300000000000021760 0ustar00zuulzuul00000000000000=================================== Queens Series Release Notes =================================== .. release-notes:: :branch: stable/queens ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/source/rocky.rst0000664000175000017500000000022100000000000021605 0ustar00zuulzuul00000000000000=================================== Rocky Series Release Notes =================================== .. release-notes:: :branch: stable/rocky ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/source/stein.rst0000664000175000017500000000022100000000000021600 0ustar00zuulzuul00000000000000=================================== Stein Series Release Notes =================================== .. release-notes:: :branch: stable/stein ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/source/train.rst0000664000175000017500000000017600000000000021604 0ustar00zuulzuul00000000000000========================== Train Series Release Notes ========================== .. release-notes:: :branch: stable/train ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/source/unreleased.rst0000664000175000017500000000014400000000000022611 0ustar00zuulzuul00000000000000========================== Unreleased Release Notes ========================== .. release-notes:: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/source/ussuri.rst0000664000175000017500000000020200000000000022007 0ustar00zuulzuul00000000000000=========================== Ussuri Series Release Notes =========================== .. release-notes:: :branch: stable/ussuri ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/source/victoria.rst0000664000175000017500000000022000000000000022275 0ustar00zuulzuul00000000000000============================= Victoria Series Release Notes ============================= .. release-notes:: :branch: unmaintained/victoria ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/source/wallaby.rst0000664000175000017500000000021400000000000022113 0ustar00zuulzuul00000000000000============================ Wallaby Series Release Notes ============================ .. release-notes:: :branch: unmaintained/wallaby ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/source/xena.rst0000664000175000017500000000020000000000000021406 0ustar00zuulzuul00000000000000========================= Xena Series Release Notes ========================= .. release-notes:: :branch: unmaintained/xena ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/source/yoga.rst0000664000175000017500000000020000000000000021412 0ustar00zuulzuul00000000000000========================= Yoga Series Release Notes ========================= .. release-notes:: :branch: unmaintained/yoga ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/releasenotes/source/zed.rst0000664000175000017500000000017400000000000021247 0ustar00zuulzuul00000000000000======================== Zed Series Release Notes ======================== .. release-notes:: :branch: unmaintained/zed ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/requirements.txt0000664000175000017500000000135100000000000017224 0ustar00zuulzuul00000000000000# Requirements lower bounds listed here are our best effort to keep them up to # date but we do not test them so no guarantee of having them all correct. If # you find any incorrect lower bounds, let us know or propose a fix. # NOTE(dhellmann): Because oslo.utils is used by the client libraries, # we do not want to add a lot of dependencies to it. If you find that # adding a new feature to oslo.utils means adding a new dependency, # that is a likely indicator that the feature belongs somewhere else. iso8601>=0.1.11 # MIT oslo.i18n>=3.15.3 # Apache-2.0 netaddr>=0.10.0 # BSD debtcollector>=1.2.0 # Apache-2.0 pyparsing>=2.1.0 # MIT packaging>=20.4 # BSD tzdata>=2022.4 # MIT PyYAML>=3.13 # MIT psutil>=3.2.2 # BST pbr>=6.1.0 # Apache-2.0 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1740145268.2890563 oslo_utils-8.2.0/setup.cfg0000664000175000017500000000154500000000000015566 0ustar00zuulzuul00000000000000[metadata] name = oslo.utils summary = Oslo Utility library description_file = README.rst author = OpenStack author_email = openstack-discuss@lists.openstack.org home_page = https://docs.openstack.org/oslo.utils/latest/ python_requires = >=3.9 classifier = Environment :: OpenStack Intended Audience :: Information Technology Intended Audience :: System Administrators License :: OSI Approved :: Apache Software License Operating System :: POSIX :: Linux Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 Programming Language :: Python :: 3 :: Only Programming Language :: Python :: Implementation :: CPython [files] packages = oslo_utils [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/setup.py0000664000175000017500000000127100000000000015453 0ustar00zuulzuul00000000000000# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import setuptools setuptools.setup( setup_requires=['pbr>=2.0.0'], pbr=True) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/test-requirements.txt0000664000175000017500000000044300000000000020202 0ustar00zuulzuul00000000000000eventlet>=0.18.2 # MIT fixtures>=3.0.0 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD testtools>=2.2.0 # MIT oslotest>=3.2.0 # Apache-2.0 ddt>=1.0.1 # MIT stestr>=2.0.0 # Apache-2.0 coverage>=4.0 # Apache-2.0 # used for oslotest cross-testing scripts oslo.config>=5.2.0 # Apache-2.0 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1740145268.2890563 oslo_utils-8.2.0/tools/0000775000175000017500000000000000000000000015100 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/tools/perf_test_mask_password.py0000664000175000017500000000272500000000000022410 0ustar00zuulzuul00000000000000#!/usr/bin/env python3 # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Performance tests for mask_password. """ import timeit from oslo_utils import strutils # A moderately sized input (~50K) string # http://paste.openstack.org/raw/155864/ # infile = '155864.txt' # Untruncated version of the above (~310K) # http://dl.sileht.net/public/payload.json.gz infile = 'large_json_payload.txt' with open(infile) as f: input_str = f.read() print('payload has %d bytes' % len(input_str)) for pattern in strutils._SANITIZE_PATTERNS_2['admin_pass']: print('\ntesting %s' % pattern.pattern) t = timeit.Timer( r"re.sub(pattern, r'\g<1>***\g<2>', payload)", """ import re payload = '''{}''' pattern = re.compile(r'''{}''') """.format(input_str, pattern.pattern)) print(t.timeit(1)) t = timeit.Timer( "strutils.mask_password('''" + input_str + "''')", "from oslo_utils import strutils", ) print(t.timeit(1)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740145228.0 oslo_utils-8.2.0/tox.ini0000664000175000017500000000304200000000000015252 0ustar00zuulzuul00000000000000[tox] minversion = 3.18.0 envlist = py3,pep8 ignore_basepython_conflict = true [testenv] basepython = python3 deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/test-requirements.txt -r{toxinidir}/requirements.txt commands = stestr run --slowest {posargs} [testenv:pep8] skip_install = true deps = pre-commit commands = pre-commit run -a [testenv:venv] commands = {posargs} [testenv:docs] allowlist_externals = rm deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/doc/requirements.txt commands = rm -fr doc/build sphinx-build -W --keep-going -b html doc/source doc/build/html [testenv:cover] # TODO(stephenfin): Remove the PYTHON hack below in favour of a [coverage] # section once we rely on coverage 4.3+ # # https://bitbucket.org/ned/coveragepy/issues/519/ setenv = PYTHON=coverage run --source oslo_utils --parallel-mode extras = commands = coverage erase stestr run {posargs} coverage combine coverage html -d cover coverage xml -o cover/coverage.xml coverage report [flake8] # E731 skipped as assign a lambda expression # W504 line break after binary operator ignore = E123,E731,H405,W504 show-source = True exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,__init__.py [testenv:releasenotes] allowlist_externals = rm deps = {[testenv:docs]deps} commands = rm -rf releasenotes/build sphinx-build -a -E -W -d releasenotes/build/doctrees --keep-going -b html releasenotes/source releasenotes/build/html